Refactoring: Parameterredesign SL::Controller::Base::render und SL::Presenter::render
authorMoritz Bunkus <m.bunkus@linet-services.de>
Fri, 1 Feb 2013 11:12:16 +0000 (12:12 +0100)
committerMoritz Bunkus <m.bunkus@linet-services.de>
Fri, 1 Feb 2013 12:40:35 +0000 (13:40 +0100)
Conflicts:
SL/Controller/FinancialControllingReport.pm
SL/Controller/ProjectType.pm

30 files changed:
SL/Controller/Base.pm
SL/Controller/CustomVariableConfig.pm
SL/Controller/Customer.pm
SL/Controller/DeliveryPlan.pm
SL/Controller/Layout.pm
SL/Controller/LoginScreen.pm
SL/Controller/Part.pm
SL/Controller/PaymentTerm.pm
SL/Controller/PriceFactor.pm
SL/Controller/Project.pm
SL/Controller/Unit.pm
SL/Controller/Warehouse.pm
SL/Dispatcher.pm
SL/JSON.pm [new file with mode: 0644]
SL/Layout/Javascript.pm
SL/Layout/MenuLeft.pm
SL/Layout/Top.pm
SL/Layout/V3.pm
SL/Presenter.pm
SL/Presenter/EscapedText.pm
SL/Template/Plugin/JSON.pm
t/controllers/base/render.t [new file with mode: 0644]
t/presenter/base/render.t [new file with mode: 0644]
t/test.sh
templates/webpages/ct/ajax_autocomplete2.html [deleted file]
templates/webpages/ct/ajax_autocomplete2.json [new file with mode: 0644]
templates/webpages/part/ajax_autocomplete.html [deleted file]
templates/webpages/part/ajax_autocomplete.json [new file with mode: 0644]
templates/webpages/t/render.html [new file with mode: 0644]
templates/webpages/t/render.js [new file with mode: 0644]

index d6bfd42..aa65645 100644 (file)
@@ -60,35 +60,69 @@ sub render {
   my $template           = shift;
   my ($options, %locals) = (@_ && ref($_[0])) ? @_ : ({ }, @_);
 
-  $options->{type}       = lc($options->{type} || 'html');
-  $options->{no_layout}  = 1 if $options->{type} eq 'js';
+  # Set defaults for all available options.
+  my %defaults = (
+    type       => 'html',
+    output     => 1,
+    header     => 1,
+    layout     => 1,
+    process    => 1,
+  );
+  $options->{$_} //= $defaults{$_} for keys %defaults;
+  $options->{type} = lc $options->{type};
+
+  # Check supplied options for validity.
+  foreach (keys %{ $options }) {
+    croak "Unsupported option: $_" unless $defaults{$_};
+  }
+
+  # Only certain types are supported.
+  croak "Unsupported type: " . $options->{type} unless $options->{type} =~ m/^(?:html|js|json)$/;
+
+  # The "template" argument must be a string or a reference to one.
+  croak "Unsupported 'template' reference type: " . ref($template) if ref($template) && (ref($template) !~ m/^(?:SCALAR|SL::Presenter::EscapedText)$/);
+
+  # If all output is turned off then don't output the header either.
+  if (!$options->{output}) {
+    $options->{header} = 0;
+    $options->{layout} = 0;
+
+  } else {
+    # Layout only makes sense if we're outputting HTML.
+    $options->{layout} = 0 if $options->{type} ne 'html';
+  }
+
+  if ($options->{header}) {
+    # Output the HTTP response and the layout in case of HTML output.
 
-  if (!$options->{partial} && !$options->{inline} && !$::form->{header}) {
-    if ($options->{no_layout}) {
+    if ($options->{layout}) {
+      $::form->{title} = $locals{title} if $locals{title};
+      $::form->header;
+
+    } else {
+      # No layout: just the standard HTTP response. Also notify
+      # $::form that the header has already been output so that
+      # $::form->header() won't output it again.
       $::form->{header} = 1;
-      my $content_type  = $options->{type} eq 'js' ? 'text/javascript' : 'text/html';
+      my $content_type  = $options->{type} eq 'html' ? 'text/html'
+                        : $options->{type} eq 'js'   ? 'text/javascript'
+                        :                              'application/json';
 
       print $::form->create_http_response(content_type => $content_type,
                                           charset      => $::lx_office_conf{system}->{dbcharset} || Common::DEFAULT_CHARSET());
-
-    } else {
-      $::form->{title} = $locals{title} if $locals{title};
-      $::form->header(no_menu => $options->{no_menu});
     }
   }
 
-  my $output;
-  if ($options->{raw}) {
-    $output = $$template;
-  } else {
-    $output = $self->presenter->render(
-      $template, $options,
-      %locals,
-      SELF => $self,
-    );
-  }
+  # Let the presenter do the rest of the work.
+  my $output = $self->presenter->render(
+    $template,
+    { type => $options->{type}, process => $options->{process} },
+    %locals,
+    SELF => $self,
+  );
 
-  print $output unless $options->{inline} || $options->{no_output};
+  # Print the output if wanted.
+  print $output if $options->{output};
 
   return $output;
 }
@@ -334,46 +368,64 @@ C<Form::parse_html_template> does.
 C<$options>, if present, must be a hash reference. All remaining
 parameters are slurped into C<%locals>.
 
-What is rendered and how C<$template> is interpreted is determined by
-the options I<type>, I<inline>, I<partial> and I<no_layout>. The
+What is rendered and how C<$template> is interpreted is determined
+both by C<$template>'s reference type and by the supplied options. The
 actual rendering is handled by L<SL::Presenter/render>.
 
-If C<< $options->{inline} >> is trueish then C<$template> is a string
-containing the template code to interprete. Additionally the output
-will not be sent to the browser. Instead it is only returned to the
-caller.
+If C<$template> is a normal scalar (not a reference) then it is meant
+to be a template file name relative to the C<templates/webpages>
+directory. The file name to use is determined by the C<type> option.
+
+If C<$template> is a reference to a scalar then the referenced
+scalar's content is used as the content to process. The C<type> option
+is not considered in this case.
+
+Other reference types, unknown options and unknown arguments to the
+C<type> option cause the function to L<croak>.
+
+The following options are available (defaults: C<type> = 'html',
+C<process> = 1, C<output> = 1, C<header> = 1, C<layout> = 1):
+
+=over 2
+
+=item C<type>
 
-If C<< $options->{raw} >> is trueish, the function will treat the
-input as already parsed, and will not filter the input through
-Template. This also means that L<SL::Presenter/render> is not
-called either. Unlike C<inline>, the input is taken as a reference.
+The template type. Can be C<html> (the default), C<js> for JavaScript
+or C<json> for JSON content. Affects the extension that's added to the
+file name given with a non-reference C<$template> argument, the
+content type HTTP header that is output and whether or not the layout
+will be output as well (see description of C<layout> below).
 
-If C<< $options->{inline} >> is falsish then C<$template> is
-interpreted as the name of a template file. It is prefixed with
-"templates/webpages/" and postfixed with a file extension based on
-C<< $options->{type} >>. C<< $options->{type} >> can be either C<html>
-or C<js> and defaults to C<html>. An exception will be thrown if that
-file does not exist.
+=item C<process>
 
-If C<< $options->{partial} >> or C<< $options->{inline} >> is trueish
-then neither the HTTP response header nor the standard HTML header is
-generated.
+If trueish (which is also the default) it causes the template/content
+to be processed by the Template toolkit. Otherwise the
+template/content is output as-is.
 
-Otherwise at least the HTTP response header will be generated based on
-the template type (C<< $options->{type} >>).
+=item C<output>
 
-If the template type is C<html> then the standard HTML header will be
-output via C<< $::form->header >> with C<< $::form->{title} >> set to
-C<$locals{title}> (the latter only if C<$locals{title}> is
-trueish). Setting C<< $options->{no_layout} >> to trueish will prevent
-this.
+If trueish (the default) then the generated output will be sent to the
+browser in addition to being returned. If falsish then the options
+C<header> and C<layout> are set to 0 as well.
+
+=item C<header>
+
+Determines whether or not to output the HTTP response
+headers. Defaults to the same value that C<output> is set to. If set
+to falsish then the layout is not output either.
+
+=item C<layout>
+
+Determines whether or not the basic HTML layout structure should be
+output (HTML header, common JavaScript and stylesheet inclusions, menu
+etc.). Defaults to 0 if C<type> is not C<html> and to the same value
+C<header> is set to otherwise.
+
+=back
 
 The template itself has access to several variables. These are listed
 in the documentation to L<SL::Presenter/render>.
 
-Unless C<< $options->{inline} >> is trueish the function will send the
-output to the browser.
-
 The function will always return the output.
 
 Example: Render a HTML template with a certain title and a few locals
@@ -383,12 +435,13 @@ Example: Render a HTML template with a certain title and a few locals
                 TODO_ITEMS => SL::DB::Manager::Todo->get_all_sorted);
 
 Example: Render a string and return its content for further processing
-by the calling function. No header is generated due to C<inline>.
+by the calling function. No header is generated due to C<output>.
 
-  my $content = $self->render('[% USE JavaScript %][% JavaScript.replace_with("#someid", "js/something") %]',
-                              { type => 'js', inline => 1 });
+  my $content = $self->render(\'[% USE JavaScript %][% JavaScript.replace_with("#someid", "js/something") %]',
+                              { output => 0 });
 
-Example: Render a JavaScript template and send it to the
+Example: Render a JavaScript template
+"templates/webpages/todo/single_item.js" and send it to the
 browser. Typical use for actions called via AJAX:
 
   $self->render('todo/single_item', { type => 'js' },
index 9a151da..6f29f52 100644 (file)
@@ -17,7 +17,7 @@ sub action_reorder {
 
   SL::DB::CustomVariableConfig->reorder_list(@{ $::form->{cvarcfg_id} || [] });
 
-  $self->render('1;', { type => 'js', inline => 1 });
+  $self->render(\'', { type => 'json' });
 }
 
 #
index 4358080..823af2e 100644 (file)
@@ -22,6 +22,5 @@ sub action_ajax_autocomplete {
   $self->{customers} = SL::DB::Manager::Customer->get_all(query => [ @filter ], limit => $limit);
   $self->{value} = $::form->{column} || 'name';
 
-  $self->render('ct/ajax_autocomplete2', { no_layout => 1 });
+  $self->render('ct/ajax_autocomplete2', { layout => 0, type => 'json' });
 }
-
index db166e3..83081f4 100644 (file)
@@ -177,8 +177,8 @@ sub prepare_report {
     controller_class      => 'DeliveryPlan',
     output_format         => 'HTML',
     top_info_text         => $::locale->text('Delivery Plan for currently outstanding sales orders'),
-    raw_top_info_text     => $self->render('delivery_plan/report_top',    { no_output => 1, partial => 1 }),
-    raw_bottom_info_text  => $self->render('delivery_plan/report_bottom', { no_output => 1, partial => 1 }),
+    raw_top_info_text     => $self->render('delivery_plan/report_top',    { output => 0 }),
+    raw_bottom_info_text  => $self->render('delivery_plan/report_bottom', { output => 0 }),
     title                 => $::locale->text('Delivery Plan'),
     allow_pdf_export      => 1,
     allow_csv_export      => 1,
index 98c012c..d5d0988 100644 (file)
@@ -3,7 +3,7 @@ package SL::Controller::Layout;
 use strict;
 use parent qw(SL::Controller::Base);
 
-use JSON ();
+use SL::JSON ();
 
 sub action_empty {
   my ($self) = @_;
@@ -20,7 +20,7 @@ sub action_empty {
       stylesheets_inline => [ $::request->{layout}->stylesheets_inline ],
     };
 
-    $self->render(\ JSON::to_json($layout), { type => 'js', raw => 1 });
+    $self->render(\ SL::JSON::to_json($layout), { type => 'json', process => 0 });
   }
 }
 
index 81ad8dd..13b7b8a 100644 (file)
@@ -20,7 +20,7 @@ sub action_user_login {
   return if $self->_redirect_to_main_script_if_already_logged_in;
 
   # Otherwise show the login form.
-  $self->render('login_screen/user_login', { no_menu => 1 }, error => error_state($::form->{error}));
+  $self->render('login_screen/user_login', error => error_state($::form->{error}));
 }
 
 sub action_logout {
@@ -28,7 +28,7 @@ sub action_logout {
 
   $::auth->destroy_session;
   $::auth->create_or_refresh_session;
-  $self->render('login_screen/user_login', { no_menu => 1 }, error => $::locale->text('You are logged out!'));
+  $self->render('login_screen/user_login', error => $::locale->text('You are logged out!'));
 }
 
 sub action_login {
@@ -57,7 +57,7 @@ sub action_login {
   # Other login errors.
   if (0 > $result) {
     $::auth->punish_wrong_login;
-    return $self->render('login_screen/user_login', { no_menu => 1 }, error => $::locale->text('Incorrect username or password!'));
+    return $self->render('login_screen/user_login', error => $::locale->text('Incorrect username or password!'));
   }
 
   # Everything is fine.
index 36859e7..6f3a136 100644 (file)
@@ -23,7 +23,7 @@ sub action_ajax_autocomplete {
   $self->{parts} = SL::DB::Manager::Part->get_all(query => [ @filter ], limit => $limit);
   $self->{value} = $::form->{column} || 'description';
 
-  $self->render('part/ajax_autocomplete', { no_layout => 1 });
+  $self->render('part/ajax_autocomplete', { layout => 0, type => 'json' });
 }
 
 
index df86252..b504bb7 100644 (file)
@@ -70,7 +70,7 @@ sub action_reorder {
 
   SL::DB::PaymentTerm->reorder_list(@{ $::form->{payment_term_id} || [] });
 
-  $self->render('1;', { type => 'js', inline => 1 });
+  $self->render(\'', { type => 'json' });
 }
 
 #
index 5def8aa..5f220cb 100644 (file)
@@ -17,7 +17,7 @@ sub action_reorder {
 
   SL::DB::PriceFactor->reorder_list(@{ $::form->{price_factor_id} || [] });
 
-  $self->render('1;', { type => 'js', inline => 1 });
+  $self->render(\'', { type => 'json' });
 }
 
 #
index 18523b7..386d755 100644 (file)
@@ -262,7 +262,7 @@ sub prepare_report {
     controller_class      => 'Project',
     output_format         => 'HTML',
     top_info_text         => $::locale->text('Projects'),
-    raw_bottom_info_text  => $self->render('project/report_bottom', { no_output => 1, partial => 1 }),
+    raw_bottom_info_text  => $self->render('project/report_bottom', { output => 0 }),
     title                 => $::locale->text('Projects'),
     allow_pdf_export      => 1,
     allow_csv_export      => 1,
index ae6e313..fc1fa2e 100644 (file)
@@ -17,7 +17,7 @@ sub action_reorder {
 
   SL::DB::Unit->reorder_list(@{ $::form->{unit_id} || [] });
 
-  $self->render('1;', { type => 'js', inline => 1 });
+  $self->render(\'', { type => 'json' });
 }
 
 #
index 33c8bfc..3f8b152 100644 (file)
@@ -17,7 +17,7 @@ sub action_reorder {
 
   SL::DB::Warehouse->reorder_list(@{ $::form->{warehouse_id} || [] });
 
-  $self->render('1;', { type => 'js', inline => 1 });
+  $self->render(\'', { type => 'json' });
 }
 
 #
index d01a0d0..44631a0 100644 (file)
@@ -82,7 +82,7 @@ sub show_error {
   $::form->{error}         = $::locale->text('The session is invalid or has expired.') if ($error_type eq 'session');
   $::form->{error}         = $::locale->text('Incorrect password!')                    if ($error_type eq 'password');
 
-  $::form->header(no_menu => 1);
+  $::form->header;
   print $::form->parse_html_template($template, \%params);
   $::lxdebug->leave_sub;
 
diff --git a/SL/JSON.pm b/SL/JSON.pm
new file mode 100644 (file)
index 0000000..6602b39
--- /dev/null
@@ -0,0 +1,90 @@
+package SL::JSON;
+
+use strict;
+
+use JSON ();
+
+use parent qw(Exporter);
+our @EXPORT = qw(encode_json decode_json to_json from_json);
+
+sub new {
+  shift;
+  return JSON->new(@_)->convert_blessed(1);
+}
+
+sub encode_json {
+  return JSON->new->convert_blessed(1)->encode(@_);
+}
+
+sub decode_json {
+  goto &JSON::decode_json;
+}
+
+sub to_json {
+  my ($object, $options)      = @_;
+  $options                  ||= {};
+  $options->{convert_blessed} = 1;
+  return JSON::to_json($object, $options);
+}
+
+sub from_json {
+  goto &JSON::decode_json;
+}
+
+1;
+__END__
+
+=pod
+
+=encoding utf8
+
+=head1 NAME
+
+SL::JSON - Thin wrapper around the JSON module that provides default options
+
+=head1 SYNOPSIS
+
+  use SL::JSON;
+
+  my $escaped_text_object = SL::Presenter->get->render('some/template');
+  my $json = encode_json($escaped_text_object);
+
+=head1 OVERVIEW
+
+JSON by default does not dump or stringify blessed
+objects. kivitendo's rendering infrastructure always returns thin
+proxy objects as instances of L<SL::Presenter::EscapedText>. This
+module provides the same functions that L<JSON> does but changes their
+default regarding converting blessed arguments.
+
+=head1 FUNCTIONS
+
+=over 4
+
+=item C<decode_json $json>
+
+Same as L<JSON/decode_json>.
+
+=item C<encode_json $object>
+
+Same as L<JSON/encode_json> but sets C<convert_blessed> first.
+
+=item C<from_json $object [, $options]>
+
+Same as L<JSON/from_json>.
+
+=item C<to_json $object [, $options ]>
+
+Same as L<JSON/to_json> but sets C<convert_blessed> first.
+
+=back
+
+=head1 BUGS
+
+Nothing here yet.
+
+=head1 AUTHOR
+
+Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
+
+=cut
index f3a2f0f..c63b1f7 100644 (file)
@@ -47,7 +47,7 @@ sub display {
   $callback               = URI->new($callback)->rel($callback) if $callback;
   $callback               = "login.pl?action=company_logo"      if $callback =~ /^(\.\/)?$/;
 
-  $self->render("menu/menunew", { partial => 1, no_output => 1 },
+  $self->render("menu/menunew", { output => 0 },
     force_ul_width  => 1,
     date            => $self->clock_line,
     menu_items      => $self->acc_menu,
index c0e937a..4cd79c4 100644 (file)
@@ -14,7 +14,7 @@ sub stylesheets {
 sub javascripts_inline {
   my $self = shift;
   my $sections = [ section_menu($self->menu) ];
-  $self->render('menu/menu', { partial => 1, no_output => 1 },
+  $self->render('menu/menu', { output => 0 },
     sections  => $sections,
   )
 }
index 075e2f2..9c4fdea 100644 (file)
@@ -6,7 +6,7 @@ use parent qw(SL::Layout::Base);
 sub pre_content {
   my ($self) = @_;
 
-  $self->SUPER::render('menu/header', { partial => 1, no_output => 1 },
+  $self->SUPER::render('menu/header', { output => 0 },
                 now        => DateTime->now_local,
                 is_fastcgi => scalar($::dispatcher->interface_type =~ /fastcgi/i),
                 is_links   => scalar($ENV{HTTP_USER_AGENT}         =~ /links/i));
index a594240..9d5e37d 100644 (file)
@@ -164,7 +164,7 @@ sub render {
   $callback               = URI->new($callback)->rel($callback) if $callback;
   $callback               = "login.pl?action=company_logo"      if $callback =~ /^(\.\/)?$/;
 
-  $self->SUPER::render('menu/menuv3', { no_menu => 1, no_output => 1 },
+  $self->SUPER::render('menu/menuv3', { output => 0 },
     force_ul_width => 1,
     date           => $self->clock_line,
     menu           => $self->print_menu,
index ec36c3b..68ef8c8 100644 (file)
@@ -25,17 +25,54 @@ sub render {
   my $template           = shift;
   my ($options, %locals) = (@_ && ref($_[0])) ? @_ : ({ }, @_);
 
-  $options->{type}       = lc($options->{type} || 'html');
+  # Set defaults for all available options.
+  my %defaults = (
+    type       => 'html',
+    process    => 1,
+  );
+  $options->{$_} //= $defaults{$_} for keys %defaults;
+  $options->{type} = lc $options->{type};
 
-  my $source;
-  if ($options->{inline}) {
-    $source = \$template;
+  # Check supplied options for validity.
+  foreach (keys %{ $options }) {
+    croak "Unsupported option: $_" unless $defaults{$_};
+  }
 
-  } else {
+  # Only certain types are supported.
+  croak "Unsupported type: " . $options->{type} unless $options->{type} =~ m/^(?:html|js|json)$/;
+
+  # The "template" argument must be a string or a reference to one.
+  croak "Unsupported 'template' reference type: " . ref($template) if ref($template) && (ref($template) !~ m/^(?:SCALAR|SL::Presenter::EscapedText)$/);
+
+  # Look for the file given by $template if $template is not a reference.
+  my $source;
+  if (!ref $template) {
     $source = "templates/webpages/${template}." . $options->{type};
     croak "Template file ${source} not found" unless -f $source;
+
+  } elsif (ref($template) eq 'SCALAR') {
+    # Normal scalar reference: hand over to Template
+    $source = $template;
+
+  } else {
+    # Instance of SL::Presenter::EscapedText. Get reference to its content.
+    $source = \$template->{text};
+  }
+
+  # If no processing is requested then return the content.
+  if (!$options->{process}) {
+    # If $template is a reference then don't try to read a file.
+    return SL::Presenter::EscapedText->new(text => ${ $template }, is_escaped => 1) if ref $template;
+
+    # Otherwise return the file's content.
+    my $file    = IO::File->new($source, "r") || croak("Template file ${source} could not be read");
+    my $content = do { local $/ = ''; <$file> };
+    $file->close;
+
+    return SL::Presenter::EscapedText->new(text => $content, is_escaped => 1);
   }
 
+  # Processing was requested. Set up all variables.
   my %params = ( %locals,
                  AUTH          => $::auth,
                  FLASH         => $::form->{FLASH},
@@ -147,20 +184,40 @@ controller. Therefore the presenter's L<render> function does not use
 all of the parameters for controlling the output that the controller's
 function does.
 
-What is rendered and how C<$template> is interpreted is determined by
-the options I<type> and I<inline>.
+What is rendered and how C<$template> is interpreted is determined
+both by C<$template>'s reference type and by the supplied options.
+
+If C<$template> is a normal scalar (not a reference) then it is meant
+to be a template file name relative to the C<templates/webpages>
+directory. The file name to use is determined by the C<type> option.
 
-If C<< $options->{inline} >> is trueish then C<$template> is a string
-containing the template code to interprete.
+If C<$template> is a reference to a scalar then the referenced
+scalar's content is used as the content to process. The C<type> option
+is not considered in this case.
 
-If C<< $options->{inline} >> is falsish then C<$template> is
-interpreted as the name of a template file. It is prefixed with
-"templates/webpages/" and postfixed with a file extension based on
-C<< $options->{type} >>. C<< $options->{type} >> can be either C<html>
-or C<js> and defaults to C<html>. An exception will be thrown if that
-file does not exist.
+Other reference types, unknown options and unknown arguments to the
+C<type> option cause the function to L<croak>.
 
-The template itself has access to the following variables:
+The following options are available:
+
+=over 2
+
+=item C<type>
+
+The template type. Can be C<html> (the default), C<js> for JavaScript
+or C<json> for JSON content. Affects only the extension that's added
+to the file name given with a non-reference C<$template> argument.
+
+=item C<process>
+
+If trueish (which is also the default) it causes the template/content
+to be processed by the Template toolkit. Otherwise the
+template/content is returned as-is.
+
+=back
+
+If template processing is requested then the template has access to
+the following variables:
 
 =over 2
 
@@ -197,9 +254,14 @@ Example: Render a HTML template with a certain title and a few locals
 Example: Render a string and return its content for further processing
 by the calling function.
 
-  my $content = $presenter->render(
-    '[% USE JavaScript %][% JavaScript.replace_with("#someid", "js/something") %]',
-    { type => 'js', inline => 1 }
+  my $content = $presenter->render(\'[% USE JavaScript %][% JavaScript.replace_with("#someid", "js/something") %]');
+
+Example: Return the content of a JSON template file without processing
+it at all:
+
+  my $template_content = $presenter->render(
+    'customer/contact',
+    { type => 'json', process => 0 }
   );
 
 =item C<escape $text>
index d482e8a..c8c78a0 100644 (file)
@@ -2,6 +2,8 @@ package SL::Presenter::EscapedText;
 
 use strict;
 
+use JSON ();
+
 use overload '""' => \&escaped;
 
 sub new {
@@ -20,6 +22,10 @@ sub escaped {
   return $self->{text};
 }
 
+sub TO_JSON {
+  goto &escaped;
+}
+
 1;
 __END__
 
index 1fe8f19..0586511 100644 (file)
@@ -21,7 +21,7 @@ sub json_converter {
   my ($self, %params) = @_;
 
   if (!$self->{json}) {
-    $self->{json} = JSON->new->allow_nonref(1);
+    $self->{json} = JSON->new->allow_nonref(1)->convert_blessed(1);
 
     my $args = $self->{json_args};
 
diff --git a/t/controllers/base/render.t b/t/controllers/base/render.t
new file mode 100644 (file)
index 0000000..9b09045
--- /dev/null
@@ -0,0 +1,78 @@
+use strict;
+use Test::Exception;
+use Test::More;
+use Test::Output;
+
+use lib 't';
+use Support::TestSetup;
+
+use SL::Presenter;
+
+no warnings 'uninitialized';
+
+Support::TestSetup::login();
+
+sub reset_test_env {
+  $ENV{HTTP_USER_AGENT} = 'Perl Tests';
+
+  $::request       = {
+    cgi => CGI->new({}),
+    layout => SL::Layout::Javascript->new,
+  };
+
+  $::myconfig{stylesheet} = 'javascript';
+
+  delete @{ $::form }{qw(header footer)};
+}
+
+my $ctrl = SL::Controller::Base->new;
+
+# Passing invalid parameters:
+throws_ok { $ctrl->render(\'dummy', { unknown => 1 }) }    qr/unsupported option/i,                     'string ref, unknown parameter';
+throws_ok { $ctrl->render(\'dummy', { type => "excel" }) } qr/unsupported type/i,                       'string ref, unsupported "type"';
+throws_ok { $ctrl->render({}) }                            qr/unsupported.*template.*reference.*type/i, 'string ref, unsupported template argument reference type';
+throws_ok { $ctrl->render('does/not/exist') }              qr/template.*file.*not.*found/i,             'non-existing template file name';
+
+# No output:
+stdout_is { $ctrl->render(\'Hallo', { output => 0 }) } '', 'no output';
+
+# Type of return value:
+is(ref($ctrl->render(\'Hallo', { output => 0 })), 'SL::Presenter::EscapedText', 'render returns SL::Presenter::EscapedText');
+
+# Actual return value for string ref parameters (enforce stringification from SL::Presenter::EscapedText before comparison):
+is("" . $ctrl->render(\'Hallo [% world %]', { output => 0 }, world => 'Welt'),               'Hallo Welt',        'render string ref, no output');
+is("" . $ctrl->render(\'Hallo [% world %]', { output => 0, process => 0 }, world => 'Welt'), 'Hallo [% world %]', 'render string ref, no output, no processing');
+is("" . $ctrl->render(\'Hallo [% world %]', { output => 0, type => 'js' }, world => 'Welt'), 'Hallo Welt',        'render string ref, no output, different type');
+
+# Actual return value for template file name parameters (enforce stringification from SL::Presenter::EscapedText before comparison):
+is("" . $ctrl->render('t/render', { output => 0 }, world => 'Welt'),                      "Hallo Welt\n",                                       'render template file, no args');
+is("" . $ctrl->render('t/render', { output => 0, process => 0 }, world  => 'Welt'),   "[\% USE HTML \%]Hallo [\% HTML.escape(world) \%]\n", 'render template file, no processing');
+is("" . $ctrl->render('t/render', { output => 0, type => 'js' }, thingy => 'jungle'), "Welcome to the jungle\n",                            'render template file, different type');
+
+# No HTTP header in screen output:
+reset_test_env();
+stdout_unlike { $ctrl->render(\'Hallo [% world %]', { header => 0 }, world => 'Welt') } qr/content-type/i, 'no HTTP header with header=0';
+
+reset_test_env();
+stdout_unlike { $ctrl->render(\'Hallo [% world %]', { header => 0 }, world => 'Welt') } qr/<html>/i,       'no HTML header with header=0';
+
+# With HTTP header in screen output:
+reset_test_env();
+stdout_like { $ctrl->render(\'Hallo [% world %]', world => 'Welt') } qr/content-type/i, 'HTTP header with header=1';
+
+reset_test_env();
+stdout_like { $ctrl->render(\'Hallo [% world %]', world => 'Welt') } qr/<html>/i,       'HTML header with header=1';
+
+# Menu yes/no:
+reset_test_env();
+stdout_like { $ctrl->render(\'Hallo [% world %]', world => 'Welt') } qr/<table.*class=.*menunew/i, 'HTML header & menu with header=1';
+
+reset_test_env();
+stdout_unlike { $ctrl->render(\'Hallo [% world %]', { header => 0 }, world => 'Welt') } qr/<table.*class=.*menunew/i, 'HTML header & menu with header=0';
+
+reset_test_env();
+stdout_unlike { $ctrl->render(\'Hallo [% world %]', { layout => 0 }, world => 'Welt') } qr/<table.*class=.*menunew/i, 'HTML header & menu with layout=0';
+
+done_testing;
+
+1;
diff --git a/t/presenter/base/render.t b/t/presenter/base/render.t
new file mode 100644 (file)
index 0000000..5d7c96e
--- /dev/null
@@ -0,0 +1,35 @@
+use strict;
+use Test::Exception;
+use Test::More;
+
+use lib 't';
+use Support::TestSetup;
+
+use SL::Presenter;
+
+Support::TestSetup::login();
+
+my $pr = SL::Presenter->get;
+
+# Passing invalid parameters:
+throws_ok { $pr->render(\'dummy', { unknown => 1 }) }    qr/unsupported option/i,                     'string ref, unknown parameter';
+throws_ok { $pr->render(\'dummy', { type => "excel" }) } qr/unsupported type/i,                       'string ref, unsupported "type"';
+throws_ok { $pr->render({}) }                            qr/unsupported.*template.*reference.*type/i, 'string ref, unsupported template argument reference type';
+throws_ok { $pr->render('does/not/exist') }              qr/template.*file.*not.*found/i,             'non-existing template file name';
+
+# Type of return value:
+is(ref($pr->render(\'Hallo')), 'SL::Presenter::EscapedText', 'render returns SL::Presenter::EscapedText');
+
+# Actual return value for string ref parameters (enforce stringification from SL::Presenter::EscapedText before comparison):
+is("" . $pr->render(\'Hallo [% world %]', world => 'Welt'),                   'Hallo Welt',        'render string ref, no args');
+is("" . $pr->render(\'Hallo [% world %]', { process => 0 }, world => 'Welt'), 'Hallo [% world %]', 'render string ref, no processing');
+is("" . $pr->render(\'Hallo [% world %]', { type => 'js' }, world => 'Welt'), 'Hallo Welt',        'render string ref, different type');
+
+# Actual return value for template file name parameters (enforce stringification from SL::Presenter::EscapedText before comparison):
+is("" . $pr->render('t/render', world => 'Welt'),                      "Hallo Welt\n",                                       'render template file, no args');
+is("" . $pr->render('t/render', { process => 0 }, world  => 'Welt'),   "[\% USE HTML \%]Hallo [\% HTML.escape(world) \%]\n", 'render template file, no processing');
+is("" . $pr->render('t/render', { type => 'js' }, thingy => 'jungle'), "Welcome to the jungle\n",                            'render template file, different type');
+
+done_testing;
+
+1;
index f5b031a..1a49d6f 100755 (executable)
--- a/t/test.sh
+++ b/t/test.sh
@@ -6,4 +6,4 @@
   else
     echo -- "$@"
   fi
-} | HARNESS_OPTIONS=j:c xargs perl -Imodules/override -MTest::Harness -e 'BEGIN { push @INC, "modules/fallback" } runtests(@ARGV)'
+} | HARNESS_OPTIONS=j:c xargs perl -X -Imodules/override -MTest::Harness -e 'BEGIN { push @INC, "modules/fallback" } runtests(@ARGV)'
diff --git a/templates/webpages/ct/ajax_autocomplete2.html b/templates/webpages/ct/ajax_autocomplete2.html
deleted file mode 100644 (file)
index f4a44e3..0000000
+++ /dev/null
@@ -1,11 +0,0 @@
-[%- USE HTML %][% USE JSON %][
-[%- FOREACH customer = SELF.customers %]
- {
-   "value": [% customer.${SELF.value}.json %],
-   "label": [% customer.displayable_name.json %],
-   "id": [% customer.id.json %],
-   "customernumber": [% customer.customernumber.json %],
-   "name": [% customer.name.json %]
-  }[% ',' UNLESS loop.last %]
-[%- END %]
-]
diff --git a/templates/webpages/ct/ajax_autocomplete2.json b/templates/webpages/ct/ajax_autocomplete2.json
new file mode 100644 (file)
index 0000000..f4a44e3
--- /dev/null
@@ -0,0 +1,11 @@
+[%- USE HTML %][% USE JSON %][
+[%- FOREACH customer = SELF.customers %]
+ {
+   "value": [% customer.${SELF.value}.json %],
+   "label": [% customer.displayable_name.json %],
+   "id": [% customer.id.json %],
+   "customernumber": [% customer.customernumber.json %],
+   "name": [% customer.name.json %]
+  }[% ',' UNLESS loop.last %]
+[%- END %]
+]
diff --git a/templates/webpages/part/ajax_autocomplete.html b/templates/webpages/part/ajax_autocomplete.html
deleted file mode 100644 (file)
index 53fa6f0..0000000
+++ /dev/null
@@ -1,13 +0,0 @@
-[%- USE HTML %][% USE JSON %][
-[%- FOREACH part = SELF.parts %]
-[%- ajax_autocomplete__label = part.partnumber _ " " _ part.description %]
- {
-   "value": [% part.${SELF.value}.json %],
-   "label": [% ajax_autocomplete__label.json %],
-   "id": [% part.id.json %],
-   "partnumber": [% part.partnumber.json %],
-   "description": [% part.description.json %],
-   "type": [% part.type.json %]
-  }[% ',' UNLESS loop.last %]
-[%- END %]
-]
diff --git a/templates/webpages/part/ajax_autocomplete.json b/templates/webpages/part/ajax_autocomplete.json
new file mode 100644 (file)
index 0000000..53fa6f0
--- /dev/null
@@ -0,0 +1,13 @@
+[%- USE HTML %][% USE JSON %][
+[%- FOREACH part = SELF.parts %]
+[%- ajax_autocomplete__label = part.partnumber _ " " _ part.description %]
+ {
+   "value": [% part.${SELF.value}.json %],
+   "label": [% ajax_autocomplete__label.json %],
+   "id": [% part.id.json %],
+   "partnumber": [% part.partnumber.json %],
+   "description": [% part.description.json %],
+   "type": [% part.type.json %]
+  }[% ',' UNLESS loop.last %]
+[%- END %]
+]
diff --git a/templates/webpages/t/render.html b/templates/webpages/t/render.html
new file mode 100644 (file)
index 0000000..250a662
--- /dev/null
@@ -0,0 +1 @@
+[% USE HTML %]Hallo [% HTML.escape(world) %]
diff --git a/templates/webpages/t/render.js b/templates/webpages/t/render.js
new file mode 100644 (file)
index 0000000..ac93f8a
--- /dev/null
@@ -0,0 +1 @@
+Welcome to the [% thingy %]