service label localization, internals and UI, #71347
authorMark Wells <mark@freeside.biz>
Wed, 6 Jul 2016 18:53:13 +0000 (11:53 -0700)
committerMark Wells <mark@freeside.biz>
Wed, 6 Jul 2016 18:53:13 +0000 (11:53 -0700)
13 files changed:
FS/FS/Mason.pm
FS/FS/Schema.pm
FS/FS/part_svc.pm
FS/FS/part_svc_msgcat.pm [new file with mode: 0644]
httemplate/browse/part_svc.cgi
httemplate/edit/elements/part_svc_column.html
httemplate/edit/part_pkg.cgi
httemplate/edit/part_svc.cgi
httemplate/edit/process/elements/process.html
httemplate/edit/process/part_pkg.cgi
httemplate/elements/freeside.css
httemplate/elements/progress-init.html
httemplate/elements/tr-input-locale-text.html [new file with mode: 0644]

index 24ddf79..d625fbd 100644 (file)
@@ -412,6 +412,7 @@ if ( -e $addl_handler_use_file ) {
   use FS::fiber_olt;
   use FS::olt_site;
   use FS::access_user_page_pref;
+  use FS::part_svc_msgcat;
   # Sammath Naur
 
   if ( $FS::Mason::addl_handler_use ) {
index a50b551..f88fea9 100644 (file)
@@ -3682,6 +3682,24 @@ sub tables_hashref {
                         ],
     },
 
+    'part_svc_msgcat' => {
+      'columns' => [
+        'svcpartmsgnum',  'serial',     '',        '', '', '',
+        'svcpart',           'int',     '',        '', '', '',
+        'locale',        'varchar',     '',        16, '', '',
+        'svc',           'varchar',     '',   $char_d, '', '',
+      ],
+      'primary_key'  => 'svcpartmsgnum',
+      'unique'       => [ [ 'svcpart', 'locale' ] ],
+      'index'        => [],
+      'foreign_keys' => [
+                          { columns    => [ 'svcpart' ],
+                            table      => 'part_svc',
+                          },
+                        ],
+    },
+
+
     #(this should be renamed to part_pop)
     'svc_acct_pop' => {
       'columns' => [
index 621a554..dcc7843 100644 (file)
@@ -1,5 +1,5 @@
 package FS::part_svc;
-use base qw(FS::Record);
+use base qw(FS::o2m_Common FS::Record);
 
 use strict;
 use vars qw( $DEBUG );
@@ -11,6 +11,7 @@ use FS::part_export;
 use FS::export_svc;
 use FS::cust_svc;
 use FS::part_svc_class;
+use FS::part_svc_msgcat;
 
 FS::UID->install_callback(sub {
     # preload the cache and make sure all modules load
@@ -621,6 +622,24 @@ sub svc_x {
   map { $_->svc_x } $self->cust_svc;
 }
 
+=item svc_locale LOCALE
+
+Returns a customer-viewable service definition label in the chosen LOCALE.
+If there is no entry for that locale or if LOCALE is empty, returns
+part_svc.svc.
+
+=cut
+
+sub svc_locale {
+  my( $self, $locale ) = @_;
+  return $self->svc unless $locale;
+  my $part_svc_msgcat = qsearchs('part_svc_msgcat', {
+    svcpart => $self->svcpart,
+    locale  => $locale
+  }) or return $self->svc;
+  $part_svc_msgcat->svc;
+}
+
 =back
 
 =head1 CLASS METHODS
@@ -883,6 +902,12 @@ sub process {
     $param->{'svcpart'} = $new->getfield('svcpart');
   }
 
+  $error ||= $new->process_o2m(
+    'table'   => 'part_svc_msgcat',
+    'params'  => $param,
+    'fields'  => [ 'locale', 'svc' ],
+  );
+
   die "$error\n" if $error;
 }
 
diff --git a/FS/FS/part_svc_msgcat.pm b/FS/FS/part_svc_msgcat.pm
new file mode 100644 (file)
index 0000000..6d69198
--- /dev/null
@@ -0,0 +1,131 @@
+package FS::part_svc_msgcat;
+use base qw( FS::Record );
+
+use strict;
+use FS::Locales;
+
+=head1 NAME
+
+FS::part_svc_msgcat - Object methods for part_svc_msgcat records
+
+=head1 SYNOPSIS
+
+  use FS::part_svc_msgcat;
+
+  $record = new FS::part_svc_msgcat \%hash;
+  $record = new FS::part_svc_msgcat { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::part_svc_msgcat object represents localized labels of a service 
+definition.  FS::part_svc_msgcat inherits from FS::Record.  The following
+fields are currently supported:
+
+=over 4
+
+=item svcpartmsgnum
+
+primary key
+
+=item svcpart
+
+Service definition
+
+=item locale
+
+locale
+
+=item svc
+
+Localized service name (customer-viewable)
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new record.  To add the record to the database, see L<"insert">.
+
+Note that this stores the hash reference, not a distinct copy of the hash it
+points to.  You can ask the object for a copy with the I<hash> method.
+
+=cut
+
+# the new method can be inherited from FS::Record, if a table method is defined
+
+sub table { 'part_svc_msgcat'; }
+
+=item insert
+
+Adds this record to the database.  If there is an error, returns the error,
+otherwise returns false.
+
+=cut
+
+# the insert method can be inherited from FS::Record
+
+=item delete
+
+Delete this record from the database.
+
+=cut
+
+# the delete method can be inherited from FS::Record
+
+=item replace OLD_RECORD
+
+Replaces the OLD_RECORD with this one in the database.  If there is an error,
+returns the error, otherwise returns false.
+
+=cut
+
+# the replace method can be inherited from FS::Record
+
+=item check
+
+Checks all fields to make sure this is a valid record.  If there is
+an error, returns the error, otherwise returns false.  Called by the insert
+and replace methods.
+
+=cut
+
+# the check method should currently be supplied - FS::Record contains some
+# data checking routines
+
+sub check {
+  my $self = shift;
+
+  my $error = 
+    $self->ut_numbern('svcpartmsgnum')
+    || $self->ut_foreign_key('svcpart', 'part_svc', 'svcpart')
+    || $self->ut_enum('locale', [ FS::Locales->locales ] )
+    || $self->ut_text('svc')
+  ;
+  return $error if $error;
+
+  $self->SUPER::check;
+}
+
+=back
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::part_svc>, L<FS::Record>, schema.html from the base documentation.
+
+=cut
+
+1;
+
index dee4394..b947463 100755 (executable)
@@ -112,8 +112,24 @@ function part_export_areyousure(href) {
     </TD>
 % } 
 
-    <TD ROWSPAN=<% $rowspan %> CLASS="grid" BGCOLOR="<% $bgcolor %>"><A HREF="<% $url %>">
-      <% $part_svc->svc %></A></TD>
+    <TD ROWSPAN=<% $rowspan %> CLASS="grid" BGCOLOR="<% $bgcolor %>">
+      <A HREF="<% $url %>">
+        <% $part_svc->svc %>
+      </A>
+%   # any alternate names of the service
+%   my %msgcat = map { $_->locale => $_ } $part_svc->part_svc_msgcat;
+%   my %labels = map { $_ => FS::Locales->description($_) } keys %msgcat;
+%   my @locales = sort { $labels{$a} cmp $labels{$b} } keys %msgcat;
+%   if ( @locales ) {
+      <BR>
+      <FONT SIZE="-1">
+%     foreach my $locale (@locales) {
+        <% $labels{$locale} %>: <% $msgcat{$locale}->get('svc') %>
+        <BR>
+%     }
+      </FONT>
+%   }
+    </TD>
 
     <TD ROWSPAN=<% $rowspan %> CLASS="grid" BGCOLOR="<% $bgcolor %>">
       <% $svcdb %></TD>
index 4e112c0..816f342 100644 (file)
@@ -267,8 +267,9 @@ my %communigate_fields = (
 <& /elements/progress-init.html,
   $svcdb, #form name
   [ # form fields to send
-    qw(svc svcpart classnum selfservice_access disabled preserve exportnum),
-    @fields
+    'ALL'
+#    qw(svc svcpart classnum selfservice_access disabled preserve exportnum),
+#    @fields
   ],
   'process/part_svc.cgi',   # target
   $p.'browse/part_svc.cgi', # redirect landing
index 80a61f8..f2c4aac 100755 (executable)
@@ -92,7 +92,7 @@
                    { type => 'columnstart' },
                    
                      { field     => 'pkg',
-                       type      => 'text',
+                       type      => 'input-locale-text',
                        size      => 40, #32
                        maxlength => 50,
                      },
@@ -495,42 +495,6 @@ my $recur_show_zero_disabled = 1;
 
 my $pkgpart = '';
 
-my $splice_locale_fields = sub {
-  my( $fields, $pkey_value_callback, $pkg_value_callback ) = @_;
-
-  my $n = 0;
-  my @locale_fields = (
-    map { 
-          my $pkey_value= $pkey_value_callback ? &$pkey_value_callback($_) : '';
-          my $pkg_value = $pkg_value_callback
-                            ? $pkg_value_callback eq 'cgiparam'
-                                ? $cgi->param('pkgpartmsgnum'. $n. '_pkg')
-                                : &$pkg_value_callback($_)
-                            : '';
-          (
-            { field     => 'pkgpartmsgnum'. $n,
-              type      => 'hidden',
-              value     => $pkey_value,
-            },
-            { field     => 'pkgpartmsgnum'. $n. '_locale',
-              type      => 'hidden',
-              value     => $_,
-            },
-            { field     => 'pkgpartmsgnum'. $n++. '_pkg',
-              type      => 'text',
-              size      => 40,
-              #maxlength => 50,
-              value     => $pkg_value,
-            },
-          );
-  
-        }
-      @locales
-  );
-  splice(@$fields, 7, 0, @locale_fields); #XXX 7 is arbitrary above
-
-};
-
 my $error_callback = sub {
   my($cgi, $object, $fields, $opt ) = @_;
 
@@ -579,16 +543,6 @@ my $error_callback = sub {
 
   $pkgpart = $object->pkgpart;
 
-  &$splice_locale_fields(
-    $fields,
-    sub {
-          my $locale = shift;
-          my $part_pkg_msgcat = $object->part_pkg_msgcat($locale);
-          $part_pkg_msgcat ? $part_pkg_msgcat->pkgpartmsgnum : '';
-        },
-    'cgiparam'
-  );
-
   if ( $cgi->param('error') =~ / is suggested with / ) {
     #yeah, detection is a shitty kludge, but we don't have exception objects
     $opt->{form_init} = '<INPUT TYPE="checkbox" NAME="part_pkg_restrict_soft_override" VALUE="Y"> Override suggestion<BR><BR>';
@@ -665,20 +619,6 @@ my $edit_callback = sub {
 
   $pkgpart = $object->pkgpart;
 
-  &$splice_locale_fields(
-    $fields,
-    sub {
-          my $locale = shift;
-          my $part_pkg_msgcat = $object->part_pkg_msgcat($locale);
-          $part_pkg_msgcat ? $part_pkg_msgcat->pkgpartmsgnum : '';
-        },
-    sub {
-          my $locale = shift;
-          my $part_pkg_msgcat = $object->part_pkg_msgcat($locale);
-          $part_pkg_msgcat ? $part_pkg_msgcat->pkg : '';
-        }
-  );
-
 };
 
 my $new_callback = sub {
@@ -692,8 +632,6 @@ my $new_callback = sub {
 
   $options{'suspend_bill'}=1 if $conf->exists('part_pkg-default_suspend_bill');
 
-  &$splice_locale_fields($fields, '', '');
-
 };
 
 my $clone_callback = sub {
@@ -732,17 +670,6 @@ my $clone_callback = sub {
       foreach keys %part_pkg_currency;
   }
 
-  $recur_disabled = $object->freq ? 0 : 1;
-
-  &$splice_locale_fields(
-    $fields,
-    '',
-    sub {
-      my $locale = shift;
-      my $part_pkg_msgcat = $object->part_pkg_msgcat($locale);
-      $part_pkg_msgcat ? $part_pkg_msgcat->pkg : '';
-    }
-  );
 };
 
 my $discount_error_callback = sub {
index a07fc60..fed2125 100755 (executable)
 }
 </STYLE>
 <SCRIPT TYPE="text/javascript">
+// copy all fields from the outer form (svc and its localizations, plus
+// preserve, selfservice_access, etc.) into the inner form, creating hidden
+// inputs if needed
 function fixup_submit(layer) {
-  document.forms[layer].submit.disabled = true;
-  fixup(document.forms[layer]);
+  var layer_form = $(document.forms[layer]);
+  var main_form = $(document.forms['SvcEditMain']);
+  var data = main_form.serializeArray();
+  for (var i = 0; i < data.length; i++) {
+    var input = layer_form.children('[name=' + data[i].name + ']');
+    if (input[0]) {
+      input.prop('value', data[i].value);
+    } else {
+      $( '<input type="hidden">' )
+        .attr('name', data[i].name)
+        .prop('value', data[i].value)
+        .appendTo(layer_form);
+    }
+  }
+  layer_form[0]['submit'].disabled = true;
+  //fixup(document.forms[layer]);
   window[layer+'process'].call();
 }
 
@@ -141,19 +158,26 @@ window.onload = function() {
 
 </SCRIPT>
 
-<FORM NAME="dummy">
+<FORM NAME="SvcEditMain">
 
 <FONT CLASS="fsinnerbox-title">Service Part #<% $part_svc->svcpart ? $part_svc->svcpart : "(NEW)" %></FONT>
 <TABLE CLASS="fsinnerbox">
-<TR>
-  <TD ALIGN="right">Service</TD>
-  <TD><INPUT TYPE="text" NAME="svc" VALUE="<% $hashref->{svc} %>"></TD>
-<TR>
+<& /elements/tr-input-locale-text.html,
+  'object' => $part_svc,
+  'cgi'    => $cgi,
+  'field'  => 'svc',
+  'label'  => 'Service',
+  'curr_value' => $hashref->{svc},
+&>
+%#<TR>
+%#  <TD ALIGN="right">Service</TD>
+%#  <TD><INPUT TYPE="text" NAME="svc" VALUE="<% $hashref->{svc} %>"></TD>
+%#<TR>
 
 <& /elements/tr-select-part_svc_class.html, curr_value=>$hashref->{classnum} &>
 
 <TR>
-  <TD ALIGN="right">Self-service access</TD>
+  <TH ALIGN="right">Self-service access</TD>
   <TD>
     <SELECT NAME="selfservice_access">
 % tie my %selfservice_access, 'Tie::IxHash', #false laziness w/browse/part_svc
@@ -172,12 +196,12 @@ window.onload = function() {
 
 
 <TR>
-  <TD ALIGN="right">Disable new orders</TD>
+  <TH ALIGN="right">Disable new orders</TD>
   <TD><INPUT TYPE="checkbox" NAME="disabled" VALUE="Y"<% $hashref->{disabled} eq 'Y' ? ' CHECKED' : '' %>></TD>
 </TR>
 
 <TR>
-  <TD ALIGN="right">Preserve this service on package cancellation</TD>
+  <TH ALIGN="right">Preserve this service on package cancellation</TD>
   <TD><INPUT TYPE="checkbox" NAME="preserve" VALUE="Y"<% $hashref->{'preserve'} eq 'Y' ? ' CHECKED' : '' %>>&nbsp;</TD>
 </TR>
 
@@ -240,12 +264,12 @@ my $widget = new HTML::Widgets::SelectLayers(
   #'selected_layer' => $p_svcdb,
   'selected_layer' => $hashref->{svcdb} || 'svc_acct',
   'options'        => \%svcdb,
-  'form_name'      => 'dummy',
+  'form_name'      => 'SvcEditMain',
   #'form_action'    => 'process/part_svc.cgi',
   'form_action'    => 'part_svc.cgi', #self
-  'form_elements'  => [qw( svc svcpart classnum selfservice_access
-                           disabled preserve
-                      )],
+#  'form_elements'  => [qw( svc svcpart classnum selfservice_access
+#                           disabled preserve
+#                      )],
   'html_between'   => $help,
   'layer_callback' => sub {
     include('elements/part_svc_column.html',
index fd12c61..60aaf74 100644 (file)
@@ -62,6 +62,8 @@ Example:
                       'fields' => [qw( fieldname fieldname2 )],
                     },
 
+   'process_locale' => 'fieldname', # update entries in the _msgcat table
+
    'process_upload' => {
                          'process'  => 'misc/mytable-import.html',
                           # fields to pass to the back end job, besides the 
@@ -363,12 +365,21 @@ foreach my $value ( @values ) {
 
   }
 
-  if ( !$error && $opt{'process_o2m'} ) {
-
-    my @process_o2m = ref($opt{'process_o2m'}) eq 'ARRAY'
-                           ? @{ $opt{'process_o2m'} }
-                           :  ( $opt{'process_o2m'} );
+  my @process_o2m;
+  if ( $opt{'process_o2m'} ) {
+    @process_o2m = ref($opt{'process_o2m'}) eq 'ARRAY'
+                        ? @{ $opt{'process_o2m'} }
+                        :  ( $opt{'process_o2m'} );
+  }
+  if ( $opt{'process_locale'} ) {
+    push @process_o2m,
+    {
+      'table'  => $table . '_msgcat',
+      'fields' => [ 'locale', $opt{'process_locale'} ],
+    };
+  }
 
+  if ( !$error ) {
 
     foreach my $process_o2m (@process_o2m) {
 
index b804202..c4d150b 100755 (executable)
@@ -9,6 +9,7 @@
               'edit_ext'          => 'cgi',
               'precheck_callback' => $precheck_callback,
               'args_callback'     => $args_callback,
+              'process_locale'    => 'pkg',
               'process_m2m'       => \@process_m2m,
               'process_o2m'       => \@process_o2m,
           )
@@ -310,10 +311,6 @@ foreach my $amount_param ( grep /^usagepricepart(\d+)_amount$/, $cgi->param ) {
 
 my @process_o2m = (
   {
-    'table'  => 'part_pkg_msgcat',
-    'fields' => [qw( locale pkg )],
-  },
-  {
     'table'  => 'part_pkg_usageprice',
     'fields' => [qw( price currency action target amount )],
 
index fb5e7d9..cc104a1 100644 (file)
@@ -235,7 +235,7 @@ div.fstabcontainer {
 .fsinnerbox th {
   font-weight:normal;
   font-size:80%;
-  valign: bottom;
+  vertical-align: bottom;
   color: #666666;
 }
 
index e38dde6..0c2b816 100644 (file)
@@ -98,14 +98,14 @@ function <%$key%>process () {
 
   overlib( 'Submitting job to server...', WIDTH, 444, HEIGHT, 168, CAPTION, 'Please wait...', STICKY, AUTOSTATUSCAP, CLOSETEXT, '', CLOSECLICK, MIDX, 0, MIDY, 0 );
 
+  // jQuery .serializeArray() maybe?
+  var copy_fields = <% encode_json(\%copy_fields) %>;
   var Hash = new Array();
   var x = 0;
   var fieldName;
   for (var i = 0; i<document.<%$formname%>.elements.length; i++) {
     field  = document.<%$formname%>.elements[i];
-    if ( <% join(' || ', map { "(field.name.indexOf('$_') > -1)" } @$fields ) %>
-       )
-    {
+    if ( <% $all_fields %> || copy_fields[ field.name ] ) {
         if ( field.type == 'select-multiple' ) {
           //alert('select-multiple ' + field.name);
           for (var j=0; j < field.options.length; j++) {
@@ -168,6 +168,14 @@ $progress_url->query_form(
   %dest_info,
 );
 
+my $all_fields = 0;
+my %copy_fields;
+if (grep '/^ALL$/', @$fields) {
+  $all_fields = 1;
+} else {
+  %copy_fields = map { $_ => 1 } @$fields;
+}
+
 #stupid safari is caching the "location" of popup iframs, and submitting them
 #instead of displaying them.  this should prevent that.
 my $popup_name = 'popup-'.random_id();
diff --git a/httemplate/elements/tr-input-locale-text.html b/httemplate/elements/tr-input-locale-text.html
new file mode 100644 (file)
index 0000000..110a8aa
--- /dev/null
@@ -0,0 +1,120 @@
+<%doc>
+Usage:
+
+In edit/foo.html:
+
+<& /elements/tr-input-locale-text.html,
+  cgi     => $cgi, # needed to preserve values in error redirect
+  object  => $record,
+  field   => 'myfield',
+  label   => 'My Field',
+&>
+
+And in edit/process/foo.html:
+<& elements/process.html,
+  ...
+  process_locale => 'myfield',
+&>
+
+'object' needs to be an FS::Record subclass instance for a table that has
+a '_msgcat' localization table. For a table "foo" where "foo.myfield"
+contains some customer-visible label (in the default locale),
+"foo_msgcat.myfield" contains the translation of that label for a customer
+locale. The foreign key in foo_msgcat must have the same name as the primary
+key of foo.
+
+Currently only a single field can be localized this way; including this
+element more than once in the form will lead to conflicts. This is how
+it should work; if at some point we need to localize several fields of the
+same record, we should modify this element to show multiple inputs for each
+locale.
+
+</%doc>
+<%init>
+
+my %opt = @_;
+my $object = delete $opt{object};
+my $field = delete $opt{field};
+
+# identify our locales
+my $conf = FS::Conf->new;
+my $default_locale = $conf->config('locale') || 'en_';
+my @locales = grep { ! /^$default_locale/ } $conf->config('available-locales');
+
+my $label = delete $opt{label};
+my %labels = map { $_ => "$label&mdash;".FS::Locales->description($_) }
+              @locales;
+@locales = sort { $labels{$a} cmp $labels{$b} } @locales;
+my %curr_values;
+
+# where are the msgcat records?
+my $msgcat_table = $object->table . '_msgcat';
+my $msgcat_pkey = dbdef->table($msgcat_table)->primary_key;
+my %msgcat_pkeyvals;
+
+# find existing msgcat records, if any, and record their message values
+# and pkeys
+my $pkey = $object->primary_key;
+my $pkeyval = $object->get($pkey);
+if ($pkeyval) { # of course if this is a new record there won't be any
+  my @linked = qsearch($msgcat_table, { $pkey => $pkeyval });
+  foreach (@linked) {
+    $curr_values{ $_->locale } = $_->get( $field );
+    $msgcat_pkeyvals{ $_->locale } = $_->get( $msgcat_pkey );
+  }
+}
+
+# sticky-on-error the locale inputs
+if( my $cgi = $opt{cgi} ) {
+  my $i = 0;
+  # they're named 'foomsgnum0_locale' and 'foomsgnum0_myfield'
+  while ( my $locale = $cgi->param($msgcat_pkey . $i . '_locale') ) {
+    my $value = $cgi->param($msgcat_pkey . $i . '_' . $field);
+    $curr_values{ $locale } = $value;
+    $i++;
+  }
+}
+
+# compat with tr-input-text for styling
+my $cell_style = $opt{'cell_style'} ? 'STYLE="'. $opt{'cell_style'}. '"' : '';
+
+my $colspan = $opt{'colspan'} ? 'COLSPAN="'.$opt{'colspan'}.'"' : '';
+
+
+</%init>
+% # pass through %opt on all of these to retain formatting
+% # one tr, td, and input for the default locale
+<& tr-input-text.html,
+  %opt,
+  'label' => $label,
+  'field' => $field
+&>
+% # and one for each of the others 
+% my $i = 0;
+% foreach my $locale (@locales) {
+%   my $basename = $msgcat_pkey . $i;
+%   my $lfield = $basename . '_' . $field;
+<& tr-td-label.html,
+  %opt,
+  'id' => $lfield, # uniqueness
+  'label' => $labels{$locale}
+&>
+  <TD <% $colspan %><% $cell_style %> ID="<% $lfield %>_input0">
+    <& hidden.html,
+      'field' => $basename,
+      'curr_value' => $msgcat_pkeyvals{$locale},
+      # will be empty if this is a new record and/or new locale, that's fine
+    &>
+    <& hidden.html,
+      'field' => $basename . '_locale',
+      'curr_value' => $locale,
+    &>
+    <& input-text.html,
+      %opt,
+      'field' => $lfield,
+      'curr_value' => $curr_values{$locale},
+    &>
+  </TD>
+</TR>
+%   $i++;
+% } # foreach $locale