summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorBernhard Posselt <dev@bernhard-posselt.com>2014-05-30 00:06:49 +0200
committerBernhard Posselt <dev@bernhard-posselt.com>2014-05-30 00:06:49 +0200
commit79032a17fb1f458aae723098ddc1a96c28daad05 (patch)
treeb7e7a7f99e6e334d9058de7445280da6624ec3e3
parent320304c909dc12c14b53335a7f32550809c06178 (diff)
add autopaging
-rw-r--r--appinfo/routes.php1
-rw-r--r--controller/itemcontroller.php18
-rw-r--r--js/app/Config.js12
-rw-r--r--js/build/app.js106
-rw-r--r--js/controller/ContentController.js54
-rw-r--r--js/service/ItemResource.js36
-rw-r--r--js/tests/unit/controller/ContentControllerSpec.js191
-rw-r--r--js/tests/unit/controller/NavigationControllerSpec.js1
-rw-r--r--js/tests/unit/service/ItemResourceSpec.js57
-rw-r--r--templates/main.php2
-rw-r--r--tests/unit/controller/ItemControllerTest.php15
11 files changed, 450 insertions, 43 deletions
diff --git a/appinfo/routes.php b/appinfo/routes.php
index 36a3936f6..2358527f9 100644
--- a/appinfo/routes.php
+++ b/appinfo/routes.php
@@ -51,6 +51,7 @@ $application->registerRoutes($this, ['routes' => [
['name' => 'item#new_items', 'url' => '/items/new', 'verb' => 'GET'],
['name' => 'item#readAll', 'url' => '/items/read', 'verb' => 'POST'],
['name' => 'item#read', 'url' => '/items/{itemId}/read', 'verb' => 'POST'],
+ ['name' => 'item#read_multiple', 'url' => '/items/read/multiple', 'verb' => 'POST'],
['name' => 'item#star', 'url' => '/items/{feedId}/{guidHash}/star', 'verb' => 'POST'],
// export
diff --git a/controller/itemcontroller.php b/controller/itemcontroller.php
index 4f3f22c62..30677d9fc 100644
--- a/controller/itemcontroller.php
+++ b/controller/itemcontroller.php
@@ -54,7 +54,7 @@ class ItemController extends Controller {
* @param int $limit
* @param int $offset
*/
- public function index($type, $id, $limit, $offset=0) {
+ public function index($type, $id, $limit=50, $offset=0) {
$showAll = $this->settings->getUserValue($this->userId, $this->appName,
'showAll') === '1';
$oldestFirst = $this->settings->getUserValue($this->userId, $this->appName,
@@ -163,4 +163,20 @@ class ItemController extends Controller {
}
+ /**
+ * @NoAdminRequired
+ *
+ * @param int[] item ids
+ */
+ public function readMultiple($itemIds) {
+ foreach($itemIds as $id) {
+ try {
+ $this->itemService->read($id, true, $this->userId);
+ } catch(ServiceNotFoundException $ex) {
+ continue;
+ }
+ }
+ }
+
+
} \ No newline at end of file
diff --git a/js/app/Config.js b/js/app/Config.js
index 5aebc3653..772547178 100644
--- a/js/app/Config.js
+++ b/js/app/Config.js
@@ -79,22 +79,26 @@ app.config(function ($routeProvider, $provide, $httpProvider) {
.when('/items', {
controller: 'ContentController as Content',
templateUrl: 'content.html',
- resolve: getResolve(feedType.SUBSCRIPTIONS)
+ resolve: getResolve(feedType.SUBSCRIPTIONS),
+ type: feedType.SUBSCRIPTIONS
})
.when('/items/starred', {
controller: 'ContentController as Content',
templateUrl: 'content.html',
- resolve: getResolve(feedType.STARRED)
+ resolve: getResolve(feedType.STARRED),
+ type: feedType.STARRED
})
.when('/items/feeds/:id', {
controller: 'ContentController as Content',
templateUrl: 'content.html',
- resolve: getResolve(feedType.FEED)
+ resolve: getResolve(feedType.FEED),
+ type: feedType.FEED
})
.when('/items/folders/:id', {
controller: 'ContentController as Content',
templateUrl: 'content.html',
- resolve: getResolve(feedType.FOLDER)
+ resolve: getResolve(feedType.FOLDER),
+ type: feedType.FOLDER
})
.otherwise({
redirectTo: '/items'
diff --git a/js/build/app.js b/js/build/app.js
index eb16bafb3..b7d44526e 100644
--- a/js/build/app.js
+++ b/js/build/app.js
@@ -68,19 +68,23 @@ var $__build_47_app__ = function () {
$routeProvider.when('/items', {
controller: 'ContentController as Content',
templateUrl: 'content.html',
- resolve: getResolve(feedType.SUBSCRIPTIONS)
+ resolve: getResolve(feedType.SUBSCRIPTIONS),
+ type: feedType.SUBSCRIPTIONS
}).when('/items/starred', {
controller: 'ContentController as Content',
templateUrl: 'content.html',
- resolve: getResolve(feedType.STARRED)
+ resolve: getResolve(feedType.STARRED),
+ type: feedType.STARRED
}).when('/items/feeds/:id', {
controller: 'ContentController as Content',
templateUrl: 'content.html',
- resolve: getResolve(feedType.FEED)
+ resolve: getResolve(feedType.FEED),
+ type: feedType.FEED
}).when('/items/folders/:id', {
controller: 'ContentController as Content',
templateUrl: 'content.html',
- resolve: getResolve(feedType.FOLDER)
+ resolve: getResolve(feedType.FOLDER),
+ type: feedType.FOLDER
}).otherwise({ redirectTo: '/items' });
}
]);
@@ -181,7 +185,9 @@ var $__build_47_app__ = function () {
'ItemResource',
'SettingsResource',
'data',
- function (Publisher, FeedResource, ItemResource, SettingsResource, data) {
+ '$route',
+ '$routeParams',
+ function (Publisher, FeedResource, ItemResource, SettingsResource, data, $route, $routeParams) {
'use strict';
var $__0 = this;
this.isAutoPagingEnabled = true;
@@ -194,9 +200,11 @@ var $__build_47_app__ = function () {
ItemResource.toggleStar(itemId);
};
this.markRead = function (itemId) {
- ItemResource.markItemRead(itemId);
var item = ItemResource.get(itemId);
- FeedResource.markItemOfFeedRead(item.feedId);
+ if (!item.keepUnread) {
+ ItemResource.markItemRead(itemId);
+ FeedResource.markItemOfFeedRead(item.feedId);
+ }
};
this.getFeed = function (feedId) {
return FeedResource.getById(feedId);
@@ -219,21 +227,50 @@ var $__build_47_app__ = function () {
this.isCompactView = function () {
return SettingsResource.get('compact');
};
- this.getRelativeDate = function (timestamp) {
- console.log(timestamp);
- };
- this.autoPage = function () {
- console.log('hi');
- };
- this.scrollRead = function (itemIds) {
- console.log(itemIds);
- };
this.autoPagingEnabled = function () {
return $__0.isAutoPagingEnabled;
};
this.markReadEnabled = function () {
return !SettingsResource.get('preventReadOnScroll');
};
+ this.scrollRead = function (itemIds) {
+ var ids = [];
+ for (var $__3 = itemIds[$traceurRuntime.toProperty(Symbol.iterator)](), $__4; !($__4 = $__3.next()).done;) {
+ try {
+ throw undefined;
+ } catch (itemId) {
+ itemId = $__4.value;
+ {
+ try {
+ throw undefined;
+ } catch (item) {
+ item = ItemResource.get(itemId);
+ if (!item.keepUnread) {
+ ids.push(itemId);
+ FeedResource.markItemOfFeedRead(item.feedId);
+ }
+ }
+ }
+ }
+ }
+ ItemResource.markItemsRead(ids);
+ };
+ this.autoPage = function () {
+ $__0.isAutoPagingEnabled = false;
+ var type = $route.current.$$route.type;
+ var id = $routeParams.id;
+ ItemResource.autoPage(type, id).success(function (data) {
+ Publisher.publishAll(data);
+ if (data.items.length > 0) {
+ $__0.isAutoPagingEnabled = true;
+ }
+ }).error(function () {
+ $__0.isAutoPagingEnabled = true;
+ });
+ };
+ this.getRelativeDate = function (timestamp) {
+ console.log(timestamp);
+ };
}
]);
app.controller('NavigationController', [
@@ -473,14 +510,16 @@ var $__build_47_app__ = function () {
'Resource',
'$http',
'BASE_URL',
- function (Resource, $http, BASE_URL) {
+ 'ITEM_BATCH_SIZE',
+ function (Resource, $http, BASE_URL, ITEM_BATCH_SIZE) {
'use strict';
- var ItemResource = function ItemResource($http, BASE_URL) {
+ var ItemResource = function ItemResource($http, BASE_URL, ITEM_BATCH_SIZE) {
$traceurRuntime.superCall(this, $ItemResource.prototype, 'constructor', [
$http,
BASE_URL
]);
this.starredCount = 0;
+ this.batchSize = ITEM_BATCH_SIZE;
};
var $ItemResource = ItemResource;
$traceurRuntime.createClass(ItemResource, {
@@ -537,6 +576,23 @@ var $__build_47_app__ = function () {
data: { isRead: isRead }
});
},
+ markItemsRead: function (itemIds) {
+ for (var $__3 = itemIds[$traceurRuntime.toProperty(Symbol.iterator)](), $__4; !($__4 = $__3.next()).done;) {
+ try {
+ throw undefined;
+ } catch (itemId) {
+ itemId = $__4.value;
+ {
+ this.get(itemId).unread = false;
+ }
+ }
+ }
+ return this.http({
+ url: this.BASE_URL + '/items/read/multiple',
+ method: 'POST',
+ data: { itemIds: itemIds }
+ });
+ },
markFeedRead: function (feedId) {
var read = arguments[1] !== void 0 ? arguments[1] : true;
for (var $__3 = this.values.filter(function (i) {
@@ -568,9 +624,21 @@ var $__build_47_app__ = function () {
},
clear: function () {
$traceurRuntime.superCall(this, $ItemResource.prototype, 'clear', []);
+ },
+ autoPage: function (type, id) {
+ return this.http({
+ url: this.BASE_URL + '/items',
+ method: 'GET',
+ params: {
+ type: type,
+ id: id,
+ offset: this.size(),
+ limit: this.batchSize
+ }
+ });
}
}, {}, Resource);
- return new ItemResource($http, BASE_URL);
+ return new ItemResource($http, BASE_URL, ITEM_BATCH_SIZE);
}
]);
app.service('Loading', function () {
diff --git a/js/controller/ContentController.js b/js/controller/ContentController.js
index 804c898e5..5a75f68ab 100644
--- a/js/controller/ContentController.js
+++ b/js/controller/ContentController.js
@@ -8,7 +8,8 @@
* @copyright Bernhard Posselt 2014
*/
app.controller('ContentController',
-function (Publisher, FeedResource, ItemResource, SettingsResource, data) {
+function (Publisher, FeedResource, ItemResource, SettingsResource, data,
+ $route, $routeParams) {
'use strict';
this.isAutoPagingEnabled = true;
@@ -27,10 +28,12 @@ function (Publisher, FeedResource, ItemResource, SettingsResource, data) {
};
this.markRead = (itemId) => {
- ItemResource.markItemRead(itemId);
-
let item = ItemResource.get(itemId);
- FeedResource.markItemOfFeedRead(item.feedId);
+
+ if (!item.keepUnread) {
+ ItemResource.markItemRead(itemId);
+ FeedResource.markItemOfFeedRead(item.feedId);
+ }
};
this.getFeed = (feedId) => {
@@ -59,25 +62,48 @@ function (Publisher, FeedResource, ItemResource, SettingsResource, data) {
return SettingsResource.get('compact');
};
- // TBD
- this.getRelativeDate = (timestamp) => {
- console.log(timestamp);
+ this.autoPagingEnabled = () => {
+ return this.isAutoPagingEnabled;
};
- this.autoPage = () => {
- console.log('hi');
+ this.markReadEnabled = () => {
+ return !SettingsResource.get('preventReadOnScroll');
};
this.scrollRead = (itemIds) => {
- console.log(itemIds);
+ let ids = [];
+
+ for (let itemId of itemIds) {
+ let item = ItemResource.get(itemId);
+ if (!item.keepUnread) {
+ ids.push(itemId);
+ FeedResource.markItemOfFeedRead(item.feedId);
+ }
+ }
+
+ ItemResource.markItemsRead(ids);
};
- this.autoPagingEnabled = () => {
- return this.isAutoPagingEnabled;
+ this.autoPage = () => {
+ this.isAutoPagingEnabled = false;
+
+ let type = $route.current.$$route.type;
+ let id = $routeParams.id;
+
+ ItemResource.autoPage(type, id).success((data) => {
+ Publisher.publishAll(data);
+
+ if (data.items.length > 0) {
+ this.isAutoPagingEnabled = true;
+ }
+ }).error(() => {
+ this.isAutoPagingEnabled = true;
+ });
};
- this.markReadEnabled = () => {
- return !SettingsResource.get('preventReadOnScroll');
+ // TBD
+ this.getRelativeDate = (timestamp) => {
+ console.log(timestamp);
};
}); \ No newline at end of file
diff --git a/js/service/ItemResource.js b/js/service/ItemResource.js
index 625f58b5e..f0bc0d485 100644
--- a/js/service/ItemResource.js
+++ b/js/service/ItemResource.js
@@ -7,15 +7,16 @@
* @author Bernhard Posselt <dev@bernhard-posselt.com>
* @copyright Bernhard Posselt 2014
*/
-app.factory('ItemResource', (Resource, $http, BASE_URL) => {
+app.factory('ItemResource', (Resource, $http, BASE_URL, ITEM_BATCH_SIZE) => {
'use strict';
class ItemResource extends Resource {
- constructor ($http, BASE_URL) {
+ constructor ($http, BASE_URL, ITEM_BATCH_SIZE) {
super($http, BASE_URL);
this.starredCount = 0;
+ this.batchSize = ITEM_BATCH_SIZE;
}
@@ -88,6 +89,21 @@ app.factory('ItemResource', (Resource, $http, BASE_URL) => {
}
+ markItemsRead (itemIds) {
+ for (let itemId of itemIds) {
+ this.get(itemId).unread = false;
+ }
+
+ return this.http({
+ url: `${this.BASE_URL}/items/read/multiple`,
+ method: 'POST',
+ data: {
+ itemIds: itemIds
+ }
+ });
+ }
+
+
markFeedRead (feedId, read=true) {
for (let item of this.values.filter(i => i.feedId === feedId)) {
item.unread = !read;
@@ -108,7 +124,21 @@ app.factory('ItemResource', (Resource, $http, BASE_URL) => {
super.clear();
}
+
+ autoPage (type, id) {
+ return this.http({
+ url: `${this.BASE_URL}/items`,
+ method: 'GET',
+ params: {
+ type: type,
+ id: id,
+ offset: this.size(),
+ limit: this.batchSize
+ }
+ });
+ }
+
}
- return new ItemResource($http, BASE_URL);
+ return new ItemResource($http, BASE_URL, ITEM_BATCH_SIZE);
}); \ No newline at end of file
diff --git a/js/tests/unit/controller/ContentControllerSpec.js b/js/tests/unit/controller/ContentControllerSpec.js
index 8757cb5c5..d814a56de 100644
--- a/js/tests/unit/controller/ContentControllerSpec.js
+++ b/js/tests/unit/controller/ContentControllerSpec.js
@@ -12,7 +12,8 @@ describe('ContentController', () => {
beforeEach(module('News', ($provide) => {
- $provide.value('BASE_URL', 'base');
+ $provide.constant('BASE_URL', 'base');
+ $provide.constant('ITEM_BATCH_SIZE', 5);
}));
@@ -78,14 +79,22 @@ describe('ContentController', () => {
'items': [{
id: 3,
feedId: 4
+ },
+ {
+ id: 5,
+ feedId: 4,
+ keepUnread: true
}]
},
});
ctrl.markRead(3);
+ ctrl.markRead(5);
expect(ItemResource.markItemRead).toHaveBeenCalledWith(3);
expect(FeedResource.markItemOfFeedRead).toHaveBeenCalledWith(4);
+ expect(ItemResource.markItemRead.callCount).toBe(1);
+ expect(FeedResource.markItemOfFeedRead.callCount).toBe(1);
}));
@@ -209,4 +218,184 @@ describe('ContentController', () => {
}));
+ it('should mark multiple items read', inject(($controller,
+ ItemResource, FeedResource, Publisher) => {
+
+ Publisher.subscribe(ItemResource).toChannels('items');
+ ItemResource.markItemsRead = jasmine.createSpy('markRead');
+ FeedResource.markItemOfFeedRead = jasmine.createSpy('markRead');
+
+ let ctrl = $controller('ContentController', {
+ ItemResource: ItemResource,
+ FeedResource: FeedResource,
+ data: {
+ 'items': [{
+ id: 3,
+ feedId: 4
+ },
+ {
+ id: 2,
+ feedId: 4,
+ keepUnread: true
+ },
+ {
+ id: 1,
+ feedId: 4
+ },]
+ },
+ });
+
+ ctrl.scrollRead([3, 2, 1]);
+
+ expect(ItemResource.markItemsRead).toHaveBeenCalledWith([3, 1]);
+ expect(FeedResource.markItemOfFeedRead.callCount).toBe(2);
+ }));
+
+
+ it('should not autopage if less than 0 elements', inject((
+ $controller, ItemResource, Publisher) => {
+
+ let $route = {
+ current: {
+ $$route: {
+ type: 3
+ }
+ }
+ };
+
+ let $routeParams = {
+ id: 2
+ };
+
+ Publisher.subscribe(ItemResource).toChannels('items');
+ ItemResource.autoPage = jasmine.createSpy('autoPage')
+ .andCallFake(() => {
+ return {
+ success: (callback) => {
+ callback({
+ 'items': []
+ });
+
+ return {
+ error: () => {}
+ };
+ }
+ }
+ });
+
+ let ctrl = $controller('ContentController', {
+ $routeParams: $routeParams,
+ $route: $route,
+ Publisher: Publisher,
+ ItemResource: ItemResource,
+ data: {},
+ });
+
+ expect(ctrl.autoPagingEnabled()).toBe(true);
+
+ ctrl.autoPage();
+
+ expect(ctrl.autoPagingEnabled()).toBe(false);
+
+ expect(ItemResource.autoPage).toHaveBeenCalledWith(3, 2);
+
+ }));
+
+
+ it('should autopage if more than 0 elements', inject((
+ $controller, ItemResource, Publisher) => {
+
+ let $route = {
+ current: {
+ $$route: {
+ type: 3
+ }
+ }
+ };
+
+ let $routeParams = {
+ id: 2
+ };
+
+ Publisher.subscribe(ItemResource).toChannels('items');
+ ItemResource.autoPage = jasmine.createSpy('autoPage')
+ .andCallFake(() => {
+ return {
+ success: (callback) => {
+ callback({
+ 'items': [{items: [{id: 3}]}]
+ });
+
+ return {
+ error: () => {}
+ };
+ }
+ }
+ });
+
+ let ctrl = $controller('ContentController', {
+ $routeParams: $routeParams,
+ $route: $route,
+ Publisher: Publisher,
+ ItemResource: ItemResource,
+ data: {},
+ });
+
+ expect(ctrl.autoPagingEnabled()).toBe(true);
+
+ ctrl.autoPage();
+
+ expect(ctrl.autoPagingEnabled()).toBe(true);
+ expect(ItemResource.size()).toBe(1);
+ }));
+
+
+ it('should autopage if error', inject((
+ $controller, ItemResource, Publisher) => {
+
+ let $route = {
+ current: {
+ $$route: {
+ type: 3
+ }
+ }
+ };
+
+ let $routeParams = {
+ id: 2
+ };
+
+ Publisher.subscribe(ItemResource).toChannels('items');
+ ItemResource.autoPage = jasmine.createSpy('autoPage')
+ .andCallFake(() => {
+ return {
+ success: (callback) => {
+ callback({
+ 'items': []
+ });
+
+ return {
+ error: (callback) => {
+ callback();
+ }
+ };
+ }
+ }
+ });
+
+ let ctrl = $controller('ContentController', {
+ $routeParams: $routeParams,
+ $route: $route,
+ Publisher: Publisher,
+ ItemResource: ItemResource,
+ data: {},
+ });
+
+ expect(ctrl.autoPagingEnabled()).toBe(true);
+
+ ctrl.autoPage();
+
+ expect(ctrl.autoPagingEnabled()).toBe(true);
+ }));
+
});
diff --git a/js/tests/unit/controller/NavigationControllerSpec.js b/js/tests/unit/controller/NavigationControllerSpec.js
index 653dfea26..433a40e3f 100644
--- a/js/tests/unit/controller/NavigationControllerSpec.js
+++ b/js/tests/unit/controller/NavigationControllerSpec.js
@@ -14,6 +14,7 @@ describe('NavigationController', () => {
beforeEach(module('News', ($provide) => {
$provide.value('BASE_URL', 'base');
+ $provide.constant('ITEM_BATCH_SIZE', 5);
}));
beforeEach(inject(($controller) => {
diff --git a/js/tests/unit/service/ItemResourceSpec.js b/js/tests/unit/service/ItemResourceSpec.js
index 50454d41a..f93d92caa 100644
--- a/js/tests/unit/service/ItemResourceSpec.js
+++ b/js/tests/unit/service/ItemResourceSpec.js
@@ -14,6 +14,7 @@ describe('ItemResource', () => {
beforeEach(module('News', ($provide) => {
$provide.value('BASE_URL', 'base');
+ $provide.constant('ITEM_BATCH_SIZE', 5);
}));
beforeEach(inject(($httpBackend) => {
@@ -59,6 +60,34 @@ describe('ItemResource', () => {
}));
+ it ('should mark multiple item as read', inject((ItemResource) => {
+ http.expectPOST('base/items/read/multiple', {
+ itemIds: [3, 4]
+ }).respond(200, {});
+
+ ItemResource.receive([
+ {
+ id: 3,
+ feedId: 4,
+ unread: true
+ },
+ {
+ id: 4,
+ feedId: 3,
+ unread: true
+ }
+ ], 'items');
+
+ ItemResource.markItemsRead([3, 4]);
+
+ http.flush();
+
+ expect(ItemResource.get(3).unread).toBe(false);
+ expect(ItemResource.get(4).unread).toBe(false);
+ }));
+
+
+
it ('should star item', inject((ItemResource) => {
http.expectPOST('base/items/4/a/star', {isStarred: true})
.respond(200, {});
@@ -169,6 +198,34 @@ describe('ItemResource', () => {
}));
+ it ('should auto page', inject((ItemResource) => {
+ http.expectGET('base/items?id=4&limit=5&offset=3&type=3')
+ .respond(200, {});
+
+ ItemResource.receive([
+ {
+ id: 3,
+ feedId: 4,
+ unread: true
+ },
+ {
+ id: 4,
+ feedId: 3,
+ unread: true
+ },
+ {
+ id: 5,
+ feedId: 4,
+ unread: true
+ }
+ ], 'items');
+
+ ItemResource.autoPage(3, 4);
+
+ http.flush();
+ }));
+
+
afterEach(() => {
http.verifyNoOutstandingExpectation();
http.verifyNoOutstandingRequest();
diff --git a/templates/main.php b/templates/main.php
index 1656dab25..b8ba812bb 100644
--- a/templates/main.php
+++ b/templates/main.php
@@ -50,6 +50,6 @@
news-scroll-enabled-auto-page="Content.isAutoPaging()"
news-scroll-enabled-mark-read="Content.isMarkRead()"
news-scroll-auto-page="Content.autoPage()"
- news-scroll-mark-read="Content.markRead($itemIds)"></div>
+ news-scroll-mark-read="Content.scrollRead($itemIds)"></div>
</div>
diff --git a/tests/unit/controller/ItemControllerTest.php b/tests/unit/controller/ItemControllerTest.php
index 4b57068a9..59bb7681b 100644
--- a/tests/unit/controller/ItemControllerTest.php
+++ b/tests/unit/controller/ItemControllerTest.php
@@ -87,6 +87,21 @@ class ItemControllerTest extends \PHPUnit_Framework_TestCase {
}
+ public function testReadMultiple() {
+ $this->itemService->expects($this->at(0))
+ ->method('read')
+ ->with($this->equalTo(2),
+ $this->equalTo(true),
+ $this->equalTo($this->user));
+ $this->itemService->expects($this->at(1))
+ ->method('read')
+ ->with($this->equalTo(4),
+ $this->equalTo(true),
+ $this->equalTo($this->user));
+ $this->controller->readMultiple([2, 4]);
+ }
+
+
public function testStar(){
$this->itemService->expects($this->once())
->method('star')