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;