basic BSE message database
authorTony Cook <tony@develop-help.com>
Thu, 27 May 2010 09:04:21 +0000 (09:04 +0000)
committertony <tony@45cb6cf1-00bc-42d2-bb5a-07f51df49f94>
Thu, 27 May 2010 09:04:21 +0000 (09:04 +0000)
35 files changed:
MANIFEST
localinst.perl
schema/bse.sql
site/cgi-bin/admin/messages.pl [new file with mode: 0755]
site/cgi-bin/bse.cfg
site/cgi-bin/modules/BSE/AdminMenu.pm
site/cgi-bin/modules/BSE/Cache.pm [new file with mode: 0644]
site/cgi-bin/modules/BSE/Cache.pod [deleted file]
site/cgi-bin/modules/BSE/Edit/Article.pm
site/cgi-bin/modules/BSE/Message.pm
site/cgi-bin/modules/BSE/MessageScanner.pm [new file with mode: 0644]
site/cgi-bin/modules/BSE/Request/Base.pm
site/cgi-bin/modules/BSE/TB/AdminUIState.pm [new file with mode: 0644]
site/cgi-bin/modules/BSE/TB/AdminUIStates.pm [new file with mode: 0644]
site/cgi-bin/modules/BSE/Template.pm
site/cgi-bin/modules/BSE/UI/AdminDispatch.pm
site/cgi-bin/modules/BSE/UI/AdminMessages.pm [new file with mode: 0644]
site/cgi-bin/modules/BSE/UI/Dispatch.pm
site/cgi-bin/modules/DevHelp/LoaderData.pm
site/data/db/bse_msg_base.data [new file with mode: 0644]
site/data/db/bse_msg_base.pkey [new file with mode: 0644]
site/data/db/bse_msg_defaults.data [new file with mode: 0644]
site/data/db/bse_msg_defaults.pkey [new file with mode: 0644]
site/data/db/sql_statements.data
site/htdocs/css/admin.css
site/htdocs/css/admin_messages.css [new file with mode: 0644]
site/htdocs/js/admin_messages.js [new file with mode: 0644]
site/htdocs/js/admin_tools.js [new file with mode: 0644]
site/htdocs/js/bse_api.js
site/templates/admin/msgs/index.tmpl [new file with mode: 0644]
site/templates/admin/xbase.tmpl
site/util/bse_msgcheck.pl [new file with mode: 0644]
site/util/loaddata.pl
site/util/mysql.str
t/t85message.t [new file with mode: 0644]

index e441d04..7d883d9 100644 (file)
--- a/MANIFEST
+++ b/MANIFEST
@@ -26,6 +26,7 @@ site/cgi-bin/admin/imageclean.pl
 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
@@ -57,7 +58,7 @@ site/cgi-bin/modules/BSE/AdminMenu.pm
 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
@@ -111,6 +112,7 @@ site/cgi-bin/modules/BSE/Mail.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
@@ -148,6 +150,8 @@ site/cgi-bin/modules/BSE/TB/AdminMembership.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
@@ -195,6 +199,7 @@ site/cgi-bin/modules/BSE/ThumbLow.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
@@ -290,6 +295,10 @@ site/cgi-bin/user.pl
 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
@@ -355,6 +364,7 @@ site/htdocs/admin/index.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
@@ -387,7 +397,9 @@ site/htdocs/images/titles/your_site.gif
 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
@@ -442,14 +454,15 @@ site/templates/admin/file_edit.tmpl
 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
@@ -663,6 +676,7 @@ site/templates/user/userpage_base.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
index 00c058f..c52866e 100644 (file)
@@ -95,7 +95,7 @@ open TESTCONF, "< $conffile"
   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;
 }
 
index 041136a..5c866e0 100644 (file)
@@ -1012,3 +1012,87 @@ create table bse_background_tasks (
   -- 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
+);
diff --git a/site/cgi-bin/admin/messages.pl b/site/cgi-bin/admin/messages.pl
new file mode 100755 (executable)
index 0000000..7a327b9
--- /dev/null
@@ -0,0 +1,18 @@
+#!/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);
index 8efe6de..09bf2f4 100644 (file)
@@ -441,3 +441,4 @@ flv=BSE::FileHandler::FLV
 
 [template descriptions]
 common/default.tmpl=Default template
+
index 934cfb0..1d7d619 100644 (file)
@@ -6,6 +6,11 @@ use base 'BSE::UI::AdminDispatch';
 my %actions =
   (
    menu=>1,
+   set_state => 1,
+   delete_state => 1,
+   get_state => 1,
+   get_matching_state => 1,
+   delete_matching_state => 1,
   );
 
 sub actions { \%actions }
@@ -31,4 +36,165 @@ sub req_menu {
   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;
diff --git a/site/cgi-bin/modules/BSE/Cache.pm b/site/cgi-bin/modules/BSE/Cache.pm
new file mode 100644 (file)
index 0000000..0742b63
--- /dev/null
@@ -0,0 +1,64 @@
+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
+
+
diff --git a/site/cgi-bin/modules/BSE/Cache.pod b/site/cgi-bin/modules/BSE/Cache.pod
deleted file mode 100644 (file)
index 2b94335..0000000
+++ /dev/null
@@ -1,47 +0,0 @@
-=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
-
-
index ea2efd3..8f1c611 100644 (file)
@@ -291,7 +291,7 @@ sub possible_parents {
       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");
        }
       }
     }
index 903a235..a0a28ad 100644 (file)
 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;
diff --git a/site/cgi-bin/modules/BSE/MessageScanner.pm b/site/cgi-bin/modules/BSE/MessageScanner.pm
new file mode 100644 (file)
index 0000000..884e772
--- /dev/null
@@ -0,0 +1,38 @@
+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;
index 469d3e7..7e1da7a 100644 (file)
@@ -64,11 +64,9 @@ sub _cache_object {
   $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};
 }
@@ -318,6 +316,21 @@ sub message {
   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) {
@@ -1050,6 +1063,71 @@ sub audit {
   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};
 }
diff --git a/site/cgi-bin/modules/BSE/TB/AdminUIState.pm b/site/cgi-bin/modules/BSE/TB/AdminUIState.pm
new file mode 100644 (file)
index 0000000..0603998
--- /dev/null
@@ -0,0 +1,13 @@
+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;
diff --git a/site/cgi-bin/modules/BSE/TB/AdminUIStates.pm b/site/cgi-bin/modules/BSE/TB/AdminUIStates.pm
new file mode 100644 (file)
index 0000000..6fd212d
--- /dev/null
@@ -0,0 +1,22 @@
+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;
index 94b9c14..794da6e 100644 (file)
@@ -4,12 +4,10 @@ use Squirrel::Template;
 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) {
@@ -20,8 +18,16 @@ sub get_page {
   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) {
@@ -44,7 +50,6 @@ sub get_page {
   else {
     $out = $obj->show_page(undef, $file, $acts);
   }
-    
 
   $out;
 }
@@ -52,8 +57,7 @@ sub get_page {
 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);
 }
index 24d4382..1db7be6 100644 (file)
@@ -56,19 +56,30 @@ sub check_action {
   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;
   }
 
diff --git a/site/cgi-bin/modules/BSE/UI/AdminMessages.pm b/site/cgi-bin/modules/BSE/UI/AdminMessages.pm
new file mode 100644 (file)
index 0000000..4c0406b
--- /dev/null
@@ -0,0 +1,216 @@
+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;
index 4d8897b..70d4b22 100644 (file)
@@ -112,6 +112,18 @@ sub error {
   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};
 }
index 100e509..a014e7a 100644 (file)
@@ -11,6 +11,9 @@ sub new {
   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);
   }
@@ -162,6 +165,32 @@ sub read {
   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);
diff --git a/site/data/db/bse_msg_base.data b/site/data/db/bse_msg_base.data
new file mode 100644 (file)
index 0000000..eb15c3f
--- /dev/null
@@ -0,0 +1,69 @@
+--
+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
+
diff --git a/site/data/db/bse_msg_base.pkey b/site/data/db/bse_msg_base.pkey
new file mode 100644 (file)
index 0000000..074d1ee
--- /dev/null
@@ -0,0 +1 @@
+id
diff --git a/site/data/db/bse_msg_defaults.data b/site/data/db/bse_msg_defaults.data
new file mode 100644 (file)
index 0000000..31786e7
--- /dev/null
@@ -0,0 +1,41 @@
+---
+# 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
diff --git a/site/data/db/bse_msg_defaults.pkey b/site/data/db/bse_msg_defaults.pkey
new file mode 100644 (file)
index 0000000..074d1ee
--- /dev/null
@@ -0,0 +1 @@
+id
index 9624526..5a199e2 100644 (file)
@@ -320,3 +320,73 @@ sql_statement: <<SQL
 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
index 6139e16..4befd09 100644 (file)
@@ -1,20 +1,20 @@
 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}
@@ -121,7 +121,7 @@ table.editform td, table.editform th {
 
 table.editform th {
   color: #FFF;
-  background-color: #999;
+  background-color: #606080;
   font-weight: bold;
   text-align: left;
   white-space: nowrap;
@@ -324,3 +324,43 @@ img.bse_image_thumb {
 #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;
+  }
+
diff --git a/site/htdocs/css/admin_messages.css b/site/htdocs/css/admin_messages.css
new file mode 100644 (file)
index 0000000..90a1ec7
--- /dev/null
@@ -0,0 +1,37 @@
+#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
diff --git a/site/htdocs/js/admin_messages.js b/site/htdocs/js/admin_messages.js
new file mode 100644 (file)
index 0000000..1ec6b0c
--- /dev/null
@@ -0,0 +1,302 @@
+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
diff --git a/site/htdocs/js/admin_tools.js b/site/htdocs/js/admin_tools.js
new file mode 100644 (file)
index 0000000..8a9cf88
--- /dev/null
@@ -0,0 +1,148 @@
+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
index 04da67a..573f47d 100644 (file)
@@ -395,6 +395,113 @@ var BSEAPI = Class.create
        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(
diff --git a/site/templates/admin/msgs/index.tmpl b/site/templates/admin/msgs/index.tmpl
new file mode 100644 (file)
index 0000000..cc2191f
--- /dev/null
@@ -0,0 +1,12 @@
+<: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
index effbf06..1477db4 100644 (file)
@@ -3,10 +3,13 @@
    "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:>
diff --git a/site/util/bse_msgcheck.pl b/site/util/bse_msgcheck.pl
new file mode 100644 (file)
index 0000000..3995ed7
--- /dev/null
@@ -0,0 +1,37 @@
+#!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;
+  }
+}
index ea035ae..cb84541 100644 (file)
@@ -50,11 +50,11 @@ while (my $inname = readdir DATADIR) {
     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;
 
index 7d1b945..f0f1f11 100644 (file)
@@ -100,6 +100,12 @@ Column src;varchar(255);NO;;
 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;
@@ -183,6 +189,24 @@ Column facilities_phone;varchar(80);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;
diff --git a/t/t85message.t b/t/t85message.t
new file mode 100644 (file)
index 0000000..a366d25
--- /dev/null
@@ -0,0 +1,44 @@
+#!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;
+}