diff options
6 files changed, 386 insertions, 0 deletions
diff --git a/extensions/BMO/Extension.pm b/extensions/BMO/Extension.pm index d679ac91e..1e0593c4e 100644 --- a/extensions/BMO/Extension.pm +++ b/extensions/BMO/Extension.pm @@ -44,6 +44,7 @@ use Encode qw(find_encoding encode_utf8); use File::MimeInfo::Magic; use List::MoreUtils qw(natatime); use Scalar::Util qw(blessed); +use List::Util qw(first); use Sys::Syslog qw(:DEFAULT setlogsock); use Bugzilla::Extension::BMO::Constants; @@ -189,6 +190,104 @@ sub page_before_template { elsif ($page eq 'query_database.html') { query_database($vars); } + elsif ($page eq 'attachment_bounty_form.html') { + bounty_attachment($vars); + } +} + +sub bounty_attachment { + my ($vars) = @_; + ThrowUserError('user_not_insider') unless Bugzilla->user->is_insider; + + my $input = Bugzilla->input_params; + my $dbh = Bugzilla->dbh; + my $bug = Bugzilla::Bug->check({ id => $input->{bug_id}, cache => 1 }); + my $attachment = first { $_ && is_bounty_attachment($_) } @{$bug->attachments}; + $vars->{bug} = $bug; + + if ($input->{submit}) { + ThrowUserError('bounty_attachment_missing_reporter') + unless $input->{reporter_email}; + + my @fields = qw( reporter_email amount_paid reported_date fixed_date awarded_date publish ); + my %form = map { $_ => $input->{$_} } @fields; + $form{credit} = [ grep { defined } map { $input->{"credit_$_"} } 1..3 ]; + + $dbh->bz_start_transaction(); + if ($attachment) { + $attachment->set( + description => format_bounty_attachment_description(\%form) + ); + $attachment->update; + } + else { + my $attachment = Bugzilla::Attachment->create({ + bug => $bug, + isprivate => 1, + mimetype => 'text/plain', + data => 'bounty', + filename => 'bugbounty.data', + description => format_bounty_attachment_description(\%form), + }); + } + $dbh->bz_commit_transaction(); + + print Bugzilla->cgi->redirect('show_bug.cgi?id=' . $bug->id); + exit; + } + + if ($attachment) { + my $form = parse_bounty_attachment_description($attachment->description); + $vars->{form} = $form; + } +} + +sub is_bounty_attachment { + my ($attachment) = @_; + + return 0 unless $attachment->filename eq 'bugbounty.data'; + return 0 unless $attachment->contenttype eq 'text/plain'; + return 0 unless $attachment->isprivate; + return $attachment->description =~ /^(?:[^,]*,)+[^,]*$/; +} + +sub format_bounty_attachment_description { + my ($form) = @_; + my @fields = ( + @$form{qw( reporter_email amount_paid reported_date fixed_date awarded_date )}, + $form->{publish} ? 'true' : 'false', + @{ $form->{credit} // [] } + ); + + return join(',', map { $_ // '' } @fields); +} + +sub parse_bounty_attachment_description { + my ($desc) = @_; + + my %map = ( true => 1, false => 0 ); + my $date = qr/\d{4}-\d{2}-\d{2}/; + $desc =~ m! + ^ + (?<reporter_email> [^,]+) \s*,\s* + (?<amount_paid> [0-9]+) ? \s*,\s* + (?<reported_date> $date) ? \s*,\s* + (?<fixed_date> $date) ? \s*,\s* + (?<awarded_date> $date) ? \s*,\s* + (?<publish> (?i: true | false )) ? + (?: \s*,\s* (?<credits>.*) ) ? + $ + !x; + + return { + reporter_email => $+{reporter_email} // '', + amount_paid => $+{amount_paid} // '', + reported_date => $+{reported_date} // '', + fixed_date => $+{fixed_date} // '', + awarded_date => $+{awarded_date} // '', + publish => $map{ $+{publish} // 'false' }, + credit => [grep { $_ } split(/\s*,\s*/, $+{credits}) ] + }; } sub _get_field_values_sort_key { diff --git a/extensions/BMO/t/bounty_attachment.t b/extensions/BMO/t/bounty_attachment.t new file mode 100644 index 000000000..bd79b0dfe --- /dev/null +++ b/extensions/BMO/t/bounty_attachment.t @@ -0,0 +1,60 @@ +#!/usr/bin/perl -T +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# This Source Code Form is "Incompatible With Secondary Licenses", as +# defined by the Mozilla Public License, v. 2.0. +use strict; +use warnings; +use lib qw( . lib ); + +use Test::More; +use Bugzilla; +use Bugzilla::Extension; + +my $class = Bugzilla::Extension->load('extensions/BMO/Extension.pm', + 'extensions/BMO/Config.pm'); + +my $parse = $class->can('parse_bounty_attachment_description'); +my $format = $class->can('format_bounty_attachment_description'); + +ok($parse, "got the function"); + +my $bughunter = $parse->('bughunter@hacker.org, , 2014-06-25, , ,false'); +is_deeply({ reporter_email => 'bughunter@hacker.org', + amount_paid => '', + reported_date => '2014-06-25', + fixed_date => '', + awarded_date => '', + publish => 0, + credit => []}, $bughunter); + +my $hfli = $parse->('hfli@fortinet.com, 1000, 2010-07-16, 2010-08-04, 2011-06-15, true, Fortiguard Labs'); +is_deeply({ reporter_email => 'hfli@fortinet.com', + amount_paid => '1000', + reported_date => '2010-07-16', + fixed_date => '2010-08-04', + awarded_date => '2011-06-15', + publish => 1, + credit => ['Fortiguard Labs']}, $hfli); + +is('batman@justiceleague.america,1000,2015-01-01,2015-02-02,2015-03-03,true,JLA,Wayne Industries,Test', + $format->({ reporter_email => 'batman@justiceleague.america', + amount_paid => 1000, + reported_date => '2015-01-01', + fixed_date => '2015-02-02', + awarded_date => '2015-03-03', + publish => 1, + credit => ['JLA', 'Wayne Industries', 'Test'] })); + +my $dylan = $parse->('dylan@hardison.net,2,2014-09-23,2014-09-24,2014-09-25,true,Foo bar,Bork,'); +is_deeply({ reporter_email => 'dylan@hardison.net', + amount_paid => 2, + reported_date => '2014-09-23', + fixed_date => '2014-09-24', + awarded_date => '2014-09-25', + publish => 1, + credit => ['Foo bar', 'Bork']}, $dylan); + +done_testing; diff --git a/extensions/BMO/template/en/default/hook/bug/show-header-end.html.tmpl b/extensions/BMO/template/en/default/hook/bug/show-header-end.html.tmpl index 5ab189045..ff2438ffd 100644 --- a/extensions/BMO/template/en/default/hook/bug/show-header-end.html.tmpl +++ b/extensions/BMO/template/en/default/hook/bug/show-header-end.html.tmpl @@ -8,6 +8,10 @@ [% style_urls.push('extensions/BMO/web/styles/edit_bug.css') %] [% javascript_urls.push('extensions/BMO/web/js/edit_bug.js') %] +[% IF user.is_insider %] + [% javascript_urls.push('extensions/BMO/web/js/attachment_bounty_form.js') %] + [% yui.push('selector') %] +[% END %] [% title = "$bug.bug_id – " %] [% IF bug.alias != '' %] [% title = title _ "($bug.alias) " %] @@ -16,3 +20,10 @@ [% javascript = javascript _ "document.title = document.title.replace(/^" _ terms.Bug _ " /, '');" %] +[% js_bug_id = bug.bug_id FILTER js %] +[% IF user.is_insider %] + [% javascript = javascript _ + "YAHOO.util.Event.onDOMReady(function () { " _ + "add_bounty_attachment('$js_bug_id'); });" + %] +[% END %] diff --git a/extensions/BMO/template/en/default/hook/global/user-error-errors.html.tmpl b/extensions/BMO/template/en/default/hook/global/user-error-errors.html.tmpl index aaf23fff5..9caf9bd29 100644 --- a/extensions/BMO/template/en/default/hook/global/user-error-errors.html.tmpl +++ b/extensions/BMO/template/en/default/hook/global/user-error-errors.html.tmpl @@ -37,4 +37,8 @@ [% title = "Too Many Bugs" %] Too many [% terms.bugs %] matched your selection criteria. +[% ELSIF error == "bounty_attachment_missing_reporter" %] + [% title = "Missing Reporter" %] + You must provide an email address for a bounty attachment. + [% END %] diff --git a/extensions/BMO/template/en/default/pages/attachment_bounty_form.html.tmpl b/extensions/BMO/template/en/default/pages/attachment_bounty_form.html.tmpl new file mode 100644 index 000000000..31a73b017 --- /dev/null +++ b/extensions/BMO/template/en/default/pages/attachment_bounty_form.html.tmpl @@ -0,0 +1,194 @@ +[%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. + # + # This Source Code Form is "Incompatible With Secondary Licenses", as + # defined by the Mozilla Public License, v. 2.0. + #%] +[% PROCESS global/variables.none.tmpl %] + +[% inline_style = BLOCK %] +#bounty_form { + padding: 10px; +} +#bounty_form .required:after { + content: " *"; + color: red; +} +#bounty_form .field_label { + font-weight: bold; + display: block; + text-align: left; +} +#bounty_form .field_desc { + padding-bottom: 3px; +} +#bounty_form .field_desc, +#bounty_form .head_desc { + width: 600px; + word-wrap: normal; +} +#bounty_form .head_desc { + padding-top: 5px; + padding-bottom: 12px; +} +#bounty_form .form_section { + margin-bottom: 10px; +} +#bounty_form textarea { + font-family: inherit; + font-size: inherit; +} +#bounty_form em { + font-size: 1em; +} +.yui-calcontainer { + z-index: 2; +} +[% END %] + +[% inline_javascript = BLOCK %] +function validateAndSubmit() { + 'use strict'; + var alert_text = ''; + var requiredLabels = YAHOO.util.Selector.query('label.required'); + if (requiredLabels) { + requiredLabels.forEach(function (label) { + var name = label.getAttribute('for'); + var ids = YAHOO.util.Selector.query( + '#bounty_form *[name="' + name + '"]' + ).map(function (e) { + return e.id + }); + + if (ids && ids[0]) { + if (!isFilledOut(ids[0])) { + var desc = label.textContent || name; + alert_text += + "Please enter a value for " + + desc.replace(/[\r\n]+/, "").replace(/\s+/g, " ") + + "\n"; + } + } + }); + } + + if (alert_text != '') { + alert(alert_text); + return false; + } + return true; +} +[% END %] + +[% PROCESS global/header.html.tmpl + title = "Bounty Attachment Form" + style = inline_style + javascript = inline_javascript + javascript_urls = [ 'extensions/BMO/web/js/form_validate.js', + 'js/field.js', 'js/util.js' ] + yui = [ "calendar", "selector" ] +%] + +[% USE Bugzilla %] +[% cgi = Bugzilla.cgi %] + +<form id="bounty_form" method="post" action="page.cgi" + enctype="multipart/form-data" onSubmit="return validateAndSubmit();"> + + <input type="hidden" name="bug_id" value="[% bug.id FILTER none %]"> + <input type="hidden" name="id" value="attachment_bounty_form.html"> + <input type="hidden" name="submit" value="1"> + + <div class="head_desc"> + Bounty Attachment for [% "$terms.Bug $bug.id" FILTER bug_link(bug.id) FILTER none %] + </div> + + <div class="form_section"> + <label for="reporter_email" class="field_label required">Reporter's Email</label> + <input type="text" name="reporter_email" id="reporter_email" size="80" + value="[% form.reporter_email FILTER none %]"> + </div> + + <div class="form_section"> + <label for="amount_paid" class="field_label">Amount Paid</label> + <input type="text" name="amount_paid" id="amount_paid" size="80" value="[% form.amount_paid FILTER none %]"> + </div> + + <div class="form_section"> + <label for="reported_date" class="field_label">Reported Date</label> + <input name="reported_date" size="20" id="reported_date" value="[% form.reported_date FILTER none %]" + onchange="updateCalendarFromField(this)"> + <button type="button" class="calendar_button" + id="button_calendar_reported_date" + onclick="showCalendar('reported_date')"> + <span>Calendar</span> + </button> + <div id="con_calendar_reported_date"></div> + <script type="text/javascript"> + createCalendar('reported_date') + </script> + </div> + + <div class="form_section"> + <label for="fixed_date" class="field_label">Fixed Date</label> + <input name="fixed_date" size="20" id="fixed_date" value="[% form.fixed_date FILTER none %]" + onchange="updateCalendarFromField(this)"> + <button type="button" class="calendar_button" + id="button_calendar_fixed_date" + onclick="showCalendar('fixed_date')"> + <span>Calendar</span> + </button> + <div id="con_calendar_fixed_date"></div> + <script type="text/javascript"> + createCalendar('fixed_date') + </script> + </div> + + <div class="form_section"> + <label for="awarded_date" class="field_label">Awarded Date</label> + <input name="awarded_date" size="20" id="awarded_date" value="[% form.awarded_date FILTER none %]" + onchange="updateCalendarFromField(this)"> + <button type="button" class="calendar_button" + id="button_calendar_awarded_date" + onclick="showCalendar('awarded_date')"> + <span>Calendar</span> + </button> + <div id="con_calendar_awarded_date"></div> + <script type="text/javascript"> + createCalendar('awarded_date') + </script> + </div> + + <div class="form_section"> + <label for="publish" class="field_label">Publish</label> + <select name="publish" id="publish"> + <option value="1"[% IF form.publish %]selected[% END %]>Yes</option> + <option value="0"[% UNLESS form.publish %]selected[% END %]>No</option> + </select> + </div> + + <div class="form_section"> + <label for="credit_1" class="field_label">Credit</label> + <input type="text" name="credit_1" id="credit_1" size="80" value="[% form.credit.0 FILTER none %]"> + </div> + + <div class="form_section"> + <label for="credit_2" class="field_label">Credit</label> + <input type="text" name="credit_2" id="credit_2" size="80" value="[% form.credit.1 FILTER none %]"> + </div> + + <div class="form_section"> + <label for="credit_3" class="field_label">Credit</label> + <input type="text" name="credit_3" id="credit_3" size="80" value="[% form.credit.2 FILTER none %]"> + </div> + + <input type="submit" id="commit" value="Submit"> + + <p> + [ <span class="required_star">*</span> <span class="required_explanation"> + Required Field</span> ] + </p> +</form> + +[% PROCESS global/footer.html.tmpl %] diff --git a/extensions/BMO/web/js/attachment_bounty_form.js b/extensions/BMO/web/js/attachment_bounty_form.js new file mode 100644 index 000000000..cdec78276 --- /dev/null +++ b/extensions/BMO/web/js/attachment_bounty_form.js @@ -0,0 +1,18 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This Source Code Form is "Incompatible With Secondary Licenses", as + * defined by the Mozilla Public License, v. 2.0. */ + +function add_bounty_attachment(bug_id) { + var nodes = YAHOO.util.Selector.query('#attachment_table tr.bz_attach_footer td'); + if (nodes) { + var td = nodes[0]; + var a = document.createElement('a'); + a.href = 'page.cgi?id=attachment_bounty_form.html&bug_id=' + bug_id; + a.appendChild(document.createTextNode('Add bounty tracking attachment')); + td.appendChild(document.createElement('br')); + td.appendChild(a); + } +} |