~launchpad-pqm/launchpad/devel

« back to all changes in this revision

Viewing changes to lib/lp/registry/model/milestone.py

  • Committer: Canonical.com Patch Queue Manager
  • Date: 2004-06-28 10:08:03 UTC
  • mfrom: (unknown (missing))
  • Revision ID: Arch-1:rocketfuel@canonical.com%soyuz--devel--0--patch-8
add ./sourcecode directory
Patches applied:

 * david.allouche@canonical.com--2004/soyuz--devel--0--base-0
   tag of rocketfuel@canonical.com/soyuz--devel--0--patch-7

 * david.allouche@canonical.com--2004/soyuz--devel--0--patch-1
   add ./sourcecode directory

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright 2009 Canonical Ltd.  This software is licensed under the
2
 
# GNU Affero General Public License version 3 (see the file LICENSE).
3
 
 
4
 
# pylint: disable-msg=E0611,W0212
5
 
"""Milestone model classes."""
6
 
 
7
 
__metaclass__ = type
8
 
__all__ = [
9
 
    'HasMilestonesMixin',
10
 
    'Milestone',
11
 
    'MilestoneSet',
12
 
    'ProjectMilestone',
13
 
    'milestone_sort_key',
14
 
    ]
15
 
 
16
 
import datetime
17
 
 
18
 
from lazr.restful.declarations import webservice_error
19
 
from sqlobject import (
20
 
    AND,
21
 
    BoolCol,
22
 
    DateCol,
23
 
    ForeignKey,
24
 
    SQLMultipleJoin,
25
 
    StringCol,
26
 
    )
27
 
from storm.locals import (
28
 
    And,
29
 
    Store,
30
 
    )
31
 
from storm.zope import IResultSet
32
 
from zope.component import getUtility
33
 
from zope.interface import implements
34
 
 
35
 
from canonical.database.sqlbase import (
36
 
    SQLBase,
37
 
    sqlvalues,
38
 
    )
39
 
from canonical.launchpad.interfaces.lpstorm import IStore
40
 
from canonical.launchpad.webapp.sorting import expand_numbers
41
 
from lp.app.errors import NotFoundError
42
 
from lp.blueprints.model.specification import Specification
43
 
from lp.bugs.interfaces.bugtarget import IHasBugs
44
 
from lp.bugs.interfaces.bugtask import (
45
 
    BugTaskSearchParams,
46
 
    BugTaskStatus,
47
 
    IBugTaskSet,
48
 
    )
49
 
from lp.bugs.model.bugtarget import HasBugsBase
50
 
from lp.bugs.model.structuralsubscription import (
51
 
    StructuralSubscriptionTargetMixin,
52
 
    )
53
 
from lp.registry.interfaces.milestone import (
54
 
    IHasMilestones,
55
 
    IMilestone,
56
 
    IMilestoneSet,
57
 
    IProjectGroupMilestone,
58
 
    )
59
 
from lp.registry.model.productrelease import ProductRelease
60
 
 
61
 
 
62
 
FUTURE_NONE = datetime.date(datetime.MAXYEAR, 1, 1)
63
 
 
64
 
 
65
 
def milestone_sort_key(milestone):
66
 
    """Enable sorting by the Milestone dateexpected and name."""
67
 
    if milestone.dateexpected is None:
68
 
        # A datetime.datetime object cannot be compared with None.
69
 
        # Milestones with dateexpected=None are sorted as being
70
 
        # way in the future.
71
 
        date = FUTURE_NONE
72
 
    elif isinstance(milestone.dateexpected, datetime.datetime):
73
 
        # XXX: EdwinGrubbs 2009-02-06 bug=326384:
74
 
        # The Milestone.dateexpected should be changed into a date column,
75
 
        # since the class defines the field as a DateCol, so that a list
76
 
        # of milestones can't have some dateexpected attributes that are
77
 
        # datetimes and others that are dates, which can't be compared.
78
 
        date = milestone.dateexpected.date()
79
 
    else:
80
 
        date = milestone.dateexpected
81
 
    return (date, expand_numbers(milestone.name))
82
 
 
83
 
 
84
 
class HasMilestonesMixin:
85
 
    implements(IHasMilestones)
86
 
 
87
 
    _milestone_order = (
88
 
        'milestone_sort_key(Milestone.dateexpected, Milestone.name) DESC')
89
 
 
90
 
    def _getMilestoneCondition(self):
91
 
        """Provides condition for milestones and all_milestones properties.
92
 
 
93
 
        Subclasses need to override this method.
94
 
 
95
 
        :return: Storm ComparableExpr object.
96
 
        """
97
 
        raise NotImplementedError(
98
 
            "Unexpected class for mixin: %r" % self)
99
 
 
100
 
    @property
101
 
    def has_milestones(self):
102
 
        return not self.all_milestones.is_empty()
103
 
 
104
 
    @property
105
 
    def all_milestones(self):
106
 
        """See `IHasMilestones`."""
107
 
        store = Store.of(self)
108
 
        result = store.find(Milestone, self._getMilestoneCondition())
109
 
        return result.order_by(self._milestone_order)
110
 
 
111
 
    def _get_milestones(self):
112
 
        """See `IHasMilestones`."""
113
 
        store = Store.of(self)
114
 
        result = store.find(Milestone,
115
 
                            And(self._getMilestoneCondition(),
116
 
                                Milestone.active == True))
117
 
        return result.order_by(self._milestone_order)
118
 
 
119
 
    milestones = property(_get_milestones)
120
 
 
121
 
 
122
 
class MultipleProductReleases(Exception):
123
 
    """Raised when a second ProductRelease is created for a milestone."""
124
 
    webservice_error(400)
125
 
 
126
 
    def __init__(self, msg='A milestone can only have one ProductRelease.'):
127
 
        super(MultipleProductReleases, self).__init__(msg)
128
 
 
129
 
 
130
 
class Milestone(SQLBase, StructuralSubscriptionTargetMixin, HasBugsBase):
131
 
    implements(IHasBugs, IMilestone)
132
 
 
133
 
    # XXX: Guilherme Salgado 2007-03-27 bug=40978:
134
 
    # Milestones should be associated with productseries/distroseriess
135
 
    # so these columns are not needed.
136
 
    product = ForeignKey(dbName='product',
137
 
        foreignKey='Product', default=None)
138
 
    distribution = ForeignKey(dbName='distribution',
139
 
        foreignKey='Distribution', default=None)
140
 
 
141
 
    productseries = ForeignKey(dbName='productseries',
142
 
        foreignKey='ProductSeries', default=None)
143
 
    distroseries = ForeignKey(dbName='distroseries',
144
 
        foreignKey='DistroSeries', default=None)
145
 
    name = StringCol(notNull=True)
146
 
    # XXX: EdwinGrubbs 2009-02-06 bug=326384:
147
 
    # The Milestone.dateexpected should be changed into a date column,
148
 
    # since the class defines the field as a DateCol, so that a list of
149
 
    # milestones can't have some dateexpected attributes that are
150
 
    # datetimes and others that are dates, which can't be compared.
151
 
    dateexpected = DateCol(notNull=False, default=None)
152
 
    active = BoolCol(notNull=True, default=True)
153
 
    summary = StringCol(notNull=False, default=None)
154
 
    code_name = StringCol(dbName='codename', notNull=False, default=None)
155
 
 
156
 
    # joins
157
 
    specifications = SQLMultipleJoin('Specification', joinColumn='milestone',
158
 
        orderBy=['-priority', 'definition_status',
159
 
                 'implementation_status', 'title'],
160
 
        prejoins=['assignee'])
161
 
 
162
 
    @property
163
 
    def product_release(self):
164
 
        store = Store.of(self)
165
 
        result = store.find(ProductRelease,
166
 
                            ProductRelease.milestone == self.id)
167
 
        releases = list(result)
168
 
        if len(releases) == 0:
169
 
            return None
170
 
        else:
171
 
            return releases[0]
172
 
 
173
 
    @property
174
 
    def target(self):
175
 
        """See IMilestone."""
176
 
        if self.product:
177
 
            return self.product
178
 
        elif self.distribution:
179
 
            return self.distribution
180
 
 
181
 
    @property
182
 
    def series_target(self):
183
 
        """See IMilestone."""
184
 
        if self.productseries:
185
 
            return self.productseries
186
 
        elif self.distroseries:
187
 
            return self.distroseries
188
 
 
189
 
    @property
190
 
    def displayname(self):
191
 
        """See IMilestone."""
192
 
        return "%s %s" % (self.target.displayname, self.name)
193
 
 
194
 
    @property
195
 
    def title(self):
196
 
        """See IMilestone."""
197
 
        if not self.code_name:
198
 
            # XXX sinzui 2009-07-16 bug=400477: code_name may be None or ''.
199
 
            return self.displayname
200
 
        return ('%s "%s"') % (self.displayname, self.code_name)
201
 
 
202
 
    def _customizeSearchParams(self, search_params):
203
 
        """Customize `search_params` for this milestone."""
204
 
        search_params.milestone = self
205
 
 
206
 
    @property
207
 
    def official_bug_tags(self):
208
 
        """See `IHasBugs`."""
209
 
        return self.target.official_bug_tags
210
 
 
211
 
    def createProductRelease(self, owner, datereleased,
212
 
                             changelog=None, release_notes=None):
213
 
        """See `IMilestone`."""
214
 
        if self.product_release is not None:
215
 
            raise MultipleProductReleases()
216
 
        release = ProductRelease(
217
 
            owner=owner,
218
 
            changelog=changelog,
219
 
            release_notes=release_notes,
220
 
            datereleased=datereleased,
221
 
            milestone=self)
222
 
        return release
223
 
 
224
 
    def closeBugsAndBlueprints(self, user):
225
 
        """See `IMilestone`."""
226
 
        for bugtask in self.open_bugtasks:
227
 
            if bugtask.status == BugTaskStatus.FIXCOMMITTED:
228
 
                bugtask.bug.setStatus(
229
 
                    bugtask.target, BugTaskStatus.FIXRELEASED, user)
230
 
 
231
 
    def destroySelf(self):
232
 
        """See `IMilestone`."""
233
 
        params = BugTaskSearchParams(milestone=self, user=None)
234
 
        bugtasks = getUtility(IBugTaskSet).search(params)
235
 
        subscriptions = IResultSet(self.getSubscriptions())
236
 
        assert subscriptions.is_empty(), (
237
 
            "You cannot delete a milestone which has structural "
238
 
            "subscriptions.")
239
 
        assert bugtasks.count() == 0, (
240
 
            "You cannot delete a milestone which has bugtasks targeted "
241
 
            "to it.")
242
 
        assert self.specifications.count() == 0, (
243
 
            "You cannot delete a milestone which has specifications targeted "
244
 
            "to it.")
245
 
        assert self.product_release is None, (
246
 
            "You cannot delete a milestone which has a product release "
247
 
            "associated with it.")
248
 
        SQLBase.destroySelf(self)
249
 
 
250
 
 
251
 
class MilestoneSet:
252
 
    implements(IMilestoneSet)
253
 
 
254
 
    def __iter__(self):
255
 
        """See lp.registry.interfaces.milestone.IMilestoneSet."""
256
 
        for ms in Milestone.select():
257
 
            yield ms
258
 
 
259
 
    def get(self, milestoneid):
260
 
        """See lp.registry.interfaces.milestone.IMilestoneSet."""
261
 
        result = list(self.getByIds([milestoneid]))
262
 
        if not result:
263
 
            raise NotFoundError(
264
 
                "Milestone with ID %d does not exist" % milestoneid)
265
 
        return result[0]
266
 
 
267
 
    def getByIds(self, milestoneids):
268
 
        """See `IMilestoneSet`."""
269
 
        return IStore(Milestone).find(Milestone,
270
 
            Milestone.id.is_in(milestoneids))
271
 
 
272
 
    def getByNameAndProduct(self, name, product, default=None):
273
 
        """See lp.registry.interfaces.milestone.IMilestoneSet."""
274
 
        query = AND(Milestone.q.name==name,
275
 
                    Milestone.q.productID==product.id)
276
 
        milestone = Milestone.selectOne(query)
277
 
        if milestone is None:
278
 
            return default
279
 
        return milestone
280
 
 
281
 
    def getByNameAndDistribution(self, name, distribution, default=None):
282
 
        """See lp.registry.interfaces.milestone.IMilestoneSet."""
283
 
        query = AND(Milestone.q.name==name,
284
 
                    Milestone.q.distributionID==distribution.id)
285
 
        milestone = Milestone.selectOne(query)
286
 
        if milestone is None:
287
 
            return default
288
 
        return milestone
289
 
 
290
 
    def getVisibleMilestones(self):
291
 
        """See lp.registry.interfaces.milestone.IMilestoneSet."""
292
 
        return Milestone.selectBy(active=True, orderBy='id')
293
 
 
294
 
 
295
 
class ProjectMilestone(HasBugsBase):
296
 
    """A virtual milestone implementation for project.
297
 
 
298
 
    The current database schema has no formal concept of milestones related to
299
 
    projects. A milestone named `milestone` is considererd to belong to
300
 
    a project if the project contains at least one product with a milestone
301
 
    of the same name. A project milestone is considered to be active if at
302
 
    least one product milestone with the same name is active.  The
303
 
    `dateexpected` attribute of a project milestone is set to the minimum of
304
 
    the `dateexpected` values of the product milestones.
305
 
    """
306
 
 
307
 
    implements(IProjectGroupMilestone)
308
 
 
309
 
    def __init__(self, target, name, dateexpected, active):
310
 
        self.name = name
311
 
        self.code_name = None
312
 
        # The id is necessary for generating a unique memcache key
313
 
        # in a page template loop. The ProjectMilestone.id is passed
314
 
        # in as the third argument to the "cache" TALes.
315
 
        self.id = 'ProjectGroup:%s/Milestone:%s' % (target.name, name)
316
 
        self.code_name = None
317
 
        self.product = None
318
 
        self.distribution = None
319
 
        self.productseries = None
320
 
        self.distroseries = None
321
 
        self.product_release = None
322
 
        self.dateexpected = dateexpected
323
 
        self.active = active
324
 
        self.target = target
325
 
        self.series_target = None
326
 
        self.summary = None
327
 
 
328
 
    @property
329
 
    def specifications(self):
330
 
        """See `IMilestone`."""
331
 
        return Specification.select(
332
 
            """milestone IN
333
 
                (SELECT milestone.id
334
 
                    FROM Milestone, Product
335
 
                    WHERE Milestone.Product = Product.id
336
 
                    AND Milestone.name = %s
337
 
                    AND Product.project = %s)
338
 
            """ % sqlvalues(self.name, self.target),
339
 
            orderBy=['-priority', 'definition_status',
340
 
                     'implementation_status', 'title'],
341
 
            prejoins=['assignee'])
342
 
 
343
 
    @property
344
 
    def displayname(self):
345
 
        """See IMilestone."""
346
 
        return "%s %s" % (self.target.displayname, self.name)
347
 
 
348
 
    @property
349
 
    def title(self):
350
 
        """See IMilestone."""
351
 
        return self.displayname
352
 
 
353
 
    def _customizeSearchParams(self, search_params):
354
 
        """Customize `search_params` for this milestone."""
355
 
        search_params.milestone = self
356
 
 
357
 
    @property
358
 
    def official_bug_tags(self):
359
 
        """See `IHasBugs`."""
360
 
        return self.target.official_bug_tags
361
 
 
362
 
    def userHasBugSubscriptions(self, user):
363
 
        """See `IStructuralSubscriptionTarget`."""
364
 
        return False