0.14_31 commit r0_14_31
authorTony Cook <tony@develop-help.com>
Fri, 10 Sep 2004 05:40:41 +0000 (05:40 +0000)
committertony <tony@45cb6cf1-00bc-42d2-bb5a-07f51df49f94>
Fri, 10 Sep 2004 05:40:41 +0000 (05:40 +0000)
23 files changed:
MANIFEST
Makefile
site/cgi-bin/bse.cfg
site/cgi-bin/modules/BSE/AdminSiteUsers.pm
site/cgi-bin/modules/BSE/Cfg.pm
site/cgi-bin/modules/BSE/DB/Mysql.pm
site/cgi-bin/modules/BSE/Formatter.pm
site/cgi-bin/modules/BSE/Request.pm
site/cgi-bin/modules/BSE/Shop/Util.pm
site/cgi-bin/modules/BSE/UI/Affiliate.pm
site/cgi-bin/modules/BSE/UI/SubAdmin.pm
site/cgi-bin/modules/BSE/UserReg.pm
site/cgi-bin/modules/DevHelp/Validate.pm
site/cgi-bin/modules/SiteUser.pm
site/cgi-bin/modules/Squirrel/Row.pm
site/cgi-bin/shop.pl
site/docs/bse.pod
site/docs/config.pod
site/templates/admin/subscr/add.tmpl
site/templates/admin/subscr/detail.tmpl
site/templates/admin/subscr/detail_delete.tmpl [new file with mode: 0644]
site/templates/admin/subscr/edit.tmpl
site/templates/admin/subscr/list.tmpl

index 6840c21..62ff768 100644 (file)
--- a/MANIFEST
+++ b/MANIFEST
@@ -279,6 +279,7 @@ site/templates/admin/subs/send_form.tmpl
 site/templates/admin/subs/start_send.tmpl
 site/templates/admin/subscr/add.tmpl
 site/templates/admin/subscr/detail.tmpl
+site/templates/admin/subscr/detail_delete.tmpl
 site/templates/admin/subscr/edit.tmpl
 site/templates/admin/subscr/list.tmpl
 site/templates/admin/userlist.tmpl
index 7836792..5f60ce4 100755 (executable)
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,4 @@
-VERSION=0.14_30
+VERSION=0.14_31
 DISTNAME=bse-$(VERSION)
 DISTBUILD=$(DISTNAME)
 DISTTAR=../$(DISTNAME).tar
index e7cca18..dd84b41 100644 (file)
@@ -317,3 +317,7 @@ display_name2=Last Name
 display_address=Street
 display_postcode=Post code
 display_telephone=Phone
+
+[includes]
+00bsecfg_d=bsecfg_d/
+50local=bse-local.cfg
\ No newline at end of file
index 58dc8eb..2547c51 100644 (file)
@@ -19,7 +19,7 @@ my %actions =
    add=>1,
   );
 
-my @donttouch = qw(id userId password email confirmed confirmSecret waitingForConfirmation flags affiliate_name); # flags is saved separately
+my @donttouch = qw(id userId password email confirmed confirmSecret waitingForConfirmation flags affiliate_name previousLogon); # flags is saved separately
 my %donttouch = map { $_, $_ } @donttouch;
 
 sub dispatch {
index fafb6d2..1265b89 100644 (file)
@@ -268,7 +268,62 @@ sub _load_cfg {
   }
   close CFG;
 
-  my $self = bless { config=>\%sections, when=>time }, $class;
+  # go through the includes
+  my @raw_includes;
+  if ($sections{includes}) {
+    my $hash = $sections{includes}{values};
+    @raw_includes = @$hash{sort keys %$hash};
+  }
+  my @includes;
+  my $path = $file;
+  $path =~ s![^\\/:]+$!!;
+  for my $include (@raw_includes) {
+    my $full = $include =~ m!^(?:\w:)[/\\]! ? $include : "$path$include";
+    if (-e $full) {
+      if (-d $full) {
+       # scan the directory
+       if (opendir CFGDIR, $full) {
+         push @includes, map "$full$_", sort grep /\.cfg$/, readdir CFGDIR;
+         closedir CFGDIR;
+       }
+       else {
+         # it's a directory but we can't read it - probably an error
+         print STDERR "Cannot scan config directory $full: $!\n";
+       }
+      }
+      else {
+       push @includes, $full;
+      }
+    }
+  }
+
+  # scan each of the include files
+  for my $include (@includes) {
+    if (open CFG, "< $include") {
+      undef $section;
+      while (<CFG>) {
+       chomp;
+       next if /^\s*$/ || /^\s*[#;]/;
+       if (/^\[([^]]+)\]\s*$/) {
+         $section = lc $1;
+       }
+       elsif (/^\s*([^=\s]+)\s*=\s*(.*)$/) {
+         $section or next;
+         $sections{$section}{values}{lc $1} = $2;
+         $sections{$section}{case}{$1} = $2;
+       }
+      }
+      close CFG;
+    }
+    else {
+      # it wouldn't be in the list unless we know it exists
+      print STDERR "Cannot open config include $include: $!\n";
+    }
+  }
+
+  my $self = bless { config=>\%sections, 
+                    includes=>\@includes, 
+                    when=>time }, $class;
   $cache{$file} = $self;
 
   return $self;
index 3162175..543dc19 100644 (file)
@@ -323,7 +323,9 @@ SQL
 insert bse_user_subscribed values (?,?,?,?,?)
 SQL
    subscriptionUserBought => <<SQL,
-select od.orderDate, oi.subscription_period, oi.max_lapsed, 
+select od.orderDate,
+  oi.subscription_period * oi.units as "subscription_period",
+  oi.max_lapsed, 
   od.id as "order_id", oi.id as "item_id", oi.productId as "product_id"
   from orders od, order_item oi
   where oi.subscription_id = ? and od.id = oi.orderId and od.siteuser_id = ?
index 5fef645..8764ce7 100644 (file)
@@ -197,11 +197,11 @@ sub remove {
 
   $$rpart =~ s#gimage\[([^\]\[]+)\]##ig
     and return 1;
-  $$rpart =~ s#popdoclink\[(\w+)\|([^\]\[]+)\]#$2#ig
+  $$rpart =~ s#popdoclink\[(\w+)\|([^\]\[]*)\]#$2#ig
     and return 1;
   $$rpart =~ s#popdoclink\[(\w+)\]# $self->remove_doclink($1) #ige
     and return 1;
-  $$rpart =~ s#doclink\[(\w+)\|([^\]\[]+)\]#$2#ig
+  $$rpart =~ s#doclink\[(\w+)\|([^\]\[]*)\]#$2#ig
     and return 1;
   $$rpart =~ s#doclink\[(\w+)\]# $self->remove_doclink($1) #ige
     and return 1;
index 32c3252..ab026b7 100644 (file)
@@ -154,7 +154,8 @@ sub message {
     $msg = join "<br />", map escape_html($_), @lines;
   }
   if (!$msg && $req->cgi->param('m')) {
-    $msg = escape_html($req->cgi->param('m'));
+    $msg = join(' ', $req->cgi->param('m'));
+    $msg = escape_html($msg);
   }
 
   return $msg;
index 027670d..5b4bc8d 100644 (file)
@@ -310,12 +310,12 @@ sub need_logon {
 
       $sub = $prod->subscription;
       if ($sub && $prod->is_renew_sub_only) {
-       unless ($user->is_subscribed_grace($sub)) {
+       unless ($user->subscribed_to_grace($sub)) {
          return ("you must be subscribed to $sub->{title} to use this renew only product", "sub/renewsubonly");
        }
       }
       if ($sub && $prod->is_start_sub_only) {
-       if ($user->is_subscribed_grace($sub)) {
+       if ($user->subscribed_to_grace($sub)) {
          return ("you must not be subscribed to $sub->{title} already to use this new subscription only product", "sub/newsubonly");
        }
       }
index 9877356..4a7a657 100644 (file)
@@ -223,13 +223,16 @@ sub req_show {
   }
   $user
     or return $class->req_none($req, "Unknown user");
-#  require BSE::TB::Subscriptions;
-#   my $subid = $cfg->entry('affiliate', 'subscription_required');
-#   if ($subid) {
-#     my $sub = BSE::TB::Subscriptions->getByPkey($subid)
-#       || BSE::TB::Subscriptions->getBy(text_id => $subid)
-#      or return $class->req_none($req, "Configuration error: Unknown subscription id");
-#   }
+  require BSE::TB::Subscriptions;
+  my $subid = $cfg->entry('affiliate', 'subscription_required');
+  if ($subid) {
+    my $sub = BSE::TB::Subscriptions->getByPkey($subid)
+      || BSE::TB::Subscriptions->getBy(text_id => $subid)
+       or return $class->req_none($req, "Configuration error: Unknown subscription id");
+    unless ($user->subscribed_to($sub)) {
+      return $class->req_none($req, "User not subscribed to the required subscription");
+    }
+  }
 
   my %acts;
   %acts =
index 272ed5c..0fde2ac 100644 (file)
@@ -276,7 +276,7 @@ sub _list_refresh {
   unless ($r) {
     $r = "/cgi-bin/admin/subadmin.pl";
   }
-  if ($msg) {
+  if ($msg && $r !~ /[&?]m=/) {
     my $sep = $r =~ /\?/ ? '&' : '?';
 
     $r .= $sep . "m=" . escape_uri($msg);
index 2b797dd..2872c25 100644 (file)
@@ -18,7 +18,7 @@ use BSE::WebUtil qw/refresh_to/;
 use constant MAX_UNACKED_CONF_MSGS => 3;
 use constant MIN_UNACKED_CONF_GAP => 2 * 24 * 60 * 60;
 
-my @donttouch = qw(id userId password email confirmed confirmSecret waitingForConfirmation disabled flags affiliate_name);
+my @donttouch = qw(id userId password email confirmed confirmSecret waitingForConfirmation disabled flags affiliate_name previousLogon);
 my %donttouch = map { $_, $_ } @donttouch;
 
 sub user_tags {
index 26ca2f0..dfa34cb 100644 (file)
@@ -115,6 +115,11 @@ my %built_ins =
    {
     integer => '1-', # 1 or higher
    },
+   dh_one_line => 
+   {
+    nomatch => qr/[\x0D\x0A]/,
+    error => '$n may only contain a single line',
+   }
   );
 
 sub dh_validate {
@@ -524,6 +529,11 @@ If this is non-zero the field is required.
 
 If present, this is used as a regular expression the field must match.
 
+=item nomatch
+
+If present, this is used as a regular expression the field must not
+match.
+
 =item error
 
 Message returned as the error if the field fails validation.
@@ -541,6 +551,104 @@ If set to 1, simply validates the value as a date.
 
 =back
 
+=head1 BUILT-IN RULES
+
+=over
+
+=item email
+
+=item phone
+
+=item postcode
+
+=item url
+
+Matches any valid url, including mailto:, ftp:, etc.  No checking of
+the scheme is done.
+
+=item weburl
+
+Matches any valid http or https url.
+
+=item newbieweburl
+
+Matches web URLs with or without "http://" or "https://".
+
+=item confirm
+
+Treats the given field as a confirmation field for a password field
+"password".
+
+=item newconfirm
+
+Treats the given field as a confirmation field for an optional
+password field "password".
+
+=item required
+
+The field is required.  This should be used where the logic of the
+code requires a field, since it cannot be overridden by the config
+file.
+
+=item abn
+
+Valid Australian Business Number
+
+=item creditcardnumber
+
+Valid credit card number (no checksum checks are performed).
+
+=item creditcardexpiry
+
+Treats the field as the month part of a credit card expiry date, with
+the year field being the field with the same name, but "month"
+replaced with "year".  The date of expiry is required to be in the
+future.
+
+=item miaa
+
+Valid MIAA membership number.
+
+=item decimal
+
+Valid simple decimal number
+
+=item money
+
+Valid decimal number with 0 or 2 digits after the decimal point.
+
+=item date
+
+A valid date.  Currently dates are limited to Australian format
+dd/mm/yy or dd/mm/yyyy format dates.
+
+=item birthdate
+
+A valid date in the past.
+
+=item adultbirthdate
+
+A valid date at least 10 years in the past, at most 100 years in the
+past.
+
+=item futuredate
+
+A valid date in the future.
+
+=item natural
+
+An integer greater or equal to zero.
+
+=item positiveint
+
+A positive integer.
+
+=item dh_one_line
+
+The field may not contain line breaks
+
+=back
+
 =head1 AUTHOR
 
 Tony Cook <tony@develop-help.com>
index 1ba3cb3..17f53b3 100644 (file)
@@ -24,6 +24,87 @@ sub columns {
             affiliate_name/;
 }
 
+sub valid_fields {
+  my ($class, $cfg, $admin) = @_;
+
+  my %fields =
+    (
+     email => { rules=>'email;required', description=>'Email Address',
+               maxlen => 255},
+     name1 => { description=>'First Name', rules=>"dh_one_line", maxlen=>127 },
+     name2 => { description=>'Surname', rules=>"dh_one_line", maxlen=>127 },
+     address => { description => 'Address', rules=>"dh_one_line", maxlen=>127 },
+     city => { description=>'City/Suburb', rules=>"dh_one_line", maxlen=>127 },
+     state => { description => 'State', rules=>"dh_one_line", maxlen=>40 },
+     postcode => { rules=>'postcode', description=>'Post Code', maxlen=>40 },
+     telephone => { rules=>'phone', description=>'Telephone', maxlen=>80 },
+     facsimile => { rules=>'phone', description=>'Facsimile', maxlen=>80 },
+     country => { description=>'Country', rules=>"dh_one_line", maxlen=>127 },
+     title => { description=>'Title', rules=>"dh_one_line", maxlen=>127  },
+     organization => { description=>'Organization', rules=>"dh_one_line", 
+                      maxlen=>127  },
+     textOnlyEmail => { description => "Text Only Email", type=>"boolean" },
+     referral => { description=>'Referral', rules=>"natural"  },
+     otherReferral => { description=>'Other Referral', rules=>"dh_one_line",
+                     maxlen=>127},
+     prompt => { description=>'Prompt', rules=>"natural" },
+     otherPrompt => { description => 'Other Prompt', rules=>"dh_one_line",
+                   maxlen=>127 },
+     profession => { description => 'Profession', rules=>"natural" },
+     otherProfession => { description=>'Other Profession',
+                         rules=>"dh_one_line", maxlen=>127 },
+     billFirstName => { description=>"Billing First Name",
+                       rules=>"dh_one_line", maxlen=>127 },
+     billLastName => { descriptin=>"Billing Last Name", rules=>"dh_one_line" },
+     billStreet => { description => "Billing Street Address",
+                    rules=>"dh_one_line", maxlen=>127 },
+     billSuburb => { description => "Billing Suburb", rules=>"dh_one_line", 
+                    maxlen=>127 },
+     billState => { description => "Billing State", rules=>"dh_one_line", 
+                   maxlen=>40 },
+     billPostCode => { description => "Billing Post Code", rules=>"postcode", 
+                      maxlen=>40 },
+     billCountry => { description => "Billing Country", rules=>"dh_one_line", 
+                     maxlen=>127 },
+     instructions => { description => "Delivery Instructions" },
+     billTelephone => { description => "Billing Phone", rules=>"phone", 
+                       maxlen=>80 },
+     billFacsimile => { description => "Billing Facsimie", rules=>"phone", 
+                       maxlen=>80 },
+     billEmail => { description => "Billing Email", rules=>"email", 
+                   maxlen=>255 },
+     customText1 => { description => "Custom Text 1" },
+     customText2 => { description => "Custom Text 2" },
+     customText3 => { description => "Custom Text 3" },
+     customStr1 => { description => "Custom String 1", rules=>"dh_one_line",
+                    maxlen=>255 },
+     customStr2 => { description => "Custom String 2", rules=>"dh_one_line",
+                    maxlen=>255 },
+     customStr3 => { description => "Custom String 3", rules=>"dh_one_line",
+                    maxlen=>255 },
+    );
+
+  if ($admin) {
+    $fields{adminNotes} =
+      { description => "Administrator Notes" };
+    $fields{disabled} =
+      { description => "User Disabled", type=>"boolean" };
+  }
+
+  for my $field_name (keys %fields) {
+    $fields{$field_name}{required} ||= $cfg->entry("site users", "require_$field_name", 0);
+    if (my $desc = $cfg->entry("site users", "display_$field_name")) {
+      $fields{$field_name}{description} = $desc;
+    }
+  }
+
+  return %fields;
+}
+
+sub valid_rules {
+  return;
+}
+
 sub removeSubscriptions {
   my ($self) = @_;
 
@@ -181,7 +262,7 @@ sub subscribed_to {
     or return;
 
   my $today = now_sqldate;
-  my $end_date = sql_normal_date($entry->{end});
+  my $end_date = sql_normal_date($entry->{ends_at});
   return $today le $end_date;
 }
 
index b611f0c..47ba63f 100644 (file)
@@ -52,7 +52,6 @@ sub new {
        or confess "Could not add $class(@data{1..$#save_cols})";
     }
     else {
-      print STDERR "add$class\n";
       my $sth = $dh->stmt("add$class")
        or confess "No add$class member in DatabaseHandle";
       my $ret = $sth->execute(@values[1..$#values]);
index 9464290..cf3d219 100755 (executable)
@@ -657,7 +657,13 @@ sub purchase {
        return 0;
      },
      item=> sub { CGI::escapeHTML($items[$item_index]{$_[0]}); },
-     product => sub { CGI::escapeHTML($products[$item_index]{$_[0]}) },
+     product => 
+     sub { 
+       my $value = $products[$item_index]{$_[0]};
+       defined $value or $value = '';
+
+       escape_html($value);
+     },
      extended =>
      sub { 
        my $what = $_[0] || 'retailPrice';
index cddad88..29d4f50 100644 (file)
@@ -10,6 +10,83 @@ Maybe I'll add some other bits here.
 
 =head1 CHANGES
 
+=head2 0.14_31
+
+=over
+
+=item *
+
+you can now configure a required subscription for affiliates, see the
+documentation for C<subscription_required> in L<config>.
+
+=item *
+
+the product tag on the checkoutfinal page now accounts for database
+NULL values
+
+=item *
+
+the subscription detail template didn't display the usage field
+correctly
+
+=item *
+
+the delete subscription link was incorrect
+
+=item *
+
+the delete subscription confirmation template wasn't supplied
+
+=item *
+
+the subscription edit/add templates were missing a closing </tr>
+
+=item *
+
+removed debug code from Squirrel::Row that was sending
+C<add>I<classname> to the error log.
+
+=item *
+
+the test for whether a user was subscribed was incorrect.
+
+=item *
+
+the number of units of a product bought for a subscription product was
+ignored, now 2 units of a 2 month sub product will act as 4 months.
+
+=item *
+
+it is now possible to include other config files from bse.cfg, see
+[includes] in C<config.pod> for more information.
+
+=item *
+
+the doclink[] and popdoclink[] tags would not be removed from the
+search page excerpts if the second parameter was present but empty.
+
+=item *
+
+more recent code using the m parameter could crash if the script was
+passed two m parameters.
+
+=item *
+
+a method rename during development meant that some code was calling a
+method that didn't exist, causing a 500 error.
+
+=item *
+
+if you supply an 'r' parameter that defines an 'm' parameter to
+subadmin targets the code will no longer add it's own 'm' parameter.
+
+=item *
+
+DevHelp::Validate now includes some documentation of its built-in
+rules.
+
+=back
+
 =head2 0.14_30
 
 The basic processing for subscriptions is now done.
index 084153a..1625f1e 100644 (file)
@@ -949,6 +949,11 @@ L<affiliate.pl> then this is used as the default refresh.
 
 Default: the site base url.
 
+=item subscription_required
+
+This is either the numeric or text of a subscription for which the
+affiliate must have an active subscription.
+
 =back
 
 =head2 [BSE Siteuser Images]
@@ -1007,6 +1012,46 @@ Error message displayed if the image uses too much storage.
 
 =back
 
+=head2 [includes]
+
+Each value is used as the relative or absolute name of a file or
+directory to load more configuration data from.
+
+The keywords must remain unique.
+
+Only the [includes] section from bse.cfg itself is used to locate more
+configuration data.
+
+If the value references a directory, all files with an extension of
+C<.cfg> are read for configuration data.
+
+The order the files are read (which later files overriding older
+files) is:
+
+=over
+
+=item 1.
+
+bse.cfg is read
+
+=item 2.
+
+the entries in [includes] are sorted alphabetically (or rather
+asciily), so an entry with key "A" is read before one with key "B",
+one with key "01" is read before "02", but key "10" would be read
+I<before> key "2".
+
+=item *
+
+if an entry is a file then that is read and the values merged.
+
+=item *
+
+if an entry is a directory, then that is scanned and the files found
+read alphabetically as above.
+
+=back
+
 =head1 AUTHOR
 
 Tony Cook <tony@develop-help.com>
index eb2be87..3b711ae 100644 (file)
@@ -33,5 +33,6 @@
 <tr>
   <td colspan="2"><input type="submit" name="a_add" value="Add Subscription" /></td>
   <td>&nbsp;</td>
+</tr>
 </table>
 </form>
\ No newline at end of file
index 4a2e158..af1283d 100644 (file)
@@ -58,9 +58,9 @@
   <td><:product subscription_period:></td>
   <td> 
     <:switch:>
-    <:case Eq [subscription subscription_usage] 1:>Start only
-    <:case Eq [subscription subscription_usage] 2:>Renew only
-    <:case Eq [subscription subscription_usage] 3:>Start or Renew
+    <:case Eq [product subscription_usage] 1:>Start only
+    <:case Eq [product subscription_usage] 2:>Renew only
+    <:case Eq [product subscription_usage] 3:>Start or Renew
     <:endswitch:>
   </td>
   <td>
diff --git a/site/templates/admin/subscr/detail_delete.tmpl b/site/templates/admin/subscr/detail_delete.tmpl
new file mode 100644 (file)
index 0000000..788d9ec
--- /dev/null
@@ -0,0 +1,37 @@
+<:wrap admin/xbase.tmpl title=>"Delete Subscription: [subscription text_id]":>
+<h1>Delete Subscription: <:subscription text_id:></h1>
+<p>
+| <a href="/cgi-bin/admin/menu.pl">Admin menu</a> |
+<:if UserCan bse_subs_list :><a href="<:script:>?a_list=1">List Subscriptions</a> |<:or UserCan:><:eif UserCan:>
+<:if UserCan bse_subs_edit :>
+<a href="<:script:>?a_edit=1&amp;subscription_id=<:subscription subscription_id:>">Edit Subscription</a> |<:or UserCan:><:eif UserCan:>
+<:if UserCan bse_subs_add :>
+<a href="<:script:>?a_addform=1">Add Subscription</a> |<:or UserCan:><:eif UserCan:>
+<:ifMessage:>
+<p><b><:message:></b></p>
+<:or:><:eif:> 
+
+<form action="<:script:>" method="POST">
+<input type="hidden" name="subscription_id" value="<:subscription subscription_id:>" />
+<table>
+<tr>
+  <th>Identifier:</th>
+  <td><:subscription text_id:></td>
+</tr>
+<tr>
+  <th>Title:</th>
+  <td><:subscription title:></td>
+</tr>
+<tr>
+  <th>Description:</th>
+  <td><:subscription description:></td>
+</tr>
+<tr>
+  <th>Max Lapsed:</th>
+  <td><:subscription max_lapsed:> Days</td>
+</tr>
+<tr>
+  <td colspan="2"><input type="submit" name="a_remove" value="Delete Subscription" /></td>
+</tr>
+</table>
+</form>
index f73b5b2..3ddea69 100644 (file)
@@ -34,5 +34,6 @@
 <tr>
   <td colspan="2"><input type="submit" name="a_save" value="Save Subscription" /></td>
   <td>&nbsp;</td>
+</tr>
 </table>
 </form>
\ No newline at end of file
index f5f23c9..7f2edd5 100644 (file)
@@ -39,12 +39,12 @@ href="/cgi-bin/admin/subs.pl">newsletters system.</a></p>
             <td valign="top"><:isubscription title:></td>
             <td valign="top"><:isubscription description:></td>
             <td valign="top"><:isubscription max_lapsed:></td>
-           <td><a href="<:script:>?a_detail=1&amp;subscription_id=<:isubscription subscription_id:>">Details</a> <:ifRemovable:><a href="<:script:>?a_detail=1&amp;_t=delete&amp;subscription_id=<:isubscription subscription_id:>">Delete</a><:or:><:eif:></td>
+           <td><a href="<:script:>?a_detail=1&amp;subscription_id=<:isubscription subscription_id:>">Details</a> <:ifRemovable:><a href="<:script:>?a_detail=1&amp;t=delete&amp;subscription_id=<:isubscription subscription_id:>">Delete</a><:or:><:eif:></td>
           </tr>
           <: iterator end subscriptions :> 
           <:or Subscriptions:> 
           <tr bgcolor="#FFFFFF"> 
-            <td colspan="4" align="center">Your system has no subscriptions.</td>
+            <td colspan="5" align="center">Your system has no subscriptions.</td>
           </tr>
           <:eif Subscriptions:> 
         </table>