Merge lp:~wgrant/launchpad/rework-bug-default-type-1 into lp:launchpad

Proposed by William Grant
Status: Merged
Merged at revision: 15725
Proposed branch: lp:~wgrant/launchpad/rework-bug-default-type-1
Merge into: lp:launchpad
Prerequisite: lp:~wgrant/launchpad/rework-bug-default-type-0
Diff against target: 544 lines (+166/-254)
5 files modified
lib/lp/bugs/browser/bugtarget.py (+117/-140)
lib/lp/bugs/browser/configure.zcml (+0/-6)
lib/lp/bugs/browser/tests/test_bugtarget_filebug.py (+5/-55)
lib/lp/bugs/templates/bugtarget-filebug-guidelines.pt (+0/-51)
lib/lp/bugs/templates/bugtarget-macros-filebug.pt (+44/-2)
To merge this branch: bzr merge lp:~wgrant/launchpad/rework-bug-default-type-1
Reviewer Review Type Date Requested Status
Richard Harding (community) Approve
Review via email: mp+117380@code.launchpad.net

Commit message

Merge FileBugReportingGuidelines into FileBugViewBase, as ProjectGroup:+filebug doesn't need it rendered separately any more.

Description of the change

This branch is the second in a series of maybe four to rework bug information type defaults, which will eventually allow us to default to Proprietary when a project is so configured.

Some awkwardness arose during UI refactoring due to the split between FileBugViewBase and FileBugReportingGuidelines (which -- contrary to its name -- includes not just the bug reporting guidelines, but the privacy widgets too). I eventually realised that the split was for some code in ProjectGroup:+filebug that has since been removed, so I opted to merge the views, templates and tests.

The only minor awkwardness was that the form relies on the JSON request cache being fully populated, but form action templates are rendered in LaunchpadFormView.initialize(). So we can't do the super() call first as is usual. This problem was accidentally worked around with the old structure, since the JSON cache ended up populated by the separately initialised subview.

To post a comment you must log in.
Revision history for this message
Richard Harding (rharding) wrote :

Think I follow. Thanks for the cleanup.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/lp/bugs/browser/bugtarget.py'
--- lib/lp/bugs/browser/bugtarget.py 2012-07-31 05:46:45 +0000
+++ lib/lp/bugs/browser/bugtarget.py 2012-08-01 01:08:21 +0000
@@ -243,30 +243,40 @@
243 self.updateContextFromData(data)243 self.updateContextFromData(data)
244244
245245
246class FileBugReportingGuidelines(LaunchpadFormView):246class FileBugViewBase(LaunchpadFormView):
247 """Provides access to common bug reporting attributes.247 """Base class for views related to filing a bug."""
248248
249 Attributes provided are: information_type and bug_reporting_guidelines.249 implements(IBrowserPublisher)
250
251 This view is a superclass of `FileBugViewBase` so that non-ajax browsers
252 can load the file bug form, and it is also invoked directly via an XHR
253 request to provide an HTML snippet for Javascript enabled browsers.
254 """
255250
256 schema = IBug251 schema = IBug
257252
258 @property
259 def field_names(self):
260 """Return the list of field names to display."""
261 if self.is_bug_supervisor:
262 return ['information_type']
263 else:
264 return ['security_related']
265
266 custom_widget('information_type', LaunchpadRadioWidgetWithDescription)253 custom_widget('information_type', LaunchpadRadioWidgetWithDescription)
267254
255 extra_data_token = None
256 advanced_form = False
257 frontpage_form = False
258 data_parser = None
259
260 def __init__(self, context, request):
261 LaunchpadFormView.__init__(self, context, request)
262 self.extra_data = FileBugData()
263
268 def initialize(self):264 def initialize(self):
269 super(FileBugReportingGuidelines, self).initialize()265 # redirect_ubuntu_filebug is a cached_property.
266 # Access it first just to compute its value. Because it
267 # makes a DB access to get the bug supervisor, it causes
268 # trouble in tests when form validation errors occur. Because the
269 # transaction is doomed, the storm cache is invalidated and accessing
270 # the property will result in a a LostObjectError, because
271 # the created objects disappeared. Not likely a problem in production
272 # since the objects will still be in the DB, but doesn't hurt there
273 # either. It makes for better diagnosis of failing tests.
274 if self.redirect_ubuntu_filebug:
275 pass
276
277 # The JSON cache must be populated before the super call, since
278 # the form is rendered during LaunchpadFormView's initialize()
279 # when an action is invokved.
270 cache = IJSONRequestCache(self.request)280 cache = IJSONRequestCache(self.request)
271 cache.objects['private_types'] = [281 cache.objects['private_types'] = [
272 type.name for type in PRIVATE_INFORMATION_TYPES]282 type.name for type in PRIVATE_INFORMATION_TYPES]
@@ -296,125 +306,12 @@
296 css_class_prefix='importance',306 css_class_prefix='importance',
297 excluded_items=[BugTaskImportance.UNKNOWN])307 excluded_items=[BugTaskImportance.UNKNOWN])
298 cache.objects['bugtask_importance_data'] = bugtask_importance_data308 cache.objects['bugtask_importance_data'] = bugtask_importance_data
299
300 def setUpFields(self):
301 """Set up the form fields. See `LaunchpadFormView`."""
302 super(FileBugReportingGuidelines, self).setUpFields()
303
304 # Project groups are special. The Next button sends you to
305 # Product:+filebug, so we need none of the usual stuff.
306 if IProjectGroup.providedBy(self.context):
307 return
308
309 if self.is_bug_supervisor:
310 info_type_vocab = InformationTypeVocabulary(
311 types=self.context.pillar.getAllowedBugInformationTypes())
312 information_type_field = copy_field(
313 IBug['information_type'], readonly=False,
314 vocabulary=info_type_vocab)
315 self.form_fields = self.form_fields.omit('information_type')
316 self.form_fields += Fields(information_type_field)
317 else:
318 security_related_field = copy_field(
319 IBug['security_related'], readonly=False)
320 self.form_fields = self.form_fields.omit('security_related')
321 self.form_fields += Fields(security_related_field)
322
323 @property
324 def initial_values(self):
325 """See `LaunchpadFormView`."""
326 value = InformationType.PUBLIC
327 if (self.context and IProduct.providedBy(self.context) and
328 self.context.private_bugs):
329 value = InformationType.USERDATA
330 return {'information_type': value}
331
332 @property
333 def bug_reporting_guidelines(self):
334 """Guidelines for filing bugs in the current context.
335
336 Returns a list of dicts, with each dict containing values for
337 "preamble" and "content".
338 """
339
340 def target_name(target):
341 # IProjectGroup can be considered the target of a bug during
342 # the bug filing process, but does not extend IBugTarget
343 # and ultimately cannot actually be the target of a
344 # bug. Hence this function to determine a suitable
345 # name/title to display. Hurrumph.
346 if IBugTarget.providedBy(target):
347 return target.bugtargetdisplayname
348 else:
349 return target.displayname
350
351 guidelines = []
352 bugtarget = self.context
353 if bugtarget is not None:
354 content = bugtarget.bug_reporting_guidelines
355 if content is not None and len(content) > 0:
356 guidelines.append({
357 "source": target_name(bugtarget),
358 "content": content,
359 })
360 # Distribution source packages are shown with both their
361 # own reporting guidelines and those of their
362 # distribution.
363 if IDistributionSourcePackage.providedBy(bugtarget):
364 distribution = bugtarget.distribution
365 content = distribution.bug_reporting_guidelines
366 if content is not None and len(content) > 0:
367 guidelines.append({
368 "source": target_name(distribution),
369 "content": content,
370 })
371 return guidelines
372
373 def getMainContext(self):
374 if IDistributionSourcePackage.providedBy(self.context):
375 return self.context.distribution
376 else:
377 return self.context
378
379 @cachedproperty
380 def is_bug_supervisor(self):
381 """ Return True if the logged in user is a bug supervisor."""
382 context = self.getMainContext()
383 return BugTask.userHasBugSupervisorPrivilegesContext(
384 context, self.user)
385
386
387class FileBugViewBase(FileBugReportingGuidelines, LaunchpadFormView):
388 """Base class for views related to filing a bug."""
389
390 implements(IBrowserPublisher)
391
392 extra_data_token = None
393 advanced_form = False
394 frontpage_form = False
395 data_parser = None
396
397 def __init__(self, context, request):
398 LaunchpadFormView.__init__(self, context, request)
399 self.extra_data = FileBugData()
400
401 def initialize(self):
402 # redirect_ubuntu_filebug is a cached_property.
403 # Access it first just to compute its value. Because it
404 # makes a DB access to get the bug supervisor, it causes
405 # trouble in tests when form validation errors occur. Because the
406 # transaction is doomed, the storm cache is invalidated and accessing
407 # the property will result in a a LostObjectError, because
408 # the created objects disappeared. Not likely a problem in production
409 # since the objects will still be in the DB, but doesn't hurt there
410 # either. It makes for better diagnosis of failing tests.
411 if self.redirect_ubuntu_filebug:
412 pass
413 super(FileBugViewBase, self).initialize()
414 cache = IJSONRequestCache(self.request)
415 cache.objects['enable_bugfiling_duplicate_search'] = (309 cache.objects['enable_bugfiling_duplicate_search'] = (
416 IProjectGroup.providedBy(self.context)310 IProjectGroup.providedBy(self.context)
417 or self.context.enable_bugfiling_duplicate_search)311 or self.context.enable_bugfiling_duplicate_search)
312
313 super(FileBugViewBase, self).initialize()
314
418 if (self.extra_data_token is not None and315 if (self.extra_data_token is not None and
419 not self.extra_data_to_process):316 not self.extra_data_to_process):
420 # self.extra_data has been initialized in publishTraverse().317 # self.extra_data has been initialized in publishTraverse().
@@ -492,10 +389,17 @@
492 @property389 @property
493 def initial_values(self):390 def initial_values(self):
494 """Give packagename a default value, if applicable."""391 """Give packagename a default value, if applicable."""
495 if not IDistributionSourcePackage.providedBy(self.context):392 if (self.context and IProduct.providedBy(self.context)
496 return {}393 and self.context.private_bugs):
497394 type = InformationType.USERDATA
498 return {'packagename': self.context.name}395 else:
396 type = InformationType.PUBLIC
397 values = {'information_type': type}
398
399 if IDistributionSourcePackage.providedBy(self.context):
400 values['packagename'] = self.context.name
401
402 return values
499403
500 def contextIsProduct(self):404 def contextIsProduct(self):
501 return IProduct.providedBy(self.context)405 return IProduct.providedBy(self.context)
@@ -588,7 +492,7 @@
588492
589 def setUpWidgets(self):493 def setUpWidgets(self):
590 """Customize the onKeyPress event of the package name chooser."""494 """Customize the onKeyPress event of the package name chooser."""
591 LaunchpadFormView.setUpWidgets(self)495 super(FileBugViewBase, self).setUpWidgets()
592496
593 if "packagename" in self.field_names:497 if "packagename" in self.field_names:
594 self.widgets["packagename"].onKeyPress = (498 self.widgets["packagename"].onKeyPress = (
@@ -598,6 +502,25 @@
598 """Set up the form fields. See `LaunchpadFormView`."""502 """Set up the form fields. See `LaunchpadFormView`."""
599 super(FileBugViewBase, self).setUpFields()503 super(FileBugViewBase, self).setUpFields()
600504
505 # Project groups are special. The Next button sends you to
506 # Product:+filebug, so we need none of the usual stuff.
507 if IProjectGroup.providedBy(self.context):
508 return
509
510 if self.is_bug_supervisor:
511 info_type_vocab = InformationTypeVocabulary(
512 types=self.context.pillar.getAllowedBugInformationTypes())
513 information_type_field = copy_field(
514 IBug['information_type'], readonly=False,
515 vocabulary=info_type_vocab)
516 self.form_fields = self.form_fields.omit('information_type')
517 self.form_fields += Fields(information_type_field)
518 else:
519 security_related_field = copy_field(
520 IBug['security_related'], readonly=False)
521 self.form_fields = self.form_fields.omit('security_related')
522 self.form_fields += Fields(security_related_field)
523
601 # Override the vocabulary for the subscribe_to_existing_bug524 # Override the vocabulary for the subscribe_to_existing_bug
602 # field.525 # field.
603 subscribe_field = Choice(526 subscribe_field = Choice(
@@ -1017,6 +940,60 @@
1017 else:940 else:
1018 return True941 return True
1019942
943 @property
944 def bug_reporting_guidelines(self):
945 """Guidelines for filing bugs in the current context.
946
947 Returns a list of dicts, with each dict containing values for
948 "preamble" and "content".
949 """
950
951 def target_name(target):
952 # IProjectGroup can be considered the target of a bug during
953 # the bug filing process, but does not extend IBugTarget
954 # and ultimately cannot actually be the target of a
955 # bug. Hence this function to determine a suitable
956 # name/title to display. Hurrumph.
957 if IBugTarget.providedBy(target):
958 return target.bugtargetdisplayname
959 else:
960 return target.displayname
961
962 guidelines = []
963 bugtarget = self.context
964 if bugtarget is not None:
965 content = bugtarget.bug_reporting_guidelines
966 if content is not None and len(content) > 0:
967 guidelines.append({
968 "source": target_name(bugtarget),
969 "content": content,
970 })
971 # Distribution source packages are shown with both their
972 # own reporting guidelines and those of their
973 # distribution.
974 if IDistributionSourcePackage.providedBy(bugtarget):
975 distribution = bugtarget.distribution
976 content = distribution.bug_reporting_guidelines
977 if content is not None and len(content) > 0:
978 guidelines.append({
979 "source": target_name(distribution),
980 "content": content,
981 })
982 return guidelines
983
984 def getMainContext(self):
985 if IDistributionSourcePackage.providedBy(self.context):
986 return self.context.distribution
987 else:
988 return self.context
989
990 @cachedproperty
991 def is_bug_supervisor(self):
992 """ Return True if the logged in user is a bug supervisor."""
993 context = self.getMainContext()
994 return BugTask.userHasBugSupervisorPrivilegesContext(
995 context, self.user)
996
1020997
1021class FileBugAdvancedView(FileBugViewBase):998class FileBugAdvancedView(FileBugViewBase):
1022 """Browser view for filing a bug.999 """Browser view for filing a bug.
@@ -1131,7 +1108,7 @@
1131 show_summary_in_results = True1108 show_summary_in_results = True
11321109
1133 def initialize(self):1110 def initialize(self):
1134 FilebugShowSimilarBugsView.initialize(self)1111 super(FileBugGuidedView, self).initialize()
1135 if self.redirect_ubuntu_filebug:1112 if self.redirect_ubuntu_filebug:
1136 # The user is trying to file a new Ubuntu bug via the web1113 # The user is trying to file a new Ubuntu bug via the web
1137 # interface and without using apport. Redirect to a page1114 # interface and without using apport. Redirect to a page
11381115
=== modified file 'lib/lp/bugs/browser/configure.zcml'
--- lib/lp/bugs/browser/configure.zcml 2012-07-29 23:38:26 +0000
+++ lib/lp/bugs/browser/configure.zcml 2012-08-01 01:08:21 +0000
@@ -103,12 +103,6 @@
103 template="../templates/bugtarget-filebug-inline-form.pt"103 template="../templates/bugtarget-filebug-inline-form.pt"
104 permission="launchpad.AnyPerson"/>104 permission="launchpad.AnyPerson"/>
105 <browser:page105 <browser:page
106 for="lp.bugs.interfaces.bugtarget.IHasBugs"
107 class="lp.bugs.browser.bugtarget.FileBugReportingGuidelines"
108 template="../templates/bugtarget-filebug-guidelines.pt"
109 permission="launchpad.AnyPerson"
110 name="+filebug-reporting-guidelines"/>
111 <browser:page
112 name="+manage-official-tags"106 name="+manage-official-tags"
113 for="lp.bugs.interfaces.bugtarget.IOfficialBugTagTargetRestricted"107 for="lp.bugs.interfaces.bugtarget.IOfficialBugTagTargetRestricted"
114 class="lp.bugs.browser.bugtarget.OfficialBugTagsManageView"108 class="lp.bugs.browser.bugtarget.OfficialBugTagsManageView"
115109
=== modified file 'lib/lp/bugs/browser/tests/test_bugtarget_filebug.py'
--- lib/lp/bugs/browser/tests/test_bugtarget_filebug.py 2012-07-26 07:54:40 +0000
+++ lib/lp/bugs/browser/tests/test_bugtarget_filebug.py 2012-08-01 01:08:21 +0000
@@ -340,17 +340,11 @@
340 self.assertEqual(0, len(view.errors))340 self.assertEqual(0, len(view.errors))
341 self.assertTrue(view.added_bug is not None)341 self.assertTrue(view.added_bug is not None)
342342
343
344class TestFileBugReportingGuidelines(TestCaseWithFactory):
345
346 layer = DatabaseFunctionalLayer
347
348 def test_filebug_reporting_details(self):343 def test_filebug_reporting_details(self):
349 product = self.factory.makeProduct()344 product = self.factory.makeProduct()
350 login_person(product.owner)345 login_person(product.owner)
351 product.bug_reporting_guidelines = "Include bug details"346 product.bug_reporting_guidelines = "Include bug details"
352 view = create_initialized_view(347 view = create_initialized_view(product, '+filebug')
353 product, '+filebug-reporting-guidelines')
354 expected_guidelines = [{348 expected_guidelines = [{
355 "source": product.displayname, "content": u"Include bug details",349 "source": product.displayname, "content": u"Include bug details",
356 }]350 }]
@@ -522,53 +516,6 @@
522 self.assertIn("Thank you for your bug report.", msg)516 self.assertIn("Thank you for your bug report.", msg)
523517
524518
525class TestFileBugGuidelinesRequestCache(TestCaseWithFactory):
526 # Tests to ensure the request cache contains the expected values for
527 # file bug guidelines views.
528
529 layer = DatabaseFunctionalLayer
530
531 def _assert_cache_values(self, view, private_bugs, duplicate_search):
532 cache = IJSONRequestCache(view.request).objects
533 self.assertContentEqual(cache['private_types'], [
534 type.name for type in PRIVATE_INFORMATION_TYPES])
535 self.assertEqual(cache['bug_private_by_default'], private_bugs)
536
537 def test_product(self):
538 project = self.factory.makeProduct(official_malone=True)
539 user = self.factory.makePerson()
540 login_person(user)
541 view = create_initialized_view(project,
542 '+filebug-reporting-guidelines', principal=user)
543 self._assert_cache_values(view, False, True)
544
545 def test_product_default_private(self):
546 product = self.factory.makeProduct(official_malone=True)
547 removeSecurityProxy(product).private_bugs = True
548 user = self.factory.makePerson()
549 login_person(user)
550 view = create_initialized_view(product,
551 '+filebug-reporting-guidelines', principal=user)
552 self._assert_cache_values(view, True, True)
553
554 def test_product_no_duplicate_search(self):
555 product = self.factory.makeProduct(official_malone=True)
556 removeSecurityProxy(product).enable_bugfiling_duplicate_search = False
557 user = self.factory.makePerson()
558 login_person(user)
559 view = create_initialized_view(product,
560 '+filebug-reporting-guidelines', principal=user)
561 self._assert_cache_values(view, False, False)
562
563 def test_project_group(self):
564 project = self.factory.makeProject()
565 user = self.factory.makePerson()
566 login_person(user)
567 view = create_initialized_view(project,
568 '+filebug-reporting-guidelines', principal=user)
569 self._assert_cache_values(view, False, True)
570
571
572class TestFileBugRequestCache(TestCaseWithFactory):519class TestFileBugRequestCache(TestCaseWithFactory):
573 # Tests to ensure the request cache contains the expected values for520 # Tests to ensure the request cache contains the expected values for
574 # file bug views.521 # file bug views.
@@ -581,7 +528,7 @@
581 'disclosure.enhanced_choice_popup.enabled': 'true'528 'disclosure.enhanced_choice_popup.enabled': 'true'
582 }))529 }))
583530
584 def _assert_cache_values(self, view, duplicate_search, private_only=False):531 def _assert_cache_values(self, view, duplicate_search, private_bugs=False):
585 cache = IJSONRequestCache(view.request).objects532 cache = IJSONRequestCache(view.request).objects
586 self.assertEqual(533 self.assertEqual(
587 duplicate_search, cache['enable_bugfiling_duplicate_search'])534 duplicate_search, cache['enable_bugfiling_duplicate_search'])
@@ -628,6 +575,9 @@
628 bugtask_importance_data.append(new_item)575 bugtask_importance_data.append(new_item)
629 self.assertEqual(576 self.assertEqual(
630 bugtask_importance_data, cache['bugtask_importance_data'])577 bugtask_importance_data, cache['bugtask_importance_data'])
578 self.assertContentEqual(cache['private_types'], [
579 type.name for type in PRIVATE_INFORMATION_TYPES])
580 self.assertEqual(cache['bug_private_by_default'], private_bugs)
631 bugtask_info_type_data = []581 bugtask_info_type_data = []
632 if not IProjectGroup.providedBy(view.context):582 if not IProjectGroup.providedBy(view.context):
633 for item in view.context.getAllowedBugInformationTypes():583 for item in view.context.getAllowedBugInformationTypes():
634584
=== removed file 'lib/lp/bugs/templates/bugtarget-filebug-guidelines.pt'
--- lib/lp/bugs/templates/bugtarget-filebug-guidelines.pt 2012-07-05 00:49:00 +0000
+++ lib/lp/bugs/templates/bugtarget-filebug-guidelines.pt 1970-01-01 00:00:00 +0000
@@ -1,51 +0,0 @@
1<tal:root
2 xmlns:tal="http://xml.zope.org/namespaces/tal"
3 xmlns:metal="http://xml.zope.org/namespaces/metal"
4 xmlns:i18n="http://xml.zope.org/namespaces/i18n"
5 >
6
7 <tr id="extra-filebug-details"><td colspan="2" width="100%"><table><tbody>
8 <tr>
9 <metal:bug_reporting_guidelines
10 use-macro="context/@@+filebug-macros/bug_reporting_guidelines" />
11 </tr>
12
13 <tr tal:define="security_context view/getMainContext">
14 <tal:information_type tal:condition="view/is_bug_supervisor">
15 <td colspan="2" width="100%"
16 tal:define="widget nocall: view/widgets/information_type|nothing"
17 tal:condition="widget">
18 <label tal:attributes="for widget/name">
19 This bug contains information that is:
20 </label>
21 <input tal:replace="structure widget" />
22 </td>
23 </tal:information_type>
24 <tal:security_related tal:condition="not: view/is_bug_supervisor">
25 <td colspan="2" width="100%"
26 tal:define="widget nocall: view/widgets/security_related|nothing"
27 tal:condition="widget">
28 <table>
29 <tbody>
30 <tr>
31 <td>
32 <input type="checkbox" tal:replace="structure widget" />
33 </td>
34 <td>
35 <label tal:attributes="for widget/name">
36 This bug is a security vulnerability
37 </label>
38 <div>
39 The security group for
40 <tal:security-context content="security_context/displayname" />
41 will be notified.
42 </div>
43 </td>
44 </tr>
45 </tbody>
46 </table>
47 </td>
48 </tal:security_related>
49 </tr>
50 </tbody></table></td></tr>
51</tal:root>
520
=== modified file 'lib/lp/bugs/templates/bugtarget-macros-filebug.pt'
--- lib/lp/bugs/templates/bugtarget-macros-filebug.pt 2012-06-15 16:23:50 +0000
+++ lib/lp/bugs/templates/bugtarget-macros-filebug.pt 2012-08-01 01:08:21 +0000
@@ -57,8 +57,50 @@
57 <metal:row use-macro="context/@@launchpad_form/widget_row" />57 <metal:row use-macro="context/@@launchpad_form/widget_row" />
58 </metal:description>58 </metal:description>
5959
60 <tr id="extra-filebug-details"60 <tr id="extra-filebug-details"><td colspan="2" width="100%"><table><tbody>
61 tal:replace="structure context/@@+filebug-reporting-guidelines" />61 <tr>
62 <metal:bug_reporting_guidelines
63 use-macro="context/@@+filebug-macros/bug_reporting_guidelines" />
64 </tr>
65
66 <tr tal:define="security_context view/getMainContext">
67 <tal:information_type tal:condition="view/is_bug_supervisor">
68 <td colspan="2" width="100%"
69 tal:define="widget nocall: view/widgets/information_type|nothing"
70 tal:condition="widget">
71 <label tal:attributes="for widget/name">
72 This bug contains information that is:
73 </label>
74 <input tal:replace="structure widget" />
75 </td>
76 </tal:information_type>
77 <tal:security_related tal:condition="not: view/is_bug_supervisor">
78 <td colspan="2" width="100%"
79 tal:define="widget nocall: view/widgets/security_related|nothing"
80 tal:condition="widget">
81 <table>
82 <tbody>
83 <tr>
84 <td>
85 <input type="checkbox" tal:replace="structure widget" />
86 </td>
87 <td>
88 <label tal:attributes="for widget/name">
89 This bug is a security vulnerability
90 </label>
91 <div>
92 The security group for
93 <tal:security-context content="security_context/displayname" />
94 will be notified.
95 </div>
96 </td>
97 </tr>
98 </tbody>
99 </table>
100 </td>
101 </tal:security_related>
102 </tr>
103 </tbody></table></td></tr>
62104
63 <tr>105 <tr>
64 <td colspan="2">106 <td colspan="2">