diff --git a/.coveragerc b/.coveragerc
deleted file mode 100644
index d1a0d79..0000000
--- a/.coveragerc
+++ /dev/null
@@ -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
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..5257a12
--- /dev/null
+++ b/.editorconfig
@@ -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
diff --git a/.isort.cfg b/.isort.cfg
deleted file mode 100644
index 0c8e37f..0000000
--- a/.isort.cfg
+++ /dev/null
@@ -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
diff --git a/.prettierrc.json b/.prettierrc.json
deleted file mode 100644
index 146a217..0000000
--- a/.prettierrc.json
+++ /dev/null
@@ -1,10 +0,0 @@
-{
- "semi": true,
- "trailingComma": "es5",
- "singleQuote": true,
- "printWidth": 90,
- "tabWidth": 2,
- "useTabs": false,
- "bracketSpacing": true,
- "arrowParens": "avoid"
-}
diff --git a/.woodpecker/build.yaml b/.woodpecker/build.yaml
index ba795b4..519e330 100644
--- a/.woodpecker/build.yaml
+++ b/.woodpecker/build.yaml
@@ -1,8 +1,10 @@
when:
- event: push
+ - event: pull_request
+ - event: manual
steps:
- - image: node:lts
+ - image: node:lts-alpine
commands:
- npm install
- - npm run build
+ - npm run build:prod
diff --git a/.woodpecker/lint.yaml b/.woodpecker/lint.yaml
index bc25a32..64ee04b 100644
--- a/.woodpecker/lint.yaml
+++ b/.woodpecker/lint.yaml
@@ -1,19 +1,18 @@
when:
- event: push
- branch: main
- event: pull_request
+ - event: manual
steps:
- name: python linting
- image: python:3.11
+ image: ghcr.io/astral-sh/uv:python3.11-alpine
commands:
- - pip install uv
- uv sync --group ci
- - ./.venv/bin/ruff check src/
- - ./.venv/bin/ruff format --check src/
+ - uv run --no-sync -- ruff check src/
+ - uv run --no-sync -- ruff format --check src/
- name: javascript linting
- image: node:lts
+ image: node:lts-alpine
commands:
- - npm install
+ - npm ci
- npm run lint
diff --git a/.woodpecker/tests.yaml b/.woodpecker/tests.yaml
index fed2254..95092f6 100644
--- a/.woodpecker/tests.yaml
+++ b/.woodpecker/tests.yaml
@@ -1,36 +1,37 @@
when:
- event: push
+ - event: pull_request
+ - event: manual
services:
- name: postgres
image: postgres:15
environment:
- POSTGRES_NAME: newsreader
- POSTGRES_USER: newsreader
- POSTGRES_PASSWORD: sekrit
+ 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: python:3.11
+ 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: newsreader
- POSTGRES_NAME: newsreader
- POSTGRES_USER: newsreader
- POSTGRES_PASSWORD: sekrit
+ POSTGRES_DB: *db-name
+ POSTGRES_USER: *db-user
+ POSTGRES_PASSWORD: *db-password
commands:
- pip install uv
- uv sync --group ci
- - ./.venv/bin/coverage run ./src/manage.py test newsreader
- - ./.venv/bin/coverage report --show-missing
+ - 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
+ image: node:lts-alpine
commands:
- - npm install
+ - npm ci
- npm test
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..0ffa683
--- /dev/null
+++ b/Dockerfile
@@ -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
diff --git a/bin/docker-entrypoint.sh b/bin/docker-entrypoint.sh
index 0006178..bb473e6 100755
--- a/bin/docker-entrypoint.sh
+++ b/bin/docker-entrypoint.sh
@@ -1,5 +1,5 @@
#!/bin/bash
-/app/.venv/bin/python /app/src/manage.py migrate
+uv run --no-sync -- /app/src/manage.py migrate
exec "$@"
diff --git a/docker-compose.development.yml b/docker-compose.development.yml
index d00550b..9045200 100644
--- a/docker-compose.development.yml
+++ b/docker-compose.development.yml
@@ -1,18 +1,13 @@
volumes:
static-files:
- node-modules:
services:
- celery:
- build:
- target: development
- volumes:
- - ./src/:/app/src
-
django:
- build:
+ build: &app-development-build
target: development
- command: /app/.venv/bin/python /app/src/manage.py runserver 0.0.0.0:8000
+ 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:
@@ -21,12 +16,21 @@ services:
stdin_open: true
tty: true
+ celery:
+ build:
+ <<: *app-development-build
+ environment:
+ <<: *django-env
+ volumes:
+ - ./src/:/app/src
+
webpack:
build:
+ target: frontend-build
context: .
- dockerfile: ./docker/webpack
+ args:
+ BUILD_ARG: "dev"
command: npm run build:watch
volumes:
- ./src/:/app/src
- static-files:/app/src/newsreader/static
- - node-modules:/app/node_modules
diff --git a/docker-compose.production.yml b/docker-compose.production.yml
index 46a9c76..24c8cd1 100644
--- a/docker-compose.production.yml
+++ b/docker-compose.production.yml
@@ -9,7 +9,6 @@ services:
django:
condition: service_healthy
ports:
- # Note that --env-file should be used to set these correctly
- "${NGINX_HTTP_PORT:-80}:80"
volumes:
- ./config/nginx/conf.d:/etc/nginx/conf.d
diff --git a/docker-compose.yml b/docker-compose.yml
index 02f1fab..c348d96 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -4,33 +4,43 @@ volumes:
postgres-data:
static-files:
-x-db-env: &db-env
- POSTGRES_HOST:
- POSTGRES_PORT:
- POSTGRES_DB:
- POSTGRES_USER:
- POSTGRES_PASSWORD:
+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-django-build-env: &django-build-env
- <<: *db-env
- DJANGO_SECRET_KEY:
- DJANGO_SETTINGS_MODULE:
+x-db-env: &db-env
+ <<: *db-connection-env
+ PGUSER: *pg-user
+ PGDATABASE: *pg-database
x-django-env: &django-env
- <<: *django-build-env
- VERSION:
+ <<: *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_PORT:
- EMAIL_HOST_USER:
- EMAIL_HOST_PASSWORD:
- EMAIL_USE_TLS:
- EMAIL_USE_SSL:
- EMAIL_DEFAULT_FROM:
+ 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: ${SENTRY_DSN:-""}
services:
db:
@@ -38,8 +48,8 @@ services:
<<: *db-env
image: postgres:15
healthcheck:
- # Note that --env-file should be used to set these correctly
- test: /usr/bin/pg_isready --username="${POSTGRES_USER}" --dbname="${POSTGRES_DB}"
+ test: /usr/bin/pg_isready
+ start_period: 10s
interval: 5s
timeout: 10s
retries: 10
@@ -55,58 +65,23 @@ services:
- memcached
- -m 64
- celery:
- build:
- context: .
- dockerfile: ./docker/django
- target: production
- args:
- <<: *django-build-env
- environment:
- <<: *django-env
- command: |
- /app/.venv/bin/celery --app newsreader
- --workdir /app/src/
- worker --loglevel INFO
- --concurrency 2
- --beat
- --scheduler django
- -n worker1@%h
- -n worker2@%h
- healthcheck:
- test: celery --app newsreader status || exit 1
- interval: 10s
- timeout: 10s
- retries: 5
- depends_on:
- rabbitmq:
- condition: service_started
- memcached:
- condition: service_started
- db:
- condition: service_healthy
- django:
- condition: service_healthy
- volumes:
- - logs:/app/logs
-
django:
- build:
+ build: &app-build
context: .
- dockerfile: ./docker/django
target: production
- args:
- <<: *django-build-env
environment:
<<: *django-env
- entrypoint: /app/bin/docker-entrypoint.sh
+ entrypoint: ["/bin/sh", "/app/bin/docker-entrypoint.sh"]
command: |
- /app/.venv/bin/gunicorn --bind 0.0.0.0:8000
+ 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
@@ -119,3 +94,33 @@ services:
- logs:/app/logs
- media:/app/media
- static-files:/app/static
+
+ celery:
+ build:
+ <<: *app-build
+ environment:
+ <<: *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:
+ condition: service_started
+ django:
+ condition: service_healthy
+ volumes:
+ - logs:/app/logs
diff --git a/docker/django b/docker/django
deleted file mode 100644
index 6e079c8..0000000
--- a/docker/django
+++ /dev/null
@@ -1,102 +0,0 @@
-# stage 1
-FROM python:3.11-bookworm AS backend
-
-RUN apt-get update && apt-get install --yes --no-install-recommends \
- vim \
- curl \
- gettext \
- && rm -rf /var/lib/apt/lists/*
-
-WORKDIR /app
-RUN mkdir /app/src
-RUN mkdir /app/logs
-RUN mkdir /app/media
-
-COPY uv.lock pyproject.toml /app/
-
-COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv
-RUN uv sync --frozen --no-default-groups --no-install-project
-
-
-# stage 2
-FROM node:lts AS frontend-build
-
-WORKDIR /app
-
-COPY ./*.json ./*.js ./babel.config.js /app/
-
-RUN npm ci
-
-COPY ./src /app/src
-
-RUN npm run build:prod
-
-
-# stage 3
-FROM python:3.11-bookworm AS production
-
-RUN apt-get update && apt-get install --yes --no-install-recommends \
- postgresql-client \
- vim \
- curl \
- gettext \
- && rm -rf /var/lib/apt/lists/*
-
-WORKDIR /app
-
-RUN mkdir /app/logs
-RUN mkdir /app/media
-RUN mkdir /app/bin
-
-COPY --from=backend /app/.venv /app/.venv
-COPY --from=backend /app/uv.lock /app/pyproject.toml /app/
-COPY --from=backend /bin/uv /bin/uv
-
-COPY ./bin/docker-entrypoint.sh /app/bin/docker-entrypoint.sh
-
-COPY --from=frontend-build /app/src/newsreader/static /app/src/newsreader/static
-
-COPY ./src /app/src
-
-RUN uv sync --frozen --only-group production --extra sentry
-
-RUN useradd --no-create-home --uid 1000 newsreader
-RUN chown --recursive newsreader:newsreader /app
-
-USER newsreader
-
-ARG POSTGRES_HOST
-ARG POSTGRES_PORT
-ARG POSTGRES_DB
-ARG POSTGRES_USER
-ARG POSTGRES_PASSWORD
-ARG DJANGO_SECRET_KEY
-ARG DJANGO_SETTINGS_MODULE
-
-# 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 /app/.venv/bin/python src/manage.py collectstatic --noinput
-
-
-# (optional) stage 4
-FROM python:3.11-bookworm AS development
-
-RUN apt-get update && apt-get install --yes --no-install-recommends \
- vim \
- curl \
- && rm -rf /var/lib/apt/lists/*
-
-WORKDIR /app
-
-RUN mkdir /app/logs
-RUN mkdir /app/media
-RUN mkdir /app/bin
-
-COPY --from=backend /app/.venv /app/.venv
-COPY --from=backend /app/uv.lock /app/pyproject.toml /app/
-COPY ./bin/docker-entrypoint.sh /app/bin/docker-entrypoint.sh
-COPY --from=backend /app/src/ /app/src/
-
-COPY --from=backend /bin/uv /bin/uv
-RUN uv sync --frozen --group development
diff --git a/docker/webpack b/docker/webpack
deleted file mode 100644
index 11c3d58..0000000
--- a/docker/webpack
+++ /dev/null
@@ -1,10 +0,0 @@
-FROM node:lts
-
-WORKDIR /app
-RUN mkdir /app/src
-
-COPY package*.json webpack.*.js babel.config.js /app/
-
-RUN npm install
-
-COPY ./src /app/src
diff --git a/jest.config.js b/jest.config.js
deleted file mode 100644
index c8473a7..0000000
--- a/jest.config.js
+++ /dev/null
@@ -1,6 +0,0 @@
-module.exports = {
- roots: ['src/newsreader/js/tests/'],
-
- clearMocks: true,
- coverageDirectory: 'coverage',
-};
diff --git a/package-lock.json b/package-lock.json
index 82a511b..59e4d2b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "newsreader",
- "version": "0.4.4",
+ "version": "0.5.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "newsreader",
- "version": "0.4.4",
+ "version": "0.5.3",
"license": "GPL-3.0-or-later",
"dependencies": {
"@fortawesome/fontawesome-free": "^5.15.2",
diff --git a/package.json b/package.json
index 3b251da..d12a88a 100644
--- a/package.json
+++ b/package.json
@@ -2,19 +2,18 @@
"name": "newsreader",
"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",
@@ -55,5 +54,22 @@
"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"
}
}
diff --git a/pyproject.toml b/pyproject.toml
index c722183..e62c754 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,66 +1,81 @@
[project]
-name = 'newsreader'
-version = '0.5.3'
-authors = [{ name = 'Sonny', email= 'sonny871@hotmail.com' }]
-license = {text = 'GPL-3.0'}
-requires-python = '>=3.11'
+name = "newsreader"
+version = "0.5.3"
+authors = [{ name = "Sonny" }]
+license = { text = "GPL-3.0" }
+requires-python = ">=3.11"
dependencies = [
- 'django~=4.2',
- 'celery~=5.4',
- 'psycopg',
- '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',
+ "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",
]
[dependency-groups]
-test-tools = ['ruff', 'factory_boy', 'freezegun']
+test-tools = ["ruff", "factory_boy", "freezegun"]
development = [
- 'django-debug-toolbar',
- 'django-stubs',
- 'django-extensions',
+ "django-debug-toolbar",
+ "django-stubs",
+ "django-extensions",
]
-ci = ['coverage~=7.6.1']
-production = ['gunicorn~=23.0']
+ci = ["coverage~=7.6.1"]
+production = ["gunicorn~=23.0"]
[project.optional-dependencies]
-sentry = ['sentry-sdk~=2.0']
+sentry = ["sentry-sdk~=2.0"]
[tool.uv]
environments = ["sys_platform == 'linux'"]
-default-groups = ['test-tools']
+default-groups = ["test-tools"]
[tool.ruff]
-include = ['pyproject.toml', 'src/**/*.py']
+include = ["pyproject.toml", "src/**/*.py"]
line-length = 88
[tool.ruff.lint]
-select = ['E4', 'E7', 'E9', 'F', 'I']
+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 = ['transip_client']
+default-section = "third-party"
+known-first-party = ["newsreader"]
section-order = [
- 'future',
- 'standard-library',
- 'django',
- 'third-party',
- 'first-party',
- 'local-folder',
+ "future",
+ "standard-library",
+ "django",
+ "third-party",
+ "first-party",
+ "local-folder",
]
[tool.ruff.lint.isort.sections]
-django = ['django']
+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"
+]
diff --git a/src/newsreader/accounts/migrations/0018_remove_user_reddit_access_token_and_more.py b/src/newsreader/accounts/migrations/0018_remove_user_reddit_access_token_and_more.py
index 19bda0c..cf8816b 100644
--- a/src/newsreader/accounts/migrations/0018_remove_user_reddit_access_token_and_more.py
+++ b/src/newsreader/accounts/migrations/0018_remove_user_reddit_access_token_and_more.py
@@ -4,18 +4,17 @@ from django.db import migrations
class Migration(migrations.Migration):
-
dependencies = [
- ('accounts', '0017_auto_20240906_0914'),
+ ("accounts", "0017_auto_20240906_0914"),
]
operations = [
migrations.RemoveField(
- model_name='user',
- name='reddit_access_token',
+ model_name="user",
+ name="reddit_access_token",
),
migrations.RemoveField(
- model_name='user',
- name='reddit_refresh_token',
+ model_name="user",
+ name="reddit_refresh_token",
),
]
diff --git a/src/newsreader/conf/base.py b/src/newsreader/conf/base.py
index 5bee027..220e8d1 100644
--- a/src/newsreader/conf/base.py
+++ b/src/newsreader/conf/base.py
@@ -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
+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 = 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 = [
@@ -48,7 +43,7 @@ INSTALLED_APPS = [
"newsreader.news.collection",
]
-SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]
+SECRET_KEY = get_env("DJANGO_SECRET_KEY", default="")
AUTHENTICATION_BACKENDS = [
"axes.backends.AxesBackend",
@@ -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,16 +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["POSTGRES_HOST"],
- "PORT": os.environ["POSTGRES_PORT"],
- "NAME": os.environ["POSTGRES_DB"],
- "USER": os.environ["POSTGRES_USER"],
- "PASSWORD": os.environ["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=""),
}
}
@@ -114,8 +106,6 @@ CACHES = {
},
}
-# Logging
-# https://docs.djangoproject.com/en/2.2/topics/logging/#configuring-logging
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
@@ -169,8 +159,6 @@ LOGGING = {
},
}
-# Password validation
-# https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"
@@ -185,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"
@@ -194,20 +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"
+
+DEFAULT_FROM_EMAIL = get_env(
+ "EMAIL_DEFAULT_FROM", required=False, default="webmaster@localhost"
+)
+
+EMAIL_HOST = get_env("EMAIL_HOST", required=False, default="localhost")
+EMAIL_PORT = get_env("EMAIL_PORT", cast=int, required=False, default=25)
+
+EMAIL_HOST_USER = get_env("EMAIL_HOST_USER", required=False, default="")
+EMAIL_HOST_PASSWORD = get_env("EMAIL_HOST_PASSWORD", required=False, default="")
+
+EMAIL_USE_TLS = get_env("EMAIL_USE_TLS", required=False, default=False)
+EMAIL_USE_SSL = get_env("EMAIL_USE_SSL", required=False, default=False)
+
# Third party settings
AXES_HANDLER = "axes.handlers.cache.AxesCacheHandler"
@@ -216,7 +213,6 @@ AXES_FAILURE_LIMIT = 5
AXES_COOLOFF_TIME = 3 # in hours
AXES_RESET_ON_SUCCESS = True
-# TODO: verify parser works correctly
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": (
"rest_framework.authentication.SessionAuthentication",
@@ -247,7 +243,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,
"integrations": [DjangoIntegration(), CeleryIntegration()]
if DjangoIntegration and CeleryIntegration
diff --git a/src/newsreader/conf/ci.py b/src/newsreader/conf/ci.py
index 40c4a2f..e69e079 100644
--- a/src/newsreader/conf/ci.py
+++ b/src/newsreader/conf/ci.py
@@ -1,5 +1,5 @@
from .base import * # noqa: F403
-from .version import get_current_version
+from .utils import get_current_version
DEBUG = True
diff --git a/src/newsreader/conf/dev.py b/src/newsreader/conf/dev.py
index d048f6d..57aaff3 100644
--- a/src/newsreader/conf/dev.py
+++ b/src/newsreader/conf/dev.py
@@ -1,5 +1,5 @@
from .base import * # noqa: F403
-from .version import get_current_version
+from .utils import get_current_version
SECRET_KEY = "mv4&5#+)-=abz3^&1r^nk_ca6y54--p(4n4cg%z*g&rb64j%wl"
@@ -10,6 +10,11 @@ EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
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()
diff --git a/src/newsreader/conf/docker.py b/src/newsreader/conf/docker.py
index 0d6e6ee..3485bf3 100644
--- a/src/newsreader/conf/docker.py
+++ b/src/newsreader/conf/docker.py
@@ -1,8 +1,8 @@
from .base import * # noqa: F403
-from .version import get_current_version
+from .utils import get_current_version
-ALLOWED_HOSTS = ["django", "127.0.0.1"]
+DEBUG = True
INSTALLED_APPS += ["debug_toolbar", "django_extensions"] # noqa: F405
@@ -16,7 +16,10 @@ LOGGING["loggers"].update( # noqa: F405
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
-DEBUG = True
+
+TEMPLATES[0]["OPTIONS"]["context_processors"].append( # noqa: F405
+ "django.template.context_processors.debug",
+)
# Project settings
VERSION = get_current_version()
diff --git a/src/newsreader/conf/production.py b/src/newsreader/conf/production.py
index ea22f30..e4053ec 100644
--- a/src/newsreader/conf/production.py
+++ b/src/newsreader/conf/production.py
@@ -1,49 +1,17 @@
-import os
+from newsreader.conf.utils import get_env
from .base import * # noqa: F403
-from .version import get_current_version
+from .utils import get_current_version
DEBUG = False
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
-ALLOWED_HOSTS = ["127.0.0.1", "localhost", "rss.fudiggity.nl", "django"]
-
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=[])
]
-TEMPLATES = [
- {
- "BACKEND": "django.template.backends.django.DjangoTemplates",
- "DIRS": [os.path.join(DJANGO_PROJECT_DIR, "templates")], # noqa: F405
- "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"
diff --git a/src/newsreader/conf/utils.py b/src/newsreader/conf/utils.py
new file mode 100644
index 0000000..c46b59d
--- /dev/null
+++ b/src/newsreader/conf/utils.py
@@ -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
diff --git a/src/newsreader/conf/version.py b/src/newsreader/conf/version.py
deleted file mode 100644
index 806adef..0000000
--- a/src/newsreader/conf/version.py
+++ /dev/null
@@ -1,26 +0,0 @@
-import os
-import subprocess
-
-
-def get_current_version(debug=True):
- version = os.environ.get("VERSION")
-
- 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 ""
diff --git a/src/newsreader/js/components/Messages.js b/src/newsreader/js/components/Messages.js
index e3d776e..dd3b2f8 100644
--- a/src/newsreader/js/components/Messages.js
+++ b/src/newsreader/js/components/Messages.js
@@ -3,13 +3,13 @@ import React from 'react';
class Messages extends React.Component {
state = { messages: this.props.messages };
- 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) => {
diff --git a/src/newsreader/js/components/Selector.js b/src/newsreader/js/components/Selector.js
index c6b117a..8933a59 100644
--- a/src/newsreader/js/components/Selector.js
+++ b/src/newsreader/js/components/Selector.js
@@ -9,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;
diff --git a/src/newsreader/js/pages/categories/App.js b/src/newsreader/js/pages/categories/App.js
index ac237c3..db81a73 100644
--- a/src/newsreader/js/pages/categories/App.js
+++ b/src/newsreader/js/pages/categories/App.js
@@ -20,15 +20,15 @@ class App extends React.Component {
};
}
- selectCategory = (categoryId) => {
+ selectCategory = categoryId => {
this.setState({ selectedCategoryId: categoryId });
- }
+ };
deselectCategory = () => {
this.setState({ selectedCategoryId: null });
- }
+ };
- deleteCategory = (categoryId) => {
+ deleteCategory = categoryId => {
const url = `/api/categories/${categoryId}/`;
const options = {
method: 'DELETE',
@@ -56,7 +56,7 @@ class App extends React.Component {
text: 'Unable to remove category, try again later',
};
return this.setState({ selectedCategoryId: null, message: message });
- }
+ };
render() {
const { categories } = this.state;
diff --git a/src/newsreader/js/pages/homepage/components/PostModal.js b/src/newsreader/js/pages/homepage/components/PostModal.js
index 5dacdf8..e319e10 100644
--- a/src/newsreader/js/pages/homepage/components/PostModal.js
+++ b/src/newsreader/js/pages/homepage/components/PostModal.js
@@ -31,13 +31,13 @@ class PostModal extends React.Component {
window.removeEventListener('click', this.modalListener);
}
- modalListener = (e) => {
+ modalListener = e => {
const targetClassName = e.target.className;
if (this.props.post && targetClassName == 'modal post-modal') {
this.props.unSelectPost();
}
- }
+ };
render() {
const post = this.props.post;
@@ -66,7 +66,7 @@ class PostModal extends React.Component {