Merge branch 'eitchtee:main' into main

This commit is contained in:
Dimitri Decrock
2025-01-29 06:10:17 +01:00
committed by GitHub
18 changed files with 195 additions and 47 deletions

View File

@@ -23,3 +23,5 @@ WEB_CONCURRENCY=4
ENABLE_SOFT_DELETE=false ENABLE_SOFT_DELETE=false
# If ENABLE_SOFT_DELETE is true, transactions deleted for more than KEEP_DELETED_TRANSACTIONS_FOR days will be truly deleted. Set to 0 to keep all. # If ENABLE_SOFT_DELETE is true, transactions deleted for more than KEEP_DELETED_TRANSACTIONS_FOR days will be truly deleted. Set to 0 to keep all.
KEEP_DELETED_TRANSACTIONS_FOR=365 KEEP_DELETED_TRANSACTIONS_FOR=365
TASK_WORKERS=1 # This only work if you're using the single container option. Increase to have more open queues via procrastinate, you probably don't need to increase this.

View File

@@ -3,6 +3,8 @@ name: Release Pipeline
on: on:
release: release:
types: [created] types: [created]
push:
branches: [ main ]
env: env:
IMAGE_NAME: wygiwyh IMAGE_NAME: wygiwyh
@@ -29,7 +31,21 @@ jobs:
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
- name: Build and push image - name: Build and push nightly image
if: github.event_name == 'push'
uses: docker/build-push-action@v6
with:
context: .
file: ./docker/prod/django/Dockerfile
push: true
provenance: false
tags: ${{ secrets.DOCKERHUB_USERNAME }}/${{ env.IMAGE_NAME }}:nightly
platforms: linux/amd64,linux/arm64
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Build and push release image
if: github.event_name == 'release'
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
with: with:
context: . context: .

View File

@@ -222,7 +222,7 @@ SESSION_COOKIE_SECURE = os.getenv("HTTPS_ENABLED", "false").lower() == "true"
DEBUG_TOOLBAR_CONFIG = { DEBUG_TOOLBAR_CONFIG = {
"ROOT_TAG_EXTRA_ATTRS": "hx-preserve", "ROOT_TAG_EXTRA_ATTRS": "hx-preserve",
"SHOW_TOOLBAR_CALLBACK": lambda r: False, # disables it} # "SHOW_TOOLBAR_CALLBACK": lambda r: False, # disables it
} }
DEBUG_TOOLBAR_PANELS = [ DEBUG_TOOLBAR_PANELS = [
"debug_toolbar.panels.history.HistoryPanel", "debug_toolbar.panels.history.HistoryPanel",

View File

@@ -1,5 +1,8 @@
import logging import logging
from asgiref.sync import sync_to_async
from django.core import management
from procrastinate import builtin_tasks from procrastinate import builtin_tasks
from procrastinate.contrib.django import app from procrastinate.contrib.django import app
@@ -24,3 +27,16 @@ async def remove_old_jobs(context, timestamp):
exc_info=True, exc_info=True,
) )
raise e raise e
@app.periodic(cron="0 6 1 * *")
@app.task(queueing_lock="remove_expired_sessions")
async def remove_expired_sessions(timestamp=None):
"""Cleanup expired sessions by using Django management command."""
try:
await sync_to_async(management.call_command)("clearsessions", verbosity=0)
except Exception:
logger.error(
"Error while executing 'remove_expired_sessions' task",
exc_info=True,
)

View File

@@ -65,7 +65,7 @@ class CSVImportSettings(BaseModel):
class ColumnMapping(BaseModel): class ColumnMapping(BaseModel):
source: Optional[str] = Field( source: Optional[str] | Optional[list[str]] = Field(
default=None, default=None,
description="CSV column header. If None, the field will be generated from transformations", description="CSV column header. If None, the field will be generated from transformations",
) )

View File

@@ -486,8 +486,18 @@ class ImportService:
mapped_data = {} mapped_data = {}
for field, mapping in self.mapping.items(): for field, mapping in self.mapping.items():
# If source is None, use None as the initial value value = None
value = row.get(mapping.source) if mapping.source else None
if isinstance(mapping.source, str):
value = row.get(mapping.source)
elif isinstance(mapping.source, list):
for source in mapping.source:
value = row.get(source)
if value is not None:
break
else:
# If source is None, use None as the initial value
value = None
# Use default_value if value is None # Use default_value if value is None
if value is None: if value is None:

View File

@@ -30,6 +30,8 @@ def index(request):
@login_required @login_required
@require_http_methods(["GET"]) @require_http_methods(["GET"])
def monthly_overview(request, month: int, year: int): def monthly_overview(request, month: int, year: int):
order = request.session.get("monthly_transactions_order", "default")
if month < 1 or month > 12: if month < 1 or month > 12:
from django.http import Http404 from django.http import Http404
@@ -54,6 +56,7 @@ def monthly_overview(request, month: int, year: int):
"previous_month": previous_month, "previous_month": previous_month,
"previous_year": previous_year, "previous_year": previous_year,
"filter": f, "filter": f,
"order": order,
}, },
) )
@@ -62,7 +65,12 @@ def monthly_overview(request, month: int, year: int):
@login_required @login_required
@require_http_methods(["GET"]) @require_http_methods(["GET"])
def transactions_list(request, month: int, year: int): def transactions_list(request, month: int, year: int):
order = request.GET.get("order") order = request.session.get("monthly_transactions_order", "default")
if "order" in request.GET:
order = request.GET["order"]
if order != request.session.get("monthly_transactions_order", "default"):
request.session["monthly_transactions_order"] = order
f = TransactionsFilter(request.GET) f = TransactionsFilter(request.GET)
transactions_filtered = ( transactions_filtered = (

View File

@@ -313,15 +313,23 @@ def transaction_pay(request, transaction_id):
@login_required @login_required
@require_http_methods(["GET"]) @require_http_methods(["GET"])
def transaction_all_index(request): def transaction_all_index(request):
order = request.session.get("all_transactions_order", "default")
f = TransactionsFilter(request.GET) f = TransactionsFilter(request.GET)
return render(request, "transactions/pages/transactions.html", {"filter": f}) return render(
request, "transactions/pages/transactions.html", {"filter": f, "order": order}
)
@only_htmx @only_htmx
@login_required @login_required
@require_http_methods(["GET"]) @require_http_methods(["GET"])
def transaction_all_list(request): def transaction_all_list(request):
order = request.GET.get("order") order = request.session.get("all_transactions_order", "default")
if "order" in request.GET:
order = request.GET["order"]
if order != request.session.get("all_transactions_order", "default"):
request.session["all_transactions_order"] = order
transactions = Transaction.objects.prefetch_related( transactions = Transaction.objects.prefetch_related(
"account", "account",

View File

@@ -113,9 +113,9 @@
<div class="text-sm-end" _="on change trigger updated on window"> <div class="text-sm-end" _="on change trigger updated on window">
<label for="order">{% translate "Order by" %}</label> <label for="order">{% translate "Order by" %}</label>
<select class="tw-border-0 focus-visible:tw-outline-0 w-full pe-2 tw-leading-normal text-bg-tertiary tw-font-medium rounded" name="order" id="order"> <select class="tw-border-0 focus-visible:tw-outline-0 w-full pe-2 tw-leading-normal text-bg-tertiary tw-font-medium rounded" name="order" id="order">
<option value="default">{% translate 'Default' %}</option> <option value="default" {% if order == 'default' %}selected{% endif %}>{% translate 'Default' %}</option>
<option value="older">{% translate 'Oldest first' %}</option> <option value="older" {% if order == 'older' %}selected{% endif %}>{% translate 'Oldest first' %}</option>
<option value="newer">{% translate 'Newest first' %}</option> <option value="newer" {% if order == 'newer' %}selected{% endif %}>{% translate 'Newest first' %}</option>
</select> </select>
</div> </div>
</div> </div>

View File

@@ -32,9 +32,9 @@
<div class="tw-content-center" _="on change trigger updated on window"> <div class="tw-content-center" _="on change trigger updated on window">
<label for="order">{% translate "Order by" %}</label> <label for="order">{% translate "Order by" %}</label>
<select class="tw-border-0 focus-visible:tw-outline-0 w-full pe-2 tw-leading-normal text-bg-tertiary tw-font-medium rounded" name="order" id="order"> <select class="tw-border-0 focus-visible:tw-outline-0 w-full pe-2 tw-leading-normal text-bg-tertiary tw-font-medium rounded" name="order" id="order">
<option value="default">{% translate 'Default' %}</option> <option value="default" {% if order == 'default' %}selected{% endif %}>{% translate 'Default' %}</option>
<option value="older">{% translate 'Oldest first' %}</option> <option value="older" {% if order == 'older' %}selected{% endif %}>{% translate 'Oldest first' %}</option>
<option value="newer">{% translate 'Newest first' %}</option> <option value="newer" {% if order == 'newer' %}selected{% endif %}>{% translate 'Newest first' %}</option>
</select> </select>
</div> </div>
</div> </div>

View File

@@ -3,13 +3,13 @@ volumes:
wygiwyh_temp: wygiwyh_temp:
services: services:
web: &django web:
build: build:
context: . context: .
dockerfile: ./docker/dev/django/Dockerfile dockerfile: ./docker/dev/django/Dockerfile
image: wygiwyh_dev_server image: wygiwyh_dev_server
container_name: wygiwyh_dev_server container_name: wygiwyh_dev_server
command: /start command: /start-supervisor
volumes: volumes:
- ./app/:/usr/src/app/:z - ./app/:/usr/src/app/:z
- ./frontend/:/usr/src/frontend:z - ./frontend/:/usr/src/frontend:z
@@ -54,12 +54,12 @@ services:
- '${SQL_PORT}:5432' - '${SQL_PORT}:5432'
restart: unless-stopped restart: unless-stopped
procrastinate: # procrastinate:
<<: *django # <<: *django
image: wygiwyh_dev_procrastinate # image: wygiwyh_dev_procrastinate
container_name: wygiwyh_dev_procrastinate # container_name: wygiwyh_dev_procrastinate
depends_on: # depends_on:
- db # - db
ports: [ ] # ports: [ ]
command: /start-procrastinate # command: /start-procrastinate
restart: unless-stopped # restart: unless-stopped

View File

@@ -2,15 +2,13 @@ services:
web: web:
image: eitchtee/wygiwyh:latest image: eitchtee/wygiwyh:latest
container_name: ${SERVER_NAME} container_name: ${SERVER_NAME}
command: /start command: /start-single
ports: ports:
- "${OUTBOUND_PORT}:8000" - "${OUTBOUND_PORT}:8000"
env_file: env_file:
- .env - .env
depends_on: depends_on:
- db - db
volumes:
- wygiwyh_temp:/usr/src/app/temp/
restart: unless-stopped restart: unless-stopped
db: db:
@@ -23,18 +21,3 @@ services:
- POSTGRES_USER=${SQL_USER} - POSTGRES_USER=${SQL_USER}
- POSTGRES_PASSWORD=${SQL_PASSWORD} - POSTGRES_PASSWORD=${SQL_PASSWORD}
- POSTGRES_DB=${SQL_DATABASE} - POSTGRES_DB=${SQL_DATABASE}
procrastinate:
image: eitchtee/wygiwyh:latest
container_name: ${PROCRASTINATE_NAME}
depends_on:
- db
env_file:
- .env
volumes:
- wygiwyh_temp:/usr/src/app/temp/
command: /start-procrastinate
restart: unless-stopped
volumes:
wygiwyh_temp:

View File

@@ -18,7 +18,7 @@ ENV PYTHONDONTWRITEBYTECODE=1 \
COPY --from=python-build-stage /usr/src/app/wheels /wheels/ COPY --from=python-build-stage /usr/src/app/wheels /wheels/
RUN apt-get update && \ RUN apt-get update && \
apt-get install --no-install-recommends -y gettext && \ apt-get install --no-install-recommends -y gettext supervisor && \
rm -rf /var/lib/apt/lists/* && \ rm -rf /var/lib/apt/lists/* && \
pip install --upgrade pip && \ pip install --upgrade pip && \
pip install --no-cache-dir --no-index --find-links=/wheels/ /wheels/* && \ pip install --no-cache-dir --no-index --find-links=/wheels/ /wheels/* && \
@@ -26,9 +26,15 @@ RUN apt-get update && \
COPY ./docker/dev/django/start /start COPY ./docker/dev/django/start /start
COPY ./docker/dev/procrastinate/start /start-procrastinate COPY ./docker/dev/procrastinate/start /start-procrastinate
COPY ./docker/dev/supervisord/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
COPY ./docker/dev/supervisord/supervisord.conf /etc/supervisord.conf
COPY ./docker/dev/supervisord/start /start-supervisor
RUN sed -i 's/\r$//g' /start && \ RUN sed -i 's/\r$//g' /start && \
chmod +x /start && \ chmod +x /start && \
sed -i 's/\r$//g' /start-procrastinate && \ sed -i 's/\r$//g' /start-procrastinate && \
chmod +x /start-procrastinate chmod +x /start-procrastinate && \
sed -i 's/\r$//g' /start-supervisor && \
chmod +x /start-supervisor
COPY ./app . COPY ./app .

View File

@@ -0,0 +1,9 @@
#!/bin/bash
set -o errexit
set -o pipefail
set -o nounset
export TASK_WORKERS=${TASK_WORKERS:=1}
exec supervisord -c /etc/supervisor/conf.d/supervisord.conf

View File

@@ -0,0 +1,39 @@
[supervisord]
nodaemon=true
logfile=/dev/null
logfile_maxbytes=0
pidfile=/tmp/supervisord.pid
user=root
[supervisorctl]
serverurl=unix:///run/supervisord.sock
[rpcinterface:supervisor]
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface
[unix_http_server]
file=/run/supervisord.sock
chmod=0700
[program:web]
directory=/usr/src/app
command=/bin/bash /start
stdout_logfile=/dev/fd/1
stdout_logfile_maxbytes=0
stderr_logfile=/dev/fd/2
stderr_logfile_maxbytes=0
autorestart=true
startretries=5
[program:procrastinate]
directory=/usr/src/app
command=/bin/bash /start-procrastinate
process_name=%(program_name)s_%(process_num)02d
numprocs=%(ENV_TASK_WORKERS)s
numprocs_start=1
stdout_logfile=/dev/fd/1
stdout_logfile_maxbytes=0
stderr_logfile=/dev/fd/2
stderr_logfile_maxbytes=0
autorestart=true
startretries=5

View File

@@ -31,7 +31,7 @@ ENV PYTHONDONTWRITEBYTECODE=1 \
COPY --from=python-build-stage /usr/src/app/wheels /wheels/ COPY --from=python-build-stage /usr/src/app/wheels /wheels/
RUN --mount=type=cache,target=/root/.cache/apt \ RUN --mount=type=cache,target=/root/.cache/apt \
apt-get update && \ apt-get update && \
apt-get install --no-install-recommends -y gettext && \ apt-get install --no-install-recommends -y gettext supervisor && \
rm -rf /var/lib/apt/lists/* && \ rm -rf /var/lib/apt/lists/* && \
pip install --upgrade pip && \ pip install --upgrade pip && \
pip install --no-cache-dir --no-index --find-links=/wheels/ /wheels/* && \ pip install --no-cache-dir --no-index --find-links=/wheels/ /wheels/* && \
@@ -39,10 +39,15 @@ RUN --mount=type=cache,target=/root/.cache/apt \
COPY --chown=app:app ./docker/prod/django/start /start COPY --chown=app:app ./docker/prod/django/start /start
COPY --chown=app:app ./docker/prod/procrastinate/start /start-procrastinate COPY --chown=app:app ./docker/prod/procrastinate/start /start-procrastinate
COPY --chown=app:app ./docker/prod/supervisord/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
COPY --chown=app:app ./docker/prod/supervisord/supervisord.conf /etc/supervisord.conf
COPY --chown=app:app ./docker/prod/supervisord/start /start-single
RUN sed -i 's/\r$//g' /start && \ RUN sed -i 's/\r$//g' /start && \
chmod +x /start && \ chmod +x /start && \
sed -i 's/\r$//g' /start-procrastinate && \ sed -i 's/\r$//g' /start-procrastinate && \
chmod +x /start-procrastinate chmod +x /start-procrastinate && \
sed -i 's/\r$//g' /start-single && \
chmod +x /start-single
COPY --chown=app:app ./app . COPY --chown=app:app ./app .

View File

@@ -0,0 +1,9 @@
#!/bin/bash
set -o errexit
set -o pipefail
set -o nounset
export TASK_WORKERS=${TASK_WORKERS:=1}
exec supervisord -c /etc/supervisor/conf.d/supervisord.conf

View File

@@ -0,0 +1,37 @@
[supervisord]
nodaemon=true
logfile=/dev/null
logfile_maxbytes=0
pidfile=/tmp/supervisord.pid
[supervisorctl]
serverurl=unix:///tmp/supervisord.sock
[unix_http_server]
file=/tmp/supervisord.sock
chmod=0700
[program:web]
user=app
directory=/usr/src/app
command=/bin/bash /start
stdout_logfile=/dev/fd/1
stdout_logfile_maxbytes=0
stderr_logfile=/dev/fd/2
stderr_logfile_maxbytes=0
autorestart=true
startretries=5
[program:procrastinate]
user=app
directory=/usr/src/app
command=/bin/bash /start-procrastinate
process_name=%(program_name)s_%(process_num)02d
numprocs=%(ENV_TASK_WORKERS)s
numprocs_start=1
stdout_logfile=/dev/fd/1
stdout_logfile_maxbytes=0
stderr_logfile=/dev/fd/2
stderr_logfile_maxbytes=0
autorestart=true
startretries=5