diff --git a/docs/changelog/7.x.x.txt b/docs/changelog/7.x.x.txt index 9a12852aa..ab090f81b 100644 --- a/docs/changelog/7.x.x.txt +++ b/docs/changelog/7.x.x.txt @@ -1,4 +1,5 @@ 7.7.5 + - Adding StoryManager. 7.7.4 - rfe: Extend DateTime for Week-Nrs (#9151) diff --git a/docs/upgrades/packages-7.7.5/root_import_storymanager.wgpkg b/docs/upgrades/packages-7.7.5/root_import_storymanager.wgpkg new file mode 100644 index 000000000..42da60941 Binary files /dev/null and b/docs/upgrades/packages-7.7.5/root_import_storymanager.wgpkg differ diff --git a/docs/upgrades/upgrade_7.7.4-7.7.5.pl b/docs/upgrades/upgrade_7.7.4-7.7.5.pl index 69e471f48..fd6ec5fe0 100644 --- a/docs/upgrades/upgrade_7.7.4-7.7.5.pl +++ b/docs/upgrades/upgrade_7.7.4-7.7.5.pl @@ -22,6 +22,7 @@ use Getopt::Long; use WebGUI::Session; use WebGUI::Storage; use WebGUI::Asset; +use WebGUI::Utility qw/isIn/; my $toVersion = '7.7.5'; @@ -32,6 +33,11 @@ my $session = start(); # this line required # upgrade functions go here +# Story Manager +installStoryManagerTables($session); +sm_upgradeConfigFiles($session); +sm_updateDailyWorkflow($session); + finish($session); # this line required @@ -44,6 +50,95 @@ finish($session); # this line required # print "DONE!\n" unless $quiet; #} +sub installStoryManagerTables { + my ($session) = @_; + print "\tAdding Story Manager tables... " unless $quiet; + my $db = $session->db; + $db->write(<write(<write(<config; + $config->addToHash( + 'assets', + 'WebGUI::Asset::Wobject::StoryTopic' => { + 'category' => 'community' + }, + ); + $config->addToHash( + 'assets', + "WebGUI::Asset::Wobject::StoryArchive" => { + "isContainer" => 1, + "category" => "community" + }, + ); + my $activities = $config->get('workflowActivities'); + my $none = $activities->{None}; + if (!isIn('WebGUI::Workflow::Activity::ArchiveOldStories', @{ $none })) { + unshift @{ $none }, 'WebGUI::Workflow::Activity::ArchiveOldStories'; + } + $config->set('workflowActivities', $activities); + print "DONE!\n" unless $quiet; +} + +sub sm_updateDailyWorkflow { + my ($session) = @_; + print "\tAdding Archive Old Stories to Daily Workflow... " unless $quiet; + my $workflow = WebGUI::Workflow->new($session, 'pbworkflow000000000001'); + foreach my $activity (@{ $workflow->getActivities }) { + return if $activity->getName() eq 'WebGUI::Workflow::Activity::ArchiveOldStories'; + } + my $activity = $workflow->addActivity('WebGUI::Workflow::Activity::ArchiveOldStories'); + $activity->set('title', 'Archive Old Stories'); + $activity->set('description', 'Archive old stories, based on the settings of the Story Archives that own them'); + print "DONE!\n" unless $quiet; +} # -------------- DO NOT EDIT BELOW THIS LINE -------------------------------- diff --git a/etc/WebGUI.conf.original b/etc/WebGUI.conf.original index cd71fcc2d..d9d3ff961 100644 --- a/etc/WebGUI.conf.original +++ b/etc/WebGUI.conf.original @@ -519,6 +519,13 @@ "isContainer" : 1, "category" : "community" }, + "WebGUI::Asset::Wobject::StoryArchive" : { + "isContainer" : 1, + "category" : "community" + }, + "WebGUI::Asset::Wobject::StoryTopic" : { + "category" : "community" + }, "WebGUI::Asset::Wobject::StockData" : { "category" : "intranet" }, @@ -805,6 +812,7 @@ "workflowActivities" : { "None" : [ + "WebGUI::Workflow::Activity::ArchiveOldStories", "WebGUI::Workflow::Activity::ArchiveOldThreads", "WebGUI::Workflow::Activity::CalendarUpdateFeeds", "WebGUI::Workflow::Activity::CleanDatabaseCache", diff --git a/lib/WebGUI/Asset.pm b/lib/WebGUI/Asset.pm index af5bcae72..05fa1bd6c 100644 --- a/lib/WebGUI/Asset.pm +++ b/lib/WebGUI/Asset.pm @@ -17,6 +17,7 @@ package WebGUI::Asset; use Carp qw( croak confess ); use Scalar::Util qw( blessed ); use Clone qw(clone); +use JSON; use WebGUI::AssetBranch; use WebGUI::AssetClipboard; @@ -1927,8 +1928,9 @@ sub outputWidgetMarkup { my $styleTemplateId = shift; # construct / retrieve the values we'll use later. - my $assetId = $self->getId; my $session = $self->session; + my $assetId = $self->getId; + my $hexId = $session->id->toHex($assetId); my $conf = $session->config; my $extras = $conf->get('extrasURL'); @@ -1952,8 +1954,10 @@ sub outputWidgetMarkup { $content = $self->session->style->process($content,$styleTemplateId); } WebGUI::Macro::process($session, \$content); + $session->log->warn($content); my ($headTags, $body) = WebGUI::HTML::splitHeadBody($content); - my $jsonContent = to_json( { "asset$assetId" => { content => $body } } ); + $body = $content; + my $jsonContent = to_json( { "asset$hexId" => { content => $body } } ); $storage->addFileFromScalar("$assetId.js", "data = $jsonContent"); my $jsonUrl = $storage->getUrl("$assetId.js"); @@ -1979,15 +1983,15 @@ sub outputWidgetMarkup { $headTags - - \${asset$assetId.content} + + \${asset$hexId.content} OUTPUT diff --git a/lib/WebGUI/Asset/Story.pm b/lib/WebGUI/Asset/Story.pm new file mode 100644 index 000000000..4dc3f777f --- /dev/null +++ b/lib/WebGUI/Asset/Story.pm @@ -0,0 +1,900 @@ +package WebGUI::Asset::Story; + +=head1 LEGAL + + ------------------------------------------------------------------- + WebGUI is Copyright 2001-2009 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 Class::C3; +use base 'WebGUI::Asset'; +use Tie::IxHash; +use WebGUI::Utility; +use WebGUI::International; +use JSON qw/from_json to_json/; +use Storable qw/dclone/; +use Data::Dumper; + +=head1 NAME + +Package WebGUI::Asset::Story + +=head1 DESCRIPTION + +The Story Asset is like a Thread for the Collaboration. + +=head1 SYNOPSIS + +use WebGUI::Asset::Story; + + +=head1 METHODS + +These methods are available from this class: + +=cut + +#------------------------------------------------------------------- + +=head2 addChild ( ) + +You can't add children to a Story. + +=cut + +sub addChild { + return undef; +} + +#------------------------------------------------------------------- + +=head2 addRevision + +Copy storage locations so that purging individual revisions works correctly. + +Request autocommit. + +=cut + +sub addRevision { + my $self = shift; + my $session = $self->session; + my $newSelf = $self->next::method(@_); + + my $newProperties = { + isHidden => 1, + }; + + $newSelf->update($newProperties); + + my $newPhotoData = $newSelf->duplicatePhotoData; + $newSelf->setPhotoData($newPhotoData); + $newSelf->requestAutoCommit; + + return $newSelf; +} + +#------------------------------------------------------------------- + +=head2 definition ( session, definition ) + +defines asset properties for New Asset instances. You absolutely need +this method in your new Assets. + +=head3 session + +=head3 definition + +A hash reference passed in from a subclass definition. + +=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_Story'); + %properties = ( + headline => { + fieldType => 'text', + #label => $i18n->get('headline'), + #hoverHelp => $i18n->get('headline help'), + defaultValue => '', + }, + subtitle => { + fieldType => 'textarea', + #label => $i18n->get('subtitle'), + #hoverHelp => $i18n->get('subtitle help'), + defaultValue => '', + }, + byline => { + fieldType => 'text', + #label => $i18n->get('byline'), + #hoverHelp => $i18n->get('byline help'), + defaultValue => '', + }, + location => { + fieldType => 'text', + #label => $i18n->get('location'), + #hoverHelp => $i18n->get('location help'), + defaultValue => '', + }, + highlights => { + fieldType => 'textarea', + #label => $i18n->get('highlights'), + #hoverHelp => $i18n->get('highlights help'), + defaultValue => '', + }, + story => { + fieldType => 'HTMLArea', + #label => $i18n->get('highlights'), + #hoverHelp => $i18n->get('highlights help'), + #richEditId => $self->parent->getStoryRichEdit, + defaultValue => '', + }, + photo => { + fieldType => 'text', + defaultValue => '[]', + noFormPost => 1, + }, + ); + push(@{$definition}, { + assetName => $i18n->get('assetName'), + icon => 'assets.gif', + tableName => 'Story', + className => 'WebGUI::Asset::Story', + properties => \%properties, + autoGenerateForms => 0, + }); + return $class->next::method($session, $definition); +} + + +#------------------------------------------------------------------- + +=head2 duplicate ( ) + +Extent the method from Asset to handle duplicating storage locations. + +=cut + +sub duplicate { + my $self = shift; + my $newSelf = $self->next::method(@_); + my $newPhotoData = $newSelf->duplicatePhotoData; + $newSelf->setPhotoData($newPhotoData); + return $newSelf; +} + +#------------------------------------------------------------------- + +=head2 duplicatePhotoData ( ) + +Duplicate photo data, particularly storage locations. Returns the duplicated +perl structure. + +=cut + +sub duplicatePhotoData { + my $self = shift; + my $session = $self->session; + my $photoData = $self->getPhotoData; + PHOTO: foreach my $photo (@{ $photoData }) { + next PHOTO unless $photo->{storageId}; + my $oldStorage = WebGUI::Storage->get($session, $photo->{storageId}); + my $newStorage = $oldStorage->copy; + $photo->{storageId} = $newStorage->getId; + } + return $photoData; +} + +#------------------------------------------------------------------- + +=head2 exportAssetData ( ) + +See WebGUI::AssetPackage::exportAssetData() for details. +Adds all storage locations to the package data. + +=cut + +sub exportAssetData { + my $self = shift; + my $exportData = $self->next::method; + PHOTO: foreach my $photo (@{ $self->getPhotoData }) { + next PHOTO unless $photo->{storageId}; + push @{ $exportData->{storage} }, $photo->{storageId}; + } + return $exportData; +} + +#------------------------------------------------------------------- + +=head2 formatDuration ( $lastUpdated ) + +Format the time since this story was last updated. If it is longer than 1 week, then +return the date. + +=head3 $lastUpdated + +The date this was last updated. If left blank, it uses the revisionDate. + +=cut + +sub formatDuration { + my ($self, $lastUpdated) = @_; + $lastUpdated = defined $lastUpdated ? $lastUpdated : $self->get('revisionDate'); + my $session = $self->session; + my $datetime = $session->datetime; + my $duration = time() - $lastUpdated; + if ($duration > 86400) { ##1 day + return join ' ', $datetime->secondsToInterval($duration); + } + else { + my $formattedDuration = ''; + my $hours = int($duration/3600) * 3600; + my @hours = $datetime->secondsToInterval($hours); + if ($hours[0]) { + $formattedDuration = join ' ', @hours; + } + my $minutes = round(($duration - $hours)/60)*60; + my @minutes = $datetime->secondsToInterval($minutes); + if ($minutes[0]) { + $formattedDuration .= ', ', if $formattedDuration; + $formattedDuration .= join ' ', @minutes; + } + return $formattedDuration; + } +} + +#------------------------------------------------------------------- + +=head2 getArchive ( ) + +Returns the parent archive for this Story. Cache the entry for speed. + +=cut + +sub getArchive { + my $self = shift; + if (!$self->{_archive}) { + $self->{_archive} = $self->getParent->getParent; + } + return $self->{_archive}; +} + +#------------------------------------------------------------------- + +=head2 getAutoCommitWorkflowId ( ) + +Get the autocommit workflow from the archive containing this Story and +use it. + +=cut + +sub getAutoCommitWorkflowId { + my $self = shift; + my $archive = $self->getArchive; + if ($archive->hasBeenCommitted) { + return $archive->get('approvalWorkflowId') + || $self->session->setting->get('defaultVersionTagWorkflow'); + } + return undef; +} + + +#------------------------------------------------------------------- + +=head2 getCrumbTrail ( ) + +Returns the crumb trail for this Story. If rendered from inside +a Topic, it will insert the Topic information into the crumb trail. + +The crumb trail will be a loop of variables, in order from this Story's +StoryArchive, the topic, if present, and then this story. + +=cut + +sub getCrumbTrail { + my $self = shift; + my $crumb_loop = []; + my $archive = $self->getArchive; + push @{ $crumb_loop }, { + title => $archive->getTitle, + url => $archive->getUrl, + }; + my $topic = $self->topic; + if ($topic) { + push @{ $crumb_loop }, { + title => $topic->getTitle, + url => $topic->getUrl, + }; + } + push @{ $crumb_loop }, { + title => $self->getTitle, + url => $self->getUrl, + }; + return $crumb_loop; +} + +#------------------------------------------------------------------- + +=head2 getEditForm ( ) + +Returns a templated form for adding or editing Stories. + +=cut + +sub getEditForm { + my $self = shift; + my $session = $self->session; + my $i18n = WebGUI::International->new($session, 'Asset_Story'); + my $form = $session->form; + my $archive = $self->getArchive; + my $isNew = $self->getId eq 'new'; + my $url = $isNew ? $archive->getUrl : $self->getUrl; + my $title = $self->getTitle; + my $var = { + formHeader => WebGUI::Form::formHeader($session, {action => $url}) + . WebGUI::Form::hidden($session, { name => 'func', value => 'editSave' }) + . WebGUI::Form::hidden($session, { name => 'proceed', value => 'showConfirmation' }) + , + formFooter => WebGUI::Form::formFooter($session), + formTitle => $isNew + ? $i18n->get('add a story','Asset_StoryArchive') + : $i18n->get('editing','Asset_WikiPage').' '.$title, + headlineForm => WebGUI::Form::text($session, { + name => 'headline', + value => $form->get('headline') || $self->get('headline'), + } ), + titleForm => WebGUI::Form::text($session, { + name => 'title', + value => $form->get('title') || $self->get('title'), + } ), + subtitleForm => WebGUI::Form::textarea($session, { + name => 'subtitle', + value => $form->get('subtitle') || $self->get('subtitle') + } ), + bylineForm => WebGUI::Form::text($session, { + name => 'byline', + value => $form->get('byline') || $self->get('byline') + } ), + locationForm => WebGUI::Form::text($session, { + name => 'location', + value => $form->get('location') || $self->get('location') + } ), + keywordsForm => WebGUI::Form::keywords($session, { + name => 'keywords', + value => $form->get('keywords') || WebGUI::Keyword->new($session)->getKeywordsForAsset({ asset => $self }) + } ), + highlightsForm => WebGUI::Form::textarea($session, { + name => 'highlights', + value => $form->get('highlights') || $self->get('highlights') + } ), + storyForm => WebGUI::Form::HTMLArea($session, { + name => 'story', + value => $form->get('story') || $self->get('story'), + richEditId => $archive->get('richEditorId') + }), + saveButton => WebGUI::Form::submit($session, { + name => 'saveStory', + value => $i18n->get('save story'), + }), + cancelButton => WebGUI::Form::button($session, { + name => 'cancel', + value => $i18n->get('cancel','WebGUI'), + extras => q|onclick="history.go(-1);" class="backwardButton"|, + }), + saveAndAddButton => WebGUI::Form::submit($session, { + name => 'saveAndReturn', + value => $i18n->get('save and add another photo'), + }), + }; + $var->{ photo_form_loop } = []; + ##Provide forms for the existing photos, if any + ##Existing photos get a delete Yes/No. + ##And a form for new ones + my $photoData = $self->getPhotoData; + my $numberOfPhotos = scalar @{ $photoData }; + foreach my $photoIndex (1..$numberOfPhotos) { + my $photo = $photoData->[$photoIndex-1]; + push @{ $var->{ photo_form_loop } }, { + imgUploadForm => WebGUI::Form::image($session, { + name => 'photo'.$photoIndex, + maxAttachments => 1, + value => $photo->{storageId}, + }), + imgCaptionForm => WebGUI::Form::text($session, { + name => 'imgCaption'.$photoIndex, + value => $photo->{caption}, + }), + imgByLineForm => WebGUI::Form::text($session, { + name => 'imgByline'.$photoIndex, + value => $photo->{byLine}, + }), + imgAltForm => WebGUI::Form::text($session, { + name => 'imgAlt'.$photoIndex, + value => $photo->{alt}, + }), + imgTitleForm => WebGUI::Form::text($session, { + name => 'imgTitle'.$photoIndex, + value => $photo->{title}, + }), + imgUrlForm => WebGUI::Form::url($session, { + name => 'imgUrl'.$photoIndex, + value => $photo->{url}, + }), + imgDeleteForm => WebGUI::Form::yesNo($session, { + name => 'deletePhoto'.$photoIndex, + value => 0, + }), + }; + } + push @{ $var->{ photo_form_loop } }, { + imgUploadForm => WebGUI::Form::image($session, { + name => 'newPhoto', + maxAttachments => 1, + }), + imgCaptionForm => WebGUI::Form::text($session, { + name => 'newImgCaption', + }), + imgByLineForm => WebGUI::Form::text($session, { + name => 'newImgByline', + }), + imgAltForm => WebGUI::Form::text($session, { + name => 'newImgAlt', + }), + imgTitleForm => WebGUI::Form::text($session, { + name => 'newImgTitle', + }), + imgUrlForm => WebGUI::Form::url($session, { + name => 'newImgUrl', + }), + }; + if ($isNew) { + $var->{formHeader} .= WebGUI::Form::hidden($session, { name => 'assetId', value => 'new' }) + . WebGUI::Form::hidden($session, { name => 'class', value => $form->process('class', 'className') }); + } + else { + $var->{formHeader} .= WebGUI::Form::hidden($session, { name => 'url', value => $url}); + } + return $self->processTemplate($var, $archive->get('editStoryTemplateId')); + +} + +#------------------------------------------------------------------- + +=head2 getPhotoData ( ) + +Returns the photo hash formatted as perl data. See also L. + +=cut + +sub getPhotoData { + my $self = shift; + if (!exists $self->{_photoData}) { + my $json = $self->get('photo'); + $json ||= '[]'; + $self->{_photoData} = from_json($json); + } + return dclone($self->{_photoData}); +} + +#------------------------------------------------------------------- + +=head2 getRssData ( ) + +Returns RSS data for this Story. The date of the RSS item is the lastModified +property of the Asset. + +=cut + +sub getRssData { + my $self = shift; + my $data = { + title => $self->get('headline') || $self->getTitle, + description => $self->get('subtitle'), + 'link' => $self->getUrl, + author => $self->get('byline'), + date => $self->get('lastModified'), + }; + return $data; +} + +#------------------------------------------------------------------- + +=head2 prepareView ( ) + +Extent the default method to handle the case when a Story Topic is rendering +this Story. + +=cut + +sub prepareView { + my $self = shift; + $self->next::method(); + my $templateId; + my $topic = $self->topic; + if ($topic) { + $templateId = $topic->get('storyTemplateId'); + } + else { + $templateId = $self->getArchive->get('storyTemplateId'); + } + my $template = WebGUI::Asset::Template->new($self->session, $templateId); + $template->prepare; + $self->{_viewTemplate} = $template; +} + + +#------------------------------------------------------------------- + +=head2 processPropertiesFromFormPost ( ) + +Handle photos and photo metadata, like captions, etc. + +=cut + +sub processPropertiesFromFormPost { + my $self = shift; + my $session = $self->session; + $self->next::method; + my $form = $session->form; + ##Handle old data first, to avoid iterating across a newly added photo. + my $photoData = $self->getPhotoData; + my $numberOfPhotos = scalar @{ $photoData }; + ##Post process photo data here. + PHOTO: foreach my $photoIndex (1..$numberOfPhotos) { + ##TODO: Deletion check and storage cleanup + my $storageId = $photoData->[$photoIndex-1]->{storageId}; + if ($form->process('deletePhoto'.$photoIndex, 'yesNo')) { + my $storage = WebGUI::Storage->get($session, $storageId); + $storage->delete if $storage; + splice @{ $photoData }, $photoIndex-1, 1; + next PHOTO; + } + my $newPhoto = { + storageId => $form->process('photo' .$photoIndex, 'image', $storageId), + caption => $form->process('imgCaption'.$photoIndex, 'text'), + alt => $form->process('imgAlt' .$photoIndex, 'text'), + title => $form->process('imgTitle' .$photoIndex, 'text'), + byLine => $form->process('imgByline' .$photoIndex, 'text'), + url => $form->process('imgUrl' .$photoIndex, 'url' ), + }; + splice @{ $photoData }, $photoIndex-1, 1, $newPhoto; + } + my $newStorage = $form->process('newPhoto', 'image'); + if ($newStorage) { + push @{ $photoData }, { + caption => $form->process('newImgCaption', 'text'), + alt => $form->process('newImgAlt', 'text'), + title => $form->process('newImgTitle', 'text'), + byLine => $form->process('newImgByline', 'text'), + url => $form->process('newImgUrl', 'url'), + storageId => $newStorage, + }; + } + $self->setPhotoData($photoData); +} + + +#------------------------------------------------------------------- + +=head2 purge ( ) + +Cleaning up all storage objects in all revisions. + +=cut + +sub purge { + my $self = shift; + ##Delete all storage locations from all revisions of the Asset + my $sth = $self->session->db->read("select photo from Story where assetId=?",[$self->getId]); + STORAGE: while (my ($json) = $sth->array) { + my $photos = from_json($json); + PHOTO: foreach my $photo (@{ $photos }) { + next PHOTO unless $photo->{storageId}; + my $storage = WebGUI::Storage->get($self->session,$photo->{storageId}); + $storage->delete if $storage; + } + } + $sth->finish; + return $self->next::method; +} + +#------------------------------------------------------------------- + +=head2 purgeRevision + +Remove the storage locations for this revision of the Asset. + +=cut + +sub purgeRevision { + my $self = shift; + my $session = $self->session; + foreach my $photo ( @{ $self->getPhotoData} ) { + my $storage = WebGUI::Storage->get($session, $self-$photo->{storageId}); + $storage->delete if $storage; + } + return $self->next::method; +} + +#------------------------------------------------------------------- + +=head2 setPhotoData ( $perlStructure ) + +Update the JSON stored in the object from its perl equivalent, and update the database +as well via update. This deletes the cached copy of the equivalent perl structure. + +=head3 $perlStructure + +This should be an array of hashes. Photos will be in the order uploaded. +The values in the hash will be metadata about the Photo, and the storageId +that holds the image. Each storageId will hold only 1 file. + +=over 4 + +=item * + +caption + +=item * + +byLine + +=item * + +alt + +=item * + +title + +=item * + +url + +=item * + +storageId + +=back + +subhash keys can be empty, or missing altogether. Shoot, you can really put anything you +want in there as there's no valid content checking. + +=cut + +sub setPhotoData { + my $self = shift; + my $photoData = shift || []; + ##Convert to JSON + my $photo = to_json($photoData); + ##Update the db. + $self->update({photo => $photo}); + delete $self->{_photoData}; + return; +} + +#------------------------------------------------------------------- + +=head2 setSize ( fileSize ) + +Set the size of this asset by including all the files in its storage +location. C is an integer of additional bytes to include in +the asset size. + +=cut + +sub setSize { + my $self = shift; + my $fileSize = shift || 0; + my $session = $self->session; + PHOTO: foreach my $photo (@{ $self->getPhotoData }) { + my $storage = WebGUI::Storage->get($session, $photo->{storageId}); + next PHOTO unless defined $storage; + foreach my $file (@{$storage->getFiles}) { + $fileSize += $storage->getFileSize($file); + } + } + return $self->next::method($fileSize); +} + +#------------------------------------------------------------------- + +=head2 topic ( $topicAsset ) + +Tells the Story that it is being viewed from a Topic, and to behave +accordingly. Returns a StoryTopic asset if set. + +=head3 $topicAsset + +The topic that is displaying this Story. + +=cut + +sub topic { + my $self = shift; + my $topic = shift; + if (defined $topic) { + $self->{_topic} = $topic; + } + return $self->{_topic}; +} + +#------------------------------------------------------------------- + +=head2 update + +Extend the superclass to make sure that the asset always stays hidden from navigation. + +=cut + +sub update { + my $self = shift; + my $properties = shift; + return $self->next::method({%$properties, isHidden => 1}); +} + +#------------------------------------------------------------------- + +=head2 validParent + +Make sure that the current session asset is a StoryArchive for pasting and adding checks. + +This is a class method. + +=cut + +sub validParent { + my $class = shift; + my $session = shift; + return $session->asset && $session->asset->isa('WebGUI::Asset::Wobject::StoryArchive'); +} + +#------------------------------------------------------------------- + +=head2 view ( ) + +method called by the container www_view method. + +=cut + +##Keyword cloud generated by WebGUI::Keyword + +sub view { + my $self = shift; + my $session = $self->session; + my $var = $self->viewTemplateVariables(); + return $self->processTemplate($var,undef, $self->{_viewTemplate}); +} + +#------------------------------------------------------------------- + +=head2 viewTemplateVars ( $var ) + +Add template variables to the existing template variables. This includes asset level variables. + +=head3 $var + +Template variables will be added onto this hash ref. + +=cut + +sub viewTemplateVariables { + my ($self) = @_; + my $session = $self->session; + my $archive = $self->getArchive; + my $var = $self->get; + + if ($var->{highlights}) { + my @highlights = split "\n+", $var->{highlights}; + foreach my $highlight (@highlights) { + push @{ $var->{highlights_loop} }, { highlight => $highlight }; + } + } + + my $key = WebGUI::Keyword->new($session); + my $keywords = $key->getKeywordsForAsset( { asArrayRef => 1, asset => $self }); + $var->{keyword_loop} = []; + foreach my $keyword (@{ $keywords }) { + push @{ $var->{keyword_loop} }, { + keyword => $keyword, + url => $archive->getUrl("func=view;keywords=".$session->url->escape($keyword)), + }; + } + $var->{updatedTime} = $self->formatDuration(); + $var->{updatedTimeEpoch} = $self->get('revisionDate'); + + $var->{crumb_loop} = $self->getCrumbTrail(); + my $photoData = $self->getPhotoData; + $var->{photo_loop} = []; + my $photoCounter = 0; + PHOTO: foreach my $photo (@{ $photoData }) { + next PHOTO unless $photo->{storageId}; + my $storage = WebGUI::Storage->get($session, $photo->{storageId}); + my $file = $storage->getFiles->[0]; + next PHOTO unless $file; + my $imageUrl = $storage->getUrl($file); + push @{ $var->{photo_loop} }, { + imageUrl => $imageUrl, + imageCaption => $photo->{caption}, + imageByline => $photo->{byLine}, + imageAlt => $photo->{alt}, + imageTitle => $photo->{title}, + imageLink => $photo->{url}, + }; + ++$photoCounter; + } + $var->{hasPhotos} = $photoCounter; + $var->{singlePhoto} = $photoCounter == 1; + return $var; +} + + +#------------------------------------------------------------------- + +=head2 www_edit ( ) + +Web facing method which is the default edit page. Unless the method needs +special handling or formatting, it does not need to be included in +the module. + +Overridden because the standard, autogenerated form is not used. + +=cut + +sub www_edit { + my $self = shift; + my $session = $self->session; + return $session->privilege->insufficient() unless $self->canEdit; + return $session->privilege->locked() unless $self->canEditIfLocked; + return $self->getArchive->processStyle($self->getEditForm); +} + +#------------------------------------------------------------------- + +=head2 www_showConfirmation ( ) + +Shows a confirmation message letting the user know their page has been submitted. + +=cut + +sub www_showConfirmation { + my $self = shift; + my $i18n = WebGUI::International->new($self->session, 'Asset_Story'); + return $self->getArchive->processStyle('

'.$i18n->get('story received').'

'.$i18n->get('493','WebGUI').'

'); +} + +#------------------------------------------------------------------- + +=head2 www_view + +Override www_view from asset because Stories inherit a style template from +the Story Archive that contains them. + +=cut + +sub www_view { + my $self = shift; + return $self->session->privilege->noAccess unless $self->canView; + $self->session->http->sendHeader; + $self->prepareView; + return $self->getArchive->processStyle($self->view); +} + + +1; + +#vim:ft=perl diff --git a/lib/WebGUI/Asset/Wobject/StoryArchive.pm b/lib/WebGUI/Asset/Wobject/StoryArchive.pm new file mode 100644 index 000000000..1f31ed1bf --- /dev/null +++ b/lib/WebGUI/Asset/Wobject/StoryArchive.pm @@ -0,0 +1,607 @@ +package WebGUI::Asset::Wobject::StoryArchive; + +our $VERSION = "1.0.0"; + +#------------------------------------------------------------------- +# WebGUI is Copyright 2001-2009 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 +#------------------------------------------------------------------- + +use strict; +use Tie::IxHash; +use WebGUI::International; +use WebGUI::Utility; +use WebGUI::Asset::Story; +use WebGUI::Asset::Wobject::Folder; +use WebGUI::Paginator; +use WebGUI::Keyword; +use WebGUI::Search; +use Class::C3; +use base qw/WebGUI::AssetAspect::RssFeed WebGUI::Asset::Wobject/; +use File::Path; + +use constant DATE_FORMAT => '%c_%D_%y'; + +#------------------------------------------------------------------- + +=head2 addChild ( ) + +Story Archive really only has Folders for children. When addChild is +called, check the date to see which folder to use. If the correct folder +does not exist, then make it. + +=cut + +sub addChild { + my $self = shift; + my ($properties) = @_; + ##Allow subclassing + return undef unless $properties->{className} =~ /^WebGUI::Asset::Story/; + my $todayFolder = $self->getFolder; + return undef unless $todayFolder; + my $story = $todayFolder->addChild(@_); + return $story; +} + +#------------------------------------------------------------------- + +=head2 canPostStories ( ) + +Determines whether or not a user can post stories to this Archive. + +=head3 userId + +An explicit userId to check against. If no userId is sent, then it +will use the current session user instead. + +=cut + +sub canPostStories { + my ($self, $userId) = @_; + $userId ||= $self->session->user->userId; + my $user = WebGUI::User->new($self->session, $userId); + return $user->isInGroup($self->get("groupToPost")) || $self->canEdit($userId); +} + +#------------------------------------------------------------------- + +=head2 definition ( ) + +defines wobject properties for New Wobject instances. You absolutely need +this method in your new Wobjects. If you choose to "autoGenerateForms", the +getEditForm method is unnecessary/redundant/useless. + +=cut + +sub definition { + my $class = shift; + my $session = shift; + my $definition = shift; + my $i18n = WebGUI::International->new($session, 'Asset_StoryArchive'); + my %properties; + tie %properties, 'Tie::IxHash'; + %properties = ( + storiesPerPage => { + tab => 'display', + fieldType => 'integer', + label => $i18n->get('stories per page'), + hoverHelp => $i18n->get('stories per page help'), + defaultValue => 25, + }, + groupToPost => { + tab => 'security', + fieldType => 'group', + label => $i18n->get('group to post'), + hoverHelp => $i18n->get('group to post help'), + defaultValue => '12', + }, + templateId => { + tab => 'display', + fieldType => 'template', + label => $i18n->get('template'), + hoverHelp => $i18n->get('template help'), + namespace => 'StoryArchive', + defaultValue => 'yxD5ka7XHebPLD-LXBwJqw', + }, + storyTemplateId => { + tab => 'display', + fieldType => 'template', + label => $i18n->get('story template'), + hoverHelp => $i18n->get('story template help'), + namespace => 'Story', + defaultValue => '3QpYtHrq_jmAk1FNutQM5A', + }, + editStoryTemplateId => { + tab => 'display', + fieldType => 'template', + label => $i18n->get('edit story template'), + hoverHelp => $i18n->get('edit story template help'), + namespace => 'Story/Edit', + defaultValue => 'E3tzZjzhmYoNlAyP2VW33Q', + }, + keywordListTemplateId => { + tab => 'display', + fieldType => 'template', + label => $i18n->get('keyword list template'), + hoverHelp => $i18n->get('keyword list template help'), + namespace => 'StoryArchive/KeywordList', + defaultValue => '0EAJ9EYb9ap2XwfrcXfdLQ', + }, + archiveAfter => { + tab => 'display', + fieldType => 'interval', + label => $i18n->get('archive after'), + hoverHelp => $i18n->get('archive after help'), + defaultValue => 31536000, + }, + richEditorId => { + tab => 'display', + fieldType => 'selectRichEditor', + label => $i18n->get('rich editor'), + hoverHelp => $i18n->get('rich editor help'), + defaultValue => 'PBrichedit000000000002', + }, + approvalWorkflowId =>{ + tab => 'security', + fieldType => 'workflow', + defaultValue => 'pbworkflow000000000003', + type => 'WebGUI::VersionTag', + label => $i18n->get('approval workflow'), + hoverHelp => $i18n->get('approval workflow help'), + }, + ); + push(@{$definition}, { + assetName=>$i18n->get('assetName'), + icon=>'assets.gif', + autoGenerateForms=>1, + tableName=>'StoryArchive', + className=>'WebGUI::Asset::Wobject::StoryArchive', + properties=>\%properties, + }); + return $class->SUPER::definition($session, $definition); +} + + +#------------------------------------------------------------------- + +=head2 exportAssetCollateral (basePath, params, session) + +Extended the master method in order to produce keyword files. + +=cut + +sub exportAssetCollateral { + # Lots of copy/paste here from AssetExportHtml.pm, since none of the methods there were + # directly useful without ginormous refactoring. + my $self = shift; + my $basepath = shift; + my $args = shift; + my $reportSession = shift; + my $session = $self->session; + + my $reporti18n = WebGUI::International->new($session, 'Asset'); + + my $basename = $basepath->basename; + my $filedir; + + # We want our keyword files to "appear" as children of the asset to avoid + # clashing with multiple story archives. + if ($basename eq 'index.html') { + # Get the parent of the file index.html, which is the asset's directory. + $filedir = $basepath->parent->absolute->stringify; + } + else { + ##Create a directory that has the same base + my $dirname = $basename; + $dirname =~ s/\.\w+$//; + $filedir = $basepath->parent->subdir($dirname)->absolute->stringify; + eval { File::Path::mkpath($filedir) }; + if($@) { + WebGUI::Error->throw(error => "could not make directory " . $filedir); + } + } + + if ( $reportSession && !$args->{quiet} ) { + $reportSession->output->print('
'); + } + + # open another session to handle printing... + my $printSession = WebGUI::Session->open( + $self->session->config->getWebguiRoot, + $self->session->config->getFilename, + undef, + undef, + $self->session->getId, + ); + $printSession->scratch->set('isExporting', 1); + + + my $keywordObj = WebGUI::Keyword->new($printSession); + my $keywords = $keywordObj->findKeywords({ + asset => $self, + limit => 50, ##This is based on the tagcloud setting + }); + + my $listTemplate = WebGUI::Asset->new($session, $self->get('keywordListTemplateId'), 'WebGUI::Asset::Template'); + foreach my $keyword (@{ $keywords }) { + ##Keywords may not be URL safe, so urlize them + my $keyword_url = $self->getKeywordFilename($keyword); + my $dest = Path::Class::File->new($filedir, $keyword_url); + + # tell the user which asset we're exporting. + if ( $reportSession && !$args->{quiet} ) { + my $message = sprintf $reporti18n->get('exporting page'), $dest->absolute->stringify; + $reportSession->output->print( + '      ' . $message . '
'); + } + + # next, get the contents, open the file, and write the contents to the file. + my $fh = eval { $dest->open('>:utf8') }; + if($@) { + $printSession->close; + WebGUI::Error->throw(error => "can't open " . $dest->absolute->stringify . " for writing: $!"); + } + $printSession->output->setHandle($fh); + + my $storyIds = $keywordObj->getMatchingAssets({ + startAsset => $self, + keyword => $keyword, + isa => 'WebGUI::Asset::Story', + rowsPerPage => 50, + }); + my $listOfStories = []; + STORYID: foreach my $storyId (@{ $storyIds }) { + my $story = WebGUI::Asset->newByDynamicClass($session, $storyId); + next STORYID unless $story; + push @{ $listOfStories }, { + title => $story->getTitle, + url => $story->getUrl, + }; + } + my $var = { + asset_loop => $listOfStories, + keyword => $keyword, + }; + my $output = $listTemplate->process($var); + my $contents = $self->processStyle($output); + $printSession->output->print($contents); + + # tell the user we did this asset collateral correctly + if ( $reportSession && !$args->{quiet} ) { + $reportSession->output->print($reporti18n->get('done')); + } + $fh->flush; + $fh->close; + } + $printSession->close; + return $self->next::method($basepath, $args, $reportSession); +} + +#------------------------------------------------------------------- + +=head2 getFolder ( date ) + +Stories are stored in Folders under the Story Archive to prevent lineage issues. +Gets the correct folder for stories. If the Folder does not exist, then it will +be created and autocommitted. The autocommit is COMPLETELY automatic. This is +because it's possible to gum up the Story submitting proces with a Folder under +a different version tag. + +=head3 date + +There is one folder for each day that Stories are submitted. The requested date +should be an epoch. If no date is passed, it will use the current time instead. + +=cut + +sub getFolder { + my ($self, $date) = @_; + my $session = $self->session; + my $folderName = $session->datetime->epochToHuman($date, DATE_FORMAT); + my $folderUrl = join '/', $self->getUrl, $folderName; + my $folder = WebGUI::Asset->newByUrl($session, $folderUrl); + return $folder if $folder; + ##The requested folder doesn't exist. Make it and autocommit it. + + ##For a fully automatic commit, save the current tag, create a new one + ##with the commit without approval workflow, commit it, then restore + ##the original if it exists + my $oldVersionTag = WebGUI::VersionTag->getWorking($session, 'noCreate'); + my $newVersionTag = WebGUI::VersionTag->create($session, { workflowId => 'pbworkflow00000000003', }); + $newVersionTag->setWorking; + + ##Call SUPER because my addChild calls getFolder + $folder = $self->SUPER::addChild({ + className => 'WebGUI::Asset::Wobject::Folder', + title => $folderName, + menuTitle => $folderName, + url => $folderUrl, + isHidden => 1, + }); + $newVersionTag->commit(); + ##Restore the old one, if it exists + $oldVersionTag->setWorking() if $oldVersionTag; + + ##Get a new version of the asset from the db with the correct state + $folder = WebGUI::Asset->newByUrl($session, $folderUrl); + return $folder; +} + +#------------------------------------------------------------------- + +=head2 getKeywordFilename ( $keyword ) + +Returns the name for the file containing stories that match this keyword. Used +in exportAssetCollateral, and in viewTemplateVariables. + +=head3 $keyword + +The keyword to generate a URL for. + +=cut + +sub getKeywordFilename { + my ($self,$keyword) = @_; + return $self->session->url->urlize('keyword_'.$keyword.'.html'); +} + +#------------------------------------------------------------------- + +=head2 getKeywordStaticURL ( $keyword ) + +Returns the whole URL for the file containing stories that match this keyword. Used +in exportAssetCollateral. + +The goal of this method is to create a "safe" URL where all the keyword files can +reside with no clashes. The best place is based on the URL for the StoryArchive. + +=head3 $keyword + +Generates a specific URL for $keyword. + +=cut + +sub getKeywordStaticURL { + my ($self,$keyword) = @_; + my $url = $self->getUrl; + my @parts = split /\//, $url; + my $lastPart = pop @parts; + if (index $lastPart, '.' == -1) { + return join '/', $self->getUrl, $self->getKeywordFilename($keyword); + } + else { + $lastPart =~ s/\.[^.]*$//; + return join '/', @parts, $lastPart, $self->getKeywordFilename($keyword); + } +} + +#------------------------------------------------------------------- + +=head2 getRssFeedItems ( ) + +Returns an arrayref of hashrefs, containing information on stories +for generating an RSS and Atom feeds. + +=cut + +sub getRssFeedItems { + my $self = shift; + my $stories = $self->getLineageIterator(['descendants'],{ + excludeClasses => ['WebGUI::Asset::Wobject::Folder'], + orderByClause => 'creationDate desc, lineage', + returnObjects => 1, + limit => $self->get('itemsPerFeed'), + }); + my $storyData = []; + while (my $story = $stories->()) { + push @{ $storyData }, $story->getRssData; + } + return $storyData; +} + +#------------------------------------------------------------------- + +=head2 prepareView ( ) + +See WebGUI::Asset::prepareView() for details. + +=cut + +sub prepareView { + my $self = shift; + $self->SUPER::prepareView(); + my $template = WebGUI::Asset::Template->new($self->session, $self->get("templateId")); + $template->prepare; + $self->{_viewTemplate} = $template; +} + + +#------------------------------------------------------------------- + +=head2 view ( ) + +method called by the www_view method. Returns a processed template +to be displayed within the page style. + +=cut + +sub view { + my $self = shift; + my $session = $self->session; + + #This automatically creates template variables for all of your wobject's properties. + my $mode = $session->form->hasParam('keyword') + ? 'keyword' + : $session->form->hasParam('search') + ? 'search' + : 'view'; + + my $var = $self->viewTemplateVariables($mode); + + return $self->processTemplate($var, undef, $self->{_viewTemplate}); +} + +#------------------------------------------------------------------- + +=head2 viewTemplateVars ( $mode ) + +Make template variables for the view template. + +=head3 $mode + +Whether to get assets in view mode, by time, or search mode, by keywords. + +If the asset is being exported for HTML, the following changes are mode: + +=over 4 + +=item * + +The search form template variables are not generated. + +=item * + +The pagination variables are not generated. + +=item * + +The pagination size is set to 10 standard pages. + +=back + +=cut + +sub viewTemplateVariables { + my ($self, $mode) = @_; + my $session = $self->session; + my $keywords = $session->form->get('keyword'); + my $query = $session->form->get('query'); + my $exporting = $session->scratch->get('isExporting'); + my $p; + my $var = $self->get(); + if ($mode eq 'keyword') { + $var->{mode} = 'keyword'; + my $wordList = WebGUI::Keyword::string2list($keywords); + my $key = WebGUI::Keyword->new($session); + $p = $key->getMatchingAssets({ + startAsset => $self, + keywords => $wordList, + isa => 'WebGUI::Asset::Story', + usePaginator => 1, + rowsPerPage => $self->get('storiesPerPage'), + }); + } + elsif ($mode eq 'search') { + $var->{mode} = 'search'; + my $search = WebGUI::Search->new($session); + $search->search({ + keywords => $query, + lineage => [ $self->get('lineage'), ], + classes => [ qw/WebGUI::Asset::Story/, ], + }); + $p = $search->getPaginatorResultSet($self->getUrl, $self->get('storiesPerPage')); + } + else { + $var->{mode} = 'view'; + ##Only return assetIds, we'll build data for the things that are actually displayed. + my $storySql = $self->getLineageSql(['descendants'],{ + excludeClasses => ['WebGUI::Asset::Wobject::Folder'], + orderByClause => 'creationDate desc, lineage', + }); + my $storiesPerPage = $self->get('storiesPerPage'); + if ($exporting) { + ##10 pages worth of data on 1 page in export mode + $storiesPerPage *= 10; + } + $p = WebGUI::Paginator->new($session, $self->getUrl, $storiesPerPage); + $p->setDataByQuery($storySql); + } + my $storyIds = $p->getPageData(); + if (! $exporting ) { + ##Pagination variables aren't useful in export mode + $p->appendTemplateVars($var); + } + $var->{date_loop} = []; + my $lastStoryDate = ''; + my $datePointer = undef; + ##Only build objects for the assets that we need + STORY: foreach my $storyId (@{ $storyIds }) { + my $story = WebGUI::Asset->new($session, $storyId->{assetId}, $storyId->{className}, $storyId->{revisionDate}); + next STORY unless $story; + my $creationDate = $story->get('creationDate'); + my ($creationDay,undef) = $session->datetime->dayStartEnd($creationDate); + my $storyDate = $session->datetime->epochToHuman($creationDay, DATE_FORMAT); + if ($storyDate ne $lastStoryDate) { + push @{ $var->{date_loop} }, {}; + $datePointer = $var->{date_loop}->[-1]; + $datePointer->{epochDate} = $creationDay; + $datePointer->{story_loop} = []; + $lastStoryDate = $storyDate; + } + push @{$datePointer->{story_loop}}, { + url => $story->getUrl, + title => $story->getTitle, + creationDate => $creationDate, + } + } + + $var->{canPostStories} = $self->canPostStories; + $var->{addStoryUrl} = $var->{canPostStories} + ? $self->getUrl('func=add;class=WebGUI::Asset::Story') + : ''; + $var->{rssUrl} = $exporting ? $self->getStaticRssFeedUrl : $self->getRssFeedUrl; + $var->{atomUrl} = $exporting ? $self->getStaticAtomFeedUrl : $self->getAtomFeedUrl; + my $cloudOptions = { + startAsset => $self, + displayFunc => 'view', + }; + ##In export mode, tags should link to the pages generated during the collateral export + if($exporting) { + $cloudOptions->{urlCallback} = 'getKeywordStaticURL'; + $cloudOptions->{displayFunc} = ''; + } + $var->{keywordCloud} = WebGUI::Keyword->new($session)->generateCloud($cloudOptions); + if (! $exporting) { + my $i18n = WebGUI::International->new($session, 'Asset'); + $var->{searchHeader} = WebGUI::Form::formHeader($session, { action => $self->getUrl }) + . WebGUI::Form::hidden($session, { name => 'func', value => 'view' }); + $var->{searchFooter} = WebGUI::Form::formFooter($session); + $var->{searchButton} = WebGUI::Form::submit($session, { name => 'search', value => $i18n->get('search')}); + $var->{searchForm} = WebGUI::Form::text($session, { name => 'query', value => $query}); + } + return $var; +} + +#------------------------------------------------------------------- + +=head2 www_add ( ) + +The only real children of StoryArchive are Folders, which then hold Stories. So we intercept +www_add, find the right folder to use, then allow that folder to continue on. + +=cut + + +sub www_add { + my $self = shift; + my $session = $self->session; + my $form = $session->form; + if ($form->get('class') ne 'WebGUI::Asset::Story') { + $session->log->warn('Refusing to add '. $form->get('class'). ' to StoryArchive'); + return undef; + } + my $todayFolder = $self->getFolder; + if (!$todayFolder) { + $session->log->warn('Unable to get folder for today. Not adding Story'); + return undef; + } + $todayFolder->www_add; +} + +1; +#vim:ft=perl diff --git a/lib/WebGUI/Asset/Wobject/StoryTopic.pm b/lib/WebGUI/Asset/Wobject/StoryTopic.pm new file mode 100644 index 000000000..c9132012a --- /dev/null +++ b/lib/WebGUI/Asset/Wobject/StoryTopic.pm @@ -0,0 +1,267 @@ +package WebGUI::Asset::Wobject::StoryTopic; + +$VERSION = "1.0.0"; + +#------------------------------------------------------------------- +# WebGUI is Copyright 2001-2009 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 +#------------------------------------------------------------------- + +use strict; +use Tie::IxHash; +use WebGUI::International; +use WebGUI::Utility; +use WebGUI::Asset::Story; +use Class::C3; +use base qw/WebGUI::AssetAspect::RssFeed WebGUI::Asset::Wobject/; + +use constant DATE_FORMAT => '%c_%D_%y'; + +#------------------------------------------------------------------- + +=head2 definition ( ) + +defines wobject properties for New Wobject instances. You absolutely need +this method in your new Wobjects. If you choose to "autoGenerateForms", the +getEditForm method is unnecessary/redundant/useless. + +=cut + +sub definition { + my $class = shift; + my $session = shift; + my $definition = shift; + my $i18n = WebGUI::International->new($session, 'Asset_StoryTopic'); + my %properties; + tie %properties, 'Tie::IxHash'; + %properties = ( + storiesPer => { + tab => 'display', + fieldType => 'integer', + label => $i18n->get('stories per topic'), + hoverHelp => $i18n->get('stories per topic help'), + defaultValue => 15, + }, + storiesShort => { + tab => 'display', + fieldType => 'integer', + label => $i18n->get('stories short'), + hoverHelp => $i18n->get('stories short help'), + defaultValue => 5, + }, + templateId => { + tab => 'display', + fieldType => 'template', + label => $i18n->get('template'), + hoverHelp => $i18n->get('template help'), + filter => 'fixId', + namespace => 'StoryTopic', + defaultValue => 'A16v-YjWAShXWvSACsraeg', + }, + storyTemplateId => { + tab => 'display', + fieldType => 'template', + label => $i18n->get('story template'), + hoverHelp => $i18n->get('story template help'), + filter => 'fixId', + namespace => 'Story', + defaultValue => 'TbDcVLbbznPi0I0rxQf2CQ', + }, + ); + push(@{$definition}, { + assetName=>$i18n->get('assetName'), + icon=>'assets.gif', + autoGenerateForms=>1, + tableName=>'StoryTopic', + className=>'WebGUI::Asset::Wobject::StoryTopic', + properties=>\%properties, + }); + return $class->SUPER::definition($session, $definition); +} + +#------------------------------------------------------------------- + +=head2 getRssFeedItems ( ) + +Returns an arrayref of hashrefs, containing information on stories +for generating an RSS and Atom feeds. + +=cut + +sub getRssFeedItems { + my ($self) = @_; + my $session = $self->session; + my $wordList = WebGUI::Keyword::string2list($self->get('keywords')); + my $key = WebGUI::Keyword->new($session); + my $storyIds = $key->getMatchingAssets({ + keywords => $wordList, + isa => 'WebGUI::Asset::Story', + rowsPerPage => $self->get('storiesPer'), + }); + my $storyData = []; + STORY: foreach my $storyId (@{ $storyIds }) { + my $story = WebGUI::Asset->newByDynamicClass($session, $storyId); + next STORY unless $story; + push @{ $storyData }, $story->getRssData; + } + return $storyData; +} + +#------------------------------------------------------------------- + +=head2 prepareView ( ) + +See WebGUI::Asset::prepareView() for details. + +=cut + +sub prepareView { + my $self = shift; + $self->SUPER::prepareView(); + my $template = WebGUI::Asset::Template->new($self->session, $self->get("templateId")); + $template->prepare; + $self->{_viewTemplate} = $template; +} + + +#------------------------------------------------------------------- + +=head2 view ( ) + +Method called by the www_view method. Returns a processed template +to be displayed within the page style. + +=cut + +sub view { + my $self = shift; + my $session = $self->session; + + #This automatically creates template variables for all of your wobject's properties. + my $var = $self->viewTemplateVariables; + + return $self->processTemplate($var, undef, $self->{_viewTemplate}); +} + +#------------------------------------------------------------------- + +=head2 viewTemplateVars ( ) + +Make template variables for the view template. + +=cut + +sub viewTemplateVariables { + my ($self) = @_; + my $session = $self->session; + my $exporting = $session->scratch->get('isExporting'); + my $numberOfStories = $self->{_standAlone} + ? $self->get('storiesPer') + : $self->get('storiesShort'); + my $var = $self->get(); + my $wordList = WebGUI::Keyword::string2list($self->get('keywords')); + my $key = WebGUI::Keyword->new($session); + my $p = $key->getMatchingAssets({ + keywords => $wordList, + isa => 'WebGUI::Asset::Story', + usePaginator => 1, + rowsPerPage => $numberOfStories, + }); + my $storyIds = $p->getPageData(); + $var->{story_loop} = []; + ##Only build objects for the assets that we need + STORY: foreach my $storyId (@{ $storyIds }) { + my $story = WebGUI::Asset->new($session, $storyId->{assetId}, $storyId->{className}, $storyId->{revisionDate}); + next STORY unless $story; + push @{$var->{story_loop}}, { + url => ( $exporting + ? $story->getUrl + : $session->url->append($self->getUrl, 'func=viewStory;assetId='.$storyId->{assetId}) ), + title => $story->getTitle, + creationDate => $story->get('creationDate'), + } + } + + if ($self->{_standAlone}) { + my $topStoryData = $storyIds->[0]; + shift @{ $var->{story_loop} }; + ##Note, this could have saved from the loop above, but this looks more clean and encapsulated to me. + my $topStory = WebGUI::Asset->new($session, $topStoryData->{assetId}, $topStoryData->{className}, $topStoryData->{revisionDate}); + $var->{topStoryTitle} = $topStory->getTitle; + $var->{topStorySubtitle} = $topStory->get('subtitle'); + $var->{topStoryUrl} = $session->url->append($self->getUrl, 'func=viewStory;assetId='.$topStoryData->{assetId}), + $var->{topStoryCreationDate} = $topStory->get('creationDate'); + ##TODO: Photo variables + my $photoData = $topStory->getPhotoData; + PHOTO: foreach my $photo (@{ $photoData }) { + next PHOTO unless $photo->{storageId}; + my $storage = WebGUI::Storage->get($session, $photo->{storageId}); + my $file = $storage->getFiles->[0]; + next PHOTO unless $file; + my $imageUrl = $storage->getUrl($file); + $var->{topStoryImageUrl} = $imageUrl; + $var->{topStoryImageCaption} = $photo->{caption}; + $var->{topStoryImageByline} = $photo->{byLine}; + $var->{topStoryImageAlt} = $photo->{alt}; + $var->{topStoryImageTitle} = $photo->{title}; + $var->{topStoryImageLink} = $photo->{url}; + last PHOTO; + } + } + $var->{standAlone} = $self->{_standAlone}; + $var->{rssUrl} = $exporting ? $self->getStaticRssFeedUrl : $self->getRssFeedUrl; + $var->{atomUrl} = $exporting ? $self->getStaticAtomFeedUrl : $self->getAtomFeedUrl; + + return $var; +} + +#------------------------------------------------------------------- + +=head2 www_view ( ) + +Overside the method inherited from Wobject to set the mode so template +variables are set correctly in viewTemplateVars. + +=cut + + +sub www_view { + my $self = shift; + $self->{_standAlone} = 1; + return $self->SUPER::www_view; +} + +#------------------------------------------------------------------- + +=head2 www_viewStory ( ) + +Display a story, set in the form variable assetId + +=cut + + +sub www_viewStory { + my $self = shift; + my $session = $self->session; + my $storyId = $session->form->get('assetId'); + my $story; + if ($storyId) { + $story = WebGUI::Asset->new($session, $storyId); + } + if (! $story) { + my $notFound = WebGUI::Asset->getNotFound($session); + $session->asset($notFound); + return $notFound->www_view; + } + $story->topic($self); + return $story->www_view; +} + + +1; +#vim:ft=perl diff --git a/lib/WebGUI/AssetExportHtml.pm b/lib/WebGUI/AssetExportHtml.pm index 9446939f7..10d447f66 100644 --- a/lib/WebGUI/AssetExportHtml.pm +++ b/lib/WebGUI/AssetExportHtml.pm @@ -128,7 +128,24 @@ sub exportCheckPath { Main logic hub for export functionality. This method calls most of the rest of the methods that handle exporting. Any exceptions thrown by the called methods are returned as strings to the caller. Returns a status description upon -completion. Takes a hashref of arguments, containing the following keys: +completion. + +Internally, it sets two scratch variables in private sessions that it creates +for exporting. + +=over 4 + +=item exportMode + +If this scratch variable exists, and is true, then the Asset is being exported. + +=item exportUrl + +This scratch variable is used by the Widget Macro. + +=back + +Takes a hashref of arguments, containing the following keys: =head3 quiet @@ -173,6 +190,8 @@ false will do nothing. sub exportAsHtml { my $self = shift; my $session = $self->session; + # set a scratch variable for Assets to know we're exporting + $session->scratch->set('isExporting', 1); my ($returnCode, $message); # get the i18n object @@ -287,8 +306,9 @@ sub exportAsHtml { my $exportSession = WebGUI::Session->open($self->session->config->getWebguiRoot, $self->session->config->getFilename); $exportSession->user( { userId => $userId } ); - # set a scratch variable for widgets to know we're exporting - $exportSession->scratch->set('exportUrl', $exportUrl); + # set a scratch variable for Assets and widgets to know we're exporting + $exportSession->scratch->set('isExporting', 1); + $exportSession->scratch->set('exportUrl', $exportUrl); my $asset = WebGUI::Asset->newByDynamicClass($exportSession, $assetId); my $fullPath = $asset->exportGetUrlAsPath; @@ -519,6 +539,8 @@ Translates a URL into an appropriate path and filename for exporting. For example, given C<'/foo/bar/baz'>, will return C<'/foo/bar/baz/index.html'> provided the value of indexFile as given to exportAsHtml was C<'index.html'>. +Returns a Path::Class::File object. + =head3 url URL of the asset we need an export path for diff --git a/lib/WebGUI/Help/Asset.pm b/lib/WebGUI/Help/Asset.pm index 498c52cfa..7df71673a 100644 --- a/lib/WebGUI/Help/Asset.pm +++ b/lib/WebGUI/Help/Asset.pm @@ -30,6 +30,8 @@ our $HELP = { { name => 'isPrototype', }, { name => 'status', }, { name => 'assetSize', }, + { name => 'keywords', + description => 'keywords template var' }, ], fields => [], related => [] diff --git a/lib/WebGUI/Help/Asset_Story.pm b/lib/WebGUI/Help/Asset_Story.pm new file mode 100644 index 000000000..1597b6b4d --- /dev/null +++ b/lib/WebGUI/Help/Asset_Story.pm @@ -0,0 +1,166 @@ +package WebGUI::Help::Asset_Story; +use strict; + +our $HELP = { + + 'edit template' => { + title => 'edit template', + body => '', + isa => [ + { namespace => 'Asset_Template', + tag => 'template variables' + }, + ], + fields => [], + variables => [ + { name => 'formHeader', + required => 1 }, + { name => 'formTitle', }, + { name => 'formFooter', + required => 1 }, + { name => 'titleForm', }, + { name => 'headlineForm', }, + { name => 'subtitleForm', }, + { name => 'bylineForm', }, + { name => 'locationForm', }, + { name => 'keywordsForm', }, + { name => 'summaryForm', }, + { name => 'highlightsForm', }, + { name => 'storyForm', }, + { name => 'saveButton', }, + { name => 'saveAndAddButton', }, + { name => 'cancelButton', }, + { name => 'photo_form_loop', + variables => [ + { name => 'imgUploadForm', }, + { name => 'imgCaptionForm', }, + { name => 'imgBylineForm', }, + { name => 'imgAltForm', }, + { name => 'imgTitleForm', }, + { name => 'imgUrlForm', }, + { name => 'imgDeleteForm', }, + ], + }, + ], + related => [] + }, + + 'view template' => { + title => 'view template', + body => '', + isa => [ + { namespace => 'Asset_Template', + tag => 'template variables' + }, + ], + fields => [], + variables => [ + { name => 'highlights_loop', + 'variables' => [ + { name => 'highlight', }, + ], + }, + { name => 'keywords_loop', + 'variables' => [ + { name => 'keyword', }, + { name => 'url', + description => 'keyword_url' + }, + ], + }, + { name => 'updatedTime', }, + { name => 'updatedTimeEpoch', }, + { name => 'crumb_loop', + 'variables' => [ + { name => 'title', + description => 'crumb_title' + }, + { name => 'url', + description => 'crumb_url' + }, + ], + }, + { name => 'hasPhotos', }, + { name => 'singlePhoto', }, + { name => 'photo_loop', + 'variables' => [ + { name => 'imageUrl', }, + { name => 'imageCaption', }, + { name => 'imageByline', }, + { name => 'imageAlt', }, + { name => 'imageTitle', }, + { name => 'imageLink', }, + ], + }, + ], + related => [] + }, + + 'story asset template variables' => { + private => 1, + title => 'story asset template variables title', + body => '', + isa => [ + { namespace => 'Asset', + tag => 'asset template asset variables' + }, + ], + fields => [], + variables => [ + { name => 'headline', + description => 'headline tmplvar', + }, + { name => 'subtitle', + description => 'subtitle tmplvar', + }, + { name => 'byline', + description => 'byline tmplvar', + }, + { name => 'updatedTime', }, + { name => 'updatedTimeEpoch', }, + ], + related => [] + }, + + 'story asset template variables' => { + private => 1, + title => 'story asset template variables title', + body => '', + isa => [ + { namespace => 'Asset', + tag => 'asset template asset variables' + }, + ], + fields => [], + variables => [ + { name => 'headline', + description => 'headline tmplvar', + }, + { name => 'subtitle', + description => 'subtitle tmplvar', + }, + { name => 'byline', + description => 'byline tmplvar', + }, + { name => 'location', + description => 'location tmplvar', + }, + { name => 'highlights', + description => 'highlights tmplvar', + }, + { name => 'story', + description => 'story tmplvar', + }, + { name => 'photo', + description => 'photo tmplvar', + }, + { name => 'storageId', + description => 'storageId tmplvar', + }, + ], + related => [] + }, + +}; + +1; diff --git a/lib/WebGUI/Help/Asset_StoryArchive.pm b/lib/WebGUI/Help/Asset_StoryArchive.pm new file mode 100644 index 000000000..ed3aa84e4 --- /dev/null +++ b/lib/WebGUI/Help/Asset_StoryArchive.pm @@ -0,0 +1,102 @@ +package WebGUI::Help::Asset_StoryArchive; +use strict; + +our $HELP = { + + 'view template' => { + title => 'view template', + body => '', + isa => [ + { namespace => "Asset_StoryArchive", + tag => "storyarchive asset template variables" + }, + { namespace => "Asset_Template", + tag => "template variables" + }, + { namespace => "Asset", + tag => "asset template" + }, + { tag => 'pagination template variables', + namespace => 'WebGUI' + }, + ], + fields => [], + variables => [ + { 'name' => 'date_loop', + 'variables' => [ + { 'name' => 'epochDate' }, + { 'name' => 'story_loop', + 'variables' => [ + { 'name' => 'url' }, + { 'name' => 'title' }, + { 'name' => 'creationDate' }, + ], + }, + ] + }, + { 'name' => 'searchHeader' }, + { 'name' => 'searchForm' }, + { 'name' => 'searchButton' }, + { 'name' => 'searchFooter' }, + { 'name' => 'canPostStories' }, + { 'name' => 'addStoryUrl' }, + { 'name' => 'rssUrl' }, + { 'name' => 'atomUrl' }, + { 'name' => 'keywordCloud' }, + ], + related => [] + }, + + 'storyarchive asset template variables' => { + private => 1, + title => 'storyarchive asset template variables title', + body => '', + isa => [ + { namespace => "Asset_Wobject", + tag => "wobject template variables" + }, + ], + fields => [], + variables => [ + { 'name' => 'storesPerPage', + 'description' => 'stories per page help', + }, + { 'name' => 'groupToPost', }, + { 'name' => 'templateId', }, + { 'name' => 'storyTemplateId', }, + { 'name' => 'editStoryTemplateId', }, + { 'name' => 'keywordListTemplateId', }, + { 'name' => 'archiveAfter', }, + { 'name' => 'richEditorId', }, + { 'name' => 'approvalWorkflowId', }, + ], + related => [] + }, + + + 'keyword list template' => { + title => 'view template', + body => '', + isa => [ + { namespace => "Asset_Template", + tag => "template variables" + }, + ], + fields => [], + variables => [ + { 'name' => 'asset_loop', + 'variables' => [ + { 'name' => 'title', + description => 'asset title' }, + { 'name' => 'url', + description => 'asset url' }, + ] + }, + { 'name' => 'keyword' }, + ], + related => [] + }, + +}; + +1; diff --git a/lib/WebGUI/Help/Asset_StoryTopic.pm b/lib/WebGUI/Help/Asset_StoryTopic.pm new file mode 100644 index 000000000..d9bd1ba62 --- /dev/null +++ b/lib/WebGUI/Help/Asset_StoryTopic.pm @@ -0,0 +1,64 @@ +package WebGUI::Help::Asset_StoryTopic; +use strict; + +our $HELP = { + + 'view template' => { + title => 'view template', + body => '', + isa => [ + { namespace => "Asset_StoryTopic", + tag => "storytopic asset template variables" + }, + { namespace => "Asset_Template", + tag => "template variables" + }, + { namespace => "Asset", + tag => "asset template" + }, + ], + fields => [], + variables => [ + { name => 'standAlone' }, + { name => 'story_loop', + variables => [ + { name => 'url' }, + { name => 'title' }, + { name => 'creationDate' }, + ], + }, + { name => 'topStoryTitle' }, + { name => 'topStorySubtitle' }, + { name => 'topStoryUrl' }, + { name => 'topStoryCreationDate' }, + { name => 'topStoryImageUrl' }, + { name => 'topStoryImageCaption' }, + { name => 'topStoryImageByline' }, + { name => 'topStoryImageAlt' }, + { name => 'topStoryImageTitle' }, + { name => 'topStoryImageLink' }, + { name => 'rssUrl' }, + { name => 'atomUrl' }, + ], + related => [] + }, + + 'storytopic asset template variables' => { + private => 1, + title => 'storytopic asset template variables title', + body => '', + isa => [ + { namespace => "Asset_Wobject", + tag => "wobject template variables" + }, + ], + fields => [], + variables => [ + ], + related => [] + }, + + +}; + +1; diff --git a/lib/WebGUI/Keyword.pm b/lib/WebGUI/Keyword.pm index b66c28cca..6a128c7b6 100644 --- a/lib/WebGUI/Keyword.pm +++ b/lib/WebGUI/Keyword.pm @@ -256,7 +256,7 @@ sub getKeywordsForAsset { =head2 getMatchingAssets ( { startAsset => $asset, keyword => $keyword } ) -Returns an array reference of asset ids matching the params. +Returns an array reference of asset ids matching the params. Assets are returned in order of creationDate. =head3 startAsset @@ -283,6 +283,11 @@ A classname pattern to match. For example, if you provide 'WebGUI::Asset::Sku' t Instead of returning an array reference of assetId's, return a paginator object. +=head3 rowsPerPage + +If usePaginator is passed, then this variable will set the number of rows per page that the paginator uses. +If usePaginator is not passed, then this variable will limit the number of assetIds that are returned. + =cut sub getMatchingAssets { @@ -331,14 +336,17 @@ sub getMatchingAssets { # write the query my $query = 'select distinct assetKeyword.assetId from assetKeyword left join asset using (assetId) - where '.join(' and ', @clauses).' order by creationDate desc'; + where '.join(' and ', @clauses).' order by creationDate desc, lineage'; # perform the search if ($options->{usePaginator}) { - my $p = WebGUI::Paginator->new($self->session); + my $p = WebGUI::Paginator->new($self->session, undef, $options->{rowsPerPage}); $p->setDataByQuery($query, undef, undef, \@params); return $p; } + elsif ($options->{rowsPerPage}) { + $query .= ' limit '. $options->{rowsPerPage}; + } return $self->session->db->buildArrayRef($query, \@params); } diff --git a/lib/WebGUI/Storage.pm b/lib/WebGUI/Storage.pm index af39508a0..8e5ece6d4 100644 --- a/lib/WebGUI/Storage.pm +++ b/lib/WebGUI/Storage.pm @@ -288,7 +288,7 @@ sub addFileFromFormPost { my $filename; my $attachmentCount = 1; foreach my $upload ($session->request->upload($formVariableName)) { - $session->errorHandler->info("Trying to get " . $upload->filename); + $session->errorHandler->info("Trying to get " . $upload->filename." from ".$formVariableName); return $filename if $attachmentCount > $attachmentLimit; my $clientFilename = $upload->filename; diff --git a/lib/WebGUI/Workflow/Activity/ArchiveOldStories.pm b/lib/WebGUI/Workflow/Activity/ArchiveOldStories.pm new file mode 100644 index 000000000..d6dd3cd54 --- /dev/null +++ b/lib/WebGUI/Workflow/Activity/ArchiveOldStories.pm @@ -0,0 +1,104 @@ +package WebGUI::Workflow::Activity::ArchiveOldStories; + + +=head1 LEGAL + + ------------------------------------------------------------------- + WebGUI is Copyright 2001-2009 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 base 'WebGUI::Workflow::Activity'; +use WebGUI::Asset; +use WebGUI::Asset::Wobject::StoryArchive; + +=head1 NAME + +Package WebGUI::Workflow::Activity::ArchiveOldStories + +=head1 DESCRIPTION + +Uses the settings in the Story Archive to determine whether the Stories (and Folders) in those Story Archives should be archived. + +=head1 SYNOPSIS + +See WebGUI::Workflow::Activity for details on how to use any activity. + +=head1 METHODS + +These methods are available from this class: + +=cut + + +#------------------------------------------------------------------- + +=head2 definition ( session, definition ) + +See WebGUI::Workflow::Activity::defintion() for details. + +=cut + +sub definition { + my $class = shift; + my $session = shift; + my $definition = shift; + my $i18n = WebGUI::International->new($session, "Workflow_Activity_ArchiveOldStories"); + push(@{$definition}, { + name=>$i18n->get("activityName"), + properties=> {} + }); + return $class->SUPER::definition($session,$definition); +} + + +#------------------------------------------------------------------- + +=head2 execute ( ) + +See WebGUI::Workflow::Activity::execute() for details. + +=cut + +sub execute { + my $self = shift; + my $session = $self->session; + my $epoch = $self->session->datetime->time(); + my $getAnArchive = WebGUI::Asset::Wobject::StoryArchive->getIsa($session); + ARCHIVE: while (my $archive = $getAnArchive->()) { + next ARCHIVE unless $archive && $archive->get("archiveAfter"); + my $archiveDate = $epoch - $archive->get("archiveAfter"); + my $folders = $archive->getLineage( + ['children'], + { + statusToInclude => ['approved'], + whereClause => 'creationDate < '.$session->db->quote($archiveDate), + returnObjects => 1, + }, + ); + FOLDER: foreach my $folder (@{ $folders }) { + next FOLDER unless $folder; + my $stories = $folder->getLineage( + ['children'], { returnObjects => 1, }, + ); + STORY: foreach my $story (@{ $stories }) { + next STORY unless $story; + $story->update({ status => 'archived' }); + } + $folder->update({ status => 'archived' }); + } + } + return $self->COMPLETE; +} + +1; + + diff --git a/lib/WebGUI/i18n/English/Asset.pm b/lib/WebGUI/i18n/English/Asset.pm index e165b1f71..67683ae1c 100644 --- a/lib/WebGUI/i18n/English/Asset.pm +++ b/lib/WebGUI/i18n/English/Asset.pm @@ -26,6 +26,12 @@ our $I18N = { context => q|help for the keywords property| }, + 'keywords template var' => { + message => q|This will be a string with the keywords for this asset. Individual keywords will be joined with spaces, unless the keyword contains spaces, in which case it will be quoted.|, + lastUpdated => 0, + context => q|help for the keywords template variable| + }, + 'add the missing page' => { message => q|Add the missing page.|, lastUpdated => 0, diff --git a/lib/WebGUI/i18n/English/Asset_Story.pm b/lib/WebGUI/i18n/English/Asset_Story.pm new file mode 100644 index 000000000..e8c9b2da2 --- /dev/null +++ b/lib/WebGUI/i18n/English/Asset_Story.pm @@ -0,0 +1,451 @@ +package WebGUI::i18n::English::Asset_Story; +use strict; + +our $I18N = { + + 'assetName' => { + message => q|Story|, + context => q|Story, as in news story.|, + lastUpdated => 0 + }, + + 'headline' => { + message => q|Headline|, + context => q|Usually the title of a story. Label in the edit screen and template.|, + lastUpdated => 0 + }, + + 'headline help' => { + message => q|Often the same as title. If left blank, it will take the headline from the title.|, + context => q|Hoverhelp in the edit screen and template.|, + lastUpdated => 0 + }, + + 'headline tmplvar' => { + message => q|The headline for the Story.|, + context => q|Template variable help.|, + lastUpdated => 0 + }, + + 'subtitle' => { + message => q|Subtitle|, + context => q|Similar to headline, but usually contains more information. Label in the edit screen and template.|, + lastUpdated => 0 + }, + + 'subtitle help' => { + message => q|Similar to headline, but usually contains more information.|, + context => q|Hoverhelp in the edit screen and template.|, + lastUpdated => 0 + }, + + 'subtitle tmplvar' => { + message => q|The subtitle from the Story.|, + context => q|Template variable help.|, + lastUpdated => 0 + }, + + 'byline' => { + message => q|By line|, + context => q|Who wrote the story. Label in the edit screen and template.|, + lastUpdated => 0 + }, + + 'byline help' => { + message => q|Who wrote the story.|, + context => q|Hoverhelp in the edit screen and template.|, + lastUpdated => 0 + }, + + 'byline tmplvar' => { + message => q|The byline from the Story.|, + context => q|Template variable help.|, + lastUpdated => 0 + }, + + 'location' => { + message => q|Location|, + context => q|Where the story takes place. Label in the edit screen and template.|, + lastUpdated => 0 + }, + + 'location help' => { + message => q|Where the story takes place.|, + context => q|Hoverhelp in the edit screen and template.|, + lastUpdated => 0 + }, + + 'location tmplvar' => { + message => q|The location from the Story.|, + context => q|Template variable help.|, + lastUpdated => 0 + }, + + 'highlights' => { + message => q|Story Highlights|, + context => q|Bullet point level summaries from the story. Label in the edit screen and template.|, + lastUpdated => 0 + }, + + 'highlights help' => { + message => q|Bullet point level items from the story. Enter 1 per line.|, + context => q|Hoverhelp in the edit screen and template.|, + lastUpdated => 0 + }, + + 'highlights tmplvar' => { + message => q|All of the highlights from the Story. Each highlight will be separated by a newline character.|, + context => q|Template variable help.|, + lastUpdated => 0 + }, + + 'story' => { + message => q|Story|, + context => q|Label in the edit screen and template.|, + lastUpdated => 0 + }, + + 'story help' => { + message => q|The story.|, + context => q|Hoverhelp in the edit screen and template.|, + lastUpdated => 0 + }, + + 'story tmplvar' => { + message => q|The story. Note that it may contain embedded content from the Rich Text Editor.|, + context => q|Template variable help.|, + lastUpdated => 0 + }, + + 'save story' => { + message => q|Save Story|, + context => q|Button label in the Edit Story form.|, + lastUpdated => 0 + }, + + 'save and add another photo' => { + message => q|Save and Add Another Photo|, + context => q|Button label in the Edit Story form.|, + lastUpdated => 0 + }, + + 'story received' => { + message => q|Your story has been received and is being processed so it can be added to the site. It will be available for further editing after being processed. Please be patient.|, + lastUpdated => 0, + }, + + 'edit template' => { + message => q|Edit Story Template.|, + lastUpdated => 0, + }, + + 'formHeader' => { + message => q|HTML code to begin the form for adding or editing a Story.|, + lastUpdated => 0, + }, + + 'formTitle' => { + message => q|Internationalized title for this form.|, + lastUpdated => 0, + }, + + 'titleForm' => { + message => q|Form for the user to enter a title for this story.|, + lastUpdated => 0, + }, + + 'headlineForm' => { + message => q|Form for the user to enter a headline for this story.|, + lastUpdated => 0, + }, + + 'subtitleForm' => { + message => q|Form for the user to enter a subtitle for this story.|, + lastUpdated => 0, + }, + + 'bylineForm' => { + message => q|Form for the user to enter a byline for this story.|, + lastUpdated => 0, + }, + + 'locationForm' => { + message => q|Form for the user to enter a location for this story.|, + lastUpdated => 0, + }, + + 'keywordsForm' => { + message => q|Form for the user to enter keywords for this story.|, + lastUpdated => 0, + }, + + 'summaryForm' => { + message => q|Form for the user to enter a summary of this story.|, + lastUpdated => 0, + }, + + 'highlightsForm' => { + message => q|Form for the user to enter highlights for this story.|, + lastUpdated => 0, + }, + + 'storyForm' => { + message => q|Form for the user to enter the actual story.|, + lastUpdated => 0, + }, + + 'saveButton' => { + message => q|Button for the user to save the form.|, + lastUpdated => 0, + }, + + 'saveAndAddButton' => { + message => q|Button for the user to save the form, and then reopen the edit form to add another photo.|, + lastUpdated => 0, + }, + + 'cancelButton' => { + message => q|Button for the user to cancel this form without saving anything.|, + lastUpdated => 0, + }, + + 'formFooter' => { + message => q|HTML code to end the form for adding or editing a Story.|, + lastUpdated => 0, + }, + + 'view template' => { + message => q|View Story Template.|, + lastUpdated => 0, + }, + + 'highlights_loop' => { + message => q|A loop containing all the highlights from the story.|, + lastUpdated => 0, + }, + + 'highlight' => { + message => q|One highlight, without formatting or extra HTML.|, + lastUpdated => 0, + }, + + 'keywords_loop' => { + message => q|A loop containing all the keywords from the story.|, + lastUpdated => 0, + }, + + 'keyword' => { + message => q|One keyword, with no formatting.|, + lastUpdated => 0, + }, + + 'keyword_url' => { + message => q|A URL to view all stories in this archive related to this keyword.|, + lastUpdated => 0, + }, + + 'crumb_loop' => { + message => q|A loop containing the crumbtrail. The first element will be a link to the archive that contains the story. The last element will be the story, with title and url. If there are 3 elements, the middle element will be the topic.|, + lastUpdated => 0, + }, + + 'crumb_title' => { + message => q|The title of a page in the crumb trail.|, + lastUpdated => 0, + }, + + 'crumb_url' => { + message => q|The title of a page in the crumb trail.|, + lastUpdated => 0, + }, + + 'updatedTime' => { + message => q|The time this Story was last updated, as a formatted duration, like 1 Hour(s) ago.|, + lastUpdated => 0, + }, + + 'updatedTimeEpoch' => { + message => q|The time this Story was last updated, as an epoch.|, + lastUpdated => 0, + }, + + 'photo tmplvar' => { + message => q|The photo JSON blob from the Story asset.|, + lastUpdated => 0, + }, + + 'storageId tmplvar' => { + message => q|The photo JSON blob from the Story asset.|, + lastUpdated => 0, + }, + + 'ago' => { + message => q|ago|, + context => q|As in the phrase, Last updated 3 hours ago.|, + lastUpdated => 0, + }, + + 'storageId tmplvar' => { + message => q|The photo JSON blob from the Story asset.|, + lastUpdated => 0, + }, + + 'story asset template variables title' => { + message => q|Story Asset Template Variables.|, + context => q|Title of a help page for asset level template variables.|, + lastUpdated => 0, + }, + + 'photo_form_loop' => { + message => q|A loop containing subforms for all photos that have been loaded, and a blank form for uploading new photos.|, + context => q|Template variable for edit form.|, + lastUpdated => 0, + }, + + 'imgUploadForm' => { + message => q|A form field to upload an image.|, + context => q|Template variable for edit form.|, + lastUpdated => 0, + }, + + 'imgCaptionForm' => { + message => q|A form field for the caption for this image.|, + context => q|Template variable for edit form.|, + lastUpdated => 0, + }, + + 'imgBylineForm' => { + message => q|A form field for a by-line for this image.|, + context => q|Template variable for edit form.|, + lastUpdated => 0, + }, + + 'imgAltForm' => { + message => q|A form field for alternate text for the image, for the IMG tag ALT field.|, + context => q|Template variable for edit form.|, + lastUpdated => 0, + }, + + 'imgTitleForm' => { + message => q|A form field for the title for the image, for the IMG tag TITLE field.|, + context => q|Template variable for edit form.|, + lastUpdated => 0, + }, + + 'imgUrlForm' => { + message => q|A field for the URL for this image. If present, then the image will be rendered as a link to this URL.|, + context => q|Template variable for edit form.|, + lastUpdated => 0, + }, + + 'imgDeleteForm' => { + message => q|A field to delete the image, along with all data attached to it. This form will not be present in the set of variables in the loop for adding a new image.|, + context => q|Template variable for edit form.|, + lastUpdated => 0, + }, + + 'photo caption' => { + message => q|Photo Caption|, + context => q|Label in the edit story form. Short for Photograph Caption.|, + lastUpdated => 0, + }, + + 'photo byline' => { + message => q|Photo By Line|, + context => q|Label in the edit story form. The person who took, or owns this photo.|, + lastUpdated => 0, + }, + + 'photo alt' => { + message => q|Photo Alternate Text|, + context => q|Label in the edit story form. Text for the ALT attribute of an IMG tag.|, + lastUpdated => 0, + }, + + 'photo title' => { + message => q|Photo Alternate Title|, + context => q|Label in the edit story form. Text for the TITLE attribute of an IMG tag.|, + lastUpdated => 0, + }, + + 'photo url' => { + message => q|Photo URL|, + context => q|Label in the edit story form. A link from the photo to more information about it, or referring to it.|, + lastUpdated => 0, + }, + + 'photo delete' => { + message => q|Delete Photo|, + context => q|Label in the edit story form. Request that the photo be deleted, and all information with it.|, + lastUpdated => 0, + }, + + 'photo_loop' => { + message => q|A loop containing photos and information about the photos.|, + context => q|Template variable|, + lastUpdated => 0, + }, + + 'imageUrl' => { + message => q|The URL to the image.|, + context => q|Template variable|, + lastUpdated => 0, + }, + + 'imageCaption' => { + message => q|A caption for the image.|, + context => q|Template variable|, + lastUpdated => 0, + }, + + 'imageByline' => { + message => q|A byline for the image.|, + context => q|Template variable|, + lastUpdated => 0, + }, + + 'imageAlt' => { + message => q|Alternate text for the image, suitable for use as the ALT parameter for an IMG tag.|, + context => q|Template variable|, + lastUpdated => 0, + }, + + 'imageTitle' => { + message => q|Alternate text for the image, suitable for use as the TITLE parameter for an IMG tag.|, + context => q|Template variable|, + lastUpdated => 0, + }, + + 'imageLink' => { + message => q|A URL for the image to link to.|, + context => q|Template variable|, + lastUpdated => 0, + }, + + 'hasPhotos' => { + message => q|This template variable will be true if the Story has photos uploaded to it.|, + context => q|Template variable|, + lastUpdated => 0, + }, + + 'singlePhoto' => { + message => q|This template variable will be true if the Story has just 1 photo uploaded to it.|, + context => q|Template variable|, + lastUpdated => 0, + }, + + 'imageLink' => { + message => q|A URL for the image to link to.|, + context => q|Template variable|, + lastUpdated => 0, + }, + + 'Source' => { + message => q|Source|, + context => q|Label for story template. Referring to who took, or who owns, a picture.|, + lastUpdated => 0, + }, + +}; + +1; diff --git a/lib/WebGUI/i18n/English/Asset_StoryArchive.pm b/lib/WebGUI/i18n/English/Asset_StoryArchive.pm new file mode 100644 index 000000000..21531acfc --- /dev/null +++ b/lib/WebGUI/i18n/English/Asset_StoryArchive.pm @@ -0,0 +1,302 @@ +package WebGUI::i18n::English::Asset_StoryArchive; +use strict; + +our $I18N = { + + 'assetName' => { + message => q|Story Archive|, + context => q|An Asset that holds stories.|, + lastUpdated => 0 + }, + + 'stories per page' => { + message => q|Stories Per Page|, + context => q|Label in the edit screen and template.|, + lastUpdated => 0 + }, + + 'stories per page help' => { + message => q|The number of stories displayed on a page. If the asset is exported as HTML, then the generated page will have 10 standard pages of stories.|, + context => q|Hoverhelp in the edit screen and template.|, + lastUpdated => 0 + }, + + 'group to post' => { + message => q|Group to Post|, + context => q|Label in the edit screen and template.|, + lastUpdated => 0 + }, + + 'group to post help' => { + message => q|The group allowed to add stories to this Story Archive.|, + context => q|Hoverhelp in the edit screen and template.|, + lastUpdated => 0 + }, + + 'groupToPost' => { + message => q|The GUID of the group allowed to add stories to this Story Archive.|, + context => q|Template variable.|, + lastUpdated => 0 + }, + + 'template' => { + message => q|Story Archive Template|, + context => q|Label in the edit screen and template.|, + lastUpdated => 0 + }, + + 'template help' => { + message => q|The Template used to display the Story Archive.|, + context => q|Hoverhelp in the edit screen and template.|, + lastUpdated => 0 + }, + + 'templateId' => { + message => q|The GUID of the template used to display the Story Archive.|, + context => q|Template variable|, + lastUpdated => 0 + }, + + 'story template' => { + message => q|Story Template|, + context => q|Label in the edit screen and template.|, + lastUpdated => 0 + }, + + 'story template help' => { + message => q|The Template used to display Story assets from this Story Archive.|, + context => q|Hoverhelp in the edit screen and template.|, + lastUpdated => 0 + }, + + 'storyTemplateId' => { + message => q|The GUID of the template used to display the Story assets.|, + context => q|Template variable|, + lastUpdated => 0 + }, + + 'edit story template' => { + message => q|Edit Story Template|, + context => q|Label in the edit screen and template.|, + lastUpdated => 0 + }, + + 'edit story template help' => { + message => q|The Template used to add or edit Story assets.|, + context => q|Hoverhelp in the edit screen and template.|, + lastUpdated => 0 + }, + + 'keyword list template' => { + message => q|Keyword List Template|, + context => q|Label in the edit screen and template.|, + lastUpdated => 0 + }, + + 'keyword list template help' => { + message => q|The Template used to render the list of assets matching a keyword when this StoryArchive is exported.|, + context => q|Hoverhelp in the edit screen and template.|, + lastUpdated => 0 + }, + + 'editStoryTemplateId' => { + message => q|The GUID of the template used to add or edit Story assets.|, + context => q|Template variable|, + lastUpdated => 0 + }, + + 'keywordListTemplateId' => { + message => q|The GUID of the template used to render list of assets matching a keyword when this StoryArchive is exported.|, + context => q|Template variable|, + lastUpdated => 0 + }, + + 'archive after' => { + message => q|Archive Stories After|, + context => q|Label in the edit screen and template.|, + lastUpdated => 0 + }, + + 'archive after help' => { + message => q|After this time, Story assets will be archived and no longer show up in the list of Stories or feeds. Set to 0 to disable archiving.|, + context => q|Hoverhelp in the edit screen and template.|, + lastUpdated => 0 + }, + + 'archiveAfter' => { + message => q|Amount of time in seconds. After this time, Stories will be archived.|, + context => q|Template variable|, + lastUpdated => 0 + }, + + 'rich editor' => { + message => q|Rich Editor|, + context => q|Label in the edit screen and template.|, + lastUpdated => 0 + }, + + 'rich editor help' => { + message => q|The WYSIWIG editor used to edit the content of Story assets.|, + context => q|Hoverhelp in the edit screen and template.|, + lastUpdated => 0 + }, + + 'richEditorId' => { + message => q|The GUID of the WYSIWIG editor used to edit the content of Story assets.|, + context => q|Template variable|, + lastUpdated => 0 + }, + + 'approval workflow' => { + message => q|Story Approval Workflow|, + context => q|Label in the edit screen and template.|, + lastUpdated => 0, + }, + + 'approval workflow help' => { + message => q|Choose a workflow to be executed on each Story as it gets submitted.|, + context => q|Hoverhelp in the edit screen and template.|, + lastUpdated => 0, + }, + + 'approvalWorkflowId' => { + message => q|The GUID of the workflow to be executed on each Story as it gets submitted.|, + context => q|Template variable|, + lastUpdated => 0, + }, + + 'storyarchive asset template variables title' => { + message => q|Story Archive Asset Template Variables.|, + context => q|Title of a help page for asset level template variables.|, + lastUpdated => 0, + }, + + 'view template' => { + message => q|Story Archive, View Template|, + context => q|Title of a help page.|, + lastUpdated => 0, + }, + + 'date_loop' => { + message => q|A loop containing stories in the date they were submitted, with subloops for each day. The loop is paginated.|, + context => q|Template variable.|, + lastUpdated => 0, + }, + + 'epochDate' => { + message => q|The epoch that is the beginning of the day for a day where stories were submitted to the Story Archive.|, + context => q|Template variable.|, + lastUpdated => 0, + }, + + 'story_loop' => { + message => q|A loop containing all stories there were submitted on the day given by epochDate.|, + context => q|Template variable.|, + lastUpdated => 0, + }, + + 'url' => { + message => q|The URL to view a story.|, + context => q|Template variable.|, + lastUpdated => 0, + }, + + 'title' => { + message => q|The title of a story.|, + context => q|Template variable.|, + lastUpdated => 0, + }, + + 'creationDate' => { + message => q|The epoch date when this story was created, or submitted, to the Story Archive.|, + context => q|Template variable.|, + lastUpdated => 0, + }, + + 'add a story' => { + message => q|Add a Story.|, + context => q|label for the URL to add a story to the archive.|, + lastUpdated => 0, + }, + + 'searchHeader' => { + message => q|HTML code for beginning the search form. This variable is empty when the Story Archive is being exported as HTML.|, + context => q|Template variable.|, + lastUpdated => 0, + }, + + 'searchForm' => { + message => q|The text field where users can enter in keywords for the search. This variable is empty when the Story Archive is being exported as HTML.|, + context => q|Template variable.|, + lastUpdated => 0, + }, + + 'searchButton' => { + message => q|Button with internationalized label for submitting the search form. This variable is empty when the Story Archive is being exported as HTML.|, + context => q|Template variable.|, + lastUpdated => 0, + }, + + 'searchFooter' => { + message => q|HTML code for ending the search form. This variable is empty when the Story Archive is being exported as HTML.|, + context => q|Template variable.|, + lastUpdated => 0, + }, + + 'canPostStories' => { + message => q|A boolean which is true if the user can post stories.|, + context => q|Template variable.|, + lastUpdated => 0, + }, + + 'addStoryUrl' => { + message => q|The URL for the user to add a Story.|, + context => q|Template variable.|, + lastUpdated => 0, + }, + + 'rssUrl' => { + message => q|The URL for the RSS feed for this Story Archive.|, + context => q|Template variable.|, + lastUpdated => 0, + }, + + 'atomUrl' => { + message => q|The URL for the Atom feed for this Story Archive.|, + context => q|Template variable.|, + lastUpdated => 0, + }, + + 'keywordCloud' => { + message => q|The tag cloud for the keywords for stories in this Story Archive.|, + context => q|Template variable.|, + lastUpdated => 0, + }, + + 'asset_loop' => { + message => q|A loop containing up to the first 50 assets that match the keyword.|, + context => q|Template variable.|, + lastUpdated => 0, + }, + + 'asset title' => { + message => q|The title of this asset.|, + context => q|Template variable.|, + lastUpdated => 0, + }, + + 'asset url' => { + message => q|The title of this url.|, + context => q|Template variable.|, + lastUpdated => 0, + }, + + 'keyword' => { + message => q|The keyword for this list of assets.|, + context => q|Template variable.|, + lastUpdated => 0, + }, + +}; + +1; diff --git a/lib/WebGUI/i18n/English/Asset_StoryTopic.pm b/lib/WebGUI/i18n/English/Asset_StoryTopic.pm new file mode 100644 index 000000000..9e9cf3f73 --- /dev/null +++ b/lib/WebGUI/i18n/English/Asset_StoryTopic.pm @@ -0,0 +1,176 @@ +package WebGUI::i18n::English::Asset_StoryTopic; +use strict; + +our $I18N = { + + 'assetName' => { + message => q|Story Topic|, + context => q|An Asset that displays stories based on keywords.|, + lastUpdated => 0 + }, + + 'stories per topic' => { + message => q|Stories Per Topic|, + context => q|Label in the edit screen and template.|, + lastUpdated => 0 + }, + + 'stories per topic help' => { + message => q|The number of stories displayed in RSS and Atom feeds from this Story Topic, and when viewing the Topic directly.|, + context => q|Hoverhelp in the edit screen and template.|, + lastUpdated => 0 + }, + + 'stories short' => { + message => q|Stories Per Page|, + context => q|Label in the edit screen and template.|, + lastUpdated => 0 + }, + + 'stories short help' => { + message => q|The number of stories displayed on a page.|, + context => q|Hoverhelp in the edit screen and template.|, + lastUpdated => 0 + }, + + 'story template' => { + message => q|Story Template|, + context => q|Label in the edit screen and template.|, + lastUpdated => 0 + }, + + 'story template help' => { + message => q|The Template used to display Story assets from this Story Topic.|, + context => q|Hoverhelp in the edit screen and template.|, + lastUpdated => 0 + }, + + 'template' => { + message => q|Main Template|, + context => q|Label in the edit screen and template.|, + lastUpdated => 0 + }, + + 'template help' => { + message => q|The Template used to display the Story Topic.|, + context => q|Hoverhelp in the edit screen and template.|, + lastUpdated => 0 + }, + + 'story_loop' => { + message => q|A loop containing the most recent stories for this topic.|, + context => q|Template variable.|, + lastUpdated => 0, + }, + + 'url' => { + message => q|The URL to view a story.|, + context => q|Template variable.|, + lastUpdated => 0, + }, + + 'title' => { + message => q|The title of a story.|, + context => q|Template variable.|, + lastUpdated => 0, + }, + + 'creationDate' => { + message => q|The epoch date when this story was created, or submitted, to its Story Archive.|, + context => q|Template variable.|, + lastUpdated => 0, + }, + + 'topStoryUrl' => { + message => q|The URL to view the top story.|, + context => q|Template variable.|, + lastUpdated => 0, + }, + + 'topStoryTitle' => { + message => q|The title of the top story.|, + context => q|Template variable.|, + lastUpdated => 0, + }, + + 'topStorySubtitle' => { + message => q|The subtitle of the top story.|, + context => q|Template variable.|, + lastUpdated => 0, + }, + + 'topStoryCreationDate' => { + message => q|The epoch date when the top story was created, or submitted, to its Story Archive.|, + context => q|Template variable.|, + lastUpdated => 0, + }, + + 'topStoryImageCaption' => { + message => q|The Caption of the first photo for the top story, if it exists.|, + context => q|Template variable.|, + lastUpdated => 0, + }, + + 'topStoryImageByline' => { + message => q|The Byline of the first photo for the top story, if it exists.|, + context => q|Template variable.|, + lastUpdated => 0, + }, + + 'topStoryImageAlt' => { + message => q|The alternate image text for the first photo for the top story, if it exists.|, + context => q|Template variable.|, + lastUpdated => 0, + }, + + 'topStoryImageTitle' => { + message => q|The image title for the first photo for the top story, if it exists.|, + context => q|Template variable.|, + lastUpdated => 0, + }, + + 'topStoryImageLink' => { + message => q|The link for the first photo for the top story, if it exists.|, + context => q|Template variable.|, + lastUpdated => 0, + }, + + 'topStoryImageUrl' => { + message => q|The URL to the first photo for the top story, if it exists.|, + context => q|Template variable.|, + lastUpdated => 0, + }, + + 'standAlone' => { + message => q|This variable will be true if the Story Topic is being viewed directly, by its URL. Otherwise, if it is viewed as part of a Page Layout or other Container, it will be false.|, + context => q|Template variable.|, + lastUpdated => 0, + }, + + 'view template' => { + message => q|View Story Topic Template|, + context => q|Template variable.|, + lastUpdated => 0, + }, + + 'storytopic asset template variables title' => { + message => q|Story Topic Asset Template Variables|, + context => q|Template variable.|, + lastUpdated => 0, + }, + + 'rssUrl' => { + message => q|The URL for the RSS feed for this Story Archive.|, + context => q|Template variable.|, + lastUpdated => 0, + }, + + 'atomUrl' => { + message => q|The URL for the Atom feed for this Story Archive.|, + context => q|Template variable.|, + lastUpdated => 0, + }, + +}; + +1; diff --git a/lib/WebGUI/i18n/English/Workflow_Activity_ArchiveOldStories.pm b/lib/WebGUI/i18n/English/Workflow_Activity_ArchiveOldStories.pm new file mode 100644 index 000000000..d57118eb6 --- /dev/null +++ b/lib/WebGUI/i18n/English/Workflow_Activity_ArchiveOldStories.pm @@ -0,0 +1,13 @@ +package WebGUI::i18n::English::Workflow_Activity_ArchiveOldStories; +use strict; + +our $I18N = { + 'activityName' => { + message => q|Archive Old Stories|, + context => q|The name of this workflow activity.|, + lastUpdated => 0, + }, + +}; + +1; diff --git a/t/Asset/Asset.t b/t/Asset/Asset.t index e86f39c33..e9bf466fe 100644 --- a/t/Asset/Asset.t +++ b/t/Asset/Asset.t @@ -150,7 +150,7 @@ $canViewMaker->prepare( }, ); -plan tests => 103 +plan tests => 104 + scalar(@fixIdTests) + scalar(@fixTitleTests) + 2*scalar(@getTitleTests) #same tests used for getTitle and getMenuTitle @@ -391,6 +391,7 @@ is($importNode->fixUrl('/extras2'), '_extras2', 'underscore prepended to URLs th is($importNode->fixUrl('one.html/two.html'), 'one/two.html', 'extensions are not allowed higher up in the path'); is($importNode->fixUrl('one.html/two.html/three.html'), 'one/two/three.html', 'extensions are not allowed anywhere in the path'); is($importNode->fixUrl('one.one.html/two.html/three.html'), 'one/two/three.html', 'multiple dot extensions are removed in any path element'); +is($importNode->fixUrl('.startsWithDot'), '.startswithdot', 'leading dots are okay'); ##Now, check duplicate URLs diff --git a/t/Asset/Story.t b/t/Asset/Story.t new file mode 100644 index 000000000..e72240bfb --- /dev/null +++ b/t/Asset/Story.t @@ -0,0 +1,391 @@ +#------------------------------------------------------------------- +# WebGUI is Copyright 2001-2009 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 +#------------------------------------------------------------------- + +use FindBin; +use strict; +use lib "$FindBin::Bin/../lib"; + +use WebGUI::Test; +use WebGUI::Session; +use WebGUI::Storage; + +use Test::More; # increment this value for each test you create +use Test::Deep; +use Data::Dumper; + +my $tests = 42; +plan tests => 1 + + $tests + ; + +#TODO: This script tests certain aspects of WebGUI::Storage and it should not + +my $session = WebGUI::Test->session; + +my $class = 'WebGUI::Asset::Story'; +my $loaded = use_ok($class); +my $story; +my $wgBday = WebGUI::Test->webguiBirthday; + +my $defaultNode = WebGUI::Asset->getDefault($session); +my $archive = $defaultNode->addChild({ + className => 'WebGUI::Asset::Wobject::StoryArchive', + title => 'Test Archive', + #1234567890123456789012 + assetId => 'TestStoryArchiveAsset1', +}); +my $topic = $defaultNode->addChild({ + className => 'WebGUI::Asset::Wobject::StoryTopic', + title => 'Test Topic', + #1234567890123456789012 + assetId => 'TestStoryTopicAsset123', + keywords => 'tango,yankee', +}); +my $archiveTag = WebGUI::VersionTag->getWorking($session); +$archiveTag->commit; + +my $storage1 = WebGUI::Storage->create($session); +my $storage2 = WebGUI::Storage->create($session); +WebGUI::Test->storagesToDelete($storage1, $storage2); + + +SKIP: { + +skip "Unable to load module $class", $tests unless $loaded; + +############################################################ +# +# validParent +# +############################################################ + +ok(! WebGUI::Asset::Story->validParent($session), 'validParent: no session asset'); +$session->asset($defaultNode); +ok(! WebGUI::Asset::Story->validParent($session), 'validParent: wrong type of asset'); +$session->asset($archive); +ok( WebGUI::Asset::Story->validParent($session), 'validParent: StoryArchive is valid'); + +############################################################ +# +# Make a new one. Test defaults +# +############################################################ + +$story = $archive->addChild({ + className => 'WebGUI::Asset::Story', + title => 'Story 1', + subtitle => 'The story of a CMS', + byline => 'JT Smith', +}); + +isa_ok($story, 'WebGUI::Asset::Story', 'Created a Story asset'); +is($story->get('photo'), '[]', 'by default, photos is an empty JSON array'); +is($story->get('isHidden'), 1, 'by default, stories are hidden'); +$story->update({isHidden => 0}); +is($story->get('isHidden'), 1, 'stories cannot be set to not be hidden'); +is($story->get('state'), 'published', 'Story is published'); + +{ + ##Version control does not alter the current object's status, fetch an updated copy from the + ##db. + my $storyDB = WebGUI::Asset->newByUrl($session, $story->getUrl); + is($storyDB->get('status'), 'approved', 'Story is approved'); +} + + +############################################################ +# +# getArchive +# +############################################################ + +is($story->getArchive->getId, $archive->getId, 'getArchive gets the parent archive for the Story'); + +############################################################ +# +# Photo JSON +# +############################################################ + +my $photoData = $story->getPhotoData(); +cmp_deeply( + $photoData, [], + 'getPhotoData: returns an empty array ref with no JSON data' +); + +$story->setPhotoData([ + { + byLine => 'Andrew Dufresne', + caption => 'Shawshank Prison', + }, +]); + +is($story->get('photo'), q|[{"caption":"Shawshank Prison","byLine":"Andrew Dufresne"}]|, 'setPhotoData: set JSON in the photo property'); + +$photoData = $story->getPhotoData(); +$photoData->[0]->{caption}="My cell"; + +cmp_deeply( + $story->getPhotoData, + [ + { + byLine => 'Andrew Dufresne', + caption => 'Shawshank Prison', + }, + ], + 'getPhotoData does not return an unsafe reference' +); + +$story->setPhotoData(); +cmp_deeply( + $story->getPhotoData, [], + 'setPhotoData: wipes the stored data if nothing is passed' +); + +############################################################ +# +# formatDuration +# +############################################################ + +is($story->formatDuration(time() - (24*3600+15)), '1 Day(s)', 'formatDuration, 1 day'); +is($story->formatDuration(time() - (48*3600+15)), '2 Day(s)', 'formatDuration, 2 day'); +like($story->formatDuration($wgBday), qr{Year.s.}, 'formatDuration: a long time ago'); +is($story->formatDuration(time() - (3600+5)), '1 Hour(s)', 'formatDuration: 1 hour'); +is($story->formatDuration(time() - (60+5)), '1 Minute(s)', 'formatDuration: 1 minute'); +is($story->formatDuration(time() - (7200+120)), '2 Hour(s), 2 Minute(s)', 'formatDuration: 2 hours, 2 minutes'); + +############################################################ +# +# getCrumbTrail +# +############################################################ + +cmp_deeply( + $story->getCrumbTrail, + [ + { + title => $archive->getTitle, + url => $archive->getUrl, + }, + { + title => $story->getTitle, + url => $story->getUrl, + }, + ], + 'getCrumbTrail: with no topic set' +); + +$story->topic($topic); + +cmp_deeply( + $story->getCrumbTrail, + [ + { + title => $archive->getTitle, + url => $archive->getUrl, + }, + { + title => $topic->getTitle, + url => $topic->getUrl, + }, + { + title => $story->getTitle, + url => $story->getUrl, + }, + ], + 'getCrumbTrail: with topic set' +); + +$story->topic(''); + +############################################################ +# +# getRssData +# +############################################################ + +can_ok($story, 'getRssData'); + +cmp_deeply( + $story->getRssData, + { + title => 'Story 1', + description => 'The story of a CMS', + 'link' => re('story-1$'), + author => 'JT Smith', + date => $story->get('lastModified'), + }, + 'getRssData: returns correct data' +); + +$story->update({headline => 'WebGUI, Web Done Right'}); + +is($story->getRssData->{title}, 'WebGUI, Web Done Right', '... headline preferred over title if present'); + +############################################################ +# +# viewTemplateVariables +# +############################################################ + +$story->update({ + highlights => "one\ntwo\nthree", + keywords => "foxtrot,tango,whiskey", +}); +is($story->get('highlights'), "one\ntwo\nthree", 'highlights set correctly for template var check'); + +$storage1->addFileFromFilesystem(WebGUI::Test->getTestCollateralPath('gooey.jpg')); +$storage2->addFileFromFilesystem(WebGUI::Test->getTestCollateralPath('lamp.jpg')); + +$story->setPhotoData([ + { + storageId => $storage1->getId, + caption => 'Mascot for a popular CMS', + byLine => 'Darcy Gibson', + alt => 'Gooey', + title => 'Mascot', + url => 'http://www.webgui.org', + }, + { + storageId => $storage2->getId, + caption => 'The Lamp', + byLine => 'Aladdin', + alt => 'Lamp', + title => '', + url => 'http://www.lamp.com', + }, +]); + + +my $viewVariables = $story->viewTemplateVariables; +#diag Dumper $viewVariables; +cmp_deeply( + $viewVariables->{highlights_loop}, + [ + { highlight => "one", }, + { highlight => "two", }, + { highlight => "three", }, + ], + 'viewTemplateVariables: highlights_loop is okay' +); + +is($viewVariables->{title}, 'Story 1', '... title is okay'); +is($viewVariables->{headline}, 'WebGUI, Web Done Right', '... headline is okay'); + +cmp_bag( + $viewVariables->{keyword_loop}, + [ + { keyword => "foxtrot", url => '/home/test-archive?func=view;keywords=foxtrot', }, + { keyword => "tango", url => '/home/test-archive?func=view;keywords=tango', }, + { keyword => "whiskey", url => '/home/test-archive?func=view;keywords=whiskey', }, + ], + 'viewTemplateVariables: keywords_loop is okay' +); + +is ($viewVariables->{updatedTimeEpoch}, $story->get('revisionDate'), 'viewTemplateVariables: updatedTimeEpoch'); + +cmp_deeply( + $viewVariables->{photo_loop}, + [ + { + imageUrl => re('gooey.jpg'), + imageCaption => 'Mascot for a popular CMS', + imageByline => 'Darcy Gibson', + imageAlt => 'Gooey', + imageTitle => 'Mascot', + imageLink => 'http://www.webgui.org', + }, + { + imageUrl => re('lamp.jpg'), + imageCaption => 'The Lamp', + imageByline => 'Aladdin', + imageAlt => 'Lamp', + imageTitle => '', + imageLink => 'http://www.lamp.com', + }, + ], + 'viewTemplateVariables: photo_loop is okay' +); + +ok(! $viewVariables->{singlePhoto}, 'viewVariables: singlePhoto: there is more than 1'); +ok( $viewVariables->{hasPhotos}, 'viewVariables: hasPhotos: it has photos'); + +##Simulate someone deleting the file stored in the storage object. +$storage2->deleteFile('lamp.jpg'); +$viewVariables = $story->viewTemplateVariables; + +cmp_deeply( + $viewVariables->{photo_loop}, + [ + { + imageUrl => re('gooey.jpg'), + imageCaption => 'Mascot for a popular CMS', + imageByline => 'Darcy Gibson', + imageAlt => 'Gooey', + imageTitle => 'Mascot', + imageLink => 'http://www.webgui.org', + }, + ], + 'viewTemplateVariables: photo_loop: if the storage has no files, it is not shown' +); + +ok($viewVariables->{singlePhoto}, 'viewVariables: singlePhoto: there is just 1'); +ok($viewVariables->{hasPhotos}, 'viewVariables: hasPhotos: it has photos'); + +############################################################ +# +# duplicatePhotoData +# +############################################################ + +$photoData = $story->getPhotoData; +$photoData->[0]->{storageId} = re('^[A-Za-z0-9_-]{22}$'); +$photoData->[1]->{storageId} = re('^[A-Za-z0-9_-]{22}$'); +my $newPhotoData = $story->duplicatePhotoData; + +cmp_deeply( + $newPhotoData, + $photoData, + 'duplicatePhotoData: checking JSON data minus storage locations' +); + +isnt($newPhotoData->[0]->{storageId}, $photoData->[0]->{storageId}, '... and storage 0 is duplicated'); +isnt($newPhotoData->[1]->{storageId}, $photoData->[1]->{storageId}, '... and storage 1 is duplicated'); + +WebGUI::Test->storagesToDelete( map { $_->{storageId} } @{ $newPhotoData } ); + +############################################################ +# +# exportAssetData +# +############################################################ + +my $exportData = $story->exportAssetData; +isa_ok($exportData, 'HASH', 'exportAssetData'); + +cmp_bag( + $exportData->{storage}, + [ + $storage1->getId, + $storage2->getId, + ], + '...asset package data has the storage locations in it' +); + +} + +END { + $story->purge if $story; + $archive->purge if $archive; + $topic->purge if $topic; + $archiveTag->rollback; + WebGUI::VersionTag->getWorking($session)->rollback; +} diff --git a/t/Asset/Wobject/StoryArchive.t b/t/Asset/Wobject/StoryArchive.t new file mode 100644 index 000000000..0c736e3e3 --- /dev/null +++ b/t/Asset/Wobject/StoryArchive.t @@ -0,0 +1,653 @@ +# vim:syntax=perl +#------------------------------------------------------------------- +# WebGUI is Copyright 2001-2009 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 +#------------------------------------------------------------------ + +# Write a little about what this script tests. +# +# + +use FindBin; +use strict; +use lib "$FindBin::Bin/../../lib"; +use Test::More; +use Test::Deep; +use File::Copy qw/mv/; +use File::Path; +use Data::Dumper; +use Path::Class; + +use WebGUI::Test; # Must use this before any other WebGUI modules +use WebGUI::Test::Maker::Permission; +use WebGUI::Session; +use WebGUI::Text; +use WebGUI::Utility; +use WebGUI::DateTime; +use DateTime; + +################################################################ +# +# setup session, users and groups for this test +# +################################################################ + +my $session = WebGUI::Test->session; + +my $staff = WebGUI::Group->new($session, 'new'); +WebGUI::Test->groupsToDelete($staff); +$staff->name('Reporting Staff'); + +my $reporter = WebGUI::User->new($session, 'new'); +$reporter->username('reporter'); +my $editor = WebGUI::User->new($session, 'new'); +$editor->username('editor'); +my $reader = WebGUI::User->new($session, 'new'); +$reader->username('reader'); +$staff->addUsers([$reporter->userId]); + +my $archive = 'placeholder for Test::Maker::Permission'; + +my $canPostMaker = WebGUI::Test::Maker::Permission->new(); +$canPostMaker->prepare({ + object => $archive, + session => $session, + method => 'canPostStories', + pass => [3, $editor, $reporter ], + fail => [1, $reader ], +}); + +my $tests = 45 + + $canPostMaker->plan + ; +plan tests => 1 + + $tests; + +#---------------------------------------------------------------------------- +# put your tests here + +my $class = 'WebGUI::Asset::Wobject::StoryArchive'; +my $loaded = use_ok($class); + +my $storage; +my $versionTag; + +my $creationDateSth = $session->db->prepare('update asset set creationDate=? where assetId=?'); + +SKIP: { + +skip "Unable to load module $class", $tests unless $loaded; + +$archive = WebGUI::Asset->getDefault($session)->addChild({className => $class, title => 'My Stories', url => '/home/mystories'}); +$versionTag = WebGUI::VersionTag->getWorking($session); +$versionTag->commit; + +isa_ok($archive, 'WebGUI::Asset::Wobject::StoryArchive', 'created StoryArchive'); + +################################################################ +# +# canPostStories +# +################################################################ + +$archive->update({ + ownerUserId => $editor->userId, + groupToPost => $staff->getId, +}); + +is($archive->get('groupToPost'), $staff->getId, 'set Staff group to post to Story Archive'); + +$canPostMaker->{_tests}->[0]->{object} = $archive; + +$canPostMaker->run(); + +################################################################ +# +# getFolder +# +################################################################ + +##Note, this is just to prevent date rollover from happening. +##We'll test implicit getFolder later on. +my $now = time(); +my $todayFolder = $archive->getFolder($now); +isa_ok($todayFolder, 'WebGUI::Asset::Wobject::Folder', 'getFolder created a Folder'); +is($archive->getChildCount, 1, 'getFolder created a child'); +my $dt = DateTime->from_epoch(epoch => $now, time_zone => $session->datetime->getTimeZone); +my $folderName = $dt->strftime('%B_%d_%Y'); +$folderName =~ s/^(\w+_)0/$1/; +is($todayFolder->getTitle, $folderName, 'getFolder: folder has the right name'); +my $folderUrl = join '/', $archive->getUrl, lc $folderName; +is($todayFolder->getUrl, $folderUrl, 'getFolder: folder has the right URL'); +is($todayFolder->getParent->getId, $archive->getId, 'getFolder: created folder has the right parent'); +is($todayFolder->get('state'), 'published', 'getFolder: created folder is published'); +is($todayFolder->get('status'), 'approved', 'getFolder: created folder is approved'); + +my $sameFolder = $archive->getFolder($now); +is($sameFolder->getId, $todayFolder->getId, 'call with same time returns the same folder'); +undef $sameFolder; + +my ($startOfDay, $endOfDay) = $session->datetime->dayStartEnd($now); +$sameFolder = $archive->getFolder($startOfDay); +is($sameFolder->getId, $todayFolder->getId, 'call within same day(start) returns the same folder'); +undef $sameFolder; +$sameFolder = $archive->getFolder($endOfDay); +is($sameFolder->getId, $todayFolder->getId, 'call within same day(end) returns the same folder'); +undef $sameFolder; +$todayFolder->purge; +is($archive->getChildCount, 0, 'leaving with an empty archive'); + +################################################################ +# +# addChild +# +################################################################ + +my $child = $archive->addChild({className => 'WebGUI::Asset::Wobject::StoryTopic'}); +is($child, undef, 'addChild: Will only add Stories'); + +$child = $archive->addChild({className => 'WebGUI::Asset::Story', title => 'First Story'}); +isa_ok($child, 'WebGUI::Asset::Story', 'addChild added and returned a Story'); +is($archive->getChildCount, 1, 'addChild: added it to the archive'); +my $folder = $archive->getFirstChild(); +isa_ok($folder, 'WebGUI::Asset::Wobject::Folder', 'Folder was added to Archive'); +is($folder->getChildCount, 1, 'The folder has 1 child...'); +is($folder->getFirstChild->getTitle, 'First Story', '... and it is the correct child'); + +################################################################ +# +# getKeywordFilename +# +################################################################ + +##Note, this method depends heavily on the default installed language pack. +##Because of that, we'll only test for whether or not url->urlize is called. + +is ($archive->getKeywordFilename('camelCase'), 'keyword_camelcase.html', 'getKeywordFilename returns a lower case keyword with _keyword.html appended'); + +################################################################ +# +# viewTemplateVariables +# +################################################################ + +my $wgBday = WebGUI::Test->webguiBirthday; +my $oldFolder = $archive->getFolder($wgBday); + +my $yesterday = $now-24*3600; +my $newFolder = $archive->getFolder($yesterday); + +my ($wgBdayMorn,undef) = $session->datetime->dayStartEnd($wgBday); +my ($yesterdayMorn,undef) = $session->datetime->dayStartEnd($yesterday); + +my $story = $oldFolder->addChild({ className => 'WebGUI::Asset::Story', title => 'WebGUI is released', keywords => 'roger,foxtrot,echo'}); +$creationDateSth->execute([$wgBday, $story->getId]); + +{ + my $storyDB = WebGUI::Asset->newByUrl($session, $story->getUrl); + is ($storyDB->get('status'), 'approved', 'addRevision always calls for an autocommit'); +} + +my $pastStory = $newFolder->addChild({ className => 'WebGUI::Asset::Story', title => "Yesterday is history" }); +$creationDateSth->execute([$yesterday, $pastStory->getId]); + +my $templateVars; +$templateVars = $archive->viewTemplateVariables(); + +cmp_deeply( + $templateVars, + superhashof({ + searchHeader => ignore(), + searchForm => ignore(), + searchButton => ignore(), + searchFooter => ignore(), + }), + 'viewTemplateVars: search variables present' +); + +cmp_deeply( + $templateVars, + superhashof({ + rssUrl => $archive->getRssFeedUrl, + atomUrl => $archive->getAtomFeedUrl, + }), + 'viewTemplateVars: RSS and Atom feed template variables' +); + +KEY: foreach my $key (keys %{ $templateVars }) { + next KEY if isIn($key, qw/canPostStories addStoryUrl date_loop mode/); + delete $templateVars->{$key}; +} + +cmp_deeply( + $templateVars, + { + canPostStories => 0, + mode => 'view', + addStoryUrl => '', + date_loop => [ + { + epochDate => ignore(), + story_loop => [ { + creationDate => ignore(), + url => re('first-story'), + title => 'First Story', + }, ], + }, + { + epochDate => $yesterdayMorn, + story_loop => [{ + creationDate => $yesterday, + url => re('yesterday-is-history'), + title => "Yesterday is history", + }, ], + }, + { + epochDate => $wgBdayMorn, + story_loop => [ { + creationDate => $wgBday, + url => '/home/mystories/august_16_2001/webgui-is-released', + title => 'WebGUI is released', + }, ], + }, + ] + }, + 'viewTemplateVariables: returns expected template variables with 3 stories in different folders' +); + +my $story2 = $folder->addChild({ className => 'WebGUI::Asset::Story', title => 'Story 2', keywords => "roger,foxtrot"}); +my $story3 = $folder->addChild({ className => 'WebGUI::Asset::Story', title => 'Story 3', keywords => "foxtrot,echo"}); +my $story4 = $folder->addChild({ className => 'WebGUI::Asset::Story', title => 'Story 4', keywords => "roger,echo"}); +foreach my $storilet ($story2, $story3, $story4) { + $session->db->write("update asset set creationDate=$now where assetId=?",[$storilet->getId]); +} +$archive->update({storiesPerPage => 3}); + +##Don't assume that Admin and Visitor have the same timezone. +$session->user({userId => 3}); +($wgBdayMorn,undef) = $session->datetime->dayStartEnd($wgBday); + +$templateVars = $archive->viewTemplateVariables(); +KEY: foreach my $key (keys %{ $templateVars }) { + next KEY if isIn($key, qw/canPostStories addStoryUrl date_loop/); + delete $templateVars->{$key}; +} + +cmp_deeply( + $templateVars, + { + canPostStories => 1, + addStoryUrl => '/home/mystories?func=add;class=WebGUI::Asset::Story', + date_loop => [ + { + epochDate => ignore(), + story_loop => [ + { + creationDate => ignore(), + url => re('first-story'), + title => 'First Story', + }, + { + creationDate => ignore(), + url => ignore(), + title => 'Story 2', + }, + { + creationDate => ignore(), + url => ignore(), + title => 'Story 3', + }, + ], + }, + ], + }, + 'viewTemplateVariables: returns expected template variables with several stories in 3 different folders' +); + +TODO: { + local $TODO = "viewTemplateVariables code to write"; + ok(0, 'Check that Stories from the future are not displayed unless the user canEdit this StoryArchive'); +} + +################################################################ +# +# viewTemplateVariables, keywords search mode +# +################################################################ + +$session->request->setup_body({ keyword => 'foxtrot' } ); +$archive->update({storiesPerPage => 25}); + +$templateVars = $archive->viewTemplateVariables('keyword'); +is($templateVars->{mode}, 'keyword', 'viewTemplateVariables mode == keyword'); +cmp_deeply( + $templateVars->{date_loop}, + [ + { + epochDate => ignore(), + story_loop => [ + { + creationDate => ignore(), + url => ignore(), + title => 'Story 2', + }, + { + creationDate => ignore(), + url => ignore(), + title => 'Story 3', + }, + ], + }, + { + epochDate => $wgBdayMorn, + story_loop => [ + { + creationDate => ignore(), + url => ignore(), + title => 'WebGUI is released', + }, + ], + }, + ], + 'viewTemplateVariables: keyword mode returns the correct assets in the same form as view mode' +); + +$archive->update({storiesPerPage => 3}); + +$session->request->setup_body({ } ); + +################################################################ +# +# viewTemplateVariables, search mode +# +################################################################ + +$session->request->setup_body({ query => 'echo' } ); +$archive->update({storiesPerPage => 25}); +$templateVars = $archive->viewTemplateVariables('search'); +is($templateVars->{mode}, 'search', 'viewTemplateVariables mode == search'); + +cmp_bag( + $templateVars->{date_loop}, + [ + { + epochDate => ignore(), + story_loop => [ + { + creationDate => ignore(), + url => ignore(), + title => 'Story 3', + }, + { + creationDate => ignore(), + url => ignore(), + title => 'Story 4', + }, + ], + }, + { + epochDate => $wgBdayMorn, + story_loop => [ + { + creationDate => ignore(), + url => ignore(), + title => 'WebGUI is released', + }, + ], + }, + ], + 'viewTemplateVariables: search mode returns the correct assets in the same form as view mode' +); + +################################################################ +# +# viewTemplateVariables, export mode +# +################################################################ + + +$session->scratch->set('isExporting', 1); +$archive->update({ storiesPerPage => 3, }); +$templateVars = $archive->viewTemplateVariables(); +ok( ( !exists $templateVars->{searchHeader} + && !exists $templateVars->{searchForm} + && !exists $templateVars->{searchButton} + && !exists $templateVars->{searchForm} + ), + '... export mode, no search variables present' +); + +cmp_deeply( + $templateVars, + superhashof({ + rssUrl => $archive->getStaticRssFeedUrl, + atomUrl => $archive->getStaticAtomFeedUrl, + }), + '... export mode, RSS and Atom feed template variables show the static url' +); + +my $storyCount = 0; +foreach my $date_loop (@{ $templateVars->{date_loop} }) { + $storyCount += scalar @{ $date_loop->{story_loop} }; +} + +cmp_ok($storyCount, '>', 3, '... export mode, pagination increased beyond storiesPerPage'); + +$session->scratch->delete('isExporting'); + +################################################################ +# +# tagCloud template variable in view +# +################################################################ + +$templateVars = $archive->viewTemplateVariables(); +my @anchors = simpleHrefParser($templateVars->{keywordCloud}); +my @expectedAnchors = (); +foreach my $keyword(qw/echo foxtrot roger/) { + push @expectedAnchors, [ $keyword, '/home/mystories?func=view;keyword='.$keyword ]; +} +cmp_bag( + \@anchors, + \@expectedAnchors, + 'keywordCloud template variable has keywords and correct links', +); + +################################################################ +# +# tagCloud template variable in view, exportMode +# +################################################################ + +$session->scratch->set('isExporting', 1); + +$templateVars = $archive->viewTemplateVariables(); +@anchors = simpleHrefParser($templateVars->{keywordCloud}); +@expectedAnchors = (); +foreach my $keyword(qw/echo foxtrot roger/) { + push @expectedAnchors, [ $keyword, '/home/mystories/keyword_'.$keyword.'.html' ]; +} +cmp_bag( + \@anchors, + \@expectedAnchors, + '... keywordCloud template variable has keywords and correct links in export mode', +); + +$session->scratch->delete('isExporting'); + +################################################################ +# +# RSS and Atom checks +# +################################################################ + +is($archive->getRssFeedUrl, '/home/mystories?func=viewRss', 'RSS Feed Url'); +is($archive->getAtomFeedUrl, '/home/mystories?func=viewAtom', 'Atom Feed Url'); + +$archive->update({itemsPerFeed => 3}); + +cmp_deeply( + $archive->getRssFeedItems(), + [ + { + title => 'First Story', + description => ignore(), + 'link' => ignore(), + date => ignore(), + author => ignore(), + }, + { + title => 'Story 2', + description => ignore(), + 'link' => ignore(), + date => ignore(), + author => ignore(), + }, + { + title => 'Story 3', + description => ignore(), + 'link' => ignore(), + date => ignore(), + author => ignore(), + }, + ], + 'rssFeedItems' +); + +################################################################ +# +# export Collateral tests +# +################################################################ + +my $exportStorage = WebGUI::Storage->create($session); +#WebGUI::Test->storagesToDelete($exportStorage); +my $basedir = Path::Class::Dir->new($exportStorage->getPath); +$exportStorage->addFileFromScalar('index', 'export story archive content'); +my $assetDir = $basedir->subdir('mystories'); +my $assetFile = $assetDir->file('index.html'); +mkpath($assetDir->stringify); +mv($exportStorage->getPath('index'), $assetFile->stringify); +$archive->exportAssetCollateral($assetFile, {}, $session); + +my $exportedFiles = $exportStorage->getFiles(); +cmp_bag( + $exportedFiles, + [qw/ + mystories.rss mystories + mystories.atom + /], + 'exportAssetCollateral: feed files exported' +); + +cmp_bag( + [ map { $_->relative($assetDir)->stringify } $assetDir->children ], + [qw/ + keyword_echo.html + keyword_roger.html + keyword_foxtrot.html + index.html + /], + 'exportAssetCollateral: keyword files exported into correct dir (below the asset)' +); + +my $roger = $exportStorage->getFileContentsAsScalar('mystories/keyword_roger.html'); +my @rogerStories = map { $_->[0] } fetchKeywordAssetList($roger); +cmp_bag( + \@rogerStories, + [ + 'Story 2', + 'Story 4', + 'WebGUI is released', + ], + '... contents of roger keyword file' +); + +my $foxtrot = $exportStorage->getFileContentsAsScalar('mystories/keyword_foxtrot.html'); +my @foxtrotStories = map { $_->[0] } fetchKeywordAssetList($foxtrot); +cmp_bag( + \@foxtrotStories, + [ + 'Story 2', + 'Story 3', + 'WebGUI is released', + ], + '... contents of foxtrot keyword file' +); + +my $echo = $exportStorage->getFileContentsAsScalar('mystories/keyword_echo.html'); +my @echoStories = map { $_->[0] } fetchKeywordAssetList($echo); +cmp_bag( + \@echoStories, + [ + 'Story 3', + 'Story 4', + 'WebGUI is released', + ], + '... contents of echo keyword file' +); + +################################################################ +# +# getKeywordStaticURL +# +################################################################ + +is($archive->getKeywordStaticURL('foo'), '/home/mystories/keyword_foo.html', 'getKeywordStaticURL: returns absolute URL to keyword file'); + +$archive->update({ url => '/home/mystories.arch' }); +is($archive->getKeywordStaticURL('bar'), '/home/mystories/keyword_bar.html', '... correct URL with file extension'); + +$archive->update({ url => '/home/mystories' }); +} + +#---------------------------------------------------------------------------- +# Cleanup +END { + if (defined $archive and ref $archive eq $class) { + $archive->purge; + } + if ($versionTag) { + $versionTag->rollback; + } + foreach my $user ($editor, $reporter, $reader) { + $user->delete if defined $user; + } + $creationDateSth->finish; +} + +sub simpleHrefParser { + my ($text) = @_; + my $p = HTML::TokeParser->new(\$text); + my @anchors = (); + while (my $token = $p->get_tag('a')) { + my $url = $token->[1]{href} || "-"; + my $label = $p->get_trimmed_text("/a"); + push @anchors, [ $label, $url ]; + } + return @anchors; +} + +sub fetchKeywordAssetList { + my ($text) = @_; + my @anchors = (); + my $p = HTML::TokeParser->new(\$text); + TOKEN: while (my $token = $p->get_tag('ul')) { + next TOKEN unless $token->[1]->{class} eq 'keywordAssetList'; + while (my $token = $p->get_tag('/ul', 'a')) { + last TOKEN if $token->[0] eq '/ul'; + my $url = $token->[1]{href} || "-"; + my $label = $p->get_trimmed_text("/a"); + push @anchors, [ $label, $url ]; + } + } + + return @anchors; +} + + diff --git a/t/Asset/Wobject/StoryTopic.t b/t/Asset/Wobject/StoryTopic.t new file mode 100644 index 000000000..bf24e09d4 --- /dev/null +++ b/t/Asset/Wobject/StoryTopic.t @@ -0,0 +1,323 @@ +# vim:syntax=perl +#------------------------------------------------------------------- +# WebGUI is Copyright 2001-2009 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 +#------------------------------------------------------------------ + +# Write a little about what this script tests. +# +# + +use FindBin; +use strict; +use lib "$FindBin::Bin/../../lib"; +use Test::More; +use Test::Deep; +use Data::Dumper; + +use WebGUI::Test; # Must use this before any other WebGUI modules +use WebGUI::Session; +use WebGUI::Text; + +#---------------------------------------------------------------------------- +# Init +my $session = WebGUI::Test->session; + +#---------------------------------------------------------------------------- +# Tests + +my $tests = 18; +plan tests => 1 + $tests; + +#---------------------------------------------------------------------------- +# put your tests here + +my $class = 'WebGUI::Asset::Wobject::StoryTopic'; +my $loaded = use_ok($class); + +my $versionTag = WebGUI::VersionTag->getWorking($session); + +my $archive = WebGUI::Asset->getDefault($session)->addChild({className => 'WebGUI::Asset::Wobject::StoryArchive', title => 'My Stories', url => '/home/mystories'}); + +my $now = time(); +my $nowFolder = $archive->getFolder($now); + +my $yesterday = $now-24*3600; +my $newFolder = $archive->getFolder($yesterday); + +my $creationDateSth = $session->db->prepare('update asset set creationDate=? where assetId=?'); + +my $pastStory = $newFolder->addChild({ className => 'WebGUI::Asset::Story', title => "Yesterday is history", keywords => 'andy,norton'}); +$creationDateSth->execute([$yesterday, $pastStory->getId]); + +my @staff = qw/norton hadley mert trout/; +my @inmates = qw/bogs red brooks andy heywood tommy jake skeet/; +my @characters = (@staff, @inmates, ); + +my @stories = (); +my $storyHandler = {}; + +STORY: foreach my $name (@characters) { + my $namedStory = $nowFolder->addChild({ className => 'WebGUI::Asset::Story', title => $name, keywords => $name, } ); + $storyHandler->{$name} = $namedStory; + $creationDateSth->execute([$now, $namedStory->getId]); +} + +$storyHandler->{bogs}->update({subtitle => 'drinking his food through a straw'}); + +my $topic; + +SKIP: { + + skip "Unable to load module $class", $tests unless $loaded; + +$topic = WebGUI::Asset->getDefault($session)->addChild({ className => 'WebGUI::Asset::Wobject::StoryTopic', title => 'Popular inmates in Shawshank Prison', keywords => join(',', @inmates)}); + +isa_ok($topic, 'WebGUI::Asset::Wobject::StoryTopic', 'made a Story Topic'); +$topic->update({ + storiesPer => 6, + storiesShort => 3, +}); + +$versionTag->commit; + +################################################################ +# +# viewTemplateVariables +# +################################################################ + +my $templateVars; +$templateVars = $topic->viewTemplateVariables(); + +cmp_deeply( + $templateVars, + superhashof({ + rssUrl => $topic->getRssFeedUrl, + atomUrl => $topic->getAtomFeedUrl, + }), + 'viewTemplateVars: RSS and Atom feed template variables' +); +cmp_deeply( + $templateVars->{story_loop}, + [ + { + title => 'bogs', + url => $session->url->append($topic->getUrl, 'func=viewStory;assetId='.$storyHandler->{'bogs'}->getId), + creationDate => $now, + }, + { + title => 'red', + url => $session->url->append($topic->getUrl, 'func=viewStory;assetId='.$storyHandler->{'red'}->getId), + creationDate => $now, + }, + { + title => 'brooks', + url => $session->url->append($topic->getUrl, 'func=viewStory;assetId='.$storyHandler->{'brooks'}->getId), + creationDate => $now, + }, + ], + 'viewTemplateVars has right number and contents in the story_loop' +); + +ok( + ! exists $templateVars->{topStoryTitle} + && ! exists $templateVars->{topStoryUrl} + && ! exists $templateVars->{topStoryCreationDate} + && ! exists $templateVars->{topStorySubtitle}, + 'topStory variables not present unless in standalone mode' +); +ok(! $templateVars->{standAlone}, 'viewTemplateVars: not in standalone mode'); + +$topic->{_standAlone} = 1; +$templateVars = $topic->viewTemplateVariables(); +cmp_deeply( + $templateVars->{story_loop}, + [ + { + title => 'red', + url => $session->url->append($topic->getUrl, 'func=viewStory;assetId='.$storyHandler->{'red'}->getId), + creationDate => $now, + }, + { + title => 'brooks', + url => $session->url->append($topic->getUrl, 'func=viewStory;assetId='.$storyHandler->{'brooks'}->getId), + creationDate => $now, + }, + { + title => 'andy', + url => $session->url->append($topic->getUrl, 'func=viewStory;assetId='.$storyHandler->{'andy'}->getId), + creationDate => $now, + }, + { + title => 'heywood', + url => $session->url->append($topic->getUrl, 'func=viewStory;assetId='.$storyHandler->{'heywood'}->getId), + creationDate => $now, + }, + { + title => 'tommy', + url => $session->url->append($topic->getUrl, 'func=viewStory;assetId='.$storyHandler->{'tommy'}->getId), + creationDate => $now, + }, + ], + 'viewTemplateVars has right number and contents in the story_loop in standalone mode. Top story not present in story_loop' +); + +is($templateVars->{topStoryTitle}, 'bogs', '... topStoryTitle'); +is( + $templateVars->{topStorySubtitle}, + 'drinking his food through a straw', + '... topStorySubtitle' +); +is( + $templateVars->{topStoryUrl}, + $session->url->append($topic->getUrl, 'func=viewStory;assetId='.$storyHandler->{'bogs'}->getId), + '... topStoryUrl' +); +is($templateVars->{topStoryCreationDate}, $now, '... topStoryCreationDate'); +ok($templateVars->{standAlone}, '... standAlone mode=1'); + +my $storage = WebGUI::Storage->create($session); +WebGUI::Test->storagesToDelete($storage); +$storyHandler->{bogs}->setPhotoData([{ + caption => "Octopus seen at the scene of Mrs. Dufresne's murder.", + byLine => 'Elmo Blatch', + alt => 'The suspect', +}]); + +$templateVars = $topic->viewTemplateVariables(); +ok( + ! exists $templateVars->{topStoryImageUrl} + && ! exists $templateVars->{topStoryImageByLine} + && ! exists $templateVars->{topStoryImageAlt} + && ! exists $templateVars->{topStoryImageCaption}, + '... no photo template variables, since there is no storage location' +); +my $bogsData = $storyHandler->{bogs}->getPhotoData(); +$bogsData->[0]->{storageId} = $storage->getId; +$storyHandler->{bogs}->setPhotoData($bogsData); +$templateVars = $topic->viewTemplateVariables(); +ok( + ! exists $templateVars->{topStoryImageUrl} + && ! exists $templateVars->{topStoryImageByLine} + && ! exists $templateVars->{topStoryImageAlt} + && ! exists $templateVars->{topStoryImageCaption}, + '... no photo template variables, since there is no file in the storage location' +); + +$storage->addFileFromFilesystem(WebGUI::Test->getTestCollateralPath('gooey.jpg')); +$templateVars = $topic->viewTemplateVariables(); +cmp_deeply( + [ @{ $templateVars }{qw/topStoryImageUrl topStoryImageByline topStoryImageAlt topStoryImageCaption/} ], + [ + $storage->getUrl('gooey.jpg'), + 'Elmo Blatch', + 'The suspect', + "Octopus seen at the scene of Mrs. Dufresne's murder.", + ], + '... photo template variables set' +); + +$topic->update({ + storiesShort => 20, +}); + +$topic->{_standAlone} = 0; + +$templateVars = $topic->viewTemplateVariables; +my @topicInmates = map { $_->{title} } @{ $templateVars->{story_loop} }; +cmp_deeply( + \@topicInmates, + [@inmates, 'Yesterday is history'], #extra for pastStory + 'viewTemplateVariables: is only finding things with its keywords' +); + +$session->scratch->set('isExporting', 1); +$topic->update({ + storiesShort => 3, +}); +$templateVars = $topic->viewTemplateVariables; +cmp_deeply( + $templateVars->{story_loop}, + [ + { + title => 'bogs', + url => $storyHandler->{'bogs'}->getUrl, + creationDate => $now, + }, + { + title => 'red', + url => $storyHandler->{'red'}->getUrl, + creationDate => $now, + }, + { + title => 'brooks', + url => $storyHandler->{'brooks'}->getUrl, + creationDate => $now, + }, + ], + '... export mode, URLs are the regular story URLs' +); +cmp_deeply( + $templateVars, + superhashof({ + rssUrl => $topic->getStaticRssFeedUrl, + atomUrl => $topic->getStaticAtomFeedUrl, + }), + '... export mode, RSS and Atom feed template variables show the static url' +); +$session->scratch->delete('isExporting'); + +################################################################ +# +# getRssFeedItems +# +################################################################ + +$topic->update({ + storiesPer => 3, +}); +cmp_deeply( + $topic->getRssFeedItems(), + [ + { + title => 'bogs', + description => ignore(), + 'link' => ignore(), + date => ignore(), + author => ignore(), + }, + { + title => 'red', + description => ignore(), + 'link' => ignore(), + date => ignore(), + author => ignore(), + }, + { + title => 'brooks', + description => ignore(), + 'link' => ignore(), + date => ignore(), + author => ignore(), + }, + ], + 'rssFeedItems' +); + +} + +#---------------------------------------------------------------------------- +# Cleanup +END { + $archive->purge if $archive; + $topic->purge if $topic; + if ($versionTag) { + $versionTag->rollback; + } +} diff --git a/t/Workflow/Activity/ArchiveOldStories.t b/t/Workflow/Activity/ArchiveOldStories.t new file mode 100644 index 000000000..4dcb77a81 --- /dev/null +++ b/t/Workflow/Activity/ArchiveOldStories.t @@ -0,0 +1,127 @@ +#------------------------------------------------------------------- +# WebGUI is Copyright 2001-2009 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 +#------------------------------------------------------------------- + +use FindBin; +use strict; +use lib "$FindBin::Bin/../../lib"; + +use WebGUI::Test; +use WebGUI::Asset; +use WebGUI::Asset::Story; +use WebGUI::Asset::Wobject::StoryArchive; +use WebGUI::Workflow::Activity::ArchiveOldStories; + +use Data::Dumper; +use Test::More; +use Test::Deep; + +plan tests => 6; # increment this value for each test you create + +my $session = WebGUI::Test->session; +$session->user({userId => 3}); + +my $home = WebGUI::Asset->getDefault($session); +my $wgBday = WebGUI::Test->webguiBirthday; + +my $creationDateSth = $session->db->prepare('update asset set creationDate=? where assetId=?'); + +my $archive1 = $home->addChild({ + className => 'WebGUI::Asset::Wobject::StoryArchive', + title => '2001 Stories', + archiveAfter => 50*365*24*3600, ##50 years ago +}); + +my $birthdayFolder = $archive1->getFolder($wgBday); +$creationDateSth->execute([$wgBday, $birthdayFolder->getId]); + +my @oldStories = (); +push @oldStories, $birthdayFolder->addChild({ className => 'WebGUI::Asset::Story',}); +push @oldStories, $birthdayFolder->addChild({ className => 'WebGUI::Asset::Story',}); +foreach my $story (@oldStories) { + $creationDateSth->execute([$wgBday, $story->getId]); +} + +my $archive2 = $home->addChild({ + className => 'WebGUI::Asset::Wobject::StoryArchive', + title => 'Stories from last week', + archiveAfter => 10*24*3600, #10 days ago +}); + +my $weekAgo = time() - (7*24*3600); +my $weekFolder = $archive2->getFolder($weekAgo); +my $weekStory = $weekFolder->addChild({className => 'WebGUI::Asset::Story',}); +$creationDateSth->execute([$weekAgo, $weekFolder->getId]); +$creationDateSth->execute([$weekAgo, $weekStory->getId]); + +my $versionTag = WebGUI::VersionTag->getWorking($session); +$versionTag->commit; + +my $workflow = WebGUI::Workflow->create($session, + { + enabled => 1, + objectType => 'None', + mode => 'realtime', + }, +); +my $activity = $workflow->addActivity('WebGUI::Workflow::Activity::ArchiveOldStories'); + +my $instance1 = WebGUI::Workflow::Instance->create($session, + { + workflowId => $workflow->getId, + skipSpectreNotification => 1, + } +); + +my $retVal; + +$retVal = $instance1->run(); +is($retVal, 'complete', 'First workflow was run'); +$retVal = $instance1->run(); +is($retVal, 'done', 'Workflow is done'); + +my $archivedAssets = $home->getLineage( + ['descendants'], + { + includeOnlyClasses => ['WebGUI::Asset::Story', 'WebGUI::Asset::Wobject::Folder', 'WebGUI::Asset::Wobject::StoryArchive'], + statusToInclude => ['archived'], + }, +); + +cmp_bag( $archivedAssets, [ ], 'Nothing archived.'); + +$archive2->update({ archiveAfter => 5*24*3600, }); + +my $instance2 = WebGUI::Workflow::Instance->create($session, + { + workflowId => $workflow->getId, + skipSpectreNotification => 1, + } +); +$retVal = $instance2->run(); +is($retVal, 'complete', 'Second workflow was run'); +$retVal = $instance2->run(); +is($retVal, 'done', 'Workflow is done'); + +$archivedAssets = $home->getLineage( + ['descendants'], + { + includeOnlyClasses => ['WebGUI::Asset::Story', 'WebGUI::Asset::Wobject::Folder', 'WebGUI::Asset::Wobject::StoryArchive'], + statusToInclude => ['archived'], + }, +); + +cmp_bag( $archivedAssets, [ $weekStory->getId, $weekFolder->getId ], 'Nothing archived.'); + +END { + $creationDateSth->finish; + $versionTag->rollback; + $workflow->delete; +} + diff --git a/t/i18n/template.t b/t/i18n/template.t index fe0a838b8..efb3b9208 100644 --- a/t/i18n/template.t +++ b/t/i18n/template.t @@ -90,4 +90,3 @@ END { defined $session && $session->config->set('macros', $originalMacros); } - diff --git a/t/lib/WebGUI/Test.pm b/t/lib/WebGUI/Test.pm index 1136ed2e3..c69d95093 100644 --- a/t/lib/WebGUI/Test.pm +++ b/t/lib/WebGUI/Test.pm @@ -370,6 +370,20 @@ sub session { return $SESSION; } +#---------------------------------------------------------------------------- + +=head2 webguiBirthday ( ) + +This constant is used in several tests, so it's reproduced here so it can +be found easy. This is the epoch date when WebGUI was released. + +=cut + +sub webguiBirthday { + return 997966800 ; +} + + #----------------------------------------------------------------------------