diff --git a/.envrc.example b/.envrc.example index e1ebd403..d3b937f2 100644 --- a/.envrc.example +++ b/.envrc.example @@ -1,5 +1,24 @@ +# Exceptions and tracebacks on errors +# 1: show +# 0: don't show export DEBUG=1 + +# Stop real emails and turn https off +# 1: stop and off +# 0: do not stop and on +export LOCALDEV=1 + +# Session cookies secret export SECRET_KEY=some-secret-key -export DATABASE_URL=postgres://mataroa:db-password@db:5432/mataroa -export EMAIL_HOST_USER=smtp-user -export EMAIL_HOST_PASSWORD=smtp-password + +# Database connection +export DATABASE_URL=postgres://mataroa:xxx@localhost:5432/mataroa + +# SMTP credentials +export EMAIL_HOST_USER= +export EMAIL_HOST_PASSWORD= + +# Stripe payments details +export STRIPE_API_KEY= +export STRIPE_PUBLIC_KEY= +export STRIPE_PRICE_ID= diff --git a/.github/workflows/django-build.yml b/.github/workflows/django-build.yml index c4adec30..051290f3 100644 --- a/.github/workflows/django-build.yml +++ b/.github/workflows/django-build.yml @@ -25,10 +25,10 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Set up Python 3.10 + - name: Set up Python uses: actions/setup-python@v4 with: - python-version: '3.10' + python-version: '3.11' - name: Install Dependencies run: | python -m pip install --upgrade pip @@ -43,6 +43,6 @@ jobs: - name: Lint run: | touch .envrc - pip install -r requirements_dev.txt + pip install -r requirements.dev.txt pip install -r requirements.txt - make lint + ruff check . diff --git a/.gitignore b/.gitignore index a53424d4..b700bf26 100644 --- a/.gitignore +++ b/.gitignore @@ -13,11 +13,6 @@ postgres-data/ .coverage htmlcov/ -# uwsgi -uwsgi.ini -uwsgi-log.txt -mataroa.pid - # docker docker-postgres-data/ docker-compose.override.yml diff --git a/Dockerfile b/Dockerfile index 7287efaa..d14e3a8a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,8 +12,8 @@ RUN apt-get update && \ && rm -rf /var/lib/apt/lists/* COPY requirements.txt /code/ -COPY requirements_dev.txt /code/ -RUN pip install -U pip && pip install -Ur /code/requirements.txt && pip install -Ur /code/requirements_dev.txt +COPY requirements.dev.txt /code/ +RUN pip install -U pip && pip install -Ur /code/requirements.txt && pip install -Ur /code/requirements.dev.txt WORKDIR /code COPY . /code/ diff --git a/Makefile b/Makefile deleted file mode 100644 index b67d4493..00000000 --- a/Makefile +++ /dev/null @@ -1,34 +0,0 @@ -.PHONY: all -all: format lint cov - -.PHONY: format -format: - $(info Formating Python code) - black --exclude '/\.venv/' . - isort --profile black . - -.PHONY: lint -lint: - $(info Running Python linters) - flake8 --exclude=.venv/ --ignore=E203,E501,W503 - isort --check-only --profile black . - black --check --exclude '/\.venv/' . - shellcheck -x *.sh - -.PHONY: test -test: - $(info Running test suite) - python -Wall manage.py test - -.PHONY: cov -cov: - $(info Generating coverage report) - coverage run --source='.' --omit '.venv/*' manage.py test - coverage report -m - -.PHONY: upgrade -upgrade: - $(info Running pip-compile -U) - pip-compile -U requirements.in - pip install --upgrade pip - pip install -r requirements.txt diff --git a/README.md b/README.md index 0356c9af..ae9008d0 100644 --- a/README.md +++ b/README.md @@ -95,7 +95,7 @@ volume, located in the root of the project. ``` python3 -m venv .venv source .venv/bin/activate -pip install -r requirements_dev.txt +pip install -r requirements.dev.txt pip install -r requirements.txt ``` @@ -197,23 +197,35 @@ python manage.py test For coverage, run: ```sh -make cov +coverage run --source='.' --omit '.venv/*' manage.py test +coverage report -m ``` ## Code linting & formatting -The following tools are used for code linting and formatting: +We use [ruff](https://github.com/astral-sh/ruff) for Python code formatting and linting. -* [black](https://github.com/psf/black) for code formatting -* [isort](https://github.com/pycqa/isort) for imports order consistency -* [flake8](https://gitlab.com/pycqa/flake8) for code linting -* [shellcheck](https://github.com/koalaman/shellcheck) for shell scripts +To format: -To use: +```sh +ruff format +``` + +To lint: ```sh -make format -make lint +ruff check +ruff check --fix +``` + +## Python dependencies + +We use [pip-tools](https://github.com/jazzband/pip-tools) to manage our Python dependencies: + +```sh +pip-compile -U requirements.in +pip install --upgrade pip +pip install -r requirements.txt ``` ## Deployment @@ -221,20 +233,12 @@ make lint See the [Deployment](./docs/deployment.md) document for an overview on steps required to deploy a mataroa instance. -See the [Server Playbook](./docs/server-playbook.md) document for a detailed -run through of setting up a mataroa instance on an Ubuntu 22.04 LTS system -using [uWSGI](https://uwsgi.readthedocs.io/en/latest/) and -[Caddy](https://caddyserver.com/). - -See the [Server Migration](./docs/server-migration.md) document for a guide on -how to migrate servers. - ### Useful Commands -To reload the uWSGI process: +To reload the gunicorn process: ```sh -sudo systemctl reload mataroa.uwsgi +sudo systemctl reload mataroa ``` To reload Caddy: @@ -243,10 +247,10 @@ To reload Caddy: systemctl restart caddy # root only ``` -uWSGI logs: +gunicorn logs: ```sh -journalctl -fb -u mataroa.uwsgi +journalctl -fb -u mataroa ``` Caddy logs: @@ -259,7 +263,7 @@ Get an overview with systemd status: ```sh systemctl status caddy -systemctl status mataroa.uwsgi +systemctl status mataroa ``` ## Backup diff --git a/ansible/.envrc.example b/ansible/.envrc.example new file mode 100644 index 00000000..e4496234 --- /dev/null +++ b/ansible/.envrc.example @@ -0,0 +1,39 @@ +# inventory.yaml + +# Server IP and user with ssh access +export ANSIBLE_HOST= +export ANSIBLE_USER=root + + +# vars.yaml + +# Domain name and email for Caddy +export DOMAIN=mataroa.blog +export EMAIL=admin@mataroa.blog + +# Show exceptions and tracebacks on errors +# 1: show +# 0: don't show +export DEBUG=1 + +# Stop real emails and turn https off +# 1: stop and off +# 0: do not stop and on +export LOCALDEV=1 + +# Session cookies secret +export SECRET_KEY=some-secret-key + +# Database connection +export DATABASE_URL=postgres://mataroa:xxx@localhost:5432/mataroa +export POSTGRES_USERNAME=mataroa +export POSTGRES_PASSWORD=xxx + +# SMTP credentials +export EMAIL_HOST_USER= +export EMAIL_HOST_PASSWORD= + +# Stripe payments details +export STRIPE_API_KEY= +export STRIPE_PUBLIC_KEY= +export STRIPE_PRICE_ID= diff --git a/ansible/Caddyfile.j2 b/ansible/Caddyfile.j2 new file mode 100644 index 00000000..1eb5a109 --- /dev/null +++ b/ansible/Caddyfile.j2 @@ -0,0 +1,14 @@ +{{ domain }} { + route { + file_server /static/* { + root /var/www/mataroa + } + reverse_proxy 127.0.0.1:5000 + } + + tls {{ email }} { + on_demand + } + + encode zstd gzip +} diff --git a/ansible/ansible.cfg b/ansible/ansible.cfg new file mode 100644 index 00000000..f9e6eec0 --- /dev/null +++ b/ansible/ansible.cfg @@ -0,0 +1,3 @@ +[defaults] +inventory = inventory.yaml +pipelining = True diff --git a/ansible/inventory.yaml b/ansible/inventory.yaml new file mode 100644 index 00000000..17a3ccc3 --- /dev/null +++ b/ansible/inventory.yaml @@ -0,0 +1,5 @@ +virtualmachines: + hosts: + main: + ansible_host: "{{ lookup('env', 'ANSIBLE_HOST') }}" + ansible_user: "{{ lookup('env', 'ANSIBLE_USER') }}" diff --git a/ansible/mataroa.service.j2 b/ansible/mataroa.service.j2 new file mode 100644 index 00000000..993aee4f --- /dev/null +++ b/ansible/mataroa.service.j2 @@ -0,0 +1,27 @@ +[Unit] +Description=mataroa +After=network.target + +[Service] +Type=simple +User=deploy +Group=www-data +WorkingDirectory=/var/www/mataroa +ExecStart=/var/www/mataroa/.venv/bin/gunicorn -b 127.0.0.1:5000 -w 4 mataroa.wsgi +ExecReload=/bin/kill -HUP $MAINPID +Environment="DOMAIN={{ domain }}" +Environment="EMAIL={{ email }}" +Environment="DEBUG={{ debug }}" +Environment="LOCALDEV={{ localdev }}" +Environment="SECRET_KEY={{ secret_key }}" +Environment="DATABASE_URL={{ database_url }}" +Environment="EMAIL_HOST_USER={{ email_host_user }}" +Environment="EMAIL_HOST_PASSWORD={{ email_host_password }}" +Environment="STRIPE_API_KEY={{ stripe_api_key }}" +Environment="STRIPE_PUBLIC_KEY={{ stripe_public_key }}" +Environment="STRIPE_PRICE_ID={{ stripe_price_id }}" +TimeoutSec=15 +Restart=always + +[Install] +WantedBy=multi-user.target diff --git a/ansible/playbook.yaml b/ansible/playbook.yaml new file mode 100644 index 00000000..a604dead --- /dev/null +++ b/ansible/playbook.yaml @@ -0,0 +1,160 @@ +--- +- hosts: virtualmachines + vars_files: + - vars.yaml + become: yes + tasks: + # smoke test and essential dependencies + - name: ping + ansible.builtin.ping: + - name: essentials + ansible.builtin.apt: + update_cache: yes + name: + - gcc + - git + - libpq-dev + - postgresql + - python3-psycopg2 + - python3.11 + - python3.11-dev + - python3.11-venv + - vim + state: present + + # caddy + - name: add caddy key + ansible.builtin.apt_key: + id: 65760C51EDEA2017CEA2CA15155B6D79CA56EA34 + url: https://dl.cloudsmith.io/public/caddy/stable/gpg.key + keyring: /etc/apt/trusted.gpg.d/caddy-stable.gpg + state: present + - name: add caddy deb repository + ansible.builtin.apt_repository: + repo: deb [signed-by=/etc/apt/trusted.gpg.d/caddy-stable.gpg] https://dl.cloudsmith.io/public/caddy/stable/deb/debian any-version main + - name: add caddy deb-src repository + ansible.builtin.apt_repository: + repo: deb [signed-by=/etc/apt/trusted.gpg.d/caddy-stable.gpg] https://dl.cloudsmith.io/public/caddy/stable/deb/debian any-version main + - name: install caddy + ansible.builtin.apt: + update_cache: yes + name: caddy + - name: caddyfile + ansible.builtin.template: + src: Caddyfile.j2 + dest: /etc/caddy/Caddyfile + owner: root + group: root + mode: '0644' + + # deploy user and directory + - name: www directory + ansible.builtin.file: + path: /var/www + state: directory + mode: '0755' + - name: create user + ansible.builtin.user: + name: deploy + password: "" + shell: /bin/bash + groups: + - sudo + - www-data + append: yes + createhome: yes + skeleton: '/etc/skel' + generate_ssh_key: yes + ssh_key_type: 'ed25519' + - name: www ownership + ansible.builtin.file: + path: /var/www + owner: deploy + group: www-data + recurse: yes + + # postgresql setup + - name: pg user + community.general.postgresql_user: + name: "{{ postgres_username }}" + password: "{{ postgres_password }}" + expires: infinity + state: present + become_user: postgres + - name: pg database + community.general.postgresql_db: + name: mataroa + owner: "{{ postgres_username }}" + state: present + become_user: postgres + - name: pg permissions + community.postgresql.postgresql_privs: + db: mataroa + privs: ALL + objs: ALL_IN_SCHEMA + role: "{{ postgres_username }}" + grant_option: true + become_user: postgres + + # repository + - name: clone + ansible.builtin.git: + repo: https://github.com/mataroa-blog/mataroa + dest: /var/www/mataroa + version: ansible + accept_hostkey: true + become_user: deploy + - name: dependencies + ansible.builtin.pip: + virtualenv_command: python3 -m venv .venv + virtualenv: /var/www/mataroa/.venv + requirements: /var/www/mataroa/requirements.txt + become_user: deploy + + # systemd + - name: systemd template + ansible.builtin.template: + src: mataroa.service.j2 + dest: /etc/systemd/system/mataroa.service + owner: root + group: root + mode: '0644' + - name: systemd reload + ansible.builtin.systemd: + daemon_reload: true + - name: systemd enable + ansible.builtin.systemd: + name: mataroa + enabled: yes + - name: systemd start + ansible.builtin.systemd: + name: mataroa + state: restarted + + # deployment specific + - name: collectstatic + ansible.builtin.shell: + cmd: | + source .venv/bin/activate + python3 manage.py collectstatic --no-input + chdir: /var/www/mataroa + args: + executable: /bin/bash + become_user: deploy + - name: migrations + ansible.builtin.shell: + cmd: | + source .venv/bin/activate + DATABASE_URL='{{ database_url }}' python3 manage.py migrate --no-input + chdir: /var/www/mataroa + args: + executable: /bin/bash + become_user: deploy + - name: gunicorn restart + ansible.builtin.systemd: + name: mataroa + state: restarted + - name: caddy restart + ansible.builtin.systemd: + name: caddy + state: restarted diff --git a/ansible/vars.yaml b/ansible/vars.yaml new file mode 100644 index 00000000..efaad82a --- /dev/null +++ b/ansible/vars.yaml @@ -0,0 +1,19 @@ +--- +domain: "{{ lookup('env', 'DOMAIN') }}" +email: "{{ lookup('env', 'EMAIL') }}" + +debug: "{{ lookup('env', 'DEBUG') }}" +localdev: "{{ lookup('env', 'LOCALDEV') }}" + +secret_key: "{{ lookup('env', 'SECRET_KEY') }}" + +database_url: "{{ lookup('env', 'DATABASE_URL') }}" +postgres_username: "{{ lookup('env', 'POSTGRES_USERNAME') }}" +postgres_password: "{{ lookup('env', 'POSTGRES_PASSWORD') }}" + +email_host_user: "{{ lookup('env', 'EMAIL_HOST_USER') }}" +email_host_password: "{{ lookup('env', 'EMAIL_HOST_PASSWORD') }}" + +stripe_api_key: "{{ lookup('env', 'STRIPE_API_KEY') }}" +stripe_public_key: "{{ lookup('env', 'STRIPE_PUBLIC_KEY') }}" +stripe_price_id: "{{ lookup('env', 'STRIPE_PRICE_ID') }}" diff --git a/deploy.sh b/deploy.sh deleted file mode 100755 index 9487d796..00000000 --- a/deploy.sh +++ /dev/null @@ -1,55 +0,0 @@ -#!/usr/bin/env bash - -set -o errexit -set -o nounset -set -o pipefail -if [[ "${TRACE-0}" == "1" ]]; then - set -o xtrace -fi - -if [[ "${1-}" =~ ^-*h(elp)?$ ]]; then - echo 'Usage: ./deploy.sh - -This script deploys the service in the production server.' - exit -fi - -cd "$(dirname "$0")" - -main() { - # check venv is enabled - if [[ -z "${VIRTUAL_ENV}" ]]; then - exit - fi - - # make sure linting checks pass - make lint - - # static - python manage.py collectstatic --noinput - - # make sure latest requirements are installed - pip install -U pip - pip install -r requirements.txt - - # make sure tests pass - make test - - # push origin srht - git push -v srht main - - # push on github - git push -v github main - - # pull on server and reload - ssh deploy@95.217.30.133 'cd /var/www/mataroa ' \ - '&& git pull ' \ - '&& source .venv/bin/activate ' \ - '&& pip install -U pip ' \ - '&& pip install -r requirements.txt ' \ - '&& python manage.py collectstatic --noinput ' \ - '&& source .envrc && python manage.py migrate ' \ - '&& sudo systemctl reload mataroa.uwsgi' -} - -main "$@" diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index bba16644..f56cd948 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -7,7 +7,6 @@ - [File Structure Walkthrough](./file-structure-walkthrough.md) - [Dependencies](./dependencies.md) - [Deployment](./deployment.md) -- [Server Playbook](./server-playbook.md) -- [Admin and Moderation](./admin-moderation.md) +- [Cronjobs](./cronjobs.md) - [Database Backup](./database-backup.md) - [Server Migration](./server-migration.md) diff --git a/docs/src/admin-moderation.md b/docs/src/admin-moderation.md deleted file mode 100644 index 7debcf84..00000000 --- a/docs/src/admin-moderation.md +++ /dev/null @@ -1,20 +0,0 @@ -# Admin and Moderation - -There are two kinds of dashboards on mataroa. - -## Django Admin Dashboard - -One is the built-in Django admin, visitable at `/dja/`. - -## Moderation Dashboard - -Second is the custom-built Moderation dashboard, visitable at: - -* `/mod/users/new/` -* `/mod/users/active/` -* `/mod/users/active-nonnew/` -* `/mod/posts/new/` -* `/mod/pages/new/` -* `/mod/comments/` - -et al, see "moderation pages" on [main/urls.py](main/urls.py). diff --git a/docs/src/cronjobs.md b/docs/src/cronjobs.md new file mode 100644 index 00000000..89f99584 --- /dev/null +++ b/docs/src/cronjobs.md @@ -0,0 +1,14 @@ +# Cronjobs + +Two every 5/10 minutes for notifications: + +``` +*/5 * * * * bash -c 'cd /var/www/mataroa && source .venv/bin/activate && source .envrc && python manage.py enqueue_notifications' +*/10 * * * * bash -c 'cd /var/www/mataroa && source .venv/bin/activate && source .envrc && python manage.py process_notifications' +``` + +One monthly for mail exports + +``` +0 0 * * * bash -c 'cd /var/www/mataroa && source .venv/bin/activate && source .envrc && python manage.py mail_exports' +``` diff --git a/docs/src/dependencies.md b/docs/src/dependencies.md index 1e76d167..966f6053 100644 --- a/docs/src/dependencies.md +++ b/docs/src/dependencies.md @@ -17,7 +17,7 @@ Current list of top-level PyPI dependencies (source at [requirements.in](/requir * [Django](https://pypi.org/project/Django/) * [psycopg2-binary](https://pypi.org/project/psycopg2-binary/) -* [uWSGI](https://pypi.org/project/uWSGI/) +* [gunicorn](https://pypi.org/project/gunicorn/) * [Markdown](https://pypi.org/project/Markdown/) * [Pygments](https://pypi.org/project/Pygments/) * [bleach](https://pypi.org/project/bleach/) @@ -27,7 +27,7 @@ Current list of top-level PyPI dependencies (source at [requirements.in](/requir After approving a dependency, the process to add it is: -1. Assuming a venv is activated and `requirements_dev.txt` are installed. +1. Assuming a venv is activated and `requirements.dev.txt` are installed. 1. Add new dependency in [`requirements.in`](/requirements.in). 1. Run `pip-compile` to generate [`requirements.txt`](/requirements.txt) 1. Run `pip install -r requirements.txt` @@ -38,7 +38,7 @@ When a new Django version is out it’s a good idea to upgrade everything. Steps: -1. Assuming a venv is activated and `requirements_dev.txt` are installed. +1. Assuming a venv is activated and `requirements.dev.txt` are installed. 1. Run `pip-compile -U` to generate an upgraded `requirements.txt`. 1. Run `git diff requirements.txt` and spot non-patch level vesion bumps. 1. Examine release notes of each one. diff --git a/docs/src/deployment.md b/docs/src/deployment.md index a5f3286b..0190b64d 100644 --- a/docs/src/deployment.md +++ b/docs/src/deployment.md @@ -1,41 +1,56 @@ # Deployment -How to deploy a new mataroa instance? +## Step 1: Ansible -1. Get a linux server -1. Follow the [server playbook](./server-playbook.md) -1. Update [mataroa/settings](../mataroa/settings.py) - * `ADMINS` - * `CANONICAL_HOST` - * `EMAIL_HOST` and `EMAIL_HOST_BROADCAST` -1. Adjust the [deploy.sh](../deploy.sh) script - * Change IP -1. Enable `deploy` user to reload the uwsgi systemd service. To do this... +We use ansible to provision a Debian 12 Linux server. -...add `deploy` user to sudo/wheel group: +(1a) First, set up configuration files: ```sh -adduser deploy sudo -``` +cd ansible/ +# Make a copy of the example file +cp .envrc.example .envrc -Then, edit sudoers with: +# Edit parameters as required +vim .envrc -```sh -visudo +# Load variables into environment +source .envrc ``` -and add the following: +(1b) Then, provision: +```sh +ansible-playbook playbook.yaml -v ``` -# Allow deploy user to restart apps -%deploy ALL=NOPASSWD: /usr/bin/systemctl reload mataroa.uwsgi -``` -Rumours are the only way to see the results is to reboot :/ +## Step 2: Wildcard certificates + +We use Automatic DNS API integration with DNSimple: -But once you do (!) — then: +https://github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_dnsimple ```sh -sudo -i -u deploy -sudo systemctl reload mataroa.uwsgi +curl https://get.acme.sh | sh -s email=person@example.com +# Note: Installation inserts a cronjob for auto-renewal + +# Setup DNSimple API +echo 'export DNSimple_OAUTH_TOKEN="token-here"' >> /root/.acme.sh/acme.sh.env + +# Issue cert +acme.sh --issue --dns dns_dnsimple -d mataroa.blog -d *.mataroa.blog + +# We "install" (copy) the cert because we should not use the cert from acme.sh's internal store +acme.sh --install-cert -d mataroa.blog -d *.mataroa.blog --key-file /etc/caddy/mataroa-blog-key.pem --fullchain-file /etc/caddy/mataroa-blog-cert.pem --reloadcmd "chown caddy:www-data /etc/caddy/mataroa-blog-{cert,key}.pem && systemctl restart caddy" ``` + +Note: acme.sh's default SSL provider is ZeroSSL which does not accept email with +plus-subaddressing. It will not error gracefully, just fail with a cryptic +message (tested with acmesh v3.0.7). + +## Step 3: Cronjobs and Automated backups + +There are a few cronjobs that need setting up and, of course, backups are essential: + +* (3a) [Cronjobs](./cronjobs.md) +* (3b) [Database Backup](./database-backup.md) diff --git a/docs/src/file-structure-walkthrough.md b/docs/src/file-structure-walkthrough.md index 92e88173..154cacc0 100644 --- a/docs/src/file-structure-walkthrough.md +++ b/docs/src/file-structure-walkthrough.md @@ -77,8 +77,7 @@ Condensed and commented sources file tree: │   └── wsgi.py ├── requirements.in # user-editable requirements file ├── requirements.txt # pip-compile generated version-locked dependencies -├── requirements_dev.txt # user-editable development requirements -└── uwsgi.example.ini # example configuration for uWSGI +└── requirements.dev.txt # user-editable development requirements ``` ## [`main/urls.py`](/main/urls.py) diff --git a/docs/src/server-playbook.md b/docs/src/server-playbook.md deleted file mode 100644 index 43cfcd2e..00000000 --- a/docs/src/server-playbook.md +++ /dev/null @@ -1,158 +0,0 @@ -# Server Playbook - -This is a basic playbook on how to setup a new mataroa instance. - -Based and tested on Ubuntu 22.04. - -## Set editor - -Optional. - -```sh -select-editor -update-alternatives --config editor -echo 'export EDITOR=vim;' >> ~/.bashrc -source ~/.bashrc -``` - -## Set timezone - -```sh -timedatectl set-timezone UTC -``` - -## Update system - -```sh -apt update -unattended-upgrade -``` - -## Install Python and Git - -```sh -apt install -y python3 python3-dev python3-venv build-essential git -``` - -## Install Caddy - -From: https://caddyserver.com/docs/install#debian-ubuntu-raspbian - -```sh -apt install -y debian-keyring debian-archive-keyring apt-transport-https -curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg -curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | tee /etc/apt/sources.list.d/caddy-stable.list -apt update -apt install caddy -``` - -## Setup deploy user - -```sh -adduser deploy # leave password empty three times -adduser deploy caddy -adduser deploy www-data -cd /var/ -mkdir www -chown -R deploy:www-data www -``` - -## Install and setup PostgreSQL - -```sh -apt install postgresql -sudo -i -u postgres -createdb mataroa -createuser mataroa -psql -ALTER USER mataroa WITH PASSWORD 'xxx'; -exit -exit -``` - -Note: Change 'xxx' with whatever password you choose. - -## Install acme.sh and get certificates - -We use Automatic DNS API integration with DNSimple in this case, because -wildcard domain auto-renew is much harder otherwise. - -https://github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_dnsimple - -```sh -curl https://get.acme.sh | sh -s email=person@example.com -# installation also inserts a cronjob for auto-renewal - -# setup DNSimple API -echo 'export DNSimple_OAUTH_TOKEN="token-here"' >> /root/.acme.sh/acme.sh.env - -# issue cert -acme.sh --issue --dns dns_dnsimple -d mataroa.blog -d *.mataroa.blog - -# we "install" (copy) the cert because we should not use the cert from acme.sh's internal store -acme.sh --install-cert -d mataroa.blog -d *.mataroa.blog --key-file /etc/caddy/mataroa-blog-key.pem --fullchain-file /etc/caddy/mataroa-blog-cert.pem --reloadcmd "chown caddy:www-data /etc/caddy/mataroa-blog-{cert,key}.pem && systemctl restart caddy" -``` - -Note: acme.sh's default SSL provider is ZeroSSL which does not accept email with -plus-subaddressing. It will not error gracefully, just fail with a cryptic -message (tested with acmesh v3.0.7). - -## Clone repository and configure - -```sh -sudo -i -u deploy -cd /var/www/ -git clone https://git.sr.ht/~sirodoht/mataroa - -cd mataroa/ -python3 -m venv .venv -source .venv/bin/activate -pip install -r requirements.txt -python manage.py collectstatic - -# setup uwsgi -cp uwsgi.example.ini uwsgi.ini -vim uwsgi.ini # edit env variables -exit - -# setup caddy -cp Caddyfile /etc/caddy/ -sudo vim /etc/caddy/Caddyfile # edit caddyfile as required -``` - -Note: We could install uWSGI from Ubuntu's repositories (it's written in C -after all) but uWSGI has multiple extensions and compile options which change -depending on the distribution. For this reason, we install from PyPI, which is -consistent. - -## Add systemd entry - -```sh -cp /var/www/mataroa/mataroa.uwsgi.service /lib/systemd/system/mataroa.uwsgi.service - -# edit and add env variables as required -vim /lib/systemd/system/mataroa.uwsgi.service - -ln -s /lib/systemd/system/mataroa.uwsgi.service /etc/systemd/system/multi-user.target.wants/ -systemctl daemon-reload -systemctl enable mataroa.uwsgi -systemctl start mataroa.uwsgi -systemctl status mataroa.uwsgi -``` - -At this point DNS should also be set and just rebooting should result in the -instance showing the landing. - -## Setup Cronjobs - -One at 10am for email notifications (newsletters): - -``` -0 10 * * * * bash -c 'cd /var/www/mataroa && source .venv/bin/activate && source .envrc && python manage.py processnotifications' -``` - -One monthly for mail exports - -``` -0 0 * * * bash -c 'cd /var/www/mataroa && source .venv/bin/activate && source .envrc && python manage.py mail_exports' -``` diff --git a/main/models.py b/main/models.py index b8607bec..ffe379fd 100644 --- a/main/models.py +++ b/main/models.py @@ -224,17 +224,15 @@ def body_as_text(self): @property def is_draft(self): - if self.published_at: - return False - return True + return not self.published_at @property def is_published(self): + # draft case if not self.published_at: - # draft case return False - if self.published_at > timezone.now().date(): - # future publishing date case + # future publishing date case + if self.published_at > timezone.now().date(): # noqa: SIM103 return False return True diff --git a/main/templates/main/guides_pricing.html b/main/templates/main/guides_pricing.html new file mode 100644 index 00000000..328c7959 --- /dev/null +++ b/main/templates/main/guides_pricing.html @@ -0,0 +1,71 @@ +{% extends 'main/layout.html' %} + +{% load static %} + +{% block title %}Pricing — Mataroa{% endblock %} + +{% block content %} +
+

Pricing

+

+ In the interest of business transparency, in this page we explain the rationale + for our pricing. +

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Apple PlanWatermelon PlanKiwi Plan
Price$1 one-off$19/year$49/year
Core functionality
Email subcribers1001,00010,000
Image hosting100MB250MB1000MB
Custom domain
Auto-exports
+
+ +
+ +{% include 'partials/footer.html' %} + +{% endblock %} diff --git a/main/tests/test_billing.py b/main/tests/test_billing.py index 16391194..4fec4cfc 100644 --- a/main/tests/test_billing.py +++ b/main/tests/test_billing.py @@ -66,9 +66,7 @@ def test_index(self): ), patch.object( billing, "_get_payment_methods", - ), patch.object( - billing, "_get_invoices" - ): + ), patch.object(billing, "_get_invoices"): response = self.client.get(reverse("billing_index")) self.assertEqual(response.status_code, 200) self.assertContains(response, b"Free Plan") @@ -95,9 +93,7 @@ def test_index(self): billing, "_get_stripe_subscription", return_value=subscription, - ), patch.object( - billing, "_get_payment_methods" - ), patch.object( + ), patch.object(billing, "_get_payment_methods"), patch.object( billing, "_get_invoices" ): response = self.client.get(reverse("billing_index")) @@ -135,9 +131,7 @@ def test_card_add_post(self): billing, "_get_stripe_subscription", return_value=subscription, - ), patch.object( - billing, "_get_payment_methods" - ), patch.object( + ), patch.object(billing, "_get_payment_methods"), patch.object( billing, "_get_invoices" ): response = self.client.post( @@ -205,9 +199,7 @@ def test_cancel_subscription_get(self): ), patch.object( billing, "_get_payment_methods", - ), patch.object( - billing, "_get_invoices" - ): + ), patch.object(billing, "_get_invoices"): response = self.client.get(reverse("billing_subscription_cancel")) # need to check inside with context because billing_index needs @@ -224,9 +216,7 @@ def test_cancel_subscription_post(self): ), patch.object( billing, "_get_payment_methods", - ), patch.object( - billing, "_get_invoices" - ): + ), patch.object(billing, "_get_invoices"): response = self.client.post(reverse("billing_subscription_cancel")) self.assertRedirects(response, reverse("billing_index")) @@ -269,9 +259,7 @@ def test_reenable_subscription_post(self): ), patch.object( billing, "_get_payment_methods", - ), patch.object( - billing, "_get_invoices" - ): + ), patch.object(billing, "_get_invoices"): response = self.client.post(reverse("billing_subscription")) self.assertRedirects(response, reverse("billing_index")) diff --git a/main/tests/test_blog.py b/main/tests/test_blog.py index 2f3b6b98..78a694e7 100644 --- a/main/tests/test_blog.py +++ b/main/tests/test_blog.py @@ -251,9 +251,9 @@ def test_blog_export(self): response = self.client.post(reverse("export_epub")) self.assertEqual(response.status_code, 200) self.assertEqual(response["Content-Type"], "application/epub") - self.assertContains(response, "OEBPS/titlepage.xhtml".encode("utf-8")) - self.assertContains(response, "OEBPS/toc.xhtml".encode("utf-8")) - self.assertContains(response, "OEBPS/author.xhtml".encode("utf-8")) + self.assertContains(response, b"OEBPS/titlepage.xhtml") + self.assertContains(response, b"OEBPS/toc.xhtml") + self.assertContains(response, b"OEBPS/author.xhtml") class BlogNotificationListTestCase(TestCase): diff --git a/main/urls.py b/main/urls.py index f572ad49..28645e5a 100644 --- a/main/urls.py +++ b/main/urls.py @@ -14,6 +14,7 @@ path("modus/operandi/", general.operandi, name="operandi"), path("modus/transparency/", general.transparency, name="transparency"), path("modus/privacy/", general.privacy_redir, name="privacy_redir"), + path("guides/pricing/", general.guides_pricing, name="guides_pricing"), path("guides/markdown/", general.guides_markdown, name="guides_markdown"), path("guides/images/", general.guides_images, name="guides_images"), path( diff --git a/main/util.py b/main/util.py index 44f986f0..462973a1 100644 --- a/main/util.py +++ b/main/util.py @@ -161,7 +161,7 @@ def remove_control_chars(text): See http://www.unicode.org/reports/tr44/#General_Category_Values """ control_char_string = "".join(denylist.DISALLOWED_CHARACTERS) - control_char_re = re.compile("[%s]" % re.escape(control_char_string)) + control_char_re = re.compile(f"[{re.escape(control_char_string)}]") return control_char_re.sub(" ", text) diff --git a/main/views/billing.py b/main/views/billing.py index a7b347db..9104f07d 100644 --- a/main/views/billing.py +++ b/main/views/billing.py @@ -35,7 +35,7 @@ def _create_setup_intent(customer_id): ) except stripe.error.StripeError as ex: logger.error(str(ex)) - raise Exception("Failed to create setup intent on Stripe.") + raise Exception("Failed to create setup intent on Stripe.") from ex return { "stripe_client_secret": stripe_setup_intent["client_secret"], @@ -61,7 +61,7 @@ def _create_stripe_subscription(customer_id): ) except stripe.error.StripeError as ex: logger.error(str(ex)) - raise Exception("Failed to create subscription on Stripe.") + raise Exception("Failed to create subscription on Stripe.") from ex return { "stripe_subscription_id": stripe_subscription["id"], @@ -78,7 +78,7 @@ def _get_stripe_subscription(stripe_subscription_id): stripe_subscription = stripe.Subscription.retrieve(stripe_subscription_id) except stripe.error.StripeError as ex: logger.error(str(ex)) - raise Exception("Failed to get subscription from Stripe.") + raise Exception("Failed to get subscription from Stripe.") from ex return stripe_subscription @@ -94,7 +94,7 @@ def _get_payment_methods(stripe_customer_id): ).invoice_settings.default_payment_method except stripe.error.StripeError as ex: logger.error(str(ex)) - raise Exception("Failed to retrieve customer data from Stripe.") + raise Exception("Failed to retrieve customer data from Stripe.") from ex # get payment methods try: @@ -104,7 +104,7 @@ def _get_payment_methods(stripe_customer_id): ) except stripe.error.StripeError as ex: logger.error(str(ex)) - raise Exception("Failed to retrieve payment methods from Stripe.") + raise Exception("Failed to retrieve payment methods from Stripe.") from ex # normalise payment methods payment_methods = {} @@ -132,7 +132,7 @@ def _get_invoices(stripe_customer_id): stripe_invoices = stripe.Invoice.list(customer=stripe_customer_id) except stripe.error.StripeError as ex: logger.error(str(ex)) - raise Exception("Failed to retrieve invoices data from Stripe.") + raise Exception("Failed to retrieve invoices data from Stripe.") from ex # normalise invoices objects invoice_list = [] @@ -179,7 +179,7 @@ def billing_index(request): stripe_response = stripe.Customer.create() except stripe.error.StripeError as ex: logger.error(str(ex)) - raise Exception("Failed to create customer on Stripe.") + raise Exception("Failed to create customer on Stripe.") from ex request.user.stripe_customer_id = stripe_response["id"] request.user.save() @@ -338,7 +338,7 @@ def dispatch(self, request, *args, **kwargs): # check if card id is valid for user card_id = self.kwargs.get(self.slug_url_kwarg) - if card_id not in self.stripe_payment_methods.keys(): + if card_id not in self.stripe_payment_methods: mail_admins( "User tried to delete card with invalid Stripe card ID", f"user.id={request.user.id}\nuser.username={request.user.username}", @@ -360,7 +360,7 @@ def billing_card_default(request, stripe_payment_method_id): stripe_payment_methods = _get_payment_methods(request.user.stripe_customer_id) - if stripe_payment_method_id not in stripe_payment_methods.keys(): + if stripe_payment_method_id not in stripe_payment_methods: return HttpResponseBadRequest("Invalid Card ID.") stripe.api_key = settings.STRIPE_API_KEY diff --git a/main/views/export.py b/main/views/export.py index 4b619343..9cad8745 100644 --- a/main/views/export.py +++ b/main/views/export.py @@ -73,7 +73,7 @@ def export_markdown(request): def export_zola(request): if request.method == "POST": # load zola templates - with open("./export_base_zola/config.toml", "r") as zola_config_file: + with open("./export_base_zola/config.toml") as zola_config_file: zola_config = ( zola_config_file.read() .replace("example.com", f"{request.user.username}.mataroa.blog") @@ -82,13 +82,13 @@ def export_zola(request): "Example blog description", f"{request.user.blog_byline or ''}" ) ) - with open("./export_base_zola/style.css", "r") as zola_styles_file: + with open("./export_base_zola/style.css") as zola_styles_file: zola_styles = zola_styles_file.read() - with open("./export_base_zola/index.html", "r") as zola_index_file: + with open("./export_base_zola/index.html") as zola_index_file: zola_index = zola_index_file.read() - with open("./export_base_zola/post.html", "r") as zola_post_file: + with open("./export_base_zola/post.html") as zola_post_file: zola_post = zola_post_file.read() - with open("./export_base_zola/_index.md", "r") as zola_content_index_file: + with open("./export_base_zola/_index.md") as zola_content_index_file: zola_content_index = zola_content_index_file.read() # get all user posts and add them into export_posts encoded @@ -129,7 +129,7 @@ def export_zola(request): def export_hugo(request): if request.method == "POST": # load hugo templates - with open("./export_base_hugo/config.toml", "r") as hugo_config_file: + with open("./export_base_hugo/config.toml") as hugo_config_file: blog_title = request.user.blog_title or f"{request.user.username} blog" blog_byline = request.user.blog_byline or "" hugo_config = ( @@ -138,17 +138,17 @@ def export_hugo(request): .replace("Example blog title", blog_title) .replace("Example blog description", blog_byline) ) - with open("./export_base_hugo/theme.toml", "r") as hugo_theme_file: + with open("./export_base_hugo/theme.toml") as hugo_theme_file: hugo_theme = hugo_theme_file.read() - with open("./export_base_hugo/style.css", "r") as hugo_styles_file: + with open("./export_base_hugo/style.css") as hugo_styles_file: hugo_styles = hugo_styles_file.read() - with open("./export_base_hugo/single.html", "r") as hugo_single_file: + with open("./export_base_hugo/single.html") as hugo_single_file: hugo_single = hugo_single_file.read() - with open("./export_base_hugo/list.html", "r") as hugo_list_file: + with open("./export_base_hugo/list.html") as hugo_list_file: hugo_list = hugo_list_file.read() - with open("./export_base_hugo/index.html", "r") as hugo_index_file: + with open("./export_base_hugo/index.html") as hugo_index_file: hugo_index = hugo_index_file.read() - with open("./export_base_hugo/baseof.html", "r") as hugo_baseof_file: + with open("./export_base_hugo/baseof.html") as hugo_baseof_file: hugo_baseof = hugo_baseof_file.read() # get all user posts and add them into export_posts encoded @@ -302,9 +302,9 @@ def export_epub(request): epub_uuid = str(uuid.uuid4()) # load mimetype and container.xml - with open("./export_base_epub/mimetype", "r") as mimetype_file: + with open("./export_base_epub/mimetype") as mimetype_file: mimetype_content = mimetype_file.read() - with open("./export_base_epub/container.xml", "r") as container_xml_file: + with open("./export_base_epub/container.xml") as container_xml_file: container_xml_content = container_xml_file.read() # process posts @@ -329,7 +329,7 @@ def export_epub(request): + "\n" ) content_opf_spine += f' ' + "\n" - with open("./export_base_epub/content.opf", "r") as opf_content_file: + with open("./export_base_epub/content.opf") as opf_content_file: content_opf_content = opf_content_file.read() content_opf_content = content_opf_content.replace( @@ -370,7 +370,7 @@ def export_epub(request): f'
  • {chapter["title"]}
  • ' + "\n" ) - with open("./export_base_epub/toc.xhtml", "r") as toc_xhtml_file: + with open("./export_base_epub/toc.xhtml") as toc_xhtml_file: toc_xhtml_content = toc_xhtml_file.read() toc_xhtml_content = toc_xhtml_content.replace( "", toc_xhtml_body @@ -399,7 +399,7 @@ def export_epub(request): chapter_title="About the Author", chapter_link="author.xhtml", ) - with open("./export_base_epub/toc.ncx", "r") as toc_ncx_file: + with open("./export_base_epub/toc.ncx") as toc_ncx_file: toc_ncx_content = toc_ncx_file.read() toc_ncx_content = toc_ncx_content.replace( diff --git a/main/views/general.py b/main/views/general.py index 1dcabc53..18132298 100644 --- a/main/views/general.py +++ b/main/views/general.py @@ -1277,3 +1277,10 @@ def guides_customdomain(request): request, "main/guides_customdomain.html", ) + + +def guides_pricing(request): + return render( + request, + "main/guides_pricing.html", + ) diff --git a/manage.py b/manage.py index db6e4630..8c140e75 100755 --- a/manage.py +++ b/manage.py @@ -1,5 +1,6 @@ #!/usr/bin/env python """Django's command-line utility for administrative tasks.""" + import os import sys diff --git a/mataroa.uwsgi.service b/mataroa.uwsgi.service deleted file mode 100644 index cb9c4837..00000000 --- a/mataroa.uwsgi.service +++ /dev/null @@ -1,17 +0,0 @@ -[Unit] -Description=uWSGI instance to serve mataroa -Documentation=https://github.com/mataroa-blog/mataroa -After=network.target - -[Service] -Type=simple -User=deploy -Group=www-data -ExecStart=/var/www/mataroa/.venv/bin/uwsgi --ini /var/www/mataroa/uwsgi.ini -ExecReload=/bin/kill -HUP $MAINPID -WorkingDirectory=/var/www/mataroa -Environment="PATH=/var/www/mataroa/.venv/bin" -ProtectSystem=full - -[Install] -WantedBy=multi-user.target diff --git a/mataroa/settings.py b/mataroa/settings.py index 8a950000..e7e71d47 100644 --- a/mataroa/settings.py +++ b/mataroa/settings.py @@ -22,23 +22,25 @@ # See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = os.environ.get("SECRET_KEY", "nonrandom_secret") +SECRET_KEY = os.getenv("SECRET_KEY", "nonrandom_secret") # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = True if os.environ.get("DEBUG") == "1" else False +DEBUG = os.getenv("DEBUG") == "1" + +LOCALDEV = os.getenv("LOCALDEV") == "1" ALLOWED_HOSTS = [ "127.0.0.1", "localhost", - ".mataroa.blog", + f".{os.getenv('DOMAIN', 'mataroa.blog')}", ".mataroalocal.blog", "*", ] ADMINS = [("Theodore Keloglou", "zf@sirodoht.com")] -CANONICAL_HOST = "mataroa.blog" -if DEBUG: +CANONICAL_HOST = os.getenv("DOMAIN", "mataroa.blog") +if LOCALDEV: CANONICAL_HOST = "mataroalocal.blog:8000" @@ -102,7 +104,7 @@ # Database # https://docs.djangoproject.com/en/3.0/ref/settings/#databases -database_url = os.environ.get("DATABASE_URL", "") +database_url = os.getenv("DATABASE_URL", "") database_url = parse.urlparse(database_url) # e.g. postgres://mataroa:password@127.0.0.1:5432/mataroa database_name = database_url.path[1:] # url.path is '/mataroa' @@ -174,19 +176,19 @@ # Email EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend" -if DEBUG: +if LOCALDEV: EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" EMAIL_USE_TLS = True EMAIL_HOST = "smtp.postmarkapp.com" EMAIL_HOST_BROADCASTS = "smtp-broadcasts.postmarkapp.com" -EMAIL_HOST_USER = os.environ.get("EMAIL_HOST_USER") -EMAIL_HOST_PASSWORD = os.environ.get("EMAIL_HOST_PASSWORD") +EMAIL_HOST_USER = os.getenv("EMAIL_HOST_USER") +EMAIL_HOST_PASSWORD = os.getenv("EMAIL_HOST_PASSWORD") EMAIL_PORT = 587 -DEFAULT_FROM_EMAIL = "Mataroa " -NOTIFICATIONS_FROM_EMAIL = "Mataroa Notifications " -EMAIL_FROM_HOST = "mataroa.blog" -SERVER_EMAIL = "DC Parlov " +EMAIL_FROM_HOST = CANONICAL_HOST +DEFAULT_FROM_EMAIL = f"Mataroa " +NOTIFICATIONS_FROM_EMAIL = f"Mataroa Notifications " +SERVER_EMAIL = f"DC Parlov " EMAIL_SUBJECT_PREFIX = "[Mataroa Notification] " EMAIL_TEST_RECEIVE_LIST = os.environ.get("EMAIL_TEST_RECEIVE_LIST") @@ -194,7 +196,7 @@ # Security middleware -if not DEBUG: +if not LOCALDEV: SECURE_CONTENT_TYPE_NOSNIFF = True X_FRAME_OPTIONS = "DENY" SESSION_COOKIE_SECURE = True @@ -204,17 +206,17 @@ # Stripe # https://stripe.com/docs/api -STRIPE_API_KEY = os.environ.get("STRIPE_API_KEY", "") -STRIPE_PUBLIC_KEY = os.environ.get("STRIPE_PUBLIC_KEY", "") -STRIPE_PRICE_ID = os.environ.get("STRIPE_PRICE_ID", "") +STRIPE_API_KEY = os.getenv("STRIPE_API_KEY", "") +STRIPE_PUBLIC_KEY = os.getenv("STRIPE_PUBLIC_KEY", "") +STRIPE_PRICE_ID = os.getenv("STRIPE_PRICE_ID", "") # Translate -TRANSLATE_API_URL = os.environ.get( +TRANSLATE_API_URL = os.getenv( "TRANSLATE_API_URL", "https://translate.mataroa.blog/api/generate" ) -TRANSLATE_API_TOKEN = os.environ.get("TRANSLATE_API_TOKEN", "") +TRANSLATE_API_TOKEN = os.getenv("TRANSLATE_API_TOKEN", "") # Logging diff --git a/mataroa/urls.py b/mataroa/urls.py index 576c91d8..a7c1ffca 100644 --- a/mataroa/urls.py +++ b/mataroa/urls.py @@ -13,6 +13,7 @@ 1. Import the include() function: from django.urls import include, path 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ + from django.contrib import admin from django.urls import include, path diff --git a/requirements.dev.txt b/requirements.dev.txt new file mode 100644 index 00000000..a97e0337 --- /dev/null +++ b/requirements.dev.txt @@ -0,0 +1,4 @@ +pip-tools==7.4.1 +ruff==0.5.0 +coverage==7.5.4 +ansible==10.1.0 diff --git a/requirements.in b/requirements.in index ce511db4..9a92a0de 100644 --- a/requirements.in +++ b/requirements.in @@ -1,6 +1,6 @@ django psycopg2-binary -uwsgi +gunicorn markdown pygments bleach[css] diff --git a/requirements.txt b/requirements.txt index 741d3eb2..5c0e6a60 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ # This file is autogenerated by pip-compile with Python 3.12 # by the following command: # -# pip-compile requirements.in +# pip-compile # asgiref==3.7.2 # via django @@ -14,10 +14,14 @@ charset-normalizer==3.3.2 # via requests django==5.0.2 # via -r requirements.in +gunicorn==21.2.0 + # via -r requirements.in idna==3.6 # via requests markdown==3.5.2 # via -r requirements.in +packaging==24.0 + # via gunicorn psycopg2-binary==2.9.9 # via -r requirements.in pygments==2.17.2 @@ -36,8 +40,6 @@ typing-extensions==4.9.0 # via stripe urllib3==2.2.0 # via requests -uwsgi==2.0.24 - # via -r requirements.in webencodings==0.5.1 # via # bleach diff --git a/requirements_dev.txt b/requirements_dev.txt deleted file mode 100644 index a49a037e..00000000 --- a/requirements_dev.txt +++ /dev/null @@ -1,5 +0,0 @@ -pip-tools==7.3.0 -isort==5.10.1 -flake8==6.1.0 -black==23.11.0 -coverage==7.3.2 diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 00000000..f514d763 --- /dev/null +++ b/ruff.toml @@ -0,0 +1,16 @@ +[lint] +select = [ + # pycodestyle + "E", + # Pyflakes + "F", + # pyupgrade + "UP", + # flake8-bugbear + "B", + # flake8-simplify + "SIM", + # isort + "I", +] +ignore = ["E501"] # line too long diff --git a/uwsgi.example.ini b/uwsgi.example.ini deleted file mode 100644 index 18f8060d..00000000 --- a/uwsgi.example.ini +++ /dev/null @@ -1,19 +0,0 @@ -[uwsgi] -master = true -module = mataroa.wsgi:application -virtualenv = .venv -strict = true -http-socket = :5000 -need-app = true -vacuum = true -max-requests = 5000 -processes = 3 -harakiri = 120 -enable-threads = true -die-on-term = true - -env = DEBUG=1 -env = SECRET_KEY=some-secret-key -env = DATABASE_URL=postgres://mataroa:db-password@db:5432/mataroa -env = EMAIL_HOST_USER=smtp-user -env = EMAIL_HOST_PASSWORD=smtp-password