[freeside-commits] branch FREESIDE_3_BRANCH_30783 created. 020fe5db4a3ad50dad3eebd26316821e114ac9e7

Mitch Jackson mitch at freeside.biz
Tue Oct 23 14:54:28 PDT 2018


The branch, FREESIDE_3_BRANCH_30783 has been created
        at  020fe5db4a3ad50dad3eebd26316821e114ac9e7 (commit)

- Log -----------------------------------------------------------------
commit 020fe5db4a3ad50dad3eebd26316821e114ac9e7
Author: Mitch Jackson <mitch at freeside.biz>
Date:   Sun Jul 8 20:02:25 2018 -0500

    RT# 30783 js fix for ip selection

diff --git a/httemplate/elements/tr-select-router_block_ip.html b/httemplate/elements/tr-select-router_block_ip.html
index eac41cfad..72640d3d5 100644
--- a/httemplate/elements/tr-select-router_block_ip.html
+++ b/httemplate/elements/tr-select-router_block_ip.html
@@ -4,8 +4,8 @@ var ip_addr_curr_value = <% $opt{'ip_addr'} |js_string %>;
 var blocknum_curr_value = <% $opt{'blocknum'} |js_string %>;
 
 function update_ip_addr() {
-  var routernum = $('#router_select_0').val();
-  var blocknum  = $('#router_select_1').val();
+  var routernum = $('#router_select_0').val() || "";
+  var blocknum  = $('#router_select_1').val() || "";
   var e_input_ip_addr = $('#input_ip_addr');
   var e_router_select_1 = $('#router_select_1');
 

commit 0327c04ef25a6879a85b7a4a352147f4746703ee
Author: Mitch Jackson <mitch at freeside.biz>
Date:   Fri Jun 29 13:04:33 2018 -0500

    RT# 30783 Clean up json code for free_addrs

diff --git a/FS/FS/svc_IP_Mixin.pm b/FS/FS/svc_IP_Mixin.pm
index 19c7e05fc..56165dce5 100644
--- a/FS/FS/svc_IP_Mixin.pm
+++ b/FS/FS/svc_IP_Mixin.pm
@@ -132,7 +132,7 @@ sub _used_addresses {
   #       parameter to bypass FS::Record objects creation and just
   #       return hashrefs from DBI.  200,000 hashrefs are many seconds faster
   #       than 200,000 FS::Record objects
-  my %qsearch = (
+  my %qsearch_param = (
       table     => $class->table,
       select    => $ip_field,
       hashref   => \%qsearch,
@@ -140,7 +140,8 @@ sub _used_addresses {
   );
   if ( $octets ) {
     my $block_str = join('.', (split(/\D/, $block_na->first))[0..$octets-1]);
-    $qsearch{extra_sql} .= " AND $ip_field LIKE ".dbh->quote("${block_str}.%");
+    $qsearch_param{extra_sql}
+      .= " AND $ip_field LIKE ".dbh->quote("${block_str}.%");
   }
 
   if ( $block->ip_netmask % 8 ) {
@@ -154,7 +155,7 @@ sub _used_addresses {
 
   return
     map { $_->$ip_field }
-    qsearch( \%qsearch );
+    qsearch( \%qsearch_param );
 }
 
 sub _is_used {
diff --git a/httemplate/elements/tr-select-router_block_ip.html b/httemplate/elements/tr-select-router_block_ip.html
index 535e953c4..eac41cfad 100644
--- a/httemplate/elements/tr-select-router_block_ip.html
+++ b/httemplate/elements/tr-select-router_block_ip.html
@@ -71,7 +71,7 @@ function populate_ip_select() {
 % }
   if ( blocknum && $.isNumeric(blocknum) && ! e.is(':hidden')) {
     $.getJSON(
-      '<% $p %>json/free_addresses_in_block.json.html',
+      '<% $p %>misc/xmlhttp-free_addresses_in_block.json.html',
       {blocknum: blocknum},
       function(ip_json) {
         $.each( ip_json, function(idx, val) {
diff --git a/httemplate/json/free_addresses_in_block.json.html b/httemplate/misc/xmlhttp-free_addresses_in_block.json.html
similarity index 94%
rename from httemplate/json/free_addresses_in_block.json.html
rename to httemplate/misc/xmlhttp-free_addresses_in_block.json.html
index 6785aac6b..801718d35 100644
--- a/httemplate/json/free_addresses_in_block.json.html
+++ b/httemplate/misc/xmlhttp-free_addresses_in_block.json.html
@@ -3,7 +3,7 @@
   Unless block is larger than /24 - Does somebody really want to populate
   65k addresses into a HTML selectbox?
 </%doc>
-<% encode_rest($json) %>\
+<% encode_json($json) %>\
 <%init>
 
 my $json = [];

commit e81c112f4cb6b4e617697a3308b5128ff14a5f4c
Author: Mitch Jackson <mitch at freeside.biz>
Date:   Tue Jun 26 18:18:51 2018 -0500

    RT# 30783 Clean up IP utility code

diff --git a/FS/FS/IP_Mixin.pm b/FS/FS/IP_Mixin.pm
index 07fa9e776..1967ccd57 100644
--- a/FS/FS/IP_Mixin.pm
+++ b/FS/FS/IP_Mixin.pm
@@ -268,45 +268,27 @@ sub router {
   FS::router->by_key($self->routernum);
 }
 
-=item used_addresses [ BLOCK ]
+=item used_addresses [ FS::addr_block ]
 
-Returns a list of all addresses that are in use by a service.  If called as an
-instance method, excludes that instance from the search.
+Returns a list of all addresses in use within the given L<FS::addr_block>.
 
-Does not filter by block, will return ALL used addresses. ref:f197bdbaa1
+If called as an instance method, excludes that instance from the search.
 
 =cut
 
 sub used_addresses {
-  my $self = shift;
-  my $block = shift;
-  return ( map { $_->_used_addresses($block, $self) } @subclasses );
-}
-
-sub _used_addresses {
-  my $class = shift;
-  die "$class->_used_addresses not implemented";
-}
-
-=item used_addresses_in_block [ FS::addr_block ]
-
-Returns a list of all addresses in use within the given L<FS::addr_block>
-
-=cut
-
-sub used_addresses_in_block {
   my ($self, $block) = @_;
 
   (
     $block->ip_gateway ? $block->ip_gateway : (),
     $block->NetAddr->broadcast->addr,
-    map { $_->_used_addresses_in_block($block, $self ) } @subclasses
+    map { $_->_used_addresses($block, $self ) } @subclasses
   );
 }
 
-sub _used_addresses_in_block {
+sub _used_addresses {
   my $class = shift;
-  die "$class->_used_addresses_in_block not implemented";
+  die "$class->_used_addresses not implemented";
 }
 
 =item is_used ADDRESS
diff --git a/FS/FS/addr_block.pm b/FS/FS/addr_block.pm
index 31c7cfff0..a9f7d4b02 100755
--- a/FS/FS/addr_block.pm
+++ b/FS/FS/addr_block.pm
@@ -249,7 +249,7 @@ sub free_addrs {
 
   my %used_addr_map =
     map {$_ => 1}
-    FS::IP_Mixin->used_addresses_in_block($self),
+    FS::IP_Mixin->used_addresses($self),
     FS::Conf->new()->config('exclude_ip_addr');
 
   [
@@ -285,7 +285,7 @@ sub next_free_addr {
     $selfaddr->addr,
     $selfaddr->network->addr,
     $selfaddr->broadcast->addr,
-    FS::IP_Mixin->used_addresses_in_block($self)
+    FS::IP_Mixin->used_addresses($self)
   );
 
   # just do a linear search of the block
diff --git a/FS/FS/svc_IP_Mixin.pm b/FS/FS/svc_IP_Mixin.pm
index ce9218c10..19c7e05fc 100644
--- a/FS/FS/svc_IP_Mixin.pm
+++ b/FS/FS/svc_IP_Mixin.pm
@@ -91,38 +91,21 @@ sub svc_ip_check {
 }
 
 sub _used_addresses {
+  my ($class, $block, $exclude_svc) = @_;
 
-  # Returns all addresses in use.  Does not filter with $block. ref:f197bdbaa1
-
-  my ($class, $block, $exclude) = @_;
-  my $ip_field = $class->table_info->{'ip_field'}
-    or return ();
-  # if the service doesn't have an ip_field, then it has no IP addresses 
-  # in use, yes? 
-
-  my %hash = ( $ip_field => { op => '!=', value => '' } );
-  #$hash{'blocknum'} = $block->blocknum if $block;
-  $hash{'svcnum'} = { op => '!=', value => $exclude->svcnum } if ref $exclude;
-  map { my $na = $_->NetAddr; $na ? $na->addr : () }
-    qsearch({
-        table     => $class->table,
-        hashref   => \%hash,
-        extra_sql => " AND $ip_field != '0e0'",
-    });
-}
-
-sub _used_addresses_in_block {
-  my ($class, $block) = @_;
-
-  croak "_used_addresses_in_block() requires an FS::addr_block parameter"
+  croak "_used_addresses() requires an FS::addr_block parameter"
     unless ref $block && $block->isa('FS::addr_block');
 
   my $ip_field = $class->table_info->{'ip_field'};
   if ( !$ip_field ) {
-    carp "_used_addresses_in_block() skipped, no ip_field";
+    carp "_used_addresses() skipped, no ip_field";
     return;
   }
 
+  my %qsearch = ( $ip_field => { op => '!=', value => '' });
+  $qsearch{svcnum} = { op => '!=', value => $exclude_svc->svcnum }
+    if ref $exclude_svc && $exclude_svc->svcnum;
+
   my $block_na = $block->NetAddr;
 
   my $octets;
@@ -152,7 +135,7 @@ sub _used_addresses_in_block {
   my %qsearch = (
       table     => $class->table,
       select    => $ip_field,
-      hashref   => { $ip_field => { op => '!=', value => '' }},
+      hashref   => \%qsearch,
       extra_sql => " AND $ip_field != '0e0' ",
   );
   if ( $octets ) {

commit f1b5eac395f720a40baac99b0fd04f0da198086e
Author: Mitch Jackson <mitch at freeside.biz>
Date:   Tue Jun 26 17:34:50 2018 -0500

    RT# 30783 Selectbox of available IPs when provisioning

diff --git a/FS/FS/addr_block.pm b/FS/FS/addr_block.pm
index b692259a4..31c7cfff0 100755
--- a/FS/FS/addr_block.pm
+++ b/FS/FS/addr_block.pm
@@ -240,7 +240,7 @@ sub cidr {
 
 =item free_addrs
 
-Returns a sorted list of free addresses in the block.
+Returns an aref sorted list of free addresses in the block.
 
 =cut
 
@@ -252,7 +252,11 @@ sub free_addrs {
     FS::IP_Mixin->used_addresses_in_block($self),
     FS::Conf->new()->config('exclude_ip_addr');
 
-  grep { !exists $used_addr_map{$_} } map { $_->addr } $self->NetAddr->hostenum;
+  [
+    grep { !exists $used_addr_map{$_} }
+    map { $_->addr }
+    $self->NetAddr->hostenum
+  ];
 }
 
 =item next_free_addr
diff --git a/httemplate/elements/tr-select-router_block_ip.html b/httemplate/elements/tr-select-router_block_ip.html
index 2aa715e29..535e953c4 100644
--- a/httemplate/elements/tr-select-router_block_ip.html
+++ b/httemplate/elements/tr-select-router_block_ip.html
@@ -2,34 +2,110 @@
 var manual_addr_routernum = <% encode_json(\%manual_addr_routernum) %>;
 var ip_addr_curr_value = <% $opt{'ip_addr'} |js_string %>;
 var blocknum_curr_value = <% $opt{'blocknum'} |js_string %>;
-function update_ip_addr(obj, i) {
-  var routernum = document.getElementById('router_select_0').value;
-  var select_blocknum = document.getElementById('router_select_1');
-  var blocknum = select_blocknum.value;
-  var input_ip_addr = document.getElementById('input_ip_addr');
+
+function update_ip_addr() {
+  var routernum = $('#router_select_0').val();
+  var blocknum  = $('#router_select_1').val();
+  var e_input_ip_addr = $('#input_ip_addr');
+  var e_router_select_1 = $('#router_select_1');
+
+  <% # Is block is automatically selected for this router? %>
   if ( manual_addr_routernum[routernum] == 'Y' ) {
-%# hide block selection and default ip address to its previous value
-    select_blocknum.style.display = 'none';
-    input_ip_addr.value = ip_addr_curr_value;
-  }
-  else {
-%# the reverse
-    select_blocknum.style.display = '';
-%# default ip address to null, unless the router/block are set to the 
-%# previous value, in which case default it to current value
+    show_ip_input();
+    hide_ip_select();
+    e_router_select_1.hide();
+    e_input_ip_addr.val( ip_addr_curr_value );
+  } else {
+    e_router_select_1.show();
+    e_input_ip_addr.attr('placeholder', <% mt('(automatic)') | js_string %> );
     if ( routernum == router_curr_values[0] &&
-         blocknum  == router_curr_values[1] ) {
-      input_ip_addr.value = ip_addr_curr_value;
+         blocknum == router_curr_values[1] ) {
+      e_input_ip_addr.val( ip_addr_curr_value );
     } else {
-      input_ip_addr.value = <% mt('(automatic)') |js_string %>;
+      e_input_ip_addr.val('');
     }
   }
+  show_or_hide_toggle_ip();
+  populate_ip_select();
+}
+
+function toggle_ip_input() {
+  if ( $('#input_ip_addr').is(':hidden') ) {
+    show_ip_input();
+  } else {
+    show_ip_select();
+  }
+}
+
+function show_ip_input() {
+  $('#input_ip_addr').show();
+  $('#select_ip_addr').hide();
+  depopulate_ip_select();
+}
+
+function show_ip_select() {
+  var e_input_ip_addr = $('#input_ip_addr');
+  var e_select_ip_addr = $('#select_ip_addr');
+
+  e_select_ip_addr.width( e_input_ip_addr.width() );
+  e_input_ip_addr.hide();
+  e_select_ip_addr.show();
+  populate_ip_select();
+}
+
+function populate_ip_select() {
+  depopulate_ip_select();
+  var e = $('#select_ip_addr');
+  var blocknum = $('#router_select_1').val();
+
+  var opts = [ '<option value="">loading...</option>' ];
+  e.html(opts.join(''));
+
+% if ( $opt{ip_addr} ) {
+  opts = [
+    '<option value="<% $opt{ip_addr} |h %>"><% $opt{ip_addr} |h %></option>',
+    '<option value="">-----------</option>'
+  ];
+% } else {
+  opts = [ '<option value=""><% mt('(automatic)') |h %></option>' ];
+% }
+  if ( blocknum && $.isNumeric(blocknum) && ! e.is(':hidden')) {
+    $.getJSON(
+      '<% $p %>json/free_addresses_in_block.json.html',
+      {blocknum: blocknum},
+      function(ip_json) {
+        $.each( ip_json, function(idx, val) {
+          opts.push(
+            '<option' + (val == ip_addr_curr_value ? 'selected' : '') + '>'
+            + val
+            + '</option>'
+          );
+        });
+        e.html(opts.join(''));
+      }
+    );
+  }
 }
-function clearhint_ip_addr (what) {
-  if ( what.value == <% mt('(automatic)') |js_string %> )
-    what.value = '';
+
+function depopulate_ip_select() {
+  $('#select_ip_addr').children().remove();
 }
+
+function propogate_ip_select() {
+  $('#input_ip_addr').val( $('#select_ip_addr').val() );
+}
+
+function show_or_hide_toggle_ip() {
+  if ( $('#router_select_1').val() ) {
+    $('#toggle_ip').show();
+  } else {
+    show_ip_input();
+    $('#toggle_ip').hide();
+  }
+}
+
 </script>
+
 <& /elements/tr-td-label.html, label => ($opt{'label'} || 'Router'), required => $opt{'required'} &>
 <td>
   <& /elements/select-tiered.html, prefix => 'router_', tiers => [
@@ -58,14 +134,20 @@ function clearhint_ip_addr (what) {
 </td></tr>
 <& /elements/tr-td-label.html, label => ($opt{'ip_addr_label'} || 'IP address'), required => $opt{'ip_addr_required'} &>
 <td>
-% #warn Dumper \%fixed;
 % if ( exists $fixed{$ip_field} ) {
   <input type="hidden" id="input_ip_addr" name="<% $ip_field %>" 
     value="<% $opt{'ip_addr'} |h%>"><% $opt{'ip_addr'} || '' %>
 % }
 % else {
-  <input type="text" id="input_ip_addr" name="<% $ip_field %>" 
-  value="<% $opt{'ip_addr'} |h%>" onfocus="clearhint_ip_addr(this)">
+    <input type="text"
+           id="input_ip_addr"
+           name="<% $ip_field %>"
+           value="<% $opt{'ip_addr'} | h %>"
+           onfocus="clearhint_ip_addr(this)">
+    <select id="select_ip_addr" style="display: none;" onChange='javascript:propogate_ip_select();'>
+      <option><% mt('loading') |h %>...</option>
+    </select>
+    <button type="button" onClick='javascript:toggle_ip_input();' id="toggle_ip" style="display: none;">▼</button>
 % }
 </td> </tr>
 <script type="text/javascript">
diff --git a/httemplate/json/free_addresses_in_block.json.html b/httemplate/json/free_addresses_in_block.json.html
new file mode 100644
index 000000000..6785aac6b
--- /dev/null
+++ b/httemplate/json/free_addresses_in_block.json.html
@@ -0,0 +1,18 @@
+<%doc>
+  Return a json array containing all free ip addresses within a given block
+  Unless block is larger than /24 - Does somebody really want to populate
+  65k addresses into a HTML selectbox?
+</%doc>
+<% encode_rest($json) %>\
+<%init>
+
+my $json = [];
+
+my $blocknum = $cgi->param('blocknum');
+
+my $addr_block = qsearchs( addr_block => { blocknum => $blocknum });
+
+$json = $addr_block->free_addrs
+  if ref $addr_block && $addr_block->ip_netmask >= 24;
+
+</%init>

commit 8162d15e07f1f998caa4ce522c732d1bc3211ac7
Author: Mitch Jackson <mitch at freeside.biz>
Date:   Mon Jun 25 14:09:42 2018 -0500

    RT# 30783 Improve speed of ip address auto-assignment

diff --git a/FS/FS/addr_block.pm b/FS/FS/addr_block.pm
index a39e1f1bb..b692259a4 100755
--- a/FS/FS/addr_block.pm
+++ b/FS/FS/addr_block.pm
@@ -281,7 +281,7 @@ sub next_free_addr {
     $selfaddr->addr,
     $selfaddr->network->addr,
     $selfaddr->broadcast->addr,
-    FS::IP_Mixin->used_addresses($self)
+    FS::IP_Mixin->used_addresses_in_block($self)
   );
 
   # just do a linear search of the block

commit 2ee7f0c27233d800254d5244fc5913d881b48800
Author: Mitch Jackson <mitch at freeside.biz>
Date:   Mon Jun 25 14:07:52 2018 -0500

    RT# 30783 Add network block enumerating utils

diff --git a/FS/FS/IP_Mixin.pm b/FS/FS/IP_Mixin.pm
index 8920cebc5..07fa9e776 100644
--- a/FS/FS/IP_Mixin.pm
+++ b/FS/FS/IP_Mixin.pm
@@ -270,9 +270,10 @@ sub router {
 
 =item used_addresses [ BLOCK ]
 
-Returns a list of all addresses (in BLOCK, or in all blocks)
-that are in use.  If called as an instance method, excludes 
-that instance from the search.
+Returns a list of all addresses that are in use by a service.  If called as an
+instance method, excludes that instance from the search.
+
+Does not filter by block, will return ALL used addresses. ref:f197bdbaa1
 
 =cut
 
@@ -287,6 +288,27 @@ sub _used_addresses {
   die "$class->_used_addresses not implemented";
 }
 
+=item used_addresses_in_block [ FS::addr_block ]
+
+Returns a list of all addresses in use within the given L<FS::addr_block>
+
+=cut
+
+sub used_addresses_in_block {
+  my ($self, $block) = @_;
+
+  (
+    $block->ip_gateway ? $block->ip_gateway : (),
+    $block->NetAddr->broadcast->addr,
+    map { $_->_used_addresses_in_block($block, $self ) } @subclasses
+  );
+}
+
+sub _used_addresses_in_block {
+  my $class = shift;
+  die "$class->_used_addresses_in_block not implemented";
+}
+
 =item is_used ADDRESS
 
 Returns a string describing what object is using ADDRESS, or 
diff --git a/FS/FS/addr_block.pm b/FS/FS/addr_block.pm
index f07de490b..a39e1f1bb 100755
--- a/FS/FS/addr_block.pm
+++ b/FS/FS/addr_block.pm
@@ -238,6 +238,23 @@ sub cidr {
   $self->NetAddr->cidr;
 }
 
+=item free_addrs
+
+Returns a sorted list of free addresses in the block.
+
+=cut
+
+sub free_addrs {
+  my $self = shift;
+
+  my %used_addr_map =
+    map {$_ => 1}
+    FS::IP_Mixin->used_addresses_in_block($self),
+    FS::Conf->new()->config('exclude_ip_addr');
+
+  grep { !exists $used_addr_map{$_} } map { $_->addr } $self->NetAddr->hostenum;
+}
+
 =item next_free_addr
 
 Returns a NetAddr::IP object corresponding to the first unassigned address 
@@ -433,4 +450,3 @@ now because that's the smallest block that makes any sense at all.
 =cut
 
 1;
-
diff --git a/FS/FS/svc_IP_Mixin.pm b/FS/FS/svc_IP_Mixin.pm
index 8b2b5f17e..ce9218c10 100644
--- a/FS/FS/svc_IP_Mixin.pm
+++ b/FS/FS/svc_IP_Mixin.pm
@@ -3,7 +3,8 @@ use base 'FS::IP_Mixin';
 
 use strict;
 use NEXT;
-use FS::Record qw(qsearchs qsearch);
+use Carp qw(croak carp);
+use FS::Record qw(qsearchs qsearch dbh);
 use FS::Conf;
 use FS::router;
 use FS::part_svc_router;
@@ -90,6 +91,9 @@ sub svc_ip_check {
 }
 
 sub _used_addresses {
+
+  # Returns all addresses in use.  Does not filter with $block. ref:f197bdbaa1
+
   my ($class, $block, $exclude) = @_;
   my $ip_field = $class->table_info->{'ip_field'}
     or return ();
@@ -107,6 +111,69 @@ sub _used_addresses {
     });
 }
 
+sub _used_addresses_in_block {
+  my ($class, $block) = @_;
+
+  croak "_used_addresses_in_block() requires an FS::addr_block parameter"
+    unless ref $block && $block->isa('FS::addr_block');
+
+  my $ip_field = $class->table_info->{'ip_field'};
+  if ( !$ip_field ) {
+    carp "_used_addresses_in_block() skipped, no ip_field";
+    return;
+  }
+
+  my $block_na = $block->NetAddr;
+
+  my $octets;
+  if ($block->ip_netmask >= 24) {
+    $octets = 3;
+  } elsif ($block->ip_netmask >= 16) {
+    $octets = 2;
+  } elsif ($block->ip_netmask >= 8) {
+    $octets = 1;
+  }
+
+  #  e.g.
+  # SELECT ip_addr
+  # FROM svc_broadband
+  # WHERE ip_addr != ''
+  #   AND ip_addr != '0e0'
+  #   AND ip_addr LIKE '10.0.2.%';
+  #
+  # For /24, /16 and /8 this approach is fast, even when svc_broadband table
+  # contains 650,000+ ip records.  For other allocations, this approach is
+  # not speedy, but usable.
+  #
+  # Note: A use case like this would could greatly benefit from a qsearch()
+  #       parameter to bypass FS::Record objects creation and just
+  #       return hashrefs from DBI.  200,000 hashrefs are many seconds faster
+  #       than 200,000 FS::Record objects
+  my %qsearch = (
+      table     => $class->table,
+      select    => $ip_field,
+      hashref   => { $ip_field => { op => '!=', value => '' }},
+      extra_sql => " AND $ip_field != '0e0' ",
+  );
+  if ( $octets ) {
+    my $block_str = join('.', (split(/\D/, $block_na->first))[0..$octets-1]);
+    $qsearch{extra_sql} .= " AND $ip_field LIKE ".dbh->quote("${block_str}.%");
+  }
+
+  if ( $block->ip_netmask % 8 ) {
+    # Some addresses returned by qsearch may be outside the network block,
+    # so each ip address is tested to be in the block before it's returned.
+    return
+      grep { $block_na->contains( NetAddr::IP->new( $_ ) ) }
+      map { $_->$ip_field }
+      qsearch( \%qsearch );
+  }
+
+  return
+    map { $_->$ip_field }
+    qsearch( \%qsearch );
+}
+
 sub _is_used {
   my ($class, $addr, $exclude) = @_;
   my $ip_field = $class->table_info->{'ip_field'}

-----------------------------------------------------------------------




More information about the freeside-commits mailing list