site/cgi-bin/admin/logon.pl
site/cgi-bin/admin/makeIndex.pl
site/cgi-bin/admin/menu.pl
+site/cgi-bin/admin/messages.pl
site/cgi-bin/admin/move.pl
site/cgi-bin/admin/nadmin.pl
site/cgi-bin/admin/reorder.pl
site/cgi-bin/modules/BSE/AdminUsers.pm
site/cgi-bin/modules/BSE/AdminSiteUsers.pm
site/cgi-bin/modules/BSE/Arrows.pm
-site/cgi-bin/modules/BSE/Cache.pod
+site/cgi-bin/modules/BSE/Cache.pm
site/cgi-bin/modules/BSE/Cache/Cache.pm
site/cgi-bin/modules/BSE/Cache/CHI.pm
site/cgi-bin/modules/BSE/Cache/Memcached.pm
site/cgi-bin/modules/BSE/Mail/SMTP.pm
site/cgi-bin/modules/BSE/Mail/Sendmail.pm
site/cgi-bin/modules/BSE/Message.pm
+site/cgi-bin/modules/BSE/MessageScanner.pm
site/cgi-bin/modules/BSE/NLFilter/SQL.pm
site/cgi-bin/modules/BSE/NotifyFiles.pm
site/cgi-bin/modules/BSE/Permissions.pm
site/cgi-bin/modules/BSE/TB/AdminMemberships.pm
site/cgi-bin/modules/BSE/TB/AdminPerm.pm
site/cgi-bin/modules/BSE/TB/AdminPerms.pm
+site/cgi-bin/modules/BSE/TB/AdminUIState.pm
+site/cgi-bin/modules/BSE/TB/AdminUIStates.pm
site/cgi-bin/modules/BSE/TB/AdminUser.pm
site/cgi-bin/modules/BSE/TB/AdminUsers.pm
site/cgi-bin/modules/BSE/TB/ArticleFile.pm
site/cgi-bin/modules/BSE/UI/API.pm
site/cgi-bin/modules/BSE/UI/Background.pm
site/cgi-bin/modules/BSE/UI/AdminDispatch.pm
+site/cgi-bin/modules/BSE/UI/AdminMessages.pm
site/cgi-bin/modules/BSE/UI/AdminNewsletter.pm
site/cgi-bin/modules/BSE/UI/AdminReport.pm
site/cgi-bin/modules/BSE/UI/AdminSendEmail.pm
site/data/stopwords.txt
site/data/db/bse_background_tasks.data
site/data/db/bse_background_tasks.pkey
+site/data/db/bse_msg_base.pkey
+site/data/db/bse_msg_base.data
+site/data/db/bse_msg_defaults.pkey
+site/data/db/bse_msg_defaults.data
site/data/db/sql_statements.data
site/data/db/sql_statements.pkey
site/docs/BSE::UI::Affiliate.html
site/htdocs/admin/sadmin.html
site/htdocs/css/admin.css
site/htdocs/css/admin.css_natural
+site/htdocs/css/admin_messages.css
site/htdocs/css/sadmin.css
site/htdocs/css/style-main.css
site/htdocs/favicon.ico
site/htdocs/images/trans_pixel.gif
site/htdocs/images/videoclose.png
# site/htdocs/js/admin_dragdrop.js
+site/htdocs/js/admin_messages.js
site/htdocs/js/admin_prodopts.js
+site/htdocs/js/admin_tools.js
site/htdocs/js/bse.js
site/htdocs/js/bse_api.js
site/htdocs/js/bse_flowplayer.js
site/templates/admin/grouplist.tmpl
site/templates/admin/helpicon.tmpl Help icon template for admin templates
site/templates/admin/image_edit.tmpl Edit a single image
+site/templates/admin/interestemail.tmpl
site/templates/admin/logon.tmpl
site/templates/admin/memberupdate/import.tmpl
site/templates/admin/memberupdate/preview.tmpl
site/templates/admin/memberupdate/request.tmpl
site/templates/admin/menu.tmpl
site/templates/admin/menu_adv.tmpl
+site/templates/admin/msgs/index.tmpl
# site/templates/admin/menu_custom.tmpl
-site/templates/admin/interestemail.tmpl
site/templates/admin/locations/add.tmpl
site/templates/admin/locations/delete.tmpl
site/templates/admin/locations/edit.tmpl
site/templates/xbase.tmpl
site/util/bseaddimages.pl
site/util/bse_back.pl
+site/util/bse_msgcheck.pl Scan for undefined message ids
site/util/bse_nightly.pl
site/util/bse_notify_files.pl
site/util/bse_s3.pl
or die "Could not open config file $conffile: $!";
while (<TESTCONF>) {
chomp;
- /^\s*(\w[^=]*\w)\.(\w+)\s*=\s*(.*)\s*$/ or next;
+ /^\s*(\w[^=]*\w)\.([\w-]+)\s*=\s*(.*)\s*$/ or next;
$conf{lc $1}{lc $2} = $3;
}
-- longer description - formatted as HTML
long_desc text null
);
+
+-- message catalog
+-- should only ever be loaded from data - maintained like code
+create table bse_msg_base (
+ -- message identifier
+ -- codebase/subsystem/messageid (message id can contain /)
+ -- eg. bse/edit/save/noaccess
+ -- referred to as msg:bse/edit/save/noaccess
+ -- in this table only, id can have a trailing /, and the description
+ -- refers to a description of message under that tree, eg
+ -- "bse/" "BSE Message"
+ -- "bse/edit/" "Article editor messages"
+ -- "bse/siteuser/" "Member management messages"
+ -- "bse/userreg/" "Member services"
+ -- id, formatting, params are limited to ascii text
+ -- description unicode
+ id varchar(40) not null primary key,
+
+ -- a semi-long description of the message, including any parameters
+ description text not null,
+
+ -- type of formatting if any to do on the message
+ -- valid values are "none" and "body"
+ formatting varchar(5) not null default 'none',
+
+ -- parameter types, as a comma separated list
+ -- U - user
+ -- A - article
+ -- M - member
+ -- for any of these describe() is called, the distinction is mostly for
+ -- the message editor preview
+ -- S - scalar
+ -- comma separation is for future expansion
+ -- %{n}:printfspec
+ -- is replaced with parameter n in the text
+ -- so %2:d is the second parameter formatted as an integer
+ -- %% is replaced with %
+ params varchar(40) not null default '',
+
+ -- non-zero if the text can be multiple lines
+ multiline integer not null default 0
+);
+
+-- default messages
+-- should only ever be loaded from data, though different priorities
+-- for the same message might be loaded from different data sets
+create table bse_msg_defaults (
+ -- message identifier
+ id varchar(40) not null,
+
+ -- language code for this message
+ -- empty as the fallback
+ language_code varchar(10) not null default '',
+
+ -- priority of this message, lowest 0
+ priority integer not null default 0,
+
+ -- message text
+ message text not null,
+
+ primary key(id, language_code, priority)
+);
+
+-- admin managed message base, should never be loaded from data
+create table bse_msg_managed (
+ -- message identifier
+ id varchar(40) not null,
+
+ -- language code
+ -- empty as the fallback
+ language_code varchar(10) not null default '',
+
+ message text not null,
+
+ primary key(id, language_code)
+);
+
+-- admin user saved UI state
+create table bse_admin_ui_state (
+ id integer not null auto_increment primary key,
+ user_id integer not null,
+ name varchar(80) not null,
+ val text not null
+);
--- /dev/null
+#!/usr/bin/perl -w
+# -d:ptkdb
+BEGIN { $ENV{DISPLAY} = '192.168.32.51:0.0' }
+use strict;
+use FindBin;
+use lib "$FindBin::Bin/../modules";
+use BSE::DB;
+use BSE::Request;
+use BSE::Template;
+use Carp 'confess';
+use BSE::UI::AdminMessages;
+
+$SIG{__DIE__} = sub { confess $@ };
+
+my $req = BSE::Request->new;
+
+my $result = BSE::UI::AdminMessages->dispatch($req);
+$req->output_result($result);
[template descriptions]
common/default.tmpl=Default template
+
my %actions =
(
menu=>1,
+ set_state => 1,
+ delete_state => 1,
+ get_state => 1,
+ get_matching_state => 1,
+ delete_matching_state => 1,
);
sub actions { \%actions }
return $req->dyn_response('admin/menu', \%acts);
}
+=item a_set_state
+
+Set a UI state value.
+
+Parameters:
+
+=over
+
+=item *
+
+name - name of the state to set
+
+=item *
+
+value - value to store
+
+=back
+
+Requires Ajax.
+
+Returns JSON: { success: 1 }
+
+or a field error.
+
+=cut
+
+sub req_set_state {
+ my ($self, $req) = @_;
+
+ $req->is_ajax
+ or return $self->error($req, "Ajax requests only");
+ my $cgi = $req->cgi;
+ my $name = $cgi->param("name");
+ my $val = $cgi->param("value");
+ my %errors;
+ defined $name or $errors{name} = "Missing name parameter";
+ defined $val or $errors{value} = "Missing value parameter";
+ keys %errors
+ and return $self->_field_error($req, \%errors);
+
+ require BSE::TB::AdminUIStates;
+ my $entry = BSE::TB::AdminUIStates->user_state($req->user, $name);
+ if ($entry) {
+ $entry->set_val($val);
+ $entry->save;
+ }
+ else {
+ BSE::TB::AdminUIStates->make
+ (
+ user_id => $req->user->id,
+ name => $name,
+ val => $val,
+ );
+ }
+
+ return $req->json_content(success => 1);
+}
+
+sub req_get_state {
+ my ($self, $req) = @_;
+
+ $req->is_ajax
+ or return $self->error($req, "Ajax requests only");
+ my $cgi = $req->cgi;
+ my $name = $cgi->param("name");
+ my %errors;
+ defined $name or $errors{name} = "Missing name parameter";
+ keys %errors
+ and return $self->_field_error($req, \%errors);
+
+ require BSE::TB::AdminUIStates;
+ my $entry = BSE::TB::AdminUIStates->user_state($req->user, $name);
+
+ unless ($entry) {
+ return $req->json_content
+ (
+ success => 0,
+ error_code => "NOTFOUND",
+ );
+ }
+
+ return $req->json_content
+ (
+ success => 1,
+ value => $entry->val,
+ );
+}
+
+sub req_delete_state {
+ my ($self, $req) = @_;
+
+ $req->is_ajax
+ or return $self->error($req, "Ajax requests only");
+ my $cgi = $req->cgi;
+ my $name = $cgi->param("name");
+ my %errors;
+ defined $name or $errors{name} = "Missing name parameter";
+ keys %errors
+ and return $self->_field_error($req, \%errors);
+
+ require BSE::TB::AdminUIStates;
+ my $entry = BSE::TB::AdminUIStates->user_state($req->user, $name);
+ if ($entry) {
+ $entry->remove;
+ }
+
+ return $req->json_content
+ (
+ success => 1,
+ );
+}
+
+sub req_get_matching_state {
+ my ($self, $req) = @_;
+
+ $req->is_ajax
+ or return $self->error($req, "Ajax requests only");
+ my $cgi = $req->cgi;
+ my $name = $cgi->param("name");
+ my %errors;
+ defined $name or $errors{name} = "Missing name parameter";
+ keys %errors
+ and return $self->_field_error($req, \%errors);
+
+ require BSE::TB::AdminUIStates;
+ my @entries = BSE::TB::AdminUIStates->user_matching_state($req->user, $name);
+
+ return $req->json_content
+ (
+ success => 1,
+ entries =>
+ [ map +{ name => $_->name, value => $_->val }, @entries ],
+ );
+}
+
+sub req_delete_matching_state {
+ my ($self, $req) = @_;
+
+ $req->is_ajax
+ or return $self->error($req, "Ajax requests only");
+ my $cgi = $req->cgi;
+ my $name = $cgi->param("name");
+ my %errors;
+ defined $name or $errors{name} = "Missing name parameter";
+ keys %errors
+ and return $self->_field_error($req, \%errors);
+
+ require BSE::TB::AdminUIStates;
+
+ # this should really use a SQL delete statement
+ my @entries = BSE::TB::AdminUIStates->user_matching_state($req->user, $name);
+ for my $entry (@entries) {
+ $entry->remove;
+ }
+
+ return $req->json_content
+ (
+ success => 1,
+ );
+}
+
1;
--- /dev/null
+package BSE::Cache;
+use strict;
+
+sub load {
+ my ($class, $cfg) = @_;
+
+ $cfg ||= BSE::Cfg->single;
+ my $cache_class = $cfg->entry("cache", "class");
+ defined $cache_class
+ or return;
+ ( my $cache_mod_file = $cache_class . ".pm" ) =~ s(::)(/)g;
+ require $cache_mod_file;
+ return $cache_class->new($cfg);
+}
+
+1;
+__END__
+=head1 NAME
+
+BSE::Cache - internal BSE cache module interface
+
+=head1 SYNOPSIS
+
+ my $cache = BSE::Cache::module->new($cfg);
+ $cache->set($key, $complex_value);
+ my $complex_value = $cache->get($key);
+ $cache->delete($key)
+
+=head1 DESCRIPTION
+
+CPAN has Cache and Cache::Cache and CHI based caching, and some
+library specific interfaces.
+
+This library wraps all the confusing in yet another layer.
+
+=head1 INTERFACE
+
+=over
+
+=item $class->new($cfg)
+
+Create a new BSE::Cache compatible object. Any parameters should be
+loaded from the BSE configuration.
+
+=item $obj->set($key, $complex_value)
+
+Sets the given key to $complex_value, which may be a reference to a
+hash or a hash of hashes, etc.
+
+=item $obj->get($key)
+
+Retrieves the value stored for $key.
+
+Returns nothing if no cached value is found.
+
+=item $obj->delete($key)
+
+Delete the value stored for $key. Returns nothing in particular.
+
+=cut
+
+=cut
+
+
+++ /dev/null
-=head1 NAME
-
-BSE::Cache - internal BSE cache module interface
-
-=head1 SYNOPSIS
-
- my $cache = BSE::Cache::module->new($cfg);
- $cache->set($key, $complex_value);
- my $complex_value = $cache->get($key);
- $cache->delete($key)
-
-=head1 DESCRIPTION
-
-CPAN has Cache and Cache::Cache and CHI based caching, and some
-library specific interfaces.
-
-This library wraps all the confusing in yet another layer.
-
-=head1 INTERFACE
-
-=over
-
-=item $class->new($cfg)
-
-Create a new BSE::Cache compatible object. Any parameters should be
-loaded from the BSE configuration.
-
-=item $obj->set($key, $complex_value)
-
-Sets the given key to $complex_value, which may be a reference to a
-hash or a hash of hashes, etc.
-
-=item $obj->get($key)
-
-Retrieves the value stored for $key.
-
-Returns nothing if no cached value is found.
-
-=item $obj->delete($key)
-
-Delete the value stored for $key. Returns nothing in particular.
-
-=cut
-
-=cut
-
-
else {
if ($req->user_can('edit_add_child')) {
push @values, -1;
- $labels{-1} = "-- move up a level -- become a section";
+ $labels{-1} = $req->catmsg("bse/admin/edit/uplabelsect");
}
}
}
package BSE::Message;
use strict;
use Carp qw/confess/;
+use BSE::DB;
+use BSE::Cfg;
+use overload "&{}" => sub { my $self = $_[0]; return sub { $self->_old_msg(@_) } };
+
+=head1 NAME
+
+BSE::Message - BSE message catalog access.
+
+=head1 SYNOPSIS
+
+ my $msgs = BSE::Message->new;
+ my $text = $msgs->text($lang, $msgid);
+ my $text = $msgs->text($lang, $msgid, [ parameters ]);
+ my $text = $msgs->text($lang, $msgid, [ parameters ], $def);
+ my $html = $msgs->html($lang, $msgid);
+ my $html = $msgs->html($lang, $msgid, [ parameters ]);
+ my $html = $msgs->html($lang, $msgid, [ parameters ], $def);
+
+=cut
sub new {
my ($class, %opts) = @_;
- my $cfg = $opts{cfg}
- or confess "No cfg supplied";
- my $section = $opts{section}
- or confess "No section supplied";
-
- return
- sub {
- my ($id, $def, @parms) = @_;
-
- my $msg = $cfg->entry("messages", "$section/$id");
- $msg or return $def;
-
-
- $msg =~ s/\$([\d\$])/$1 eq '$' ? '$' : $parms[$1-1]/eg;
-
- $msg;
- };
+ $opts{cache_age} = BSE::Cfg->single->entry("messages", "cache_age", 60);
+ $opts{mycache} = {};
+
+ return bless \%opts, $class;
+}
+
+sub text {
+ my ($self, $lang, $msgid, $parms, $def) = @_;
+
+ my $msg = $self->_get_replaced($lang, $msgid, $parms);
+ if ($msg) {
+ if ($msg->{formatting} eq 'body') {
+ require BSE::Formatter;
+ my $formatter = BSE::Formatter->new;
+ return $formatter->remove_format($msg->{message});
+ }
+
+ return $msg->{message};
+ }
+ else {
+ $def and return $def;
+ return;
+ }
+}
+
+sub html {
+ my ($self, $lang, $msgid, $parms, $def) = @_;
+
+ my $msg = $self->_get_replaced($lang, $msgid, $parms);
+ if ($msg) {
+ if ($msg->{formatting} eq 'body') {
+ require Generate;
+ require BSE::Template;
+ my $gen = Generate->new(cfg => BSE::Cfg->single);
+ my $templater = BSE::Template->templater(BSE::Cfg->single);
+ return $gen->format_body(acts => {},
+ articles => "Articles",
+ text => $msg->{message},
+ templater => $templater);
+ }
+
+ return escape_html($msg->{message});
+ }
+ else {
+ $def and return $def;
+ return;
+ }
+}
+
+=item $self->get_replaced($lang, $msgid, $parms)
+
+Retrieve the base message text + formatting info, and replace parameters.
+
+Currently just replaces parameters, should also:
+
+=over
+
+=item *
+
+have a mechanism to replace messages eg.
+
+ %msg{bse/bar}
+
+=item *
+
+have a zero/single/plural mechanism, eg.:
+
+ %c1{zero text;single text;multiple text}
+
+=back
+
+=cut
+
+sub _get_replaced {
+ my ($self, $lang, $msgid, $parms) = @_;
+
+ my $msg = $self->_get_base($lang, $msgid)
+ or return;
+
+ $parms ||= [];
+ $msg->{message} =~ s/%(%|[0-9]+:[-+ #0]?[0-9]*(?:\.[0-9]+)?[duoxXfFeEgGs])/
+ $1 eq "%" ? "%" : $self->_value($msg, $1, $parms)/ge;
+
+ return $msg;
+}
+
+sub _value {
+ my ($self, $msg, $code, $parms) = @_;
+
+ my ($index, $format) = $code =~ /^([0-9]+):(.*)$/;
+ $index >= 1 && $index <= @$parms
+ or return "(bad index $index in %$code)";
+
+ my $value = $parms->[$index-1];
+ if (ref $index) {
+ local $@;
+ my $good = eval { $value = $value->describe; 1; };
+ unless ($good) {
+ return "(Bad parameter $index - ref but no describe)";
+ }
+ }
+
+ return sprintf "%$format", $value;
+}
+
+=item $self->get_base($lang, $msgid)
+
+Retrieve the base message text + formatting info.
+
+=cut
+
+sub _get_base {
+ my ($self, $lang, $msgid) = @_;
+
+ defined $lang or $lang = BSE::Cfg->single->entry("basic", "language_code", "en");
+
+ if ($self->{cache_age} == 0) {
+ return $self->_get_base_low($lang, $msgid);
+ }
+
+ my $key = "$lang.$msgid";
+ my $entry = $self->{mycache}{$key};
+ if (!$entry && $self->{cache}) {
+ $entry = $self->{cache}->get("msg-$key");
+ }
+
+ my $now = time;
+ if ($entry) {
+ if ($entry->[0] < $now - $self->{cache_age}) {
+ undef $entry;
+ }
+ }
+
+ $entry and return $entry->[1];
+
+ my $msg = $self->_get_base_low($lang, $msgid);
+ $entry = [ $now, $msg ];
+ $self->{mycache}{$key} = $entry;
+ if ($self->{cache}) {
+ $self->{cache}->set("msg-$key", $entry);
+ }
+
+ return $msg;
+}
+
+sub _get_base_low {
+ my ($self, $lang, $msgid) = @_;
+
+ # build a list of languages to search
+ my @langs = $lang;
+ if ($lang =~ /^([a-z]+)_[a-z]+$/) {
+ push @langs, $1;
+ }
+
+ my $msg;
+ for my $search_lang (@langs) {
+ ($msg) = BSE::DB->query(bseGetMsgManaged => $msgid, $search_lang)
+ and return $msg;
+ }
+
+ for my $search_lang (@langs) {
+ ($msg) = BSE::DB->query(bseGetMsgDefault => $msgid, $search_lang)
+ and return $msg;
+ }
+
+ for my $fallback ($self->fallback) {
+ ($msg) = BSE::DB->query(bseGetMsgManaged => $msgid, $fallback)
+ and return $msg;
+
+ ($msg) = BSE::DB->query(bseGetMsgDefault => $msgid, "")
+ and return $msg;
+ }
+
+ return;
+}
+
+sub _old_msg {
+ my ($self, $msgid, $def, @parms) = @_;
+
+ my $msg = BSE::Cfg->single->entry("messages", "$self->{section}/$msgid");
+ if ($msg) {
+ $msg =~ s/\$([\d\$])/$1 eq '$' ? '$' : $parms[$1-1]/eg;
+ return $msg;
+ }
+
+ $DB::single = 1;
+ $msgid = "bse/$self->{section}/$msgid";
+ my $text = $self->text(undef, $msgid, \@parms);
+ $text and return $text;
+
+ return $def;
+}
+
+sub languages {
+ my ($self) = @_;
+
+ my $cfg = BSE::Cfg->single;
+ my %langs = $cfg->entries("languages");
+ delete $langs{fallback};
+ $langs{en} ||= "English";
+ my @langs = map +{ id => $_, name => $langs{$_} }, sort keys %langs;
+
+ return @langs;
+}
+
+sub fallback {
+ my ($self) = @_;
+
+ my $cfg = BSE::Cfg->single;
+ my $fallback = $cfg->entry("languages", "fallback", "en");
+ return split /,/, $fallback;
+}
+
+sub uncache {
+ my ($self, $id, $cache) = @_;
+
+ unless ($cache) {
+ require BSE::Cache;
+ $cache = BSE::Cache->load;
+ }
+
+ for my $lang ($self->languages) {
+ $cache->delete("msg-$lang->{id}.$id");
+ }
}
1;
--- /dev/null
+package BSE::MessageScanner;
+use strict;
+use File::Find;
+
+=item BSE::MessageScanner->scan(\@basepaths)
+
+Scan .tmpl, .pm and .pl files under the given directories for apparent
+message uses and return each use with the id, file and line number.
+
+=cut
+
+sub scan {
+ my ($class, $bases) = @_;
+
+ my @files;
+ find
+ (
+ sub {
+ -f && /\.(tmpl|pm|pl)$/ && push @files, $File::Find::name;
+ },
+ @$bases
+ );
+ my @ids;
+ for my $file (@files) {
+ open my $fh, "<", $file
+ or die "Cannot open $file: $!\n";
+ my $errors = 0;
+ while (my $line = <$fh>) {
+ my @msgs = $line =~ m(\b(msg:[\w-]+(?:/[\w-]+)*));
+ push @ids, map [ $_, $file, $. ], @msgs;
+ }
+ close $fh;
+ }
+
+ return @ids;
+}
+
+1;
$self->_cache_available or return;
$self->{_cache} and return $self->{_cache};
- my $cache_class = $self->cfg->entry("cache", "class");
- ( my $cache_mod_file = $cache_class . ".pm" ) =~ s(::)(/)g;
- require $cache_mod_file;
+ require BSE::Cache;
- $self->{_cache} = $cache_class->new($self->cfg);
+ $self->{_cache} = BSE::Cache->load($self->cfg);
return $self->{_cache};
}
my $msg = '';
my @lines;
if ($errors and keys %$errors) {
+ # do any translation needed
+ for my $key (keys %$errors) {
+ my @msgs = ref $errors->{$key} ? @{$errors->{$key}} : $errors->{$key};
+
+ for my $msg (@msgs) {
+ if ($msg =~ /^(msg:[\w-]+(?:\/[\w-]+)+)(?::(.*))/) {
+ my $id = $1;
+ my $params = $2;
+ my @params = defined $params ? split(/:/, $params) : ();
+ $msg = $req->catmsg($id, \@params);
+ }
+ }
+ $errors->{$key} = ref $errors->{$key} ? \@msgs : $msgs[0];
+ }
+
my @fields = $req->cgi->param;
my %work = %$errors;
for my $field (@fields) {
BSE::TB::AuditLog->make(%entry);
}
+=item message_catalog
+
+Retrieve the message catalog.
+
+=cut
+
+sub message_catalog {
+ my ($self) = @_;
+
+ unless ($self->{message_catalog}) {
+ require BSE::Message;
+ my %opts;
+ $self->_cache_available and $opts{cache} = $self->_cache_object;
+ $self->{message_catalog} = BSE::Message->new(%opts);
+ }
+
+ return $self->{message_catalog};
+}
+
+=item catmsg($id)
+
+=item catmsg($id, \@params)
+
+=item catmsg($id, \@params, $default)
+
+=item catmsg($id, \@params, $default, $lang)
+
+Retrieve a message from the message catalog, performing substitution.
+
+This retrieves the text version of the message only.
+
+=cut
+
+sub catmsg {
+ my ($self, $id, $params, $default, $lang) = @_;
+
+ defined $lang or $lang = $self->language;
+ defined $params or $params = [];
+
+ $id =~ s/^msg://
+ or return "* bad message id - missing leading msg: *";
+
+ my $result = $self->message_catalog->text($lang, $id, $params, $default);
+ unless ($result) {
+ $result = "Unknown message id $id";
+ }
+
+ return $result;
+}
+
+=item language
+
+Fetch the language for the current system/user.
+
+Warning: this currently fetches a system configured default, in the
+future it will use a user default and/or a browser set default.
+
+=cut
+
+sub language {
+ my ($self) = @_;
+
+ return $self->cfg->entry("basic", "language_code", "en");
+}
+
sub ip_address {
return $ENV{REMOTE_ADDR};
}
--- /dev/null
+package BSE::TB::AdminUIState;
+use strict;
+use base "Squirrel::Row";
+
+sub columns {
+ qw/id user_id name val/;
+}
+
+sub table {
+ "bse_admin_ui_state";
+}
+
+1;
--- /dev/null
+package BSE::TB::AdminUIStates;
+use strict;
+use base "Squirrel::Table";
+use BSE::TB::AdminUIState;
+
+sub rowClass {
+ "BSE::TB::AdminUIState";
+}
+
+sub user_matching_state {
+ my ($self, $user, $prefix) = @_;
+
+ return $self->getSpecial(matchingState => $user->id, $prefix . "%");
+}
+
+sub user_state {
+ my ($self, $user, $name) = @_;
+
+ return $self->getBy(user_id => $user->id, name => $name);
+}
+
+1;
use Carp 'confess';
use Config ();
-sub get_page {
- my ($class, $template, $cfg, $acts, $base_template, $rsets) = @_;
+sub templater {
+ my ($class, $cfg, $rsets) = @_;
my @conf_dirs = $class->template_dirs($cfg);
- my $file = $cfg->entry('templates', $template) || $template;
- $file =~ /\.\w+$/ or $file .= ".tmpl";
my @dirs;
if ($rsets && @$rsets) {
for my $set (@$rsets) {
else {
@dirs = @conf_dirs;
}
-
- my $obj = Squirrel::Template->new(template_dir => \@dirs);
+
+ return Squirrel::Template->new(template_dir => \@dirs);
+}
+
+sub get_page {
+ my ($class, $template, $cfg, $acts, $base_template, $rsets) = @_;
+
+ my $file = $cfg->entry('templates', $template) || $template;
+ $file =~ /\.\w+$/ or $file .= ".tmpl";
+ my $obj = $class->templater($cfg, $rsets);
my $out;
if ($base_template) {
else {
$out = $obj->show_page(undef, $file, $acts);
}
-
$out;
}
sub replace {
my ($class, $source, $cfg, $acts) = @_;
- my @dirs = $class->template_dirs($cfg);
- my $obj = Squirrel::Template->new(template_dir => \@dirs);
+ my $obj = $class->templater($cfg);
$obj->replace_template($source, $acts);
}
unless ($req->check_admin_logon) {
# time to logon
# if this was a GET, try to refresh back to it after logon
- my %extras =
- (
- 'm' => 'You must logon to use this function'
- );
- if ($ENV{REQUEST_METHOD} eq 'GET') {
- my $rurl = admin_base_url($req->cfg) . $ENV{SCRIPT_NAME};
- $rurl .= "?" . $ENV{QUERY_STRING} if $ENV{QUERY_STRING};
- $rurl .= $rurl =~ /\?/ ? '&' : '?';
- $rurl .= "refreshed=1";
- $extras{r} = $rurl;
+ if ($req->is_ajax || $req->cgi->param("_")) {
+ $$rresult = $req->json_content
+ (
+ success => 0,
+ error_code => "LOGON",
+ message => "Access forbidden: user not logged on",
+ errors => {},
+ );
+ }
+ else {
+ my %extras =
+ (
+ 'm' => 'You must logon to use this function'
+ );
+ if ($ENV{REQUEST_METHOD} eq 'GET') {
+ my $rurl = admin_base_url($req->cfg) . $ENV{SCRIPT_NAME};
+ $rurl .= "?" . $ENV{QUERY_STRING} if $ENV{QUERY_STRING};
+ $rurl .= $rurl =~ /\?/ ? '&' : '?';
+ $rurl .= "refreshed=1";
+ $extras{r} = $rurl;
+ }
+ my $url = $req->url(logon => \%extras);
+ $$rresult = $req->get_refresh($url);
}
- my $url = $req->url(logon => \%extras);
- $$rresult = $req->get_refresh($url);
return;
}
--- /dev/null
+package BSE::UI::AdminMessages;
+use strict;
+use base 'BSE::UI::AdminDispatch';
+use BSE::Util::Iterate;
+use BSE::Message;
+
+my %actions =
+ (
+ index => "bse_msg_view",
+ catalog => "bse_msg_view",
+ detail => "bse_msg_view",
+ save => "bse_msg_save",
+ delete => "bse_msg_delete",
+ );
+
+sub actions { \%actions }
+
+sub rights { \%actions }
+
+sub default_action { "index" }
+
+sub req_index {
+ my ($self, $req) = @_;
+
+ my %acts =
+ (
+ $req->admin_tags,
+ );
+
+ return $req->dyn_response("admin/msgs/index", \%acts);
+}
+
+sub req_catalog {
+ my ($self, $req) = @_;
+
+ return $req->json_content
+ (
+ success => 1,
+ messages => [ BSE::DB->query("bseAllMsgs") ],
+ );
+}
+
+sub req_detail {
+ my ($self, $req) = @_;
+
+ my $id = $req->cgi->param("id");
+ my %errors;
+ unless ($id) {
+ $errors{id} = "msg:bse/admin/message/noid";
+ }
+ unless ($errors{id}) {
+ $id =~ m(^[a-z]\w*(/\w+)+$)
+ or $errors{id} = "msg:bse/admin/message/invalidid";
+ }
+ keys %errors
+ and return $self->_field_error($req, \%errors);
+
+ my ($base) = BSE::DB->query(bseMessageDetail => $id);
+ my (@defaults) = BSE::DB->query(bseMessageDefaults => $id);
+ # we only want the highest priority defaults
+ my %real_defs;
+ for my $def (@defaults) {
+ if (!$real_defs{$def->{language_code}}
+ || $real_defs{$def->{language_code}}{priority} < $def->{priority}) {
+ $real_defs{$def->{language_code}} = $def;
+ }
+ }
+ my @real_defs = sort { $a->{language_code} cmp $b->{language_code} } values %real_defs;
+ my (@defns) = BSE::DB->query(bseMessageDefinitions => $id);
+ my @langs = BSE::Message->languages;
+
+ return $req->json_content
+ (
+ success => 1,
+ base => $base,
+ languages => \@langs,
+ defaults => { map { $_->{language_code} => $_ } @real_defs },
+ definitions => { map { $_->{language_code} => $_ } @defns },
+ );
+}
+
+my %msg_rules =
+ (
+ bse_msg_id =>
+ {
+ match => qr(^(?:x-)?[a-z]\w*(?:/\w+)+$),
+ error => "msg:bse/admin/message/invalidid",
+ },
+ bse_msg_language =>
+ {
+ match => qr/^[a-z_-]+$/i,
+ error => "msg:bse/admin/message/invalidlang",
+ },
+ );
+
+my %save_fields =
+ (
+ id =>
+ {
+ description => "Message ID",
+ rules => "required;bse_msg_id",
+ },
+ language_code =>
+ {
+ description => "Language Code",
+ rules => "required;bse_msg_language",
+ },
+ message =>
+ {
+ description => "Message text",
+ rules => "required",
+ },
+ );
+
+sub req_save {
+ my ($self, $req) = @_;
+
+ my $cgi = $req->cgi;
+ my $id = $cgi->param("id");
+ my $lang = $cgi->param("language_code");
+ my $text = $cgi->param("message");
+ my %errors;
+ $req->validate(fields => \%save_fields,
+ rules => \%msg_rules,
+ errors => \%errors);
+
+ my $base;
+ unless ($errors{id}) {
+ ($base) = BSE::DB->query(bseMessageDetail => $id);
+ $base
+ or $errors{id} = "msg:bse/admin/message/unknownid";
+ }
+ unless ($errors{language_code}) {
+ my @langs = BSE::Message->languages;
+ grep $lang eq $_->{id}, @langs
+ or $errors{language_code} = "msg:bse/admin/message/unknownlang";
+ }
+ if ($base && !$base->{multiline}) {
+ $text =~ /\n/
+ and $errors{message} = "msg:bse/admin/message/badmultiline:$id";
+ }
+ keys %errors
+ and return $self->_field_error($req, \%errors);
+
+ if (my ($row) = BSE::DB->query(bseMessageFetch => $id, $lang)) {
+ BSE::DB->run(bseMessageUpdate => $text, $id, $lang);
+ }
+ else {
+ BSE::DB->run(bseMessageCreate => $id, $lang, $text);
+ }
+
+ BSE::Message->uncache($id);
+
+ return $req->json_content
+ (
+ success => 1,
+ definition =>
+ {
+ id => $id,
+ language_code => $lang,
+ message => $text,
+ },
+ );
+}
+
+my %delete_fields =
+ (
+ id =>
+ {
+ description => "Message ID",
+ rules => "required;bse_msg_id",
+ },
+ language_code =>
+ {
+ description => "Language Code",
+ rules => "required;bse_msg_language",
+ },
+ );
+
+sub req_delete {
+ my ($self, $req) = @_;
+
+ my $cgi = $req->cgi;
+ my $id = $cgi->param("id");
+ my $lang = $cgi->param("language_code");
+
+ my %errors;
+ $req->validate(fields => \%delete_fields,
+ rules => \%msg_rules,
+ errors => \%errors);
+
+ my $base;
+ unless ($errors{id}) {
+ ($base) = BSE::DB->query(bseMessageDetail => $id);
+ $base
+ or $errors{id} = "msg:bse/admin/message/unknownid";
+ }
+ unless ($errors{language_code}) {
+ my @langs = BSE::Message->languages;
+ grep $lang eq $_->{id}, @langs
+ or $errors{language_code} = "msg:bse/admin/message/unknownlang";
+ }
+ keys %errors
+ and return $self->_field_error($req, \%errors);
+
+ BSE::DB->run(bseMessageDelete => $id, $lang);
+
+ BSE::Message->uncache($id);
+
+ return $req->json_content
+ (
+ success => 1,
+ );
+}
+
+1;
return $req->response($template, \%acts);
}
+sub _field_error {
+ my ($self, $req, $errors) = @_;
+
+ return $req->json_content
+ (
+ success => 0,
+ error_code => "FIELD",
+ errors => $errors,
+ message => "Fields failed validation",
+ );
+}
+
sub controller_id {
$_[0]{controller_id};
}
if ($intro eq '--') {
$result = DevHelp::LoaderData::Fields->new($file);
}
+ elsif ($intro eq '---') {
+ $result = DevHelp::LoaderData::FieldsDefaulted->new($file);
+ }
elsif ($intro =~ /\t/) {
$result = DevHelp::LoaderData::Tab->new($file, $intro);
}
return \%data;
}
+package DevHelp::LoaderData::FieldsDefaulted;
+use vars qw(@ISA);
+@ISA = qw(DevHelp::LoaderData::Fields);
+
+sub new {
+ my ($class, $file) = @_;
+
+ my $self = $class->SUPER::new($file);
+ $self->{defaults} = $self->SUPER::read() || {};
+
+ return $self;
+}
+
+sub read {
+ my ($self) = @_;
+
+ my $values = $self->SUPER::read()
+ or return;
+
+ for my $key (keys %{$self->{defaults}}) {
+ exists $values->{$key} or $values->{$key} = $self->{defaults}{$key};
+ }
+
+ return $values;
+}
+
package DevHelp::LoaderData::CSV;
use vars qw(@ISA);
@ISA = qw(DevHelp::LoaderData::Delimited);
--- /dev/null
+--
+id: bse/
+description: BSE messages
+
+id: bse/user/
+description: <<TEXT
+Messages displayed by user.pl (member user registration, logon, etc)
+TEXT
+
+id: bse/user/needlogon
+description: <<TEXT
+Message displayed when a member doesn't enter a user name on the login page
+TEXT
+
+id: bse/user/needpass
+description: <<TEXT
+Message displayed when a member doesn't enter a password on the login page
+TEXT
+
+id: bse/user/baduserpass
+description: <<TEXT
+Message displayed when the username or password is invalid when logging in
+TEXT
+
+id: bse/admin/
+description: BSE Administration
+
+id: bse/admin/edit/
+description: Article editor messages
+
+id: bse/admin/edit/uplabelsect
+description: label in parent list to make article a section
+
+id: test/
+description: <<TEXT
+Test category
+TEXT
+
+id: test/test/
+description: More testing
+
+id: test/test/test
+description: an actual message
+
+id: test/test/multiline
+description: a multiline message
+multiline: 1
+
+id: bse/admin/message/
+description: Message administration tool
+
+id: bse/admin/message/noid
+description: Error for when no id is supplied to a_detail
+
+id: bse/admin/message/invalidid
+description: Error for when an invalid id is supplied to a_detail
+
+id: bse/admin/message/invalidlang
+description: Invalid language code supplied when creating, removing or saving a message definition
+
+id: bse/admin/message/unknownid
+description: Attempted to create or save an unknown message id (not in bse_msg_base)
+
+id: bse/admin/message/unknownlang
+description: Attempted to create or save a message to a language not in the config file.
+
+id: bse/admin/message/badmultiline
+description: Multiple lines of text supplied when creating or saving a single line message
+
--- /dev/null
+---
+# defaults for the following
+language_code: en
+priority: 0
+
+id: bse/user/needlogon
+message: Please enter your username
+
+id: bse/user/needpass
+message: Please enter your password
+
+id: bse/user/baduserpass
+message: Invalid username or password
+
+id: bse/admin/edit/uplabelsect
+message: -- move up a level -- become a section
+
+id: bse/admin/message/noid
+message: Missing id parameter
+
+id: bse/admin/message/invalidid
+message: Invalid id parameter - must be word/word/word...
+
+id: bse/admin/message/invalidlang
+message: Invalid language_code parameter - must contain only letters, underscore or dash
+
+id: bse/admin/message/unknownid
+message: Unknown message identifier - no entry found in bse_msg_base
+
+id: bse/admin/message/unknownlang
+message: Unknown language code - no entry found in [languages]
+
+id: bse/admin/message/badmultiline
+message: Message $1:s may contain only a single line of text
+
+id: test/test/multiline
+message: <<TEXT
+This message has
+multiple
+lines
+TEXT
\ No newline at end of file
delete from bse_wishlist
where product_id = ?
SQL
+
+name: bseGetMsgManaged
+sql_statement: <<SQL
+select m.message, m.language_code, b.formatting, b.params
+from bse_msg_managed m, bse_msg_base b
+where m.id = ?
+ and m.language_code = ?
+ and m.id = b.id
+SQL
+
+name: bseGetMsgDefault
+sql_statement: <<SQL
+select d.message, d.language_code, b.formatting, b.params
+from bse_msg_defaults d, bse_msg_base b
+where d.id = ?
+ and d.language_code = ?
+ and d.id = b.id
+order by d.priority desc
+SQL
+
+name: bseAllMsgs
+sql_statement: <<SQL
+select * from bse_msg_base order by id
+SQL
+
+name: bseMessageDetail
+sql_statement: select * from bse_msg_base where id = ?
+
+name: bseMessageDefaults
+sql_statement: <<SQL
+select * from bse_msg_defaults
+where id = ?
+SQL
+
+name: bseMessageDefinitions
+sql_statement: <<SQL
+select * from bse_msg_managed
+where id = ?
+SQL
+
+name: bseMessageFetch
+sql_statement: <<SQL
+select * from bse_msg_managed
+where id = ? and language_code = ?
+SQL
+
+name: bseMessageCreate
+sql_statement: <<SQL
+insert into bse_msg_managed(id, language_code, message) values(?,?,?)
+SQL
+
+name: bseMessageUpdate
+sql_statement: <<SQL
+update bse_msg_managed
+ set message = ?
+ where id = ? and language_code = ?
+SQL
+
+name: bseMessageDelete
+sql_statement: <<SQL
+delete from bse_msg_managed where id = ? and language_code = ?
+SQL
+
+name: AdminUIStates.matchingState
+sql_statement: <<SQL
+select *
+from bse_admin_ui_state
+where user_id = ?
+ and name like ?
+SQL
a { color: #333366; font-weight: bold}
a:hover { color: #6666CC}
a:visited { color: #333366}
-li { font: 10px Verdana, Arial, Helvetica, sans-serif}
+/*li { font: 10px Verdana, Arial, Helvetica, sans-serif}*/
h1 { font-size: 18px; margin-left: -1.5em}
h2 { font-size: 16px; margin-left: -1em; padding-top: 1em; color: #333333; font-family: Arial, Helvetica, sans-serif; font-weight: bold}
form { display: inline}
-li { padding-bottom: 10px;}
+li { padding-top: 3px;padding-bottom: 7px;}
td.sep { background: #339; font-size: 0.1em}
input.float { position: absolute; right: 1em}
-body { font-family: Verdana, Arial, Helvetica, sans-serif; font-size: 10px; margin-left: 4em; background-color: #CCCCCC}
-td { font: 10px Verdana, Arial, Helvetica, sans-serif}
-p { font: 10px Verdana, Arial, Helvetica, sans-serif}
+body { font-family: Verdana, Arial, Helvetica, sans-serif; font-size: 12px; margin-left: 4em; background-color: #CCCCFF}
+td { font: 12px Verdana, Arial, Helvetica, sans-serif}
+p { font: 12px Verdana, Arial, Helvetica, sans-serif}
select { font-family: "MS Sans Serif", Verdana, sans-serif; font-size: 12px}
input { font-family: "MS Sans Serif", Verdana, sans-serif; font-size: 12px}
textarea { font-family: "MS Sans Serif", Verdana, sans-serif; font-size: 12px}
-th { font-size: 10px; font-weight: bold; background-color: #999999; color: #FFFFFF}
+th { font-size: 12px; font-weight: bold; background-color: #606080; color: #FFFFFF}
.table { background-color: #666666}
h3 { font-size: 14px}
h4 { font-size: 12px}
table.editform th {
color: #FFF;
- background-color: #999;
+ background-color: #606080;
font-weight: bold;
text-align: left;
white-space: nowrap;
#message.error {
color: red;
}
+
+.wind
+{
+ border: 1px solid #888;
+ position: absolute;
+ margin-left: -200px;
+ left: 50%;
+ width: 400px;
+ top: 150px;
+ display: none;
+ background: #fff;
+ }
+
+.wind.wider
+{
+ margin-left: -410px;
+ width: 820px;
+ }
+
+.wind .head
+{
+ color: white;
+ font-weight: bold;
+ background-color: #00F;
+ padding: 2px 1px;
+ }
+
+#windowclose
+{
+ display: none;
+ }
+
+.wind .head #windowclose
+{
+ display: block;
+ float: right;
+ font-weight: bold;
+ background-color: #888;
+ }
+
--- /dev/null
+#message_catalog ul {
+ display: none;
+}
+
+#message_detail .langhead {
+ background-color: #CCCCFF;
+ font-weight: bold;
+ padding: 3px 3px 2px;
+ border-top: 1px solid #00f;
+ margin-top: 3px;
+}
+
+#message_detail .default,
+#message_detail .definition {
+ padding-left: 10px;
+}
+
+#message_detail .label {
+ font-weight: bold;
+}
+
+#message_detail .multiline .content {
+ display: block;
+ white-space: pre-wrap;
+}
+
+#message_detail input.bse_ipe_edit {
+ width: 50%;
+}
+#message_detail textarea.bse_ipe_edit {
+ width: 50%;
+ display: block;
+}
+
+#message_detail .bse_ipe_over {
+ background-color: #CCC;
+}
\ No newline at end of file
--- /dev/null
+var api;
+
+var msgs_shown = {};
+
+var save_state = false;
+
+function messages_load(msgs) {
+ $("message_catalog").innerHTML = "";
+
+ // build a tree structure, anything ending in / is a parent (of something)
+ var root = [];
+ var byid = {};
+ var parents = {};
+ for (var i = 0; i < msgs.length; ++i) {
+ var msg = msgs[i];
+ if (msg.id.match(/^[a-z0-9_]+\/$/)) {
+ // a root node
+ msg.kids = [];
+ byid[msg.id] = msg;
+ parents[msg.id] = msg;
+ root.push(msg);
+ }
+ else {
+ // look for a parent
+ var parent_id = msg.id.replace(/[a-z0-9_]+\/?$/, "");
+ var parent = parents[parent_id];
+ if (!parent) {
+ alert("No parent " + parent_id + " found for " + msg.id);
+ }
+ else {
+ parent.kids.push(msg);
+ if (msg.id.match(/\/$/)) {
+ msg.kids = [];
+ parents[msg.id] = msg;
+ }
+ }
+ }
+ }
+
+ populate_msgs($("message_catalog"), root);
+
+ api.get_base_config
+ ({
+ onSuccess: function(result) {
+ if (result.access_control) {
+ save_state = true;
+ api.get_matching_state
+ ({
+ name: "messages_open:",
+ onSuccess: function(entries) {
+ for (var i = 0; i < entries.length; ++i) {
+ var msgid = entries[i].name.replace(/^messages_open:/, "");
+ var workid = msgid.replace(/\//g, "-");
+ var ele = $("kids-"+workid);
+ if (ele) {
+ msg_hide_show(msgid, workid);
+ }
+ }
+ }
+ });
+ }
+ }
+ });
+}
+
+function populate_msgs(ele, tree) {
+ for (var i = 0; i < tree.length; ++i) {
+ var msg = tree[i];
+ var kid = new Element("li");
+ var workid = msg.id.replace(/\//g, "-");
+ kid.id = "msg-" + workid;
+ if (msg.kids) {
+ var a = new Element
+ ("a",
+ {
+ href: "#"
+ }
+ );
+ a.appendChild(document.createTextNode(msg.description));
+ a.onclick = msg_hide_show.bind(this, msg.id, workid);
+ kid.appendChild(a);
+ var ul = new Element("ul", { id: "kids-"+workid });
+ populate_msgs(ul, msg.kids);
+ kid.appendChild(ul);
+ msgs_shown[msg.id] = false;
+ }
+ else {
+ kid.appendChild(document.createTextNode(msg.description + " "));
+ var detail = new Element("a", { href: "#" });
+ detail.update("Details");
+ detail.onclick = message_detail.bind(this, msg);
+ kid.appendChild(detail);
+ }
+ ele.appendChild(kid);
+ }
+}
+
+function msg_hide_show(msgid, workid) {
+ var ul = $("kids-"+workid);
+ if (msgs_shown[msgid]) {
+ ul.style.display = "none";
+ msgs_shown[msgid] = false;
+ if (save_state) api.delete_state({ name: "messages_open:"+msgid });
+ }
+ else {
+ ul.style.display = "block";
+ msgs_shown[msgid] = true;
+ if (save_state) api.set_state({ name: "messages_open:"+msgid, value: 1});
+ }
+
+ return false;
+}
+
+function messages_start() {
+ api = new BSEAPI;
+ api.message_catalog
+ ({
+ onSuccess: function(messages) {
+ messages_load(messages);
+ $("open_all").observe("click", open_all_messages);
+ $("close_all").observe("click", close_all_messages);
+ }
+ });
+}
+
+function open_all_messages(event) {
+ for (var i in msgs_shown) {
+ if (!msgs_shown[i]) {
+ var workid = i.replace(/\//g, "-");
+ msg_hide_show(i, workid);
+ }
+ }
+
+ event.stop();
+}
+
+function close_all_messages(event) {
+ for (var i in msgs_shown) {
+ if (msgs_shown[i]) {
+ var workid = i.replace(/\//g, "-");
+ msg_hide_show(i, workid);
+ }
+ }
+
+ event.stop();
+}
+
+function message_detail(msg) {
+ $("message_detail_list").innerHTML = "Loading...";
+
+ api.message_detail
+ ({
+ id: msg.id,
+ onSuccess: function(result) {
+ populate_detail(result);
+ }
+ });
+
+ _open_window($("message_detail"), "Detail: "+msg.description);
+
+ return false;
+}
+
+function defn_detail_entry(ele, base, lang, defn) {
+ ele.innerHTML = "";
+ var label = new Element("span", { className: "label" });
+ label.update("Message: ");
+ ele.appendChild(label);
+ var cont_ele = new Element("span", { className: "content", id: "cont-"+lang.id });
+
+ cont_ele.appendChild(document.createTextNode(defn.message));
+ ele.appendChild(cont_ele);
+
+ var edit_parms =
+ {
+ onSave: function(ele, base, lang, ipe, text) {
+ api.message_save
+ ({
+ id: base.id,
+ language_code: lang.id,
+ message: text,
+ onSuccess: function(ele, base, lang, ipe, defn) {
+ defn_detail_entry(ele, base, lang, defn);
+ ipe.on_success();
+ }.bind(this, ele, base, lang, ipe),
+ onFailure: function(ipe, err) {
+ if (err.error_code == "FIELD") {
+ var msgs = new Array;
+ for (var i in err.errors) {
+ msgs.push(err.errors[i]);
+ }
+ ipe.on_error(msgs.join("\n"));
+ }
+ else
+ ipe.on_error(err.error_code);
+ }.bind(this, ipe)
+ });
+ }.bind(this, ele, base, lang),
+ value: defn.message
+ };
+ if (base.multiline != 0)
+ edit_parms.rows = 5;
+ new BSE.InPlaceEdit
+ (
+ cont_ele,
+ edit_parms
+ );
+
+ ele.appendChild(document.createTextNode(" "));
+ var del = new Element("a");
+ del.update("Delete");
+ del.onclick = remove_message.bind(this, base, lang, ele);
+ ele.appendChild(del);
+}
+
+function no_detail_entry(ele, base, lang) {
+ ele.innerHTML = "";
+ var label = new Element("span", { className: "label" });
+ label.update("Message: ");
+ ele.appendChild(label);
+ var cont_ele = new Element("span", { className: "content", id: "cont-"+lang.id });
+ ele.addClassName("undefined");
+ cont_ele.update("(undefined)");
+ var edit_parms =
+ {
+ onSave: function(ele, base, lang, ipe, text) {
+ api.message_save
+ ({
+ id: base.id,
+ language_code: lang.id,
+ message: text,
+ onSuccess: function(ele, base, lang, ipe, defn) {
+ defn_detail_entry(ele, base, lang, defn);
+ ipe.on_success();
+ }.bind(this, ele, base, lang, ipe),
+ onFailure: function(ipe, err) {
+ ipe.on_error(err.error_code);
+ }.bind(this, ipe)
+ });
+ }.bind(this, ele, base, lang)
+ };
+ if (base.multiline != 0)
+ edit_parms.rows = 5;
+ new BSE.InPlaceEdit
+ (
+ cont_ele,
+ edit_parms
+ );
+ ele.appendChild(cont_ele);
+}
+
+function populate_detail(detail) {
+ // build a map of defaults and definitions
+ var div = $("message_detail_list");
+ div.innerHTML = "";
+ var base = detail.base;
+ var defs = detail.defaults;
+ var defns = detail.definitions;
+ var langs = detail.languages;
+ for (var i = 0; i < langs.length; ++i) {
+ var lang = langs[i];
+ var lang_ele = new Element("div", { className: "langhead" });
+ lang_ele.appendChild(document.createTextNode("Language: " +lang.name));
+ div.appendChild(lang_ele);
+ if (defs[lang.id]) {
+ var def_ele = new Element("div", { className: "default" });
+ var label = new Element("span", { className: "label" });
+ label.update("Default: ");
+ def_ele.appendChild(label);
+ var cont_ele = new Element("span", { className: "content" });
+ cont_ele.appendChild(document.createTextNode(defs[lang.id].message));
+ def_ele.appendChild(cont_ele);
+ if (base.multiline != 0)
+ def_ele.addClassName("multiline");
+ div.appendChild(def_ele);
+ }
+ var defn = defns[lang.id];
+ var defn_ele = new Element("div", { className: "definition", id: "defn-"+lang.id });
+ if (base.multiline != 0)
+ defn_ele.addClassName("multiline");
+ if (defn)
+ defn_detail_entry(defn_ele, base, lang, defn);
+ else
+ no_detail_entry(defn_ele, base, lang);
+ div.appendChild(defn_ele);
+ }
+}
+
+function remove_message(base, lang, ele) {
+ if (window.confirm("Are you sure you want to delete this definition?")) {
+ api.message_delete
+ ({
+ id: base.id,
+ language_code: lang.id,
+ onSuccess: function(base, lang, ele) {
+ no_detail_entry(ele, base, lang);
+ }.bind(this, base, lang, ele)
+ });
+ }
+}
+
+Event.observe(document, "dom:loaded", messages_start);
\ No newline at end of file
--- /dev/null
+function _open_window(ele, title) {
+ var closer = $("windowclose");
+ if (!closer) {
+ closer = new Element("div", { id: "windowclose" });
+ var closer_a = new Element("a", { href: "#" });
+ closer_a.update("X");
+ closer.appendChild(closer_a);
+ }
+ else {
+ closer.parentNode.removeChild(closer);
+ }
+ closer.onclick = _close_window.bind(this, ele);
+ if (title) {
+ ele.firstDescendant().innerHTML = "";
+ ele.firstDescendant().appendChild(document.createTextNode(title));
+ }
+ ele.firstDescendant().appendChild(closer);
+ ele.style.display="block";
+}
+
+function _close_window(ele) {
+ ele.style.display="none";
+ return false;
+}
+
+var BSE = {};
+BSE.InPlaceEdit = Class.create
+ (
+ {
+ initialize: function(ele, options) {
+ ele = $(ele);
+ ele.observe("click", this.start_edit.bind(this));
+ ele.observe("mouseover", this._onmouseover.bind(this));
+ ele.observe("mouseout", this._onmouseout.bind(this));
+ this.options = Object.clone(this.DefaultOptions);
+ Object.extend(this.options, options || { } );
+ this.element = ele;
+ },
+ on_success: function() {
+ if (this.saving) {
+ this.remove_saving();
+ this.remove_form();
+ this.saving = false;
+ }
+ },
+ on_error: function(msg) {
+ if (this.saving) {
+ if (msg) alert(msg);
+ this.remove_saving();
+ this.form.show();
+ this.editing = true;
+ this.saving = false;
+ }
+ },
+ start_edit: function() {
+ this.element.hide();
+ this.make_form();
+ this.edit.focus();
+ },
+ make_form: function() {
+ this.form = new Element("form", { className: "bse_ipe" });
+ this.form.onsubmit = this._onsubmit.bind(this);
+ this.make_edit_field();
+ this.make_submit();
+ this.make_cancel();
+ this.element.hide();
+ this.element.parentNode.insertBefore(this.form, this.element);
+ this.editing = true;
+ },
+ make_edit_field: function() {
+ if (this.options.rows == 1) {
+ this.edit = new Element("input", { className: this.options.edit_class, value: this.options.value });
+ }
+ else {
+ this.edit = new Element("textarea", { className: this.options.edit_class, rows: this.options.rows });
+ this.edit.appendChild(document.createTextNode(this.options.value));
+ }
+ this.form.appendChild(this.edit);
+ },
+ make_submit: function() {
+ this.submit = new Element("input", { type: "submit", className: this.options.submit_class, value: this.options.submit_text });
+ this.submit.onclick = this._onsubmit.bind(this);
+ this.form.appendChild(this.submit);
+ },
+ make_cancel: function() {
+ this.cancel = new Element("input", { type: "submit", className: this.options.cancel_class, value: this.options.cancel_text });
+ this.cancel.onclick = this._oncancel.bind(this);
+ this.form.appendChild(this.cancel);
+ },
+ _onsubmit: function() {
+ if (this.editing) {
+ this.form.hide();
+ this.show_saving();
+ this.options.onSave(this, this.edit.value);
+ this.editing = false;
+ this.saving = true;
+ }
+ return false;
+ },
+ show_saving: function() {
+ this.saving_ele = new Element("span", { className: this.options.saving_class });
+ this.saving_ele.appendChild(document.createTextNode(this.options.saving_text));
+ this.element.parentNode.insertBefore(this.saving_ele, this.element);
+ },
+ remove_saving: function() {
+ // it's possible the parent has been destroyed during the save processing
+ if (this.element.parentNode)
+ this.element.parentNode.removeChild(this.saving_ele);
+ delete this.saving_ele;
+ },
+ remove_form: function() {
+ if (this.element.parentNode)
+ this.element.parentNode.removeChild(this.form);
+ delete this.form;
+ delete this.edit;
+ delete this.submit;
+ delete this.cancel;
+ },
+ _oncancel: function() {
+ if (this.editing) {
+ if (this.form.parentNode)
+ this.form.parentNode.removeChild(this.form);
+ this.element.show();
+ this.editing = false;
+ }
+ return false;
+ },
+ _onmouseover: function() {
+ this.element.addClassName(this.options.mouseover_class);
+ },
+ _onmouseout: function() {
+ this.element.removeClassName(this.options.mouseover_class);
+ },
+ DefaultOptions:
+ {
+ rows: 1,
+ form_id: null,
+ value: "",
+ saving_text: "Saving...",
+ saving_class: "bse_ipe_saving",
+ edit_class: "bse_ipe_edit",
+ submit_class: "bse_ipe_submit",
+ submit_text: "Save",
+ cancel_class: "bse_ipe_cancel",
+ cancel_text: "Cancel",
+ mouseover_class: "bse_ipe_over"
+ }
+ });
\ No newline at end of file
var order = parameters.order.join(",");
this._do_add_request("a_order_images", { id: id, order: order }, success, failure);
},
+
+ // Message catalog functions
+ message_catalog: function(parameters) {
+ var success = parameters.onSuccess;
+ if (!success) this._badparm("remove_image_file() missing onSuccess parameter");
+ var failure = parameters.onFailure;
+ if (!failure) failure = this.onFailure;
+ this._do_request
+ (
+ "/cgi-bin/admin/messages.pl", "a_catalog", { },
+ function(success, resp) {
+ success(resp.messages);
+ }.bind(this, success),
+ failure
+ );
+ },
+ message_detail: function(parameters) {
+ var success = parameters.onSuccess;
+ if (!success) this._badparm("message_detail() missing onSuccess parameter");
+ var failure = parameters.onFailure;
+ if (!failure) failure = this.onFailure;
+ var id = parameters.id;
+ if (id == null) this._badparm("message_detail() missing id parameter");
+ this._do_request
+ (
+ "/cgi-bin/admin/messages.pl", "a_detail", { id: id }, success, failure
+ );
+ },
+ // requires id, language_code, message
+ message_save: function(parameters) {
+ var success = parameters.onSuccess;
+ if (!success) this._badparm("message_save() missing onSuccess parameter");
+ var my_success = function(success, resp) {
+ success(resp.definition);
+ }.bind(this, success);
+ delete parameters.success;
+ var failure = parameters.onFailure;
+ if (!failure) failure = this.onFailure;
+ delete parameters.failure;
+ this._do_request("/cgi-bin/admin/messages.pl", "a_save", parameters,
+ my_success, failure);
+ },
+ // requires id, language_code
+ message_delete: function(parameters) {
+ var success = parameters.onSuccess;
+ if (!success) this._badparm("message_delete() missing onSuccess parameter");
+ delete parameters.success;
+ var failure = parameters.onFailure;
+ if (!failure) failure = this.onFailure;
+ delete parameters.failure;
+ this._do_request("/cgi-bin/admin/messages.pl", "a_delete", parameters,
+ success, failure);
+ },
+
+ // requires name, value
+ set_state: function(parameters) {
+ var success = parameters.onSuccess || function() {};
+ var failure = parameters.onFailure || this.onFailure;
+ delete parameters.onSuccess;
+ delete parameters.onFailure;
+ this._do_request("/cgi-bin/admin/menu.pl", "a_set_state", parameters, success, failure);
+ },
+ // requires name
+ get_state: function(parameters) {
+ var success = parameters.onSuccess;
+ if (!success) this._badparm("get_state() missing onSuccess parameter");
+ var my_success = function(success, result) {
+ success(result.value);
+ }.bind(this, success);
+ var failure = parameters.onFailure || this.onFailure;
+ delete parameters.onSuccess;
+ delete parameters.onFailure;
+ this._do_request("/cgi-bin/admin/menu.pl", "a_get_state", parameters, my_success, failure);
+ },
+ // requires name
+ delete_state: function(parameters) {
+ var success = parameters.onSuccess || function() {};
+ var failure = parameters.onFailure || this.onFailure;
+ delete parameters.onSuccess;
+ delete parameters.onFailure;
+ this._do_request("/cgi-bin/admin/menu.pl", "a_delete_state", parameters, success, failure);
+ },
+
+ // requires name, a prefix for the state entries we want
+ get_matching_state: function(parameters) {
+ var success = parameters.onSuccess;
+ if (!success) this._badparm("get_matching_state() missing onSuccess parameter");
+ var my_success = function(success, result) {
+ success(result.entries);
+ }.bind(this, success);
+ var failure = parameters.onFailure || this.onFailure;
+ delete parameters.onSuccess;
+ delete parameters.onFailure;
+ this._do_request("/cgi-bin/admin/menu.pl", "a_get_matching_state", parameters, my_success, failure);
+
+ },
+
+ // requires name, a prefix for the state entries we want
+ delete_matching_state: function(parameters) {
+ var success = parameters.onSuccess || function() {};
+ var failure = parameters.onFailure || this.onFailure;
+ delete parameters.onSuccess;
+ delete parameters.onFailure;
+ this._do_request("/cgi-bin/admin/menu.pl", "a_delete_matching_state", parameters, success, failure);
+
+ },
+
_progress_handler: function(parms) {
if (parms.finished) return;
this.get_file_progress(
--- /dev/null
+<:wrap admin/xbase.tmpl title => "Message Catalog", js=>"admin_messages.js", showtitle => 1, css => "admin_messages.css", api => 1, jstools=>1:>
+<p>| <a href="/cgi-bin/admin/menu.pl">Admin Menu</a>
+| <a href="#" id="close_all">Close all</a>
+| <a href="#" id="open_all">Open all</a>
+|
+<ul id="message_catalog">
+<li>Loading...</li>
+</ul>
+<div id="message_detail" class="wind wider">
+ <div class="head">(filled in at runtime)</div>
+ <div id="message_detail_list"></div>
+</div
\ No newline at end of file
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd" >
<html><head><title>BSE - <:param title:></title>
<link rel="stylesheet" href="/css/admin.css" />
+<:ifParam css:><link rel="stylesheet" href="/css/<:param css:>" /><:or:><:eif:>
<:ajax includes:>
-<script src="/js/bse.js"></script>
-<script src="/js/swfobject.js"></script>
-<:ifAnd [param js] [ifAjax]:><script type="text/javascript" src="/js/<:param js:>"></script><:or:><:eif:>
+<script type="text/javascript" src="/js/bse.js"></script>
+<script type="text/javascript" src="/js/swfobject.js"></script>
+<:ifParam api:><script type="text/javascript" src="/js/bse_api.js"></script><:or:><:eif:>
+<:ifParam jstools:><script type="text/javascript" src="/js/admin_tools.js"></script><:or:><:eif:>
+<:ifParam js:><script type="text/javascript" src="/js/<:param js:>"></script><:or:><:eif:>
</head>
<body>
<:ifParam showtitle:><h1><:param title:></h1><:or:><:eif:>
--- /dev/null
+#!perl -w
+use strict;
+use FindBin;
+use lib "$FindBin::Bin/../cgi-bin/modules";
+use BSE::API qw(bse_init);
+use File::Find;
+use BSE::MessageScanner;
+
+bse_init("../cgi-bin");
+
+@ARGV
+ or die "Usage: $0 directory ...\n";
+
+# scan for message ids in files under each tree given
+my %id_reported;
+my @ids = BSE::MessageScanner->scan(\@ARGV);
+my $last_filename = '';
+my $errors = 0;
+for my $entry (@ids) {
+ my ($id, $file, $lineno) = @$entry;
+ $id_reported{$id} and next;
+ (my $subid = $id) =~ s/^msg://;
+ my ($detail) = BSE::DB->query(bseMessageDetail => $subid);
+ my ($def) = BSE::DB->query(bseMessageDefaults => $subid);
+ if ($detail) {
+ unless ($def) {
+ print "$file:$lineno:$id - no default entry\n";
+ ++$errors;
+ $id_reported{$id} = 1;
+ }
+ }
+ else {
+ print "$file:$lineno: $id - no base entry\n";
+ ++$errors;
+ $id_reported{$id} = 1;
+ }
+}
or die;
while (my $row = $datafile->read) {
- for my $pkey_col (@pkey) {
- unless (exists $row->{$pkey_col}) {
- die "Missing value for $pkey_col in record ending $. of $inname\n";
- }
- }
+ #for my $pkey_col (@pkey) {
+ # unless (exists $row->{$pkey_col}) {
+ # die "Missing value for $pkey_col in record ending $. of $inname\n";
+ # }
+ #}
defined($dbh->do($del_sql, {}, @{$row}{@pkey}))
or die "Error deleting old record: ", DBI->errstr;
Column category;varchar(20);NO;;
Column file_handler;varchar(20);NO;;
Index PRIMARY;1;[id]
+Table bse_admin_ui_state
+Column id;int(11);NO;NULL;auto_increment
+Column user_id;int(11);NO;NULL;
+Column name;varchar(80);NO;NULL;
+Column val;text;NO;NULL;
+Index PRIMARY;1;[id]
Table bse_article_file_meta
Column id;int(11);NO;NULL;auto_increment
Column file_id;int(11);NO;NULL;
Column admin_notes;text;NO;NULL;
Column disabled;int(11);NO;0;
Index PRIMARY;1;[id]
+Table bse_msg_base
+Column id;varchar(40);NO;NULL;
+Column description;text;NO;NULL;
+Column formatting;varchar(5);NO;none;
+Column params;varchar(40);NO;;
+Column multiline;int(11);NO;0;
+Index PRIMARY;1;[id]
+Table bse_msg_defaults
+Column id;varchar(40);NO;NULL;
+Column language_code;varchar(10);NO;;
+Column priority;int(11);NO;0;
+Column message;text;NO;NULL;
+Index PRIMARY;1;[id;language_code;priority]
+Table bse_msg_managed
+Column id;varchar(40);NO;NULL;
+Column language_code;varchar(10);NO;;
+Column message;text;NO;NULL;
+Index PRIMARY;1;[id;language_code]
Table bse_order_item_options
Column id;int(11);NO;NULL;auto_increment
Column order_item_id;int(11);NO;NULL;
--- /dev/null
+#!perl -w
+use strict;
+use DevHelp::LoaderData;
+use BSE::MessageScanner;
+use Test::More;
+
+# check that the core message bases and defaults cover all of the ids
+# used in BSE.
+
+# load the data
+my @base = _load("site/data/db/bse_msg_base.data");
+my %base = map { $_->{id} => 1 } @base;
+my @defs = _load("site/data/db/bse_msg_defaults.data");
+my %defs = map { $_->{id} => 1 } @defs;
+
+# scan for ids
+my @msgs = BSE::MessageScanner->scan([ "site" ]);
+
+# make them unique, we don't need a bunch of errors for one id
+my %seen;
+@msgs = grep !$seen{$_->[0]}, @msgs;
+
+plan tests => 2 * scalar(@msgs);
+
+for my $msg (@msgs) {
+ my ($id, $file, $line) = @$msg;
+ (my $subid = $id) =~ s/^msg://;
+ ok($base{$subid}, "found base for $id ($file:$line)");
+ ok($defs{$subid}, "found default for $id ($file:$line)");
+}
+
+sub _load {
+ my ($in_name) = @_;
+
+ open my $fh, "<", $in_name
+ or die "Cannot open $in_name: $!";
+ my $loader = DevHelp::LoaderData->new($fh);
+ my @data;
+ while (my $row = $loader->read) {
+ push @data, $row;
+ }
+
+ return @data;
+}