Add RSSCapable mixin for RSS-capable assets, and RSSFromParent asset for

the feeds for those assets.  Convert Collaboration to use this mechanism
rather than its existing viewRSS, because this is more export-friendly.
This commit is contained in:
Drake 2006-11-03 01:18:55 +00:00
parent 61cdd872ac
commit 9a61dd2f38
7 changed files with 465 additions and 99 deletions

View file

@ -23,6 +23,7 @@ my $session = start(); # this line required
commerceSalesTax($session);
createDictionaryStorage($session);
addRssUrlMacroProcessing($session);
addRSSFromParent($session);
finish($session); # this line required
@ -66,6 +67,75 @@ sub createDictionaryStorage {
}
#-------------------------------------------------
sub addRSSFromParent {
my $session = shift;
print "\tAdding RSS From Parent capability.\n" unless $quiet;
$session->db->write($_) for (<<'EOT',
CREATE TABLE RSSFromParent (
assetId varchar(22) character set utf8 collate utf8_bin NOT NULL,
revisionDate bigint(20) NOT NULL,
PRIMARY KEY (assetId, revisionDate)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
EOT
<<'EOT',
CREATE TABLE RSSCapable (
assetId varchar(22) character set utf8 collate utf8_bin NOT NULL,
revisionDate bigint(20) NOT NULL,
rssCapableRssEnabled int(11) NOT NULL DEFAULT 1,
rssCapableRssTemplateId varchar(22) character set utf8 collate utf8_bin NOT NULL
DEFAULT 'PBtmpl0000000000000142',
rssCapableRssFromParentId varchar(22) character set utf8 collate utf8_bin NULL DEFAULT NULL,
PRIMARY KEY (assetId, revisionDate)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
EOT
);
my $oldTag = WebGUI::VersionTag->getWorking($session, 0);
my $templateTag = WebGUI::VersionTag->create($session, { name => '7.2.0 RSS template update' });
$templateTag->setWorking;
foreach my $templateId ($session->db->buildArray("SELECT assetId FROM template WHERE namespace = 'Collaboration/RSS'")) {
my $template = WebGUI::Asset->newByDynamicClass($session, $templateId)->addRevision;
$template->update({ namespace => 'RSSCapable/RSS' });
}
WebGUI::Asset->newByDynamicClass($session, 'PBtmpl0000000000000142')
->update({ title => 'Default RSS', menuTitle => 'Default RSS' });
$templateTag->commit;
# Need to get the Collaborations, since those now have RSS capability.
$session->db->write($_) for (<<'EOT',
INSERT INTO RSSCapable (assetId, revisionDate, rssCapableRssEnabled, rssCapableRssTemplateId,
rssCapableRssFromParentId)
SELECT assetId, revisionDate, 0, 'PBtmpl0000000000000142', NULL
FROM Collaboration
EOT
<<'EOT',
ALTER TABLE Collaboration
DROP COLUMN rssTemplateId
EOT
);
my $csTag = WebGUI::VersionTag->create($session, { name => '7.2.0 Collaboration RSS update' });
$csTag->setWorking;
foreach my $csId ($session->db->buildArray("SELECT assetId FROM Collaboration")) {
# Blah, some duplication with RSSCapable.pm.
my $cs = WebGUI::Asset->newByDynamicClass($session, $csId)->addRevision;
next if $cs->get('isPrototype'); # Uh.
my $rssFromParent =
$cs->addChild({ className => 'WebGUI::Asset::RSSFromParent',
title => $cs->get('title'),
menuTitle => $cs->get('menuTitle'),
url => $cs->get('url').'.rss'
});
$cs->update({ rssCapableRssFromParentId => $rssFromParent->getId });
}
$csTag->commit;
$oldTag->setWorking if $oldTag;
}
# ---- DO NOT EDIT BELOW THIS LINE ----
#-------------------------------------------------

View file

@ -2117,6 +2117,18 @@ sub www_view {
return undef;
}
#-------------------------------------------------------------------
=head2 isValidRssItem ( )
Returns true iff this asset should be included in RSS feeds from the
RSS From Parent asset. If false, this asset will be ignored when
generating feeds, even if it appears in the item list. Defaults to
true.
=cut
sub isValidRssItem { 1 }
1;

View file

@ -0,0 +1,174 @@
package WebGUI::Asset::RSSCapable;
=head1 LEGAL
-------------------------------------------------------------------
WebGUI is Copyright 2001-2006 Plain Black Corporation.
-------------------------------------------------------------------
Please read the legal notices (docs/legal.txt) and the license
(docs/license.txt) that came with this distribution before using
this software.
-------------------------------------------------------------------
http://www.plainblack.com info@plainblack.com
-------------------------------------------------------------------
=cut
use strict;
use NEXT;
use WebGUI::Asset::RSSFromParent;
=head1 NAME
WebGUI::Asset::RSSCapable
=head1 DESCRIPTION
An extra mixin class to be included before WebGUI::Asset in any asset
class that wishes its instances to be capable of generating RSS feeds
using the RSSFromParent asset.
=head1 SYNOPSIS
use base 'WebGUI::Asset::RSSCapable';
=cut
sub definition {
my $class = shift;
my $session = shift;
my $definition = shift;
my %properties;
tie %properties, 'Tie::IxHash';
my $i18n = WebGUI::International->new($session, 'Asset_RSSCapable');
# We do this prefixing to avoid name collisions because properties aren't namespaced.
%properties =
(
rssCapableRssEnabled => { tab => 'display',
fieldType => 'yesNo',
defaultValue => 1,
label => $i18n->get('rssEnabled label'),
hoverHelp => $i18n->get('rssEnabled hoverHelp')
},
rssCapableRssTemplateId => { tab => 'display',
fieldType => 'template',
defaultValue => 'PBtmpl0000000000000142',
namespace => 'RSSCapable/RSS',
label => $i18n->get('rssTemplateId label'),
hoverHelp => $i18n->get('rssTemplateId hoverHelp')
},
rssCapableRssFromParentId => { fieldType => 'hidden',
noFormPost => 1,
defaultValue => undef,
},
);
push @$definition, { assetName => $i18n->get('assetName'),
tableName => 'RSSCapable',
autoGenerateForms => 1,
className => 'WebGUI::Asset::RSSCapable',
icon => 'rssCapable.gif',
properties => \%properties
};
return $class->NEXT::definition($session, $definition);
}
sub _rssFromParentValid {
my $self = shift;
my $rssFromParentId = $self->get('rssCapableRssFromParentId');
return 0 unless $rssFromParentId;
my $rssFromParent = WebGUI::Asset->newByDynamicClass($self->session, $rssFromParentId);
return ($rssFromParent->isa('WebGUI::Asset::RSSFromParent')
&& $rssFromParent->getParent->getId eq $self->getId);
}
sub _updateRssFromParentProperties {
my $self = shift;
my $rssFromParent = WebGUI::Asset->newByDynamicClass($self->session,
$self->get('rssCapableRssFromParentId'));
$rssFromParent->update({ title => $self->get('title'),
menuTitle => $self->get('menuTitle') });
}
sub _purgeExtraRssFromParentAssets {
my $self = shift;
my $rssFromParentId = $self->get('rssCapableRssFromParentId');
foreach my $rssFromParent (@{$self->getLineage(['children'],
{returnObjects => 1,
includeOnlyClasses =>
['WebGUI::Asset::RSSFromParent']})}) {
$rssFromParent->purge unless $rssFromParent->getId eq $rssFromParentId;
}
}
sub _ensureRssFromParentPresent {
my $self = shift;
if (!$self->_rssFromParentValid) {
# Create a new one.
my $rssFromParent = $self->addChild({ className => 'WebGUI::Asset::RSSFromParent',
title => $self->get('title'),
menuTitle => $self->get('menuTitle'),
url => $self->get('url').'.rss'
});
$self->update({ rssCapableRssFromParentId => $rssFromParent->getId });
}
$self->_updateRssFromParentProperties;
$self->_purgeExtraRssFromParentAssets;
}
sub _ensureRssFromParentAbsent {
my $self = shift;
# Invalidate it, and then it'll get purged along with any others.
$self->update({ rssCapableRssFromParentId => undef });
$self->_purgeExtraRssFromParentAssets;
}
sub processPropertiesFromFormPost {
my $self = shift;
my $error = $self->NEXT::processPropertiesFromFormPost(@_);
return $error if ref $error eq 'ARRAY';
if ($self->get('rssCapableRssEnabled')) {
$self->_ensureRssFromParentPresent;
} else {
$self->_ensureRssFromParentAbsent;
}
return;
}
#-------------------------------------------------------------------
=head2 getRssUrl ( )
Returns the site-relative URL to the RSS feed for this asset, or undef
if there is no such feed.
=cut
sub getRssUrl {
my $self = shift;
my $rssFromParentId = $self->get('rssCapableRssFromParentId');
return undef unless defined $rssFromParentId;
WebGUI::Asset->newByDynamicClass($self->session, $rssFromParentId)->getUrl;
}
#-------------------------------------------------------------------
=head2 getRssItems ( )
Returns a list of RSS items for a feed corresponding to this asset.
Each item may be another asset, or a hash of (properly XMLized)
properties for the <item>..</item> tag. Defaults to no items.
This is the primary method that RSSCapable assets should override.
=cut
sub getRssItems { () }
1;

View file

@ -0,0 +1,123 @@
package WebGUI::Asset::RSSFromParent;
=head1 LEGAL
-------------------------------------------------------------------
WebGUI is Copyright 2001-2006 Plain Black Corporation.
-------------------------------------------------------------------
Please read the legal notices (docs/legal.txt) and the license
(docs/license.txt) that came with this distribution before using
this software.
-------------------------------------------------------------------
http://www.plainblack.com info@plainblack.com
-------------------------------------------------------------------
=cut
use strict;
use Tie::IxHash;
use base 'WebGUI::Asset';
use WebGUI::Utility;
=head1 NAME
Package WebGUI::Asset::RSSFromParent
=head1 DESCRIPTION
Generates an RSS feed from the children/descendants of its parent.
=head1 SYNOPSIS
use WebGUI::Asset::RSSFromParent;
=cut
#-------------------------------------------------------------------
sub definition {
my $class = shift;
my $session = shift;
my $definition = shift;
my %properties;
tie %properties, 'Tie::IxHash';
my $i18n = WebGUI::International->new($session, "Asset_RSSFromParent");
%properties = ();
push(@{$definition}, {
assetName=>$i18n->get('assetName'),
icon=>'rssFromParent.gif',
autoGenerateForms=>1,
tableName=>'RSSFromParent',
className=>'WebGUI::Asset::RSSFromParent',
properties=>\%properties
});
return $class->SUPER::definition($session, $definition);
}
#-------------------------------------------------------------------
sub _escapeXml {
my $text = shift;
my %entities = ('<' => '&lt;', '>' => '&gt;', '"' => '&quot;', "'" => "&apos;");
$text =~ s/([<>\"\'])/$entities{$1}/g;
return $text;
}
sub _tlsOfAsset {
my $self = shift;
my $asset = shift;
return (_escapeXml($asset->get('title')),
_escapeXml($self->session->url->getSiteURL() . $asset->getUrl),
_escapeXml($asset->get('synopsis')));
}
sub isValidRssItem { 0 }
sub displayInFolder2 { 0 }
sub www_view {
my $self = shift;
return '' unless $self->session->asset->getId eq $self->getId;
return '' unless $self->getParent->isa('WebGUI::Asset::RSSCapable');
my $parent = $self->getParent;
my $template = WebGUI::Asset::Template->new($self->session, $parent->get('rssCapableRssTemplateId'));
$template->prepare;
$self->session->http->setMimeType('text/xml');
my $var = {};
@$var{'title', 'link', 'description'} = $self->_tlsOfAsset($parent);
$var->{'generator'} = "WebGUI $WebGUI::VERSION";
$var->{'lastBuildDate'} = $self->session->datetime->epochToMail($parent->getContentLastModified);
$var->{'webMaster'} = $self->session->setting->get('companyEmail');
$var->{'docs'} = 'http://blogs.law.harvard.edu/tech/rss';
my @items = $parent->getRssItems;
$var->{'item_loop'} = [];
foreach my $item (@items) {
my $subvar = {};
if (UNIVERSAL::isa($item, 'WebGUI::Asset')) {
next unless $item->isValidRssItem;
$subvar = {};
@$subvar{'title', 'link', 'description'} = $self->_tlsOfAsset($item);
$subvar->{guid} = $subvar->{link};
$subvar->{pubDate} = _escapeXml($self->session->datetime->epochToMail($item->get('dateUpdated')));
} elsif (ref $item eq 'HASH') {
foreach my $key (keys %$item) {
my $value = $item->{$key};
$subvar->{$key} = (ref $value eq 'ARRAY')? join($,, @$value) : _escapeXml($value);
}
} else {
$self->session->errorHandler->error("Don't know what to do with this RSS item: $item");
next;
}
push @{$var->{'item_loop'}}, $subvar;
}
return $self->processTemplate($var, undef, $template);
}
1;

View file

@ -20,16 +20,17 @@ use WebGUI::Paginator;
use WebGUI::Utility;
use WebGUI::Asset::Wobject;
use WebGUI::Workflow::Cron;
our @ISA = qw(WebGUI::Asset::Wobject);
use WebGUI::Asset::RSSCapable;
use base 'WebGUI::Asset::RSSCapable';
use base 'WebGUI::Asset::Wobject';
#-------------------------------------------------------------------
sub addChild {
my $self = shift;
my $properties = shift;
my @other = @_;
if ($properties->{className} ne "WebGUI::Asset::Post::Thread") {
if ($properties->{className} ne "WebGUI::Asset::Post::Thread"
and $properties->{className} ne 'WebGUI::Asset::RSSFromParent') {
$self->session->errorHandler->security("add a ".$properties->{className}." to a ".$self->get("className"));
return undef;
}
@ -544,14 +545,6 @@ sub definition {
label=>$i18n->get('sort by'),
hoverHelp=>$i18n->get('sort by description'),
},
rssTemplateId =>{
fieldType=>"template",
namespace=>"Collaboration/RSS",
defaultValue=>'PBtmpl0000000000000142',
tab=>'display',
label=>$i18n->get('rss template'),
hoverHelp=>$i18n->get('rss template description'),
},
notificationTemplateId =>{
fieldType=>"template",
namespace=>"Collaboration/Notification",
@ -659,6 +652,50 @@ sub duplicate {
return $newAsset;
}
#-------------------------------------------------------------------
sub getRssItems {
my $self = shift;
# XXX copied and reformatted this query from www_viewRSS, but why is it constructed like this?
# And it's duplicated inside view, too! Eeeagh! And it uses the versionTag scratch var...
my ($sortBy, $sortOrder) = ($self->getValue('sortBy'), $self->getValue('sortOrder'));
my @postIds = $self->session->db->buildArray(<<"SQL", [$self->getId, $self->session->scratch->get('versionTag')]);
SELECT asset.assetId
FROM Thread
LEFT JOIN asset ON Thread.assetId = asset.assetId
LEFT JOIN Post ON Post.assetId = Thread.assetId AND Post.revisionDate = Thread.revisionDate
LEFT JOIN assetData ON assetData.assetId = Thread.assetId
AND assetData.revisionDate = Thread.revisionDate
WHERE asset.parentId = ? AND asset.state = 'published' AND asset.className = 'WebGUI::Asset::Post::Thread'
AND (assetData.status = 'approved' OR assetData.tagId = ?)
GROUP BY assetData.assetId
ORDER BY $sortBy $sortOrder
SQL
return map {
my $postId = $_;
my $post = WebGUI::Asset->newByDynamicClass($self->session, $postId);
my $postUrl = $self->session->url->getSiteURL() . $post->getUrl;
# Buggo: this is an abuse of 'author'. 'author' is supposed to be an email address.
# But this is how it was in the original Collaboration RSS, so.
({ author => $post->get('username'),
title => $post->get('title'),
link => $postUrl, guid => $postUrl,
description => $post->get('synopsis'),
pubDate => $self->session->datetime->epochToMail($post->get('dateUpdated')),
attachmentLoop => do {
if ($post->get('storageId')) {
my $storage = $post->getStorageLocation;
[map {({ 'attachment.url' => $storage->getUrl($_),
'attachment.path' => $storage->getPath($_),
'attachment.length' => $storage->getFileSize($_) })}
@{$storage->getFiles}]
} else { undef }
}
})
} @postIds;
}
#-------------------------------------------------------------------
sub getEditTabs {
my $self = shift;
@ -682,19 +719,6 @@ sub getNewThreadUrl {
#-------------------------------------------------------------------
=head2 getRssUrl ( )
Formats the url to start a new thread.
=cut
sub getRssUrl {
my $self = shift;
$self->getUrl("func=viewRSS");
}
#-------------------------------------------------------------------
=head2 getSearchUrl ( )
Formats the url to the forum search engine.
@ -751,6 +775,17 @@ sub getUnsubscribeUrl {
}
#-------------------------------------------------------------------
sub _computeThreadCount {
my $self = shift;
return scalar @{$self->getLineage(['children'], {includeOnlyClasses => ['WebGUI::Asset::Post::Thread']})};
}
sub _computePostCount {
my $self = shift;
return scalar @{$self->getLineage(['descendants'], {includeOnlyClasses => ['WebGUI::Asset::Post']})};
}
#-------------------------------------------------------------------
=head2 incrementReplies ( lastPostDate, lastPostId )
@ -769,8 +804,8 @@ The unique identifier of the post being added.
sub incrementReplies {
my ($self, $lastPostDate, $lastPostId) = @_;
my $threads = $self->getChildCount;
my $replies = $self->getDescendantCount - $threads;
my $threads = $self->_computeThreadCount;
my $replies = $self->_computePostCount;
$self->update({replies=>$replies, threads=>$threads, lastPostId=>$lastPostId, lastPostDate=>$lastPostDate});
}
@ -792,7 +827,7 @@ The unique identifier of the post that was just added.
sub incrementThreads {
my ($self, $lastPostDate, $lastPostId) = @_;
$self->update({threads=>$self->getChildCount, lastPostId=>$lastPostId, lastPostDate=>$lastPostDate});
$self->update({threads=>$self->_computeThreadCount, lastPostId=>$lastPostId, lastPostDate=>$lastPostDate});
}
#-------------------------------------------------------------------
@ -938,8 +973,8 @@ Calculates the number of replies to this collaboration system and updates the co
sub sumReplies {
my $self = shift;
my $threads = $self->getChildCount;
my $replies = $self->getDescendantCount - $threads;
my $threads = $self->_computeThreadCount;
my $replies = $self->_computePostCount;
$self->update({replies=>$replies, threads=>$threads});
}
@ -953,7 +988,7 @@ Calculates the number of threads in this collaboration system and updates the co
sub sumThreads {
my $self = shift;
$self->update({threads=>$self->getChildCount});
$self->update({threads=>$self->_computeThreadCount});
}
#-------------------------------------------------------------------
@ -1165,73 +1200,5 @@ sub www_view {
return $self->SUPER::www_view(@_);
}
#-------------------------------------------------------------------
# print out RSS 2.0 feed describing the items visible on the first page
sub www_viewRSS {
my $self = shift;
return $self->session->privilege->noAccess() unless $self->canView;
my %var;
$self->logView() if ($self->session->setting->get("passiveProfilingEnabled"));
# Set the required channel variables
$var{'title'} = _xml_encode($self->get("title"));
$var{'link'} = _xml_encode($self->session->url->getSiteURL().$self->getUrl);
$self->session->errorHandler->warn( $var{'link'} );
$var{'description'} = _xml_encode($self->get("description"));
# Set some of the optional channel variables
$var{'generator'} = "WebGUI ".$WebGUI::VERSION;
$var{'lastBuildDate'} = _xml_encode($self->_get_rfc822_date($self->get("dateUpdated")));
$var{'webMaster'} = $self->session->setting->get("companyEmail");
$var{'docs'} = "http://blogs.law.harvard.edu/tech/rss";
my $sth = $self->session->db->read("select asset.assetId, asset.className, max(assetData.revisionDate)
from Thread
left join asset on Thread.assetId=asset.assetId
left join Post on Post.assetId=Thread.assetId and Thread.revisionDate=Post.revisionDate
left join assetData on assetData.assetId=Thread.assetId and Thread.revisionDate=assetData.revisionDate
where asset.parentId=".$self->session->db->quote($self->getId)." and asset.state='published'
and asset.className='WebGUI::Asset::Post::Thread'
and (assetData.status='approved'
or assetData.tagId=".$self->session->db->quote($self->session->scratch->get("versionTag")).")
group by assetData.assetId
order by ".$self->getValue("sortBy")." ".$self->getValue("sortOrder"));
my $i = 1;
while (my ($id, $class, $version) = $sth->array) {
my $post = WebGUI::Asset::Wobject::Collaboration->new($self->session, $id, $class, $version);
my $encUrl = _xml_encode($self->session->url->getSiteURL().$post->getUrl);
my @attachmentLoop = ();
unless ($post->get("storageId") eq "") {
my $storage = $post->getStorageLocation;
foreach my $filename (@{ $storage->getFiles }) {
push @attachmentLoop, {
'attachment.url' => $storage->getUrl($filename),
'attachment.path' => $storage->getPath($filename),
'attachment.length' => $storage->getFileSize($filename),
};
}
}
push(@{$var{'item_loop'}}, {
author => _xml_encode($post->get('username')),
title => _xml_encode($post->get("title")),
link => $encUrl,
description => _xml_encode($post->get("synopsis")),
guid => $encUrl,
pubDate => _xml_encode($self->_get_rfc822_date($post->get("dateUpdated"))),
attachmentLoop => \@attachmentLoop,
});
$i++;
last if ($i == $self->get("threadsPerPage"));
}
$self->session->http->setMimeType("text/xml");
my $output = $self->processTemplate(\%var,$self->get("rssTemplateId"));
WebGUI::Macro::process($self->session,\$output);
return $output;
}
1;

View file

@ -0,0 +1,12 @@
package WebGUI::i18n::English::Asset_RSSCapable;
our $I18N =
{
'rssEnabled label' => { message => 'Enable RSS', lastUpdate => 1162487361 },
'rssEnabled hoverHelp' => { message => q|Whether or not to enable the RSS feed for this asset. If enabled, an RSS From Parent asset will be created and managed as an extra child for this purpose. If not enabled, no such child will be created and the existing one will be deleted.|, lastUpdate => 1162487361 },
'rssTemplateId label' => { message => 'RSS Template', lastUpdate => 1162487361 },
'rssTemplateId hoverHelp' => { message => q|The template to use for the RSS feed of this asset.|, lastUpdate => 1162487361 },
'assetName' => { message => 'RSS Capable', lastUpdate => 1162487361 },
};
1;

View file

@ -0,0 +1,8 @@
package WebGUI::i18n::English::Asset_RSSFromParent;
our $I18N =
{
'assetName' => { message => 'RSS From Parent', lastUpdated => 1162257377 },
};
1;