Refactor endpoint tests
Replace force_login calls with login call from client class in setUp
This commit is contained in:
parent
61702e720a
commit
858f84aaad
132 changed files with 5158 additions and 661 deletions
10
.babelrc
10
.babelrc
|
|
@ -1,3 +1,11 @@
|
|||
{
|
||||
"presets": ["@babel/preset-env"]
|
||||
"presets": ["@babel/preset-env"],
|
||||
"plugins": [
|
||||
"@babel/plugin-transform-runtime",
|
||||
"@babel/plugin-syntax-dynamic-import",
|
||||
"@babel/plugin-transform-react-jsx",
|
||||
"@babel/plugin-syntax-function-bind",
|
||||
"@babel/plugin-proposal-function-bind",
|
||||
["@babel/plugin-proposal-class-properties", {loose: true}],
|
||||
]
|
||||
}
|
||||
|
|
|
|||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -154,6 +154,7 @@ dmypy.json
|
|||
# Translations
|
||||
|
||||
# Django stuff:
|
||||
src/newsreader/fixtures/local
|
||||
|
||||
# Flask stuff:
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,57 @@
|
|||
services:
|
||||
- postgres:9.6
|
||||
stages:
|
||||
- build
|
||||
- test
|
||||
- lint
|
||||
|
||||
variables:
|
||||
POSTGRES_DB: newsreader
|
||||
POSTGRES_USER: newsreader
|
||||
javascript build:
|
||||
image: node:12
|
||||
stage: build
|
||||
cache:
|
||||
key: "$CI_JOB_NAME-$CI_COMMIT_REF_SLUG"
|
||||
paths:
|
||||
- node_modules/
|
||||
before_script:
|
||||
- npm install --dev
|
||||
script:
|
||||
- npx gulp
|
||||
|
||||
python tests:
|
||||
services:
|
||||
- postgres:11
|
||||
image: python:3.7.4-slim-stretch
|
||||
stage: test
|
||||
variables:
|
||||
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
|
||||
DJANGO_SETTINGS_MODULE: "newsreader.conf.gitlab"
|
||||
POSTGRES_DB: newsreader
|
||||
POSTGRES_USER: newsreader
|
||||
cache:
|
||||
key: "$CI_JOB_NAME-$CI_COMMIT_REF_SLUG"
|
||||
paths:
|
||||
- .cache/pip
|
||||
- env/
|
||||
before_script:
|
||||
- python3 -m venv env
|
||||
- source env/bin/activate
|
||||
- pip install -r requirements/gitlab.txt
|
||||
script:
|
||||
- python src/manage.py test newsreader
|
||||
|
||||
javascript linting:
|
||||
image: node:12
|
||||
stage: lint
|
||||
cache:
|
||||
key: "$CI_JOB_NAME-$CI_COMMIT_REF_SLUG"
|
||||
paths:
|
||||
- node_modules/
|
||||
before_script:
|
||||
- npm install --dev
|
||||
script:
|
||||
- npm run lint
|
||||
|
||||
python linting:
|
||||
image: python:3.7.4-slim-stretch
|
||||
stage: lint
|
||||
variables:
|
||||
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
|
||||
DJANGO_SETTINGS_MODULE: "newsreader.conf.gitlab"
|
||||
|
|
@ -21,6 +65,6 @@ python tests:
|
|||
- source env/bin/activate
|
||||
- pip install -r requirements/gitlab.txt
|
||||
script:
|
||||
- python src/manage.py test newsreader
|
||||
- isort -rc src/ --check-only
|
||||
- black -l 90 --check src/
|
||||
- autoflake -rc src/
|
||||
|
|
|
|||
10
.prettierrc.json
Normal file
10
.prettierrc.json
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"semi": true,
|
||||
"trailingComma": "es5",
|
||||
"singleQuote": true,
|
||||
"printWidth": 90,
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"bracketSpacing": true,
|
||||
"arrowParens": "avoid"
|
||||
}
|
||||
16
Dockerfile
16
Dockerfile
|
|
@ -1,14 +1,18 @@
|
|||
FROM python:3.7-buster
|
||||
|
||||
# Run project binaries from the user's local bin folder
|
||||
ENV PATH=/home/newsreader/.local/bin:$PATH
|
||||
|
||||
# Set the default shell
|
||||
RUN useradd -ms /bin/bash newsreader
|
||||
|
||||
RUN mkdir /app
|
||||
WORKDIR /app
|
||||
RUN chown newsreader:newsreader /app
|
||||
USER newsreader
|
||||
|
||||
# Use a seperate layer for the project requirements
|
||||
COPY ./requirements /app/requirements
|
||||
RUN pip install -r requirements/dev.txt
|
||||
COPY requirements /app/requirements
|
||||
RUN pip install --user -r requirements/dev.txt
|
||||
|
||||
COPY . /app/
|
||||
|
||||
# Set the default shell & add a home dir
|
||||
RUN useradd -ms /bin/bash newsreader
|
||||
USER newsreader
|
||||
|
|
|
|||
|
|
@ -4,11 +4,26 @@ services:
|
|||
db:
|
||||
# See https://hub.docker.com/_/postgres
|
||||
image: postgres
|
||||
container_name: postgres
|
||||
environment:
|
||||
- POSTGRES_USER=newsreader
|
||||
- POSTGRES_DB=newsreader
|
||||
rabbitmq:
|
||||
image: rabbitmq:3.7
|
||||
container_name: rabbitmq
|
||||
celery:
|
||||
build: .
|
||||
container_name: celery
|
||||
command: celery -A newsreader worker --beat --scheduler django --workdir=/app/src/
|
||||
environment:
|
||||
- DJANGO_SETTINGS_MODULE=newsreader.conf.docker
|
||||
volumes:
|
||||
- .:/app
|
||||
depends_on:
|
||||
- rabbitmq
|
||||
web:
|
||||
build: .
|
||||
container_name: web
|
||||
command: src/entrypoint.sh
|
||||
environment:
|
||||
- DJANGO_SETTINGS_MODULE=newsreader.conf.docker
|
||||
|
|
@ -18,14 +33,3 @@ services:
|
|||
- '8000:8000'
|
||||
depends_on:
|
||||
- db
|
||||
rabbitmq:
|
||||
image: rabbitmq:3.7
|
||||
celery:
|
||||
build: .
|
||||
command: celery -A newsreader worker --beat --scheduler django --loglevel=info --workdir=/app/src/
|
||||
environment:
|
||||
- DJANGO_SETTINGS_MODULE=newsreader.conf.docker
|
||||
volumes:
|
||||
- .:/app
|
||||
depends_on:
|
||||
- rabbitmq
|
||||
|
|
|
|||
28
gulp/babel.js
Normal file
28
gulp/babel.js
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import path from 'path';
|
||||
|
||||
import { dest } from 'gulp';
|
||||
import babelify from 'babelify';
|
||||
import browserify from 'browserify';
|
||||
import source from 'vinyl-source-stream';
|
||||
import buffer from 'vinyl-buffer';
|
||||
import concat from 'gulp-concat';
|
||||
|
||||
const PROJECT_DIR = path.join('src', 'newsreader');
|
||||
const STATIC_DIR = path.join(PROJECT_DIR, 'js');
|
||||
const CORE_DIR = path.join(PROJECT_DIR, 'news', 'core', 'static');
|
||||
|
||||
const babelTask = () => {
|
||||
const config = browserify({
|
||||
entries: `${STATIC_DIR}/homepage/index.js`,
|
||||
debug: true,
|
||||
}).transform(babelify);
|
||||
|
||||
return config
|
||||
.bundle()
|
||||
.pipe(source('index.js'))
|
||||
.pipe(buffer())
|
||||
.pipe(concat('homepage.js'))
|
||||
.pipe(dest(`${CORE_DIR}/core/dist/js/`));
|
||||
};
|
||||
|
||||
export default babelTask;
|
||||
30
gulp/sass.js
30
gulp/sass.js
|
|
@ -1,17 +1,25 @@
|
|||
import { src, dest } from "gulp";
|
||||
import { src, dest } from 'gulp';
|
||||
|
||||
import concat from "gulp-concat";
|
||||
import path from "path";
|
||||
import sass from "gulp-sass";
|
||||
import concat from 'gulp-concat';
|
||||
import path from 'path';
|
||||
import sass from 'gulp-sass';
|
||||
|
||||
const PROJECT_DIR = path.join("src", "newsreader");
|
||||
const STATIC_DIR = path.join(PROJECT_DIR, "static");
|
||||
const PROJECT_DIR = path.join('src', 'newsreader');
|
||||
const STATIC_DIR = path.join(PROJECT_DIR, 'scss');
|
||||
|
||||
export const ACCOUNTS_DIR = path.join(PROJECT_DIR, "accounts", "static");
|
||||
export const ACCOUNTS_DIR = path.join(PROJECT_DIR, 'accounts', 'static');
|
||||
export const CORE_DIR = path.join(PROJECT_DIR, 'news', 'core', 'static');
|
||||
|
||||
export default function accountsTask(){
|
||||
return src(`${STATIC_DIR}/src/scss/accounts/index.scss`)
|
||||
.pipe(sass().on("error", sass.logError))
|
||||
.pipe(concat("accounts.css"))
|
||||
export const accountsTask = () => {
|
||||
return src(`${STATIC_DIR}/accounts/index.scss`)
|
||||
.pipe(sass().on('error', sass.logError))
|
||||
.pipe(concat('accounts.css'))
|
||||
.pipe(dest(`${ACCOUNTS_DIR}/accounts/dist/css`));
|
||||
};
|
||||
|
||||
export const coreTask = () => {
|
||||
return src(`${STATIC_DIR}/homepage/index.scss`)
|
||||
.pipe(sass().on('error', sass.logError))
|
||||
.pipe(concat('core.css'))
|
||||
.pipe(dest(`${CORE_DIR}/core/dist/css`));
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,22 +1,27 @@
|
|||
import { series, watch as _watch } from 'gulp';
|
||||
import { parallel, series, watch as _watch } from 'gulp';
|
||||
|
||||
import path from "path";
|
||||
import del from "del";
|
||||
import path from 'path';
|
||||
import del from 'del';
|
||||
|
||||
import buildSass, { ACCOUNTS_DIR } from "./gulp/sass";
|
||||
import { ACCOUNTS_DIR, CORE_DIR, accountsTask, coreTask } from './gulp/sass';
|
||||
import babelTask from './gulp/babel';
|
||||
|
||||
const STATIC_DIR = path.join("src", "newsreader", "static");
|
||||
const PROJECT_DIR = path.join('src', 'newsreader');
|
||||
const sassTasks = [accountsTask, coreTask];
|
||||
|
||||
function clean(){
|
||||
const clean = () => {
|
||||
return del([
|
||||
`${ACCOUNTS_DIR}/accounts/dist/css/*`,
|
||||
|
||||
`${CORE_DIR}/core/dist/css/*`,
|
||||
`${CORE_DIR}/core/dist/js/*`,
|
||||
]);
|
||||
};
|
||||
|
||||
export function watch(){
|
||||
_watch(`${STATIC_DIR}/src/scss/**/*.scss`, (done) => {
|
||||
series(clean, buildSass)(done);
|
||||
export const watch = () => {
|
||||
return _watch([`${PROJECT_DIR}/scss/**/*.scss`, `${PROJECT_DIR}/js/**/*.js`], done => {
|
||||
series(clean, ...sassTasks, babelTask)(done);
|
||||
});
|
||||
};
|
||||
|
||||
export default series(clean, buildSass);
|
||||
export default series(clean, ...sassTasks, babelTask);
|
||||
|
|
|
|||
1367
package-lock.json
generated
1367
package-lock.json
generated
File diff suppressed because it is too large
Load diff
36
package.json
36
package.json
|
|
@ -4,8 +4,8 @@
|
|||
"description": "Application for viewing RSS feeds",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"lint": "prettier \"src/newsreader/**/*.js\" --check",
|
||||
"format": "prettier \"src/newsreader/**/*.js\" --write"
|
||||
"lint": "prettier \"src/newsreader/js/**/*.js\" --check",
|
||||
"format": "prettier \"src/newsreader/js/**/*.js\" --write"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
@ -13,21 +13,27 @@
|
|||
},
|
||||
"author": "Sonny",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"prettier": {
|
||||
"semi": true,
|
||||
"trailingComma": "es5",
|
||||
"singleQuote": false,
|
||||
"printWidth": 80,
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"bracketSpacing": false,
|
||||
"arrowParens": "always"
|
||||
"dependencies": {
|
||||
"js-cookie": "^2.2.1",
|
||||
"lodash": "^4.17.15",
|
||||
"react-redux": "^7.1.0",
|
||||
"redux": "^4.0.4",
|
||||
"redux-logger": "^3.0.6",
|
||||
"redux-thunk": "^2.3.0"
|
||||
},
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.5.4",
|
||||
"@babel/plugin-proposal-class-properties": "^7.5.5",
|
||||
"@babel/plugin-proposal-function-bind": "^7.2.0",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
|
||||
"@babel/plugin-syntax-function-bind": "^7.2.0",
|
||||
"@babel/plugin-transform-react-jsx": "^7.3.0",
|
||||
"@babel/plugin-transform-runtime": "^7.5.5",
|
||||
"@babel/preset-env": "^7.5.4",
|
||||
"@babel/register": "^7.4.4",
|
||||
"@babel/runtime": "^7.5.5",
|
||||
"babelify": "^10.0.0",
|
||||
"browserify": "^16.3.0",
|
||||
"del": "^5.0.0",
|
||||
"gulp": "^4.0.2",
|
||||
"gulp-babel": "^8.0.0-beta.2",
|
||||
|
|
@ -35,6 +41,10 @@
|
|||
"gulp-concat": "^2.6.1",
|
||||
"gulp-sass": "^4.0.2",
|
||||
"node-sass": "^4.12.0",
|
||||
"prettier": "^1.18.2"
|
||||
"prettier": "^1.18.2",
|
||||
"react": "^16.8.6",
|
||||
"react-dom": "^16.8.6",
|
||||
"vinyl-buffer": "^1.0.1",
|
||||
"vinyl-source-stream": "^2.0.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ chardet==3.0.4
|
|||
Django==2.2
|
||||
django-celery-beat==1.5.0
|
||||
djangorestframework==3.9.4
|
||||
django-rest-swagger-2.2.0
|
||||
django-rest-swagger==2.2.0
|
||||
lxml==4.3.4
|
||||
feedparser==5.2.1
|
||||
idna==2.8
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
-r base.txt
|
||||
-r testing.txt
|
||||
|
||||
factory-boy==2.12.0
|
||||
freezegun==0.3.12
|
||||
black==19.3b0
|
||||
isort==4.3.20
|
||||
autoflake==1.3
|
||||
|
|
|
|||
|
|
@ -3,13 +3,11 @@ from django.contrib.auth.views import LogoutView as DjangoLogoutView
|
|||
from django.urls import reverse_lazy
|
||||
|
||||
|
||||
# TODO redirect to homepage when logged in
|
||||
class LoginView(DjangoLoginView):
|
||||
template_name = "accounts/login.html"
|
||||
|
||||
def get_success_url(self):
|
||||
# TODO redirect to homepage
|
||||
return reverse_lazy("admin:index")
|
||||
return reverse_lazy("index")
|
||||
|
||||
|
||||
class LogoutView(DjangoLogoutView):
|
||||
|
|
|
|||
|
|
@ -112,6 +112,8 @@ USE_TZ = True
|
|||
# https://docs.djangoproject.com/en/2.2/howto/static-files/
|
||||
STATIC_URL = "/static/"
|
||||
|
||||
STATICFILES_DIRS = ["src/newsreader/static/icons"]
|
||||
|
||||
# Third party settings
|
||||
REST_FRAMEWORK = {
|
||||
"DEFAULT_AUTHENTICATION_CLASSES": (
|
||||
|
|
@ -123,3 +125,9 @@ REST_FRAMEWORK = {
|
|||
),
|
||||
"DEFAULT_RENDERER_CLASSES": ("rest_framework.renderers.JSONRenderer",),
|
||||
}
|
||||
|
||||
SWAGGER_SETTINGS = {
|
||||
"LOGIN_URL": "rest_framework:login",
|
||||
"LOGOUT_URL": "rest_framework:logout",
|
||||
"DOC_EXPANSION": "list",
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
13
src/newsreader/js/components/LoadingIndicator.js
Normal file
13
src/newsreader/js/components/LoadingIndicator.js
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import React from 'react';
|
||||
|
||||
const LoadingIndicator = props => {
|
||||
return (
|
||||
<div className="loading-indicator">
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoadingIndicator;
|
||||
63
src/newsreader/js/homepage/App.js
Normal file
63
src/newsreader/js/homepage/App.js
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import React from 'react';
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
import { isEqual } from 'lodash';
|
||||
|
||||
import { fetchCategories } from './actions/categories';
|
||||
|
||||
import Sidebar from './components/sidebar/Sidebar.js';
|
||||
import FeedList from './components/feedlist/FeedList.js';
|
||||
import PostModal from './components/PostModal.js';
|
||||
|
||||
class App extends React.Component {
|
||||
componentDidMount() {
|
||||
this.props.fetchCategories();
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
<main className="main">
|
||||
<Sidebar />
|
||||
<FeedList />
|
||||
|
||||
{!isEqual(this.props.post, {}) && (
|
||||
<PostModal
|
||||
post={this.props.post}
|
||||
rule={this.props.rule}
|
||||
category={this.props.category}
|
||||
/>
|
||||
)}
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = state => {
|
||||
if (!isEqual(state.selected.post, {})) {
|
||||
const ruleId = state.selected.post.rule;
|
||||
|
||||
const rule = state.rules.items[ruleId];
|
||||
const category = state.categories.items[rule.category];
|
||||
|
||||
return {
|
||||
post: state.selected.post,
|
||||
rule,
|
||||
category,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
post: state.selected.post,
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
fetchCategories: () => dispatch(fetchCategories()),
|
||||
});
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(App);
|
||||
86
src/newsreader/js/homepage/actions/categories.js
Normal file
86
src/newsreader/js/homepage/actions/categories.js
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
import { requestRules, receiveRules, fetchRulesByCategory } from './rules.js';
|
||||
|
||||
export const SELECT_CATEGORY = 'SELECT_CATEGORY';
|
||||
|
||||
export const RECEIVE_CATEGORY = 'RECEIVE_CATEGORY';
|
||||
export const RECEIVE_CATEGORIES = 'RECEIVE_CATEGORIES';
|
||||
|
||||
export const REQUEST_CATEGORY = 'REQUEST_CATEGORY';
|
||||
export const REQUEST_CATEGORIES = 'REQUEST_CATEGORIES';
|
||||
|
||||
export const selectCategory = category => ({
|
||||
type: SELECT_CATEGORY,
|
||||
item: category,
|
||||
});
|
||||
|
||||
export const receiveCategory = category => ({
|
||||
type: RECEIVE_CATEGORY,
|
||||
category,
|
||||
});
|
||||
|
||||
export const receiveCategories = json => ({
|
||||
type: RECEIVE_CATEGORIES,
|
||||
categories: json,
|
||||
});
|
||||
|
||||
export const requestCategory = () => ({ type: REQUEST_CATEGORY });
|
||||
export const requestCategories = () => ({ type: REQUEST_CATEGORIES });
|
||||
|
||||
export const fetchCategories = () => {
|
||||
return dispatch => {
|
||||
dispatch(requestCategories());
|
||||
|
||||
return fetch('/api/categories/')
|
||||
.then(response => response.json())
|
||||
.then(json => {
|
||||
const categories = {};
|
||||
|
||||
json.forEach(category => {
|
||||
categories[category.id] = { ...category };
|
||||
});
|
||||
|
||||
dispatch(receiveCategories(categories));
|
||||
return json;
|
||||
})
|
||||
.then(json => {
|
||||
const promises = json.map(category => {
|
||||
return fetch(`/api/categories/${category.id}/rules/`);
|
||||
});
|
||||
|
||||
dispatch(requestRules());
|
||||
return Promise.all(promises);
|
||||
})
|
||||
.then(responses => {
|
||||
return Promise.all(responses.map(response => response.json()));
|
||||
})
|
||||
.then(responseData => {
|
||||
let rules = {};
|
||||
|
||||
responseData.forEach(json => {
|
||||
const data = Object.values(json);
|
||||
|
||||
data.forEach(item => {
|
||||
rules = { ...rules, [item.id]: item };
|
||||
});
|
||||
});
|
||||
|
||||
setTimeout(dispatch, 500, receiveRules(rules));
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
export const fetchCategory = category => {
|
||||
return dispatch => {
|
||||
dispatch(requestCategory());
|
||||
|
||||
return fetch(`/api/categories/${category.id}`)
|
||||
.then(response => response.json())
|
||||
.then(json => {
|
||||
dispatch(receiveCategory({ ...json }));
|
||||
|
||||
if (category.unread === 0) {
|
||||
dispatch(fetchRulesByCategory(category));
|
||||
}
|
||||
});
|
||||
};
|
||||
};
|
||||
119
src/newsreader/js/homepage/actions/posts.js
Normal file
119
src/newsreader/js/homepage/actions/posts.js
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
export const SELECT_POST = 'SELECT_POST';
|
||||
export const UNSELECT_POST = 'UNSELECT_POST';
|
||||
|
||||
export const RECEIVE_POSTS = 'RECEIVE_POSTS';
|
||||
export const RECEIVE_POST = 'RECEIVE_POST';
|
||||
export const REQUEST_POSTS = 'REQUEST_POSTS';
|
||||
|
||||
export const MARK_POST_READ = 'MARK_POST_READ';
|
||||
|
||||
export const selectPost = post => ({
|
||||
type: SELECT_POST,
|
||||
post,
|
||||
});
|
||||
|
||||
export const unSelectPost = () => ({
|
||||
type: UNSELECT_POST,
|
||||
});
|
||||
|
||||
export const postRead = (post, rule, category) => ({
|
||||
type: MARK_POST_READ,
|
||||
category: category,
|
||||
post: post,
|
||||
rule: rule,
|
||||
});
|
||||
|
||||
export const markPostRead = (post, token) => {
|
||||
return (dispatch, getState) => {
|
||||
const { rules } = getState();
|
||||
const { categories } = getState();
|
||||
|
||||
const url = `/api/posts/${post.id}/`;
|
||||
const options = {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': token,
|
||||
},
|
||||
body: JSON.stringify({ read: true }),
|
||||
};
|
||||
|
||||
const rule = rules.items[post.rule];
|
||||
const category = categories.items[rule.category];
|
||||
|
||||
return fetch(url, options)
|
||||
.then(response => response.json())
|
||||
.then(updatedPost => {
|
||||
const updatedRule = { ...rule, unread: rule.unread - 1 };
|
||||
const updatedCategory = { ...category, unread: category.unread - 1 };
|
||||
|
||||
dispatch(receivePost({ ...updatedPost }));
|
||||
dispatch(postRead({ ...updatedPost }, updatedRule, updatedCategory));
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
export const receivePosts = json => ({
|
||||
type: RECEIVE_POSTS,
|
||||
posts: json.items,
|
||||
next: json.next,
|
||||
});
|
||||
|
||||
export const receivePost = post => ({
|
||||
type: RECEIVE_POST,
|
||||
post,
|
||||
});
|
||||
|
||||
export const requestPosts = () => ({ type: REQUEST_POSTS });
|
||||
|
||||
export const fetchPostsByCategory = (category, page = false) => {
|
||||
return dispatch => {
|
||||
dispatch(requestPosts());
|
||||
|
||||
const url = page ? page : `/api/categories/${category.id}/posts/?read=false`;
|
||||
return fetch(url)
|
||||
.then(response => response.json())
|
||||
.then(json => {
|
||||
const posts = {};
|
||||
|
||||
json.results.forEach(post => {
|
||||
posts[post.id] = post;
|
||||
});
|
||||
|
||||
dispatch(receivePosts({ items: posts, next: json.next }));
|
||||
})
|
||||
.catch(error => {
|
||||
if (error instanceof TypeError) {
|
||||
console.log(`Unable to parse posts from request: ${error}`);
|
||||
}
|
||||
|
||||
dispatch(receivePosts({ items: {}, next: null }));
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
export const fetchPostsByRule = (rule, page = false) => {
|
||||
return dispatch => {
|
||||
dispatch(requestPosts());
|
||||
|
||||
const url = page ? page : `/api/rules/${rule.id}/posts/?read=false`;
|
||||
return fetch(url)
|
||||
.then(response => response.json())
|
||||
.then(json => {
|
||||
const posts = {};
|
||||
|
||||
json.results.forEach(post => {
|
||||
posts[post.id] = post;
|
||||
});
|
||||
|
||||
dispatch(receivePosts({ items: posts, next: json.next }));
|
||||
})
|
||||
.catch(error => {
|
||||
if (error instanceof TypeError) {
|
||||
console.log(`Unable to parse posts from request: ${error}`);
|
||||
}
|
||||
|
||||
dispatch(receivePosts({ items: {}, next: null }));
|
||||
});
|
||||
};
|
||||
};
|
||||
68
src/newsreader/js/homepage/actions/rules.js
Normal file
68
src/newsreader/js/homepage/actions/rules.js
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
import { fetchCategory } from './categories.js';
|
||||
|
||||
export const SELECT_RULE = 'SELECT_RULE';
|
||||
export const SELECT_RULES = 'SELECT_RULES';
|
||||
|
||||
export const RECEIVE_RULE = 'RECEIVE_RULE';
|
||||
export const RECEIVE_RULES = 'RECEIVE_RULES';
|
||||
|
||||
export const REQUEST_RULE = 'REQUEST_RULE';
|
||||
export const REQUEST_RULES = 'REQUEST_RULES';
|
||||
|
||||
export const selectRule = rule => ({
|
||||
type: SELECT_RULE,
|
||||
item: rule,
|
||||
});
|
||||
|
||||
export const requestRule = () => ({ type: REQUEST_RULE });
|
||||
export const requestRules = () => ({ type: REQUEST_RULES });
|
||||
|
||||
export const receiveRule = rule => ({
|
||||
type: RECEIVE_RULE,
|
||||
rule,
|
||||
});
|
||||
|
||||
export const receiveRules = rules => ({
|
||||
type: RECEIVE_RULES,
|
||||
rules,
|
||||
});
|
||||
|
||||
export const fetchRule = rule => {
|
||||
return (dispatch, getState) => {
|
||||
dispatch(requestRule());
|
||||
|
||||
const { categories } = getState();
|
||||
const category = categories['items'][rule.category];
|
||||
|
||||
return fetch(`/api/rules/${rule.id}`)
|
||||
.then(response => response.json())
|
||||
.then(receivedRule => {
|
||||
dispatch(receiveRule({ ...receivedRule }));
|
||||
|
||||
// fetch & update category info when the rule is read
|
||||
if (rule.unread === 0) {
|
||||
dispatch(fetchCategory({ ...category }));
|
||||
}
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
export const fetchRulesByCategory = category => {
|
||||
return (dispatch, getState) => {
|
||||
dispatch(requestRules());
|
||||
|
||||
return fetch(`/api/categories/${category.id}/rules/`)
|
||||
.then(response => response.json())
|
||||
.then(responseData => {
|
||||
dispatch(receiveRules());
|
||||
|
||||
const rules = {};
|
||||
|
||||
responseData.forEach(rule => {
|
||||
rules[rule.id] = { ...rule };
|
||||
});
|
||||
|
||||
setTimeout(dispatch, 500, receiveRules(rules));
|
||||
});
|
||||
};
|
||||
};
|
||||
65
src/newsreader/js/homepage/actions/selected.js
Normal file
65
src/newsreader/js/homepage/actions/selected.js
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
import { receiveCategory, requestCategory } from './categories.js';
|
||||
import { receiveRule, requestRule } from './rules.js';
|
||||
|
||||
export const MARK_SECTION_READ = 'MARK_SECTION_READ';
|
||||
|
||||
export const markSectionRead = (category, rule = {}) => ({
|
||||
category: category,
|
||||
rule: rule,
|
||||
type: MARK_SECTION_READ,
|
||||
});
|
||||
|
||||
const markCategoryRead = (category, token) => {
|
||||
return dispatch => {
|
||||
dispatch(requestCategory(category));
|
||||
|
||||
const url = `/api/categories/${category.id}/read/`;
|
||||
const options = {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRFToken': token,
|
||||
},
|
||||
};
|
||||
|
||||
return fetch(url, options)
|
||||
.then(response => response.json())
|
||||
.then(updatedCategory => {
|
||||
dispatch(receiveCategory({ ...updatedCategory }));
|
||||
dispatch(markSectionRead({ ...category, ...updatedCategory }));
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
const markRuleRead = (rule, token) => {
|
||||
return (dispatch, getState) => {
|
||||
const { categories } = getState();
|
||||
const category = categories.items[rule.category];
|
||||
|
||||
dispatch(requestRule(rule));
|
||||
|
||||
const url = `/api/rules/${rule.id}/read/`;
|
||||
const options = {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRFToken': token,
|
||||
},
|
||||
};
|
||||
|
||||
return fetch(url, options)
|
||||
.then(response => response.json())
|
||||
.then(updatedRule => {
|
||||
dispatch(receiveRule({ ...updatedRule }));
|
||||
|
||||
// Use the old rule to decrement category with old unread count
|
||||
dispatch(markSectionRead({ ...category }, { ...rule }));
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
export const markRead = (selected, token) => {
|
||||
if ('category' in selected) {
|
||||
return markRuleRead(selected, token);
|
||||
} else {
|
||||
return markCategoryRead(selected, token);
|
||||
}
|
||||
};
|
||||
84
src/newsreader/js/homepage/components/PostModal.js
Normal file
84
src/newsreader/js/homepage/components/PostModal.js
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import Cookies from 'js-cookie';
|
||||
|
||||
import { unSelectPost, markPostRead } from '../actions/posts.js';
|
||||
import { formatDatetime } from '../../utils.js';
|
||||
|
||||
class PostModal extends React.Component {
|
||||
readTimer = null;
|
||||
|
||||
componentDidMount() {
|
||||
const post = { ...this.props.post };
|
||||
const markPostRead = this.props.markPostRead;
|
||||
const token = Cookies.get('csrftoken');
|
||||
|
||||
if (!post.read) {
|
||||
this.readTimer = setTimeout(markPostRead, 30000, post, token);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.readTimer) {
|
||||
clearTimeout(this.readTimer);
|
||||
}
|
||||
|
||||
this.readTimer = null;
|
||||
}
|
||||
|
||||
render() {
|
||||
const post = this.props.post;
|
||||
const publicationDate = formatDatetime(post.publication_date);
|
||||
const titleClassName = post.read ? 'post__title post__title--read' : 'post__title';
|
||||
|
||||
return (
|
||||
<div className="modal">
|
||||
<div className="post">
|
||||
<button
|
||||
className="button post__close-button"
|
||||
onClick={() => this.props.unSelectPost()}
|
||||
>
|
||||
Close{' '}
|
||||
<span>
|
||||
<img src="/static/times.svg" />
|
||||
</span>
|
||||
</button>
|
||||
<div className="post__header">
|
||||
<h1 className={titleClassName}>
|
||||
{`${post.title} `}
|
||||
<a
|
||||
className="post__link"
|
||||
href={post.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<img src="/static/link.svg" width="15" height="15" />
|
||||
</a>
|
||||
</h1>
|
||||
<span className="post__date">{publicationDate}</span>
|
||||
</div>
|
||||
<aside className="post__meta-info">
|
||||
{this.props.category && (
|
||||
<h5 title={this.props.category.name}>{this.props.category.name}</h5>
|
||||
)}
|
||||
|
||||
<h5 title={this.props.rule.name}>{this.props.rule.name}</h5>
|
||||
</aside>
|
||||
|
||||
{/* HTML is sanitized by the collectors */}
|
||||
<div className="post__body" dangerouslySetInnerHTML={{ __html: post.body }} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
unSelectPost: () => dispatch(unSelectPost()),
|
||||
markPostRead: (post, token) => dispatch(markPostRead(post, token)),
|
||||
});
|
||||
|
||||
export default connect(
|
||||
null,
|
||||
mapDispatchToProps
|
||||
)(PostModal);
|
||||
102
src/newsreader/js/homepage/components/feedlist/FeedList.js
Normal file
102
src/newsreader/js/homepage/components/feedlist/FeedList.js
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { isEqual } from 'lodash';
|
||||
|
||||
import { fetchPostsByRule, fetchPostsByCategory } from '../../actions/posts.js';
|
||||
import { filterPosts } from './filters.js';
|
||||
|
||||
import LoadingIndicator from '../../../components/LoadingIndicator.js';
|
||||
import RuleItem from './RuleItem.js';
|
||||
|
||||
class FeedList extends React.Component {
|
||||
checkScrollHeight = ::this.checkScrollHeight;
|
||||
|
||||
componentDidMount() {
|
||||
window.addEventListener('scroll', this.checkScrollHeight);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener('scroll', this.checkScrollHeight);
|
||||
}
|
||||
|
||||
checkScrollHeight(e) {
|
||||
const currentHeight = window.scrollY + window.innerHeight;
|
||||
const totalHeight = document.body.offsetHeight;
|
||||
|
||||
const currentPercentage = (currentHeight / totalHeight) * 100;
|
||||
|
||||
if (this.props.next && !this.props.lastReached) {
|
||||
if (currentPercentage > 60 && !this.props.isFetching) {
|
||||
this.paginate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
paginate() {
|
||||
if ('category' in this.props.selected) {
|
||||
return this.props.fetchPostsByRule(this.props.selected, this.props.next);
|
||||
} else {
|
||||
return this.props.fetchPostsByCategory(this.props.selected, this.props.next);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const ruleItems = this.props.posts.map((item, index) => {
|
||||
return <RuleItem key={index} posts={item.posts} rule={item.rule} />;
|
||||
});
|
||||
|
||||
if (ruleItems.length > 0) {
|
||||
return (
|
||||
<div className="post-block">
|
||||
{ruleItems}
|
||||
{this.props.isFetching && <LoadingIndicator />}
|
||||
</div>
|
||||
);
|
||||
} else if (isEqual(this.props.selected, {})) {
|
||||
return (
|
||||
<div className="post-message">
|
||||
<div className="post-message__block">
|
||||
<img src="/static/arrow-left.svg" height="28" width="28" />
|
||||
<p className="post-message__text">
|
||||
Select a category or rule to show its unread posts
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else if (ruleItems.length === 0) {
|
||||
return (
|
||||
<div className="post-message">
|
||||
<div className="post-message__block">
|
||||
<p className="post-message__text">
|
||||
No unread posts from the selected section at this moment, try again later
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div className="post-block">{this.props.isFetching && <LoadingIndicator />}</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
isFetching: state.posts.isFetching,
|
||||
posts: filterPosts(state),
|
||||
next: state.selected.next,
|
||||
lastReached: state.selected.lastReached,
|
||||
selected: state.selected.item,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
fetchPostsByRule: (rule, page = false) => dispatch(fetchPostsByRule(rule, page)),
|
||||
fetchPostsByCategory: (category, page = false) => {
|
||||
dispatch(fetchPostsByCategory(category, page));
|
||||
},
|
||||
});
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(FeedList);
|
||||
50
src/newsreader/js/homepage/components/feedlist/PostItem.js
Normal file
50
src/newsreader/js/homepage/components/feedlist/PostItem.js
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { selectPost } from '../../actions/posts.js';
|
||||
|
||||
import { formatDatetime } from '../../../utils.js';
|
||||
|
||||
class PostItem extends React.Component {
|
||||
render() {
|
||||
const post = this.props.post;
|
||||
const publicationDate = formatDatetime(post.publication_date);
|
||||
const titleClassName = post.read
|
||||
? 'posts-header__title posts-header__title--read'
|
||||
: 'posts-header__title';
|
||||
|
||||
return (
|
||||
<li
|
||||
className="posts__item"
|
||||
onClick={() => {
|
||||
this.props.selectPost(post);
|
||||
}}
|
||||
>
|
||||
<div className="posts-header">
|
||||
<h5 className={titleClassName} title={post.title}>
|
||||
{post.title}
|
||||
</h5>
|
||||
|
||||
<a
|
||||
className="posts-header__link"
|
||||
href={post.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<img src="/static/link.svg" width="15" height="15" />
|
||||
</a>
|
||||
</div>
|
||||
<span title={publicationDate}>{publicationDate}</span>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
selectPost: post => dispatch(selectPost(post)),
|
||||
});
|
||||
|
||||
export default connect(
|
||||
null,
|
||||
mapDispatchToProps
|
||||
)(PostItem);
|
||||
25
src/newsreader/js/homepage/components/feedlist/RuleItem.js
Normal file
25
src/newsreader/js/homepage/components/feedlist/RuleItem.js
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import React from 'react';
|
||||
|
||||
import PostItem from './PostItem.js';
|
||||
|
||||
class RuleItem extends React.Component {
|
||||
render() {
|
||||
const posts = Object.values(this.props.posts).sort((firstEl, secondEl) => {
|
||||
return new Date(secondEl.publication_date) - new Date(firstEl.publication_date);
|
||||
});
|
||||
|
||||
const postItems = posts.map(post => {
|
||||
return <PostItem key={post.id} post={post} />;
|
||||
});
|
||||
|
||||
return (
|
||||
<section className="posts-section">
|
||||
<h3 className="posts-section__name">{this.props.rule.name}</h3>
|
||||
{/* TODO: Add empty posts message */}
|
||||
<ul className="posts">{postItems}</ul>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default RuleItem;
|
||||
44
src/newsreader/js/homepage/components/feedlist/filters.js
Normal file
44
src/newsreader/js/homepage/components/feedlist/filters.js
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
const isEmpty = (object = {}) => {
|
||||
return Object.keys(object).length === 0;
|
||||
};
|
||||
|
||||
export const filterPostsByRule = (rule = {}, posts = []) => {
|
||||
const filteredPosts = posts.filter(post => {
|
||||
return post.rule === rule.id && !post.read;
|
||||
});
|
||||
|
||||
return filteredPosts.length > 0 ? [{ rule, posts: filteredPosts }] : [];
|
||||
};
|
||||
|
||||
export const filterPostsByCategory = (category = {}, rules = [], posts = []) => {
|
||||
const filteredRules = rules.filter(rule => {
|
||||
return rule.category === category.id;
|
||||
});
|
||||
|
||||
const filteredData = filteredRules.map(rule => {
|
||||
const filteredPosts = posts.filter(post => {
|
||||
return post.rule === rule.id && !post.read;
|
||||
});
|
||||
|
||||
return {
|
||||
rule: { ...rule },
|
||||
posts: filteredPosts,
|
||||
};
|
||||
});
|
||||
|
||||
return filteredData.filter(rule => rule.posts.length > 0);
|
||||
};
|
||||
|
||||
export const filterPosts = state => {
|
||||
const posts = Object.values({ ...state.posts.items });
|
||||
|
||||
if (!isEmpty(state.selected.item) && !('category' in state.selected.item)) {
|
||||
const rules = Object.values({ ...state.rules.items });
|
||||
|
||||
return filterPostsByCategory({ ...state.selected.item }, rules, posts);
|
||||
} else if ('category' in state.selected.item) {
|
||||
return filterPostsByRule({ ...state.selected.item }, posts);
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { isEqual } from 'lodash';
|
||||
|
||||
import { selectCategory, fetchCategory } from '../../actions/categories.js';
|
||||
import { fetchPostsByCategory } from '../../actions/posts.js';
|
||||
import RuleItem from './RuleItem.js';
|
||||
|
||||
class CategoryItem extends React.Component {
|
||||
state = { open: false };
|
||||
|
||||
toggleRules() {
|
||||
this.setState({ open: !this.state.open });
|
||||
}
|
||||
|
||||
handleSelect() {
|
||||
const category = this.props.category;
|
||||
|
||||
this.props.selectCategory(category);
|
||||
this.props.fetchPostsByCategory(category);
|
||||
|
||||
if (category.unread === 0) {
|
||||
this.props.fetchCategory(category);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const imageSrc = this.state.open
|
||||
? '/static/chevron-down.svg'
|
||||
: '/static/chevron-right.svg';
|
||||
const selected = isEqual(this.props.category, this.props.selected);
|
||||
const className = selected ? 'category category--selected' : 'category';
|
||||
|
||||
const ruleItems = this.props.rules.map(rule => {
|
||||
return <RuleItem key={rule.id} rule={rule} selected={this.props.selectedRule} />;
|
||||
});
|
||||
|
||||
return (
|
||||
<li className="categories__item">
|
||||
<div className={className}>
|
||||
<div className="category__menu" onClick={() => this.toggleRules()}>
|
||||
<img src={imageSrc} width="20" heigth="20" />
|
||||
</div>
|
||||
|
||||
<div className="category__info" onClick={() => this.handleSelect()}>
|
||||
<h4>{this.props.category.name}</h4>
|
||||
<span className="badge">{this.props.category.unread}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{ruleItems.length > 0 && this.state.open && (
|
||||
<ul className="rules">{ruleItems}</ul>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
selectCategory: category => dispatch(selectCategory(category)),
|
||||
fetchPostsByCategory: category => dispatch(fetchPostsByCategory(category)),
|
||||
fetchCategory: category => dispatch(fetchCategory(category)),
|
||||
});
|
||||
|
||||
export default connect(
|
||||
null,
|
||||
mapDispatchToProps
|
||||
)(CategoryItem);
|
||||
36
src/newsreader/js/homepage/components/sidebar/ReadButton.js
Normal file
36
src/newsreader/js/homepage/components/sidebar/ReadButton.js
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import Cookies from 'js-cookie';
|
||||
|
||||
import { markRead } from '../../actions/selected.js';
|
||||
|
||||
class ReadButton extends React.Component {
|
||||
markSelectedRead = ::this.markSelectedRead;
|
||||
|
||||
markSelectedRead() {
|
||||
const token = Cookies.get('csrftoken');
|
||||
|
||||
if (this.props.selected.unread > 0) {
|
||||
this.props.markRead({ ...this.props.selected }, token);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<button className="button read-button" onClick={this.markSelectedRead}>
|
||||
Mark selected read
|
||||
</button>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
markRead: (selected, token) => dispatch(markRead(selected, token)),
|
||||
});
|
||||
|
||||
const mapStateToProps = state => ({ selected: state.selected.item });
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(ReadButton);
|
||||
56
src/newsreader/js/homepage/components/sidebar/RuleItem.js
Normal file
56
src/newsreader/js/homepage/components/sidebar/RuleItem.js
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { isEqual } from 'lodash';
|
||||
|
||||
import { selectRule, fetchRule } from '../../actions/rules.js';
|
||||
import { fetchPostsByRule } from '../../actions/posts.js';
|
||||
|
||||
class RuleItem extends React.Component {
|
||||
handleSelect() {
|
||||
const rule = { ...this.props.rule };
|
||||
|
||||
this.props.selectRule(rule);
|
||||
this.props.fetchPostsByRule(rule);
|
||||
|
||||
if (rule.unread === 0) {
|
||||
this.props.fetchRule(rule);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const selected = isEqual(this.props.selected, this.props.rule);
|
||||
const className = `rules__item ${selected ? 'rules__item--selected' : ''}`;
|
||||
|
||||
return (
|
||||
<li className={className} onClick={() => this.handleSelect()}>
|
||||
<div className="rule">
|
||||
{this.props.rule.favicon && (
|
||||
<span>
|
||||
<img
|
||||
className="icon"
|
||||
width="15"
|
||||
height="15"
|
||||
src={this.props.rule.favicon}
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
<h5 className="rule__title" title={this.props.rule.name}>
|
||||
{this.props.rule.name}
|
||||
</h5>
|
||||
</div>
|
||||
<span className="badge">{this.props.rule.unread}</span>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
selectRule: rule => dispatch(selectRule(rule)),
|
||||
fetchPostsByRule: rule => dispatch(fetchPostsByRule(rule)),
|
||||
fetchRule: rule => dispatch(fetchRule(rule)),
|
||||
});
|
||||
|
||||
export default connect(
|
||||
null,
|
||||
mapDispatchToProps
|
||||
)(RuleItem);
|
||||
52
src/newsreader/js/homepage/components/sidebar/Sidebar.js
Normal file
52
src/newsreader/js/homepage/components/sidebar/Sidebar.js
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { isEqual } from 'lodash';
|
||||
|
||||
import { filterCategories, filterRules } from './filters.js';
|
||||
|
||||
import LoadingIndicator from '../../../components/LoadingIndicator.js';
|
||||
import CategoryItem from './CategoryItem.js';
|
||||
import ReadButton from './ReadButton.js';
|
||||
|
||||
// TODO: show empty category message
|
||||
class Sidebar extends React.Component {
|
||||
render() {
|
||||
const items = this.props.categories.items.map(category => {
|
||||
const rules = this.props.rules.items.filter(rule => {
|
||||
return rule.category === category.id;
|
||||
});
|
||||
|
||||
return (
|
||||
<CategoryItem
|
||||
key={category.id}
|
||||
category={category}
|
||||
rules={rules}
|
||||
selected={this.props.selected.item}
|
||||
selectedRule={this.props.selected.item}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="sidebar">
|
||||
<nav className="categories">
|
||||
{(this.props.categories.isFetching || this.props.rules.isFetching) && (
|
||||
<LoadingIndicator />
|
||||
)}
|
||||
|
||||
<ul>{items}</ul>
|
||||
</nav>
|
||||
|
||||
{!isEqual(this.props.selected.item, {}) && <ReadButton />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
categories: { ...state.categories, items: filterCategories(state.categories.items) },
|
||||
rules: { ...state.rules, items: filterRules(state.rules.items) },
|
||||
selected: state.selected,
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps)(Sidebar);
|
||||
7
src/newsreader/js/homepage/components/sidebar/filters.js
Normal file
7
src/newsreader/js/homepage/components/sidebar/filters.js
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
export const filterCategories = (categories = {}) => {
|
||||
return Object.values({ ...categories });
|
||||
};
|
||||
|
||||
export const filterRules = (rules = {}) => {
|
||||
return Object.values({ ...rules });
|
||||
};
|
||||
18
src/newsreader/js/homepage/configureStore.js
Normal file
18
src/newsreader/js/homepage/configureStore.js
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { createStore, applyMiddleware } from 'redux';
|
||||
import thunkMiddleware from 'redux-thunk';
|
||||
|
||||
import { createLogger } from 'redux-logger';
|
||||
|
||||
import rootReducer from './reducers/index.js';
|
||||
|
||||
const loggerMiddleware = createLogger();
|
||||
|
||||
const configureStore = preloadedState => {
|
||||
return createStore(
|
||||
rootReducer,
|
||||
preloadedState,
|
||||
applyMiddleware(thunkMiddleware, loggerMiddleware)
|
||||
);
|
||||
};
|
||||
|
||||
export default configureStore;
|
||||
16
src/newsreader/js/homepage/index.js
Normal file
16
src/newsreader/js/homepage/index.js
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import { Provider } from 'react-redux';
|
||||
import configureStore from './configureStore.js';
|
||||
|
||||
import App from './App.js';
|
||||
|
||||
const store = configureStore();
|
||||
|
||||
ReactDOM.render(
|
||||
<Provider store={store}>
|
||||
<App />
|
||||
</Provider>,
|
||||
document.getElementsByClassName('content')[0]
|
||||
);
|
||||
116
src/newsreader/js/homepage/reducers/categories.js
Normal file
116
src/newsreader/js/homepage/reducers/categories.js
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
import { isEqual } from 'lodash';
|
||||
|
||||
import {
|
||||
RECEIVE_CATEGORY,
|
||||
RECEIVE_CATEGORIES,
|
||||
REQUEST_CATEGORY,
|
||||
REQUEST_CATEGORIES,
|
||||
} from '../actions/categories.js';
|
||||
|
||||
import { RECEIVE_RULE, RECEIVE_RULES } from '../actions/rules.js';
|
||||
|
||||
import { MARK_POST_READ } from '../actions/posts.js';
|
||||
import { MARK_SECTION_READ } from '../actions/selected.js';
|
||||
|
||||
const defaultState = { items: {}, isFetching: false };
|
||||
|
||||
export const categories = (state = { ...defaultState }, action) => {
|
||||
switch (action.type) {
|
||||
case RECEIVE_CATEGORY:
|
||||
return {
|
||||
...state,
|
||||
items: {
|
||||
...state.items,
|
||||
[action.category.id]: { ...action.category, rules: {} },
|
||||
},
|
||||
isFetching: false,
|
||||
};
|
||||
case RECEIVE_CATEGORIES:
|
||||
const receivedCategories = {};
|
||||
|
||||
Object.values({ ...action.categories }).forEach(category => {
|
||||
receivedCategories[category.id] = {
|
||||
...category,
|
||||
rules: {},
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
...state,
|
||||
items: { ...state.items, ...receivedCategories },
|
||||
isFetching: false,
|
||||
};
|
||||
case RECEIVE_RULE:
|
||||
const category = { ...state.items[action.rule.category] };
|
||||
|
||||
category['rules'][action.rule.id] = { ...action.rule };
|
||||
|
||||
return {
|
||||
...state,
|
||||
items: {
|
||||
...state.items,
|
||||
[category.id]: { ...category },
|
||||
},
|
||||
};
|
||||
case RECEIVE_RULES:
|
||||
const relevantCategories = {};
|
||||
|
||||
Object.values({ ...action.rules }).forEach(rule => {
|
||||
if (!(rule.category in relevantCategories)) {
|
||||
const category = { ...state.items[rule.category] };
|
||||
|
||||
relevantCategories[rule.category] = {
|
||||
...category,
|
||||
rules: {
|
||||
...category.rules,
|
||||
[rule.id]: { ...rule },
|
||||
},
|
||||
};
|
||||
} else {
|
||||
relevantCategories[rule.category]['rules'][rule.id] = { ...rule };
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
...state,
|
||||
items: {
|
||||
...state.items,
|
||||
...relevantCategories,
|
||||
},
|
||||
};
|
||||
case REQUEST_CATEGORIES:
|
||||
case REQUEST_CATEGORY:
|
||||
return {
|
||||
...state,
|
||||
isFetching: true,
|
||||
};
|
||||
case MARK_POST_READ:
|
||||
return {
|
||||
...state,
|
||||
items: { ...state.items, [action.category.id]: { ...action.category } },
|
||||
};
|
||||
case MARK_SECTION_READ:
|
||||
if (!isEqual(action.rule, {})) {
|
||||
return {
|
||||
...state,
|
||||
items: {
|
||||
...state.items,
|
||||
[action.category.id]: {
|
||||
...action.category,
|
||||
unread: action.category.unread - action.rule.unread,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
items: {
|
||||
...state.items,
|
||||
[action.category.id]: { ...action.category, unread: 0 },
|
||||
},
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
10
src/newsreader/js/homepage/reducers/index.js
Normal file
10
src/newsreader/js/homepage/reducers/index.js
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { combineReducers } from 'redux';
|
||||
|
||||
import { categories } from './categories.js';
|
||||
import { rules } from './rules.js';
|
||||
import { posts } from './posts.js';
|
||||
import { selected } from './selected.js';
|
||||
|
||||
const rootReducer = combineReducers({ categories, rules, posts, selected });
|
||||
|
||||
export default rootReducer;
|
||||
67
src/newsreader/js/homepage/reducers/posts.js
Normal file
67
src/newsreader/js/homepage/reducers/posts.js
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
import { isEqual } from 'lodash';
|
||||
|
||||
import {
|
||||
SELECT_POST,
|
||||
RECEIVE_POST,
|
||||
RECEIVE_POSTS,
|
||||
REQUEST_POSTS,
|
||||
} 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 };
|
||||
|
||||
export const posts = (state = { ...defaultState }, action) => {
|
||||
switch (action.type) {
|
||||
case RECEIVE_POSTS:
|
||||
return {
|
||||
...state,
|
||||
type: RECEIVE_POSTS,
|
||||
isFetching: false,
|
||||
items: { ...state.items, ...action.posts },
|
||||
};
|
||||
case REQUEST_POSTS:
|
||||
return {
|
||||
...state,
|
||||
type: REQUEST_POSTS,
|
||||
isFetching: true,
|
||||
};
|
||||
case RECEIVE_POST:
|
||||
const items = { ...state.items, [action.post.id]: { ...action.post } };
|
||||
|
||||
return {
|
||||
...state,
|
||||
items: items,
|
||||
type: RECEIVE_POST,
|
||||
};
|
||||
case MARK_SECTION_READ:
|
||||
const updatedPosts = {};
|
||||
let relatedPosts = [];
|
||||
|
||||
if (!isEqual(action.rule, {})) {
|
||||
relatedPosts = Object.values({ ...state.items }).filter(post => {
|
||||
return post.rule === action.rule.id;
|
||||
});
|
||||
} else {
|
||||
relatedPosts = Object.values({ ...state.items }).filter(post => {
|
||||
return post.rule in { ...action.category.rules };
|
||||
});
|
||||
}
|
||||
|
||||
relatedPosts.forEach(post => {
|
||||
updatedPosts[post.id] = { ...post, read: true };
|
||||
});
|
||||
|
||||
return {
|
||||
...state,
|
||||
items: {
|
||||
...state.items,
|
||||
...updatedPosts,
|
||||
},
|
||||
};
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
65
src/newsreader/js/homepage/reducers/rules.js
Normal file
65
src/newsreader/js/homepage/reducers/rules.js
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
import { isEqual } from 'lodash';
|
||||
|
||||
import {
|
||||
REQUEST_RULES,
|
||||
REQUEST_RULE,
|
||||
RECEIVE_RULES,
|
||||
RECEIVE_RULE,
|
||||
} from '../actions/rules.js';
|
||||
import { MARK_POST_READ } from '../actions/posts.js';
|
||||
import { MARK_SECTION_READ } from '../actions/selected.js';
|
||||
|
||||
const defaultState = { items: {}, isFetching: false };
|
||||
|
||||
export const rules = (state = { ...defaultState }, action) => {
|
||||
switch (action.type) {
|
||||
case REQUEST_RULE:
|
||||
case REQUEST_RULES:
|
||||
return {
|
||||
...state,
|
||||
isFetching: true,
|
||||
};
|
||||
case RECEIVE_RULES:
|
||||
return {
|
||||
...state,
|
||||
items: { ...state.items, ...action.rules },
|
||||
isFetching: false,
|
||||
};
|
||||
case RECEIVE_RULE:
|
||||
return {
|
||||
...state,
|
||||
items: { ...state.items, [action.rule.id]: { ...action.rule } },
|
||||
isFetching: false,
|
||||
};
|
||||
case MARK_POST_READ:
|
||||
case MARK_SECTION_READ:
|
||||
if (!isEqual(action.rule, {})) {
|
||||
return {
|
||||
...state,
|
||||
items: {
|
||||
...state.items,
|
||||
[action.rule.id]: {
|
||||
...action.rule,
|
||||
unread: 0,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const updatedRules = {};
|
||||
Object.values({ ...state.items }).forEach(rule => {
|
||||
if (rule.category === action.category.id) {
|
||||
updatedRules[rule.id] = {
|
||||
...rule,
|
||||
unread: 0,
|
||||
};
|
||||
} else {
|
||||
updatedRules[rule.id] = { ...rule };
|
||||
}
|
||||
});
|
||||
|
||||
return { ...state, items: { ...updatedRules } };
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
70
src/newsreader/js/homepage/reducers/selected.js
Normal file
70
src/newsreader/js/homepage/reducers/selected.js
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
import { isEqual } from 'lodash';
|
||||
|
||||
import { SELECT_CATEGORY } from '../actions/categories.js';
|
||||
import { SELECT_RULE } from '../actions/rules.js';
|
||||
import {
|
||||
RECEIVE_POST,
|
||||
RECEIVE_POSTS,
|
||||
SELECT_POST,
|
||||
UNSELECT_POST,
|
||||
} from '../actions/posts.js';
|
||||
|
||||
import { MARK_SECTION_READ } from '../actions/selected.js';
|
||||
|
||||
const defaultState = { item: {}, next: false, lastReached: false, post: {} };
|
||||
|
||||
export const selected = (state = { ...defaultState }, action) => {
|
||||
switch (action.type) {
|
||||
case SELECT_CATEGORY:
|
||||
case SELECT_RULE:
|
||||
return {
|
||||
...state,
|
||||
item: action.item,
|
||||
next: false,
|
||||
lastReached: false,
|
||||
};
|
||||
case RECEIVE_POSTS:
|
||||
return {
|
||||
...state,
|
||||
next: action.next,
|
||||
lastReached: !action.next,
|
||||
};
|
||||
case RECEIVE_POST:
|
||||
const isCurrentPost = !isEqual(state.post, {}) && state.post.id === action.post.id;
|
||||
|
||||
if (isCurrentPost) {
|
||||
return {
|
||||
...state,
|
||||
post: { ...action.post },
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
};
|
||||
case SELECT_POST:
|
||||
return {
|
||||
...state,
|
||||
post: action.post,
|
||||
};
|
||||
case UNSELECT_POST:
|
||||
return {
|
||||
...state,
|
||||
post: {},
|
||||
};
|
||||
case MARK_SECTION_READ:
|
||||
if (!isEqual(action.rule, {})) {
|
||||
return {
|
||||
...state,
|
||||
item: { ...action.rule, unread: 0 },
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
item: { ...action.category },
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
14
src/newsreader/js/utils.js
Normal file
14
src/newsreader/js/utils.js
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
export const formatDatetime = dateString => {
|
||||
const locale = navigator.language ? navigator.language : 'en-US';
|
||||
const dateOptions = {
|
||||
hour: '2-digit',
|
||||
weekday: 'long',
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
};
|
||||
|
||||
const date = new Date(dateString);
|
||||
|
||||
return date.toLocaleDateString(locale, dateOptions);
|
||||
};
|
||||
|
|
@ -4,9 +4,10 @@ from newsreader.news.collection.models import CollectionRule
|
|||
|
||||
|
||||
class CollectionRuleAdmin(admin.ModelAdmin):
|
||||
fields = ("url", "name", "timezone", "category", "favicon")
|
||||
fields = ("url", "name", "timezone", "category", "favicon", "user")
|
||||
|
||||
list_display = ("name", "category", "url", "last_suceeded", "succeeded")
|
||||
list_filter = ("user",)
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
if not change:
|
||||
|
|
|
|||
69
src/newsreader/news/collection/endpoints.py
Normal file
69
src/newsreader/news/collection/endpoints.py
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
from django.db.models.query import QuerySet
|
||||
|
||||
from rest_framework import status
|
||||
from rest_framework.generics import (
|
||||
GenericAPIView,
|
||||
ListAPIView,
|
||||
ListCreateAPIView,
|
||||
RetrieveUpdateDestroyAPIView,
|
||||
get_object_or_404,
|
||||
)
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.serializers import Serializer
|
||||
|
||||
from newsreader.core.pagination import LargeResultSetPagination, ResultSetPagination
|
||||
from newsreader.news.collection.models import CollectionRule
|
||||
from newsreader.news.collection.serializers import RuleSerializer
|
||||
from newsreader.news.core.filters import ReadFilter
|
||||
from newsreader.news.core.models import Post
|
||||
from newsreader.news.core.serializers import PostSerializer
|
||||
|
||||
|
||||
class ListRuleView(ListCreateAPIView):
|
||||
queryset = CollectionRule.objects.all()
|
||||
serializer_class = RuleSerializer
|
||||
pagination_class = ResultSetPagination
|
||||
|
||||
def get_queryset(self) -> QuerySet:
|
||||
user = self.request.user
|
||||
return self.queryset.filter(user=user).order_by("-created")
|
||||
|
||||
|
||||
class DetailRuleView(RetrieveUpdateDestroyAPIView):
|
||||
queryset = CollectionRule.objects.all()
|
||||
serializer_class = RuleSerializer
|
||||
pagination_class = ResultSetPagination
|
||||
|
||||
|
||||
class NestedRuleView(ListAPIView):
|
||||
queryset = CollectionRule.objects.prefetch_related("posts").all()
|
||||
serializer_class = PostSerializer
|
||||
pagination_class = LargeResultSetPagination
|
||||
filter_backends = [ReadFilter]
|
||||
|
||||
def get_queryset(self) -> QuerySet:
|
||||
lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
|
||||
|
||||
# Default permission is IsOwner, therefore there shouldn't have to be
|
||||
# filtered on the user.
|
||||
filter_kwargs = {self.lookup_field: self.kwargs[lookup_url_kwarg]}
|
||||
|
||||
rule = get_object_or_404(self.queryset, **filter_kwargs)
|
||||
self.check_object_permissions(self.request, rule)
|
||||
|
||||
return rule.posts.order_by("-publication_date")
|
||||
|
||||
|
||||
class RuleReadView(GenericAPIView):
|
||||
queryset = CollectionRule.objects.all()
|
||||
serializer_class = RuleSerializer
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
rule = self.get_object()
|
||||
|
||||
Post.objects.filter(rule=rule).update(read=True)
|
||||
|
||||
rule.refresh_from_db()
|
||||
serializer_class = self.get_serializer_class()
|
||||
serializer = serializer_class(rule)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
|
|
@ -1,23 +1,15 @@
|
|||
from rest_framework import serializers
|
||||
|
||||
from newsreader.news import core
|
||||
from newsreader.news.collection.models import CollectionRule
|
||||
|
||||
|
||||
class CollectionRuleSerializer(serializers.HyperlinkedModelSerializer):
|
||||
posts = serializers.SerializerMethodField()
|
||||
class RuleSerializer(serializers.ModelSerializer):
|
||||
user = serializers.HiddenField(default=serializers.CurrentUserDefault())
|
||||
unread = serializers.SerializerMethodField()
|
||||
|
||||
def get_posts(self, instance):
|
||||
request = self.context.get("request")
|
||||
posts = instance.posts.order_by("-publication_date")
|
||||
|
||||
serializer = core.serializers.PostSerializer(
|
||||
posts, context={"request": request}, many=True
|
||||
)
|
||||
return serializer.data
|
||||
def get_unread(self, rule):
|
||||
return rule.posts.filter(read=False).count()
|
||||
|
||||
class Meta:
|
||||
model = CollectionRule
|
||||
fields = ("id", "name", "url", "favicon", "category", "posts", "user")
|
||||
extra_kwargs = {"category": {"view_name": "api:categories-detail"}}
|
||||
fields = ("id", "name", "url", "favicon", "category", "user", "unread")
|
||||
|
|
|
|||
|
|
@ -1,26 +1,22 @@
|
|||
import json
|
||||
|
||||
from urllib.parse import urljoin
|
||||
|
||||
from django.test import Client, TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from newsreader.accounts.tests.factories import UserFactory
|
||||
from newsreader.news.collection.tests.factories import CollectionRuleFactory
|
||||
from newsreader.news.core.tests.factories import CategoryFactory
|
||||
from newsreader.news.core.models import Post
|
||||
from newsreader.news.core.tests.factories import CategoryFactory, PostFactory
|
||||
|
||||
|
||||
class CollectionRuleDetailViewTestCase(TestCase):
|
||||
def setUp(self):
|
||||
self.maxDiff = None
|
||||
|
||||
self.user = UserFactory(is_staff=True, password="test")
|
||||
self.client = Client()
|
||||
self.user = UserFactory(password="test")
|
||||
self.client.login(email=self.user.email, password="test")
|
||||
|
||||
def test_simple(self):
|
||||
rule = CollectionRuleFactory(user=self.user)
|
||||
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.get(reverse("api:rules-detail", args=[rule.pk]))
|
||||
data = response.json()
|
||||
|
||||
|
|
@ -31,10 +27,8 @@ class CollectionRuleDetailViewTestCase(TestCase):
|
|||
self.assertTrue("url" in data)
|
||||
self.assertTrue("favicon" in data)
|
||||
self.assertTrue("category" in data)
|
||||
self.assertTrue("posts" in data)
|
||||
|
||||
def test_not_known(self):
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.get(reverse("api:rules-detail", args=[100]))
|
||||
data = response.json()
|
||||
|
||||
|
|
@ -44,7 +38,6 @@ class CollectionRuleDetailViewTestCase(TestCase):
|
|||
def test_post(self):
|
||||
rule = CollectionRuleFactory(user=self.user)
|
||||
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.post(reverse("api:rules-detail", args=[rule.pk]))
|
||||
data = response.json()
|
||||
|
||||
|
|
@ -54,7 +47,6 @@ class CollectionRuleDetailViewTestCase(TestCase):
|
|||
def test_patch(self):
|
||||
rule = CollectionRuleFactory(name="BBC", user=self.user)
|
||||
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.patch(
|
||||
reverse("api:rules-detail", args=[rule.pk]),
|
||||
data=json.dumps({"name": "The guardian"}),
|
||||
|
|
@ -65,18 +57,12 @@ class CollectionRuleDetailViewTestCase(TestCase):
|
|||
self.assertEquals(response.status_code, 200)
|
||||
self.assertEquals(data["name"], "The guardian")
|
||||
|
||||
def test_category_change_with_absolute_url(self):
|
||||
def test_category_change(self):
|
||||
old_category = CategoryFactory(user=self.user)
|
||||
new_category = CategoryFactory(user=self.user)
|
||||
|
||||
base_url = "http://testserver"
|
||||
relative_url = reverse("api:categories-detail", args=[new_category.pk])
|
||||
|
||||
absolute_url = urljoin(base_url, relative_url)
|
||||
|
||||
rule = CollectionRuleFactory(name="BBC", category=old_category, user=self.user)
|
||||
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.patch(
|
||||
reverse("api:rules-detail", args=[rule.pk]),
|
||||
data=json.dumps({"category": absolute_url}),
|
||||
|
|
@ -85,34 +71,11 @@ class CollectionRuleDetailViewTestCase(TestCase):
|
|||
data = response.json()
|
||||
|
||||
self.assertEquals(response.status_code, 200)
|
||||
self.assertEquals(data["category"], absolute_url)
|
||||
|
||||
def test_category_change_with_relative_url(self):
|
||||
old_category = CategoryFactory(user=self.user)
|
||||
new_category = CategoryFactory(user=self.user)
|
||||
|
||||
base_url = "http://testserver"
|
||||
relative_url = reverse("api:categories-detail", args=[new_category.pk])
|
||||
|
||||
absolute_url = urljoin(base_url, relative_url)
|
||||
|
||||
rule = CollectionRuleFactory(name="BBC", category=old_category, user=self.user)
|
||||
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.patch(
|
||||
reverse("api:rules-detail", args=[rule.pk]),
|
||||
data=json.dumps({"category": relative_url}),
|
||||
content_type="application/json",
|
||||
)
|
||||
data = response.json()
|
||||
|
||||
self.assertEquals(response.status_code, 200)
|
||||
self.assertEquals(data["category"], absolute_url)
|
||||
self.assertEquals(data["category"], new_category.pk)
|
||||
|
||||
def test_identifier_cannot_be_changed(self):
|
||||
rule = CollectionRuleFactory(user=self.user)
|
||||
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.patch(
|
||||
reverse("api:rules-detail", args=[rule.pk]),
|
||||
data=json.dumps({"id": 44}),
|
||||
|
|
@ -127,26 +90,20 @@ class CollectionRuleDetailViewTestCase(TestCase):
|
|||
rule = CollectionRuleFactory(user=self.user)
|
||||
category = CategoryFactory(user=self.user)
|
||||
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.patch(
|
||||
reverse("api:rules-detail", args=[rule.pk]),
|
||||
data=json.dumps(
|
||||
{"category": reverse("api:categories-detail", args=[category.pk])}
|
||||
),
|
||||
data=json.dumps({"category": category.pk}),
|
||||
content_type="application/json",
|
||||
)
|
||||
data = response.json()
|
||||
url = data["category"]
|
||||
data["category"]
|
||||
|
||||
self.assertEquals(response.status_code, 200)
|
||||
self.assertTrue(
|
||||
url.endswith(reverse("api:categories-detail", args=[category.pk]))
|
||||
)
|
||||
self.assertEquals(data["category"], category.pk)
|
||||
|
||||
def test_put(self):
|
||||
rule = CollectionRuleFactory(name="BBC", user=self.user)
|
||||
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.put(
|
||||
reverse("api:rules-detail", args=[rule.pk]),
|
||||
data=json.dumps({"name": "BBC", "url": "https://www.bbc.co.uk"}),
|
||||
|
|
@ -160,12 +117,13 @@ class CollectionRuleDetailViewTestCase(TestCase):
|
|||
def test_delete(self):
|
||||
rule = CollectionRuleFactory(user=self.user)
|
||||
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.delete(reverse("api:rules-detail", args=[rule.pk]))
|
||||
|
||||
self.assertEquals(response.status_code, 204)
|
||||
|
||||
def test_rule_with_unauthenticated_user(self):
|
||||
self.client.logout()
|
||||
|
||||
rule = CollectionRuleFactory(name="BBC", user=self.user)
|
||||
|
||||
response = self.client.patch(
|
||||
|
|
@ -180,7 +138,6 @@ class CollectionRuleDetailViewTestCase(TestCase):
|
|||
other_user = UserFactory()
|
||||
rule = CollectionRuleFactory(name="BBC", user=other_user)
|
||||
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.patch(
|
||||
reverse("api:rules-detail", args=[rule.pk]),
|
||||
data=json.dumps({"name": "The guardian"}),
|
||||
|
|
@ -188,3 +145,95 @@ class CollectionRuleDetailViewTestCase(TestCase):
|
|||
)
|
||||
|
||||
self.assertEquals(response.status_code, 403)
|
||||
|
||||
def test_read_count(self):
|
||||
rule = CollectionRuleFactory(user=self.user)
|
||||
|
||||
PostFactory.create_batch(size=20, read=False, rule=rule)
|
||||
PostFactory.create_batch(size=20, read=True, rule=rule)
|
||||
|
||||
response = self.client.get(reverse("api:rules-detail", args=[rule.pk]))
|
||||
data = response.json()
|
||||
|
||||
self.assertEquals(response.status_code, 200)
|
||||
self.assertEquals(data["unread"], 20)
|
||||
|
||||
|
||||
class CollectionRuleReadTestCase(TestCase):
|
||||
def setUp(self):
|
||||
self.user = UserFactory(password="test")
|
||||
self.client.login(email=self.user.email, password="test")
|
||||
|
||||
def test_rule_read(self):
|
||||
rule = CollectionRuleFactory(user=self.user)
|
||||
|
||||
PostFactory.create_batch(size=20, read=False, rule=rule)
|
||||
|
||||
response = self.client.post(reverse("api:rules-read", args=[rule.pk]))
|
||||
data = response.json()
|
||||
|
||||
self.assertEquals(response.status_code, 201)
|
||||
self.assertEquals(data["unread"], 0)
|
||||
|
||||
def test_rule_unknown(self):
|
||||
response = self.client.post(reverse("api:rules-read", args=[101]))
|
||||
|
||||
self.assertEquals(response.status_code, 404)
|
||||
|
||||
def test_unauthenticated_user(self):
|
||||
self.client.logout()
|
||||
|
||||
rule = CollectionRuleFactory(user=self.user)
|
||||
|
||||
PostFactory.create_batch(size=20, read=False, rule=rule)
|
||||
|
||||
response = self.client.post(reverse("api:rules-read", args=[rule.pk]))
|
||||
|
||||
self.assertEquals(response.status_code, 403)
|
||||
|
||||
def test_unauthorized_user(self):
|
||||
other_user = UserFactory()
|
||||
rule = CollectionRuleFactory(user=other_user)
|
||||
|
||||
PostFactory.create_batch(size=20, read=False, rule=rule)
|
||||
|
||||
response = self.client.post(reverse("api:rules-read", args=[rule.pk]))
|
||||
|
||||
self.assertEquals(response.status_code, 403)
|
||||
self.assertEquals(Post.objects.filter(read=False).count(), 20)
|
||||
|
||||
def test_get(self):
|
||||
rule = CollectionRuleFactory(user=self.user)
|
||||
|
||||
response = self.client.get(reverse("api:rules-read", args=[rule.pk]))
|
||||
|
||||
self.assertEquals(response.status_code, 405)
|
||||
|
||||
def test_patch(self):
|
||||
rule = CollectionRuleFactory(name="BBC", user=self.user)
|
||||
|
||||
response = self.client.patch(
|
||||
reverse("api:rules-read", args=[rule.pk]),
|
||||
data=json.dumps({"name": "Not possible"}),
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
self.assertEquals(response.status_code, 405)
|
||||
|
||||
def test_put(self):
|
||||
rule = CollectionRuleFactory(name="BBC", user=self.user)
|
||||
|
||||
response = self.client.put(
|
||||
reverse("api:rules-read", args=[rule.pk]),
|
||||
data=json.dumps({"name": "Not possible"}),
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
self.assertEquals(response.status_code, 405)
|
||||
|
||||
def test_delete(self):
|
||||
rule = CollectionRuleFactory(user=self.user)
|
||||
|
||||
response = self.client.delete(reverse("api:rules-read", args=[rule.pk]))
|
||||
|
||||
self.assertEquals(response.status_code, 405)
|
||||
|
|
@ -0,0 +1,371 @@
|
|||
import json
|
||||
|
||||
from datetime import date, datetime, time
|
||||
|
||||
from django.test import Client, TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
import pytz
|
||||
|
||||
from newsreader.accounts.tests.factories import UserFactory
|
||||
from newsreader.news.collection.tests.factories import CollectionRuleFactory
|
||||
from newsreader.news.core.tests.factories import CategoryFactory, PostFactory
|
||||
|
||||
|
||||
class RuleListViewTestCase(TestCase):
|
||||
def setUp(self):
|
||||
self.user = UserFactory(password="test")
|
||||
self.client.login(email=self.user.email, password="test")
|
||||
|
||||
def test_simple(self):
|
||||
CollectionRuleFactory.create_batch(size=3, user=self.user)
|
||||
|
||||
response = self.client.get(reverse("api:rules-list"))
|
||||
data = response.json()
|
||||
|
||||
self.assertEquals(response.status_code, 200)
|
||||
self.assertTrue("results" in data)
|
||||
self.assertTrue("count" in data)
|
||||
self.assertEquals(data["count"], 3)
|
||||
|
||||
def test_ordering(self):
|
||||
rules = [
|
||||
CollectionRuleFactory(
|
||||
created=datetime.combine(
|
||||
date(2019, 5, 20), time(hour=16, minute=7, second=37), pytz.utc
|
||||
),
|
||||
user=self.user,
|
||||
),
|
||||
CollectionRuleFactory(
|
||||
created=datetime.combine(
|
||||
date(2019, 7, 20), time(hour=18, minute=7, second=37), pytz.utc
|
||||
),
|
||||
user=self.user,
|
||||
),
|
||||
CollectionRuleFactory(
|
||||
created=datetime.combine(
|
||||
date(2019, 7, 20), time(hour=16, minute=7, second=37), pytz.utc
|
||||
),
|
||||
user=self.user,
|
||||
),
|
||||
]
|
||||
|
||||
response = self.client.get(reverse("api:rules-list"))
|
||||
data = response.json()
|
||||
|
||||
self.assertEquals(response.status_code, 200)
|
||||
self.assertTrue("results" in data)
|
||||
self.assertTrue("count" in data)
|
||||
self.assertEquals(data["count"], 3)
|
||||
|
||||
self.assertEquals(data["results"][0]["id"], rules[1].pk)
|
||||
self.assertEquals(data["results"][1]["id"], rules[2].pk)
|
||||
self.assertEquals(data["results"][2]["id"], rules[0].pk)
|
||||
|
||||
def test_pagination_count(self):
|
||||
CollectionRuleFactory.create_batch(size=80, user=self.user)
|
||||
|
||||
response = self.client.get(reverse("api:rules-list"), {"count": 30})
|
||||
data = response.json()
|
||||
|
||||
self.assertEquals(response.status_code, 200)
|
||||
self.assertEquals(data["count"], 80)
|
||||
self.assertEquals(len(data["results"]), 30)
|
||||
|
||||
def test_empty(self):
|
||||
response = self.client.get(reverse("api:rules-list"))
|
||||
data = response.json()
|
||||
|
||||
self.assertEquals(response.status_code, 200)
|
||||
self.assertTrue("results" in data)
|
||||
self.assertTrue("count" in data)
|
||||
|
||||
self.assertEquals(data["count"], 0)
|
||||
self.assertEquals(len(data["results"]), 0)
|
||||
|
||||
def test_post(self):
|
||||
category = CategoryFactory(user=self.user)
|
||||
|
||||
data = {"name": "BBC", "url": "https://www.bbc.co.uk", "category": category.pk}
|
||||
|
||||
response = self.client.post(
|
||||
reverse("api:rules-list"),
|
||||
data=json.dumps(data),
|
||||
content_type="application/json",
|
||||
)
|
||||
data = response.json()
|
||||
data["category"]
|
||||
|
||||
self.assertEquals(response.status_code, 201)
|
||||
|
||||
self.assertEquals(data["name"], "BBC")
|
||||
self.assertEquals(data["url"], "https://www.bbc.co.uk")
|
||||
self.assertEquals(data["category"], category.pk)
|
||||
|
||||
def test_patch(self):
|
||||
response = self.client.patch(reverse("api:rules-list"))
|
||||
data = response.json()
|
||||
|
||||
self.assertEquals(response.status_code, 405)
|
||||
self.assertEquals(data["detail"], 'Method "PATCH" not allowed.')
|
||||
|
||||
def test_put(self):
|
||||
response = self.client.put(reverse("api:rules-list"))
|
||||
data = response.json()
|
||||
|
||||
self.assertEquals(response.status_code, 405)
|
||||
self.assertEquals(data["detail"], 'Method "PUT" not allowed.')
|
||||
|
||||
def test_delete(self):
|
||||
response = self.client.delete(reverse("api:rules-list"))
|
||||
data = response.json()
|
||||
|
||||
self.assertEquals(response.status_code, 405)
|
||||
self.assertEquals(data["detail"], 'Method "DELETE" not allowed.')
|
||||
|
||||
def test_rules_with_unauthenticated_user(self):
|
||||
self.client.logout()
|
||||
|
||||
CollectionRuleFactory.create_batch(size=3, user=self.user)
|
||||
|
||||
response = self.client.get(reverse("api:rules-list"))
|
||||
|
||||
self.assertEquals(response.status_code, 403)
|
||||
|
||||
def test_rules_with_unauthorized_user(self):
|
||||
other_user = UserFactory()
|
||||
CollectionRuleFactory.create_batch(size=3, user=other_user)
|
||||
|
||||
response = self.client.get(reverse("api:rules-list"))
|
||||
data = response.json()
|
||||
|
||||
self.assertEquals(response.status_code, 200)
|
||||
|
||||
self.assertEquals(data["count"], 0)
|
||||
self.assertEquals(len(data["results"]), 0)
|
||||
|
||||
|
||||
class NestedRuleListViewTestCase(TestCase):
|
||||
def setUp(self):
|
||||
self.user = UserFactory(password="test")
|
||||
self.client.login(email=self.user.email, password="test")
|
||||
|
||||
def test_simple(self):
|
||||
rule = CollectionRuleFactory.create(user=self.user)
|
||||
PostFactory.create_batch(size=5, rule=rule)
|
||||
|
||||
response = self.client.get(
|
||||
reverse("api:rules-nested-posts", kwargs={"pk": rule.pk})
|
||||
)
|
||||
data = response.json()
|
||||
|
||||
self.assertEquals(response.status_code, 200)
|
||||
|
||||
self.assertTrue("results" in data)
|
||||
self.assertTrue("count" in data)
|
||||
self.assertEquals(data["count"], 5)
|
||||
|
||||
def test_pagination(self):
|
||||
rule = CollectionRuleFactory.create(user=self.user)
|
||||
PostFactory.create_batch(size=80, rule=rule)
|
||||
|
||||
response = self.client.get(
|
||||
reverse("api:rules-nested-posts", kwargs={"pk": rule.pk}), {"count": 30}
|
||||
)
|
||||
data = response.json()
|
||||
|
||||
self.assertEquals(response.status_code, 200)
|
||||
self.assertEquals(data["count"], 80)
|
||||
self.assertEquals(len(data["results"]), 30)
|
||||
|
||||
def test_empty(self):
|
||||
rule = CollectionRuleFactory.create(user=self.user)
|
||||
|
||||
response = self.client.get(
|
||||
reverse("api:rules-nested-posts", kwargs={"pk": rule.pk})
|
||||
)
|
||||
data = response.json()
|
||||
|
||||
self.assertEquals(response.status_code, 200)
|
||||
self.assertEquals(data["count"], 0)
|
||||
self.assertEquals(len(data["results"]), 0)
|
||||
|
||||
def test_not_known(self):
|
||||
response = self.client.get(reverse("api:rules-nested-posts", kwargs={"pk": 0}))
|
||||
|
||||
self.assertEquals(response.status_code, 404)
|
||||
|
||||
def test_post(self):
|
||||
rule = CollectionRuleFactory.create(user=self.user)
|
||||
|
||||
response = self.client.post(
|
||||
reverse("api:rules-nested-posts", kwargs={"pk": rule.pk}),
|
||||
data=json.dumps({}),
|
||||
content_type="application/json",
|
||||
)
|
||||
data = response.json()
|
||||
|
||||
self.assertEquals(response.status_code, 405)
|
||||
self.assertEquals(data["detail"], 'Method "POST" not allowed.')
|
||||
|
||||
def test_patch(self):
|
||||
rule = CollectionRuleFactory.create(user=self.user)
|
||||
|
||||
response = self.client.patch(
|
||||
reverse("api:rules-nested-posts", kwargs={"pk": rule.pk}),
|
||||
data=json.dumps({}),
|
||||
content_type="application/json",
|
||||
)
|
||||
data = response.json()
|
||||
|
||||
self.assertEquals(response.status_code, 405)
|
||||
self.assertEquals(data["detail"], 'Method "PATCH" not allowed.')
|
||||
|
||||
def test_put(self):
|
||||
rule = CollectionRuleFactory.create(user=self.user)
|
||||
|
||||
response = self.client.put(
|
||||
reverse("api:rules-nested-posts", kwargs={"pk": rule.pk}),
|
||||
data=json.dumps({}),
|
||||
content_type="application/json",
|
||||
)
|
||||
data = response.json()
|
||||
|
||||
self.assertEquals(response.status_code, 405)
|
||||
self.assertEquals(data["detail"], 'Method "PUT" not allowed.')
|
||||
|
||||
def test_delete(self):
|
||||
rule = CollectionRuleFactory.create(user=self.user)
|
||||
|
||||
response = self.client.delete(
|
||||
reverse("api:rules-nested-posts", kwargs={"pk": rule.pk}),
|
||||
data=json.dumps({}),
|
||||
content_type="application/json",
|
||||
)
|
||||
data = response.json()
|
||||
|
||||
self.assertEquals(response.status_code, 405)
|
||||
self.assertEquals(data["detail"], 'Method "DELETE" not allowed.')
|
||||
|
||||
def test_rule_with_unauthenticated_user(self):
|
||||
self.client.logout()
|
||||
|
||||
rule = CollectionRuleFactory(user=self.user)
|
||||
|
||||
response = self.client.get(
|
||||
reverse("api:rules-nested-posts", kwargs={"pk": rule.pk})
|
||||
)
|
||||
|
||||
self.assertEquals(response.status_code, 403)
|
||||
|
||||
def test_rule_with_unauthorized_user(self):
|
||||
other_user = UserFactory()
|
||||
rule = CollectionRuleFactory(user=other_user)
|
||||
|
||||
response = self.client.get(
|
||||
reverse("api:rules-nested-posts", kwargs={"pk": rule.pk})
|
||||
)
|
||||
|
||||
self.assertEquals(response.status_code, 403)
|
||||
|
||||
def test_posts_ordering(self):
|
||||
rule = CollectionRuleFactory(
|
||||
user=self.user, category=CategoryFactory(user=self.user)
|
||||
)
|
||||
|
||||
posts = [
|
||||
PostFactory(
|
||||
title="I'm the first post",
|
||||
rule=rule,
|
||||
publication_date=datetime.combine(
|
||||
date(2019, 5, 20), time(hour=16, minute=7, second=37), pytz.utc
|
||||
),
|
||||
),
|
||||
PostFactory(
|
||||
title="I'm the second post",
|
||||
rule=rule,
|
||||
publication_date=datetime.combine(
|
||||
date(2019, 7, 20), time(hour=18, minute=7, second=37), pytz.utc
|
||||
),
|
||||
),
|
||||
PostFactory(
|
||||
title="I'm the third post",
|
||||
rule=rule,
|
||||
publication_date=datetime.combine(
|
||||
date(2019, 7, 20), time(hour=16, minute=7, second=37), pytz.utc
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
response = self.client.get(
|
||||
reverse("api:rules-nested-posts", kwargs={"pk": rule.pk})
|
||||
)
|
||||
data = response.json()
|
||||
|
||||
self.assertEquals(response.status_code, 200)
|
||||
self.assertTrue("results" in data)
|
||||
self.assertTrue("count" in data)
|
||||
self.assertEquals(data["count"], 3)
|
||||
|
||||
self.assertEquals(data["results"][0]["id"], posts[1].pk)
|
||||
self.assertEquals(data["results"][1]["id"], posts[2].pk)
|
||||
self.assertEquals(data["results"][2]["id"], posts[0].pk)
|
||||
|
||||
def test_only_posts_from_rule_are_returned(self):
|
||||
rule = CollectionRuleFactory.create(user=self.user)
|
||||
other_rule = CollectionRuleFactory.create(user=self.user)
|
||||
|
||||
PostFactory.create_batch(size=5, rule=rule)
|
||||
PostFactory.create_batch(size=5, rule=other_rule)
|
||||
|
||||
response = self.client.get(
|
||||
reverse("api:rules-nested-posts", kwargs={"pk": rule.pk})
|
||||
)
|
||||
data = response.json()
|
||||
|
||||
self.assertEquals(response.status_code, 200)
|
||||
|
||||
self.assertTrue("results" in data)
|
||||
self.assertTrue("count" in data)
|
||||
self.assertEquals(data["count"], 5)
|
||||
|
||||
for post in data["results"]:
|
||||
self.assertEquals(post["rule"], rule.pk)
|
||||
|
||||
def test_unread_posts(self):
|
||||
rule = CollectionRuleFactory.create(user=self.user)
|
||||
|
||||
PostFactory.create_batch(size=10, rule=rule, read=False)
|
||||
PostFactory.create_batch(size=10, rule=rule, read=True)
|
||||
|
||||
response = self.client.get(
|
||||
reverse("api:rules-nested-posts", kwargs={"pk": rule.pk}), {"read": "false"}
|
||||
)
|
||||
|
||||
data = response.json()
|
||||
posts = data["results"]
|
||||
|
||||
self.assertEquals(response.status_code, 200)
|
||||
self.assertEquals(data["count"], 10)
|
||||
|
||||
for post in posts:
|
||||
self.assertEquals(post["read"], False)
|
||||
|
||||
def test_read_posts(self):
|
||||
rule = CollectionRuleFactory.create(user=self.user)
|
||||
|
||||
PostFactory.create_batch(size=20, rule=rule, read=False)
|
||||
PostFactory.create_batch(size=10, rule=rule, read=True)
|
||||
|
||||
response = self.client.get(
|
||||
reverse("api:rules-nested-posts", kwargs={"pk": rule.pk}), {"read": "true"}
|
||||
)
|
||||
|
||||
data = response.json()
|
||||
posts = data["results"]
|
||||
|
||||
self.assertEquals(response.status_code, 200)
|
||||
self.assertEquals(data["count"], 10)
|
||||
|
||||
for post in posts:
|
||||
self.assertEquals(post["read"], True)
|
||||
|
|
@ -1,210 +0,0 @@
|
|||
import json
|
||||
|
||||
from datetime import date, datetime, time
|
||||
|
||||
from django.test import Client, TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
import pytz
|
||||
|
||||
from newsreader.accounts.tests.factories import UserFactory
|
||||
from newsreader.news.collection.tests.factories import CollectionRuleFactory
|
||||
from newsreader.news.core.tests.factories import CategoryFactory, PostFactory
|
||||
|
||||
|
||||
class CollectionRuleListViewTestCase(TestCase):
|
||||
def setUp(self):
|
||||
self.maxDiff = None
|
||||
|
||||
self.user = UserFactory(is_staff=True, password="test")
|
||||
self.client = Client()
|
||||
|
||||
def test_simple(self):
|
||||
CollectionRuleFactory.create_batch(size=3, user=self.user)
|
||||
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.get(reverse("api:rules-list"))
|
||||
data = response.json()
|
||||
|
||||
self.assertEquals(response.status_code, 200)
|
||||
self.assertTrue("results" in data)
|
||||
self.assertTrue("count" in data)
|
||||
self.assertEquals(data["count"], 3)
|
||||
|
||||
def test_ordering(self):
|
||||
rules = [
|
||||
CollectionRuleFactory(
|
||||
created=datetime.combine(
|
||||
date(2019, 5, 20), time(hour=16, minute=7, second=37), pytz.utc
|
||||
),
|
||||
user=self.user,
|
||||
),
|
||||
CollectionRuleFactory(
|
||||
created=datetime.combine(
|
||||
date(2019, 7, 20), time(hour=18, minute=7, second=37), pytz.utc
|
||||
),
|
||||
user=self.user,
|
||||
),
|
||||
CollectionRuleFactory(
|
||||
created=datetime.combine(
|
||||
date(2019, 7, 20), time(hour=16, minute=7, second=37), pytz.utc
|
||||
),
|
||||
user=self.user,
|
||||
),
|
||||
]
|
||||
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.get(reverse("api:rules-list"))
|
||||
data = response.json()
|
||||
|
||||
self.assertEquals(response.status_code, 200)
|
||||
self.assertTrue("results" in data)
|
||||
self.assertTrue("count" in data)
|
||||
self.assertEquals(data["count"], 3)
|
||||
|
||||
self.assertEquals(data["results"][0]["id"], rules[1].pk)
|
||||
self.assertEquals(data["results"][1]["id"], rules[2].pk)
|
||||
self.assertEquals(data["results"][2]["id"], rules[0].pk)
|
||||
|
||||
def test_pagination_count(self):
|
||||
CollectionRuleFactory.create_batch(size=80, user=self.user)
|
||||
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.get(reverse("api:rules-list"), {"count": 30})
|
||||
data = response.json()
|
||||
|
||||
self.assertEquals(response.status_code, 200)
|
||||
self.assertEquals(data["count"], 80)
|
||||
self.assertEquals(len(data["results"]), 30)
|
||||
|
||||
def test_empty(self):
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.get(reverse("api:rules-list"))
|
||||
data = response.json()
|
||||
|
||||
self.assertEquals(response.status_code, 200)
|
||||
self.assertTrue("results" in data)
|
||||
self.assertTrue("count" in data)
|
||||
|
||||
self.assertEquals(data["count"], 0)
|
||||
self.assertEquals(len(data["results"]), 0)
|
||||
|
||||
def test_post(self):
|
||||
category = CategoryFactory(user=self.user)
|
||||
|
||||
data = {
|
||||
"name": "BBC",
|
||||
"url": "https://www.bbc.co.uk",
|
||||
"category": reverse("api:categories-detail", args=[category.pk]),
|
||||
}
|
||||
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.post(
|
||||
reverse("api:rules-list"),
|
||||
data=json.dumps(data),
|
||||
content_type="application/json",
|
||||
)
|
||||
data = response.json()
|
||||
category_url = data["category"]
|
||||
|
||||
self.assertEquals(response.status_code, 201)
|
||||
|
||||
self.assertEquals(data["name"], "BBC")
|
||||
self.assertEquals(data["url"], "https://www.bbc.co.uk")
|
||||
|
||||
self.assertTrue(
|
||||
category_url.endswith(reverse("api:categories-detail", args=[category.pk]))
|
||||
)
|
||||
|
||||
def test_patch(self):
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.patch(reverse("api:rules-list"))
|
||||
data = response.json()
|
||||
|
||||
self.assertEquals(response.status_code, 405)
|
||||
self.assertEquals(data["detail"], 'Method "PATCH" not allowed.')
|
||||
|
||||
def test_put(self):
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.put(reverse("api:rules-list"))
|
||||
data = response.json()
|
||||
|
||||
self.assertEquals(response.status_code, 405)
|
||||
self.assertEquals(data["detail"], 'Method "PUT" not allowed.')
|
||||
|
||||
def test_delete(self):
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.delete(reverse("api:rules-list"))
|
||||
data = response.json()
|
||||
|
||||
self.assertEquals(response.status_code, 405)
|
||||
self.assertEquals(data["detail"], 'Method "DELETE" not allowed.')
|
||||
|
||||
def test_rules_with_posts(self):
|
||||
rules = {
|
||||
rule: PostFactory.create_batch(size=5, rule=rule)
|
||||
for rule in CollectionRuleFactory.create_batch(size=5, user=self.user)
|
||||
}
|
||||
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.get(reverse("api:rules-list"))
|
||||
data = response.json()
|
||||
|
||||
self.assertEquals(response.status_code, 200)
|
||||
self.assertTrue("results" in data)
|
||||
self.assertTrue("count" in data)
|
||||
self.assertEquals(data["count"], 5)
|
||||
|
||||
self.assertEquals(len(data["results"]), 5)
|
||||
|
||||
self.assertEquals(len(data["results"][0]["posts"]), 5)
|
||||
|
||||
def test_rules_with_posts_ordered(self):
|
||||
rules = {
|
||||
rule: PostFactory.create_batch(size=5, rule=rule)
|
||||
for rule in CollectionRuleFactory.create_batch(size=2, user=self.user)
|
||||
}
|
||||
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.get(reverse("api:rules-list"))
|
||||
data = response.json()
|
||||
|
||||
first_post_set = data["results"][0]["posts"]
|
||||
second_post_set = data["results"][1]["posts"]
|
||||
|
||||
self.assertEquals(response.status_code, 200)
|
||||
self.assertTrue("results" in data)
|
||||
self.assertTrue("count" in data)
|
||||
self.assertEquals(data["count"], 2)
|
||||
|
||||
self.assertEquals(len(data["results"]), 2)
|
||||
|
||||
for result_set in [first_post_set, second_post_set]:
|
||||
for count, post in enumerate(result_set):
|
||||
if count < 1:
|
||||
continue
|
||||
|
||||
self.assertTrue(
|
||||
post["publication_date"] < result_set[count - 1]["publication_date"]
|
||||
)
|
||||
|
||||
def test_rule_with_unauthenticated_user(self):
|
||||
CollectionRuleFactory.create_batch(size=3, user=self.user)
|
||||
|
||||
response = self.client.get(reverse("api:rules-list"))
|
||||
response.json()
|
||||
|
||||
self.assertEquals(response.status_code, 403)
|
||||
|
||||
def test_rule_with_unauthorized_user(self):
|
||||
other_user = UserFactory()
|
||||
CollectionRuleFactory.create_batch(size=3, user=other_user)
|
||||
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.get(reverse("api:rules-list"))
|
||||
data = response.json()
|
||||
|
||||
self.assertEquals(response.status_code, 200)
|
||||
|
||||
self.assertEquals(data["count"], 0)
|
||||
self.assertEquals(len(data["results"]), 0)
|
||||
|
|
@ -1,12 +1,16 @@
|
|||
from django.urls import path
|
||||
|
||||
from newsreader.news.collection.views import (
|
||||
CollectionRuleAPIListView,
|
||||
CollectionRuleDetailView,
|
||||
from newsreader.news.collection.endpoints import (
|
||||
DetailRuleView,
|
||||
ListRuleView,
|
||||
NestedRuleView,
|
||||
RuleReadView,
|
||||
)
|
||||
|
||||
|
||||
endpoints = [
|
||||
path("rules/<int:pk>", CollectionRuleDetailView.as_view(), name="rules-detail"),
|
||||
path("rules/", CollectionRuleAPIListView.as_view(), name="rules-list"),
|
||||
path("rules/<int:pk>", DetailRuleView.as_view(), name="rules-detail"),
|
||||
path("rules/<int:pk>/posts/", NestedRuleView.as_view(), name="rules-nested-posts"),
|
||||
path("rules/<int:pk>/read/", RuleReadView.as_view(), name="rules-read"),
|
||||
path("rules/", ListRuleView.as_view(), name="rules-list"),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,21 +0,0 @@
|
|||
from rest_framework.generics import ListCreateAPIView, RetrieveUpdateDestroyAPIView
|
||||
|
||||
from newsreader.core.pagination import ResultSetPagination
|
||||
from newsreader.news.collection.models import CollectionRule
|
||||
from newsreader.news.collection.serializers import CollectionRuleSerializer
|
||||
|
||||
|
||||
class CollectionRuleAPIListView(ListCreateAPIView):
|
||||
queryset = CollectionRule.objects.all()
|
||||
serializer_class = CollectionRuleSerializer
|
||||
pagination_class = ResultSetPagination
|
||||
|
||||
def get_queryset(self):
|
||||
user = self.request.user
|
||||
return self.queryset.filter(user=user).order_by("-created")
|
||||
|
||||
|
||||
class CollectionRuleDetailView(RetrieveUpdateDestroyAPIView):
|
||||
queryset = CollectionRule.objects.all()
|
||||
serializer_class = CollectionRuleSerializer
|
||||
pagination_class = ResultSetPagination
|
||||
118
src/newsreader/news/core/endpoints.py
Normal file
118
src/newsreader/news/core/endpoints.py
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
from django.db.models import Q
|
||||
from django.db.models.query import QuerySet
|
||||
|
||||
from rest_framework import status
|
||||
from rest_framework.generics import (
|
||||
GenericAPIView,
|
||||
ListAPIView,
|
||||
ListCreateAPIView,
|
||||
RetrieveUpdateAPIView,
|
||||
RetrieveUpdateDestroyAPIView,
|
||||
get_object_or_404,
|
||||
)
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
|
||||
from newsreader.accounts.permissions import IsPostOwner
|
||||
from newsreader.core.pagination import LargeResultSetPagination, ResultSetPagination
|
||||
from newsreader.news.collection.models import CollectionRule
|
||||
from newsreader.news.collection.serializers import RuleSerializer
|
||||
from newsreader.news.core.filters import ReadFilter
|
||||
from newsreader.news.core.models import Category, Post
|
||||
from newsreader.news.core.serializers import CategorySerializer, PostSerializer
|
||||
|
||||
|
||||
class ListPostView(ListAPIView):
|
||||
queryset = Post.objects.all()
|
||||
serializer_class = PostSerializer
|
||||
pagination_class = LargeResultSetPagination
|
||||
filter_backends = [ReadFilter]
|
||||
|
||||
def get_queryset(self):
|
||||
user = self.request.user
|
||||
queryset = (
|
||||
self.queryset.filter(rule__user=user)
|
||||
.filter(Q(rule__category=None) | Q(rule__category__user=user))
|
||||
.order_by("rule", "-publication_date", "-created")
|
||||
)
|
||||
|
||||
return queryset
|
||||
|
||||
|
||||
class DetailPostView(RetrieveUpdateAPIView):
|
||||
queryset = Post.objects.all()
|
||||
serializer_class = PostSerializer
|
||||
permission_classes = (IsAuthenticated, IsPostOwner)
|
||||
|
||||
|
||||
class ListCategoryView(ListCreateAPIView):
|
||||
queryset = Category.objects.all()
|
||||
serializer_class = CategorySerializer
|
||||
|
||||
def get_queryset(self):
|
||||
user = self.request.user
|
||||
return self.queryset.filter(user=user).order_by("-created", "-modified")
|
||||
|
||||
|
||||
class DetailCategoryView(RetrieveUpdateDestroyAPIView):
|
||||
queryset = Category.objects.all()
|
||||
serializer_class = CategorySerializer
|
||||
|
||||
|
||||
class NestedRuleCategoryView(ListAPIView):
|
||||
queryset = Category.objects.prefetch_related("rules").all()
|
||||
serializer_class = RuleSerializer
|
||||
|
||||
def get_queryset(self) -> QuerySet:
|
||||
lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
|
||||
|
||||
# Default permission is IsOwner, therefore there shouldn't have to be
|
||||
# filtered on the user.
|
||||
filter_kwargs = {self.lookup_field: self.kwargs[lookup_url_kwarg]}
|
||||
|
||||
category = get_object_or_404(self.queryset, **filter_kwargs)
|
||||
self.check_object_permissions(self.request, category)
|
||||
|
||||
return category.rules.order_by("name")
|
||||
|
||||
|
||||
class NestedPostCategoryView(ListAPIView):
|
||||
queryset = Category.objects.prefetch_related("rules", "rules__posts").all()
|
||||
serializer_class = PostSerializer
|
||||
pagination_class = LargeResultSetPagination
|
||||
filter_backends = [ReadFilter]
|
||||
|
||||
def get_queryset(self):
|
||||
lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
|
||||
|
||||
# Default permission is IsOwner, therefore there shouldn't have to be
|
||||
# filtered on the user.
|
||||
filter_kwargs = {self.lookup_field: self.kwargs[lookup_url_kwarg]}
|
||||
|
||||
category = get_object_or_404(self.queryset, **filter_kwargs)
|
||||
self.check_object_permissions(self.request, category)
|
||||
|
||||
queryset = Post.objects.filter(
|
||||
rule__in=category.rules.values_list("id", flat=True)
|
||||
).order_by("rule__name", "-publication_date")
|
||||
|
||||
return queryset
|
||||
|
||||
|
||||
class CategoryReadView(GenericAPIView):
|
||||
queryset = Category.objects.all()
|
||||
serializer_class = CategorySerializer
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
category = self.get_object()
|
||||
|
||||
(
|
||||
Post.objects.filter(
|
||||
rule__in=category.rules.values_list("pk", flat=True)
|
||||
).update(read=True)
|
||||
)
|
||||
|
||||
category.refresh_from_db()
|
||||
serializer_class = self.get_serializer_class()
|
||||
serializer = serializer_class(category)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
32
src/newsreader/news/core/filters.py
Normal file
32
src/newsreader/news/core/filters.py
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
from django.utils.encoding import force_text
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from rest_framework import filters
|
||||
from rest_framework.compat import coreapi, coreschema
|
||||
|
||||
|
||||
class ReadFilter(filters.BaseFilterBackend):
|
||||
query_param = "read"
|
||||
|
||||
def filter_queryset(self, request, queryset, view):
|
||||
key = request.query_params.get(self.query_param, None)
|
||||
available_values = {"True": True, "true": True, "False": False, "false": False}
|
||||
|
||||
if not key or key not in available_values.keys():
|
||||
return queryset
|
||||
|
||||
value = available_values[key]
|
||||
return queryset.filter(read=value)
|
||||
|
||||
def get_schema_fields(self, view):
|
||||
return [
|
||||
coreapi.Field(
|
||||
name=self.query_param,
|
||||
required=False,
|
||||
location="query",
|
||||
schema=coreschema.String(
|
||||
title=force_text(self.query_param),
|
||||
description=force_text(_("Wether posts should be read or not")),
|
||||
),
|
||||
)
|
||||
]
|
||||
14
src/newsreader/news/core/migrations/0003_post_read.py
Normal file
14
src/newsreader/news/core/migrations/0003_post_read.py
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
# Generated by Django 2.2 on 2019-09-09 19:06
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [("core", "0002_auto_20190714_1425")]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="post", name="read", field=models.BooleanField(default=False)
|
||||
)
|
||||
]
|
||||
|
|
@ -12,6 +12,8 @@ class Post(TimeStampedModel):
|
|||
publication_date = models.DateTimeField(blank=True, null=True)
|
||||
url = models.URLField(max_length=1024, blank=True, null=True)
|
||||
|
||||
read = models.BooleanField(default=False)
|
||||
|
||||
rule = models.ForeignKey(
|
||||
CollectionRule, on_delete=models.CASCADE, editable=False, related_name="posts"
|
||||
)
|
||||
|
|
@ -24,7 +26,7 @@ class Post(TimeStampedModel):
|
|||
|
||||
|
||||
class Category(TimeStampedModel):
|
||||
name = models.CharField(max_length=50, unique=True)
|
||||
name = models.CharField(max_length=50, unique=True) # TODO remove unique value
|
||||
user = models.ForeignKey("accounts.User", _("Owner"), related_name="categories")
|
||||
|
||||
class Meta:
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ from newsreader.news import collection
|
|||
from newsreader.news.core.models import Category, Post
|
||||
|
||||
|
||||
class PostSerializer(serializers.HyperlinkedModelSerializer):
|
||||
class PostSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Post
|
||||
fields = (
|
||||
|
|
@ -16,24 +16,19 @@ class PostSerializer(serializers.HyperlinkedModelSerializer):
|
|||
"publication_date",
|
||||
"url",
|
||||
"rule",
|
||||
"read",
|
||||
)
|
||||
extra_kwargs = {"rule": {"view_name": "api:rules-detail"}}
|
||||
|
||||
|
||||
class CategorySerializer(serializers.HyperlinkedModelSerializer):
|
||||
rules = serializers.SerializerMethodField()
|
||||
class CategorySerializer(serializers.ModelSerializer):
|
||||
user = serializers.HiddenField(default=serializers.CurrentUserDefault())
|
||||
unread = serializers.SerializerMethodField()
|
||||
|
||||
def get_rules(self, instance):
|
||||
request = self.context.get("request")
|
||||
rules = instance.rules.order_by("-modified", "-created")
|
||||
|
||||
serializer = collection.serializers.CollectionRuleSerializer(
|
||||
rules, context={"request": request}, many=True
|
||||
)
|
||||
return serializer.data
|
||||
def get_unread(self, category):
|
||||
return Post.objects.filter(
|
||||
rule__in=category.rules.values_list("pk", flat=True), read=False
|
||||
).count()
|
||||
|
||||
class Meta:
|
||||
model = Category
|
||||
fields = ("id", "name", "rules", "user")
|
||||
extra_kwargs = {"rules": {"view_name": "api:rules-detail"}}
|
||||
fields = ("id", "name", "user", "unread")
|
||||
|
|
|
|||
15
src/newsreader/news/core/templates/core/main.html
Normal file
15
src/newsreader/news/core/templates/core/main.html
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% load static %}
|
||||
|
||||
{% block head %}
|
||||
<link href="{% static 'core/dist/css/core.css' %}" rel="stylesheet" />
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="content"></div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="{% static 'core/dist/js/homepage.js' %}"></script>
|
||||
{% endblock %}
|
||||
|
|
@ -10,25 +10,20 @@ from newsreader.news.core.tests.factories import CategoryFactory, PostFactory
|
|||
|
||||
class CategoryDetailViewTestCase(TestCase):
|
||||
def setUp(self):
|
||||
self.maxDiff = None
|
||||
|
||||
self.client = Client()
|
||||
self.user = UserFactory(is_staff=True, password="test")
|
||||
self.user = UserFactory(password="test")
|
||||
self.client.login(email=self.user.email, password="test")
|
||||
|
||||
def test_simple(self):
|
||||
category = CategoryFactory(user=self.user)
|
||||
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.get(reverse("api:categories-detail", args=[category.pk]))
|
||||
data = response.json()
|
||||
|
||||
self.assertEquals(response.status_code, 200)
|
||||
self.assertTrue("id" in data)
|
||||
self.assertTrue("name" in data)
|
||||
self.assertTrue("rules" in data)
|
||||
|
||||
def test_not_known(self):
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.get(reverse("api:categories-detail", args=[100]))
|
||||
data = response.json()
|
||||
|
||||
|
|
@ -38,7 +33,6 @@ class CategoryDetailViewTestCase(TestCase):
|
|||
def test_post(self):
|
||||
category = CategoryFactory(user=self.user)
|
||||
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.post(reverse("api:categories-detail", args=[category.pk]))
|
||||
data = response.json()
|
||||
|
||||
|
|
@ -48,7 +42,6 @@ class CategoryDetailViewTestCase(TestCase):
|
|||
def test_patch(self):
|
||||
category = CategoryFactory(name="Clickbait", user=self.user)
|
||||
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.patch(
|
||||
reverse("api:categories-detail", args=[category.pk]),
|
||||
data=json.dumps({"name": "Interesting posts"}),
|
||||
|
|
@ -62,7 +55,6 @@ class CategoryDetailViewTestCase(TestCase):
|
|||
def test_identifier_cannot_be_changed(self):
|
||||
category = CategoryFactory(user=self.user)
|
||||
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.patch(
|
||||
reverse("api:categories-detail", args=[category.pk]),
|
||||
data=json.dumps({"id": 44}),
|
||||
|
|
@ -76,7 +68,6 @@ class CategoryDetailViewTestCase(TestCase):
|
|||
def test_put(self):
|
||||
category = CategoryFactory(name="Clickbait", user=self.user)
|
||||
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.put(
|
||||
reverse("api:categories-detail", args=[category.pk]),
|
||||
data=json.dumps({"name": "Interesting posts"}),
|
||||
|
|
@ -90,74 +81,15 @@ class CategoryDetailViewTestCase(TestCase):
|
|||
def test_delete(self):
|
||||
category = CategoryFactory(user=self.user)
|
||||
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.delete(
|
||||
reverse("api:categories-detail", args=[category.pk])
|
||||
)
|
||||
|
||||
self.assertEquals(response.status_code, 204)
|
||||
|
||||
def test_rules(self):
|
||||
category = CategoryFactory(user=self.user)
|
||||
rules = CollectionRuleFactory.create_batch(
|
||||
size=5, category=category, user=self.user
|
||||
)
|
||||
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.get(reverse("api:categories-detail", args=[category.pk]))
|
||||
data = response.json()
|
||||
|
||||
self.assertEquals(response.status_code, 200)
|
||||
|
||||
self.assertTrue("id" in data["rules"][0])
|
||||
self.assertTrue("name" in data["rules"][0])
|
||||
self.assertTrue("url" in data["rules"][0])
|
||||
|
||||
def test_rules_with_posts(self):
|
||||
category = CategoryFactory(user=self.user)
|
||||
|
||||
rules = {
|
||||
rule.pk: PostFactory.create_batch(size=5, rule=rule)
|
||||
for rule in CollectionRuleFactory.create_batch(
|
||||
size=5, category=category, user=self.user
|
||||
)
|
||||
}
|
||||
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.get(reverse("api:categories-detail", args=[category.pk]))
|
||||
data = response.json()
|
||||
|
||||
self.assertEquals(response.status_code, 200)
|
||||
|
||||
self.assertEquals(len(data["rules"][0]["posts"]), 5)
|
||||
|
||||
def test_rules_with_posts_ordered(self):
|
||||
category = CategoryFactory(user=self.user)
|
||||
|
||||
rules = {
|
||||
rule.pk: PostFactory.create_batch(size=5, rule=rule)
|
||||
for rule in CollectionRuleFactory.create_batch(
|
||||
size=5, category=category, user=self.user
|
||||
)
|
||||
}
|
||||
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.get(reverse("api:categories-detail", args=[category.pk]))
|
||||
data = response.json()
|
||||
|
||||
self.assertEquals(response.status_code, 200)
|
||||
|
||||
posts = data["rules"][0]["posts"]
|
||||
|
||||
for count, post in enumerate(posts):
|
||||
if count < 1:
|
||||
continue
|
||||
|
||||
self.assertTrue(
|
||||
post["publication_date"] < posts[count - 1]["publication_date"]
|
||||
)
|
||||
|
||||
def test_category_with_unauthenticated_user(self):
|
||||
self.client.logout()
|
||||
|
||||
category = CategoryFactory(user=self.user)
|
||||
|
||||
response = self.client.get(reverse("api:categories-detail", args=[category.pk]))
|
||||
|
|
@ -168,7 +100,112 @@ class CategoryDetailViewTestCase(TestCase):
|
|||
other_user = UserFactory()
|
||||
category = CategoryFactory(user=other_user)
|
||||
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.get(reverse("api:categories-detail", args=[category.pk]))
|
||||
|
||||
self.assertEquals(response.status_code, 403)
|
||||
|
||||
def test_read_count(self):
|
||||
category = CategoryFactory(user=self.user)
|
||||
unread_rule = CollectionRuleFactory(category=category)
|
||||
read_rule = CollectionRuleFactory(category=category)
|
||||
|
||||
PostFactory.create_batch(size=20, read=False, rule=unread_rule)
|
||||
PostFactory.create_batch(size=20, read=True, rule=read_rule)
|
||||
|
||||
response = self.client.get(reverse("api:categories-detail", args=[category.pk]))
|
||||
data = response.json()
|
||||
|
||||
self.assertEquals(response.status_code, 200)
|
||||
self.assertEquals(data["unread"], 20)
|
||||
|
||||
|
||||
class CategoryReadTestCase(TestCase):
|
||||
def setUp(self):
|
||||
self.user = UserFactory(password="test")
|
||||
self.client.login(email=self.user.email, password="test")
|
||||
|
||||
def test_category_read(self):
|
||||
category = CategoryFactory(user=self.user)
|
||||
rules = [
|
||||
PostFactory.create_batch(size=5, read=False, rule=rule)
|
||||
for rule in CollectionRuleFactory.create_batch(size=5, category=category)
|
||||
]
|
||||
|
||||
response = self.client.post(reverse("api:categories-read", args=[category.pk]))
|
||||
|
||||
data = response.json()
|
||||
|
||||
self.assertEquals(response.status_code, 201)
|
||||
self.assertEquals(data["unread"], 0)
|
||||
self.assertEquals(data["id"], category.pk)
|
||||
|
||||
def test_category_unknown(self):
|
||||
response = self.client.post(reverse("api:categories-read", args=[101]))
|
||||
|
||||
self.assertEquals(response.status_code, 404)
|
||||
|
||||
def test_unauthenticated_user(self):
|
||||
self.client.logout()
|
||||
|
||||
category = CategoryFactory(user=self.user)
|
||||
rules = [
|
||||
PostFactory.create_batch(size=5, read=False, rule=rule)
|
||||
for rule in CollectionRuleFactory.create_batch(
|
||||
size=5, category=category, user=self.user
|
||||
)
|
||||
]
|
||||
|
||||
response = self.client.post(reverse("api:categories-read", args=[category.pk]))
|
||||
|
||||
self.assertEquals(response.status_code, 403)
|
||||
|
||||
def test_unauthorized_user(self):
|
||||
other_user = UserFactory()
|
||||
category = CategoryFactory(user=other_user)
|
||||
|
||||
rules = [
|
||||
PostFactory.create_batch(size=5, read=False, rule=rule)
|
||||
for rule in CollectionRuleFactory.create_batch(
|
||||
size=5, category=category, user=other_user
|
||||
)
|
||||
]
|
||||
|
||||
response = self.client.post(reverse("api:categories-read", args=[category.pk]))
|
||||
|
||||
self.assertEquals(response.status_code, 403)
|
||||
|
||||
def test_get(self):
|
||||
category = CategoryFactory(name="Clickbait", user=self.user)
|
||||
|
||||
response = self.client.get(reverse("api:categories-read", args=[category.pk]))
|
||||
|
||||
self.assertEquals(response.status_code, 405)
|
||||
|
||||
def test_patch(self):
|
||||
category = CategoryFactory(name="Clickbait", user=self.user)
|
||||
|
||||
response = self.client.patch(
|
||||
reverse("api:categories-read", args=[category.pk]),
|
||||
data=json.dumps({"name": "Not possible"}),
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
self.assertEquals(response.status_code, 405)
|
||||
|
||||
def test_put(self):
|
||||
category = CategoryFactory(name="Clickbait", user=self.user)
|
||||
|
||||
response = self.client.put(
|
||||
reverse("api:categories-read", args=[category.pk]),
|
||||
data=json.dumps({"name": "Not possible"}),
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
self.assertEquals(response.status_code, 405)
|
||||
|
||||
def test_delete(self):
|
||||
category = CategoryFactory(name="Clickbait", user=self.user)
|
||||
|
||||
response = self.client.delete(reverse("api:categories-read", args=[category.pk]))
|
||||
|
||||
self.assertEquals(response.status_code, 405)
|
||||
|
|
|
|||
|
|
@ -14,22 +14,17 @@ from newsreader.news.core.tests.factories import CategoryFactory, PostFactory
|
|||
|
||||
class CategoryListViewTestCase(TestCase):
|
||||
def setUp(self):
|
||||
self.maxDiff = None
|
||||
|
||||
self.client = Client()
|
||||
self.user = UserFactory(is_staff=True, password="test")
|
||||
self.user = UserFactory(password="test")
|
||||
self.client.login(email=self.user.email, password="test")
|
||||
|
||||
def test_simple(self):
|
||||
CategoryFactory.create_batch(size=3, user=self.user)
|
||||
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.get(reverse("api:categories-list"))
|
||||
data = response.json()
|
||||
|
||||
self.assertEquals(response.status_code, 200)
|
||||
self.assertTrue("results" in data)
|
||||
self.assertTrue("count" in data)
|
||||
self.assertEquals(data["count"], 3)
|
||||
self.assertEquals(len(data), 3)
|
||||
|
||||
def test_ordering(self):
|
||||
categories = [
|
||||
|
|
@ -53,35 +48,25 @@ class CategoryListViewTestCase(TestCase):
|
|||
),
|
||||
]
|
||||
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.get(reverse("api:categories-list"))
|
||||
data = response.json()
|
||||
|
||||
self.assertEquals(response.status_code, 200)
|
||||
self.assertTrue("results" in data)
|
||||
self.assertTrue("count" in data)
|
||||
self.assertEquals(data["count"], 3)
|
||||
|
||||
self.assertEquals(data["results"][0]["id"], categories[1].pk)
|
||||
self.assertEquals(data["results"][1]["id"], categories[2].pk)
|
||||
self.assertEquals(data["results"][2]["id"], categories[0].pk)
|
||||
self.assertEquals(data[0]["id"], categories[1].pk)
|
||||
self.assertEquals(data[1]["id"], categories[2].pk)
|
||||
self.assertEquals(data[2]["id"], categories[0].pk)
|
||||
|
||||
def test_empty(self):
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.get(reverse("api:categories-list"))
|
||||
data = response.json()
|
||||
|
||||
self.assertEquals(response.status_code, 200)
|
||||
self.assertTrue("results" in data)
|
||||
self.assertTrue("count" in data)
|
||||
|
||||
self.assertEquals(data["count"], 0)
|
||||
self.assertEquals(len(data["results"]), 0)
|
||||
self.assertEquals(len(data), 0)
|
||||
|
||||
def test_post(self):
|
||||
data = {"name": "Tech"}
|
||||
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.post(
|
||||
reverse("api:categories-list"),
|
||||
data=json.dumps(data),
|
||||
|
|
@ -93,7 +78,6 @@ class CategoryListViewTestCase(TestCase):
|
|||
self.assertEquals(response_data["name"], "Tech")
|
||||
|
||||
def test_patch(self):
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.patch(reverse("api:categories-list"))
|
||||
data = response.json()
|
||||
|
||||
|
|
@ -101,7 +85,6 @@ class CategoryListViewTestCase(TestCase):
|
|||
self.assertEquals(data["detail"], 'Method "PATCH" not allowed.')
|
||||
|
||||
def test_put(self):
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.put(reverse("api:categories-list"))
|
||||
data = response.json()
|
||||
|
||||
|
|
@ -109,66 +92,15 @@ class CategoryListViewTestCase(TestCase):
|
|||
self.assertEquals(data["detail"], 'Method "PUT" not allowed.')
|
||||
|
||||
def test_delete(self):
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.delete(reverse("api:categories-list"))
|
||||
data = response.json()
|
||||
|
||||
self.assertEquals(response.status_code, 405)
|
||||
self.assertEquals(data["detail"], 'Method "DELETE" not allowed.')
|
||||
|
||||
def test_rules(self):
|
||||
categories = {
|
||||
category.pk: CollectionRuleFactory.create_batch(
|
||||
size=5, category=category, user=self.user
|
||||
)
|
||||
for category in CategoryFactory.create_batch(size=5, user=self.user)
|
||||
}
|
||||
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.get(reverse("api:categories-list"))
|
||||
data = response.json()
|
||||
|
||||
self.assertEquals(response.status_code, 200)
|
||||
self.assertTrue("results" in data)
|
||||
self.assertTrue("count" in data)
|
||||
self.assertEquals(data["count"], 5)
|
||||
|
||||
self.assertEquals(len(data["results"]), 5)
|
||||
|
||||
self.assertEquals(len(data["results"][0]["rules"]), 5)
|
||||
|
||||
self.assertTrue("id" in data["results"][0]["rules"][0])
|
||||
self.assertTrue("name" in data["results"][0]["rules"][0])
|
||||
self.assertTrue("url" in data["results"][0]["rules"][0])
|
||||
self.assertTrue("posts" in data["results"][0]["rules"][0])
|
||||
|
||||
def test_rules_with_posts(self):
|
||||
categories = {
|
||||
category.pk: CollectionRuleFactory.create_batch(
|
||||
size=5, category=category, user=self.user
|
||||
)
|
||||
for category in CategoryFactory.create_batch(size=5, user=self.user)
|
||||
}
|
||||
|
||||
for category in categories:
|
||||
for rule in categories[category]:
|
||||
PostFactory.create_batch(size=5, rule=rule)
|
||||
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.get(reverse("api:categories-list"))
|
||||
data = response.json()
|
||||
|
||||
self.assertEquals(response.status_code, 200)
|
||||
self.assertTrue("results" in data)
|
||||
self.assertTrue("count" in data)
|
||||
self.assertEquals(data["count"], 5)
|
||||
|
||||
self.assertEquals(len(data["results"]), 5)
|
||||
|
||||
self.assertEquals(len(data["results"][0]["rules"]), 5)
|
||||
self.assertEquals(len(data["results"][0]["rules"][0]["posts"]), 5)
|
||||
|
||||
def test_categories_with_unauthenticated_user(self):
|
||||
self.client.logout()
|
||||
|
||||
CategoryFactory.create_batch(size=3, user=self.user)
|
||||
|
||||
response = self.client.get(reverse("api:categories-list"))
|
||||
|
|
@ -179,10 +111,458 @@ class CategoryListViewTestCase(TestCase):
|
|||
other_user = UserFactory()
|
||||
CategoryFactory.create_batch(size=3, user=other_user)
|
||||
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.get(reverse("api:categories-list"))
|
||||
data = response.json()
|
||||
|
||||
self.assertEquals(response.status_code, 200)
|
||||
self.assertEquals(len(data["results"]), 0)
|
||||
self.assertEquals(len(data), 0)
|
||||
|
||||
|
||||
class NestedCategoryListViewTestCase(TestCase):
|
||||
def setUp(self):
|
||||
self.user = UserFactory(password="test")
|
||||
self.client.login(email=self.user.email, password="test")
|
||||
|
||||
def test_simple(self):
|
||||
category = CategoryFactory.create(user=self.user)
|
||||
rules = CollectionRuleFactory.create_batch(size=5, category=category)
|
||||
|
||||
response = self.client.get(
|
||||
reverse("api:categories-nested-rules", kwargs={"pk": category.pk})
|
||||
)
|
||||
data = response.json()
|
||||
|
||||
self.assertEquals(response.status_code, 200)
|
||||
self.assertEquals(len(data), 5)
|
||||
|
||||
self.assertTrue("id" in data[0])
|
||||
self.assertTrue("name" in data[0])
|
||||
self.assertTrue("category" in data[0])
|
||||
self.assertTrue("url" in data[0])
|
||||
self.assertTrue("favicon" in data[0])
|
||||
|
||||
def test_empty(self):
|
||||
category = CategoryFactory.create(user=self.user)
|
||||
|
||||
response = self.client.get(
|
||||
reverse("api:categories-nested-rules", kwargs={"pk": category.pk})
|
||||
)
|
||||
data = response.json()
|
||||
|
||||
self.assertEquals(response.status_code, 200)
|
||||
self.assertEquals(len(data), 0)
|
||||
self.assertEquals(data, [])
|
||||
|
||||
def test_not_known(self):
|
||||
response = self.client.get(
|
||||
reverse("api:categories-nested-rules", kwargs={"pk": 100})
|
||||
)
|
||||
|
||||
self.assertEquals(response.status_code, 404)
|
||||
|
||||
def test_post(self):
|
||||
response = self.client.post(
|
||||
reverse("api:categories-nested-rules", kwargs={"pk": 100}),
|
||||
data=json.dumps({}),
|
||||
content_type="application/json",
|
||||
)
|
||||
data = response.json()
|
||||
|
||||
self.assertEquals(response.status_code, 405)
|
||||
self.assertEquals(data["detail"], 'Method "POST" not allowed.')
|
||||
|
||||
def test_patch(self):
|
||||
category = CategoryFactory.create(user=self.user)
|
||||
|
||||
response = self.client.patch(
|
||||
reverse("api:categories-nested-rules", kwargs={"pk": category.pk}),
|
||||
data=json.dumps({"name": "test"}),
|
||||
content_type="application/json",
|
||||
)
|
||||
data = response.json()
|
||||
|
||||
self.assertEquals(response.status_code, 405)
|
||||
self.assertEquals(data["detail"], 'Method "PATCH" not allowed.')
|
||||
|
||||
def test_put(self):
|
||||
category = CategoryFactory.create(user=self.user)
|
||||
|
||||
response = self.client.put(
|
||||
reverse("api:categories-nested-rules", kwargs={"pk": category.pk}),
|
||||
data=json.dumps({"name": "test"}),
|
||||
content_type="application/json",
|
||||
)
|
||||
data = response.json()
|
||||
|
||||
self.assertEquals(response.status_code, 405)
|
||||
self.assertEquals(data["detail"], 'Method "PUT" not allowed.')
|
||||
|
||||
def test_delete(self):
|
||||
category = CategoryFactory.create(user=self.user)
|
||||
|
||||
response = self.client.delete(
|
||||
reverse("api:categories-nested-rules", kwargs={"pk": category.pk}),
|
||||
content_type="application/json",
|
||||
)
|
||||
data = response.json()
|
||||
|
||||
self.assertEquals(response.status_code, 405)
|
||||
self.assertEquals(data["detail"], 'Method "DELETE" not allowed.')
|
||||
|
||||
def test_with_unauthenticated_user(self):
|
||||
self.client.logout()
|
||||
|
||||
category = CategoryFactory.create(user=self.user)
|
||||
rules = CollectionRuleFactory.create_batch(size=5, category=category)
|
||||
|
||||
response = self.client.get(
|
||||
reverse("api:categories-nested-rules", kwargs={"pk": category.pk})
|
||||
)
|
||||
|
||||
self.assertEquals(response.status_code, 403)
|
||||
|
||||
def test_with_unauthorized_user(self):
|
||||
other_user = UserFactory.create()
|
||||
|
||||
category = CategoryFactory.create(user=other_user)
|
||||
rules = CollectionRuleFactory.create_batch(size=5, category=category)
|
||||
|
||||
response = self.client.get(
|
||||
reverse("api:categories-nested-rules", kwargs={"pk": category.pk})
|
||||
)
|
||||
|
||||
self.assertEquals(response.status_code, 403)
|
||||
|
||||
def test_ordering(self):
|
||||
category = CategoryFactory.create(user=self.user)
|
||||
rules = [
|
||||
CollectionRuleFactory.create(category=category, name="Durp"),
|
||||
CollectionRuleFactory.create(category=category, name="Slurp"),
|
||||
CollectionRuleFactory.create(category=category, name="Burp"),
|
||||
]
|
||||
|
||||
response = self.client.get(
|
||||
reverse("api:categories-nested-rules", kwargs={"pk": category.pk})
|
||||
)
|
||||
data = response.json()
|
||||
|
||||
self.assertEquals(response.status_code, 200)
|
||||
self.assertEquals(len(data), 3)
|
||||
|
||||
self.assertEquals(data[0]["id"], rules[2].pk)
|
||||
self.assertEquals(data[1]["id"], rules[0].pk)
|
||||
self.assertEquals(data[2]["id"], rules[1].pk)
|
||||
|
||||
def test_only_rules_from_category_are_returned(self):
|
||||
other_category = CategoryFactory(user=self.user)
|
||||
CollectionRuleFactory.create_batch(size=5, category=other_category)
|
||||
|
||||
category = CategoryFactory.create(user=self.user)
|
||||
rules = [
|
||||
CollectionRuleFactory.create(category=category, name="Durp"),
|
||||
CollectionRuleFactory.create(category=category, name="Slurp"),
|
||||
CollectionRuleFactory.create(category=category, name="Burp"),
|
||||
]
|
||||
|
||||
response = self.client.get(
|
||||
reverse("api:categories-nested-rules", kwargs={"pk": category.pk})
|
||||
)
|
||||
data = response.json()
|
||||
|
||||
self.assertEquals(response.status_code, 200)
|
||||
self.assertEquals(len(data), 3)
|
||||
|
||||
self.assertEquals(data[0]["id"], rules[2].pk)
|
||||
self.assertEquals(data[1]["id"], rules[0].pk)
|
||||
self.assertEquals(data[2]["id"], rules[1].pk)
|
||||
|
||||
|
||||
class NestedCategoryPostView(TestCase):
|
||||
def setUp(self):
|
||||
self.user = UserFactory(password="test")
|
||||
self.client.login(email=self.user.email, password="test")
|
||||
|
||||
def test_simple(self):
|
||||
category = CategoryFactory.create(user=self.user)
|
||||
rules = {
|
||||
rule.pk: PostFactory.create_batch(size=5, rule=rule)
|
||||
for rule in CollectionRuleFactory.create_batch(
|
||||
size=5, category=category, user=self.user
|
||||
)
|
||||
}
|
||||
|
||||
response = self.client.get(
|
||||
reverse("api:categories-nested-posts", kwargs={"pk": category.pk})
|
||||
)
|
||||
data = response.json()
|
||||
posts = data["results"]
|
||||
|
||||
self.assertEquals(response.status_code, 200)
|
||||
self.assertEquals(data["count"], 25)
|
||||
|
||||
self.assertTrue("id" in posts[0])
|
||||
self.assertTrue("title" in posts[0])
|
||||
self.assertTrue("body" in posts[0])
|
||||
self.assertTrue("rule" in posts[0])
|
||||
self.assertTrue("url" in posts[0])
|
||||
|
||||
def test_no_rules(self):
|
||||
category = CategoryFactory.create(user=self.user)
|
||||
|
||||
response = self.client.get(
|
||||
reverse("api:categories-nested-posts", kwargs={"pk": category.pk})
|
||||
)
|
||||
data = response.json()
|
||||
posts = data["results"]
|
||||
|
||||
self.assertEquals(response.status_code, 200)
|
||||
self.assertEquals(data["count"], 0)
|
||||
self.assertEquals(posts, [])
|
||||
|
||||
def test_no_posts(self):
|
||||
category = CategoryFactory.create(user=self.user)
|
||||
rules = CollectionRuleFactory.create_batch(
|
||||
size=5, user=self.user, category=category
|
||||
)
|
||||
|
||||
response = self.client.get(
|
||||
reverse("api:categories-nested-posts", kwargs={"pk": category.pk})
|
||||
)
|
||||
data = response.json()
|
||||
posts = data["results"]
|
||||
|
||||
self.assertEquals(response.status_code, 200)
|
||||
self.assertEquals(data["count"], 0)
|
||||
self.assertEquals(posts, [])
|
||||
|
||||
def test_not_known(self):
|
||||
response = self.client.get(
|
||||
reverse("api:categories-nested-posts", kwargs={"pk": 100})
|
||||
)
|
||||
|
||||
self.assertEquals(response.status_code, 404)
|
||||
|
||||
def test_post(self):
|
||||
response = self.client.post(
|
||||
reverse("api:categories-nested-posts", kwargs={"pk": 100}),
|
||||
data=json.dumps({}),
|
||||
content_type="application/json",
|
||||
)
|
||||
data = response.json()
|
||||
|
||||
self.assertEquals(response.status_code, 405)
|
||||
self.assertEquals(data["detail"], 'Method "POST" not allowed.')
|
||||
|
||||
def test_patch(self):
|
||||
category = CategoryFactory.create(user=self.user)
|
||||
|
||||
response = self.client.patch(
|
||||
reverse("api:categories-nested-posts", kwargs={"pk": category.pk}),
|
||||
data=json.dumps({}),
|
||||
content_type="application/json",
|
||||
)
|
||||
data = response.json()
|
||||
|
||||
self.assertEquals(response.status_code, 405)
|
||||
self.assertEquals(data["detail"], 'Method "PATCH" not allowed.')
|
||||
|
||||
def test_put(self):
|
||||
category = CategoryFactory.create(user=self.user)
|
||||
|
||||
response = self.client.put(
|
||||
reverse("api:categories-nested-posts", kwargs={"pk": category.pk}),
|
||||
data=json.dumps({}),
|
||||
content_type="application/json",
|
||||
)
|
||||
data = response.json()
|
||||
|
||||
self.assertEquals(response.status_code, 405)
|
||||
self.assertEquals(data["detail"], 'Method "PUT" not allowed.')
|
||||
|
||||
def test_delete(self):
|
||||
category = CategoryFactory.create(user=self.user)
|
||||
|
||||
response = self.client.delete(
|
||||
reverse("api:categories-nested-posts", kwargs={"pk": category.pk}),
|
||||
content_type="application/json",
|
||||
)
|
||||
data = response.json()
|
||||
|
||||
self.assertEquals(response.status_code, 405)
|
||||
self.assertEquals(data["detail"], 'Method "DELETE" not allowed.')
|
||||
|
||||
def test_with_unauthenticated_user(self):
|
||||
self.client.logout()
|
||||
|
||||
category = CategoryFactory.create(user=self.user)
|
||||
|
||||
response = self.client.get(
|
||||
reverse("api:categories-nested-posts", kwargs={"pk": category.pk})
|
||||
)
|
||||
|
||||
self.assertEquals(response.status_code, 403)
|
||||
|
||||
def test_with_unauthorized_user(self):
|
||||
other_user = UserFactory.create()
|
||||
category = CategoryFactory.create(user=other_user)
|
||||
|
||||
response = self.client.get(
|
||||
reverse("api:categories-nested-posts", kwargs={"pk": category.pk})
|
||||
)
|
||||
|
||||
self.assertEquals(response.status_code, 403)
|
||||
|
||||
def test_ordering(self):
|
||||
category = CategoryFactory.create(user=self.user)
|
||||
|
||||
bbc_rule = CollectionRuleFactory.create(
|
||||
name="BBC", category=category, user=self.user
|
||||
)
|
||||
guardian_rule = CollectionRuleFactory.create(
|
||||
name="The Guardian", category=category, user=self.user
|
||||
)
|
||||
reuters_rule = CollectionRuleFactory.create(
|
||||
name="Reuters", category=category, user=self.user
|
||||
)
|
||||
|
||||
reuters_rule = [
|
||||
PostFactory.create(
|
||||
title="Second Reuters post",
|
||||
rule=reuters_rule,
|
||||
publication_date=datetime.combine(
|
||||
date(2019, 5, 21), time(hour=16, minute=7, second=37), pytz.utc
|
||||
),
|
||||
),
|
||||
PostFactory.create(
|
||||
title="First Reuters post",
|
||||
rule=reuters_rule,
|
||||
publication_date=datetime.combine(
|
||||
date(2019, 5, 20), time(hour=16, minute=7, second=37), pytz.utc
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
guardian_posts = [
|
||||
PostFactory.create(
|
||||
title="Second Guardian post",
|
||||
rule=guardian_rule,
|
||||
publication_date=datetime.combine(
|
||||
date(2019, 5, 21), time(hour=16, minute=7, second=37), pytz.utc
|
||||
),
|
||||
),
|
||||
PostFactory.create(
|
||||
title="First Guardian post",
|
||||
rule=guardian_rule,
|
||||
publication_date=datetime.combine(
|
||||
date(2019, 5, 20), time(hour=16, minute=7, second=37), pytz.utc
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
bbc_posts = [
|
||||
PostFactory.create(
|
||||
title="Second BBC post",
|
||||
rule=bbc_rule,
|
||||
publication_date=datetime.combine(
|
||||
date(2019, 5, 21), time(hour=16, minute=7, second=37), pytz.utc
|
||||
),
|
||||
),
|
||||
PostFactory.create(
|
||||
title="First BBC post",
|
||||
rule=bbc_rule,
|
||||
publication_date=datetime.combine(
|
||||
date(2019, 5, 20), time(hour=16, minute=7, second=37), pytz.utc
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
response = self.client.get(
|
||||
reverse("api:categories-nested-posts", kwargs={"pk": category.pk})
|
||||
)
|
||||
data = response.json()
|
||||
posts = data["results"]
|
||||
|
||||
self.assertEquals(response.status_code, 200)
|
||||
self.assertEquals(data["count"], 6)
|
||||
|
||||
self.assertEquals(posts[0]["title"], "Second BBC post")
|
||||
self.assertEquals(posts[1]["title"], "First BBC post")
|
||||
|
||||
self.assertEquals(posts[2]["title"], "Second Reuters post")
|
||||
self.assertEquals(posts[3]["title"], "First Reuters post")
|
||||
|
||||
self.assertEquals(posts[4]["title"], "Second Guardian post")
|
||||
self.assertEquals(posts[5]["title"], "First Guardian post")
|
||||
|
||||
def test_only_posts_from_category_are_returned(self):
|
||||
category = CategoryFactory.create(user=self.user)
|
||||
other_category = CategoryFactory.create(user=self.user)
|
||||
|
||||
guardian_rule = CollectionRuleFactory.create(
|
||||
name="BBC", category=category, user=self.user
|
||||
)
|
||||
other_rule = CollectionRuleFactory.create(name="The Guardian", user=self.user)
|
||||
|
||||
guardian_posts = [
|
||||
PostFactory.create(rule=guardian_rule),
|
||||
PostFactory.create(rule=guardian_rule),
|
||||
]
|
||||
|
||||
other_posts = [
|
||||
PostFactory.create(rule=other_rule),
|
||||
PostFactory.create(rule=other_rule),
|
||||
]
|
||||
|
||||
response = self.client.get(
|
||||
reverse("api:categories-nested-posts", kwargs={"pk": category.pk})
|
||||
)
|
||||
data = response.json()
|
||||
posts = data["results"]
|
||||
|
||||
self.assertEquals(response.status_code, 200)
|
||||
self.assertEquals(data["count"], 2)
|
||||
|
||||
self.assertEquals(posts[0]["rule"], guardian_rule.pk)
|
||||
self.assertEquals(posts[1]["rule"], guardian_rule.pk)
|
||||
|
||||
def test_unread_posts(self):
|
||||
category = CategoryFactory.create(user=self.user)
|
||||
rule = CollectionRuleFactory(category=category)
|
||||
|
||||
PostFactory.create_batch(size=10, rule=rule, read=False)
|
||||
PostFactory.create_batch(size=10, rule=rule, read=True)
|
||||
|
||||
response = self.client.get(
|
||||
reverse("api:categories-nested-posts", kwargs={"pk": category.pk}),
|
||||
{"read": "false"},
|
||||
)
|
||||
|
||||
data = response.json()
|
||||
posts = data["results"]
|
||||
|
||||
self.assertEquals(response.status_code, 200)
|
||||
self.assertEquals(data["count"], 10)
|
||||
|
||||
for post in posts:
|
||||
self.assertEquals(post["read"], False)
|
||||
|
||||
def test_read_posts(self):
|
||||
category = CategoryFactory.create(user=self.user)
|
||||
rule = CollectionRuleFactory(category=category)
|
||||
|
||||
PostFactory.create_batch(size=20, rule=rule, read=False)
|
||||
PostFactory.create_batch(size=10, rule=rule, read=True)
|
||||
|
||||
response = self.client.get(
|
||||
reverse("api:categories-nested-posts", kwargs={"pk": category.pk}),
|
||||
{"read": "true"},
|
||||
)
|
||||
|
||||
data = response.json()
|
||||
posts = data["results"]
|
||||
|
||||
self.assertEquals(response.status_code, 200)
|
||||
self.assertEquals(data["count"], 10)
|
||||
|
||||
for post in posts:
|
||||
self.assertEquals(post["read"], True)
|
||||
|
|
|
|||
|
|
@ -10,11 +10,8 @@ from newsreader.news.core.tests.factories import CategoryFactory, PostFactory
|
|||
|
||||
class PostDetailViewTestCase(TestCase):
|
||||
def setUp(self):
|
||||
self.maxDiff = None
|
||||
self.client = Client()
|
||||
|
||||
self.client = Client()
|
||||
self.user = UserFactory(is_staff=True, password="test")
|
||||
self.user = UserFactory(password="test")
|
||||
self.client.login(email=self.user.email, password="test")
|
||||
|
||||
def test_simple(self):
|
||||
rule = CollectionRuleFactory(
|
||||
|
|
@ -22,7 +19,6 @@ class PostDetailViewTestCase(TestCase):
|
|||
)
|
||||
post = PostFactory(rule=rule)
|
||||
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.get(reverse("api:posts-detail", args=[post.pk]))
|
||||
data = response.json()
|
||||
|
||||
|
|
@ -38,7 +34,6 @@ class PostDetailViewTestCase(TestCase):
|
|||
self.assertTrue("remote_identifier" in data)
|
||||
|
||||
def test_not_known(self):
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.get(reverse("api:posts-detail", args=[100]))
|
||||
data = response.json()
|
||||
|
||||
|
|
@ -51,7 +46,6 @@ class PostDetailViewTestCase(TestCase):
|
|||
)
|
||||
post = PostFactory(rule=rule)
|
||||
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.post(reverse("api:posts-detail", args=[post.pk]))
|
||||
data = response.json()
|
||||
|
||||
|
|
@ -64,7 +58,6 @@ class PostDetailViewTestCase(TestCase):
|
|||
)
|
||||
post = PostFactory(title="This is clickbait for sure", rule=rule)
|
||||
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.patch(
|
||||
reverse("api:posts-detail", args=[post.pk]),
|
||||
data=json.dumps({"title": "This title is very accurate"}),
|
||||
|
|
@ -81,7 +74,6 @@ class PostDetailViewTestCase(TestCase):
|
|||
)
|
||||
post = PostFactory(title="This is clickbait for sure", rule=rule)
|
||||
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.patch(
|
||||
reverse("api:posts-detail", args=[post.pk]),
|
||||
data=json.dumps({"id": 44}),
|
||||
|
|
@ -101,18 +93,16 @@ class PostDetailViewTestCase(TestCase):
|
|||
)
|
||||
post = PostFactory(title="This is clickbait for sure", rule=rule)
|
||||
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.patch(
|
||||
reverse("api:posts-detail", args=[post.pk]),
|
||||
data=json.dumps({"rule": reverse("api:rules-detail", args=[new_rule.pk])}),
|
||||
content_type="application/json",
|
||||
)
|
||||
data = response.json()
|
||||
rule_url = data["rule"]
|
||||
|
||||
self.assertEquals(response.status_code, 200)
|
||||
|
||||
self.assertTrue(rule_url.endswith(reverse("api:rules-detail", args=[rule.pk])))
|
||||
self.assertTrue(data["rule"], rule.pk)
|
||||
|
||||
def test_put(self):
|
||||
rule = CollectionRuleFactory(
|
||||
|
|
@ -120,7 +110,6 @@ class PostDetailViewTestCase(TestCase):
|
|||
)
|
||||
post = PostFactory(title="This is clickbait for sure", rule=rule)
|
||||
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.put(
|
||||
reverse("api:posts-detail", args=[post.pk]),
|
||||
data=json.dumps({"title": "This title is very accurate"}),
|
||||
|
|
@ -137,7 +126,6 @@ class PostDetailViewTestCase(TestCase):
|
|||
)
|
||||
post = PostFactory(rule=rule)
|
||||
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.delete(reverse("api:posts-detail", args=[post.pk]))
|
||||
data = response.json()
|
||||
|
||||
|
|
@ -145,6 +133,8 @@ class PostDetailViewTestCase(TestCase):
|
|||
self.assertEquals(data["detail"], 'Method "DELETE" not allowed.')
|
||||
|
||||
def test_post_with_unauthenticated_user_without_category(self):
|
||||
self.client.logout()
|
||||
|
||||
rule = CollectionRuleFactory(user=self.user, category=None)
|
||||
post = PostFactory(rule=rule)
|
||||
|
||||
|
|
@ -153,6 +143,8 @@ class PostDetailViewTestCase(TestCase):
|
|||
self.assertEquals(response.status_code, 403)
|
||||
|
||||
def test_post_with_unauthenticated_user_with_category(self):
|
||||
self.client.logout()
|
||||
|
||||
rule = CollectionRuleFactory(
|
||||
user=self.user, category=CategoryFactory(user=self.user)
|
||||
)
|
||||
|
|
@ -167,7 +159,6 @@ class PostDetailViewTestCase(TestCase):
|
|||
rule = CollectionRuleFactory(user=other_user, category=None)
|
||||
post = PostFactory(rule=rule)
|
||||
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.get(reverse("api:posts-detail", args=[post.pk]))
|
||||
|
||||
self.assertEquals(response.status_code, 403)
|
||||
|
|
@ -179,7 +170,6 @@ class PostDetailViewTestCase(TestCase):
|
|||
)
|
||||
post = PostFactory(rule=rule)
|
||||
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.get(reverse("api:posts-detail", args=[post.pk]))
|
||||
|
||||
self.assertEquals(response.status_code, 403)
|
||||
|
|
@ -191,7 +181,38 @@ class PostDetailViewTestCase(TestCase):
|
|||
)
|
||||
post = PostFactory(rule=rule)
|
||||
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.get(reverse("api:posts-detail", args=[post.pk]))
|
||||
|
||||
self.assertEquals(response.status_code, 403)
|
||||
|
||||
def test_mark_read(self):
|
||||
rule = CollectionRuleFactory(
|
||||
user=self.user, category=CategoryFactory(user=self.user)
|
||||
)
|
||||
post = PostFactory(rule=rule, read=False)
|
||||
|
||||
response = self.client.patch(
|
||||
reverse("api:posts-detail", args=[post.pk]),
|
||||
data=json.dumps({"read": True}),
|
||||
content_type="application/json",
|
||||
)
|
||||
data = response.json()
|
||||
|
||||
self.assertEquals(response.status_code, 200)
|
||||
self.assertEquals(data["read"], True)
|
||||
|
||||
def test_mark_unread(self):
|
||||
rule = CollectionRuleFactory(
|
||||
user=self.user, category=CategoryFactory(user=self.user)
|
||||
)
|
||||
post = PostFactory(rule=rule, read=True)
|
||||
|
||||
response = self.client.patch(
|
||||
reverse("api:posts-detail", args=[post.pk]),
|
||||
data=json.dumps({"read": False}),
|
||||
content_type="application/json",
|
||||
)
|
||||
data = response.json()
|
||||
|
||||
self.assertEquals(response.status_code, 200)
|
||||
self.assertEquals(data["read"], False)
|
||||
|
|
|
|||
|
|
@ -12,10 +12,8 @@ from newsreader.news.core.tests.factories import CategoryFactory, PostFactory
|
|||
|
||||
class PostListViewTestCase(TestCase):
|
||||
def setUp(self):
|
||||
self.maxDiff = None
|
||||
|
||||
self.client = Client()
|
||||
self.user = UserFactory(is_staff=True, password="test")
|
||||
self.client.login(email=self.user.email, password="test")
|
||||
|
||||
def test_simple(self):
|
||||
rule = CollectionRuleFactory(
|
||||
|
|
@ -23,7 +21,6 @@ class PostListViewTestCase(TestCase):
|
|||
)
|
||||
PostFactory.create_batch(size=3, rule=rule)
|
||||
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.get(reverse("api:posts-list"))
|
||||
data = response.json()
|
||||
|
||||
|
|
@ -61,7 +58,6 @@ class PostListViewTestCase(TestCase):
|
|||
),
|
||||
]
|
||||
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.get(reverse("api:posts-list"))
|
||||
data = response.json()
|
||||
|
||||
|
|
@ -81,7 +77,6 @@ class PostListViewTestCase(TestCase):
|
|||
PostFactory.create_batch(size=80, rule=rule)
|
||||
page_size = 50
|
||||
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.get(reverse("api:posts-list"), {"count": 50})
|
||||
data = response.json()
|
||||
|
||||
|
|
@ -90,7 +85,6 @@ class PostListViewTestCase(TestCase):
|
|||
self.assertEquals(len(data["results"]), page_size)
|
||||
|
||||
def test_empty(self):
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.get(reverse("api:posts-list"))
|
||||
data = response.json()
|
||||
|
||||
|
|
@ -102,7 +96,6 @@ class PostListViewTestCase(TestCase):
|
|||
self.assertEquals(len(data["results"]), 0)
|
||||
|
||||
def test_post(self):
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.post(reverse("api:posts-list"))
|
||||
data = response.json()
|
||||
|
||||
|
|
@ -110,7 +103,6 @@ class PostListViewTestCase(TestCase):
|
|||
self.assertEquals(data["detail"], 'Method "POST" not allowed.')
|
||||
|
||||
def test_patch(self):
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.patch(reverse("api:posts-list"))
|
||||
data = response.json()
|
||||
|
||||
|
|
@ -118,7 +110,6 @@ class PostListViewTestCase(TestCase):
|
|||
self.assertEquals(data["detail"], 'Method "PATCH" not allowed.')
|
||||
|
||||
def test_put(self):
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.put(reverse("api:posts-list"))
|
||||
data = response.json()
|
||||
|
||||
|
|
@ -126,7 +117,6 @@ class PostListViewTestCase(TestCase):
|
|||
self.assertEquals(data["detail"], 'Method "PUT" not allowed.')
|
||||
|
||||
def test_delete(self):
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.delete(reverse("api:posts-list"))
|
||||
data = response.json()
|
||||
|
||||
|
|
@ -134,6 +124,8 @@ class PostListViewTestCase(TestCase):
|
|||
self.assertEquals(data["detail"], 'Method "DELETE" not allowed.')
|
||||
|
||||
def test_posts_with_unauthenticated_user_without_category(self):
|
||||
self.client.logout()
|
||||
|
||||
PostFactory.create_batch(size=3, rule=CollectionRuleFactory(user=self.user))
|
||||
|
||||
response = self.client.get(reverse("api:posts-list"))
|
||||
|
|
@ -141,6 +133,8 @@ class PostListViewTestCase(TestCase):
|
|||
self.assertEquals(response.status_code, 403)
|
||||
|
||||
def test_posts_with_unauthenticated_user_with_category(self):
|
||||
self.client.logout()
|
||||
|
||||
category = CategoryFactory(user=self.user)
|
||||
|
||||
PostFactory.create_batch(
|
||||
|
|
@ -157,7 +151,6 @@ class PostListViewTestCase(TestCase):
|
|||
rule = CollectionRuleFactory(user=other_user, category=None)
|
||||
PostFactory.create_batch(size=3, rule=rule)
|
||||
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.get(reverse("api:posts-list"))
|
||||
data = response.json()
|
||||
|
||||
|
|
@ -173,7 +166,6 @@ class PostListViewTestCase(TestCase):
|
|||
size=3, rule=CollectionRuleFactory(user=other_user, category=category)
|
||||
)
|
||||
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.get(reverse("api:posts-list"))
|
||||
data = response.json()
|
||||
|
||||
|
|
@ -191,7 +183,6 @@ class PostListViewTestCase(TestCase):
|
|||
)
|
||||
PostFactory.create_batch(size=3, rule=rule)
|
||||
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.get(reverse("api:posts-list"))
|
||||
data = response.json()
|
||||
|
||||
|
|
@ -201,12 +192,9 @@ class PostListViewTestCase(TestCase):
|
|||
self.assertEquals(data["count"], 0)
|
||||
|
||||
def test_posts_with_authorized_user_without_category(self):
|
||||
UserFactory()
|
||||
|
||||
rule = CollectionRuleFactory(user=self.user, category=None)
|
||||
PostFactory.create_batch(size=3, rule=rule)
|
||||
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.get(reverse("api:posts-list"))
|
||||
data = response.json()
|
||||
|
||||
|
|
@ -214,3 +202,41 @@ class PostListViewTestCase(TestCase):
|
|||
self.assertTrue("results" in data)
|
||||
self.assertTrue("count" in data)
|
||||
self.assertEquals(data["count"], 3)
|
||||
|
||||
def test_unread_posts(self):
|
||||
rule = CollectionRuleFactory(
|
||||
user=self.user, category=CategoryFactory(user=self.user)
|
||||
)
|
||||
|
||||
PostFactory.create_batch(size=10, rule=rule, read=False)
|
||||
PostFactory.create_batch(size=10, rule=rule, read=True)
|
||||
|
||||
response = self.client.get(reverse("api:posts-list"), {"read": "false"})
|
||||
|
||||
data = response.json()
|
||||
posts = data["results"]
|
||||
|
||||
self.assertEquals(response.status_code, 200)
|
||||
self.assertEquals(data["count"], 10)
|
||||
|
||||
for post in posts:
|
||||
self.assertEquals(post["read"], False)
|
||||
|
||||
def test_read_posts(self):
|
||||
rule = CollectionRuleFactory(
|
||||
user=self.user, category=CategoryFactory(user=self.user)
|
||||
)
|
||||
|
||||
PostFactory.create_batch(size=20, rule=rule, read=False)
|
||||
PostFactory.create_batch(size=10, rule=rule, read=True)
|
||||
|
||||
response = self.client.get(reverse("api:posts-list"), {"read": "true"})
|
||||
|
||||
data = response.json()
|
||||
posts = data["results"]
|
||||
|
||||
self.assertEquals(response.status_code, 200)
|
||||
self.assertEquals(data["count"], 10)
|
||||
|
||||
for post in posts:
|
||||
self.assertEquals(post["read"], True)
|
||||
|
|
|
|||
|
|
@ -25,5 +25,7 @@ class PostFactory(factory.django.DjangoModelFactory):
|
|||
"newsreader.news.collection.tests.factories.CollectionRuleFactory"
|
||||
)
|
||||
|
||||
read = False
|
||||
|
||||
class Meta:
|
||||
model = Post
|
||||
|
|
|
|||
|
|
@ -1,18 +1,34 @@
|
|||
from django.contrib.auth.decorators import login_required
|
||||
from django.urls import path
|
||||
|
||||
from newsreader.news.core.views import (
|
||||
DetailCategoryAPIView,
|
||||
DetailPostAPIView,
|
||||
ListCategoryAPIView,
|
||||
ListPostAPIView,
|
||||
from newsreader.news.core.endpoints import (
|
||||
CategoryReadView,
|
||||
DetailCategoryView,
|
||||
DetailPostView,
|
||||
ListCategoryView,
|
||||
ListPostView,
|
||||
NestedPostCategoryView,
|
||||
NestedRuleCategoryView,
|
||||
)
|
||||
from newsreader.news.core.views import MainView
|
||||
|
||||
|
||||
index_page = login_required(MainView.as_view())
|
||||
|
||||
endpoints = [
|
||||
path("posts/", ListPostAPIView.as_view(), name="posts-list"),
|
||||
path("posts/<int:pk>/", DetailPostAPIView.as_view(), name="posts-detail"),
|
||||
path("categories/", ListCategoryAPIView.as_view(), name="categories-list"),
|
||||
path("posts/", ListPostView.as_view(), name="posts-list"),
|
||||
path("posts/<int:pk>/", DetailPostView.as_view(), name="posts-detail"),
|
||||
path("categories/", ListCategoryView.as_view(), name="categories-list"),
|
||||
path("categories/<int:pk>/", DetailCategoryView.as_view(), name="categories-detail"),
|
||||
path("categories/<int:pk>/read/", CategoryReadView.as_view(), name="categories-read"),
|
||||
path(
|
||||
"categories/<int:pk>/", DetailCategoryAPIView.as_view(), name="categories-detail"
|
||||
"categories/<int:pk>/rules/",
|
||||
NestedRuleCategoryView.as_view(),
|
||||
name="categories-nested-rules",
|
||||
),
|
||||
path(
|
||||
"categories/<int:pk>/posts/",
|
||||
NestedPostCategoryView.as_view(),
|
||||
name="categories-nested-posts",
|
||||
),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,49 +1,25 @@
|
|||
from django.db.models import Q
|
||||
from typing import Dict
|
||||
|
||||
from rest_framework.generics import (
|
||||
ListAPIView,
|
||||
ListCreateAPIView,
|
||||
RetrieveUpdateAPIView,
|
||||
RetrieveUpdateDestroyAPIView,
|
||||
)
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
|
||||
from newsreader.accounts.permissions import IsPostOwner
|
||||
from newsreader.core.pagination import LargeResultSetPagination, ResultSetPagination
|
||||
from newsreader.news.core.models import Category, Post
|
||||
from newsreader.news.core.serializers import CategorySerializer, PostSerializer
|
||||
from django.views.generic.base import TemplateView
|
||||
|
||||
|
||||
class ListPostAPIView(ListAPIView):
|
||||
queryset = Post.objects.all()
|
||||
serializer_class = PostSerializer
|
||||
pagination_class = LargeResultSetPagination
|
||||
permission_classes = (IsAuthenticated, IsPostOwner)
|
||||
class MainView(TemplateView):
|
||||
template_name = "core/main.html"
|
||||
|
||||
def get_queryset(self):
|
||||
# TODO serialize objects to show filled main page
|
||||
def get_context_data(self, **kwargs) -> Dict:
|
||||
context = super().get_context_data(**kwargs)
|
||||
user = self.request.user
|
||||
initial_queryset = self.queryset.filter(rule__user=user)
|
||||
return initial_queryset.filter(
|
||||
Q(rule__category=None) | Q(rule__category__user=user)
|
||||
).order_by("rule", "-publication_date", "-created")
|
||||
|
||||
categories = {
|
||||
category: category.rules.order_by("-created")
|
||||
for category in user.categories.order_by("name")
|
||||
}
|
||||
|
||||
class DetailPostAPIView(RetrieveUpdateAPIView):
|
||||
queryset = Post.objects.all()
|
||||
serializer_class = PostSerializer
|
||||
permission_classes = (IsAuthenticated, IsPostOwner)
|
||||
rules = {
|
||||
rule: rule.posts.order_by("-publication_date")[:30]
|
||||
for rule in user.rules.order_by("-created")
|
||||
}
|
||||
|
||||
|
||||
class ListCategoryAPIView(ListCreateAPIView):
|
||||
queryset = Category.objects.all()
|
||||
serializer_class = CategorySerializer
|
||||
pagination_class = ResultSetPagination
|
||||
|
||||
def get_queryset(self):
|
||||
user = self.request.user
|
||||
return self.queryset.filter(user=user).order_by("-created", "-modified")
|
||||
|
||||
|
||||
class DetailCategoryAPIView(RetrieveUpdateDestroyAPIView):
|
||||
queryset = Category.objects.all()
|
||||
serializer_class = CategorySerializer
|
||||
context.update(categories=categories, rules=rules)
|
||||
return context
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@
|
|||
|
||||
&__fieldset {
|
||||
@extend .form__fieldset;
|
||||
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
&__fieldset * {
|
||||
|
|
@ -1,4 +1,6 @@
|
|||
// General imports
|
||||
@import "../partials/variables";
|
||||
@import "../components/index";
|
||||
|
||||
// Page specific
|
||||
@import "./components/index";
|
||||
|
|
@ -2,4 +2,9 @@
|
|||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: $gainsboro;
|
||||
|
||||
& * {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -6,8 +6,6 @@
|
|||
|
||||
padding: 10px 50px;
|
||||
|
||||
width: 50px;
|
||||
|
||||
border: none;
|
||||
border-radius: 2px;
|
||||
|
||||
|
|
@ -4,3 +4,5 @@
|
|||
@import "./main/index";
|
||||
@import "./navbar/index";
|
||||
@import "./error/index";
|
||||
@import "./loading-indicator/index";
|
||||
@import "./modal/index";
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
.loading-indicator {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
|
||||
& div {
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
left: 6px;
|
||||
width: 13px;
|
||||
background-color: $lavendal-pink;
|
||||
animation: loading-indicator 1.2s cubic-bezier(0, 0.5, 0.5, 1) infinite;
|
||||
|
||||
&:nth-child(1){
|
||||
left: 6px;
|
||||
animation-delay: -0.24s;
|
||||
}
|
||||
|
||||
&:nth-child(2){
|
||||
left: 26px;
|
||||
animation-delay: -0.12s;
|
||||
}
|
||||
|
||||
&:nth-child(3){
|
||||
left: 45px;
|
||||
animation-delay: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes loading-indicator {
|
||||
0% {
|
||||
top: 6px;
|
||||
height: 51px;
|
||||
}
|
||||
50%, 100% {
|
||||
top: 19px;
|
||||
height: 26px;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
@import "loading-indicator";
|
||||
9
src/newsreader/scss/components/modal/_modal.scss
Normal file
9
src/newsreader/scss/components/modal/_modal.scss
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
.modal {
|
||||
position: fixed;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
|
||||
background-color: $dark;
|
||||
}
|
||||
1
src/newsreader/scss/components/modal/index.scss
Normal file
1
src/newsreader/scss/components/modal/index.scss
Normal file
|
|
@ -0,0 +1 @@
|
|||
@import "modal";
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
padding: 15px 0;
|
||||
width: 100%;
|
||||
|
||||
background-color: $white;
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
.categories {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
width: 90%;
|
||||
|
||||
font-family: $sidebar-font;
|
||||
border-radius: 2px;
|
||||
|
||||
& ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
width: 100%;
|
||||
|
||||
list-style: none;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
&__item {
|
||||
padding: 2px 10px 5px 10px;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
@import "categories";
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
.category {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
padding: 5px;
|
||||
|
||||
border-radius: 5px;
|
||||
|
||||
&__info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
width: 100%;
|
||||
padding: 0 0 0 20px;
|
||||
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
|
||||
& h4 {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
&__menu {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: darken($gainsboro, 10%);
|
||||
}
|
||||
|
||||
&--selected {
|
||||
color: $white;
|
||||
background-color: darken($gainsboro, 10%);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
@import "category";
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
margin: 2% 0 0 0;
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
@import "content";
|
||||
17
src/newsreader/scss/homepage/components/index.scss
Normal file
17
src/newsreader/scss/homepage/components/index.scss
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
@import "content/index";
|
||||
@import "main/index";
|
||||
|
||||
@import "sidebar/index";
|
||||
@import "categories/index";
|
||||
@import "category/index";
|
||||
|
||||
@import "rules/index";
|
||||
@import "rule/index";
|
||||
|
||||
@import "post-block/index";
|
||||
@import "posts-section/index";
|
||||
@import "posts/index";
|
||||
@import "posts-header/index";
|
||||
@import "post/index";
|
||||
@import "post-message/index";
|
||||
@import "read-button/index";
|
||||
12
src/newsreader/scss/homepage/components/main/_main.scss
Normal file
12
src/newsreader/scss/homepage/components/main/_main.scss
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
.main {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
|
||||
margin: 0;
|
||||
background-color: initial;
|
||||
|
||||
&--centered {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
1
src/newsreader/scss/homepage/components/main/index.scss
Normal file
1
src/newsreader/scss/homepage/components/main/index.scss
Normal file
|
|
@ -0,0 +1 @@
|
|||
@import "main";
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
.post-block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
width: 60%;
|
||||
margin: 0 0 2% 0;
|
||||
|
||||
font-family: $article-font;
|
||||
|
||||
border-radius: 2px;
|
||||
background-color: $white;
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
@import "post-block";
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue