Compare commits

...

476 commits

Author SHA1 Message Date
e40d69d5ff Use correct settings module for development
All checks were successful
ci/woodpecker/push/build Pipeline was successful
ci/woodpecker/push/lint Pipeline was successful
ci/woodpecker/push/tests Pipeline was successful
2025-05-11 09:44:55 +02:00
83707701e9 Fix template formatting issues
All checks were successful
ci/woodpecker/push/build Pipeline was successful
ci/woodpecker/push/lint Pipeline was successful
ci/woodpecker/push/tests Pipeline was successful
2025-05-05 16:49:34 +02:00
116e2c1577 Fix cache permissions
All checks were successful
ci/woodpecker/push/build Pipeline was successful
ci/woodpecker/push/lint Pipeline was successful
ci/woodpecker/push/tests Pipeline was successful
see https://github.com/moby/buildkit/blob/master/frontend/dockerfile/docs/reference.md#run---mounttypecache
2025-05-05 16:22:07 +02:00
cf96371b90 Fix formatting errors warnings 2025-05-05 15:42:12 +02:00
eadd7a5612 Add missing command invocation
Some checks failed
ci/woodpecker/push/build Pipeline was successful
ci/woodpecker/push/lint Pipeline failed
ci/woodpecker/push/tests Pipeline was successful
2025-05-05 15:34:37 +02:00
62053a1048 Use uv image build with same python version
Some checks failed
ci/woodpecker/push/build Pipeline was successful
ci/woodpecker/push/lint Pipeline failed
ci/woodpecker/push/tests Pipeline failed
2025-05-05 15:32:51 +02:00
b4340176da Use correct project name
Some checks failed
ci/woodpecker/push/build Pipeline was successful
ci/woodpecker/push/lint Pipeline failed
ci/woodpecker/push/tests Pipeline was successful
2025-05-05 15:16:48 +02:00
433ff9413d Specify javascript build target 2025-05-05 15:14:54 +02:00
91949622b7 Update woodpecker configuration
Some checks failed
ci/woodpecker/push/build Pipeline failed
ci/woodpecker/push/lint Pipeline failed
ci/woodpecker/push/tests Pipeline was successful
2025-05-05 15:11:55 +02:00
10affeb32f Docker compose refactor
Added shell interpolation for environment variables
2025-05-05 15:02:03 +02:00
e96c6f3528 Use psycopg-binary package
To prevent building the package from source
2025-05-05 14:40:40 +02:00
a534a3b691 Move jest configuration
Some checks failed
ci/woodpecker/push/build Pipeline was successful
ci/woodpecker/push/lint Pipeline failed
ci/woodpecker/push/tests Pipeline failed
2025-05-04 19:52:24 +02:00
ebbbe99eaf Update package.json 2025-05-04 19:44:55 +02:00
c7f90e233e Move prettier configuration 2025-05-04 19:44:00 +02:00
9ba6824dd3 Remove unused isort configuration 2025-05-04 19:38:45 +02:00
4c5d3aec28 Move coverage configuration to pyproject.toml 2025-05-04 19:38:26 +02:00
dd9aaf467e Add editorconfig configuration 2025-05-04 19:34:25 +02:00
1417c52007 Apply prettier formatting
All checks were successful
ci/woodpecker/push/lint Pipeline was successful
ci/woodpecker/push/build Pipeline was successful
ci/woodpecker/push/tests Pipeline was successful
2025-03-28 21:55:35 +01:00
bfd081337b Run formatting / fix lint errors
Some checks failed
ci/woodpecker/push/build Pipeline was successful
ci/woodpecker/push/lint Pipeline failed
ci/woodpecker/push/tests Pipeline was successful
2025-03-28 21:41:47 +01:00
b8559f0499 Remove reddit code
Some checks failed
ci/woodpecker/push/build Pipeline was successful
ci/woodpecker/push/lint Pipeline failed
ci/woodpecker/push/tests Pipeline was successful
2025-03-27 22:02:12 +01:00
b465d0bb8d Remove leftover function binding usages
Some checks failed
ci/woodpecker/push/build Pipeline was successful
ci/woodpecker/push/lint Pipeline failed
ci/woodpecker/push/tests Pipeline failed
2025-03-27 21:44:21 +01:00
1a54fdbcd1 Remove function binding usage
Some checks failed
ci/woodpecker/push/build Pipeline failed
ci/woodpecker/push/lint Pipeline failed
ci/woodpecker/push/tests Pipeline failed
2025-03-24 09:17:30 +01:00
34afcc02b6 Remove requests oathlib
All checks were successful
ci/woodpecker/push/build Pipeline was successful
ci/woodpecker/push/lint Pipeline was successful
ci/woodpecker/push/tests Pipeline was successful
2025-03-23 21:16:36 +01:00
1574661c57 Fix ruff errors
All checks were successful
ci/woodpecker/push/build Pipeline was successful
ci/woodpecker/push/lint Pipeline was successful
ci/woodpecker/push/tests Pipeline was successful
2025-03-23 21:05:01 +01:00
3160becb72 Remove django-registration-redux
Some checks failed
ci/woodpecker/push/build Pipeline was successful
ci/woodpecker/push/lint Pipeline failed
ci/woodpecker/push/tests Pipeline was successful
2025-03-23 21:01:23 +01:00
105371abaf Use long command options
Some checks failed
ci/woodpecker/push/build Pipeline was successful
ci/woodpecker/push/lint Pipeline failed
ci/woodpecker/push/tests Pipeline was successful
2025-03-23 16:25:03 +01:00
ed37be0c60 Add celery healthcheck & update existing healthcheck 2025-03-23 16:24:33 +01:00
161234defd Bump rabbitmq version 2025-03-23 16:23:45 +01:00
f3ba0f1d09 Update ruff & uv usage
Some checks failed
ci/woodpecker/push/build Pipeline was successful
ci/woodpecker/push/lint Pipeline failed
ci/woodpecker/push/tests Pipeline was successful
2025-03-23 16:19:15 +01:00
aff565862c Add woodpecker CI configuration
All checks were successful
ci/woodpecker/push/build Pipeline was successful
ci/woodpecker/push/lint Pipeline was successful
ci/woodpecker/push/tests Pipeline was successful
2024-12-26 20:20:21 +01:00
bf43603d65 Update versions 2024-10-13 12:52:06 +02:00
91a7f6325c Update changelog 2024-10-13 12:49:55 +02:00
e33497569a Apply query optimizations for posts 2024-10-13 10:16:57 +02:00
2d5801f226 Update changelog 2024-10-07 21:43:52 +02:00
89d4ebdc49 Add missing VERSION environment variable 2024-10-07 21:42:20 +02:00
174912a967 Update changelog 2024-10-07 21:02:03 +02:00
bb92f07f00 Use full screen height for mobile post layout 2024-10-07 21:00:03 +02:00
fa491120a0 Use line-through to indicate read status 2024-10-07 20:57:08 +02:00
ccde406193 Update CI after branch changes 2024-10-06 21:23:42 +02:00
a498417bad Update changelog 2024-10-06 20:56:09 +02:00
16ebf3bdb3 Apply ruff formatting 2024-10-06 20:47:57 +02:00
99c232fea2 Apply javascript formatting 2024-10-06 20:46:33 +02:00
fbb6405da9 Sidebar refactor 2024-10-06 20:39:05 +02:00
03b5847641 Apply formatting 2024-09-09 20:35:44 +02:00
dfb049ae14 Django 4.2 upgrade 2024-09-07 20:50:38 +02:00
b78f03d3b0 Remove twitter integration 2024-09-06 09:17:23 +02:00
e09b3d6e4c Use root user for development docker containers 2024-09-06 08:48:18 +02:00
cc5b4cc0bb Add missing migration 2024-09-06 08:47:56 +02:00
70a0d5a96d Remove drf-yasg 2024-09-06 08:41:10 +02:00
cc8aafa310 Remove deprecated ruff optiong 2024-09-05 07:08:35 +02:00
57375591b5 Use ruff for formatting/linting 2024-09-05 06:58:35 +02:00
bb74e875e0 Fix typo 2024-08-31 10:21:25 +02:00
bc8ec0257e Update unknown request tests 2024-08-31 10:08:22 +02:00
a041d5f7fa Use uv to manage requirements 2024-08-30 21:05:55 +02:00
e95c2a440e Remove pip-tools & rerun requirements 2024-08-28 09:06:58 +02:00
5fc0742688 Fix multiline linting job 2024-08-28 08:44:41 +02:00
f5f7f99f71 Fix javascript tests 2024-08-26 09:33:00 +02:00
284f64d202 Set default babel preset targets 2024-08-26 09:19:30 +02:00
b34bef899c Fix jest setup 2024-08-25 08:56:44 +02:00
aa0a29fefb Use commonjs module for testing 2024-08-24 21:19:26 +02:00
2a5372166e Add modules: false for test transforms 2024-08-24 21:03:35 +02:00
fd3bf4f542 Update babel plugins 2024-08-24 20:53:59 +02:00
c7fb545096 Update babel config 2024-08-24 20:34:24 +02:00
c7aa431e4a Move .babelrc to babel.config.json 2024-08-24 16:38:53 +02:00
3152c8f14e Update jest setup 2024-08-24 16:15:25 +02:00
9e6be5c807 Remove trailing 's' 2024-08-24 16:00:51 +02:00
106bd6cb4c Add ignore pattern & use correct transform patter 2024-08-24 15:56:14 +02:00
040193a3ed Update jest configuration 2024-08-24 15:49:09 +02:00
d8b04b3329 Remove unknown --system pip flag 2024-08-24 15:40:06 +02:00
b6805c1675 Update CI installation steps 2024-08-24 15:36:14 +02:00
07c685401f Update webpack 2024-08-24 15:19:01 +02:00
8b080a3cee Remove loose option 2024-08-24 15:15:02 +02:00
12c1ac9d17 Remove version from docker-compose 2024-08-24 15:14:42 +02:00
67d7b10632 Fix docker images 2024-08-24 15:14:04 +02:00
1b8b9dcd41 Add .nvmrc 2024-08-24 14:39:59 +02:00
35c9e78809 Update docker images 2024-08-24 14:39:10 +02:00
4935d7d186 Use node lts for CI 2024-08-13 09:22:40 +02:00
2b3e35078d Update webpack configuration 2024-08-13 09:11:54 +02:00
d05e29b5e0 Use uv for dependency management 2024-08-13 09:07:47 +02:00
e9e8fc351c Add volume notes 2024-08-10 14:26:09 +02:00
16168cc9d9 Remove proxy_redirect directive 2023-10-01 21:59:23 +02:00
9097caf438 Use production webpack configuration 2023-09-28 20:34:08 +02:00
0f89fc2447 Update version 2023-09-28 20:30:18 +02:00
b36bf4e0bc Merge branch 'master' into development 2023-09-28 20:03:07 +02:00
40749403b9 Sort posts before storing in redux 2023-09-28 20:02:27 +02:00
15884d3b4e Update CHANGELOG.md 2023-08-13 18:19:08 +02:00
40a0b72d87 Prevent observer from observing while loading posts 2023-08-13 17:45:11 +02:00
a4f5a7bdd7 Use React's ref feature 2023-08-13 17:45:11 +02:00
fedeed15c5 Use IntersectionObserver to paginate 2023-08-13 17:45:11 +02:00
ff6dfcaa05 Set DEBUG=True for gitlab configuration 2023-08-13 17:44:30 +02:00
2790e9c82e Merge branch 'master' into development 2023-07-02 13:08:12 +02:00
f0689ebfab 0.4.2 2023-07-02 12:55:19 +02:00
41f249ed5a 0.4.1 2023-07-02 11:00:47 +02:00
8e04436b68 Update changelog 2023-07-02 10:51:45 +02:00
5b59b189d6 Add missing env vars 2023-07-02 10:50:13 +02:00
8e728200ec Merge branch 'master' into development 2023-07-02 10:34:46 +02:00
8e7b059ad3 0.4.0 2023-07-02 10:23:16 +02:00
df848b1e43 Rerun black 2023-07-02 10:13:19 +02:00
e80579af4b Update changelog 2023-07-02 10:07:47 +02:00
d479b5e5f7 Update rabbitmq 2023-07-02 09:11:12 +02:00
b06af33a19 Update celery 2023-07-02 09:06:21 +02:00
858c2c6eb3 Use docker extensions for env variables 2023-07-02 08:24:11 +02:00
72f8426f72 Downgrade django-celery-beat
As this is the last version support django 2.2
2023-07-01 20:44:08 +02:00
1aea2df2ea Update cache settings 2023-06-29 10:04:45 +02:00
492b8d33ff Add missing build target for development celery 2023-06-29 09:43:43 +02:00
cbc6a73b76 Remove duplicate setting 2023-06-29 09:35:34 +02:00
4b04178a4f Set env files explicitly 2023-06-29 09:35:24 +02:00
ba4b17a8e2 Set correct default settings module 2023-06-28 19:56:06 +02:00
70a1ae306b Set correct celery broker URL 2023-06-28 19:55:19 +02:00
b91f5c8939 Add missing env_file setting & remove redundant database settings 2023-06-28 19:42:44 +02:00
5a73707d61 Split production dependecies & update production configuration 2023-06-28 19:38:44 +02:00
0f66c5eb9b Rebuild dependencies with python 3.9 for now 2023-06-28 09:57:44 +02:00
7f4a3a3e49 Update logging configuration for gitlab 2023-06-28 08:43:05 +02:00
9258d33f4e Update gitlab configuration 2023-06-28 08:37:28 +02:00
a9741d4063 Use django docker image for CI 2023-06-27 20:50:00 +02:00
61827b955d Use debug celery logging for development 2023-06-27 20:47:08 +02:00
b03f2fc902 Update fixture 2023-06-27 20:45:15 +02:00
89d88ccceb Update logging configuration 2023-06-27 20:37:47 +02:00
2a0c0072a4 Update gitlab configuration 2023-06-27 10:29:07 +02:00
6a46dc01e2 Remove variable defaults
These are not set whenever a merged compose file is used
2023-06-27 10:11:07 +02:00
60af3ba4f6 Set development env file 2023-06-27 09:35:33 +02:00
65dae40e9a Update npm packages 2023-06-27 09:35:24 +02:00
bfacd97c73 Update gitignore 2023-06-27 09:28:15 +02:00
3ebba6df47 Use more enviroment variables 2023-06-27 09:06:24 +02:00
b8a9d885f5 Use older python directories for now 2023-06-27 08:35:28 +02:00
fd5f910ac0 Remove redundant override 2023-06-27 08:35:07 +02:00
6ac4e5d5c2 Downgrade docker images for now 2023-06-26 21:25:41 +02:00
ef0c070755 Update docker django image 2023-06-26 20:27:58 +02:00
59f719d7c3 Update gitlab configuration 2023-06-26 20:25:36 +02:00
720f6fdb78 Use Makefile to generate requirements 2023-06-26 20:21:00 +02:00
82a7176629 Increase healthcheck interval 2023-03-05 15:32:15 +01:00
89f23fe668 Initial refactor 2023-03-05 15:21:04 +01:00
12b4aa0b91 Remove unused imports 2022-05-26 12:58:33 +02:00
c48de9c6e1 Rerun isort 2022-05-26 12:00:20 +02:00
e5220eb9a5 Remove deprecated option isort option 2022-05-26 11:59:31 +02:00
1f0a8a54da Use quiet option for CI jobs 2022-05-26 11:57:10 +02:00
bea7afb355 Use python 3.9 to build dependencies 2022-05-26 11:54:21 +02:00
bd48634509 Use coverage run command 2022-05-26 11:44:54 +02:00
d3f9a11f44 Replace node-sass with dart sass 2022-05-26 11:39:59 +02:00
9d05cac15c Update gitlab jobs 2022-05-26 11:15:46 +02:00
20309e70fa Use pip-tools to manage dependencies 2022-05-26 11:12:22 +02:00
53aa8da2dd Update twitter error handling 2021-09-25 10:39:43 +02:00
f5b708aafe Change loading posts from 80% to 100% 2021-07-24 20:18:42 +02:00
85e152f6e8 Update dependencies 2021-07-21 21:14:24 +02:00
da05b3ac2e Add scroll to top/bottom component 2021-07-21 21:13:22 +02:00
e6cfef8d96 Update dependencies 2021-07-08 20:26:14 +02:00
04d95386f5 Update theme js 2021-06-30 21:30:57 +02:00
879c6ebc90 Merge branch 'master' into development 2021-06-30 21:12:46 +02:00
6b2c4996d5 0.3.13.8 2021-06-30 21:12:30 +02:00
7d36763eff Bump versions 2021-06-30 21:11:48 +02:00
ba5001fafa Update dependencies 2021-06-30 21:04:15 +02:00
106a087291 Fix forms not saving 2021-06-30 21:03:17 +02:00
426f857f05 Merge branch 'master' into development 2021-04-27 12:42:15 +02:00
510f7187a8 0.3.13.7 2021-04-27 12:42:02 +02:00
ea541bfe64 Bump version 2021-04-27 12:36:39 +02:00
83829b7d19 Check for Twitter error codes
Important for expired tokens as 401's are returned for various reasons
2021-04-27 12:33:12 +02:00
b67724220a Merge branch 'master' into development 2021-04-25 20:26:59 +02:00
8498303006 0.3.13.6 2021-04-25 20:26:43 +02:00
8f37eec519 Bump version 2021-04-25 20:26:21 +02:00
48d48885c4 Check for response attribute 2021-04-25 20:21:03 +02:00
9e572534aa Use server side PII rules 2021-04-25 20:17:36 +02:00
392900956c Revert "Fix commit hash not displaying in sentry release"
This reverts commit f326a4c923.
2021-04-25 12:53:55 +02:00
b106ebf827 Load dotenv on all environments 2021-04-25 12:53:43 +02:00
f326a4c923 Fix commit hash not displaying in sentry release 2021-04-25 12:49:20 +02:00
fee2a4f17b Use sentry in all environments
It still is optional though
2021-04-25 12:15:12 +02:00
db5780f9f1 Merge branch 'master' into development 2021-04-24 21:13:53 +02:00
d89e1bc6d4 0.3.13.5 2021-04-24 21:13:39 +02:00
9c5378cf67 Bump version 2021-04-24 21:08:45 +02:00
02cbaeb491 Set response keyword 2021-04-24 20:51:01 +02:00
9b1408160d Merge branch 'master' into development 2021-04-24 20:09:20 +02:00
8af8dab6db 0.3.13.4 2021-04-24 20:09:06 +02:00
782671542f Bump version number 2021-04-24 20:02:22 +02:00
e42653f7fd Fix import error 2021-04-24 19:56:28 +02:00
223656f2d2 Merge branch 'master' into development 2021-04-24 15:10:53 +02:00
e008d2f53c 0.3.13.3 2021-04-24 15:06:12 +02:00
501022db3b Bump version numbers 2021-04-24 15:05:50 +02:00
3c4e659bc8 Use sentry's set_extra for debug purposes 2021-04-24 15:04:55 +02:00
104a5575fa Merge branch 'master' into development 2021-04-24 14:24:09 +02:00
0241de95cd 0.3.13.2 2021-04-24 14:21:06 +02:00
d1badbef30 Bump versions 2021-04-24 14:19:51 +02:00
ecb99425e0 Update sentry-sdk 2021-04-24 14:18:33 +02:00
2afeb3c102 Merge branch 'master' into development 2021-04-24 13:37:42 +02:00
1a1bfdfbab 0.3.13.1 2021-04-24 13:29:34 +02:00
1e8a3aedb1 Bump version 2021-04-24 13:27:49 +02:00
5603b23468 Temporarly set exception level for Twitter denied exceptions 2021-04-24 13:24:16 +02:00
0ba632a2a6 Prevent mutual exclusive exception 2021-04-24 13:11:43 +02:00
813222073e Merge branch 'master' into development 2021-04-23 23:05:10 +02:00
712f601e9c 0.3.13 2021-04-23 23:01:31 +02:00
2ed828a243 Update versioning numbers 2021-04-23 23:00:09 +02:00
679dc2a0d6 Update django version 2021-04-23 22:42:18 +02:00
f02a7b6eb7 Send re-authentication notification emails 2021-04-23 22:25:35 +02:00
101058672b Merge branch 'master' into development 2021-03-06 18:44:06 +01:00
73bac1301b 0.3.12.1 2021-03-06 18:43:48 +01:00
3aa3c29613 Bump version 2021-03-06 18:41:41 +01:00
146401117b Add missing background-color 2021-03-06 18:40:27 +01:00
1909916b64 Merge branch 'master' into development 2021-03-06 18:21:47 +01:00
6d09629c8e 0.3.12 2021-03-06 18:21:29 +01:00
e99b6653d8 Bump version 2021-03-06 17:43:16 +01:00
85e02a8147 Add sticky post header 2021-03-06 17:40:36 +01:00
6db7d6c3f5 Update light theme & add sticky navbar 2021-03-06 17:20:22 +01:00
4d0613df2e Merge branch 'master' into development 2021-02-27 16:00:07 +01:00
18dbf2d312 0.3.11 2021-02-27 15:59:50 +01:00
0e108c8110 Bump version numbers 2021-02-27 15:54:34 +01:00
c53e9756dd Update django version 2021-02-27 15:51:44 +01:00
ab1e4c44ec Add saved posts section 2021-02-27 15:50:11 +01:00
8c69e4a27e Merge branch 'master' into development 2021-02-19 09:28:32 +01:00
66edc1e8dd 0.3.10 2021-02-19 09:28:04 +01:00
0eefafe3db Bump version number 2021-02-19 09:23:47 +01:00
9c88cfde59 Add specific color for confirm buttons 2021-02-19 09:22:01 +01:00
a24d06b257 Update font sizes 2021-02-19 09:13:59 +01:00
fef4729e0b Update dependencies 2021-02-18 22:25:54 +01:00
8b7850b17b Merge branch 'master' into development 2021-02-18 22:15:24 +01:00
c61ce0bcb7 0.3.9 2021-02-18 22:14:47 +01:00
091bcdbef3 Bump version numbers 2021-02-18 22:09:02 +01:00
90cb3ad1d4 Bump django version 2021-02-18 22:05:37 +01:00
dfa43fa8a2 Cursor based pagination
Fixes #70
2021-02-18 22:04:15 +01:00
91d1757bde Merge branch 'master' into development 2021-02-07 16:40:03 +01:00
00164bd3b5 0.3.8 2021-02-07 13:39:39 +01:00
f0a1179d23 Update changelog 2021-02-07 13:27:47 +01:00
ee5f59fd7c Update deploy job 2021-02-07 13:26:36 +01:00
039e8b803d Styling changes
- Replace css.gg with fontawesome
- Update more colors (for light & dark themes)
2021-02-07 13:24:44 +01:00
439a54c0ce Merge branch 'master' into development 2021-01-23 17:15:06 +01:00
cf078ee42a 0.3.7 2021-01-23 17:10:43 +01:00
9e25f14c73 Update changelog 2021-01-23 17:10:10 +01:00
282d64a923 Add dark theme
Fixes #69
2021-01-23 17:02:15 +01:00
9095f35545 Update object representations
Fixes #71
2021-01-17 21:44:54 +01:00
4496972205 Move sentry dependency to optional dependencies
Fixes #72
2021-01-17 21:26:20 +01:00
d2a1fd7f3a Use changelog for listing changes 2020-12-20 21:13:43 +01:00
e251f633e1 Specify tag when deploying 2020-12-20 16:44:35 +01:00
0ac8842431 Remove change of working dir
See https://git.fudiggity.nl/sonny/newsreader/-/jobs/4124 for correct deploy flow
2020-12-20 12:58:03 +01:00
55eee6c6ed Merge branch 'master' into development 2020-12-20 12:56:24 +01:00
01f86399b2 0.3.6.3
- Update deploy job
2020-12-20 12:48:12 +01:00
32e85ec05e 0.3.6.2
- Use warning logging level for BuilderSkippedException's
- Change working directory before running ansible
2020-12-20 12:40:07 +01:00
4cb3846e36 Change directory before running ansible 2020-12-20 12:39:21 +01:00
06e4ea33b5 Don't use error logging level for BuilderSkippedExceptions 2020-12-20 12:13:46 +01:00
ceaee1165b Merge branch 'master' into development 2020-12-19 22:14:11 +01:00
db25e240e3 0.3.6.1
- Install ansible required roles
2020-12-19 21:52:25 +01:00
73401b6ca3 Install ansible required roles 2020-12-19 21:52:01 +01:00
517a89d2da Merge branch 'master' into development 2020-12-19 21:04:38 +01:00
04af0f9c5d 0.3.6
- Update deploy job
- Add user manageable reddit filters
2020-12-19 21:03:29 +01:00
57dcabd685 Remove TODO's 2020-12-19 20:57:07 +01:00
f98220f8cc Add user manageable reddit filters 2020-12-19 20:47:18 +01:00
73e823bb05 Update deploy job
The seperated newsreader repo will be used from now on
2020-11-24 12:22:04 +01:00
06bc705c00 Use max-content for post items 2020-11-01 14:02:58 +01:00
4cdb16b2c1 Merge branch 'master' into development 2020-10-31 14:32:53 +01:00
116b6d1308 0.3.5
- Show timezone next to post datetimes
- Take read status in consideration when sorting posts
2020-10-31 14:31:27 +01:00
c1d11ae94e Take read status in consideration when sorting 2020-10-31 14:26:27 +01:00
29f20cca24 Make auto marking optional through a setting
Fixes #68
2020-10-30 23:01:22 +01:00
ffefc76acc Show correct timezone
Timezone is for now converted to django's TIME_ZONE setting for all users
2020-10-27 22:38:49 +01:00
9ad6a1a7b8 Revert "Show timezone in posts"
This reverts commit 6ae3b5c508.

Datetimes will be converted to the timezone set by the TIME_ZONE django setting
2020-10-27 20:19:58 +01:00
6ae3b5c508 Show timezone in posts
Fixes #67
2020-10-26 22:18:23 +01:00
0a1bf0d5e6 Import from base in docker settings 2020-10-17 22:14:45 +02:00
aaef828837 Merge branch 'master' into development 2020-10-17 17:31:03 +02:00
ee9b36d8ae 0.3.4
- Use warnings for BuilderDuplicateExceptions
- Show user known urls in rules list view
2020-10-17 17:29:42 +02:00
00e0705d12 Show user known urls in rules list view 2020-10-17 17:23:51 +02:00
ccc9726c8a Log warnings for duplicates 2020-10-17 17:13:30 +02:00
ec4f1c9300 Merge branch 'master' into development 2020-10-17 13:21:23 +02:00
9e5e05c056 0.3.3
- Update static configuration
- Builder refactor
- Fix for images stretching to far
2020-10-17 13:19:49 +02:00
51ffd82648 Only clear known static nested folders on startup 2020-10-17 12:01:30 +02:00
ab7a4d9a8a Dont remove fonts after rebuilding 2020-10-15 22:40:45 +02:00
003889d29e Fix images stretching full width 2020-10-15 19:47:02 +02:00
195597afa0 Split rules view buttons into seperate sections 2020-10-15 19:35:48 +02:00
4b9de97d70 Rename feeds -> sources 2020-10-15 19:32:19 +02:00
763d8ee093 Refactor builders to use custom exceptions 2020-10-15 19:30:53 +02:00
b0c6714002 Merge branch 'master' into development 2020-10-06 22:52:43 +02:00
b6921a20e7 0.3.2
- Add user runnable favicon task
- Update messages styling
2020-10-06 22:51:23 +02:00
1c3a33c1d8 Fix failing test 2020-10-06 22:46:33 +02:00
593b06006c Fix broken view 2020-10-06 22:37:14 +02:00
f12639987f Update messages styling 2020-10-06 22:34:24 +02:00
48388a47f6 Add user runnable favicon task 2020-10-06 22:06:19 +02:00
77103eb680 Merge branch 'master' into development 2020-09-27 16:45:49 +02:00
ca5c2f6b55 0.3.1
Use ansible repo's master branch for deployments
2020-09-27 16:45:28 +02:00
d228dc5f45 Merge branch 'master' into development 2020-09-27 16:25:41 +02:00
d4a41a62da 0.3.0
- Add Twitter integration
- Refactor alot of existing code in collection app
- Update webpack font configuration
2020-09-27 16:19:32 +02:00
576ab9a917 Fix isort errors 2020-09-27 16:13:59 +02:00
40a027587b Add Twitter integration
Fixes #46
2020-09-27 16:08:30 +02:00
a7b4271a7d Update font configuration
Fixes #63, See https://webpack.js.org/loaders/file-loader/#publicpath
2020-09-09 20:30:59 +02:00
6120b26a44 Update logging configuration 2020-09-09 19:58:09 +02:00
4074df3f09 Merge branch 'master' into development 2020-09-02 22:10:56 +02:00
805321f66d 0.2.6.4
Update deploy job
2020-09-02 21:37:33 +02:00
84802fd48b Merge branch 'master' into development 2020-09-02 21:10:53 +02:00
65e4f3bb80 0.2.6.3
- Fallback to variable for vault password as file variables get execute permission set
2020-09-01 22:27:31 +02:00
f0df342f61 0.2.6.2
- Update deploy job to use file variables
- Fix truncating values with exotic characters
2020-09-01 22:07:49 +02:00
47eaef40b3 Update deploy job to use file variables 2020-09-01 22:02:47 +02:00
0d9163d363 Fix truncating exotic values
Fixes #65
2020-09-01 19:48:04 +02:00
64a3d2aab5 Merge branch 'master' into development 2020-08-31 23:09:01 +02:00
b035526848 Fix npm vulnerabilities & update deploy job 2020-08-31 23:08:37 +02:00
af7fbaf1e8 Merge branch 'master' into development 2020-08-31 22:49:14 +02:00
30bd140483 0.2.6
- Fix sorting posts by rule
2020-08-31 22:42:18 +02:00
7ee5ad7879 Merge branch 'post-sorting' into 'development'
Fix post sorting by rule

See merge request sonny/newsreader!36
2020-08-31 22:38:59 +02:00
1429e5a7ec Fix post sorting by rule 2020-08-31 22:38:59 +02:00
e0af3dcc20 Merge branch 'master' into development 2020-08-12 20:40:46 +02:00
d14aff1baa Update deploy job 2020-08-12 19:48:31 +02:00
03ac016dd3 Fix FeedTask collecting reddit rules 2020-08-12 19:38:16 +02:00
e58c5a4559 Merge branch 'master' into development 2020-08-12 09:54:56 +02:00
ad51d17d2d Show feed URL's when catching feed client exceptions 2020-08-12 09:31:50 +02:00
bd9573cebc Show current version number in user agent 2020-08-05 21:39:13 +02:00
285da805cb Merge branch 'master' into development 2020-08-05 21:34:34 +02:00
78bc696294 Fix white text in transparent error messages 2020-08-04 20:34:09 +02:00
286971649a Add version number to django settings
Fixes #54
2020-08-04 20:20:28 +02:00
4bca6a432f Fix invalid release job 2020-07-30 23:14:15 +02:00
7adb1cddb8 Add release job & update deploy job 2020-07-30 23:09:46 +02:00
cba167c98c Merge branch 'master' into development 2020-07-29 22:50:14 +02:00
7af681887f Update CI jobs 2020-07-29 22:43:42 +02:00
6ff996b674 Merge branch 'master' into development 2020-07-27 22:33:01 +02:00
7d803bbfa0 Show rule errors
Fixes #56
2020-07-27 20:56:01 +02:00
5a7222d9a4 Merge branch 'master' into development 2020-07-26 22:10:01 +02:00
a1c11baa3d Update table row error styling 2020-07-26 21:23:19 +02:00
fd16478909 Update rule table styling 2020-07-26 19:41:17 +02:00
6a13c58712 Show icons instead of boolean values in rules table
Fixes #58
2020-07-26 19:02:51 +02:00
629b35b69b Update post modal list styling
Fixes #60
2020-07-26 18:39:49 +02:00
c04f23b3ee Dont't overwrite default checkbox
This caused issues in the admin
2020-07-26 18:33:52 +02:00
2b10612304 Add branding to admin 2020-07-26 12:32:22 +02:00
c52c30fb08 Update default fixture 2020-07-26 12:03:30 +02:00
632b3b14f1 Extend UserAdmin
To allow changing password
2020-07-26 11:57:48 +02:00
ac9e6a7224 Remove rounded component styling 2020-07-25 22:11:41 +02:00
5ec7239a1b Merge branch 'master' into development 2020-07-25 16:55:50 +02:00
2d640eac04 Merge branch 'master' into development 2020-07-25 16:54:18 +02:00
4df50b3a16 Fix multiline yaml statement 2020-07-25 16:53:13 +02:00
2e56d3208b Update deploy job 2020-07-25 16:35:28 +02:00
96f016260d Merge branch 'master' into development 2020-07-22 23:51:10 +02:00
e73edd5083 Adjust reddit token expiry loglevel
Fixes #51
2020-07-22 23:39:37 +02:00
a3a812b949 Merge branch 'fix-reddit-form-urls' into 'development'
Fix reddit form urls

Closes #53

See merge request sonny/newsreader!29
2020-07-22 23:29:23 +02:00
9a212c6288 Fix reddit form urls 2020-07-22 23:29:23 +02:00
0f5e9e7fca Fix post modal rules not linking properly 2020-07-22 22:56:00 +02:00
296202b69f Merge branch 'fix-reddit-post-urls' into 'development'
Fix reddit post urls

Closes #52

See merge request sonny/newsreader!28
2020-07-22 22:50:17 +02:00
863e8671da Fix reddit post urls 2020-07-22 22:50:17 +02:00
21dff8eb15 Show reddit images/videos or a direct url whenever possbile 2020-07-21 23:04:16 +02:00
b5b174a213 Merge branch 'master' into development 2020-07-13 23:48:00 +02:00
ed8584603e Deduplicate reddit posts 2020-07-13 23:14:43 +02:00
24b1704da6 Update subreddit helptext 2020-07-12 22:33:40 +02:00
5ce5c5cfe1 Set reddit callback url through env var 2020-07-12 22:26:49 +02:00
046117cfb3 Merge branch 'master' into development 2020-07-12 21:09:49 +02:00
7cef924c37 Fix reddit redirect url 2020-07-12 20:52:56 +02:00
e57a4c656d Merge branch 'master' into development 2020-07-12 20:50:28 +02:00
d4b58624d6 Fix favicon not showing in admin 2020-07-12 20:22:23 +02:00
ec72827bd8 Merge branch '25-sub-reddit-integration' into 'development'
Resolve "(Sub)Reddit integration"

Closes #25

See merge request sonny/newsreader!13
2020-07-12 20:10:57 +02:00
6ce013d0d4 Add reddit integration 2020-07-12 20:10:57 +02:00
6f30571dd1 Merge branch 'master' into development 2020-06-30 22:58:52 +02:00
03d673e6c0 Fixed task passing through disabled rules 2020-06-30 20:27:17 +02:00
78608b2b57 Merge branch 'master' into development 2020-06-29 21:28:43 +02:00
fcf49fa123 Update logging calls 2020-06-29 20:54:46 +02:00
73ddb785e0 Update logging 2020-06-29 20:42:58 +02:00
3c080e1300 Merge branch 'master' into development 2020-06-28 23:37:35 +02:00
6661b69094 Optionally load sentry 2020-06-28 20:35:58 +02:00
eb09c4f729 Merge branch 'master' into development 2020-06-18 20:31:56 +02:00
017dd9a582 Make poetry/pip less verbose 2020-06-18 20:23:47 +02:00
bcd051e992 Update logging 2020-06-18 20:18:05 +02:00
a46b6fade5 Merge branch 'update-duplicate-handler' into 'development'
Update duplicate handler

See merge request sonny/newsreader!24
2020-06-18 20:07:37 +02:00
0bd47f1bb0 Update duplicate handler 2020-06-18 20:07:37 +02:00
68bba5f835 Merge branch 'master' into development 2020-06-16 21:22:31 +02:00
722afe8c12 Fix sidebar category overflow 2020-06-16 20:56:38 +02:00
083f728404 Use celery configuration similar to production 2020-06-16 20:48:21 +02:00
c8771cb272 Update post admin 2020-06-16 09:24:52 +02:00
d3ccc16f81 Merge branch 'master' into development 2020-06-16 09:07:16 +02:00
4dfa755881 Use django.forms.renderers.TemplatesSetting
As this doesn't ignore the DIRS setting in TEMPLATES
2020-06-15 22:56:06 +02:00
18c0046e61 Merge branch 'master' into development 2020-06-07 21:37:47 +02:00
3b2384a266 Update django version 2020-06-07 21:29:21 +02:00
2d3eae6e39 Remove django entrypoint script 2020-06-07 10:29:51 +02:00
cbc6d646ce Merge branch 'master' into development 2020-06-03 22:14:50 +02:00
ddfd208d1c Merge frontend redesign 2020-06-03 20:46:01 +02:00
27ed0259a1 Merge branch 'master' into development 2020-05-24 13:35:14 +02:00
a22ef354be Update duplicate checker
- deduplicate collected entries
- set default fallback publication_date
2020-05-24 13:28:05 +02:00
61ddef3b63 Merge branch 'master' into development 2020-05-23 17:02:57 +02:00
e3840342b3 Update duplicate handler & publication date saving 2020-05-23 16:53:05 +02:00
a4b5373ed2 Add form / card components & refactor forms 2020-05-23 13:10:46 +02:00
13d33749da Merge branch '20-settings-page' into 'development'
Resolve "Settings page"

Closes #20

See merge request sonny/newsreader!18
2020-05-11 22:44:19 +02:00
bb47e2af8d Resolve "Settings page" 2020-05-11 22:44:19 +02:00
f4adb9635a Skip disabled rules 2020-05-10 23:04:57 +02:00
aeb85bd2cf Fix old url reference 2020-05-10 22:50:13 +02:00
69eaedf89c Rerun autoflake 2020-05-10 22:43:37 +02:00
3c613b59e2 Merge branch '50-user-admin' into 'development'
Resolve "User admin"

Closes #50

See merge request sonny/newsreader!17
2020-05-10 22:36:03 +02:00
ed415f2b5c Resolve "User admin" 2020-05-10 22:36:03 +02:00
d6d19fa9b9 Fix isort warning 2020-05-10 21:58:44 +02:00
7ee727e96e Move nested collection & core app urls inside news app 2020-05-10 21:52:54 +02:00
9285672273 Merge branch '48-feeds-list-view' into 'development'
Resolve "Feeds list view"

Closes #48

See merge request sonny/newsreader!16
2020-05-10 20:11:12 +02:00
bec3488e63 Resolve "Feeds list view" 2020-05-10 20:11:12 +02:00
7fc899937d Refactor much needed docker setup
- Build static files inside seperate container
- Remove unnecessary env variables
2020-05-02 20:21:22 +02:00
d61b3a9498 Merge branch 'master' into development 2020-04-26 21:29:15 +02:00
57e9073f6b Allow admins to be specified through env variables 2020-04-26 21:17:55 +02:00
62da6e0d8e Fix wrong axes handler 2020-04-26 21:12:02 +02:00
b811d1945b Update logging 2020-04-26 21:06:29 +02:00
3152254f43 Merge branch 'master' into development 2020-04-22 23:16:05 +02:00
e9f05868c1 Update data migration 2020-04-22 23:11:24 +02:00
708076b2ab Change collection task to class based task & update behavior 2020-04-22 23:04:53 +02:00
992df528f6 Merge branch 'master' into development 2020-04-19 21:26:22 +02:00
6ff26c71a0 Update navbar styling 2020-04-19 21:13:38 +02:00
9b56cf53e3 Merge branch '44-error-pages' into 'development'
Resolve "Error pages"

Closes #44

See merge request sonny/newsreader!14
2020-04-19 20:59:14 +02:00
6f00da37b9 Resolve "Error pages" 2020-04-19 20:59:14 +02:00
cac71d5475 Merge branch 'master' into development 2020-04-15 22:08:17 +02:00
7d86cea6ec Update project / gitlab ci settings 2020-04-15 21:59:34 +02:00
e495d7c188 Use poetry for dependency management 2020-04-13 17:06:31 +02:00
cda2654573 Merge branch '41-javascript-error-handling' into 'development'
Resolve "Javascript error handling"

Closes #41

See merge request sonny/newsreader!12
2020-04-11 09:49:08 +02:00
ae942f5ef9 Fix category action test
This was the same test as before -.-
2020-04-11 09:49:07 +02:00
73ae0271e4 Update npm commands 2020-03-25 22:38:56 +01:00
66408cc218 Merge branch '42-replace-gulp-with-webpack' into 'development'
Resolve "Replace Gulp with Webpack"

Closes #42

See merge request sonny/newsreader!11
2020-03-25 22:24:32 +01:00
b28d42b97b Resolve "Replace Gulp with Webpack" 2020-03-25 22:24:32 +01:00
16230a11f3 Merge branch '39-celery-task-deduplication' into 'development'
Resolve "Celery task deduplication"

Closes #39

See merge request sonny/newsreader!10
2020-03-24 19:40:33 +01:00
a3a2033e37 Resolve "Celery task deduplication" 2020-03-24 19:40:33 +01:00
7c888c1461 Merge branch 'new-icons' into 'development'
Icon refactor

See merge request sonny/newsreader!8
2020-03-22 22:26:14 +01:00
ab5d9ea46d Icon refactor 2020-03-22 22:26:14 +01:00
dc2fb681d0 Merge branch '40-set-default_from_email' into 'development'
Resolve "Set DEFAULT_FROM_EMAIL"

Closes #40

See merge request sonny/newsreader!9
2020-03-22 20:55:11 +01:00
fdb90525a8 Set default_from_email 2020-03-22 20:50:51 +01:00
6bfb84dab9 Remove manual tag & multiline statement 2020-03-22 20:27:15 +01:00
b5b59c5baf Remove deploy_hosts 2020-03-22 20:18:28 +01:00
770ace2f5d Set correct file permissions 2020-03-22 19:24:26 +01:00
f6100416c3 Add (known) ssh host key 2020-03-22 19:17:54 +01:00
99ba773b91 Another attempt 2020-03-22 19:03:37 +01:00
fdec1efcf0 Attempt 2 2020-03-22 18:50:40 +01:00
9ecca7a80b Install git in deploy stage 2020-03-22 18:41:00 +01:00
e4e4e97cfd Add a deployment stage 2020-03-22 18:34:05 +01:00
420481f18a Squashed commit of the following:
commit f1db9b9dc1026760a43028e548572db4e639976e
Author: Sonny <sonnyba871@gmail.com>
Date:   Mon Mar 16 20:47:15 2020 +0100

    Add port setting
2020-03-20 21:58:22 +01:00
4da301eb3e Add production settings 2020-03-15 21:10:10 +01:00
4345a006a6 Merge branch 'fix-data-error' into 'development'
Fix data errors

See merge request sonny/newsreader!7
2020-03-05 00:02:40 +01:00
533561ba1e Fix data errors 2020-03-05 00:02:40 +01:00
afc3c11775 Set celery logging level 2020-03-02 23:34:57 +01:00
8b47fd8216 Merge branch '28-swagger-response-examples' into 'development'
Resolve "Swagger response examples"

Closes #28

See merge request sonny/newsreader!6
2020-03-02 20:14:38 +01:00
a87d4f387f Replace rest_framework_swagger with drf_yasg
rest_framework is deprecated see https://github.com/marcgibbons/django-rest-swagger#django-rest-swagger-deprecated-2019-06-04
2020-03-02 20:14:38 +01:00
acd9bd30cb Update isort 2020-03-01 22:35:48 +01:00
c22fdfe4ce More formatting ugh 2020-03-01 22:31:03 +01:00
21b9e4f0fd Set black to default line length (88) 2020-03-01 22:25:47 +01:00
a5de001f35 Apply hooks 2020-03-01 22:17:09 +01:00
b3bff398d9 Update db settings 2020-03-01 22:07:40 +01:00
Sonny
61bc7e9b04 Add cache setting to dev settings 2020-03-01 22:07:25 +01:00
961535dd60 Add axes integration & add cache configuration 2020-02-27 19:47:45 +01:00
ab0b24b3d2 Remove type annotations 2020-02-23 10:39:55 +01:00
3045702a1e Merge static refactor 2020-02-22 22:44:09 +01:00
5976870d38 Squashed commit of the following:
commit 99fd94580f95dcbfb77b73e2de846f76a5709ef9
Author: Sonny <sonnyba871@gmail.com>
Date:   Sat Feb 15 21:45:16 2020 +0100

    Use postgres password

    As of https://gitlab.com/gitlab-com/support-forum/issues/5199
2020-02-16 12:21:25 +01:00
ee22e0a0ae Merge branch '14-opml-import-export' into 'development'
Resolve "OPML import/export"

Closes #14

See merge request sonny/newsreader!5
2020-02-08 12:26:29 +01:00
61e45ed0cc [#14] opml import export 2020-02-08 12:18:45 +01:00
6c6ea6c481 Merge branch '23-whitelist-more-html-elements' into 'development'
Resolve "Whitelist more HTML elements"

Closes #23

See merge request sonny/newsreader!4
2020-02-03 20:42:32 +01:00
c13f968234 Resolve "Whitelist more HTML elements" 2020-02-03 20:42:31 +01:00
36076dbc40 Some style changes 2020-02-02 15:43:37 +01:00
f9bce3507c Redux actions refactor 2020-02-01 21:42:29 +01:00
e1e6571bb0 Update post serializer fields 2020-01-31 21:29:11 +01:00
c3b087e004 Squashed commit of the following:
commit f6174179405cbf696415b17bbfcb157b6c3415cf
Author: sonny <sonnyba871@gmail.com>
Date:   Thu Jan 2 23:26:49 2020 +0100

    redux tests
2020-01-30 20:29:32 +01:00
62e763604e Install all javascript dependencies by default 2020-01-18 19:44:11 +01:00
12693dac10 Fix auto read marking with category selected 2020-01-02 20:50:09 +01:00
06ce82bf0c Merge category view tests into single file 2020-01-02 20:15:35 +01:00
28620eab29 Rerun prettier 2020-01-01 21:46:35 +01:00
68534cd541 Remove deprecated npm options 2020-01-01 21:39:07 +01:00
5a630ea4b3 Various changes
- Update npm dependencies
- Update default fixture to exclude default celery task
2020-01-01 16:38:10 +01:00
8acf3bbc7f Rerun black 2019-12-31 14:46:42 +01:00
38d9d74db4 Collection rule pages 2019-12-31 14:33:14 +01:00
d345bc2595 Add missing model to fixture 2019-11-30 16:27:50 +01:00
08e4d043b9 Update default fixture 2019-11-30 00:02:04 +01:00
a9edd520a7 Fix negative unread count 2019-11-28 21:18:45 +01:00
a4102130d2 Merge branch 'account-management' into 'development'
Account management

See merge request sonny/newsreader!3
2019-11-27 22:10:02 +01:00
b2829716b0 Account management 2019-11-27 22:10:02 +01:00
94f4ed6327 Redux refactor 2019-11-24 16:41:46 +01:00
b952d70d92 Category pages 2019-11-19 12:21:34 +01:00
a350cc280d Restructure scss file layout & task 2019-11-15 23:31:02 +01:00
bb0c2e792c #24 task-creation 2019-11-14 20:58:57 +01:00
fb709fe1d0 Merge branch 'main-page' into 'development'
Article page

See merge request sonny/newsreader!2
2019-10-28 21:35:20 +01:00
858f84aaad Refactor endpoint tests
Replace force_login calls with login call from client class in setUp
2019-10-28 21:35:19 +01:00
61702e720a Add swagger integration 2019-08-25 10:06:51 +02:00
752ba62aee Add pre-commit config 2019-08-18 11:49:04 +02:00
8c3bc408b9 Add a default fixture 2019-08-10 22:28:00 +02:00
0658d6404f Add docker specific files 2019-08-10 21:12:59 +02:00
679414a703 Add Login page 2019-07-20 09:57:10 +02:00
b1c5be61f1 increase duplicate handler's history range 2019-07-15 08:29:24 +02:00
a74ffae9a7 Celery integration 2019-07-14 18:44:15 +02:00
1b774a7208 Construct datetime without converting to localtime 2019-07-12 20:20:58 +02:00
8f314e0ca6 Remove leftover print statement 2019-07-10 22:01:15 +02:00
a95db91726 Add truncate tests for title and author fields 2019-07-10 21:55:28 +02:00
88cf5f9bd4 Add gitlab ci settings 2019-07-09 20:33:05 +02:00
a798b9858c Set the default content type 2019-07-09 19:17:39 +02:00
9c6be7357d Add post/category/rule endpoints 2019-07-08 22:54:24 +02:00
982c5bb132 Update isort & rerun formatting 2019-07-01 12:11:44 +02:00
ed658c4dfd Handle request exceptions 2019-07-01 11:38:23 +02:00
cfd064ec85 Add missing requirements 2019-07-01 09:39:48 +02:00
48a9b25545 Favicon fetcher 2019-07-01 09:36:01 +02:00
c75de1c469 Add type hinting 2019-06-23 12:52:49 +02:00
69e4e7b269 Update project requirements 2019-06-22 21:55:47 +02:00
c508cca080 Merge first implementation 2019-06-22 09:29:56 +00:00
abacb72b30 Add gitignore 2019-04-07 15:34:33 +00:00
346 changed files with 16643 additions and 28442 deletions

View file

@ -1,11 +0,0 @@
{
"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}],
]
}

View file

@ -1,16 +0,0 @@
[run]
source = ./src/newsreader/
omit =
**/tests/**
**/migrations/**
**/conf/**
**/apps.py
**/admin.py
**/tests.py
**/urls.py
**/wsgi.py
**/celery.py
**/__init__.py
[html]
directory = coverage

25
.editorconfig Normal file
View file

@ -0,0 +1,25 @@
# https://editorconfig.org
# top-most EditorConfig file
root = true
# Unix-style newlines with a newline ending every file
[*]
end_of_line = lf
trim_trailing_whitespace = true
[*.py]
indent_style = space
indent_size = 4
[*.{yaml,yml,toml,md}]
indent_style = space
indent_size = 2
[Dockerfile*]
indent_style = space
indent_size = 4
[*.json]
indent_style = space
indent_size = 2

3
.gitignore vendored
View file

@ -35,6 +35,7 @@ eggs/
lib/
!src/newsreader/scss/lib
!src/newsreader/js/lib
lib64/
parts/
@ -114,7 +115,7 @@ celerybeat-schedule
*.sage.py
# Environments
.env
*.env
.venv
env/
venv/

View file

@ -1,30 +0,0 @@
stages:
- build
- test
- lint
- release
- deploy
variables:
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
DJANGO_SETTINGS_MODULE: "newsreader.conf.gitlab"
POSTGRES_HOST: "$POSTGRES_HOST"
POSTGRES_DB: "$POSTGRES_NAME"
POSTGRES_NAME: "$POSTGRES_NAME"
POSTGRES_USER: "$POSTGRES_USER"
POSTGRES_PASSWORD: "$POSTGRES_PASSWORD"
cache:
key: "$CI_COMMIT_REF_SLUG"
paths:
- .venv/
- .cache/pip
- .cache/poetry
- node_modules/
include:
- local: '/gitlab-ci/build.yml'
- local: '/gitlab-ci/test.yml'
- local: '/gitlab-ci/lint.yml'
- local: '/gitlab-ci/release.yml'
- local: '/gitlab-ci/deploy.yml'

View file

@ -1,12 +0,0 @@
[settings]
include_trailing_comma = true
line_length = 88
multi_line_output = 3
skip = env/, venv/
default_section = THIRDPARTY
known_first_party = newsreader
known_django = django
sections = FUTURE,STDLIB,DJANGO,THIRDPARTY,FIRSTPARTY,LOCALFOLDER
lines_between_types=1
lines_after_imports=2
lines_between_types=1

1
.nvmrc Normal file
View file

@ -0,0 +1 @@
lts/*

View file

@ -1,10 +0,0 @@
{
"semi": true,
"trailingComma": "es5",
"singleQuote": true,
"printWidth": 90,
"tabWidth": 2,
"useTabs": false,
"bracketSpacing": true,
"arrowParens": "avoid"
}

10
.woodpecker/build.yaml Normal file
View file

@ -0,0 +1,10 @@
when:
- event: push
- event: pull_request
- event: manual
steps:
- image: node:lts-alpine
commands:
- npm install
- npm run build:prod

18
.woodpecker/lint.yaml Normal file
View file

@ -0,0 +1,18 @@
when:
- event: push
- event: pull_request
- event: manual
steps:
- name: python linting
image: ghcr.io/astral-sh/uv:python3.11-alpine
commands:
- uv sync --group ci
- uv run --no-sync -- ruff check src/
- uv run --no-sync -- ruff format --check src/
- name: javascript linting
image: node:lts-alpine
commands:
- npm ci
- npm run lint

37
.woodpecker/tests.yaml Normal file
View file

@ -0,0 +1,37 @@
when:
- event: push
- event: pull_request
- event: manual
services:
- name: postgres
image: postgres:15
environment:
POSTGRES_NAME: &db-name newsreader
POSTGRES_USER: &db-user newsreader
POSTGRES_PASSWORD: &db-password sekrit
- name: memcached
image: memcached:1.5.22
steps:
- name: python tests
image: ghcr.io/astral-sh/uv:python3.11-alpine
environment:
DJANGO_SETTINGS_MODULE: "newsreader.conf.ci"
DJANGO_SECRET_KEY: sekrit
POSTGRES_HOST: postgres
POSTGRES_PORT: 5432
POSTGRES_DB: *db-name
POSTGRES_USER: *db-user
POSTGRES_PASSWORD: *db-password
commands:
- pip install uv
- uv sync --group ci
- uv run --no-sync -- coverage run ./src/manage.py test newsreader
- uv run --no-sync -- coverage report --show-missing
- name: javascript tests
image: node:lts-alpine
commands:
- npm ci
- npm test

149
CHANGELOG.md Normal file
View file

@ -0,0 +1,149 @@
# Changelog
## 0.5.3
- Apply query optimizations for retrieving posts
## 0.5.2
- Add missing `VERSION` environment variable
## 0.5.1
- Use line-through styling for read posts
- Use full height for post layout
## 0.5.0
- Upgrade python to 3.11
- Upgrade django to 4.2
- Migrate from pip-tools to uv
- Migrate from black to ruff for formatting
- Upgrade webpack to 5.9 (with various tooling)
- Styling refactor
- Mobile/tablet layout added
## 0.4.4
- Sort posts before storing in redux store
## 0.4.3
- Use `IntersectionObserver` to paginate
## 0.4.2
- Set `SECURE_PROXY_SSL_HEADER` setting for production
## 0.4.1
- Add missing env variables
## 0.4.0
- Add Makefile & use `pip-tools` to generate dependencies
- Add `pyproject.toml`
- Update dependencies
- Update docker-compose setup
- Default to `newsreader.conf.docker` settings module
- Add scroll to top/bottom buttons
## 0.3.13.8
- Update dependencies
- Fix csrf_token's not rendering
## 0.3.13.7
- Check for Twitter error codes in response
## 0.3.13.6
- Try to load sentry by default for all environments
## 0.3.13.5
- Set response keyword argument
## 0.3.13.4
- Fix import error
## 0.3.13.3
- Use sentry's set_extra to provide extra debug variables
## 0.3.13.2
- Update sentry-sdk
## 0.3.13.1
- Fix mutual exclusive exception for email settings
- Temporarly set exception level for StreamDeniedException exceptions
## 0.3.13
- Update django to 3.2
- Notify users of expired credentials
## 0.3.12.1
- Add missing background-color
## 0.3.12
- Update light theme
- Sticky navbar
- Sticky post modal header
## 0.3.11
- Add saved posts section
- Bump django version
## 0.3.10
- Add custom color for confirm buttons
- Update font sizes
## 0.3.9
- Cursor based pagination
- Updated django version
## 0.3.8
- Update light / dark theme
- Replace css.gg with fontawesome
- Update deploy job
## 0.3.7
- Add a dark theme
- Update object representations
- Move sentry to optional dependency
- Add CHANGELOG.md
## 0.3.6.3
- Update deploy job
## 0.3.6.2
- Use warning logging level for BuilderSkippedException's
- Change working directory before running ansible
## 0.3.6.1
- Install ansible required roles
## 0.3.6
- Update deploy job
- Add user manageable reddit filters
## 0.3.5
- Show timezone next to post datetimes
- Take read status in consideration when sorting posts

84
Dockerfile Normal file
View file

@ -0,0 +1,84 @@
# stage 1
FROM python:3.11-alpine AS backend
ARG USER_ID=1000
ARG GROUP_ID=1000
ARG UV_LINK_MODE=copy
RUN apk update \
&& apk add --no-cache \
vim \
curl \
gettext
RUN addgroup -g $USER_ID newsreader && adduser -Du $GROUP_ID -G newsreader newsreader
RUN mkdir --parents /app/src /app/logs /app/media /app/bin /app/static \
&& chown -R newsreader:newsreader /app
WORKDIR /app
USER newsreader
COPY --chown=newsreader:newsreader uv.lock pyproject.toml /app/
COPY --from=ghcr.io/astral-sh/uv:python3.11-alpine /usr/local/bin/uv /bin/uv
RUN --mount=type=cache,uid=$USER_ID,gid=$GROUP_ID,target=/home/newsreader/.cache/uv \
uv sync --frozen --no-default-groups --no-install-project
COPY --chown=newsreader:newsreader ./bin/docker-entrypoint.sh /app/bin/docker-entrypoint.sh
VOLUME ["/app/logs", "/app/media", "/app/static"]
# stage 2
FROM node:lts-alpine AS frontend-build
ARG BUILD_ARG=prod
WORKDIR /app
RUN chown node:node /app
USER node
COPY --chown=node:node ./package*.json ./webpack.*.js ./babel.config.js /app/
RUN --mount=type=cache,uid=1000,gid=1000,target=/home/node/.npm \
npm ci
COPY --chown=node:node ./src /app/src
RUN npm run build:$BUILD_ARG
# stage 3
FROM backend AS production
COPY --from=frontend-build --chown=newsreader:newsreader \
/app/src/newsreader/static /app/src/newsreader/static
RUN --mount=type=cache,uid=$USER_ID,gid=$GROUP_ID,target=/home/newsreader/.cache/uv \
uv sync --frozen --only-group production --extra sentry
COPY --chown=newsreader:newsreader ./src /app/src
ENV DJANGO_SETTINGS_MODULE=newsreader.conf.production
# Note that the static volume will have to be recreated to be pre-populated
# correctly with the latest static files. See
# https://docs.docker.com/storage/volumes/#populate-a-volume-using-a-container
RUN uv run --no-sync -- src/manage.py collectstatic --noinput
# (optional) stage 4
FROM backend AS development
RUN --mount=type=cache,uid=$USER_ID,gid=$GROUP_ID,target=/home/newsreader/.cache/uv \
uv sync --frozen --group development
ENV DJANGO_SETTINGS_MODULE=newsreader.conf.docker

21
babel.config.js Normal file
View file

@ -0,0 +1,21 @@
module.exports = api => {
const isTest = api.env('test');
const preset = [
"@babel/preset-env", { targets: 'defaults' }
];
const testPreset = [
"@babel/preset-env", { targets: { node: process.versions.node } }
];
const plugins = [
"@babel/plugin-syntax-dynamic-import",
"@babel/plugin-transform-react-jsx",
"@babel/plugin-proposal-class-properties"
]
return {
"presets": [isTest ? testPreset : preset],
"plugins": plugins
}
}

5
bin/docker-entrypoint.sh Executable file
View file

@ -0,0 +1,5 @@
#!/bin/bash
uv run --no-sync -- /app/src/manage.py migrate
exec "$@"

View file

@ -0,0 +1,21 @@
upstream gunicorn {
server django:8000;
}
server {
listen 80;
server_name localhost;
access_log /var/log/nginx/access_log;
error_log /var/log/nginx/error_log;
location /static/ {
root /app;
}
location / {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_pass http://gunicorn;
}
}

View file

@ -0,0 +1,36 @@
volumes:
static-files:
services:
django:
build: &app-development-build
target: development
command: uv run --no-sync -- /app/src/manage.py runserver 0.0.0.0:8000
environment: &django-env
DJANGO_SETTINGS_MODULE: ${DJANGO_SETTINGS_MODULE:-newsreader.conf.docker}
ports:
- "${DJANGO_PORT:-8000}:8000"
volumes:
- ./src:/app/src
- static-files:/app/src/newsreader/static
stdin_open: true
tty: true
celery:
build:
<<: *app-development-build
environment:
<<: *django-env
volumes:
- ./src/:/app/src
webpack:
build:
target: frontend-build
context: .
args:
BUILD_ARG: "dev"
command: npm run build:watch
volumes:
- ./src/:/app/src
- static-files:/app/src/newsreader/static

View file

@ -0,0 +1,16 @@
volumes:
logs:
static-files:
services:
nginx:
image: nginx:1.23
depends_on:
django:
condition: service_healthy
ports:
- "${NGINX_HTTP_PORT:-80}:80"
volumes:
- ./config/nginx/conf.d:/etc/nginx/conf.d
- logs:/var/log/nginx
- static-files:/app/static

View file

@ -1,60 +1,126 @@
version: '3'
volumes:
logs:
media:
postgres-data:
static-files:
node-modules:
x-db-connection-env: &db-connection-env
POSTGRES_HOST: ${POSTGRES_HOST:-db}
POSTGRES_PORT: ${POSTGRES_PORT:-5432}
POSTGRES_DB: &pg-database ${POSTGRES_DB:-newsreader}
POSTGRES_USER: &pg-user ${POSTGRES_USER:-newsreader}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-newsreader}
x-db-env: &db-env
<<: *db-connection-env
PGUSER: *pg-user
PGDATABASE: *pg-database
x-django-env: &django-env
<<: *db-connection-env
ALLOWED_HOSTS: ${ALLOWED_HOSTS:-localhost,127.0.0.1,django}
INTERNAL_IPS: ${INTERNAL_IPS:-localhost,127.0.0.1,django}
# see token_urlsafe from python's secret module to generate one
DJANGO_SECRET_KEY: ${DJANGO_SECRET_KEY:-Ojg68lYsP3kq2r5JgozUzKVSRFywm17BTMS5iwpLM44}
DJANGO_SETTINGS_MODULE: ${DJANGO_SETTINGS_MODULE:-newsreader.conf.production}
ADMINS: ${ADMINS:-""}
VERSION: ${VERSION:-""}
# Email
EMAIL_HOST: ${EMAIL_HOST:-localhost}
EMAIL_PORT: ${EMAIL_PORT:-25}
EMAIL_HOST_USER: ${EMAIL_HOST_USER:-""}
EMAIL_HOST_PASSWORD: ${EMAIL_HOST_PASSWORD:-""}
EMAIL_USE_TLS: ${EMAIL_USE_TLS:-no}
EMAIL_USE_SSL: ${EMAIL_USE_SSL:-no}
EMAIL_DEFAULT_FROM: ${EMAIL_DEFAULT_FROM:-webmaster@localhost}
# Sentry
SENTRY_DSN: ${SENTRY_DSN:-""}
services:
db:
image: postgres
environment:
POSTGRES_DB: "newsreader"
POSTGRES_USER: "newsreader"
POSTGRES_PASSWORD: "newsreader"
<<: *db-env
image: postgres:15
healthcheck:
test: /usr/bin/pg_isready
start_period: 10s
interval: 5s
timeout: 10s
retries: 10
volumes:
- postgres-data:/var/lib/postgresql/data
rabbitmq:
image: rabbitmq:3.7
image: rabbitmq:4
memcached:
image: memcached:1.5.22
ports:
- "11211:11211"
image: memcached:1.6
entrypoint:
- memcached
- -m 64
django:
build: &app-build
context: .
target: production
environment:
<<: *django-env
entrypoint: ["/bin/sh", "/app/bin/docker-entrypoint.sh"]
command: |
uv run --no-sync --
gunicorn
--bind 0.0.0.0:8000
--workers 3
--chdir /app/src/
newsreader.wsgi:application
healthcheck:
test: /usr/bin/curl --fail http://django:8000 || exit 1
start_period: 10s
interval: 10s
timeout: 10s
retries: 5
depends_on:
memcached:
condition: service_started
db:
condition: service_healthy
volumes:
- logs:/app/logs
- media:/app/media
- static-files:/app/static
celery:
build:
context: .
dockerfile: ./docker/django
command: celery worker -n worker1@%h -n worker2@%h --app newsreader --loglevel INFO --concurrency 2 --workdir /app/src/ --beat --scheduler django
<<: *app-build
environment:
- DJANGO_SETTINGS_MODULE=newsreader.conf.docker
<<: *django-env
command: |
uv run --no-sync --
celery
--app newsreader
--workdir /app/src/
worker --loglevel INFO
--concurrency 2
--beat
--scheduler django
-n worker1@%h
-n worker2@%h
healthcheck:
test: uv run --no-sync -- celery --app newsreader status || exit 1
start_period: 10s
interval: 10s
timeout: 10s
retries: 5
depends_on:
- rabbitmq
rabbitmq:
condition: service_started
django:
condition: service_healthy
volumes:
- .:/app
django:
build:
context: .
dockerfile: ./docker/django
command: python /app/src/manage.py runserver 0.0.0.0:8000
environment:
- DJANGO_SETTINGS_MODULE=newsreader.conf.docker
ports:
- '8000:8000'
depends_on:
- db
volumes:
- .:/app
- static-files:/app/src/newsreader/static
stdin_open: true
tty: true
webpack:
build:
context: .
dockerfile: ./docker/webpack
command: npm run build:watch
volumes:
- .:/app
- static-files:/app/src/newsreader/static
- node-modules:/app/node_modules
- logs:/app/logs

View file

@ -1,10 +0,0 @@
FROM python:3.7-buster
RUN pip install poetry
WORKDIR /app
COPY poetry.lock pyproject.toml /app/
RUN poetry config virtualenvs.create false && poetry install --no-interaction
COPY . /app/

View file

@ -1,9 +0,0 @@
FROM node:12
WORKDIR /app
COPY package.json package-lock.json /app/
RUN npm install
COPY . /app/

View file

@ -1,7 +0,0 @@
static:
stage: build
image: node:12
before_script:
- npm install
script:
- npm run build

View file

@ -1,24 +0,0 @@
deploy:
stage: deploy
image: python:3.7
environment:
name: production
url: rss.fudiggity.nl
rules:
- if: $CI_COMMIT_TAG
before_script:
- pip install ansible --quiet
- git clone https://git.fudiggity.nl/sonny/ansible-playbooks.git deployment
- mkdir /root/.ssh
- echo "$DEPLOY_HOST_KEY" > /root/.ssh/known_hosts
- echo "$DEPLOY_KEY" > deployment/deploy_key && chmod 0600 deployment/deploy_key
- mkdir /root/.vaults
- echo "$VAULT_PASSWORD" > deployment/vault && chmod 0700 deployment/vault
script:
- >
ansible-playbook deployment/playbook.yml
--inventory deployment/apps.yml
--limit newsreader
--user ansible
--private-key deployment/deploy_key
--vault-password-file deployment/vault

View file

@ -1,28 +0,0 @@
python-linting:
stage: lint
image: python:3.7
before_script:
- pip install poetry --quiet
- poetry config cache-dir ~/.cache/poetry
- poetry config virtualenvs.in-project true
- poetry install --no-interaction --quiet
script:
- poetry run isort src/ --check-only --recursive
- poetry run black src/ --line-length 88 --check
- poetry run autoflake src/ --check --recursive --remove-all-unused-imports --ignore-init-module-imports
only:
refs:
- development
- merge_requests
javascript-linting:
stage: lint
image: node:12
before_script:
- npm install
script:
- npm run lint
only:
refs:
- development
- merge_requests

View file

@ -1,12 +0,0 @@
release:
stage: release
image: registry.gitlab.com/gitlab-org/release-cli:latest
rules:
- if: $CI_COMMIT_TAG
script:
- echo 'running release job'
release:
name: 'Release $CI_COMMIT_TAG'
description: 'Auto created release'
tag_name: '$CI_COMMIT_TAG'
ref: '$CI_COMMIT_TAG'

View file

@ -1,23 +0,0 @@
python-tests:
stage: test
coverage: '/TOTAL\s+\d+\s+\d+\s+(\d+%)/'
services:
- postgres:11
- memcached:1.5.22
image: python:3.7
before_script:
- pip install poetry --quiet
- poetry config cache-dir .cache/poetry
- poetry config virtualenvs.in-project true
- poetry install --no-interaction --quiet
script:
- poetry run coverage run src/manage.py test newsreader
- poetry run coverage report
javascript-tests:
stage: test
image: node:12
before_script:
- npm install
script:
- npm test

View file

@ -1,188 +0,0 @@
// For a detailed explanation regarding each configuration property, visit:
// https://jestjs.io/docs/en/configuration.html
module.exports = {
// All imported modules in your tests should be mocked automatically
// automock: false,
// Stop running tests after `n` failures
// bail: 0,
// Respect "browser" field in package.json when resolving modules
// browser: false,
// The directory where Jest should store its cached dependency information
// cacheDirectory: "/tmp/jest_rs",
// Automatically clear mock calls and instances between every test
clearMocks: true,
// Indicates whether the coverage information should be collected while executing the test
// collectCoverage: false,
// An array of glob patterns indicating a set of files for which coverage information should be collected
// collectCoverageFrom: null,
// The directory where Jest should output its coverage files
coverageDirectory: 'coverage',
// An array of regexp pattern strings used to skip coverage collection
// coveragePathIgnorePatterns: [
// "/node_modules/"
// ],
// A list of reporter names that Jest uses when writing coverage reports
// coverageReporters: [
// "json",
// "text",
// "lcov",
// "clover"
// ],
// An object that configures minimum threshold enforcement for coverage results
// coverageThreshold: null,
// A path to a custom dependency extractor
// dependencyExtractor: null,
// Make calling deprecated APIs throw helpful error messages
// errorOnDeprecated: false,
// Force coverage collection from ignored files using an array of glob patterns
// forceCoverageMatch: [],
// A path to a module which exports an async function that is triggered once before all test suites
// globalSetup: null,
// A path to a module which exports an async function that is triggered once after all test suites
// globalTeardown: null,
// A set of global variables that need to be available in all test environments
// globals: {},
// The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
// maxWorkers: "50%",
// An array of directory names to be searched recursively up from the requiring module's location
// moduleDirectories: [
// "node_modules"
// ],
// An array of file extensions your modules use
// moduleFileExtensions: [
// "js",
// "json",
// "jsx",
// "ts",
// "tsx",
// "node"
// ],
// A map from regular expressions to module names that allow to stub out resources with a single module
// moduleNameMapper: {},
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
// modulePathIgnorePatterns: [],
// Activates notifications for test results
// notify: false,
// An enum that specifies notification mode. Requires { notify: true }
// notifyMode: "failure-change",
// A preset that is used as a base for Jest's configuration
// preset: null,
// Run tests from one or more projects
// projects: null,
// Use this configuration option to add custom reporters to Jest
// reporters: undefined,
// Automatically reset mock state between every test
// resetMocks: false,
// Reset the module registry before running each individual test
// resetModules: false,
// A path to a custom resolver
// resolver: null,
// Automatically restore mock state between every test
// restoreMocks: false,
// The root directory that Jest should scan for tests and modules within
rootDir: 'src/newsreader/js/tests/',
// A list of paths to directories that Jest should use to search for files in
// roots: [
// "<rootDir>"
// ],
// Allows you to use a custom runner instead of Jest's default test runner
// runner: "jest-runner",
// The paths to modules that run some code to configure or set up the testing environment before each test
// setupFiles: [],
// A list of paths to modules that run some code to configure or set up the testing framework before each test
// setupFilesAfterEnv: [],
// A list of paths to snapshot serializer modules Jest should use for snapshot testing
// snapshotSerializers: [],
// The test environment that will be used for testing
testEnvironment: 'node',
// Options that will be passed to the testEnvironment
// testEnvironmentOptions: {},
// Adds a location field to test results
// testLocationInResults: false,
// The glob patterns Jest uses to detect test files
// testMatch: [
// "**/__tests__/**/*.[jt]s?(x)",
// "**/?(*.)+(spec|test).[tj]s?(x)"
// ],
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
// testPathIgnorePatterns: [
// "/node_modules/"
// ],
// The regexp pattern or array of patterns that Jest uses to detect test files
// testRegex: [],
// This option allows the use of a custom results processor
// testResultsProcessor: null,
// This option allows use of a custom test runner
// testRunner: "jasmine2",
// This option sets the URL for the jsdom environment. It is reflected in properties such as location.href
// testURL: "http://localhost",
// Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout"
// timers: "real",
// A map from regular expressions to paths to transformers
// transform: null,
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
// transformIgnorePatterns: [
// "/node_modules/"
// ],
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
// unmockedModulePathPatterns: undefined,
// Indicates whether each individual test should be reported during the run
// verbose: null,
// An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
// watchPathIgnorePatterns: [],
// Whether to use watchman for file crawling
// watchman: true,
};

16913
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,63 +1,75 @@
{
"name": "newsreader",
"version": "0.1.0",
"version": "0.5.3",
"description": "Application for viewing RSS feeds",
"main": "index.js",
"scripts": {
"lint": "npx prettier \"src/newsreader/js/**/*.js\" --check",
"format": "npx prettier \"src/newsreader/js/**/*.js\" --write",
"build": "npx webpack --config webpack.dev.babel.js",
"build:watch": "npx webpack --config webpack.dev.babel.js --watch",
"build:dev": "npx webpack --config webpack.dev.babel.js",
"build:prod": "npx webpack --config webpack.prod.babel.js",
"test": "npx jest",
"test:watch": "npm test -- --watch"
},
"repository": {
"type": "git",
"url": "[git@git.fudiggity.nl:5000]:sonny/newsreader.git"
"url": "forgejo.fudiggity.nl:sonny/newsreader"
},
"author": "Sonny",
"license": "GPL-3.0-or-later",
"dependencies": {
"css.gg": "^1.0.6",
"@fortawesome/fontawesome-free": "^5.15.2",
"js-cookie": "^2.2.1",
"lodash": "^4.17.15",
"lodash": "^4.17.20",
"object-assign": "^4.1.1",
"react-redux": "^7.1.3",
"react-redux": "^7.2.2",
"redux": "^4.0.5",
"redux-logger": "^3.0.6",
"redux-thunk": "^2.3.0"
},
"devDependencies": {
"@babel/core": "^7.7.7",
"@babel/plugin-proposal-class-properties": "^7.7.4",
"@babel/plugin-proposal-function-bind": "^7.7.4",
"@babel/plugin-syntax-dynamic-import": "^7.7.4",
"@babel/plugin-syntax-function-bind": "^7.7.4",
"@babel/plugin-transform-react-jsx": "^7.7.7",
"@babel/plugin-transform-runtime": "^7.7.6",
"@babel/preset-env": "^7.7.7",
"@babel/register": "^7.7.7",
"@babel/runtime": "^7.7.7",
"babel-jest": "^24.9.0",
"babel-loader": "^8.1.0",
"@babel/core": "^7.12.13",
"@babel/plugin-proposal-class-properties": "^7.12.13",
"@babel/plugin-proposal-function-bind": "^7.12.13",
"@babel/plugin-transform-react-jsx": "^7.12.13",
"@babel/preset-env": "^7.12.13",
"@babel/register": "^7.12.13",
"@babel/runtime": "^7.12.13",
"babel-jest": "^29.7.0",
"babel-loader": "^8.2.2",
"clean-webpack-plugin": "^3.0.0",
"css-loader": "^3.4.2",
"fetch-mock": "^8.3.1",
"file-loader": "^6.0.0",
"jest": "^24.9.0",
"mini-css-extract-plugin": "^0.9.0",
"node-fetch": "^2.6.0",
"node-sass": "^4.14.1",
"css-loader": "^7.1.2",
"fetch-mock": "^8.3.2",
"jest": "^29.7.0",
"mini-css-extract-plugin": "^2.9.1",
"node-fetch": "^2.6.1",
"prettier": "^1.19.1",
"react": "^16.12.0",
"react-dom": "^16.12.0",
"react": "^16.14.0",
"react-dom": "^16.14.0",
"redux-mock-store": "^1.5.4",
"sass": "^1.52.1",
"sass-loader": "^8.0.2",
"style-loader": "^1.1.3",
"url-loader": "^4.1.0",
"webpack": "^4.42.1",
"webpack-cli": "^3.3.11",
"style-loader": "^2.0.0",
"url-loader": "^4.1.1",
"webpack": "^5.94.0",
"webpack-cli": "^5.1.4",
"webpack-merge": "^4.2.2"
},
"prettier": {
"semi": true,
"trailingComma": "es5",
"singleQuote": true,
"printWidth": 90,
"tabWidth": 2,
"useTabs": false,
"bracketSpacing": true,
"arrowParens": "avoid"
},
"jest": {
"roots": [
"src/newsreader/js/tests/"
],
"clearMocks": true,
"coverageDirectory": "coverage"
}
}

1189
poetry.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,42 +1,81 @@
[tool.poetry]
[project]
name = "newsreader"
version = "0.2"
description = "Webapplication for reading RSS feeds"
authors = ["Sonny <sonnyba871@gmail.com>"]
license = "GPL-3.0"
version = "0.5.3"
authors = [{ name = "Sonny" }]
license = { text = "GPL-3.0" }
requires-python = ">=3.11"
dependencies = [
"django~=4.2",
"celery~=5.4",
"psycopg[binary]",
"django-axes",
"django-celery-beat~=2.7.0",
"django-rest-framework",
"djangorestframework-camel-case",
"pymemcache",
"python-dotenv~=1.0.1",
"ftfy~=6.2",
"requests",
"feedparser",
"bleach",
"beautifulsoup4",
"lxml",
]
[tool.poetry.dependencies]
python = "^3.7"
bleach = "^3.1.4"
Django = "^3.0.5"
celery = "^4.4.2"
beautifulsoup4 = "^4.9.0"
django-axes = "^5.3.1"
django-celery-beat = "^2.0.0"
djangorestframework = "^3.11.0"
drf-yasg = "^1.17.1"
django-registration-redux = "^2.7"
lxml = "^4.5.0"
feedparser = "^5.2.1"
python-memcached = "^1.59"
requests = "^2.23.0"
psycopg2-binary = "^2.8.5"
gunicorn = "^20.0.4"
python-dotenv = "^0.12.0"
django = ">=3.0.7"
sentry-sdk = "^0.15.1"
[dependency-groups]
test-tools = ["ruff", "factory_boy", "freezegun"]
development = [
"django-debug-toolbar",
"django-stubs",
"django-extensions",
]
ci = ["coverage~=7.6.1"]
production = ["gunicorn~=23.0"]
[tool.poetry.dev-dependencies]
factory-boy = "^2.12.0"
freezegun = "^0.3.15"
django-debug-toolbar = "^2.2"
django-extensions = "^2.2.9"
black = "19.3b0"
isort = "4.3.21"
autoflake = "1.3.1"
tblib = "1.6.0"
coverage = "^5.1"
[project.optional-dependencies]
sentry = ["sentry-sdk~=2.0"]
[build-system]
requires = ["poetry>=0.12"]
build-backend = "poetry.masonry.api"
[tool.uv]
environments = ["sys_platform == 'linux'"]
default-groups = ["test-tools"]
[tool.ruff]
include = ["pyproject.toml", "src/**/*.py"]
line-length = 88
[tool.ruff.lint]
select = ["E4", "E7", "E9", "F", "I"]
[tool.ruff.lint.isort]
lines-between-types=1
lines-after-imports=2
default-section = "third-party"
known-first-party = ["newsreader"]
section-order = [
"future",
"standard-library",
"django",
"third-party",
"first-party",
"local-folder",
]
[tool.ruff.lint.isort.sections]
django = ["django"]
[tool.coverage.run]
source = ["./src/newsreader/"]
omit = [
"**/tests/**",
"**/migrations/**",
"**/conf/**",
"**/apps.py",
"**/admin.py",
"**/tests.py",
"**/urls.py",
"**/wsgi.py",
"**/celery.py",
"**/__init__.py"
]

View file

@ -1,11 +1,12 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "newsreader.conf.dev")
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "newsreader.conf.docker")
try:
from django.core.management import execute_from_command_line
except ImportError as exc:

View file

@ -2,7 +2,7 @@ from django import forms
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as DjangoUserAdmin
from django.contrib.auth.forms import UserChangeForm
from django.utils.translation import ugettext as _
from django.utils.translation import gettext as _
from newsreader.accounts.models import User
@ -11,8 +11,6 @@ class UserAdminForm(UserChangeForm):
class Meta:
widgets = {
"email": forms.EmailInput(attrs={"size": "50"}),
"reddit_access_token": forms.TextInput(attrs={"size": "90"}),
"reddit_refresh_token": forms.TextInput(attrs={"size": "90"}),
}
@ -30,10 +28,6 @@ class UserAdmin(DjangoUserAdmin):
_("User settings"),
{"fields": ("email", "password", "first_name", "last_name", "is_active")},
),
(
_("Reddit settings"),
{"fields": ("reddit_access_token", "reddit_refresh_token")},
),
(
_("Permission settings"),
{"classes": ("collapse",), "fields": ("is_staff", "is_superuser")},

View file

@ -2,4 +2,4 @@ from django.apps import AppConfig
class AccountsConfig(AppConfig):
name = "accounts"
name = "newsreader.accounts"

View file

@ -1,9 +1,11 @@
from django import forms
from newsreader.accounts.models import User
from newsreader.core.forms import CheckboxInput
class UserSettingsForm(forms.ModelForm):
class Meta:
model = User
fields = ("first_name", "last_name")
fields = ("first_name", "last_name", "auto_mark_read")
widgets = {"auto_mark_read": CheckboxInput}

View file

@ -8,7 +8,6 @@ from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [

View file

@ -4,7 +4,6 @@ from django.db import migrations
class Migration(migrations.Migration):
dependencies = [("accounts", "0001_initial")]
operations = [migrations.RemoveField(model_name="user", name="username")]

View file

@ -6,7 +6,6 @@ import newsreader.accounts.models
class Migration(migrations.Migration):
dependencies = [("accounts", "0002_remove_user_username")]
operations = [

View file

@ -4,7 +4,6 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [("accounts", "0003_auto_20190714_1417")]
operations = [

View file

@ -4,7 +4,6 @@ from django.db import migrations
class Migration(migrations.Migration):
dependencies = [("accounts", "0004_auto_20190714_1501")]
operations = [migrations.RemoveField(model_name="user", name="task_interval")]

View file

@ -6,7 +6,6 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [("accounts", "0005_remove_user_task_interval")]
operations = [

View file

@ -6,7 +6,6 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [("accounts", "0006_auto_20191116_1253")]
operations = [

View file

@ -15,7 +15,6 @@ def update_task_name(apps, schema_editor):
class Migration(migrations.Migration):
dependencies = [("accounts", "0007_auto_20191116_1255")]
operations = [migrations.RunPython(update_task_name)]

View file

@ -6,7 +6,6 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("django_celery_beat", "0012_periodictask_expire_seconds"),
("accounts", "0008_auto_20200422_2243"),

View file

@ -4,7 +4,6 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [("accounts", "0009_auto_20200524_1218")]
operations = [

View file

@ -0,0 +1,20 @@
# Generated by Django 3.0.7 on 2020-09-13 19:01
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [("accounts", "0010_auto_20200603_2230")]
operations = [
migrations.AddField(
model_name="user",
name="twitter_oauth_token",
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AddField(
model_name="user",
name="twitter_oauth_token_secret",
field=models.CharField(blank=True, max_length=255, null=True),
),
]

View file

@ -0,0 +1,9 @@
# Generated by Django 3.0.7 on 2020-09-26 15:34
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [("accounts", "0011_auto_20200913_2101")]
operations = [migrations.RemoveField(model_name="user", name="task")]

View file

@ -0,0 +1,19 @@
# Generated by Django 3.0.7 on 2020-10-27 21:45
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [("accounts", "0012_remove_user_task")]
operations = [
migrations.AddField(
model_name="user",
name="auto_mark_read",
field=models.BooleanField(
default=True,
help_text="Wether posts should be marked as read after x amount of seconds of reading",
verbose_name="Auto read marking",
),
)
]

View file

@ -0,0 +1,48 @@
# Generated by Django 3.0.7 on 2020-12-18 21:16
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [("accounts", "0013_user_auto_mark_read")]
operations = [
migrations.AddField(
model_name="user",
name="reddit_allow_nfsw",
field=models.BooleanField(default=False, verbose_name="Allow NSFW posts"),
),
migrations.AddField(
model_name="user",
name="reddit_allow_spoiler",
field=models.BooleanField(default=False, verbose_name="Allow spoilers"),
),
migrations.AddField(
model_name="user",
name="reddit_allow_viewed",
field=models.BooleanField(
default=True, verbose_name="Allow already seen posts"
),
),
migrations.AddField(
model_name="user",
name="reddit_comments_min",
field=models.PositiveIntegerField(
default=0, verbose_name="Minimum amount of comments"
),
),
migrations.AddField(
model_name="user",
name="reddit_downvotes_max",
field=models.PositiveIntegerField(
default=0, verbose_name="Maximum amount of downvotes"
),
),
migrations.AddField(
model_name="user",
name="reddit_upvotes_min",
field=models.PositiveIntegerField(
default=0, verbose_name="Minimum amount of upvotes"
),
),
]

View file

@ -0,0 +1,16 @@
# Generated by Django 3.0.7 on 2020-12-19 12:30
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [("accounts", "0014_auto_20201218_2216")]
operations = [
migrations.RemoveField(model_name="user", name="reddit_allow_nfsw"),
migrations.RemoveField(model_name="user", name="reddit_allow_spoiler"),
migrations.RemoveField(model_name="user", name="reddit_allow_viewed"),
migrations.RemoveField(model_name="user", name="reddit_comments_min"),
migrations.RemoveField(model_name="user", name="reddit_downvotes_max"),
migrations.RemoveField(model_name="user", name="reddit_upvotes_min"),
]

View file

@ -0,0 +1,17 @@
# Generated by Django 3.2 on 2021-04-23 20:37
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [("accounts", "0015_auto_20201219_1330")]
operations = [
migrations.AlterField(
model_name="user",
name="first_name",
field=models.CharField(
blank=True, max_length=150, verbose_name="first name"
),
)
]

View file

@ -0,0 +1,20 @@
# Generated by Django 3.2.25 on 2024-09-06 07:14
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("accounts", "0016_alter_user_first_name"),
]
operations = [
migrations.RemoveField(
model_name="user",
name="twitter_oauth_token",
),
migrations.RemoveField(
model_name="user",
name="twitter_oauth_token_secret",
),
]

View file

@ -0,0 +1,20 @@
# Generated by Django 4.2.16 on 2025-03-26 08:46
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("accounts", "0017_auto_20240906_0914"),
]
operations = [
migrations.RemoveField(
model_name="user",
name="reddit_access_token",
),
migrations.RemoveField(
model_name="user",
name="reddit_refresh_token",
),
]

View file

@ -1,11 +1,9 @@
import json
from django.contrib.auth.models import AbstractUser
from django.contrib.auth.models import UserManager as DjangoUserManager
from django.db import models
from django.utils.translation import gettext as _
from django_celery_beat.models import IntervalSchedule, PeriodicTask
from django_celery_beat.models import PeriodicTask
class UserManager(DjangoUserManager):
@ -41,18 +39,15 @@ class UserManager(DjangoUserManager):
class User(AbstractUser):
email = models.EmailField(_("email address"), unique=True)
task = models.OneToOneField(
PeriodicTask,
on_delete=models.CASCADE,
null=True,
blank=True,
editable=False,
verbose_name="collection task",
# settings
auto_mark_read = models.BooleanField(
_("Auto read marking"),
default=True,
help_text=_(
"Wether posts should be marked as read after x amount of seconds of reading"
),
)
reddit_refresh_token = models.CharField(max_length=255, blank=True, null=True)
reddit_access_token = models.CharField(max_length=255, blank=True, null=True)
username = None
objects = UserManager()
@ -60,24 +55,8 @@ class User(AbstractUser):
USERNAME_FIELD = "email"
REQUIRED_FIELDS = []
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
if not self.task:
task_interval, _ = IntervalSchedule.objects.get_or_create(
every=1, period=IntervalSchedule.HOURS
)
self.task, _ = PeriodicTask.objects.get_or_create(
enabled=True,
interval=task_interval,
name=f"{self.email}-collection-task",
task="FeedTask",
args=json.dumps([self.pk]),
)
self.save()
def delete(self, *args, **kwargs):
self.task.delete()
tasks = PeriodicTask.objects.filter(name__contains=self.email)
tasks.delete()
return super().delete(*args, **kwargs)

View file

@ -2,29 +2,23 @@
{% load i18n %}
{% block actions %}
<section class="section form__section--last">
<fieldset class="fieldset form__fieldset">
{% include "components/form/cancel-button.html" %}
</fieldset>
<section class="section form__section--last">
<fieldset class="fieldset form__fieldset">
{% include "components/form/confirm-button.html" %}
<fieldset class="fieldset form__fieldset">
<a class="link button button--primary" href="{% url 'accounts:password-change' %}">
{% trans "Change password" %}
</a>
<a class="link button button--primary" href="{% url 'accounts:password-change' %}">
{% trans "Change password" %}
</a>
{% include "components/form/confirm-button.html" %}
{% if reddit_authorization_url %}
<a class="link button button--reddit" href="{{ reddit_authorization_url }}">
{% trans "Authorize Reddit account" %}
</a>
{% endif %}
{% if reddit_refresh_url %}
<a class="link button button--reddit" href="{{ reddit_refresh_url }}">
{% trans "Refresh Reddit access token" %}
</a>
{% endif %}
</fieldset>
</section>
{% if favicon_task_allowed %}
<a class="link button button--primary" href="{% url 'accounts:settings:favicon' %}">
{% trans "Fetch favicons" %}
</a>
{% else %}
<button class="button button--primary button--disabled" disabled>
{% trans "Fetch favicons" %}
</button>
{% endif %}
</fieldset>
</section>
{% endblock actions %}

View file

@ -1,7 +1,9 @@
{% extends "base.html" %}
{% extends "sidebar.html" %}
{% block content %}
<main id="login--page" class="main">
{% include "accounts/components/login-form.html" with form=form title="Login" confirm_text="Login" %}
<main id="login--page" class="main" data-render-sidebar=true>
<div class="main__container">
{% include "accounts/components/login-form.html" with form=form title="Login" confirm_text="Login" %}
</div>
</main>
{% endblock %}

View file

@ -1,8 +1,12 @@
{% extends "base.html" %}
{% extends "sidebar.html" %}
{% block content %}
<main id="password-change--page" class="main">
{% url 'accounts:settings' as cancel_url %}
{% include "components/form/form.html" with form=form title="Change password" confirm_text="Change password" cancel_url=cancel_url %}
{% url 'accounts:settings:home' as cancel_url %}
<main id="password-change--page" class="main" data-render-sidebar=true>
<div class="main__container">
{% include "components/form/form.html" with form=form title="Change password" confirm_text="Change password" cancel_url=cancel_url %}
</div>
</main>
{% endblock %}

View file

@ -1,17 +0,0 @@
{% extends "base.html" %}
{% block content %}
<main id="settings--page" class="main">
<section class="section text-section">
{% if error %}
<h1 class="h1">Reddit authorization failed</h1>
<p>{{ error }}</p>
{% elif access_token and refresh_token %}
<h1 class="h1">Reddit account is linked</h1>
<p>Your reddit account was successfully linked.</p>
{% endif %}
<p><a href="{% url 'accounts:settings' %}">Return to settings page</a></p>
</section>
</main>
{% endblock %}

View file

@ -1,7 +1,9 @@
{% extends "base.html" %}
{% extends "sidebar.html" %}
{% block content %}
<main id="settings--page" class="main">
{% include "accounts/components/settings-form.html" with form=form title="User profile" confirm_text="Save" %}
<main id="settings--page" class="main" data-render-sidebar=true>
<div class="main__container">
{% include "accounts/components/settings-form.html" with form=form title="User profile" confirm_text="Save" %}
</div>
</main>
{% endblock %}

View file

@ -5,8 +5,6 @@ from django.utils.crypto import get_random_string
import factory
from registration.models import RegistrationProfile
from newsreader.accounts.models import User
@ -29,11 +27,3 @@ class UserFactory(factory.django.DjangoModelFactory):
class Meta:
model = User
class RegistrationProfileFactory(factory.django.DjangoModelFactory):
user = factory.SubFactory(UserFactory)
activation_key = factory.LazyFunction(get_activation_key)
class Meta:
model = RegistrationProfile

View file

@ -1,99 +0,0 @@
import datetime
from django.conf import settings
from django.test import TestCase
from django.urls import reverse
from django.utils.translation import gettext as _
from registration.models import RegistrationProfile
from newsreader.accounts.models import User
class ActivationTestCase(TestCase):
def setUp(self):
self.register_url = reverse("accounts:register")
self.register_success_url = reverse("accounts:register-complete")
self.success_url = reverse("accounts:activate-complete")
def test_activation(self):
data = {
"email": "test@test.com",
"password1": "test12456",
"password2": "test12456",
}
response = self.client.post(self.register_url, data)
self.assertRedirects(response, self.register_success_url)
register_profile = RegistrationProfile.objects.get()
kwargs = {"activation_key": register_profile.activation_key}
response = self.client.get(reverse("accounts:activate", kwargs=kwargs))
self.assertRedirects(response, self.success_url)
def test_expired_key(self):
data = {
"email": "test@test.com",
"password1": "test12456",
"password2": "test12456",
}
response = self.client.post(self.register_url, data)
register_profile = RegistrationProfile.objects.get()
user = register_profile.user
user.date_joined -= datetime.timedelta(days=settings.ACCOUNT_ACTIVATION_DAYS)
user.save()
kwargs = {"activation_key": register_profile.activation_key}
response = self.client.get(reverse("accounts:activate", kwargs=kwargs))
self.assertEqual(200, response.status_code)
self.assertContains(response, _("Account activation failed"))
user.refresh_from_db()
self.assertFalse(user.is_active)
def test_invalid_key(self):
data = {
"email": "test@test.com",
"password1": "test12456",
"password2": "test12456",
}
response = self.client.post(self.register_url, data)
self.assertRedirects(response, self.register_success_url)
kwargs = {"activation_key": "not-a-valid-key"}
response = self.client.get(reverse("accounts:activate", kwargs=kwargs))
self.assertContains(response, _("Account activation failed"))
user = User.objects.get()
self.assertEquals(user.is_active, False)
def test_activated_key(self):
data = {
"email": "test@test.com",
"password1": "test12456",
"password2": "test12456",
}
response = self.client.post(self.register_url, data)
self.assertRedirects(response, self.register_success_url)
register_profile = RegistrationProfile.objects.get()
kwargs = {"activation_key": register_profile.activation_key}
response = self.client.get(reverse("accounts:activate", kwargs=kwargs))
self.assertRedirects(response, self.success_url)
# try this a second time
response = self.client.get(reverse("accounts:activate", kwargs=kwargs))
self.assertRedirects(response, self.success_url)

View file

@ -0,0 +1,37 @@
from unittest.mock import patch
from django.core.cache import cache
from django.test import TestCase
from django.urls import reverse
from newsreader.accounts.tests.factories import UserFactory
class FaviconRedirectViewTestCase(TestCase):
def setUp(self):
self.user = UserFactory(email="test@test.nl", password="test")
self.client.force_login(self.user)
self.patch = patch("newsreader.accounts.views.favicon.FaviconTask")
self.mocked_task = self.patch.start()
def tearDown(self):
cache.clear()
def test_simple(self):
response = self.client.get(reverse("accounts:settings:favicon"))
self.assertRedirects(response, reverse("accounts:settings:home"))
self.mocked_task.delay.assert_called_once_with(self.user.pk)
self.assertEqual(1, cache.get(f"{self.user.email}-favicon-task"))
def test_not_active(self):
cache.set(f"{self.user.email}-favicon-task", 1)
response = self.client.get(reverse("accounts:settings:favicon"))
self.assertRedirects(response, reverse("accounts:settings:home"))
self.mocked_task.delay.assert_not_called()

View file

@ -1,110 +0,0 @@
from django.core import mail
from django.test import TransactionTestCase as TestCase
from django.test.utils import override_settings
from django.urls import reverse
from django.utils.translation import gettext as _
from registration.models import RegistrationProfile
from newsreader.accounts.models import User
from newsreader.accounts.tests.factories import UserFactory
class RegistrationTestCase(TestCase):
def setUp(self):
self.url = reverse("accounts:register")
self.success_url = reverse("accounts:register-complete")
self.disallowed_url = reverse("accounts:register-closed")
def test_simple(self):
response = self.client.get(self.url)
self.assertEquals(response.status_code, 200)
def test_registration(self):
data = {
"email": "test@test.com",
"password1": "test12456",
"password2": "test12456",
}
response = self.client.post(self.url, data)
self.assertRedirects(response, self.success_url)
self.assertEquals(User.objects.count(), 1)
self.assertEquals(RegistrationProfile.objects.count(), 1)
user = User.objects.get()
self.assertEquals(user.is_active, False)
self.assertEquals(len(mail.outbox), 1)
def test_existing_email(self):
UserFactory(email="test@test.com")
data = {
"email": "test@test.com",
"password1": "test12456",
"password2": "test12456",
}
response = self.client.post(self.url, data)
self.assertEquals(response.status_code, 200)
self.assertEquals(User.objects.count(), 1)
self.assertContains(response, _("User with this Email address already exists"))
def test_pending_registration(self):
data = {
"email": "test@test.com",
"password1": "test12456",
"password2": "test12456",
}
response = self.client.post(self.url, data)
self.assertRedirects(response, self.success_url)
self.assertEquals(User.objects.count(), 1)
self.assertEquals(RegistrationProfile.objects.count(), 1)
user = User.objects.get()
self.assertEquals(user.is_active, False)
self.assertEquals(len(mail.outbox), 1)
response = self.client.post(self.url, data)
self.assertEquals(response.status_code, 200)
self.assertContains(response, _("User with this Email address already exists"))
def test_disabled_account(self):
UserFactory(email="test@test.com", is_active=False)
data = {
"email": "test@test.com",
"password1": "test12456",
"password2": "test12456",
}
response = self.client.post(self.url, data)
self.assertEquals(response.status_code, 200)
self.assertEquals(User.objects.count(), 1)
self.assertContains(response, _("User with this Email address already exists"))
@override_settings(REGISTRATION_OPEN=False)
def test_registration_closed(self):
response = self.client.get(self.url)
self.assertRedirects(response, self.disallowed_url)
data = {
"email": "test@test.com",
"password1": "test12456",
"password2": "test12456",
}
response = self.client.post(self.url, data)
self.assertRedirects(response, self.disallowed_url)
self.assertEquals(User.objects.count(), 0)
self.assertEquals(RegistrationProfile.objects.count(), 0)

View file

@ -1,77 +0,0 @@
from django.core import mail
from django.test import TransactionTestCase as TestCase
from django.urls import reverse
from django.utils.translation import gettext as _
from registration.models import RegistrationProfile
from newsreader.accounts.tests.factories import RegistrationProfileFactory, UserFactory
class ResendActivationTestCase(TestCase):
def setUp(self):
self.url = reverse("accounts:activate-resend")
self.success_url = reverse("accounts:activate-complete")
self.register_url = reverse("accounts:register")
def test_simple(self):
response = self.client.get(self.url)
self.assertEquals(response.status_code, 200)
def test_resent_form(self):
data = {
"email": "test@test.com",
"password1": "test12456",
"password2": "test12456",
}
response = self.client.post(self.register_url, data)
register_profile = RegistrationProfile.objects.get()
original_kwargs = {"activation_key": register_profile.activation_key}
response = self.client.post(self.url, {"email": "test@test.com"})
self.assertContains(response, _("We have sent an email to"))
self.assertEquals(len(mail.outbox), 2)
register_profile.refresh_from_db()
kwargs = {"activation_key": register_profile.activation_key}
response = self.client.get(reverse("accounts:activate", kwargs=kwargs))
self.assertRedirects(response, self.success_url)
register_profile.refresh_from_db()
user = register_profile.user
self.assertEquals(user.is_active, True)
# test the old activation code
response = self.client.get(reverse("accounts:activate", kwargs=original_kwargs))
self.assertEquals(response.status_code, 200)
self.assertContains(response, _("Account activation failed"))
def test_existing_account(self):
user = UserFactory(is_active=True)
profile = RegistrationProfileFactory(user=user, activated=True)
response = self.client.post(self.url, {"email": user.email})
self.assertEquals(response.status_code, 200)
# default behaviour is to show success page but not send an email
self.assertContains(response, _("We have sent an email to"))
self.assertEquals(len(mail.outbox), 0)
def test_no_account(self):
response = self.client.post(self.url, {"email": "fake@mail.com"})
self.assertEquals(response.status_code, 200)
# default behaviour is to show success page but not send an email
self.assertContains(response, _("We have sent an email to"))
self.assertEquals(len(mail.outbox), 0)

View file

@ -1,14 +1,8 @@
from unittest.mock import patch
from urllib.parse import urlencode
from uuid import uuid4
from django.core.cache import cache
from django.test import TestCase
from django.urls import reverse
from newsreader.accounts.models import User
from newsreader.accounts.tests.factories import UserFactory
from newsreader.news.collection.exceptions import StreamTooManyException
class SettingsViewTestCase(TestCase):
@ -16,146 +10,22 @@ class SettingsViewTestCase(TestCase):
self.user = UserFactory(email="test@test.nl", password="test")
self.client.force_login(self.user)
self.url = reverse("accounts:settings")
self.url = reverse("accounts:settings:home")
def test_simple(self):
response = self.client.get(self.url)
self.assertEquals(response.status_code, 200)
self.assertContains(response, "Authorize Reddit account")
def test_user_credential_change(self):
response = self.client.post(
reverse("accounts:settings"),
reverse("accounts:settings:home"),
{"first_name": "First name", "last_name": "Last name"},
)
user = User.objects.get()
self.assertRedirects(response, reverse("accounts:settings"))
self.assertRedirects(response, reverse("accounts:settings:home"))
self.assertEquals(user.first_name, "First name")
self.assertEquals(user.last_name, "Last name")
def test_linked_reddit_account(self):
self.user.reddit_refresh_token = "test"
self.user.save()
response = self.client.get(self.url)
self.assertEquals(response.status_code, 200)
self.assertNotContains(response, "Authorize Reddit account")
class RedditTemplateViewTestCase(TestCase):
def setUp(self):
self.user = UserFactory(email="test@test.nl", password="test")
self.client.force_login(self.user)
self.base_url = reverse("accounts:reddit-template")
self.state = str(uuid4())
self.patch = patch("newsreader.news.collection.reddit.post")
self.mocked_post = self.patch.start()
def tearDown(self):
patch.stopall()
def test_simple(self):
response = self.client.get(self.base_url)
self.assertEquals(response.status_code, 200)
self.assertContains(response, "Return to settings page")
def test_successful_authorization(self):
self.mocked_post.return_value.json.return_value = {
"access_token": "1001010412",
"refresh_token": "134510143",
}
cache.set(f"{self.user.email}-reddit-auth", self.state)
params = {"state": self.state, "code": "Valid code"}
url = f"{self.base_url}?{urlencode(params)}"
response = self.client.get(url)
self.mocked_post.assert_called_once()
self.assertEquals(response.status_code, 200)
self.assertContains(response, "Your reddit account was successfully linked.")
self.user.refresh_from_db()
self.assertEquals(self.user.reddit_access_token, "1001010412")
self.assertEquals(self.user.reddit_refresh_token, "134510143")
self.assertEquals(cache.get(f"{self.user.email}-reddit-auth"), None)
def test_error(self):
params = {"error": "Denied authorization"}
url = f"{self.base_url}?{urlencode(params)}"
response = self.client.get(url)
self.assertEquals(response.status_code, 200)
self.assertContains(response, "Denied authorization")
def test_invalid_state(self):
cache.set(f"{self.user.email}-reddit-auth", str(uuid4()))
params = {"code": "Valid code", "state": "Invalid state"}
url = f"{self.base_url}?{urlencode(params)}"
response = self.client.get(url)
self.assertEquals(response.status_code, 200)
self.assertContains(
response, "The saved state for Reddit authorization did not match"
)
def test_stream_error(self):
self.mocked_post.side_effect = StreamTooManyException
cache.set(f"{self.user.email}-reddit-auth", self.state)
params = {"state": self.state, "code": "Valid code"}
url = f"{self.base_url}?{urlencode(params)}"
response = self.client.get(url)
self.mocked_post.assert_called_once()
self.assertEquals(response.status_code, 200)
self.assertContains(response, "Too many requests")
self.user.refresh_from_db()
self.assertEquals(self.user.reddit_access_token, None)
self.assertEquals(self.user.reddit_refresh_token, None)
self.assertEquals(cache.get(f"{self.user.email}-reddit-auth"), self.state)
def test_unexpected_json(self):
self.mocked_post.return_value.json.return_value = {"message": "Happy eastern"}
cache.set(f"{self.user.email}-reddit-auth", self.state)
params = {"state": self.state, "code": "Valid code"}
url = f"{self.base_url}?{urlencode(params)}"
response = self.client.get(url)
self.mocked_post.assert_called_once()
self.assertEquals(response.status_code, 200)
self.assertContains(response, "Access and refresh token not found in response")
self.user.refresh_from_db()
self.assertEquals(self.user.reddit_access_token, None)
self.assertEquals(self.user.reddit_refresh_token, None)
self.assertEquals(cache.get(f"{self.user.email}-reddit-auth"), self.state)

View file

@ -1,22 +1,21 @@
from django.test import TestCase
from django_celery_beat.models import PeriodicTask
from django_celery_beat.models import IntervalSchedule, PeriodicTask
from newsreader.accounts.models import User
from newsreader.accounts.tests.factories import UserFactory
class UserTestCase(TestCase):
def test_task_is_created(self):
user = User.objects.create(email="durp@burp.nl", task=None)
task = PeriodicTask.objects.get(name=f"{user.email}-collection-task")
user.refresh_from_db()
self.assertEquals(task, user.task)
self.assertEquals(PeriodicTask.objects.count(), 1)
def test_task_is_deleted(self):
user = User.objects.create(email="durp@burp.nl", task=None)
user = UserFactory(email="durp@burp.nl")
interval = IntervalSchedule.objects.create(
every=1, period=IntervalSchedule.HOURS
)
PeriodicTask.objects.create(
name=f"{user.email}-feed", task="FeedTask", interval=interval
)
user.delete()
self.assertEquals(PeriodicTask.objects.count(), 0)

View file

@ -1,10 +1,8 @@
from django.contrib.auth.decorators import login_required
from django.urls import path
from django.urls import include, path
from newsreader.accounts.views import (
ActivationCompleteView,
ActivationResendView,
ActivationView,
FaviconRedirectView,
LoginView,
LogoutView,
PasswordChangeView,
@ -12,35 +10,21 @@ from newsreader.accounts.views import (
PasswordResetConfirmView,
PasswordResetDoneView,
PasswordResetView,
RedditTemplateView,
RedditTokenRedirectView,
RegistrationClosedView,
RegistrationCompleteView,
RegistrationView,
SettingsView,
)
settings_patterns = [
# Misc
path("favicon/", login_required(FaviconRedirectView.as_view()), name="favicon"),
path("", login_required(SettingsView.as_view()), name="home"),
]
urlpatterns = [
# Auth
path("login/", LoginView.as_view(), name="login"),
path("logout/", LogoutView.as_view(), name="logout"),
path("register/", RegistrationView.as_view(), name="register"),
path(
"register/complete/",
RegistrationCompleteView.as_view(),
name="register-complete",
),
path("register/closed/", RegistrationClosedView.as_view(), name="register-closed"),
path(
"activate/complete/", ActivationCompleteView.as_view(), name="activate-complete"
),
path("activate/resend/", ActivationResendView.as_view(), name="activate-resend"),
path(
# This URL should be placed after all activate/ url's (see arg)
"activate/<str:activation_key>/",
ActivationView.as_view(),
name="activate",
),
# Password
path("password-reset/", PasswordResetView.as_view(), name="password-reset"),
path(
"password-reset/done/",
@ -62,15 +46,6 @@ urlpatterns = [
login_required(PasswordChangeView.as_view()),
name="password-change",
),
path("settings/", login_required(SettingsView.as_view()), name="settings"),
path(
"settings/reddit/callback/",
login_required(RedditTemplateView.as_view()),
name="reddit-template",
),
path(
"settings/reddit/refresh/",
login_required(RedditTokenRedirectView.as_view()),
name="reddit-refresh",
),
# Settings
path("settings/", include((settings_patterns, "settings"))),
]

View file

@ -1,210 +0,0 @@
from django.contrib import messages
from django.contrib.auth import views as django_views
from django.core.cache import cache
from django.shortcuts import render
from django.urls import reverse_lazy
from django.utils.translation import gettext as _
from django.views.generic import RedirectView, TemplateView
from django.views.generic.edit import FormView, ModelFormMixin
from registration.backends.default import views as registration_views
from newsreader.accounts.forms import UserSettingsForm
from newsreader.accounts.models import User
from newsreader.news.collection.exceptions import StreamException
from newsreader.news.collection.reddit import (
get_reddit_access_token,
get_reddit_authorization_url,
)
from newsreader.news.collection.tasks import RedditTokenTask
class LoginView(django_views.LoginView):
template_name = "accounts/views/login.html"
success_url = reverse_lazy("index")
class LogoutView(django_views.LogoutView):
next_page = reverse_lazy("accounts:login")
# RegistrationView shows a registration form and sends the email
# RegistrationCompleteView shows after filling in the registration form
# ActivationView is send within the activation email and activates the account
# ActivationCompleteView shows the success screen when activation was succesful
# ActivationResendView can be used when activation links are expired
# RegistrationClosedView shows when registration is disabled
class RegistrationView(registration_views.RegistrationView):
disallowed_url = reverse_lazy("accounts:register-closed")
template_name = "registration/registration_form.html"
success_url = reverse_lazy("accounts:register-complete")
class RegistrationCompleteView(TemplateView):
template_name = "registration/registration_complete.html"
class RegistrationClosedView(TemplateView):
template_name = "registration/registration_closed.html"
# Redirects or renders failed activation template
class ActivationView(registration_views.ActivationView):
template_name = "registration/activation_failure.html"
def get_success_url(self, user):
return ("accounts:activate-complete", (), {})
class ActivationCompleteView(TemplateView):
template_name = "registration/activation_complete.html"
# Renders activation form resend or resend_activation_complete
class ActivationResendView(registration_views.ResendActivationView):
template_name = "registration/activation_resend_form.html"
def render_form_submitted_template(self, form):
"""
Renders resend activation complete template with the submitted email.
"""
email = form.cleaned_data["email"]
context = {"email": email}
return render(
self.request, "registration/activation_resend_complete.html", context
)
# PasswordResetView sends the mail
# PasswordResetDoneView shows a success message for the above
# PasswordResetConfirmView checks the link the user clicked and
# prompts for a new password
# PasswordResetCompleteView shows a success message for the above
class PasswordResetView(django_views.PasswordResetView):
template_name = "password-reset/password-reset.html"
subject_template_name = "password-reset/password-reset-subject.txt"
email_template_name = "password-reset/password-reset-email.html"
success_url = reverse_lazy("accounts:password-reset-done")
class PasswordResetDoneView(django_views.PasswordResetDoneView):
template_name = "password-reset/password-reset-done.html"
class PasswordResetConfirmView(django_views.PasswordResetConfirmView):
template_name = "password-reset/password-reset-confirm.html"
success_url = reverse_lazy("accounts:password-reset-complete")
class PasswordResetCompleteView(django_views.PasswordResetCompleteView):
template_name = "password-reset/password-reset-complete.html"
class PasswordChangeView(django_views.PasswordChangeView):
template_name = "accounts/views/password-change.html"
success_url = reverse_lazy("accounts:settings")
class SettingsView(ModelFormMixin, FormView):
template_name = "accounts/views/settings.html"
success_url = reverse_lazy("accounts:settings")
form_class = UserSettingsForm
model = User
def get(self, request, *args, **kwargs):
self.object = self.get_object()
return super().get(request, *args, **kwargs)
def get_object(self, **kwargs):
return self.request.user
def get_context_data(self, **kwargs):
user = self.request.user
reddit_authorization_url = None
reddit_refresh_url = None
reddit_task_active = cache.get(f"{user.email}-reddit-refresh")
if (
user.reddit_refresh_token
and not user.reddit_access_token
and not reddit_task_active
):
reddit_refresh_url = reverse_lazy("accounts:reddit-refresh")
if not user.reddit_refresh_token:
reddit_authorization_url = get_reddit_authorization_url(user)
return {
**super().get_context_data(**kwargs),
"reddit_authorization_url": reddit_authorization_url,
"reddit_refresh_url": reddit_refresh_url,
}
def get_form_kwargs(self):
return {**super().get_form_kwargs(), "instance": self.request.user}
class RedditTemplateView(TemplateView):
template_name = "accounts/views/reddit.html"
def get(self, request, *args, **kwargs):
context = self.get_context_data(**kwargs)
error = request.GET.get("error", None)
state = request.GET.get("state", None)
code = request.GET.get("code", None)
if error:
return self.render_to_response({**context, "error": error})
if not code or not state:
return self.render_to_response(context)
cached_state = cache.get(f"{request.user.email}-reddit-auth")
if state != cached_state:
return self.render_to_response(
{
**context,
"error": "The saved state for Reddit authorization did not match",
}
)
try:
access_token, refresh_token = get_reddit_access_token(code, request.user)
return self.render_to_response(
{
**context,
"access_token": access_token,
"refresh_token": refresh_token,
}
)
except StreamException as e:
return self.render_to_response({**context, "error": str(e)})
except KeyError:
return self.render_to_response(
{**context, "error": "Access and refresh token not found in response"}
)
class RedditTokenRedirectView(RedirectView):
url = reverse_lazy("accounts:settings")
def get(self, request, *args, **kwargs):
response = super().get(request, *args, **kwargs)
user = request.user
task_active = cache.get(f"{user.email}-reddit-refresh")
if not task_active:
RedditTokenTask.delay(user.pk)
messages.success(request, _("Access token is being retrieved"))
cache.set(f"{user.email}-reddit-refresh", 1, 300)
return response
messages.error(request, _("Unable to retrieve token"))
return response

View file

@ -0,0 +1,23 @@
from newsreader.accounts.views.auth import LoginView, LogoutView
from newsreader.accounts.views.favicon import FaviconRedirectView
from newsreader.accounts.views.password import (
PasswordChangeView,
PasswordResetCompleteView,
PasswordResetConfirmView,
PasswordResetDoneView,
PasswordResetView,
)
from newsreader.accounts.views.settings import SettingsView
__all__ = [
"LoginView",
"LogoutView",
"FaviconRedirectView",
"PasswordChangeView",
"PasswordResetCompleteView",
"PasswordResetConfirmView",
"PasswordResetDoneView",
"PasswordResetView",
"SettingsView",
]

View file

@ -0,0 +1,13 @@
from django.contrib.auth import views as django_views
from django.urls import reverse_lazy
from newsreader.utils.views import NavListMixin
class LoginView(NavListMixin, django_views.LoginView):
template_name = "accounts/views/login.html"
success_url = reverse_lazy("index")
class LogoutView(django_views.LogoutView):
next_page = reverse_lazy("accounts:login")

View file

@ -0,0 +1,26 @@
from django.contrib import messages
from django.core.cache import cache
from django.urls import reverse_lazy
from django.utils.translation import gettext as _
from django.views.generic import RedirectView
from newsreader.news.collection.tasks import FaviconTask
class FaviconRedirectView(RedirectView):
url = reverse_lazy("accounts:settings:home")
def get(self, request, *args, **kwargs):
response = super().get(request, *args, **kwargs)
user = request.user
task_active = cache.get(f"{user.email}-favicon-task")
if not task_active:
FaviconTask.delay(user.pk)
messages.success(request, _("Favicons are being fetched"))
cache.set(f"{user.email}-favicon-task", 1, 18000) # 5 hours
return response
messages.error(request, _("Limit reached, try again later"))
return response

View file

@ -0,0 +1,34 @@
from django.contrib.auth import views as django_views
from django.urls import reverse_lazy
from newsreader.utils.views import NavListMixin
# PasswordResetView sends the mail
# PasswordResetDoneView shows a success message for the above
# PasswordResetConfirmView checks the link the user clicked and
# prompts for a new password
# PasswordResetCompleteView shows a success message for the above
class PasswordResetView(NavListMixin, django_views.PasswordResetView):
template_name = "password-reset/password-reset.html"
subject_template_name = "password-reset/password-reset-subject.txt"
email_template_name = "password-reset/password-reset-email.html"
success_url = reverse_lazy("accounts:password-reset-done")
class PasswordResetDoneView(NavListMixin, django_views.PasswordResetDoneView):
template_name = "password-reset/password-reset-done.html"
class PasswordResetConfirmView(NavListMixin, django_views.PasswordResetConfirmView):
template_name = "password-reset/password-reset-confirm.html"
success_url = reverse_lazy("accounts:password-reset-complete")
class PasswordResetCompleteView(NavListMixin, django_views.PasswordResetCompleteView):
template_name = "password-reset/password-reset-complete.html"
class PasswordChangeView(NavListMixin, django_views.PasswordChangeView):
template_name = "accounts/views/password-change.html"
success_url = reverse_lazy("accounts:settings")

View file

@ -0,0 +1,32 @@
from django.core.cache import cache
from django.urls import reverse_lazy
from django.views.generic.edit import FormView, ModelFormMixin
from newsreader.accounts.forms import UserSettingsForm
from newsreader.accounts.models import User
from newsreader.utils.views import NavListMixin
class SettingsView(NavListMixin, ModelFormMixin, FormView):
template_name = "accounts/views/settings.html"
success_url = reverse_lazy("accounts:settings:home")
form_class = UserSettingsForm
model = User
def get(self, request, *args, **kwargs):
self.object = self.get_object()
return super().get(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
return {
**context,
"favicon_task_allowed": not cache.get(f"{self.request.user}-favicon-task"),
}
def get_object(self, **kwargs):
return self.request.user
def get_form_kwargs(self):
return {**super().get_form_kwargs(), "instance": self.request.user}

View file

@ -1,101 +0,0 @@
name: "Rubik"
designer: "Hubert and Fischer, Meir Sadan, Cyreal"
license: "OFL"
category: "SANS_SERIF"
date_added: "2015-07-22"
fonts {
name: "Rubik"
style: "normal"
weight: 300
filename: "Rubik-Light.ttf"
post_script_name: "Rubik-Light"
full_name: "Rubik Light"
copyright: "Copyright 2015 The Rubik Project Authors (https://github.com/googlefonts/rubik)"
}
fonts {
name: "Rubik"
style: "italic"
weight: 300
filename: "Rubik-LightItalic.ttf"
post_script_name: "Rubik-LightItalic"
full_name: "Rubik Light Italic"
copyright: "Copyright 2015 The Rubik Project Authors (https://github.com/googlefonts/rubik)"
}
fonts {
name: "Rubik"
style: "normal"
weight: 400
filename: "Rubik-Regular.ttf"
post_script_name: "Rubik-Regular"
full_name: "Rubik Regular"
copyright: "Copyright 2015 The Rubik Project Authors (https://github.com/googlefonts/rubik)"
}
fonts {
name: "Rubik"
style: "italic"
weight: 400
filename: "Rubik-Italic.ttf"
post_script_name: "Rubik-Italic"
full_name: "Rubik Italic"
copyright: "Copyright 2015 The Rubik Project Authors (https://github.com/googlefonts/rubik)"
}
fonts {
name: "Rubik"
style: "normal"
weight: 500
filename: "Rubik-Medium.ttf"
post_script_name: "Rubik-Medium"
full_name: "Rubik Medium"
copyright: "Copyright 2015 The Rubik Project Authors (https://github.com/googlefonts/rubik)"
}
fonts {
name: "Rubik"
style: "italic"
weight: 500
filename: "Rubik-MediumItalic.ttf"
post_script_name: "Rubik-MediumItalic"
full_name: "Rubik Medium Italic"
copyright: "Copyright 2015 The Rubik Project Authors (https://github.com/googlefonts/rubik)"
}
fonts {
name: "Rubik"
style: "normal"
weight: 700
filename: "Rubik-Bold.ttf"
post_script_name: "Rubik-Bold"
full_name: "Rubik Bold"
copyright: "Copyright 2015 The Rubik Project Authors (https://github.com/googlefonts/rubik)"
}
fonts {
name: "Rubik"
style: "italic"
weight: 700
filename: "Rubik-BoldItalic.ttf"
post_script_name: "Rubik-BoldItalic"
full_name: "Rubik Bold Italic"
copyright: "Copyright 2015 The Rubik Project Authors (https://github.com/googlefonts/rubik)"
}
fonts {
name: "Rubik"
style: "normal"
weight: 900
filename: "Rubik-Black.ttf"
post_script_name: "Rubik-Black"
full_name: "Rubik Black"
copyright: "Copyright 2015 The Rubik Project Authors (https://github.com/googlefonts/rubik)"
}
fonts {
name: "Rubik"
style: "italic"
weight: 900
filename: "Rubik-BlackItalic.ttf"
post_script_name: "Rubik-BlackItalic"
full_name: "Rubik Black Italic"
copyright: "Copyright 2015 The Rubik Project Authors (https://github.com/googlefonts/rubik)"
}
subsets: "cyrillic"
subsets: "cyrillic-ext"
subsets: "hebrew"
subsets: "latin"
subsets: "latin-ext"
subsets: "menu"

View file

@ -3,7 +3,7 @@ import os
from celery import Celery
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "newsreader.conf.dev")
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "newsreader.conf.docker")
app = Celery("newsreader")
app.config_from_object("django.conf:settings", namespace="CELERY")

View file

@ -1,20 +1,25 @@
import os
from dotenv import load_dotenv
from pathlib import Path
from .version import get_current_version
from newsreader.conf.utils import get_env, get_root_dir
BASE_DIR = Path(__file__).resolve().parent.parent.parent.parent
DJANGO_PROJECT_DIR = os.path.join(BASE_DIR, "src", "newsreader")
load_dotenv()
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/
# SECURITY WARNING: don"t run with debug turned on in production!
DEBUG = True
try:
from sentry_sdk.integrations.celery import CeleryIntegration
from sentry_sdk.integrations.django import DjangoIntegration
except ImportError:
CeleryIntegration = None
DjangoIntegration = None
ALLOWED_HOSTS = ["127.0.0.1", "localhost"]
INTERNAL_IPS = ["127.0.0.1", "localhost"]
BASE_DIR = get_root_dir()
DJANGO_PROJECT_DIR = BASE_DIR / "src" / "newsreader"
DEBUG = False
ALLOWED_HOSTS = get_env("ALLOWED_HOSTS", split=",", default=["127.0.0.1", "localhost"])
INTERNAL_IPS = get_env("INTERNAL_IPS", split=",", default=["127.0.0.1", "localhost"])
# Application definition
INSTALLED_APPS = [
@ -27,10 +32,8 @@ INSTALLED_APPS = [
"django.forms",
# third party apps
"rest_framework",
"drf_yasg",
"celery",
"django_celery_beat",
"registration",
"axes",
# app modules
"newsreader.accounts",
@ -40,6 +43,8 @@ INSTALLED_APPS = [
"newsreader.news.collection",
]
SECRET_KEY = get_env("DJANGO_SECRET_KEY", default="")
AUTHENTICATION_BACKENDS = [
"axes.backends.AxesBackend",
"django.contrib.auth.backends.ModelBackend",
@ -63,11 +68,10 @@ FORM_RENDERER = "django.forms.renderers.TemplatesSetting"
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [os.path.join(DJANGO_PROJECT_DIR, "templates")],
"DIRS": [DJANGO_PROJECT_DIR / "templates"],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
@ -78,31 +82,30 @@ TEMPLATES = [
WSGI_APPLICATION = "newsreader.wsgi.application"
# Database
# https://docs.djangoproject.com/en/2.2/ref/settings/#databases
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"HOST": os.environ.get("POSTGRES_HOST", ""),
"NAME": os.environ.get("POSTGRES_NAME", "newsreader"),
"USER": os.environ.get("POSTGRES_USER"),
"PASSWORD": os.environ.get("POSTGRES_PASSWORD"),
"HOST": get_env("POSTGRES_HOST", default=""),
"PORT": get_env("POSTGRES_PORT", default=""),
"NAME": get_env("POSTGRES_DB", default=""),
"USER": get_env("POSTGRES_USER", default=""),
"PASSWORD": get_env("POSTGRES_PASSWORD", default=""),
}
}
DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.memcached.MemcachedCache",
"LOCATION": "localhost:11211",
"BACKEND": "django.core.cache.backends.memcached.PyMemcacheCache",
"LOCATION": "memcached:11211",
},
"axes": {
"BACKEND": "django.core.cache.backends.memcached.MemcachedCache",
"LOCATION": "localhost:11211",
"BACKEND": "django.core.cache.backends.memcached.PyMemcacheCache",
"LOCATION": "memcached:11211",
},
}
# Logging
# https://docs.djangoproject.com/en/2.2/topics/logging/#configuring-logging
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
@ -116,66 +119,46 @@ LOGGING = {
"format": "[{server_time}] {message}",
"style": "{",
},
"syslog": {
"class": "logging.Formatter",
"format": "[newsreader] {message}",
"style": "{",
},
},
"handlers": {
"console": {
"level": "INFO",
"filters": ["require_debug_true"],
"class": "logging.StreamHandler",
"formatter": "timestamped",
},
"mail_admins": {
"level": "ERROR",
"filters": ["require_debug_false"],
"class": "django.utils.log.AdminEmailHandler",
"file": {
"level": "DEBUG",
"class": "logging.handlers.RotatingFileHandler",
"filename": BASE_DIR / "logs" / "newsreader.log",
"backupCount": 5,
"maxBytes": 50000000, # 50 mB
"formatter": "timestamped",
},
"syslog": {
"celery": {
"level": "INFO",
"filters": ["require_debug_false"],
"class": "logging.handlers.SysLogHandler",
"formatter": "syslog",
"address": "/dev/log",
},
"syslog_errors": {
"level": "ERROR",
"filters": ["require_debug_false"],
"class": "logging.handlers.SysLogHandler",
"formatter": "syslog",
"address": "/dev/log",
"class": "logging.handlers.RotatingFileHandler",
"filename": BASE_DIR / "logs" / "celery.log",
"backupCount": 5,
"maxBytes": 50000000, # 50 mB
"formatter": "timestamped",
},
},
"loggers": {
"django": {
"handlers": ["console", "mail_admins", "syslog_errors"],
"level": "WARNING",
},
"django": {"handlers": ["console"], "level": "INFO"},
"django.server": {
"handlers": ["console", "syslog_errors"],
"handlers": ["console"],
"level": "INFO",
"propagate": False,
},
"django.request": {
"handlers": ["console", "syslog_errors"],
"level": "INFO",
"celery.task": {"handlers": ["console", "celery"], "level": "INFO"},
"newsreader": {
"handlers": ["console", "file"],
"level": "DEBUG",
"propagate": False,
},
"celery": {"handlers": ["syslog", "console"], "level": "INFO"},
"celery.task": {
"handlers": ["syslog", "console"],
"level": "INFO",
"propagate": False,
},
"newsreader": {"handlers": ["syslog", "console"], "level": "INFO"},
},
}
# Password validation
# https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"
@ -190,8 +173,6 @@ AUTH_USER_MODEL = "accounts.User"
LOGIN_REDIRECT_URL = "/"
# Internationalization
# https://docs.djangoproject.com/en/2.2/topics/i18n/
LANGUAGE_CODE = "en-us"
TIME_ZONE = "Europe/Amsterdam"
@ -199,27 +180,31 @@ USE_I18N = True
USE_L10N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/2.2/howto/static-files/
STATIC_URL = "/static/"
STATIC_ROOT = os.path.join(BASE_DIR, "static")
STATICFILES_DIRS = [os.path.join(DJANGO_PROJECT_DIR, "static")]
STATIC_ROOT = BASE_DIR / "static"
STATICFILES_DIRS = (DJANGO_PROJECT_DIR / "static",)
# https://docs.djangoproject.com/en/2.2/ref/settings/#std:setting-STATICFILES_FINDERS
STATICFILES_FINDERS = [
"django.contrib.staticfiles.finders.FileSystemFinder",
"django.contrib.staticfiles.finders.AppDirectoriesFinder",
]
DEFAULT_FROM_EMAIL = "newsreader@rss.fudiggity.nl"
# Email
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
# Project settings
VERSION = get_current_version()
DEFAULT_FROM_EMAIL = get_env(
"EMAIL_DEFAULT_FROM", required=False, default="webmaster@localhost"
)
EMAIL_HOST = get_env("EMAIL_HOST", required=False, default="localhost")
EMAIL_PORT = get_env("EMAIL_PORT", cast=int, required=False, default=25)
EMAIL_HOST_USER = get_env("EMAIL_HOST_USER", required=False, default="")
EMAIL_HOST_PASSWORD = get_env("EMAIL_HOST_PASSWORD", required=False, default="")
EMAIL_USE_TLS = get_env("EMAIL_USE_TLS", required=False, default=False)
EMAIL_USE_SSL = get_env("EMAIL_USE_SSL", required=False, default=False)
# Reddit integration
REDDIT_CLIENT_ID = "CLIENT_ID"
REDDIT_CLIENT_SECRET = "CLIENT_SECRET"
REDDIT_REDIRECT_URL = "http://127.0.0.1:8000/accounts/settings/reddit/callback/"
# Third party settings
AXES_HANDLER = "axes.handlers.cache.AxesCacheHandler"
@ -236,7 +221,12 @@ REST_FRAMEWORK = {
"rest_framework.permissions.IsAuthenticated",
"newsreader.accounts.permissions.IsOwner",
),
"DEFAULT_RENDERER_CLASSES": ("rest_framework.renderers.JSONRenderer",),
"DEFAULT_RENDERER_CLASSES": (
"djangorestframework_camel_case.render.CamelCaseJSONRenderer",
),
"DEFAULT_PARSER_CLASSES": (
"djangorestframework_camel_case.parser.CamelCaseJSONParser",
),
}
SWAGGER_SETTINGS = {
@ -247,8 +237,15 @@ SWAGGER_SETTINGS = {
# Celery
# https://docs.celeryproject.org/en/stable/userguide/configuration.html
# Note that celery settings are prefix with CELERY. See src/newsreader/celery.py.
CELERY_WORKER_HIJACK_ROOT_LOGGER = False
CELERY_BROKER_URL = "amqp://guest@rabbitmq:5672"
REGISTRATION_OPEN = True
REGISTRATION_AUTO_LOGIN = True
ACCOUNT_ACTIVATION_DAYS = 7
# Sentry
SENTRY_CONFIG = {
"dsn": get_env("SENTRY_DSN", default="", required=False),
"send_default_pii": False,
"integrations": [DjangoIntegration(), CeleryIntegration()]
if DjangoIntegration and CeleryIntegration
else [],
}

46
src/newsreader/conf/ci.py Normal file
View file

@ -0,0 +1,46 @@
from .base import * # noqa: F403
from .utils import get_current_version
DEBUG = True
del LOGGING["handlers"]["file"] # noqa: F405
del LOGGING["handlers"]["celery"] # noqa: F405
LOGGING["loggers"].update( # noqa: F405
{
"celery.task": {"handlers": ["console"], "level": "DEBUG"},
"newsreader": {"handlers": ["console"], "level": "INFO"},
}
)
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
AXES_ENABLED = False
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.memcached.PyMemcacheCache",
"LOCATION": "memcached:11211",
},
"axes": {
"BACKEND": "django.core.cache.backends.memcached.PyMemcacheCache",
"LOCATION": "memcached:11211",
},
}
# Project settings
VERSION = get_current_version()
ENVIRONMENT = "ci"
try:
# Optionally use sentry integration
from sentry_sdk import init as sentry_init
SENTRY_CONFIG.update({"release": VERSION, "environment": ENVIRONMENT}) # noqa: F405
sentry_init(**SENTRY_CONFIG) # noqa: F405
except ImportError:
pass

View file

@ -1,19 +1,35 @@
from .base import * # isort:skip
from .base import * # noqa: F403
from .utils import get_current_version
SECRET_KEY = "mv4&5#+)-=abz3^&1r^nk_ca6y54--p(4n4cg%z*g&rb64j%wl"
MIDDLEWARE += ["debug_toolbar.middleware.DebugToolbarMiddleware"]
MIDDLEWARE += ["debug_toolbar.middleware.DebugToolbarMiddleware"] # noqa: F405
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
INSTALLED_APPS += ["debug_toolbar", "django_extensions"]
INSTALLED_APPS += ["debug_toolbar", "django_extensions"] # noqa: F405
TEMPLATES[0]["OPTIONS"]["context_processors"].append( # noqa: F405
"django.template.context_processors.debug",
)
# Project settings
VERSION = get_current_version()
# Third party settings
AXES_FAILURE_LIMIT = 50
AXES_COOLOFF_TIME = None
try:
# Optionally use sentry integration
from sentry_sdk import init as sentry_init
from .local import * # noqa
SENTRY_CONFIG.update({"release": VERSION}) # noqa: F405
sentry_init(**SENTRY_CONFIG) # noqa: F405
except ImportError:
pass

View file

@ -1,29 +1,43 @@
from .dev import * # isort:skip
from .base import * # noqa: F403
from .utils import get_current_version
SECRET_KEY = "=q(ztyo)b6noom#a164g&s9vcj1aawa^g#ing_ir99=_zl4g&$"
DEBUG = True
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": "newsreader",
"USER": "newsreader",
"PASSWORD": "newsreader",
"HOST": "db",
INSTALLED_APPS += ["debug_toolbar", "django_extensions"] # noqa: F405
MIDDLEWARE += ["debug_toolbar.middleware.DebugToolbarMiddleware"] # noqa: F405
LOGGING["loggers"].update( # noqa: F405
{
"celery.task": {"handlers": ["console", "celery"], "level": "DEBUG"},
}
}
)
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.memcached.MemcachedCache",
"LOCATION": "memcached:11211",
},
"axes": {
"BACKEND": "django.core.cache.backends.memcached.MemcachedCache",
"LOCATION": "memcached:11211",
},
}
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
# Celery
# https://docs.celeryproject.org/en/latest/userguide/configuration.html
CELERY_BROKER_URL = "amqp://guest:guest@rabbitmq:5672//"
TEMPLATES[0]["OPTIONS"]["context_processors"].append( # noqa: F405
"django.template.context_processors.debug",
)
# Project settings
VERSION = get_current_version()
ENVIRONMENT = "docker"
# Third party settings
# Axes
AXES_FAILURE_LIMIT = 50
AXES_COOLOFF_TIME = None
try:
# Optionally use sentry integration
from sentry_sdk import init as sentry_init
from .local import * # noqa
SENTRY_CONFIG.update({"release": VERSION, "environment": ENVIRONMENT}) # noqa: F405
sentry_init(**SENTRY_CONFIG) # noqa: F405
except ImportError:
pass

View file

@ -1,19 +0,0 @@
from .base import * # isort:skip
SECRET_KEY = "29%lkw+&n%^w4k#@_db2mo%*tc&xzb)x7xuq*(0$eucii%4r0c"
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
AXES_ENABLED = False
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.memcached.MemcachedCache",
"LOCATION": "memcached:11211",
},
"axes": {
"BACKEND": "django.core.cache.backends.memcached.MemcachedCache",
"LOCATION": "memcached:11211",
},
}

View file

@ -1,71 +1,32 @@
import os
from newsreader.conf.utils import get_env
from dotenv import load_dotenv
from .base import * # noqa: F403
from .utils import get_current_version
from .base import * # isort:skip
load_dotenv()
DEBUG = False
ALLOWED_HOSTS = ["rss.fudiggity.nl"]
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
ADMINS = [
("", email)
for email in os.getenv("ADMINS", "").split(",")
if os.environ.get("ADMINS")
("", email) for email in get_env("ADMINS", split=",", required=False, default=[])
]
SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"HOST": os.environ["POSTGRES_HOST"],
"PORT": os.environ["POSTGRES_PORT"],
"NAME": os.environ["POSTGRES_NAME"],
"USER": os.environ["POSTGRES_USER"],
"PASSWORD": os.environ["POSTGRES_PASSWORD"],
}
}
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [os.path.join(DJANGO_PROJECT_DIR, "templates")],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
]
},
}
]
# Reddit integration
REDDIT_CLIENT_ID = os.environ["REDDIT_CLIENT_ID"]
REDDIT_CLIENT_SECRET = os.environ["REDDIT_CLIENT_SECRET"]
REDDIT_REDIRECT_URL = os.environ["REDDIT_CALLBACK_URL"]
# Project settings
VERSION = get_current_version(debug=False)
ENVIRONMENT = "production"
# Third party settings
AXES_HANDLER = "axes.handlers.database.AxesDatabaseHandler"
REGISTRATION_OPEN = False
# Optionally use sentry integration
try:
from sentry_sdk import init as sentry_init
from sentry_sdk.integrations.celery import CeleryIntegration
from sentry_sdk.integrations.django import DjangoIntegration
sentry_init(
dsn=os.environ.get("SENTRY_DSN"),
integrations=[DjangoIntegration(), CeleryIntegration()],
send_default_pii=False,
release=VERSION,
SENTRY_CONFIG.update( # noqa: F405
{"release": VERSION, "environment": ENVIRONMENT, "debug": False}
)
sentry_init(**SENTRY_CONFIG) # noqa: F405
except ImportError:
pass

View file

@ -0,0 +1,85 @@
import logging
import os
import subprocess
from pathlib import Path
from typing import Any, Iterable, Type
logger = logging.getLogger(__name__)
def get_env(
name: str,
cast: Type = str,
required: bool = True,
default: Any = None,
split: str = "",
) -> Any:
if cast is not str and split:
raise TypeError(f"Split is not possible with {cast}")
value = os.getenv(name)
if not value:
if required:
logger.warning(f"Missing environment variable: {name}")
return default
bool_mapping = {"yes": True, "true": True, "false": False, "no": False}
if cast is bool:
_value = bool_mapping.get(value.lower())
if not value:
raise ValueError(f"Unknown boolean value: {_value}")
return _value
value = value if not cast else cast(value)
return value if not split else value.split(split)
def get_current_version(debug: bool = True) -> str:
version = get_env("VERSION", required=False)
if version:
return version
if debug:
try:
output = subprocess.check_output(
["git", "show", "--no-patch", "--format=%H"], universal_newlines=True
)
return output.strip()
except (subprocess.CalledProcessError, OSError):
return ""
try:
output = subprocess.check_output(
["git", "describe", "--tags"], universal_newlines=True
)
return output.strip()
except (subprocess.CalledProcessError, OSError):
return ""
ROOT_MARKERS = ("pyproject.toml", "package.json", "README.md", "CHANGELOG.md")
def get_root_dir() -> Path:
file = Path(__file__)
return _traverse_dirs(file.parent, ROOT_MARKERS)
def _traverse_dirs(path: Path, root_markers: Iterable[str]) -> Path:
if path.parent == path:
raise OSError("Root directory detected")
files = (file.name for file in path.iterdir())
if not any((marker for marker in root_markers if marker in files)):
return _traverse_dirs(path.parent, root_markers)
return path

View file

@ -1,15 +0,0 @@
import os
import subprocess
def get_current_version():
if "VERSION" in os.environ:
return os.environ["VERSION"]
try:
output = subprocess.check_output(
["git", "describe", "--tags"], universal_newlines=True
)
return output.strip()
except (subprocess.CalledProcessError, OSError):
return ""

View file

@ -2,4 +2,4 @@ from django.apps import AppConfig
class CoreConfig(AppConfig):
name = "core"
name = "newsreader.core"

View file

@ -1,7 +1,7 @@
from rest_framework.pagination import PageNumberPagination
from rest_framework import pagination
class ResultSetPagination(PageNumberPagination):
class ResultSetPagination(pagination.PageNumberPagination):
page_size_query_param = "count"
max_page_size = 50
page_size = 30
@ -10,3 +10,9 @@ class ResultSetPagination(PageNumberPagination):
class LargeResultSetPagination(ResultSetPagination):
max_page_size = 100
page_size = 50
class CursorPagination(pagination.CursorPagination):
page_size_query_param = "count"
ordering = "-publication_date"
page_size = 30

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -47,7 +47,7 @@
"user" : 2,
"succeeded" : true,
"modified" : "2019-07-20T11:28:16.473Z",
"last_suceeded" : "2019-07-20T11:28:16.316Z",
"last_run" : "2019-07-20T11:28:16.316Z",
"name" : "Hackers News",
"website_url" : null,
"created" : "2019-07-14T13:08:10.374Z",
@ -65,7 +65,7 @@
"error" : null,
"user" : 2,
"succeeded" : true,
"last_suceeded" : "2019-07-20T11:28:15.691Z",
"last_run" : "2019-07-20T11:28:15.691Z",
"name" : "BBC",
"modified" : "2019-07-20T12:07:49.164Z",
"timezone" : "UTC",
@ -85,7 +85,7 @@
"website_url" : null,
"name" : "Ars Technica",
"succeeded" : true,
"last_suceeded" : "2019-07-20T11:28:15.986Z",
"last_run" : "2019-07-20T11:28:15.986Z",
"modified" : "2019-07-20T11:28:16.033Z",
"user" : 2
},
@ -102,7 +102,7 @@
"user" : 2,
"name" : "The Guardian",
"succeeded" : true,
"last_suceeded" : "2019-07-20T11:28:16.078Z",
"last_run" : "2019-07-20T11:28:16.078Z",
"modified" : "2019-07-20T12:07:44.292Z",
"created" : "2019-07-20T11:25:02.089Z",
"website_url" : null,
@ -119,7 +119,7 @@
"website_url" : null,
"created" : "2019-07-20T11:25:30.121Z",
"user" : 2,
"last_suceeded" : "2019-07-20T11:28:15.860Z",
"last_run" : "2019-07-20T11:28:15.860Z",
"succeeded" : true,
"modified" : "2019-07-20T12:07:28.473Z",
"name" : "Tweakers"
@ -139,7 +139,7 @@
"website_url" : null,
"timezone" : "UTC",
"user" : 2,
"last_suceeded" : "2019-07-20T11:28:16.034Z",
"last_run" : "2019-07-20T11:28:16.034Z",
"succeeded" : true,
"modified" : "2019-07-20T12:07:21.704Z",
"name" : "The Verge"

Some files were not shown because too many files have changed in this diff Show more