1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
|
# Copyright 2009 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
# pylint: disable-msg=E0611,W0212
__metaclass__ = type
__all__ = [
'ProductRelease',
'ProductReleaseFile',
'ProductReleaseSet',
'productrelease_to_milestone',
]
from StringIO import StringIO
from sqlobject import (
ForeignKey,
SQLMultipleJoin,
StringCol,
)
from storm.expr import (
And,
Desc,
)
from storm.store import EmptyResultSet
from zope.component import getUtility
from zope.interface import implements
from canonical.database.constants import UTC_NOW
from canonical.database.datetimecol import UtcDateTimeCol
from canonical.database.enumcol import EnumCol
from canonical.database.sqlbase import (
SQLBase,
sqlvalues,
)
from canonical.launchpad.webapp.interfaces import (
DEFAULT_FLAVOR,
IStoreSelector,
MAIN_STORE,
)
from lp.app.errors import NotFoundError
from lp.registry.interfaces.person import (
validate_person,
validate_public_person,
)
from lp.registry.interfaces.productrelease import (
IProductRelease,
IProductReleaseFile,
IProductReleaseSet,
UpstreamFileType,
)
from lp.services.database.lpstorm import IStore
from lp.services.librarian.interfaces import ILibraryFileAliasSet
SEEK_END = 2 # Python2.4 has no definition for SEEK_END.
class ProductRelease(SQLBase):
"""A release of a product."""
implements(IProductRelease)
_table = 'ProductRelease'
_defaultOrder = ['-datereleased']
datereleased = UtcDateTimeCol(notNull=True)
release_notes = StringCol(notNull=False, default=None)
changelog = StringCol(notNull=False, default=None)
datecreated = UtcDateTimeCol(
dbName='datecreated', notNull=True, default=UTC_NOW)
owner = ForeignKey(
dbName="owner", foreignKey="Person",
storm_validator=validate_person,
notNull=True)
milestone = ForeignKey(dbName='milestone', foreignKey='Milestone')
files = SQLMultipleJoin(
'ProductReleaseFile', joinColumn='productrelease',
orderBy='-date_uploaded', prejoins=['productrelease'])
# properties
@property
def codename(self):
"""Backwards compatible codename attribute.
This attribute was moved to the Milestone."""
# XXX EdwinGrubbs 2009-02-02 bug=324394: Remove obsolete attributes.
return self.milestone.code_name
@property
def version(self):
"""Backwards compatible version attribute.
This attribute was replaced by the Milestone.name."""
# XXX EdwinGrubbs 2009-02-02 bug=324394: Remove obsolete attributes.
return self.milestone.name
@property
def summary(self):
"""Backwards compatible summary attribute.
This attribute was replaced by the Milestone.summary."""
# XXX EdwinGrubbs 2009-02-02 bug=324394: Remove obsolete attributes.
return self.milestone.summary
@property
def productseries(self):
"""Backwards compatible summary attribute.
This attribute was replaced by the Milestone.productseries."""
# XXX EdwinGrubbs 2009-02-02 bug=324394: Remove obsolete attributes.
return self.milestone.productseries
@property
def product(self):
"""Backwards compatible summary attribute.
This attribute was replaced by the Milestone.productseries.product."""
# XXX EdwinGrubbs 2009-02-02 bug=324394: Remove obsolete attributes.
return self.productseries.product
@property
def displayname(self):
"""See `IProductRelease`."""
return self.milestone.displayname
@property
def title(self):
"""See `IProductRelease`."""
return self.milestone.title
@staticmethod
def normalizeFilename(filename):
# Replace slashes in the filename with less problematic dashes.
return filename.replace('/', '-')
def destroySelf(self):
"""See `IProductRelease`."""
assert self.files.count() == 0, (
"You can't delete a product release which has files associated "
"with it.")
SQLBase.destroySelf(self)
def _getFileObjectAndSize(self, file_or_data):
"""Return an object and length for file_or_data.
:param file_or_data: A string or a file object or StringIO object.
:return: file object or StringIO object and size.
"""
if isinstance(file_or_data, basestring):
file_size = len(file_or_data)
file_obj = StringIO(file_or_data)
else:
assert isinstance(file_or_data, (file, StringIO)), (
"file_or_data is not an expected type")
file_obj = file_or_data
start = file_obj.tell()
file_obj.seek(0, SEEK_END)
file_size = file_obj.tell()
file_obj.seek(start)
return file_obj, file_size
def addReleaseFile(self, filename, file_content, content_type,
uploader, signature_filename=None,
signature_content=None,
file_type=UpstreamFileType.CODETARBALL,
description=None):
"""See `IProductRelease`."""
# Create the alias for the file.
filename = self.normalizeFilename(filename)
file_obj, file_size = self._getFileObjectAndSize(file_content)
alias = getUtility(ILibraryFileAliasSet).create(
name=filename,
size=file_size,
file=file_obj,
contentType=content_type)
if signature_filename is not None and signature_content is not None:
signature_obj, signature_size = self._getFileObjectAndSize(
signature_content)
signature_filename = self.normalizeFilename(
signature_filename)
signature_alias = getUtility(ILibraryFileAliasSet).create(
name=signature_filename,
size=signature_size,
file=signature_obj,
contentType='application/pgp-signature')
else:
signature_alias = None
return ProductReleaseFile(productrelease=self,
libraryfile=alias,
signature=signature_alias,
filetype=file_type,
description=description,
uploader=uploader)
def getFileAliasByName(self, name):
"""See `IProductRelease`."""
for file_ in self.files:
if file_.libraryfile.filename == name:
return file_.libraryfile
elif file_.signature and file_.signature.filename == name:
return file_.signature
raise NotFoundError(name)
def getProductReleaseFileByName(self, name):
"""See `IProductRelease`."""
for file_ in self.files:
if file_.libraryfile.filename == name:
return file_
raise NotFoundError(name)
class ProductReleaseFile(SQLBase):
"""A file of a product release."""
implements(IProductReleaseFile)
_table = 'ProductReleaseFile'
productrelease = ForeignKey(dbName='productrelease',
foreignKey='ProductRelease', notNull=True)
libraryfile = ForeignKey(dbName='libraryfile',
foreignKey='LibraryFileAlias', notNull=True)
signature = ForeignKey(dbName='signature',
foreignKey='LibraryFileAlias')
filetype = EnumCol(dbName='filetype', enum=UpstreamFileType,
notNull=True, default=UpstreamFileType.CODETARBALL)
description = StringCol(notNull=False, default=None)
uploader = ForeignKey(
dbName="uploader", foreignKey='Person',
storm_validator=validate_public_person, notNull=True)
date_uploaded = UtcDateTimeCol(notNull=True, default=UTC_NOW)
class ProductReleaseSet(object):
"""See `IProductReleaseSet`."""
implements(IProductReleaseSet)
def getBySeriesAndVersion(self, productseries, version, default=None):
"""See `IProductReleaseSet`."""
# Local import of Milestone to avoid circular imports.
from lp.registry.model.milestone import Milestone
store = IStore(productseries)
# The Milestone is cached too because most uses of a ProductRelease
# need it.
result = store.find(
(ProductRelease, Milestone),
Milestone.productseries == productseries,
ProductRelease.milestone == Milestone.id,
Milestone.name == version)
found = result.one()
if found is None:
return None
product_release, milestone = found
return product_release
def getReleasesForSeries(self, series):
"""See `IProductReleaseSet`."""
# Local import of Milestone to avoid import loop.
from lp.registry.model.milestone import Milestone
if len(list(series)) == 0:
return EmptyResultSet()
store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR)
series_ids = [s.id for s in series]
result = store.find(
ProductRelease,
And(ProductRelease.milestone == Milestone.id),
Milestone.productseriesID.is_in(series_ids))
return result.order_by(Desc(ProductRelease.datereleased))
def getFilesForReleases(self, releases):
"""See `IProductReleaseSet`."""
releases = list(releases)
if len(releases) == 0:
return EmptyResultSet()
return ProductReleaseFile.select(
"""ProductReleaseFile.productrelease IN %s""" % (
sqlvalues([release.id for release in releases])),
orderBy='-date_uploaded',
prejoins=['libraryfile', 'libraryfile.content', 'productrelease'])
def productrelease_to_milestone(productrelease):
"""Adapt an `IProductRelease` to an `IMilestone`."""
return productrelease.milestone
|