summaryrefslogtreecommitdiffstats
path: root/extensions/Review/Extension.pm
diff options
context:
space:
mode:
Diffstat (limited to 'extensions/Review/Extension.pm')
-rw-r--r--extensions/Review/Extension.pm939
1 files changed, 939 insertions, 0 deletions
diff --git a/extensions/Review/Extension.pm b/extensions/Review/Extension.pm
new file mode 100644
index 000000000..f6a3bf743
--- /dev/null
+++ b/extensions/Review/Extension.pm
@@ -0,0 +1,939 @@
+# 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.
+
+package Bugzilla::Extension::Review;
+use strict;
+use warnings;
+
+use base qw(Bugzilla::Extension);
+our $VERSION = '1';
+
+use Bugzilla;
+use Bugzilla::Constants;
+use Bugzilla::Error;
+use Bugzilla::Extension::Review::FlagStateActivity;
+use Bugzilla::Extension::Review::Util;
+use Bugzilla::Install::Filesystem;
+use Bugzilla::Search;
+use Bugzilla::User;
+use Bugzilla::Util qw(clean_text diff_arrays);
+
+use constant UNAVAILABLE_RE => qr/\b(?:unavailable|pto|away)\b/i;
+
+#
+# monkey-patched methods
+#
+
+BEGIN {
+ *Bugzilla::Product::reviewers = \&_product_reviewers;
+ *Bugzilla::Product::reviewers_objs = \&_product_reviewers_objs;
+ *Bugzilla::Product::reviewer_required = \&_product_reviewer_required;
+ *Bugzilla::Component::reviewers = \&_component_reviewers;
+ *Bugzilla::Component::reviewers_objs = \&_component_reviewers_objs;
+ *Bugzilla::Bug::mentors = \&_bug_mentors;
+ *Bugzilla::Bug::bug_mentors = \&_bug_mentors;
+ *Bugzilla::Bug::is_mentor = \&_bug_is_mentor;
+ *Bugzilla::Bug::set_bug_mentors = \&_bug_set_bug_mentors;
+ *Bugzilla::User::review_count = \&_user_review_count;
+}
+
+#
+# monkey-patched methods
+#
+
+sub _product_reviewers { _reviewers($_[0], 'product', $_[1]) }
+sub _product_reviewers_objs { _reviewers_objs($_[0], 'product', $_[1]) }
+sub _component_reviewers { _reviewers($_[0], 'component', $_[1]) }
+sub _component_reviewers_objs { _reviewers_objs($_[0], 'component', $_[1]) }
+
+sub _reviewers {
+ my ($object, $type, $include_disabled) = @_;
+ return join(', ', map { $_->login } @{ _reviewers_objs($object, $type, $include_disabled) });
+}
+
+sub _reviewers_objs {
+ my ($object, $type, $include_disabled) = @_;
+ if (!$object->{reviewers}) {
+ my $dbh = Bugzilla->dbh;
+ my $user_ids = $dbh->selectcol_arrayref(
+ "SELECT user_id FROM ${type}_reviewers WHERE ${type}_id = ? ORDER BY sortkey",
+ undef,
+ $object->id,
+ );
+ # new_from_list always sorts according to the object's definition,
+ # so we have to reorder the list
+ my $users = Bugzilla::User->new_from_list($user_ids);
+ my %user_map = map { $_->id => $_ } @$users;
+ my @reviewers = map { $user_map{$_} } @$user_ids;
+ if (!$include_disabled) {
+ @reviewers = grep { $_->is_enabled
+ && $_->name !~ UNAVAILABLE_RE } @reviewers;
+ }
+ $object->{reviewers} = \@reviewers;
+ }
+ return $object->{reviewers};
+}
+
+sub _user_review_count {
+ my ($self) = @_;
+ if (!exists $self->{review_count}) {
+ my $dbh = Bugzilla->dbh;
+ ($self->{review_count}) = $dbh->selectrow_array(
+ "SELECT COUNT(*)
+ FROM flags
+ INNER JOIN flagtypes ON flagtypes.id = flags.type_id
+ WHERE flags.requestee_id = ?
+ AND " . $dbh->sql_in('flagtypes.name', [ "'review'", "'feedback'" ]),
+ undef,
+ $self->id,
+ );
+ }
+ return $self->{review_count};
+}
+
+#
+# mentor
+#
+
+sub _bug_mentors {
+ my ($self) = @_;
+ my $dbh = Bugzilla->dbh;
+ if (!$self->{bug_mentors}) {
+ my $mentor_ids = $dbh->selectcol_arrayref("
+ SELECT user_id FROM bug_mentors WHERE bug_id = ?",
+ undef,
+ $self->id);
+ $self->{bug_mentors} = [];
+ foreach my $mentor_id (@$mentor_ids) {
+ push(@{ $self->{bug_mentors} },
+ Bugzilla::User->new({ id => $mentor_id, cache => 1 }));
+ }
+ $self->{bug_mentors} = [
+ sort { $a->login cmp $b->login } @{ $self->{bug_mentors} }
+ ];
+ }
+ return $self->{bug_mentors};
+}
+
+sub _bug_is_mentor {
+ my ($self, $user) = @_;
+ my $user_id = ($user || Bugzilla->user)->id;
+ return (grep { $_->id == $user_id} @{ $self->mentors }) ? 1 : 0;
+}
+
+sub _bug_set_bug_mentors {
+ my ($self, $value) = @_;
+ $self->set('bug_mentors', $value);
+}
+
+sub object_validators {
+ my ($self, $args) = @_;
+ return unless $args->{class} eq 'Bugzilla::Bug';
+ $args->{validators}->{bug_mentors} = \&_bug_check_bug_mentors;
+}
+
+sub _bug_check_bug_mentors {
+ my ($self, $value) = @_;
+ return [
+ map { Bugzilla::User->check({ name => $_, cache => 1 }) }
+ ref($value) ? @$value : ($value)
+ ];
+}
+
+sub bug_user_match_fields {
+ my ($self, $args) = @_;
+ $args->{fields}->{bug_mentors} = { type => 'multi' };
+}
+
+sub bug_before_create {
+ my ($self, $args) = @_;
+ my $params = $args->{params};
+ my $stash = $args->{stash};
+ $stash->{bug_mentors} = delete $params->{bug_mentors};
+}
+
+sub bug_end_of_create {
+ my ($self, $args) = @_;
+ my $bug = $args->{bug};
+ my $stash = $args->{stash};
+ if (my $mentors = $stash->{bug_mentors}) {
+ $self->_update_user_table({
+ object => $bug,
+ old_users => [],
+ new_users => $self->_bug_check_bug_mentors($mentors),
+ table => 'bug_mentors',
+ id_field => 'bug_id',
+ });
+ }
+}
+
+sub _update_user_table {
+ my ($self, $args) = @_;
+ my ($object, $old_users, $new_users, $table, $id_field, $has_sortkey, $return) =
+ @$args{qw(object old_users new_users table id_field has_sortkey return)};
+ my $dbh = Bugzilla->dbh;
+ my (@removed, @added);
+
+ # remove deleted users
+ foreach my $old_user (@$old_users) {
+ if (!grep { $_->id == $old_user->id } @$new_users) {
+ $dbh->do(
+ "DELETE FROM $table WHERE $id_field = ? AND user_id = ?",
+ undef,
+ $object->id, $old_user->id,
+ );
+ push @removed, $old_user;
+ }
+ }
+ # add new users
+ foreach my $new_user (@$new_users) {
+ if (!grep { $_->id == $new_user->id } @$old_users) {
+ $dbh->do(
+ "INSERT INTO $table ($id_field, user_id) VALUES (?, ?)",
+ undef,
+ $object->id, $new_user->id,
+ );
+ push @added, $new_user;
+ }
+ }
+
+ return unless @removed || @added;
+
+ if ($has_sortkey) {
+ # update the sortkey for all users
+ for (my $i = 0; $i < scalar(@$new_users); $i++) {
+ $dbh->do(
+ "UPDATE $table SET sortkey=? WHERE $id_field = ? AND user_id = ?",
+ undef,
+ ($i + 1) * 10, $object->id, $new_users->[$i]->id,
+ );
+ }
+ }
+
+ if (!$return) {
+ return undef;
+ }
+ elsif ($return eq 'diff') {
+ return [
+ @removed ? join(', ', map { $_->login } @removed) : undef,
+ @added ? join(', ', map { $_->login } @added) : undef,
+ ];
+ }
+ elsif ($return eq 'old-new') {
+ return [
+ @$old_users ? join(', ', map { $_->login } @$old_users) : '',
+ @$new_users ? join(', ', map { $_->login } @$new_users) : '',
+ ];
+ }
+}
+
+#
+# reviewer-required, review counters, etc
+#
+
+sub _product_reviewer_required { $_[0]->{reviewer_required} }
+
+sub object_columns {
+ my ($self, $args) = @_;
+ my ($class, $columns) = @$args{qw(class columns)};
+ if ($class->isa('Bugzilla::Product')) {
+ push @$columns, 'reviewer_required';
+ }
+ elsif ($class->isa('Bugzilla::User')) {
+ push @$columns, qw(review_request_count feedback_request_count needinfo_request_count);
+ }
+}
+
+sub object_update_columns {
+ my ($self, $args) = @_;
+ my ($object, $columns) = @$args{qw(object columns)};
+ if ($object->isa('Bugzilla::Product')) {
+ push @$columns, 'reviewer_required';
+ }
+ elsif ($object->isa('Bugzilla::User')) {
+ push @$columns, qw(review_request_count feedback_request_count needinfo_request_count);
+ }
+}
+
+sub _new_users_from_input {
+ my ($field) = @_;
+ my $input_params = Bugzilla->input_params;
+ return undef unless exists $input_params->{$field};
+ return [] unless $input_params->{$field};
+ Bugzilla::User::match_field({ $field => {'type' => 'multi'} });;
+ my $value = $input_params->{$field};
+ return [
+ map { Bugzilla::User->check({ name => $_, cache => 1 }) }
+ ref($value) ? @$value : ($value)
+ ];
+}
+
+#
+# create/update
+#
+
+sub object_before_create {
+ my ($self, $args) = @_;
+ my ($class, $params) = @$args{qw(class params)};
+ return unless $class->isa('Bugzilla::Product');
+
+ $params->{reviewer_required} = Bugzilla->cgi->param('reviewer_required') ? 1 : 0;
+}
+
+sub object_end_of_set_all {
+ my ($self, $args) = @_;
+ my ($object, $params) = @$args{qw(object params)};
+ return unless $object->isa('Bugzilla::Product');
+
+ $object->set('reviewer_required', Bugzilla->cgi->param('reviewer_required') ? 1 : 0);
+}
+
+sub object_end_of_create {
+ my ($self, $args) = @_;
+ my ($object, $params) = @$args{qw(object params)};
+
+ if ($object->isa('Bugzilla::Product')) {
+ $self->_update_user_table({
+ object => $object,
+ old_users => [],
+ new_users => _new_users_from_input('reviewers'),
+ table => 'product_reviewers',
+ id_field => 'product_id',
+ has_sortkey => 1,
+ });
+ }
+ elsif ($object->isa('Bugzilla::Component')) {
+ $self->_update_user_table({
+ object => $object,
+ old_users => [],
+ new_users => _new_users_from_input('reviewers'),
+ table => 'component_reviewers',
+ id_field => 'component_id',
+ has_sortkey => 1,
+ });
+ }
+ elsif (_is_countable_flag($object) && $object->requestee_id && $object->status eq '?') {
+ _adjust_request_count($object, +1);
+ }
+ if (_is_countable_flag($object)) {
+ $self->_log_flag_state_activity($object, $object->status, $object->modification_date);
+ }
+}
+
+sub object_end_of_update {
+ my ($self, $args) = @_;
+ my ($object, $old_object, $changes) = @$args{qw(object old_object changes)};
+
+ if ($object->isa('Bugzilla::Product') && exists Bugzilla->input_params->{reviewers}) {
+ my $diff = $self->_update_user_table({
+ object => $object,
+ old_users => $old_object->reviewers_objs(1),
+ new_users => _new_users_from_input('reviewers'),
+ table => 'product_reviewers',
+ id_field => 'product_id',
+ has_sortkey => 1,
+ return => 'old-new',
+ });
+ $changes->{reviewers} = $diff if $diff;
+ }
+ elsif ($object->isa('Bugzilla::Component')) {
+ my $diff = $self->_update_user_table({
+ object => $object,
+ old_users => $old_object->reviewers_objs(1),
+ new_users => _new_users_from_input('reviewers'),
+ table => 'component_reviewers',
+ id_field => 'component_id',
+ has_sortkey => 1,
+ return => 'old-new',
+ });
+ $changes->{reviewers} = $diff if $diff;
+ }
+ elsif ($object->isa('Bugzilla::Bug')) {
+ my $diff = $self->_update_user_table({
+ object => $object,
+ old_users => $old_object->mentors,
+ new_users => $object->mentors,
+ table => 'bug_mentors',
+ id_field => 'bug_id',
+ return => 'diff',
+ });
+ $changes->{bug_mentor} = $diff if $diff;
+ }
+ elsif (_is_countable_flag($object)) {
+ my ($old_status, $new_status) = ($old_object->status, $object->status);
+ if ($old_status ne '?' && $new_status eq '?') {
+ # setting flag to ?
+ _adjust_request_count($object, +1);
+ }
+ elsif ($old_status eq '?' && $new_status ne '?') {
+ # setting flag from ?
+ _adjust_request_count($old_object, -1);
+ }
+ elsif ($old_object->requestee_id && !$object->requestee_id) {
+ # removing requestee
+ _adjust_request_count($old_object, -1);
+ }
+ elsif (!$old_object->requestee_id && $object->requestee_id) {
+ # setting requestee
+ _adjust_request_count($object, +1);
+ }
+ elsif ($old_object->requestee_id && $object->requestee_id
+ && $old_object->requestee_id != $object->requestee_id)
+ {
+ # changing requestee
+ _adjust_request_count($old_object, -1);
+ _adjust_request_count($object, +1);
+ }
+ }
+}
+
+sub flag_updated {
+ my ($self, $args) = @_;
+ my $flag = $args->{flag};
+ my $timestamp = $args->{timestamp};
+ my $changes = $args->{changes};
+
+ return unless scalar(keys %$changes);
+ if (_is_countable_flag($flag)) {
+ $self->_log_flag_state_activity($flag, $flag->status, $timestamp);
+ }
+}
+
+sub flag_deleted {
+ my ($self, $args) = @_;
+ my $flag = $args->{flag};
+ my $timestamp = $args->{timestamp};
+
+ if (_is_countable_flag($flag) && $flag->requestee_id && $flag->status eq '?') {
+ _adjust_request_count($flag, -1);
+ }
+
+ if (_is_countable_flag($flag)) {
+ $self->_log_flag_state_activity($flag, 'X', $timestamp, Bugzilla->user->id);
+ }
+}
+
+sub _is_countable_flag {
+ my ($object) = @_;
+ return unless $object->isa('Bugzilla::Flag');
+ my $type_name = $object->type->name;
+ return $type_name eq 'review' || $type_name eq 'feedback' || $type_name eq 'needinfo';
+}
+
+sub _log_flag_state_activity {
+ my ($self, $flag, $status, $timestamp, $setter_id) = @_;
+
+ $setter_id //= $flag->setter_id;
+
+ Bugzilla::Extension::Review::FlagStateActivity->create({
+ flag_when => $timestamp,
+ setter_id => $setter_id,
+ status => $status,
+ type_id => $flag->type_id,
+ flag_id => $flag->id,
+ requestee_id => $flag->requestee_id,
+ bug_id => $flag->bug_id,
+ attachment_id => $flag->attach_id,
+ });
+}
+
+sub _adjust_request_count {
+ my ($flag, $add) = @_;
+ return unless my $requestee_id = $flag->requestee_id;
+ my $field = $flag->type->name . '_request_count';
+
+ # update the current user's object so things are display correctly on the
+ # post-processing page
+ my $user = Bugzilla->user;
+ if ($requestee_id == $user->id) {
+ $user->{$field} += $add;
+ }
+
+ # update database directly to avoid creating audit_log entries
+ $add = $add == -1 ? ' - 1' : ' + 1';
+ Bugzilla->dbh->do(
+ "UPDATE profiles SET $field = $field $add WHERE userid = ?",
+ undef,
+ $requestee_id
+ );
+ Bugzilla->memcached->clear({ table => 'profiles', id => $requestee_id });
+}
+
+# bugzilla's handling of requestee matching when creating bugs is "if it's
+# wrong, or matches too many, default to empty", which breaks mandatory
+# reviewer requirements. instead we just throw an error.
+sub post_bug_attachment_flags {
+ my ($self, $args) = @_;
+ $self->_check_review_flag($args);
+}
+
+sub create_attachment_flags {
+ my ($self, $args) = @_;
+ $self->_check_review_flag($args);
+}
+
+sub _check_review_flag {
+ my ($self, $args) = @_;
+ my $bug = $args->{bug};
+ my $cgi = Bugzilla->cgi;
+
+ # extract the set flag-types
+ my @flagtype_ids = map { /^flag_type-(\d+)$/ ? $1 : () } $cgi->param();
+ @flagtype_ids = grep { $cgi->param("flag_type-$_") eq '?' } @flagtype_ids;
+ return unless scalar(@flagtype_ids);
+
+ # find valid review flagtypes
+ my $flag_types = Bugzilla::FlagType::match({
+ product_id => $bug->product_id,
+ component_id => $bug->component_id,
+ is_active => 1
+ });
+ foreach my $flag_type (@$flag_types) {
+ next unless $flag_type->name eq 'review'
+ && $flag_type->target_type eq 'attachment';
+ my $type_id = $flag_type->id;
+ next unless scalar(grep { $_ == $type_id } @flagtype_ids);
+
+ my $reviewers = clean_text($cgi->param("requestee_type-$type_id") || '');
+ if ($reviewers eq '' && $bug->product_obj->reviewer_required) {
+ ThrowUserError('reviewer_required');
+ }
+
+ foreach my $reviewer (split(/[,;]+/, $reviewers)) {
+ # search on the reviewer
+ my $users = Bugzilla::User::match($reviewer, 2, 1);
+
+ # no matches
+ if (scalar(@$users) == 0) {
+ ThrowUserError('user_match_failed', { name => $reviewer });
+ }
+
+ # more than one match, throw error
+ if (scalar(@$users) > 1) {
+ ThrowUserError('user_match_too_many', { fields => [ 'review' ] });
+ }
+ }
+ }
+}
+
+sub flag_end_of_update {
+ my ($self, $args) = @_;
+ my ($object, $new_flags) = @$args{qw(object new_flags)};
+ my $bug = $object->isa('Bugzilla::Attachment') ? $object->bug : $object;
+ return unless $bug->product_obj->reviewer_required;
+
+ foreach my $orig_change (@$new_flags) {
+ my $change = $orig_change; # work on a copy
+ $change =~ s/^[^:]+://;
+ my $reviewer = '';
+ if ($change =~ s/\(([^\)]+)\)$//) {
+ $reviewer = $1;
+ }
+ my ($name, $value) = $change =~ /^(.+)(.)$/;
+
+ if ($name eq 'review' && $value eq '?' && $reviewer eq '') {
+ ThrowUserError('reviewer_required');
+ }
+ }
+}
+
+#
+# search
+#
+
+sub buglist_columns {
+ my ($self, $args) = @_;
+ my $dbh = Bugzilla->dbh;
+ my $columns = $args->{columns};
+ $columns->{bug_mentor} = { title => 'Mentor' };
+ if (Bugzilla->user->id) {
+ $columns->{bug_mentor}->{name}
+ = $dbh->sql_group_concat('map_mentors_names.login_name');
+ }
+ else {
+ $columns->{bug_mentor}->{name}
+ = $dbh->sql_group_concat('map_mentors_names.realname');
+
+ }
+}
+
+sub buglist_column_joins {
+ my ($self, $args) = @_;
+ my $column_joins = $args->{column_joins};
+ $column_joins->{bug_mentor} = {
+ as => 'map_mentors',
+ table => 'bug_mentors',
+ then_to => {
+ as => 'map_mentors_names',
+ table => 'profiles',
+ from => 'map_mentors.user_id',
+ to => 'userid',
+ },
+ },
+}
+
+sub search_operator_field_override {
+ my ($self, $args) = @_;
+ my $operators = $args->{operators};
+ $operators->{bug_mentor} = {
+ _non_changed => sub {
+ Bugzilla::Search::_user_nonchanged(@_)
+ }
+ };
+}
+
+#
+# web service / pages
+#
+
+sub webservice {
+ my ($self, $args) = @_;
+ my $dispatch = $args->{dispatch};
+ $dispatch->{Review} = "Bugzilla::Extension::Review::WebService";
+}
+
+sub page_before_template {
+ my ($self, $args) = @_;
+
+ if ($args->{page_id} eq 'review_suggestions.html') {
+ $self->review_suggestions_report($args);
+ }
+ elsif ($args->{page_id} eq 'review_requests_rebuild.html') {
+ $self->review_requests_rebuild($args);
+ }
+ elsif ($args->{page_id} eq 'review_history.html') {
+ $self->review_history($args);
+ }
+}
+
+sub review_suggestions_report {
+ my ($self, $args) = @_;
+
+ my $user = Bugzilla->login(LOGIN_REQUIRED);
+ my $products = [];
+ my @products = sort { lc($a->name) cmp lc($b->name) }
+ @{ Bugzilla->user->get_accessible_products };
+ foreach my $product_obj (@products) {
+ my $has_reviewers = 0;
+ my $product = {
+ name => $product_obj->name,
+ components => [],
+ reviewers => $product_obj->reviewers_objs(1),
+ };
+ $has_reviewers = scalar @{ $product->{reviewers} };
+
+ foreach my $component_obj (@{ $product_obj->components }) {
+ my $component = {
+ name => $component_obj->name,
+ reviewers => $component_obj->reviewers_objs(1),
+ };
+ if (@{ $component->{reviewers} }) {
+ push @{ $product->{components} }, $component;
+ $has_reviewers = 1;
+ }
+ }
+
+ if ($has_reviewers) {
+ push @$products, $product;
+ }
+ }
+ $args->{vars}->{products} = $products;
+}
+
+sub review_requests_rebuild {
+ my ($self, $args) = @_;
+
+ Bugzilla->user->in_group('admin')
+ || ThrowUserError('auth_failure', { group => 'admin',
+ action => 'run',
+ object => 'review_requests_rebuild' });
+ if (Bugzilla->cgi->param('rebuild')) {
+ my $processed_users = 0;
+ rebuild_review_counters(sub {
+ my ($count, $total) = @_;
+ $processed_users = $total;
+ });
+ $args->{vars}->{rebuild} = 1;
+ $args->{vars}->{total} = $processed_users;
+ }
+}
+
+sub review_history {
+ my ($self, $args) = @_;
+
+ my $user = Bugzilla->login(LOGIN_REQUIRED);
+
+ Bugzilla::User::match_field({ 'requestee' => { 'type' => 'single' } });
+ my $requestee = Bugzilla->input_params->{requestee};
+ if ($requestee) {
+ $args->{vars}{requestee} = Bugzilla::User->check({ name => $requestee, cache => 1 });
+ }
+ else {
+ $args->{vars}{requestee} = $user;
+ }
+}
+
+#
+# installation
+#
+
+sub db_schema_abstract_schema {
+ my ($self, $args) = @_;
+ $args->{'schema'}->{'product_reviewers'} = {
+ FIELDS => [
+ id => {
+ TYPE => 'MEDIUMSERIAL',
+ NOTNULL => 1,
+ PRIMARYKEY => 1,
+ },
+ user_id => {
+ TYPE => 'INT3',
+ NOTNULL => 1,
+ REFERENCES => {
+ TABLE => 'profiles',
+ COLUMN => 'userid',
+ DELETE => 'CASCADE',
+ }
+ },
+ display_name => {
+ TYPE => 'VARCHAR(64)',
+ },
+ product_id => {
+ TYPE => 'INT2',
+ NOTNULL => 1,
+ REFERENCES => {
+ TABLE => 'products',
+ COLUMN => 'id',
+ DELETE => 'CASCADE',
+ }
+ },
+ sortkey => {
+ TYPE => 'INT2',
+ NOTNULL => 1,
+ DEFAULT => 0,
+ },
+ ],
+ INDEXES => [
+ product_reviewers_idx => {
+ FIELDS => [ 'user_id', 'product_id' ],
+ TYPE => 'UNIQUE',
+ },
+ ],
+ };
+ $args->{'schema'}->{'component_reviewers'} = {
+ FIELDS => [
+ id => {
+ TYPE => 'MEDIUMSERIAL',
+ NOTNULL => 1,
+ PRIMARYKEY => 1,
+ },
+ user_id => {
+ TYPE => 'INT3',
+ NOTNULL => 1,
+ REFERENCES => {
+ TABLE => 'profiles',
+ COLUMN => 'userid',
+ DELETE => 'CASCADE',
+ }
+ },
+ display_name => {
+ TYPE => 'VARCHAR(64)',
+ },
+ component_id => {
+ TYPE => 'INT2',
+ NOTNULL => 1,
+ REFERENCES => {
+ TABLE => 'components',
+ COLUMN => 'id',
+ DELETE => 'CASCADE',
+ }
+ },
+ sortkey => {
+ TYPE => 'INT2',
+ NOTNULL => 1,
+ DEFAULT => 0,
+ },
+ ],
+ INDEXES => [
+ component_reviewers_idx => {
+ FIELDS => [ 'user_id', 'component_id' ],
+ TYPE => 'UNIQUE',
+ },
+ ],
+ };
+
+ $args->{'schema'}->{'flag_state_activity'} = {
+ FIELDS => [
+ id => {
+ TYPE => 'MEDIUMSERIAL',
+ NOTNULL => 1,
+ PRIMARYKEY => 1,
+ },
+
+ flag_when => {
+ TYPE => 'DATETIME',
+ NOTNULL => 1,
+ },
+
+ type_id => {
+ TYPE => 'INT2',
+ NOTNULL => 1,
+ REFERENCES => {
+ TABLE => 'flagtypes',
+ COLUMN => 'id',
+ DELETE => 'CASCADE'
+ }
+ },
+
+ flag_id => {
+ TYPE => 'INT3',
+ NOTNULL => 1,
+ },
+
+ setter_id => {
+ TYPE => 'INT3',
+ NOTNULL => 1,
+ REFERENCES => {
+ TABLE => 'profiles',
+ COLUMN => 'userid',
+ },
+ },
+
+ requestee_id => {
+ TYPE => 'INT3',
+ REFERENCES => {
+ TABLE => 'profiles',
+ COLUMN => 'userid',
+ },
+ },
+
+ bug_id => {
+ TYPE => 'INT3',
+ NOTNULL => 1,
+ REFERENCES => {
+ TABLE => 'bugs',
+ COLUMN => 'bug_id',
+ DELETE => 'CASCADE'
+ }
+ },
+
+ attachment_id => {
+ TYPE => 'INT3',
+ REFERENCES => {
+ TABLE => 'attachments',
+ COLUMN => 'attach_id',
+ DELETE => 'CASCADE'
+ }
+ },
+
+ status => {
+ TYPE => 'CHAR(1)',
+ NOTNULL => 1,
+ },
+ ],
+ };
+
+ $args->{'schema'}->{'bug_mentors'} = {
+ FIELDS => [
+ bug_id => {
+ TYPE => 'INT3',
+ NOTNULL => 1,
+ REFERENCES => {
+ TABLE => 'bugs',
+ COLUMN => 'bug_id',
+ DELETE => 'CASCADE',
+ },
+ },
+ user_id => {
+ TYPE => 'INT3',
+ NOTNULL => 1,
+ REFERENCES => {
+ TABLE => 'profiles',
+ COLUMN => 'userid',
+ DELETE => 'CASCADE',
+ }
+ },
+ ],
+ INDEXES => [
+ bug_mentors_idx => {
+ FIELDS => [ 'bug_id', 'user_id' ],
+ TYPE => 'UNIQUE',
+ },
+ bug_mentors_bug_id_idx => [ 'bug_id' ],
+ ],
+ };
+
+ $args->{'schema'}->{'bug_mentors'} = {
+ FIELDS => [
+ bug_id => {
+ TYPE => 'INT3',
+ NOTNULL => 1,
+ REFERENCES => {
+ TABLE => 'bugs',
+ COLUMN => 'bug_id',
+ DELETE => 'CASCADE',
+ },
+ },
+ user_id => {
+ TYPE => 'INT3',
+ NOTNULL => 1,
+ REFERENCES => {
+ TABLE => 'profiles',
+ COLUMN => 'userid',
+ DELETE => 'CASCADE',
+ }
+ },
+ ],
+ INDEXES => [
+ bug_mentors_idx => {
+ FIELDS => [ 'bug_id', 'user_id' ],
+ TYPE => 'UNIQUE',
+ },
+ bug_mentors_bug_id_idx => [ 'bug_id' ],
+ ],
+ };
+}
+
+sub install_update_db {
+ my $dbh = Bugzilla->dbh;
+ $dbh->bz_add_column(
+ 'products',
+ 'reviewer_required', { TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE' }
+ );
+ $dbh->bz_add_column(
+ 'profiles',
+ 'review_request_count', { TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0 }
+ );
+ $dbh->bz_add_column(
+ 'profiles',
+ 'feedback_request_count', { TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0 }
+ );
+ $dbh->bz_add_column(
+ 'profiles',
+ 'needinfo_request_count', { TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0 }
+ );
+
+ my $field = Bugzilla::Field->new({ name => 'bug_mentor' });
+ if (!$field) {
+ Bugzilla::Field->create({
+ name => 'bug_mentor',
+ description => 'Mentor'
+ });
+ }
+}
+
+sub install_filesystem {
+ my ($self, $args) = @_;
+ my $files = $args->{files};
+ my $extensions_dir = bz_locations()->{extensionsdir};
+ $files->{"$extensions_dir/Review/bin/review_requests_rebuild.pl"} = {
+ perms => Bugzilla::Install::Filesystem::OWNER_EXECUTE
+ };
+}
+
+
+__PACKAGE__->NAME;