diff options
author | Bernhard Posselt <nukeawhale@gmail.com> | 2013-05-10 13:30:00 +0200 |
---|---|---|
committer | Bernhard Posselt <nukeawhale@gmail.com> | 2013-05-10 13:30:00 +0200 |
commit | 347000cc2d5597c1971942ebdbe97461c9fa5802 (patch) | |
tree | 8a2f6dc166ad871c646b4cf8bf13f4a9fcd02e55 | |
parent | c487bfe23312a5b266d8eb21a3825ab791a1c3e8 (diff) |
use a deleted flag for deleted feeds to handle undo
27 files changed, 642 insertions, 111 deletions
diff --git a/appinfo/database.xml b/appinfo/database.xml index af0073098..7d3cf2378 100644 --- a/appinfo/database.xml +++ b/appinfo/database.xml @@ -38,6 +38,13 @@ <default>true</default> <notnull>true</notnull> </field> + <field> + <name>deleted_at</name> + <type>integer</type> + <default>0</default> + <notnull>false</notnull> + <unsigned>true</unsigned> + </field> <index> <name>news_folders_parent_id_index</name> @@ -107,6 +114,13 @@ <unsigned>true</unsigned> </field> <field> + <name>deleted_at</name> + <type>integer</type> + <default>0</default> + <notnull>false</notnull> + <unsigned>true</unsigned> + </field> + <field> <name>folder_id</name> <type>integer</type> <notnull>true</notnull> diff --git a/appinfo/info.xml b/appinfo/info.xml index d20593797..4003fae84 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -5,6 +5,6 @@ <description>An RSS/Atom feed reader. Requires the App Framework app and backgroundjobs need to be enabled. See the README.rst in the apps top directory</description> <licence>AGPL</licence> <author>Alessandro Cosentino, Bernhard Posselt, Jan-Christoph Borchardt. Powered by SimplePie (Ryan Parman, Geoffrey Sneddon, Ryan McCue and contributors).</author> - <version>0.99</version> + <version>0.101</version> <require>5.0.6</require> </info> diff --git a/appinfo/routes.php b/appinfo/routes.php index 28a6cadff..8c89d49fb 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -71,6 +71,12 @@ $this->create('news_folders_delete', '/folders/{folderId}/delete')->post()->acti } ); +$this->create('news_folders_restore', '/folders/{folderId}/restore')->post()->action( + function($params){ + App::main('FolderController', 'restore', $params, new DIContainer()); + } +); + $this->create('news_folders_rename', '/folders/{folderId}/rename')->post()->action( function($params){ App::main('FolderController', 'rename', $params, new DIContainer()); @@ -110,6 +116,12 @@ $this->create('news_feeds_delete', '/feeds/{feedId}/delete')->post()->action( } ); +$this->create('news_feeds_restore', '/feeds/{feedId}/restore')->post()->action( + function($params){ + App::main('FeedController', 'restore', $params, new DIContainer()); + } +); + $this->create('news_feeds_update', '/feeds/{feedId}/update')->post()->action( function($params){ App::main('FeedController', 'update', $params, new DIContainer()); diff --git a/appinfo/version b/appinfo/version index f7274f3da..b4fad5b0f 100644 --- a/appinfo/version +++ b/appinfo/version @@ -1 +1 @@ -0.99
\ No newline at end of file +0.101
\ No newline at end of file diff --git a/backgroundjob/task.php b/backgroundjob/task.php index 820d36cc7..9a21a662f 100644 --- a/backgroundjob/task.php +++ b/backgroundjob/task.php @@ -34,7 +34,9 @@ class Task { static public function run() { $container = new DIContainer(); + $container['FolderBusinessLayer']->purgeDeleted(); $container['FeedBusinessLayer']->updateAll(); + $container['FeedBusinessLayer']->purgeDeleted(); $container['ItemBusinessLayer']->autoPurgeOld(); } diff --git a/businesslayer/feedbusinesslayer.php b/businesslayer/feedbusinesslayer.php index 1f5d5d6c7..f6ab1c144 100644 --- a/businesslayer/feedbusinesslayer.php +++ b/businesslayer/feedbusinesslayer.php @@ -43,17 +43,20 @@ class FeedBusinessLayer extends BusinessLayer { private $api; private $timeFactory; private $importParser; + private $autoPurgeMinimumInterval; public function __construct(FeedMapper $feedMapper, Fetcher $feedFetcher, ItemMapper $itemMapper, API $api, TimeFactory $timeFactory, - ImportParser $importParser){ + ImportParser $importParser, + $autoPurgeMinimumInterval){ parent::__construct($feedMapper); $this->feedFetcher = $feedFetcher; $this->itemMapper = $itemMapper; $this->api = $api; $this->timeFactory = $timeFactory; $this->importParser = $importParser; + $this->autoPurgeMinimumInterval = $autoPurgeMinimumInterval; } @@ -236,4 +239,37 @@ class FeedBusinessLayer extends BusinessLayer { } + /** + * Use this to mark a feed as deleted. That way it can be undeleted + * @throws BusinessLayerException when feed does not exist + */ + public function markDeleted($feedId, $userId) { + $feed = $this->find($feedId, $userId); + $feed->setDeletedAt($this->timeFactory->getTime()); + $this->mapper->update($feed); + } + + + /** + * Use this to undo a feed deletion + * @throws BusinessLayerException when feed does not exist + */ + public function unmarkDeleted($feedId, $userId) { + $feed = $this->find($feedId, $userId); + $feed->setDeletedAt(0); + $this->mapper->update($feed); + } + + + public function purgeDeleted($userId=null) { + $now = $this->timeFactory->getTime(); + $deleteOlderThan = $now - $this->autoPurgeMinimumInterval; + $toDelete = $this->mapper->getToDelete($deleteOlderThan, $userId); + + foreach ($toDelete as $feed) { + $this->mapper->delete($feed); + } + } + + } diff --git a/businesslayer/folderbusinesslayer.php b/businesslayer/folderbusinesslayer.php index a4c025b18..e511c4dda 100644 --- a/businesslayer/folderbusinesslayer.php +++ b/businesslayer/folderbusinesslayer.php @@ -26,6 +26,7 @@ namespace OCA\News\BusinessLayer; use \OCA\AppFramework\Core\API; +use \OCA\AppFramework\Utility\TimeFactory; use \OCA\News\Db\Folder; use \OCA\News\Db\FolderMapper; @@ -34,11 +35,17 @@ use \OCA\News\Db\FolderMapper; class FolderBusinessLayer extends BusinessLayer { private $api; + private $timeFactory; + private $autoPurgeMinimumInterval; public function __construct(FolderMapper $folderMapper, - API $api){ + API $api, + TimeFactory $timeFactory, + $autoPurgeMinimumInterval){ parent::__construct($folderMapper); $this->api = $api; + $this->timeFactory = $timeFactory; + $this->autoPurgeMinimumInterval = $autoPurgeMinimumInterval; } @@ -93,5 +100,40 @@ class FolderBusinessLayer extends BusinessLayer { } + /** + * Use this to mark a folder as deleted. That way it can be undeleted + * @throws BusinessLayerException when folder does not exist + */ + public function markDeleted($folderId, $userId) { + $folder = $this->find($folderId, $userId); + $folder->setDeletedAt($this->timeFactory->getTime()); + $this->mapper->update($folder); + } + + + /** + * Use this to undo a folder deletion + * @throws BusinessLayerException when folder does not exist + */ + public function unmarkDeleted($folderId, $userId) { + $folder = $this->find($folderId, $userId); + $folder->setDeletedAt(0); + $this->mapper->update($folder); + } + + + /** + * Purges marked as deleted folders + */ + public function purgeDeleted($userId=null) { + $now = $this->timeFactory->getTime(); + $deleteOlderThan = $now - $this->autoPurgeMinimumInterval; + $toDelete = $this->mapper->getToDelete($deleteOlderThan, $userId); + + foreach ($toDelete as $folder) { + $this->mapper->delete($folder); + } + } + } diff --git a/controller/feedcontroller.php b/controller/feedcontroller.php index b453338d0..d3cc62449 100644 --- a/controller/feedcontroller.php +++ b/controller/feedcontroller.php @@ -133,6 +133,10 @@ class FeedController extends Controller { $userId = $this->api->getUserId(); try { + // we need to purge deleted feeds if a feed is created to + // prevent already exists exceptions + $this->feedBusinessLayer->purgeDeleted($userId); + $feed = $this->feedBusinessLayer->create($url, $parentFolderId, $userId); $params = array( 'feeds' => array($feed) @@ -160,7 +164,7 @@ class FeedController extends Controller { $userId = $this->api->getUserId(); try { - $this->feedBusinessLayer->delete($feedId, $userId); + $this->feedBusinessLayer->markDeleted($feedId, $userId); return $this->renderJSON(); } catch(BusinessLayerException $ex) { return $this->renderJSON(array(), $ex->getMessage()); @@ -259,4 +263,22 @@ class FeedController extends Controller { } + /** + * @IsAdminExemption + * @IsSubAdminExemption + * @Ajax + */ + public function restore(){ + $feedId = (int) $this->params('feedId'); + $userId = $this->api->getUserId(); + + try { + $this->feedBusinessLayer->unmarkDeleted($feedId, $userId); + return $this->renderJSON(); + } catch(BusinessLayerException $ex) { + return $this->renderJSON(array(), $ex->getMessage()); + } + } + + }
\ No newline at end of file diff --git a/controller/foldercontroller.php b/controller/foldercontroller.php index 7542a5260..4bae118c2 100644 --- a/controller/foldercontroller.php +++ b/controller/foldercontroller.php @@ -114,6 +114,10 @@ class FolderController extends Controller { $folderName = $this->params('folderName'); try { + // we need to purge deleted folders if a folder is created to + // prevent already exists exceptions + $this->folderBusinessLayer->purgeDeleted($userId); + $folder = $this->folderBusinessLayer->create($folderName, $userId); $params = array( @@ -138,7 +142,7 @@ class FolderController extends Controller { $folderId = (int) $this->params('folderId'); try { - $this->folderBusinessLayer->delete($folderId, $userId); + $this->folderBusinessLayer->markDeleted($folderId, $userId); return $this->renderJSON(); } catch (BusinessLayerException $ex){ return $this->renderJSON(array(), $ex->getMessage()); @@ -188,4 +192,23 @@ class FolderController extends Controller { } + /** + * @IsAdminExemption + * @IsSubAdminExemption + * @Ajax + */ + public function restore(){ + $userId = $this->api->getUserId(); + $folderId = (int) $this->params('folderId'); + + try { + $this->folderBusinessLayer->unmarkDeleted($folderId, $userId); + return $this->renderJSON(); + } catch (BusinessLayerException $ex){ + return $this->renderJSON(array(), $ex->getMessage()); + } + + } + + }
\ No newline at end of file diff --git a/db/feed.php b/db/feed.php index 88879f8c4..22fbc359b 100644 --- a/db/feed.php +++ b/db/feed.php @@ -40,6 +40,7 @@ class Feed extends Entity implements IAPI { public $unreadCount; public $link; public $preventUpdate; + public $deletedAt; public function __construct(){ $this->addType('parentId', 'int'); @@ -47,6 +48,7 @@ class Feed extends Entity implements IAPI { $this->addType('folderId', 'int'); $this->addType('unreadCount', 'int'); $this->addType('preventUpdate', 'bool'); + $this->addType('deletedAt', 'int'); } diff --git a/db/feedmapper.php b/db/feedmapper.php index 9e371f1f0..438f2e78c 100644 --- a/db/feedmapper.php +++ b/db/feedmapper.php @@ -88,6 +88,7 @@ class FeedMapper extends Mapper implements IMapper { 'AND (`items`.`status` & ' . StatusFlag::UNREAD . ') = ' . StatusFlag::UNREAD . ' ' . 'WHERE `feeds`.`user_id` = ? ' . + 'AND `feeds`.`deleted_at` = 0 ' . 'GROUP BY `feeds`.`id`'; $params = array($userId); @@ -137,5 +138,20 @@ class FeedMapper extends Mapper implements IMapper { } + public function getToDelete($deleteOlderThan, $userId=null) { + $sql = 'SELECT * FROM `*PREFIX*news_feeds` ' . + 'WHERE `deleted_at` > 0 ' . + 'AND `deleted_at` < ?'; + $params = array($deleteOlderThan); + + // we need to sometimes only delete feeds of a user + if($userId !== null) { + $sql .= ' AND `user_id` = ?'; + array_push($params, $userId); + } + + return $this->findAllRows($sql, $params); + } + }
\ No newline at end of file diff --git a/db/folder.php b/db/folder.php index 24fa80000..7d1403c02 100644 --- a/db/folder.php +++ b/db/folder.php @@ -34,10 +34,12 @@ class Folder extends Entity implements IAPI { public $name; public $userId; public $opened; + public $deletedAt; public function __construct(){ $this->addType('parentId', 'int'); $this->addType('opened', 'bool'); + $this->addType('deletedAt', 'int'); } diff --git a/db/foldermapper.php b/db/foldermapper.php index 8978bb45b..f8d088cdb 100644 --- a/db/foldermapper.php +++ b/db/foldermapper.php @@ -65,7 +65,8 @@ class FolderMapper extends Mapper implements IMapper { public function findAllFromUser($userId){ $sql = 'SELECT * FROM `*PREFIX*news_folders` ' . - 'WHERE `user_id` = ?'; + 'WHERE `user_id` = ? ' . + 'AND `deleted_at` = 0'; $params = array($userId); return $this->findAllRows($sql, $params); @@ -74,7 +75,7 @@ class FolderMapper extends Mapper implements IMapper { public function findByName($folderName, $userId){ $sql = 'SELECT * FROM `*PREFIX*news_folders` ' . - 'WHERE `name` = ?' . + 'WHERE `name` = ? ' . 'AND `user_id` = ?'; $params = array($folderName, $userId); @@ -96,4 +97,21 @@ class FolderMapper extends Mapper implements IMapper { } + public function getToDelete($deleteOlderThan, $userId=null) { + $sql = 'SELECT * FROM `*PREFIX*news_folders` ' . + 'WHERE `deleted_at` > 0 ' . + 'AND `deleted_at` < ?'; + $params = array($deleteOlderThan); + + // we need to sometimes only delete feeds of a user + if($userId !== null) { + $sql .= ' AND `user_id` = ?'; + array_push($params, $userId); + } + + return $this->findAllRows($sql, $params); + } + + + }
\ No newline at end of file diff --git a/dependencyinjection/dicontainer.php b/dependencyinjection/dicontainer.php index 8334a40ad..464dc1d6a 100644 --- a/dependencyinjection/dicontainer.php +++ b/dependencyinjection/dicontainer.php @@ -70,7 +70,11 @@ class DIContainer extends BaseContainer { /** * Configuration values */ - $this['autoPurgeCount'] = 200; // per feed + $this['autoPurgeMinimumInterval'] = 60; // seconds, used to define how + // long deleted folders and feeds + // should still be kept for an + // undo actions + $this['autoPurgeCount'] = 200; // number of allowed unread articles per feed $this['simplePieCacheDuration'] = 30*60; // seconds $this['simplePieCacheDirectory'] = $this->share(function($c) { @@ -145,18 +149,30 @@ class DIContainer extends BaseContainer { * Business Layer */ $this['FolderBusinessLayer'] = $this->share(function($c){ - return new FolderBusinessLayer($c['FolderMapper'], $c['API']); + return new FolderBusinessLayer( + $c['FolderMapper'], + $c['API'], + $c['TimeFactory'], + $c['autoPurgeMinimumInterval']); }); $this['FeedBusinessLayer'] = $this->share(function($c){ - return new FeedBusinessLayer($c['FeedMapper'], $c['Fetcher'], - $c['ItemMapper'], $c['API'], $c['TimeFactory'], - $c['ImportParser']); + return new FeedBusinessLayer( + $c['FeedMapper'], + $c['Fetcher'], + $c['ItemMapper'], + $c['API'], + $c['TimeFactory'], + $c['ImportParser'], + $c['autoPurgeMinimumInterval']); }); $this['ItemBusinessLayer'] = $this->share(function($c){ - return new ItemBusinessLayer($c['ItemMapper'], $c['StatusFlag'], - $c['TimeFactory'], $c['autoPurgeCount']); + return new ItemBusinessLayer( + $c['ItemMapper'], + $c['StatusFlag'], + $c['TimeFactory'], + $c['autoPurgeCount']); }); diff --git a/external/feedapi.php b/external/feedapi.php index 7273a5413..9d8f75482 100644 --- a/external/feedapi.php +++ b/external/feedapi.php @@ -82,6 +82,8 @@ class FeedAPI extends Controller { $folderId = (int) $this->params('folderId', 0); try { + $this->feedBusinessLayer->purgeDeleted($userId); + $feed = $this->feedBusinessLayer->create($feedUrl, $folderId, $userId); $result = array( 'feeds' => array($feed->toAPI()) diff --git a/external/folderapi.php b/external/folderapi.php index 879d62b5a..314b4ea2f 100644 --- a/external/folderapi.php +++ b/external/folderapi.php @@ -72,6 +72,7 @@ class FolderAPI extends Controller { ); try { + $this->folderBusinessLayer->purgeDeleted($userId); $folder = $this->folderBusinessLayer->create($folderName, $userId); array_push($result['folders'], $folder->toAPI()); diff --git a/js/app/services/businesslayer/folderbusinesslayer.coffee b/js/app/services/businesslayer/folderbusinesslayer.coffee index 39c307c96..3d1fbb7c7 100644 --- a/js/app/services/businesslayer/folderbusinesslayer.coffee +++ b/js/app/services/businesslayer/folderbusinesslayer.coffee @@ -43,11 +43,12 @@ NewestItem, FeedModel) -> delete: (folderId) -> feeds = [] - folder = @_folderModel.removeById(folderId) - # also delete feeds for feed in @_feedBusinessLayer.getFeedsOfFolder(folderId) feeds.push(@_feedModel.removeById(feed.id)) + + folder = @_folderModel.removeById(folderId) + callback = => @_persistence.deleteFolder(folderId) @@ -159,8 +160,8 @@ NewestItem, FeedModel) -> _importElement: (opml, parentFolderId) -> for item in opml.getItems() - if item.isFolder() - do (item) => + do (item) => + if item.isFolder() try @create item.getName(), (data) => @_importElement(item, data.folders[0].id) @@ -171,14 +172,13 @@ NewestItem, FeedModel) -> @_importElement(item, folder.id) else console.info error - else - try - do (item) => + else + try @_feedBusinessLayer.create(item.getUrl(), parentFolderId) - catch error - if not error instanceof _ExistsError - console.info error + catch error + if not error instanceof _ExistsError + console.info error return new FolderBusinessLayer(FolderModel, FeedBusinessLayer, ShowAll, diff --git a/js/app/services/undoqueue.coffee b/js/app/services/undoqueue.coffee index 93f6fb577..631013d13 100644 --- a/js/app/services/undoqueue.coffee +++ b/js/app/services/undoqueue.coffee @@ -44,7 +44,7 @@ angular.module('News').factory 'UndoQueue', an command has been canceled. Usually this will add back a deleted object back to the interface, defaults to an empty function ### - @_executeAll() + @executeAll() command = _undoCallback: @_undoCallback or= -> @@ -70,7 +70,7 @@ angular.module('News').factory 'UndoQueue', @_queue.push(command) - _executeAll: -> + executeAll: -> ### Executes the callback before the timeout has run out This is useful to execute all remaining commands if a new command is diff --git a/js/public/app.js b/js/public/app.js index 8329f90f6..df5202f88 100644 --- a/js/public/app.js +++ b/js/public/app.js @@ -1076,12 +1076,12 @@ License along with this library. If not, see <http://www.gnu.org/licenses/>. _this = this; feeds = []; - folder = this._folderModel.removeById(folderId); _ref = this._feedBusinessLayer.getFeedsOfFolder(folderId); for (_i = 0, _len = _ref.length; _i < _len; _i++) { feed = _ref[_i]; feeds.push(this._feedModel.removeById(feed.id)); } + folder = this._folderModel.removeById(folderId); callback = function() { return _this._persistence.deleteFolder(folderId); }; @@ -1228,17 +1228,17 @@ License along with this library. If not, see <http://www.gnu.org/licenses/>. }; FolderBusinessLayer.prototype._importElement = function(opml, parentFolderId) { - var error, item, _i, _len, _ref, _results, + var item, _i, _len, _ref, _results, _this = this; _ref = opml.getItems(); _results = []; for (_i = 0, _len = _ref.length; _i < _len; _i++) { item = _ref[_i]; - if (item.isFolder()) { - _results.push((function(item) { - var error, folder; + _results.push((function(item) { + var error, folder; + if (item.isFolder()) { try { return _this.create(item.getName(), function(data) { return _this._importElement(item, data.folders[0].id); @@ -1253,21 +1253,17 @@ License along with this library. If not, see <http://www.gnu.org/licenses/>. return console.info(error); } } - })(item)); - } else { - try { - _results.push((function(item) { + } else { + try { return _this._feedBusinessLayer.create(item.getUrl(), parentFolderId); - })(item)); - } catch (_error) { - error = _error; - if (!error instanceof _ExistsError) { - _results.push(console.info(error)); - } else { - _results.push(void 0); + } catch (_error) { + error = _error; + if (!error instanceof _ExistsError) { + return console.info(error); + } } } - } + })(item)); } return _results; }; @@ -3234,7 +3230,7 @@ License along with this library. If not, see <http://www.gnu.org/licenses/>. object back to the interface, defaults to an empty function */ - this._executeAll(); + this.executeAll(); command = { _undoCallback: this._undoCallback || (this._undoCallback = function() {}), _callback: this._callback, @@ -3260,7 +3256,7 @@ License along with this library. If not, see <http://www.gnu.org/licenses/>. return this._queue.push(command); }; - UndoQueue.prototype._executeAll = function() { + UndoQueue.prototype.executeAll = function() { /* Executes the callback before the timeout has run out This is useful to execute all remaining commands if a new command is diff --git a/tests/unit/businesslayer/FeedBusinessLayerTest.php b/tests/unit/businesslayer/FeedBusinessLa |