From 09f60e75c90e5734a3b11a0cca944bd42bc41665 Mon Sep 17 00:00:00 2001 From: Bernhard Posselt Date: Wed, 11 Sep 2013 16:42:03 +0200 Subject: #342 implement export --- appinfo/routes.php | 6 +++ businesslayer/itembusinesslayer.php | 9 ++++ controller/exportcontroller.php | 39 ++++++++++++-- css/settings.css | 16 ++++++ db/item.php | 17 ++++++ db/itemmapper.php | 9 ++++ dependencyinjection/dicontainer.php | 1 + img/download.svg | 58 ++++++++++++++++++++ templates/part.settings.php | 28 +++++++--- tests/unit/businesslayer/ItemBusinessLayerTest.php | 12 +++++ tests/unit/controller/ExportControllerTest.php | 62 +++++++++++++++++++++- tests/unit/db/ItemMapperTest.php | 12 +++++ tests/unit/db/ItemTest.php | 40 ++++++++++++++ tests/unit/utility/ConfigTest.php | 4 +- utility/config.php | 2 +- 15 files changed, 301 insertions(+), 14 deletions(-) create mode 100644 img/download.svg diff --git a/appinfo/routes.php b/appinfo/routes.php index 617b63c53..90c5f2378 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -196,6 +196,12 @@ $this->create('news_export_opml', '/export/opml')->get()->action( } ); +$this->create('news_export_articles', '/export/articles')->get()->action( + function($params){ + App::main('ExportController', 'articles', $params, new DIContainer()); + } +); + /** * User Settings */ diff --git a/businesslayer/itembusinesslayer.php b/businesslayer/itembusinesslayer.php index 9280153a6..6a00d8ba1 100644 --- a/businesslayer/itembusinesslayer.php +++ b/businesslayer/itembusinesslayer.php @@ -228,4 +228,13 @@ class ItemBusinessLayer extends BusinessLayer { } + /** + * @param string $userId from which user the items should be taken + * @return array of items which are starred or unread + */ + public function getUnreadOrStarred($userId) { + return $this->mapper->findAllUnreadOrStarred($userId); + } + + } diff --git a/controller/exportcontroller.php b/controller/exportcontroller.php index 637219706..357c54d54 100644 --- a/controller/exportcontroller.php +++ b/controller/exportcontroller.php @@ -29,9 +29,11 @@ use \OCA\AppFramework\Controller\Controller; use \OCA\AppFramework\Core\API; use \OCA\AppFramework\Http\Request; use \OCA\AppFramework\Http\TextDownloadResponse; +use \OCA\AppFramework\Http\JSONResponse; use \OCA\News\BusinessLayer\FeedBusinessLayer; use \OCA\News\BusinessLayer\FolderBusinessLayer; +use \OCA\News\BusinessLayer\ItemBusinessLayer; use \OCA\News\Utility\OPMLExporter; class ExportController extends Controller { @@ -39,15 +41,18 @@ class ExportController extends Controller { private $opmlExporter; private $folderBusinessLayer; private $feedBusinessLayer; + private $itemBusinessLayer; public function __construct(API $api, Request $request, FeedBusinessLayer $feedBusinessLayer, FolderBusinessLayer $folderBusinessLayer, + ItemBusinessLayer $itemBusinessLayer, OPMLExporter $opmlExporter){ parent::__construct($api, $request); $this->feedBusinessLayer = $feedBusinessLayer; $this->folderBusinessLayer = $folderBusinessLayer; $this->opmlExporter = $opmlExporter; + $this->itemBusinessLayer = $itemBusinessLayer; } @@ -57,12 +62,40 @@ class ExportController extends Controller { * @CSRFExemption */ public function opml(){ - $user = $this->api->getUserId(); - $feeds = $this->feedBusinessLayer->findAll($user); - $folders = $this->folderBusinessLayer->findAll($user); + $userId = $this->api->getUserId(); + $feeds = $this->feedBusinessLayer->findAll($userId); + $folders = $this->folderBusinessLayer->findAll($userId); $opml = $this->opmlExporter->build($folders, $feeds)->saveXML(); return new TextDownloadResponse($opml, 'subscriptions.opml', 'text/xml'); } + /** + * @IsAdminExemption + * @IsSubAdminExemption + * @CSRFExemption + */ + public function articles(){ + $userId = $this->api->getUserId(); + $feeds = $this->feedBusinessLayer->findAll($userId); + $items = $this->itemBusinessLayer->getUnreadOrStarred($userId); + + // build assoc array for fast access + $feedsDict = array(); + foreach($feeds as $feed) { + $feedsDict['feed' . $feed->getId()] = $feed; + } + + $articles = array(); + foreach($items as $item) { + array_push($articles, $item->toExport($feedsDict)); + } + + $response = new JSONResponse($articles); + $response->addHeader('Content-Disposition', + 'attachment; filename="articles.json"'); + return $response; + } + + } \ No newline at end of file diff --git a/css/settings.css b/css/settings.css index 54338ed5f..7cba9dde0 100644 --- a/css/settings.css +++ b/css/settings.css @@ -34,4 +34,20 @@ #app-settings-content { padding-bottom: 25px; +} + +.upload-icon, +.download-icon { + padding-left: 25px; + background-repeat: no-repeat; + background-position: 5px center; + opacity: .8; +} + +.upload-icon { + background-image: url('%webroot%/core/img/actions/upload.svg'); +} + +.download-icon { + background-image: url('%appswebroot%/news/img/download.svg'); } \ No newline at end of file diff --git a/db/item.php b/db/item.php index 1326b65ba..c83da572d 100644 --- a/db/item.php +++ b/db/item.php @@ -109,6 +109,23 @@ class Item extends Entity implements IAPI { } + public function toExport($feeds) { + return array( + 'guid' => $this->getGuid(), + 'url' => $this->getUrl(), + 'title' => $this->getTitle(), + 'author' => $this->getAuthor(), + 'pubDate' => $this->getPubDate(), + 'body' => $this->getBody(), + 'enclosureMime' => $this->getEnclosureMime(), + 'enclosureLink' => $this->getEnclosureLink(), + 'unread' => $this->isUnread(), + 'starred' => $this->isStarred(), + 'feedLink' => $feeds['feed'. $this->getFeedId()]->getLink() + ); + } + + public function setAuthor($name) { parent::setAuthor(strip_tags($name)); } diff --git a/db/itemmapper.php b/db/itemmapper.php index 0a78b02df..8fa40e8eb 100644 --- a/db/itemmapper.php +++ b/db/itemmapper.php @@ -230,6 +230,15 @@ class ItemMapper extends Mapper implements IMapper { } + public function findAllUnreadOrStarred($userId) { + $params = array($userId); + $status = StatusFlag::UNREAD | StatusFlag::STARRED; + $sql = 'AND ((`items`.`status` & ' . $status . ') > 0) '; + $sql = $this->makeSelectQuery($sql); + return $this->findAllRows($sql, $params); + } + + public function findByGuidHash($guidHash, $feedId, $userId){ $sql = $this->makeSelectQuery( 'AND `items`.`guid_hash` = ? ' . diff --git a/dependencyinjection/dicontainer.php b/dependencyinjection/dicontainer.php index 8ad816f8d..77e7b4aa6 100644 --- a/dependencyinjection/dicontainer.php +++ b/dependencyinjection/dicontainer.php @@ -165,6 +165,7 @@ class DIContainer extends BaseContainer { return new ExportController($c['API'], $c['Request'], $c['FeedBusinessLayer'], $c['FolderBusinessLayer'], + $c['ItemBusinessLayer'], $c['OPMLExporter']); }); diff --git a/img/download.svg b/img/download.svg new file mode 100644 index 000000000..ef0618017 --- /dev/null +++ b/img/download.svg @@ -0,0 +1,58 @@ + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/templates/part.settings.php b/templates/part.settings.php index b245ee3be..9d7763978 100644 --- a/templates/part.settings.php +++ b/templates/part.settings.php @@ -8,25 +8,27 @@ }"> -
+
- t('Import / Export OPML')); ?> + t('Subscriptions')); ?> - - t('Export')); ?>
- t('Import Google Reader JSON')); ?> -

t('To import starred and shared articles from Google - Reader please upload the .json files from the Google Takeout archive')); ?> + t('Unread/Starred Articles')); ?> + + t('Export')); ?> + + +

t('Error when importing: file does not contain valid JSON')); ?>

diff --git a/tests/unit/businesslayer/ItemBusinessLayerTest.php b/tests/unit/businesslayer/ItemBusinessLayerTest.php index 21a776c05..5dc6c9895 100644 --- a/tests/unit/businesslayer/ItemBusinessLayerTest.php +++ b/tests/unit/businesslayer/ItemBusinessLayerTest.php @@ -348,6 +348,18 @@ class ItemBusinessLayerTest extends \OCA\AppFramework\Utility\TestUtility { } + public function testGetUnreadOrStarred(){ + $star = 18; + + $this->mapper->expects($this->once()) + ->method('findAllUnreadOrStarred') + ->with($this->equalTo($this->user)) + ->will($this->returnValue($star)); + + $result = $this->itemBusinessLayer->getUnreadOrStarred($this->user); + + $this->assertEquals($star, $result); + } } diff --git a/tests/unit/controller/ExportControllerTest.php b/tests/unit/controller/ExportControllerTest.php index 29e0b6f71..2b9d41dde 100644 --- a/tests/unit/controller/ExportControllerTest.php +++ b/tests/unit/controller/ExportControllerTest.php @@ -27,11 +27,14 @@ namespace OCA\News\Controller; use \OCA\AppFramework\Http\Request; use \OCA\AppFramework\Http\TextDownloadResponse; +use \OCA\AppFramework\Http\JSONResponse; use \OCA\AppFramework\Utility\ControllerTestUtility; use \OCA\AppFramework\Db\DoesNotExistException; use \OCA\AppFramework\Db\MultipleObjectsReturnedException; use \OCA\News\Utility\OPMLExporter; +use \OCA\News\Db\Item; +use \OCA\News\Db\Feed; require_once(__DIR__ . "/../../classloader.php"); @@ -44,6 +47,7 @@ class ExportControllerTest extends ControllerTestUtility { private $user; private $feedBusinessLayer; private $folderBusinessLayer; + private $itemBusinessLayer; private $opmlExporter; /** @@ -51,6 +55,9 @@ class ExportControllerTest extends ControllerTestUtility { */ public function setUp(){ $this->api = $this->getAPIMock(); + $this->itemBusinessLayer = $this->getMockBuilder('\OCA\News\BusinessLayer\ItemBusinessLayer') + ->disableOriginalConstructor() + ->getMock(); $this->feedBusinessLayer = $this->getMockBuilder('\OCA\News\BusinessLayer\FeedBusinessLayer') ->disableOriginalConstructor() ->getMock(); @@ -60,7 +67,8 @@ class ExportControllerTest extends ControllerTestUtility { $this->request = new Request(); $this->opmlExporter = new OPMLExporter(); $this->controller = new ExportController($this->api, $this->request, - $this->feedBusinessLayer, $this->folderBusinessLayer, $this->opmlExporter); + $this->feedBusinessLayer, $this->folderBusinessLayer, + $this->itemBusinessLayer, $this->opmlExporter); $this->user = 'john'; } @@ -72,6 +80,13 @@ class ExportControllerTest extends ControllerTestUtility { } + public function testArticlesAnnotations(){ + $annotations = array('IsAdminExemption', 'IsSubAdminExemption', + 'CSRFExemption'); + $this->assertAnnotations($this->controller, 'articles', $annotations); + } + + public function testOpmlExportNoFeeds(){ $opml = "\n" . @@ -100,4 +115,49 @@ class ExportControllerTest extends ControllerTestUtility { } + public function testGetAllArticles(){ + $item1 = new Item(); + $item1->setFeedId(3); + $item2 = new Item(); + $item2->setFeedId(5); + + $feed1 = new Feed(); + $feed1->setId(3); + $feed1->setLink('http://goo'); + $feed2 = new Feed(); + $feed2->setId(5); + $feed2->setLink('http://gee'); + $feeds = array($feed1, $feed2); + + $articles = array( + $item1, $item2 + ); + + $this->api->expects($this->once()) + ->method('getUserId') + ->will($this->returnValue($this->user)); + $this->feedBusinessLayer->expects($this->once()) + ->method('findAll') + ->with($this->equalTo($this->user)) + ->will($this->returnValue($feeds)); + $this->itemBusinessLayer->expects($this->once()) + ->method('getUnreadOrStarred') + ->with($this->equalTo($this->user)) + ->will($this->returnValue($articles)); + + + $return = $this->controller->articles(); + $headers = $return->getHeaders(); + $this->assertTrue($return instanceof JSONResponse); + $this->assertEquals('attachment; filename="articles.json"', $headers ['Content-Disposition']); + + $this->assertEquals('[{"guid":null,"url":null,"title":null,' . + '"author":null,"pubDate":null,"body":null,"enclosureMime":null,' . + '"enclosureLink":null,"unread":false,"starred":false,' . + '"feedLink":"http:\/\/goo"},{"guid":null,"url":null,"title":null,' . + '"author":null,"pubDate":null,"body":null,"enclosureMime":null,' . + '"enclosureLink":null,"unread":false,"starred":false,' . + '"feedLink":"http:\/\/gee"}]', $return->render()); + } + } \ No newline at end of file diff --git a/tests/unit/db/ItemMapperTest.php b/tests/unit/db/ItemMapperTest.php index eb04b1514..ae045ce31 100644 --- a/tests/unit/db/ItemMapperTest.php +++ b/tests/unit/db/ItemMapperTest.php @@ -221,6 +221,18 @@ class ItemMapperTest extends \OCA\AppFramework\Utility\MapperTestUtility { } + public function testFindAllUnreadOrStarred(){ + $status = StatusFlag::UNREAD | StatusFlag::STARRED; + $sql = 'AND ((`items`.`status` & ' . $status . ') > 0) '; + $sql = $this->makeSelectQuery($sql); + $params = array($this->user); + $this->setMapperResult($sql, $params, $this->rows); + $result = $this->mapper->findAllUnreadOrStarred($this->user); + + $this->assertEquals($this->items, $result); + } + + public function testFindAllFeed(){ $sql = 'AND `items`.`feed_id` = ? ' . 'AND `items`.`id` < ? '; diff --git a/tests/unit/db/ItemTest.php b/tests/unit/db/ItemTest.php index daaf64a65..511badeeb 100644 --- a/tests/unit/db/ItemTest.php +++ b/tests/unit/db/ItemTest.php @@ -103,6 +103,46 @@ class ItemTest extends \PHPUnit_Framework_TestCase { } + public function testToExport() { + $item = new Item(); + $item->setId(3); + $item->setGuid('guid'); + $item->setGuidHash('hash'); + $item->setUrl('https://google'); + $item->setTitle('title'); + $item->setAuthor('author'); + $item->setPubDate(123); + $item->setBody('body'); + $item->setEnclosureMime('audio/ogg'); + $item->setEnclosureLink('enclink'); + $item->setFeedId(1); + $item->setStatus(0); + $item->setUnread(); + $item->setStarred(); + $item->setLastModified(321); + + $feed = new Feed(); + $feed->setLink('http://test'); + $feeds = array( + "feed1" => $feed + ); + + $this->assertEquals(array( + 'guid' => 'guid', + 'url' => 'https://google', + 'title' => 'title', + 'author' => 'author', + 'pubDate' => 123, + 'body' => 'body', + 'enclosureMime' => 'audio/ogg', + 'enclosureLink' => 'enclink', + 'unread' => true, + 'starred' => true, + 'feedLink' => 'http://test' + ), $item->toExport($feeds)); + } + + public function testSetAuthor(){ $item = new Item(); $item->setAuthor('my link'); diff --git a/tests/unit/utility/ConfigTest.php b/tests/unit/utility/ConfigTest.php index 479acabb5..0e2d6ab4e 100644 --- a/tests/unit/utility/ConfigTest.php +++ b/tests/unit/utility/ConfigTest.php @@ -51,7 +51,7 @@ class ConfigFetcherTest extends \OCA\AppFramework\Utility\TestUtility { public function testDefaults() { $this->assertEquals(60, $this->config->getAutoPurgeMinimumInterval()); - $this->assertEquals(200, $this->config->getAutoPurgeCount()); + $this->assertEquals(5000, $this->config->getAutoPurgeCount()); $this->assertEquals(30*60, $this->config->getSimplePieCacheDuration()); $this->assertEquals(60, $this->config->getFeedFetcherTimeout()); $this->assertEquals(true, $this->config->getUseCronUpdates()); @@ -139,7 +139,7 @@ class ConfigFetcherTest extends \OCA\AppFramework\Utility\TestUtility { $this->config->setUseCronUpdates(false); $json = "autoPurgeMinimumInterval = 60\n" . - "autoPurgeCount = 200\n" . + "autoPurgeCount = 5000\n" . "simplePieCacheDuration = 1800\n" . "feedFetcherTimeout = 60\n" . "useCronUpdates = false"; diff --git a/utility/config.php b/utility/config.php index 54145c993..3c0b1edb1 100644 --- a/utility/config.php +++ b/utility/config.php @@ -45,7 +45,7 @@ class Config { public function __construct($fileSystem, API $api) { $this->fileSystem = $fileSystem; $this->autoPurgeMinimumInterval = 60; - $this->autoPurgeCount = 200; + $this->autoPurgeCount = 5000; $this->simplePieCacheDuration = 30*60; $this->feedFetcherTimeout = 60; $this->useCronUpdates = true; -- cgit v1.2.3