From 3b08ed73e7862a7989bbb73243fd687d4fec0157 Mon Sep 17 00:00:00 2001 From: 4-dash <120916864+4-dash@users.noreply.github.com> Date: Mon, 19 May 2025 23:55:24 +0200 Subject: [PATCH 01/32] remove binary files, wsgi rename --- .gitignore | 2 ++ src/locale/de/LC_MESSAGES/django.mo | Bin 3853 -> 0 bytes src/locale/en/LC_MESSAGES/django.mo | Bin 46184 -> 0 bytes src/{django.wsgi => wsgi.py} | 0 4 files changed, 2 insertions(+) delete mode 100644 src/locale/de/LC_MESSAGES/django.mo delete mode 100644 src/locale/en/LC_MESSAGES/django.mo rename src/{django.wsgi => wsgi.py} (100%) diff --git a/.gitignore b/.gitignore index 8e614510..573b7551 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,5 @@ venv/ .python-version node_modules/ + +*.mo \ No newline at end of file diff --git a/src/locale/de/LC_MESSAGES/django.mo b/src/locale/de/LC_MESSAGES/django.mo deleted file mode 100644 index 21fa752fae783f54f039080262e8554fe5467ea5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3853 zcmcImO^hSO6|V4)*$^NkKuF;2DhoSe+6zcX;N4m9u04yj_RKJz-4$F=xw~vvrd?%K zSC4l_iWDhQj!`Z_B8e0!N-iiDqzEa}o|qiCamy`8+~9@;ms}9vt8S0&9d8hav^4X% z-Bqu@_r34E@+Xhn^?QNqB>sMizn_0kh_k@eJMa(JXTU4K&w*ROm+lnehrr(ep8);= z*aH3ocmnt}@CU#L?h@ir;FG}nfX@T@`&WSX16$?uF7O`Q4}rYS0`CRh06q@&x1La>(r| zAYTs`5d6a*S$Fm->n-Z!huS64SUSH?GLCI-JoTL&(djc|XZLiX+)746nNd$EL$NhR z#a<=kE$!RfO*d7)d#prfvvA>^w*)NdTq@qFt;s|^Nl4O8AiWhx*Nau6Zb&(29oaa} zodd(kP^XHd_i8lu+6GWnxX4%BLM3iI}|*vtzqw$;e)DbdBMkX=^~47H3Xk*x7h zo4GGOTpb6@$6{xw<^xZQvZQX+Z(loKm)kGb8OhWsnd~o`LQdp8FwQs$=OPrg+olBpnp*+1zvK~zwO9Tt$89QpBoYQoV_s^0zY=&U>S$*F&a-`5~(>6S^tGAWYXENUBYg5~21R|Frgp+K`__$WA(vPhnx=3|2TxNn`(*pK z1X$81@H9&tdd>KBA70y2Wb86!v#I#S+(5LWEDLE86=Twbp#p|z9&S{0>VTyNPaZTZ zK*`Y>#TW0ojcbBqdRcbMPykdf>C254`a#tN`gs~Q@;$7z@4V~(*ojQFc42HLJ zm#$=|E|dzLKPxX(#g?$p$)! zF6M(Q+OajNY;8u{YESc?s3E~W^%sWel68i>jtwm zV9;rGnlsujRG+W#tU{YaJNsks@#=;@J5HrG=O`XZmnpw?b!R<#dA5#ep`W9siEV25SSzcM8Hd|!?ZtEOB0pI>W9Z}>y|8kwBATdT!)foO>1o*0?Sh1* zJ!=-m8%=VTC=|?73w0UMm13E4z1$?qRKH^&jz1r0pghKs@l_E5vo1Ytk{r|C+%EJ` zn}fW6c$1f0RC+}0fjl0r!jJKg4sZ69Iqc2LDIDKccVkHyT6&k|f+~Zdu2TZzw>cQNYf?`9)LkEJ0kFy7H zX-~O{Vil>Y6;r{dP!p_fAAXR9ZC&jT3OS0fcs7e)j0= zorqa9)2OHPVzx%SHBlO8)GiuZnP_@S_4NV7g4Q^~6})5;N+fo#2A3F>tEI9XYng+b zE6X-!C;dNT1;5XFbLNBcREi3fS%HPqs)901`v50bnh{Q@0~nLK>`{;3n};{?YpF0M zMhnJ#0@L95g1DFmJYw=xa?{|@;S_fdKfupWKFQF6OsOb0+kCof$-uz|CO1D3WN04l z5e_><0SqC^0enQe95h%5U$X9Hx2)m^PBEl_+3_hAidmGR2pCvm)AHuo><43NEpyyq16Z58_rn1j#!keNugN_~j{^V)Lg U+Z}vOZTR3-hL1k}*??L34gM*MP5=M^ diff --git a/src/locale/en/LC_MESSAGES/django.mo b/src/locale/en/LC_MESSAGES/django.mo deleted file mode 100644 index fbe24ad2e7ab3081b74e3d8aaeaaa7aa9188eafd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 46184 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

5=NlV};>FbUz(IOe!N%C@-C|ufGo{UAuJc8y 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 Date: Mon, 19 May 2025 23:55:40 +0200 Subject: [PATCH 02/32] add Dockerfile --- .dockerignore | 27 +++++++++++++++++++++ Dockerfile | 62 ++++++++++++++++++++++++++++++++++++++++++++++++ requirements.txt | 1 + 3 files changed, 90 insertions(+) create mode 100644 .dockerignore create mode 100644 Dockerfile diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..573b7551 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,27 @@ +latex/anschreiben/veranstalter.adr +# Ignore all files in db folder +db/* +# But keep the folder +!db/.gitkeep + +*.pyc +htmlcov +pyenv +*.aux +*.log +.idea +*.pdf +*.adr +.project +.pydevproject +.settings/* +.coverage +settings_secret.py +static/* +venv/ +.vscode/* +.python-version + +node_modules/ + +*.mo \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..e9d8850f --- /dev/null +++ b/Dockerfile @@ -0,0 +1,62 @@ +# Stage 1 +FROM python:3.13-slim AS builder + +RUN mkdir /app + +WORKDIR /app + +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 + +RUN pip install --upgrade pip + +RUN apt-get update && apt-get install -y \ + curl \ + gnupg \ + ca-certificates \ + && curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \ + && apt-get install -y nodejs \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +COPY requirements.txt /app/ + +RUN pip install --no-cache-dir -r requirements.txt + +COPY package*.json ./ + +RUN npm i + +# Stage 2 +FROM python:3.13-slim + +RUN useradd -m -r appuser && \ + mkdir /app && \ + chown -R appuser /app + +COPY --from=builder /usr/local/lib/python3.13/site-packages/ /usr/local/lib/python3.13/site-packages/ +COPY --from=builder /usr/local/bin/ /usr/local/bin/ + +RUN apt-get update && apt-get install -y gettext \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + + +WORKDIR /app + +COPY --chown=appuser:appuser . . + +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 + +WORKDIR /app/src + +RUN python manage.py migrate --no-input +RUN python manage.py compilemessages + +USER appuser + +EXPOSE 8000 + + +CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "3", "wsgi:application"] \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 7cd115d3..eb640240 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,3 +4,4 @@ django-debug-toolbar==6.3.0 freezegun==1.5.5 django-formtools==2.5.1 django-allauth[socialaccount,keycloak]==65.16.1 +gunicorn==26.0.0 \ No newline at end of file From 427b09bf0858b8ec37ccbddcbe7f3b99eec011be Mon Sep 17 00:00:00 2001 From: 4-dash <120916864+4-dash@users.noreply.github.com> Date: Tue, 20 May 2025 23:59:35 +0200 Subject: [PATCH 03/32] add docker compose and .env --- .env | 11 +++++++++++ docker-compose.yml | 25 +++++++++++++++++++++++++ src/settings_production.py | 22 +++++++++++----------- src/wsgi.py | 4 +++- 4 files changed, 50 insertions(+), 12 deletions(-) create mode 100644 .env create mode 100644 docker-compose.yml diff --git a/.env b/.env new file mode 100644 index 00000000..31fa3b69 --- /dev/null +++ b/.env @@ -0,0 +1,11 @@ +SETTINGS_FILE=settings +DEBUG=False +ALLOWED_HOSTS=.fachschaft.informatik.tu-darmstadt.de,.d120.de +SECRET_KEY= +KEYCLOAK_CLIENT_ID= +KEYCLOAK_SECRET= +KEYCLOAK_SERVER_URL= +EMAIL_HOST=mail.d120.de +EMAIL_PORT=587 +EMAIL_HOST_USER=pyfeedback +EMAIL_HOST_PASSWORD= \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..b85d87b1 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,25 @@ +services: + django-web: + build: . + container_name: pyfeedback-docker + ports: + - "8000:8000" + environment: + - SETTINGS_FILE=${SETTINGS_FILE} + - DEBUG=${DEBUG} + - ALLOWED_HOSTS=${ALLOWED_HOSTS} + - SECRET_KEY=${SECRET_KEY} + - KEYCLOAK_CLIENT_ID=${KEYCLOAK_CLIENT_ID} + - KEYCLOAK_SECRET=${KEYCLOAK_SECRET} + - KEYCLOAK_SERVER_URL=${KEYCLOAK_SERVER_URL} + - EMAIL_HOST=${EMAIL_HOST} + - EMAIL_PORT=${EMAIL_PORT} + - EMAIL_HOST_USER=${EMAIL_HOST_USER} + - EMAIL_HOST_PASSWORD=${EMAIL_HOST_PASSWORD} + env_file: + - .env + volumes: + - db_data:/app/db/ + +volumes: + db_data: \ No newline at end of file diff --git a/src/settings_production.py b/src/settings_production.py index ba6f04ea..e5096749 100644 --- a/src/settings_production.py +++ b/src/settings_production.py @@ -1,11 +1,11 @@ from settings import * -import settings_secret as secrets +import os -DEBUG = False +DEBUG = os.getenv("DEBUG") -ALLOWED_HOSTS = ['.fachschaft.informatik.tu-darmstadt.de', '.d120.de'] +ALLOWED_HOSTS = os.getenv("ALLOWED_HOSTS").split(",") -SECRET_KEY = secrets.SECRET_KEY +SECRET_KEY = os.getenv("SECRET_KEY") URL_PREFIX = 'feedback/' @@ -20,10 +20,10 @@ { "provider_id": "keycloak", "name": "Keycloak", - "client_id": secrets.KEYCLOAK_CLIENT_ID, - "secret": secrets.KEYCLOAK_SECRET, + "client_id": os.getenv("KEYCLOAK_CLIENT_ID"), + "secret": os.getenv("KEYCLOAK_SECRET"), "settings": { - "server_url": secrets.KEYCLOAK_SERVER_URL, + "server_url": os.getenv("KEYCLOAK_SERVER_URL"), }, } ] @@ -33,11 +33,11 @@ # @see https://docs.djangoproject.com/es/1.9/topics/email/ EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' -EMAIL_HOST = 'mail.d120.de' -EMAIL_PORT = 587 +EMAIL_HOST = os.getenv("EMAIL_HOST") +EMAIL_PORT = int(os.getenv("EMAIL_PORT")) EMAIL_USE_TLS = True -EMAIL_HOST_USER = 'pyfeedback' -EMAIL_HOST_PASSWORD = secrets.EMAIL_HOST_PASSWORD +EMAIL_HOST_USER = os.getenv("EMAIL_HOST_USER") +EMAIL_HOST_PASSWORD = os.getenv("EMAIL_HOST_PASSWORD") SESSION_COOKIE_PATH = '/feedback' SESSION_COOKIE_SECURE = True diff --git a/src/wsgi.py b/src/wsgi.py index ac4b0cc0..872c4101 100644 --- a/src/wsgi.py +++ b/src/wsgi.py @@ -2,7 +2,9 @@ from django.core.wsgi import get_wsgi_application +# select in .env which settings to use +settings_file = os.getenv("SETTINGS_FILE", "settings") -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings") +os.environ.setdefault("DJANGO_SETTINGS_MODULE", settings_file) application = get_wsgi_application() From 26ac563dfc419f0515ff4b2e402be8d67ef12dd3 Mon Sep 17 00:00:00 2001 From: 4-dash <120916864+4-dash@users.noreply.github.com> Date: Wed, 21 May 2025 00:29:26 +0200 Subject: [PATCH 04/32] update Dockerfile --- Dockerfile | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/Dockerfile b/Dockerfile index e9d8850f..64d73022 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -# Stage 1 +## Stage 1: build stage FROM python:3.13-slim AS builder RUN mkdir /app @@ -8,6 +8,7 @@ WORKDIR /app ENV PYTHONDONTWRITEBYTECODE=1 ENV PYTHONUNBUFFERED=1 +# install packages and node RUN pip install --upgrade pip RUN apt-get update && apt-get install -y \ @@ -19,31 +20,36 @@ RUN apt-get update && apt-get install -y \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* +# install pip requirements COPY requirements.txt /app/ RUN pip install --no-cache-dir -r requirements.txt +# install node_modules COPY package*.json ./ RUN npm i -# Stage 2 +## Stage 2: production stage FROM python:3.13-slim RUN useradd -m -r appuser && \ mkdir /app && \ chown -R appuser /app +# Copy the python dependencies and node_modules from the builder stage COPY --from=builder /usr/local/lib/python3.13/site-packages/ /usr/local/lib/python3.13/site-packages/ COPY --from=builder /usr/local/bin/ /usr/local/bin/ +COPY --from=builder /app/node_modules/ /app/node_modules/ +# install gettext for translations (compiling) RUN apt-get update && apt-get install -y gettext \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* - WORKDIR /app +# copy the code COPY --chown=appuser:appuser . . ENV PYTHONDONTWRITEBYTECODE=1 @@ -51,12 +57,16 @@ ENV PYTHONUNBUFFERED=1 WORKDIR /app/src -RUN python manage.py migrate --no-input +# compile translations RUN python manage.py compilemessages +# migrate db changes +RUN python manage.py migrate --no-input + +# switch to non-root user USER appuser EXPOSE 8000 - +# start applicaiton with gunicorn CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "3", "wsgi:application"] \ No newline at end of file From f0408054ad6d0816957f65e21d3cec9a83b44050 Mon Sep 17 00:00:00 2001 From: 4-dash <120916864+4-dash@users.noreply.github.com> Date: Thu, 22 May 2025 21:34:42 +0200 Subject: [PATCH 05/32] dockerfile fix --- Dockerfile | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index 64d73022..e9a86cd0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -57,15 +57,15 @@ ENV PYTHONUNBUFFERED=1 WORKDIR /app/src +# switch to non-root user +USER appuser + # compile translations -RUN python manage.py compilemessages +RUN django-admin compilemessages # migrate db changes RUN python manage.py migrate --no-input -# switch to non-root user -USER appuser - EXPOSE 8000 # start applicaiton with gunicorn From 61495e8972f523695e11335015671150ca09da8f Mon Sep 17 00:00:00 2001 From: 4-dash <120916864+4-dash@users.noreply.github.com> Date: Thu, 22 May 2025 23:41:45 +0200 Subject: [PATCH 06/32] bug fix --- src/settings_production.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/settings_production.py b/src/settings_production.py index e5096749..939eb06e 100644 --- a/src/settings_production.py +++ b/src/settings_production.py @@ -1,7 +1,7 @@ from settings import * import os -DEBUG = os.getenv("DEBUG") +DEBUG = (os.getenv("DEBUG", "False") == "True") # os.getenv only returns a string ALLOWED_HOSTS = os.getenv("ALLOWED_HOSTS").split(",") From 0a2c05e8e980d55535a5d372328bb105f373be43 Mon Sep 17 00:00:00 2001 From: 4-dash <120916864+4-dash@users.noreply.github.com> Date: Mon, 1 Dec 2025 23:05:31 +0100 Subject: [PATCH 07/32] minor change --- .env | 3 +-- docker-compose.yml | 3 +-- src/settings_production.py | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/.env b/.env index 31fa3b69..ec31d8b5 100644 --- a/.env +++ b/.env @@ -1,5 +1,4 @@ SETTINGS_FILE=settings -DEBUG=False ALLOWED_HOSTS=.fachschaft.informatik.tu-darmstadt.de,.d120.de SECRET_KEY= KEYCLOAK_CLIENT_ID= @@ -8,4 +7,4 @@ KEYCLOAK_SERVER_URL= EMAIL_HOST=mail.d120.de EMAIL_PORT=587 EMAIL_HOST_USER=pyfeedback -EMAIL_HOST_PASSWORD= \ No newline at end of file +EMAIL_HOST_PASSWORD= diff --git a/docker-compose.yml b/docker-compose.yml index b85d87b1..343f1356 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,7 +6,6 @@ services: - "8000:8000" environment: - SETTINGS_FILE=${SETTINGS_FILE} - - DEBUG=${DEBUG} - ALLOWED_HOSTS=${ALLOWED_HOSTS} - SECRET_KEY=${SECRET_KEY} - KEYCLOAK_CLIENT_ID=${KEYCLOAK_CLIENT_ID} @@ -22,4 +21,4 @@ services: - db_data:/app/db/ volumes: - db_data: \ No newline at end of file + db_data: diff --git a/src/settings_production.py b/src/settings_production.py index 939eb06e..ccc1db5c 100644 --- a/src/settings_production.py +++ b/src/settings_production.py @@ -1,7 +1,7 @@ from settings import * import os -DEBUG = (os.getenv("DEBUG", "False") == "True") # os.getenv only returns a string +DEBUG = False ALLOWED_HOSTS = os.getenv("ALLOWED_HOSTS").split(",") From c706cfee6d5c923e6a2bf8ec61640536b9ca2566 Mon Sep 17 00:00:00 2001 From: 4-dash <120916864+4-dash@users.noreply.github.com> Date: Tue, 2 Dec 2025 00:05:53 +0100 Subject: [PATCH 08/32] minor fix --- .env | 7 ++++--- docker-compose.yml | 15 ++------------- src/settings_production.py | 2 +- 3 files changed, 7 insertions(+), 17 deletions(-) diff --git a/.env b/.env index ec31d8b5..3fe5de79 100644 --- a/.env +++ b/.env @@ -1,10 +1,11 @@ SETTINGS_FILE=settings +DEBUG=False ALLOWED_HOSTS=.fachschaft.informatik.tu-darmstadt.de,.d120.de SECRET_KEY= KEYCLOAK_CLIENT_ID= KEYCLOAK_SECRET= KEYCLOAK_SERVER_URL= -EMAIL_HOST=mail.d120.de -EMAIL_PORT=587 -EMAIL_HOST_USER=pyfeedback +EMAIL_HOST= +EMAIL_PORT= +EMAIL_HOST_USER= EMAIL_HOST_PASSWORD= diff --git a/docker-compose.yml b/docker-compose.yml index 343f1356..b889e065 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,21 +4,10 @@ services: container_name: pyfeedback-docker ports: - "8000:8000" - environment: - - SETTINGS_FILE=${SETTINGS_FILE} - - ALLOWED_HOSTS=${ALLOWED_HOSTS} - - SECRET_KEY=${SECRET_KEY} - - KEYCLOAK_CLIENT_ID=${KEYCLOAK_CLIENT_ID} - - KEYCLOAK_SECRET=${KEYCLOAK_SECRET} - - KEYCLOAK_SERVER_URL=${KEYCLOAK_SERVER_URL} - - EMAIL_HOST=${EMAIL_HOST} - - EMAIL_PORT=${EMAIL_PORT} - - EMAIL_HOST_USER=${EMAIL_HOST_USER} - - EMAIL_HOST_PASSWORD=${EMAIL_HOST_PASSWORD} env_file: - .env volumes: - - db_data:/app/db/ + - pyfeedback-db-vol:/app/db/ volumes: - db_data: + pyfeedback-db-vol: diff --git a/src/settings_production.py b/src/settings_production.py index ccc1db5c..939eb06e 100644 --- a/src/settings_production.py +++ b/src/settings_production.py @@ -1,7 +1,7 @@ from settings import * import os -DEBUG = False +DEBUG = (os.getenv("DEBUG", "False") == "True") # os.getenv only returns a string ALLOWED_HOSTS = os.getenv("ALLOWED_HOSTS").split(",") From 33fcaf3f8bffe5c1cb2b2befaed9d3c5649ce07f Mon Sep 17 00:00:00 2001 From: 4-dash <120916864+4-dash@users.noreply.github.com> Date: Wed, 6 May 2026 21:04:43 +0200 Subject: [PATCH 09/32] rename env --- .env => .env.example | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .env => .env.example (100%) diff --git a/.env b/.env.example similarity index 100% rename from .env rename to .env.example From c6eb85f32e0bc04e75549660090e8677c627412e Mon Sep 17 00:00:00 2001 From: 4-dash <120916864+4-dash@users.noreply.github.com> Date: Wed, 6 May 2026 21:17:53 +0200 Subject: [PATCH 10/32] minor changes --- .dockerignore | 5 ++++- .env.example | 1 + .gitignore | 5 ++++- Dockerfile | 2 +- docker-compose.yml | 6 +++--- 5 files changed, 13 insertions(+), 6 deletions(-) diff --git a/.dockerignore b/.dockerignore index 573b7551..e18fae4c 100644 --- a/.dockerignore +++ b/.dockerignore @@ -19,9 +19,12 @@ pyenv settings_secret.py static/* venv/ +.venv .vscode/* .python-version node_modules/ -*.mo \ No newline at end of file +*.mo + +.env \ No newline at end of file diff --git a/.env.example b/.env.example index 3fe5de79..d0a33ace 100644 --- a/.env.example +++ b/.env.example @@ -9,3 +9,4 @@ EMAIL_HOST= EMAIL_PORT= EMAIL_HOST_USER= EMAIL_HOST_PASSWORD= +GUNICORN_WORKERS= \ No newline at end of file diff --git a/.gitignore b/.gitignore index 573b7551..25255289 100644 --- a/.gitignore +++ b/.gitignore @@ -19,9 +19,12 @@ pyenv settings_secret.py static/* venv/ +.venv/ .vscode/* .python-version node_modules/ -*.mo \ No newline at end of file +*.mo + +.env \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index e9a86cd0..30caeea7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -69,4 +69,4 @@ RUN python manage.py migrate --no-input EXPOSE 8000 # start applicaiton with gunicorn -CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "3", "wsgi:application"] \ No newline at end of file +CMD ["sh", "-c", "gunicorn --bind 0.0.0.0:8000 --workers ${GUNICORN_WORKERS:-3} wsgi:application"] \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index b889e065..8320c1b9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,5 @@ services: - django-web: + pyfeedback: build: . container_name: pyfeedback-docker ports: @@ -7,7 +7,7 @@ services: env_file: - .env volumes: - - pyfeedback-db-vol:/app/db/ + - pyfeedback-db:/app/db/ volumes: - pyfeedback-db-vol: + pyfeedback-db: From e87233c33da9bcc99dd54781a540eaad54ebb8ec Mon Sep 17 00:00:00 2001 From: 4-dash <120916864+4-dash@users.noreply.github.com> Date: Wed, 6 May 2026 22:25:02 +0200 Subject: [PATCH 11/32] improve dockerfile --- Dockerfile | 40 +++++++++++++++++++++------------------- entrypoint.sh | 11 +++++++++++ 2 files changed, 32 insertions(+), 19 deletions(-) create mode 100644 entrypoint.sh diff --git a/Dockerfile b/Dockerfile index 30caeea7..b96f6326 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,16 +1,13 @@ ## Stage 1: build stage FROM python:3.13-slim AS builder -RUN mkdir /app - -WORKDIR /app - +ARG DEBIAN_FRONTEND=noninteractive ENV PYTHONDONTWRITEBYTECODE=1 ENV PYTHONUNBUFFERED=1 -# install packages and node -RUN pip install --upgrade pip +WORKDIR /app +# install packages and node RUN apt-get update && apt-get install -y \ curl \ gnupg \ @@ -21,28 +18,31 @@ RUN apt-get update && apt-get install -y \ && rm -rf /var/lib/apt/lists/* # install pip requirements -COPY requirements.txt /app/ - -RUN pip install --no-cache-dir -r requirements.txt +COPY requirements.txt . +RUN pip install --upgrade pip && \ + pip install --no-cache-dir -r requirements.txt # install node_modules COPY package*.json ./ - RUN npm i ## Stage 2: production stage FROM python:3.13-slim +ARG DEBIAN_FRONTEND=noninteractive +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 + RUN useradd -m -r appuser && \ mkdir /app && \ chown -R appuser /app -# Copy the python dependencies and node_modules from the builder stage +# Copy dependencies from builder COPY --from=builder /usr/local/lib/python3.13/site-packages/ /usr/local/lib/python3.13/site-packages/ COPY --from=builder /usr/local/bin/ /usr/local/bin/ -COPY --from=builder /app/node_modules/ /app/node_modules/ +COPY --from=builder /app/node_modules/ /app/node_modules/ -# install gettext for translations (compiling) +# install gettext for translations RUN apt-get update && apt-get install -y gettext \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* @@ -52,19 +52,21 @@ WORKDIR /app # copy the code COPY --chown=appuser:appuser . . -ENV PYTHONDONTWRITEBYTECODE=1 -ENV PYTHONUNBUFFERED=1 - WORKDIR /app/src -# switch to non-root user USER appuser # compile translations RUN django-admin compilemessages -# migrate db changes -RUN python manage.py migrate --no-input +# --- Runtime Setup --- +USER root +COPY entrypoint.sh /usr/local/bin/entrypoint.sh +RUN chmod +x /usr/local/bin/entrypoint.sh + +USER appuser + +ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] EXPOSE 8000 diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100644 index 00000000..5573e743 --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,11 @@ +#!/bin/sh + +# Exit on error +set -e + +echo "Applying database migrations..." +python manage.py migrate --no-input + +# Start the application +echo "Starting Gunicorn..." +exec "$@" \ No newline at end of file From 17d6e5de95d36ad68c870a130f202f26a211af82 Mon Sep 17 00:00:00 2001 From: 4-dash <120916864+4-dash@users.noreply.github.com> Date: Wed, 22 Jul 2026 20:08:43 +0200 Subject: [PATCH 12/32] seperate node stage, etc --- Dockerfile | 87 +++++++++++++++++++++--------------------------- entrypoint.sh | 11 ++++-- requirements.txt | 2 ++ 3 files changed, 48 insertions(+), 52 deletions(-) diff --git a/Dockerfile b/Dockerfile index b96f6326..d31be3f5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,74 +1,63 @@ -## Stage 1: build stage -FROM python:3.13-slim AS builder - -ARG DEBIAN_FRONTEND=noninteractive -ENV PYTHONDONTWRITEBYTECODE=1 -ENV PYTHONUNBUFFERED=1 +# ========================================== +# Build Node.js Dependencies +# ========================================== +FROM node:22-slim AS node-builder WORKDIR /app -# install packages and node -RUN apt-get update && apt-get install -y \ - curl \ - gnupg \ - ca-certificates \ - && curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \ - && apt-get install -y nodejs \ - && apt-get clean \ - && rm -rf /var/lib/apt/lists/* +COPY package*.json ./ +RUN npm ci --only=production -# install pip requirements -COPY requirements.txt . -RUN pip install --upgrade pip && \ - pip install --no-cache-dir -r requirements.txt +# ========================================== +# Build Python Dependencies +# ========================================== +FROM python:3.13-slim AS python-builder -# install node_modules -COPY package*.json ./ -RUN npm i +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 -## Stage 2: production stage -FROM python:3.13-slim +WORKDIR /app -ARG DEBIAN_FRONTEND=noninteractive -ENV PYTHONDONTWRITEBYTECODE=1 -ENV PYTHONUNBUFFERED=1 +COPY requirements.txt . +RUN pip install --root-user-action ignore --upgrade pip && \ + pip install --root-user-action ignore --no-cache-dir -r requirements.txt -RUN useradd -m -r appuser && \ - mkdir /app && \ - chown -R appuser /app +# ========================================== +# Final Production Image +# ========================================== +FROM python:3.13-slim AS production -# Copy dependencies from builder -COPY --from=builder /usr/local/lib/python3.13/site-packages/ /usr/local/lib/python3.13/site-packages/ -COPY --from=builder /usr/local/bin/ /usr/local/bin/ -COPY --from=builder /app/node_modules/ /app/node_modules/ +ARG DEBIAN_FRONTEND=noninteractive +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 -# install gettext for translations -RUN apt-get update && apt-get install -y gettext \ +RUN apt-get update && apt-get install -y --no-install-recommends \ + gettext \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* +# non-root user +RUN useradd -m -r appuser + WORKDIR /app -# copy the code -COPY --chown=appuser:appuser . . +COPY --from=python-builder /usr/local/lib/python3.13/site-packages/ /usr/local/lib/python3.13/site-packages/ +COPY --from=python-builder /usr/local/bin/ /usr/local/bin/ -WORKDIR /app/src +COPY --from=node-builder /app/node_modules /app/node_modules -USER appuser - -# compile translations -RUN django-admin compilemessages +COPY --chown=appuser:appuser . . -# --- Runtime Setup --- -USER root -COPY entrypoint.sh /usr/local/bin/entrypoint.sh +COPY --chown=appuser:appuser entrypoint.sh /usr/local/bin/entrypoint.sh RUN chmod +x /usr/local/bin/entrypoint.sh +# Build static assets & translations as non-root user USER appuser -ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] +RUN django-admin compilemessages && \ + python src/manage.py collectstatic --no-input --clear EXPOSE 8000 +WORKDIR /app/src -# start applicaiton with gunicorn -CMD ["sh", "-c", "gunicorn --bind 0.0.0.0:8000 --workers ${GUNICORN_WORKERS:-3} wsgi:application"] \ No newline at end of file +ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] \ No newline at end of file diff --git a/entrypoint.sh b/entrypoint.sh index 5573e743..bda2108f 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -6,6 +6,11 @@ set -e echo "Applying database migrations..." python manage.py migrate --no-input -# Start the application -echo "Starting Gunicorn..." -exec "$@" \ No newline at end of file +if [ $# -eq 0 ]; then + echo "Starting Gunicorn with ${GUNICORN_WORKERS:-3} workers..." + exec gunicorn --bind 0.0.0.0:8000 --workers "${GUNICORN_WORKERS:-3}" wsgi:application +else + # (bash, python manage.py createsuperuser etc.) + echo "Running custom command: $@" + exec "$@" +fi diff --git a/requirements.txt b/requirements.txt index 85a2141d..664af879 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,3 +4,5 @@ django-debug-toolbar==7.0.0 freezegun==1.5.5 django-formtools==2.7 django-allauth[socialaccount]==65.18.0 +gunicorn==26.0.0 +whitenoise[brotli]==6.12.0 \ No newline at end of file From f2692a9adcfff6916506abdfc962b6b7a2cd458a Mon Sep 17 00:00:00 2001 From: 4-dash <120916864+4-dash@users.noreply.github.com> Date: Wed, 22 Jul 2026 20:09:30 +0200 Subject: [PATCH 13/32] fix logger --- src/settings.py | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/src/settings.py b/src/settings.py index 7d119690..684542fc 100644 --- a/src/settings.py +++ b/src/settings.py @@ -117,7 +117,7 @@ 'django.contrib.auth.middleware.AuthenticationMiddleware', 'feedback.auth.FSDebugRemoteUserMiddleware', 'allauth.account.middleware.AccountMiddleware', - # 'whitenoise.middleware.WhiteNoiseMiddleware', # while DEBUG=False servers static files, Note:first pip install whitenoise + 'whitenoise.middleware.WhiteNoiseMiddleware', ] if not TESTING: MIDDLEWARE += ['debug_toolbar.middleware.DebugToolbarMiddleware'] @@ -202,15 +202,8 @@ 'class': 'django.utils.log.AdminEmailHandler' }, 'console': { - 'level':'DEBUG', - 'class':'logging.StreamHandler', - }, - 'file_all': { 'level': 'INFO', - 'class': 'logging.handlers.RotatingFileHandler', - 'filename': os.path.join(BASE_PATH, 'all.log'), - 'maxBytes': 1024 * 1024 * 5, - 'backupCount': 5, + 'class': 'logging.StreamHandler', 'formatter': 'verbose', }, }, @@ -225,7 +218,7 @@ 'level': 'DEBUG', }, '': { - 'handlers': ['file_all'], + 'handlers': ['console'], 'level': 'INFO', 'propagate': True, }, From 40cb86aaf01749669c0976235da5e5c2423fe5fc Mon Sep 17 00:00:00 2001 From: 4-dash <120916864+4-dash@users.noreply.github.com> Date: Fri, 24 Jul 2026 19:27:26 +0200 Subject: [PATCH 14/32] cleaner setting management --- .env.example | 5 +-- src/manage.py | 2 +- src/settings/__init__.py | 0 src/{settings.py => settings/base.py} | 11 +++---- src/settings/dev.py | 33 +++++++++++++++++++ .../prod.py} | 33 ++++++++++++------- src/wsgi.py | 5 +-- 7 files changed, 63 insertions(+), 26 deletions(-) create mode 100644 src/settings/__init__.py rename src/{settings.py => settings/base.py} (97%) create mode 100644 src/settings/dev.py rename src/{settings_production.py => settings/prod.py} (60%) diff --git a/.env.example b/.env.example index d0a33ace..2c94e7e4 100644 --- a/.env.example +++ b/.env.example @@ -1,12 +1,13 @@ -SETTINGS_FILE=settings -DEBUG=False +DJANGO_SETTINGS_MODULE=settings.prod ALLOWED_HOSTS=.fachschaft.informatik.tu-darmstadt.de,.d120.de +CSRF_TRUSTED_ORIGINS= SECRET_KEY= KEYCLOAK_CLIENT_ID= KEYCLOAK_SECRET= KEYCLOAK_SERVER_URL= EMAIL_HOST= EMAIL_PORT= +EMAIL_USE_TLS=True EMAIL_HOST_USER= EMAIL_HOST_PASSWORD= GUNICORN_WORKERS= \ No newline at end of file diff --git a/src/manage.py b/src/manage.py index f9726f9e..b326bd60 100755 --- a/src/manage.py +++ b/src/manage.py @@ -3,7 +3,7 @@ import sys if __name__ == "__main__": - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings") + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings.dev") from django.core.management import execute_from_command_line diff --git a/src/settings/__init__.py b/src/settings/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/settings.py b/src/settings/base.py similarity index 97% rename from src/settings.py rename to src/settings/base.py index 684542fc..03d0261b 100644 --- a/src/settings.py +++ b/src/settings/base.py @@ -1,15 +1,12 @@ # coding=utf-8 # Django settings for feedback project. -# determine if this is a production system import os import sys import ipaddress from django.utils.translation import gettext_lazy as _ -DEBUG = True - -# default is False, make True to see exeptions when DEBUG = False +# make True to see exeptions when DEBUG = False DEBUG_PROPAGATE_EXCEPTIONS = False ADMINS = ( @@ -19,7 +16,7 @@ MANAGERS = ADMINS EMAIL_SUBJECT_PREFIX = '' -BASE_PATH = os.path.dirname(os.path.abspath(__file__)) + '/../' +BASE_PATH = os.path.dirname(os.path.abspath(__file__)) + '/../../' DATABASES = { 'default': { @@ -109,8 +106,8 @@ # überschreiben! MIDDLEWARE = [ - 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', 'django.middleware.locale.LocaleMiddleware', 'django.middleware.common.CommonMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', @@ -272,4 +269,4 @@ # application-specific-cookies CSRF_COOKIE_NAME = 'pyfeedback_csrftoken' -SESSION_COOKIE_NAME = 'pyfeedback_sessionid' +SESSION_COOKIE_NAME = 'pyfeedback_sessionid' \ No newline at end of file diff --git a/src/settings/dev.py b/src/settings/dev.py new file mode 100644 index 00000000..8a4a2243 --- /dev/null +++ b/src/settings/dev.py @@ -0,0 +1,33 @@ +from .base import * + +DEBUG = True + +# Disable secure cookies for local HTTP development +SESSION_COOKIE_SECURE = False +CSRF_COOKIE_SECURE = False + +ALLOWED_HOSTS = [] + +CSRF_TRUSTED_ORIGINS = [] + + +## for testing SSO, set DEBUG=False and ALLOWED_HOSTS = ["*"] +# from dotenv import load_dotenv, find_dotenv + +# load_dotenv(find_dotenv()) + +# SOCIALACCOUNT_PROVIDERS = { +# "openid_connect": { +# "APPS": [ +# { +# "provider_id": "keycloak", +# "name": "Keycloak", +# "client_id": os.getenv("KEYCLOAK_CLIENT_ID"), +# "secret": os.getenv("KEYCLOAK_SECRET"), +# "settings": { +# "server_url": os.getenv("KEYCLOAK_SERVER_URL"), +# }, +# } +# ] +# } +# } diff --git a/src/settings_production.py b/src/settings/prod.py similarity index 60% rename from src/settings_production.py rename to src/settings/prod.py index 939eb06e..6d470ab5 100644 --- a/src/settings_production.py +++ b/src/settings/prod.py @@ -1,19 +1,32 @@ -from settings import * import os +from .base import * -DEBUG = (os.getenv("DEBUG", "False") == "True") # os.getenv only returns a string +DEBUG = os.getenv("DEBUG", "False").lower() in ("true", "1", "yes") -ALLOWED_HOSTS = os.getenv("ALLOWED_HOSTS").split(",") +ALLOWED_HOSTS = [ + host.strip() for host in os.getenv("ALLOWED_HOSTS", "").split(",") if host.strip() +] + +CSRF_TRUSTED_ORIGINS = [ + origin.strip() for origin in os.getenv("CSRF_TRUSTED_ORIGINS", "").split(",") if origin.strip() +] SECRET_KEY = os.getenv("SECRET_KEY") +if not SECRET_KEY and not DEBUG: + raise ValueError("CRITICAL: SECRET_KEY environment variable is missing!") URL_PREFIX = 'feedback/' LOGIN_URL = '/' + URL_PREFIX[:-1] + LOGIN_URL LOGIN_REDIRECT_URL = '/' + URL_PREFIX[:-1] + LOGIN_REDIRECT_URL ACCOUNT_LOGOUT_REDIRECT_URL = '/' + URL_PREFIX[:-1] + ACCOUNT_LOGOUT_REDIRECT_URL + +SESSION_COOKIE_PATH = '/feedback' SESSION_COOKIE_SECURE = True +CSRF_COOKIE_PATH = SESSION_COOKIE_PATH +CSRF_COOKIE_SECURE = True + SOCIALACCOUNT_PROVIDERS = { "openid_connect": { "APPS": [ @@ -32,15 +45,11 @@ # @see https://docs.djangoproject.com/es/1.9/topics/email/ EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' - EMAIL_HOST = os.getenv("EMAIL_HOST") -EMAIL_PORT = int(os.getenv("EMAIL_PORT")) -EMAIL_USE_TLS = True -EMAIL_HOST_USER = os.getenv("EMAIL_HOST_USER") -EMAIL_HOST_PASSWORD = os.getenv("EMAIL_HOST_PASSWORD") -SESSION_COOKIE_PATH = '/feedback' -SESSION_COOKIE_SECURE = True +email_port_env = os.getenv("EMAIL_PORT", "").strip() +EMAIL_PORT = int(email_port_env) if email_port_env else 587 -CSRF_COOKIE_PATH = SESSION_COOKIE_PATH -CSRF_COOKIE_SECURE = True +EMAIL_USE_TLS = os.getenv("EMAIL_USE_TLS", "True").lower() in ("true", "1", "yes") +EMAIL_HOST_USER = os.getenv("EMAIL_HOST_USER") +EMAIL_HOST_PASSWORD = os.getenv("EMAIL_HOST_PASSWORD") \ No newline at end of file diff --git a/src/wsgi.py b/src/wsgi.py index 872c4101..c1e47dc3 100644 --- a/src/wsgi.py +++ b/src/wsgi.py @@ -2,9 +2,6 @@ from django.core.wsgi import get_wsgi_application -# select in .env which settings to use -settings_file = os.getenv("SETTINGS_FILE", "settings") - -os.environ.setdefault("DJANGO_SETTINGS_MODULE", settings_file) +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings.prod") application = get_wsgi_application() From 27e9fe1a23e664af4917fc4edeaecec5732563f8 Mon Sep 17 00:00:00 2001 From: 4-dash <120916864+4-dash@users.noreply.github.com> Date: Sat, 25 Jul 2026 11:03:05 +0200 Subject: [PATCH 15/32] add toml file, update readme --- Readme.md | 23 ++++++++++++++++++++--- pyproject.toml | 43 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 3 deletions(-) create mode 100644 pyproject.toml diff --git a/Readme.md b/Readme.md index ecea5d74..0bfe5282 100644 --- a/Readme.md +++ b/Readme.md @@ -16,17 +16,34 @@ To use pyfeedback the following tools have to be installed: ## Preparing development environment -* Create a virtualenv with `python -m venv venv` -* Activate the virtualenv with `source venv/bin/activate` -* Install all requirements with `pip install -r requirements.txt` +* Create a virtualenv with `python -m venv .venv` +* Activate the virtualenv with `source .venv/bin/activate` +* Install all requirements with `pip install -e ".[dev]"` * Create the test database with `python src/manage.py migrate` * Compile translations with `(cd src && django-admin compilemessages)` * Install frontend dependencies with `npm i` * Start the development server with `python src/manage.py runserver` +## Production +- Use `pip install .` to install dependencies. +- Docker: make sure to set `DJANGO_SETTINGS_MODULE=settings.prod` in .env +- Without docker: `gunicorn wsgi:application` + ## Tests pyfeedback is using a test driven development and tries to get to 100% coverage. Tests can be run with ``` python src/manage.py test feedback ``` Do not implement new functionality without providing a test for it. + +## Settings + +`src/manage.py` uses settings.dev, `src/wsgi.py` uses settings.prod + +Use `python src/manage.py runserver --settings=settings.prod` to run production settings during development. + + +## Docker + +- DJANGO_SETTINGS_MODULE: `settings.prod` or `settings.dev`. Docker uses wsgi.py, making `settings.prod` *default*. +- GUNICORN_WORKERS: *default* 3 workers diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..b13bddd0 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,43 @@ +[build-system] +requires = ["setuptools>=61.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "pyfeedback" +description = "Web application to assist feedback for modules at TU Darmstadt" +readme = "README.md" +requires-python = ">=3.13" +authors = [ + { name = "d120" } +] +dependencies = [ + "Django==6.0.7", + "django-formtools==2.7", + "django-allauth[socialaccount]==65.18.0", + "gunicorn==26.0.0", + "whitenoise[brotli]==6.12.0", +] + +[project.optional-dependencies] +dev = [ + "django-debug-toolbar==7.0.0", + "coverage==7.15.1", + "freezegun==1.5.5", + "python-dotenv>=1.0.0", +] + +[tool.setuptools.packages.find] +where = ["src"] + +[tool.coverage.run] +source = ["src"] +omit = [ + "*/migrations/*", + "*/tests/*", + "src/manage.py", + "src/wsgi.py", +] + +[tool.coverage.report] +show_missing = true +fail_under = 100 \ No newline at end of file From 6547817a12de175be3d77c0066df20652d2aa436 Mon Sep 17 00:00:00 2001 From: 4-dash <120916864+4-dash@users.noreply.github.com> Date: Sat, 25 Jul 2026 11:11:08 +0200 Subject: [PATCH 16/32] remove requirements.txt --- .github/workflows/django.yml | 2 +- Dockerfile | 7 +++++-- requirements.txt | 8 -------- script/bootstrap | 26 -------------------------- 4 files changed, 6 insertions(+), 37 deletions(-) delete mode 100644 requirements.txt delete mode 100755 script/bootstrap diff --git a/.github/workflows/django.yml b/.github/workflows/django.yml index e7463d29..ac74c208 100644 --- a/.github/workflows/django.yml +++ b/.github/workflows/django.yml @@ -21,7 +21,7 @@ jobs: run: | python -m pip install --upgrade pip pip install coveralls - pip install -r requirements.txt + pip install . echo "PASSWORD_HASHERS = ['django.contrib.auth.hashers.MD5PasswordHasher',]" >> src/settings.py - name: Static tests diff --git a/Dockerfile b/Dockerfile index d31be3f5..116b6602 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,9 +18,12 @@ ENV PYTHONDONTWRITEBYTECODE=1 \ WORKDIR /app -COPY requirements.txt . +COPY pyproject.toml ./ + +COPY src ./src + RUN pip install --root-user-action ignore --upgrade pip && \ - pip install --root-user-action ignore --no-cache-dir -r requirements.txt + pip install --root-user-action ignore --no-cache-dir . # ========================================== # Final Production Image diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 664af879..00000000 --- a/requirements.txt +++ /dev/null @@ -1,8 +0,0 @@ -coverage==7.15.1 -Django==6.0.7 -django-debug-toolbar==7.0.0 -freezegun==1.5.5 -django-formtools==2.7 -django-allauth[socialaccount]==65.18.0 -gunicorn==26.0.0 -whitenoise[brotli]==6.12.0 \ No newline at end of file diff --git a/script/bootstrap b/script/bootstrap deleted file mode 100755 index 9a54ec5e..00000000 --- a/script/bootstrap +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/bash - -set -e - -# set environment variable when we want to update a production instance -if [ "$1" = "--prod" ]; then - export DJANGO_SETTINGS_MODULE=settings_production -fi - -virtualenv -p python3 ../virtualenv -source ../virtualenv/bin/activate -pip install -r requirements.txt -./src/manage.py migrate -./src/manage.py collectstatic - -if [ "$2" == "--pass" ]; then - ./src/manage.py shell -c "from django.contrib.auth.models import User; User.objects.create_superuser('admin', 'admin@example.com', '"$3"')" -else - echo "Creating super user, enter credentials:" - ./src/manage.py createsuperuser -fi - -deactivate - -echo -echo "environment successfully bootstrapped. Start a server with script/server" From c1de86168e25f05d6e1afdfe74d24194c0ad0647 Mon Sep 17 00:00:00 2001 From: 4-dash <120916864+4-dash@users.noreply.github.com> Date: Sat, 25 Jul 2026 11:19:48 +0200 Subject: [PATCH 17/32] move coverage settings to toml --- .coveragerc | 12 ------------ pyproject.toml | 5 +++++ 2 files changed, 5 insertions(+), 12 deletions(-) delete mode 100644 .coveragerc diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index fb7547f9..00000000 --- a/.coveragerc +++ /dev/null @@ -1,12 +0,0 @@ -[run] -omit = - */migrations/* - */tests.py - */tests_*.py - */tests/* - venv/* - -source = . -[report] -precision = 2 -show_missing = True diff --git a/pyproject.toml b/pyproject.toml index b13bddd0..b2cefa95 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,6 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "pyfeedback" +version = "3.0.0" description = "Web application to assist feedback for modules at TU Darmstadt" readme = "README.md" requires-python = ">=3.13" @@ -34,10 +35,14 @@ source = ["src"] omit = [ "*/migrations/*", "*/tests/*", + "*/tests.py", + "*/tests_*.py", "src/manage.py", "src/wsgi.py", + "src/asgi.py", ] [tool.coverage.report] +precision = 2 show_missing = true fail_under = 100 \ No newline at end of file From c2461358967edd7b466feea3247f044469f2a839 Mon Sep 17 00:00:00 2001 From: 4-dash <120916864+4-dash@users.noreply.github.com> Date: Sat, 25 Jul 2026 11:22:39 +0200 Subject: [PATCH 18/32] fix --- .github/workflows/django.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/django.yml b/.github/workflows/django.yml index ac74c208..19cd5cda 100644 --- a/.github/workflows/django.yml +++ b/.github/workflows/django.yml @@ -21,7 +21,7 @@ jobs: run: | python -m pip install --upgrade pip pip install coveralls - pip install . + pip install -e ".[dev]" echo "PASSWORD_HASHERS = ['django.contrib.auth.hashers.MD5PasswordHasher',]" >> src/settings.py - name: Static tests From abedbfa4b760b5edc6bd6f47e8b1363b4c3c9bab Mon Sep 17 00:00:00 2001 From: 4-dash <120916864+4-dash@users.noreply.github.com> Date: Sat, 25 Jul 2026 11:31:51 +0200 Subject: [PATCH 19/32] fix debugtoolbar urls etc --- src/settings/base.py | 8 ++++++-- src/urls.py | 9 ++++++--- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/settings/base.py b/src/settings/base.py index 03d0261b..47077055 100644 --- a/src/settings/base.py +++ b/src/settings/base.py @@ -5,6 +5,7 @@ import sys import ipaddress from django.utils.translation import gettext_lazy as _ +import importlib.util # make True to see exeptions when DEBUG = False DEBUG_PROPAGATE_EXCEPTIONS = False @@ -116,7 +117,10 @@ 'allauth.account.middleware.AccountMiddleware', 'whitenoise.middleware.WhiteNoiseMiddleware', ] -if not TESTING: + +HAS_DEBUG_TOOLBAR = importlib.util.find_spec("debug_toolbar") is not None + +if not TESTING and HAS_DEBUG_TOOLBAR : MIDDLEWARE += ['debug_toolbar.middleware.DebugToolbarMiddleware'] ROOT_URLCONF = 'urls' @@ -155,7 +159,7 @@ 'allauth.socialaccount.providers.openid_connect', ] -if not TESTING: +if not TESTING and HAS_DEBUG_TOOLBAR : INSTALLED_APPS += ['debug_toolbar'] AUTHENTICATION_BACKENDS = ( diff --git a/src/urls.py b/src/urls.py index fd09af2c..130a3d7c 100644 --- a/src/urls.py +++ b/src/urls.py @@ -1,10 +1,9 @@ # coding=utf-8 -from django.conf.urls import include from django.contrib import admin from django.contrib.auth.models import User -from django.urls import re_path, reverse_lazy, path +from django.urls import include, re_path, reverse_lazy, path from django.conf.urls.i18n import i18n_patterns from django.views.generic.base import RedirectView from django.contrib.staticfiles.urls import staticfiles_urlpatterns @@ -12,6 +11,8 @@ from allauth.account.decorators import secure_admin_login +import importlib.util + # Admin-Seiten konfigurieren # admin.site.unregister((User, Group)) @@ -41,7 +42,9 @@ # don't forget to put sample favicon.ico in static files urlpatterns += [path('favicon.ico', RedirectView.as_view(url='/feedback/static/img/favicon.ico', permanent=True))] -if not settings.TESTING and settings.DEBUG : +HAS_DEBUG_TOOLBAR = importlib.util.find_spec("debug_toolbar") is not None + +if not settings.TESTING and settings.DEBUG and HAS_DEBUG_TOOLBAR: # avoided directly using debug_toolbar.urls(), as importing caused error in django-debug-toolbar v6.0.0 # see https://github.com/django-commons/django-debug-toolbar/issues/2167 urlpatterns += [path("__debug__/", include("debug_toolbar.urls"))] \ No newline at end of file From a796681f1c5cd92c521187073babcc05e212038b Mon Sep 17 00:00:00 2001 From: 4-dash <120916864+4-dash@users.noreply.github.com> Date: Sat, 25 Jul 2026 11:34:14 +0200 Subject: [PATCH 20/32] fix --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b2cefa95..b30e184f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,4 +45,4 @@ omit = [ [tool.coverage.report] precision = 2 show_missing = true -fail_under = 100 \ No newline at end of file +fail_under = 85 \ No newline at end of file From e31d4c6de354d9ad341328fc811fe5e11d5b3012 Mon Sep 17 00:00:00 2001 From: 4-dash <120916864+4-dash@users.noreply.github.com> Date: Sat, 25 Jul 2026 17:40:06 +0200 Subject: [PATCH 21/32] coverage fix --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index b30e184f..f0e5c9e8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,6 +40,7 @@ omit = [ "src/manage.py", "src/wsgi.py", "src/asgi.py", + "*/locale/*", ] [tool.coverage.report] From bef3d84b5a1c06a4070444c97d9357c64edfec50 Mon Sep 17 00:00:00 2001 From: 4-dash <120916864+4-dash@users.noreply.github.com> Date: Sun, 9 Aug 2026 10:49:51 +0200 Subject: [PATCH 22/32] try to increase coverage --- pyproject.toml | 1 + src/feedback/tests/test_adapters.py | 93 +++++++++++++++++++++++++++++ src/feedback/tests/test_commands.py | 83 +++++++++++++++++++++++++ 3 files changed, 177 insertions(+) create mode 100644 src/feedback/tests/test_adapters.py create mode 100644 src/feedback/tests/test_commands.py diff --git a/pyproject.toml b/pyproject.toml index f0e5c9e8..6c2d67c9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,6 +41,7 @@ omit = [ "src/wsgi.py", "src/asgi.py", "*/locale/*", + "*/settings/*", ] [tool.coverage.report] diff --git a/src/feedback/tests/test_adapters.py b/src/feedback/tests/test_adapters.py new file mode 100644 index 00000000..ef12393c --- /dev/null +++ b/src/feedback/tests/test_adapters.py @@ -0,0 +1,93 @@ +from unittest.mock import MagicMock +from django.test import TestCase, RequestFactory +from django.contrib.auth.models import User + +# Import your custom adapter +from feedback.auth import FeedbackSocialAccountAdapter + + +class FeedbackSocialAccountAdapterTests(TestCase): + + def setUp(self): + self.factory = RequestFactory() + self.request = self.factory.get("/") + self.adapter = FeedbackSocialAccountAdapter() + + def test_pre_social_login_with_existing_user_by_username(self): + """ + Scenario 1: Unsaved social user matches an existing username in DB. + Should connect to existing user and elevate permissions. + """ + # Create an existing non-admin user + existing_user = User.objects.create_user( + username="johndoe", + email="john@example.com", + is_staff=False, + is_superuser=False, + ) + + # Mock an unsaved social user with the same username + unsaved_user = User(username="johndoe", email="john@example.com") + + mock_sociallogin = MagicMock() + mock_sociallogin.user = unsaved_user + + # Run method + self.adapter.pre_social_login(self.request, mock_sociallogin) + + # Verify sociallogin.connect was called with existing_user + mock_sociallogin.connect.assert_called_once_with(self.request, existing_user) + + # Verify the existing user was updated to superuser/staff + existing_user.refresh_from_db() + self.assertTrue(existing_user.is_staff) + self.assertTrue(existing_user.is_superuser) + + def test_pre_social_login_with_new_user(self): + """ + Scenario 2: Unsaved social user does NOT match any username in DB. + Should save the new user and elevate permissions. + """ + # Mock an unsaved social user with a brand new username + unsaved_user = User(username="newuser", email="new@example.com") + + mock_sociallogin = MagicMock() + mock_sociallogin.user = unsaved_user + + # Run method + self.adapter.pre_social_login(self.request, mock_sociallogin) + + # Verify connect was NOT called + mock_sociallogin.connect.assert_not_called() + + # Verify user now exists in DB and has superuser/staff permissions + db_user = User.objects.get(username="newuser") + self.assertIsNotNone(db_user.id) + self.assertTrue(db_user.is_staff) + self.assertTrue(db_user.is_superuser) + + def test_pre_social_login_with_already_saved_user(self): + """ + Scenario 3: Social user already has an ID (already saved/authenticated). + Should skip user lookup/creation and elevate permissions. + """ + # User is already saved in DB + saved_user = User.objects.create_user( + username="saveduser", + is_staff=False, + is_superuser=False, + ) + + mock_sociallogin = MagicMock() + mock_sociallogin.user = saved_user + + # Run method + self.adapter.pre_social_login(self.request, mock_sociallogin) + + # Verify connect was NOT called + mock_sociallogin.connect.assert_not_called() + + # Verify existing saved user is now superuser/staff + saved_user.refresh_from_db() + self.assertTrue(saved_user.is_staff) + self.assertTrue(saved_user.is_superuser) \ No newline at end of file diff --git a/src/feedback/tests/test_commands.py b/src/feedback/tests/test_commands.py new file mode 100644 index 00000000..ee4eb271 --- /dev/null +++ b/src/feedback/tests/test_commands.py @@ -0,0 +1,83 @@ +from io import StringIO +from datetime import timedelta +from unittest.mock import patch + +from django.test import TestCase +from django.core.management import call_command +from django.utils import timezone + +from feedback.models import EmailChange + + +class CleanupEmailChangeReqCommandTests(TestCase): + + def setUp(self): + """Set up test instances for different statuses and dates.""" + now = timezone.now() + + # 1. EXPIRED: > 7 days old -> SHOULD BE DELETED + self.expired_old = EmailChange.objects.create( + status=EmailChange.Status.EXPIRED, + dynamic_expiry_time=now - timedelta(days=8), + ) + + # 2. EXPIRED: < 7 days old -> SHOULD NOT BE DELETED + self.expired_recent = EmailChange.objects.create( + status=EmailChange.Status.EXPIRED, + dynamic_expiry_time=now - timedelta(days=5), + ) + + # 3. MAGIC_LINK_SENT: > 30 days old -> SHOULD BE DELETED + self.link_sent_old = EmailChange.objects.create( + status=EmailChange.Status.MAGIC_LINK_SENT, + dynamic_expiry_time=now - timedelta(days=31), + ) + + # 4. OTP_SENT: > 30 days old -> SHOULD BE DELETED + self.otp_sent_old = EmailChange.objects.create( + status=EmailChange.Status.OTP_SENT, + dynamic_expiry_time=now - timedelta(days=32), + ) + + # 5. MAGIC_LINK_SENT: < 30 days old -> SHOULD NOT BE DELETED + self.link_sent_recent = EmailChange.objects.create( + status=EmailChange.Status.MAGIC_LINK_SENT, + dynamic_expiry_time=now - timedelta(days=15), + ) + + @patch("feedback.management.commands.cleanup_email_change_req.logger") + def test_command_deletes_eligible_records(self, mock_logger): + """Test deleting eligible records (hits if branch: deleted_count > 0).""" + out = StringIO() + + # Execute management command + call_command("cleanup_email_change_req", stdout=out) + + output = out.getvalue() + + # Verify stdout output & logger call + self.assertIn("Cleanup successful: Deleted 3 EmailChange records.", output) + mock_logger.info.assert_called_once_with( + "Cleanup successful: Deleted 3 EmailChange records." + ) + + # Verify correct records were deleted from DB + remaining_ids = set(EmailChange.objects.values_list("id", flat=True)) + self.assertEqual(remaining_ids, {self.expired_recent.id, self.link_sent_recent.id}) + + @patch("feedback.management.commands.cleanup_email_change_req.logger") + def test_command_when_no_records_eligible(self, mock_logger): + """Test behavior when no records match (hits else branch: deleted_count == 0).""" + # Clear out all objects first + EmailChange.objects.all().delete() + + out = StringIO() + call_command("cleanup_email_change_req", stdout=out) + + output = out.getvalue() + + # Verify stdout output & logger call for 0 records branch + self.assertIn("Cleanup run: No records were eligible for deletion.", output) + mock_logger.info.assert_called_once_with( + "Cleanup run: No records were eligible for deletion." + ) \ No newline at end of file From c7ae1346205c6ab852333550d7374b49eddae230 Mon Sep 17 00:00:00 2001 From: 4-dash <120916864+4-dash@users.noreply.github.com> Date: Sun, 9 Aug 2026 12:04:32 +0200 Subject: [PATCH 23/32] fixes and more tests --- src/feedback/tests/test_adapters.py | 23 ++------ src/feedback/tests/test_commands.py | 15 ++--- src/feedback/tests/test_parser_ergebnisse.py | 5 +- src/feedback/tests/test_tools.py | 62 +++++++++++++++++++- 4 files changed, 73 insertions(+), 32 deletions(-) diff --git a/src/feedback/tests/test_adapters.py b/src/feedback/tests/test_adapters.py index ef12393c..56419cb8 100644 --- a/src/feedback/tests/test_adapters.py +++ b/src/feedback/tests/test_adapters.py @@ -2,9 +2,7 @@ from django.test import TestCase, RequestFactory from django.contrib.auth.models import User -# Import your custom adapter -from feedback.auth import FeedbackSocialAccountAdapter - +from feedback.auth_adapter import FeedbackSocialAccountAdapter class FeedbackSocialAccountAdapterTests(TestCase): @@ -15,10 +13,9 @@ def setUp(self): def test_pre_social_login_with_existing_user_by_username(self): """ - Scenario 1: Unsaved social user matches an existing username in DB. + Unsaved social user matches an existing username in DB. Should connect to existing user and elevate permissions. """ - # Create an existing non-admin user existing_user = User.objects.create_user( username="johndoe", email="john@example.com", @@ -26,41 +23,33 @@ def test_pre_social_login_with_existing_user_by_username(self): is_superuser=False, ) - # Mock an unsaved social user with the same username unsaved_user = User(username="johndoe", email="john@example.com") mock_sociallogin = MagicMock() mock_sociallogin.user = unsaved_user - # Run method self.adapter.pre_social_login(self.request, mock_sociallogin) - # Verify sociallogin.connect was called with existing_user mock_sociallogin.connect.assert_called_once_with(self.request, existing_user) - # Verify the existing user was updated to superuser/staff existing_user.refresh_from_db() self.assertTrue(existing_user.is_staff) self.assertTrue(existing_user.is_superuser) def test_pre_social_login_with_new_user(self): """ - Scenario 2: Unsaved social user does NOT match any username in DB. + Unsaved social user does NOT match any username in DB. Should save the new user and elevate permissions. """ - # Mock an unsaved social user with a brand new username unsaved_user = User(username="newuser", email="new@example.com") mock_sociallogin = MagicMock() mock_sociallogin.user = unsaved_user - # Run method self.adapter.pre_social_login(self.request, mock_sociallogin) - # Verify connect was NOT called mock_sociallogin.connect.assert_not_called() - # Verify user now exists in DB and has superuser/staff permissions db_user = User.objects.get(username="newuser") self.assertIsNotNone(db_user.id) self.assertTrue(db_user.is_staff) @@ -68,10 +57,9 @@ def test_pre_social_login_with_new_user(self): def test_pre_social_login_with_already_saved_user(self): """ - Scenario 3: Social user already has an ID (already saved/authenticated). + Social user already has an ID (already saved/authenticated). Should skip user lookup/creation and elevate permissions. """ - # User is already saved in DB saved_user = User.objects.create_user( username="saveduser", is_staff=False, @@ -81,13 +69,10 @@ def test_pre_social_login_with_already_saved_user(self): mock_sociallogin = MagicMock() mock_sociallogin.user = saved_user - # Run method self.adapter.pre_social_login(self.request, mock_sociallogin) - # Verify connect was NOT called mock_sociallogin.connect.assert_not_called() - # Verify existing saved user is now superuser/staff saved_user.refresh_from_db() self.assertTrue(saved_user.is_staff) self.assertTrue(saved_user.is_superuser) \ No newline at end of file diff --git a/src/feedback/tests/test_commands.py b/src/feedback/tests/test_commands.py index ee4eb271..840e006c 100644 --- a/src/feedback/tests/test_commands.py +++ b/src/feedback/tests/test_commands.py @@ -15,31 +15,31 @@ def setUp(self): """Set up test instances for different statuses and dates.""" now = timezone.now() - # 1. EXPIRED: > 7 days old -> SHOULD BE DELETED + # 1. EXPIRED: > 7 days old -> should be deleted self.expired_old = EmailChange.objects.create( status=EmailChange.Status.EXPIRED, dynamic_expiry_time=now - timedelta(days=8), ) - # 2. EXPIRED: < 7 days old -> SHOULD NOT BE DELETED + # 2. EXPIRED: < 7 days old -> should not be deleted self.expired_recent = EmailChange.objects.create( status=EmailChange.Status.EXPIRED, dynamic_expiry_time=now - timedelta(days=5), ) - # 3. MAGIC_LINK_SENT: > 30 days old -> SHOULD BE DELETED + # 3. MAGIC_LINK_SENT: > 30 days old -> should be deleted self.link_sent_old = EmailChange.objects.create( status=EmailChange.Status.MAGIC_LINK_SENT, dynamic_expiry_time=now - timedelta(days=31), ) - # 4. OTP_SENT: > 30 days old -> SHOULD BE DELETED + # 4. OTP_SENT: > 30 days old -> should be deleted self.otp_sent_old = EmailChange.objects.create( status=EmailChange.Status.OTP_SENT, dynamic_expiry_time=now - timedelta(days=32), ) - # 5. MAGIC_LINK_SENT: < 30 days old -> SHOULD NOT BE DELETED + # 5. MAGIC_LINK_SENT: < 30 days old -> should not be deleted self.link_sent_recent = EmailChange.objects.create( status=EmailChange.Status.MAGIC_LINK_SENT, dynamic_expiry_time=now - timedelta(days=15), @@ -50,25 +50,21 @@ def test_command_deletes_eligible_records(self, mock_logger): """Test deleting eligible records (hits if branch: deleted_count > 0).""" out = StringIO() - # Execute management command call_command("cleanup_email_change_req", stdout=out) output = out.getvalue() - # Verify stdout output & logger call self.assertIn("Cleanup successful: Deleted 3 EmailChange records.", output) mock_logger.info.assert_called_once_with( "Cleanup successful: Deleted 3 EmailChange records." ) - # Verify correct records were deleted from DB remaining_ids = set(EmailChange.objects.values_list("id", flat=True)) self.assertEqual(remaining_ids, {self.expired_recent.id, self.link_sent_recent.id}) @patch("feedback.management.commands.cleanup_email_change_req.logger") def test_command_when_no_records_eligible(self, mock_logger): """Test behavior when no records match (hits else branch: deleted_count == 0).""" - # Clear out all objects first EmailChange.objects.all().delete() out = StringIO() @@ -76,7 +72,6 @@ def test_command_when_no_records_eligible(self, mock_logger): output = out.getvalue() - # Verify stdout output & logger call for 0 records branch self.assertIn("Cleanup run: No records were eligible for deletion.", output) mock_logger.info.assert_called_once_with( "Cleanup run: No records were eligible for deletion." diff --git a/src/feedback/tests/test_parser_ergebnisse.py b/src/feedback/tests/test_parser_ergebnisse.py index 4ebcc8a2..8859aaee 100644 --- a/src/feedback/tests/test_parser_ergebnisse.py +++ b/src/feedback/tests/test_parser_ergebnisse.py @@ -3,7 +3,7 @@ from django.conf import settings from django.test import TestCase -from feedback.models import Semester, Veranstaltung, Fragebogen2008, Fragebogen2009, Fragebogen2012, Fragebogen2016, Fragebogen2025 +from feedback.models import Semester, Veranstaltung, Fragebogen2008, Fragebogen2009, Fragebogen2012, Fragebogen2016, Fragebogen2020, Fragebogen2025 from feedback.parser.ergebnisse import parse_ergebnisse from feedback.parser.ergebnisse.parser import Parser @@ -79,5 +79,8 @@ def test_parse_ergebnisse2012(self): def test_parse_ergebnisse2016(self): self._do_parse_ergebnisse_test(20155, '2016', Fragebogen2016) + def test_parse_ergebnisse2020(self): + self._do_parse_ergebnisse_test(20155, '2020', Fragebogen2020) + def test_parse_ergebnisse2025(self): self._do_parse_ergebnisse_test(20255, '2025', Fragebogen2025) diff --git a/src/feedback/tests/test_tools.py b/src/feedback/tests/test_tools.py index fa911535..538a4d7b 100644 --- a/src/feedback/tests/test_tools.py +++ b/src/feedback/tests/test_tools.py @@ -1,11 +1,19 @@ # coding=utf-8 +from django.core import mail from django.template import Context -from django.test import TestCase +from django.test import TestCase, override_settings from feedback.models import Fragebogen2009, Ergebnis2009, Fragebogen2012, Ergebnis2012 from feedback.tests.tools import get_veranstaltung -from feedback.tools import get_average, render_email, ean_checksum_calc, ean_checksum_valid +from feedback.tools import ( + get_average, + render_email, + ean_checksum_calc, + ean_checksum_valid, + send_change_email_link, + send_change_email_otp, +) class GetAverageTest(TestCase): @@ -52,6 +60,7 @@ def test_empty_sheets(self): self.assertSequenceEqual(get_average(Ergebnis2009, self.f[:2], 'v_feedbackpreis'), [None, 0]) +@override_settings(DEFAULT_FROM_EMAIL='test@example.com') class ToolsTest(TestCase): def test_render_email(self): template = 'Die Antwort ist {{op}} {{antwort}}.' @@ -65,3 +74,52 @@ def test_ean_calc(self): self.assertEqual(ean_checksum_calc(200000151700), 0) self.assertEqual(ean_checksum_calc(2000001517000), 0) self.assertTrue(ean_checksum_valid(2000001517000)) + + def test_ean_invalid_length(self): + self.assertFalse(ean_checksum_valid(1234567)) + self.assertFalse(ean_checksum_valid(123456789)) + + def test_ean_invalid_checksum(self): + self.assertFalse(ean_checksum_valid(2000001517001)) + + def test_send_change_email_link(self): + send_change_email_link( + 'new@example.com', + 'https://example.com/change-email/abc', + 15, + ) + + self.assertEqual(len(mail.outbox), 1) + + email = mail.outbox[0] + self.assertEqual( + email.subject, + 'E-Mail-Änderungsanfrage / Email change request', + ) + self.assertEqual(email.from_email, 'test@example.com') + self.assertEqual(email.to, ['new@example.com']) + self.assertIn('new@example.com', email.body) + self.assertIn('https://example.com/change-email/abc', email.body) + self.assertIn('15', email.body) + + def test_send_change_email_otp(self): + send_change_email_otp( + 'new@example.com', + 'old@example.com', + '123456', + 10, + ) + + self.assertEqual(len(mail.outbox), 1) + + email = mail.outbox[0] + self.assertEqual( + email.subject, + 'E-Mail-Änderung OTP / Email change OTP', + ) + self.assertEqual(email.from_email, 'test@example.com') + self.assertEqual(email.to, ['new@example.com']) + self.assertIn('new@example.com', email.body) + self.assertIn('old@example.com', email.body) + self.assertIn('123456', email.body) + self.assertIn('10', email.body) \ No newline at end of file From e238a0e648345addff60a384ccf7d7866994fe24 Mon Sep 17 00:00:00 2001 From: 4-dash <120916864+4-dash@users.noreply.github.com> Date: Sun, 9 Aug 2026 12:06:33 +0200 Subject: [PATCH 24/32] fix --- .dockerignore | 4 +++- .gitignore | 4 +++- .../tests/.test_views_veranstalter.py.swp | Bin 32768 -> 0 bytes 3 files changed, 6 insertions(+), 2 deletions(-) delete mode 100644 src/feedback/tests/.test_views_veranstalter.py.swp diff --git a/.dockerignore b/.dockerignore index e18fae4c..8841f67f 100644 --- a/.dockerignore +++ b/.dockerignore @@ -27,4 +27,6 @@ node_modules/ *.mo -.env \ No newline at end of file +.env + +.swp \ No newline at end of file diff --git a/.gitignore b/.gitignore index 25255289..f2f148e4 100644 --- a/.gitignore +++ b/.gitignore @@ -27,4 +27,6 @@ node_modules/ *.mo -.env \ No newline at end of file +.env + +.swp \ No newline at end of file diff --git a/src/feedback/tests/.test_views_veranstalter.py.swp b/src/feedback/tests/.test_views_veranstalter.py.swp deleted file mode 100644 index bbcce0d72a0c9675d02d73d8d6dbd3d37c3bc9d0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32768 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 HloI!U7*{{s From 51a78e10fa948e9faebce7cf199675f3507c10b1 Mon Sep 17 00:00:00 2001 From: 4-dash <120916864+4-dash@users.noreply.github.com> Date: Sun, 9 Aug 2026 12:15:19 +0200 Subject: [PATCH 25/32] more tests and improve templatetag --- src/feedback/templatetags/translate_url.py | 36 ++++--- src/feedback/tests/test_templatetags.py | 119 +++++++++++++++++++++ 2 files changed, 139 insertions(+), 16 deletions(-) create mode 100644 src/feedback/tests/test_templatetags.py diff --git a/src/feedback/templatetags/translate_url.py b/src/feedback/templatetags/translate_url.py index b8c95403..7c3ef4a2 100644 --- a/src/feedback/templatetags/translate_url.py +++ b/src/feedback/templatetags/translate_url.py @@ -6,25 +6,29 @@ @register.simple_tag(takes_context=True) def translate_url(context, language): - ''' - used to translate urls for switching languages - ''' - if 'request' in context : # during some errors this prevents server error - try: - view = resolve(context['request'].path_info) - except Resolver404: - return "" + """ + Used to translate URLs for switching languages. + """ + if "request" not in context: + return "" + + try: + view = resolve(context["request"].path_info) + except Resolver404: + return "" - request_language = translation.get_language() + request_language = translation.get_language() + + try: translation.activate(language) namespace = view.namespace view_name = f"{namespace}:{view.url_name}" if namespace else view.url_name - - url = reverse(view_name, args=view.args, kwargs=view.kwargs) - + + return reverse( + view_name, + args=view.args, + kwargs=view.kwargs, + ) + finally: translation.activate(request_language) - return url - else : - return "" - \ No newline at end of file diff --git a/src/feedback/tests/test_templatetags.py b/src/feedback/tests/test_templatetags.py new file mode 100644 index 00000000..a094c9b3 --- /dev/null +++ b/src/feedback/tests/test_templatetags.py @@ -0,0 +1,119 @@ +from django.http import HttpResponse +from django.test import RequestFactory, SimpleTestCase, override_settings +from django.urls import include, path +from django.utils.translation import gettext_lazy as _ +from django.utils import translation +from django.conf.urls.i18n import i18n_patterns + +from feedback.templatetags.filter_range import filter_range +from feedback.templatetags.translate_url import translate_url + + +class FilterRangeTests(SimpleTestCase): + def test_returns_range_as_list(self): + self.assertEqual(filter_range(0, 5), [0, 1, 2, 3, 4]) + + def test_start_is_inclusive(self): + self.assertEqual(filter_range(2, 5), [2, 3, 4]) + + def test_end_is_exclusive(self): + self.assertEqual(filter_range(2, 5), [2, 3, 4]) + self.assertNotIn(5, filter_range(2, 5)) + + def test_empty_range(self): + self.assertEqual(filter_range(5, 5), []) + + def test_reverse_range(self): + self.assertEqual(filter_range(5, 2), []) + + def test_negative_numbers(self): + self.assertEqual(filter_range(-2, 3), [-2, -1, 0, 1, 2]) + + +# URL namespace used by translate_url tests. +def test_view(request, pk=None): + return HttpResponse("OK") + +urlpatterns = [ + path( + "", + include( + ( + [ + path( + "product//", + test_view, + name="detail", + ), + ], + "testapp", + ), + namespace="products", + ), + ), +] + +@override_settings(ROOT_URLCONF=__name__) +class TranslateUrlTests(SimpleTestCase): + def setUp(self): + self.factory = RequestFactory() + translation.activate("en") + + def tearDown(self): + translation.deactivate() + + def test_returns_empty_string_when_request_is_missing(self): + context = {} + + result = translate_url(context, "de") + + self.assertEqual(result, "") + + def test_returns_empty_string_for_unresolvable_url(self): + request = self.factory.get("/does-not-exist/") + + context = { + "request": request, + } + + result = translate_url(context, "de") + + self.assertEqual(result, "") + + def test_translates_url(self): + request = self.factory.get("/product/123/") + + context = { + "request": request, + } + + result = translate_url(context, "de") + + self.assertEqual(result, "/product/123/") + + def test_preserves_original_language(self): + translation.activate("en") + + request = self.factory.get("/product/123/") + + context = { + "request": request, + } + + translate_url(context, "de") + + self.assertEqual( + translation.get_language(), + "en", + ) + + def test_uses_url_kwargs(self): + request = self.factory.get("/product/42/") + + context = { + "request": request, + } + + result = translate_url(context, "de") + + self.assertEqual(result, "/product/42/") \ No newline at end of file From c073cd4b5221502a8365a39e3a5487d48bd006a8 Mon Sep 17 00:00:00 2001 From: 4-dash <120916864+4-dash@users.noreply.github.com> Date: Sun, 9 Aug 2026 12:50:07 +0200 Subject: [PATCH 26/32] add comment --- .dockerignore | 2 +- .gitignore | 2 +- src/settings/prod.py | 16 +++++++++------- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/.dockerignore b/.dockerignore index 8841f67f..5455e764 100644 --- a/.dockerignore +++ b/.dockerignore @@ -29,4 +29,4 @@ node_modules/ .env -.swp \ No newline at end of file +*.swp \ No newline at end of file diff --git a/.gitignore b/.gitignore index f2f148e4..9eab4f7e 100644 --- a/.gitignore +++ b/.gitignore @@ -29,4 +29,4 @@ node_modules/ .env -.swp \ No newline at end of file +*.swp \ No newline at end of file diff --git a/src/settings/prod.py b/src/settings/prod.py index 6d470ab5..01a5107d 100644 --- a/src/settings/prod.py +++ b/src/settings/prod.py @@ -16,15 +16,17 @@ raise ValueError("CRITICAL: SECRET_KEY environment variable is missing!") -URL_PREFIX = 'feedback/' -LOGIN_URL = '/' + URL_PREFIX[:-1] + LOGIN_URL -LOGIN_REDIRECT_URL = '/' + URL_PREFIX[:-1] + LOGIN_REDIRECT_URL -ACCOUNT_LOGOUT_REDIRECT_URL = '/' + URL_PREFIX[:-1] + ACCOUNT_LOGOUT_REDIRECT_URL +## uncomment lines below to fix isses with prefix "feedback" -SESSION_COOKIE_PATH = '/feedback' -SESSION_COOKIE_SECURE = True +# URL_PREFIX = 'feedback/' +# LOGIN_URL = '/' + URL_PREFIX[:-1] + LOGIN_URL +# LOGIN_REDIRECT_URL = '/' + URL_PREFIX[:-1] + LOGIN_REDIRECT_URL +# ACCOUNT_LOGOUT_REDIRECT_URL = '/' + URL_PREFIX[:-1] + ACCOUNT_LOGOUT_REDIRECT_URL + +# SESSION_COOKIE_PATH = '/feedback' +# CSRF_COOKIE_PATH = SESSION_COOKIE_PATH -CSRF_COOKIE_PATH = SESSION_COOKIE_PATH +SESSION_COOKIE_SECURE = True CSRF_COOKIE_SECURE = True SOCIALACCOUNT_PROVIDERS = { From f9146b2d42dc953ad3015ba64ec9d27b7c0c2391 Mon Sep 17 00:00:00 2001 From: 4-dash <120916864+4-dash@users.noreply.github.com> Date: Sun, 9 Aug 2026 12:56:50 +0200 Subject: [PATCH 27/32] update readme badge --- Readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Readme.md b/Readme.md index 0bfe5282..31ed9a00 100644 --- a/Readme.md +++ b/Readme.md @@ -1,5 +1,5 @@ # Feedback-Software -![Build Status](https://github.com/d120/pyfeedback/workflows/Test/badge.svg?branch=master) +[![Test](https://github.com/d120/pyfeedback/actions/workflows/django.yml/badge.svg?branch=master)](https://github.com/d120/pyfeedback/actions/workflows/django.yml) [![Coverage Status](https://coveralls.io/repos/github/d120/pyfeedback/badge.svg?branch=master)](https://coveralls.io/github/d120/pyfeedback?branch=master) From bc13765e95a785675b3fdfac61ab58b1b56d4ec3 Mon Sep 17 00:00:00 2001 From: 4-dash <120916864+4-dash@users.noreply.github.com> Date: Sun, 9 Aug 2026 13:13:13 +0200 Subject: [PATCH 28/32] fix --- entrypoint.sh | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/entrypoint.sh b/entrypoint.sh index bda2108f..b1e18774 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -7,10 +7,24 @@ echo "Applying database migrations..." python manage.py migrate --no-input if [ $# -eq 0 ]; then - echo "Starting Gunicorn with ${GUNICORN_WORKERS:-3} workers..." - exec gunicorn --bind 0.0.0.0:8000 --workers "${GUNICORN_WORKERS:-3}" wsgi:application + + if [ "$DJANGO_SETTINGS_MODULE" = "settings.dev" ]; then + echo "Starting Django development server..." + exec python manage.py runserver 0.0.0.0:8000 + + elif [ "$DJANGO_SETTINGS_MODULE" = "settings.prod" ]; then + echo "Starting Gunicorn with ${GUNICORN_WORKERS:-3} workers..." + exec gunicorn \ + --bind 0.0.0.0:8000 \ + --workers "${GUNICORN_WORKERS:-3}" \ + wsgi:application + + else + echo "Unknown DJANGO_SETTINGS_MODULE: $DJANGO_SETTINGS_MODULE" + exit 1 + fi + else - # (bash, python manage.py createsuperuser etc.) echo "Running custom command: $@" exec "$@" fi From 42c779c35bd50b4ef4c1c69d3ca4dbaef5450a5f Mon Sep 17 00:00:00 2001 From: 4-dash <120916864+4-dash@users.noreply.github.com> Date: Sun, 9 Aug 2026 13:16:33 +0200 Subject: [PATCH 29/32] Update Readme.md --- Readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Readme.md b/Readme.md index 31ed9a00..7a67d03c 100644 --- a/Readme.md +++ b/Readme.md @@ -45,5 +45,5 @@ Use `python src/manage.py runserver --settings=settings.prod` to run production ## Docker -- DJANGO_SETTINGS_MODULE: `settings.prod` or `settings.dev`. Docker uses wsgi.py, making `settings.prod` *default*. +- DJANGO_SETTINGS_MODULE: `settings.prod` or `settings.dev`. `settings.prod` runs with wsgi.py and gunicorn, `settings.dev` with manage.py - GUNICORN_WORKERS: *default* 3 workers From 7e36e29b6983da794dc8d68bbec899fac49b66f5 Mon Sep 17 00:00:00 2001 From: 4-dash <120916864+4-dash@users.noreply.github.com> Date: Sun, 9 Aug 2026 13:34:23 +0200 Subject: [PATCH 30/32] build action --- .github/workflows/docker-build.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 .github/workflows/docker-build.yml diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml new file mode 100644 index 00000000..ee352d5f --- /dev/null +++ b/.github/workflows/docker-build.yml @@ -0,0 +1,13 @@ +name: Docker Build + +on: [push, pull_request] + +jobs: + docker-build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Build Docker image + run: docker build -t pyfeedback:test . \ No newline at end of file From 004cd36c87e4fa9301efe9f9a50078dbefe523a1 Mon Sep 17 00:00:00 2001 From: 4-dash <120916864+4-dash@users.noreply.github.com> Date: Sun, 9 Aug 2026 13:43:29 +0200 Subject: [PATCH 31/32] fix --- Dockerfile | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 116b6602..929a8ab5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,7 @@ # ========================================== # Build Node.js Dependencies # ========================================== + FROM node:22-slim AS node-builder WORKDIR /app @@ -11,6 +12,7 @@ RUN npm ci --only=production # ========================================== # Build Python Dependencies # ========================================== + FROM python:3.13-slim AS python-builder ENV PYTHONDONTWRITEBYTECODE=1 \ @@ -28,18 +30,22 @@ RUN pip install --root-user-action ignore --upgrade pip && \ # ========================================== # Final Production Image # ========================================== + FROM python:3.13-slim AS production ARG DEBIAN_FRONTEND=noninteractive + ENV PYTHONDONTWRITEBYTECODE=1 \ PYTHONUNBUFFERED=1 -RUN apt-get update && apt-get install -y --no-install-recommends \ - gettext \ +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + gettext \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* # non-root user + RUN useradd -m -r appuser WORKDIR /app @@ -54,7 +60,11 @@ COPY --chown=appuser:appuser . . COPY --chown=appuser:appuser entrypoint.sh /usr/local/bin/entrypoint.sh RUN chmod +x /usr/local/bin/entrypoint.sh +RUN mkdir -p /app/static && \ + chown appuser:appuser /app/static + # Build static assets & translations as non-root user + USER appuser RUN django-admin compilemessages && \ From db666bfad815fafa016c9fffa07cc171427c39f9 Mon Sep 17 00:00:00 2001 From: 4-dash <120916864+4-dash@users.noreply.github.com> Date: Sun, 9 Aug 2026 13:52:01 +0200 Subject: [PATCH 32/32] badge --- Readme.md | 1 + 1 file changed, 1 insertion(+) diff --git a/Readme.md b/Readme.md index 7a67d03c..e4d6f3dd 100644 --- a/Readme.md +++ b/Readme.md @@ -1,5 +1,6 @@ # Feedback-Software [![Test](https://github.com/d120/pyfeedback/actions/workflows/django.yml/badge.svg?branch=master)](https://github.com/d120/pyfeedback/actions/workflows/django.yml) +[![Docker Build](https://github.com/d120/pyfeedback/actions/workflows/docker-build.yml/badge.svg?branch=master)](https://github.com/d120/pyfeedback/actions/workflows/docker-build.yml) [![Coverage Status](https://coveralls.io/repos/github/d120/pyfeedback/badge.svg?branch=master)](https://coveralls.io/github/d120/pyfeedback?branch=master)