package Rose::DBx::Object::Renderer; use strict; use Exporter 'import'; use base qw(Rose::Object); our @EXPORT = qw(config load); our @EXPORT_OK = qw(config load render_as_form render_as_table render_as_menu render_as_chart stringify_me stringify_class delete_with_file); our %EXPORT_TAGS = (object => [qw(render_as_form stringify_me stringify_class delete_with_file)], manager => [qw(render_as_table render_as_menu render_as_chart)]); use Lingua::EN::Inflect qw (PL); use DateTime; use Rose::DB::Object::Loader; use Rose::DB::Object::Helpers 'clone_and_reset'; use CGI; use CGI::FormBuilder; use Template; use File::Path; use File::Copy; use File::Copy::Recursive 'dircopy'; use File::Spec; use Digest::MD5 qw(md5_hex); our $VERSION = 0.57; # 146.43 sub config { my $self = shift; unless ($self && defined $self->{CONFIG}) { $self->{CONFIG} = { db => {name => undef, type => 'mysql', host => '127.0.0.1', port => undef, username => 'root', password => 'root', tables_are_singular => undef, like_operator => 'like'}, template => {path => 'templates', url => 'templates'}, upload => {path => 'uploads', url => 'uploads', keep_old_files => undef}, form => {download_message => 'Download File', cancel => 'Cancel', delimiter => ','}, table => {search_result_title => 'Search Results for "[% q %]"', empty_message => 'No Record Found.', no_pagination => undef, per_page => 15, pages => 9, or_filter => undef, delimiter => ', ', keyword_delimiter => ','}, misc => {time_zone => 'Australia/Sydney', stringify_delimiter => ' ', doctype => '', html_head => ''}, columns => { 'integer' => {validate => 'INT', sortopts => 'NUM'}, 'numeric' => {validate => 'NUM', sortopts => 'NUM'}, 'float' => {validate => 'FLOAT', sortopts => 'NUM', comment => 'e.g.: 2982.21'}, 'text' => {sortopts => 'LABELNAME', type => 'textarea', cols => '55', rows => '10', class => 'disable_editor'}, 'address' => {sortopts => 'LABELNAME', format => {for_view => sub {_view_address(@_);}}}, 'postcode' => {sortopts => 'NUM', validate => '/^\d{3,4}$/', maxlength => 4}, 'date' => {validate => '/^(0?[1-9]|[1-2][0-9]|3[0-1])\/(0?[1-9]|1[0-2])\/[0-9]{4}$/', format => {for_edit => sub {_edit_date(@_);}, for_update => sub {_update_date(@_);}, for_search => sub {_search_date(@_);}, for_filter => sub {_search_date(@_);}, for_view => sub {_edit_date(@_);}}}, 'datetime' => {validate => '/^(0?[1-9]|[1-2][0-9]|3[0-1])\/(0?[1-9]|1[0-2])\/[0-9]{4}\s+[0-9]{1,2}:[0-9]{2}$/', format => {for_edit => sub{_view_datetime(@_);}, for_view => sub{_view_datetime(@_);}, for_update => sub{_update_datetime(@_);}, for_search => sub {_search_date(@_);}, for_filter => sub {_search_date(@_);}}}, 'timestamp' => {readonly => 1, disabled => 1, format => {for_view => sub {_view_timestamp(@_);}, for_create => sub {_create_timestamp(@_);}, for_edit => sub {_create_timestamp(@_);}, for_update => sub {_update_timestamp(@_);}, for_search => sub {_search_date(@_);}, for_filter => sub {_search_date(@_);}}}, 'description' => {sortopts => 'LABELNAME', type => 'textarea', cols => '55', rows => '10'}, 'time' => {validate => 'TIME', maxlength => 5, format => {for_update => sub {_udpate_time(@_)}, for_edit => sub{_edit_time(@_);}, for_view => sub{_edit_time(@_);}}}, 'length' => {validate => 'NUM', sortopts => 'NUM', format => {for_view => sub {my ($self, $column) = @_;my $value = $self->$column;return unless $value;return $value.' cm';}}}, 'weight' => {validate => 'NUM', sortopts => 'NUM', format => {for_view => sub {my ($self, $column) = @_;my $value = $self->$column;return unless $value;return $value.' kg';}}}, 'volume' => {validate => 'NUM', sortopts => 'NUM', format => {for_view => sub {my ($self, $column) = @_;my $value = $self->$column;return unless $value;return $value.' cm3';}}}, 'gender' => {options => ['Male', 'Female']}, 'name' => {sortopts => 'LABELNAME', required => 1, stringify => 1}, 'first_name' => {validate => 'FNAME', sortopts => 'LABELNAME', required => 1, stringify => 1}, 'last_name' => {validate => 'LNAME', sortopts => 'LABELNAME', required => 1, stringify => 1}, 'email' => {required => 1, validate => 'EMAIL', sortopts => 'LABELNAME', format => {for_view => sub {my ($self, $column) = @_;my $value = $self->$column;return unless $value;return qq($value);}}, comment => 'e.g. your.name@work.com'}, 'url' => {sortopts => 'LABELNAME', format => {for_view => sub {my ($self, $column) = @_;my $value = $self->$column;return unless $value;return qq($value);}}, comment => 'e.g. http://www.yourbusiness.com/'}, 'mobile' => {validate => '/^((\()?(\+)?\d{2}(\))?)?[-\s]?(\d{3}|\d{4})[-\s]?\d{3}[-\s]?\d{3}$/', comment => 'e.g. 0433 123 456'}, 'phone' => {validate => '/^((\()?(\+)?\d{2}[-\s]?\d?(\))?)?[-\s]?\d{4}[-\s]?\d{4}$/', comment => 'e.g. 02 9988 1288'}, 'username' => {validate => '/^[a-zA-Z0-9]{4,}$/', sortopts => 'LABELNAME', required => 1}, 'password' => {validate => '/^[\w.!?@#$%&*]{5,}$/', type => 'password', format => {for_view => sub {return '****';}, for_edit => sub {return;}, for_update => sub {my ($self, $column, $value) = @_;return $self->$column(md5_hex($value)) if $value;}}, comment => 'Minimum 5 characters', unsortable => 1}, 'confirm_password' => {required => 1, type => 'password', validate => {javascript => "!= form.elements['password'].value"}}, 'abn' => {label => 'ABN', validate => '/^(\d{2}\s*\d{3}\s*\d{3}\s*\d{3})$/', comment => 'e.g. 12 234 456 678'}, 'money' => {validate => '/^\-?\d{1,11}(\.\d{2})?$/', sortopts => 'NUM', format => {for_view => sub {my ($self, $column) = @_;return unless $self->$column;return sprintf ('$%.02f', $self->$column);}, for_edit => sub {my ($self, $column) = @_;return unless $self->$column;return sprintf ('%.02f', $self->$column);}}}, 'percentage' => {validate => 'NUM', sortopts => 'NUM', comment => 'e.g.: 99.8', format => {for_view => sub {my ($self, $column, $value) = @_;$value = $self->$column;return unless $value;my $p = $value*100;return "$p%";}, for_edit => sub {my ($self, $column) = @_;my $value = $self->$column;return unless defined $value;return $value*100;}, for_update => sub {my ($self, $column, $value) = @_;return $self->$column($value/100) if $value;}, for_search => sub {_search_percentage(@_);}, for_filter => sub {_search_percentage(@_);}}}, 'document' => {validate => '/^\S+[\w\s.!?@#$\(\)\'\_\-:%&*\/\\\\\[\]]{1,255}$/', format => {path => sub {_get_file_path(@_);}, url => sub {_get_file_url(@_);}, for_update => sub {_update_file(@_);}, for_view => sub {_view_file(@_)}}, type => 'file'}, 'image' => {validate => '/^\S+[\w\s.!?@#$\(\)\'\_\-:%&*\/\\\\\[\]]{1,255}$/', format => {path => sub {_get_file_path(@_);}, url => sub {_get_file_url(@_);}, for_view => sub {_view_image(@_);}, for_update => sub {_update_file(@_);}}, type => 'file'}, 'media' => {validate => '/^\S+[\w\s.!?@#$\(\)\'\_\-:%&*\/\\\\\[\]]{1,255}$/', format => {path => sub {_get_file_path(@_);}, url => sub {_get_file_url(@_);}, for_view => sub {_view_media(@_);}, for_update => sub {_update_file(@_);}}, type => 'file'}, 'ipv4' => {validate => 'IPV4'}, 'boolean' => {validate => '/^[0-1]$/', sortopts => 'LABELNAME', options => {1 => 'Yes', 0 => 'No'}, format => {for_view => sub {my ($self, $column) = @_;my $options = {1 => 'Yes', 0 => 'No'};return $options->{$self->$column};}, for_search => sub {_search_boolean(@_)}, for_filter => sub {_search_boolean(@_)}}}, } }; $self->{CONFIG}->{columns}->{'doubleprecision'} = $self->{CONFIG}->{columns}->{'numeric'}; $self->{CONFIG}->{columns}->{'decimal'} = $self->{CONFIG}->{columns}->{'numeric'}; $self->{CONFIG}->{columns}->{'bigint'} = $self->{CONFIG}->{columns}->{'integer'}; $self->{CONFIG}->{columns}->{'serial'} = $self->{CONFIG}->{columns}->{'integer'}; $self->{CONFIG}->{columns}->{'bigserial'} = $self->{CONFIG}->{columns}->{'integer'}; $self->{CONFIG}->{columns}->{'quantity'} = $self->{CONFIG}->{columns}->{'integer'}; $self->{CONFIG}->{columns}->{'height'} = $self->{CONFIG}->{columns}->{'length'}; $self->{CONFIG}->{columns}->{'width'} = $self->{CONFIG}->{columns}->{'length'}; $self->{CONFIG}->{columns}->{'depth'} = $self->{CONFIG}->{columns}->{'length'}; $self->{CONFIG}->{columns}->{'title'} = $self->{CONFIG}->{columns}->{'name'}; $self->{CONFIG}->{columns}->{'birth'} = $self->{CONFIG}->{columns}->{'date'}; $self->{CONFIG}->{columns}->{'fax'} = $self->{CONFIG}->{columns}->{'phone'}; $self->{CONFIG}->{columns}->{'cost'} = $self->{CONFIG}->{columns}->{'money'}; $self->{CONFIG}->{columns}->{'price'} = $self->{CONFIG}->{columns}->{'money'}; $self->{CONFIG}->{columns}->{'blob'} = $self->{CONFIG}->{columns}->{'text'}; $self->{CONFIG}->{columns}->{'comment'} = $self->{CONFIG}->{columns}->{'text'}; $self->{CONFIG}->{columns}->{'file'} = $self->{CONFIG}->{columns}->{'document'}; $self->{CONFIG}->{columns}->{'report'} = $self->{CONFIG}->{columns}->{'document'}; $self->{CONFIG}->{columns}->{'photo'} = $self->{CONFIG}->{columns}->{'image'}; $self->{CONFIG}->{columns}->{'logo'} = $self->{CONFIG}->{columns}->{'image'}; $self->{CONFIG}->{columns}->{'sound'} = $self->{CONFIG}->{columns}->{'media'}; $self->{CONFIG}->{columns}->{'voice'} = $self->{CONFIG}->{columns}->{'media'}; $self->{CONFIG}->{columns}->{'video'} = $self->{CONFIG}->{columns}->{'media'}; $self->{CONFIG}->{columns}->{'movie'} = $self->{CONFIG}->{columns}->{'media'}; } if (@_) { my $config = shift; foreach my $hash (keys %{$config}) { if ($hash eq 'columns') { foreach my $column (keys %{$config->{columns}}) { foreach my $key (keys %{$config->{columns}->{$column}}) { if ($key eq 'format') { foreach my $method (keys %{$config->{columns}->{$column}->{format}}) { $self->{CONFIG}->{columns}->{$column}->{format}->{$method} = $config->{columns}->{$column}->{format}->{$method}; } } else { $self->{CONFIG}->{columns}->{$column}->{$key} = $config->{columns}->{$column}->{$key}; } } } } else { foreach my $key (keys %{$config->{$hash}}) { $self->{CONFIG}->{$hash}->{$key} = $config->{$hash}->{$key}; } } } } return $self->{CONFIG}; } sub load { my ($self, $args) = @_; $args = {} unless ref $args eq 'HASH'; my $config = $self->config; unless (exists $args->{loader} && defined $args->{loader}->{class_prefix}) { return unless $config->{db}->{name}; $args->{loader}->{class_prefix} = $config->{db}->{name}; $args->{loader}->{class_prefix} =~ s/_(.)/\U$1/g; $args->{loader}->{class_prefix} =~ s/[^\w:]/_/g; $args->{loader}->{class_prefix} =~ s/\b(\w)/\u$1/g; } return if "$args->{loader}->{class_prefix}::DB::Object::AutoBase1"->isa('Rose::DB::Object'); unless (defined $args->{loader}->{db}) { unless (defined $args->{loader}->{db_dsn}) { my $host; $host = 'host='. $config->{db}->{host} if $config->{db}->{host}; $host .= ';port='.$config->{db}->{port} if $config->{db}->{port}; $args->{loader}->{db_dsn} = qq(dbi:$config->{db}->{type}:dbname=$config->{db}->{name};$host); } $args->{loader}->{db_options}->{AutoCommit} ||= 1; $args->{loader}->{db_options}->{ChopBlanks} ||= 1; $args->{loader}->{db_username} ||= $config->{db}->{username}; $args->{loader}->{db_password} ||= $config->{db}->{password}; } my $loader = Rose::DB::Object::Loader->new(%{$args->{loader}}); $loader->convention_manager->tables_are_singular(1) if $config->{db}->{tables_are_singular}; my (@loaded, $custom_definitions, $validated_unique_keys); foreach my $class ($loader->make_classes(%{$args->{make_classes}})) { my $package = qq(package $class;); if (($class)->isa('Rose::DB::Object')) { my $foreign_keys = _get_foreign_keys($class); my $unique_keys = _get_unique_keys($class); $package .= 'use Rose::DBx::Object::Renderer qw(:object);'; foreach my $column (@{$class->meta->columns}) { my $column_type; unless ($column->{is_primary_key_member}) { if (exists $config->{columns}->{$column} && ! exists $custom_definitions->{$column}) { $column_type = $column; if (exists $foreign_keys->{$column}) { my $foreign_object_name = $foreign_keys->{$column}->{name}; $config->{columns}->{$column}->{label} = _label($foreign_object_name) unless exists $config->{columns}->{$column}->{label}; $config->{columns}->{$column}->{required} = 1 unless exists $config->{columns}->{$column}->{required}; $config->{columns}->{$column}->{validate} = 'INT' unless exists $config->{columns}->{$column}->{validate}; $config->{columns}->{$column}->{format}->{for_view} = sub {my ($self, $column) = @_;return unless $self->$column;return $self->$foreign_object_name->stringify_me;} unless exists $config->{columns}->{$column}->{format} && exists $config->{columns}->{$column}->{format}->{for_view}; } } elsif (exists $foreign_keys->{$column}) # special treatment { my $foreign_object_name = $foreign_keys->{$column}->{name}; $config->{columns}->{$column} = {label => _label($foreign_object_name), required => 1, validate => 'INT', sortopts => 'LABELNAME', format => {for_view => sub {my ($self, $column) = @_;return unless $self->$column;return $self->$foreign_object_name->stringify_me;}}}; $column_type = $column; } else { DEF: foreach my $column_key (keys %{$config->{columns}}) { if ($column =~ /$column_key/ && ! exists $custom_definitions->{$column_key}) # first match { $column_type = $column_key; last DEF; } } unless (defined $column_type) { my $rdbo_column_type = lc ref $class->meta->{columns}->{$column}; ($rdbo_column_type) = $rdbo_column_type =~ /^.*::([\w_]+)$/; if (exists $config->{columns}->{$rdbo_column_type}) { $column_type = $rdbo_column_type; } else { my $custom_definition; $custom_definition->{required} = 1 if $class->meta->{columns}->{$column}->{not_null}; $custom_definition->{maxlength} = $class->meta->{columns}->{$column}->{length} if defined $class->meta->{columns}->{$column}->{length}; if (defined $class->meta->{columns}->{$column}->{check_in}) { $custom_definition->{options} = $class->meta->{columns}->{$column}->{check_in}; $custom_definition->{multiple} = 1 if ref $class->meta->{columns}->{$column} eq 'Rose::DB::Object::Metadata::Column::Set'; } $config->{columns}->{$column} = $custom_definition; $column_type = $column; $custom_definitions->{$column} = undef; } } } if (exists $unique_keys->{$column}) { unless ($column eq $column_type) { foreach my $key (keys %{$config->{columns}->{$column_type}}) { $config->{columns}->{$column}->{$key} = $config->{columns}->{$column_type}->{$key}; } } if (exists $config->{columns}->{$column_type}->{validate}) { if (ref $config->{columns}->{$column_type}->{validate} eq 'HASH') { $validated_unique_keys->{$column} = $config->{columns}->{$column_type}->{validate}->{javascript}; } else { if (ref $validated_unique_keys->{$column} eq 'CODE') { $validated_unique_keys->{$column} = undef; } else { if (ref $validated_unique_keys->{$column} eq 'ARRAY') { $validated_unique_keys->{$column} = $config->{columns}->{$column_type}->{validate}; $config->{columns}->{$column}->{validate} = { javascript => $validated_unique_keys->{$column}, perl => sub {my ($value, $form) = @_;return unless length($value);my $found;foreach my $v (@{$validated_unique_keys->{$column}}){if($value eq $v){$found = 1;last;}};return if ! $found;return _unique($config, $class, $column, $value, $form);} }; } else { if (exists $CGI::FormBuilder::Field::VALIDATE{$config->{columns}->{$column_type}->{validate}}) { $validated_unique_keys->{$column} = $CGI::FormBuilder::Field::VALIDATE{$config->{columns}->{$column_type}->{validate}}; } else { $validated_unique_keys->{$column} = $config->{columns}->{$column_type}->{validate}; } if ($validated_unique_keys->{$column} =~ /^m(\S)(.*)\1$/ || $validated_unique_keys->{$column} =~ /^(\/)(.*)\1$/) { (my $regex = $2) =~ s#\\/#/#g; $regex =~ s#/#\\/#g; $config->{columns}->{$column}->{validate} = { javascript => $validated_unique_keys->{$column}, perl => sub {my ($value, $form) = @_;return if ! length($value) || ! ($value =~ /$regex/);return _unique($config, $class, $column, $value, $form);} }; } else { $config->{columns}->{$column}->{validate} = { javascript => $validated_unique_keys->{$column}, perl => sub {my ($value, $form) = @_;return if $value ne $validated_unique_keys->{$column};return _unique($config, $class, $column, $value, $form);} }; } } } } } else { $validated_unique_keys->{$column} = undef; $config->{columns}->{$column}->{validate} = sub {my ($value, $form) = @_;return unless length($value);return _unique($config, $class, $column, $value, $form);}; } $column_type = $column; $config->{columns}->{$column}->{required} = 1 unless exists $config->{columns}->{$column}->{required}; unless (defined $config->{columns}->{$column}->{message}) { my $column_label; if (defined $config->{columns}->{$column}->{label}) { $column_label = $config->{columns}->{$column}->{label}; } else { $column_label = _label($column); } $config->{columns}->{$column}->{message} = qq($column_label already exists or is invalid, please choose another one.); unless (defined $config->{columns}->{$column}->{jsmessage}) { if (exists $foreign_keys->{$column}) { $config->{columns}->{$column}->{jsmessage} = qq(- Choose one of the "$column_label" options); } else { $config->{columns}->{$column}->{jsmessage} = qq(- Invalid entry for the "$column_label" field); } } } } elsif (exists $validated_unique_keys->{$column_type} && $column ne $column_type) # prevent inheriting validation subref from matching unique column type { foreach my $key (keys %{$config->{columns}->{$column_type}}) { $config->{columns}->{$column}->{$key} = $config->{columns}->{$column_type}->{$key}; } $config->{columns}->{$column}->{validate} = $validated_unique_keys->{$column_type}; delete $config->{columns}->{$column}->{message}; delete $config->{columns}->{$column}->{jsmessage}; $column_type = $column; } $package .= _generate_methods($config, $column, $column_type); } } $package .= "sub renderer_config {return \$config;}\n"; } else { $package .= 'use Rose::DBx::Object::Renderer qw(:manager);'; } $package .= '1;'; eval $package; die "Can't load $class: $@" if $@; push @loaded, $class; } return @loaded; } sub _generate_methods { my ($config, $column, $column_type) = @_; my $method; if (exists $config->{columns}->{$column_type}->{format}) { foreach my $custom_method_key (keys %{$config->{columns}->{$column_type}->{format}}) { $method .= "sub $column\_$custom_method_key {my (\$self, \$value) = \@_; return \$config->{columns}->{$column_type}->{format}->{$custom_method_key}->(\$self, '$column', \$value);}\n"; } } $method .= "sub $column\_definition {return \$config->{columns}->{$column_type};}\n"; return $method; } sub render_as_form { my ($self, %args) = (@_); my ($class, $form_action, $field_order, $output, $relationship_object); my $table = $self->meta->table; my $form_title = $args{title}; if (ref $self) { $class = ref $self; if ($args{copy}) { $form_action = 'copy'; } else { $form_action = 'update'; } $form_title ||= _label($form_action.' '.$self->stringify_me); } else { $class = $self; $form_action = 'create'; $form_title ||= _label($form_action.' '.$table); } my $renderer_config = _get_renderer_config($class); my $download_message = $args{download_message} || $renderer_config->{form}->{download_message}; my $cancel = $args{cancel} || $renderer_config->{form}->{cancel}; my $template_url = $args{template_url} || $renderer_config->{template}->{url}; my $template_path = $args{template_path} || $renderer_config->{template}->{path}; (my $html_head = $args{html_head} || $renderer_config->{misc}->{html_head}) =~ s/\[%\s*title\s*%\]/$form_title/; my $foreign_keys = _get_foreign_keys($class); my $relationships = _get_relationships($class); my $column_order = $args{order} || _get_column_order($class, $relationships); my $ui_type = (caller(0))[3]; ($ui_type) = $ui_type =~ /^.*_(\w+)$/; my $form_id = _identify($class, $args{prefix}, $ui_type); my $form_template; if ($args{template} eq 1) { $form_template = $ui_type . '.tt'; } else { $form_template = $args{template}; } my $form_def = $args{form}; $form_def->{name} ||= $form_id; $form_def->{enctype} ||= 'multipart/form-data'; $form_def->{method} ||= 'post'; $form_def->{params} ||= $args{cgi} if exists $args{cgi}; $form_def->{td} ||= {valign => 'middle'}; if($args{template}) { $form_def->{jserror} ||= 'notify_error'; } else { $form_def->{messages}->{form_required_text} = ''; } $form_def->{jsfunc} ||= qq(if (form._submit.value == '$cancel' || form.$form_id\_submit_cancel.value == 1) {return true;}); my $form = CGI::FormBuilder->new($form_def); foreach my $column (@{$column_order}) { my $field_def; $field_def = $args{fields}->{$column} if exists $args{fields} && exists $args{fields}->{$column}; my $column_definition_method = $column . '_definition'; if ($class->can($column_definition_method)) { my $column_definition = $class->$column_definition_method; foreach my $property (keys %{$column_definition}) { $field_def->{$property} = $column_definition->{$property} unless defined $field_def->{$property} || $property eq 'format' || $property eq 'stringify' || $property eq 'unsortable'; } } if (exists $relationships->{$column}) #one to many or many to many relationships { $field_def->{validate} ||= 'INT'; $field_def->{sortopts} ||= 'LABELNAME'; $field_def->{multiple} ||= 1; my $foreign_class_primary_key = $relationships->{$column}->{class}->meta->primary_key_column_names->[0]; if (ref $self && ! exists $field_def->{value}) { my $foreign_object_value; foreach my $foreign_object ($self->$column) { $foreign_object_value->{$foreign_object->$foreign_class_primary_key} = $foreign_object->stringify_me; $relationship_object->{$column}->{$foreign_object->$foreign_class_primary_key} = undef; #keep it for update } $field_def->{value} = $foreign_object_value; } unless ($field_def->{static} || $field_def->{type} eq 'hidden' || exists $field_def->{options}) { my $objects = Rose::DB::Object::Manager->get_objects(object_class => $relationships->{$column}->{class}); if (@{$objects}) { foreach my $object (@{$objects}) { $field_def->{options}->{$object->$foreign_class_primary_key} = $object->stringify_me; } } else { $field_def->{type} ||= 'select'; $field_def->{disabled} ||= 1; } } } elsif (exists $class->meta->{columns}->{$column}) #normal column { unless (exists $field_def->{options} || $field_def->{type} eq 'hidden') { if (exists $foreign_keys->{$column}) #create or edit { my $foreign_class = $foreign_keys->{$column}->{class}; my $foreign_class_primary_key = $foreign_class->meta->primary_key_column_names->[0]; if ($field_def->{static}) { if (ref $self) { if ($self->$column) { my $foreign_column = $foreign_keys->{$column}->{name}; $field_def->{options} = {$self->$column => $self->$foreign_column->stringify_me}; } } else { my $foreign_object_id; if (defined $field_def->{value}) { $foreign_object_id = $field_def->{value}; } elsif(defined $self->meta->{columns}->{$column}->{default}) { $foreign_object_id = $self->meta->{columns}->{$column}->{default}; } if ($foreign_object_id) { my $foreign_object = $foreign_class->new($foreign_class_primary_key => $foreign_object_id); $field_def->{options} = {$foreign_object_id => $foreign_object->stringify_me} if $foreign_object->load(speculative => 1); } } } else { my $objects = Rose::DB::Object::Manager->get_objects(object_class => $foreign_keys->{$column}->{class}); if (@{$objects}) { foreach my $object (@{$objects}) { $field_def->{options}->{$object->$foreign_class_primary_key} = $object->stringify_me; } } else { $field_def->{type} ||= 'select'; $field_def->{disabled} ||= 1; } } } elsif (exists $class->meta->{columns}->{$column}->{check_in}) { $field_def->{options} = $class->meta->{columns}->{$column}->{check_in}; $field_def->{multiple} = 1 if ! exists $field_def->{multiple} && ref $class->meta->{columns}->{$column} eq 'Rose::DB::Object::Metadata::Column::Set'; } elsif (! exists $field_def->{type} && ref $class->meta->{columns}->{$column} eq 'Rose::DB::Object::Metadata::Column::Text') { $field_def->{type} = 'textarea'; $field_def->{cols} ||= '55'; $field_def->{rows} ||= '10'; } } if (ref $self) #edit { unless (exists $field_def->{value}) { my $current_value; if ($class->can($column . '_for_edit')) { my $edit_method = $column . '_for_edit'; $current_value = $self->$edit_method; $field_def->{value} = "$current_value"; } else { if (ref $self->meta->{columns}->{$column} eq 'Rose::DB::Object::Metadata::Column::Set') { $field_def->{value} = $self->$column; } elsif ($field_def->{options} && $field_def->{multiple}) { my $delimiter = '\\' . $renderer_config->{form}->{delimiter}; $field_def->{value} = [split /$delimiter/, $self->$column]; } else { $current_value = $self->$column; $field_def->{value} = "$current_value"; # double quote to make it literal to stringify object refs such as DateTime } } } if ($field_def->{type} eq 'file' && ! defined $field_def->{comment}) #file: if value exist in db, or in cgi param when the same form reloads { my $value = $form->cgi_param($form_id.'_'.$column) || $form->cgi_param($column) || $self->$column; my $file_location = _get_file_url($self, $column, $value); $field_def->{comment} = ''.$download_message.'' if $file_location; } } else { unless (exists $field_def->{value}) { if ($class->can($column . '_for_create')) { my $create_method = $column.'_for_create'; my $create_result = $self->$create_method($self->meta->{columns}->{$column}->{default}); $field_def->{value} = $create_result if defined $create_result; } else { $field_def->{value} = $self->meta->{columns}->{$column}->{default} if defined $self->meta->{columns}->{$column}->{default}; } } } } delete $field_def->{value} if $field_def->{multiple} && $form->submitted && not $form->cgi_param($column) && not $form->cgi_param($form_id.'_'.$column); $field_def->{label} ||= _label($column); unless (exists $field_def->{name}) { if ($args{prefix}) { push @{$field_order}, $form_id.'_'.$column; $field_def->{name} = $form_id.'_'.$column; } else { push @{$field_order}, $column; $field_def->{name} = $column; } } $form->field(%{$field_def}); } foreach my $query_key (keys %{$args{queries}}) { $form->field(name => $query_key, value => $args{queries}->{$query_key}, type => 'hidden', force => 1); } $form->field(name => $form_id . '_submit_cancel', type => 'hidden', force => 1); unless (defined $args{controller_order}) { foreach my $controller (keys %{$args{controllers}}) { push @{$args{controller_order}}, $controller; } push @{$args{controller_order}}, ucfirst ($form_action) unless $args{controllers} && exists $args{controllers}->{ucfirst ($form_action)}; push @{$args{controller_order}}, $cancel unless $args{controllers} && exists $args{controllers}->{$cancel}; } $form->{submit} = $args{controller_order}; $args{template_data} ||= {}; $form->template({ variable => 'form', data => { template_url => $template_url, javascript_code => $args{javascript_code}, field_order => $field_order, form_id => $form_id, form_submit => _touch_up($form->prepare->{submit}, $cancel, $form_id), title => $form_title, description => $args{description}, doctype => $renderer_config->{misc}->{doctype}, html_head => $html_head, no_head => $args{no_head}, self => $self, cancel => $cancel, extra => $args{extra}, %{$args{template_data}} }, template => $form_template, engine => {INCLUDE_PATH => $template_path}, type => 'TT2' }) if $args{template}; if ($form->submitted) { if ($form->submitted ne $cancel) { my $form_validate = $form->validate(%{$args{validate}}); if ($form_validate) { no strict 'refs'; my $form_action_callback = '_'.$form_action.'_object'; if (exists $args{controllers}->{$form->submitted}) #method buttons { if (ref $args{controllers}->{$form->submitted} eq 'HASH') { if ($args{controllers}->{$form->submitted}->{$form_action}) { unless (ref $args{controllers}->{$form->submitted}->{$form_action} eq 'CODE' && ! $args{controllers}->{$form->submitted}->{$form_action}->($self)) { $self = $form_action_callback->($self, $class, $table, $field_order, $form, $form_id, $args{prefix}, $relationships, $relationship_object); } } $output->{controller} = $args{controllers}->{$form->submitted}->{callback}->($self) if ref $args{controllers}->{$form->submitted}->{callback} eq 'CODE'; $args{hide_form} = 1 if exists $args{controllers}->{$form->submitted}->{hide_form}; } else { $output->{controller} = $args{controllers}->{$form->submitted}->($self) if ref $args{controllers}->{$form->submitted} eq 'CODE'; } } elsif($form->submitted eq ucfirst ($form_action)) { $self = $form_action_callback->($self, $class, $table, $field_order, $form, $form_id, $args{prefix}, $relationships, $relationship_object); } $output->{validate} = $form_validate; } } else { $output->{validate} = 1; } } my ($hide_form, $html_form); $hide_form = $form_id.'_' if $args{prefix}; $hide_form .= 'hide_form'; $args{hide_form} = 1 if $form->cgi_param($hide_form); unless ($args{hide_form}) { if ($args{template}) { $html_form .= $form->render; } else { $args{description} = qq(

$args{description}

) if defined $args{description}; $html_form .= qq(

$form_title

$args{description}) . _touch_up($form->render, $cancel, $form_id) . '
'; $html_form = qq($renderer_config->{misc}->{doctype}$form_title$html_head$html_form) unless $args{no_head}; $html_form .= qq() if $args{javascript_code}; } $args{output}?$output->{output} = $html_form:print $html_form; } return $output; } sub render_as_table { my ($self, %args) = (@_); my ($table, @controllers, $output, $query_hidden_fields, $q, $sort_by_column, $table_config); my $class = $self->object_class(); my $query = $args{cgi} || CGI->new; my $url = $args{url} || $query->url(-absolute => 1); my $renderer_config = _get_renderer_config($class); foreach my $option (keys %{$renderer_config->{table}}) { if (defined $args{$option}) { $table_config->{$option} = $args{$option}; } else { $table_config->{$option} = $renderer_config->{table}->{$option}; } } my $table_title = $args{title} || _label($class->meta->table); my $like_operator = $args{like_operator} || $renderer_config->{db}->{like_operator}; my $template_url = $args{template_url} || $renderer_config->{template}->{url}; my $template_path = $args{template_path} || $renderer_config->{template}->{path}; (my $html_head = $args{html_head} || $renderer_config->{misc}->{html_head}) =~ s/\[%\s*title\s*%\]/$table_title/; my $ui_type = (caller(0))[3]; ($ui_type) = $ui_type =~ /^.*_(\w+)$/; my $table_id = _identify($class, $args{prefix}, $ui_type); my $primary_key = $class->meta->primary_key_column_names->[0]; my $relationships = _get_relationships($class); my $column_order = $args{order} || _get_column_order($class, $relationships); my $foreign_keys = _get_foreign_keys($class); my $param_list = {'sort_by' => 'sort_by', 'per_page' => 'per_page', 'page' => 'page', 'q' => 'q', 'ajax' => 'ajax', 'action' => 'action', 'object' => 'object', 'hide_table'}; if ($args{prefix}) { foreach my $param (keys %{$param_list}) { $param_list->{$param} = $table_id.'_'.$param; } } my $sort_by = $query->param($param_list->{'sort_by'}); if ($sort_by) { my $sort_by_column = $sort_by; $sort_by_column =~ s/\sdesc$//; my $sort_by_column_definition_method = $sort_by_column . '_definition'; my $sort_by_column_definition; $sort_by_column_definition = $class->$sort_by_column_definition_method if $class->can($sort_by_column_definition_method); unless (! exists $class->meta->{columns}->{$sort_by_column} || (defined $sort_by_column_definition && $sort_by_column_definition->{unsortable}) || (exists $args{columns} && exists $args{columns}->{$sort_by_column} && (exists $args{columns}->{$sort_by_column}->{value} || exists $args{columns}->{$sort_by_column}->{accessor} || $args{columns}->{$sort_by_column}->{unsortable}))) { if ($sort_by_column eq $primary_key) { $args{get}->{sort_by} = 't1.' . $sort_by; } else { $args{get}->{sort_by} = 't1.' . $sort_by . ', '. $class->meta->table . '.' . $primary_key; # append an unique column to the sort by clause to prevent inconsistent results using LIMIT and OFFSET in PostgreSQL } } } else { $args{get}->{sort_by} ||= $primary_key; # always sort by primary key by default to prevent inconsistent results using LIMIT and OFFSET in PostgreSQL } if ($args{searchable}) { $query_hidden_fields = _create_hidden_field($args{queries}); # this has to be done before appending 'q' to $args{queries}, which get serialised later as query stings if (defined $args{q}) { $q = $args{q}; } elsif (length $query->param($param_list->{'q'})) { $q = $query->param($param_list->{'q'}); } if (defined $q) { my ($or, @raw_qs, @qs); my $keyword_delimiter = $table_config->{keyword_delimiter}; if ($keyword_delimiter) { @raw_qs = split /$keyword_delimiter/, $q; } else { @raw_qs = $q; } my $like_search_values; foreach my $raw_q (@raw_qs) { $raw_q =~ s/^\s+|\s+$//g; push @qs, $raw_q; push @{$like_search_values}, '%' . $raw_q . '%'; } my $table_alias = {$class => 't1'}; my $table_to_class; if ($class->meta->isa('Rose::DB::Object::Metadata::Auto::Pg') && $args{get}) { my $counter = 1; ($table_alias, $table_to_class) = _alias_table($args{get}->{with_objects}, $class, \$counter, $table_alias, $table_to_class) if $args{get}->{with_objects}; ($table_alias, $table_to_class) = _alias_table($args{get}->{require_objects}, $class, \$counter, $table_alias, $table_to_class) if $args{get}->{require_objects}; } foreach my $searchable_column (@{$args{searchable}}) { my ($search_values, $search_class, $search_column, $search_method); if ($searchable_column =~ /\./) { my $search_table; ($search_table, $search_column) = split /\./, $searchable_column; $search_class = $table_to_class->{$search_table} || $class; } else { $search_class = $class; $search_column = $searchable_column; } if ($search_class->can($search_column . '_for_search')) { $search_method = $search_column.'_for_search'; foreach my $q (@qs) { my $search_result = $search_class->$search_method($q); push @{$search_values}, '%' . $search_result . '%' if $search_result; } } else { $search_values = $like_search_values; } if ($search_class && $search_class->meta->isa('Rose::DB::Object::Metadata::Auto::Pg') && exists $search_class->meta->{columns}->{$search_column} && ! $search_class->meta->{columns}->{$search_column}->isa('Rose::DB::Object::Metadata::Column::Character')) { my $searchable_column_text = 'text(' . $table_alias->{$search_class} . '.' . $search_column . ') ' . $like_operator . ' ?'; foreach my $search_value (@{$search_values}) { push @{$or}, [\$searchable_column_text => $search_value]; } } else { push @{$or}, $searchable_column => {$like_operator => $search_values} } } push @{$args{get}->{query}}, 'or' => $or; $args{queries}->{$param_list->{q}} = $q; $table_title = $args{search_result_title} || $table_config->{search_result_title}; $table_title =~ s/\[%\s*q\s*%\]/$q/; } } my $filtered_columns; my $filterable = $args{filterable} || $column_order; foreach my $column (@{$filterable}) { unless (exists $relationships->{$column}) { my $cgi_column; $cgi_column = $table_id.'_' if $args{prefix}; $cgi_column .= $column; if (length $query->param($cgi_column)) { my @cgi_column_values = $query->param($cgi_column); my $formatted_values; if ($class->can($column . '_for_filter')) { my $filter_method = $column . '_for_filter'; foreach my $cgi_column_value (@cgi_column_values) { my $filter_result = $class->$filter_method($cgi_column_value); push @{$formatted_values}, $filter_result if $filter_result; } } elsif ($class->can($column)) { $formatted_values = \@cgi_column_values; } if ($formatted_values) { push @{$filtered_columns}, $column => $formatted_values; } } } } if ($filtered_columns) { if($table_config->{or_filter}) { push @{$args{get}->{query}}, 'or' => $filtered_columns; } else { foreach my $filtered_column (@{$filtered_columns}) { push @{$args{get}->{query}}, $filtered_column; } } } $args{get}->{per_page} ||= $query->param($param_list->{'per_page'}) || $table_config->{per_page}; $args{get}->{page} ||= $query->param($param_list->{'page'}) || 1; my $objects = $self->get_objects(%{$args{get}}); ##Handle Submission my $reload_object; if ($query->param($param_list->{action})) { my $valid_form_actions = {create => undef, edit => undef, copy => undef}; my $action = $query->param($param_list->{action}); if (exists $valid_form_actions->{$action} && $args{$action}) { $args{$action} = {} if $args{$action} eq 1; $args{$action}->{output} = 1; $args{$action}->{no_head} ||= 1 if $args{no_head}; $args{$action}->{order} ||= $args{order} if $args{order}; $args{$action}->{template} ||= 1 if $args{template}; @{$args{$action}->{queries}}{keys %{$args{queries}}} = values %{$args{queries}}; $args{$action}->{queries}->{$param_list->{action}} = $action; $args{$action}->{queries}->{$param_list->{sort_by}} = $query->param($param_list->{sort_by}) if $query->param($param_list->{sort_by}); $args{$action}->{queries}->{$param_list->{page}} = $query->param($param_list->{page}) if $query->param($param_list->{page}); $args{$action}->{prefix} ||= $table_id.'_form'; my $form; if ($action eq 'create') { $form = $class->render_as_form(%{$args{$action}}); } elsif ($query->param($param_list->{object})) { $args{$action}->{queries}->{$param_list->{object}} = $query->param($param_list->{object}); $args{$action}->{copy} = 1 if $action eq 'copy'; foreach my $object (@{$objects}) { if ($object->$primary_key eq $query->param($param_list->{object})) { $form = $object->render_as_form(%{$args{$action}}); last; } } } $output->{form}->{controller} = $form->{controller} if exists $form->{controller}; $form->{validate}?$reload_object = 1:$output->{output} = $form->{output}; } elsif ($query->param($param_list->{object})) { $reload_object = 1; my @object_ids = $query->param($param_list->{object}); my (%valid_object_ids, @action_objects); @valid_object_ids{@object_ids} = (); foreach my $object (@{$objects}) { push @action_objects, $object if exists $valid_object_ids{$object->$primary_key}; } if ($query->param($param_list->{action}) eq 'delete' && $args{delete}) { foreach my $action_object (@action_objects) { $action_object->delete_with_file; } } elsif (exists $args{controllers} && exists $args{controllers}->{$query->param($param_list->{action})}) { no strict 'refs'; foreach my $action_object (@action_objects) { if (ref $args{controllers}->{$query->param($param_list->{action})} eq 'HASH') { $output->{controller} = $args{controllers}->{$query->param($param_list->{action})}->{callback}->($action_object) if ref $args{controllers}->{$query->param($param_list->{action})}->{callback} eq 'CODE'; $args{hide_table} = 1 if exists $args{controllers}->{$query->param($param_list->{action})}->{hide_table}; } else { $output->{controller} = $args{controllers}->{$query->param($param_list->{action})}->($action_object) if ref $args{controllers}->{$query->param($param_list->{action})} eq 'CODE'; } } } } if(defined $output->{output}) { return $output if $args{output}; print $output->{output}; return; } } my ($previous_page, $next_page, $last_page, $total) = _pagination($self, $class, $args{get}); if($reload_object) { $args{get}->{page} = $last_page if $args{get}->{page} > $last_page; $objects = $self->get_objects(%{$args{get}}); } ##Render Table $args{hide_table} = 1 if $query->param($param_list->{'hide_table'}); unless ($args{hide_table}) { my ($html_table, $query_string); if ($args{controller_order}) { @controllers = @{$args{controller_order}}; } else { @controllers = keys %{$args{controllers}} if $args{controllers}; push @controllers, 'copy' if $args{copy}; push @controllers, 'edit' if $args{edit}; push @controllers, 'delete' if $args{delete}; } $args{queries}->{$param_list->{ajax}} = 1 if $args{ajax} && $args{template}; if(exists $args{queries}) { $query_string->{base} = _create_query_string($args{queries}); $query_string->{sort_by} = _create_query_string($args{queries}); $query_string->{page} = _create_query_string($args{queries}); } if($query->param($param_list->{sort_by})) { $query_string->{page} .= $param_list->{sort_by}.'='.$query->param($param_list->{sort_by}).'&'; $query_string->{exclusive} = $param_list->{sort_by}.'='.$query->param($param_list->{sort_by}).'&'; } $query_string->{complete} = $query_string->{page}; if ($query->param($param_list->{page})) { $query_string->{complete} .= $param_list->{page}.'='.$args{get}->{page}.'&'; $query_string->{exclusive} .= $param_list->{page}.'='.$args{get}->{page}.'&'; } ##Define Table if ($args{create}) { my $create_value = 'Create'; $create_value = $args{create}->{title} if ref $args{create} eq 'HASH' && exists $args{create}->{title}; $table->{create} = {value => $create_value, link => qq($url?$query_string->{complete}$param_list->{action}=create)} if $args{create}; } $table->{total_columns} = scalar @{$column_order} + scalar @controllers; foreach my $column (@{$column_order}) { my $head; $head->{name} = $column; my $column_definition_method = $column . '_definition'; my $column_definition; $column_definition = $class->$column_definition_method if $class->can($column_definition_method); if (exists $args{columns} && exists $args{columns}->{$column} && exists $args{columns}->{$column}->{label}) { $head->{value} = $args{columns}->{$column}->{label}; } else { $head->{value} = $column_definition->{label} || _label($column); } unless (exists $relationships->{$column} || $column_definition->{unsortable} || (exists $args{columns} && exists $args{columns}->{$column} && (exists $args{columns}->{$column}->{value} || exists $args{columns}->{$column}->{accessor} || $args{columns}->{$column}->{unsortable}))) { if ($query->param($param_list->{'sort_by'}) eq $column) { $head->{link} = qq($url?$query_string->{sort_by}$param_list->{sort_by}=$column desc); } else { $head->{link} = qq($url?$query_string->{sort_by}$param_list->{sort_by}=$column); } } push @{$table->{head}}, $head; } foreach my $controller (@controllers) { my $label; if (ref $args{controllers}->{$controller} eq 'HASH' && exists $args{controllers}->{$controller}->{label}) { $label = $args{controllers}->{$controller}->{label}; } else { $label = _label($controller); } push @{$table->{head}}, {name => $controller, value => $label}; } foreach my $object (@{$objects}) { my $row; $row->{object} = $object; my $object_id = $object->$primary_key; foreach my $column (@{$column_order}) { my $value; if(exists $args{columns} && exists $args{columns}->{$column} && exists $args{columns}->{$column}->{value}) #custom column value { $value = $args{columns}->{$column}->{value}->{$object_id} if exists $args{columns}->{$column}->{value}->{$object_id}; } elsif(exists $args{columns} && exists $args{columns}->{$column} && exists $args{columns}->{$column}->{accessor}) #custom column accessor { my $accessor = $args{columns}->{$column}->{accessor}; $value = $object->$accessor($column) if $object->can($accessor); } elsif (exists $relationships->{$column}) { $value = join $table_config->{delimiter}, map {$_->stringify_me} $object->$column; } else { my $view_method; if ($class->can($column . '_for_view')) { $view_method = $column . '_for_view'; } elsif ($class->can($column)) { $view_method = $column; } if ($view_method) { if (ref $class->meta->{columns}->{$column} eq 'Rose::DB::Object::Metadata::Column::Set') { $value = join $table_config->{delimiter}, $object->$view_method; } else { $value = $object->$view_method; } } } push @{$row->{columns}}, {name => $column, value => $value}; } foreach my $controller (@controllers) { my $label; if (ref $args{controllers}->{$controller} eq 'HASH' && exists $args{controllers}->{$controller}->{label}) { $label = $args{controllers}->{$controller}->{label}; } else { $label = _label($controller); } my $controller_query_string; if (ref $args{controllers}->{$controller} eq 'HASH' && exists $args{controllers}->{$controller}->{queries}) { $controller_query_string = $query_string->{exclusive}; $controller_query_string .= _create_query_string($args{controllers}->{$controller}->{queries}); } else { $controller_query_string = $query_string->{complete}; } push @{$row->{columns}}, {name => $controller, value => $label, link => qq($url?$controller_query_string$param_list->{action}=$controller&$param_list->{object}=$object_id)}; } push @{$table->{rows}}, $row; } unless ($table_config->{no_pagination}) { $table->{pager}->{first_page} = {value => 1, link => qq($url?$query_string->{page}$param_list->{page}=1)}; $table->{pager}->{previous_page} = {value => $previous_page, link => qq($url?$query_string->{page}$param_list->{page}=$previous_page)}; $table->{pager}->{next_page} = {value => $next_page, link => qq($url?$query_string->{page}$param_list->{page}=$next_page)}; $table->{pager}->{last_page} = {value => $last_page, link => qq($url?$query_string->{page}$param_list->{page}=$last_page)}; $table->{pager}->{current_page} = {value => $args{get}->{page}, link => qq($url?$query_string->{page}$param_list->{page}=$args{get}->{page})}; $table->{pager}->{total} = $total; if ($table_config->{pages} % 2) { $table->{pager}->{start_page} = $table->{pager}->{current_page}->{value} - ($table_config->{pages} - 1)/2; } else { $table->{pager}->{start_page} = $table->{pager}->{current_page}->{value} - $table_config->{pages}/2; } if ($table->{pager}->{start_page} < 1) { $table->{pager}->{start_page} = 1; } elsif ($table->{pager}->{last_page}->{value} >= $table_config->{pages} && $table->{pager}->{start_page} > $table->{pager}->{last_page}->{value} - $table_config->{pages}) { $table->{pager}->{start_page} = $table->{pager}->{last_page}->{value} - $table_config->{pages} + 1; } if ($table->{pager}->{last_page}->{value} < $table->{pager}->{start_page} + $table_config->{pages}) { $table->{pager}->{end_page} = $table->{pager}->{last_page}->{value} + 1; } else { $table->{pager}->{end_page} = $table->{pager}->{start_page} + $table_config->{pages}; } } if ($args{template}) { my ($template, $ajax); if($args{ajax}) { $template = $args{ajax_template} || $ui_type . '_ajax.tt'; $ajax = 1 if $query->param($param_list->{ajax}); } elsif($args{template} eq 1) { $template = $ui_type . '.tt'; } else { $template = $args{template}; } $args{template_data} ||= {}; $html_table = _render_template(options => $args{template_options}, template_path => $template_path, file => $template, output => 1, data => { template_url => $template_url, javascript_code => $args{javascript_code}, ajax => $ajax, url => $url, query_string => $query_string, query_hidden_fields => $query_hidden_fields, q => $q, param_list => $param_list, sort_by_column => $sort_by_column, searchable => $args{searchable}, table => $table, objects => $objects, column_order => $column_order, table_id => $table_id, title => $table_title, description => $args{description}, class_label => _label($class->meta->table), doctype => $renderer_config->{misc}->{doctype}, html_head => $html_head, no_head => $args{no_head}, no_pagination => $table_config->{no_pagination}, extra => $args{extra}, %{$args{template_data}} }); } else { $args{description} = qq(

$args{description}

) if defined $args{description}; $html_table .= '
'; $html_table .= qq(
$query_hidden_fields
) if $args{searchable}; $html_table .= qq(

$table_title

$args{description}); $html_table .= qq(
$table->{create}->{value}
) if exists $table->{create}; $html_table .= qq(); $html_table .= ''; foreach my $head (@{$table->{head}}) { if (exists $head->{link}) { $html_table .= qq(); } else { $html_table .= qq(); } } $html_table .= ''; if($table->{rows}) { foreach my $row (@{$table->{rows}}) { $html_table .= ''; foreach my $column (@{$row->{columns}}) { if (exists $column->{link}) { $html_table .= qq(); } else { $html_table .= qq(); } } $html_table .= ''; } } else { $html_table .= qq(); } $html_table .= '
$head->{value}$head->{value}
$column->{value}$column->{value}
$table_config->{empty_message}
'; unless ($table_config->{no_pagination}) { $html_table .= '
'; if ($table->{pager}->{current_page}->{value} eq $table->{pager}->{first_page}->{value}) { $html_table .= qq(«); } else { $html_table .= qq(«); $html_table .= qq(); } while ($table->{pager}->{start_page} < $table->{pager}->{end_page}) { if ($table->{pager}->{start_page} == $table->{pager}->{current_page}->{value}) { $html_table .= qq($table->{pager}->{start_page}); } else { $html_table .= qq($table->{pager}->{start_page}); } $table->{pager}->{start_page}++; } if ($table->{pager}->{current_page}->{value} eq $table->{pager}->{last_page}->{value}) { $html_table .= qq(»); } else { $html_table .= qq(); $html_table .= qq(»); } $html_table .= '
'; } $html_table .= '
'; $html_table .= qq() if $args{javascript_code}; $html_table = qq($renderer_config->{misc}->{doctype}$table_title$html_head$html_table) unless $args{no_head}; } $args{output}?$output->{output} = $html_table:print $html_table; } return $output; } sub render_as_menu { my ($self, %args) = (@_); my($menu, $hide_menu_param, $current_param, $output, $content, $item_order, $items, $current, $template); my $class = $self->object_class(); my $menu_title = $args{title}; my $renderer_config = _get_renderer_config($class); my $template_url = $args{template_url} || $renderer_config->{template}->{url}; my $template_path = $args{template_path} || $renderer_config->{template}->{path}; my $ui_type = (caller(0))[3]; ($ui_type) = $ui_type =~ /^.*_(\w+)$/; my $menu_id = _identify($class, $args{prefix}, $ui_type); if ($args{prefix}) { $hide_menu_param = $menu_id.'_hide_menu'; $current_param = $menu_id.'_current'; } else { $hide_menu_param='hide_menu'; $current_param = 'current'; } my $query = $args{cgi} || CGI->new; my $url = $args{url} || $query->url(-absolute => 1); my $query_string = join ('&', map {"$_=$args{queries}->{$_}"} keys %{$args{queries}}); $query_string .= '&' if $query_string; if ($args{template} eq 1) { $template = $ui_type . '.tt'; } else { $template = $args{template}; } $current = $query->param($current_param) || $class->meta->table; $item_order = $args{order} || [$class]; $args{template_data} ||= {}; foreach my $item (@{$item_order}) { my $table = $item->meta->table; $items->{$item}->{table} = $table; if (defined $args{items} && defined $args{items}->{$item} && defined $args{items}->{$item}->{title}) { $items->{$item}->{label} = $args{items}->{$item}->{title}; } else { $items->{$item}->{label} = _label($table); } $items->{$item}->{link} = qq($url?$query_string$current_param=$table); if ($table eq $current) { my $options; $options = $args{items}->{$item} if exists $args{items} && exists $args{items}->{$item}; $options->{output} = 1; @{$options->{queries}}{keys %{$args{queries}}} = values %{$args{queries}}; $options->{queries}->{$current_param} = $table; $options->{prefix} ||= $menu_id.'_table'; $options->{url} ||= $url; $options->{template_data} = $args{template_data} unless exists $options->{template_data}; if ($args{ajax}) { my $valid_form_actions = {create => undef, edit => undef, copy => undef}; $args{hide_menu} = 1 if $query->param($options->{prefix}.'_ajax') && ! exists $valid_form_actions->{$query->param($options->{prefix}.'_action')}; } my $shortcuts = ['create', 'edit', 'copy', 'delete', 'template', 'ajax']; foreach my $shortcut (@{$shortcuts}) { $options->{$shortcut} = 1 if $args{$shortcut} && ! exists $options->{$shortcut}; } $options->{no_head} = 1; $output->{table} = "$item\::Manager"->render_as_table(%{$options}); $menu_title ||= $items->{$item}->{label}; } } $args{hide_menu} = 1 if $query->param($hide_menu_param); my $html_head = $args{html_head} || $renderer_config->{misc}->{html_head}; if ($args{template}) { $menu = _render_template( options => $args{template_options}, template_path => $template_path, file => $template, output => 1, data => { menu_id => $menu_id, no_head => $args{no_head}, doctype => $renderer_config->{misc}->{doctype}, html_head => $html_head, template_url => $template_url, items => $items, item_order => $item_order, current => $current, title => $menu_title, description => $args{'description'}, content => $output->{table}->{output}, hide => $args{hide_menu}, extra => $args{extra}, %{$args{template_data}} } ); } else { unless ($args{hide_menu}) { $args{description} = qq(

$args{description}

) if defined $args{description}; $menu = '
'.$args{'description'}.'
'; } $menu .= $output->{table}->{output}; $menu = qq($renderer_config->{misc}->{doctype}$menu_title$html_head$menu) unless $args{no_head}; } $args{output}?$output->{output} = $menu:print $menu; return $output; } sub render_as_chart { my ($self, %args) = (@_); my $class = $self->object_class(); my $title = $args{title} || _label($class->meta->table); my $renderer_config = _get_renderer_config($class); my $template_url = $args{template_url} || $renderer_config->{template}->{url}; my $template_path = $args{template_path} || $renderer_config->{template}->{path}; (my $html_head = $args{html_head} || $renderer_config->{misc}->{html_head}) =~ s/\[%\s*title\s*%\]/$title/; my $ui_type = (caller(0))[3]; ($ui_type) = $ui_type =~ /^.*_(\w+)$/; my $chart_id = _identify($class, $args{prefix}, $ui_type); my $hide_chart_param; if ($args{prefix}) { $hide_chart_param = $chart_id . '_hide_chart'; } else { $hide_chart_param = 'hide_chart'; } my $query = $args{cgi} || CGI->new; $args{hide_chart} ||= $query->param($hide_chart_param); return $args{output}?{}:undef if $args{hide_chart}; my ($chart, $output, $template); if (ref $args{engine} eq 'CODE') { no strict 'refs'; $chart = $args{engine}->($self, %args); } else { $args{options}->{chs} ||= $args{size} || '600x300'; $args{options}->{chco} ||= 'ff6600'; if (exists $args{type}) { my $type = { pie => 'p', bar => 'bvg', line => 'ls' }; if (exists $type->{$args{type}}) { $args{options}->{cht} ||= $type->{$args{type}}; unless (exists $args{options}->{chd}) { my (@values, @labels); if ($args{type} eq 'pie' && $args{column} && $args{values}) { my $foreign_keys = _get_foreign_keys($class); foreach my $value (@{$args{values}}) { push @values, $self->get_objects_count(query => [ $args{column} => $value ]); if (exists $foreign_keys->{$args{column}}) { my $foreign_class = $foreign_keys->{$args{column}}->{class}; my $foreign_class_primary_key = $foreign_class->meta->primary_key_column_names->[0]; my $foreign_object = $foreign_class->new($foreign_class_primary_key => $value); if($foreign_object->load(speculative => 1)) { push @labels, $foreign_object->stringify_me; } } else { push @labels, $value; } } $args{options}->{chd} = 't:' . join (',', @values); } elsif ($args{objects} && $args{columns}) { my $min = 0; my $max = 0; $args{options}->{chxt} ||= 'x,y'; $args{options}->{chdl} ||= join ('|', @{$args{columns}}); my $objects = $self->get_objects(query => [id => $args{objects}]); @labels = map {$_->stringify_me} @{$objects}; foreach my $column (@{$args{columns}}) { my @object_values; foreach my $object (@{$objects}) { if ($object->$column) { push (@object_values, $object->$column); if ($object->$column > $max) { $max = $object->$column; } elsif($object->$column < $min) { $min = $object->$column; } } else { push (@object_values, 0); } } push (@values, join (',', @object_values)); } $args{options}->{chd} = 't:' . join ('|', @values); $args{options}->{chds} ||= $min . ',' . $max; unless (exists $args{options}->{chxl} || ($max <= 100 && $min >= 0)) { my $avg = ($max - abs($min)) / 2; my $max_avg = ($max - abs($avg)) / 2 + $avg; my $min_avg = ($avg - abs($min)) / 2; $args{options}->{chxl} = '1:|' . join ('|', ($min, $min_avg, $avg, $max_avg, $max)); } } $args{options}->{chl} = join ('|', @labels); } } } my $chart_url = 'http://chart.apis.google.com/chart?' . _create_query_string($args{options}); if ($args{template}) { if($args{template} eq 1) { $template = $ui_type . '.tt'; } else { $template = $args{template}; } $args{template_data} ||= {}; $chart = _render_template( options => $args{template_options}, template_path => $template_path, file => $template, output => 1, data => { template_url => $template_url, chart => $chart_url, options => $args{'options'}, chart_id => $chart_id, title => $title , description => $args{'description'}, no_head => $args{no_head}, doctype => $renderer_config->{misc}->{doctype}, html_head => $html_head, extra => $args{extra}, %{$args{template_data}} } ); } else { $args{description} = qq(

$args{description}

) if defined $args{description}; $chart = qq(

$title

$args{'description'}$title
); $chart = qq($renderer_config->{misc}->{doctype}$title$html_head$chart) unless $args{no_head}; } } $args{output}?$output->{output} = $chart:print $chart; return $output; } sub _render_template { my %args = (@_); if ($args{file} && $args{data} && $args{template_path}) { my $options = $args{options}; $options->{INCLUDE_PATH} ||= $args{template_path}; my $template = Template->new(%{$options}); if($args{output}) { my $output = ''; $template->process($args{file},$args{data}, \$output) || die $template->error(), "\n"; return $output; } else { $template->process($args{file},$args{data}); } } } #rdbo util sub _get_renderer_config { my $self = shift; return $self->renderer_config if $self->can('renderer_config'); return config(); } sub _pagination { my ($self, $class, $get) = @_; my $total = $self->get_objects_count(%{$get}); my ($last_page, $next_page, $previous_page); if ($total < $get->{per_page}) { $last_page = 1; } else { my $pages = $total / $get->{per_page}; if ($pages == int $pages) { $last_page = $pages; } else { $last_page = 1 + int($pages); } } if ($get->{page} eq $last_page) { $next_page = $last_page; } else { $next_page = $get->{page} + 1; } if ($get->{page} eq 1) { $previous_page = 1; } else { $previous_page = $get->{page} - 1; } return ($previous_page, $next_page, $last_page, $total); } sub _copy_object { my ($self, $class, $table, $field_order, $form, $form_id, $prefix, $relationships, $relationship_object) = @_; my $clone = Rose::DB::Object::Helpers::clone_and_reset($self); $clone->save(); # need the auto generated primary key for files; my $renderer_config = _get_renderer_config($self); my $primary_key = $self->meta->primary_key_column_names->[0]; my $self_upload_path = File::Spec->catdir($renderer_config->{upload}->{path}, $self->stringify_class, $self->$primary_key); File::Copy::Recursive::dircopy($self_upload_path, File::Spec->catdir($renderer_config->{upload}->{path}, $self->stringify_class, $clone->$primary_key)) if -d $self_upload_path; return _update_object($clone, $class, $table, $field_order, $form, $form_id, $prefix, $relationships, $relationship_object); } sub _update_object { my ($self, $class, $table, $field_order, $form, $form_id, $prefix, $relationships, $relationship_object) = @_; my $primary_key = $self->meta->primary_key_column_names->[0]; foreach my $field (@{$field_order}) { my $column = $field; $column =~ s/$form_id\_// if $prefix; my $field_value; my @values = $form->field($field); my $values_size = scalar @values; if($values_size > 1) { $field_value = join _get_renderer_config($self)->{form}->{delimiter}, @values; } else { $field_value = $form->field($field); #if this line is removed, $form->field function will still think it should return an array, which will fail for file upload } if (exists $relationships->{$column}) #one to many or many to many { my $foreign_class = $relationships->{$column}->{class}; my $foreign_class_foreign_keys = _get_foreign_keys($foreign_class); my $foreign_key; foreach my $fk (keys %{$foreign_class_foreign_keys}) { if ($foreign_class_foreign_keys->{$fk}->{class} eq $class) { $foreign_key = $fk; last; } } my $default = undef; $default = $relationships->{$column}->{class}->meta->{columns}->{$table.'_id'}->{default} if defined $relationships->{$column}->{class}->meta->{columns}->{$table.'_id'}->{default}; if(length($form->cgi_param($field))) # $form->field($field) won't work { my ($new_foreign_object_id, $old_foreign_object_id, $value_hash, $new_foreign_object_id_hash); my $foreign_class_primary_key = $relationships->{$column}->{class}->meta->primary_key_column_names->[0]; foreach my $id (@values) { push @{$new_foreign_object_id}, $foreign_class_primary_key => $id; $value_hash->{$id} = undef; push @{$new_foreign_object_id_hash}, {$foreign_class_primary_key => $id}; } foreach my $id (keys %{$relationship_object->{$column}}) { push @{$old_foreign_object_id}, $foreign_class_primary_key => $id unless exists $value_hash->{$id}; } if ($relationships->{$column}->{type} eq 'one to many') { Rose::DB::Object::Manager->update_objects(object_class => $foreign_class, set => {$foreign_key => $default}, where => [or => $old_foreign_object_id]) if $old_foreign_object_id; Rose::DB::Object::Manager->update_objects(object_class => $foreign_class, set => {$foreign_key => $self->$primary_key}, where => [or => $new_foreign_object_id]) if $new_foreign_object_id; } else #many to many { $self->$column(@{$new_foreign_object_id_hash}); } } else { if ($relationships->{$column}->{type} eq 'one to many') { Rose::DB::Object::Manager->update_objects(object_class => $foreign_class, set => {$foreign_key => $default}, where => [$foreign_key => $self->$primary_key]); } else #many to many { $self->$column([]); # cascade deletes foreign objects } } } else { my $update_method; if ($class->can($column . '_for_update')) { $update_method = $column . '_for_update'; } elsif ($class->can($column)) { $update_method = $column; } if ($update_method) { if (length($form->cgi_param($field))) { $self->$update_method($field_value); } else { $self->$update_method(undef); } } } } $self->save; return $self; } sub _create_object { my ($self, $class, $table, $field_order, $form, $form_id, $prefix, $relationships, $relationship_object) = @_; my $custom_field_value; $self = $self->new(); foreach my $field (@{$field_order}) { if(defined $form->cgi_param($field) && length($form->cgi_param($field))) { my $column = $field; $column =~ s/$form_id\_// if $prefix; my @values = $form->field($field); if (exists $relationships->{$column}) #one to many or many to many { my $new_foreign_object_id_hash; my $foreign_class_primary_key = $relationships->{$column}->{class}->meta->primary_key_column_names->[0]; foreach my $id (@values) { push @{$new_foreign_object_id_hash}, {$foreign_class_primary_key => $id}; } $self->$column(@{$new_foreign_object_id_hash}); } else { my $field_value; my $values_size = scalar @values; if($values_size > 1) { $field_value = join _get_renderer_config($self)->{form}->{delimiter}, @values; } else { $field_value = $form->field($field); #if this line is removed, $form->field function will still think it should return an array, which will fail for file upload } if ($class->can($column . '_for_update')) { $custom_field_value->{$column . '_for_update'} = $field_value; #save it for later $self->$column('0') if $self->meta->{columns}->{$column}->{not_null}; # zero fill not null columns } elsif ($class->can($column)) { $self->$column($field_value); } } } } $self->save; #after save, run formatting methods, which may require an id, such as file upload if ($custom_field_value) { foreach my $update_method (keys %{$custom_field_value}) { $self->$update_method($custom_field_value->{$update_method}); } $self->save; } return $self; } sub _get_column_order { my ($class, $relationships) = @_; my $order; foreach my $column (sort {$a->ordinal_position <=> $b->ordinal_position} @{$class->meta->columns}) { push @{$order}, "$column" unless exists $column->{is_primary_key_member}; } foreach my $relationship (keys %{$relationships}) { push @{$order}, $relationship; } return $order; } sub _get_foreign_keys { my $class = shift; my $foreign_keys; foreach my $foreign_key (@{$class->meta->foreign_keys}) { (my $key, my $value) = $foreign_key->_key_columns; $foreign_keys->{$key} = {name => $foreign_key->name, table => $foreign_key->class->meta->table, column => $value, is_required => $foreign_key->is_required, class => $foreign_key->class}; } return $foreign_keys; } sub _get_unique_keys { my $class = shift; my $unique_keys; foreach my $unique_key (@{$class->meta->{unique_keys}}) { $unique_keys->{$unique_key->columns->[0]} = undef; } return $unique_keys; } sub _get_relationships { my $class = shift; my $relationships; foreach my $relationship (@{$class->meta->relationships}) { if ($relationship->type eq 'one to many') { $relationships->{$relationship->name}->{type} = $relationship->type; $relationships->{$relationship->name}->{class} = $relationship->class; } elsif($relationship->type eq 'many to many') { $relationships->{$relationship->name}->{type} = $relationship->type; $relationships->{$relationship->name}->{class} = $relationship->foreign_class; } } return $relationships; } sub delete_with_file { my $self = shift; return unless ref $self; my $primary_key = $self->meta->primary_key_column_names->[0]; my $directory = File::Spec->catdir(_get_renderer_config($self)->{upload}->{path}, $self->stringify_class, $self->$primary_key); rmtree($directory) || die ("Could not remove $directory") if -d $directory; return $self->delete(); } sub stringify_me { my $self = shift; my @values; foreach my $column (sort {$a->ordinal_position <=> $b->ordinal_position} @{$self->meta->columns}) { my $column_definition_method = $column . '_definition'; if ($self->can($column_definition_method) && $self->$column_definition_method->{stringify}) # filter primary keys and custom coded columns { my $for_view_method = $column . '_for_view'; if ($self->can($for_view_method)) { push @values, $self->$for_view_method; } else { push @values, $self->$column; } } } return join _get_renderer_config($self)->{misc}->{stringify_delimiter}, @values if @values; my $primary_key = $self->meta->primary_key_column_names->[0]; return $self->$primary_key; } sub stringify_class { my $self = shift; my $package_name = lc ref $self || lc $self; $package_name =~ s/::/_/g; return $package_name; } # file util sub _get_file_path { my ($self, $column) = @_; my $value = $self->$column; return unless $value; my $primary_key = $self->meta->primary_key_column_names->[0]; return File::Spec->catfile(_get_renderer_config($self)->{upload}->{path}, $self->stringify_class, $self->$primary_key, $column, $value); } sub _get_file_url { my ($self, $column) = @_; my $value = $self->$column; return unless $value; my $primary_key = $self->meta->primary_key_column_names->[0]; return File::Spec->catfile(_get_renderer_config($self)->{upload}->{url}, $self->stringify_class, $self->$primary_key, $column, CGI::escape($value)); } # formatting methods sub _create_timestamp { my ($self, $column) = @_; my $dt = DateTime->now->set_time_zone(_get_renderer_config($self)->{misc}->{time_zone}); return $dt->dmy('/').' '.$dt->hms; } sub _edit_date { my ($self, $column) = @_; return $self->$column unless ref $self->$column eq 'DateTime'; $self->$column->set_time_zone(_get_renderer_config($self)->{misc}->{time_zone}); return $self->$column->dmy('/') if $self->$column; } sub _edit_time { my ($self, $column) = @_; return $self->$column unless ref $self->$column eq 'Time::Clock'; return $self->$column->format('%H:%M'); } sub _update_date { my ($self, $column, $value) = @_; return unless $value; my ($d, $m, $y) = split '/', $value; my $dt; eval {$dt = DateTime->new(year => $y, month => $m, day => $d, time_zone => _get_renderer_config($self)->{misc}->{time_zone})}; return if $@; return $self->$column($dt->ymd); } sub _udpate_time { my ($self, $column, $value) = @_; return unless $value; my ($h, $m) = split ':', $value; my $t; eval {$t = Time::Clock->new(hour => $h, minute => $m)}; return if $@; return $self->$column($t); } sub _update_file { my ($self, $column, $value) = @_; return unless $value && $value ne ''; my $renderer_config = _get_renderer_config($self); my $primary_key = $self->meta->primary_key_column_names->[0]; my $upload_path = File::Spec->catdir($renderer_config->{upload}->{path}, $self->stringify_class, $self->$primary_key, $column); mkpath($upload_path) unless -d $upload_path; my $file_name = "$value"; $file_name =~ s/.*[\/\\](.*)/$1/; my ($actual_name, $extension) = ($file_name =~ /(.*)\.(.*)$/); $actual_name ||= $file_name; my $current_file = $self->$column; my $old_file; $old_file = File::Spec->catfile($upload_path, $current_file) if $current_file; my $new_file = File::Spec->catfile($upload_path, $file_name); if ($old_file eq $new_file && -e $old_file) # same file name { my $counter = 1; my $backup_file = File::Spec->catfile($upload_path, $actual_name.'-'.$counter.'.'.$extension); while (-e $backup_file) { $counter++; $backup_file = File::Spec->catfile($upload_path, $actual_name.'-'.$counter.'.'.$extension); } move($old_file, $backup_file); $old_file = $backup_file; } if (copy($value, File::Spec->catfile($upload_path, $file_name))) { unlink($old_file) if $old_file && !$renderer_config->{upload}->{keep_old_files}; return $self->$column($file_name); } else { move($old_file, File::Spec->catfile($upload_path, $current_file)) if $old_file; return; } } sub _update_timestamp { my ($self, $column) = @_; return $self->$column(DateTime->now->set_time_zone(_get_renderer_config($self)->{misc}->{time_zone})); } sub _update_datetime { my ($self, $column, $value) = @_; return $self->$column(undef) if $value eq ''; my ($date, $time) = split /\s+/, $value; my ($d, $m, $y) = split '/', $date; my ($hour, $minute) = split ':', $time; my $dt; eval {$dt = DateTime->new(year => $y, month => $m, day => $d, hour => $hour, minute => $minute, time_zone => _get_renderer_config($self)->{misc}->{time_zone})}; return if $@; return $self->$column($dt); } sub _view_file { my ($self, $column) = @_; my $value = $self->$column; return unless $value; my $file_url = _get_file_url($self, $column); return qq($value); } sub _view_image { my ($self, $column) = @_; my $value = $self->$column; return unless $value; my $file_url = _get_file_url($self, $column); return qq($value); } sub _view_media { my ($self, $column) = @_; my $url = _get_file_url($self, $column); return unless $url; return qq(media); } sub _view_address { my ($self, $column) = @_; my $value = $self->$column; return unless $value; my $a = $value; $a =~ s/([^A-Za-z0-9])/sprintf("%%%02X", ord($1))/seg; return qq($value); # output=js for inline map } sub _view_timestamp { my ($self, $column) = @_; return unless $self->$column && ref $self->$column eq 'DateTime'; return $self->$column->strftime('%d/%m/%Y %H:%M:%S'); } sub _view_datetime { my ($self, $column) = @_; return unless $self->$column && ref $self->$column eq 'DateTime'; return $self->$column->strftime('%d/%m/%Y %H:%M'); } sub _search_boolean { my ($self, $column, $value) = @_; my $mapping; if ($self->meta->isa('Rose::DB::Object::Metadata::Auto::Pg')) { $mapping = {'Yes' => 'true', 'No' => 'false', 'yes' => 'true', 'no' => 'false'}; } else { $mapping = {'Yes' => 1, 'No' => 0, 'yes' => 1, 'no' => 0}; } return $mapping->{$value}; } sub _search_date { my ($self, $column, $value) = @_; $value =~ s/\//-/g; my ($d, $m, $y) = ($value =~ /^(0?[1-9]|[1-2][0-9]|3[0-1])\-(0?[1-9]|1[0-2])\-([0-9]{4})/); if ($d && $m && $y) { $value =~ s/$d\-$m\-$y/$y\-$m\-$d/; } else { ($d, $m) = ($value =~ /^(0?[1-9]|[1-2][0-9]|3[0-1])\-(0?[1-9]|1[0-2])/); if ($d && $m) { $value =~ s/$d\-$m/$m\-$d/; } } return $value; } sub _search_percentage { my ($self, $column, $value) = @_; return $value/100; } #misc util sub _unique { my ($config, $class, $column, $value, $form) = @_; my $existing; if (exists $config->{columns}->{$column} && exists $config->{columns}->{$column}->{format} && exists $config->{columns}->{$column}->{format}->{for_filter}) { $existing = $class->new($column => $config->{columns}->{$column}->{format}->{for_filter}->($class, $column, $value))->load(speculative => 1); } else { $existing = $class->new($column => $value)->load(speculative => 1); } return 1 unless $existing; (my $prefix = $form->name) =~ s/_form$//; return unless $form->field('action') eq 'edit' || $form->field($prefix.'_action') eq 'edit'; my $primary_key = $class->meta->primary_key_column_names->[0]; return 1 if $existing->$primary_key == $form->field('object') || $existing->$primary_key == $form->field($prefix.'_object'); return; } sub _identify { my ($class, $prefix, $ui_type) = @_; unless ($prefix) { ($prefix = lc $class) =~ s/::/_/g; $prefix .= '_'. $ui_type; } return $prefix; } sub _label { my $string = shift; $string =~ s/_/ /g; $string =~ s/\b(\w)/\u$1/g; return $string; } sub _create_hidden_field { my $queries = shift; my $hidden_field; foreach my $query_key (keys %{$queries}) { if (ref $queries->{$query_key} eq 'ARRAY') { foreach my $value (@{$queries->{$query_key}}) { $hidden_field .= ''; } } else { $hidden_field .= ''; } } return $hidden_field; } sub _create_query_string { my $queries = shift; my $query_string; foreach my $query_key (keys %{$queries}) { if (ref $queries->{$query_key} eq 'ARRAY') { foreach my $value (@{$queries->{$query_key}}) { $query_string .= $query_key.'='.CGI::escape($value).'&'; } } else { $query_string .= $query_key.'='.CGI::escape($queries->{$query_key}).'&'; } } return $query_string; } sub _touch_up { my ($rendering, $cancel, $form_id) = @_; $rendering =~ s/onclick="this\.form\._submit\.value = this\.value;" type="submit" value="$cancel"/onclick="this.form.$form_id\_submit_cancel.value = 1;" type="submit" value="$cancel"/; return $rendering; } sub _alias_table { my ($with_require_objects, $class, $counter, $table_alias, $table_to_class) = @_; foreach my $with_require_object (@{$with_require_objects}) { if (exists $class->meta->{relationships}->{$with_require_object}) { if ($class->meta->{relationships}->{$with_require_object}->type eq 'many to many') { $table_alias->{$class->meta->{relationships}->{$with_require_object}->{map_class}} = 't' . ++$$counter; $table_to_class->{$class->meta->{relationships}->{$with_require_object}->{map_class}->meta->table} = $class->meta->{relationships}->{$with_require_object}->{map_class}; $table_alias->{$class->meta->{relationships}->{$with_require_object}->{foreign_class}} = 't' . ++$$counter; $table_to_class->{$class->meta->{relationships}->{$with_require_object}->{foreign_class}->meta->table} = $class->meta->{relationships}->{$with_require_object}->{foreign_class}; } else { $table_alias->{$class->meta->{relationships}->{$with_require_object}->{class}} = 't' . ++$$counter; $table_to_class->{$class->meta->{relationships}->{$with_require_object}->{class}->meta->table} = $class->meta->{relationships}->{$with_require_object}->{class}; } } } return ($table_alias, $table_to_class); } 1; __END__ =head1 NAME Rose::DBx::Object::Renderer - Web UI Rendering for Rose::DB::Object =head1 SYNOPSIS use Rose::DBx::Object::Renderer; use CGI; my $query = new CGI; print $query->header(); # Load all tables in the local MySQL database named 'company' my $renderer = Rose::DBx::Object::Renderer->new( config => { db => {name => 'company', username => 'root', password => 'password'} }, load => 1 ); # Render a form to add new employee Company::Employee->render_as_form(); # Render an object as a form my $e = Company::Employee->new(id => 1); $e->load; $e->render_as_form(); # Render a link to a google map for the 'address' column print $e->address_for_view(); # Render a table Company::Employee::Manager->render_as_table(); # Render a table for all the employees who love 'Coding' with create, copy, edit, and delete access Company::Employee::Manager->render_as_table( get => {query => [hobby => 'Coding']} order => ['first_name', 'email', 'address', 'phone'], create => 1, copy => 1, edit => 1, delete => 1, searchable => ['first_name', 'address'] ); # Render a menu my $menu = Company::Employee::Manager->render_as_menu( order => ['Company::Employee', 'Company::Position'], edit => 1, delete => 1, ); # Render a pie chart via Google Chart API Company::Employee::Manager->render_as_chart( type => 'pie', values => ['Coding', 'Cooking'], column => 'hobby', ); # Render a bar chart Company::Employee::Manager->render_as_chart( type => 'bar', title => 'The Employee Bar Chart', description => 'A useful bar chart.', columns => ['salary', 'tax'], objects => [1, 2, 3], options => {chco => 'ff6600,ffcc00'} # the color for each bar ); =head1 DESCRIPTION Rose::DBx::Object::Renderer generates web UIs for L. It encapsulates many web conventions in the generated UIs as default behaviours. For example, email addresses are by default rendered as C links in tables and appropiate validation is enforced automatically in forms. These behaviours are highly extensible. Renderer uses L to generate forms and the Google Chart API to render charts. L is used for template processing, however, Renderer can dynamically generate a full set of UIs without any templates. =head1 RESTRICTIONS =over 4 =item * Must follow the default conventions in L. =item * Limited support for database tables with multiple primary keys. =back =head1 METHODS =head2 C To instantiate a new Renderer object: my $renderer = Rose::DBx::Object::Renderer->new(config => {db => {name => 'company', username => 'root', password => 'root'}}, load => 1); Since Renderer inherits from L, the above line is equivalent to: my $renderer = Rose::DBx::Object::Renderer->new(); $renderer->config({db => {name => 'company', username => 'root', password => 'root'}}); $renderer->load(); =head2 C A Renderer instance inherits the default configurations in Renderer, which is accessible by: my $config = $renderer->config(); C accepts a hashref for configuring the Renderer object. =head3 C The C option is for configuring database related settings, for instance: $renderer->config({ db => { name => 'product', type => 'Pg', # defaulted to 'mysql' host => '10.0.0.1', port => '5432', username => 'admin', password => 'password', tables_are_singular => 1, # defines table name conventions, defaulted to undef like_operator => 'ilike', # to perform case-insensitive LIKE pattern matching in PostgreSQL, defaulted to 'like' } }); =head3 C