launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #09578
[Merge] lp:~wallyworld/launchpad/filebug-non-bugsupervisors-1020790 into lp:launchpad
Ian Booth has proposed merging lp:~wallyworld/launchpad/filebug-non-bugsupervisors-1020790 into lp:launchpad.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
Related bugs:
Bug #1020790 in Launchpad itself: "Information type widget on +filebug confuses users"
https://bugs.launchpad.net/launchpad/+bug/1020790
For more details, see:
https://code.launchpad.net/~wallyworld/launchpad/filebug-non-bugsupervisors-1020790/+merge/113471
== Implementation ==
This branch ensures that only people in a bug supervisor role are able to choose a specific information type with which to file a new bug. Other non privileged uses just get a "security related" checkbox.
The existing method BugTask.userHasBugSupervisorPrivilegesContext() was used to determine if a user is considered a bug supervisor. So this includes admins, drivers etc in the allowed list.
== Tests ==
Add yui tests for the filebug form when rendered with the security_related checkbox.
Add new test case TestFileBugForNonBugSupervisors to check that the form rendering and submission works as expected
== Lint ==
Checking for conflicts and issues in changed files.
Linting changed files:
lib/lp/app/javascript/choice.js
lib/lp/bugs/browser/bugtarget.py
lib/lp/bugs/browser/tests/test_bugtarget_filebug.py
lib/lp/bugs/javascript/filebug.js
lib/lp/bugs/javascript/tests/test_filebug.html
lib/lp/bugs/javascript/tests/test_filebug.js
lib/lp/bugs/templates/bugtarget-filebug-guidelines.pt
--
https://code.launchpad.net/~wallyworld/launchpad/filebug-non-bugsupervisors-1020790/+merge/113471
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~wallyworld/launchpad/filebug-non-bugsupervisors-1020790 into lp:launchpad.
=== modified file 'lib/lp/app/javascript/choice.js'
--- lib/lp/app/javascript/choice.js 2012-07-03 02:28:08 +0000
+++ lib/lp/app/javascript/choice.js 2012-07-05 00:58:19 +0000
@@ -161,8 +161,11 @@
*/
namespace.addPopupChoiceForRadioButtons = function(field_name, choices, cfg) {
cfg = Y.merge(default_popup_choice_config, cfg);
- var legacy_node = cfg.container.one('[name="field.' + field_name + '"]')
- .ancestor('table.radio-button-widget');
+ var field_node = cfg.container.one('[name="field.' + field_name + '"]');
+ if (!Y.Lang.isValue(field_node)) {
+ return;
+ }
+ var legacy_node = field_node.ancestor('table.radio-button-widget');
if (!Y.Lang.isValue(legacy_node)) {
return;
}
=== modified file 'lib/lp/bugs/browser/bugtarget.py'
--- lib/lp/bugs/browser/bugtarget.py 2012-06-29 08:40:05 +0000
+++ lib/lp/bugs/browser/bugtarget.py 2012-07-05 00:58:19 +0000
@@ -121,7 +121,6 @@
from lp.registry.enums import (
InformationType,
PRIVATE_INFORMATION_TYPES,
- PUBLIC_INFORMATION_TYPES,
SECURITY_INFORMATION_TYPES,
)
from lp.registry.interfaces.distribution import IDistribution
@@ -260,7 +259,10 @@
@property
def field_names(self):
"""Return the list of field names to display."""
- return ['information_type']
+ if self.is_bug_supervisor:
+ return ['information_type']
+ else:
+ return ['security_related']
custom_widget('information_type', LaunchpadRadioWidgetWithDescription)
@@ -298,11 +300,17 @@
"""Set up the form fields. See `LaunchpadFormView`."""
super(FileBugReportingGuidelines, self).setUpFields()
- information_type_field = copy_field(
- IBug['information_type'], readonly=False,
- vocabulary=InformationTypeVocabulary(self.context))
- self.form_fields = self.form_fields.omit('information_type')
- self.form_fields += Fields(information_type_field)
+ if self.is_bug_supervisor:
+ information_type_field = copy_field(
+ IBug['information_type'], readonly=False,
+ vocabulary=InformationTypeVocabulary(self.context))
+ self.form_fields = self.form_fields.omit('information_type')
+ self.form_fields += Fields(information_type_field)
+ else:
+ security_related_field = copy_field(
+ IBug['security_related'], readonly=False)
+ self.form_fields = self.form_fields.omit('security_related')
+ self.form_fields += Fields(security_related_field)
@property
def initial_values(self):
@@ -360,6 +368,19 @@
else:
return self.context
+ @cachedproperty
+ def is_bug_supervisor(self):
+ """ Return True if the logged in user is a bug supervisor.
+
+ If the main context doesn't have a bug supervisor set, return True if
+ the user is a maintainer.
+ This check allows authorised users to set the specific information type
+ when filing a bug.
+ """
+ context = self.getMainContext()
+ return BugTask.userHasBugSupervisorPrivilegesContext(
+ context, self.user)
+
class FileBugViewBase(FileBugReportingGuidelines, LaunchpadFormView):
"""Base class for views related to filing a bug."""
@@ -435,9 +456,14 @@
def field_names(self):
"""Return the list of field names to display."""
context = self.context
- field_names = ['title', 'comment', 'tags', 'information_type',
+ field_names = ['title', 'comment', 'tags']
+ if self.is_bug_supervisor:
+ field_names.append('information_type')
+ else:
+ field_names.append('security_related')
+ field_names.extend([
'bug_already_reported_as', 'filecontent', 'patch',
- 'attachment_description', 'subscribe_to_existing_bug']
+ 'attachment_description', 'subscribe_to_existing_bug'])
if (IDistribution.providedBy(context) or
IDistributionSourcePackage.providedBy(context)):
field_names.append('packagename')
@@ -453,9 +479,7 @@
# selected project supports them.
include_extra_fields = IProjectGroup.providedBy(context)
if not include_extra_fields:
- include_extra_fields = (
- BugTask.userHasBugSupervisorPrivilegesContext(
- context, self.user))
+ include_extra_fields = self.is_bug_supervisor
if include_extra_fields:
field_names.extend(
@@ -612,6 +636,7 @@
packagename = data.get("packagename")
information_type = data.get(
"information_type", InformationType.PUBLIC)
+ security_related = data.get("security_related", False)
distribution = data.get(
"distribution", getUtility(ILaunchBag).distribution)
@@ -630,6 +655,14 @@
if self.request.form.get("packagename_option") == "none":
packagename = None
+ if not self.is_bug_supervisor:
+ # If the old UI is enabled, security bugs are always embargoed
+ # when filed, but can be disclosed after they've been reported.
+ if security_related:
+ information_type = InformationType.EMBARGOEDSECURITY
+ else:
+ information_type = InformationType.PUBLIC
+
linkified_ack = structured(FormattersAPI(
self.getAcknowledgementMessage(self.context)).text_to_html(
last_paragraph_class="last"))
@@ -665,12 +698,12 @@
notifications.append(
'Additional information was added to the bug description.')
- if extra_data.private:
- if params.information_type in PUBLIC_INFORMATION_TYPES:
+ if not self.is_bug_supervisor and extra_data.private:
+ if params.information_type == InformationType.PUBLIC:
params.information_type = InformationType.USERDATA
# Apply any extra options given by privileged users.
- if BugTask.userHasBugSupervisorPrivilegesContext(context, self.user):
+ if self.is_bug_supervisor:
if 'assignee' in data:
params.assignee = data['assignee']
if 'status' in data:
=== modified file 'lib/lp/bugs/browser/tests/test_bugtarget_filebug.py'
--- lib/lp/bugs/browser/tests/test_bugtarget_filebug.py 2012-06-20 05:25:44 +0000
+++ lib/lp/bugs/browser/tests/test_bugtarget_filebug.py 2012-07-05 00:58:19 +0000
@@ -421,6 +421,19 @@
for info_type in InformationType:
self.assertIsNotNone(soup.find('label', text=info_type.title))
+ def test_filebug_view_renders_info_type_widget(self):
+ # The info type widget is rendered for bug supervisor roles.
+ product = self.factory.makeProduct(official_malone=True)
+ with person_logged_in(product.owner):
+ view = create_initialized_view(
+ product, '+filebug', principal=product.owner)
+ html = view.render()
+ soup = BeautifulSoup(html)
+ self.assertIsNone(
+ soup.find('input', attrs={'name': 'field.security_related'}))
+ self.assertIsNotNone(
+ soup.find('input', attrs={'name': 'field.information_type'}))
+
def test_filebug_information_type_vocabulary_private_projects(self):
# The vocabulary for information_type when filing a bug only has
# private info types for private bug projects.
@@ -437,6 +450,69 @@
self.assertIsNone(soup.find('label', text=info_type.title))
+class TestFileBugForNonBugSupervisors(TestCaseWithFactory):
+
+ layer = DatabaseFunctionalLayer
+
+ def filebug_via_view(self, private_bugs=False, security_related=False):
+ form = {
+ 'field.title': 'A bug',
+ 'field.comment': 'A comment',
+ 'field.security_related': 'on' if security_related else '',
+ 'field.actions.submit_bug': 'Submit Bug Request',
+ }
+ product = self.factory.makeProduct(official_malone=True)
+ if private_bugs:
+ removeSecurityProxy(product).private_bugs = True
+ anyone = self.factory.makePerson()
+ with person_logged_in(anyone):
+ view = create_initialized_view(
+ product, '+filebug', form=form, principal=anyone)
+ bug_url = view.request.response.getHeader('Location')
+ bug_number = bug_url.split('/')[-1]
+ return getUtility(IBugSet).getByNameOrID(bug_number)
+
+ def test_filebug_non_security_related(self):
+ # Non security related bugs are PUBLIC for products with
+ # private_bugs=False.
+ bug = self.filebug_via_view()
+ self.assertEqual(InformationType.PUBLIC, bug.information_type)
+
+ def test_filebug_security_related(self):
+ # Security related bugs are EMBARGOEDSECURITY for products with
+ # private_bugs=False.
+ bug = self.filebug_via_view(security_related=True)
+ self.assertEqual(
+ InformationType.EMBARGOEDSECURITY, bug.information_type)
+
+ def test_filebug_security_related_with_private_bugs(self):
+ # Security related bugs are EMBARGOEDSECURITY for products with
+ # private_bugs=True.
+ bug = self.filebug_via_view(private_bugs=True, security_related=True)
+ self.assertEqual(
+ InformationType.EMBARGOEDSECURITY, bug.information_type)
+
+ def test_filebug_with_private_bugs(self):
+ # Non security related bugs are USERDATA for products with
+ # private_bugs=True.
+ bug = self.filebug_via_view(private_bugs=True)
+ self.assertEqual(InformationType.USERDATA, bug.information_type)
+
+ def test_filebug_view_renders_security_related(self):
+ # The security_related checkbox is rendered for non bug supervisors.
+ product = self.factory.makeProduct(official_malone=True)
+ anyone = self.factory.makePerson()
+ with person_logged_in(anyone):
+ view = create_initialized_view(
+ product, '+filebug', principal=anyone)
+ html = view.render()
+ soup = BeautifulSoup(html)
+ self.assertIsNotNone(
+ soup.find('input', attrs={'name': 'field.security_related'}))
+ self.assertIsNone(
+ soup.find('input', attrs={'name': 'field.information_type'}))
+
+
class TestFileBugSourcePackage(TestCaseWithFactory):
layer = DatabaseFunctionalLayer
=== modified file 'lib/lp/bugs/javascript/filebug.js'
--- lib/lp/bugs/javascript/filebug.js 2012-06-27 14:05:07 +0000
+++ lib/lp/bugs/javascript/filebug.js 2012-07-05 00:58:19 +0000
@@ -29,8 +29,13 @@
search_button.set('value', 'Check again');
}
setup_information_type();
+ setup_security_related();
setupChoiceWidgets();
+ set_default_privacy_banner();
}
+};
+
+var set_default_privacy_banner = function() {
var filebug_privacy_text = "This report will be private. " +
"You can disclose it later.";
update_privacy_banner(
@@ -65,6 +70,9 @@
var setup_information_type = function() {
var itypes_table = Y.one('.radio-button-widget');
+ if (!Y.Lang.isValue(itypes_table)) {
+ return;
+ }
itypes_table.delegate('change', function() {
var banner_text = get_new_banner_text(this.get('value'));
var private_type = (Y.Array.indexOf(
@@ -82,6 +90,25 @@
'information_type', LP.cache.information_type_data, true);
};
+var setup_security_related = function() {
+ var security_related = Y.one('[id="field.security_related"]');
+ if (!Y.Lang.isValue(security_related)) {
+ return;
+ }
+ var notification_text = "This report will be private " +
+ "because it is a security " +
+ "vulnerability. You can " +
+ "disclose it later.";
+ security_related.on('change', function() {
+ var checked = security_related.get('checked');
+ if (checked) {
+ update_privacy_banner(true, notification_text);
+ } else {
+ set_default_privacy_banner();
+ }
+ });
+};
+
namespace.setup_filebug = setup_filebug;
}, "0.1", {"requires": [
=== modified file 'lib/lp/bugs/javascript/tests/test_filebug.html'
--- lib/lp/bugs/javascript/tests/test_filebug.html 2012-06-15 01:13:39 +0000
+++ lib/lp/bugs/javascript/tests/test_filebug.html 2012-07-05 00:58:19 +0000
@@ -67,7 +67,7 @@
</ul>
<div class='login-logout'></div>
<div id="fixture"></div>
- <script type="text/x-template" id="privacy-banner-template">
+ <script type="text/x-template" id="bugsupervisor-filebug-template">
<div id="filebug-form">
<table class="radio-button-widget">
<tbody>
@@ -117,5 +117,27 @@
</div>
</div>
</script>
+ <script type="text/x-template" id="filebug-template">
+ <div id="filebug-form">
+ <div>
+ <input type="checkbox" value="on" name="field.security_related" id="field.security_related" class="checkboxType">
+ <label for="field.security_related">
+ This bug is a security vulnerability
+ </label>
+ </div>
+ <div class="value">
+ <select size="1" name="field.status" id="field.status">
+ <option value="New" selected="selected">New</option>
+ <option value="Incomplete">Incomplete</option>
+ </select>
+ </div>
+ <div class="value">
+ <select size="1" name="field.importance" id="field.importance">
+ <option value="Undecided" selected="selected">Undecided</option>
+ <option value="High">High</option>
+ </select>
+ </div>
+ </div>
+ </script>
</body>
</html>
=== modified file 'lib/lp/bugs/javascript/tests/test_filebug.js'
--- lib/lp/bugs/javascript/tests/test_filebug.js 2012-07-03 01:35:50 +0000
+++ lib/lp/bugs/javascript/tests/test_filebug.js 2012-07-05 00:58:19 +0000
@@ -33,17 +33,24 @@
]
}
};
+ },
+
+ setupForm: function(bugsupervisor_version) {
this.fixture = Y.one('#fixture');
- var banner = Y.Node.create(
- Y.one('#privacy-banner-template').getContent());
- this.fixture.appendChild(banner);
+ var form_id = 'filebug-template';
+ if (bugsupervisor_version) {
+ form_id = 'bugsupervisor-' + form_id;
+ }
+ var form = Y.Node.create(Y.one('#' + form_id).getContent());
+ this.fixture.appendChild(form);
+ Y.lp.bugs.filebug.setup_filebug(true);
},
tearDown: function () {
- if (this.fixture !== null) {
+ if (Y.Lang.isValue(this.fixture)) {
this.fixture.empty(true);
+ delete this.fixture;
}
- delete this.fixture;
delete window.LP;
},
@@ -55,7 +62,7 @@
// Filing a public bug does not show the privacy banner.
test_setup_filebug_public: function () {
- Y.lp.bugs.filebug.setup_filebug(true);
+ this.setupForm(true);
var banner_hidden = Y.one('.yui3-privacybanner-hidden');
Y.Assert.isNotNull(banner_hidden);
},
@@ -64,7 +71,7 @@
// banner.
test_setup_filebug_private: function () {
window.LP.cache.bug_private_by_default = true;
- Y.lp.bugs.filebug.setup_filebug(true);
+ this.setupForm(true);
var banner_hidden = Y.one('.yui3-privacybanner-hidden');
Y.Assert.isNull(banner_hidden);
var banner_text = Y.one('.banner-text').get('text');
@@ -76,7 +83,7 @@
// Selecting a private info type using the legacy radio buttons
// turns on the privacy banner.
test_legacy_select_private_info_type: function () {
- Y.lp.bugs.filebug.setup_filebug(true);
+ this.setupForm(true);
var banner_hidden = Y.one('.yui3-privacybanner-hidden');
Y.Assert.isNotNull(banner_hidden);
Y.one('[id="field.information_type.2"]').simulate('click');
@@ -92,7 +99,7 @@
// turns off the privacy banner.
test_legacy_select_public_info_type: function () {
window.LP.cache.bug_private_by_default = true;
- Y.lp.bugs.filebug.setup_filebug(true);
+ this.setupForm(true);
Y.one('[id="field.information_type.2"]').simulate('click');
var banner_hidden = Y.one('.yui3-privacybanner-hidden');
Y.Assert.isNull(banner_hidden);
@@ -101,6 +108,52 @@
Y.Assert.isNotNull(banner_hidden);
},
+ // When non bug supervisors select a security related bug the privacy
+ // banner is turned on.
+ test_select_security_related: function () {
+ this.setupForm(false);
+ var banner_hidden = Y.one('.yui3-privacybanner-hidden');
+ Y.Assert.isNotNull(banner_hidden);
+ Y.one('[id="field.security_related"]').simulate('click');
+ banner_hidden = Y.one('.yui3-privacybanner-hidden');
+ Y.Assert.isNull(banner_hidden);
+ var banner_text = Y.one('.banner-text').get('text');
+ Y.Assert.areEqual(
+ 'This report will be private because it is a ' +
+ 'security vulnerability. You can disclose it later.',
+ banner_text);
+ },
+
+ // When non bug supervisors unselect a security related bug the privacy
+ // banner is turned off.
+ test_unselect_security_related: function () {
+ this.setupForm(false);
+ Y.one('[id="field.security_related"]').simulate('click');
+ var banner_hidden = Y.one('.yui3-privacybanner-hidden');
+ Y.Assert.isNull(banner_hidden);
+ Y.one('[id="field.security_related"]').simulate('click');
+ banner_hidden = Y.one('.yui3-privacybanner-hidden');
+ Y.Assert.isNotNull(banner_hidden);
+ },
+
+ // When non bug supervisors unselect a security related bug the privacy
+ // banner remains on for private_by_default bugs.
+ test_unselect_security_related_default_private: function () {
+ window.LP.cache.bug_private_by_default = true;
+ this.setupForm(false);
+ Y.one('[id="field.security_related"]').simulate('click');
+ var banner_hidden = Y.one('.yui3-privacybanner-hidden');
+ Y.Assert.isNull(banner_hidden);
+ Y.one('[id="field.security_related"]').simulate('click');
+ banner_hidden = Y.one('.yui3-privacybanner-hidden');
+ Y.Assert.isNull(banner_hidden);
+ var banner_text = Y.one('.banner-text').get('text');
+ Y.Assert.areEqual(
+ 'This report will be private. ' +
+ 'You can disclose it later.',
+ banner_text);
+ },
+
// The dupe finder functionality is setup.
test_dupe_finder_setup: function () {
window.LP.cache.enable_bugfiling_duplicate_search = true;
@@ -116,7 +169,7 @@
Y.lp.bugs.filebug_dupefinder.setup_dupes = function() {
setup_dupes_called = true;
};
- Y.lp.bugs.filebug.setup_filebug(true);
+ this.setupForm(true);
Y.Assert.isTrue(setup_dupe_finder_called);
Y.Assert.isTrue(setup_dupes_called);
Y.lp.bugs.filebug_dupefinder.setup_dupes = orig_setup_dupes;
@@ -126,7 +179,7 @@
// The bugtask status choice popup is rendered.
test_status_setup: function () {
- Y.lp.bugs.filebug.setup_filebug(true);
+ this.setupForm(true);
var status_node = Y.one('.status-content .value');
Y.Assert.areEqual('New', status_node.get('text'));
var status_edit_node = Y.one('.status-content a.sprite.edit');
@@ -135,9 +188,7 @@
Y.Assert.isTrue(legacy_dropdown.hasClass('unseen'));
},
- // The bugtask importance choice popup is rendered.
- test_importance_setup: function () {
- Y.lp.bugs.filebug.setup_filebug(true);
+ _perform_test_importance: function() {
var importance_node = Y.one('.importance-content .value');
Y.Assert.areEqual('Undecided', importance_node.get('text'));
var importance_edit_node =
@@ -147,17 +198,24 @@
Y.Assert.isTrue(legacy_dropdown.hasClass('unseen'));
},
+ // The bugtask importance choice popup is rendered.
+ test_importance_setup: function () {
+ this.setupForm(true);
+ this._perform_test_importance();
+ },
+
// The choice popup wiring works even if the field is missing.
// This is so fields which the user does not have permission to see
// can be missing and everything still works as expected.
test_missing_fields: function () {
+ this.setupForm(true);
Y.one('[id="field.status"]').remove(true);
- this.test_importance_setup();
+ this._perform_test_importance();
},
// The bugtask status choice popup updates the form.
test_status_selection: function() {
- Y.lp.bugs.filebug.setup_filebug(true);
+ this.setupForm(true);
var status_popup = Y.one('.status-content a');
status_popup.simulate('click');
var status_choice = Y.one(
@@ -169,7 +227,7 @@
// The bugtask importance choice popup updates the form.
test_importance_selection: function() {
- Y.lp.bugs.filebug.setup_filebug(true);
+ this.setupForm(true);
var status_popup = Y.one('.importance-content a');
status_popup.simulate('click');
var status_choice = Y.one(
@@ -181,7 +239,7 @@
// The bugtask information_type choice popup is rendered.
test_information_type_setup: function () {
- Y.lp.bugs.filebug.setup_filebug(true);
+ this.setupForm(true);
var information_type_node =
Y.one('.information_type-content .value');
Y.Assert.areEqual('Public', information_type_node.get('text'));
@@ -194,7 +252,7 @@
// The bugtask information_type choice popup updates the form.
test_information_type_selection: function() {
- Y.lp.bugs.filebug.setup_filebug(true);
+ this.setupForm(true);
var information_type_popup = Y.one('.information_type-content a');
information_type_popup.simulate('click');
var header_text =
@@ -211,7 +269,7 @@
// Selecting a private info type using the popup choice widget
// turns on the privacy banner.
test_select_private_info_type: function () {
- Y.lp.bugs.filebug.setup_filebug(true);
+ this.setupForm(true);
var banner_hidden = Y.one('.yui3-privacybanner-hidden');
Y.Assert.isNotNull(banner_hidden);
var information_type_popup = Y.one('.information_type-content a');
@@ -229,7 +287,7 @@
test_select_private_info_type_with_private_flag: function () {
window.LP.cache.show_userdata_as_private = true;
- Y.lp.bugs.filebug.setup_filebug(true);
+ this.setupForm(true);
var banner_hidden = Y.one('.yui3-privacybanner-hidden');
Y.Assert.isNotNull(banner_hidden);
var information_type_popup = Y.one('.information_type-content a');
@@ -248,7 +306,7 @@
// turns off the privacy banner.
test_select_public_info_type: function () {
window.LP.cache.bug_private_by_default = true;
- Y.lp.bugs.filebug.setup_filebug(true);
+ this.setupForm(true);
var information_type_popup = Y.one('.information_type-content a');
information_type_popup.simulate('click');
var information_type_choice = Y.one(
=== modified file 'lib/lp/bugs/templates/bugtarget-filebug-guidelines.pt'
--- lib/lp/bugs/templates/bugtarget-filebug-guidelines.pt 2012-06-20 05:25:44 +0000
+++ lib/lp/bugs/templates/bugtarget-filebug-guidelines.pt 2012-07-05 00:58:19 +0000
@@ -11,6 +11,7 @@
</tr>
<tr tal:define="security_context view/getMainContext">
+ <tal:information_type tal:condition="view/is_bug_supervisor">
<td colspan="2" width="100%"
tal:define="widget nocall: view/widgets/information_type|nothing"
tal:condition="widget">
@@ -19,6 +20,32 @@
</label>
<input tal:replace="structure widget" />
</td>
+ </tal:information_type>
+ <tal:security_related tal:condition="not: view/is_bug_supervisor">
+ <td colspan="2" width="100%"
+ tal:define="widget nocall: view/widgets/security_related|nothing"
+ tal:condition="widget">
+ <table>
+ <tbody>
+ <tr>
+ <td>
+ <input type="checkbox" tal:replace="structure widget" />
+ </td>
+ <td>
+ <label tal:attributes="for widget/name">
+ This bug is a security vulnerability
+ </label>
+ <div>
+ The security group for
+ <tal:security-context content="security_context/displayname" />
+ will be notified.
+ </div>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </td>
+ </tal:security_related>
</tr>
</tbody></table></td></tr>
</tal:root>
Follow ups