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
-VERSION=0.14_30
+VERSION=0.14_31
DISTNAME=bse-$(VERSION)
DISTBUILD=$(DISTNAME)
DISTTAR=../$(DISTNAME).tar
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
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 {
}
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;
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 = ?
$$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;
$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;
$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");
}
}
}
$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 =
unless ($r) {
$r = "/cgi-bin/admin/subadmin.pl";
}
- if ($msg) {
+ if ($msg && $r !~ /[&?]m=/) {
my $sep = $r =~ /\?/ ? '&' : '?';
$r .= $sep . "m=" . escape_uri($msg);
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 {
{
integer => '1-', # 1 or higher
},
+ dh_one_line =>
+ {
+ nomatch => qr/[\x0D\x0A]/,
+ error => '$n may only contain a single line',
+ }
);
sub dh_validate {
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.
=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>
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) = @_;
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;
}
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]);
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';
=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.
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]
=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>
<tr>
<td colspan="2"><input type="submit" name="a_add" value="Add Subscription" /></td>
<td> </td>
+</tr>
</table>
</form>
\ No newline at end of file
<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>
--- /dev/null
+<: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&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>
<tr>
<td colspan="2"><input type="submit" name="a_save" value="Save Subscription" /></td>
<td> </td>
+</tr>
</table>
</form>
\ No newline at end of file
<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&subscription_id=<:isubscription subscription_id:>">Details</a> <:ifRemovable:><a href="<:script:>?a_detail=1&_t=delete&subscription_id=<:isubscription subscription_id:>">Delete</a><:or:><:eif:></td>
+ <td><a href="<:script:>?a_detail=1&subscription_id=<:isubscription subscription_id:>">Details</a> <:ifRemovable:><a href="<:script:>?a_detail=1&t=delete&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>