diff --git a/.coveragerc b/.coveragerc
new file mode 100644
index 0000000..d1a0d79
--- /dev/null
+++ b/.coveragerc
@@ -0,0 +1,16 @@
+[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
deleted file mode 100644
index 5257a12..0000000
--- a/.editorconfig
+++ /dev/null
@@ -1,25 +0,0 @@
-# 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
new file mode 100644
index 0000000..0c8e37f
--- /dev/null
+++ b/.isort.cfg
@@ -0,0 +1,12 @@
+[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
new file mode 100644
index 0000000..146a217
--- /dev/null
+++ b/.prettierrc.json
@@ -0,0 +1,10 @@
+{
+ "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 519e330..ba795b4 100644
--- a/.woodpecker/build.yaml
+++ b/.woodpecker/build.yaml
@@ -1,10 +1,8 @@
when:
- event: push
- - event: pull_request
- - event: manual
steps:
- - image: node:lts-alpine
+ - image: node:lts
commands:
- npm install
- - npm run build:prod
+ - npm run build
diff --git a/.woodpecker/lint.yaml b/.woodpecker/lint.yaml
index 64ee04b..bc25a32 100644
--- a/.woodpecker/lint.yaml
+++ b/.woodpecker/lint.yaml
@@ -1,18 +1,19 @@
when:
- event: push
+ branch: main
- event: pull_request
- - event: manual
steps:
- name: python linting
- image: ghcr.io/astral-sh/uv:python3.11-alpine
+ image: python:3.11
commands:
+ - pip install uv
- uv sync --group ci
- - uv run --no-sync -- ruff check src/
- - uv run --no-sync -- ruff format --check src/
+ - ./.venv/bin/ruff check src/
+ - ./.venv/bin/ruff format --check src/
- name: javascript linting
- image: node:lts-alpine
+ image: node:lts
commands:
- - npm ci
+ - npm install
- npm run lint
diff --git a/.woodpecker/tests.yaml b/.woodpecker/tests.yaml
index 95092f6..fed2254 100644
--- a/.woodpecker/tests.yaml
+++ b/.woodpecker/tests.yaml
@@ -1,37 +1,36 @@
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
+ POSTGRES_NAME: newsreader
+ POSTGRES_USER: newsreader
+ POSTGRES_PASSWORD: sekrit
- name: memcached
image: memcached:1.5.22
steps:
- name: python tests
- image: ghcr.io/astral-sh/uv:python3.11-alpine
+ image: python:3.11
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
+ POSTGRES_DB: newsreader
+ POSTGRES_NAME: newsreader
+ POSTGRES_USER: newsreader
+ POSTGRES_PASSWORD: sekrit
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
+ - ./.venv/bin/coverage run ./src/manage.py test newsreader
+ - ./.venv/bin/coverage report --show-missing
- name: javascript tests
- image: node:lts-alpine
+ image: node:lts
commands:
- - npm ci
+ - npm install
- npm test
diff --git a/Dockerfile b/Dockerfile
deleted file mode 100644
index 0ffa683..0000000
--- a/Dockerfile
+++ /dev/null
@@ -1,84 +0,0 @@
-# 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 bb473e6..0006178 100755
--- a/bin/docker-entrypoint.sh
+++ b/bin/docker-entrypoint.sh
@@ -1,5 +1,5 @@
#!/bin/bash
-uv run --no-sync -- /app/src/manage.py migrate
+/app/.venv/bin/python /app/src/manage.py migrate
exec "$@"
diff --git a/docker-compose.development.yml b/docker-compose.development.yml
index 9045200..d00550b 100644
--- a/docker-compose.development.yml
+++ b/docker-compose.development.yml
@@ -1,13 +1,18 @@
volumes:
static-files:
+ node-modules:
services:
- django:
- build: &app-development-build
+ celery:
+ 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}
+ volumes:
+ - ./src/:/app/src
+
+ django:
+ build:
+ target: development
+ command: /app/.venv/bin/python /app/src/manage.py runserver 0.0.0.0:8000
ports:
- "${DJANGO_PORT:-8000}:8000"
volumes:
@@ -16,21 +21,12 @@ services:
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"
+ dockerfile: ./docker/webpack
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 24c8cd1..46a9c76 100644
--- a/docker-compose.production.yml
+++ b/docker-compose.production.yml
@@ -9,6 +9,7 @@ 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 c348d96..02f1fab 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -4,43 +4,33 @@ volumes:
postgres-data:
static-files:
-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
+ POSTGRES_HOST:
+ POSTGRES_PORT:
+ POSTGRES_DB:
+ POSTGRES_USER:
+ POSTGRES_PASSWORD:
+
+x-django-build-env: &django-build-env
+ <<: *db-env
+ DJANGO_SECRET_KEY:
+ DJANGO_SETTINGS_MODULE:
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:-""}
+ <<: *django-build-env
+ 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}
+ EMAIL_HOST:
+ EMAIL_PORT:
+ EMAIL_HOST_USER:
+ EMAIL_HOST_PASSWORD:
+ EMAIL_USE_TLS:
+ EMAIL_USE_SSL:
+ EMAIL_DEFAULT_FROM:
# Sentry
- SENTRY_DSN: ${SENTRY_DSN:-""}
+ SENTRY_DSN:
services:
db:
@@ -48,8 +38,8 @@ services:
<<: *db-env
image: postgres:15
healthcheck:
- test: /usr/bin/pg_isready
- start_period: 10s
+ # Note that --env-file should be used to set these correctly
+ test: /usr/bin/pg_isready --username="${POSTGRES_USER}" --dbname="${POSTGRES_DB}"
interval: 5s
timeout: 10s
retries: 10
@@ -65,23 +55,58 @@ services:
- memcached
- -m 64
- django:
- build: &app-build
+ 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:
+ context: .
+ dockerfile: ./docker/django
+ target: production
+ args:
+ <<: *django-build-env
environment:
<<: *django-env
- entrypoint: ["/bin/sh", "/app/bin/docker-entrypoint.sh"]
+ entrypoint: /app/bin/docker-entrypoint.sh
command: |
- uv run --no-sync --
- gunicorn
- --bind 0.0.0.0:8000
+ /app/.venv/bin/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
@@ -94,33 +119,3 @@ 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
new file mode 100644
index 0000000..6e079c8
--- /dev/null
+++ b/docker/django
@@ -0,0 +1,102 @@
+# 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
new file mode 100644
index 0000000..11c3d58
--- /dev/null
+++ b/docker/webpack
@@ -0,0 +1,10 @@
+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
new file mode 100644
index 0000000..c8473a7
--- /dev/null
+++ b/jest.config.js
@@ -0,0 +1,6 @@
+module.exports = {
+ roots: ['src/newsreader/js/tests/'],
+
+ clearMocks: true,
+ coverageDirectory: 'coverage',
+};
diff --git a/package-lock.json b/package-lock.json
index 59e4d2b..82a511b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "newsreader",
- "version": "0.5.3",
+ "version": "0.4.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "newsreader",
- "version": "0.5.3",
+ "version": "0.4.4",
"license": "GPL-3.0-or-later",
"dependencies": {
"@fortawesome/fontawesome-free": "^5.15.2",
diff --git a/package.json b/package.json
index d12a88a..3b251da 100644
--- a/package.json
+++ b/package.json
@@ -2,18 +2,19 @@
"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": "forgejo.fudiggity.nl:sonny/newsreader"
+ "url": "[git@git.fudiggity.nl:5000]:sonny/newsreader.git"
},
"author": "Sonny",
"license": "GPL-3.0-or-later",
@@ -54,22 +55,5 @@
"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 e62c754..c722183 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,81 +1,66 @@
[project]
-name = "newsreader"
-version = "0.5.3"
-authors = [{ name = "Sonny" }]
-license = { text = "GPL-3.0" }
-requires-python = ">=3.11"
+name = 'newsreader'
+version = '0.5.3'
+authors = [{ name = 'Sonny', email= 'sonny871@hotmail.com' }]
+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",
+ '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',
]
[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 = ["newsreader"]
+default-section = 'third-party'
+known-first-party = ['transip_client']
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"]
-
-[tool.coverage.run]
-source = ["./src/newsreader/"]
-omit = [
- "**/tests/**",
- "**/migrations/**",
- "**/conf/**",
- "**/apps.py",
- "**/admin.py",
- "**/tests.py",
- "**/urls.py",
- "**/wsgi.py",
- "**/celery.py",
- "**/__init__.py"
-]
+django = ['django']
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 cf8816b..19bda0c 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,17 +4,18 @@ 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 220e8d1..5bee027 100644
--- a/src/newsreader/conf/base.py
+++ b/src/newsreader/conf/base.py
@@ -1,6 +1,8 @@
-from dotenv import load_dotenv
+import os
-from newsreader.conf.utils import get_env, get_root_dir
+from pathlib import Path
+
+from dotenv import load_dotenv
load_dotenv()
@@ -13,13 +15,16 @@ except ImportError:
DjangoIntegration = None
-BASE_DIR = get_root_dir()
+BASE_DIR = Path(__file__).resolve().parent.parent.parent.parent
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 = get_env("ALLOWED_HOSTS", split=",", default=["127.0.0.1", "localhost"])
-INTERNAL_IPS = get_env("INTERNAL_IPS", split=",", default=["127.0.0.1", "localhost"])
+ALLOWED_HOSTS = ["127.0.0.1", "localhost"]
+INTERNAL_IPS = ["127.0.0.1", "localhost"]
# Application definition
INSTALLED_APPS = [
@@ -43,7 +48,7 @@ INSTALLED_APPS = [
"newsreader.news.collection",
]
-SECRET_KEY = get_env("DJANGO_SECRET_KEY", default="")
+SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]
AUTHENTICATION_BACKENDS = [
"axes.backends.AxesBackend",
@@ -68,10 +73,11 @@ FORM_RENDERER = "django.forms.renderers.TemplatesSetting"
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
- "DIRS": [DJANGO_PROJECT_DIR / "templates"],
+ "DIRS": [os.path.join(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",
@@ -82,14 +88,16 @@ 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": 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=""),
+ "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"],
}
}
@@ -106,6 +114,8 @@ CACHES = {
},
}
+# Logging
+# https://docs.djangoproject.com/en/2.2/topics/logging/#configuring-logging
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
@@ -159,6 +169,8 @@ 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"
@@ -173,6 +185,8 @@ 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"
@@ -180,31 +194,20 @@ 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 = BASE_DIR / "static"
-STATICFILES_DIRS = (DJANGO_PROJECT_DIR / "static",)
+STATIC_ROOT = os.path.join(BASE_DIR, "static")
+STATICFILES_DIRS = [os.path.join(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.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)
-
+EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
# Third party settings
AXES_HANDLER = "axes.handlers.cache.AxesCacheHandler"
@@ -213,6 +216,7 @@ 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",
@@ -243,7 +247,7 @@ CELERY_BROKER_URL = "amqp://guest@rabbitmq:5672"
# Sentry
SENTRY_CONFIG = {
- "dsn": get_env("SENTRY_DSN", default="", required=False),
+ "dsn": os.environ.get("SENTRY_DSN"),
"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 e69e079..40c4a2f 100644
--- a/src/newsreader/conf/ci.py
+++ b/src/newsreader/conf/ci.py
@@ -1,5 +1,5 @@
from .base import * # noqa: F403
-from .utils import get_current_version
+from .version import get_current_version
DEBUG = True
diff --git a/src/newsreader/conf/dev.py b/src/newsreader/conf/dev.py
index 57aaff3..d048f6d 100644
--- a/src/newsreader/conf/dev.py
+++ b/src/newsreader/conf/dev.py
@@ -1,5 +1,5 @@
from .base import * # noqa: F403
-from .utils import get_current_version
+from .version import get_current_version
SECRET_KEY = "mv4&5#+)-=abz3^&1r^nk_ca6y54--p(4n4cg%z*g&rb64j%wl"
@@ -10,11 +10,6 @@ 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 3485bf3..0d6e6ee 100644
--- a/src/newsreader/conf/docker.py
+++ b/src/newsreader/conf/docker.py
@@ -1,8 +1,8 @@
from .base import * # noqa: F403
-from .utils import get_current_version
+from .version import get_current_version
-DEBUG = True
+ALLOWED_HOSTS = ["django", "127.0.0.1"]
INSTALLED_APPS += ["debug_toolbar", "django_extensions"] # noqa: F405
@@ -16,10 +16,7 @@ LOGGING["loggers"].update( # noqa: F405
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
-
-TEMPLATES[0]["OPTIONS"]["context_processors"].append( # noqa: F405
- "django.template.context_processors.debug",
-)
+DEBUG = True
# Project settings
VERSION = get_current_version()
diff --git a/src/newsreader/conf/production.py b/src/newsreader/conf/production.py
index e4053ec..ea22f30 100644
--- a/src/newsreader/conf/production.py
+++ b/src/newsreader/conf/production.py
@@ -1,17 +1,49 @@
-from newsreader.conf.utils import get_env
+import os
from .base import * # noqa: F403
-from .utils import get_current_version
+from .version 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 get_env("ADMINS", split=",", required=False, default=[])
+ ("", email)
+ for email in os.getenv("ADMINS", "").split(",")
+ if os.environ.get("ADMINS")
]
+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
deleted file mode 100644
index c46b59d..0000000
--- a/src/newsreader/conf/utils.py
+++ /dev/null
@@ -1,85 +0,0 @@
-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
new file mode 100644
index 0000000..806adef
--- /dev/null
+++ b/src/newsreader/conf/version.py
@@ -0,0 +1,26 @@
+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 dd3b2f8..e3d776e 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 8933a59..c6b117a 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 db81a73..ac237c3 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 e319e10..5dacdf8 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 {