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 {