From 116b6d1308bd232357027ca3c138f8e344d131c6 Mon Sep 17 00:00:00 2001 From: Sonny Bakker Date: Sat, 31 Oct 2020 14:31:27 +0100 Subject: [PATCH] 0.3.5 - Show timezone next to post datetimes - Take read status in consideration when sorting posts --- src/newsreader/accounts/forms.py | 4 +- .../migrations/0013_user_auto_mark_read.py | 20 ++++++++ src/newsreader/accounts/models.py | 9 ++++ src/newsreader/conf/docker.py | 18 ++++++- src/newsreader/js/pages/homepage/App.js | 11 ++-- .../js/pages/homepage/actions/posts.js | 5 ++ .../js/pages/homepage/components/PostModal.js | 50 +++++++++++++------ .../homepage/components/postlist/PostItem.js | 2 +- .../homepage/components/postlist/PostList.js | 1 + .../homepage/components/postlist/filters.js | 21 +++++--- src/newsreader/js/pages/homepage/index.js | 16 +++--- .../js/pages/homepage/reducers/posts.js | 8 ++- .../js/tests/homepage/actions/post.test.js | 10 +++- .../js/tests/homepage/reducers/post.test.js | 4 +- .../templates/news/core/views/homepage.html | 5 +- src/newsreader/news/core/views.py | 21 +++++--- .../scss/components/post/_post.scss | 12 +++-- .../scss/components/sidebar/_sidebar.scss | 4 ++ .../scss/elements/button/_read-button.scss | 2 - 19 files changed, 166 insertions(+), 57 deletions(-) create mode 100644 src/newsreader/accounts/migrations/0013_user_auto_mark_read.py diff --git a/src/newsreader/accounts/forms.py b/src/newsreader/accounts/forms.py index 7a29f99..6247eff 100644 --- a/src/newsreader/accounts/forms.py +++ b/src/newsreader/accounts/forms.py @@ -1,9 +1,11 @@ from django import forms from newsreader.accounts.models import User +from newsreader.core.forms import CheckboxInput class UserSettingsForm(forms.ModelForm): class Meta: model = User - fields = ("first_name", "last_name") + fields = ("first_name", "last_name", "auto_mark_read") + widgets = {"auto_mark_read": CheckboxInput} diff --git a/src/newsreader/accounts/migrations/0013_user_auto_mark_read.py b/src/newsreader/accounts/migrations/0013_user_auto_mark_read.py new file mode 100644 index 0000000..3d975e0 --- /dev/null +++ b/src/newsreader/accounts/migrations/0013_user_auto_mark_read.py @@ -0,0 +1,20 @@ +# Generated by Django 3.0.7 on 2020-10-27 21:45 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [("accounts", "0012_remove_user_task")] + + operations = [ + migrations.AddField( + model_name="user", + name="auto_mark_read", + field=models.BooleanField( + default=True, + help_text="Wether posts should be marked as read after x amount of seconds of reading", + verbose_name="Auto read marking", + ), + ) + ] diff --git a/src/newsreader/accounts/models.py b/src/newsreader/accounts/models.py index 2451445..c46dd93 100644 --- a/src/newsreader/accounts/models.py +++ b/src/newsreader/accounts/models.py @@ -45,6 +45,15 @@ class User(AbstractUser): twitter_oauth_token = models.CharField(max_length=255, blank=True, null=True) twitter_oauth_token_secret = models.CharField(max_length=255, blank=True, null=True) + # settings + auto_mark_read = models.BooleanField( + _("Auto read marking"), + default=True, + help_text=_( + "Wether posts should be marked as read after x amount of seconds of reading" + ), + ) + username = None objects = UserManager() diff --git a/src/newsreader/conf/docker.py b/src/newsreader/conf/docker.py index dd2471f..f5665fa 100644 --- a/src/newsreader/conf/docker.py +++ b/src/newsreader/conf/docker.py @@ -1,8 +1,14 @@ -from .dev import * # isort:skip +from .base import * # isort:skip SECRET_KEY = "=q(ztyo)b6noom#a164g&s9vcj1aawa^g#ing_ir99=_zl4g&$" +INSTALLED_APPS += ["debug_toolbar", "django_extensions"] + +MIDDLEWARE += ["debug_toolbar.middleware.DebugToolbarMiddleware"] + +EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" + DATABASES = { "default": { "ENGINE": "django.db.backends.postgresql", @@ -24,6 +30,16 @@ CACHES = { }, } +# Third party settings +# Axes +AXES_FAILURE_LIMIT = 50 +AXES_COOLOFF_TIME = None + # Celery # https://docs.celeryproject.org/en/latest/userguide/configuration.html CELERY_BROKER_URL = "amqp://guest:guest@rabbitmq:5672//" + +try: + from .local import * # noqa +except ImportError: + pass diff --git a/src/newsreader/js/pages/homepage/App.js b/src/newsreader/js/pages/homepage/App.js index 77b6222..0b2aedb 100644 --- a/src/newsreader/js/pages/homepage/App.js +++ b/src/newsreader/js/pages/homepage/App.js @@ -23,12 +23,9 @@ class App extends React.Component { feedUrl={this.props.feedUrl} subredditUrl={this.props.subredditUrl} timelineUrl={this.props.timelineUrl} + timezone={this.props.timezone} /> - {this.props.error && ( - - )} - {!isEqual(this.props.post, {}) && ( )} + + {this.props.error && ( + + )} ); } diff --git a/src/newsreader/js/pages/homepage/actions/posts.js b/src/newsreader/js/pages/homepage/actions/posts.js index b7ad5cb..f04f3e1 100644 --- a/src/newsreader/js/pages/homepage/actions/posts.js +++ b/src/newsreader/js/pages/homepage/actions/posts.js @@ -9,6 +9,7 @@ export const RECEIVE_POST = 'RECEIVE_POST'; export const REQUEST_POSTS = 'REQUEST_POSTS'; export const MARK_POST_READ = 'MARK_POST_READ'; +export const MARKING_POST = 'MARKING_POST'; export const requestPosts = () => ({ type: REQUEST_POSTS }); @@ -30,10 +31,14 @@ export const postRead = (post, section) => ({ section, }); +export const markingPostRead = () => ({ type: MARKING_POST }); + export const markPostRead = (post, token) => { return (dispatch, getState) => { const { selected } = getState(); + dispatch(markingPostRead()); + const url = `/api/posts/${post.id}/`; const options = { method: 'PATCH', diff --git a/src/newsreader/js/pages/homepage/components/PostModal.js b/src/newsreader/js/pages/homepage/components/PostModal.js index 5196102..ab508ae 100644 --- a/src/newsreader/js/pages/homepage/components/PostModal.js +++ b/src/newsreader/js/pages/homepage/components/PostModal.js @@ -21,7 +21,7 @@ class PostModal extends React.Component { const markPostRead = this.props.markPostRead; const token = Cookies.get('csrftoken'); - if (!post.read) { + if (this.props.autoMarking && !post.read) { this.readTimer = setTimeout(markPostRead, 3000, post, token); } @@ -48,31 +48,47 @@ class PostModal extends React.Component { render() { const post = this.props.post; + const token = Cookies.get('csrftoken'); const publicationDate = formatDatetime(post.publicationDate); const titleClassName = post.read ? 'post__title post__title--read' : 'post__title'; - let ruleUrl = ''; + const readButtonDisabled = post.read || this.props.isMarkingPost; - if (this.props.rule.type === SUBREDDIT) { - ruleUrl = `${this.props.subredditUrl}/${this.props.rule.id}/`; - } else if (this.props.rule.type === TWITTER_TIMELINE) { - ruleUrl = `${this.props.timelineUrl}/${this.props.rule.id}/`; - } else { - ruleUrl = `${this.props.feedUrl}/${this.props.rule.id}/`; + let ruleUrl = ''; + switch (this.props.rule.type) { + case SUBREDDIT: + ruleUrl = `${this.props.subredditUrl}/${this.props.rule.id}/`; + break; + case TWITTER_TIMELINE: + ruleUrl = `${this.props.timelineUrl}/${this.props.rule.id}/`; + break; + default: + ruleUrl = `${this.props.feedUrl}/${this.props.rule.id}/`; + break; } return (
- this.props.unSelectPost()} - > - Close - +
+ + +

{`${post.title} `}

- {publicationDate} + + {publicationDate} {this.props.timezone} + {post.author && {post.author}} {this.props.category && ( @@ -114,4 +130,6 @@ const mapDispatchToProps = dispatch => ({ markPostRead: (post, token) => dispatch(markPostRead(post, token)), }); -export default connect(null, mapDispatchToProps)(PostModal); +const mapStateToProps = state => ({ isMarkingPost: state.posts.isMarking }); + +export default connect(mapStateToProps, mapDispatchToProps)(PostModal); diff --git a/src/newsreader/js/pages/homepage/components/postlist/PostItem.js b/src/newsreader/js/pages/homepage/components/postlist/PostItem.js index f69a463..90a08f2 100644 --- a/src/newsreader/js/pages/homepage/components/postlist/PostItem.js +++ b/src/newsreader/js/pages/homepage/components/postlist/PostItem.js @@ -41,7 +41,7 @@ class PostItem extends React.Component {
- {publicationDate} {post.author && `By ${post.author}`} + {publicationDate} {this.props.timezone} {post.author && `By ${post.author}`} {this.props.selected.type == CATEGORY_TYPE && ( diff --git a/src/newsreader/js/pages/homepage/components/postlist/PostList.js b/src/newsreader/js/pages/homepage/components/postlist/PostList.js index cff2437..7a5422e 100644 --- a/src/newsreader/js/pages/homepage/components/postlist/PostList.js +++ b/src/newsreader/js/pages/homepage/components/postlist/PostList.js @@ -46,6 +46,7 @@ class PostList extends React.Component { feedUrl={this.props.feedUrl} subredditUrl={this.props.subredditUrl} timelineUrl={this.props.timelineUrl} + timezone={this.props.timezone} /> ); }); diff --git a/src/newsreader/js/pages/homepage/components/postlist/filters.js b/src/newsreader/js/pages/homepage/components/postlist/filters.js index 02d6c28..3024aaf 100644 --- a/src/newsreader/js/pages/homepage/components/postlist/filters.js +++ b/src/newsreader/js/pages/homepage/components/postlist/filters.js @@ -4,6 +4,19 @@ 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; +}; + export const filterPostsByRule = (rule = {}, posts = []) => { const filteredPosts = posts.filter(post => { return post.rule === rule.id; @@ -11,9 +24,7 @@ export const filterPostsByRule = (rule = {}, posts = []) => { const filteredData = filteredPosts.map(post => ({ ...post, rule: { ...rule } })); - return filteredData.sort((firstPost, secondPost) => { - return new Date(secondPost.publicationDate) - new Date(firstPost.publicationDate); - }); + return filteredData.sort(sortOrdering); }; export const filterPostsByCategory = (category = {}, rules = [], posts = []) => { @@ -29,9 +40,7 @@ export const filterPostsByCategory = (category = {}, rules = [], posts = []) => return filteredPosts.map(post => ({ ...post, rule: { ...rule } })); }); - const sortedPosts = [...filteredData.flat()].sort((firstPost, secondPost) => { - return new Date(secondPost.publicationDate) - new Date(firstPost.publicationDate); - }); + const sortedPosts = [...filteredData.flat()].sort(sortOrdering); return sortedPosts; }; diff --git a/src/newsreader/js/pages/homepage/index.js b/src/newsreader/js/pages/homepage/index.js index 394a06c..b934ad3 100644 --- a/src/newsreader/js/pages/homepage/index.js +++ b/src/newsreader/js/pages/homepage/index.js @@ -11,18 +11,18 @@ const page = document.getElementById('homepage--page'); if (page) { const store = configureStore(); - let feedUrl = document.getElementById('feedUrl').textContent; - let subredditUrl = document.getElementById('subredditUrl').textContent; - let timelineUrl = document.getElementById('timelineUrl').textContent; - let categoriesUrl = document.getElementById('categoriesUrl').textContent; + const settings = JSON.parse(document.getElementById('homepageSettings').textContent); + const { feedUrl, subredditUrl, timelineUrl, categoriesUrl } = settings; ReactDOM.render( , page diff --git a/src/newsreader/js/pages/homepage/reducers/posts.js b/src/newsreader/js/pages/homepage/reducers/posts.js index 220c59b..608deb2 100644 --- a/src/newsreader/js/pages/homepage/reducers/posts.js +++ b/src/newsreader/js/pages/homepage/reducers/posts.js @@ -5,6 +5,8 @@ import { CATEGORY_TYPE, RULE_TYPE } from '../constants.js'; import { SELECT_POST, + MARKING_POST, + MARK_POST_READ, RECEIVE_POST, RECEIVE_POSTS, REQUEST_POSTS, @@ -13,7 +15,7 @@ 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 }; +const defaultState = { items: {}, isFetching: false, isMarking: false }; export const posts = (state = { ...defaultState }, action) => { switch (action.type) { @@ -62,6 +64,10 @@ export const posts = (state = { ...defaultState }, action) => { ...updatedPosts, }, }; + case MARKING_POST: + return { ...state, isMarking: true }; + case MARK_POST_READ: + return { ...state, isMarking: false }; default: return state; } diff --git a/src/newsreader/js/tests/homepage/actions/post.test.js b/src/newsreader/js/tests/homepage/actions/post.test.js index e8f84de..ce2ffdc 100644 --- a/src/newsreader/js/tests/homepage/actions/post.test.js +++ b/src/newsreader/js/tests/homepage/actions/post.test.js @@ -14,12 +14,18 @@ describe('post actions', () => { fetchMock.restore(); }); - it('should create an action request posts', () => { + it('should create an action to request posts', () => { const expectedAction = { type: actions.REQUEST_POSTS }; expect(actions.requestPosts()).toEqual(expectedAction); }); + it('should create an action to mark a post read', () => { + const expectedAction = { type: actions.MARKING_POST }; + + expect(actions.markingPostRead()).toEqual(expectedAction); + }); + it('should create an action receive a post', () => { const post = { id: 2067, @@ -147,6 +153,7 @@ describe('post actions', () => { }); const expectedActions = [ + { type: actions.MARKING_POST }, { type: actions.RECEIVE_POST, post: { ...post, read: true }, @@ -362,6 +369,7 @@ describe('post actions', () => { }); const expectedActions = [ + { type: actions.MARKING_POST }, { type: actions.RECEIVE_POST, post: {} }, { type: errorActions.RECEIVE_API_ERROR, error: TypeError(errorMessage) }, ]; diff --git a/src/newsreader/js/tests/homepage/reducers/post.test.js b/src/newsreader/js/tests/homepage/reducers/post.test.js index ef4234a..6fe728f 100644 --- a/src/newsreader/js/tests/homepage/reducers/post.test.js +++ b/src/newsreader/js/tests/homepage/reducers/post.test.js @@ -12,7 +12,7 @@ describe('post actions', () => { it('should return state after requesting posts', () => { const action = { type: actions.REQUEST_POSTS }; - const expectedState = { ...defaultState, isFetching: true }; + const expectedState = { ...defaultState, isFetching: true, isMarking: false }; expect(reducer(undefined, action)).toEqual(expectedState); }); @@ -40,6 +40,7 @@ describe('post actions', () => { const expectedState = { ...defaultState, isFetching: false, + isMarking: false, items: { [post.id]: post }, }; @@ -85,6 +86,7 @@ describe('post actions', () => { const expectedState = { ...defaultState, isFetching: false, + isMarking: false, items: expectedPosts, }; diff --git a/src/newsreader/news/core/templates/news/core/views/homepage.html b/src/newsreader/news/core/templates/news/core/views/homepage.html index 502ef63..a135314 100644 --- a/src/newsreader/news/core/templates/news/core/views/homepage.html +++ b/src/newsreader/news/core/templates/news/core/views/homepage.html @@ -6,10 +6,7 @@ {% endblock content %} {% block scripts %} - {{ feed_url|json_script:"feedUrl" }} - {{ subreddit_url|json_script:"subredditUrl" }} - {{ twitter_timeline_url|json_script:"timelineUrl" }} - {{ categories_url|json_script:"categoriesUrl" }} + {{ homepageSettings|json_script:"homepageSettings" }} {{ block.super }} {% endblock scripts %} diff --git a/src/newsreader/news/core/views.py b/src/newsreader/news/core/views.py index 981e7b2..c2ff4d5 100644 --- a/src/newsreader/news/core/views.py +++ b/src/newsreader/news/core/views.py @@ -1,3 +1,4 @@ +from django.conf import settings from django.urls import reverse_lazy from django.views.generic.base import TemplateView from django.views.generic.edit import CreateView, UpdateView @@ -16,14 +17,18 @@ class NewsView(TemplateView): return { **context, - "feed_url": reverse_lazy("news:collection:feed-update", args=(0,)), - "subreddit_url": reverse_lazy( - "news:collection:subreddit-update", args=(0,) - ), - "twitter_timeline_url": reverse_lazy( - "news:collection:twitter-timeline-update", args=(0,) - ), - "categories_url": reverse_lazy("news:core:category-update", args=(0,)), + "homepageSettings": { + "feedUrl": reverse_lazy("news:collection:feed-update", args=(0,)), + "subredditUrl": reverse_lazy( + "news:collection:subreddit-update", args=(0,) + ), + "timelineUrl": reverse_lazy( + "news:collection:twitter-timeline-update", args=(0,) + ), + "categoriesUrl": reverse_lazy("news:core:category-update", args=(0,)), + "timezone": settings.TIME_ZONE, + "autoMarking": self.request.user.auto_mark_read, + }, } diff --git a/src/newsreader/scss/components/post/_post.scss b/src/newsreader/scss/components/post/_post.scss index ae94f6c..7a8ea22 100644 --- a/src/newsreader/scss/components/post/_post.scss +++ b/src/newsreader/scss/components/post/_post.scss @@ -15,6 +15,15 @@ cursor: initial; + &__actions { + display: flex; + justify-content: flex-end; + width: 100%; + + padding: 20px 50px 0; + gap: 20px; + } + &__header { display: flex; flex-direction: column; @@ -81,9 +90,6 @@ } &__close-button { - position: relative; - margin: 1% 2% 0 0; - align-self: flex-end; background-color: $blue; color: $white; diff --git a/src/newsreader/scss/components/sidebar/_sidebar.scss b/src/newsreader/scss/components/sidebar/_sidebar.scss index f13faf3..c70594a 100644 --- a/src/newsreader/scss/components/sidebar/_sidebar.scss +++ b/src/newsreader/scss/components/sidebar/_sidebar.scss @@ -20,4 +20,8 @@ padding: 2px 10px 5px 10px; } } + + .read-button { + margin: 20px 0 0 0; + } } diff --git a/src/newsreader/scss/elements/button/_read-button.scss b/src/newsreader/scss/elements/button/_read-button.scss index 2c345b5..71e8e75 100644 --- a/src/newsreader/scss/elements/button/_read-button.scss +++ b/src/newsreader/scss/elements/button/_read-button.scss @@ -1,8 +1,6 @@ .read-button { @extend .button; - margin: 20px 0 0 0; - color: $white; background-color: $green;