fix the default template name for the embedded report tags.
authorTony Cook <tony@develop-help.com>
Fri, 28 Apr 2006 06:06:41 +0000 (06:06 +0000)
committertony <tony@45cb6cf1-00bc-42d2-bb5a-07f51df49f94>
Fri, 28 Apr 2006 06:06:41 +0000 (06:06 +0000)
=item *

added dynamic tag ifUserMemberOf which checks if the current user is a
member of the named group.  Supply a leading * to name query groups.

=item *

added a name or identifier field to the article_files table, and
administration tools to manage it.

=item *

re-worked the formatting API for easier extension

=item *

added filelink[] body text tag

=item *

removed some old commented code from Generate.pm

13 files changed:
schema/bse.sql
site/cgi-bin/modules/ArticleFile.pm
site/cgi-bin/modules/BSE/DB/Mysql.pm
site/cgi-bin/modules/BSE/Edit/Article.pm
site/cgi-bin/modules/BSE/Formatter.pm
site/cgi-bin/modules/BSE/TB/SiteUserGroup.pm
site/cgi-bin/modules/BSE/TB/SiteUserGroups.pm
site/cgi-bin/modules/BSE/Util/DynamicTags.pm
site/cgi-bin/modules/BSE/Util/Tags.pm
site/cgi-bin/modules/Generate.pm
site/cgi-bin/modules/Generate/Article.pm
site/docs/bse.pod
site/templates/admin/filelist.tmpl

index e75a4162822fd02f1fc7ed17ec3e5b3bb7da6347..e9624455721916a14ecce1a53e3667eec6ebe1df 100644 (file)
@@ -395,6 +395,9 @@ create table article_files (
   -- more descriptive stuff
   notes text not null default '',
 
+  -- identifier for the file for use with filelink[]
+  name varchar(80) not null default '',
+
   primary key (id)
 );
 
index c2dcc07b22fdbf3a181a8cf3286d2aee0b9a1d25..6000e3a74e37967bfe4d737fc84f8c7a445da437 100644 (file)
@@ -9,7 +9,7 @@ use Carp 'confess';
 sub columns {
   return qw/id articleId displayName filename sizeInBytes description 
             contentType displayOrder forSale download whenUploaded
-            requireUser notes/;
+            requireUser notes name/;
 }
 
 sub remove {
index e8fa58c1161cd3140aecc485f13a5d8ae55c8c09..164d26d9a59d790d1ef1b273e67f643522b2162a 100644 (file)
@@ -119,9 +119,9 @@ EOS
    'select * from other_parents where childId = ? or parentId = ?',
 
    addArticleFile =>
-   'insert into article_files values (null,?,?,?,?,?,?,?,?,?,?,?,?)',
+   'insert into article_files values (null,?,?,?,?,?,?,?,?,?,?,?,?,?)',
    replaceArticleFile =>
-   'replace article_files values (?,?,?,?,?,?,?,?,?,?,?,?,?)',
+   'replace article_files values (?,?,?,?,?,?,?,?,?,?,?,?,?,?)',
    deleteArticleFile => 'delete from article_files where id = ?',
    getArticleFileByArticleId =>
    'select * from article_files where articleId = ? order by displayOrder desc',
@@ -470,6 +470,7 @@ SQL
    replaceSiteUserGroup => 'replace bse_siteuser_groups values(?,?)',
    deleteSiteUserGroup => 'delete from bse_siteuser_groups where id = ?',
    getSiteUserGroupByPkey => 'select * from bse_siteuser_groups where id = ?',
+   getSiteUserGroupByName => 'select * from bse_siteuser_groups where name = ?',
    siteuserGroupMemberIds => <<SQL,
 select siteuser_id as "id" 
 from bse_siteuser_membership 
index 6bfaa72a498b7ec4a10b1f014df66b9a9d0719de..203ddac5b6e218350474fa4372d1d30879c0267e 100644 (file)
@@ -2593,6 +2593,8 @@ sub fileadd {
       $file{$col} = $cgi->param($col);
     }
   }
+
+  my %errors;
   
   $file{forSale} = 0 + exists $file{forSale};
   $file{articleId} = $article->{id};
@@ -2604,14 +2606,10 @@ sub fileadd {
   # build a filename
   my $file = $cgi->param('file');
   unless ($file) {
-    return $self->edit_form($req, $article, $articles,
-                          "Enter or select the name of a file on your machine",
-                         { file => 'Please enter a filename' });
+    $errors{file} = 'Please enter a filename';
   }
-  if (-z $file) {
-    return $self->edit_form($req, $article, $articles,
-                          "File is empty",
-                          { file => 'File is empty' });
+  if ($file && -z $file) {
+    $errors{file} = 'File is empty';
   }
 
   unless ($file{contentType}) {
@@ -2629,6 +2627,20 @@ sub fileadd {
       $file{contentType} = $type;
     }
   }
+
+  defined $file{name} or $file{name} = '';
+  if (length $file{name} && $file{name} !~/^\w+$/) {
+    $errors{name} = "Identifier must be a single word";
+  }
+  if (!$errors{name} && length $file{name}) {
+    my @files = $article->files;
+    if (grep lc $_->{name} eq lc $file{name}, @files) {
+      $errors{name} = "Duplicate file identifier $file{name}";
+    }
+  }
+
+  keys %errors
+    and return $self->edit_form($req, $article, $articles, undef, \%errors);
   
   my $basename = '';
   my $workfile = $file;
@@ -2746,22 +2758,35 @@ sub filesave {
   my @files = $article->files;
 
   my $cgi = $req->cgi;
+  my %seen_name;
   for my $file (@files) {
     if (defined $cgi->param("description_$file->{id}")) {
       $file->{description} = $cgi->param("description_$file->{id}");
-      if (my $type = $cgi->param("contentType_$file->{id}")) {
+      if (defined(my $type = $cgi->param("contentType_$file->{id}"))) {
        $file->{contentType} = $type;
       }
-      if (my $notes = $cgi->param("notes_$file->{id}")) {
+      if (defined(my $notes = $cgi->param("notes_$file->{id}"))) {
        $file->{notes} = $notes;
       }
+      my $name = $cgi->param("name_$file->{id}");
+      defined $name or $name = $file->{name};
+      if (length $name && $name !~ /^\w+$/) {
+       return $self->edit_form($req, $article, $articles,
+                               "Invalid file identifier $name");
+      }
+      if (length $name && $seen_name{lc $name}++) {
+       return $self->edit_form($req, $article, $articles,
+                               "Duplicate file identifier $name");
+      }
+      $file->{name} = $name;
       $file->{download} = 0 + defined $cgi->param("download_$file->{id}");
       $file->{forSale} = 0 + defined $cgi->param("forSale_$file->{id}");
       $file->{requireUser} = 0 + defined $cgi->param("requireUser_$file->{id}");
-      $file->save;
     }
   }
-
+  for my $file (@files) {
+    $file->save;
+  }
   use Util 'generate_article';
   generate_article($articles, $article) if $Constants::AUTO_GENERATE;
 
index 906c0b44936cb64667d196048bdd19a3be1e140f..c1cd298ed6ca9987131f0e3b73e3f4d105c2a6a0 100644 (file)
@@ -8,20 +8,30 @@ use base 'DevHelp::Formatter';
 my $pop_nameid = 'AAAAAA';
 
 sub new {
-  my ($class, $gen, $acts, $articles, $abs_urls, $rauto_images, $images, $templater) = @_;
+  my $class = shift;
 
-  my $self = $class->SUPER::new;
+  my (%opts) = 
+    ( 
+     images => [], 
+     files => [],
+     abs_urls => 0, 
+     acts => {}, 
+     @_
+    );
 
-  $self->{gen} = $gen;
-  $self->{acts} = $acts;
-  $self->{articles} = $articles;
-  $self->{abs_urls} = $abs_urls;
-  $self->{auto_images} = $rauto_images;
-  $self->{images} = $images;
-  $self->{templater} = $templater;
-  #$self->{level} = $level;
+  my $self = $class->SUPER::new;
 
-  my $cfg = $gen->{cfg};
+  $self->{gen} = $opts{gen};
+  $self->{acts} = $opts{acts};
+  $self->{articles} = $opts{articles};
+  $self->{abs_urls} = $opts{abs_urls};
+  my $dummy;
+  $self->{auto_images} = $opts{auto_images} || \$dummy;
+  $self->{images} = $opts{images};
+  $self->{files} = $opts{files};
+  $self->{templater} = $opts{templater};
+
+  my $cfg = $self->{gen}->{cfg};
   if ($cfg->entry('html', 'mbcs', 0)) {
     $self->{conservative_escape} = 1;
   }
@@ -275,6 +285,23 @@ sub popimage {
   return $self->_fix_spanned($link_start, $link_end, $inside);
 }
 
+sub filelink {
+  my ($self, $fileid, $text) = @_;
+
+  my ($file) = grep $_->{name} eq $fileid, @{$self->{files}}
+    or return "** unknown file $fileid **";
+
+  my $title = defined $text ? $text : $file->{displayName};
+  if ($file->{forSale}) {
+    return escape_html($title);
+  }
+  else {
+    my $url = "/cgi-bin/user.pl?download_file=1&file=$file->{id}";
+    return qq!<a href="! . escape_html($url) . qq!">! .
+      escape_html($title) . "</a>";
+  }
+}
+
 sub replace {
   my ($self, $rpart) = @_;
 
@@ -300,6 +327,10 @@ sub replace {
     and return 1;
   $$rpart =~ s#formlink\[(\w+)\]# $self->formlink($1, 'formlink', undef) #ige
     and return 1;
+  $$rpart =~ s#filelink\[\s*(\w+)\s*\|([^\]\[]+)\]# $self->filelink($1, $2) #ige
+      and return 1;
+  $$rpart =~ s#filelink\[\s*(\w+)\s*\]# $self->filelink($1) #ige
+      and return 1;
   $$rpart =~ s#popimage\[([^\[\]]+)\]# $self->popimage($1) #ige
     and return 1;
 
@@ -362,6 +393,15 @@ sub remove_popimage {
   }
 }
 
+sub remove_filelink {
+  my ($self, $fileid, $text) = @_;
+
+  my ($file) = grep $_->{name} eq $fileid, @{$self->{files}}
+    or return "** unknown file $fileid **";
+
+  return defined $text ? $text : $file->{displayName};
+}
+
 sub remove {
   my ($self, $rpart) = @_;
 
@@ -380,6 +420,12 @@ sub remove {
     and return 1;
   $$rpart =~ s#popformlink\[(\w+)\]# $self->remove_formlink($1) #ige
     and return 1;
+
+  $$rpart =~ s#filelink\[\s*(\w+)\s*\|([^\]\[]+)\]# $self->remove_filelink($1, $2) #ige
+      and return 1;
+  $$rpart =~ s#filelink\[\s*(\w+)\s*\]# $self->remove_filelink($1) #ige
+      and return 1;
+
   $$rpart =~ s#formlink\[(\w+)\|([^\]\[]*)\]#$2#ig
     and return 1;
   $$rpart =~ s#formlink\[(\w+)\]# $self->remove_formlink($1) #ige
index b4fed504d1c7521121d3c6c3e899707be10867c6..e21484a9eb39bec40168f464137e19e19b8e73a6 100644 (file)
@@ -68,4 +68,15 @@ sub remove_member {
   BSE::DB->single->run(siteuserGroupDeleteMember => $self->{id}, $id);
 }
 
+sub contains_user {
+  my ($self, $user) = @_;
+
+  my $user_id = ref $user ? $user->{id} : $user;
+
+  my @membership = BSE::DB->single->query(siteuserMemberOfGroup => 
+                                         $user_id, $self->{id});
+
+  return scalar @membership;
+}
+
 1;
index c9c30c765764e9b8b4fc524847376063f5872e0d..7e8a75aba3538cafc375b2ec75d348cc8332dd36 100644 (file)
@@ -33,7 +33,42 @@ sub getQueryGroup {
   my $sql = $cfg->entry(SECT_QUERY_GROUP_PREFIX.$name, 'sql')
     or return;
 
-  return { id => $id, name => $name, sql=>$sql };
+  return bless { id => $id, name => "*$name", sql=>$sql }, 
+    "BSE::TB::SiteUserQueryGroup";
+}
+
+sub getByName {
+  my ($class, $cfg, $name) = @_;
+
+  if ($name =~ /^\*/) {
+    $name = substr($name, 1);
+
+    my %q_groups = map lc, reverse $cfg->entries(SECT_QUERY_GROUPS);
+    if ($q_groups{lc $name}) {
+      return $class->getQueryGroup($cfg, -$q_groups{lc $name})
+       or return;
+    }
+    else {
+      return;
+    }
+  }
+  else {
+    return $class->getBy(name => $name);
+  }
+}
+
+package BSE::TB::SiteUserQueryGroup;
+
+sub contains_user {
+  my ($self, $user) = @_;
+
+  my $id = ref $user ? $user->{id} : $user;
+
+  my $rows = BSE::DB->single->dbh->selectall_arrayref($self->{sql}, { MaxRows=>1 }, $id);
+  $rows && @$rows
+    and return 1;
+  
+  return 0;
 }
 
 1;
index c4ef43ddfabab1f7b0753acadca3d4e0060a0cb7..3736624fe41186be1c0a112d86129e81d5da95a5 100644 (file)
@@ -25,6 +25,7 @@ sub tags {
      $self->dyn_iterator('dynchildren_of', 'dynofchild'),
      url => [ tag_url => $self ],
      ifAncestor => 0,
+     ifUserMemberOf => [ tag_ifUserMemberOf => $self ],
     );
 }
 
@@ -76,6 +77,29 @@ sub tag_ifUserCanSee {
   $req->siteuser_has_access($article);
 }
 
+sub tag_ifUserMemberOf {
+  my ($self, $args, $acts, $func, $templater) = @_;
+
+  my $req = $self->{req};
+
+  my $user = $req->siteuser
+    or return 0; # no user, no group
+
+  my ($name) = DevHelp::Tags->get_parms($args, $acts, $templater);
+
+  $name
+    or return 0; # no group name
+  
+  require BSE::TB::SiteUserGroups;
+  my $group = BSE::TB::SiteUserGroups->getByName($req->cfg, $name);
+  unless ($group) {
+    print STDERR "Unknown group name '$name' in ifUserMemberOf\n";
+    return 0;
+  }
+
+  return $group->contains_user($user);
+}
+
 sub tag_url {
   my ($self, $name, $acts, $func, $templater) = @_;
 
index 09977214ae61207b3de9a45066ee7e33d10392db..b6adfeab254d904d90db05e0171d373b975d3f7b 100644 (file)
@@ -131,8 +131,10 @@ sub static {
        $value = decode_entities($value);
        require Generate;
        my $gen = Generate->new(cfg=>$cfg);
-       return $gen->format_body($acts, 'Articles', $value, 'tr',
-                               1, 0, $templater);
+       return $gen->format_body(acts => $acts, 
+                               articles => 'Articles', 
+                               text => $value, 
+                               templater => $templater);
      },
      nobodytext => [\&tag_nobodytext, $cfg ],
      ifEq =>
index 7065a4656b4a0ac0ddbff714651e4af93a31cd28..323667e065897e2f2914093a02d9d327e77075e5 100644 (file)
@@ -13,6 +13,11 @@ my $excerptSize = 300;
 
 sub new {
   my ($class, %opts) = @_;
+  unless ($opts{cfg}) {
+    require Carp;
+    Carp->import('confess');
+    confess("cfg missing on generator->new call");
+  }
   $opts{maxdepth} = $EMBED_MAX_DEPTH unless exists $opts{maxdepth};
   $opts{depth} = 0 unless $opts{depth};
   return bless \%opts, $class;
@@ -56,7 +61,8 @@ sub summarize {
 
   # the formatter now adds <p></p> around the text, but we don't
   # want that here
-  my $result = $self->format_body({}, $articles, $text, 'tr', 1, 0);
+  my $result = $self->format_body(articles => $articles, 
+                                 text => $text);
   $result =~ s!<p>|</p>!!g;
 
   return $result;
@@ -82,85 +88,6 @@ sub adjust_for_html {
   return $pos;
 }
 
-# sub _make_hr {
-#   my ($width, $height) = @_;
-#   my $tag = "<hr";
-#   $tag .= qq!width="$width"! if length $width;
-#   $tag .= qq!height="$height"! if length $height;
-#   $tag .= " />";
-#   return $tag;
-# }
-
-# # produces a table, possibly with options for the <table> and <tr> tags
-# sub _make_table {
-#   my ($options, $text) = @_;
-#   my $tag = "<table";
-#   my $cellend = '';
-#   my $cellstart = '';
-#   if ($options =~ /=/) {
-#     $tag .= " " . $options;
-#   }
-#   elsif ($options =~ /\S/) {
-#     $options =~ s/\s+$//;
-#     my ($width, $bg, $pad, $fontsz, $fontface) = split /\|/, $options;
-#     for ($width, $bg, $pad, $fontsz, $fontface) {
-#       $_ = '' unless defined;
-#     }
-#     $tag .= qq! width="$width"! if length $width;
-#     $tag .= qq! bgcolor="$bg"! if length $bg;
-#     $tag .= qq! cellpadding="$pad"! if length $pad;
-#     if (length $fontsz || length $fontface) {
-#       $cellstart = qq!<font!;
-#       $cellstart .= qq! size="$fontsz"! if length $fontsz;
-#       $cellstart .= qq! face="$fontface"! if length $fontface;
-#       $cellstart .= qq!>!;
-#       $cellend = "</font>";
-#     }
-#   }
-#   $tag .= ">";
-#   my @rows = split '\n', $text;
-#   my $maxwidth = 0;
-#   for my $row (@rows) {
-#     my ($opts, @cols) = split /\|/, $row;
-#     $tag .= "<tr";
-#     if ($opts =~ /=/) {
-#       $tag .= " ".$opts;
-#     }
-#     $tag .= "><td>$cellstart".join("$cellend</td><td>$cellstart", @cols)
-#       ."$cellend</td></tr>";
-#   }
-#   $tag .= "</table>";
-#   return $tag;
-# }
-
-# # make a UL
-# sub _format_bullets {
-#   my ($text) = @_;
-
-#   $text =~ s/^\s+|\s+$//g;
-#   my @points = split /(?:\r?\n)?\*\*\s*/, $text;
-#   shift @points if @points and $points[0] eq '';
-#   return '' unless @points;
-#   for my $point (@points) {
-#     $point =~ s!\n$!<br /><br />!;
-#   }
-#   return "<ul><li>".join("<li>", @points)."</ul>";
-# }
-
-# # make a OL
-# sub _format_ol {
-#   my ($text) = @_;
-#   $text =~ s/^\s+|\s+$//g;
-#   my @points = split /(?:\r?\n)?##\s*/, $text;
-#   shift @points if @points and $points[0] eq '';
-#   return '' unless @points;
-#   for my $point (@points) {
-#     #print STDERR  "point: ",unpack("H*", $point),"\n";
-#     $point =~ s!\n$!<br /><br />!;
-#   }
-#   return "<ol><li>".join("<li>", @points)."</ol>";
-# }
-
 # raw html - this has some limitations
 # the input text has already been escaped, so we need to unescape it
 # too bad if you want [] in your html (but you can use entities)
@@ -279,21 +206,45 @@ sub _make_img {
 
 # replace markup, insert img tags
 sub format_body {
-  my ($self, $acts, $articles, $body, $imagePos, $abs_urls, 
-      $auto_images, $templater, @images)  = @_;
+  my $self = shift;
+  my (%opts) =
+    (
+     abs_urls => 0, 
+     imagepos => 'tr', 
+     auto_images => 1,
+     images => [], 
+     files => [],
+     acts => {}, 
+     @_
+    );
+
+  my $acts = $opts{acts};
+  my $articles = $opts{articles};
+  my $body = $opts{text};
+  my $imagePos = $opts{imagepos};
+  my $abs_urls = $opts{abs_urls};
+  my $auto_images = $opts{auto_images};
+  my $templater = $opts{templater};
+  my $images = $opts{images};
+  my $files = $opts{files};
 
   return substr($body, 6) if $body =~ /^<html>/i;
 
   require BSE::Formatter;
 
-  my $formatter = BSE::Formatter->new($self, $acts, $articles,
-                                     $abs_urls, \$auto_images,
-                                     \@images, $templater);
+  my $formatter = BSE::Formatter->new(gen => $self, 
+                                     acts => $acts, 
+                                     articles => $articles,
+                                     abs_urls => $abs_urls, 
+                                     auto_images => \$auto_images,
+                                     images => $images, 
+                                     files => $files,
+                                     templater => $templater);
 
   $body = $formatter->format($body);
 
   # we don't format named images
-  @images = grep $_->{name} eq '', @images;
+  my @images = grep $_->{name} eq '', @$images;
   if ($auto_images && @images) {
     # the first image simply goes where we're told to put it
     # the imagePos is [tb][rl] (top|bottom)(right|left)
@@ -748,8 +699,9 @@ sub remove_block {
 
   require BSE::Formatter;
 
-  my $formatter = BSE::Formatter->new($self, $acts, $articles,
-                                     1, \0, []);
+  my $formatter = BSE::Formatter->new(gen => $self, 
+                                     acts => $acts, 
+                                     article => $articles);
 
   $$body = $formatter->remove_format($$body);
 }
index 8517ff5bf04dd3d14530e3a33e8566bc65f57e15..2048f43f4b258f11e2d01dacdf615c5d3d74c25a 100644 (file)
@@ -331,9 +331,15 @@ sub baseActs {
      # transform the article or response body (entities, images)
      body=>sub {
        my ($args, $acts, $funcname, $templater) = @_;
-       return $self->format_body($acts, $articles, $article->{body},
-                                $article->{imagePos}, $abs_urls,
-                                !$had_image_tags, $templater, @images);
+       return $self->format_body(acts => $acts, 
+                                article => $articles, 
+                                text => $article->{body},
+                                imagepos => $article->{imagePos}, 
+                                abs_urls => $abs_urls,
+                                auto_images => !$had_image_tags, 
+                                templater => $templater, 
+                                images => \@images,
+                                files => \@files);
      },
 
      # used to display a navigation path of parent sections
index 041fddec955b41a876ea2b7dd6f26f16d7b3d7e9..44da093b9f5f648b68bd91e2c9e36644aac8bd86 100644 (file)
@@ -31,6 +31,28 @@ page other than the default done page.
 
 fix the default template name for the embedded report tags.
 
+=item *
+
+added dynamic tag ifUserMemberOf which checks if the current user is a
+member of the named group.  Supply a leading * to name query groups.
+
+=item *
+
+added a name or identifier field to the article_files table, and
+administration tools to manage it.
+
+=item *
+
+re-worked the formatting API for easier extension
+
+=item *
+
+added filelink[] body text tag
+
+=item *
+
+removed some old commented code from Generate.pm
+
 =back
 
 =head2 0.15_36
index dbfe346e17ea1f6ed5f3b870b7ebdd84945498a8..362a58a7a50172d324491b242357e2f3583c723e 100644 (file)
               (blank for guess) </td>
             <td nowrap="nowrap" bgcolor="#FFFFFF"><:help file content_type:> <:error_img contentType:></td>
           </tr>
+          <tr> 
+            <th bgcolor="#FFFFFF" align="left">Identifier:</th>
+            <td bgcolor="#FFFFFF"> 
+              <input type="text" name="name" value="<:old name:>" /> </td>
+            <td nowrap="nowrap" bgcolor="#FFFFFF"><:help file name:> <:error_img name:></td>
+          </tr>
           <tr> 
             <th bgcolor="#FFFFFF" align="left">Treat as download:</th>
             <td bgcolor="#FFFFFF"> 
           </tr>
           <: iterator begin files :> 
           <tr bgcolor="#FFFFFF"> 
-            <td nowrap="nowrap"> <:file displayName:></td>
+            <td nowrap="nowrap" rowspan="2"> <:file displayName:></td>
             <td valign="top"> 
               <:ifUserCan edit_files_save:article:><input name="description_<:file id:>" type="text" value="<: file description :>" size="35" />
               <:or:><: file description :><:eif:>
                <:ifUserCan edit_files_save:article:><input name="contentType_<:file id:>" type="text" value="<: file contentType :>" size="20" />
                <:or:><: file contentType :><:eif:>
             </td>
-            <td valign="top" rowspan="2">
+            <td valign="top" rowspan="3">
               <:ifUserCan edit_files_save:article:><textarea name="notes_<:file id:>" cols="40" rows="5"><:file notes:></textarea><:or:><:replace [file notes] "
 " "<br />" :><:eif:>
             </td>
           </tr>
+          <tr>
+            <td valign="top" colspan="2" nowrap="nowrap" bgcolor="#FFFFFF"> 
+               Identfier: <:ifUserCan edit_files_save:article:><input name="name_<:file id:>" type="text" value="<: file name :>" size="20" />
+               <:or:><: file name :><:eif:>
+            </td>
+          </tr>
           <tr bgcolor="#FFFFFF"> 
             <td colspan="3"> 
               <table width="100%" border="0" cellspacing="0" cellpadding="0">
           </tr>
           <: iterator separator files :> 
           <tr bgcolor="#FFFFFF"> 
-            <td colspan="3">&nbsp;</td>
+            <td colspan="4">&nbsp;</td>
           </tr>
           <: iterator end files :> 
 <:ifUserCan edit_files_save:article:>