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 {