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
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
|
# Copyright 2009-2011 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
__metaclass__ = type
__all__ = [
'ProductReleaseAddDownloadFileView',
'ProductReleaseAddView',
'ProductReleaseFromSeriesAddView',
'ProductReleaseContextMenu',
'ProductReleaseDeleteView',
'ProductReleaseEditView',
'ProductReleaseNavigation',
'ProductReleaseRdfView',
'ProductReleaseView',
]
import cgi
import mimetypes
from lazr.restful.interface import copy_field
from z3c.ptcompat import ViewPageTemplateFile
from zope.app.form.browser import (
TextAreaWidget,
TextWidget,
)
from zope.event import notify
from zope.formlib.form import FormFields
from zope.lifecycleevent import ObjectCreatedEvent
from zope.schema import Bool
from zope.schema.vocabulary import (
SimpleTerm,
SimpleVocabulary,
)
from canonical.launchpad import _
from canonical.launchpad.webapp import (
canonical_url,
ContextMenu,
enabled_with_permission,
LaunchpadView,
Link,
Navigation,
stepthrough,
)
from canonical.lazr.utils import smartquote
from lp.app.browser.launchpadform import (
action,
custom_widget,
LaunchpadEditFormView,
LaunchpadFormView,
)
from lp.app.widgets.date import DateTimeWidget
from lp.registry.browser import (
BaseRdfView,
MilestoneOverlayMixin,
RegistryDeleteViewMixin,
)
from lp.registry.browser.product import ProductDownloadFileMixin
from lp.registry.interfaces.productrelease import (
IProductRelease,
IProductReleaseFileAddForm,
)
class ProductReleaseNavigation(Navigation):
usedfor = IProductRelease
@stepthrough('+download')
def download(self, name):
return self.context.getFileAliasByName(name)
@stepthrough('+file')
def fileaccess(self, name):
return self.context.getProductReleaseFileByName(name)
class ProductReleaseContextMenu(ContextMenu):
usedfor = IProductRelease
links = ('edit', 'add_file', 'download', 'delete')
@enabled_with_permission('launchpad.Edit')
def edit(self):
text = 'Change release details'
summary = "Edit this release"
return Link('+edit', text, summary=summary, icon='edit')
@enabled_with_permission('launchpad.Edit')
def delete(self):
text = 'Delete release'
summary = "Delete release"
return Link('+delete', text, summary=summary, icon='remove')
@enabled_with_permission('launchpad.Edit')
def add_file(self):
text = 'Add download file'
return Link('+adddownloadfile', text, icon='add')
def download(self):
text = 'Download RDF metadata'
return Link('+rdf', text, icon='download')
class ProductReleaseAddViewBase(LaunchpadFormView):
"""Base class for creating a release from an existing or new milestone.
Subclasses need to define the field_names a form action.
"""
schema = IProductRelease
custom_widget('datereleased', DateTimeWidget)
custom_widget('release_notes', TextAreaWidget, height=7, width=62)
custom_widget('changelog', TextAreaWidget, height=7, width=62)
def _prependKeepMilestoneActiveField(self):
keep_milestone_active_checkbox = FormFields(
Bool(
__name__='keep_milestone_active',
title=_("Keep the %s milestone active." % self.context.name),
description=_(
"Only select this if bugs or blueprints still need "
"to be targeted to this project release's milestone.")),
render_context=self.render_context)
self.form_fields = keep_milestone_active_checkbox + self.form_fields
def _createRelease(self, milestone, data):
"""Create product release for this milestone."""
newrelease = milestone.createProductRelease(
self.user, changelog=data['changelog'],
release_notes=data['release_notes'],
datereleased=data['datereleased'])
# Set Milestone.active to false, since bugs & blueprints
# should not be targeted to a milestone in the past.
if data.get('keep_milestone_active') is False:
milestone.active = False
milestone_link = '<a href="%s">%s milestone</a>' % (
canonical_url(milestone), cgi.escape(milestone.name))
self.next_url = canonical_url(newrelease.milestone)
notify(ObjectCreatedEvent(newrelease))
@property
def label(self):
"""The form label."""
return smartquote('Create a new release for %s' %
self.context.product.displayname)
page_title = label
@property
def cancel_url(self):
return canonical_url(self.context)
class ProductReleaseAddView(ProductReleaseAddViewBase):
"""Create a product release.
Also, deactivate the milestone it is attached to.
"""
field_names = [
'datereleased',
'release_notes',
'changelog',
]
def initialize(self):
if self.context.product_release is not None:
self.request.response.addErrorNotification(
_("A project release already exists for this milestone."))
self.request.response.redirect(
canonical_url(self.context.product_release) + '/+edit')
else:
super(ProductReleaseAddView, self).initialize()
def setUpFields(self):
super(ProductReleaseAddView, self).setUpFields()
if self.context.active is True:
self._prependKeepMilestoneActiveField()
@action(_('Create release'), name='create')
def createRelease(self, action, data):
self._createRelease(self.context, data)
class ProductReleaseFromSeriesAddView(ProductReleaseAddViewBase,
MilestoneOverlayMixin):
"""Create a product release from an existing or new milestone.
Also, deactivate the milestone it is attached to.
"""
field_names = [
'datereleased',
'release_notes',
'changelog',
]
def initialize(self):
# The dynamically loaded milestone form needs this javascript
# enabled in the base-layout.
self.request.needs_datepicker_iframe = True
super(ProductReleaseFromSeriesAddView, self).initialize()
def setUpFields(self):
super(ProductReleaseFromSeriesAddView, self).setUpFields()
self._prependKeepMilestoneActiveField()
self._prependMilestoneField()
def _prependMilestoneField(self):
"""Add Milestone Choice field with custom terms."""
terms = [
SimpleTerm(milestone, milestone.name, milestone.name)
for milestone in self.context.all_milestones
if milestone.product_release is None]
terms.insert(0, SimpleTerm(None, None, '- Select Milestone -'))
milestone_field = FormFields(
copy_field(
IProductRelease['milestone'],
__name__='milestone_for_release',
vocabulary=SimpleVocabulary(terms)))
self.form_fields = milestone_field + self.form_fields
@action(_('Create release'), name='create')
def createRelease(self, action, data):
milestone = data['milestone_for_release']
self._createRelease(milestone, data)
class ProductReleaseEditView(LaunchpadEditFormView):
"""Edit view for ProductRelease objects"""
schema = IProductRelease
field_names = [
"datereleased",
"release_notes",
"changelog",
]
custom_widget('datereleased', DateTimeWidget)
custom_widget('release_notes', TextAreaWidget, height=7, width=62)
custom_widget('changelog', TextAreaWidget, height=7, width=62)
@property
def label(self):
"""The form label."""
return smartquote('Edit %s release details' % self.context.title)
page_title = label
@action('Change', name='change')
def change_action(self, action, data):
self.updateContextFromData(data)
self.next_url = canonical_url(self.context)
@property
def cancel_url(self):
return canonical_url(self.context)
class ProductReleaseRdfView(BaseRdfView):
"""A view that sets its mime-type to application/rdf+xml"""
template = ViewPageTemplateFile('../templates/productrelease-rdf.pt')
@property
def filename(self):
return '%s-%s-%s' % (
self.context.product.name,
self.context.productseries.name,
self.context.version)
class ProductReleaseAddDownloadFileView(LaunchpadFormView):
"""A view for adding a file to an `IProductRelease`."""
schema = IProductReleaseFileAddForm
custom_widget('description', TextWidget, width=62)
@property
def label(self):
"""The form label."""
return smartquote('Add a download file to %s' % self.context.title)
page_title = label
@action('Upload', name='add')
def add_action(self, action, data):
form = self.request.form
file_upload = form.get(self.widgets['filecontent'].name)
signature_upload = form.get(self.widgets['signature'].name)
filetype = data['contenttype']
# XXX: BradCrittenden 2007-04-26 bug=115215 Write a proper upload
# widget.
if file_upload is not None and len(data['description']) > 0:
# XXX Edwin Grubbs 2008-09-10 bug=268680
# Once python-magic is available on the production servers,
# the content-type should be verified instead of trusting
# the extension that mimetypes.guess_type() examines.
content_type, encoding = mimetypes.guess_type(
file_upload.filename)
if content_type is None:
content_type = "text/plain"
# signature_upload is u'' if no file is specified in
# the browser.
if signature_upload:
signature_filename = signature_upload.filename
signature_content = data['signature']
else:
signature_filename = None
signature_content = None
release_file = self.context.addReleaseFile(
filename=file_upload.filename,
file_content=data['filecontent'],
content_type=content_type,
uploader=self.user,
signature_filename=signature_filename,
signature_content=signature_content,
file_type=filetype,
description=data['description'])
self.request.response.addNotification(
"Your file '%s' has been uploaded."
% release_file.libraryfile.filename)
self.next_url = canonical_url(self.context)
@property
def cancel_url(self):
return canonical_url(self.context)
class ProductReleaseView(LaunchpadView, ProductDownloadFileMixin):
"""View for ProductRelease overview."""
def initialize(self):
self.form = self.request.form
self.processDeleteFiles()
def getReleases(self):
"""See `ProductDownloadFileMixin`."""
return set([self.context])
class ProductReleaseDeleteView(LaunchpadFormView, RegistryDeleteViewMixin):
"""A view for deleting an `IProductRelease`."""
schema = IProductRelease
field_names = []
@property
def label(self):
"""The form label."""
return smartquote('Delete %s' % self.context.title)
page_title = label
@action('Delete Release', name='delete')
def delete_action(self, action, data):
series = self.context.productseries
version = self.context.version
self._deleteRelease(self.context)
self.request.response.addInfoNotification(
"Release %s deleted." % version)
self.next_url = canonical_url(series)
@property
def cancel_url(self):
return canonical_url(self.context)
|