From 7647d46acbc2a8253c0afeac5c706c3eb76995d5 Mon Sep 17 00:00:00 2001 From: Moritz Bunkus Date: Fri, 1 Feb 2013 12:12:16 +0100 Subject: [PATCH] Refactoring: Parameterredesign SL::Controller::Base::render und SL::Presenter::render Conflicts: SL/Controller/FinancialControllingReport.pm SL/Controller/ProjectType.pm --- SL/Controller/Base.pm | 159 ++++++++++++------ SL/Controller/CustomVariableConfig.pm | 2 +- SL/Controller/Customer.pm | 3 +- SL/Controller/DeliveryPlan.pm | 4 +- SL/Controller/Layout.pm | 4 +- SL/Controller/LoginScreen.pm | 6 +- SL/Controller/Part.pm | 2 +- SL/Controller/PaymentTerm.pm | 2 +- SL/Controller/PriceFactor.pm | 2 +- SL/Controller/Project.pm | 2 +- SL/Controller/Unit.pm | 2 +- SL/Controller/Warehouse.pm | 2 +- SL/Dispatcher.pm | 2 +- SL/JSON.pm | 90 ++++++++++ SL/Layout/Javascript.pm | 2 +- SL/Layout/MenuLeft.pm | 2 +- SL/Layout/Top.pm | 2 +- SL/Layout/V3.pm | 2 +- SL/Presenter.pm | 100 ++++++++--- SL/Presenter/EscapedText.pm | 6 + SL/Template/Plugin/JSON.pm | 2 +- t/controllers/base/render.t | 78 +++++++++ t/presenter/base/render.t | 35 ++++ t/test.sh | 2 +- ...complete2.html => ajax_autocomplete2.json} | 0 ...tocomplete.html => ajax_autocomplete.json} | 0 templates/webpages/t/render.html | 1 + templates/webpages/t/render.js | 1 + 28 files changed, 420 insertions(+), 95 deletions(-) create mode 100644 SL/JSON.pm create mode 100644 t/controllers/base/render.t create mode 100644 t/presenter/base/render.t rename templates/webpages/ct/{ajax_autocomplete2.html => ajax_autocomplete2.json} (100%) rename templates/webpages/part/{ajax_autocomplete.html => ajax_autocomplete.json} (100%) create mode 100644 templates/webpages/t/render.html create mode 100644 templates/webpages/t/render.js diff --git a/SL/Controller/Base.pm b/SL/Controller/Base.pm index d6bfd42e5..aa65645ae 100644 --- a/SL/Controller/Base.pm +++ b/SL/Controller/Base.pm @@ -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 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, I, I and I. 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. -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 +directory. The file name to use is determined by the C 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 option +is not considered in this case. + +Other reference types, unknown options and unknown arguments to the +C option cause the function to L. + +The following options are available (defaults: C = 'html', +C = 1, C = 1, C
= 1, C = 1): + +=over 2 + +=item C -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 is not -called either. Unlike C, the input is taken as a reference. +The template type. Can be C (the default), C for JavaScript +or C 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 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 -or C and defaults to C. An exception will be thrown if that -file does not exist. +=item C -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 -If the template type is C 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
and C are set to 0 as well. + +=item C
+ +Determines whether or not to output the HTTP response +headers. Defaults to the same value that C is set to. If set +to falsish then the layout is not output either. + +=item C + +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 is not C and to the same value +C
is set to otherwise. + +=back The template itself has access to several variables. These are listed in the documentation to L. -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. +by the calling function. No header is generated due to C. - 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' }, diff --git a/SL/Controller/CustomVariableConfig.pm b/SL/Controller/CustomVariableConfig.pm index 9a151da0e..6f29f52cf 100644 --- a/SL/Controller/CustomVariableConfig.pm +++ b/SL/Controller/CustomVariableConfig.pm @@ -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' }); } # diff --git a/SL/Controller/Customer.pm b/SL/Controller/Customer.pm index 4358080fe..823af2e59 100644 --- a/SL/Controller/Customer.pm +++ b/SL/Controller/Customer.pm @@ -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' }); } - diff --git a/SL/Controller/DeliveryPlan.pm b/SL/Controller/DeliveryPlan.pm index db166e376..83081f4ec 100644 --- a/SL/Controller/DeliveryPlan.pm +++ b/SL/Controller/DeliveryPlan.pm @@ -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, diff --git a/SL/Controller/Layout.pm b/SL/Controller/Layout.pm index 98c012cbc..d5d0988f3 100644 --- a/SL/Controller/Layout.pm +++ b/SL/Controller/Layout.pm @@ -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 }); } } diff --git a/SL/Controller/LoginScreen.pm b/SL/Controller/LoginScreen.pm index 81ad8dd5c..13b7b8a8c 100644 --- a/SL/Controller/LoginScreen.pm +++ b/SL/Controller/LoginScreen.pm @@ -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. diff --git a/SL/Controller/Part.pm b/SL/Controller/Part.pm index 36859e71a..6f3a136c9 100644 --- a/SL/Controller/Part.pm +++ b/SL/Controller/Part.pm @@ -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' }); } diff --git a/SL/Controller/PaymentTerm.pm b/SL/Controller/PaymentTerm.pm index df86252fe..b504bb7ad 100644 --- a/SL/Controller/PaymentTerm.pm +++ b/SL/Controller/PaymentTerm.pm @@ -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' }); } # diff --git a/SL/Controller/PriceFactor.pm b/SL/Controller/PriceFactor.pm index 5def8aabf..5f220cbe5 100644 --- a/SL/Controller/PriceFactor.pm +++ b/SL/Controller/PriceFactor.pm @@ -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' }); } # diff --git a/SL/Controller/Project.pm b/SL/Controller/Project.pm index 18523b7ce..386d7552b 100644 --- a/SL/Controller/Project.pm +++ b/SL/Controller/Project.pm @@ -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, diff --git a/SL/Controller/Unit.pm b/SL/Controller/Unit.pm index ae6e31305..fc1fa2e9d 100644 --- a/SL/Controller/Unit.pm +++ b/SL/Controller/Unit.pm @@ -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' }); } # diff --git a/SL/Controller/Warehouse.pm b/SL/Controller/Warehouse.pm index 33c8bfcdc..3f8b1528a 100644 --- a/SL/Controller/Warehouse.pm +++ b/SL/Controller/Warehouse.pm @@ -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' }); } # diff --git a/SL/Dispatcher.pm b/SL/Dispatcher.pm index d01a0d03b..44631a046 100644 --- a/SL/Dispatcher.pm +++ b/SL/Dispatcher.pm @@ -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 index 000000000..6602b393d --- /dev/null +++ b/SL/JSON.pm @@ -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. This +module provides the same functions that L does but changes their +default regarding converting blessed arguments. + +=head1 FUNCTIONS + +=over 4 + +=item C + +Same as L. + +=item C + +Same as L but sets C first. + +=item C + +Same as L. + +=item C + +Same as L but sets C first. + +=back + +=head1 BUGS + +Nothing here yet. + +=head1 AUTHOR + +Moritz Bunkus Em.bunkus@linet-services.deE + +=cut diff --git a/SL/Layout/Javascript.pm b/SL/Layout/Javascript.pm index f3a2f0f06..c63b1f7c7 100644 --- a/SL/Layout/Javascript.pm +++ b/SL/Layout/Javascript.pm @@ -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, diff --git a/SL/Layout/MenuLeft.pm b/SL/Layout/MenuLeft.pm index c0e937ab8..4cd79c4cc 100644 --- a/SL/Layout/MenuLeft.pm +++ b/SL/Layout/MenuLeft.pm @@ -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, ) } diff --git a/SL/Layout/Top.pm b/SL/Layout/Top.pm index 075e2f267..9c4fdea36 100644 --- a/SL/Layout/Top.pm +++ b/SL/Layout/Top.pm @@ -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)); diff --git a/SL/Layout/V3.pm b/SL/Layout/V3.pm index a59424001..9d5e37d86 100644 --- a/SL/Layout/V3.pm +++ b/SL/Layout/V3.pm @@ -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, diff --git a/SL/Presenter.pm b/SL/Presenter.pm index ec36c3b52..68ef8c888 100644 --- a/SL/Presenter.pm +++ b/SL/Presenter.pm @@ -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 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 and I. +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 +directory. The file name to use is determined by the C 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 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 -or C and defaults to C. An exception will be thrown if that -file does not exist. +Other reference types, unknown options and unknown arguments to the +C option cause the function to L. -The template itself has access to the following variables: +The following options are available: + +=over 2 + +=item C + +The template type. Can be C (the default), C for JavaScript +or C for JSON content. Affects only the extension that's added +to the file name given with a non-reference C<$template> argument. + +=item C + +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 diff --git a/SL/Presenter/EscapedText.pm b/SL/Presenter/EscapedText.pm index d482e8a30..c8c78a092 100644 --- a/SL/Presenter/EscapedText.pm +++ b/SL/Presenter/EscapedText.pm @@ -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__ diff --git a/SL/Template/Plugin/JSON.pm b/SL/Template/Plugin/JSON.pm index 1fe8f19d4..0586511c6 100644 --- a/SL/Template/Plugin/JSON.pm +++ b/SL/Template/Plugin/JSON.pm @@ -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 index 000000000..9b09045d6 --- /dev/null +++ b/t/controllers/base/render.t @@ -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//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//i, 'HTML header with header=1'; + +# Menu yes/no: +reset_test_env(); +stdout_like { $ctrl->render(\'Hallo [% world %]', world => 'Welt') } qr/render(\'Hallo [% world %]', { header => 0 }, world => 'Welt') } qr/render(\'Hallo [% world %]', { layout => 0 }, world => 'Welt') } qr/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; diff --git a/t/test.sh b/t/test.sh index f5b031a59..1a49d6fc5 100755 --- 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.json similarity index 100% rename from templates/webpages/ct/ajax_autocomplete2.html rename to templates/webpages/ct/ajax_autocomplete2.json diff --git a/templates/webpages/part/ajax_autocomplete.html b/templates/webpages/part/ajax_autocomplete.json similarity index 100% rename from templates/webpages/part/ajax_autocomplete.html rename to templates/webpages/part/ajax_autocomplete.json diff --git a/templates/webpages/t/render.html b/templates/webpages/t/render.html new file mode 100644 index 000000000..250a6625c --- /dev/null +++ b/templates/webpages/t/render.html @@ -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 index 000000000..ac93f8a55 --- /dev/null +++ b/templates/webpages/t/render.js @@ -0,0 +1 @@ +Welcome to the [% thingy %] -- 2.20.1