Merge branch 'main' into 'production'

PIP Changes, Jäsenrekisteri searchbar, Ilmomasiina changes, version bumps

See merge request sahkoinsinoorikilta/vtmk/web2.0-backend!118
This commit is contained in:
J4DER4
2025-11-20 18:47:57 +00:00
18 changed files with 293 additions and 1987 deletions
+2 -1
View File
@@ -11,4 +11,5 @@ node_modules/
.idea/ .idea/
*.code-workspace *.code-workspace
venv/ venv/
.venv/ .venv/
poetry.lock
+4 -2
View File
@@ -27,7 +27,8 @@ audit:
- pushes - pushes
needs: [] needs: []
before_script: before_script:
- pip install poetry==2.0.1 - pip install pip==25.3
- pip install poetry==2.1.1
- poetry config virtualenvs.create false - poetry config virtualenvs.create false
- poetry install --no-interaction --no-ansi - poetry install --no-interaction --no-ansi
script: script:
@@ -48,7 +49,8 @@ test:
DATABASE_URL: "postgresql://postgres:postgres@postgres:5432/$POSTGRES_DB" DATABASE_URL: "postgresql://postgres:postgres@postgres:5432/$POSTGRES_DB"
DB_HOST: postgres DB_HOST: postgres
before_script: before_script:
- pip install poetry==2.0.1 - pip install pip==25.3
- pip install poetry==2.1.1
- poetry config virtualenvs.create false - poetry config virtualenvs.create false
- poetry install --no-interaction --no-ansi - poetry install --no-interaction --no-ansi
script: script:
+2
View File
@@ -0,0 +1,2 @@
python 3.12.9
poetry 2.1.1
+4 -4
View File
@@ -1,12 +1,11 @@
FROM python:3.12.9-slim-bullseye AS builder FROM python:3.12.9-slim-bullseye AS builder
ENV PYTHONUNBUFFERED 1 ENV PYTHONUNBUFFERED 1
COPY . ./ COPY . ./
ENV POETRY_VERSION=2.1.1
ENV POETRY_VERSION=2.0.1 RUN pip install pip==25.3
RUN pip install "poetry==$POETRY_VERSION" RUN pip install "poetry==$POETRY_VERSION"
RUN poetry self add poetry-plugin-export RUN poetry self add poetry-plugin-export
RUN poetry export --without-hashes > requirements.txt RUN poetry export --without-hashes --format=requirements.txt --output requirements.txt
FROM python:3.12.9-slim-bullseye AS server FROM python:3.12.9-slim-bullseye AS server
@@ -23,6 +22,7 @@ ENV PYTHONUNBUFFERED=1 \
PIP_DEFAULT_TIMEOUT=100 PIP_DEFAULT_TIMEOUT=100
RUN apt-get update && apt-get install --no-install-recommends -y build-essential RUN apt-get update && apt-get install --no-install-recommends -y build-essential
RUN pip install pip==25.3
RUN pip install --no-deps -r requirements.txt RUN pip install --no-deps -r requirements.txt
RUN python manage.py collectstatic --noinput RUN python manage.py collectstatic --noinput
+32 -12
View File
@@ -30,24 +30,34 @@ For depedencies and virtual environment, we use [poetry](https://python-poetry.o
First install [python](https://wiki.python.org/moin/BeginnersGuide/Download). Then install poetry: First install [python](https://wiki.python.org/moin/BeginnersGuide/Download). Then install poetry:
```bash ```bash
python -m pip install poetry==2.0.1 python -m pip install poetry==2.1.1
``` ```
The easiest integration with VSCode is to have poetry install virtual environment in project folder, configured with CMD Install dependencies with
```bash ```bash
python -m poetry config virtualenvs.in-project true poetry install
```
Poetry is configured to install dependencies in a virtual environment, so you should see `.venv` folder in repo root.
Activate virtual environment in shell
```bash
eval $(poetry env activate)
``` ```
### Node ### Node
We use Node.js for few development tasks, like linting. Easiest way to install Node is [nvm](https://github.com/nvm-sh/nvm). After installing install dependencies: We use Node.js for few development tasks, like linting.
Easiest way to install Node is [nvm](https://github.com/nvm-sh/nvm).
After installing install dependencies:
```bash ```bash
npm install npm install
``` ```
TODO: List scripts See [Linting](#linting) for more info
### Database ### Database
@@ -61,18 +71,28 @@ docker run --name sik.web.db -e POSTGRES_PASSWORD=postgres -p 5432:5432 -d postg
## Development ## Development
Activate virtual environment in shell Install dependencies with
```bash
eval $(python -m poetry env activate)
```
Install dependencies
```bash ```bash
poetry install poetry install
``` ```
and make sure you are using Python from your virutal environment.
Virtual environment can be activated with
```bash
eval $(poetry env activate)
```
and you verify correct Python executable with
```bash
which python
# should return path similar to {your-system path}/web2.0-backend/.venv/bin/python
```
### Initializing data ### Initializing data
Run the following `manage.py` commands to initialize a new database. Do not run these in production without thinking! Run the following `manage.py` commands to initialize a new database. Do not run these in production without thinking!
+15 -5
View File
@@ -4,17 +4,27 @@ services:
volumes: volumes:
- dbdata:/var/lib/postgresql/data - dbdata:/var/lib/postgresql/data
ports: ports:
- "5432:5432" - 5432:5432
environment: environment:
- POSTGRES_PASSWORD=postgres - POSTGRES_PASSWORD=postgres
web: web:
build: . build: .
image: registry.gitlab.com/sahkoinsinoorikilta/vtmk/web2.0-backend environment:
env_file: - DEPLOY_ENV=local
- .env - HOST=localhost
- DEBUG=True
- SECRET_KEY=7p$85^4ibb^p4-=vs44b7!y0e-zemugze18@a#30&71=a8)dp(
- DB_NAME=postgres
- DB_USER=postgres
- DB_PASSWD=postgres
- DB_HOST=db
- DB_PORT=5432
- EMAIL_API_KEY=
- GROUP_KEY=
- GOOGLE_CREDS='{}'
ports: ports:
- "8000:8000" - 8000:8000
depends_on: depends_on:
- db - db
@@ -0,0 +1,65 @@
# Generated by Django 4.2.24 on 2025-10-13 14:48
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("kaehmy", "0011_delete_kaehmybaserole"),
]
operations = [
migrations.AlterField(
model_name="baserole",
name="category",
field=models.CharField(
choices=[
("board", "Board"),
("corporate", "Corporate affairs"),
("freshman", "Freshmen"),
("international", "International"),
("siwa", "SIK's free time"),
("media", "Media"),
("tech", "Technology"),
("wellbeing", "Wellbeing"),
("sikpaja", "Sik-paja"),
("ceremonies", "Ceremonies"),
("studies", "Studies"),
("sosso", "Sössö magazine"),
("pota", "PoTa"),
("alumni", "Alumni relations"),
("n", "N"),
("others", "Others"),
],
default="others",
max_length=255,
verbose_name="Category",
),
),
migrations.AlterField(
model_name="customrole",
name="baserole_ptr",
field=models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="kaehmy.baserole",
),
),
migrations.AlterField(
model_name="presetrole",
name="baserole_ptr",
field=models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="kaehmy.baserole",
),
),
]
+6 -1
View File
@@ -3,8 +3,13 @@
from django.contrib import admin from django.contrib import admin
from members.models import Member, Request, Payment from members.models import Member, Request, Payment
# Register your models here. # Register your models here.
admin.site.register(Member) class MemberAdmin(admin.ModelAdmin):
search_fields = ("first_name", "last_name", "email", "POR")
admin.site.register(Member, MemberAdmin)
admin.site.register(Request) admin.site.register(Request)
admin.site.register(Payment) admin.site.register(Payment)
Generated
-1925
View File
File diff suppressed because it is too large Load Diff
+41 -36
View File
@@ -5,44 +5,49 @@ authors = [
description = "Backend for sahkoinsinoorikilta.fi" description = "Backend for sahkoinsinoorikilta.fi"
name = "web2.0-backend" name = "web2.0-backend"
readme = "README.md" readme = "README.md"
requires-python = ">=3.12" requires-python = "~3.12"
version = "0.1.0" version = "0.1.0"
dependencies = [ [virtualenvs]
"decorator (>=4.4.2,<5.0.0)", create = true
"Django (>=4.2.19,<5.0.0)", in-project = true
"django-app-namespace-template-loader (>=0.4.1,<1.0.0)",
"django-auditlog (>=2.1.1,<3.0.0)", [tool.poetry.dependencies]
"django-autocomplete-light (>=3.4.1,<4.0.0)", decorator = "^4.4.2"
"django-bootstrap3 (>=21.2.0,<22.0.0)", Django = "^4.2.19"
"django-cors-headers (>=3.13.0,<4.0.0)", django-app-namespace-template-loader = "^0.4.1"
"django-filter (>=22.1.0,<23.0.0)", django-auditlog = "^2.1.1"
"django-import-export (>=2.8.0,<3.0.0)", django-autocomplete-light = "^3.4.1"
"django-modeltranslation (>=0.18.4,<1.0.0)", django-bootstrap3 = "^21.2.0"
"django-phonenumber-field[phonenumbers] (>=6.4.0,<7.0.0)", django-cors-headers = "^3.13.0"
"django-polymorphic (>=3.1.0,<4.0.0)", django-filter = "^22.1.0"
"django-tables2 (>=2.4.1,<3.0.0)", django-import-export = "^2.8.0"
"djangorestframework (>=3.12.4,<4.0.0)", django-modeltranslation = "^0.18.4"
"djangorestframework-simplejwt (>=5.2.0,<6.0.0)", django-phonenumber-field = {version = "^6.4.0", extras = ["phonenumbers"]}
"google-auth (>=2.9.1,<3.0.0)", django-polymorphic = "^3.1.0"
"google-api-python-client (>=2.54.0,<3.0.0)", django-tables2 = "^2.4.1"
"gunicorn (>=20.1.0,<21.0.0)", djangorestframework = "^3.12.4"
"jsonschema (>=4.9.0,<5.0.0)", djangorestframework-simplejwt = "^5.5.0"
"Markdown (>=3.2.2,<4.0.0)", google-auth = "^2.9.1"
"openpyxl (>=2.6.4,<3.0.0)", google-api-python-client = "^2.54.0"
"Pillow (>=10.0.0,<11.0.0)", gunicorn = "^23.0.0"
"psycopg2-binary (>=2.9.3,<3.0.0)", jsonschema = "^4.9.0"
"pyexcel (>=0.7.0,<1.0.0)", Markdown = "^3.2.2"
"pyexcel-io (>=0.6.0,<1.0.0)", openpyxl = "^2.6.4"
"pyexcel-xlsx (>=0.6.0,<1.0.0)", Pillow = "^10.0.0"
"python-dotenv (>=0.20.0,<1.0.0)", psycopg2-binary = "^2.9.3"
"requests (>=2.28.1,<3.0.0)", pyexcel = "^0.7.0"
"sendgrid (>=6.7.0,<7.0.0)", pyexcel-io = "^0.6.0"
"sentry-sdk (>=1.4.3,<2.0.0)", pyexcel-xlsx = "^0.6.0"
"six (>=1.12.0,<2.0.0)", python-dotenv = "^0.20.0"
"uWSGI (>=2.0.28,<3.0.0)", requests = "^2.28.1"
"whitenoise (>=6.2.0,<7.0.0)", sendgrid = "^6.7.0"
] sentry-sdk = "^2.24.1"
six = "^1.12.0"
uWSGI = "^2.0.28"
whitenoise = "^6.2.0"
pyjwt = "^2.9.0"
setuptools = "^80.9.0"
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
black = "^25.1.0" black = "^25.1.0"
@@ -0,0 +1,32 @@
# Generated by Django 4.2.24 on 2025-10-13 14:48
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
dependencies = [
("contenttypes", "0002_remove_content_type_name"),
("webapp", "0082_delete_baserole"),
]
operations = [
migrations.AddField(
model_name="signup",
name="submit_id",
field=models.UUIDField(default=uuid.uuid4, editable=False, null=True),
),
migrations.AlterField(
model_name="basewebhook",
name="polymorphic_ctype",
field=models.ForeignKey(
editable=False,
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="polymorphic_%(app_label)s.%(class)s_set+",
to="contenttypes.contenttype",
),
),
]
@@ -0,0 +1,18 @@
# Generated by Django 4.2.24 on 2025-10-13 15:19
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("webapp", "0083_signup_submit_id_alter_basewebhook_polymorphic_ctype"),
]
operations = [
migrations.AlterField(
model_name="signup",
name="submit_id",
field=models.UUIDField(null=True),
),
]
@@ -0,0 +1,18 @@
# Generated by Django 4.2.24 on 2025-10-13 15:46
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("webapp", "0084_alter_signup_submit_id"),
]
operations = [
migrations.AlterField(
model_name="signup",
name="submit_id",
field=models.CharField(null=True),
),
]
@@ -0,0 +1,18 @@
# Generated by Django 4.2.24 on 2025-10-13 15:51
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("webapp", "0085_alter_signup_submit_id"),
]
operations = [
migrations.AlterField(
model_name="signup",
name="submit_id",
field=models.UUIDField(editable=False, null=True),
),
]
@@ -0,0 +1,18 @@
# Generated by Django 4.2.24 on 2025-10-13 15:53
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("webapp", "0086_alter_signup_submit_id"),
]
operations = [
migrations.AlterField(
model_name="signup",
name="submit_id",
field=models.UUIDField(editable=False, null=True, unique=True),
),
]
+2
View File
@@ -195,6 +195,8 @@ class Signup(models.Model):
email = models.EmailField(blank=True, null=True) email = models.EmailField(blank=True, null=True)
# Random unique identifier. Used for signup editing by the user. # Random unique identifier. Used for signup editing by the user.
uuid = models.UUIDField(default=uuid4, editable=False) uuid = models.UUIDField(default=uuid4, editable=False)
# Random unique identifier generated by browser upon opening signup form. Used to prevent duplicate signups.
submit_id = models.UUIDField(null=True, editable=False, unique=True)
deleted = models.BooleanField(default=False) deleted = models.BooleanField(default=False)
def __str__(self): def __str__(self):
+2 -1
View File
@@ -7,6 +7,7 @@ class SignupSerializer(serializers.ModelSerializer):
source="signupForm", queryset=SignupForm.objects.all() source="signupForm", queryset=SignupForm.objects.all()
) )
list_name = serializers.CharField(read_only=True) list_name = serializers.CharField(read_only=True)
submit_id = serializers.UUIDField(required=False)
def add_extra_fields(self, validated_data): def add_extra_fields(self, validated_data):
questions = validated_data["signupForm"].questions questions = validated_data["signupForm"].questions
@@ -34,7 +35,7 @@ class SignupSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Signup model = Signup
fields = ("id", "signupForm_id", "answer", "list_name") fields = ("id", "submit_id", "signupForm_id", "answer", "list_name")
extra_kwargs = { extra_kwargs = {
"url": { "url": {
"view_name": "signup-detail", "view_name": "signup-detail",
+14
View File
@@ -1,6 +1,7 @@
"""Webapp views.""" """Webapp views."""
import json import json
import time
from jwt import decode from jwt import decode
from jwt.exceptions import InvalidTokenError from jwt.exceptions import InvalidTokenError
from django.utils import timezone from django.utils import timezone
@@ -11,6 +12,7 @@ from django.views.decorators.http import require_http_methods
from django_filters import rest_framework as filters from django_filters import rest_framework as filters
from django.db.models import Prefetch from django.db.models import Prefetch
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.db.utils import IntegrityError
from rest_framework import routers from rest_framework import routers
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
@@ -212,6 +214,14 @@ class SignupViewSet(ModelViewSet):
return self.partial_update(request, *args, **kwargs) return self.partial_update(request, *args, **kwargs)
def create(self, request, *args, **kwargs): def create(self, request, *args, **kwargs):
# Temporary manual duplicate check as submit_id uniqueness is not enforced in deployment database
if "submit_id" in request.data and Signup.objects.filter(
submit_id=request.data["submit_id"]
):
return JsonResponse(
status=200, data={"message": "The submission has already been received"}
)
id = request.data["signupForm_id"] id = request.data["signupForm_id"]
try: try:
answer = request.data["answer"] answer = request.data["answer"]
@@ -226,6 +236,10 @@ class SignupViewSet(ModelViewSet):
return JsonResponse( return JsonResponse(
status=404, data={"error": f"SignupForm {id} not found"} status=404, data={"error": f"SignupForm {id} not found"}
) )
except IntegrityError:
return JsonResponse(
status=200, data={"message": "The submission has already been received"}
)
else: else:
return JsonResponse( return JsonResponse(
status=404, data={"error": f"SignupForm {id} not found"} status=404, data={"error": f"SignupForm {id} not found"}