Compare commits

...

445 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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
d228dc5f45 Merge branch 'master' into development 2020-09-27 16:25:41 +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
84802fd48b Merge branch 'master' into development 2020-09-02 21:10:53 +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
af7fbaf1e8 Merge branch 'master' into development 2020-08-31 22:49:14 +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
313 changed files with 13265 additions and 35992 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

2
.gitignore vendored
View file

@ -115,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

View file

@ -1,5 +1,53 @@
# 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

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,62 +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.6
ports:
- "11211:11211"
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
- memcached
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
- memcached
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 --extras sentry
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,22 +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/ansible/newsreader.git deployment --branch master
- cd deployment
- ansible-galaxy install -r requirements.yml
- mkdir /root/.ssh && echo "$DEPLOY_HOST_KEY" > /root/.ssh/known_hosts
- echo "$DEPLOY_KEY" > deploy_key && chmod 0600 deploy_key
- echo "$VAULT_PASSWORD" > vault
script:
- >
ansible-playbook playbook.yml
--private-key deploy_key
--vault-password-file vault
--extra-vars "app_branch=$CI_COMMIT_TAG"

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: './CHANGELOG.md'
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 --extras sentry
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,
};

18061
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,20 +1,19 @@
{
"name": "newsreader",
"version": "0.3.13.8",
"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",
@ -32,32 +31,45 @@
"@babel/core": "^7.12.13",
"@babel/plugin-proposal-class-properties": "^7.12.13",
"@babel/plugin-proposal-function-bind": "^7.12.13",
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/plugin-syntax-function-bind": "^7.12.13",
"@babel/plugin-transform-react-jsx": "^7.12.13",
"@babel/plugin-transform-runtime": "^7.12.15",
"@babel/preset-env": "^7.12.13",
"@babel/register": "^7.12.13",
"@babel/runtime": "^7.12.13",
"babel-jest": "^24.9.0",
"babel-jest": "^29.7.0",
"babel-loader": "^8.2.2",
"clean-webpack-plugin": "^3.0.0",
"css-loader": "^3.6.0",
"css-loader": "^7.1.2",
"fetch-mock": "^8.3.2",
"file-loader": "^6.2.0",
"jest": "^24.9.0",
"mini-css-extract-plugin": "^0.9.0",
"jest": "^29.7.0",
"mini-css-extract-plugin": "^2.9.1",
"node-fetch": "^2.6.1",
"node-sass": "^4.14.1",
"prettier": "^1.19.1",
"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.3.0",
"style-loader": "^2.0.0",
"url-loader": "^4.1.1",
"webpack": "^4.46.0",
"webpack-cli": "^3.3.12",
"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"
}
}

1298
poetry.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,46 +1,81 @@
[tool.poetry]
[project]
name = "newsreader"
version = "0.3.13.8"
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.2"
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"
sentry-sdk = {version = "^1.0.0", optional = true}
ftfy = "^5.8"
requests_oauthlib = "^1.3.0"
[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.extras]
sentry = ["sentry_sdk"]
[project.optional-dependencies]
sentry = ["sentry-sdk~=2.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"
[tool.uv]
environments = ["sys_platform == 'linux'"]
default-groups = ["test-tools"]
[build-system]
requires = ["poetry>=1.0.10"]
build-backend = "poetry.masonry.api"
[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,18 +11,6 @@ class UserAdminForm(UserChangeForm):
class Meta:
widgets = {
"email": forms.EmailInput(attrs={"size": "50"}),
"reddit_access_token": forms.PasswordInput(
attrs={"size": "90"}, render_value=True
),
"reddit_refresh_token": forms.PasswordInput(
attrs={"size": "90"}, render_value=True
),
"twitter_oauth_token": forms.PasswordInput(
attrs={"size": "90"}, render_value=True
),
"twitter_oauth_token_secret": forms.PasswordInput(
attrs={"size": "90"}, render_value=True
),
}
@ -40,14 +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")},
),
(
_("Twitter settings"),
{"fields": ("twitter_oauth_token", "twitter_oauth_token_secret")},
),
(
_("Permission settings"),
{"classes": ("collapse",), "fields": ("is_staff", "is_superuser")},

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

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

View file

@ -4,7 +4,6 @@ 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

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

View file

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

View file

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

View file

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

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

@ -39,14 +39,6 @@ class UserManager(DjangoUserManager):
class User(AbstractUser):
email = models.EmailField(_("email address"), unique=True)
# reddit settings
reddit_refresh_token = models.CharField(max_length=255, blank=True, null=True)
reddit_access_token = models.CharField(max_length=255, blank=True, null=True)
# twitter settings
twitter_oauth_token = models.CharField(max_length=255, blank=True, null=True)
twitter_oauth_token_secret = models.CharField(max_length=255, blank=True, null=True)
# settings
auto_mark_read = models.BooleanField(
_("Auto read marking"),
@ -68,7 +60,3 @@ class User(AbstractUser):
tasks.delete()
return super().delete(*args, **kwargs)
@property
def has_twitter_auth(self):
return self.twitter_oauth_token and self.twitter_oauth_token_secret

View file

@ -2,27 +2,23 @@
{% load i18n %}
{% block actions %}
<section class="section form__section--last">
<fieldset class="fieldset form__fieldset">
{% include "components/form/confirm-button.html" %}
<section class="section form__section--last">
<fieldset class="fieldset form__fieldset">
{% include "components/form/confirm-button.html" %}
<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>
{% 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 %}
<a class="link button button--primary" href="{% url 'accounts:settings:integrations' %}">
{% trans "Third party integrations" %}
</a>
</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,70 +0,0 @@
{% extends "base.html" %}
{% load i18n %}
{% block content %}
<main id="integrations--page" class="main">
<section class="section">
{% include "components/header/header.html" with title="Integrations" only %}
<div class="integrations">
<h3 class="integrations__title">Reddit</h3>
<div class="integrations__controls">
{% if reddit_authorization_url %}
<a class="link button button--reddit" href="{{ reddit_authorization_url }}">
{% trans "Authorize account" %}
</a>
{% else %}
<button class="button button--reddit button--disabled" disabled>
{% trans "Authorize account" %}
</button>
{% endif %}
{% if reddit_refresh_url %}
<a class="link button button--reddit" href="{{ reddit_refresh_url }}">
{% trans "Refresh token" %}
</a>
{% else %}
<button class="button button--reddit button--disabled" disabled>
{% trans "Refresh token" %}
</button>
{% endif %}
{% if reddit_revoke_url %}
<a class="link button button--reddit" href="{{ reddit_revoke_url }}">
{% trans "Deauthorize account" %}
</a>
{% else %}
<button class="button button--reddit button--disabled" disabled>
{% trans "Deauthorize account" %}
</button>
{% endif %}
</div>
</div>
<div class="integrations">
<h3 class="integrations__title">Twitter</h3>
<div class="integrations__controls">
{% if twitter_auth_url %}
<a class="link button button--twitter" href="{{ twitter_auth_url }}">
{% trans "Authorize account" %}
</a>
{% else %}
<button class="button button--twitter button--disabled" disabled>
{% trans "Authorize account" %}
</button>
{% endif %}
{% if twitter_revoke_url %}
<a class="link button button--twitter" href="{{ twitter_revoke_url }}">
{% trans "Deauthorize account" %}
</a>
{% else %}
<button class="button button--twitter button--disabled" disabled>
{% trans "Deauthorize account" %}
</button>
{% endif %}
</div>
</div>
</section>
</main>
{% endblock %}

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:home' 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,20 +0,0 @@
{% extends "base.html" %}
{% load i18n %}
{% block content %}
<main id="reddit--page" class="main">
<section class="section text-section">
{% if error %}
<h1 class="h1">{% trans "Reddit authorization failed" %}</h1>
<p>{{ error }}</p>
{% elif access_token and refresh_token %}
<h1 class="h1">{% trans "Reddit account is linked" %}</h1>
<p>{% trans "Your reddit account was successfully linked." %}</p>
{% endif %}
<p>
<a class="link" href="{% url 'accounts:settings:integrations' %}">{% trans "Return to integrations 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

@ -1,20 +0,0 @@
{% extends "base.html" %}
{% load i18n %}
{% block content %}
<main id="twitter--page" class="main">
<section class="section text-section">
{% if error %}
<h1 class="h1">{% trans "Twitter authorization failed" %}</h1>
<p>{{ error }}</p>
{% elif authorized %}
<h1 class="h1">{% trans "Twitter account is linked" %}</h1>
<p>{% trans "Your Twitter account was successfully linked." %}</p>
{% endif %}
<p>
<a class="link" href="{% url 'accounts:settings:integrations' %}">{% trans "Return to integrations page" %}</a>
</p>
</section>
</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

@ -1,537 +0,0 @@
from unittest.mock import Mock, 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 django.utils.translation import gettext as _
from bs4 import BeautifulSoup
from newsreader.accounts.tests.factories import UserFactory
from newsreader.news.collection.exceptions import (
StreamException,
StreamTooManyException,
)
from newsreader.news.collection.twitter import TWITTER_AUTH_URL
class IntegrationsViewTestCase(TestCase):
def setUp(self):
self.user = UserFactory(email="test@test.nl", password="test")
self.client.force_login(self.user)
self.url = reverse("accounts:settings:integrations")
class RedditIntegrationsTestCase(IntegrationsViewTestCase):
def test_reddit_authorization(self):
self.user.reddit_refresh_token = None
self.user.save()
response = self.client.get(self.url)
soup = BeautifulSoup(response.content, features="lxml")
button = soup.find("a", class_="link button button--reddit")
self.assertEquals(button.text.strip(), "Authorize account")
def test_reddit_refresh_token(self):
self.user.reddit_refresh_token = "jadajadajada"
self.user.reddit_access_token = None
self.user.save()
response = self.client.get(self.url)
soup = BeautifulSoup(response.content, features="lxml")
button = soup.find("a", class_="link button button--reddit")
self.assertEquals(button.text.strip(), "Refresh token")
def test_reddit_revoke(self):
self.user.reddit_refresh_token = "jadajadajada"
self.user.reddit_access_token = None
self.user.save()
response = self.client.get(self.url)
soup = BeautifulSoup(response.content, features="lxml")
buttons = soup.find_all("a", class_="link button button--reddit")
self.assertIn(
"Deauthorize account", [button.text.strip() for button in buttons]
)
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:settings: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 integrations 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)
class RedditTokenRedirectViewTestCase(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.integrations.RedditTokenTask")
self.mocked_task = self.patch.start()
def tearDown(self):
cache.clear()
def test_simple(self):
response = self.client.get(reverse("accounts:settings:reddit-refresh"))
self.assertRedirects(response, reverse("accounts:settings:integrations"))
self.mocked_task.delay.assert_called_once_with(self.user.pk)
self.assertEquals(1, cache.get(f"{self.user.email}-reddit-refresh"))
def test_not_active(self):
cache.set(f"{self.user.email}-reddit-refresh", 1)
response = self.client.get(reverse("accounts:settings:reddit-refresh"))
self.assertRedirects(response, reverse("accounts:settings:integrations"))
self.mocked_task.delay.assert_not_called()
class RedditRevokeRedirectViewTestCase(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.integrations.revoke_reddit_token")
self.mocked_revoke = self.patch.start()
def test_simple(self):
self.user.reddit_access_token = "jadajadajada"
self.user.reddit_refresh_token = "jadajadajada"
self.user.save()
self.mocked_revoke.return_value = True
response = self.client.get(reverse("accounts:settings:reddit-revoke"))
self.assertRedirects(response, reverse("accounts:settings:integrations"))
self.mocked_revoke.assert_called_once_with(self.user)
self.user.refresh_from_db()
self.assertEquals(self.user.reddit_access_token, None)
self.assertEquals(self.user.reddit_refresh_token, None)
def test_no_refresh_token(self):
self.user.reddit_refresh_token = None
self.user.save()
response = self.client.get(reverse("accounts:settings:reddit-revoke"))
self.assertRedirects(response, reverse("accounts:settings:integrations"))
self.mocked_revoke.assert_not_called()
def test_unsuccessful_response(self):
self.user.reddit_access_token = "jadajadajada"
self.user.reddit_refresh_token = "jadajadajada"
self.user.save()
self.mocked_revoke.return_value = False
response = self.client.get(reverse("accounts:settings:reddit-revoke"))
self.assertRedirects(response, reverse("accounts:settings:integrations"))
self.user.refresh_from_db()
self.assertEquals(self.user.reddit_access_token, "jadajadajada")
self.assertEquals(self.user.reddit_refresh_token, "jadajadajada")
def test_stream_exception(self):
self.user.reddit_access_token = "jadajadajada"
self.user.reddit_refresh_token = "jadajadajada"
self.user.save()
self.mocked_revoke.side_effect = StreamException
response = self.client.get(reverse("accounts:settings:reddit-revoke"))
self.assertRedirects(response, reverse("accounts:settings:integrations"))
self.user.refresh_from_db()
self.assertEquals(self.user.reddit_access_token, "jadajadajada")
self.assertEquals(self.user.reddit_refresh_token, "jadajadajada")
class TwitterRevokeRedirectView(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.integrations.post")
self.mocked_post = self.patch.start()
def tearDown(self):
patch.stopall()
def test_simple(self):
self.user.twitter_oauth_token = "jadajadajada"
self.user.twitter_oauth_token_secret = "jadajadajada"
self.user.save()
response = self.client.get(reverse("accounts:settings:twitter-revoke"))
self.assertRedirects(response, reverse("accounts:settings:integrations"))
self.user.refresh_from_db()
self.assertIsNone(self.user.twitter_oauth_token)
self.assertIsNone(self.user.twitter_oauth_token_secret)
def test_no_authorized_account(self):
self.user.twitter_oauth_token = None
self.user.twitter_oauth_token_secret = None
self.user.save()
response = self.client.get(reverse("accounts:settings:twitter-revoke"))
self.assertRedirects(response, reverse("accounts:settings:integrations"))
self.mocked_post.assert_not_called()
def test_stream_exception(self):
self.user.twitter_oauth_token = "jadajadajada"
self.user.twitter_oauth_token_secret = "jadajadajada"
self.user.save()
self.mocked_post.side_effect = StreamException
response = self.client.get(reverse("accounts:settings:twitter-revoke"))
self.assertRedirects(response, reverse("accounts:settings:integrations"))
self.user.refresh_from_db()
self.assertEquals(self.user.twitter_oauth_token, "jadajadajada")
self.assertEquals(self.user.twitter_oauth_token_secret, "jadajadajada")
class TwitterAuthRedirectViewTestCase(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.integrations.post")
self.mocked_post = self.patch.start()
def tearDown(self):
cache.clear()
def test_simple(self):
self.mocked_post.return_value = Mock(
text="oauth_token=foo&oauth_token_secret=bar"
)
response = self.client.get(reverse("accounts:settings:twitter-auth"))
self.assertRedirects(
response,
f"{TWITTER_AUTH_URL}/?oauth_token=foo",
fetch_redirect_response=False,
)
cached_token = cache.get(f"twitter-{self.user.email}-token")
cached_secret = cache.get(f"twitter-{self.user.email}-secret")
self.assertEquals(cached_token, "foo")
self.assertEquals(cached_secret, "bar")
def test_stream_exception(self):
self.mocked_post.side_effect = StreamException
response = self.client.get(reverse("accounts:settings:twitter-auth"))
self.assertRedirects(response, reverse("accounts:settings:integrations"))
cached_token = cache.get(f"twitter-{self.user.email}-token")
cached_secret = cache.get(f"twitter-{self.user.email}-secret")
self.assertIsNone(cached_token)
self.assertIsNone(cached_secret)
def test_unexpected_contents(self):
self.mocked_post.return_value = Mock(text="foo=bar&oauth_token_secret=bar")
response = self.client.get(reverse("accounts:settings:twitter-auth"))
self.assertRedirects(response, reverse("accounts:settings:integrations"))
cached_token = cache.get(f"twitter-{self.user.email}-token")
cached_secret = cache.get(f"twitter-{self.user.email}-secret")
self.assertIsNone(cached_token)
self.assertIsNone(cached_secret)
class TwitterTemplateViewTestCase(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.integrations.post")
self.mocked_post = self.patch.start()
def tearDown(self):
cache.clear()
def test_simple(self):
cache.set_many(
{
f"twitter-{self.user.email}-token": "foo",
f"twitter-{self.user.email}-secret": "bar",
}
)
params = {"denied": "", "oauth_token": "foo", "oauth_verifier": "barfoo"}
self.mocked_post.return_value = Mock(
text="oauth_token=realtoken&oauth_token_secret=realsecret"
)
response = self.client.get(
f"{reverse('accounts:settings:twitter-template')}?{urlencode(params)}"
)
self.assertContains(response, _("Twitter account is linked"))
self.user.refresh_from_db()
self.assertEquals(self.user.twitter_oauth_token, "realtoken")
self.assertEquals(self.user.twitter_oauth_token_secret, "realsecret")
self.assertIsNone(cache.get(f"twitter-{self.user.email}-token"))
self.assertIsNone(cache.get(f"twitter-{self.user.email}-secret"))
def test_denied(self):
params = {"denied": "true", "oauth_token": "foo", "oauth_verifier": "barfoo"}
response = self.client.get(
f"{reverse('accounts:settings:twitter-template')}?{urlencode(params)}"
)
self.assertContains(response, _("Twitter authorization failed"))
self.user.refresh_from_db()
self.assertIsNone(self.user.twitter_oauth_token)
self.assertIsNone(self.user.twitter_oauth_token_secret)
self.mocked_post.assert_not_called()
def test_mismatched_token(self):
cache.set_many(
{
f"twitter-{self.user.email}-token": "foo",
f"twitter-{self.user.email}-secret": "bar",
}
)
params = {"denied": "", "oauth_token": "boo", "oauth_verifier": "barfoo"}
response = self.client.get(
f"{reverse('accounts:settings:twitter-template')}?{urlencode(params)}"
)
self.assertContains(response, _("OAuth tokens failed to match"))
self.user.refresh_from_db()
self.assertIsNone(self.user.twitter_oauth_token)
self.assertIsNone(self.user.twitter_oauth_token_secret)
self.mocked_post.assert_not_called()
def test_missing_secret(self):
cache.set_many({f"twitter-{self.user.email}-token": "foo"})
params = {"denied": "", "oauth_token": "foo", "oauth_verifier": "barfoo"}
response = self.client.get(
f"{reverse('accounts:settings:twitter-template')}?{urlencode(params)}"
)
self.assertContains(response, _("No matching tokens found for this user"))
self.user.refresh_from_db()
self.assertIsNone(self.user.twitter_oauth_token_secret)
self.mocked_post.assert_not_called()
def test_stream_exception(self):
cache.set_many(
{
f"twitter-{self.user.email}-token": "foo",
f"twitter-{self.user.email}-secret": "bar",
}
)
params = {"denied": "", "oauth_token": "foo", "oauth_verifier": "barfoo"}
self.mocked_post.side_effect = StreamException
response = self.client.get(
f"{reverse('accounts:settings:twitter-template')}?{urlencode(params)}"
)
self.assertContains(response, _("Failed requesting access token"))
self.user.refresh_from_db()
self.assertIsNone(self.user.twitter_oauth_token)
self.assertIsNone(self.user.twitter_oauth_token_secret)
self.assertIsNotNone(cache.get(f"twitter-{self.user.email}-token"))
self.assertIsNotNone(cache.get(f"twitter-{self.user.email}-secret"))
def test_unexpected_contents(self):
cache.set_many(
{
f"twitter-{self.user.email}-token": "foo",
f"twitter-{self.user.email}-secret": "bar",
}
)
params = {"denied": "", "oauth_token": "foo", "oauth_verifier": "barfoo"}
self.mocked_post.return_value = Mock(
text="foobar=boo&oauth_token_secret=realsecret"
)
response = self.client.get(
f"{reverse('accounts:settings:twitter-template')}?{urlencode(params)}"
)
self.assertContains(response, _("No credentials found in Twitter response"))
self.user.refresh_from_db()
self.assertIsNone(self.user.twitter_oauth_token)
self.assertIsNone(self.user.twitter_oauth_token_secret)
self.assertIsNotNone(cache.get(f"twitter-{self.user.email}-token"))
self.assertIsNotNone(cache.get(f"twitter-{self.user.email}-secret"))

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

@ -15,9 +15,6 @@ class UserTestCase(TestCase):
PeriodicTask.objects.create(
name=f"{user.email}-feed", task="FeedTask", interval=interval
)
PeriodicTask.objects.create(
name=f"{user.email}-timeline", task="TwitterTimelineTask", interval=interval
)
user.delete()

View file

@ -2,11 +2,7 @@ from django.contrib.auth.decorators import login_required
from django.urls import include, path
from newsreader.accounts.views import (
ActivationCompleteView,
ActivationResendView,
ActivationView,
FaviconRedirectView,
IntegrationsView,
LoginView,
LogoutView,
PasswordChangeView,
@ -14,54 +10,11 @@ from newsreader.accounts.views import (
PasswordResetConfirmView,
PasswordResetDoneView,
PasswordResetView,
RedditRevokeRedirectView,
RedditTemplateView,
RedditTokenRedirectView,
RegistrationClosedView,
RegistrationCompleteView,
RegistrationView,
SettingsView,
TwitterAuthRedirectView,
TwitterRevokeRedirectView,
TwitterTemplateView,
)
settings_patterns = [
# Integrations
path(
"integrations/reddit/callback/",
login_required(RedditTemplateView.as_view()),
name="reddit-template",
),
path(
"integrations/reddit/refresh/",
login_required(RedditTokenRedirectView.as_view()),
name="reddit-refresh",
),
path(
"integrations/reddit/revoke/",
login_required(RedditRevokeRedirectView.as_view()),
name="reddit-revoke",
),
path(
"integrations/twitter/auth/",
login_required(TwitterAuthRedirectView.as_view()),
name="twitter-auth",
),
path(
"integrations/twitter/callback/",
login_required(TwitterTemplateView.as_view()),
name="twitter-template",
),
path(
"integrations/twitter/revoke/",
login_required(TwitterRevokeRedirectView.as_view()),
name="twitter-revoke",
),
path(
"integrations/", login_required(IntegrationsView.as_view()), name="integrations"
),
# Misc
path("favicon/", login_required(FaviconRedirectView.as_view()), name="favicon"),
path("", login_required(SettingsView.as_view()), name="home"),
@ -71,24 +24,6 @@ urlpatterns = [
# Auth
path("login/", LoginView.as_view(), name="login"),
path("logout/", LogoutView.as_view(), name="logout"),
# Register
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(

View file

@ -1,14 +1,5 @@
from newsreader.accounts.views.auth import LoginView, LogoutView
from newsreader.accounts.views.favicon import FaviconRedirectView
from newsreader.accounts.views.integrations import (
IntegrationsView,
RedditRevokeRedirectView,
RedditTemplateView,
RedditTokenRedirectView,
TwitterAuthRedirectView,
TwitterRevokeRedirectView,
TwitterTemplateView,
)
from newsreader.accounts.views.password import (
PasswordChangeView,
PasswordResetCompleteView,
@ -16,12 +7,17 @@ from newsreader.accounts.views.password import (
PasswordResetDoneView,
PasswordResetView,
)
from newsreader.accounts.views.registration import (
ActivationCompleteView,
ActivationResendView,
ActivationView,
RegistrationClosedView,
RegistrationCompleteView,
RegistrationView,
)
from newsreader.accounts.views.settings import SettingsView
__all__ = [
"LoginView",
"LogoutView",
"FaviconRedirectView",
"PasswordChangeView",
"PasswordResetCompleteView",
"PasswordResetConfirmView",
"PasswordResetDoneView",
"PasswordResetView",
"SettingsView",
]

View file

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

View file

@ -1,343 +0,0 @@
import logging
from urllib.parse import parse_qs, urlencode
from django.conf import settings
from django.contrib import messages
from django.core.cache import cache
from django.shortcuts import redirect
from django.urls import reverse_lazy
from django.utils.translation import gettext as _
from django.views.generic import RedirectView, TemplateView
from requests_oauthlib import OAuth1 as OAuth
from newsreader.news.collection.exceptions import StreamException
from newsreader.news.collection.reddit import (
get_reddit_access_token,
get_reddit_authorization_url,
revoke_reddit_token,
)
from newsreader.news.collection.tasks import RedditTokenTask
from newsreader.news.collection.twitter import (
TWITTER_ACCESS_TOKEN_URL,
TWITTER_AUTH_URL,
TWITTER_REQUEST_TOKEN_URL,
TWITTER_REVOKE_URL,
)
from newsreader.news.collection.utils import post
logger = logging.getLogger(__name__)
class IntegrationsView(TemplateView):
template_name = "accounts/views/integrations.html"
def get_context_data(self, **kwargs):
return {
**super().get_context_data(**kwargs),
**self.get_reddit_context(**kwargs),
**self.get_twitter_context(**kwargs),
}
def get_reddit_context(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:settings:reddit-refresh")
if not user.reddit_refresh_token:
reddit_authorization_url = get_reddit_authorization_url(user)
return {
"reddit_authorization_url": reddit_authorization_url,
"reddit_refresh_url": reddit_refresh_url,
"reddit_revoke_url": (
reverse_lazy("accounts:settings:reddit-revoke")
if not reddit_authorization_url
else None
),
}
def get_twitter_context(self, **kwargs):
twitter_revoke_url = None
if self.request.user.has_twitter_auth:
twitter_revoke_url = reverse_lazy("accounts:settings:twitter-revoke")
return {
"twitter_auth_url": reverse_lazy("accounts:settings:twitter-auth"),
"twitter_revoke_url": twitter_revoke_url,
}
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:integrations")
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
class RedditRevokeRedirectView(RedirectView):
url = reverse_lazy("accounts:settings:integrations")
def get(self, request, *args, **kwargs):
response = super().get(request, *args, **kwargs)
user = request.user
if not user.reddit_refresh_token:
messages.error(request, _("No reddit account is linked to this account"))
return response
try:
is_revoked = revoke_reddit_token(user)
except StreamException:
logger.exception(f"Unable to revoke reddit token for {user.pk}")
messages.error(request, _("Unable to revoke reddit token"))
return response
if not is_revoked:
messages.error(request, _("Unable to revoke reddit token"))
return response
user.reddit_access_token = None
user.reddit_refresh_token = None
user.save()
messages.success(request, _("Reddit account deathorized"))
return response
class TwitterRevokeRedirectView(RedirectView):
url = reverse_lazy("accounts:settings:integrations")
def get(self, request, *args, **kwargs):
if not request.user.has_twitter_auth:
messages.error(request, _("No twitter credentials found"))
return super().get(request, *args, **kwargs)
oauth = OAuth(
settings.TWITTER_CONSUMER_ID,
client_secret=settings.TWITTER_CONSUMER_SECRET,
resource_owner_key=request.user.twitter_oauth_token,
resource_owner_secret=request.user.twitter_oauth_token_secret,
)
try:
post(TWITTER_REVOKE_URL, auth=oauth)
except StreamException:
logger.exception("Failed revoking Twitter account")
messages.error(request, _("Unable revoke Twitter account"))
return super().get(request, *args, **kwargs)
request.user.twitter_oauth_token = None
request.user.twitter_oauth_token_secret = None
request.user.save()
messages.success(request, _("Twitter account revoked"))
return super().get(request, *args, **kwargs)
class TwitterAuthRedirectView(RedirectView):
url = reverse_lazy("accounts:settings:integrations")
def get(self, request, *args, **kwargs):
oauth = OAuth(
settings.TWITTER_CONSUMER_ID,
client_secret=settings.TWITTER_CONSUMER_SECRET,
callback_uri=settings.TWITTER_REDIRECT_URL,
)
try:
response = post(TWITTER_REQUEST_TOKEN_URL, auth=oauth)
except StreamException:
logger.exception("Failed requesting Twitter authentication token")
messages.error(request, _("Unable to retrieve initial Twitter token"))
return super().get(request, *args, **kwargs)
params = parse_qs(response.text)
try:
request_oauth_token = params["oauth_token"][0]
request_oauth_secret = params["oauth_token_secret"][0]
except KeyError:
logger.exception("No credentials found in response")
messages.error(request, _("Unable to retrieve initial Twitter token"))
return super().get(request, *args, **kwargs)
cache.set_many(
{
f"twitter-{request.user.email}-token": request_oauth_token,
f"twitter-{request.user.email}-secret": request_oauth_secret,
}
)
request_params = urlencode({"oauth_token": request_oauth_token})
return redirect(f"{TWITTER_AUTH_URL}/?{request_params}")
class TwitterTemplateView(TemplateView):
template_name = "accounts/views/twitter.html"
def get(self, request, *args, **kwargs):
context = self.get_context_data(**kwargs)
denied = request.GET.get("denied", False)
oauth_token = request.GET.get("oauth_token")
oauth_verifier = request.GET.get("oauth_verifier")
if denied:
return self.render_to_response(
{
**context,
"error": _("Twitter authorization failed"),
"authorized": False,
}
)
cached_token = cache.get(f"twitter-{request.user.email}-token")
if oauth_token != cached_token:
return self.render_to_response(
{
**context,
"error": _("OAuth tokens failed to match"),
"authorized": False,
}
)
cached_secret = cache.get(f"twitter-{request.user.email}-secret")
if not cached_token or not cached_secret:
return self.render_to_response(
{
**context,
"error": _("No matching tokens found for this user"),
"authorized": False,
}
)
oauth = OAuth(
settings.TWITTER_CONSUMER_ID,
client_secret=settings.TWITTER_CONSUMER_SECRET,
resource_owner_key=cached_token,
resource_owner_secret=cached_secret,
verifier=oauth_verifier,
)
try:
response = post(TWITTER_ACCESS_TOKEN_URL, auth=oauth)
except StreamException:
logger.exception("Failed requesting Twitter access token")
return self.render_to_response(
{
**context,
"error": _("Failed requesting access token"),
"authorized": False,
}
)
params = parse_qs(response.text)
try:
oauth_token = params["oauth_token"][0]
oauth_secret = params["oauth_token_secret"][0]
except KeyError:
logger.exception("No credentials in Twitter response")
return self.render_to_response(
{
**context,
"error": _("No credentials found in Twitter response"),
"authorized": False,
}
)
request.user.twitter_oauth_token = oauth_token
request.user.twitter_oauth_token_secret = oauth_secret
request.user.save()
cache.delete_many(
[
f"twitter-{request.user.email}-token",
f"twitter-{request.user.email}-secret",
]
)
return self.render_to_response({**context, "error": None, "authorized": True})

View file

@ -1,10 +1,7 @@
from django.contrib.auth import views as django_views
from django.urls import reverse_lazy
from newsreader.news.collection.reddit import (
get_reddit_access_token,
get_reddit_authorization_url,
)
from newsreader.utils.views import NavListMixin
# PasswordResetView sends the mail
@ -12,26 +9,26 @@ from newsreader.news.collection.reddit import (
# 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):
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(django_views.PasswordResetDoneView):
class PasswordResetDoneView(NavListMixin, django_views.PasswordResetDoneView):
template_name = "password-reset/password-reset-done.html"
class PasswordResetConfirmView(django_views.PasswordResetConfirmView):
class PasswordResetConfirmView(NavListMixin, django_views.PasswordResetConfirmView):
template_name = "password-reset/password-reset-confirm.html"
success_url = reverse_lazy("accounts:password-reset-complete")
class PasswordResetCompleteView(django_views.PasswordResetCompleteView):
class PasswordResetCompleteView(NavListMixin, django_views.PasswordResetCompleteView):
template_name = "password-reset/password-reset-complete.html"
class PasswordChangeView(django_views.PasswordChangeView):
class PasswordChangeView(NavListMixin, django_views.PasswordChangeView):
template_name = "accounts/views/password-change.html"
success_url = reverse_lazy("accounts:settings")

View file

@ -1,59 +0,0 @@
from django.shortcuts import render
from django.urls import reverse_lazy
from django.views.generic import TemplateView
from registration.backends.default import views as registration_views
from newsreader.news.collection.reddit import (
get_reddit_access_token,
get_reddit_authorization_url,
)
# 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
)

View file

@ -4,13 +4,10 @@ from django.views.generic.edit import FormView, ModelFormMixin
from newsreader.accounts.forms import UserSettingsForm
from newsreader.accounts.models import User
from newsreader.news.collection.reddit import (
get_reddit_access_token,
get_reddit_authorization_url,
)
from newsreader.utils.views import NavListMixin
class SettingsView(ModelFormMixin, FormView):
class SettingsView(NavListMixin, ModelFormMixin, FormView):
template_name = "accounts/views/settings.html"
success_url = reverse_lazy("accounts:settings:home")
form_class = UserSettingsForm

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,9 +1,7 @@
import os
from pathlib import Path
from dotenv import load_dotenv
from newsreader.conf.utils import get_env, get_root_dir
load_dotenv()
@ -15,16 +13,13 @@ except ImportError:
DjangoIntegration = None
BASE_DIR = Path(__file__).resolve().parent.parent.parent.parent
DJANGO_PROJECT_DIR = os.path.join(BASE_DIR, "src", "newsreader")
BASE_DIR = get_root_dir()
DJANGO_PROJECT_DIR = BASE_DIR / "src" / "newsreader"
# 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
DEBUG = False
ALLOWED_HOSTS = ["127.0.0.1", "localhost"]
INTERNAL_IPS = ["127.0.0.1", "localhost"]
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 = [
@ -37,10 +32,8 @@ INSTALLED_APPS = [
"django.forms",
# third party apps
"rest_framework",
"drf_yasg",
"celery",
"django_celery_beat",
"registration",
"axes",
# app modules
"newsreader.accounts",
@ -50,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",
@ -73,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",
@ -88,15 +82,14 @@ 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=""),
}
}
@ -104,17 +97,15 @@ 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,
@ -128,48 +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",
},
"file": {
"level": "DEBUG",
"class": "logging.handlers.RotatingFileHandler",
"filename": BASE_DIR / "logs" / "newsreader.log",
"backupCount": 5,
"maxBytes": 50000000, # 50 mB
"formatter": "timestamped",
},
"celery": {
"level": "INFO",
"filters": ["require_debug_false"],
"class": "logging.handlers.SysLogHandler",
"formatter": "syslog",
"address": "/dev/log",
},
"syslog": {
"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", "syslog"], "level": "INFO"},
"django": {"handlers": ["console"], "level": "INFO"},
"django.server": {
"handlers": ["console", "syslog"],
"handlers": ["console"],
"level": "INFO",
"propagate": False,
},
"celery": {"handlers": ["celery", "console"], "level": "INFO"},
"newsreader": {"handlers": ["syslog", "console"], "level": "INFO"},
"celery.task": {"handlers": ["console", "celery"], "level": "INFO"},
"newsreader": {
"handlers": ["console", "file"],
"level": "DEBUG",
"propagate": False,
},
},
}
# Password validation
# https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"
@ -184,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"
@ -193,37 +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",
]
# Email
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
# Project settings
ENVIRONMENT = "development"
# Reddit integration
REDDIT_CLIENT_ID = "CLIENT_ID"
REDDIT_CLIENT_SECRET = "CLIENT_SECRET"
REDDIT_REDIRECT_URL = (
"http://127.0.0.1:8000/accounts/settings/integrations/reddit/callback/"
DEFAULT_FROM_EMAIL = get_env(
"EMAIL_DEFAULT_FROM", required=False, default="webmaster@localhost"
)
# Twitter integration
TWITTER_CONSUMER_ID = "CONSUMER_ID"
TWITTER_CONSUMER_SECRET = "CONSUMER_SECRET"
TWITTER_REDIRECT_URL = (
"http://127.0.0.1:8000/accounts/settings/integrations/twitter/callback/"
)
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)
# Third party settings
AXES_HANDLER = "axes.handlers.cache.AxesCacheHandler"
@ -240,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 = {
@ -251,17 +237,14 @@ 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
REGISTRATION_OPEN = True
REGISTRATION_AUTO_LOGIN = True
ACCOUNT_ACTIVATION_DAYS = 7
CELERY_BROKER_URL = "amqp://guest@rabbitmq:5672"
# Sentry
SENTRY_CONFIG = {
"dsn": os.environ.get("SENTRY_DSN"),
"dsn": get_env("SENTRY_DSN", default="", required=False),
"send_default_pii": False,
"environment": ENVIRONMENT,
"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,14 +1,19 @@
from .base import * # isort:skip
from .version import get_current_version
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()
@ -18,13 +23,13 @@ AXES_FAILURE_LIMIT = 50
AXES_COOLOFF_TIME = None
try:
from .local import * # noqa
# Optionally use sentry integration
from sentry_sdk import init as sentry_init
SENTRY_CONFIG.update({"release": VERSION})
from .local import * # noqa
sentry_init(**SENTRY_CONFIG)
SENTRY_CONFIG.update({"release": VERSION}) # noqa: F405
sentry_init(**SENTRY_CONFIG) # noqa: F405
except ImportError:
pass

View file

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

View file

@ -1,34 +0,0 @@
from .base import * # isort:skip
from .version import get_current_version
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",
},
}
# Project settings
VERSION = get_current_version()
ENVIRONMENT = "gitlab"
try:
# Optionally use sentry integration
from sentry_sdk import init as sentry_init
SENTRY_CONFIG.update({"release": VERSION, "environment": ENVIRONMENT})
sentry_init(**SENTRY_CONFIG)
except ImportError:
pass

View file

@ -1,87 +1,32 @@
import os
from newsreader.conf.utils import get_env
from .version import get_current_version
from .base import * # noqa: F403
from .utils import get_current_version
from .base import * # isort:skip
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",
]
},
}
]
# Email
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
DEFAULT_FROM_EMAIL = os.environ.get("EMAIL_DEFAULT_FROM", "webmaster@localhost")
EMAIL_HOST = os.environ.get("EMAIL_HOST", "localhost")
EMAIL_PORT = os.environ.get("EMAIL_PORT", 25)
EMAIL_HOST_USER = os.environ.get("EMAIL_HOST_USER", "")
EMAIL_HOST_PASSWORD = os.environ.get("EMAIL_HOST_PASSWORD", "")
EMAIL_USE_TLS = bool(os.environ.get("EMAIL_USE_TLS"))
EMAIL_USE_SSL = bool(os.environ.get("EMAIL_USE_SSL"))
# Project settings
VERSION = get_current_version(debug=False)
ENVIRONMENT = "production"
# Reddit integration
REDDIT_CLIENT_ID = os.environ.get("REDDIT_CLIENT_ID", "")
REDDIT_CLIENT_SECRET = os.environ.get("REDDIT_CLIENT_SECRET", "")
REDDIT_REDIRECT_URL = os.environ.get("REDDIT_CALLBACK_URL", "")
# Twitter integration
TWITTER_CONSUMER_ID = os.environ.get("TWITTER_CONSUMER_ID", "")
TWITTER_CONSUMER_SECRET = os.environ.get("TWITTER_CONSUMER_SECRET", "")
TWITTER_REDIRECT_URL = os.environ.get("TWITTER_REDIRECT_URL", "")
# 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
SENTRY_CONFIG.update(
SENTRY_CONFIG.update( # noqa: F405
{"release": VERSION, "environment": ENVIRONMENT, "debug": False}
)
sentry_init(**SENTRY_CONFIG)
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,24 +0,0 @@
import os
import subprocess
def get_current_version(debug=True):
if "VERSION" in os.environ:
return os.environ["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 ""

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -3,15 +3,13 @@ import React from 'react';
class Messages extends React.Component {
state = { messages: this.props.messages };
close = ::this.close;
close(index) {
close = index => {
const newMessages = this.state.messages.filter((message, currentIndex) => {
return currentIndex != index;
});
this.setState({ messages: newMessages });
}
};
render() {
const messages = this.state.messages.map((message, index) => {

View file

@ -0,0 +1,22 @@
import React from 'react';
class NavList extends React.Component {
render() {
const entries = Object.entries(this.props.navLinks);
const links = entries.map(([name, link], index) => {
return (
<li key={index} className="nav-list__item">
<a href={link}>{name}</a>
</li>
);
});
const className = this.props.includeBorder
? 'nav-list nav-list--bordered'
: 'nav-list';
return <ol className={className}>{links}</ol>;
}
}
export default NavList;

View file

@ -1,6 +1,4 @@
class Selector {
onClick = ::this.onClick;
inputs = [];
constructor() {
@ -11,13 +9,13 @@ class Selector {
selectAllInput.onchange = this.onClick;
}
onClick(e) {
onClick = e => {
const targetValue = e.target.checked;
this.inputs.forEach(input => {
input.checked = targetValue;
});
}
};
}
export default Selector;

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