diff --git a/src/newsreader/js/pages/homepage/App.js b/src/newsreader/js/pages/homepage/App.js
index 5b5acde..1834bee 100644
--- a/src/newsreader/js/pages/homepage/App.js
+++ b/src/newsreader/js/pages/homepage/App.js
@@ -56,7 +56,7 @@ const mapStateToProps = state => {
const { error } = state.error;
if (!isEqual(state.selected.post, {})) {
- const ruleId = state.selected.post.rule;
+ const ruleId = state.selected.post.rule.id;
const rule = state.rules.items[ruleId];
const category = state.categories.items[rule.category];
diff --git a/src/newsreader/js/pages/homepage/components/postlist/PostItem.js b/src/newsreader/js/pages/homepage/components/postlist/PostItem.js
index ffb6832..ddd60db 100644
--- a/src/newsreader/js/pages/homepage/components/postlist/PostItem.js
+++ b/src/newsreader/js/pages/homepage/components/postlist/PostItem.js
@@ -16,7 +16,7 @@ import { formatDatetime } from '../../../../utils.js';
class PostItem extends React.Component {
render() {
const rule = { ...this.props.post.rule };
- const post = { ...this.props.post, rule: rule.id };
+ const post = { ...this.props.post };
const token = Cookies.get('csrftoken');
const publicationDate = formatDatetime(post.publicationDate);
diff --git a/src/newsreader/js/pages/homepage/components/postlist/PostList.js b/src/newsreader/js/pages/homepage/components/postlist/PostList.js
index 168ca8e..197d81b 100644
--- a/src/newsreader/js/pages/homepage/components/postlist/PostList.js
+++ b/src/newsreader/js/pages/homepage/components/postlist/PostList.js
@@ -56,8 +56,9 @@ class PostList extends React.Component {
}
render() {
+ const isLastItem = this.props.postsByType.toReversed().find(item => !item.read);
+
const postItems = this.props.postsByType.map((item, index) => {
- const isLastItem = this.props.postsByType.length - 1 == index;
const defaultProps = {
key: index,
post: item,
@@ -68,7 +69,7 @@ class PostList extends React.Component {
timezone: this.props.timezone,
};
- if (isLastItem && !item.read) {
+ if (isLastItem?.id === item.id) {
return ;
} else {
return ;
diff --git a/src/newsreader/js/pages/homepage/components/postlist/filters.js b/src/newsreader/js/pages/homepage/components/postlist/filters.js
index 8439fc9..b3dd16a 100644
--- a/src/newsreader/js/pages/homepage/components/postlist/filters.js
+++ b/src/newsreader/js/pages/homepage/components/postlist/filters.js
@@ -4,31 +4,10 @@ const isEmpty = (object = {}) => {
return Object.keys(object).length === 0;
};
-const sortOrdering = (firstPost, secondPost) => {
- const dateOrdering =
- new Date(firstPost.publicationDate) < new Date(secondPost.publicationDate);
-
- if (firstPost.read && !secondPost.read) {
- return 1;
- } else if (secondPost.read && !firstPost.read) {
- return -1;
- }
-
- return dateOrdering;
-};
-
-const savedOrdering = (firstPost, secondPost) => {
- return new Date(firstPost.publicationDate) < new Date(secondPost.publicationDate);
-};
-
export const filterPostsByRule = (rule = {}, posts = []) => {
- const filteredPosts = posts.filter(post => {
- return post.rule === rule.id;
+ return posts.filter(post => {
+ return post.rule.id === rule.id;
});
-
- const filteredData = filteredPosts.map(post => ({ ...post, rule: { ...rule } }));
-
- return filteredData.sort(sortOrdering);
};
export const filterPostsByCategory = (category = {}, rules = [], posts = []) => {
@@ -36,24 +15,13 @@ export const filterPostsByCategory = (category = {}, rules = [], posts = []) =>
return rule.category === category.id;
});
- const filteredData = filteredRules.map(rule => {
- const filteredPosts = posts.filter(post => {
- return post.rule === rule.id;
- });
+ const ruleIds = filteredRules.map(rule => rule.id);
- return filteredPosts.map(post => ({ ...post, rule: { ...rule } }));
- });
-
- const sortedPosts = [...filteredData.flat()].sort(sortOrdering);
-
- return sortedPosts;
+ return [...posts].filter(post => ruleIds.includes(post.rule.id));
};
export const filterPostsBySaved = (rules = [], posts = []) => {
- const filteredPosts = posts.filter(post => post.saved);
- return filteredPosts
- .map(post => ({ ...post, rule: { ...rules.find(rule => rule.id === post.rule) } }))
- .sort(savedOrdering);
+ return [...posts].filter(post => post.saved);
};
export const filterPosts = state => {
diff --git a/src/newsreader/js/pages/homepage/reducers/posts.js b/src/newsreader/js/pages/homepage/reducers/posts.js
index dd795a0..2c1ff86 100644
--- a/src/newsreader/js/pages/homepage/reducers/posts.js
+++ b/src/newsreader/js/pages/homepage/reducers/posts.js
@@ -1,5 +1,3 @@
-import { isEqual } from 'lodash';
-
import { objectsFromArray } from '../../../utils.js';
import { CATEGORY_TYPE, RULE_TYPE } from '../constants.js';
@@ -12,58 +10,83 @@ import {
TOGGLING_POST,
TOGGLED_POST,
} from '../actions/posts.js';
-import { SELECT_CATEGORY } from '../actions/categories.js';
-import { SELECT_RULE } from '../actions/rules.js';
import { MARK_SECTION_READ } from '../actions/selected.js';
-const defaultState = { items: {}, isFetching: false, isUpdating: false };
+const sortOrdering = (firstPost, secondPost) => {
+ const dateOrdering =
+ new Date(firstPost.publicationDate) < new Date(secondPost.publicationDate);
+
+ if (firstPost.read && !secondPost.read) {
+ return 1;
+ } else if (secondPost.read && !firstPost.read) {
+ return -1;
+ }
+
+ return dateOrdering;
+};
+
+const defaultState = { items: [], isFetching: false, isUpdating: false };
export const posts = (state = { ...defaultState }, action) => {
switch (action.type) {
case REQUEST_POSTS:
return { ...state, isFetching: true };
case RECEIVE_POST:
+ const postIndex = state.items.findIndex(post => post.id === action.post.id);
+ let newItems = [...state.items];
+
+ if (postIndex >= 0) {
+ newItems.splice(postIndex, 1, { ...action.post });
+ } else {
+ newItems = [...state.items, { ...action.post }];
+ }
+
+ newItems.sort(sortOrdering);
+
return {
...state,
- items: { ...state.items, [action.post.id]: { ...action.post } },
+ items: newItems,
};
case RECEIVE_POSTS:
- const receivedItems = objectsFromArray(action.posts, 'id');
+ const receivedPosts = objectsFromArray(action.posts, 'id');
+
+ let mergedPosts = { ...objectsFromArray(state.items, 'id'), ...receivedPosts };
+ let sortedPosts = Object.values(mergedPosts).sort(sortOrdering);
return {
...state,
isFetching: false,
- items: { ...state.items, ...receivedItems },
+ items: sortedPosts,
};
case MARK_SECTION_READ:
- const updatedPosts = {};
- let relatedPosts = [];
+ let posts = [];
switch (action.section.type) {
case CATEGORY_TYPE:
- relatedPosts = Object.values({ ...state.items }).filter(post => {
- return post.rule in { ...action.section.rules };
+ posts = [...state.items];
+
+ posts.forEach(post => {
+ if (!(post.rule.id in { ...action.section.rules })) return;
+
+ post.read = true;
});
break;
case RULE_TYPE:
- relatedPosts = Object.values({ ...state.items }).filter(post => {
- return post.rule === action.section.id;
+ posts = [...state.items];
+
+ posts.forEach(post => {
+ if (post.rule.id != action.section.id) return;
+
+ post.read = true;
});
break;
}
- relatedPosts.forEach(post => {
- updatedPosts[post.id] = { ...post, read: true };
- });
-
return {
...state,
- items: {
- ...state.items,
- ...updatedPosts,
- },
+ items: posts,
};
case MARKING_POST:
return { ...state, isUpdating: true };
diff --git a/src/newsreader/js/tests/homepage/reducers/post.test.js b/src/newsreader/js/tests/homepage/reducers/post.test.js
index adb8983..249a9f1 100644
--- a/src/newsreader/js/tests/homepage/reducers/post.test.js
+++ b/src/newsreader/js/tests/homepage/reducers/post.test.js
@@ -6,7 +6,7 @@ import * as actions from '../../../pages/homepage/actions/posts.js';
import * as selectedActions from '../../../pages/homepage/actions/selected.js';
import * as constants from '../../../pages/homepage/constants.js';
-const defaultState = { items: {}, isFetching: false };
+const defaultState = { items: [], isFetching: false };
describe('post actions', () => {
it('should return state after requesting posts', () => {
@@ -42,7 +42,7 @@ describe('post actions', () => {
...defaultState,
isFetching: false,
isUpdating: false,
- items: { [post.id]: post },
+ items: [post],
};
expect(reducer(undefined, action)).toEqual(expectedState);
@@ -85,12 +85,11 @@ describe('post actions', () => {
posts,
};
- const expectedPosts = objectsFromArray(posts, 'id');
const expectedState = {
...defaultState,
isFetching: false,
isUpdating: false,
- items: expectedPosts,
+ items: posts,
};
expect(reducer(undefined, action)).toEqual(expectedState);
@@ -108,7 +107,14 @@ describe('post actions', () => {
author: 'Kyle Orland',
publicationDate: '2020-01-24T19:50:12Z',
url: 'https://arstechnica.com/?p=1648607',
- rule: 5,
+ rule: {
+ id: 5,
+ name: 'Ars Technica',
+ url: 'http://feeds.arstechnica.com/arstechnica/index?fmt=xml',
+ favicon: 'https://cdn.arstechnica.net/favicon.ico',
+ category: 9,
+ unread: 544,
+ },
read: false,
},
2141: {
@@ -120,7 +126,14 @@ describe('post actions', () => {
author: 'WIRED',
publicationDate: '2020-01-25T11:06:46Z',
url: 'https://arstechnica.com/?p=1648757',
- rule: 5,
+ rule: {
+ id: 5,
+ name: 'Ars Technica',
+ url: 'http://feeds.arstechnica.com/arstechnica/index?fmt=xml',
+ favicon: 'https://cdn.arstechnica.net/favicon.ico',
+ category: 9,
+ unread: 544,
+ },
read: false,
},
4637: {
@@ -132,7 +145,9 @@ describe('post actions', () => {
author: null,
publicationDate: '2020-01-29T19:08:25Z',
url: 'https://www.bbc.co.uk/news/world-asia-china-51299195',
- rule: 4,
+ rule: {
+ id: 4,
+ },
read: false,
saved: false,
},
@@ -145,7 +160,9 @@ describe('post actions', () => {
author: null,
publicationDate: '2020-01-29T18:27:56Z',
url: 'https://www.bbc.co.uk/news/world-europe-51294305',
- rule: 4,
+ rule: {
+ id: 4,
+ },
read: false,
saved: false,
},
@@ -165,16 +182,17 @@ describe('post actions', () => {
section: { ...rule, type: constants.RULE_TYPE },
};
- const state = { ...defaultState, items: { ...posts } };
+ const state = { ...defaultState, items: Object.values(posts) };
const expectedState = {
...defaultState,
isFetching: false,
- items: {
- ...posts,
- 2067: { ...posts[2067], read: true },
- 2141: { ...posts[2141], read: true },
- },
+ items: [
+ { ...posts[2067], read: true },
+ { ...posts[2141], read: true },
+ { ...posts[4637] },
+ { ...posts[4638] },
+ ],
};
expect(reducer(state, action)).toEqual(expectedState);
@@ -192,7 +210,15 @@ describe('post actions', () => {
author: 'Kyle Orland',
publicationDate: '2020-01-24T19:50:12Z',
url: 'https://arstechnica.com/?p=1648607',
- rule: 5,
+ rule: {
+ id: 5,
+ name: 'BBC',
+ url: 'http://feeds.bbci.co.uk/news/world/rss.xml',
+ favicon:
+ 'https://m.files.bbci.co.uk/modules/bbc-morph-news-waf-page-meta/2.5.2/apple-touch-icon-57x57-precomposed.png',
+ category: 8,
+ unread: 632,
+ },
read: false,
saved: false,
},
@@ -205,7 +231,15 @@ describe('post actions', () => {
author: 'WIRED',
publicationDate: '2020-01-25T11:06:46Z',
url: 'https://arstechnica.com/?p=1648757',
- rule: 5,
+ rule: {
+ id: 5,
+ name: 'BBC',
+ url: 'http://feeds.bbci.co.uk/news/world/rss.xml',
+ favicon:
+ 'https://m.files.bbci.co.uk/modules/bbc-morph-news-waf-page-meta/2.5.2/apple-touch-icon-57x57-precomposed.png',
+ category: 8,
+ unread: 632,
+ },
read: false,
saved: false,
},
@@ -218,7 +252,15 @@ describe('post actions', () => {
author: null,
publicationDate: '2020-01-29T19:08:25Z',
url: 'https://www.bbc.co.uk/news/world-asia-china-51299195',
- rule: 4,
+ rule: {
+ id: 4,
+ name: 'BBC',
+ url: 'http://feeds.bbci.co.uk/news/world/rss.xml',
+ favicon:
+ 'https://m.files.bbci.co.uk/modules/bbc-morph-news-waf-page-meta/2.5.2/apple-touch-icon-57x57-precomposed.png',
+ category: 8,
+ unread: 321,
+ },
read: false,
saved: false,
},
@@ -231,7 +273,15 @@ describe('post actions', () => {
author: null,
publicationDate: '2020-01-29T18:27:56Z',
url: 'https://www.bbc.co.uk/news/world-europe-51294305',
- rule: 4,
+ rule: {
+ id: 4,
+ name: 'BBC',
+ url: 'http://feeds.bbci.co.uk/news/world/rss.xml',
+ favicon:
+ 'https://m.files.bbci.co.uk/modules/bbc-morph-news-waf-page-meta/2.5.2/apple-touch-icon-57x57-precomposed.png',
+ category: 8,
+ unread: 321,
+ },
read: false,
saved: false,
},
@@ -245,7 +295,9 @@ describe('post actions', () => {
publicationDate: '2020-01-29T19:03:01Z',
url:
'https://tweakers.net/nieuws/162878/analyse-nintendo-verdiende-miljard-dollar-aan-mobiele-games.html',
- rule: 7,
+ rule: {
+ id: 7,
+ },
read: false,
saved: false,
},
@@ -259,7 +311,9 @@ describe('post actions', () => {
publicationDate: '2020-01-29T16:29:40Z',
url:
'https://tweakers.net/nieuws/162870/samsung-kondigt-eerste-tablet-met-5g-aan.html',
- rule: 7,
+ rule: {
+ id: 7,
+ },
read: false,
saved: false,
},
@@ -276,7 +330,7 @@ describe('post actions', () => {
unread: 321,
},
5: {
- id: 4,
+ id: 5,
name: 'BBC',
url: 'http://feeds.bbci.co.uk/news/world/rss.xml',
favicon:
@@ -301,18 +355,19 @@ describe('post actions', () => {
},
};
- const state = { ...defaultState, items: { ...posts } };
+ const state = { ...defaultState, items: Object.values(posts) };
const expectedState = {
...defaultState,
isFetching: false,
- items: {
- ...posts,
- 2067: { ...posts[2067], read: true },
- 2141: { ...posts[2141], read: true },
- 4637: { ...posts[4637], read: true },
- 4638: { ...posts[4638], read: true },
- },
+ items: [
+ { ...posts[2067], read: true },
+ { ...posts[2141], read: true },
+ { ...posts[4589] },
+ { ...posts[4594] },
+ { ...posts[4637], read: true },
+ { ...posts[4638], read: true },
+ ],
};
expect(reducer(state, action)).toEqual(expectedState);
diff --git a/src/newsreader/news/collection/tests/endpoints/rule/list/tests.py b/src/newsreader/news/collection/tests/endpoints/rule/list/tests.py
index 5a34dbc..2ae3810 100644
--- a/src/newsreader/news/collection/tests/endpoints/rule/list/tests.py
+++ b/src/newsreader/news/collection/tests/endpoints/rule/list/tests.py
@@ -202,7 +202,7 @@ class NestedRuleListViewTestCase(TestCase):
for post in data["results"]:
with self.subTest(post=post):
- self.assertEqual(post["rule"], rule.pk)
+ self.assertEqual(post["rule"]["id"], rule.pk)
def test_unread_posts(self):
rule = FeedFactory.create(user=self.user)
diff --git a/src/newsreader/news/core/serializers.py b/src/newsreader/news/core/serializers.py
index 38619a1..c375dde 100644
--- a/src/newsreader/news/core/serializers.py
+++ b/src/newsreader/news/core/serializers.py
@@ -1,5 +1,6 @@
from rest_framework import serializers
+from newsreader.news.collection.serializers import RuleSerializer
from newsreader.news.core.models import Category, Post
@@ -9,6 +10,8 @@ class PostSerializer(serializers.ModelSerializer):
)
remoteIdentifier = serializers.CharField(source="remote_identifier", required=False)
+ rule = RuleSerializer(read_only=True)
+
class Meta:
model = Post
fields = (
diff --git a/src/newsreader/news/core/tests/endpoints/category/list/tests.py b/src/newsreader/news/core/tests/endpoints/category/list/tests.py
index c822950..65f4433 100644
--- a/src/newsreader/news/core/tests/endpoints/category/list/tests.py
+++ b/src/newsreader/news/core/tests/endpoints/category/list/tests.py
@@ -510,8 +510,8 @@ class NestedCategoryPostView(TestCase):
self.assertEqual(response.status_code, 200)
self.assertEqual(len(data["results"]), 2)
- self.assertEqual(posts[0]["rule"], guardian_rule.pk)
- self.assertEqual(posts[1]["rule"], guardian_rule.pk)
+ self.assertEqual(posts[0]["rule"]["id"], guardian_rule.pk)
+ self.assertEqual(posts[1]["rule"]["id"], guardian_rule.pk)
def test_unread_posts(self):
category = CategoryFactory.create(user=self.user)
diff --git a/src/newsreader/news/core/tests/endpoints/post/detail/tests.py b/src/newsreader/news/core/tests/endpoints/post/detail/tests.py
index 92444cc..295f92f 100644
--- a/src/newsreader/news/core/tests/endpoints/post/detail/tests.py
+++ b/src/newsreader/news/core/tests/endpoints/post/detail/tests.py
@@ -111,6 +111,7 @@ class PostDetailViewTestCase(TestCase):
data=json.dumps({"title": "This title is very accurate"}),
content_type="application/json",
)
+
data = response.json()
self.assertEqual(response.status_code, 200)