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 Plan |
+ Watermelon Plan |
+ Kiwi Plan |
+
+
+
+
+
+ Price |
+ $1 one-off |
+ $19/year |
+ $49/year |
+
+
+ Core functionality |
+ ✓ |
+ ✓ |
+ ✓ |
+
+
+ Email subcribers |
+ 100 |
+ 1,000 |
+ 10,000 |
+
+
+ Image hosting |
+ 100MB |
+ 250MB |
+ 1000MB |
+
+
+ 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