Add RSSCapable mixin for RSS-capable assets, and RSSFromParent asset for
the feeds for those assets. Convert Collaboration to use this mechanism rather than its existing viewRSS, because this is more export-friendly.
This commit is contained in:
parent
61cdd872ac
commit
9a61dd2f38
7 changed files with 465 additions and 99 deletions
174
lib/WebGUI/Asset/RSSCapable.pm
Normal file
174
lib/WebGUI/Asset/RSSCapable.pm
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
package WebGUI::Asset::RSSCapable;
|
||||
|
||||
=head1 LEGAL
|
||||
|
||||
-------------------------------------------------------------------
|
||||
WebGUI is Copyright 2001-2006 Plain Black Corporation.
|
||||
-------------------------------------------------------------------
|
||||
Please read the legal notices (docs/legal.txt) and the license
|
||||
(docs/license.txt) that came with this distribution before using
|
||||
this software.
|
||||
-------------------------------------------------------------------
|
||||
http://www.plainblack.com info@plainblack.com
|
||||
-------------------------------------------------------------------
|
||||
|
||||
=cut
|
||||
|
||||
use strict;
|
||||
use NEXT;
|
||||
use WebGUI::Asset::RSSFromParent;
|
||||
|
||||
=head1 NAME
|
||||
|
||||
WebGUI::Asset::RSSCapable
|
||||
|
||||
=head1 DESCRIPTION
|
||||
|
||||
An extra mixin class to be included before WebGUI::Asset in any asset
|
||||
class that wishes its instances to be capable of generating RSS feeds
|
||||
using the RSSFromParent asset.
|
||||
|
||||
=head1 SYNOPSIS
|
||||
|
||||
use base 'WebGUI::Asset::RSSCapable';
|
||||
|
||||
=cut
|
||||
|
||||
sub definition {
|
||||
my $class = shift;
|
||||
my $session = shift;
|
||||
my $definition = shift;
|
||||
my %properties;
|
||||
tie %properties, 'Tie::IxHash';
|
||||
my $i18n = WebGUI::International->new($session, 'Asset_RSSCapable');
|
||||
|
||||
# We do this prefixing to avoid name collisions because properties aren't namespaced.
|
||||
%properties =
|
||||
(
|
||||
rssCapableRssEnabled => { tab => 'display',
|
||||
fieldType => 'yesNo',
|
||||
defaultValue => 1,
|
||||
label => $i18n->get('rssEnabled label'),
|
||||
hoverHelp => $i18n->get('rssEnabled hoverHelp')
|
||||
},
|
||||
rssCapableRssTemplateId => { tab => 'display',
|
||||
fieldType => 'template',
|
||||
defaultValue => 'PBtmpl0000000000000142',
|
||||
namespace => 'RSSCapable/RSS',
|
||||
label => $i18n->get('rssTemplateId label'),
|
||||
hoverHelp => $i18n->get('rssTemplateId hoverHelp')
|
||||
},
|
||||
rssCapableRssFromParentId => { fieldType => 'hidden',
|
||||
noFormPost => 1,
|
||||
defaultValue => undef,
|
||||
},
|
||||
);
|
||||
|
||||
push @$definition, { assetName => $i18n->get('assetName'),
|
||||
tableName => 'RSSCapable',
|
||||
autoGenerateForms => 1,
|
||||
className => 'WebGUI::Asset::RSSCapable',
|
||||
icon => 'rssCapable.gif',
|
||||
properties => \%properties
|
||||
};
|
||||
return $class->NEXT::definition($session, $definition);
|
||||
}
|
||||
|
||||
sub _rssFromParentValid {
|
||||
my $self = shift;
|
||||
my $rssFromParentId = $self->get('rssCapableRssFromParentId');
|
||||
return 0 unless $rssFromParentId;
|
||||
|
||||
my $rssFromParent = WebGUI::Asset->newByDynamicClass($self->session, $rssFromParentId);
|
||||
return ($rssFromParent->isa('WebGUI::Asset::RSSFromParent')
|
||||
&& $rssFromParent->getParent->getId eq $self->getId);
|
||||
}
|
||||
|
||||
sub _updateRssFromParentProperties {
|
||||
my $self = shift;
|
||||
my $rssFromParent = WebGUI::Asset->newByDynamicClass($self->session,
|
||||
$self->get('rssCapableRssFromParentId'));
|
||||
$rssFromParent->update({ title => $self->get('title'),
|
||||
menuTitle => $self->get('menuTitle') });
|
||||
}
|
||||
|
||||
sub _purgeExtraRssFromParentAssets {
|
||||
my $self = shift;
|
||||
my $rssFromParentId = $self->get('rssCapableRssFromParentId');
|
||||
|
||||
foreach my $rssFromParent (@{$self->getLineage(['children'],
|
||||
{returnObjects => 1,
|
||||
includeOnlyClasses =>
|
||||
['WebGUI::Asset::RSSFromParent']})}) {
|
||||
$rssFromParent->purge unless $rssFromParent->getId eq $rssFromParentId;
|
||||
}
|
||||
}
|
||||
|
||||
sub _ensureRssFromParentPresent {
|
||||
my $self = shift;
|
||||
if (!$self->_rssFromParentValid) {
|
||||
# Create a new one.
|
||||
my $rssFromParent = $self->addChild({ className => 'WebGUI::Asset::RSSFromParent',
|
||||
title => $self->get('title'),
|
||||
menuTitle => $self->get('menuTitle'),
|
||||
url => $self->get('url').'.rss'
|
||||
});
|
||||
$self->update({ rssCapableRssFromParentId => $rssFromParent->getId });
|
||||
}
|
||||
|
||||
$self->_updateRssFromParentProperties;
|
||||
$self->_purgeExtraRssFromParentAssets;
|
||||
}
|
||||
|
||||
sub _ensureRssFromParentAbsent {
|
||||
my $self = shift;
|
||||
# Invalidate it, and then it'll get purged along with any others.
|
||||
$self->update({ rssCapableRssFromParentId => undef });
|
||||
$self->_purgeExtraRssFromParentAssets;
|
||||
}
|
||||
|
||||
sub processPropertiesFromFormPost {
|
||||
my $self = shift;
|
||||
my $error = $self->NEXT::processPropertiesFromFormPost(@_);
|
||||
return $error if ref $error eq 'ARRAY';
|
||||
|
||||
if ($self->get('rssCapableRssEnabled')) {
|
||||
$self->_ensureRssFromParentPresent;
|
||||
} else {
|
||||
$self->_ensureRssFromParentAbsent;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
#-------------------------------------------------------------------
|
||||
|
||||
=head2 getRssUrl ( )
|
||||
|
||||
Returns the site-relative URL to the RSS feed for this asset, or undef
|
||||
if there is no such feed.
|
||||
|
||||
=cut
|
||||
|
||||
sub getRssUrl {
|
||||
my $self = shift;
|
||||
my $rssFromParentId = $self->get('rssCapableRssFromParentId');
|
||||
return undef unless defined $rssFromParentId;
|
||||
WebGUI::Asset->newByDynamicClass($self->session, $rssFromParentId)->getUrl;
|
||||
}
|
||||
|
||||
#-------------------------------------------------------------------
|
||||
|
||||
=head2 getRssItems ( )
|
||||
|
||||
Returns a list of RSS items for a feed corresponding to this asset.
|
||||
Each item may be another asset, or a hash of (properly XMLized)
|
||||
properties for the <item>..</item> tag. Defaults to no items.
|
||||
|
||||
This is the primary method that RSSCapable assets should override.
|
||||
|
||||
=cut
|
||||
|
||||
sub getRssItems { () }
|
||||
|
||||
1;
|
||||
123
lib/WebGUI/Asset/RSSFromParent.pm
Normal file
123
lib/WebGUI/Asset/RSSFromParent.pm
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
package WebGUI::Asset::RSSFromParent;
|
||||
|
||||
=head1 LEGAL
|
||||
|
||||
-------------------------------------------------------------------
|
||||
WebGUI is Copyright 2001-2006 Plain Black Corporation.
|
||||
-------------------------------------------------------------------
|
||||
Please read the legal notices (docs/legal.txt) and the license
|
||||
(docs/license.txt) that came with this distribution before using
|
||||
this software.
|
||||
-------------------------------------------------------------------
|
||||
http://www.plainblack.com info@plainblack.com
|
||||
-------------------------------------------------------------------
|
||||
|
||||
=cut
|
||||
|
||||
use strict;
|
||||
use Tie::IxHash;
|
||||
use base 'WebGUI::Asset';
|
||||
use WebGUI::Utility;
|
||||
|
||||
=head1 NAME
|
||||
|
||||
Package WebGUI::Asset::RSSFromParent
|
||||
|
||||
=head1 DESCRIPTION
|
||||
|
||||
Generates an RSS feed from the children/descendants of its parent.
|
||||
|
||||
=head1 SYNOPSIS
|
||||
|
||||
use WebGUI::Asset::RSSFromParent;
|
||||
|
||||
=cut
|
||||
|
||||
#-------------------------------------------------------------------
|
||||
sub definition {
|
||||
my $class = shift;
|
||||
my $session = shift;
|
||||
my $definition = shift;
|
||||
my %properties;
|
||||
tie %properties, 'Tie::IxHash';
|
||||
my $i18n = WebGUI::International->new($session, "Asset_RSSFromParent");
|
||||
|
||||
%properties = ();
|
||||
|
||||
push(@{$definition}, {
|
||||
assetName=>$i18n->get('assetName'),
|
||||
icon=>'rssFromParent.gif',
|
||||
autoGenerateForms=>1,
|
||||
tableName=>'RSSFromParent',
|
||||
className=>'WebGUI::Asset::RSSFromParent',
|
||||
properties=>\%properties
|
||||
});
|
||||
|
||||
return $class->SUPER::definition($session, $definition);
|
||||
}
|
||||
|
||||
#-------------------------------------------------------------------
|
||||
sub _escapeXml {
|
||||
my $text = shift;
|
||||
my %entities = ('<' => '<', '>' => '>', '"' => '"', "'" => "'");
|
||||
$text =~ s/([<>\"\'])/$entities{$1}/g;
|
||||
return $text;
|
||||
}
|
||||
|
||||
sub _tlsOfAsset {
|
||||
my $self = shift;
|
||||
my $asset = shift;
|
||||
return (_escapeXml($asset->get('title')),
|
||||
_escapeXml($self->session->url->getSiteURL() . $asset->getUrl),
|
||||
_escapeXml($asset->get('synopsis')));
|
||||
}
|
||||
|
||||
sub isValidRssItem { 0 }
|
||||
sub displayInFolder2 { 0 }
|
||||
|
||||
sub www_view {
|
||||
my $self = shift;
|
||||
return '' unless $self->session->asset->getId eq $self->getId;
|
||||
return '' unless $self->getParent->isa('WebGUI::Asset::RSSCapable');
|
||||
my $parent = $self->getParent;
|
||||
|
||||
my $template = WebGUI::Asset::Template->new($self->session, $parent->get('rssCapableRssTemplateId'));
|
||||
$template->prepare;
|
||||
$self->session->http->setMimeType('text/xml');
|
||||
|
||||
my $var = {};
|
||||
@$var{'title', 'link', 'description'} = $self->_tlsOfAsset($parent);
|
||||
$var->{'generator'} = "WebGUI $WebGUI::VERSION";
|
||||
$var->{'lastBuildDate'} = $self->session->datetime->epochToMail($parent->getContentLastModified);
|
||||
$var->{'webMaster'} = $self->session->setting->get('companyEmail');
|
||||
$var->{'docs'} = 'http://blogs.law.harvard.edu/tech/rss';
|
||||
|
||||
my @items = $parent->getRssItems;
|
||||
$var->{'item_loop'} = [];
|
||||
|
||||
foreach my $item (@items) {
|
||||
my $subvar = {};
|
||||
|
||||
if (UNIVERSAL::isa($item, 'WebGUI::Asset')) {
|
||||
next unless $item->isValidRssItem;
|
||||
$subvar = {};
|
||||
@$subvar{'title', 'link', 'description'} = $self->_tlsOfAsset($item);
|
||||
$subvar->{guid} = $subvar->{link};
|
||||
$subvar->{pubDate} = _escapeXml($self->session->datetime->epochToMail($item->get('dateUpdated')));
|
||||
} elsif (ref $item eq 'HASH') {
|
||||
foreach my $key (keys %$item) {
|
||||
my $value = $item->{$key};
|
||||
$subvar->{$key} = (ref $value eq 'ARRAY')? join($,, @$value) : _escapeXml($value);
|
||||
}
|
||||
} else {
|
||||
$self->session->errorHandler->error("Don't know what to do with this RSS item: $item");
|
||||
next;
|
||||
}
|
||||
|
||||
push @{$var->{'item_loop'}}, $subvar;
|
||||
}
|
||||
|
||||
return $self->processTemplate($var, undef, $template);
|
||||
}
|
||||
|
||||
1;
|
||||
|
|
@ -20,16 +20,17 @@ use WebGUI::Paginator;
|
|||
use WebGUI::Utility;
|
||||
use WebGUI::Asset::Wobject;
|
||||
use WebGUI::Workflow::Cron;
|
||||
|
||||
our @ISA = qw(WebGUI::Asset::Wobject);
|
||||
|
||||
use WebGUI::Asset::RSSCapable;
|
||||
use base 'WebGUI::Asset::RSSCapable';
|
||||
use base 'WebGUI::Asset::Wobject';
|
||||
|
||||
#-------------------------------------------------------------------
|
||||
sub addChild {
|
||||
my $self = shift;
|
||||
my $properties = shift;
|
||||
my @other = @_;
|
||||
if ($properties->{className} ne "WebGUI::Asset::Post::Thread") {
|
||||
if ($properties->{className} ne "WebGUI::Asset::Post::Thread"
|
||||
and $properties->{className} ne 'WebGUI::Asset::RSSFromParent') {
|
||||
$self->session->errorHandler->security("add a ".$properties->{className}." to a ".$self->get("className"));
|
||||
return undef;
|
||||
}
|
||||
|
|
@ -544,14 +545,6 @@ sub definition {
|
|||
label=>$i18n->get('sort by'),
|
||||
hoverHelp=>$i18n->get('sort by description'),
|
||||
},
|
||||
rssTemplateId =>{
|
||||
fieldType=>"template",
|
||||
namespace=>"Collaboration/RSS",
|
||||
defaultValue=>'PBtmpl0000000000000142',
|
||||
tab=>'display',
|
||||
label=>$i18n->get('rss template'),
|
||||
hoverHelp=>$i18n->get('rss template description'),
|
||||
},
|
||||
notificationTemplateId =>{
|
||||
fieldType=>"template",
|
||||
namespace=>"Collaboration/Notification",
|
||||
|
|
@ -659,6 +652,50 @@ sub duplicate {
|
|||
return $newAsset;
|
||||
}
|
||||
|
||||
#-------------------------------------------------------------------
|
||||
sub getRssItems {
|
||||
my $self = shift;
|
||||
|
||||
# XXX copied and reformatted this query from www_viewRSS, but why is it constructed like this?
|
||||
# And it's duplicated inside view, too! Eeeagh! And it uses the versionTag scratch var...
|
||||
my ($sortBy, $sortOrder) = ($self->getValue('sortBy'), $self->getValue('sortOrder'));
|
||||
my @postIds = $self->session->db->buildArray(<<"SQL", [$self->getId, $self->session->scratch->get('versionTag')]);
|
||||
SELECT asset.assetId
|
||||
FROM Thread
|
||||
LEFT JOIN asset ON Thread.assetId = asset.assetId
|
||||
LEFT JOIN Post ON Post.assetId = Thread.assetId AND Post.revisionDate = Thread.revisionDate
|
||||
LEFT JOIN assetData ON assetData.assetId = Thread.assetId
|
||||
AND assetData.revisionDate = Thread.revisionDate
|
||||
WHERE asset.parentId = ? AND asset.state = 'published' AND asset.className = 'WebGUI::Asset::Post::Thread'
|
||||
AND (assetData.status = 'approved' OR assetData.tagId = ?)
|
||||
GROUP BY assetData.assetId
|
||||
ORDER BY $sortBy $sortOrder
|
||||
SQL
|
||||
|
||||
return map {
|
||||
my $postId = $_;
|
||||
my $post = WebGUI::Asset->newByDynamicClass($self->session, $postId);
|
||||
my $postUrl = $self->session->url->getSiteURL() . $post->getUrl;
|
||||
# Buggo: this is an abuse of 'author'. 'author' is supposed to be an email address.
|
||||
# But this is how it was in the original Collaboration RSS, so.
|
||||
({ author => $post->get('username'),
|
||||
title => $post->get('title'),
|
||||
link => $postUrl, guid => $postUrl,
|
||||
description => $post->get('synopsis'),
|
||||
pubDate => $self->session->datetime->epochToMail($post->get('dateUpdated')),
|
||||
attachmentLoop => do {
|
||||
if ($post->get('storageId')) {
|
||||
my $storage = $post->getStorageLocation;
|
||||
[map {({ 'attachment.url' => $storage->getUrl($_),
|
||||
'attachment.path' => $storage->getPath($_),
|
||||
'attachment.length' => $storage->getFileSize($_) })}
|
||||
@{$storage->getFiles}]
|
||||
} else { undef }
|
||||
}
|
||||
})
|
||||
} @postIds;
|
||||
}
|
||||
|
||||
#-------------------------------------------------------------------
|
||||
sub getEditTabs {
|
||||
my $self = shift;
|
||||
|
|
@ -682,19 +719,6 @@ sub getNewThreadUrl {
|
|||
|
||||
#-------------------------------------------------------------------
|
||||
|
||||
=head2 getRssUrl ( )
|
||||
|
||||
Formats the url to start a new thread.
|
||||
|
||||
=cut
|
||||
|
||||
sub getRssUrl {
|
||||
my $self = shift;
|
||||
$self->getUrl("func=viewRSS");
|
||||
}
|
||||
|
||||
#-------------------------------------------------------------------
|
||||
|
||||
=head2 getSearchUrl ( )
|
||||
|
||||
Formats the url to the forum search engine.
|
||||
|
|
@ -751,6 +775,17 @@ sub getUnsubscribeUrl {
|
|||
}
|
||||
|
||||
|
||||
#-------------------------------------------------------------------
|
||||
sub _computeThreadCount {
|
||||
my $self = shift;
|
||||
return scalar @{$self->getLineage(['children'], {includeOnlyClasses => ['WebGUI::Asset::Post::Thread']})};
|
||||
}
|
||||
|
||||
sub _computePostCount {
|
||||
my $self = shift;
|
||||
return scalar @{$self->getLineage(['descendants'], {includeOnlyClasses => ['WebGUI::Asset::Post']})};
|
||||
}
|
||||
|
||||
#-------------------------------------------------------------------
|
||||
|
||||
=head2 incrementReplies ( lastPostDate, lastPostId )
|
||||
|
|
@ -769,8 +804,8 @@ The unique identifier of the post being added.
|
|||
|
||||
sub incrementReplies {
|
||||
my ($self, $lastPostDate, $lastPostId) = @_;
|
||||
my $threads = $self->getChildCount;
|
||||
my $replies = $self->getDescendantCount - $threads;
|
||||
my $threads = $self->_computeThreadCount;
|
||||
my $replies = $self->_computePostCount;
|
||||
$self->update({replies=>$replies, threads=>$threads, lastPostId=>$lastPostId, lastPostDate=>$lastPostDate});
|
||||
}
|
||||
|
||||
|
|
@ -792,7 +827,7 @@ The unique identifier of the post that was just added.
|
|||
|
||||
sub incrementThreads {
|
||||
my ($self, $lastPostDate, $lastPostId) = @_;
|
||||
$self->update({threads=>$self->getChildCount, lastPostId=>$lastPostId, lastPostDate=>$lastPostDate});
|
||||
$self->update({threads=>$self->_computeThreadCount, lastPostId=>$lastPostId, lastPostDate=>$lastPostDate});
|
||||
}
|
||||
|
||||
#-------------------------------------------------------------------
|
||||
|
|
@ -938,8 +973,8 @@ Calculates the number of replies to this collaboration system and updates the co
|
|||
|
||||
sub sumReplies {
|
||||
my $self = shift;
|
||||
my $threads = $self->getChildCount;
|
||||
my $replies = $self->getDescendantCount - $threads;
|
||||
my $threads = $self->_computeThreadCount;
|
||||
my $replies = $self->_computePostCount;
|
||||
$self->update({replies=>$replies, threads=>$threads});
|
||||
}
|
||||
|
||||
|
|
@ -953,7 +988,7 @@ Calculates the number of threads in this collaboration system and updates the co
|
|||
|
||||
sub sumThreads {
|
||||
my $self = shift;
|
||||
$self->update({threads=>$self->getChildCount});
|
||||
$self->update({threads=>$self->_computeThreadCount});
|
||||
}
|
||||
|
||||
#-------------------------------------------------------------------
|
||||
|
|
@ -1165,73 +1200,5 @@ sub www_view {
|
|||
return $self->SUPER::www_view(@_);
|
||||
}
|
||||
|
||||
|
||||
#-------------------------------------------------------------------
|
||||
# print out RSS 2.0 feed describing the items visible on the first page
|
||||
sub www_viewRSS {
|
||||
my $self = shift;
|
||||
return $self->session->privilege->noAccess() unless $self->canView;
|
||||
my %var;
|
||||
|
||||
$self->logView() if ($self->session->setting->get("passiveProfilingEnabled"));
|
||||
# Set the required channel variables
|
||||
$var{'title'} = _xml_encode($self->get("title"));
|
||||
$var{'link'} = _xml_encode($self->session->url->getSiteURL().$self->getUrl);
|
||||
$self->session->errorHandler->warn( $var{'link'} );
|
||||
$var{'description'} = _xml_encode($self->get("description"));
|
||||
# Set some of the optional channel variables
|
||||
$var{'generator'} = "WebGUI ".$WebGUI::VERSION;
|
||||
$var{'lastBuildDate'} = _xml_encode($self->_get_rfc822_date($self->get("dateUpdated")));
|
||||
$var{'webMaster'} = $self->session->setting->get("companyEmail");
|
||||
$var{'docs'} = "http://blogs.law.harvard.edu/tech/rss";
|
||||
|
||||
my $sth = $self->session->db->read("select asset.assetId, asset.className, max(assetData.revisionDate)
|
||||
from Thread
|
||||
left join asset on Thread.assetId=asset.assetId
|
||||
left join Post on Post.assetId=Thread.assetId and Thread.revisionDate=Post.revisionDate
|
||||
left join assetData on assetData.assetId=Thread.assetId and Thread.revisionDate=assetData.revisionDate
|
||||
where asset.parentId=".$self->session->db->quote($self->getId)." and asset.state='published'
|
||||
and asset.className='WebGUI::Asset::Post::Thread'
|
||||
and (assetData.status='approved'
|
||||
or assetData.tagId=".$self->session->db->quote($self->session->scratch->get("versionTag")).")
|
||||
group by assetData.assetId
|
||||
order by ".$self->getValue("sortBy")." ".$self->getValue("sortOrder"));
|
||||
my $i = 1;
|
||||
while (my ($id, $class, $version) = $sth->array) {
|
||||
my $post = WebGUI::Asset::Wobject::Collaboration->new($self->session, $id, $class, $version);
|
||||
my $encUrl = _xml_encode($self->session->url->getSiteURL().$post->getUrl);
|
||||
my @attachmentLoop = ();
|
||||
unless ($post->get("storageId") eq "") {
|
||||
my $storage = $post->getStorageLocation;
|
||||
foreach my $filename (@{ $storage->getFiles }) {
|
||||
push @attachmentLoop, {
|
||||
'attachment.url' => $storage->getUrl($filename),
|
||||
'attachment.path' => $storage->getPath($filename),
|
||||
'attachment.length' => $storage->getFileSize($filename),
|
||||
};
|
||||
}
|
||||
}
|
||||
push(@{$var{'item_loop'}}, {
|
||||
author => _xml_encode($post->get('username')),
|
||||
title => _xml_encode($post->get("title")),
|
||||
link => $encUrl,
|
||||
description => _xml_encode($post->get("synopsis")),
|
||||
guid => $encUrl,
|
||||
pubDate => _xml_encode($self->_get_rfc822_date($post->get("dateUpdated"))),
|
||||
attachmentLoop => \@attachmentLoop,
|
||||
});
|
||||
$i++;
|
||||
last if ($i == $self->get("threadsPerPage"));
|
||||
}
|
||||
|
||||
$self->session->http->setMimeType("text/xml");
|
||||
my $output = $self->processTemplate(\%var,$self->get("rssTemplateId"));
|
||||
WebGUI::Macro::process($self->session,\$output);
|
||||
return $output;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
1;
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue