From e4953b938ed652365eeba7c967ea74882afbb0a3 Mon Sep 17 00:00:00 2001 From: Dave Lawrence Date: Wed, 28 Mar 2012 17:25:04 -0400 Subject: Bug 684701: add "watch user" to component watching --- extensions/BMO/Extension.pm | 3 +- extensions/ComponentWatching/Config.pm | 24 +- extensions/ComponentWatching/Extension.pm | 163 ++++++++-- extensions/ComponentWatching/Extension.pm.orig | 345 +++++++++++++++++++++ .../ComponentWatching/reset-watch-preferences.pl | 75 ----- .../account/prefs/component_watch.html.tmpl | 65 ++-- .../account/prefs/email-relationships.html.tmpl | 22 +- .../hook/account/prefs/prefs-tabs.html.tmpl | 23 +- .../admin/components/edit-common-rows.html.tmpl | 20 ++ .../admin/components/list-before_table.html.tmpl | 17 + .../messages-component_updated_fields.html.tmpl | 15 + .../default/hook/global/reason-descs-end.none.tmpl | 22 +- .../hook/global/user-error-errors.html.tmpl | 13 + t/008filter.t | 3 +- 14 files changed, 613 insertions(+), 197 deletions(-) create mode 100644 extensions/ComponentWatching/Extension.pm.orig delete mode 100755 extensions/ComponentWatching/reset-watch-preferences.pl create mode 100644 extensions/ComponentWatching/template/en/default/hook/admin/components/edit-common-rows.html.tmpl create mode 100644 extensions/ComponentWatching/template/en/default/hook/admin/components/list-before_table.html.tmpl create mode 100644 extensions/ComponentWatching/template/en/default/hook/global/messages-component_updated_fields.html.tmpl create mode 100644 extensions/ComponentWatching/template/en/default/hook/global/user-error-errors.html.tmpl diff --git a/extensions/BMO/Extension.pm b/extensions/BMO/Extension.pm index 115d944b6..47f022c97 100644 --- a/extensions/BMO/Extension.pm +++ b/extensions/BMO/Extension.pm @@ -564,7 +564,8 @@ sub bug_check_groups { my ($self, $args) = @_; my $group_names = $args->{'group_names'}; my $add_groups = $args->{'add_groups'}; - + + return unless $group_names; $group_names = ref $group_names ? $group_names : [ map { trim($_) } split(',', $group_names) ]; diff --git a/extensions/ComponentWatching/Config.pm b/extensions/ComponentWatching/Config.pm index 534afaa61..560b5c3c5 100644 --- a/extensions/ComponentWatching/Config.pm +++ b/extensions/ComponentWatching/Config.pm @@ -1,23 +1,9 @@ -# -*- Mode: perl; indent-tabs-mode: nil -*- +# 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/. # -# The contents of this file are subject to the Mozilla Public -# License Version 1.1 (the "License"); you may not use this file -# except in compliance with the License. You may obtain a copy of -# the License at http://www.mozilla.org/MPL/ -# -# Software distributed under the License is distributed on an "AS -# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or -# implied. See the License for the specific language governing -# rights and limitations under the License. -# -# The Original Code is the Component Watching Extension -# -# The Initial Developer of the Original Code is the Mozilla Foundation -# Portions created by the Initial Developers are Copyright (C) 2011 the -# Initial Developer. All Rights Reserved. -# -# Contributor(s): -# Byron Jones +# This Source Code Form is "Incompatible With Secondary Licenses", as +# defined by the Mozilla Public License, v. 2.0. package Bugzilla::Extension::ComponentWatching; use strict; diff --git a/extensions/ComponentWatching/Extension.pm b/extensions/ComponentWatching/Extension.pm index d39d9cd55..c47fa4749 100644 --- a/extensions/ComponentWatching/Extension.pm +++ b/extensions/ComponentWatching/Extension.pm @@ -1,23 +1,9 @@ -# -*- Mode: perl; indent-tabs-mode: nil -*- +# 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/. # -# The contents of this file are subject to the Mozilla Public -# License Version 1.1 (the "License"); you may not use this file -# except in compliance with the License. You may obtain a copy of -# the License at http://www.mozilla.org/MPL/ -# -# Software distributed under the License is distributed on an "AS -# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or -# implied. See the License for the specific language governing -# rights and limitations under the License. -# -# The Original Code is the Component Watching Extension -# -# The Initial Developer of the Original Code is the Mozilla Foundation -# Portions created by the Initial Developers are Copyright (C) 2011 the -# Initial Developer. All Rights Reserved. -# -# Contributor(s): -# Byron Jones +# This Source Code Form is "Incompatible With Secondary Licenses", as +# defined by the Mozilla Public License, v. 2.0. package Bugzilla::Extension::ComponentWatching; use strict; @@ -28,8 +14,9 @@ use Bugzilla::Error; use Bugzilla::Group; use Bugzilla::User; use Bugzilla::User::Setting; +use Bugzilla::Util qw(trim); -our $VERSION = '1.1'; +our $VERSION = '2'; use constant REL_COMPONENT_WATCHER => 15; @@ -72,6 +59,22 @@ sub db_schema_abstract_schema { }; } +sub install_update_db { + my $dbh = Bugzilla->dbh; + $dbh->bz_add_column( + 'components', + 'watch_user', + { + TYPE => 'INT3', + REFERENCES => { + TABLE => 'profiles', + COLUMN => 'userid', + DELETE => 'SET NULL', + } + } + ); +} + # # templates # @@ -83,6 +86,87 @@ sub template_before_create { $constants->{REL_COMPONENT_WATCHER} = REL_COMPONENT_WATCHER; } +# +# user-watch +# + +BEGIN { + *Bugzilla::Component::watch_user = \&_component_watch_user; +} + +sub _component_watch_user { + my ($self) = @_; + return unless $self->{watch_user}; + $self->{watch_user_object} ||= Bugzilla::User->new($self->{watch_user}); + return $self->{watch_user_object}; +} + +sub object_columns { + my ($self, $args) = @_; + my $class = $args->{class}; + my $columns = $args->{columns}; + return unless $class->isa('Bugzilla::Component'); + + push(@$columns, 'watch_user'); +} + +sub object_update_columns { + my ($self, $args) = @_; + my $object = $args->{object}; + my $columns = $args->{columns}; + return unless $object->isa('Bugzilla::Component'); + + push(@$columns, 'watch_user'); + + # editcomponents.cgi doesn't call set_all, so we have to do this here + my $input = Bugzilla->input_params; + $object->set('watch_user', $input->{watch_user}); +} + +sub object_validators { + my ($self, $args) = @_; + my $class = $args->{class}; + my $validators = $args->{validators}; + return unless $class->isa('Bugzilla::Component'); + + $validators->{watch_user} = \&_check_watch_user; +} + +sub object_before_create { + my ($self, $args) = @_; + my $class = $args->{class}; + my $params = $args->{params}; + return unless $class->isa('Bugzilla::Component'); + + my $input = Bugzilla->input_params; + $params->{watch_user} = $input->{watch_user}; +} + +sub object_end_of_update { + my ($self, $args) = @_; + my $object = $args->{object}; + my $old_object = $args->{old_object}; + my $changes = $args->{changes}; + return unless $object->isa('Bugzilla::Component'); + + my $old_id = $old_object->watch_user ? $old_object->watch_user->id : 0; + my $new_id = $object->watch_user ? $object->watch_user->id : 0; + return unless $old_id == $new_id; + + $changes->{watch_user} = [ $old_id ? $old_id : undef, $new_id ? $new_id : undef ]; +} + +sub _check_watch_user { + my ($self, $value, $field) = @_; + + $value = trim($value || ''); + return if $value eq ''; + if ($value !~ /\.bugs$/i) { + ThrowUserError('component_watch_invalid_watch_user'); + } + return Bugzilla::User->check($value)->id; +} + # # preferences # @@ -95,15 +179,16 @@ sub user_preferences { my $save = $args->{'save_changes'}; my $handled = $args->{'handled'}; my $user = Bugzilla->user; + my $input = Bugzilla->input_params; if ($save) { my ($sth, $sthAdd, $sthDel); - if (Bugzilla->input_params->{'add'}) { + if ($input->{'add'} && $input->{'add_product'}) { # add watch - my $productName = Bugzilla->input_params->{'add_product'}; - my $ra_componentNames = Bugzilla->input_params->{'add_component'}; + my $productName = $input->{'add_product'}; + my $ra_componentNames = $input->{'add_component'}; $ra_componentNames = [$ra_componentNames] unless ref($ra_componentNames); # load product and verify access @@ -132,7 +217,7 @@ sub user_preferences { } else { # remove watch(s) - foreach my $name (keys %{Bugzilla->input_params}) { + foreach my $name (keys %$input) { if ($name =~ /^del_(\d+)$/) { _deleteProductWatch($user, $1); } elsif ($name =~ /^del_(\d+)_(\d+)$/) { @@ -185,6 +270,7 @@ sub bugmail_recipients { } } + # add component watchers my $dbh = Bugzilla->dbh; my $sth = $dbh->prepare(" SELECT user_id @@ -198,6 +284,35 @@ sub bugmail_recipients { $recipients->{$uid}->{+REL_COMPONENT_WATCHER} = Bugzilla::BugMail::BIT_WATCHING(); } } + + # add component watchers from watch-users + my $uidList = join(',', keys %$recipients); + $sth = $dbh->prepare(" + SELECT component_watch.user_id + FROM components + INNER JOIN component_watch ON component_watch.component_id = components.id + WHERE components.watch_user in ($uidList) + "); + $sth->execute(); + while (my ($uid) = $sth->fetchrow_array) { + if (!exists $recipients->{$uid}) { + $recipients->{$uid}->{+REL_COMPONENT_WATCHER} = Bugzilla::BugMail::BIT_WATCHING(); + } + } + + # add watch-users from component watchers + $sth = $dbh->prepare(" + SELECT watch_user + FROM components + WHERE (id = ? OR id = ?) + AND (watch_user IS NOT NULL) + "); + $sth->execute($oldComponentId, $newComponentId); + while (my ($uid) = $sth->fetchrow_array) { + if (!exists $recipients->{$uid}) { + $recipients->{$uid}->{+REL_COMPONENT_WATCHER} = Bugzilla::BugMail::BIT_DIRECT(); + } + } } sub bugmail_relationships { diff --git a/extensions/ComponentWatching/Extension.pm.orig b/extensions/ComponentWatching/Extension.pm.orig new file mode 100644 index 000000000..d39d9cd55 --- /dev/null +++ b/extensions/ComponentWatching/Extension.pm.orig @@ -0,0 +1,345 @@ +# -*- Mode: perl; indent-tabs-mode: nil -*- +# +# The contents of this file are subject to the Mozilla Public +# License Version 1.1 (the "License"); you may not use this file +# except in compliance with the License. You may obtain a copy of +# the License at http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS +# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or +# implied. See the License for the specific language governing +# rights and limitations under the License. +# +# The Original Code is the Component Watching Extension +# +# The Initial Developer of the Original Code is the Mozilla Foundation +# Portions created by the Initial Developers are Copyright (C) 2011 the +# Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Byron Jones + +package Bugzilla::Extension::ComponentWatching; +use strict; +use base qw(Bugzilla::Extension); + +use Bugzilla::Constants; +use Bugzilla::Error; +use Bugzilla::Group; +use Bugzilla::User; +use Bugzilla::User::Setting; + +our $VERSION = '1.1'; + +use constant REL_COMPONENT_WATCHER => 15; + +# +# installation +# + +sub db_schema_abstract_schema { + my ($self, $args) = @_; + $args->{'schema'}->{'component_watch'} = { + FIELDS => [ + user_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => { + TABLE => 'profiles', + COLUMN => 'userid', + DELETE => 'CASCADE', + } + }, + component_id => { + TYPE => 'INT2', + NOTNULL => 0, + REFERENCES => { + TABLE => 'components', + COLUMN => 'id', + DELETE => 'CASCADE', + } + }, + product_id => { + TYPE => 'INT2', + NOTNULL => 0, + REFERENCES => { + TABLE => 'products', + COLUMN => 'id', + DELETE => 'CASCADE', + } + }, + ], + }; +} + +# +# templates +# + +sub template_before_create { + my ($self, $args) = @_; + my $config = $args->{config}; + my $constants = $config->{CONSTANTS}; + $constants->{REL_COMPONENT_WATCHER} = REL_COMPONENT_WATCHER; +} + +# +# preferences +# + +sub user_preferences { + my ($self, $args) = @_; + my $tab = $args->{'current_tab'}; + return unless $tab eq 'component_watch'; + + my $save = $args->{'save_changes'}; + my $handled = $args->{'handled'}; + my $user = Bugzilla->user; + + if ($save) { + my ($sth, $sthAdd, $sthDel); + + if (Bugzilla->input_params->{'add'}) { + # add watch + + my $productName = Bugzilla->input_params->{'add_product'}; + my $ra_componentNames = Bugzilla->input_params->{'add_component'}; + $ra_componentNames = [$ra_componentNames] unless ref($ra_componentNames); + + # load product and verify access + my $product = Bugzilla::Product->new({ name => $productName }); + unless ($product && $user->can_access_product($product)) { + ThrowUserError('product_access_denied', { product => $productName }); + } + + if (grep { $_ eq '' } @$ra_componentNames) { + # watching a product + _addProductWatch($user, $product); + + } else { + # watching specific components + foreach my $componentName (@$ra_componentNames) { + my $component = Bugzilla::Component->new({ name => $componentName, product => $product }); + unless ($component) { + ThrowUserError('product_access_denied', { product => $productName }); + } + _addComponentWatch($user, $component); + } + } + + _addDefaultSettings($user); + + } else { + # remove watch(s) + + foreach my $name (keys %{Bugzilla->input_params}) { + if ($name =~ /^del_(\d+)$/) { + _deleteProductWatch($user, $1); + } elsif ($name =~ /^del_(\d+)_(\d+)$/) { + _deleteComponentWatch($user, $1, $2); + } + } + } + } + + $args->{'vars'}->{'watches'} = _getWatches($user); + + $$handled = 1; +} + +# +# bugmail +# + +sub bugmail_recipients { + my ($self, $args) = @_; + my $bug = $args->{'bug'}; + my $recipients = $args->{'recipients'}; + my $diffs = $args->{'diffs'}; + + my ($oldProductId, $newProductId) = ($bug->product_id, $bug->product_id); + my ($oldComponentId, $newComponentId) = ($bug->component_id, $bug->component_id); + + # notify when the product/component is switch from one being watched + if (@$diffs) { + # we need the product to process the component, so scan for that first + my $product; + foreach my $ra (@$diffs) { + next if !(exists $ra->{'old'} + && exists $ra->{'field_name'}); + if ($ra->{'field_name'} eq 'product') { + $product = Bugzilla::Product->new({ name => $ra->{'old'} }); + $oldProductId = $product->id; + } + } + if (!$product) { + $product = Bugzilla::Product->new($oldProductId); + } + foreach my $ra (@$diffs) { + next if !(exists $ra->{'old'} + && exists $ra->{'field_name'}); + if ($ra->{'field_name'} eq 'component') { + my $component = Bugzilla::Component->new({ name => $ra->{'old'}, product => $product }); + $oldComponentId = $component->id; + } + } + } + + my $dbh = Bugzilla->dbh; + my $sth = $dbh->prepare(" + SELECT user_id + FROM component_watch + WHERE ((product_id = ? OR product_id = ?) AND component_id IS NULL) + OR (component_id = ? OR component_id = ?) + "); + $sth->execute($oldProductId, $newProductId, $oldComponentId, $newComponentId); + while (my ($uid) = $sth->fetchrow_array) { + if (!exists $recipients->{$uid}) { + $recipients->{$uid}->{+REL_COMPONENT_WATCHER} = Bugzilla::BugMail::BIT_WATCHING(); + } + } +} + +sub bugmail_relationships { + my ($self, $args) = @_; + my $relationships = $args->{relationships}; + $relationships->{+REL_COMPONENT_WATCHER} = 'Component-Watcher'; +} + +# +# db +# + +sub _getWatches { + my ($user) = @_; + my $dbh = Bugzilla->dbh; + + my $sth = $dbh->prepare(" + SELECT product_id, component_id + FROM component_watch + WHERE user_id = ? + "); + $sth->execute($user->id); + my @watches; + while (my ($productId, $componentId) = $sth->fetchrow_array) { + my $product = Bugzilla::Product->new($productId); + next unless $product && $user->can_access_product($product); + + my %watch = ( product => $product ); + if ($componentId) { + my $component = Bugzilla::Component->new($componentId); + next unless $component; + $watch{'component'} = $component; + } + + push @watches, \%watch; + } + + @watches = sort { + $a->{'product'}->name cmp $b->{'product'}->name + || $a->{'component'}->name cmp $b->{'component'}->name + } @watches; + + return \@watches; +} + +sub _addProductWatch { + my ($user, $product) = @_; + my $dbh = Bugzilla->dbh; + + my $sth = $dbh->prepare(" + SELECT 1 + FROM component_watch + WHERE user_id = ? AND product_id = ? AND component_id IS NULL + "); + $sth->execute($user->id, $product->id); + return if $sth->fetchrow_array; + + $sth = $dbh->prepare(" + DELETE FROM component_watch + WHERE user_id = ? AND product_id = ? + "); + $sth->execute($user->id, $product->id); + + $sth = $dbh->prepare(" + INSERT INTO component_watch(user_id, product_id) + VALUES (?, ?) + "); + $sth->execute($user->id, $product->id); +} + +sub _addComponentWatch { + my ($user, $component) = @_; + my $dbh = Bugzilla->dbh; + + my $sth = $dbh->prepare(" + SELECT 1 + FROM component_watch + WHERE user_id = ? + AND (component_id = ? OR (product_id = ? AND component_id IS NULL)) + "); + $sth->execute($user->id, $component->id, $component->product_id); + return if $sth->fetchrow_array; + + $sth = $dbh->prepare(" + INSERT INTO component_watch(user_id, product_id, component_id) + VALUES (?, ?, ?) + "); + $sth->execute($user->id, $component->product_id, $component->id); +} + +sub _deleteProductWatch { + my ($user, $productId) = @_; + my $dbh = Bugzilla->dbh; + + my $sth = $dbh->prepare(" + DELETE FROM component_watch + WHERE user_id = ? AND product_id = ? AND component_id IS NULL + "); + $sth->execute($user->id, $productId); +} + +sub _deleteComponentWatch { + my ($user, $productId, $componentId) = @_; + my $dbh = Bugzilla->dbh; + + my $sth = $dbh->prepare(" + DELETE FROM component_watch + WHERE user_id = ? AND product_id = ? AND component_id = ? + "); + $sth->execute($user->id, $productId, $componentId); +} + +sub _addDefaultSettings { + my ($user) = @_; + my $dbh = Bugzilla->dbh; + + my $sth = $dbh->prepare(" + SELECT 1 + FROM email_setting + WHERE user_id = ? AND relationship = ? + "); + $sth->execute($user->id, REL_COMPONENT_WATCHER); + return if $sth->fetchrow_array; + + my @defaultEvents = ( + EVT_OTHER, + EVT_COMMENT, + EVT_ATTACHMENT, + EVT_ATTACHMENT_DATA, + EVT_PROJ_MANAGEMENT, + EVT_OPENED_CLOSED, + EVT_KEYWORD, + EVT_DEPEND_BLOCK, + EVT_BUG_CREATED, + ); + foreach my $event (@defaultEvents) { + $dbh->do( + "INSERT INTO email_setting(user_id,relationship,event) VALUES (?,?,?)", + undef, + $user->id, REL_COMPONENT_WATCHER, $event + ); + } +} + +__PACKAGE__->NAME; diff --git a/extensions/ComponentWatching/reset-watch-preferences.pl b/extensions/ComponentWatching/reset-watch-preferences.pl deleted file mode 100755 index f190ebd48..000000000 --- a/extensions/ComponentWatching/reset-watch-preferences.pl +++ /dev/null @@ -1,75 +0,0 @@ -#!/usr/bin/perl - -# -*- Mode: perl; indent-tabs-mode: nil -*- -# -# The contents of this file are subject to the Mozilla Public -# License Version 1.1 (the "License"); you may not use this file -# except in compliance with the License. You may obtain a copy of -# the License at http://www.mozilla.org/MPL/ -# -# Software distributed under the License is distributed on an "AS -# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or -# implied. See the License for the specific language governing -# rights and limitations under the License. -# -# The Original Code is the Component Watching Extension -# -# The Initial Developer of the Original Code is the Mozilla Foundation -# Portions created by the Initial Developers are Copyright (C) 2011 the -# Initial Developer. All Rights Reserved. -# -# Contributor(s): -# Byron Jones - -use strict; -use warnings; - -use lib '.'; -$| = 1; - -use Bugzilla; -use Bugzilla::Constants; -use Bugzilla::Install::Util qw(indicate_progress); - -Bugzilla->usage_mode(USAGE_MODE_CMDLINE); - -my @DEFAULT_EVENTS = qw(0 2 3 4 5 6 7 9 10 50); -my $REL_COMP_WATCH = 15; - -print "This script resets the component watching preferences back to\n"; -print "default values. It is required to be run when upgrading from\n"; -print "version 1.0 to 1.1\n"; -print "Press to start, or CTRL+C to cancel... "; -getc(); -print "\n"; - -my $dbh = Bugzilla->dbh; - -$dbh->bz_start_transaction(); - -my @users; -my $ra_user_ids = $dbh->selectcol_arrayref( - "SELECT DISTINCT user_id FROM component_watch" -); - -my $total = scalar @$ra_user_ids; -my $count = 0; -foreach my $user_id (@$ra_user_ids) { - indicate_progress({ current => $count++, total => $total }) if $total > 10; - $dbh->do( - "DELETE FROM email_setting WHERE user_id=? AND relationship=?", - undef, - $user_id, $REL_COMP_WATCH - ); - foreach my $event (@DEFAULT_EVENTS) { - $dbh->do( - "INSERT INTO email_setting(user_id,relationship,event) VALUES (?,?,?)", - undef, - $user_id, $REL_COMP_WATCH, $event - ); - } -} - -$dbh->bz_commit_transaction(); - -print "Done.\n"; diff --git a/extensions/ComponentWatching/template/en/default/account/prefs/component_watch.html.tmpl b/extensions/ComponentWatching/template/en/default/account/prefs/component_watch.html.tmpl index dd2fb0560..46acd386f 100644 --- a/extensions/ComponentWatching/template/en/default/account/prefs/component_watch.html.tmpl +++ b/extensions/ComponentWatching/template/en/default/account/prefs/component_watch.html.tmpl @@ -1,22 +1,9 @@ -[%# - # The contents of this file are subject to the Mozilla Public - # License Version 1.1 (the "License"); you may not use this file - # except in compliance with the License. You may obtain a copy of - # the License at http://www.mozilla.org/MPL/ +[%# 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/. # - # Software distributed under the License is distributed on an "AS - # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or - # implied. See the License for the specific language governing - # rights and limitations under the License. - # - # The Original Code is the Component Watching Extension - # - # The Initial Developer of the Original Code is the Mozilla Foundation - # Portions created by the Initial Developers are Copyright (C) 2011 the - # Initial Developer. All Rights Reserved. - # - # Contributor(s): - # Byron Jones + # This Source Code Form is "Incompatible With Secondary Licenses", as + # defined by the Mozilla Public License, v. 2.0. #%] [%# initialise product to component mapping #%] @@ -29,15 +16,22 @@ var useclassification = false; var first_load = true; var last_sel = []; var cpts = new Array(); +var watch_users = new Array(); [% n = 0 %] [% FOREACH prod = selectable_products %] cpts['[% n %]'] = [ [%- FOREACH comp = prod.components %]'[% comp.name FILTER js %]'[% ", " UNLESS loop.last %] [%- END -%] ]; - [% n = n+1 %] + [% n = n + 1 %] + [% FOREACH comp = prod.components %] + [% IF comp.watch_user %] + if (!watch_users['[% prod.name FILTER js %]']) + watch_users['[% prod.name FILTER js %]'] = new Array(); + watch_users['[% prod.name FILTER js %]']['[% comp.name FILTER js %]'] = '[% comp.watch_user.login FILTER js %]'; + [% END %] + [% END %] [% END %] -