From f4df3b1280057755f3cf33e39471b7db1c94259c Mon Sep 17 00:00:00 2001 From: Paul Driver Date: Fri, 1 May 2009 23:03:36 +0000 Subject: [PATCH] template attachments (scripts and stylesheets) --- docs/upgrades/upgrade_7.7.5-7.7.6.pl | 17 ++ lib/WebGUI/Asset/Template.pm | 292 +++++++++++++++++++++- lib/WebGUI/Session/Style.pm | 99 ++++++-- lib/WebGUI/i18n/English/Asset_Template.pm | 42 ++++ t/Asset/Template.t | 47 +++- www/extras/templateAttachments.js | 203 +++++++++++++++ 6 files changed, 664 insertions(+), 36 deletions(-) create mode 100644 www/extras/templateAttachments.js diff --git a/docs/upgrades/upgrade_7.7.5-7.7.6.pl b/docs/upgrades/upgrade_7.7.5-7.7.6.pl index ed32524f8..9d49e3244 100644 --- a/docs/upgrades/upgrade_7.7.5-7.7.6.pl +++ b/docs/upgrades/upgrade_7.7.5-7.7.6.pl @@ -31,6 +31,7 @@ my $quiet; my $session = start(); # upgrade functions go here +addTemplateAttachmentsTable($session); revertUsePacked( $session ); finish($session); @@ -45,6 +46,22 @@ finish($session); # print "DONE!\n" unless $quiet; #} +sub addTemplateAttachmentsTable { + my $session = shift; + my $create = q{ + create table template_attachments ( + templateId varchar(22), + revisionDate bigint(20), + url varchar(256), + type varchar(20), + sequence int(11), + + primary key (templateId, revisionDate, url) + ) + }; + $session->db->write($create); +} + #---------------------------------------------------------------------------- # Rollback usePacked. It should be carefully applied manually for now sub revertUsePacked { diff --git a/lib/WebGUI/Asset/Template.pm b/lib/WebGUI/Asset/Template.pm index 972740542..9f2d7ba42 100644 --- a/lib/WebGUI/Asset/Template.pm +++ b/lib/WebGUI/Asset/Template.pm @@ -19,9 +19,17 @@ use base 'WebGUI::Asset'; use WebGUI::International; use WebGUI::Asset::Template::HTMLTemplate; use WebGUI::Utility; +use WebGUI::Form; +use Tie::IxHash; use Clone qw/clone/; use HTML::Packer; +tie my %attachmentTypeNames, 'Tie::IxHash' => ( + stylesheet => 'CSS Stylesheet', + headScript => 'Javascript (head)', + bodyScript => 'Javascript (body)', +); + =head1 NAME Package WebGUI::Asset::Template @@ -43,6 +51,52 @@ These methods are available from this class: #------------------------------------------------------------------- +=head2 addAttachments ( attachments ) + +Adds attachments to this template. Attachments is an arrayref of hashrefs, +where each hashref should have at least url, type, and sequence as keys. + +=cut + +sub addAttachments { + my ($self, $attachments) = @_; + my $db = $self->session->db; + + my $sql = q{ + INSERT INTO template_attachments + (templateId, revisionDate, url, type, sequence) + VALUES + (?, ?, ?, ?, ?) + }; + + foreach my $a (@$attachments) { + my @params = ( + $self->getId, + $self->get('revisionDate'), + @{$a}{qw(url type sequence)} + ); + $db->write($sql, \@params); + } +} + +#------------------------------------------------------------------- + +=head2 addRevision ( ) + +Override the master addRevision to copy attachments + +=cut + +sub addRevision { + my $self = shift; + my $object = $self->SUPER::addRevision(@_); + $object->addAttachments($self->getAttachments); + + return $object; +} + +#------------------------------------------------------------------- + =head2 definition ( session, definition ) Defines the properties of this asset. @@ -146,6 +200,39 @@ sub duplicate { #------------------------------------------------------------------- +=head2 removeAttachments ( urls ) + +Removes the specified attachments (specified by their urls, NOT by hashrefs, +since urls are unique to a particular revision) from this template. urls +should be an arrayref of strings. + +=cut + +sub removeAttachments { + my ($self, $urls) = @_; + + return unless @$urls; + + my $db = $self->session->db; + my $dbh = $db->dbh; + my $in = join(',', map { $dbh->quote($_) } @$urls); + my $rmsql = qq{ + DELETE FROM + template_attachments + WHERE + templateId = ? + AND revisionDate = ? + AND url IN ($in) + }; + my @params = ( + $self->getId, + $self->get('revisionDate'), + ); + $db->write($rmsql, \@params); +} + +#------------------------------------------------------------------- + =head2 packTemplate ( template ) Pack the template into a minified version for faster downloads. @@ -185,11 +272,76 @@ sub processPropertiesFromFormPost { $needsUpdate = 1; $data{extraHeadTags} = ''; } + + my $f = $self->session->form; + my $p = $f->paramsHashRef; + my @nums = grep {$_} map { my ($i) = /^attachmentUrl(\d+)$/; $i } keys %$p; + my @add; + + foreach my $n (@nums) { + my ($index, $type, $url) = + map { $f->process('attachment' . $_ . $n) } + qw(Index Type Url); + + push( + @add, { + sequence => $index, + url => $url, + type => $type, + } + ); + } + + my @remove = $f->process('removeAttachment', 'list'); + $data{removeAttachments} = \@remove; + $data{attachments} = \@add; + + $needsUpdate = 1 if @remove || @add; + $self->update(\%data) if $needsUpdate; } #------------------------------------------------------------------- +=head2 getAttachments ( [type] ) + +Returns an arrayref of hashrefs representing all attachments for this template +of the specified type (link, bodyScript, headScript). + +=head3 type + +If defined, will limit the attachments to this type; e.g., passing +'stylesheet' will return only stylesheets. + +=cut + +sub getAttachments { + my ( $self, $type ) = @_; + my @params = ($self->getId, $self->get('revisionDate')); + my $typeString; + + if ($type) { + $typeString = 'AND type = ?'; + push(@params, $type); + } + + my $sql = qq{ + SELECT + * + FROM + template_attachments + WHERE + templateId = ? + AND revisionDate = ? + $typeString + ORDER BY + type, sequence ASC + }; + return $self->session->db->buildArrayRefOfHashRefs($sql, \@params); +} + +#------------------------------------------------------------------- + =head2 getEditForm ( ) Returns the TabForm object that will be used in generating the edit page for this asset. @@ -230,11 +382,11 @@ sub getEditForm { -label=>$i18n->get('show in forms'), -hoverHelp=>$i18n->get('show in forms description'), ); - $tabform->getTab("properties")->codearea( + $tabform->getTab("properties")->codearea( -name=>"template", -label=>$i18n->get('assetName'), -hoverHelp=>$i18n->get('template description'), - -syntax => "html", + -syntax => "html", -value=>$self->getValue("template") ); $tabform->getTab('properties')->yesNo( @@ -257,8 +409,67 @@ sub getEditForm { -value=>$value, -label=>$i18n->get('parser'), -hoverHelp=>$i18n->get('parser description'), - ); + ); } + + my $session = $self->session; + my @headers = map { '' . $i18n->get("attachment header $_") . '' } + qw(index type url remove); + + my $table = ''; + $table .= "@headers"; + foreach my $a ( @{ $self->getAttachments } ) { + my ($seq, $type, $url) = @{$a}{qw(sequence type url)}; + # escape macros in the url so they don't get processed + $url =~ s/\^/^/g; + my $del = WebGUI::Form::checkbox( + $session, { + name => 'removeAttachment', + value => $url, + extras => 'class="id"', + } + ); + my @data = ( + "", + "", + "", + "", + ); + $table .= "@data"; + } + $table .= '
$seq$type$url$del
'; + my $properties = $tabform->getTab('properties'); + my $label = $i18n->get('attachment display label'); + $properties->raw("$label$table"); + + my @data = map { "$_" } ( + WebGUI::Form::integer( + $session, { size => '2', id => 'addBoxIndex' } + ), + WebGUI::Form::selectBox( + $session, { options => \%attachmentTypeNames, id => 'addBoxType' } + ), + WebGUI::Form::text($session, { id => 'addBoxUrl', size => 40 }), + WebGUI::Form::button( + $session, { + value => $i18n->get('attachment add button'), + extras => 'onclick="addClick()"' + } + ), + ); + + my ($style, $url) = $self->session->quick(qw(style url)); + $style->setScript($url->extras('yui/build/yahoo/yahoo-min.js')); + $style->setScript($url->extras('yui/build/json/json-min.js')); + $style->setScript($url->extras('yui/build/dom/dom-min.js')); + + pop(@headers); + my $scriptUrl = $url->extras('templateAttachments.js'); + $table = "@headers@data
"; + $table .= qq(); + $label = $i18n->get('attachment add field label'); + $properties->raw("$label$table"); + return $tabform; } @@ -370,15 +581,42 @@ A hash reference containing template variables to be processed for the head bloc sub prepare { my $self = shift; - my $vars = shift; + my $vars = shift; $self->{_prepared} = 1; - my $templateHeadersSent = $self->session->stow->get("templateHeadersSent") || []; - my @sent = @{$templateHeadersSent}; - unless (isIn($self->getId, @sent)) { # don't send head block if we've already sent it for this template - $self->session->style->setRawHeadTags($self->getParser($self->session, $self->get('parser'))->process($self->getExtraHeadTags, $vars)); - } - push(@sent, $self->getId); - $self->session->stow->set("templateHeadersSent", \@sent); + + my $sent = $self->session->stow->get('templateHeadersSent'); + unless ($sent) { + $self->session->stow->set('templateHeadersSent', $sent = []); + } + + my $id = $self->getId; + # don't send head block if we've already sent it for this template + return if isIn($id, @$sent); + + my $session = $self->session; + my ($db, $style) = $session->quick(qw(db style)); + my $parser = $self->getParser($session, $self->get('parser')); + my $headBlock = $parser->process($self->getExtraHeadTags, $vars); + + $style->setRawHeadTags($headBlock); + + foreach my $sheet ( @{ $self->getAttachments('stylesheet') } ) { + my %props = ( type => 'text/css', rel => 'stylesheet' ); + $style->setLink($sheet->{url}, \%props); + } + + my $doScripts = sub { + my ($type, $body) = @_; + foreach my $script ( @{ $self->getAttachments($type) } ) { + my %props = ( type => 'text/javascript' ); + $style->setScript($script->{url}, \%props, $body); + } + }; + + $doScripts->('headScript'); + $doScripts->('bodyScript', 1); + + push(@$sent, $id); } @@ -445,6 +683,27 @@ sub processRaw { return $class->getParser($session,$parser)->process($template, $vars); } +#------------------------------------------------------------------- + +=head2 purgeRevision ( ) + +Override the master purgeRevision to purge attachments + +=cut + +sub purgeRevision { + my $self = shift; + my $db = $self->session->db; + my $sql = q{ + DELETE FROM + template_attachments + WHERE + templateId = ? + AND revisionDate = ? + }; + $db->write($sql, [$self->getId, $self->get('revisionDate')]); + return $self->SUPER::purgeRevision(@_); +} #------------------------------------------------------------------- @@ -456,16 +715,27 @@ packages that contain headBlocks. This method is deprecated and will be removed in the future. Don't plan on this being here. +Note, we are also using this now to update template attachments. So maybe it +will stay. + =cut sub update { my $self = shift; my $requestedProperties = shift; my $properties = clone($requestedProperties); + + my $attachments = delete $properties->{attachments} || []; + my $removeAttachments = delete $properties->{removeAttachments} || []; + + $self->removeAttachments($removeAttachments); + $self->addAttachments($attachments); + if (exists $properties->{headBlock}) { $properties->{extraHeadTags} .= $properties->{headBlock}; delete $properties->{headBlock}; } + $self->SUPER::update($properties); } diff --git a/lib/WebGUI/Session/Style.pm b/lib/WebGUI/Session/Style.pm index 54b1bd3b5..e5d021bf4 100644 --- a/lib/WebGUI/Session/Style.pm +++ b/lib/WebGUI/Session/Style.pm @@ -67,6 +67,30 @@ sub DESTROY { undef $self; } +#------------------------------------------------------------------- + +sub _generateAdditionalTags { + my $var = shift; + return sub { + my $self = shift; + my $tags = $self->{$var}; + delete $self->{$var}; + WebGUI::Macro::process($self->session,\$tags); + return $tags; + }; +} + +#------------------------------------------------------------------- + +=head2 generateAdditionalBodyTags ( ) + +Creates tags that were set using setScript (if inBody was true) and setRawBodyTags. +Macros are processed in the tags if processed by this method. + +=cut + +BEGIN { *generateAdditionalBodyTags = _generateAdditionalTags('_rawBody') } + #------------------------------------------------------------------- @@ -77,15 +101,7 @@ Macros are processed in the tags if processed by this method. =cut -sub generateAdditionalHeadTags { - my $self = shift; - my $tags = $self->{_raw}; - delete $self->{_raw}; - WebGUI::Macro::process($self->session,\$tags); - return $tags; -} - - +BEGIN { *generateAdditionalHeadTags = _generateAdditionalTags('_raw') } #------------------------------------------------------------------- @@ -196,10 +212,16 @@ if ($self->session->user->isRegistered || $self->session->setting->get("preventP } else { $var{'head.tags'} .= '' } + # Removing the newlines will probably annoy people. # Perhaps turn it off under debug mode? $var{'head.tags'} =~ s/\n//g; + # head.tags = head_attachments . body_attachments + # keeping head.tags for backwards compatibility + $var{'head_attachments'} = $var{'head.tags'}; + $var{'head.tags'} .= ($var{'body_attachments'} = ''); + my $style = WebGUI::Asset::Template->new($self->session,$templateId); my $output; if (defined $style) { @@ -216,8 +238,10 @@ if ($self->session->user->isRegistered || $self->session->setting->get("preventP } WebGUI::Macro::process($self->session,\$output); $self->sent(1); - my $macroHeadTags = $self->generateAdditionalHeadTags(); - $output =~ s/\<\!--morehead--\>/$macroHeadTags/; + my $macroHeadTags = $self->generateAdditionalHeadTags(); + my $macroBodyTags = $self->generateAdditionalBodyTags(); + $output =~ s/\<\!--morehead--\>/$macroHeadTags/; + $output =~ s/\<\!--morebody--\>/$macroBodyTags/; return $output; } @@ -339,7 +363,33 @@ sub setMeta { $self->setRawHeadTags($tag); } +#------------------------------------------------------------------- +sub _setRawTags { + my $var = shift; + return sub { + my $self = shift; + my $tags = shift; + if ($self->sent) { + $self->session->output->print($tags); + } + else { + $self->{$var} .= $tags; + } + }; +} + +#------------------------------------------------------------------- + +=head2 setRawBodyTags ( tags ) + +Does exactly the same thing as setRawHeadTags, except that the tags will be +appended to a seperate variable (to be output after the body if the style +template supports it) instead. + +=cut + +BEGIN { *setRawBodyTags = _setRawTags('_rawBody') } #------------------------------------------------------------------- @@ -357,21 +407,11 @@ A raw string containing tags. This is just a raw string so you must actually pas =cut -sub setRawHeadTags { - my $self = shift; - my $tags = shift; - if ($self->sent) { - $self->session->output->print($tags); - } - else { - $self->{_raw} .= $tags; - } -} - +BEGIN { *setRawHeadTags = _setRawTags('_raw') } #------------------------------------------------------------------- -=head2 setScript ( url, params ) +=head2 setScript ( url, params, [inBody] ) Sets a '."\n"; $self->{_javascript}{$url} = 1; - $self->setRawHeadTags($tag); + if ($inBody) { + $self->setRawBodyTags($tag); + } + else { + $self->setRawHeadTags($tag); + } } #------------------------------------------------------------------- diff --git a/lib/WebGUI/i18n/English/Asset_Template.pm b/lib/WebGUI/i18n/English/Asset_Template.pm index 7b17560ee..cde93e6d9 100644 --- a/lib/WebGUI/i18n/English/Asset_Template.pm +++ b/lib/WebGUI/i18n/English/Asset_Template.pm @@ -298,6 +298,48 @@ Any scratch variables will be available in the template with this syntax:
context => q{Label for URL to make a duplicate and open the duplicate's edit screen}, }, + 'attachment header index' => { + message => 'Index', + lastUpdated => 1241192473, + context => q|header for the sequence number column for attachments|, + }, + + 'attachment header type' => { + message => 'Type', + lastUpdated => 1241192473, + context => q|header for the attachment types column|, + }, + + 'attachment header url' => { + message => 'Url', + lastUpdated => 1241192473, + context => q|header for the url column for attachments|, + }, + + 'attachment header remove' => { + message => 'Remove', + lastUpdated => 1241192473, + context => q|header for the remove button column for attachments|, + }, + + 'attachment display label' => { + message => 'Attachments', + lastUpdated => 1241192473, + context => q|field label for displaying existing attachments|, + }, + + 'attachment add field label' => { + message => 'Add Attachments', + lastUpdated => 1241192473, + context => q|field label for adding new attachments|, + }, + + 'attachment add button' => { + message => 'Add', + lastUpdated => 1241192473, + context => q|button text for adding a new attachment|, + }, + 'usePacked label' => { message => q{Use Packed Template}, lastUpdated => 0, diff --git a/t/Asset/Template.t b/t/Asset/Template.t index ae3579181..605ff673e 100644 --- a/t/Asset/Template.t +++ b/t/Asset/Template.t @@ -15,7 +15,7 @@ use lib "$FindBin::Bin/../lib"; use WebGUI::Test; use WebGUI::Session; use WebGUI::Asset::Template; -use Test::More tests => 15; # increment this value for each test you create +use Test::More tests => 32; # increment this value for each test you create use Test::Deep; my $session = WebGUI::Test->session; @@ -67,6 +67,51 @@ my $template3 = $importNode->addChild({ ok(!$template3->get('headBlock'), 'headBlock is empty'); is($template3->get('extraHeadTags'), 'tag1 tag2 tag3', 'extraHeadTags contains headBlock info'); +my @atts = ( + {type => 'headScript', sequence => 1, url => 'bar'}, + {type => 'headScript', sequence => 0, url => 'foo'}, + {type => 'stylesheet', sequence => 0, url => 'style'}, + {type => 'bodyScript', sequence => 0, url => 'body'}, +); + +$template3->addAttachments(\@atts); +my $att3 = $template3->getAttachments('headScript'); +is($att3->[0]->{url}, 'foo', 'has foo'); +is($att3->[1]->{url}, 'bar', 'has bar'); +is(@$att3, 2, 'proper size'); + +$template3->prepare; +ok(exists $session->style->{_link}->{style}, 'style in style'); +ok(exists $session->style->{_javascript}->{$_}, "$_ in style") for qw(foo bar body); + +# revision-ness of attachments + +# sleep so the revisiondate isn't duplicated +sleep 1; + +my $template3rev = $template3->addRevision(); +my $att4 = $template3rev->getAttachments('headScript'); +is($att4->[0]->{url}, 'foo', 'rev has foo'); +is($att4->[1]->{url}, 'bar', 'rev has bar'); +is(@$att4, 2, 'rev is proper size'); + +$template3rev->addAttachments([{type => 'headScript', sequence => 2, url => 'baz'}]); +$att4 = $template3rev->getAttachments('headScript'); +$att3 = $template3->getAttachments('headScript'); +is($att3->[0]->{url}, 'foo', 'original still has foo'); +is($att3->[1]->{url}, 'bar', 'original still has bar'); +is(@$att3, 2, 'original does not have new thing'); + +is($att4->[0]->{url}, 'foo', 'rev still has foo'); +is($att4->[1]->{url}, 'bar', 'rev still has bar'); +is($att4->[2]->{url}, 'baz', 'rev does have new thing'); +is(@$att4, 3, 'rev is proper size'); + +$template3rev->purgeRevision(); + + +# done checking revision stuff + $template->purge; $templateCopy->purge; $template3->purge; diff --git a/www/extras/templateAttachments.js b/www/extras/templateAttachments.js new file mode 100644 index 000000000..442712029 --- /dev/null +++ b/www/extras/templateAttachments.js @@ -0,0 +1,203 @@ +// Really only used by the Template editor, but it doesn't belong in perl +// code. It's too long. + +var addClick = (function() { + var uniqueId = 1; + var count = 0; + var urls = {}; + var types = {}; + var originals = {}; + var addAnchor = document.getElementById('addAnchor'); + var displayTable = document.getElementById('attachmentDisplay'); + + var nodes = { + table: '', + index: 'Index', + type: 'Type', + url: 'Url' + }; + + function init() { + displayTable.style.display = 'none'; + + for (var k in nodes) { + var id = 'addBox' + nodes[k]; + nodes[k] = document.getElementById(id); + } + + var opts = nodes.type.options; + for (var i = 0; i < opts.length; i++) { + var o = opts[i]; + types[o.value] = o.text; + } + + var query = YAHOO.util.Dom.getElementsByClassName; + var existing = query('existingAttachment', 'tr'); + for (var i = 0; i < existing.length; i++) { + var node = existing[i]; + var d = { + index: query('index', 'td', node)[0].innerHTML, + type: query('type', 'td', node)[0].innerHTML, + url: query('url', 'td', node)[0].innerHTML + }; + originals[d.url] = true; + add(d); + node.parentNode.removeChild(node); + } + } + + // When an original box gets changed, we should insert a removal node and + // name the fields so that we remove the old and insert the new. + function updater(u) { + return function() { + obj = urls[u]; + obj.index.onchange = null; + obj.type.onchange = null; + insertHidden(u); + nameFields(u); + }; + } + + // Give the fields for an attachment entry some names so that they'll get + // posted to the backend. + function nameFields(u) { + var id = uniqueId++; + var obj = urls[u]; + obj.index.name = 'attachmentIndex' + id; + obj.type.name = 'attachmentType' + id; + obj.url.name = 'attachmentUrl' + id; + } + + // Insert a hidden input field in the form so that the backend knows to + // remove one of the original attachments + function insertHidden(u) { + var tr = urls[u].tr; + var hid = document.createElement('input'); + hid.type = 'hidden'; + hid.name = 'removeAttachment'; + hid.value = u; + tr.parentNode.appendChild(hid); + delete originals[u]; + } + + // Make a function which will remove an attachment (remove the table row + // and insertHidden if necesary) + function remover(u) { + return function() { + var tr = urls[u].tr; + + if (originals[u]) { + insertHidden(u); + } + + tr.parentNode.removeChild(tr); + delete urls[u]; + + if (--count == 0) { + displayTable.style.display = 'none'; + } + } + } + + // Add a new attachment (proper table row, etc). Attachments that already + // existed (originals) will have unnamed fields so they don't get posted + // to the backend. + function add(d) { + if (urls[d.url]) { + alert('Already attached!'); + return; + } + + if (++count == 1) { + displayTable.style.display = 'block'; + } + + var index = document.createElement('input'); + index.size = 2; + index.value = d.index; + + var type = document.createElement('select'); + + for (var k in types) { + var o = document.createElement('option'); + o.value = k; + o.text = types[k]; + if (k == d.type) { + o.selected = true; + } + type.appendChild(o); + } + + var url = document.createElement('input'); + url.size = 40; + + var update = updater(d.url); + url.value = d.url; + url.oldValue = d.url; + url.onchange = function() { + var newValue = url.value; + var oldValue = url.oldValue; + if (urls[newValue]) { + url.value = oldValue; + alert('Already attached!'); + } + else { + url.oldValue = newValue; + var d = urls[oldValue]; + if (originals[oldValue]) { + update(); + } + + delete urls[oldValue]; + urls[newValue] = d; + } + }; + + var btn = document.createElement('input'); + btn.type = 'button'; + btn.value = 'Remove'; + btn.onclick = remover(d.url); + + var tr = document.createElement('tr'); + var data = [index, type, url, btn]; + for (var i = 0; i < data.length; i++) { + var td = document.createElement('td'); + td.appendChild(data[i]); + tr.appendChild(td); + } + + urls[d.url] = { + tr : tr, + index : index, + type : type, + url : url, + }; + + if (originals[d.url]) { + // url's is already taken care of above + index.onchange = update; + type.onchange = update; + } + else { + nameFields(d.url); + } + + addAnchor.appendChild(tr); + } + + init(); + return function() { + var d = { + index: nodes.index.value, + type: nodes.type.value, + url: nodes.url.value + }; + + d.url = d.url.replace(/^\s+|\s+$/g, '') + if (d.url == '') { + alert('No url!'); + return; + } + add(d); + }; +})();