template attachments (scripts and stylesheets)

This commit is contained in:
Paul Driver 2009-05-01 23:03:36 +00:00
parent 676b124697
commit f4df3b1280
6 changed files with 664 additions and 36 deletions

View file

@ -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 {

View file

@ -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 { '<th>' . $i18n->get("attachment header $_") . '</th>' }
qw(index type url remove);
my $table = '<table id="attachmentDisplay">';
$table .= "<thead><tr>@headers</tr></thead><tbody id='addAnchor'>";
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/\^/&#94;/g;
my $del = WebGUI::Form::checkbox(
$session, {
name => 'removeAttachment',
value => $url,
extras => 'class="id"',
}
);
my @data = (
"<td class='index'>$seq</td>",
"<td class='type'>$type</td>",
"<td class='url'>$url</td>",
"<td>$del</td>",
);
$table .= "<tr class='existingAttachment'>@data</tr>";
}
$table .= '</tbody></table>';
my $properties = $tabform->getTab('properties');
my $label = $i18n->get('attachment display label');
$properties->raw("<tr><td>$label</td><td>$table</td></tr>");
my @data = map { "<td>$_</td>" } (
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 = "<table id='addBox'><tr>@headers</tr><tr>@data</tr></table>";
$table .= qq(<script type="text/javascript" src="$scriptUrl"></script>);
$label = $i18n->get('attachment add field label');
$properties->raw("<tr><td>$label</td><td>$table</td></tr>");
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);
}

View file

@ -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'} .= '<meta http-equiv="Cache-Control" content="must-revalidate" />'
}
# 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'} = '<!--morebody-->');
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 <script> tag into the <head> of this rendered page for this
page view. This is typically used for dynamically adding references
@ -388,12 +428,18 @@ The URL to your script.
A hash reference containing the additional parameters to include in the script tag, such as "type" and "language".
=head3 inBody
Optional, defaults to false. If true, the script will be added to the
body_attachments variable instead of to head_attachments.
=cut
sub setScript {
my $self = shift;
my $url = shift;
my $params = shift;
my $inBody = shift;
return undef if ($self->{_javascript}{$url});
my $tag = '<script src="'.$url.'"';
foreach my $name (keys %{$params}) {
@ -401,7 +447,12 @@ sub setScript {
}
$tag .= '></script>'."\n";
$self->{_javascript}{$url} = 1;
$self->setRawHeadTags($tag);
if ($inBody) {
$self->setRawBodyTags($tag);
}
else {
$self->setRawHeadTags($tag);
}
}
#-------------------------------------------------------------------

View file

@ -298,6 +298,48 @@ Any scratch variables will be available in the template with this syntax:<br/>
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,

View file

@ -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;

View file

@ -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);
};
})();