Compare commits
43 Commits
9cc1068e63
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 3bd36fe8dc | |||
| 2558c81860 | |||
| d99df85e26 | |||
| 6eeb7769a4 | |||
| d2c4a6ef6a | |||
| 90dc566511 | |||
| 0f616f51e1 | |||
| c9f6b2d163 | |||
| c71f2ae0f1 | |||
|
|
3b4e42c119 | ||
|
|
a1cbd66da6 | ||
|
|
4d8a7a68f4 | ||
|
|
acc8375d2b | ||
|
|
dac53d2d06 | ||
|
|
7613f3a547 | ||
| 2d6a06e1b2 | |||
|
|
bba8ac4703 | ||
| 28d3d99754 | |||
| 78c2860cca | |||
| 2024466a48 | |||
| 72570e25c2 | |||
|
|
54c344d262 | ||
|
|
047dfcb147 | ||
| a3b252c9be | |||
| b0e686245a | |||
|
|
b50c010b3b | ||
| be581675cd | |||
| c1519bab0f | |||
| 585bc60676 | |||
| b9943b7d41 | |||
| 65ac5ae18e | |||
| 8ff3905657 | |||
| 5d2a052c1e | |||
| 4a7076b120 | |||
| 2204c07deb | |||
| 07449db128 | |||
| 5d9ad679de | |||
| 0e1a61cefc | |||
| 4d35e498c5 | |||
| 370577493a | |||
| e10fa77c3a | |||
|
|
1e71779c6d | ||
|
|
d01dde658f |
14
.env.default
Normal file
14
.env.default
Normal file
@@ -0,0 +1,14 @@
|
||||
HOSTNAME="fet.at" # Hostname that will be used to filter requests
|
||||
DEBUG="false" # Debug flag ( disables ldap integration )
|
||||
LDAP="True" # Ldap flag ( enables connection with fet ldap )
|
||||
#SECRET_KEY="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" # Secret key for the Website encryption
|
||||
ETHERPAD_GROUP="g.snlbqn7S6ksRbom3" # The Etherpad Group to use
|
||||
|
||||
EMAIL_HOST_USER="verleih@fet.at" # The user to be used for rental Emails
|
||||
#EMAIL_HOST_PASSWORD="password" # The password for the smtp account for the rental Email
|
||||
|
||||
#ETHERPAD_DB_USER="user" # User for the Eterpad MySql Database
|
||||
#ETHERPAD_DB_PASSWORD="password" # Password for the Etherpad MySql Database
|
||||
|
||||
#DJANGO_MYSQL_USER="user" # The MySql User used for Database
|
||||
#DJANGO_MYSQL_PASSWORD"="password" # The Password for the MySql Database
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,4 +1,4 @@
|
||||
.env/*
|
||||
.env*
|
||||
*.pyc
|
||||
*_design1
|
||||
fet2020/.env/*
|
||||
@@ -19,3 +19,4 @@ flowbite
|
||||
gallery/*
|
||||
tailwind
|
||||
whoosh_index
|
||||
databases
|
||||
|
||||
61
Dockerfile
61
Dockerfile
@@ -1,61 +0,0 @@
|
||||
FROM tiangolo/uwsgi-nginx:python3.8-alpine AS builder
|
||||
|
||||
RUN python -m venv /opt/venv
|
||||
# Make sure we use the virtualenv:
|
||||
ENV PATH="/opt/venv/bin:$PATH"
|
||||
|
||||
RUN apk add --no-cache --virtual .build-deps ca-certificates gcc linux-headers \
|
||||
musl-dev \
|
||||
jpeg-dev \
|
||||
zlib-dev \
|
||||
libffi-dev \
|
||||
mysql \
|
||||
mariadb-dev \
|
||||
freetype-dev \
|
||||
# install for pymupdf
|
||||
# && apk add --no-cache \
|
||||
# libffi-dev \
|
||||
# zlib-dev \
|
||||
# freetype-dev \
|
||||
# make \
|
||||
# build-base \
|
||||
# gcc \
|
||||
# jbig2dec \
|
||||
# jpeg-dev \
|
||||
# harfbuzz-dev \
|
||||
# libc-dev \
|
||||
# mupdf-dev \
|
||||
# musl-dev \
|
||||
# openjpeg-dev \
|
||||
# swig \
|
||||
# && ln -s /usr/lib/libjbig2dec.so.0 /usr/lib/libjbig2dec.so \
|
||||
# && pip install pymupdf==1.23.18 \
|
||||
# upgrade pip
|
||||
&& pip install --upgrade pip
|
||||
|
||||
# setting for pymupdf
|
||||
# ENV PYMUPDF_SETUP_MUPDF_TESSERACT="0"
|
||||
|
||||
COPY ./fet2020/requirements.txt /app/requirements.txt
|
||||
|
||||
RUN pip install -r requirements.txt
|
||||
|
||||
# Remove virtual environment dependencies
|
||||
# RUN apk del .builddeps
|
||||
|
||||
FROM tiangolo/uwsgi-nginx:python3.8-alpine
|
||||
|
||||
RUN apk add ghostscript-dev mariadb-connector-c
|
||||
COPY --from=builder /opt/venv /opt/venv
|
||||
|
||||
# Make sure we use the virtualenv:
|
||||
ENV PATH="/opt/venv/bin:$PATH"
|
||||
|
||||
#COPY ./fet2020 /app
|
||||
COPY ./fet2020 /app
|
||||
COPY ./assets /app/assets
|
||||
COPY ./deployment/nginx.conf /etc/nginx/conf.d/fet2020.conf
|
||||
|
||||
#RUN python manage.py makemigrations && python manage.py makemigrations posts members
|
||||
|
||||
|
||||
50
Jenkinsfile
vendored
50
Jenkinsfile
vendored
@@ -1,50 +0,0 @@
|
||||
pipeline {
|
||||
agent any
|
||||
stages {
|
||||
stage('Checkout'){
|
||||
steps {
|
||||
git credentialsId: '3e336704-7d58-4dcb-88b6-1ef7b5392576', url: 'https://git.fet.at/bofh/fet2020.git'
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
stage('build Test') {
|
||||
steps{
|
||||
script {
|
||||
app = docker.build("django2020test", "-f Dockerfile .")
|
||||
}
|
||||
}
|
||||
}
|
||||
stage('Test Inside') {
|
||||
steps{
|
||||
script {
|
||||
app.inside("-u root:root") {
|
||||
sh 'ls && cd fet2020 && ls && ./manage.py test'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('build') {
|
||||
steps{
|
||||
script {
|
||||
app = docker.build("fet2020django", "-f Dockerfile .")
|
||||
}
|
||||
}
|
||||
}
|
||||
stage('Deploy Image') {
|
||||
steps{
|
||||
script {
|
||||
docker.withRegistry( 'https://docker.fet.at/', '3e336704-7d58-4dcb-88b6-1ef7b5392576') {
|
||||
app.push("1.$BUILD_NUMBER")
|
||||
app.push('latest')
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -96,6 +96,8 @@ docker build -t django-nginx-image -f nginx/Dockerfile ./nginx
|
||||
|
||||
### Start docker container
|
||||
|
||||
Add email password for 'Verleih' account to EMAIL_HOST_PASSWORD in the docker compose file!
|
||||
|
||||
Build the docker containers:
|
||||
|
||||
```bash
|
||||
@@ -134,6 +136,10 @@ ckeditor -> django-prose-editor
|
||||
|
||||
## Version History
|
||||
|
||||
2.2.1
|
||||
|
||||
* Fix rental (view, pdf file, sending mail)
|
||||
|
||||
2.2.0
|
||||
|
||||
* Add rental
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -1 +0,0 @@
|
||||
docker build -t fet2020django .
|
||||
@@ -1,102 +0,0 @@
|
||||
version: "2"
|
||||
services:
|
||||
mysql:
|
||||
image: mariadb:10.7
|
||||
container_name: test_mysql
|
||||
networks:
|
||||
- testfet
|
||||
environment:
|
||||
MYSQL_DATABASE: fet2020db
|
||||
MYSQL_USER: user
|
||||
MYSQL_PASSWORD: hgu
|
||||
MYSQL_COLLATION: utf8_general_ci
|
||||
MYSQL_CHARSET: utf8
|
||||
MYSQL_ALLOW_EMPTY_PASSWORD: "yes"
|
||||
volumes:
|
||||
#- mysql-volume:/var/lib/mysql
|
||||
- /srv/initdb/mysql:/docker-entrypoint-initdb.d/
|
||||
healthcheck:
|
||||
test: "mysqladmin ping -h localhost && echo 'show tables;' | mysql 'fet2020db' >/dev/null "
|
||||
timeout: 20s
|
||||
retries: 20
|
||||
etherpadsql:
|
||||
image: mariadb:10.7
|
||||
container_name: test_mysql_etherpad
|
||||
networks:
|
||||
- testfet
|
||||
environment:
|
||||
MYSQL_DATABASE: etherpaddb
|
||||
MYSQL_USER: user
|
||||
MYSQL_PASSWORD: "hgu"
|
||||
MYSQL_COLLATION: utf8_general_ci
|
||||
MYSQL_CHARSET: utf8
|
||||
MYSQL_ALLOW_EMPTY_PASSWORD: "yes"
|
||||
healthcheck:
|
||||
test: "mysqladmin ping -h localhost && echo 'show tables;' | mysql --port 3306 --protocol tcp 'etherpaddb' > /dev/null"
|
||||
timeout: 20s
|
||||
retries: 20
|
||||
volumes:
|
||||
#- ep-mysql-volume:/var/lib/mysql
|
||||
- /srv/initdb/etherpad:/docker-entrypoint-initdb.d/
|
||||
etherpad:
|
||||
image: etherpad/etherpad:1.8.17
|
||||
container_name: test_etherpad
|
||||
networks:
|
||||
- testfet
|
||||
environment:
|
||||
DB_TYPE: mysql
|
||||
DB_HOST: etherpadsql
|
||||
DB_PORT: 3306
|
||||
DB_NAME: etherpaddb
|
||||
DB_USER: root
|
||||
DB_PASS: ""
|
||||
DB_CHARSET: utf8
|
||||
TRUST_PROXY: "true"
|
||||
REQUIRE_SESSION: "true"
|
||||
depends_on:
|
||||
etherpadsql:
|
||||
condition: "service_healthy"
|
||||
volumes:
|
||||
- /srv/APIKEY.txt:/opt/etherpad-lite/APIKEY.txt
|
||||
restart: unless-stopped
|
||||
# - ./deployment/mysql.cnf:/etc/mysql/conf.d
|
||||
healthcheck:
|
||||
test: "curl --fail localhost:9001 >/dev/null "
|
||||
timeout: 10s
|
||||
interval: 15s
|
||||
retries: 30
|
||||
fet2020:
|
||||
image: docker.fet.at/fet2020django
|
||||
container_name: test_fet2020
|
||||
networks:
|
||||
- testfet
|
||||
environment:
|
||||
HOST_NAME: "test.fet.at"
|
||||
DEBUG: "False"
|
||||
SECRET_KEY: "sadfreigjopi4qgjpjrp"
|
||||
MYSQL_USER: "user"
|
||||
MYSQL_PASSWORD: "hgu"
|
||||
ETHERPAD_GROUP: "g.snlbqn7S6ksRbom3"
|
||||
depends_on:
|
||||
mysql:
|
||||
condition: service_healthy
|
||||
etherpad:
|
||||
condition: service_healthy
|
||||
ports:
|
||||
- "8005:8080"
|
||||
healthcheck:
|
||||
test: "python3 manage.py check --database default"
|
||||
timeout: 20s
|
||||
retries: 20
|
||||
volumes:
|
||||
- /srv/APIKEY.txt:/app/etherpad/APIKEY.txt
|
||||
- /srv/files:/app/files
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
ep-mysql-volume:
|
||||
driver: local
|
||||
mysql-volume:
|
||||
driver: local
|
||||
networks:
|
||||
testfet:
|
||||
name: testfet
|
||||
@@ -9,7 +9,7 @@ services:
|
||||
depends_on:
|
||||
- django-homepage
|
||||
volumes:
|
||||
- files-volume:/usr/src/app/files
|
||||
- ./fet2020/files:/usr/src/app/files
|
||||
- ./gallery:/usr/src/app/files/uploads/gallery
|
||||
- ./assets:/usr/src/app/assets:ro
|
||||
networks:
|
||||
@@ -18,13 +18,15 @@ services:
|
||||
container_name: django-container
|
||||
image: django-image:latest
|
||||
environment:
|
||||
HOST_NAME: "fet.at"
|
||||
DEBUG: "False"
|
||||
LDAP: "True"
|
||||
SECRET_KEY: "sae34sADfrFr89E!Gl#f!34hdjGR#!jopi4qFEr#4R56rT56zT2#wE1!feGp"
|
||||
MYSQL_USER: "user"
|
||||
MYSQL_PASSWORD: "hgu"
|
||||
ETHERPAD_GROUP: "g.snlbqn7S6ksRbom3"
|
||||
HOST_NAME: ${HOSTNAME}
|
||||
DEBUG: ${DEBUG}
|
||||
LDAP: ${LDAP}
|
||||
SECRET_KEY: ${SECRET_KEY}
|
||||
MYSQL_USER: ${DJANGO_MYSQL_USER}
|
||||
MYSQL_PASSWORD: ${DJANGO_MYSQL_PASSWORD}
|
||||
ETHERPAD_GROUP: ${ETHERPAD_GROUP}
|
||||
EMAIL_HOST_USER: ${EMAIL_HOST_USER}
|
||||
EMAIL_HOST_PASSWORD: ${EMAIL_HOST_PASSWORD}
|
||||
depends_on:
|
||||
mysql:
|
||||
condition: service_healthy
|
||||
@@ -33,7 +35,7 @@ services:
|
||||
volumes:
|
||||
- ./fet2020:/usr/src/app
|
||||
- ./gallery:/usr/src/app/files/uploads/gallery:shared
|
||||
- files-volume:/usr/src/app/files
|
||||
- ./fet2020/files:/usr/src/app/files
|
||||
- ./assets:/usr/src/app/assets:ro
|
||||
networks:
|
||||
- fet-network
|
||||
@@ -45,7 +47,7 @@ services:
|
||||
retries: 20
|
||||
etherpad:
|
||||
container_name: etherpad-container
|
||||
image: etherpad/etherpad:1.8.17
|
||||
image: etherpad/etherpad:2.5.2
|
||||
# ports:
|
||||
# - 9001:9001
|
||||
environment:
|
||||
@@ -53,10 +55,11 @@ services:
|
||||
DB_HOST: etherpadsql
|
||||
DB_PORT: 3306
|
||||
DB_NAME: etherpaddb
|
||||
DB_USER: user
|
||||
DB_PASS: "hgu"
|
||||
DB_CHARSET: utf8
|
||||
DB_USER: ${ETHERPAD_DB_USER}
|
||||
DB_PASS: ${ETHERPAD_DB_PASSWORD}
|
||||
DB_CHARSET: "utf8mb4"
|
||||
TRUST_PROXY: false
|
||||
AUTHENTICATION_METHOD: "apikey"
|
||||
depends_on:
|
||||
etherpadsql:
|
||||
condition: "service_healthy"
|
||||
@@ -76,13 +79,14 @@ services:
|
||||
image: mariadb:10.7
|
||||
environment:
|
||||
MYSQL_DATABASE: fet2020db
|
||||
MYSQL_USER: user
|
||||
MYSQL_PASSWORD: hgu
|
||||
MYSQL_USER: ${DJANGO_MYSQL_USER}
|
||||
MYSQL_PASSWORD: ${DJANGO_MYSQL_PASSWORD}
|
||||
MYSQL_COLLATION: utf8_general_ci
|
||||
MYSQL_CHARSET: utf8
|
||||
MYSQL_ALLOW_EMPTY_PASSWORD: "yes"
|
||||
volumes:
|
||||
- mysql-volume:/docker-entrypoint-initdb.d/
|
||||
- ./inits/django:/docker-entrypoint-initdb.d/
|
||||
- ./databases/django:/var/lib/mysql:Z
|
||||
networks:
|
||||
- django-db-network
|
||||
healthcheck:
|
||||
@@ -94,13 +98,14 @@ services:
|
||||
image: mariadb:10.7
|
||||
environment:
|
||||
MYSQL_DATABASE: etherpaddb
|
||||
MYSQL_USER: user
|
||||
MYSQL_PASSWORD: "hgu"
|
||||
MYSQL_USER: ${ETHERPAD_DB_USER}
|
||||
MYSQL_PASSWORD: ${ETHERPAD_DB_PASSWORD}
|
||||
MYSQL_COLLATION: utf8_general_ci
|
||||
MYSQL_CHARSET: utf8
|
||||
MYSQL_ALLOW_EMPTY_PASSWORD: "yes"
|
||||
volumes:
|
||||
- etherpad-mysql-volume:/docker-entrypoint-initdb.d/
|
||||
- ./init/etherpad:/docker-entrypoint-initdb.d/
|
||||
- ./databases/etherpad:/var/lib/mysql:Z
|
||||
networks:
|
||||
- etherpad-db-network
|
||||
healthcheck:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from django.utils.version import get_version
|
||||
|
||||
VERSION = (2, 2, 0, "final", 0)
|
||||
VERSION = (2, 2, 1, "final", 0)
|
||||
BUILD = 0
|
||||
|
||||
__version__ = get_version(VERSION)
|
||||
|
||||
@@ -18,6 +18,8 @@ env = environ.Env(
|
||||
ETHERPAD_GROUP=(str, ""),
|
||||
GALLERY_PATH=(str, "uploads/gallery"),
|
||||
MC_MASTERPASSWORD=(str, ""),
|
||||
EMAIL_HOST_USER=(str, ""),
|
||||
EMAIL_HOST_PASSWORD=(str, ""),
|
||||
)
|
||||
|
||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||
@@ -103,6 +105,8 @@ else:
|
||||
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
|
||||
EMAIL_HOST = "buran.htu.tuwien.ac.at"
|
||||
EMAIL_PORT = 587
|
||||
EMAIL_HOST_USER = env("EMAIL_HOST_USER")
|
||||
EMAIL_HOST_PASSWORD = env("EMAIL_HOST_PASSWORD")
|
||||
EMAIL_USE_TLS = True
|
||||
|
||||
|
||||
@@ -165,14 +169,32 @@ WSGI_APPLICATION = "fet2020.wsgi.application"
|
||||
LOGGING = {
|
||||
"version": 1,
|
||||
"disable_existing_loggers": False,
|
||||
"handlers": {
|
||||
"console": {
|
||||
"class": "logging.StreamHandler",
|
||||
"formatters": {
|
||||
"logger": {
|
||||
"()": "logging.Formatter",
|
||||
"format": "[%(asctime)s] %(levelname)8s | %(message)s",
|
||||
"style": "%",
|
||||
"datefmt": "%Y-%m-%d %H:%M:%S",
|
||||
},
|
||||
},
|
||||
"root": {
|
||||
"handlers": ["console"],
|
||||
"level": "DEBUG",
|
||||
"handlers": {
|
||||
"console": {
|
||||
"level": "INFO",
|
||||
"class": "logging.StreamHandler",
|
||||
"formatter": "logger",
|
||||
},
|
||||
},
|
||||
"loggers": {
|
||||
"root": {
|
||||
"handlers": ["console"],
|
||||
"level": "INFO",
|
||||
"propagate": False,
|
||||
},
|
||||
"django": {
|
||||
"handlers": ["console"],
|
||||
"level": "INFO",
|
||||
"propagate": True,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -1,39 +1,39 @@
|
||||
# util functions for all apps
|
||||
import uuid
|
||||
|
||||
from django.contrib.admin.utils import construct_change_message
|
||||
|
||||
|
||||
def add_log_action(request, form, app_label, model, add=True):
|
||||
from django.contrib.admin.models import ADDITION, CHANGE, LogEntry
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
obj = form.save()
|
||||
content_type = ContentType.objects.get(app_label=app_label, model=model)
|
||||
change_message = construct_change_message(form, None, add)
|
||||
action_flag = ADDITION if add else CHANGE
|
||||
|
||||
LogEntry.objects.log_action(
|
||||
user_id=request.user.pk,
|
||||
content_type_id=content_type.pk,
|
||||
object_id=obj.pk,
|
||||
object_repr=str(obj),
|
||||
action_flag=action_flag,
|
||||
change_message=change_message,
|
||||
)
|
||||
|
||||
|
||||
def create_perms(sender, **kwargs):
|
||||
from django.contrib.auth.models import Group, Permission
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
group, created = Group.objects.get_or_create(name=sender.label)
|
||||
|
||||
content_types = ContentType.objects.filter(app_label=sender.label)
|
||||
for content_type in content_types:
|
||||
permissions = Permission.objects.filter(content_type=content_type)
|
||||
[group.permissions.add(permission) for permission in permissions]
|
||||
|
||||
|
||||
def create_random_id():
|
||||
return str(uuid.uuid4())[:8]
|
||||
# util functions for all apps
|
||||
import uuid
|
||||
|
||||
from django.contrib.admin.utils import construct_change_message
|
||||
|
||||
|
||||
def add_log_action(request, form, app_label, model, add=True):
|
||||
from django.contrib.admin.models import ADDITION, CHANGE, LogEntry
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
obj = form.save()
|
||||
content_type = ContentType.objects.get(app_label=app_label, model=model)
|
||||
change_message = construct_change_message(form, None, add)
|
||||
action_flag = ADDITION if add else CHANGE
|
||||
|
||||
LogEntry.objects.log_action(
|
||||
user_id=request.user.pk,
|
||||
content_type_id=content_type.pk,
|
||||
object_id=obj.pk,
|
||||
object_repr=str(obj),
|
||||
action_flag=action_flag,
|
||||
change_message=change_message,
|
||||
)
|
||||
|
||||
|
||||
def create_perms(sender, **kwargs):
|
||||
from django.contrib.auth.models import Group, Permission
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
group, _ = Group.objects.get_or_create(name=sender.label)
|
||||
|
||||
content_types = ContentType.objects.filter(app_label=sender.label)
|
||||
for content_type in content_types:
|
||||
permissions = Permission.objects.filter(content_type=content_type)
|
||||
[group.permissions.add(permission) for permission in permissions]
|
||||
|
||||
|
||||
def create_random_id():
|
||||
return str(uuid.uuid4())[:8]
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import logging
|
||||
from datetime import date, datetime
|
||||
from urllib.parse import urljoin
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib import admin, messages
|
||||
from django.contrib.sites.models import Site
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.urls import reverse
|
||||
from django.utils.html import format_html
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.utils.translation import ngettext
|
||||
@@ -103,8 +105,6 @@ class BillPeriodeFilter(admin.SimpleListFilter):
|
||||
return self.__lst
|
||||
|
||||
def queryset(self, request, queryset):
|
||||
qs = queryset
|
||||
|
||||
if self.value():
|
||||
try:
|
||||
period = datetime.strptime(self.value()[:4], "%Y") # noqa: DTZ007
|
||||
@@ -120,14 +120,15 @@ class BillPeriodeFilter(admin.SimpleListFilter):
|
||||
|
||||
# Return bills from current period.
|
||||
else:
|
||||
tmp_qs = queryset.order_by("-date")
|
||||
if not queryset.exists():
|
||||
return queryset
|
||||
|
||||
# Get first period.
|
||||
start_year = tmp_qs.first().date.year
|
||||
start_year = queryset.first().date.year
|
||||
|
||||
# Check if date of first bill is in first half of year. If yes, start of period is the
|
||||
# year before.
|
||||
if tmp_qs.first().date < date(start_year, 7, 1):
|
||||
if queryset.first().date < date(start_year, 7, 1):
|
||||
start_year -= 1
|
||||
|
||||
start_date = date(start_year, 7, 1)
|
||||
@@ -204,7 +205,12 @@ class BillAdmin(admin.ModelAdmin):
|
||||
actions = ["make_cleared", "make_finished"]
|
||||
autocomplete_fields = ["resolution"]
|
||||
list_filter = ["status", "affiliation", "payer", BillPeriodeFilter]
|
||||
search_fields = ["purpose", "bankdata__name"]
|
||||
search_fields = [
|
||||
"purpose",
|
||||
"bankdata__name",
|
||||
"bill_creator__firstname",
|
||||
"bill_creator__surname",
|
||||
]
|
||||
show_facets = admin.ShowFacets.ALWAYS
|
||||
ordering = ["-id"]
|
||||
|
||||
@@ -469,7 +475,9 @@ class ResolutionAdmin(admin.ModelAdmin):
|
||||
fetmeeting = FetMeeting.objects.get_queryset().filter(date=obj.date).first()
|
||||
|
||||
if fetmeeting is not None:
|
||||
link = f"https://{settings.HOST_NAME}/posts/{fetmeeting.slug}/"
|
||||
site = Site.objects.get_current()
|
||||
path = reverse("show", kwargs={"id": fetmeeting.slug})
|
||||
link = urljoin(f"https://{site.domain}", path)
|
||||
return format_html('<a href="{}" target="_blank">Link zur Fachschaftssitzung</a>', link)
|
||||
|
||||
return format_html("-")
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import datetime
|
||||
import decimal
|
||||
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from django import forms
|
||||
from django.core.validators import ValidationError
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db.models import Count, Q
|
||||
from django.forms import DateInput
|
||||
from django.utils import timezone
|
||||
|
||||
from members.models import Member
|
||||
|
||||
@@ -412,7 +412,7 @@ class BillAdminForm(forms.ModelForm):
|
||||
self.fields["resolution"].queryset = self.fields["resolution"].queryset.filter(
|
||||
(
|
||||
Q(option=Resolution.Option.FINANCE)
|
||||
& Q(date__gt=datetime.datetime.now(tz=datetime.UTC).date() - relativedelta(years=2))
|
||||
& Q(date__gt=timezone.now().date() - relativedelta(years=2))
|
||||
)
|
||||
| Q(option=Resolution.Option.PERMANENT)
|
||||
)
|
||||
@@ -450,8 +450,8 @@ class ResolutionAdminForm(forms.ModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs) # to get the self.fields set
|
||||
|
||||
budget = decimal.Decimal(0.0)
|
||||
total = decimal.Decimal(0.0)
|
||||
budget = decimal.Decimal("0.0")
|
||||
total = decimal.Decimal("0.0")
|
||||
if resolution := kwargs.get("instance"):
|
||||
for elem in Bill.objects.filter(resolution=resolution):
|
||||
total += elem.amount
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from django.core.validators import FileExtensionValidator, ValidationError
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import FileExtensionValidator
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
|
||||
@@ -8,6 +10,8 @@ from members.models import Member
|
||||
|
||||
from .validators import validate_bill_file_extension
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BankData(models.Model):
|
||||
# members can be deleted but never their bank datas
|
||||
@@ -86,8 +90,18 @@ class Resolution(models.Model):
|
||||
if not Resolution.objects.filter(id=_id).exists():
|
||||
break
|
||||
else:
|
||||
msg = (
|
||||
"Es wurden zu viele Beschlüsse in dieser Woche %(week)s vom Jahr %(year)s "
|
||||
"erstellt."
|
||||
)
|
||||
logger.error(
|
||||
"Too many resolutions created in week %(week)s of year %(year)s.",
|
||||
extra={"week": week, "year": year},
|
||||
)
|
||||
raise ValidationError(
|
||||
f"Es wurden zu viele Beschlüsse in dieser Woche angelegt. (ID: {_id})"
|
||||
msg,
|
||||
code="too_many_resolutions",
|
||||
params={"week": week, "year": year},
|
||||
)
|
||||
|
||||
self.id = _id
|
||||
@@ -217,6 +231,8 @@ class Bill(models.Model):
|
||||
verbose_name = "Rechnung"
|
||||
verbose_name_plural = "Rechnungen"
|
||||
|
||||
ordering = ["-date"]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.purpose}"
|
||||
|
||||
|
||||
Binary file not shown.
@@ -1,82 +1,83 @@
|
||||
import datetime
|
||||
import io
|
||||
from pathlib import Path
|
||||
|
||||
from django.core.files import File
|
||||
from pypdf import PdfReader, PdfWriter
|
||||
from pypdf.constants import FieldDictionaryAttributes as FA # noqa: N814
|
||||
|
||||
from .models import Bill, Wiref
|
||||
|
||||
|
||||
def generate_pdf(wiref):
|
||||
if wiref is None or wiref.status != Wiref.Status.OPENED:
|
||||
return False
|
||||
|
||||
bills = Bill.objects.filter(wiref=wiref).order_by("date")
|
||||
|
||||
# Get data for pdf
|
||||
data = {}
|
||||
data_invoice = {} # Own dict for fixing text to multiline
|
||||
for count, elem in enumerate(bills):
|
||||
data.update(
|
||||
{
|
||||
f"DatumRow{count + 1}": str(elem.date.strftime("%d.%m.%Y")),
|
||||
f"VerwendungszweckRow{count + 1}": elem.purpose,
|
||||
# Replace decimal separator from '.' to ','
|
||||
f"BetragRow{count + 1}": str(elem.amount).replace(".", ","),
|
||||
},
|
||||
)
|
||||
data_invoice.update(
|
||||
{
|
||||
f"RechnungsaustellerinRow{count + 1}": elem.invoice,
|
||||
},
|
||||
)
|
||||
|
||||
# Get budget year
|
||||
today = datetime.datetime.now(tz=datetime.UTC).date()
|
||||
if today.month < 7:
|
||||
budget_year = f"{today.year - 1}-{today.year}"
|
||||
else:
|
||||
budget_year = f"{today.year}-{today.year + 1}"
|
||||
|
||||
# Get total of all bills of wiref form
|
||||
total = 0
|
||||
for elem in bills:
|
||||
total += elem.amount
|
||||
|
||||
data.update(
|
||||
{
|
||||
"Rechnungsnummer": str(wiref.wiref_id),
|
||||
"Budgetjahr": budget_year,
|
||||
# Replace decimal separator from '.' to ','
|
||||
"Summe": str(total).replace(".", ","),
|
||||
},
|
||||
)
|
||||
|
||||
# Write data in pdf
|
||||
pdf_path = Path(Path(__file__).parent) / "static/wiref/Vorlage.pdf"
|
||||
reader = PdfReader(pdf_path)
|
||||
writer = PdfWriter()
|
||||
writer.append(reader)
|
||||
|
||||
writer.update_page_form_field_values(
|
||||
writer.pages[0],
|
||||
data,
|
||||
)
|
||||
|
||||
# Add invoices and fix text to multiline
|
||||
writer.update_page_form_field_values(
|
||||
writer.pages[0],
|
||||
data_invoice,
|
||||
flags=FA.FfBits.Multiline,
|
||||
)
|
||||
|
||||
with io.BytesIO() as bytes_stream:
|
||||
writer.write(bytes_stream)
|
||||
|
||||
# Save pdf in wiref
|
||||
wiref_name = f"Abrechnungsformular-{wiref.wiref_id}.pdf"
|
||||
wiref.file_field.save(wiref_name, File(bytes_stream, wiref_name))
|
||||
|
||||
return True
|
||||
import io
|
||||
|
||||
from django.contrib.staticfiles import finders
|
||||
from django.core.files import File
|
||||
from django.utils import timezone
|
||||
from pypdf import PdfReader, PdfWriter
|
||||
from pypdf.constants import FieldDictionaryAttributes as FA # noqa: N814
|
||||
|
||||
from .models import Bill, Wiref
|
||||
|
||||
|
||||
def generate_pdf(wiref):
|
||||
if wiref is None or wiref.status != Wiref.Status.OPENED:
|
||||
return False
|
||||
|
||||
bills = Bill.objects.filter(wiref=wiref).order_by("date")
|
||||
|
||||
# Get data for pdf
|
||||
data = {}
|
||||
data_invoice = {} # Own dict for fixing text to multiline
|
||||
for count, elem in enumerate(bills):
|
||||
data.update(
|
||||
{
|
||||
f"DatumRow{count + 1}": str(elem.date.strftime("%d.%m.%Y")),
|
||||
f"VerwendungszweckRow{count + 1}": elem.purpose,
|
||||
# Replace decimal separator from '.' to ','
|
||||
f"BetragRow{count + 1}": str(elem.amount).replace(".", ","),
|
||||
},
|
||||
)
|
||||
data_invoice.update(
|
||||
{
|
||||
f"RechnungsaustellerinRow{count + 1}": elem.invoice,
|
||||
},
|
||||
)
|
||||
|
||||
# Get budget year
|
||||
today = timezone.now().date()
|
||||
if today.month < 7:
|
||||
budget_year = f"{today.year - 1}-{today.year}"
|
||||
else:
|
||||
budget_year = f"{today.year}-{today.year + 1}"
|
||||
|
||||
# Get total of all bills of wiref form
|
||||
total = 0
|
||||
for elem in bills:
|
||||
total += elem.amount
|
||||
|
||||
data.update(
|
||||
{
|
||||
"Rechnungsnummer": str(wiref.wiref_id),
|
||||
"Budgetjahr": budget_year,
|
||||
# Replace decimal separator from '.' to ','
|
||||
"Summe": str(total).replace(".", ","),
|
||||
},
|
||||
)
|
||||
|
||||
# Write data in pdf
|
||||
pdf_path_str = finders.find("wiref/Vorlage.pdf")
|
||||
reader = PdfReader(pdf_path_str)
|
||||
writer = PdfWriter()
|
||||
writer.append(reader)
|
||||
|
||||
writer.update_page_form_field_values(
|
||||
writer.pages[0],
|
||||
data,
|
||||
)
|
||||
|
||||
# Add invoices and fix text to multiline
|
||||
writer.update_page_form_field_values(
|
||||
writer.pages[0],
|
||||
data_invoice,
|
||||
flags=FA.FfBits.Multiline,
|
||||
)
|
||||
|
||||
with io.BytesIO() as bytes_stream:
|
||||
writer.write(bytes_stream)
|
||||
bytes_stream.seek(0)
|
||||
|
||||
# Save pdf in wiref
|
||||
wiref_name = f"Abrechnungsformular-{wiref.wiref_id}.pdf"
|
||||
wiref.file_field.save(wiref_name, File(bytes_stream, wiref_name))
|
||||
|
||||
return True
|
||||
|
||||
@@ -27,11 +27,11 @@ def get_image_list(folder_name: str) -> list:
|
||||
|
||||
Path(thumb_path).mkdir(exist_ok=True)
|
||||
|
||||
for _file in os.listdir(image_path):
|
||||
if Path(_file).suffix.lower()[1:] not in get_available_image_extensions():
|
||||
for _file in image_path.iterdir():
|
||||
if _file.suffix.lower()[1:] not in get_available_image_extensions():
|
||||
continue
|
||||
|
||||
thumb_file_path = Path(thumb_path) / f"thumb_{_file}"
|
||||
thumb_file_path = Path(thumb_path) / f"thumb_{_file.name}"
|
||||
if not Path(thumb_file_path).exists():
|
||||
with Image.open(Path(image_path) / _file, "r") as im:
|
||||
if im._getexif() is not None:
|
||||
|
||||
@@ -1,65 +1,67 @@
|
||||
import logging
|
||||
from collections import deque
|
||||
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.http import Http404
|
||||
from django.shortcuts import render
|
||||
from django.utils.text import slugify
|
||||
from django.views.generic.detail import DetailView
|
||||
|
||||
from .models import Album
|
||||
from .utils import get_folder_list
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def index(request):
|
||||
if request.user.is_authenticated:
|
||||
albums = deque(Album.objects.all())
|
||||
|
||||
# Get albums that are in the server but not in the db.
|
||||
for folder in get_folder_list():
|
||||
if not Album.objects.filter(folder_name=folder):
|
||||
albums.append(
|
||||
Album(title=folder, slug=slugify(folder), folder_name=folder, event_date=None)
|
||||
)
|
||||
|
||||
else:
|
||||
# Show only PUBLIC albums.
|
||||
albums = Album.objects.public()
|
||||
|
||||
context = {"albums": albums}
|
||||
|
||||
return render(request, "gallery/index.html", context)
|
||||
|
||||
|
||||
class AlbumDetailView(DetailView):
|
||||
model = Album
|
||||
template_name = "gallery/album.html"
|
||||
|
||||
def get_queryset(self):
|
||||
return (
|
||||
Album.objects.public()
|
||||
if not self.request.user.is_authenticated
|
||||
else Album.objects.all()
|
||||
)
|
||||
|
||||
|
||||
class DraftAlbumDetailView(LoginRequiredMixin, DetailView):
|
||||
model = Album
|
||||
template_name = "gallery/album.html"
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
slug = self.kwargs.get(self.slug_url_kwarg)
|
||||
|
||||
if not (album := Album.objects.filter(slug=slug).first()):
|
||||
for folder in get_folder_list():
|
||||
if slug == slugify(folder):
|
||||
album = Album(
|
||||
title=folder, slug=slugify(folder), folder_name=folder, event_date=None
|
||||
)
|
||||
break
|
||||
else:
|
||||
raise Http404("Album slug not found.")
|
||||
|
||||
return album
|
||||
import logging
|
||||
from collections import deque
|
||||
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.http import Http404
|
||||
from django.shortcuts import render
|
||||
from django.utils.text import slugify
|
||||
from django.views.generic.detail import DetailView
|
||||
|
||||
from .models import Album
|
||||
from .utils import get_folder_list
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def index(request):
|
||||
if request.user.is_authenticated:
|
||||
albums = deque(Album.objects.all())
|
||||
|
||||
# Get albums that are in the server but not in the db.
|
||||
for folder in get_folder_list():
|
||||
if not Album.objects.filter(folder_name=folder):
|
||||
albums.append(
|
||||
Album(title=folder, slug=slugify(folder), folder_name=folder, event_date=None)
|
||||
)
|
||||
|
||||
else:
|
||||
# Show only PUBLIC albums.
|
||||
albums = Album.objects.public()
|
||||
|
||||
context = {"albums": albums}
|
||||
|
||||
return render(request, "gallery/index.html", context)
|
||||
|
||||
|
||||
class AlbumDetailView(DetailView):
|
||||
model = Album
|
||||
template_name = "gallery/album.html"
|
||||
|
||||
def get_queryset(self):
|
||||
return (
|
||||
Album.objects.public()
|
||||
if not self.request.user.is_authenticated
|
||||
else Album.objects.all()
|
||||
)
|
||||
|
||||
|
||||
class DraftAlbumDetailView(LoginRequiredMixin, DetailView):
|
||||
model = Album
|
||||
template_name = "gallery/album.html"
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
slug = self.kwargs.get(self.slug_url_kwarg)
|
||||
|
||||
if not (album := Album.objects.filter(slug=slug).first()):
|
||||
for folder in get_folder_list():
|
||||
if slug == slugify(folder):
|
||||
album = Album(
|
||||
title=folder, slug=slugify(folder), folder_name=folder, event_date=None
|
||||
)
|
||||
break
|
||||
else:
|
||||
msg = f"Album mit dem Slug '{slug}' nicht gefunden."
|
||||
logger.error("Album with slug '%s' not found.", slug)
|
||||
raise Http404(msg)
|
||||
|
||||
return album
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
#!/bin/sh
|
||||
pip install -r requirements.txt
|
||||
|
||||
python3 manage.py makemigrations blackboard core gallery members posts tasks
|
||||
python3 manage.py makemigrations intern
|
||||
python3 manage.py migrate
|
||||
@@ -1,205 +1,219 @@
|
||||
import logging
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
|
||||
from django.core.validators import ValidationError
|
||||
from django.db import models
|
||||
from django.db.models.constraints import UniqueConstraint
|
||||
from django.urls import reverse
|
||||
from django.utils.text import slugify
|
||||
|
||||
from documents.api import ep_create_new_pad, ep_get_html, ep_get_url
|
||||
from fet2020.utils import create_random_id
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TopicGroup(models.Model):
|
||||
title = models.CharField(verbose_name="Titel", max_length=128)
|
||||
|
||||
shortterm = models.CharField(max_length=128, unique=True, blank=True)
|
||||
slug = models.SlugField(unique=True)
|
||||
|
||||
short_description = models.TextField(blank=True, default="")
|
||||
|
||||
order = models.PositiveSmallIntegerField(
|
||||
verbose_name="Reihenfolge",
|
||||
unique=True,
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
|
||||
objects = models.Manager()
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Themenbereich"
|
||||
verbose_name_plural = "Themenbereiche"
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("intern:index") + "#" + self.slug
|
||||
|
||||
def clean(self, *args, **kwargs):
|
||||
if not self.shortterm:
|
||||
self.shortterm = self.title
|
||||
self.slug = slugify(self.shortterm)
|
||||
|
||||
|
||||
class Topic(models.Model):
|
||||
title = models.CharField(max_length=128, verbose_name="Titel")
|
||||
slug = models.SlugField()
|
||||
|
||||
archive = models.BooleanField(default=False, verbose_name="Archiv")
|
||||
|
||||
description = models.TextField(blank=True, default="")
|
||||
|
||||
topic_group = models.ForeignKey(
|
||||
TopicGroup,
|
||||
on_delete=models.CASCADE,
|
||||
verbose_name="Themenbereich",
|
||||
)
|
||||
|
||||
objects = models.Manager()
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Thema"
|
||||
verbose_name_plural = "Themen"
|
||||
|
||||
constraints = [
|
||||
UniqueConstraint(fields=["slug", "topic_group"], name="unique_intern_slug_topic_group"),
|
||||
UniqueConstraint(
|
||||
fields=["title", "topic_group"],
|
||||
name="unique_intern_title_topic_group",
|
||||
),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
def get_absolute_url(self):
|
||||
kwargs = {
|
||||
"topic_group_slug": self.topic_group.slug,
|
||||
"slug": self.slug,
|
||||
}
|
||||
return reverse("intern:topic", kwargs=kwargs)
|
||||
|
||||
def clean(self, *args, **kwargs):
|
||||
self.slug = slugify(self.title)
|
||||
|
||||
|
||||
class Attachment(models.Model):
|
||||
title = models.CharField(max_length=128, verbose_name="Titel")
|
||||
|
||||
slug = models.SlugField()
|
||||
|
||||
description = models.TextField(blank=True, default="")
|
||||
|
||||
topic = models.ForeignKey(Topic, on_delete=models.CASCADE, verbose_name="Thema")
|
||||
|
||||
objects = models.Manager()
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Anhang Ordner"
|
||||
verbose_name_plural = "Anhang Ordner"
|
||||
|
||||
constraints = [
|
||||
UniqueConstraint(fields=["slug", "topic"], name="unique_intern_slug_topic"),
|
||||
UniqueConstraint(fields=["title", "topic"], name="unique_intern_title_topic"),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return self.topic.title + " / " + self.title
|
||||
|
||||
def get_absolute_url(self):
|
||||
kwargs = {
|
||||
"topic_group_slug": self.topic.topic_group.slug,
|
||||
"topic_slug": self.topic.slug,
|
||||
"slug": self.slug,
|
||||
}
|
||||
return reverse("intern:attachment", kwargs=kwargs)
|
||||
|
||||
def clean(self, *args, **kwargs):
|
||||
self.slug = slugify(self.title)
|
||||
|
||||
|
||||
class Etherpad(models.Model):
|
||||
title = models.CharField(max_length=128, verbose_name="Titel")
|
||||
|
||||
slug_id = models.CharField(default=create_random_id, editable=False, max_length=8, unique=True)
|
||||
etherpad_key = models.CharField(blank=True, max_length=50)
|
||||
date = models.DateField(default=date.today, verbose_name="Datum")
|
||||
|
||||
attachment = models.ForeignKey(
|
||||
Attachment,
|
||||
on_delete=models.CASCADE,
|
||||
verbose_name="Anhang Ordner",
|
||||
)
|
||||
|
||||
objects = models.Manager()
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Etherpad"
|
||||
verbose_name_plural = "Etherpads"
|
||||
|
||||
constraints = [
|
||||
UniqueConstraint(fields=["title", "date", "attachment"], name="unique_intern_etherpad"),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
def get_absolute_url(self):
|
||||
return ep_get_url(self.etherpad_key)
|
||||
|
||||
def clean(self):
|
||||
pad_name = slugify(str(self.slug_id) + "-" + self.title[:40])
|
||||
if len(pad_name) > 50:
|
||||
raise ValidationError(
|
||||
(
|
||||
"Name zum Erstellen des Etherpads ist zu lange - max. 50 Zeichen. ("
|
||||
"Länge: %(length)s, Name: %(pad_name)s)"
|
||||
),
|
||||
params={"length": len(pad_name), "pad_name": pad_name},
|
||||
)
|
||||
|
||||
if self.etherpad_key == "":
|
||||
if ep_create_new_pad(pad_name):
|
||||
self.etherpad_key = pad_name
|
||||
else:
|
||||
raise ValidationError(
|
||||
f"Etherpad '{pad_name}' konnte nicht erstellt werden. This should never happen!"
|
||||
)
|
||||
|
||||
@property
|
||||
def etherpad_html(self):
|
||||
return ep_get_html(self.etherpad_key)
|
||||
|
||||
def get_model_name(self):
|
||||
return self._meta.model_name
|
||||
|
||||
|
||||
class FileUpload(models.Model):
|
||||
title = models.CharField(blank=True, max_length=128, verbose_name="Titel")
|
||||
file_field = models.FileField(upload_to="uploads/intern/files/", verbose_name="Dokument")
|
||||
date = models.DateField(default=date.today, verbose_name="Datum")
|
||||
|
||||
attachment = models.ForeignKey(
|
||||
Attachment,
|
||||
on_delete=models.CASCADE,
|
||||
verbose_name="Anhang Ordner",
|
||||
)
|
||||
|
||||
objects = models.Manager()
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Datei"
|
||||
verbose_name_plural = "Dateien"
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
def clean(self):
|
||||
if not self.title:
|
||||
self.title = Path(self.file_field.name).stem
|
||||
import logging
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.db.models.constraints import UniqueConstraint
|
||||
from django.urls import reverse
|
||||
from django.utils.text import slugify
|
||||
|
||||
from documents.api import ep_create_new_pad, ep_get_html, ep_get_url
|
||||
from fet2020.utils import create_random_id
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TopicGroup(models.Model):
|
||||
title = models.CharField(verbose_name="Titel", max_length=128)
|
||||
|
||||
shortterm = models.CharField(max_length=128, unique=True, blank=True)
|
||||
slug = models.SlugField(unique=True)
|
||||
|
||||
short_description = models.TextField(blank=True, default="")
|
||||
|
||||
order = models.PositiveSmallIntegerField(
|
||||
verbose_name="Reihenfolge",
|
||||
unique=True,
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
|
||||
objects = models.Manager()
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Themenbereich"
|
||||
verbose_name_plural = "Themenbereiche"
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("intern:index") + "#" + self.slug
|
||||
|
||||
def clean(self, *args, **kwargs):
|
||||
if not self.shortterm:
|
||||
self.shortterm = self.title
|
||||
self.slug = slugify(self.shortterm)
|
||||
|
||||
|
||||
class Topic(models.Model):
|
||||
title = models.CharField(max_length=128, verbose_name="Titel")
|
||||
slug = models.SlugField()
|
||||
|
||||
archive = models.BooleanField(default=False, verbose_name="Archiv")
|
||||
|
||||
description = models.TextField(blank=True, default="")
|
||||
|
||||
topic_group = models.ForeignKey(
|
||||
TopicGroup,
|
||||
on_delete=models.CASCADE,
|
||||
verbose_name="Themenbereich",
|
||||
)
|
||||
|
||||
objects = models.Manager()
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Thema"
|
||||
verbose_name_plural = "Themen"
|
||||
|
||||
constraints = [
|
||||
UniqueConstraint(fields=["slug", "topic_group"], name="unique_intern_slug_topic_group"),
|
||||
UniqueConstraint(
|
||||
fields=["title", "topic_group"],
|
||||
name="unique_intern_title_topic_group",
|
||||
),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
def get_absolute_url(self):
|
||||
kwargs = {
|
||||
"topic_group_slug": self.topic_group.slug,
|
||||
"slug": self.slug,
|
||||
}
|
||||
return reverse("intern:topic", kwargs=kwargs)
|
||||
|
||||
def clean(self, *args, **kwargs):
|
||||
self.slug = slugify(self.title)
|
||||
|
||||
|
||||
class Attachment(models.Model):
|
||||
title = models.CharField(max_length=128, verbose_name="Titel")
|
||||
|
||||
slug = models.SlugField()
|
||||
|
||||
description = models.TextField(blank=True, default="")
|
||||
|
||||
topic = models.ForeignKey(Topic, on_delete=models.CASCADE, verbose_name="Thema")
|
||||
|
||||
objects = models.Manager()
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Anhang Ordner"
|
||||
verbose_name_plural = "Anhang Ordner"
|
||||
|
||||
constraints = [
|
||||
UniqueConstraint(fields=["slug", "topic"], name="unique_intern_slug_topic"),
|
||||
UniqueConstraint(fields=["title", "topic"], name="unique_intern_title_topic"),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return self.topic.title + " / " + self.title
|
||||
|
||||
def get_absolute_url(self):
|
||||
kwargs = {
|
||||
"topic_group_slug": self.topic.topic_group.slug,
|
||||
"topic_slug": self.topic.slug,
|
||||
"slug": self.slug,
|
||||
}
|
||||
return reverse("intern:attachment", kwargs=kwargs)
|
||||
|
||||
def clean(self, *args, **kwargs):
|
||||
self.slug = slugify(self.title)
|
||||
|
||||
|
||||
class Etherpad(models.Model):
|
||||
title = models.CharField(max_length=128, verbose_name="Titel")
|
||||
|
||||
slug_id = models.CharField(default=create_random_id, editable=False, max_length=8, unique=True)
|
||||
etherpad_key = models.CharField(blank=True, max_length=50)
|
||||
date = models.DateField(default=date.today, verbose_name="Datum")
|
||||
|
||||
attachment = models.ForeignKey(
|
||||
Attachment,
|
||||
on_delete=models.CASCADE,
|
||||
verbose_name="Anhang Ordner",
|
||||
)
|
||||
|
||||
objects = models.Manager()
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Etherpad"
|
||||
verbose_name_plural = "Etherpads"
|
||||
|
||||
constraints = [
|
||||
UniqueConstraint(fields=["title", "date", "attachment"], name="unique_intern_etherpad"),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
def get_absolute_url(self):
|
||||
return ep_get_url(self.etherpad_key)
|
||||
|
||||
def clean(self):
|
||||
pad_name = slugify(str(self.slug_id) + "-" + self.title[:40])
|
||||
if len(pad_name) > 50:
|
||||
msg = (
|
||||
"Name ist zum Erstellen des Etherpads zu lange - max. 50 Zeichen. (Länge: "
|
||||
"%(length)s, Name: %(pad_name)s)"
|
||||
)
|
||||
logger.error(
|
||||
(
|
||||
"Name '%(pad_name)s' is too long for creating a new etherpad - max. 50 "
|
||||
"characters. (Length: %(length)s)",
|
||||
),
|
||||
extra={"length": len(pad_name), "pad_name": pad_name},
|
||||
)
|
||||
raise ValidationError(
|
||||
msg,
|
||||
code="pad_name_too_long",
|
||||
params={"length": len(pad_name), "pad_name": pad_name},
|
||||
)
|
||||
|
||||
if self.etherpad_key == "":
|
||||
if ep_create_new_pad(pad_name):
|
||||
self.etherpad_key = pad_name
|
||||
else:
|
||||
msg = "Etherpad '%(pad_name)s' konnte nicht erstellt werden."
|
||||
logger.error(
|
||||
"Etherpad '%(pad_name)s' could not be created. This should never happen!",
|
||||
extra={"pad_name": pad_name},
|
||||
)
|
||||
raise ValidationError(
|
||||
msg, code="pad_creation_failed", params={"pad_name": pad_name}
|
||||
)
|
||||
|
||||
@property
|
||||
def etherpad_html(self):
|
||||
return ep_get_html(self.etherpad_key)
|
||||
|
||||
def get_model_name(self):
|
||||
return self._meta.model_name
|
||||
|
||||
|
||||
class FileUpload(models.Model):
|
||||
title = models.CharField(blank=True, max_length=128, verbose_name="Titel")
|
||||
file_field = models.FileField(upload_to="uploads/intern/files/", verbose_name="Dokument")
|
||||
date = models.DateField(default=date.today, verbose_name="Datum")
|
||||
|
||||
attachment = models.ForeignKey(
|
||||
Attachment,
|
||||
on_delete=models.CASCADE,
|
||||
verbose_name="Anhang Ordner",
|
||||
)
|
||||
|
||||
objects = models.Manager()
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Datei"
|
||||
verbose_name_plural = "Dateien"
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
def clean(self):
|
||||
if not self.title:
|
||||
self.title = Path(self.file_field.name).stem
|
||||
|
||||
@@ -1,230 +1,232 @@
|
||||
from django.contrib import admin
|
||||
|
||||
from .forms import (
|
||||
ActiveMemberForm,
|
||||
InactiveMemberForm,
|
||||
JobForm,
|
||||
JobGroupForm,
|
||||
JobInlineForm,
|
||||
MemberForm,
|
||||
)
|
||||
from .models import Job, JobGroup, JobMember, Member
|
||||
|
||||
|
||||
class MemberRoleFilter(admin.SimpleListFilter):
|
||||
title = "Rolle"
|
||||
parameter_name = "role"
|
||||
|
||||
def lookups(self, request, model_admin):
|
||||
return (
|
||||
("A", "Aktiv"),
|
||||
("P", "Pension"),
|
||||
)
|
||||
|
||||
def queryset(self, request, queryset):
|
||||
if self.value() in Member.MemberRole:
|
||||
return queryset.filter(role=self.value())
|
||||
|
||||
|
||||
class JobMemberInline(admin.TabularInline):
|
||||
model = JobMember
|
||||
extra = 0
|
||||
|
||||
|
||||
class JobOverviewInline(JobMemberInline):
|
||||
verbose_name = "Tätigkeit"
|
||||
verbose_name_plural = "Tätigkeitsübersicht"
|
||||
|
||||
def get_queryset(self, request):
|
||||
return JobMember.members.get_all_jobs()
|
||||
|
||||
|
||||
class ActiveMemberInline(JobMemberInline):
|
||||
form = ActiveMemberForm
|
||||
verbose_name = "Mitglied"
|
||||
verbose_name_plural = "Aktive Mitglieder Liste"
|
||||
|
||||
def get_queryset(self, request):
|
||||
return JobMember.active_member.get_queryset()
|
||||
|
||||
|
||||
class InactiveMemberInline(JobMemberInline):
|
||||
form = InactiveMemberForm
|
||||
verbose_name = "Mitglied"
|
||||
verbose_name_plural = "Inaktive Mitglieder Liste"
|
||||
|
||||
def get_queryset(self, request):
|
||||
return JobMember.inactive_member.get_queryset()
|
||||
|
||||
|
||||
class JobInline(admin.TabularInline):
|
||||
form = JobInlineForm
|
||||
model = Job
|
||||
extra = 0
|
||||
show_change_link = True
|
||||
|
||||
|
||||
@admin.register(Member)
|
||||
class MemberAdmin(admin.ModelAdmin):
|
||||
form = MemberForm
|
||||
model = Member
|
||||
|
||||
fieldsets = (
|
||||
(
|
||||
None,
|
||||
{
|
||||
"fields": (
|
||||
(
|
||||
"firstname",
|
||||
"surname",
|
||||
),
|
||||
"nickname",
|
||||
"mailaccount",
|
||||
"role",
|
||||
"image",
|
||||
"description",
|
||||
),
|
||||
},
|
||||
),
|
||||
)
|
||||
inlines = (JobOverviewInline,)
|
||||
|
||||
list_display = ["nickname", "firstname", "surname", "mailaccount", "role"]
|
||||
list_filter = [MemberRoleFilter]
|
||||
ordering = ["firstname"]
|
||||
search_fields = ["firstname", "surname", "nickname", "mailaccount"]
|
||||
|
||||
def add_view(self, request, form_url="", extra_context=None):
|
||||
extra_context = extra_context or {}
|
||||
extra_context["help_text"] = "Fette Schriften sind Pflichtfelder."
|
||||
return super().add_view(
|
||||
request,
|
||||
form_url,
|
||||
extra_context=extra_context,
|
||||
)
|
||||
|
||||
def change_view(self, request, object_id, form_url="", extra_context=None):
|
||||
extra_context = extra_context or {}
|
||||
extra_context["help_text"] = "Fette Schriften sind Pflichtfelder."
|
||||
return super().change_view(
|
||||
request,
|
||||
object_id,
|
||||
form_url,
|
||||
extra_context=extra_context,
|
||||
)
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
obj.author = request.user
|
||||
super().save_model(request, obj, form, change)
|
||||
|
||||
|
||||
@admin.register(Job)
|
||||
class JobAdmin(admin.ModelAdmin):
|
||||
form = JobForm
|
||||
model = Job
|
||||
|
||||
list_display = ["name", "job_group"]
|
||||
ordering = ["name"]
|
||||
search_fields = ["name"]
|
||||
|
||||
fieldsets = (
|
||||
(
|
||||
None,
|
||||
{
|
||||
"fields": (
|
||||
"name",
|
||||
"job_group",
|
||||
),
|
||||
},
|
||||
),
|
||||
(
|
||||
"Permalink",
|
||||
{
|
||||
"fields": (
|
||||
"shortterm",
|
||||
"slug",
|
||||
),
|
||||
},
|
||||
),
|
||||
)
|
||||
inlines = (ActiveMemberInline, InactiveMemberInline)
|
||||
readonly_fields = ["slug"]
|
||||
|
||||
def add_view(self, request, form_url="", extra_context=None):
|
||||
extra_context = extra_context or {}
|
||||
extra_context["help_text"] = "Fette Schriften sind Pflichtfelder."
|
||||
return super().add_view(
|
||||
request,
|
||||
form_url,
|
||||
extra_context=extra_context,
|
||||
)
|
||||
|
||||
def change_view(self, request, object_id, form_url="", extra_context=None):
|
||||
extra_context = extra_context or {}
|
||||
extra_context["help_text"] = "Fette Schriften sind Pflichfelder."
|
||||
return super().change_view(
|
||||
request,
|
||||
object_id,
|
||||
form_url,
|
||||
extra_context=extra_context,
|
||||
)
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
obj.author = request.user
|
||||
super().save_model(request, obj, form, change)
|
||||
|
||||
|
||||
@admin.register(JobGroup)
|
||||
class JobGroupAdmin(admin.ModelAdmin):
|
||||
form = JobGroupForm
|
||||
model = JobGroup
|
||||
|
||||
list_display = ["name"]
|
||||
ordering = ["name"]
|
||||
search_fields = ["name"]
|
||||
|
||||
fieldsets = (
|
||||
(
|
||||
None,
|
||||
{
|
||||
"fields": (
|
||||
"name",
|
||||
"description",
|
||||
),
|
||||
},
|
||||
),
|
||||
(
|
||||
"Permalink",
|
||||
{
|
||||
"fields": (
|
||||
"shortterm",
|
||||
"slug",
|
||||
),
|
||||
},
|
||||
),
|
||||
)
|
||||
inlines = (JobInline,)
|
||||
readonly_fields = ["slug"]
|
||||
|
||||
def add_view(self, request, form_url="", extra_context=None):
|
||||
extra_context = extra_context or {}
|
||||
extra_context["help_text"] = "Fette Schriften sind Pflichtfelder."
|
||||
return super().add_view(
|
||||
request,
|
||||
form_url,
|
||||
extra_context=extra_context,
|
||||
)
|
||||
|
||||
def change_view(self, request, object_id, form_url="", extra_context=None):
|
||||
extra_context = extra_context or {}
|
||||
extra_context["help_text"] = "Fette Schriften sind Pflichfelder."
|
||||
return super().change_view(
|
||||
request,
|
||||
object_id,
|
||||
form_url,
|
||||
extra_context=extra_context,
|
||||
)
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
obj.author = request.user
|
||||
super().save_model(request, obj, form, change)
|
||||
from django.contrib import admin
|
||||
|
||||
from .forms import (
|
||||
ActiveMemberForm,
|
||||
InactiveMemberForm,
|
||||
JobForm,
|
||||
JobGroupForm,
|
||||
JobInlineForm,
|
||||
MemberForm,
|
||||
)
|
||||
from .models import Job, JobGroup, JobMember, Member
|
||||
|
||||
|
||||
class MemberRoleFilter(admin.SimpleListFilter):
|
||||
title = "Rolle"
|
||||
parameter_name = "role"
|
||||
|
||||
def lookups(self, request, model_admin):
|
||||
return (
|
||||
("A", "Aktiv"),
|
||||
("P", "Pension"),
|
||||
)
|
||||
|
||||
def queryset(self, request, queryset):
|
||||
if self.value() in Member.MemberRole:
|
||||
return queryset.filter(role=self.value())
|
||||
|
||||
return queryset
|
||||
|
||||
|
||||
class JobMemberInline(admin.TabularInline):
|
||||
model = JobMember
|
||||
extra = 0
|
||||
|
||||
|
||||
class JobOverviewInline(JobMemberInline):
|
||||
verbose_name = "Tätigkeit"
|
||||
verbose_name_plural = "Tätigkeitsübersicht"
|
||||
|
||||
def get_queryset(self, request):
|
||||
return JobMember.members.get_all_jobs()
|
||||
|
||||
|
||||
class ActiveMemberInline(JobMemberInline):
|
||||
form = ActiveMemberForm
|
||||
verbose_name = "Mitglied"
|
||||
verbose_name_plural = "Aktive Mitglieder Liste"
|
||||
|
||||
def get_queryset(self, request):
|
||||
return JobMember.active_member.get_queryset()
|
||||
|
||||
|
||||
class InactiveMemberInline(JobMemberInline):
|
||||
form = InactiveMemberForm
|
||||
verbose_name = "Mitglied"
|
||||
verbose_name_plural = "Inaktive Mitglieder Liste"
|
||||
|
||||
def get_queryset(self, request):
|
||||
return JobMember.inactive_member.get_queryset()
|
||||
|
||||
|
||||
class JobInline(admin.TabularInline):
|
||||
form = JobInlineForm
|
||||
model = Job
|
||||
extra = 0
|
||||
show_change_link = True
|
||||
|
||||
|
||||
@admin.register(Member)
|
||||
class MemberAdmin(admin.ModelAdmin):
|
||||
form = MemberForm
|
||||
model = Member
|
||||
|
||||
fieldsets = (
|
||||
(
|
||||
None,
|
||||
{
|
||||
"fields": (
|
||||
(
|
||||
"firstname",
|
||||
"surname",
|
||||
),
|
||||
"nickname",
|
||||
"mailaccount",
|
||||
"role",
|
||||
"image",
|
||||
"description",
|
||||
),
|
||||
},
|
||||
),
|
||||
)
|
||||
inlines = (JobOverviewInline,)
|
||||
|
||||
list_display = ["nickname", "firstname", "surname", "mailaccount", "role"]
|
||||
list_filter = [MemberRoleFilter]
|
||||
ordering = ["firstname"]
|
||||
search_fields = ["firstname", "surname", "nickname", "mailaccount"]
|
||||
|
||||
def add_view(self, request, form_url="", extra_context=None):
|
||||
extra_context = extra_context or {}
|
||||
extra_context["help_text"] = "Fette Schriften sind Pflichtfelder."
|
||||
return super().add_view(
|
||||
request,
|
||||
form_url,
|
||||
extra_context=extra_context,
|
||||
)
|
||||
|
||||
def change_view(self, request, object_id, form_url="", extra_context=None):
|
||||
extra_context = extra_context or {}
|
||||
extra_context["help_text"] = "Fette Schriften sind Pflichtfelder."
|
||||
return super().change_view(
|
||||
request,
|
||||
object_id,
|
||||
form_url,
|
||||
extra_context=extra_context,
|
||||
)
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
obj.author = request.user
|
||||
super().save_model(request, obj, form, change)
|
||||
|
||||
|
||||
@admin.register(Job)
|
||||
class JobAdmin(admin.ModelAdmin):
|
||||
form = JobForm
|
||||
model = Job
|
||||
|
||||
list_display = ["name", "job_group"]
|
||||
ordering = ["name"]
|
||||
search_fields = ["name"]
|
||||
|
||||
fieldsets = (
|
||||
(
|
||||
None,
|
||||
{
|
||||
"fields": (
|
||||
"name",
|
||||
"job_group",
|
||||
),
|
||||
},
|
||||
),
|
||||
(
|
||||
"Permalink",
|
||||
{
|
||||
"fields": (
|
||||
"shortterm",
|
||||
"slug",
|
||||
),
|
||||
},
|
||||
),
|
||||
)
|
||||
inlines = (ActiveMemberInline, InactiveMemberInline)
|
||||
readonly_fields = ["slug"]
|
||||
|
||||
def add_view(self, request, form_url="", extra_context=None):
|
||||
extra_context = extra_context or {}
|
||||
extra_context["help_text"] = "Fette Schriften sind Pflichtfelder."
|
||||
return super().add_view(
|
||||
request,
|
||||
form_url,
|
||||
extra_context=extra_context,
|
||||
)
|
||||
|
||||
def change_view(self, request, object_id, form_url="", extra_context=None):
|
||||
extra_context = extra_context or {}
|
||||
extra_context["help_text"] = "Fette Schriften sind Pflichfelder."
|
||||
return super().change_view(
|
||||
request,
|
||||
object_id,
|
||||
form_url,
|
||||
extra_context=extra_context,
|
||||
)
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
obj.author = request.user
|
||||
super().save_model(request, obj, form, change)
|
||||
|
||||
|
||||
@admin.register(JobGroup)
|
||||
class JobGroupAdmin(admin.ModelAdmin):
|
||||
form = JobGroupForm
|
||||
model = JobGroup
|
||||
|
||||
list_display = ["name"]
|
||||
ordering = ["name"]
|
||||
search_fields = ["name"]
|
||||
|
||||
fieldsets = (
|
||||
(
|
||||
None,
|
||||
{
|
||||
"fields": (
|
||||
"name",
|
||||
"description",
|
||||
),
|
||||
},
|
||||
),
|
||||
(
|
||||
"Permalink",
|
||||
{
|
||||
"fields": (
|
||||
"shortterm",
|
||||
"slug",
|
||||
),
|
||||
},
|
||||
),
|
||||
)
|
||||
inlines = (JobInline,)
|
||||
readonly_fields = ["slug"]
|
||||
|
||||
def add_view(self, request, form_url="", extra_context=None):
|
||||
extra_context = extra_context or {}
|
||||
extra_context["help_text"] = "Fette Schriften sind Pflichtfelder."
|
||||
return super().add_view(
|
||||
request,
|
||||
form_url,
|
||||
extra_context=extra_context,
|
||||
)
|
||||
|
||||
def change_view(self, request, object_id, form_url="", extra_context=None):
|
||||
extra_context = extra_context or {}
|
||||
extra_context["help_text"] = "Fette Schriften sind Pflichfelder."
|
||||
return super().change_view(
|
||||
request,
|
||||
object_id,
|
||||
form_url,
|
||||
extra_context=extra_context,
|
||||
)
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
obj.author = request.user
|
||||
super().save_model(request, obj, form, change)
|
||||
|
||||
@@ -1,210 +1,207 @@
|
||||
import logging
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.validators import ValidationError, validate_email
|
||||
from django.db import models
|
||||
from django.db.models import F
|
||||
from django.urls import reverse
|
||||
from django.utils.text import slugify
|
||||
from easy_thumbnails.fields import ThumbnailerImageField
|
||||
|
||||
from .managers import (
|
||||
ActiveJobMemberManager,
|
||||
InactiveJobMemberManager,
|
||||
JobMemberManager,
|
||||
MemberManager,
|
||||
)
|
||||
from .validators import (
|
||||
validate_domainonly_email,
|
||||
validate_file_size,
|
||||
validate_image_dimension,
|
||||
)
|
||||
|
||||
fet_logo_url = settings.STATIC_URL + "img/FET-Logo-2014-quadrat.png"
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Member(models.Model):
|
||||
firstname = models.CharField("Vorname", max_length=128)
|
||||
surname = models.CharField("Nachname", max_length=128)
|
||||
nickname = models.CharField("Spitzname", max_length=128)
|
||||
|
||||
# LDAP Username
|
||||
username = models.CharField(max_length=128, blank=True)
|
||||
|
||||
# fet mail account
|
||||
mailaccount = models.CharField(
|
||||
"Mailadresse",
|
||||
unique=True,
|
||||
max_length=128,
|
||||
validators=[validate_email, validate_domainonly_email],
|
||||
error_messages={
|
||||
"unique": "Diese Mailadresse existiert schon.",
|
||||
},
|
||||
)
|
||||
|
||||
class MemberRole(models.TextChoices):
|
||||
ACTIVE = "A", "Active"
|
||||
PENSION = "P", "Pension"
|
||||
|
||||
role = models.CharField(
|
||||
"Rolle",
|
||||
max_length=1,
|
||||
choices=MemberRole.choices,
|
||||
default=MemberRole.ACTIVE,
|
||||
)
|
||||
|
||||
description = models.TextField(blank=True, default="")
|
||||
|
||||
image = ThumbnailerImageField(
|
||||
upload_to="uploads/members/image/",
|
||||
validators=[validate_file_size, validate_image_dimension],
|
||||
)
|
||||
|
||||
date_modified = models.DateTimeField(auto_now=True)
|
||||
date_created = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
# Managers
|
||||
objects = models.Manager()
|
||||
all_members = MemberManager()
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Mitglied"
|
||||
verbose_name_plural = "Mitglieder"
|
||||
|
||||
def __str__(self):
|
||||
return self.firstname + " " + self.surname
|
||||
|
||||
# need to have 'View on site' link in admin app
|
||||
def get_absolute_url(self):
|
||||
return reverse("members:member", kwargs={"pk": self.pk})
|
||||
|
||||
def clean(self):
|
||||
if not self.image:
|
||||
raise ValidationError("Es fehlt das Profilbild.")
|
||||
|
||||
if self.username:
|
||||
try:
|
||||
user = User.objects.get(username=self.username.lower())
|
||||
except User.DoesNotExist as e:
|
||||
logger.info("Username not found. Error: %s", e)
|
||||
else:
|
||||
user.first_name = self.firstname
|
||||
user.save()
|
||||
|
||||
def get_model_name(self):
|
||||
return self._meta.model_name
|
||||
|
||||
@property
|
||||
def image_url(self):
|
||||
return self.image.url if self.image else fet_logo_url
|
||||
|
||||
@property
|
||||
def avatar_url(self):
|
||||
return self.image["avatar"].url if self.image else fet_logo_url
|
||||
|
||||
@property
|
||||
def portrait_url(self):
|
||||
return self.image["portrait"].url if self.image else fet_logo_url
|
||||
|
||||
@property
|
||||
def thumb_url(self):
|
||||
return self.image["thumb"].url if self.image else fet_logo_url
|
||||
|
||||
|
||||
class JobGroup(models.Model):
|
||||
name = models.CharField(max_length=128)
|
||||
|
||||
shortterm = models.CharField(max_length=128, unique=True, blank=True)
|
||||
slug = models.SlugField(unique=True, null=True, blank=True)
|
||||
|
||||
description = models.TextField(blank=True, default="")
|
||||
|
||||
# Managers
|
||||
objects = models.Manager()
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Tätigkeitsbereich"
|
||||
verbose_name_plural = "Tätigkeitsbereiche"
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
# need to have 'View on site' link in admin app
|
||||
def get_absolute_url(self):
|
||||
return reverse("members:jobs", kwargs={"slug": self.slug})
|
||||
|
||||
def clean(self):
|
||||
if not self.shortterm:
|
||||
self.shortterm = slugify(self.name)
|
||||
self.slug = slugify(self.shortterm)
|
||||
|
||||
|
||||
class Job(models.Model):
|
||||
name = models.CharField(max_length=128)
|
||||
|
||||
shortterm = models.CharField(max_length=128, unique=True, blank=True)
|
||||
slug = models.SlugField(unique=True, null=True, blank=True)
|
||||
|
||||
order = models.PositiveSmallIntegerField(null=True, blank=True)
|
||||
|
||||
job_group = models.ForeignKey(
|
||||
JobGroup,
|
||||
on_delete=models.CASCADE,
|
||||
verbose_name="Job Gruppe",
|
||||
)
|
||||
|
||||
# Managers
|
||||
objects = models.Manager()
|
||||
|
||||
class Meta:
|
||||
ordering = (F("order").asc(nulls_last=True), "name")
|
||||
|
||||
verbose_name = "Tätigkeit"
|
||||
verbose_name_plural = "Tätigkeiten"
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("members:jobs", kwargs={"slug": self.job_group.slug}) + "#" + self.slug
|
||||
|
||||
def clean(self):
|
||||
if not self.shortterm:
|
||||
self.shortterm = slugify(self.name)
|
||||
self.slug = slugify(self.shortterm)
|
||||
|
||||
|
||||
class JobMember(models.Model):
|
||||
member = models.ForeignKey(
|
||||
Member,
|
||||
on_delete=models.CASCADE,
|
||||
verbose_name="Mitglied",
|
||||
)
|
||||
job = models.ForeignKey(
|
||||
Job,
|
||||
on_delete=models.CASCADE,
|
||||
verbose_name="Tätigkeit",
|
||||
)
|
||||
|
||||
job_start = models.DateField("Job Start")
|
||||
job_end = models.DateField("Job Ende", null=True, blank=True)
|
||||
|
||||
class JobRole(models.TextChoices):
|
||||
PRESIDENT = ("10", "Vorsitz")
|
||||
VICE_PRESIDENT = ("20", "Stv. Vorsitz")
|
||||
SECOND_VICE_PRESIDENT = ("30", "2. Stv. Vorsitz")
|
||||
PERSON_RESPONSIBLE = ("40", "Verantwortliche_r")
|
||||
MEMBER = ("50", "Mitglied")
|
||||
SUBSTITUTE_MEMBER = ("60", "Ersatzmitglied")
|
||||
|
||||
job_role = models.CharField(max_length=2, choices=JobRole.choices, default=JobRole.MEMBER)
|
||||
|
||||
objects = models.Manager()
|
||||
members = JobMemberManager()
|
||||
active_member = ActiveJobMemberManager()
|
||||
inactive_member = InactiveJobMemberManager()
|
||||
|
||||
def __str__(self):
|
||||
return self.job.name
|
||||
import logging
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.validators import validate_email
|
||||
from django.db import models
|
||||
from django.db.models import F
|
||||
from django.urls import reverse
|
||||
from django.utils.text import slugify
|
||||
from easy_thumbnails.fields import ThumbnailerImageField
|
||||
|
||||
from .managers import (
|
||||
ActiveJobMemberManager,
|
||||
InactiveJobMemberManager,
|
||||
JobMemberManager,
|
||||
MemberManager,
|
||||
)
|
||||
from .validators import (
|
||||
validate_domainonly_email,
|
||||
validate_file_size,
|
||||
validate_image_dimension,
|
||||
)
|
||||
|
||||
fet_logo_url = settings.STATIC_URL + "img/FET-Logo-2014-quadrat.png"
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Member(models.Model):
|
||||
firstname = models.CharField("Vorname", max_length=128)
|
||||
surname = models.CharField("Nachname", max_length=128)
|
||||
nickname = models.CharField("Spitzname", max_length=128)
|
||||
|
||||
# LDAP Username
|
||||
username = models.CharField(max_length=128, blank=True)
|
||||
|
||||
# fet mail account
|
||||
mailaccount = models.CharField(
|
||||
"Mailadresse",
|
||||
unique=True,
|
||||
max_length=128,
|
||||
validators=[validate_email, validate_domainonly_email],
|
||||
error_messages={
|
||||
"unique": "Diese Mailadresse existiert schon.",
|
||||
},
|
||||
)
|
||||
|
||||
class MemberRole(models.TextChoices):
|
||||
ACTIVE = "A", "Active"
|
||||
PENSION = "P", "Pension"
|
||||
|
||||
role = models.CharField(
|
||||
"Rolle",
|
||||
max_length=1,
|
||||
choices=MemberRole.choices,
|
||||
default=MemberRole.ACTIVE,
|
||||
)
|
||||
|
||||
description = models.TextField(blank=True, default="")
|
||||
|
||||
image = ThumbnailerImageField(
|
||||
upload_to="uploads/members/image/",
|
||||
validators=[validate_file_size, validate_image_dimension],
|
||||
)
|
||||
|
||||
date_modified = models.DateTimeField(auto_now=True)
|
||||
date_created = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
# Managers
|
||||
objects = models.Manager()
|
||||
all_members = MemberManager()
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Mitglied"
|
||||
verbose_name_plural = "Mitglieder"
|
||||
|
||||
def __str__(self):
|
||||
return self.firstname + " " + self.surname
|
||||
|
||||
# need to have 'View on site' link in admin app
|
||||
def get_absolute_url(self):
|
||||
return reverse("members:member", kwargs={"pk": self.pk})
|
||||
|
||||
def clean(self):
|
||||
if self.username:
|
||||
try:
|
||||
user = User.objects.get(username=self.username.lower())
|
||||
except User.DoesNotExist as e:
|
||||
logger.info("Username not found. Error: %s", e)
|
||||
else:
|
||||
user.first_name = self.firstname
|
||||
user.save()
|
||||
|
||||
def get_model_name(self):
|
||||
return self._meta.model_name
|
||||
|
||||
@property
|
||||
def image_url(self):
|
||||
return self.image.url if self.image else fet_logo_url
|
||||
|
||||
@property
|
||||
def avatar_url(self):
|
||||
return self.image["avatar"].url if self.image else fet_logo_url
|
||||
|
||||
@property
|
||||
def portrait_url(self):
|
||||
return self.image["portrait"].url if self.image else fet_logo_url
|
||||
|
||||
@property
|
||||
def thumb_url(self):
|
||||
return self.image["thumb"].url if self.image else fet_logo_url
|
||||
|
||||
|
||||
class JobGroup(models.Model):
|
||||
name = models.CharField(max_length=128)
|
||||
|
||||
shortterm = models.CharField(max_length=128, unique=True, blank=True)
|
||||
slug = models.SlugField(unique=True, null=True, blank=True)
|
||||
|
||||
description = models.TextField(blank=True, default="")
|
||||
|
||||
# Managers
|
||||
objects = models.Manager()
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Tätigkeitsbereich"
|
||||
verbose_name_plural = "Tätigkeitsbereiche"
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
# need to have 'View on site' link in admin app
|
||||
def get_absolute_url(self):
|
||||
return reverse("members:jobs", kwargs={"slug": self.slug})
|
||||
|
||||
def clean(self):
|
||||
if not self.shortterm:
|
||||
self.shortterm = slugify(self.name)
|
||||
self.slug = slugify(self.shortterm)
|
||||
|
||||
|
||||
class Job(models.Model):
|
||||
name = models.CharField(max_length=128)
|
||||
|
||||
shortterm = models.CharField(max_length=128, unique=True, blank=True)
|
||||
slug = models.SlugField(unique=True, null=True, blank=True)
|
||||
|
||||
order = models.PositiveSmallIntegerField(null=True, blank=True)
|
||||
|
||||
job_group = models.ForeignKey(
|
||||
JobGroup,
|
||||
on_delete=models.CASCADE,
|
||||
verbose_name="Job Gruppe",
|
||||
)
|
||||
|
||||
# Managers
|
||||
objects = models.Manager()
|
||||
|
||||
class Meta:
|
||||
ordering = (F("order").asc(nulls_last=True), "name")
|
||||
|
||||
verbose_name = "Tätigkeit"
|
||||
verbose_name_plural = "Tätigkeiten"
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("members:jobs", kwargs={"slug": self.job_group.slug}) + "#" + self.slug
|
||||
|
||||
def clean(self):
|
||||
if not self.shortterm:
|
||||
self.shortterm = slugify(self.name)
|
||||
self.slug = slugify(self.shortterm)
|
||||
|
||||
|
||||
class JobMember(models.Model):
|
||||
member = models.ForeignKey(
|
||||
Member,
|
||||
on_delete=models.CASCADE,
|
||||
verbose_name="Mitglied",
|
||||
)
|
||||
job = models.ForeignKey(
|
||||
Job,
|
||||
on_delete=models.CASCADE,
|
||||
verbose_name="Tätigkeit",
|
||||
)
|
||||
|
||||
job_start = models.DateField("Job Start")
|
||||
job_end = models.DateField("Job Ende", null=True, blank=True)
|
||||
|
||||
class JobRole(models.TextChoices):
|
||||
PRESIDENT = ("10", "Vorsitz")
|
||||
VICE_PRESIDENT = ("20", "Stv. Vorsitz")
|
||||
SECOND_VICE_PRESIDENT = ("30", "2. Stv. Vorsitz")
|
||||
PERSON_RESPONSIBLE = ("40", "Verantwortliche_r")
|
||||
MEMBER = ("50", "Mitglied")
|
||||
SUBSTITUTE_MEMBER = ("60", "Ersatzmitglied")
|
||||
|
||||
job_role = models.CharField(max_length=2, choices=JobRole.choices, default=JobRole.MEMBER)
|
||||
|
||||
objects = models.Manager()
|
||||
members = JobMemberManager()
|
||||
active_member = ActiveJobMemberManager()
|
||||
inactive_member = InactiveJobMemberManager()
|
||||
|
||||
def __str__(self):
|
||||
return self.job.name
|
||||
|
||||
@@ -1,33 +1,31 @@
|
||||
from collections import deque
|
||||
|
||||
from django import template
|
||||
from django.db.models import F
|
||||
|
||||
from members.models import JobGroup, JobMember
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.inclusion_tag("members/partials/jobs_sidebar.html")
|
||||
def get_jobs_sidebar(slug):
|
||||
job_groups = deque(JobGroup.objects.all())
|
||||
|
||||
# remove job group if there is no longer an active member
|
||||
for elem in job_groups.copy():
|
||||
job_members = JobMember.active_member.get_all(slug=elem.slug)
|
||||
if not job_members:
|
||||
job_groups.remove(elem)
|
||||
|
||||
job_members = JobMember.active_member.get_all(slug=slug).order_by(
|
||||
F("job__order").asc(nulls_last=True),
|
||||
"job__name",
|
||||
)
|
||||
active_job_group = JobGroup.objects.filter(slug=slug).first()
|
||||
|
||||
context = {
|
||||
"job_groups": job_groups,
|
||||
"job_members": job_members,
|
||||
"active_job_group": active_job_group,
|
||||
}
|
||||
|
||||
return context
|
||||
from collections import deque
|
||||
|
||||
from django import template
|
||||
from django.db.models import F
|
||||
|
||||
from members.models import JobGroup, JobMember
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.inclusion_tag("members/partials/jobs_sidebar.html")
|
||||
def get_jobs_sidebar(slug):
|
||||
job_groups = deque(JobGroup.objects.all())
|
||||
|
||||
# remove job group if there is no longer an active member
|
||||
for elem in job_groups.copy():
|
||||
job_members = JobMember.active_member.get_all(slug=elem.slug)
|
||||
if not job_members:
|
||||
job_groups.remove(elem)
|
||||
|
||||
job_members = JobMember.active_member.get_all(slug=slug).order_by(
|
||||
F("job__order").asc(nulls_last=True),
|
||||
"job__name",
|
||||
)
|
||||
active_job_group = JobGroup.objects.filter(slug=slug).first()
|
||||
|
||||
return {
|
||||
"job_groups": job_groups,
|
||||
"job_members": job_members,
|
||||
"active_job_group": active_job_group,
|
||||
}
|
||||
|
||||
@@ -1,22 +1,26 @@
|
||||
from django.core.validators import ValidationError
|
||||
|
||||
|
||||
def validate_domainonly_email(value):
|
||||
if "fet.at" not in value:
|
||||
raise ValidationError("In der Mailadresse fehlt die richtige Domäne.")
|
||||
|
||||
|
||||
def validate_file_size(value):
|
||||
if value.size > 10 * 1024 * 1024:
|
||||
raise ValidationError("Die maximale Dateigröße ist 10MB.")
|
||||
|
||||
|
||||
def validate_image_dimension(value):
|
||||
if value.height < 150 or value.width < 150:
|
||||
raise ValidationError(
|
||||
"Das Bild ist zu klein. (Höhe: %(height)s, Breite: %(width)s)",
|
||||
params={
|
||||
"height": value.height,
|
||||
"width": value.width,
|
||||
},
|
||||
)
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
|
||||
def validate_domainonly_email(value):
|
||||
if "fet.at" not in value:
|
||||
msg = "In der Mailadresse fehlt die richtige Domäne."
|
||||
raise ValidationError(msg, code="invalid_domain")
|
||||
|
||||
|
||||
def validate_file_size(value):
|
||||
if value.size > 10 * 1024 * 1024:
|
||||
msg = "Die maximale Dateigröße ist 10MB."
|
||||
raise ValidationError(msg, code="file_too_large")
|
||||
|
||||
|
||||
def validate_image_dimension(value):
|
||||
if value.height < 150 or value.width < 150:
|
||||
msg = "Das Bild ist zu klein. (Höhe: %(height)s, Breite: %(width)s)"
|
||||
raise ValidationError(
|
||||
msg,
|
||||
code="image_too_small",
|
||||
params={
|
||||
"height": value.height,
|
||||
"width": value.width,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -23,10 +23,7 @@ def create_token(username, masterpassword):
|
||||
|
||||
@authenticated_user
|
||||
def index(request):
|
||||
context = {
|
||||
"mctoken": "",
|
||||
"valid_master_pwd": True
|
||||
}
|
||||
context = {"mctoken": "", "valid_master_pwd": True}
|
||||
|
||||
masterpassword = settings.MC_MASTERPASSWORD
|
||||
|
||||
|
||||
@@ -1,275 +1,275 @@
|
||||
from ckeditor_uploader.widgets import CKEditorUploadingWidget
|
||||
from django import forms
|
||||
from django.forms.widgets import CheckboxInput
|
||||
from django.utils.dates import MONTHS
|
||||
from taggit.models import Tag
|
||||
|
||||
from .models import Event, FetMeeting, News, Post
|
||||
|
||||
|
||||
class PostForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Post
|
||||
fields = [
|
||||
"title",
|
||||
"subtitle",
|
||||
"tags",
|
||||
"image",
|
||||
"body",
|
||||
"slug",
|
||||
"author",
|
||||
"public_date",
|
||||
]
|
||||
|
||||
widgets = {"body": CKEditorUploadingWidget(config_name="default")}
|
||||
|
||||
class Media:
|
||||
js = (
|
||||
"js/auto_slug.js", # automatic slag completion via ajax
|
||||
"js/tag_completion.js", # to get a list for tag autocompletion via ajax
|
||||
)
|
||||
|
||||
|
||||
class NewsForm(PostForm):
|
||||
class Meta:
|
||||
model = News
|
||||
fields = "__all__"
|
||||
|
||||
help_texts = {
|
||||
"tags": (
|
||||
"Die Hashtags ohne '#' eintragen, und mit Komma kann man mehrere Tags anfügen."
|
||||
),
|
||||
"image": "Verwendbare Formate: ...",
|
||||
"is_pinned": (
|
||||
"Der Post soll als erster auf der Startseite angeheftet werden und sich "
|
||||
"automatisch einen Monat nach der Veröffentlichung wieder lösen."
|
||||
),
|
||||
}
|
||||
|
||||
labels = {
|
||||
"title": "Titel",
|
||||
"subtitle": "Untertitel",
|
||||
"image": "Hintergrundbild",
|
||||
"body": "Text",
|
||||
"slug": "Permalink",
|
||||
"author": "Autor",
|
||||
"public_date": "Veröffentlichung",
|
||||
"is_pinned": "Post anheften",
|
||||
}
|
||||
|
||||
widgets = {"body": CKEditorUploadingWidget(config_name="default")}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs) # to get the self.fields set
|
||||
author_qs = self.fields["author"].queryset.order_by("username")
|
||||
self.fields["author"].queryset = author_qs
|
||||
|
||||
|
||||
class EventForm(PostForm):
|
||||
class Meta:
|
||||
model = Event
|
||||
fields = "__all__"
|
||||
|
||||
help_texts = {
|
||||
"tags": (
|
||||
"Die Hashtags ohne '#' eintragen, und mit Komma kann man mehrere Tags anfügen."
|
||||
),
|
||||
"image": "Verwendbare Formate: Bildformate",
|
||||
"is_pinned": (
|
||||
"Dieses Event soll als erstes auf der Startseite angeheftet werden und sich "
|
||||
"automatisch einen Monat nach der Veröffentlichung wieder lösen."
|
||||
),
|
||||
}
|
||||
|
||||
labels = {
|
||||
"title": "Titel",
|
||||
"subtitle": "Untertitel",
|
||||
"image": "Hintergrundbild",
|
||||
"body": "Text",
|
||||
"event_start": "Start des Events",
|
||||
"event_end": "Ende des Events",
|
||||
"event_place": "Ort des Events",
|
||||
"slug": "Permalink",
|
||||
"author": "Autor",
|
||||
"public_date": "Veröffentlichung",
|
||||
"is_pinned": "Event anheften",
|
||||
}
|
||||
|
||||
widgets = {"body": CKEditorUploadingWidget(config_name="default")}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs) # to get the self.fields set
|
||||
author_qs = self.fields["author"].queryset.order_by("username")
|
||||
self.fields["author"].queryset = author_qs
|
||||
|
||||
self.fields["event_start"].required = True
|
||||
self.fields["event_end"].required = False
|
||||
|
||||
if "event_place" in self.fields:
|
||||
self.fields["event_place"].required = True
|
||||
|
||||
|
||||
class FetMeetingForm(PostForm):
|
||||
class Meta:
|
||||
model = FetMeeting
|
||||
fields = ["event_start", "event_end", "event_place", "tags"]
|
||||
|
||||
labels = {
|
||||
"event_start": "Start der Sitzung",
|
||||
"event_end": "Ende der Sitzung",
|
||||
"event_place": "Ort der Sitzung",
|
||||
}
|
||||
|
||||
help_texts = {
|
||||
"event_end": "Bei einer leeren Eingabe werden 2 Stunden zur Startzeit dazugezählt.",
|
||||
"tags": (
|
||||
"Die Hashtags ohne '#' eintragen, und mit Komma kann man mehrere Tags anfügen."
|
||||
),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs) # to get the self.fields set
|
||||
|
||||
self.fields["event_start"].required = True
|
||||
self.fields["event_end"].required = False
|
||||
|
||||
self.fields["event_place"].initial = "FET"
|
||||
|
||||
tags = []
|
||||
tags.append(Tag())
|
||||
tags[0].name = "fachschaft"
|
||||
self.fields["tags"].initial = tags
|
||||
|
||||
|
||||
class PostSearchForm(forms.Form):
|
||||
year_choices = [("", "Alle")]
|
||||
month_choices = [("", "Alle")] + list(MONTHS.items())
|
||||
|
||||
year = forms.ChoiceField(label="Jahr", choices=year_choices, required=False)
|
||||
month = forms.ChoiceField(label="Monat", choices=month_choices, required=False)
|
||||
|
||||
compact_view = forms.BooleanField(
|
||||
label="Kompakte Ansicht",
|
||||
required=False,
|
||||
widget=CheckboxInput,
|
||||
)
|
||||
fet_meeting_only = forms.BooleanField(
|
||||
label="nur FET Sitzungen",
|
||||
required=False,
|
||||
widget=CheckboxInput,
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs) # to get the self.fields set
|
||||
|
||||
try:
|
||||
first_post = Post.objects.get_queryset().last()
|
||||
last_post = Post.objects.get_queryset().first()
|
||||
if first_post and last_post:
|
||||
years = range(last_post.date.year, first_post.date.year - 1, -1)
|
||||
year_choices = [("", "Alle")] + [(i, i) for i in years]
|
||||
self.fields["year"].choices = year_choices
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
class NewsUpdateForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = News
|
||||
|
||||
fields = [
|
||||
"title",
|
||||
"status",
|
||||
"body",
|
||||
]
|
||||
|
||||
labels = {
|
||||
"title": "Titel",
|
||||
"image": "Hintergrundbild",
|
||||
"body": "Text",
|
||||
}
|
||||
|
||||
widgets = {"body": CKEditorUploadingWidget(config_name="default")}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs) # to get the self.fields set
|
||||
|
||||
self.fields["title"].autofocus = True
|
||||
|
||||
|
||||
class EventUpdateForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Event
|
||||
fields = [
|
||||
"title",
|
||||
"status",
|
||||
"event_start",
|
||||
"event_end",
|
||||
"event_place",
|
||||
]
|
||||
|
||||
labels = {
|
||||
"title": "Titel",
|
||||
"event_start": "Start des Events",
|
||||
"event_end": "Ende des Events",
|
||||
"event_place": "Ort des Events",
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs) # to get the self.fields set
|
||||
|
||||
self.fields["event_start"].required = True
|
||||
self.fields["event_start"].autofocus = True
|
||||
|
||||
self.fields["event_end"].required = False
|
||||
|
||||
if "event_place" in self.fields:
|
||||
self.fields["event_place"].required = True
|
||||
|
||||
|
||||
class FetMeetingCreateForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = FetMeeting
|
||||
fields = ["event_start", "event_end", "event_place"]
|
||||
|
||||
help_texts = {
|
||||
"event_end": "Bei einer leeren Eingabe werden 2 Stunden zur Startzeit dazugezählt.",
|
||||
}
|
||||
|
||||
labels = {
|
||||
"event_start": "Start der Sitzung",
|
||||
"event_end": "Ende der Sitzung",
|
||||
"event_place": "Ort der Sitzung",
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs) # to get the self.fields set
|
||||
|
||||
self.fields["event_start"].required = True
|
||||
self.fields["event_start"].autofocus = True
|
||||
|
||||
self.fields["event_end"].required = False
|
||||
|
||||
self.fields["event_place"].initial = "FET"
|
||||
|
||||
|
||||
class FetMeetingUpdateForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = FetMeeting
|
||||
fields = ["event_start", "event_end", "event_place"]
|
||||
|
||||
labels = {
|
||||
"event_start": "Start der Sitzung",
|
||||
"event_end": "Ende der Sitzung",
|
||||
"event_place": "Ort der Sitzung",
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs) # to get the self.fields set
|
||||
|
||||
self.fields["event_start"].required = True
|
||||
self.fields["event_start"].autofocus = True
|
||||
|
||||
self.fields["event_end"].required = False
|
||||
|
||||
self.fields["event_place"].initial = "FET"
|
||||
from ckeditor_uploader.widgets import CKEditorUploadingWidget
|
||||
from django import forms
|
||||
from django.forms.widgets import CheckboxInput
|
||||
from django.utils.dates import MONTHS
|
||||
from taggit.models import Tag
|
||||
|
||||
from .models import Event, FetMeeting, News, Post
|
||||
|
||||
|
||||
class PostForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Post
|
||||
fields = [
|
||||
"title",
|
||||
"subtitle",
|
||||
"tags",
|
||||
"image",
|
||||
"body",
|
||||
"slug",
|
||||
"author",
|
||||
"public_date",
|
||||
]
|
||||
|
||||
widgets = {"body": CKEditorUploadingWidget(config_name="default")}
|
||||
|
||||
class Media:
|
||||
js = (
|
||||
"js/auto_slug.js", # automatic slag completion via ajax
|
||||
"js/tag_completion.js", # to get a list for tag autocompletion via ajax
|
||||
)
|
||||
|
||||
|
||||
class NewsForm(PostForm):
|
||||
class Meta:
|
||||
model = News
|
||||
fields = "__all__"
|
||||
|
||||
help_texts = {
|
||||
"tags": (
|
||||
"Die Hashtags ohne '#' eintragen, und mit Komma kann man mehrere Tags anfügen."
|
||||
),
|
||||
"image": "Verwendbare Formate: ...",
|
||||
"is_pinned": (
|
||||
"Der Post soll als erster auf der Startseite angeheftet werden und sich "
|
||||
"automatisch einen Monat nach der Veröffentlichung wieder lösen."
|
||||
),
|
||||
}
|
||||
|
||||
labels = {
|
||||
"title": "Titel",
|
||||
"subtitle": "Untertitel",
|
||||
"image": "Hintergrundbild",
|
||||
"body": "Text",
|
||||
"slug": "Permalink",
|
||||
"author": "Autor",
|
||||
"public_date": "Veröffentlichung",
|
||||
"is_pinned": "Post anheften",
|
||||
}
|
||||
|
||||
widgets = {"body": CKEditorUploadingWidget(config_name="default")}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs) # to get the self.fields set
|
||||
author_qs = self.fields["author"].queryset.order_by("username")
|
||||
self.fields["author"].queryset = author_qs
|
||||
|
||||
|
||||
class EventForm(PostForm):
|
||||
class Meta:
|
||||
model = Event
|
||||
fields = "__all__"
|
||||
|
||||
help_texts = {
|
||||
"tags": (
|
||||
"Die Hashtags ohne '#' eintragen, und mit Komma kann man mehrere Tags anfügen."
|
||||
),
|
||||
"image": "Verwendbare Formate: Bildformate",
|
||||
"is_pinned": (
|
||||
"Dieses Event soll als erstes auf der Startseite angeheftet werden und sich "
|
||||
"automatisch ein Tag nach dem Eventende wieder lösen."
|
||||
),
|
||||
}
|
||||
|
||||
labels = {
|
||||
"title": "Titel",
|
||||
"subtitle": "Untertitel",
|
||||
"image": "Hintergrundbild",
|
||||
"body": "Text",
|
||||
"event_start": "Start des Events",
|
||||
"event_end": "Ende des Events",
|
||||
"event_place": "Ort des Events",
|
||||
"slug": "Permalink",
|
||||
"author": "Autor",
|
||||
"public_date": "Veröffentlichung",
|
||||
"is_pinned": "Event anheften",
|
||||
}
|
||||
|
||||
widgets = {"body": CKEditorUploadingWidget(config_name="default")}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs) # to get the self.fields set
|
||||
author_qs = self.fields["author"].queryset.order_by("username")
|
||||
self.fields["author"].queryset = author_qs
|
||||
|
||||
self.fields["event_start"].required = True
|
||||
self.fields["event_end"].required = False
|
||||
|
||||
if "event_place" in self.fields:
|
||||
self.fields["event_place"].required = True
|
||||
|
||||
|
||||
class FetMeetingForm(PostForm):
|
||||
class Meta:
|
||||
model = FetMeeting
|
||||
fields = ["event_start", "event_end", "event_place", "tags"]
|
||||
|
||||
labels = {
|
||||
"event_start": "Start der Sitzung",
|
||||
"event_end": "Ende der Sitzung",
|
||||
"event_place": "Ort der Sitzung",
|
||||
}
|
||||
|
||||
help_texts = {
|
||||
"event_end": "Bei einer leeren Eingabe werden 2 Stunden zur Startzeit dazugezählt.",
|
||||
"tags": (
|
||||
"Die Hashtags ohne '#' eintragen, und mit Komma kann man mehrere Tags anfügen."
|
||||
),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs) # to get the self.fields set
|
||||
|
||||
self.fields["event_start"].required = True
|
||||
self.fields["event_end"].required = False
|
||||
|
||||
self.fields["event_place"].initial = "FET"
|
||||
|
||||
tags = []
|
||||
tags.append(Tag())
|
||||
tags[0].name = "fachschaft"
|
||||
self.fields["tags"].initial = tags
|
||||
|
||||
|
||||
class PostSearchForm(forms.Form):
|
||||
year_choices = [("", "Alle")]
|
||||
month_choices = [("", "Alle")] + list(MONTHS.items())
|
||||
|
||||
year = forms.ChoiceField(label="Jahr", choices=year_choices, required=False)
|
||||
month = forms.ChoiceField(label="Monat", choices=month_choices, required=False)
|
||||
|
||||
compact_view = forms.BooleanField(
|
||||
label="Kompakte Ansicht",
|
||||
required=False,
|
||||
widget=CheckboxInput,
|
||||
)
|
||||
fet_meeting_only = forms.BooleanField(
|
||||
label="nur FET Sitzungen",
|
||||
required=False,
|
||||
widget=CheckboxInput,
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs) # to get the self.fields set
|
||||
|
||||
try:
|
||||
first_post = Post.objects.get_queryset().last()
|
||||
last_post = Post.objects.get_queryset().first()
|
||||
if first_post and last_post:
|
||||
years = range(last_post.date.year, first_post.date.year - 1, -1)
|
||||
year_choices = [("", "Alle")] + [(i, i) for i in years]
|
||||
self.fields["year"].choices = year_choices
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
class NewsUpdateForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = News
|
||||
|
||||
fields = [
|
||||
"title",
|
||||
"status",
|
||||
"body",
|
||||
]
|
||||
|
||||
labels = {
|
||||
"title": "Titel",
|
||||
"image": "Hintergrundbild",
|
||||
"body": "Text",
|
||||
}
|
||||
|
||||
widgets = {"body": CKEditorUploadingWidget(config_name="default")}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs) # to get the self.fields set
|
||||
|
||||
self.fields["title"].autofocus = True
|
||||
|
||||
|
||||
class EventUpdateForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Event
|
||||
fields = [
|
||||
"title",
|
||||
"status",
|
||||
"event_start",
|
||||
"event_end",
|
||||
"event_place",
|
||||
]
|
||||
|
||||
labels = {
|
||||
"title": "Titel",
|
||||
"event_start": "Start des Events",
|
||||
"event_end": "Ende des Events",
|
||||
"event_place": "Ort des Events",
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs) # to get the self.fields set
|
||||
|
||||
self.fields["event_start"].required = True
|
||||
self.fields["event_start"].autofocus = True
|
||||
|
||||
self.fields["event_end"].required = False
|
||||
|
||||
if "event_place" in self.fields:
|
||||
self.fields["event_place"].required = True
|
||||
|
||||
|
||||
class FetMeetingCreateForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = FetMeeting
|
||||
fields = ["event_start", "event_end", "event_place"]
|
||||
|
||||
help_texts = {
|
||||
"event_end": "Bei einer leeren Eingabe werden 2 Stunden zur Startzeit dazugezählt.",
|
||||
}
|
||||
|
||||
labels = {
|
||||
"event_start": "Start der Sitzung",
|
||||
"event_end": "Ende der Sitzung",
|
||||
"event_place": "Ort der Sitzung",
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs) # to get the self.fields set
|
||||
|
||||
self.fields["event_start"].required = True
|
||||
self.fields["event_start"].autofocus = True
|
||||
|
||||
self.fields["event_end"].required = False
|
||||
|
||||
self.fields["event_place"].initial = "FET"
|
||||
|
||||
|
||||
class FetMeetingUpdateForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = FetMeeting
|
||||
fields = ["event_start", "event_end", "event_place"]
|
||||
|
||||
labels = {
|
||||
"event_start": "Start der Sitzung",
|
||||
"event_end": "Ende der Sitzung",
|
||||
"event_place": "Ort der Sitzung",
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs) # to get the self.fields set
|
||||
|
||||
self.fields["event_start"].required = True
|
||||
self.fields["event_start"].autofocus = True
|
||||
|
||||
self.fields["event_end"].required = False
|
||||
|
||||
self.fields["event_place"].initial = "FET"
|
||||
|
||||
@@ -1,201 +1,206 @@
|
||||
import datetime
|
||||
|
||||
from django.db import models
|
||||
from django.db.models import Case, Q, When
|
||||
|
||||
from .choices import PostType, Status
|
||||
|
||||
|
||||
class PublishedManager(models.Manager):
|
||||
def published(self, public=True):
|
||||
"""
|
||||
publish all posts with status 'PUBLIC'
|
||||
"""
|
||||
return self.get_queryset().filter(status=Status.PUBLIC) if public else self.get_queryset()
|
||||
|
||||
def published_all(self, public=True):
|
||||
"""
|
||||
publish all posts with status 'PUBLIC' and 'ONLY_INTERN'
|
||||
"""
|
||||
return (
|
||||
self.get_queryset().filter(~Q(status=Status.DRAFT)) if public else self.get_queryset()
|
||||
)
|
||||
|
||||
|
||||
class PostManager(PublishedManager, models.Manager):
|
||||
def get_queryset(self):
|
||||
qs = (
|
||||
super()
|
||||
.get_queryset()
|
||||
.annotate(
|
||||
date=Case(
|
||||
When(post_type=PostType.NEWS, then="public_date"),
|
||||
When(post_type=PostType.EVENT, then="event_start__date"),
|
||||
When(post_type=PostType.FETMEETING, then="event_start__date"),
|
||||
),
|
||||
)
|
||||
)
|
||||
return qs.order_by("-date", "-id")
|
||||
|
||||
def date_sorted(self, public=True):
|
||||
return self.published(public)
|
||||
|
||||
def date_filter(
|
||||
self,
|
||||
public=True,
|
||||
year=None,
|
||||
month=None,
|
||||
fet_meeting_only=None,
|
||||
):
|
||||
qs_filter = Q()
|
||||
|
||||
if fet_meeting_only:
|
||||
qs_filter &= Q(post_type=PostType.FETMEETING)
|
||||
|
||||
if year:
|
||||
qs_filter &= Q(date__year=year)
|
||||
if month:
|
||||
qs_filter &= Q(date__month=month)
|
||||
|
||||
return self.published(public).filter(qs_filter)
|
||||
|
||||
|
||||
class ArticleManager(PublishedManager, models.Manager):
|
||||
"""
|
||||
Provide a query set only for "Article"
|
||||
regular fet meetings should not be contained in the news stream
|
||||
"""
|
||||
|
||||
def get_queryset(self):
|
||||
qs = super().get_queryset().filter(Q(post_type=PostType.NEWS) | Q(post_type=PostType.EVENT))
|
||||
qs = qs.annotate(
|
||||
date=Case(
|
||||
When(post_type=PostType.NEWS, then="public_date"),
|
||||
When(post_type=PostType.EVENT, then="event_start__date"),
|
||||
),
|
||||
)
|
||||
return qs.order_by("-date", "-id")
|
||||
|
||||
def date_sorted(self, public=True):
|
||||
return self.published(public)
|
||||
|
||||
def pinned(self, public=True):
|
||||
# Get date for pinned news that is max 1 month old.
|
||||
post_date = datetime.datetime.now(tz=datetime.UTC).date()
|
||||
__month = post_date.month
|
||||
__year = post_date.year
|
||||
|
||||
if __month != 1:
|
||||
__month -= 1
|
||||
else:
|
||||
# If the current month is January, you get the date from December of previous year.
|
||||
__month = 12
|
||||
__year -= 1
|
||||
|
||||
post_date = post_date.replace(year=__year, month=__month)
|
||||
|
||||
# Get date for event posts that is max 1 day old.
|
||||
event_date = datetime.datetime.now(tz=datetime.UTC).date() - datetime.timedelta(1)
|
||||
|
||||
return (
|
||||
self.published(public)
|
||||
.filter(
|
||||
Q(is_pinned=True)
|
||||
& (
|
||||
(Q(post_type=PostType.NEWS) & Q(public_date__gt=post_date))
|
||||
| (Q(post_type=PostType.EVENT) & Q(event_end__date__gt=event_date))
|
||||
)
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
|
||||
class NewsManager(PublishedManager, models.Manager):
|
||||
"""
|
||||
Provide a query set only for "News"
|
||||
"""
|
||||
|
||||
def get_queryset(self):
|
||||
qs = super().get_queryset().filter(post_type=PostType.NEWS)
|
||||
qs = qs.annotate(
|
||||
date=Case(
|
||||
When(post_type=PostType.NEWS, then="public_date"),
|
||||
),
|
||||
)
|
||||
return qs.order_by("-date")
|
||||
|
||||
|
||||
class AllEventManager(PublishedManager, models.Manager):
|
||||
"""
|
||||
Provide a query set for all events ("Event" and "Fet Meeting")
|
||||
"""
|
||||
|
||||
def get_queryset(self):
|
||||
qs = (
|
||||
super()
|
||||
.get_queryset()
|
||||
.filter(Q(post_type=PostType.EVENT) | Q(post_type=PostType.FETMEETING))
|
||||
)
|
||||
qs = qs.annotate(
|
||||
date=Case(
|
||||
When(post_type=PostType.EVENT, then="event_start__date"),
|
||||
When(post_type=PostType.FETMEETING, then="event_start__date"),
|
||||
),
|
||||
)
|
||||
return qs.order_by("-date")
|
||||
|
||||
def future_events(self, public=True):
|
||||
date_today = datetime.datetime.now(tz=datetime.UTC).date()
|
||||
qs = self.published(public).filter(event_start__gt=date_today)
|
||||
return qs.reverse()
|
||||
|
||||
|
||||
class EventManager(PublishedManager, models.Manager):
|
||||
"""
|
||||
Provide a query set only for "Events"
|
||||
regular fet meetings should not be contained in the news stream
|
||||
"""
|
||||
|
||||
def get_queryset(self):
|
||||
qs = super().get_queryset().filter(post_type=PostType.EVENT)
|
||||
qs = qs.annotate(
|
||||
date=Case(
|
||||
When(post_type=PostType.EVENT, then="event_start__date"),
|
||||
),
|
||||
)
|
||||
return qs.order_by("-date")
|
||||
|
||||
def future_events(self, public=True):
|
||||
date_today = datetime.datetime.now(tz=datetime.UTC).date()
|
||||
qs = self.published(public).filter(event_start__gt=date_today)
|
||||
return qs.reverse()
|
||||
|
||||
def past_events(self, public=True):
|
||||
date_today = datetime.datetime.now(tz=datetime.UTC).date()
|
||||
qs = self.published(public).filter(event_start__lt=date_today)
|
||||
return qs
|
||||
|
||||
|
||||
class FetMeetingManager(PublishedManager, models.Manager):
|
||||
"""
|
||||
Provide a query set only for "Fet Meeting"
|
||||
"""
|
||||
|
||||
def get_queryset(self):
|
||||
qs = super().get_queryset().filter(post_type=PostType.FETMEETING)
|
||||
qs = qs.annotate(
|
||||
date=Case(
|
||||
When(post_type=PostType.FETMEETING, then="event_start__date"),
|
||||
),
|
||||
)
|
||||
return qs.order_by("-date")
|
||||
|
||||
def future_events(self):
|
||||
date_today = datetime.datetime.now(tz=datetime.UTC).date()
|
||||
qs = self.published().filter(event_start__gt=date_today)
|
||||
return qs.reverse()
|
||||
|
||||
def past_events(self):
|
||||
date_today = datetime.datetime.now(tz=datetime.UTC).date()
|
||||
qs = self.published().filter(event_start__lt=date_today)
|
||||
return qs
|
||||
import calendar
|
||||
import datetime
|
||||
|
||||
from django.db import models
|
||||
from django.db.models import Case, Q, When
|
||||
from django.utils import timezone
|
||||
|
||||
from .choices import PostType, Status
|
||||
|
||||
|
||||
class PublishedManager(models.Manager):
|
||||
def published(self, public=True):
|
||||
"""
|
||||
publish all posts with status 'PUBLIC'
|
||||
"""
|
||||
return self.get_queryset().filter(status=Status.PUBLIC) if public else self.get_queryset()
|
||||
|
||||
def published_all(self, public=True):
|
||||
"""
|
||||
publish all posts with status 'PUBLIC' and 'ONLY_INTERN'
|
||||
"""
|
||||
return (
|
||||
self.get_queryset().filter(~Q(status=Status.DRAFT)) if public else self.get_queryset()
|
||||
)
|
||||
|
||||
|
||||
class PostManager(PublishedManager, models.Manager):
|
||||
def get_queryset(self):
|
||||
qs = (
|
||||
super()
|
||||
.get_queryset()
|
||||
.annotate(
|
||||
date=Case(
|
||||
When(post_type=PostType.NEWS, then="public_date"),
|
||||
When(post_type=PostType.EVENT, then="event_start__date"),
|
||||
When(post_type=PostType.FETMEETING, then="event_start__date"),
|
||||
),
|
||||
)
|
||||
)
|
||||
return qs.order_by("-date", "-id")
|
||||
|
||||
def date_sorted(self, public=True):
|
||||
return self.published(public)
|
||||
|
||||
def date_filter(
|
||||
self,
|
||||
public=True,
|
||||
year=None,
|
||||
month=None,
|
||||
fet_meeting_only=None,
|
||||
):
|
||||
qs_filter = Q()
|
||||
|
||||
if fet_meeting_only:
|
||||
qs_filter &= Q(post_type=PostType.FETMEETING)
|
||||
|
||||
if year:
|
||||
qs_filter &= Q(date__year=year)
|
||||
if month:
|
||||
qs_filter &= Q(date__month=month)
|
||||
|
||||
return self.published(public).filter(qs_filter)
|
||||
|
||||
|
||||
class ArticleManager(PublishedManager, models.Manager):
|
||||
"""
|
||||
Provide a query set only for "Article"
|
||||
regular fet meetings should not be contained in the news stream
|
||||
"""
|
||||
|
||||
def get_queryset(self):
|
||||
qs = super().get_queryset().filter(Q(post_type=PostType.NEWS) | Q(post_type=PostType.EVENT))
|
||||
qs = qs.annotate(
|
||||
date=Case(
|
||||
When(post_type=PostType.NEWS, then="public_date"),
|
||||
When(post_type=PostType.EVENT, then="event_start__date"),
|
||||
),
|
||||
)
|
||||
return qs.order_by("-date", "-id")
|
||||
|
||||
def date_sorted(self, public=True):
|
||||
return self.published(public)
|
||||
|
||||
def pinned(self, public=True):
|
||||
# Get date for pinned news that is max 1 month old.
|
||||
post_date = timezone.now().date()
|
||||
_day = post_date.day
|
||||
_month = post_date.month
|
||||
_year = post_date.year
|
||||
|
||||
if _month != 1:
|
||||
_month -= 1
|
||||
else:
|
||||
# If the current month is January, you get the date from December of previous year.
|
||||
_month = 12
|
||||
_year -= 1
|
||||
|
||||
# Clamp day to last day of target month (handles 30/31 and Feb)
|
||||
last_day = calendar.monthrange(_year, _month)[1]
|
||||
safe_day = min(_day, last_day)
|
||||
|
||||
post_date = post_date.replace(year=_year, month=_month, day=safe_day)
|
||||
|
||||
# Get date for event posts that is max 1 day old.
|
||||
event_date = timezone.now().date() - datetime.timedelta(1)
|
||||
|
||||
return (
|
||||
self.published(public)
|
||||
.filter(
|
||||
Q(is_pinned=True)
|
||||
& (
|
||||
(Q(post_type=PostType.NEWS) & Q(public_date__gt=post_date))
|
||||
| (Q(post_type=PostType.EVENT) & Q(event_end__date__gt=event_date))
|
||||
)
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
|
||||
class NewsManager(PublishedManager, models.Manager):
|
||||
"""
|
||||
Provide a query set only for "News"
|
||||
"""
|
||||
|
||||
def get_queryset(self):
|
||||
qs = super().get_queryset().filter(post_type=PostType.NEWS)
|
||||
qs = qs.annotate(
|
||||
date=Case(
|
||||
When(post_type=PostType.NEWS, then="public_date"),
|
||||
),
|
||||
)
|
||||
return qs.order_by("-date")
|
||||
|
||||
|
||||
class AllEventManager(PublishedManager, models.Manager):
|
||||
"""
|
||||
Provide a query set for all events ("Event" and "Fet Meeting")
|
||||
"""
|
||||
|
||||
def get_queryset(self):
|
||||
qs = (
|
||||
super()
|
||||
.get_queryset()
|
||||
.filter(Q(post_type=PostType.EVENT) | Q(post_type=PostType.FETMEETING))
|
||||
)
|
||||
qs = qs.annotate(
|
||||
date=Case(
|
||||
When(post_type=PostType.EVENT, then="event_start__date"),
|
||||
When(post_type=PostType.FETMEETING, then="event_start__date"),
|
||||
),
|
||||
)
|
||||
return qs.order_by("-date")
|
||||
|
||||
def future_events(self, public=True):
|
||||
date_today = timezone.now().date()
|
||||
qs = self.published(public).filter(event_start__gt=date_today)
|
||||
return qs.reverse()
|
||||
|
||||
|
||||
class EventManager(PublishedManager, models.Manager):
|
||||
"""
|
||||
Provide a query set only for "Events"
|
||||
regular fet meetings should not be contained in the news stream
|
||||
"""
|
||||
|
||||
def get_queryset(self):
|
||||
qs = super().get_queryset().filter(post_type=PostType.EVENT)
|
||||
qs = qs.annotate(
|
||||
date=Case(
|
||||
When(post_type=PostType.EVENT, then="event_start__date"),
|
||||
),
|
||||
)
|
||||
return qs.order_by("-date")
|
||||
|
||||
def future_events(self, public=True):
|
||||
date_today = timezone.now().date()
|
||||
qs = self.published(public).filter(event_start__gt=date_today)
|
||||
return qs.reverse()
|
||||
|
||||
def past_events(self, public=True):
|
||||
date_today = timezone.now().date()
|
||||
return self.published(public).filter(event_start__lt=date_today)
|
||||
|
||||
|
||||
class FetMeetingManager(PublishedManager, models.Manager):
|
||||
"""
|
||||
Provide a query set only for "Fet Meeting"
|
||||
"""
|
||||
|
||||
def get_queryset(self):
|
||||
qs = super().get_queryset().filter(post_type=PostType.FETMEETING)
|
||||
qs = qs.annotate(
|
||||
date=Case(
|
||||
When(post_type=PostType.FETMEETING, then="event_start__date"),
|
||||
),
|
||||
)
|
||||
return qs.order_by("-date")
|
||||
|
||||
def future_events(self):
|
||||
date_today = timezone.now().date()
|
||||
qs = self.published().filter(event_start__gt=date_today)
|
||||
return qs.reverse()
|
||||
|
||||
def past_events(self):
|
||||
date_today = timezone.now().date()
|
||||
return self.published().filter(event_start__lt=date_today)
|
||||
|
||||
@@ -1,376 +1,384 @@
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.validators import ValidationError
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.utils.text import slugify
|
||||
from taggit.managers import TaggableManager
|
||||
|
||||
from core.models import CustomFlatPage
|
||||
from documents.api import ep_create_new_pad, ep_get_html, ep_get_url, ep_pad_exists, ep_set_html
|
||||
|
||||
from .choices import PostType, Status
|
||||
from .managers import (
|
||||
AllEventManager,
|
||||
ArticleManager,
|
||||
EventManager,
|
||||
FetMeetingManager,
|
||||
NewsManager,
|
||||
PostManager,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def create_pad_for_post(slug, item="agenda"):
|
||||
logger.info("Pad-Type: %s", item)
|
||||
|
||||
pad_id = slug + "-" + item
|
||||
if not ep_create_new_pad(pad_id):
|
||||
return ""
|
||||
|
||||
# Set template into the newly created pad if it exists.
|
||||
if page := CustomFlatPage.objects.filter(title__iexact=item).first():
|
||||
ep_set_html(pad_id, page.content)
|
||||
logger.info("Template '%s' is set.", page.title)
|
||||
|
||||
return pad_id
|
||||
|
||||
|
||||
class Post(models.Model):
|
||||
# legacy id is for the posts from the old website
|
||||
legacy_id = models.IntegerField(null=True, blank=True)
|
||||
|
||||
title = models.CharField(verbose_name="Titel", max_length=200)
|
||||
subtitle = models.CharField(max_length=500, blank=True, default="")
|
||||
tags = TaggableManager(blank=True)
|
||||
|
||||
slug = models.SlugField(unique=True, blank=True)
|
||||
|
||||
body = models.TextField(blank=True, default="")
|
||||
image = models.ImageField(null=True, blank=True)
|
||||
|
||||
author = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True)
|
||||
public_date = models.DateField(
|
||||
verbose_name="Veröffentlichung",
|
||||
null=True,
|
||||
blank=True,
|
||||
default=timezone.now,
|
||||
)
|
||||
|
||||
post_type = models.CharField(max_length=1, choices=PostType.choices, editable=True)
|
||||
|
||||
status = models.CharField(
|
||||
verbose_name="Status", max_length=2, choices=Status.choices, default=Status.DRAFT
|
||||
)
|
||||
|
||||
# post is pinned at main page
|
||||
is_pinned = models.BooleanField(verbose_name="ANGEHEFTET", default=False)
|
||||
|
||||
# addional infos for events
|
||||
event_start = models.DateTimeField(verbose_name="Event Start", null=True, blank=True)
|
||||
event_end = models.DateTimeField(verbose_name="Event Ende", null=True, blank=True)
|
||||
event_place = models.CharField(max_length=200, blank=True, default="")
|
||||
|
||||
# protocol for fet meeting
|
||||
has_protocol = models.BooleanField(default=False)
|
||||
has_agenda = models.BooleanField(default=False)
|
||||
protocol_key = models.CharField(max_length=200, blank=True, default="")
|
||||
agenda_key = models.CharField(max_length=200, blank=True, default="")
|
||||
|
||||
# TimeStamps
|
||||
date_modified = models.DateTimeField(auto_now=True)
|
||||
date_created = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
# Managers
|
||||
objects = PostManager()
|
||||
articles = ArticleManager()
|
||||
|
||||
def __str__(self):
|
||||
return "Post ({}, {}): {}".format(
|
||||
self.slug,
|
||||
self.public_date.strftime("%d.%m.%Y"),
|
||||
self.title,
|
||||
)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# save the post with some defaults
|
||||
if not self.public_date:
|
||||
self.public_date = timezone.now().date()
|
||||
|
||||
if not self.slug:
|
||||
self.slug = slugify(self.public_date) + "-" + slugify(self.title)
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("posts:post", kwargs={"slug": self.slug})
|
||||
|
||||
# "#" for backward compatibility
|
||||
_possible_empty_key_value = ["#", "", None]
|
||||
|
||||
@property
|
||||
def agenda_html(self) -> str | None:
|
||||
if self.agenda_key in self._possible_empty_key_value:
|
||||
return None
|
||||
|
||||
return ep_get_html(self.agenda_key)
|
||||
|
||||
@agenda_html.setter
|
||||
def agenda_html(self, value: str) -> str | None:
|
||||
if self.agenda_key in self._possible_empty_key_value:
|
||||
self.create_agenda_key()
|
||||
|
||||
if not value or not self.agenda_key:
|
||||
return None
|
||||
|
||||
ep_set_html(self.agenda_key, value)
|
||||
logger.info("Set agenda etherpad '%s' for post '%s'.", self.agenda_key, self.slug)
|
||||
return value
|
||||
|
||||
@property
|
||||
def protocol_html(self) -> str | None:
|
||||
if self.protocol_key in self._possible_empty_key_value:
|
||||
return None
|
||||
|
||||
return ep_get_html(self.protocol_key)
|
||||
|
||||
@protocol_html.setter
|
||||
def protocol_html(self, value: str) -> str | None:
|
||||
if self.protocol_key in self._possible_empty_key_value:
|
||||
self.create_protocol_key()
|
||||
|
||||
if not value or not self.protocol_key:
|
||||
return None
|
||||
|
||||
ep_set_html(self.protocol_key, value)
|
||||
logger.info("Set protocol etherpad '%s' for post '%s'.", self.protocol_key, self.slug)
|
||||
return value
|
||||
|
||||
_agenda_filename = None
|
||||
_agenda_url = None
|
||||
|
||||
@property
|
||||
def agenda_url(self) -> str | None:
|
||||
if not self.has_agenda:
|
||||
self._agenda_url = None
|
||||
self._agenda_filename = None
|
||||
return self._agenda_url
|
||||
|
||||
if self._agenda_url:
|
||||
return self._agenda_url
|
||||
|
||||
if url := ep_get_url(self.agenda_key):
|
||||
self._agenda_url = url
|
||||
self._agenda_filename = self.slug + "-agenda.pdf"
|
||||
else:
|
||||
self._agenda_url = None
|
||||
self._agenda_filename = None
|
||||
|
||||
return self._agenda_url
|
||||
|
||||
@property
|
||||
def agenda_filename(self) -> str | None:
|
||||
# TODO: fix pdf render
|
||||
# if self._agenda_filename:
|
||||
# return self._agenda_filename
|
||||
|
||||
# if self.has_agenda and self.agenda_url:
|
||||
# return self.slug + "-agenda.pdf"
|
||||
|
||||
return None
|
||||
|
||||
_protocol_filename = None
|
||||
_protocol_url = None
|
||||
|
||||
@property
|
||||
def protocol_url(self) -> str | None:
|
||||
if not self.has_protocol:
|
||||
self._protocol_url = None
|
||||
self._protocol_filename = None
|
||||
return self._protocol_url
|
||||
|
||||
if self._protocol_url:
|
||||
return self._protocol_url
|
||||
|
||||
if url := ep_get_url(self.protocol_key):
|
||||
self._protocol_url = url
|
||||
self._protocol_filename = self.slug + "-protokoll.pdf"
|
||||
else:
|
||||
self._protocol_url = None
|
||||
self._protocol_filename = None
|
||||
|
||||
return self._protocol_url
|
||||
|
||||
@property
|
||||
def protocol_filename(self) -> str | None:
|
||||
# TODO: fix pdf render
|
||||
# if self._protocol_filename:
|
||||
# return self._protocol_filename
|
||||
|
||||
# if self.has_protocol and self.protocol_url:
|
||||
# return self.slug + "-protokoll.pdf"
|
||||
|
||||
return None
|
||||
|
||||
def create_agenda_key(self) -> None:
|
||||
"""
|
||||
Create a Etherpad Id for the Pad associated to this post.
|
||||
Create the pad if it doesn't exist.
|
||||
"""
|
||||
if self.slug:
|
||||
self.agenda_key = create_pad_for_post(self.slug, "agenda")
|
||||
|
||||
def create_protocol_key(self) -> None:
|
||||
"""
|
||||
Create a Etherpad Id for the Pad associated to this post.
|
||||
Create the pad if it doesn't exist.
|
||||
"""
|
||||
if self.slug:
|
||||
self.protocol_key = create_pad_for_post(self.slug, "protocol")
|
||||
|
||||
def get_model_name(self):
|
||||
return self._meta.model_name
|
||||
|
||||
@property
|
||||
def three_tag_names(self):
|
||||
return self.tags.names()[:3]
|
||||
|
||||
@property
|
||||
def tag_names(self):
|
||||
return [t for t in self.tags.names()]
|
||||
|
||||
@property
|
||||
def imageurl(self) -> str:
|
||||
return (
|
||||
self.image.url if self.image else settings.STATIC_URL + "img/FET-Logo-2014-quadrat.png"
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
if self.event_end and self.event_end < self.event_start:
|
||||
raise ValidationError("Das Ende des Events liegt vor dem Beginn.")
|
||||
if self.event_start and self.post_type not in ["E", "F"]:
|
||||
raise ValidationError("Für diesen Post Typ ist kein Event Start zulässig.")
|
||||
|
||||
@property
|
||||
def published(self):
|
||||
return self.status == Status.PUBLIC
|
||||
|
||||
|
||||
class News(Post):
|
||||
objects = NewsManager()
|
||||
|
||||
class Meta:
|
||||
proxy = True
|
||||
|
||||
verbose_name = "News"
|
||||
verbose_name_plural = "News"
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.post_type:
|
||||
self.post_type = "N"
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
|
||||
class Event(Post):
|
||||
only_events = EventManager()
|
||||
all_events = AllEventManager()
|
||||
|
||||
class Meta:
|
||||
proxy = True
|
||||
|
||||
verbose_name = "Event"
|
||||
verbose_name_plural = "Events"
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.post_type = "E"
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.post_type:
|
||||
self.post_type = "E"
|
||||
if not self.event_end:
|
||||
self.event_end = self.event_start + timedelta(hours=2)
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
if not self.event_start:
|
||||
raise ValidationError("Das Datum des Events fehlt.")
|
||||
|
||||
|
||||
class FetMeeting(Event):
|
||||
objects = FetMeetingManager()
|
||||
|
||||
class Meta:
|
||||
proxy = True
|
||||
|
||||
verbose_name = "Fet Sitzung"
|
||||
verbose_name_plural = "Fet Sitzungen"
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.post_type = "F"
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.title = "Fachschaftssitzung"
|
||||
if not self.slug:
|
||||
self.slug = self.__get_slug()
|
||||
|
||||
if not ep_pad_exists(self.agenda_key) or self.agenda_key in self._possible_empty_key_value:
|
||||
self.create_agenda_key()
|
||||
if self.agenda_key:
|
||||
self.has_agenda = True
|
||||
|
||||
if (
|
||||
not ep_pad_exists(self.protocol_key)
|
||||
or self.protocol_key in self._possible_empty_key_value
|
||||
):
|
||||
self.create_protocol_key()
|
||||
if self.protocol_key:
|
||||
self.has_protocol = True
|
||||
|
||||
if not self.post_type:
|
||||
self.post_type = "F"
|
||||
|
||||
if not self.event_place:
|
||||
self.event_place = "FET"
|
||||
|
||||
# make duration 2 hours if not specified otherwise
|
||||
if not self.event_end:
|
||||
self.event_end = self.event_start + timedelta(hours=2)
|
||||
|
||||
# set FET Meeting always public
|
||||
self.status = Status.PUBLIC
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def __get_slug(self) -> str:
|
||||
slug = slugify(self.event_start.date()) + "-" + slugify("Fachschaftssitzung")
|
||||
|
||||
if Post.objects.filter(slug=slug).exists() and Post.objects.get(slug=slug).id != self.id:
|
||||
raise ValidationError("Es existiert bereits eine Sitzung mit demselben Datum.")
|
||||
|
||||
return slug
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
if not self.slug:
|
||||
self.slug = self.__get_slug()
|
||||
|
||||
|
||||
class FileUpload(models.Model):
|
||||
title = models.CharField(verbose_name="Titel", max_length=200)
|
||||
file_field = models.FileField(verbose_name="Dokument", upload_to="uploads/posts/files/")
|
||||
post = models.ForeignKey(Post, on_delete=models.CASCADE)
|
||||
|
||||
objects = models.Manager()
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.utils.text import slugify
|
||||
from taggit.managers import TaggableManager
|
||||
|
||||
from core.models import CustomFlatPage
|
||||
from documents.api import ep_create_new_pad, ep_get_html, ep_get_url, ep_pad_exists, ep_set_html
|
||||
|
||||
from .choices import PostType, Status
|
||||
from .managers import (
|
||||
AllEventManager,
|
||||
ArticleManager,
|
||||
EventManager,
|
||||
FetMeetingManager,
|
||||
NewsManager,
|
||||
PostManager,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def create_pad_for_post(slug, item="agenda"):
|
||||
logger.info("Pad-Type: %s", item)
|
||||
|
||||
pad_id = slug + "-" + item
|
||||
if not ep_create_new_pad(pad_id):
|
||||
return ""
|
||||
|
||||
# Set template into the newly created pad if it exists.
|
||||
if page := CustomFlatPage.objects.filter(title__iexact=item).first():
|
||||
ep_set_html(pad_id, page.content)
|
||||
logger.info("Template '%s' is set.", page.title)
|
||||
|
||||
return pad_id
|
||||
|
||||
|
||||
class Post(models.Model):
|
||||
# legacy id is for the posts from the old website
|
||||
legacy_id = models.IntegerField(null=True, blank=True)
|
||||
|
||||
title = models.CharField(verbose_name="Titel", max_length=200)
|
||||
subtitle = models.CharField(max_length=500, blank=True, default="")
|
||||
tags = TaggableManager(blank=True)
|
||||
|
||||
slug = models.SlugField(unique=True, blank=True)
|
||||
|
||||
body = models.TextField(blank=True, default="")
|
||||
image = models.ImageField(null=True, blank=True)
|
||||
|
||||
author = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True)
|
||||
public_date = models.DateField(
|
||||
verbose_name="Veröffentlichung",
|
||||
null=True,
|
||||
blank=True,
|
||||
default=timezone.now,
|
||||
)
|
||||
|
||||
post_type = models.CharField(max_length=1, choices=PostType.choices, editable=True)
|
||||
|
||||
status = models.CharField(
|
||||
verbose_name="Status", max_length=2, choices=Status.choices, default=Status.DRAFT
|
||||
)
|
||||
|
||||
# post is pinned at main page
|
||||
is_pinned = models.BooleanField(verbose_name="ANGEHEFTET", default=False)
|
||||
|
||||
# addional infos for events
|
||||
event_start = models.DateTimeField(verbose_name="Event Start", null=True, blank=True)
|
||||
event_end = models.DateTimeField(verbose_name="Event Ende", null=True, blank=True)
|
||||
event_place = models.CharField(max_length=200, blank=True, default="")
|
||||
|
||||
# protocol for fet meeting
|
||||
has_protocol = models.BooleanField(default=False)
|
||||
has_agenda = models.BooleanField(default=False)
|
||||
protocol_key = models.CharField(max_length=200, blank=True, default="")
|
||||
agenda_key = models.CharField(max_length=200, blank=True, default="")
|
||||
|
||||
# TimeStamps
|
||||
date_modified = models.DateTimeField(auto_now=True)
|
||||
date_created = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
# Managers
|
||||
objects = PostManager()
|
||||
articles = ArticleManager()
|
||||
|
||||
def __str__(self):
|
||||
return "Post ({}, {}): {}".format(
|
||||
self.slug,
|
||||
self.public_date.strftime("%d.%m.%Y"),
|
||||
self.title,
|
||||
)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# save the post with some defaults
|
||||
if not self.public_date:
|
||||
self.public_date = timezone.now().date()
|
||||
|
||||
if not self.slug:
|
||||
self.slug = slugify(self.public_date) + "-" + slugify(self.title)
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("posts:post", kwargs={"slug": self.slug})
|
||||
|
||||
# "#" for backward compatibility
|
||||
_possible_empty_key_value = ["#", "", None]
|
||||
|
||||
@property
|
||||
def agenda_html(self) -> str | None:
|
||||
if self.agenda_key in self._possible_empty_key_value:
|
||||
return None
|
||||
|
||||
return ep_get_html(self.agenda_key)
|
||||
|
||||
@agenda_html.setter
|
||||
def agenda_html(self, value: str) -> str | None:
|
||||
if self.agenda_key in self._possible_empty_key_value:
|
||||
self.create_agenda_key()
|
||||
|
||||
if not value or not self.agenda_key:
|
||||
return None
|
||||
|
||||
ep_set_html(self.agenda_key, value)
|
||||
logger.info("Set agenda etherpad '%s' for post '%s'.", self.agenda_key, self.slug)
|
||||
return value
|
||||
|
||||
@property
|
||||
def protocol_html(self) -> str | None:
|
||||
if self.protocol_key in self._possible_empty_key_value:
|
||||
return None
|
||||
|
||||
return ep_get_html(self.protocol_key)
|
||||
|
||||
@protocol_html.setter
|
||||
def protocol_html(self, value: str) -> str | None:
|
||||
if self.protocol_key in self._possible_empty_key_value:
|
||||
self.create_protocol_key()
|
||||
|
||||
if not value or not self.protocol_key:
|
||||
return None
|
||||
|
||||
ep_set_html(self.protocol_key, value)
|
||||
logger.info("Set protocol etherpad '%s' for post '%s'.", self.protocol_key, self.slug)
|
||||
return value
|
||||
|
||||
_agenda_filename = None
|
||||
_agenda_url = None
|
||||
|
||||
@property
|
||||
def agenda_url(self) -> str | None:
|
||||
if not self.has_agenda:
|
||||
self._agenda_url = None
|
||||
self._agenda_filename = None
|
||||
return self._agenda_url
|
||||
|
||||
if self._agenda_url:
|
||||
return self._agenda_url
|
||||
|
||||
if url := ep_get_url(self.agenda_key):
|
||||
self._agenda_url = url
|
||||
self._agenda_filename = self.slug + "-agenda.pdf"
|
||||
else:
|
||||
self._agenda_url = None
|
||||
self._agenda_filename = None
|
||||
|
||||
return self._agenda_url
|
||||
|
||||
@property
|
||||
def agenda_filename(self) -> str | None:
|
||||
# TODO: fix pdf render
|
||||
# if self._agenda_filename:
|
||||
# return self._agenda_filename
|
||||
|
||||
# if self.has_agenda and self.agenda_url:
|
||||
# return self.slug + "-agenda.pdf"
|
||||
|
||||
return None
|
||||
|
||||
_protocol_filename = None
|
||||
_protocol_url = None
|
||||
|
||||
@property
|
||||
def protocol_url(self) -> str | None:
|
||||
if not self.has_protocol:
|
||||
self._protocol_url = None
|
||||
self._protocol_filename = None
|
||||
return self._protocol_url
|
||||
|
||||
if self._protocol_url:
|
||||
return self._protocol_url
|
||||
|
||||
if url := ep_get_url(self.protocol_key):
|
||||
self._protocol_url = url
|
||||
self._protocol_filename = self.slug + "-protokoll.pdf"
|
||||
else:
|
||||
self._protocol_url = None
|
||||
self._protocol_filename = None
|
||||
|
||||
return self._protocol_url
|
||||
|
||||
@property
|
||||
def protocol_filename(self) -> str | None:
|
||||
# TODO: fix pdf render
|
||||
# if self._protocol_filename:
|
||||
# return self._protocol_filename
|
||||
|
||||
# if self.has_protocol and self.protocol_url:
|
||||
# return self.slug + "-protokoll.pdf"
|
||||
|
||||
return None
|
||||
|
||||
def create_agenda_key(self) -> None:
|
||||
"""
|
||||
Create a Etherpad Id for the Pad associated to this post.
|
||||
Create the pad if it doesn't exist.
|
||||
"""
|
||||
if self.slug:
|
||||
self.agenda_key = create_pad_for_post(self.slug, "agenda")
|
||||
|
||||
def create_protocol_key(self) -> None:
|
||||
"""
|
||||
Create a Etherpad Id for the Pad associated to this post.
|
||||
Create the pad if it doesn't exist.
|
||||
"""
|
||||
if self.slug:
|
||||
self.protocol_key = create_pad_for_post(self.slug, "protocol")
|
||||
|
||||
def get_model_name(self):
|
||||
return self._meta.model_name
|
||||
|
||||
@property
|
||||
def three_tag_names(self):
|
||||
return self.tags.names()[:3]
|
||||
|
||||
@property
|
||||
def tag_names(self):
|
||||
return [t for t in self.tags.names()]
|
||||
|
||||
@property
|
||||
def imageurl(self) -> str:
|
||||
return (
|
||||
self.image.url if self.image else settings.STATIC_URL + "img/FET-Logo-2014-quadrat.png"
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
if self.event_end and self.event_end < self.event_start:
|
||||
msg = "Das Ende des Events liegt vor dem Beginn."
|
||||
raise ValidationError(msg, code="invalid_event_end")
|
||||
if self.event_start and self.post_type not in ["E", "F"]:
|
||||
msg = "Für diesen Post Typ ist kein Event Start zulässig."
|
||||
raise ValidationError(msg, code="invalid_event_start")
|
||||
|
||||
@property
|
||||
def published(self):
|
||||
return self.status == Status.PUBLIC
|
||||
|
||||
|
||||
class News(Post):
|
||||
objects = NewsManager()
|
||||
|
||||
class Meta:
|
||||
proxy = True
|
||||
|
||||
verbose_name = "News"
|
||||
verbose_name_plural = "News"
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.post_type:
|
||||
self.post_type = "N"
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
|
||||
class Event(Post):
|
||||
only_events = EventManager()
|
||||
all_events = AllEventManager()
|
||||
|
||||
class Meta:
|
||||
proxy = True
|
||||
|
||||
verbose_name = "Event"
|
||||
verbose_name_plural = "Events"
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.post_type = "E"
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.post_type:
|
||||
self.post_type = "E"
|
||||
if not self.event_end:
|
||||
self.event_end = self.event_start + timedelta(hours=2)
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
if not self.event_start:
|
||||
msg = "Das Datum des Events fehlt."
|
||||
raise ValidationError(msg, code="missing_event_start")
|
||||
|
||||
|
||||
class FetMeeting(Event):
|
||||
objects = FetMeetingManager()
|
||||
|
||||
class Meta:
|
||||
proxy = True
|
||||
|
||||
verbose_name = "Fet Sitzung"
|
||||
verbose_name_plural = "Fet Sitzungen"
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.post_type = "F"
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.title = "Fachschaftssitzung"
|
||||
if not self.slug:
|
||||
self.slug = self.__get_slug()
|
||||
|
||||
if not ep_pad_exists(self.agenda_key) or self.agenda_key in self._possible_empty_key_value:
|
||||
self.create_agenda_key()
|
||||
if self.agenda_key:
|
||||
self.has_agenda = True
|
||||
|
||||
if (
|
||||
not ep_pad_exists(self.protocol_key)
|
||||
or self.protocol_key in self._possible_empty_key_value
|
||||
):
|
||||
self.create_protocol_key()
|
||||
if self.protocol_key:
|
||||
self.has_protocol = True
|
||||
|
||||
if not self.post_type:
|
||||
self.post_type = "F"
|
||||
|
||||
if not self.event_place:
|
||||
self.event_place = "FET"
|
||||
|
||||
# make duration 2 hours if not specified otherwise
|
||||
if not self.event_end:
|
||||
self.event_end = self.event_start + timedelta(hours=2)
|
||||
|
||||
# set FET Meeting always public
|
||||
self.status = Status.PUBLIC
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def __get_slug(self) -> str:
|
||||
slug = slugify(self.event_start.date()) + "-" + slugify("Fachschaftssitzung")
|
||||
|
||||
if Post.objects.filter(slug=slug).exists() and Post.objects.get(slug=slug).id != self.id:
|
||||
msg = "Es existiert bereits eine Sitzung mit demselben Datum."
|
||||
logger.error(
|
||||
"A fet meeting with same date (slug: %(slug)s) is already existing.",
|
||||
extra={"slug": slug},
|
||||
)
|
||||
raise ValidationError(msg, code="duplicate_fet_meeting")
|
||||
|
||||
return slug
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
if not self.slug:
|
||||
self.slug = self.__get_slug()
|
||||
|
||||
|
||||
class FileUpload(models.Model):
|
||||
title = models.CharField(verbose_name="Titel", max_length=200)
|
||||
file_field = models.FileField(verbose_name="Dokument", upload_to="uploads/posts/files/")
|
||||
post = models.ForeignKey(Post, on_delete=models.CASCADE)
|
||||
|
||||
objects = models.Manager()
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
@@ -1,36 +1,36 @@
|
||||
from haystack import indexes
|
||||
from html2text import html2text
|
||||
|
||||
from .models import Post
|
||||
|
||||
|
||||
class PostIndex(indexes.SearchIndex, indexes.Indexable):
|
||||
text = indexes.CharField(document=True)
|
||||
title = indexes.EdgeNgramField(model_attr="title")
|
||||
body = indexes.EdgeNgramField(model_attr="body", null=True)
|
||||
status = indexes.CharField(model_attr="status")
|
||||
date = indexes.DateField()
|
||||
agenda = indexes.EdgeNgramField()
|
||||
protocol = indexes.EdgeNgramField()
|
||||
|
||||
def get_model(self):
|
||||
return Post
|
||||
|
||||
def index_queryset(self, using=None):
|
||||
return self.get_model().objects.date_sorted(public=False)
|
||||
|
||||
def prepare_date(self, obj):
|
||||
if obj.post_type == "N":
|
||||
return obj.public_date
|
||||
elif obj.post_type == "E" or obj.post_type == "F":
|
||||
return obj.event_start.date()
|
||||
|
||||
def prepare_agenda(self, obj):
|
||||
if obj.has_agenda and obj.agenda_html:
|
||||
return html2text(obj.agenda_html)
|
||||
return None
|
||||
|
||||
def prepare_protocol(self, obj):
|
||||
if obj.has_protocol and obj.protocol_html:
|
||||
return html2text(obj.protocol_html)
|
||||
return None
|
||||
from haystack import indexes
|
||||
from html2text import html2text
|
||||
|
||||
from .models import Post
|
||||
|
||||
|
||||
class PostIndex(indexes.SearchIndex, indexes.Indexable):
|
||||
text = indexes.CharField(document=True)
|
||||
title = indexes.EdgeNgramField(model_attr="title")
|
||||
body = indexes.EdgeNgramField(model_attr="body", null=True)
|
||||
status = indexes.CharField(model_attr="status")
|
||||
date = indexes.DateField()
|
||||
agenda = indexes.EdgeNgramField()
|
||||
protocol = indexes.EdgeNgramField()
|
||||
|
||||
def get_model(self):
|
||||
return Post
|
||||
|
||||
def index_queryset(self, using=None):
|
||||
return self.get_model().objects.date_sorted(public=False)
|
||||
|
||||
def prepare_date(self, obj):
|
||||
if obj.post_type == "E" or obj.post_type == "F":
|
||||
return obj.event_start.date()
|
||||
|
||||
return obj.public_date # obj.post_type == "N"
|
||||
|
||||
def prepare_agenda(self, obj):
|
||||
if obj.has_agenda and obj.agenda_html:
|
||||
return html2text(obj.agenda_html)
|
||||
return None
|
||||
|
||||
def prepare_protocol(self, obj):
|
||||
if obj.has_protocol and obj.protocol_html:
|
||||
return html2text(obj.protocol_html)
|
||||
return None
|
||||
|
||||
@@ -117,15 +117,17 @@ class PostDetailView(DetailView):
|
||||
if not obj.published:
|
||||
related_posts.remove(obj)
|
||||
|
||||
context = {
|
||||
"post": self.object,
|
||||
"files": files,
|
||||
"author": author,
|
||||
"author_image": author_image,
|
||||
"next": self.post_next(),
|
||||
"prev": self.post_prev(),
|
||||
"related_posts": related_posts[:4],
|
||||
}
|
||||
context.update(
|
||||
{
|
||||
"post": self.object,
|
||||
"files": files,
|
||||
"author": author,
|
||||
"author_image": author_image,
|
||||
"next": self.post_next(),
|
||||
"prev": self.post_prev(),
|
||||
"related_posts": related_posts[:4],
|
||||
}
|
||||
)
|
||||
|
||||
return context
|
||||
|
||||
@@ -309,7 +311,8 @@ def show_pdf(request, html, filename):
|
||||
html = html[:idx] + rendered + html[idx:]
|
||||
|
||||
if not (pdf := render_to_pdf(html)):
|
||||
raise Http404("can't create pdf file.")
|
||||
msg = "PDF konnte nicht erstellt werden."
|
||||
raise Http404(msg)
|
||||
|
||||
response = HttpResponse(pdf, content_type="application/pdf")
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
[project]
|
||||
name = "fet-homepage"
|
||||
version = "2.1.0"
|
||||
version = "2.1.1"
|
||||
description = "Homepage for Fachschaft Elektrotechnik"
|
||||
requires-python = ">=3.13"
|
||||
dependencies = [
|
||||
"django",
|
||||
"django<6.0", # django 6 has to be tested
|
||||
"django-ckeditor<6.7.3",
|
||||
"django-environ",
|
||||
"django-filter",
|
||||
@@ -38,7 +38,7 @@ dev = [
|
||||
line-length = 100
|
||||
indent-width = 4
|
||||
target-version = "py313"
|
||||
exclude = ["tests", "migrations", "search", "tests.py"]
|
||||
exclude = ["tests", "migrations", "search", "manage.py", "tests.py"]
|
||||
|
||||
[tool.ruff.format]
|
||||
docstring-code-format = true
|
||||
|
||||
@@ -1,124 +1,121 @@
|
||||
from django.contrib import admin, messages
|
||||
from django.http import HttpResponseRedirect
|
||||
|
||||
from .forms import RentalAdminForm, RentalItemAdminForm
|
||||
from .models import Rental, RentalItem
|
||||
from .utils import generate_rental_pdf
|
||||
|
||||
|
||||
@admin.register(Rental)
|
||||
class RentalAdmin(admin.ModelAdmin):
|
||||
form = RentalAdminForm
|
||||
model = Rental
|
||||
|
||||
list_display = [
|
||||
"id",
|
||||
"firstname",
|
||||
"surname",
|
||||
"status",
|
||||
"total_disposit",
|
||||
"date_start",
|
||||
"date_end",
|
||||
]
|
||||
ordering = ["-id"]
|
||||
|
||||
readonly_fields = ["total_disposit"]
|
||||
|
||||
fieldsets = (
|
||||
(
|
||||
"Persönliche Daten",
|
||||
{
|
||||
"fields": (
|
||||
("firstname", "surname"),
|
||||
("organization", "matriculation_number"),
|
||||
("email", "phone"),
|
||||
),
|
||||
},
|
||||
),
|
||||
(
|
||||
"Verleih",
|
||||
{
|
||||
"fields": (
|
||||
("date_start", "date_end"),
|
||||
"reason",
|
||||
"rentalitems",
|
||||
"total_disposit",
|
||||
),
|
||||
},
|
||||
),
|
||||
(
|
||||
"Sonstiges",
|
||||
{
|
||||
"fields": (
|
||||
"comment",
|
||||
"file_field",
|
||||
"status",
|
||||
),
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
def add_view(self, request, form_url="", extra_context=None):
|
||||
extra_context = extra_context or {}
|
||||
extra_context["help_text"] = "Fette Schriften sind Pflichtfelder."
|
||||
return super().add_view(request, form_url, extra_context=extra_context)
|
||||
|
||||
def change_view(self, request, object_id, form_url="", extra_context=None):
|
||||
extra_context = extra_context or {}
|
||||
extra_context["help_text"] = "Fette Schriften sind Pflichtfelder."
|
||||
extra_context["generate_rental_pdf"] = True
|
||||
return super().change_view(request, object_id, form_url, extra_context=extra_context)
|
||||
|
||||
def response_change(self, request, obj):
|
||||
if "_generate_rental_pdf" in request.POST:
|
||||
if generate_rental_pdf(obj):
|
||||
self.message_user(
|
||||
request,
|
||||
"Neues Verleihformular wurde generiert.",
|
||||
messages.SUCCESS,
|
||||
)
|
||||
else:
|
||||
self.message_user(
|
||||
request,
|
||||
(
|
||||
"Das PDF-Dokument konnte nicht generiert werden, da der Status nicht auf "
|
||||
"'Verleih genehmigt' gesetzt ist."
|
||||
),
|
||||
messages.WARNING,
|
||||
)
|
||||
return HttpResponseRedirect(".")
|
||||
return super().response_change(request, obj)
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
obj.author = request.user
|
||||
super().save_model(request, obj, form, change)
|
||||
|
||||
@admin.display(description="Kaution (EUR)")
|
||||
def total_disposit(self, obj):
|
||||
total_disposit = 0
|
||||
for elem in obj.rentalitems.all():
|
||||
total_disposit += elem.deposit
|
||||
|
||||
return f"{total_disposit}"
|
||||
|
||||
|
||||
@admin.register(RentalItem)
|
||||
class RentalItemAdmin(admin.ModelAdmin):
|
||||
form = RentalItemAdminForm
|
||||
model = RentalItem
|
||||
|
||||
ordering = ["name"]
|
||||
|
||||
def add_view(self, request, form_url="", extra_context=None):
|
||||
extra_context = extra_context or {}
|
||||
extra_context["help_text"] = "Fette Schriften sind Pflichtfelder."
|
||||
return super().add_view(request, form_url, extra_context=extra_context)
|
||||
|
||||
def change_view(self, request, object_id, form_url="", extra_context=None):
|
||||
extra_context = extra_context or {}
|
||||
extra_context["help_text"] = "Fette Schriften sind Pflichtfelder."
|
||||
return super().change_view(request, object_id, form_url, extra_context=extra_context)
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
obj.author = request.user
|
||||
super().save_model(request, obj, form, change)
|
||||
from django.contrib import admin, messages
|
||||
from django.http import HttpResponseRedirect
|
||||
|
||||
from .forms import RentalAdminForm, RentalItemAdminForm
|
||||
from .models import Rental, RentalItem
|
||||
from .utils import generate_rental_pdf
|
||||
|
||||
|
||||
@admin.register(Rental)
|
||||
class RentalAdmin(admin.ModelAdmin):
|
||||
form = RentalAdminForm
|
||||
model = Rental
|
||||
|
||||
list_display = [
|
||||
"id",
|
||||
"firstname",
|
||||
"surname",
|
||||
"status",
|
||||
"total_disposit",
|
||||
"date_start",
|
||||
"date_end",
|
||||
]
|
||||
ordering = ["-id"]
|
||||
|
||||
readonly_fields = ["total_disposit"]
|
||||
|
||||
fieldsets = (
|
||||
(
|
||||
"Persönliche Daten",
|
||||
{
|
||||
"fields": (
|
||||
("firstname", "surname"),
|
||||
("organization", "matriculation_number"),
|
||||
("email", "phone"),
|
||||
),
|
||||
},
|
||||
),
|
||||
(
|
||||
"Verleih",
|
||||
{
|
||||
"fields": (
|
||||
("date_start", "date_end"),
|
||||
"reason",
|
||||
"rentalitems",
|
||||
("total_disposit", "intern"),
|
||||
),
|
||||
},
|
||||
),
|
||||
(
|
||||
"Sonstiges",
|
||||
{
|
||||
"fields": (
|
||||
"comment",
|
||||
"file_field",
|
||||
"status",
|
||||
),
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
def add_view(self, request, form_url="", extra_context=None):
|
||||
extra_context = extra_context or {}
|
||||
extra_context["help_text"] = "Fette Schriften sind Pflichtfelder."
|
||||
return super().add_view(request, form_url, extra_context=extra_context)
|
||||
|
||||
def change_view(self, request, object_id, form_url="", extra_context=None):
|
||||
extra_context = extra_context or {}
|
||||
extra_context["help_text"] = "Fette Schriften sind Pflichtfelder."
|
||||
extra_context["generate_rental_pdf"] = True
|
||||
return super().change_view(request, object_id, form_url, extra_context=extra_context)
|
||||
|
||||
def response_change(self, request, obj):
|
||||
if "_generate_rental_pdf" in request.POST:
|
||||
if generate_rental_pdf(obj):
|
||||
self.message_user(
|
||||
request,
|
||||
"Neues Verleihformular wurde generiert.",
|
||||
messages.SUCCESS,
|
||||
)
|
||||
else:
|
||||
self.message_user(
|
||||
request,
|
||||
(
|
||||
"Das PDF-Dokument konnte nicht generiert werden, da der Status nicht auf "
|
||||
"'Verleih genehmigt' gesetzt ist."
|
||||
),
|
||||
messages.WARNING,
|
||||
)
|
||||
return HttpResponseRedirect(".")
|
||||
return super().response_change(request, obj)
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
obj.author = request.user
|
||||
super().save_model(request, obj, form, change)
|
||||
|
||||
@admin.display(description="Kaution insgesamt")
|
||||
def total_disposit(self, obj):
|
||||
total_disposit = obj.calc_total_deposit()
|
||||
return f"{total_disposit} €"
|
||||
|
||||
|
||||
@admin.register(RentalItem)
|
||||
class RentalItemAdmin(admin.ModelAdmin):
|
||||
form = RentalItemAdminForm
|
||||
model = RentalItem
|
||||
|
||||
ordering = ["name"]
|
||||
|
||||
def add_view(self, request, form_url="", extra_context=None):
|
||||
extra_context = extra_context or {}
|
||||
extra_context["help_text"] = "Fette Schriften sind Pflichtfelder."
|
||||
return super().add_view(request, form_url, extra_context=extra_context)
|
||||
|
||||
def change_view(self, request, object_id, form_url="", extra_context=None):
|
||||
extra_context = extra_context or {}
|
||||
extra_context["help_text"] = "Fette Schriften sind Pflichtfelder."
|
||||
return super().change_view(request, object_id, form_url, extra_context=extra_context)
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
obj.author = request.user
|
||||
super().save_model(request, obj, form, change)
|
||||
|
||||
@@ -1,58 +1,65 @@
|
||||
from django import forms
|
||||
from django.forms import DateInput
|
||||
|
||||
from .models import Rental, RentalItem
|
||||
|
||||
|
||||
class DateInput(DateInput):
|
||||
input_type = "date"
|
||||
|
||||
|
||||
class RentalCreateForm(forms.ModelForm):
|
||||
# Conformation
|
||||
conformation = forms.BooleanField(
|
||||
required=True,
|
||||
label=("Ich habe die Verleihregeln gelesen und akzeptiere sie."),
|
||||
initial=False,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Rental
|
||||
|
||||
fields = [
|
||||
"firstname",
|
||||
"surname",
|
||||
"matriculation_number",
|
||||
"email",
|
||||
"phone",
|
||||
"organization",
|
||||
"date_start",
|
||||
"date_end",
|
||||
"reason",
|
||||
"comment",
|
||||
"rentalitems",
|
||||
]
|
||||
|
||||
widgets = {
|
||||
"date_start": DateInput(format=("%Y-%m-%d")),
|
||||
"date_end": DateInput(format=("%Y-%m-%d")),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs) # to get the self.fields set
|
||||
|
||||
self.fields["firstname"].autofocus = True
|
||||
|
||||
|
||||
class RentalAdminForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Rental
|
||||
fields = "__all__"
|
||||
|
||||
widgets = {"rentalitems": forms.CheckboxSelectMultiple()}
|
||||
|
||||
|
||||
class RentalItemAdminForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = RentalItem
|
||||
fields = "__all__"
|
||||
from django import forms
|
||||
from django.forms import DateInput
|
||||
|
||||
from .models import Rental, RentalItem
|
||||
|
||||
|
||||
class DateInput(DateInput):
|
||||
input_type = "date"
|
||||
|
||||
|
||||
class RentalCreateForm(forms.ModelForm):
|
||||
# Conformation
|
||||
conformation = forms.BooleanField(
|
||||
required=True,
|
||||
label=("Ich habe die Verleihregeln gelesen und akzeptiere sie."),
|
||||
initial=False,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Rental
|
||||
|
||||
fields = [
|
||||
"firstname",
|
||||
"surname",
|
||||
"matriculation_number",
|
||||
"email",
|
||||
"phone",
|
||||
"organization",
|
||||
"date_start",
|
||||
"date_end",
|
||||
"reason",
|
||||
"comment",
|
||||
"rentalitems",
|
||||
]
|
||||
|
||||
widgets = {
|
||||
"date_start": DateInput(format=("%Y-%m-%d")),
|
||||
"date_end": DateInput(format=("%Y-%m-%d")),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs) # to get the self.fields set
|
||||
|
||||
self.fields["firstname"].autofocus = True
|
||||
|
||||
|
||||
class RentalAdminForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Rental
|
||||
fields = "__all__"
|
||||
|
||||
widgets = {"rentalitems": forms.CheckboxSelectMultiple()}
|
||||
|
||||
help_texts = {
|
||||
"status": (
|
||||
"Wird der Status auf 'Verleih genehmigt' oder 'Verleih abgelehnt' gesetzt, wird "
|
||||
"eine E-Mail gesendet."
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
class RentalItemAdminForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = RentalItem
|
||||
fields = "__all__"
|
||||
|
||||
@@ -1,53 +1,102 @@
|
||||
import logging
|
||||
|
||||
from django.core.mail import EmailMessage
|
||||
|
||||
RENTAL_EMAIL = "verleih@fet.at"
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def send_mail_approved(obj):
|
||||
subject = f"FET-Verleih #{obj.id}: {obj.get_status_display()}"
|
||||
|
||||
total_deposit = 0
|
||||
for rentalitem in obj.rentalitems.all():
|
||||
total_deposit += rentalitem.deposit
|
||||
|
||||
message = (
|
||||
f"Hallo {obj.firstname},\n"
|
||||
f"deine Verleihanfrage mit der Nummer #{obj.id} wurde erfolgreich genehmigt. Die "
|
||||
f"Gegenstände können am {obj.date_start.strftime('%d.%m.%Y')} während der Beratungszeit "
|
||||
"(Montag - Donnerstag: 09:00 - 14:00, Freitag: 09:00 - 12:00) abgeholt werden. Bitte bring "
|
||||
f"den Gesamtpfand in Höhe von {total_deposit} € in bar mit.\n"
|
||||
"Liebe Grüße,\n"
|
||||
"das Verleih-Team"
|
||||
)
|
||||
|
||||
email = EmailMessage(
|
||||
subject, message, from_email=RENTAL_EMAIL, to=[obj.email], cc=[RENTAL_EMAIL]
|
||||
)
|
||||
|
||||
try:
|
||||
email.send()
|
||||
except Exception as exc:
|
||||
logger.error("Failed to send approval email for rental #%s. Error: %s", obj.id, exc)
|
||||
|
||||
|
||||
def send_mail_rejected(obj):
|
||||
subject = f"FET-Verleih #{obj.id}: {obj.get_status_display()}"
|
||||
|
||||
message = (
|
||||
f"Hallo {obj.firstname},\n"
|
||||
f"deine Verleihanfrage mit der Nummer #{obj.id} wurde abgelehnt.\n"
|
||||
"Liebe Grüße,\n"
|
||||
"das Verleih-Team"
|
||||
)
|
||||
|
||||
email = EmailMessage(
|
||||
subject, message, from_email=RENTAL_EMAIL, to=[obj.email], cc=[RENTAL_EMAIL]
|
||||
)
|
||||
|
||||
try:
|
||||
email.send()
|
||||
except Exception as exc:
|
||||
logger.error("Failed to send rejection email for rental #%s. Error: %s", obj.id, exc)
|
||||
import logging
|
||||
from urllib.parse import urljoin
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.sites.models import Site
|
||||
from django.core.mail import EmailMessage
|
||||
|
||||
RENTAL_EMAIL = settings.EMAIL_HOST_USER
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def send_mail_approved(obj):
|
||||
subject = f"FET-Verleih: {obj.get_status_display()}"
|
||||
total_deposit = obj.calc_total_deposit()
|
||||
|
||||
# Build URL to contact page
|
||||
site = Site.objects.get_current()
|
||||
contact_link = urljoin(f"https://{site.domain}", "/kontakt/")
|
||||
|
||||
# Build email message
|
||||
message = (
|
||||
f"Hallo {obj.firstname},\n\ndeine Verleihanfrage wurde erfolgreich genehmigt. Für den "
|
||||
f"Zeitraum vom {obj.date_start.strftime('%d.%m.%Y')} bis einschließlich "
|
||||
f"{obj.date_end.strftime('%d.%m.%Y')} stehen dir folgende Gegenstände zur Verfügung:\n"
|
||||
)
|
||||
|
||||
for item in obj.rentalitems.all():
|
||||
message += f"- {item.name}\n"
|
||||
|
||||
message += (
|
||||
f"\nDie Abholung ist am {obj.date_start.strftime('%d.%m.%Y')} während der Beratungszeit "
|
||||
f"möglich: {contact_link}\n"
|
||||
)
|
||||
|
||||
if total_deposit > 0:
|
||||
message += (
|
||||
f"Bitte bring zur Abholung den Gesamtpfand in Höhe von {total_deposit} € in bar mit.\n"
|
||||
)
|
||||
|
||||
message += "\nLiebe Grüße,\ndas Verleih-Team"
|
||||
|
||||
# Create email
|
||||
email = EmailMessage(
|
||||
subject, message, from_email=RENTAL_EMAIL, to=[obj.email], cc=[RENTAL_EMAIL]
|
||||
)
|
||||
|
||||
try:
|
||||
email.send()
|
||||
except Exception as exc:
|
||||
logger.info("Failed to send approval email for rental #%s. Error: %s", obj.id, exc)
|
||||
|
||||
|
||||
def send_mail_rejected(obj):
|
||||
subject = f"FET-Verleih: {obj.get_status_display()}"
|
||||
|
||||
# Build email message
|
||||
message = (
|
||||
f"Hallo {obj.firstname},\n\nleider müssen wir dir mitteilen, dass deine Verleihanfrage für "
|
||||
f"den Zeitraum vom {obj.date_start.strftime('%d.%m.%Y')} bis einschließlich "
|
||||
f"{obj.date_end.strftime('%d.%m.%Y')} nicht genehmigt werden konnte. Für diesen Zeitraum "
|
||||
"waren folgende Gegenstände angefragt:\n"
|
||||
)
|
||||
|
||||
for item in obj.rentalitems.all():
|
||||
message += f"- {item.name}\n"
|
||||
|
||||
message += (
|
||||
"\nVielen Dank für dein Verständnis. Bei Fragen oder für alternative Termine kannst du "
|
||||
"dich gerne bei uns melden.\n\nLiebe Grüße,\ndas Verleih-Team"
|
||||
)
|
||||
|
||||
# Create email
|
||||
email = EmailMessage(
|
||||
subject, message, from_email=RENTAL_EMAIL, to=[obj.email], cc=[RENTAL_EMAIL]
|
||||
)
|
||||
|
||||
try:
|
||||
email.send()
|
||||
except Exception as exc:
|
||||
logger.info("Failed to send rejection email for rental #%s. Error: %s", obj.id, exc)
|
||||
|
||||
|
||||
def send_mail_returned(obj):
|
||||
subject = f"FET-Verleih: {obj.get_status_display()}"
|
||||
|
||||
# Build email message
|
||||
message = (
|
||||
f"Hallo {obj.firstname},\n\n"
|
||||
"deine Verleihgegenstände wurden erfolgreich zurückgegeben.\n\n"
|
||||
"Liebe Grüße,\n"
|
||||
"das Verleih-Team"
|
||||
)
|
||||
|
||||
# Create email
|
||||
email = EmailMessage(
|
||||
subject, message, from_email=RENTAL_EMAIL, to=[obj.email], cc=[RENTAL_EMAIL]
|
||||
)
|
||||
|
||||
try:
|
||||
email.send()
|
||||
except Exception as exc:
|
||||
logger.info("Failed to send returned email for rental #%s. Error: %s", obj.id, exc)
|
||||
|
||||
6
fet2020/rental/managers.py
Normal file
6
fet2020/rental/managers.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from django.db import models
|
||||
|
||||
|
||||
class RentalItemsManager(models.Manager):
|
||||
def get_queryset(self):
|
||||
return super().get_queryset().order_by("name")
|
||||
55
fet2020/rental/migrations/0001_initial.py
Normal file
55
fet2020/rental/migrations/0001_initial.py
Normal file
@@ -0,0 +1,55 @@
|
||||
# Generated by Django 5.2.7 on 2025-10-30 12:05
|
||||
|
||||
import django.core.validators
|
||||
import rental.validators
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='RentalItem',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=128, verbose_name='Name')),
|
||||
('description', models.CharField(blank=True, default='', max_length=128, verbose_name='Beschreibung')),
|
||||
('deposit', models.DecimalField(decimal_places=2, default=0, max_digits=6, verbose_name='Kaution (EUR)')),
|
||||
('image', models.ImageField(blank=True, null=True, upload_to='rentalitems', verbose_name='Bild')),
|
||||
('induction', models.BooleanField(default=False, verbose_name='Einschulung notwendig')),
|
||||
('location', models.CharField(blank=True, default='', max_length=128, verbose_name='Standort')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Verleihgegenstand',
|
||||
'verbose_name_plural': 'Verleihgegenstände',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Rental',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('firstname', models.CharField(max_length=128, verbose_name='Vorname')),
|
||||
('surname', models.CharField(max_length=128, verbose_name='Nachname')),
|
||||
('matriculation_number', models.CharField(max_length=8, verbose_name='Matrikelnummer')),
|
||||
('email', models.EmailField(max_length=254, verbose_name='E-Mail')),
|
||||
('phone', models.CharField(max_length=32, validators=[rental.validators.PhoneNumberValidator()], verbose_name='Telefonnummer')),
|
||||
('organization', models.CharField(max_length=128, verbose_name='Organisation')),
|
||||
('date_start', models.DateField(verbose_name='Abholdatum')),
|
||||
('date_end', models.DateField(verbose_name='Rückgabedatum')),
|
||||
('reason', models.TextField(max_length=500, verbose_name='Grund der Ausleihe')),
|
||||
('comment', models.TextField(blank=True, default='', max_length=500, verbose_name='Kommentar')),
|
||||
('file_field', models.FileField(blank=True, null=True, upload_to='uploads/rental/rental/', validators=[django.core.validators.FileExtensionValidator(['pdf'])], verbose_name='Verleihformular')),
|
||||
('status', models.CharField(choices=[('S', 'Eingereicht'), ('A', 'Verleih genehmigt'), ('J', 'Verleih abgelehnt'), ('I', 'Verleihgegenstände ausgegeben'), ('R', 'Verleihgegenstände zurückgegeben')], default='S', max_length=1, verbose_name='Status')),
|
||||
('rentalitems', models.ManyToManyField(to='rental.rentalitem', verbose_name='Verleihgegenstände')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Verleih',
|
||||
'verbose_name_plural': 'Verleih',
|
||||
},
|
||||
),
|
||||
]
|
||||
18
fet2020/rental/migrations/0002_rental_intern.py
Normal file
18
fet2020/rental/migrations/0002_rental_intern.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.2.7 on 2025-10-30 14:40
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('rental', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='rental',
|
||||
name='intern',
|
||||
field=models.BooleanField(default=False, verbose_name='Interner Verleih'),
|
||||
),
|
||||
]
|
||||
0
fet2020/rental/migrations/__init__.py
Normal file
0
fet2020/rental/migrations/__init__.py
Normal file
@@ -1,8 +1,9 @@
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import FileExtensionValidator
|
||||
from django.db import models
|
||||
from django.forms import ValidationError
|
||||
|
||||
from .mails import send_mail_approved, send_mail_rejected
|
||||
from .mails import send_mail_approved, send_mail_rejected, send_mail_returned
|
||||
from .managers import RentalItemsManager
|
||||
from .validators import PhoneNumberValidator
|
||||
|
||||
|
||||
@@ -22,6 +23,8 @@ class RentalItem(models.Model):
|
||||
|
||||
location = models.CharField(verbose_name="Standort", max_length=128, blank=True, default="")
|
||||
|
||||
objects = RentalItemsManager()
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Verleihgegenstand"
|
||||
verbose_name_plural = "Verleihgegenstände"
|
||||
@@ -41,6 +44,7 @@ class Rental(models.Model):
|
||||
)
|
||||
|
||||
organization = models.CharField(verbose_name="Organisation", max_length=128)
|
||||
intern = models.BooleanField(verbose_name="Interner Verleih", default=False)
|
||||
|
||||
date_start = models.DateField(verbose_name="Abholdatum")
|
||||
date_end = models.DateField(verbose_name="Rückgabedatum")
|
||||
@@ -100,10 +104,26 @@ class Rental(models.Model):
|
||||
and self.status == self.Status.REJECTED
|
||||
):
|
||||
send_mail_rejected(self)
|
||||
elif (
|
||||
pre_obj
|
||||
and pre_obj.status != self.Status.RETURNED
|
||||
and self.status == self.Status.RETURNED
|
||||
):
|
||||
send_mail_returned(self)
|
||||
|
||||
def clean(self):
|
||||
if not self.date_end:
|
||||
self.date_end = self.date_start
|
||||
|
||||
if self.date_start > self.date_end:
|
||||
raise ValidationError("Das Abholdatum muss vor dem Rückgabedatum liegen.")
|
||||
msg = "Das Abholdatum muss vor dem Rückgabedatum liegen."
|
||||
raise ValidationError(msg, code="invalid_date_range")
|
||||
|
||||
def calc_total_deposit(self) -> int:
|
||||
total_deposit = 0
|
||||
|
||||
if not self.intern:
|
||||
for item in self.rentalitems.all():
|
||||
total_deposit += item.deposit
|
||||
|
||||
return total_deposit
|
||||
|
||||
Binary file not shown.
@@ -10,7 +10,7 @@ urlpatterns = [
|
||||
path("overview/", RentalListView.as_view(), name="index"),
|
||||
path("request-rental/", RentalCreateView.as_view(), name="rental_create"),
|
||||
path(
|
||||
"request-rental/<int:pk>/done/",
|
||||
"request-rental/done/",
|
||||
RentalCreateDoneView.as_view(),
|
||||
name="rental_create_done",
|
||||
),
|
||||
|
||||
@@ -1,66 +1,61 @@
|
||||
import io
|
||||
from pathlib import Path
|
||||
|
||||
from django.core.files import File
|
||||
from pypdf import PdfReader, PdfWriter
|
||||
|
||||
from .models import Rental
|
||||
|
||||
|
||||
def generate_rental_pdf(rental: Rental) -> bool:
|
||||
if not rental or rental.status != Rental.Status.APPROVED:
|
||||
return False
|
||||
|
||||
# Get data for pdf
|
||||
data = {}
|
||||
data.update(
|
||||
{
|
||||
"Vorname": rental.firstname,
|
||||
"Nachname": rental.surname,
|
||||
"Orga": rental.organization,
|
||||
"Matrikelnummer": rental.matriculation_number,
|
||||
"E-Mail": rental.email,
|
||||
"Telefonnummer": rental.phone,
|
||||
# Change to the correct date format
|
||||
"Abholdatum": str(rental.date_start.strftime("%d.%m.%Y")),
|
||||
"Rückgabedatum": str(rental.date_end.strftime("%d.%m.%Y")),
|
||||
},
|
||||
)
|
||||
|
||||
total_deposit = 0
|
||||
|
||||
for i, item in enumerate(rental.rentalitems.all(), start=1):
|
||||
total_deposit += item.deposit
|
||||
data.update(
|
||||
{
|
||||
f"Produkt Row{i}": item.name,
|
||||
f"Menge Row{i}": "1",
|
||||
f"Kaution Row{i}": item.deposit,
|
||||
},
|
||||
)
|
||||
|
||||
data.update(
|
||||
{
|
||||
"Gesamtkaution": total_deposit,
|
||||
},
|
||||
)
|
||||
|
||||
# Write data in pdf
|
||||
pdf_path = Path(Path(__file__).parent) / "static/rental/Verleihformular.pdf"
|
||||
reader = PdfReader(pdf_path)
|
||||
writer = PdfWriter()
|
||||
writer.append(reader)
|
||||
|
||||
writer.update_page_form_field_values(
|
||||
writer.pages[0],
|
||||
data,
|
||||
)
|
||||
|
||||
with io.BytesIO() as bytes_stream:
|
||||
writer.write(bytes_stream)
|
||||
|
||||
# Save pdf in rental
|
||||
rental_name = f"Verleihformular-{str(rental.pk).zfill(4)}.pdf"
|
||||
rental.file_field.save(rental_name, File(bytes_stream, rental_name))
|
||||
|
||||
return True
|
||||
import io
|
||||
|
||||
from django.contrib.staticfiles import finders
|
||||
from django.core.files import File
|
||||
from pypdf import PdfReader, PdfWriter
|
||||
|
||||
from .models import Rental
|
||||
|
||||
|
||||
def generate_rental_pdf(rental: Rental) -> bool:
|
||||
if not rental or rental.status != Rental.Status.APPROVED:
|
||||
return False
|
||||
|
||||
# Get data for pdf
|
||||
data = {}
|
||||
data.update(
|
||||
{
|
||||
"Vorname": rental.firstname,
|
||||
"Nachname": rental.surname,
|
||||
"Orga": rental.organization,
|
||||
"Matrikelnummer": rental.matriculation_number,
|
||||
"E-Mail": rental.email,
|
||||
"Telefonnummer": rental.phone,
|
||||
# Change to the correct date format
|
||||
"Abholdatum": str(rental.date_start.strftime("%d.%m.%Y")),
|
||||
"Rückgabedatum": str(rental.date_end.strftime("%d.%m.%Y")),
|
||||
},
|
||||
)
|
||||
|
||||
for i, item in enumerate(rental.rentalitems.all(), start=1):
|
||||
data.update(
|
||||
{
|
||||
f"Produkt Row{i}": item.name,
|
||||
f"Menge Row{i}": "1",
|
||||
f"Kaution Row{i}": (str(item.deposit) if not rental.intern else "0"),
|
||||
},
|
||||
)
|
||||
|
||||
total_deposit = rental.calc_total_deposit()
|
||||
data.update({"Gesamtkaution": str(total_deposit)})
|
||||
|
||||
# Write data in pdf
|
||||
pdf_path_str = finders.find("rental/Verleihformular.pdf")
|
||||
reader = PdfReader(pdf_path_str)
|
||||
writer = PdfWriter()
|
||||
writer.append(reader)
|
||||
|
||||
writer.update_page_form_field_values(
|
||||
writer.pages[0],
|
||||
data,
|
||||
)
|
||||
|
||||
with io.BytesIO() as bytes_stream:
|
||||
writer.write(bytes_stream)
|
||||
bytes_stream.seek(0)
|
||||
|
||||
# Save pdf in rental
|
||||
rental_name = f"Verleihformular-{str(rental.pk).zfill(4)}.pdf"
|
||||
rental.file_field.save(rental_name, File(bytes_stream, rental_name))
|
||||
|
||||
return True
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import calendar
|
||||
import datetime
|
||||
from datetime import date
|
||||
|
||||
from django.db.models import Q
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.shortcuts import render
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.views.generic import ListView, TemplateView
|
||||
from django.views.generic.detail import DetailView
|
||||
from django.views.generic.edit import CreateView
|
||||
@@ -11,91 +14,164 @@ from django.views.generic.edit import CreateView
|
||||
from .forms import RentalCreateForm
|
||||
from .models import Rental, RentalItem
|
||||
|
||||
# Maximum number of rental items per rental entry because of table size limitations in PDF file
|
||||
RENTAL_ITEMS_MAX = 5
|
||||
|
||||
|
||||
def _calc_days_from_current_month(month: date) -> list:
|
||||
last_day_of_month = month.replace(day=calendar.monthrange(month.year, month.month)[1])
|
||||
|
||||
return [month + datetime.timedelta(days=i) for i in range((last_day_of_month - month).days + 1)]
|
||||
|
||||
|
||||
def _calc_days_from_prev_month(month: date) -> list:
|
||||
days_of_prev_period = []
|
||||
|
||||
if month.weekday() != calendar.MONDAY:
|
||||
for i in range(1, 7):
|
||||
day = month + datetime.timedelta(days=-i)
|
||||
days_of_prev_period.append(day)
|
||||
if day.weekday() == calendar.MONDAY:
|
||||
break
|
||||
|
||||
return sorted(days_of_prev_period)
|
||||
|
||||
|
||||
def _calc_days_from_next_month(month: date) -> list:
|
||||
last_day_of_month = month.replace(day=calendar.monthrange(month.year, month.month)[1])
|
||||
days_of_next_period = []
|
||||
|
||||
if last_day_of_month.weekday() != calendar.SUNDAY:
|
||||
for i in range(1, 7):
|
||||
day = last_day_of_month + datetime.timedelta(days=i)
|
||||
days_of_next_period.append(day)
|
||||
if day.weekday() == calendar.SUNDAY:
|
||||
break
|
||||
|
||||
return days_of_next_period
|
||||
|
||||
|
||||
def _get_display_period(view_type: str, period: str) -> date:
|
||||
display_date: date = None
|
||||
|
||||
# Handle week view
|
||||
if view_type == "week":
|
||||
try:
|
||||
# Parse the requested calendar week
|
||||
display_date = (
|
||||
datetime.datetime.strptime(f"{period}-1", "%G-KW%V-%u")
|
||||
.replace(tzinfo=datetime.UTC)
|
||||
.date()
|
||||
)
|
||||
except Exception:
|
||||
# Get first day of the current week
|
||||
today = timezone.now().date()
|
||||
display_date = today - datetime.timedelta(days=today.weekday())
|
||||
|
||||
# Handle month view
|
||||
else:
|
||||
try:
|
||||
# Parse the requested month
|
||||
display_date = (
|
||||
datetime.datetime.strptime(period, "%Y-%m").replace(tzinfo=datetime.UTC).date()
|
||||
)
|
||||
except Exception:
|
||||
# Get the first day of the current month
|
||||
display_date = timezone.now().date().replace(day=1)
|
||||
|
||||
return display_date
|
||||
|
||||
|
||||
class RentalListView(ListView):
|
||||
model = Rental
|
||||
template_name = "rental/calendar.html"
|
||||
|
||||
# Month is the month displayed in the calendar (and should be the first day of the month)
|
||||
month = None
|
||||
# Rental items to filter
|
||||
rentalitem_filters = []
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
# Current display period and view settings
|
||||
self.display_period = None
|
||||
self.view_type = "month" # Default view
|
||||
self.rentalitem_filters = []
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
# Get the rental items from the filter (max. 4)
|
||||
self.rentalitem_filters = request.GET.getlist("rentalitems", [])[:4]
|
||||
if not self.rentalitem_filters:
|
||||
for rentalitem in RentalItem.objects.all()[:4]:
|
||||
self.rentalitem_filters.append(rentalitem.name)
|
||||
# Get view parameters from request
|
||||
_view_type = request.GET.get("view_type", "week")
|
||||
_period = request.GET.get("period_value", "")
|
||||
_prev_period = request.GET.get("prev_period", "")
|
||||
_next_period = request.GET.get("next_period", "")
|
||||
|
||||
# Get the displayed month from the request
|
||||
_date_str = request.GET.get("month", "")
|
||||
if _date_str:
|
||||
self.month = (
|
||||
datetime.datetime.strptime(_date_str, "%Y-%m").replace(tzinfo=datetime.UTC).date()
|
||||
)
|
||||
else:
|
||||
self.month = datetime.datetime.now(tz=datetime.UTC).date().replace(day=1)
|
||||
if _prev_period:
|
||||
_period = _prev_period
|
||||
elif _next_period:
|
||||
_period = _next_period
|
||||
|
||||
self.view_type = _view_type
|
||||
self.display_period = _get_display_period(_view_type, _period)
|
||||
|
||||
self.rentalitem_filters = request.GET.getlist("rentalitems", [])
|
||||
if not self.rentalitem_filters:
|
||||
items = RentalItem.objects.all()
|
||||
self.rentalitem_filters = [item.name for item in items]
|
||||
|
||||
# Update request.GET
|
||||
_request_get_list = request.GET.copy()
|
||||
_request_get_list.pop("prev_period", None)
|
||||
_request_get_list.pop("next_period", None)
|
||||
_request_get_list["period_value"] = _period
|
||||
request.GET = _request_get_list
|
||||
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
# Calculate the day of the previous month from Monday
|
||||
days_of_prev_month = []
|
||||
if self.view_type != "week":
|
||||
# Add the displayed, previous and next month
|
||||
context["view_period"] = self.display_period
|
||||
context["prev_period"] = self.display_period + datetime.timedelta(days=-1)
|
||||
context["next_period"] = self.display_period + datetime.timedelta(
|
||||
days=calendar.monthrange(self.display_period.year, self.display_period.month)[1] + 1
|
||||
)
|
||||
|
||||
if self.month.weekday() != calendar.MONDAY:
|
||||
for i in range(1, 7):
|
||||
day = self.month + datetime.timedelta(days=-i)
|
||||
days_of_prev_month.append(day)
|
||||
if day.weekday() == calendar.MONDAY:
|
||||
break
|
||||
# Add the days of the displayed, previous and next month
|
||||
days_of_view_period = _calc_days_from_current_month(self.display_period)
|
||||
context["days_of_view_period"] = days_of_view_period
|
||||
context["days_of_prev_period"] = _calc_days_from_prev_month(self.display_period)
|
||||
context["days_of_next_period"] = _calc_days_from_next_month(self.display_period)
|
||||
|
||||
# Calculate the days of the next month until Sunday
|
||||
last_day_of_month = self.month.replace(
|
||||
day=calendar.monthrange(self.month.year, self.month.month)[1]
|
||||
)
|
||||
days_of_next_month = []
|
||||
context["view_type"] = "month"
|
||||
|
||||
if last_day_of_month.weekday() != calendar.SUNDAY:
|
||||
for i in range(1, 7):
|
||||
day = last_day_of_month + datetime.timedelta(days=i)
|
||||
days_of_next_month.append(day)
|
||||
if day.weekday() == calendar.SUNDAY:
|
||||
break
|
||||
context["week_num"] = None
|
||||
|
||||
# Calculate the days of the displayed month
|
||||
days_of_month = [
|
||||
self.month + datetime.timedelta(days=i)
|
||||
for i in range((last_day_of_month - self.month).days + 1)
|
||||
]
|
||||
else:
|
||||
# Current week
|
||||
year, week_num, _ = self.display_period.isocalendar()
|
||||
context["view_period"] = f"{year}-KW{week_num:02d}" # formats as "2025-KW02"
|
||||
|
||||
# Create a dictionary with the rental items for each day
|
||||
rental_dict = {}
|
||||
for rental in self.get_queryset():
|
||||
for day in days_of_month:
|
||||
if rental["date_start"] <= day and rental["date_end"] >= day:
|
||||
if day not in rental_dict:
|
||||
rental_dict[day] = []
|
||||
# Calculate previous week
|
||||
prev_week = self.display_period - datetime.timedelta(days=7)
|
||||
prev_year, prev_week_num, _ = prev_week.isocalendar()
|
||||
context["prev_period"] = f"{prev_year}-KW{prev_week_num:02d}"
|
||||
|
||||
if rental["rentalitems__name"] not in rental_dict[day]:
|
||||
rental_dict[day].append(rental["rentalitems__name"])
|
||||
# Calculate next week
|
||||
next_week = self.display_period + datetime.timedelta(days=7)
|
||||
next_year, next_week_num, _ = next_week.isocalendar()
|
||||
context["next_period"] = f"{next_year}-KW{next_week_num:02d}"
|
||||
|
||||
# Add the displayed, previous and next month
|
||||
context["month"] = self.month
|
||||
context["prev_month"] = self.month + datetime.timedelta(days=-1)
|
||||
context["next_month"] = self.month + datetime.timedelta(
|
||||
days=calendar.monthrange(self.month.year, self.month.month)[1] + 1
|
||||
)
|
||||
# Add days of week (7 days starting from first_day_of_week)
|
||||
days_of_view_period = [
|
||||
self.display_period + datetime.timedelta(days=i) for i in range(7)
|
||||
]
|
||||
context["days_of_view_period"] = days_of_view_period
|
||||
context["days_of_prev_period"] = []
|
||||
context["days_of_next_period"] = []
|
||||
|
||||
# Add the days of the displayed, previous and next month
|
||||
context["days_of_month"] = days_of_month
|
||||
context["days_of_prev_month"] = sorted(days_of_prev_month)
|
||||
context["days_of_next_month"] = days_of_next_month
|
||||
context["view_type"] = "week"
|
||||
|
||||
context["week_num"] = week_num
|
||||
|
||||
# Get the current date for the calendar
|
||||
context["today"] = datetime.datetime.now(tz=datetime.UTC).date()
|
||||
context["today"] = timezone.now().date()
|
||||
|
||||
# Add rental items to the context for the filter
|
||||
context["rentalitems"] = RentalItem.objects.all()
|
||||
@@ -103,7 +179,18 @@ class RentalListView(ListView):
|
||||
# Add the selected rental items to the context for the filter
|
||||
context["rentalitem_filters"] = {"rentalitems": self.rentalitem_filters}
|
||||
|
||||
context["rental_dict"] = rental_dict
|
||||
# Create a dictionary with the rental items for each day
|
||||
rental_dict = {}
|
||||
for rental in self.get_queryset():
|
||||
for day in days_of_view_period:
|
||||
if rental["date_start"] <= day and rental["date_end"] >= day:
|
||||
if day not in rental_dict:
|
||||
rental_dict[day] = []
|
||||
|
||||
if rental["rentalitems__name"] not in rental_dict[day]:
|
||||
rental_dict[day].append(rental["rentalitems__name"])
|
||||
|
||||
context["rental_dict"] = {k: sorted(v, key=str.casefold) for k, v in rental_dict.items()}
|
||||
|
||||
return context
|
||||
|
||||
@@ -118,20 +205,22 @@ class RentalListView(ListView):
|
||||
)
|
||||
)
|
||||
|
||||
last_day_of_month = self.month.replace(
|
||||
day=calendar.monthrange(self.month.year, self.month.month)[1]
|
||||
)
|
||||
if self.view_type == "week":
|
||||
qs_date_end = self.display_period + datetime.timedelta(days=6)
|
||||
else:
|
||||
qs_date_end = self.display_period.replace(
|
||||
day=calendar.monthrange(self.display_period.year, self.display_period.month)[1]
|
||||
)
|
||||
|
||||
# Filter by date
|
||||
qs_new = qs.filter(date_start__gte=self.month, date_start__lte=last_day_of_month)
|
||||
qs_new |= qs.filter(date_end__gte=self.month, date_end__lte=last_day_of_month)
|
||||
if self.display_period and qs_date_end:
|
||||
qs_new = qs.filter(date_start__gte=self.display_period, date_start__lte=qs_date_end)
|
||||
qs_new |= qs.filter(date_end__gte=self.display_period, date_end__lte=qs_date_end)
|
||||
|
||||
# Filter by rental items
|
||||
qs = qs.filter(rentalitems__name__in=self.rentalitem_filters).distinct()
|
||||
|
||||
qs = qs.values("id", "date_start", "date_end", "rentalitems__name").distinct()
|
||||
|
||||
return qs
|
||||
return qs.values("id", "date_start", "date_end", "rentalitems__name").distinct()
|
||||
|
||||
|
||||
class RentalCreateView(CreateView):
|
||||
@@ -139,6 +228,35 @@ class RentalCreateView(CreateView):
|
||||
model = Rental
|
||||
template_name = "rental/create.html"
|
||||
|
||||
def form_valid(self, form):
|
||||
# Get unsaved base Rental with all form data
|
||||
base_rental = form.save(commit=False)
|
||||
items = list(form.cleaned_data["rentalitems"])
|
||||
selected_items = sorted(items, key=lambda x: x.name.lower())
|
||||
|
||||
for i in range(0, len(selected_items), RENTAL_ITEMS_MAX):
|
||||
batch = selected_items[i : i + RENTAL_ITEMS_MAX]
|
||||
|
||||
# Clone base_rental — copying all its field values
|
||||
new_rental = Rental.objects.create(
|
||||
firstname=base_rental.firstname,
|
||||
surname=base_rental.surname,
|
||||
matriculation_number=base_rental.matriculation_number,
|
||||
email=base_rental.email,
|
||||
phone=base_rental.phone,
|
||||
organization=base_rental.organization,
|
||||
date_start=base_rental.date_start,
|
||||
date_end=base_rental.date_end,
|
||||
reason=base_rental.reason,
|
||||
comment=base_rental.comment,
|
||||
status=base_rental.status,
|
||||
)
|
||||
|
||||
# Important: Add M2M relations after the object has been saved
|
||||
new_rental.rentalitems.add(*batch)
|
||||
|
||||
return HttpResponseRedirect(self.get_success_url())
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
@@ -147,26 +265,12 @@ class RentalCreateView(CreateView):
|
||||
return context
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse("rental:rental_create_done", kwargs={"pk": self.object.pk})
|
||||
return reverse("rental:rental_create_done")
|
||||
|
||||
|
||||
class RentalCreateDoneView(TemplateView):
|
||||
template_name = "rental/create_done.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
obj = Rental.objects.get(pk=self.kwargs["pk"])
|
||||
|
||||
total_deposit = 0
|
||||
for elem in obj.rentalitems.all():
|
||||
total_deposit += elem.deposit
|
||||
|
||||
context["object"] = obj
|
||||
context["total_deposit"] = total_deposit
|
||||
|
||||
return context
|
||||
|
||||
|
||||
class RentalItemDetailView(DetailView):
|
||||
model = RentalItem
|
||||
@@ -175,7 +279,7 @@ class RentalItemDetailView(DetailView):
|
||||
|
||||
def rental_calendar(request):
|
||||
"""
|
||||
ICS-calendar for outlook, google calender, ...
|
||||
ICS-calendar for Outlook, Google Calendar, etc.
|
||||
"""
|
||||
rentals = Rental.objects.all()
|
||||
|
||||
|
||||
@@ -1,192 +1,281 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Verleih{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<main class="container mx-auto w-full px-4 my-8 flex-1">
|
||||
<h1 class="page-title">Verleih</h1>
|
||||
|
||||
<section class="text-center my-6">
|
||||
<p class="my-6 text-gray-900 dark:text-gray-100">
|
||||
Willkommen bei unserem Verleih!
|
||||
</p>
|
||||
|
||||
{% if user.is_authenticated %}
|
||||
<a href="{% url 'rental:calendar' %}" class="block btn btn-secondary max-w-xs mx-auto sm:w-max sm:mr-0 sm:ml-auto my-6"><i class="fa-solid fa-calendar-days mr-2"></i>Verleih-Kalender abonnieren</a>
|
||||
{% endif %}
|
||||
|
||||
<a href="{% url 'rental:rental_create' %}" class="page-subtitle block btn-small btn-primary max-w-xs mx-auto sm:w-max sm:mr-0 sm:ml-auto">
|
||||
<i class="fa-solid fa-plus mr-1"></i> Verleih anfragen
|
||||
</a>
|
||||
</section>
|
||||
|
||||
<form action="" method="GET">
|
||||
<section>
|
||||
<div class="grid grid-cols-1 gap-x-6 gap-y-6 lg:grid-cols-6 sm:grid-cols-3 pb-6">
|
||||
<button
|
||||
id="filterDropdownButton"
|
||||
data-dropdown-toggle="filterDropdown"
|
||||
class="w-full md:w-auto flex items-center justify-center py-2 px-4 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded-lg border border-gray-200 hover:bg-gray-100 hover:text-primary-700 focus:z-10 focus:ring-4 focus:ring-gray-200 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700"
|
||||
type="button"
|
||||
>
|
||||
<i class="h-4 w-4 mr-2 fa-solid fa-filter"></i>
|
||||
Filter
|
||||
<i class="-mr-1 ml-1.5 mt-1 w-5 h-5 fa-solid fa-chevron-down"></i>
|
||||
</button>
|
||||
<div id="filterDropdown" class="z-10 hidden w-56 p-3 bg-white rounded-lg shadow dark:bg-gray-700">
|
||||
<h6 class="mb-3 text-sm font-medium text-gray-900 dark:text-white">Verleihgegenstände</h6>
|
||||
<ul class="space-y-2 text-sm" aria-labelledby="filterDropdownButton">
|
||||
{% for item in rentalitems %}
|
||||
<li class="flex items-center">
|
||||
<input
|
||||
id="item_{{ item.id }}"
|
||||
type="checkbox"
|
||||
name="rentalitems"
|
||||
value="{{ item.name }}"
|
||||
{% for key, value in rentalitem_filters.items %}
|
||||
{% if key == "rentalitems" and item.name in value %}
|
||||
checked
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
class="w-4 h-4 bg-gray-100 border-gray-300 rounded text-primary-600 focus:ring-primary-500 dark:focus:ring-primary-600 dark:ring-offset-gray-700 focus:ring-2 dark:bg-gray-600 dark:border-gray-500"
|
||||
>
|
||||
<label for="item_{{ item.id }}" class="ml-2 text-sm font-medium text-gray-900 dark:text-gray-100">{{ item.name }}</label>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="block btn btn-primary" name="" value="Submit">Filter anwenden</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="text-center">
|
||||
<div class="wrapper mx-auto bg-white rounded shadow max-w-full">
|
||||
<div class="header flex justify-between border-b p-2">
|
||||
<span class="text-lg font-bold">{{ month|date:'F Y' }}</span>
|
||||
|
||||
<div class="buttons">
|
||||
<button type="submit" class="p-1" name="month" value="{{ prev_month|date:'Y-m' }}">
|
||||
<i class="fa-regular fa-circle-left"></i>
|
||||
</button>
|
||||
<button type="submit" class="p-1" name="month" value="{{ next_month|date:'Y-m' }}">
|
||||
<i class="fa-regular fa-circle-right"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="table-fixed w-full mx-auto">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="p-2 border-r h-10 xl:text-sm text-xs">
|
||||
<span class="xl:block lg:block md:block sm:block hidden">Monday</span>
|
||||
<span class="xl:hidden lg:hidden md:hidden sm:hidden block">Mon</span>
|
||||
</th>
|
||||
<th class="p-2 border-r h-10 xl:text-sm text-xs">
|
||||
<span class="xl:block lg:block md:block sm:block hidden">Tuesday</span>
|
||||
<span class="xl:hidden lg:hidden md:hidden sm:hidden block">Tue</span>
|
||||
</th>
|
||||
<th class="p-2 border-r h-10 xl:text-sm text-xs">
|
||||
<span class="xl:block lg:block md:block sm:block hidden">Wednesday</span>
|
||||
<span class="xl:hidden lg:hidden md:hidden sm:hidden block">Wed</span>
|
||||
</th>
|
||||
<th class="p-2 border-r h-10 xl:text-sm text-xs">
|
||||
<span class="xl:block lg:block md:block sm:block hidden">Thursday</span>
|
||||
<span class="xl:hidden lg:hidden md:hidden sm:hidden block">Thu</span>
|
||||
</th>
|
||||
<th class="p-2 border-r h-10 xl:text-sm text-xs">
|
||||
<span class="xl:block lg:block md:block sm:block hidden">Friday</span>
|
||||
<span class="xl:hidden lg:hidden md:hidden sm:hidden block">Fri</span>
|
||||
</th>
|
||||
<th class="p-2 border-r h-10 xl:text-sm text-xs">
|
||||
<span class="xl:block lg:block md:block sm:block hidden">Saturday</span>
|
||||
<span class="xl:hidden lg:hidden md:hidden sm:hidden block">Sat</span>
|
||||
</th>
|
||||
<th class="p-2 border-r h-10 xl:text-sm text-xs">
|
||||
<span class="xl:block lg:block md:block sm:block hidden">Sunday</span>
|
||||
<span class="xl:hidden lg:hidden md:hidden sm:hidden block">Sun</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{% for day in days_of_prev_month %}
|
||||
{% if day.weekday == 0 %}
|
||||
<tr class="text-center h-20">
|
||||
{% endif %}
|
||||
|
||||
<td class="border p-1 h-40">
|
||||
<div class="flex flex-col h-40">
|
||||
<div class="top h-5 w-full">
|
||||
<span class="text-gray-500 text-sm">{{ day.day }}</span>
|
||||
</div>
|
||||
<div class="bottom flex-grow h-30 py-1 w-full"></div>
|
||||
</div>
|
||||
</td>
|
||||
{% endfor %}
|
||||
|
||||
{% for day in days_of_month %}
|
||||
{% if day.weekday == 0 %}
|
||||
<tr class="text-center h-20">
|
||||
{% endif %}
|
||||
|
||||
<td class="border p-1 h-40">
|
||||
<div class="flex flex-col h-40">
|
||||
<div class="top h-5 w-full">
|
||||
{% if day == today %}
|
||||
<span class="text-gray-100 dark:text-gray-900 border-2 border-blue-900 dark:border-blue-100 rounded-full bg-blue-900 dark:bg-blue-100">{{ day.day }}</span>
|
||||
{% else %}
|
||||
<span class="text-gray-900 dark:text-gray-100">{{ day.day }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% for key, names in rental_dict.items %}
|
||||
{% if key == day %}
|
||||
{% for name in names %}
|
||||
<div class="bottom h-30 py-1 w-full">
|
||||
<div class="event bg-purple-400 text-white rounded p-1 text-sm">
|
||||
<span class="event-name whitespace-nowrap 2xl:block xl:hidden lg:hidden md:hidden sm:hidden hidden">{{ name|truncatechars:26 }}</span>
|
||||
<span class="event-name whitespace-nowrap 2xl:hidden xl:block lg:hidden md:hidden sm:hidden hidden">{{ name|truncatechars:20 }}</span>
|
||||
<span class="event-name whitespace-nowrap 2xl:hidden xl:hidden lg:block md:hidden sm:hidden hidden">{{ name|truncatechars:15 }}</span>
|
||||
<span class="event-name whitespace-nowrap 2xl:hidden xl:hidden lg:hidden md:block sm:hidden hidden">{{ name|truncatechars:10 }}</span>
|
||||
<span class="event-name whitespace-nowrap 2xl:hidden xl:hidden lg:hidden md:hidden sm:block hidden">{{ name|truncatechars:6 }}</span>
|
||||
<span class="event-name whitespace-nowrap 2xl:hidden xl:hidden lg:hidden md:hidden sm:hidden block">{{ name|truncatechars:3 }}</span>
|
||||
</div>
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="bottom flex-grow h-30 py-1 w-full"></div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
{% if day.weekday == 6 %}
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% for day in days_of_next_month %}
|
||||
<td class="border p-1 h-40">
|
||||
<div class="flex flex-col h-40">
|
||||
<div class="top h-5 w-full">
|
||||
<span class="text-gray-500 text-sm">{{ day.day }}</span>
|
||||
</div>
|
||||
<div class="bottom flex-grow h-30 py-1 w-full"></div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
{% if day.weekday == 6 %}
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</form>
|
||||
</main>
|
||||
{% endblock content %}
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Verleih{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<main class="container mx-auto w-full px-4 my-8 flex-1">
|
||||
<h1 class="page-title">Verleih</h1>
|
||||
|
||||
<section class="text-center my-6">
|
||||
<p class="my-6 text-gray-900 dark:text-gray-100">
|
||||
Willkommen bei unserem Verleih!
|
||||
</p>
|
||||
|
||||
{% if user.is_authenticated %}
|
||||
<a
|
||||
href="{% url 'rental:calendar' %}"
|
||||
class="block btn btn-secondary max-w-xs mx-auto sm:w-max sm:mr-0 sm:ml-auto my-6"
|
||||
title="Kalender im iCal-Format abonnieren"
|
||||
>
|
||||
<i class="fa-solid fa-calendar-days mr-2"></i>Verleih-Kalender abonnieren
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
<a
|
||||
href="{% url 'rental:rental_create' %}"
|
||||
class="page-subtitle block btn-small btn-primary max-w-xs mx-auto sm:w-max sm:mr-0 sm:ml-auto"
|
||||
title="Neue Verleihanfrage erstellen"
|
||||
>
|
||||
<i class="fa-solid fa-plus mr-1"></i> Verleih anfragen
|
||||
</a>
|
||||
</section>
|
||||
|
||||
<form action="" method="GET">
|
||||
<section>
|
||||
<div class="grid grid-cols-1 gap-x-6 gap-y-6 xl:grid-cols-6 sm:grid-cols-3 pb-6">
|
||||
<button
|
||||
id="filterDropdownButton"
|
||||
data-dropdown-toggle="filterDropdown"
|
||||
class="w-full md:w-auto flex items-center justify-center py-2 px-4 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded-lg border border-gray-200 hover:bg-gray-100 hover:text-primary-700 focus:z-10 focus:ring-4 focus:ring-gray-200 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700"
|
||||
type="button"
|
||||
>
|
||||
<i class="h-4 w-4 mr-2 fa-solid fa-filter"></i>
|
||||
Filter
|
||||
<i class="-mr-1 ml-1.5 mt-1 w-5 h-5 fa-solid fa-chevron-down"></i>
|
||||
</button>
|
||||
<div id="filterDropdown" class="z-10 hidden w-56 p-3 bg-white rounded-lg shadow dark:bg-gray-700">
|
||||
<h6 class="mb-3 text-sm font-medium text-gray-900 dark:text-white">Verleihgegenstände</h6>
|
||||
<ul class="space-y-2 text-sm" aria-labelledby="filterDropdownButton">
|
||||
{% for item in rentalitems %}
|
||||
<li class="flex items-center">
|
||||
<input
|
||||
id="item_{{ item.id }}"
|
||||
type="checkbox"
|
||||
name="rentalitems"
|
||||
value="{{ item.name }}"
|
||||
{% for key, value in rentalitem_filters.items %}
|
||||
{% if key == "rentalitems" and item.name in value %}
|
||||
checked
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
class="w-4 h-4 bg-gray-100 border-gray-300 rounded text-primary-600 focus:ring-primary-500 dark:focus:ring-primary-600 dark:ring-offset-gray-700 focus:ring-2 dark:bg-gray-600 dark:border-gray-500"
|
||||
>
|
||||
<label for="item_{{ item.id }}" class="ml-2 text-sm font-medium text-gray-900 dark:text-gray-100">{{ item.name }}</label>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="flex w-full md:w-auto items-center justify-center py-2 px-4 border border-gray-200 rounded-sm dark:border-gray-700">
|
||||
<input
|
||||
{% if view_type == 'month' %}checked{% endif %}
|
||||
id="bordered-radio-1"
|
||||
type="radio"
|
||||
value="month"
|
||||
name="view_type"
|
||||
title="Zur Monatsansicht wechseln"
|
||||
class="ml-2 mr-2 w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600"
|
||||
>
|
||||
<label for="bordered-radio-1" class="w-full ms-2 text-sm font-medium text-gray-900 dark:text-gray-300">Monatsansicht</label>
|
||||
</div>
|
||||
<div class="flex w-full md:w-auto items-center justify-center py-2 px-4 border border-gray-200 rounded-sm dark:border-gray-700">
|
||||
<input
|
||||
{% if view_type == 'week' %}checked{% endif %}
|
||||
id="bordered-radio-2"
|
||||
type="radio"
|
||||
value="week"
|
||||
name="view_type"
|
||||
title="Zur Wochenansicht wechseln"
|
||||
class="ml-2 mr-2 w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600"
|
||||
>
|
||||
<label for="bordered-radio-2" class="w-full ms-2 text-sm font-medium text-gray-900 dark:text-gray-300">Wochenansicht</label>
|
||||
</div>
|
||||
|
||||
<input
|
||||
type="hidden" name="period_value"
|
||||
value="{% if view_type == 'month' %}{{ view_period|date:'Y-m' }}{% else %}{{ view_period }}{% endif %}"
|
||||
>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="block btn btn-primary"
|
||||
title="Filter anwenden und Kalender aktualisieren"
|
||||
>
|
||||
Filter anwenden
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="text-center">
|
||||
<div class="wrapper mx-auto bg-white rounded shadow max-w-full">
|
||||
<div class="header flex justify-between border-b p-2">
|
||||
<span class="text-lg font-bold">
|
||||
{% if view_type == 'month' %}
|
||||
{{ view_period|date:'F Y' }}
|
||||
{% else %}
|
||||
{% if days_of_view_period.0.month != days_of_view_period.6.month %}
|
||||
KW{{ week_num|stringformat:"02d" }} - {{ days_of_view_period.0|date:'F' }} / {{ days_of_view_period.6|date:'F Y' }}
|
||||
{% else %}
|
||||
KW{{ week_num|stringformat:"02d" }} - {{ days_of_view_period.0|date:'F Y' }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</span>
|
||||
|
||||
<div class="buttons">
|
||||
<button
|
||||
type="submit" class="p-1" name="prev_period"
|
||||
title="{% if view_type == 'month' %}Zum vorherigen Monat{% else %}Zur vorherigen Woche{% endif %}"
|
||||
value="{% if view_type == 'month' %}{{ prev_period|date:'Y-m' }}{% else %}{{ prev_period }}{% endif %}"
|
||||
>
|
||||
<i class="fa-regular fa-circle-left"></i>
|
||||
</button>
|
||||
<button
|
||||
type="submit" class="p-1" name="next_period"
|
||||
title="{% if view_type == 'month' %}Zum nächsten Monat{% else %}Zur nächsten Woche{% endif %}"
|
||||
value="{% if view_type == 'month' %}{{ next_period|date:'Y-m' }}{% else %}{{ next_period }}{% endif %}"
|
||||
>
|
||||
<i class="fa-regular fa-circle-right"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="table-fixed w-full mx-auto">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="p-2 border-r h-10 xl:text-sm text-xs">
|
||||
<span class="xl:block lg:block md:block sm:block hidden">Montag</span>
|
||||
<span class="xl:hidden lg:hidden md:hidden sm:hidden block">Mo</span>
|
||||
</th>
|
||||
<th class="p-2 border-r h-10 xl:text-sm text-xs">
|
||||
<span class="xl:block lg:block md:block sm:block hidden">Dienstag</span>
|
||||
<span class="xl:hidden lg:hidden md:hidden sm:hidden block">Di</span>
|
||||
</th>
|
||||
<th class="p-2 border-r h-10 xl:text-sm text-xs">
|
||||
<span class="xl:block lg:block md:block sm:block hidden">Mittwoch</span>
|
||||
<span class="xl:hidden lg:hidden md:hidden sm:hidden block">Mi</span>
|
||||
</th>
|
||||
<th class="p-2 border-r h-10 xl:text-sm text-xs">
|
||||
<span class="xl:block lg:block md:block sm:block hidden">Donnerstag</span>
|
||||
<span class="xl:hidden lg:hidden md:hidden sm:hidden block">Do</span>
|
||||
</th>
|
||||
<th class="p-2 border-r h-10 xl:text-sm text-xs">
|
||||
<span class="xl:block lg:block md:block sm:block hidden">Freitag</span>
|
||||
<span class="xl:hidden lg:hidden md:hidden sm:hidden block">Fr</span>
|
||||
</th>
|
||||
<th class="p-2 border-r h-10 xl:text-sm text-xs">
|
||||
<span class="xl:block lg:block md:block sm:block hidden">Samstag</span>
|
||||
<span class="xl:hidden lg:hidden md:hidden sm:hidden block">Sa</span>
|
||||
</th>
|
||||
<th class="p-2 border-r h-10 xl:text-sm text-xs">
|
||||
<span class="xl:block lg:block md:block sm:block hidden">Sonntag</span>
|
||||
<span class="xl:hidden lg:hidden md:hidden sm:hidden block">So</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{% for day in days_of_prev_period %}
|
||||
{% if day.weekday == 0 %}
|
||||
<tr class="text-center h-20">
|
||||
{% endif %}
|
||||
|
||||
<td class="border p-1 h-40">
|
||||
<div class="flex flex-col h-40">
|
||||
<div class="top h-5 w-full">
|
||||
<span class="text-gray-500 text-sm">{{ day.day }}</span>
|
||||
</div>
|
||||
<div class="bottom flex-grow h-30 py-1 w-full"></div>
|
||||
</div>
|
||||
</td>
|
||||
{% endfor %}
|
||||
|
||||
{% for day in days_of_view_period %}
|
||||
{% if day.weekday == 0 %}
|
||||
<tr class="text-center h-40">
|
||||
{% endif %}
|
||||
|
||||
<td class="border p-1 h-full">
|
||||
<div
|
||||
class="flex flex-col{% if view_type == 'month' %} h-40{% else %} h-full{% endif %}"
|
||||
>
|
||||
<div class="top h-5 w-full">
|
||||
{% if day == today %}
|
||||
<span class="text-gray-100 dark:text-gray-900 border-2 border-blue-900 dark:border-blue-100 rounded-full bg-blue-900 dark:bg-blue-100">{{ day.day }}</span>
|
||||
{% else %}
|
||||
<span class="text-gray-900 dark:text-gray-100">{{ day.day }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% for key, names in rental_dict.items %}
|
||||
{% if key == day %}
|
||||
{% if view_type == 'month' %}
|
||||
{% for name in names|slice:":3" %}
|
||||
<div class="bottom h-30 py-1 w-full">
|
||||
<div class="event bg-purple-400 text-white rounded p-1 text-sm" title="{{ name }}">
|
||||
<span class="event-name whitespace-nowrap 2xl:block xl:hidden lg:hidden md:hidden sm:hidden hidden">{{ name|truncatechars:26 }}</span>
|
||||
<span class="event-name whitespace-nowrap 2xl:hidden xl:block lg:hidden md:hidden sm:hidden hidden">{{ name|truncatechars:20 }}</span>
|
||||
<span class="event-name whitespace-nowrap 2xl:hidden xl:hidden lg:block md:hidden sm:hidden hidden">{{ name|truncatechars:15 }}</span>
|
||||
<span class="event-name whitespace-nowrap 2xl:hidden xl:hidden lg:hidden md:block sm:hidden hidden">{{ name|truncatechars:10 }}</span>
|
||||
<span class="event-name whitespace-nowrap 2xl:hidden xl:hidden lg:hidden md:hidden sm:block hidden">{{ name|truncatechars:6 }}</span>
|
||||
<span class="event-name whitespace-nowrap 2xl:hidden xl:hidden lg:hidden md:hidden sm:hidden block">{{ name|truncatechars:3 }}</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% if names|length > 3 %}
|
||||
<div class="bottom h-30 py-1 w-full">
|
||||
<a href="?view_type=week&period_value={{ day|date:'o-\K\WW' }}"
|
||||
class="event bg-gray-300 hover:bg-gray-400 text-gray-700 rounded p-1 text-sm hover:underline block cursor-pointer"
|
||||
title="Alle Verleihgegenstände in der Wochenansicht anzeigen">
|
||||
<span class="event-name whitespace-nowrap sm:block hidden">+{{ names|length|add:"-3" }} weitere</span>
|
||||
<span class="event-name whitespace-nowrap sm:hidden block">+{{ names|length|add:"-3" }}</span>
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% for name in names %}
|
||||
<div class="bottom h-30 py-1 w-full">
|
||||
<div class="event bg-purple-400 text-white rounded p-1 text-sm" title="{{ name }}">
|
||||
<span class="event-name whitespace-nowrap 2xl:block xl:hidden lg:hidden md:hidden sm:hidden hidden">{{ name|truncatechars:26 }}</span>
|
||||
<span class="event-name whitespace-nowrap 2xl:hidden xl:block lg:hidden md:hidden sm:hidden hidden">{{ name|truncatechars:20 }}</span>
|
||||
<span class="event-name whitespace-nowrap 2xl:hidden xl:hidden lg:block md:hidden sm:hidden hidden">{{ name|truncatechars:15 }}</span>
|
||||
<span class="event-name whitespace-nowrap 2xl:hidden xl:hidden lg:hidden md:block sm:hidden hidden">{{ name|truncatechars:10 }}</span>
|
||||
<span class="event-name whitespace-nowrap 2xl:hidden xl:hidden lg:hidden md:hidden sm:block hidden">{{ name|truncatechars:6 }}</span>
|
||||
<span class="event-name whitespace-nowrap 2xl:hidden xl:hidden lg:hidden md:hidden sm:hidden block">{{ name|truncatechars:3 }}</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
{% if day.weekday == 6 %}
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% for day in days_of_next_period %}
|
||||
<td class="border p-1 h-40">
|
||||
<div class="flex flex-col h-40">
|
||||
<div class="top h-5 w-full">
|
||||
<span class="text-gray-500 text-sm">{{ day.day }}</span>
|
||||
</div>
|
||||
<div class="bottom flex-grow h-30 py-1 w-full"></div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
{% if day.weekday == 6 %}
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</form>
|
||||
</main>
|
||||
{% endblock content %}
|
||||
|
||||
@@ -1,121 +1,123 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Verleih Anfrage{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<!-- Main Content -->
|
||||
<main class="container mx-auto w-full px-4 my-8 flex-1">
|
||||
<h1 class="page-title">Verleih Anfrage</h1>
|
||||
|
||||
<form action="" enctype="multipart/form-data" method="POST" class="multiSectionForm max-w-2xl">
|
||||
{% csrf_token %}
|
||||
{% include "baseform/non_field_errors.html" %}
|
||||
|
||||
<section>
|
||||
<h2>Persönliche Daten</h2>
|
||||
<small>Bitte gib deine persönlichen Daten ein.</small>
|
||||
|
||||
<div class="grid grid-cols-1 gap-x-6 gap-y-6 sm:grid-cols-6">
|
||||
<div class="sm:col-span-3">
|
||||
{% include "baseform/text.html" with field=form.firstname %}
|
||||
</div>
|
||||
<div class="sm:col-span-3">
|
||||
{% include "baseform/text.html" with field=form.surname %}
|
||||
</div>
|
||||
<div class="sm:col-span-3">
|
||||
{% include "baseform/text.html" with field=form.organization %}
|
||||
</div>
|
||||
<div class="sm:col-span-3">
|
||||
{% include "baseform/text.html" with field=form.matriculation_number %}
|
||||
</div>
|
||||
<div class="sm:col-span-3">
|
||||
{% include "baseform/email.html" with field=form.email %}
|
||||
</div>
|
||||
<div class="sm:col-span-3">
|
||||
{% include "baseform/text.html" with field=form.phone %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Verleihgegenstände</h2>
|
||||
<small>Wähl deine gewünschten Verleihgegenstände aus.</small>
|
||||
|
||||
<div class="grid grid-cols-1 gap-x-6 gap-y-6 sm:grid-cols-6 pb-6 col-span-full">
|
||||
{% if form.rentalitems.errors %}
|
||||
<div class="col-span-full alert alert-danger">
|
||||
<div class="alert-body">{{ form.rentalitems.errors }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div id="{{ form.rentalitems.auto_id }}" class="col-span-full">
|
||||
{% for elem in form.rentalitems %}
|
||||
<div class="col-span-2 mb-2">
|
||||
|
||||
<label for="{{ elem.id_for_label }}">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="{{ elem.id_for_label }}"
|
||||
name="{{ form.rentalitems.html_name }}"
|
||||
value="{{ elem.data.value }}"
|
||||
class="rounded border-gray-300 dark:border-none text-proprietary shadow-sm focus:border-blue-300 focus:ring focus:ring-offset-0 focus:ring-blue-200 dark:focus:ring-sky-700 focus:ring-opacity-50"
|
||||
>
|
||||
<a
|
||||
href="{% url 'rental:rentalitem_detail' elem.data.value %}"
|
||||
class="text-gray-700 dark:text-gray-200 underline hover:text-blue-700 dark:hover:text-blue-300"
|
||||
>{{ elem.choice_label }}</a>
|
||||
</label>
|
||||
|
||||
{% for item in rentalitems_addinfo %}
|
||||
{% if item.name == elem.choice_label and item.induction %}
|
||||
<p class="text-xs text-gray-700 dark:text-gray-200">Einschulung erforderlich!</p>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 gap-x-6 gap-y-6 sm:grid-cols-6">
|
||||
<div class="sm:col-span-3">
|
||||
{% include "baseform/date.html" with field=form.date_start %}
|
||||
</div>
|
||||
<div class="sm:col-span-3">
|
||||
{% include "baseform/date.html" with field=form.date_end %}
|
||||
</div>
|
||||
<div class="col-span-full">
|
||||
{% include "baseform/textarea.html" with field=form.reason %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Zusätzliche Informationen</h2>
|
||||
<small>Hier kannst du zusätzliche Informationen, Anliegen und Sonstiges angeben.</small>
|
||||
|
||||
<div class="grid grid-cols-1 gap-x-6 gap-y-6 sm:grid-cols-6">
|
||||
<div class="col-span-full">
|
||||
{% include "baseform/textarea.html" with field=form.comment %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="grid grid-cols-1 gap-x-6 gap-y-6 sm:grid-cols-6">
|
||||
<div class="col-span-full text-gray-700 dark:text-gray-200">
|
||||
{% include "baseform/checkbox.html" with field=form.conformation %}
|
||||
</div>
|
||||
<a href="{% static 'rental/verleihregeln.pdf' %}" target='_blank' class="inline-flex items-center px-2 py-1">
|
||||
<i class="fa-solid fa-file-pdf fa-fw text-red-800 dark:text-red-500 md:text-inherit group-hover:text-red-800 dark:group-hover:text-red-500"></i>
|
||||
<span class="ml-2 sm:ml-1 text-gray-700 dark:text-gray-200">Verleihregeln</span>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="flex justify-end">
|
||||
<button type="submit" class="btn btn-primary w-full sm:w-auto" value="Einreichen">Anfrage abschicken</button>
|
||||
</section>
|
||||
</form>
|
||||
</main>
|
||||
{% endblock content %}
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Verleih Anfrage{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<!-- Main Content -->
|
||||
<main class="container mx-auto w-full px-4 my-8 flex-1">
|
||||
<h1 class="page-title">Verleih Anfrage</h1>
|
||||
|
||||
<form action="" enctype="multipart/form-data" method="POST" class="multiSectionForm max-w-2xl">
|
||||
{% csrf_token %}
|
||||
{% include "baseform/non_field_errors.html" %}
|
||||
|
||||
<section>
|
||||
<h2>Persönliche Daten</h2>
|
||||
<small>Bitte gib deine persönlichen Daten ein.</small>
|
||||
|
||||
<div class="grid grid-cols-1 gap-x-6 gap-y-6 sm:grid-cols-6">
|
||||
<div class="sm:col-span-3">
|
||||
{% include "baseform/text.html" with field=form.firstname %}
|
||||
</div>
|
||||
<div class="sm:col-span-3">
|
||||
{% include "baseform/text.html" with field=form.surname %}
|
||||
</div>
|
||||
<div class="sm:col-span-3">
|
||||
{% include "baseform/text.html" with field=form.organization %}
|
||||
</div>
|
||||
<div class="sm:col-span-3">
|
||||
{% include "baseform/text.html" with field=form.matriculation_number %}
|
||||
</div>
|
||||
<div class="sm:col-span-3">
|
||||
{% include "baseform/email.html" with field=form.email %}
|
||||
</div>
|
||||
<div class="sm:col-span-3">
|
||||
{% include "baseform/text.html" with field=form.phone %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Verleihgegenstände</h2>
|
||||
<small>Wähl deine gewünschten Verleihgegenstände aus.</small>
|
||||
|
||||
<div class="grid grid-cols-1 gap-x-6 gap-y-6 sm:grid-cols-6 pb-6 col-span-full">
|
||||
{% if form.rentalitems.errors %}
|
||||
<div class="col-span-full alert alert-danger">
|
||||
<div class="alert-body">{{ form.rentalitems.errors }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div id="{{ form.rentalitems.auto_id }}" class="col-span-full">
|
||||
{% for elem in form.rentalitems %}
|
||||
<div class="col-span-2 mb-2">
|
||||
<label for="{{ elem.data.value }}">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="{{ elem.data.value }}"
|
||||
name="{{ form.rentalitems.html_name }}"
|
||||
value="{{ elem.data.value }}"
|
||||
{% if elem.data.selected %}checked{% endif %}
|
||||
class="rounded border-gray-300 dark:border-none text-proprietary shadow-sm focus:border-blue-300 focus:ring focus:ring-offset-0 focus:ring-blue-200 dark:focus:ring-sky-700 focus:ring-opacity-50"
|
||||
>
|
||||
<a
|
||||
href="{% url 'rental:rentalitem_detail' elem.data.value %}"
|
||||
class="text-gray-700 dark:text-gray-200 underline hover:text-blue-700 dark:hover:text-blue-300"
|
||||
title="Details zu {{ elem.choice_label }}"
|
||||
aria-label="Details zu {{ elem.choice_label }}"
|
||||
>{{ elem.choice_label }}</a>
|
||||
</label>
|
||||
|
||||
{% for item in rentalitems_addinfo %}
|
||||
{% if item.name == elem.choice_label and item.induction %}
|
||||
<p class="text-xs text-gray-700 dark:text-gray-200">Einschulung erforderlich!</p>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 gap-x-6 gap-y-6 sm:grid-cols-6">
|
||||
<div class="sm:col-span-3">
|
||||
{% include "baseform/date.html" with field=form.date_start %}
|
||||
</div>
|
||||
<div class="sm:col-span-3">
|
||||
{% include "baseform/date.html" with field=form.date_end %}
|
||||
</div>
|
||||
<div class="col-span-full">
|
||||
{% include "baseform/textarea.html" with field=form.reason %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Zusätzliche Informationen</h2>
|
||||
<small>Hier kannst du zusätzliche Informationen, Anliegen und Sonstiges angeben.</small>
|
||||
|
||||
<div class="grid grid-cols-1 gap-x-6 gap-y-6 sm:grid-cols-6">
|
||||
<div class="col-span-full">
|
||||
{% include "baseform/textarea.html" with field=form.comment %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="grid grid-cols-1 gap-x-6 gap-y-6 sm:grid-cols-6">
|
||||
<div class="col-span-full text-gray-700 dark:text-gray-200">
|
||||
{% include "baseform/checkbox.html" with field=form.conformation %}
|
||||
</div>
|
||||
<a href="{% static 'rental/verleihregeln.pdf' %}" target='_blank' class="inline-flex items-center px-2 py-1">
|
||||
<i class="fa-solid fa-file-pdf fa-fw text-red-800 dark:text-red-500 md:text-inherit group-hover:text-red-800 dark:group-hover:text-red-500"></i>
|
||||
<span class="ml-2 sm:ml-1 text-gray-700 dark:text-gray-200">Verleihregeln</span>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="flex justify-end">
|
||||
<button type="submit" class="btn btn-primary w-full sm:w-auto" value="Einreichen">Anfrage abschicken</button>
|
||||
</section>
|
||||
</form>
|
||||
</main>
|
||||
{% endblock content %}
|
||||
|
||||
@@ -1,17 +1,22 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Verleihanfrage erfolgreich eingereicht{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<!-- Main Content -->
|
||||
<main class="container mx-auto w-full px-4 my-8 flex-1 max-w-2xl">
|
||||
<section class="block w-full">
|
||||
<p class="mt-6 text-gray-900 dark:text-gray-100 hyphens-auto" lang="de">
|
||||
Deine Verleihanfrage mit der Nummer #{{ pk }} wurde erfolgreich eingereicht. Die Gegenstände können am {{ object.date_start }} während der Beratungszeit (Montag - Donnerstag: 09:00 - 14:00, Freitag: 09:00 - 12:00) abgeholt werden. Bitte bring den Gesamtpfand in Höhe von {{ total_deposit }} € in bar mit.
|
||||
</p>
|
||||
<div class="mt-10 flex items-center justify-center">
|
||||
<a href="{% url 'home' %}" type="submit" class="block btn btn-primary">Zur Startseite</a>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
{% endblock content %}
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Verleihanfrage erfolgreich eingereicht{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<!-- Main Content -->
|
||||
<main class="container mx-auto w-full px-4 my-8 flex-1 max-w-2xl">
|
||||
<section class="block w-full">
|
||||
<p class="mt-6 text-gray-900 dark:text-gray-100 hyphens-auto" lang="de">
|
||||
Deine Verleihanfrage ist eingegangen - danke dir! 🎉
|
||||
Wir kümmern uns jetzt darum und melden uns per E-Mail mit den nächsten Schritten.
|
||||
</p>
|
||||
<p class="mt-6 text-gray-900 dark:text-gray-100 hyphens-auto" lang="de">
|
||||
Kleiner Hinweis: Bevor du die Sachen abholen kannst, wird deine Anfrage kurz geprüft und freigegeben.
|
||||
Sobald alles genehmigt ist, bekommst du von uns eine Mail.
|
||||
</p>
|
||||
<div class="mt-10 flex items-center justify-center">
|
||||
<a href="{% url 'home' %}" type="submit" class="block btn btn-primary">Zur Startseite</a>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
{% endblock content %}
|
||||
|
||||
283
fet2020/uv.lock
generated
283
fet2020/uv.lock
generated
@@ -3,54 +3,66 @@ revision = 3
|
||||
requires-python = ">=3.13"
|
||||
|
||||
[[package]]
|
||||
name = "asgiref"
|
||||
version = "3.10.0"
|
||||
name = "anyio"
|
||||
version = "4.12.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/46/08/4dfec9b90758a59acc6be32ac82e98d1fbfc321cb5cfa410436dbacf821c/asgiref-3.10.0.tar.gz", hash = "sha256:d89f2d8cd8b56dada7d52fa7dc8075baa08fb836560710d38c292a7a3f78c04e", size = 37483, upload-time = "2025-10-05T09:15:06.557Z" }
|
||||
dependencies = [
|
||||
{ name = "idna" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/17/9c/fc2331f538fbf7eedba64b2052e99ccf9ba9d6888e2f41441ee28847004b/asgiref-3.10.0-py3-none-any.whl", hash = "sha256:aef8a81283a34d0ab31630c9b7dfe70c812c95eba78171367ca8745e88124734", size = 24050, upload-time = "2025-10-05T09:15:05.11Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "asgiref"
|
||||
version = "3.11.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/76/b9/4db2509eabd14b4a8c71d1b24c8d5734c52b8560a7b1e1a8b56c8d25568b/asgiref-3.11.0.tar.gz", hash = "sha256:13acff32519542a1736223fb79a715acdebe24286d98e8b164a73085f40da2c4", size = 37969, upload-time = "2025-11-19T15:32:20.106Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/91/be/317c2c55b8bbec407257d45f5c8d1b6867abc76d12043f2d3d58c538a4ea/asgiref-3.11.0-py3-none-any.whl", hash = "sha256:1db9021efadb0d9512ce8ffaf72fcef601c7b73a8807a1bb2ef143dc6b14846d", size = 24096, upload-time = "2025-11-19T15:32:19.004Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2025.10.5"
|
||||
version = "2026.1.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/4c/5b/b6ce21586237c77ce67d01dc5507039d444b630dd76611bbca2d8e5dcd91/certifi-2025.10.5.tar.gz", hash = "sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43", size = 164519, upload-time = "2025-10-05T04:12:15.808Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/37/af0d2ef3967ac0d6113837b44a4f0bfe1328c2b9763bd5b1744520e5cfed/certifi-2025.10.5-py3-none-any.whl", hash = "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de", size = 163286, upload-time = "2025-10-05T04:12:14.03Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "djade"
|
||||
version = "1.6.0"
|
||||
version = "1.7.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a9/9c/637184b2595d38d9ddcd31076c7ac6b57cd259821481cc8f029616be2b25/djade-1.6.0.tar.gz", hash = "sha256:a7f173d6949ce248ee538ab85676e3277b4b4ae956c3383f96348355ab8983b7", size = 40150, upload-time = "2025-09-08T15:35:20.097Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d5/31/05baa6b0419c6cd13e4ecae275aadfb6d5c0b332f1b06d4fccb4cb4a2b29/djade-1.7.0.tar.gz", hash = "sha256:b49e7ad42050ecac10ae990a6adbecec08c62b0a32a33de748d63d948bced815", size = 39433, upload-time = "2025-12-10T16:03:53.618Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/e6/ddfcf10d010a3905232ba2962d2ef00533052913966d06237356871fa48f/djade-1.6.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e43eb2227a02599df038ba64b7a0c8599d6cef9d0de978ac635592d58299d477", size = 1126877, upload-time = "2025-09-08T15:35:02.534Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/78/ce1cc79f9311f101ea6721498b1027b7698cd40702af6b50a7842442644d/djade-1.6.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fe820c5be3d08952ad42ea9ee5475e1c60d32326fae57ee98a4887e99d2d1765", size = 1054501, upload-time = "2025-09-08T15:35:04.115Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/db/2b7d51041c47b65ad0f483379958ac44fb12db7a761a390e0f3423932e6e/djade-1.6.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4dd8a7919f1fa953e332548aeb3b793dea0563fc5e9f89017da6fe7318b603a2", size = 1098200, upload-time = "2025-09-08T15:35:05.794Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/82/8d829ba658337ee9138f8850d91cab4ea9ccad2a6d23a60fb7c3cbb7ae9b/djade-1.6.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:113a8db4657bd488ecf383c3db0953530012afbccb596302a294c7b629573269", size = 1041283, upload-time = "2025-09-08T15:35:07.125Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/ac/7f3fbedf3f229bed3c55fe0e6db4b669d2b5d995c07918739dd4713997f7/djade-1.6.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:53511e1d3d55ff408fe11edc7bc982b0af53ab6225870d031804e209ec0515d8", size = 1222400, upload-time = "2025-09-08T15:35:08.826Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/2a/3f3053cc7fba84faee5cd8307cb2d548bc26f05e9c4d28bb48b47bf25d0a/djade-1.6.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9ed89688c9baa733cbf4ec08b8cc0102bdd1b539bd0775f5ad5be98e20ab934", size = 1195877, upload-time = "2025-09-08T15:35:10.464Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/35/9324569b8cfa714c1992dba661deb9bf9001c0471986c71c331172c10388/djade-1.6.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:4c4c42d86f317742ad1d33f4ca85d4d507530b7cfbfc9b2dc015dda4d06199ad", size = 1086171, upload-time = "2025-09-08T15:35:11.923Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/39/e924089cddb183295f7c51ed1587c2f6c956e0efe8b5b594494104c93037/djade-1.6.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:a99661ad8b155c3aa62ac2fdb3879df0e5a5bcb1c66f8a142f3964cd2a0bb1cb", size = 1240677, upload-time = "2025-09-08T15:35:13.761Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/2e/5d942c51cd7c5bb868e942fc3fc9d7cd97617d94a56db0bb40da18d0d0ef/djade-1.6.0-py3-none-win32.whl", hash = "sha256:4555387841d0ef6290ff8221ebea48321262ff1011874debd30010e5deba0ff3", size = 963337, upload-time = "2025-09-08T15:35:15.878Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/ff/3b4a9ff45c0f8584931031d511721c27f5f4d09852611be2164b42eb74e0/djade-1.6.0-py3-none-win_amd64.whl", hash = "sha256:d6111763d133a7a85db0f6f0e59586fdbbd08e6c22238e84de730bb1a21cbab4", size = 1056597, upload-time = "2025-09-08T15:35:17.562Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/a7/d3f58feeb1169a41c569b63cb8f23886694f0d72290cbc7eb691b083e0aa/djade-1.6.0-py3-none-win_arm64.whl", hash = "sha256:c52efaa5d64572ddc4a986d74202195c8574f969877fcfbdbea3a41718334804", size = 985920, upload-time = "2025-09-08T15:35:18.856Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/41/d25b88a4ff05a495aaf992793eae116b867e6a4ebda64701f737f22abf18/djade-1.7.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:084f505338862542343d4cabbd52a8e58a8b05cc1a0eb37d78824073485d4c8d", size = 1156096, upload-time = "2025-12-10T16:03:35.626Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/1f/8ca54f7d4ab2c26095f938cedb13d2508032d9eadcfd07757a46a02cc38a/djade-1.7.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:9676b72b9e9e494d977f887cda825e333e699ae7184384795d5a9e2e5d2c7f69", size = 1097385, upload-time = "2025-12-10T16:03:38.21Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/5d/781d7204973cd12138805d4a6b67ec7666771b66709242f6b7b6834f862b/djade-1.7.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0102aaf6f1a259f2ca78839c796b130fc53868965b01fb0b8d5768c5fe142b99", size = 1142211, upload-time = "2025-12-10T16:03:39.72Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/aa/ba674b4a78be552f9bba496369f0a436b0eebc0bfc7c41bd250776428967/djade-1.7.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:db11d4563b73c3eacd2433005e9467d67c3a58c61ce6b5d821b89b124d1dd168", size = 1077205, upload-time = "2025-12-10T16:03:41.001Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/2d/30f9ce233e02e9e29793b8eb2784a09d5d18b8aa6bbb53374403cee39ac8/djade-1.7.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6aa4659a2c3c172cf0ed35d19cb2d396248e6889b47b4ae52263be1d89f0a532", size = 1264698, upload-time = "2025-12-10T16:03:43.481Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/88/a5a79f2e57e13c0403a238173744d3eb08f03616f0e399a6fe88a806bf66/djade-1.7.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9596896b44e25fbd897e0e8d134a108184f56c32feff8607028daa2315506f47", size = 1217375, upload-time = "2025-12-10T16:03:44.908Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/a2/73839c8d4f9e96ed3f4a4e6e51cc2934fdbc1f1f4ff8be1effbdb7574fe5/djade-1.7.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:7fbdfece645f5816adae816eed847db1905d1e14cba3da5384706adce1eefb2a", size = 1139307, upload-time = "2025-12-10T16:03:46.599Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/a1/462b4e004c5fc9ce9d4f93968e8ee5ba646c033f95e05fc70d8864ac3cb8/djade-1.7.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:51221cff697e1eaa709e375ecac331366ec68cd90567de36f4bd3daca94e8327", size = 1277128, upload-time = "2025-12-10T16:03:47.997Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/54/8b2fc833d1fdeb08d9e87a1db7dd8d4b64854b9d62c7867b69519c357f21/djade-1.7.0-py3-none-win32.whl", hash = "sha256:20308b03c51bd2551137f5852afb663ac5f86015321cc4f45363427f7dbd9534", size = 988134, upload-time = "2025-12-10T16:03:49.264Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/dd/fea876ed43f13182a6381c56dbe8daf4ed4d745228292c38ea44e85ea56d/djade-1.7.0-py3-none-win_amd64.whl", hash = "sha256:a1d41819a0409c74e0e521e856799cbce38c50a1bd5b020fa209a40f9441b499", size = 1098723, upload-time = "2025-12-10T16:03:50.788Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/51/438fcae17c2afa62966f0fbda3799f3bada7110d92cc3c58ebc47b636925/djade-1.7.0-py3-none-win_arm64.whl", hash = "sha256:fef26be22c5b3c15e6b8be3888ae0672aab5067d219cf2be145af22860757c08", size = 1028482, upload-time = "2025-12-10T16:03:52.098Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "django"
|
||||
version = "5.2.7"
|
||||
version = "5.2.9"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "asgiref" },
|
||||
{ name = "sqlparse" },
|
||||
{ name = "tzdata", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b1/96/bd84e2bb997994de8bcda47ae4560991084e86536541d7214393880f01a8/django-5.2.7.tar.gz", hash = "sha256:e0f6f12e2551b1716a95a63a1366ca91bbcd7be059862c1b18f989b1da356cdd", size = 10865812, upload-time = "2025-10-01T14:22:12.081Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/eb/1c/188ce85ee380f714b704283013434976df8d3a2df8e735221a02605b6794/django-5.2.9.tar.gz", hash = "sha256:16b5ccfc5e8c27e6c0561af551d2ea32852d7352c67d452ae3e76b4f6b2ca495", size = 10848762, upload-time = "2025-12-02T14:01:08.418Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/ef/81f3372b5dd35d8d354321155d1a38894b2b766f576d0abffac4d8ae78d9/django-5.2.7-py3-none-any.whl", hash = "sha256:59a13a6515f787dec9d97a0438cd2efac78c8aca1c80025244b0fe507fe0754b", size = 8307145, upload-time = "2025-10-01T14:22:49.476Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/b0/7f42bfc38b8f19b78546d47147e083ed06e12fc29c42da95655e0962c6c2/django-5.2.9-py3-none-any.whl", hash = "sha256:3a4ea88a70370557ab1930b332fd2887a9f48654261cdffda663fef5976bb00a", size = 8290652, upload-time = "2025-12-02T14:01:03.485Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -111,14 +123,14 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "django-static-jquery-ui"
|
||||
version = "1.13.3.1"
|
||||
version = "1.13.3.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "django" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/87/51/045015a87bf97d24ce9a1f34e664f75a8a2ea01c04ee965c632ca9843aef/django-static-jquery-ui-1.13.3.1.tar.gz", hash = "sha256:e2bd1b66f5ee604cfb4f2788398c8274c3307c547eb19e7325f9e5626f040a95", size = 1506385, upload-time = "2024-05-12T13:22:58.54Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1f/b3/47e5fa5166872e7fb90b28d4ea50f30a3018b4fd2c242d56fa350c8dc901/django_static_jquery_ui-1.13.3.2.tar.gz", hash = "sha256:a15aaa58807abf808b0ff9f499bccf23edc3a8e3f0d197d9881b149b9c39e3fc", size = 1506447, upload-time = "2025-10-23T03:03:33.571Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/92/10/8e4409fe2faa8afa66937f91a8ecf671e180e6437e9198f328d6044aaa4c/django_static_jquery_ui-1.13.3.1-py3-none-any.whl", hash = "sha256:ab2f474619e03d15d5016ec83cc6ed2672411c464cf4e9e3bf046ba002f5cba7", size = 1924566, upload-time = "2024-05-12T13:22:55.787Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/e9/b2ca4adfbbebc59ed189aadc1a71d6081dd1b659a183a50038d65c29e8f3/django_static_jquery_ui-1.13.3.2-py3-none-any.whl", hash = "sha256:8cf8efb892533b34970fceb35921470c1fbabaad4c7bb27128bf23d6b428f7e3", size = 1924641, upload-time = "2025-10-23T03:03:31.742Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -135,14 +147,14 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "django-upgrade"
|
||||
version = "1.29.0"
|
||||
version = "1.29.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "tokenize-rt" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ee/07/900c8783c26a66aec4cc6afdfa47fdd3b0ba82436988b852412bc9c64663/django_upgrade-1.29.0.tar.gz", hash = "sha256:f981e62fbccd2bbc4f165034c5c95de60010fdf1ad2c46e7ae9a66e9baa6955a", size = 39691, upload-time = "2025-10-06T15:46:51.229Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9d/dc/8242d9fbf0ae64feccbdd781fa741db5cf0521127a26ed9361e5f2f31f1b/django_upgrade-1.29.1.tar.gz", hash = "sha256:8c53b6bcd326a638a5dc908a707f26d71593bad5789b33775c90a8dc8a76afd5", size = 39638, upload-time = "2025-10-23T16:34:57.003Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/16/33b4645fb10e649656354f02af5c794b26ed0ebf7b70c52bb656af5c662c/django_upgrade-1.29.0-py3-none-any.whl", hash = "sha256:05d174815ed86e5cfe91f22b941f9edc424d5f648dad9fd50b967d795b466523", size = 68922, upload-time = "2025-10-06T15:46:49.94Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/b0/c9fe7b4dbfcacb2402169f8189d4f7fa1609237317d714f01143d167db1c/django_upgrade-1.29.1-py3-none-any.whl", hash = "sha256:39a4d71365189ce8a5ccab534b2d1f0ed69c71bea7f1ef4c46041918d5247e64", size = 68847, upload-time = "2025-10-23T16:34:55.511Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -172,29 +184,32 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "elastic-transport"
|
||||
version = "9.1.0"
|
||||
version = "9.2.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "certifi" },
|
||||
{ name = "sniffio" },
|
||||
{ name = "urllib3" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ae/1f/2d1a1790df2b75e1e1eb90d8a3fe066a47ef95e34430657447e549cc274c/elastic_transport-9.1.0.tar.gz", hash = "sha256:1590e44a25b0fe208107d5e8d7dea15c070525f3ac9baafbe4cb659cd14f073d", size = 76483, upload-time = "2025-07-24T16:41:31.017Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/23/0a/a92140b666afdcb9862a16e4d80873b3c887c1b7e3f17e945fc3460edf1b/elastic_transport-9.2.1.tar.gz", hash = "sha256:97d9abd638ba8aa90faa4ca1bf1a18bde0fe2088fbc8757f2eb7b299f205773d", size = 77403, upload-time = "2025-12-23T11:54:12.849Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/5d/dd5a919dd887fe20a91f18faf5b4345ee3a058e483d2aa84cef0f2567e17/elastic_transport-9.1.0-py3-none-any.whl", hash = "sha256:369fa56874c74daae4ea10cbf40636d139f38f42bec0e006b9cd45a168ee7fce", size = 65142, upload-time = "2025-07-24T16:41:29.648Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/e6/a42b600ae8b808371f740381f6c32050cad93f870d36cc697b8b7006bf7c/elastic_transport-9.2.1-py3-none-any.whl", hash = "sha256:39e1a25e486af34ce7aa1bc9005d1c736f1b6fb04c9b64ea0604ded5a61fc1d4", size = 65327, upload-time = "2025-12-23T11:54:11.681Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "elasticsearch"
|
||||
version = "9.1.1"
|
||||
version = "9.2.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
{ name = "elastic-transport" },
|
||||
{ name = "python-dateutil" },
|
||||
{ name = "sniffio" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/59/6a/5eecef6f1ac8005b04714405cb65971d46031bd897e47c29af86e0f87353/elasticsearch-9.1.1.tar.gz", hash = "sha256:be20acda2a97591a9a6cf4981fc398ee6fca3291cf9e7a9e52b6a9f41a46d393", size = 857802, upload-time = "2025-09-12T13:27:38.62Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/bc/6c/67bb17ca0035b0cac4cfbbe64e18d120203fef22da66dd4c636563a0ea63/elasticsearch-9.2.1.tar.gz", hash = "sha256:97f473418e8976611349757287ac982acf12f4e305182863d985d5a031c36830", size = 878062, upload-time = "2025-12-23T14:37:31.694Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/4c/c0c95d3d881732a5d1b28e12c9be4dea5953ade71810f94565bd5bd2101a/elasticsearch-9.1.1-py3-none-any.whl", hash = "sha256:2a5c27c57ca3dd3365f665c82c9dcd8666ccfb550d5b07c688c21ec636c104e5", size = 937483, upload-time = "2025-09-12T13:27:34.948Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/d5/84264c29ec67f2f8129676ce11f05defb52f44e97e5f411db9a220f2aa43/elasticsearch-9.2.1-py3-none-any.whl", hash = "sha256:8665f5a0b4d29a7c2772851c05ea8a09279abb7928b7d727524613bd61d75958", size = 963593, upload-time = "2025-12-23T14:37:28.047Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -208,7 +223,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "fet-homepage"
|
||||
version = "2.1.0"
|
||||
version = "2.1.1"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "django" },
|
||||
@@ -243,7 +258,7 @@ dev = [
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "django" },
|
||||
{ name = "django", specifier = "<6.0" },
|
||||
{ name = "django-ckeditor", specifier = "<6.7.3" },
|
||||
{ name = "django-environ" },
|
||||
{ name = "django-filter" },
|
||||
@@ -290,6 +305,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/84/1a0f9555fd5f2b1c924ff932d99b40a0f8a6b12f6dd625e2a47f415b00ea/html2text-2025.4.15-py3-none-any.whl", hash = "sha256:00569167ffdab3d7767a4cdf589b7f57e777a5ed28d12907d8c58769ec734acc", size = 34656, upload-time = "2025-04-15T04:02:28.44Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.11"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ldap3"
|
||||
version = "2.9.1"
|
||||
@@ -322,60 +346,60 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "pillow"
|
||||
version = "12.0.0"
|
||||
version = "12.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/cace85a1b0c9775a9f8f5d5423c8261c858760e2466c79b2dd184638b056/pillow-12.0.0.tar.gz", hash = "sha256:87d4f8125c9988bfbed67af47dd7a953e2fc7b0cc1e7800ec6d2080d490bb353", size = 47008828, upload-time = "2025-10-15T18:24:14.008Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d0/02/d52c733a2452ef1ffcc123b68e6606d07276b0e358db70eabad7e40042b7/pillow-12.1.0.tar.gz", hash = "sha256:5c5ae0a06e9ea030ab786b0251b32c7e4ce10e58d983c0d5c56029455180b5b9", size = 46977283, upload-time = "2026-01-02T09:13:29.892Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/62/f2/de993bb2d21b33a98d031ecf6a978e4b61da207bef02f7b43093774c480d/pillow-12.0.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0869154a2d0546545cde61d1789a6524319fc1897d9ee31218eae7a60ccc5643", size = 4045493, upload-time = "2025-10-15T18:22:25.758Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/b6/bc8d0c4c9f6f111a783d045310945deb769b806d7574764234ffd50bc5ea/pillow-12.0.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a7921c5a6d31b3d756ec980f2f47c0cfdbce0fc48c22a39347a895f41f4a6ea4", size = 4120461, upload-time = "2025-10-15T18:22:27.286Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/57/d60d343709366a353dc56adb4ee1e7d8a2cc34e3fbc22905f4167cfec119/pillow-12.0.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:1ee80a59f6ce048ae13cda1abf7fbd2a34ab9ee7d401c46be3ca685d1999a399", size = 3576912, upload-time = "2025-10-15T18:22:28.751Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/a4/a0a31467e3f83b94d37568294b01d22b43ae3c5d85f2811769b9c66389dd/pillow-12.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c50f36a62a22d350c96e49ad02d0da41dbd17ddc2e29750dbdba4323f85eb4a5", size = 5249132, upload-time = "2025-10-15T18:22:30.641Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/06/48eab21dd561de2914242711434c0c0eb992ed08ff3f6107a5f44527f5e9/pillow-12.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5193fde9a5f23c331ea26d0cf171fbf67e3f247585f50c08b3e205c7aeb4589b", size = 4650099, upload-time = "2025-10-15T18:22:32.73Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/bd/69ed99fd46a8dba7c1887156d3572fe4484e3f031405fcc5a92e31c04035/pillow-12.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bde737cff1a975b70652b62d626f7785e0480918dece11e8fef3c0cf057351c3", size = 6230808, upload-time = "2025-10-15T18:22:34.337Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/94/8fad659bcdbf86ed70099cb60ae40be6acca434bbc8c4c0d4ef356d7e0de/pillow-12.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a6597ff2b61d121172f5844b53f21467f7082f5fb385a9a29c01414463f93b07", size = 8037804, upload-time = "2025-10-15T18:22:36.402Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/39/c685d05c06deecfd4e2d1950e9a908aa2ca8bc4e6c3b12d93b9cafbd7837/pillow-12.0.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b817e7035ea7f6b942c13aa03bb554fc44fea70838ea21f8eb31c638326584e", size = 6345553, upload-time = "2025-10-15T18:22:38.066Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/38/57/755dbd06530a27a5ed74f8cb0a7a44a21722ebf318edbe67ddbd7fb28f88/pillow-12.0.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f4f1231b7dec408e8670264ce63e9c71409d9583dd21d32c163e25213ee2a344", size = 7037729, upload-time = "2025-10-15T18:22:39.769Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/b6/7e94f4c41d238615674d06ed677c14883103dce1c52e4af16f000338cfd7/pillow-12.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e51b71417049ad6ab14c49608b4a24d8fb3fe605e5dfabfe523b58064dc3d27", size = 6459789, upload-time = "2025-10-15T18:22:41.437Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/14/4448bb0b5e0f22dd865290536d20ec8a23b64e2d04280b89139f09a36bb6/pillow-12.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d120c38a42c234dc9a8c5de7ceaaf899cf33561956acb4941653f8bdc657aa79", size = 7130917, upload-time = "2025-10-15T18:22:43.152Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/ca/16c6926cc1c015845745d5c16c9358e24282f1e588237a4c36d2b30f182f/pillow-12.0.0-cp313-cp313-win32.whl", hash = "sha256:4cc6b3b2efff105c6a1656cfe59da4fdde2cda9af1c5e0b58529b24525d0a098", size = 6302391, upload-time = "2025-10-15T18:22:44.753Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/2a/dd43dcfd6dae9b6a49ee28a8eedb98c7d5ff2de94a5d834565164667b97b/pillow-12.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:4cf7fed4b4580601c4345ceb5d4cbf5a980d030fd5ad07c4d2ec589f95f09905", size = 7007477, upload-time = "2025-10-15T18:22:46.838Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/f0/72ea067f4b5ae5ead653053212af05ce3705807906ba3f3e8f58ddf617e6/pillow-12.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:9f0b04c6b8584c2c193babcccc908b38ed29524b29dd464bc8801bf10d746a3a", size = 2435918, upload-time = "2025-10-15T18:22:48.399Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/5e/9046b423735c21f0487ea6cb5b10f89ea8f8dfbe32576fe052b5ba9d4e5b/pillow-12.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7fa22993bac7b77b78cae22bad1e2a987ddf0d9015c63358032f84a53f23cdc3", size = 5251406, upload-time = "2025-10-15T18:22:49.905Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/12/66/982ceebcdb13c97270ef7a56c3969635b4ee7cd45227fa707c94719229c5/pillow-12.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f135c702ac42262573fe9714dfe99c944b4ba307af5eb507abef1667e2cbbced", size = 4653218, upload-time = "2025-10-15T18:22:51.587Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/b3/81e625524688c31859450119bf12674619429cab3119eec0e30a7a1029cb/pillow-12.0.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c85de1136429c524e55cfa4e033b4a7940ac5c8ee4d9401cc2d1bf48154bbc7b", size = 6266564, upload-time = "2025-10-15T18:22:53.215Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/59/dfb38f2a41240d2408096e1a76c671d0a105a4a8471b1871c6902719450c/pillow-12.0.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:38df9b4bfd3db902c9c2bd369bcacaf9d935b2fff73709429d95cc41554f7b3d", size = 8069260, upload-time = "2025-10-15T18:22:54.933Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/3d/378dbea5cd1874b94c312425ca77b0f47776c78e0df2df751b820c8c1d6c/pillow-12.0.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d87ef5795da03d742bf49439f9ca4d027cde49c82c5371ba52464aee266699a", size = 6379248, upload-time = "2025-10-15T18:22:56.605Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/b0/d525ef47d71590f1621510327acec75ae58c721dc071b17d8d652ca494d8/pillow-12.0.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aff9e4d82d082ff9513bdd6acd4f5bd359f5b2c870907d2b0a9c5e10d40c88fe", size = 7066043, upload-time = "2025-10-15T18:22:58.53Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/61/2c/aced60e9cf9d0cde341d54bf7932c9ffc33ddb4a1595798b3a5150c7ec4e/pillow-12.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:8d8ca2b210ada074d57fcee40c30446c9562e542fc46aedc19baf758a93532ee", size = 6490915, upload-time = "2025-10-15T18:23:00.582Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/26/69dcb9b91f4e59f8f34b2332a4a0a951b44f547c4ed39d3e4dcfcff48f89/pillow-12.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:99a7f72fb6249302aa62245680754862a44179b545ded638cf1fef59befb57ef", size = 7157998, upload-time = "2025-10-15T18:23:02.627Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/61/2b/726235842220ca95fa441ddf55dd2382b52ab5b8d9c0596fe6b3f23dafe8/pillow-12.0.0-cp313-cp313t-win32.whl", hash = "sha256:4078242472387600b2ce8d93ade8899c12bf33fa89e55ec89fe126e9d6d5d9e9", size = 6306201, upload-time = "2025-10-15T18:23:04.709Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/3d/2afaf4e840b2df71344ababf2f8edd75a705ce500e5dc1e7227808312ae1/pillow-12.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2c54c1a783d6d60595d3514f0efe9b37c8808746a66920315bfd34a938d7994b", size = 7013165, upload-time = "2025-10-15T18:23:06.46Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/75/3fa09aa5cf6ed04bee3fa575798ddf1ce0bace8edb47249c798077a81f7f/pillow-12.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:26d9f7d2b604cd23aba3e9faf795787456ac25634d82cd060556998e39c6fa47", size = 2437834, upload-time = "2025-10-15T18:23:08.194Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/2a/9a8c6ba2c2c07b71bec92cf63e03370ca5e5f5c5b119b742bcc0cde3f9c5/pillow-12.0.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:beeae3f27f62308f1ddbcfb0690bf44b10732f2ef43758f169d5e9303165d3f9", size = 4045531, upload-time = "2025-10-15T18:23:10.121Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/54/836fdbf1bfb3d66a59f0189ff0b9f5f666cee09c6188309300df04ad71fa/pillow-12.0.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:d4827615da15cd59784ce39d3388275ec093ae3ee8d7f0c089b76fa87af756c2", size = 4120554, upload-time = "2025-10-15T18:23:12.14Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/cd/16aec9f0da4793e98e6b54778a5fbce4f375c6646fe662e80600b8797379/pillow-12.0.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:3e42edad50b6909089750e65c91aa09aaf1e0a71310d383f11321b27c224ed8a", size = 3576812, upload-time = "2025-10-15T18:23:13.962Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/b7/13957fda356dc46339298b351cae0d327704986337c3c69bb54628c88155/pillow-12.0.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e5d8efac84c9afcb40914ab49ba063d94f5dbdf5066db4482c66a992f47a3a3b", size = 5252689, upload-time = "2025-10-15T18:23:15.562Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/f5/eae31a306341d8f331f43edb2e9122c7661b975433de5e447939ae61c5da/pillow-12.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:266cd5f2b63ff316d5a1bba46268e603c9caf5606d44f38c2873c380950576ad", size = 4650186, upload-time = "2025-10-15T18:23:17.379Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/62/2a88339aa40c4c77e79108facbd307d6091e2c0eb5b8d3cf4977cfca2fe6/pillow-12.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58eea5ebe51504057dd95c5b77d21700b77615ab0243d8152793dc00eb4faf01", size = 6230308, upload-time = "2025-10-15T18:23:18.971Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/33/5425a8992bcb32d1cb9fa3dd39a89e613d09a22f2c8083b7bf43c455f760/pillow-12.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f13711b1a5ba512d647a0e4ba79280d3a9a045aaf7e0cc6fbe96b91d4cdf6b0c", size = 8039222, upload-time = "2025-10-15T18:23:20.909Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/61/3f5d3b35c5728f37953d3eec5b5f3e77111949523bd2dd7f31a851e50690/pillow-12.0.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6846bd2d116ff42cba6b646edf5bf61d37e5cbd256425fa089fee4ff5c07a99e", size = 6346657, upload-time = "2025-10-15T18:23:23.077Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/be/ee90a3d79271227e0f0a33c453531efd6ed14b2e708596ba5dd9be948da3/pillow-12.0.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c98fa880d695de164b4135a52fd2e9cd7b7c90a9d8ac5e9e443a24a95ef9248e", size = 7038482, upload-time = "2025-10-15T18:23:25.005Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/34/a16b6a4d1ad727de390e9bd9f19f5f669e079e5826ec0f329010ddea492f/pillow-12.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa3ed2a29a9e9d2d488b4da81dcb54720ac3104a20bf0bd273f1e4648aff5af9", size = 6461416, upload-time = "2025-10-15T18:23:27.009Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/39/1aa5850d2ade7d7ba9f54e4e4c17077244ff7a2d9e25998c38a29749eb3f/pillow-12.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d034140032870024e6b9892c692fe2968493790dd57208b2c37e3fb35f6df3ab", size = 7131584, upload-time = "2025-10-15T18:23:29.752Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/db/4fae862f8fad0167073a7733973bfa955f47e2cac3dc3e3e6257d10fab4a/pillow-12.0.0-cp314-cp314-win32.whl", hash = "sha256:1b1b133e6e16105f524a8dec491e0586d072948ce15c9b914e41cdadd209052b", size = 6400621, upload-time = "2025-10-15T18:23:32.06Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/24/b350c31543fb0107ab2599464d7e28e6f856027aadda995022e695313d94/pillow-12.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:8dc232e39d409036af549c86f24aed8273a40ffa459981146829a324e0848b4b", size = 7142916, upload-time = "2025-10-15T18:23:34.71Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/9b/0ba5a6fd9351793996ef7487c4fdbde8d3f5f75dbedc093bb598648fddf0/pillow-12.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:d52610d51e265a51518692045e372a4c363056130d922a7351429ac9f27e70b0", size = 2523836, upload-time = "2025-10-15T18:23:36.967Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/7a/ceee0840aebc579af529b523d530840338ecf63992395842e54edc805987/pillow-12.0.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1979f4566bb96c1e50a62d9831e2ea2d1211761e5662afc545fa766f996632f6", size = 5255092, upload-time = "2025-10-15T18:23:38.573Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/76/20776057b4bfd1aef4eeca992ebde0f53a4dce874f3ae693d0ec90a4f79b/pillow-12.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b2e4b27a6e15b04832fe9bf292b94b5ca156016bbc1ea9c2c20098a0320d6cf6", size = 4653158, upload-time = "2025-10-15T18:23:40.238Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/3f/d9ff92ace07be8836b4e7e87e6a4c7a8318d47c2f1463ffcf121fc57d9cb/pillow-12.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fb3096c30df99fd01c7bf8e544f392103d0795b9f98ba71a8054bcbf56b255f1", size = 6267882, upload-time = "2025-10-15T18:23:42.434Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/7a/4f7ff87f00d3ad33ba21af78bfcd2f032107710baf8280e3722ceec28cda/pillow-12.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7438839e9e053ef79f7112c881cef684013855016f928b168b81ed5835f3e75e", size = 8071001, upload-time = "2025-10-15T18:23:44.29Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/87/fcea108944a52dad8cca0715ae6247e271eb80459364a98518f1e4f480c1/pillow-12.0.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d5c411a8eaa2299322b647cd932586b1427367fd3184ffbb8f7a219ea2041ca", size = 6380146, upload-time = "2025-10-15T18:23:46.065Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/52/0d31b5e571ef5fd111d2978b84603fce26aba1b6092f28e941cb46570745/pillow-12.0.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7e091d464ac59d2c7ad8e7e08105eaf9dafbc3883fd7265ffccc2baad6ac925", size = 7067344, upload-time = "2025-10-15T18:23:47.898Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/f4/2dd3d721f875f928d48e83bb30a434dee75a2531bca839bb996bb0aa5a91/pillow-12.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:792a2c0be4dcc18af9d4a2dfd8a11a17d5e25274a1062b0ec1c2d79c76f3e7f8", size = 6491864, upload-time = "2025-10-15T18:23:49.607Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/4b/667dfcf3d61fc309ba5a15b141845cece5915e39b99c1ceab0f34bf1d124/pillow-12.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:afbefa430092f71a9593a99ab6a4e7538bc9eabbf7bf94f91510d3503943edc4", size = 7158911, upload-time = "2025-10-15T18:23:51.351Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/2f/16cabcc6426c32218ace36bf0d55955e813f2958afddbf1d391849fee9d1/pillow-12.0.0-cp314-cp314t-win32.whl", hash = "sha256:3830c769decf88f1289680a59d4f4c46c72573446352e2befec9a8512104fa52", size = 6408045, upload-time = "2025-10-15T18:23:53.177Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/73/e29aa0c9c666cf787628d3f0dcf379f4791fba79f4936d02f8b37165bdf8/pillow-12.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:905b0365b210c73afb0ebe9101a32572152dfd1c144c7e28968a331b9217b94a", size = 7148282, upload-time = "2025-10-15T18:23:55.316Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/70/6b41bdcddf541b437bbb9f47f94d2db5d9ddef6c37ccab8c9107743748a4/pillow-12.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:99353a06902c2e43b43e8ff74ee65a7d90307d82370604746738a1e0661ccca7", size = 2525630, upload-time = "2025-10-15T18:23:57.149Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/c7/2530a4aa28248623e9d7f27316b42e27c32ec410f695929696f2e0e4a778/pillow-12.1.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7b5dd7cbae20285cdb597b10eb5a2c13aa9de6cde9bb64a3c1317427b1db1ae1", size = 4062543, upload-time = "2026-01-02T09:11:31.566Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/1f/40b8eae823dc1519b87d53c30ed9ef085506b05281d313031755c1705f73/pillow-12.1.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:29a4cef9cb672363926f0470afc516dbf7305a14d8c54f7abbb5c199cd8f8179", size = 4138373, upload-time = "2026-01-02T09:11:33.367Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/77/6fa60634cf06e52139fd0e89e5bbf055e8166c691c42fb162818b7fda31d/pillow-12.1.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:681088909d7e8fa9e31b9799aaa59ba5234c58e5e4f1951b4c4d1082a2e980e0", size = 3601241, upload-time = "2026-01-02T09:11:35.011Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/bf/28ab865de622e14b747f0cd7877510848252d950e43002e224fb1c9ababf/pillow-12.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:983976c2ab753166dc66d36af6e8ec15bb511e4a25856e2227e5f7e00a160587", size = 5262410, upload-time = "2026-01-02T09:11:36.682Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/34/583420a1b55e715937a85bd48c5c0991598247a1fd2eb5423188e765ea02/pillow-12.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:db44d5c160a90df2d24a24760bbd37607d53da0b34fb546c4c232af7192298ac", size = 4657312, upload-time = "2026-01-02T09:11:38.535Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/fd/f5a0896839762885b3376ff04878f86ab2b097c2f9a9cdccf4eda8ba8dc0/pillow-12.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6b7a9d1db5dad90e2991645874f708e87d9a3c370c243c2d7684d28f7e133e6b", size = 6232605, upload-time = "2026-01-02T09:11:40.602Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/aa/938a09d127ac1e70e6ed467bd03834350b33ef646b31edb7452d5de43792/pillow-12.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6258f3260986990ba2fa8a874f8b6e808cf5abb51a94015ca3dc3c68aa4f30ea", size = 8041617, upload-time = "2026-01-02T09:11:42.721Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/e8/538b24cb426ac0186e03f80f78bc8dc7246c667f58b540bdd57c71c9f79d/pillow-12.1.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e115c15e3bc727b1ca3e641a909f77f8ca72a64fff150f666fcc85e57701c26c", size = 6346509, upload-time = "2026-01-02T09:11:44.955Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/9a/632e58ec89a32738cabfd9ec418f0e9898a2b4719afc581f07c04a05e3c9/pillow-12.1.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6741e6f3074a35e47c77b23a4e4f2d90db3ed905cb1c5e6e0d49bff2045632bc", size = 7038117, upload-time = "2026-01-02T09:11:46.736Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/a2/d40308cf86eada842ca1f3ffa45d0ca0df7e4ab33c83f81e73f5eaed136d/pillow-12.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:935b9d1aed48fcfb3f838caac506f38e29621b44ccc4f8a64d575cb1b2a88644", size = 6460151, upload-time = "2026-01-02T09:11:48.625Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/88/f5b058ad6453a085c5266660a1417bdad590199da1b32fb4efcff9d33b05/pillow-12.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5fee4c04aad8932da9f8f710af2c1a15a83582cfb884152a9caa79d4efcdbf9c", size = 7164534, upload-time = "2026-01-02T09:11:50.445Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/ce/c17334caea1db789163b5d855a5735e47995b0b5dc8745e9a3605d5f24c0/pillow-12.1.0-cp313-cp313-win32.whl", hash = "sha256:a786bf667724d84aa29b5db1c61b7bfdde380202aaca12c3461afd6b71743171", size = 6332551, upload-time = "2026-01-02T09:11:52.234Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/07/74a9d941fa45c90a0d9465098fe1ec85de3e2afbdc15cc4766622d516056/pillow-12.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:461f9dfdafa394c59cd6d818bdfdbab4028b83b02caadaff0ffd433faf4c9a7a", size = 7040087, upload-time = "2026-01-02T09:11:54.822Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/09/c99950c075a0e9053d8e880595926302575bc742b1b47fe1bbcc8d388d50/pillow-12.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:9212d6b86917a2300669511ed094a9406888362e085f2431a7da985a6b124f45", size = 2452470, upload-time = "2026-01-02T09:11:56.522Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/ba/970b7d85ba01f348dee4d65412476321d40ee04dcb51cd3735b9dc94eb58/pillow-12.1.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:00162e9ca6d22b7c3ee8e61faa3c3253cd19b6a37f126cad04f2f88b306f557d", size = 5264816, upload-time = "2026-01-02T09:11:58.227Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/60/650f2fb55fdba7a510d836202aa52f0baac633e50ab1cf18415d332188fb/pillow-12.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7d6daa89a00b58c37cb1747ec9fb7ac3bc5ffd5949f5888657dfddde6d1312e0", size = 4660472, upload-time = "2026-01-02T09:12:00.798Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/c0/5273a99478956a099d533c4f46cbaa19fd69d606624f4334b85e50987a08/pillow-12.1.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e2479c7f02f9d505682dc47df8c0ea1fc5e264c4d1629a5d63fe3e2334b89554", size = 6268974, upload-time = "2026-01-02T09:12:02.572Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/26/0bf714bc2e73d5267887d47931d53c4ceeceea6978148ed2ab2a4e6463c4/pillow-12.1.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f188d580bd870cda1e15183790d1cc2fa78f666e76077d103edf048eed9c356e", size = 8073070, upload-time = "2026-01-02T09:12:04.75Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/cf/1ea826200de111a9d65724c54f927f3111dc5ae297f294b370a670c17786/pillow-12.1.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0fde7ec5538ab5095cc02df38ee99b0443ff0e1c847a045554cf5f9af1f4aa82", size = 6380176, upload-time = "2026-01-02T09:12:06.626Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/e0/7938dd2b2013373fd85d96e0f38d62b7a5a262af21ac274250c7ca7847c9/pillow-12.1.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0ed07dca4a8464bada6139ab38f5382f83e5f111698caf3191cb8dbf27d908b4", size = 7067061, upload-time = "2026-01-02T09:12:08.624Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/ad/a2aa97d37272a929a98437a8c0ac37b3cf012f4f8721e1bd5154699b2518/pillow-12.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f45bd71d1fa5e5749587613037b172e0b3b23159d1c00ef2fc920da6f470e6f0", size = 6491824, upload-time = "2026-01-02T09:12:10.488Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/44/80e46611b288d51b115826f136fb3465653c28f491068a72d3da49b54cd4/pillow-12.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:277518bf4fe74aa91489e1b20577473b19ee70fb97c374aa50830b279f25841b", size = 7190911, upload-time = "2026-01-02T09:12:12.772Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/77/eacc62356b4cf81abe99ff9dbc7402750044aed02cfd6a503f7c6fc11f3e/pillow-12.1.0-cp313-cp313t-win32.whl", hash = "sha256:7315f9137087c4e0ee73a761b163fc9aa3b19f5f606a7fc08d83fd3e4379af65", size = 6336445, upload-time = "2026-01-02T09:12:14.775Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/3c/57d81d0b74d218706dafccb87a87ea44262c43eef98eb3b164fd000e0491/pillow-12.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:0ddedfaa8b5f0b4ffbc2fa87b556dc59f6bb4ecb14a53b33f9189713ae8053c0", size = 7045354, upload-time = "2026-01-02T09:12:16.599Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/82/8b9b97bba2e3576a340f93b044a3a3a09841170ab4c1eb0d5c93469fd32f/pillow-12.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:80941e6d573197a0c28f394753de529bb436b1ca990ed6e765cf42426abc39f8", size = 2454547, upload-time = "2026-01-02T09:12:18.704Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/87/bdf971d8bbcf80a348cc3bacfcb239f5882100fe80534b0ce67a784181d8/pillow-12.1.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:5cb7bc1966d031aec37ddb9dcf15c2da5b2e9f7cc3ca7c54473a20a927e1eb91", size = 4062533, upload-time = "2026-01-02T09:12:20.791Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/4f/5eb37a681c68d605eb7034c004875c81f86ec9ef51f5be4a63eadd58859a/pillow-12.1.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:97e9993d5ed946aba26baf9c1e8cf18adbab584b99f452ee72f7ee8acb882796", size = 4138546, upload-time = "2026-01-02T09:12:23.664Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/6d/19a95acb2edbace40dcd582d077b991646b7083c41b98da4ed7555b59733/pillow-12.1.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:414b9a78e14ffeb98128863314e62c3f24b8a86081066625700b7985b3f529bd", size = 3601163, upload-time = "2026-01-02T09:12:26.338Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/36/2b8138e51cb42e4cc39c3297713455548be855a50558c3ac2beebdc251dd/pillow-12.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e6bdb408f7c9dd2a5ff2b14a3b0bb6d4deb29fb9961e6eb3ae2031ae9a5cec13", size = 5266086, upload-time = "2026-01-02T09:12:28.782Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/53/4b/649056e4d22e1caa90816bf99cef0884aed607ed38075bd75f091a607a38/pillow-12.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3413c2ae377550f5487991d444428f1a8ae92784aac79caa8b1e3b89b175f77e", size = 4657344, upload-time = "2026-01-02T09:12:31.117Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/6b/c5742cea0f1ade0cd61485dc3d81f05261fc2276f537fbdc00802de56779/pillow-12.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e5dcbe95016e88437ecf33544ba5db21ef1b8dd6e1b434a2cb2a3d605299e643", size = 6232114, upload-time = "2026-01-02T09:12:32.936Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/8f/9f521268ce22d63991601aafd3d48d5ff7280a246a1ef62d626d67b44064/pillow-12.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d0a7735df32ccbcc98b98a1ac785cc4b19b580be1bdf0aeb5c03223220ea09d5", size = 8042708, upload-time = "2026-01-02T09:12:34.78Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/eb/257f38542893f021502a1bbe0c2e883c90b5cff26cc33b1584a841a06d30/pillow-12.1.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c27407a2d1b96774cbc4a7594129cc027339fd800cd081e44497722ea1179de", size = 6347762, upload-time = "2026-01-02T09:12:36.748Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/5a/8ba375025701c09b309e8d5163c5a4ce0102fa86bbf8800eb0d7ac87bc51/pillow-12.1.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15c794d74303828eaa957ff8070846d0efe8c630901a1c753fdc63850e19ecd9", size = 7039265, upload-time = "2026-01-02T09:12:39.082Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/dc/cf5e4cdb3db533f539e88a7bbf9f190c64ab8a08a9bc7a4ccf55067872e4/pillow-12.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c990547452ee2800d8506c4150280757f88532f3de2a58e3022e9b179107862a", size = 6462341, upload-time = "2026-01-02T09:12:40.946Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/47/0291a25ac9550677e22eda48510cfc4fa4b2ef0396448b7fbdc0a6946309/pillow-12.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b63e13dd27da389ed9475b3d28510f0f954bca0041e8e551b2a4eb1eab56a39a", size = 7165395, upload-time = "2026-01-02T09:12:42.706Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/4c/e005a59393ec4d9416be06e6b45820403bb946a778e39ecec62f5b2b991e/pillow-12.1.0-cp314-cp314-win32.whl", hash = "sha256:1a949604f73eb07a8adab38c4fe50791f9919344398bdc8ac6b307f755fc7030", size = 6431413, upload-time = "2026-01-02T09:12:44.944Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/af/f23697f587ac5f9095d67e31b81c95c0249cd461a9798a061ed6709b09b5/pillow-12.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:4f9f6a650743f0ddee5593ac9e954ba1bdbc5e150bc066586d4f26127853ab94", size = 7176779, upload-time = "2026-01-02T09:12:46.727Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/36/6a51abf8599232f3e9afbd16d52829376a68909fe14efe29084445db4b73/pillow-12.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:808b99604f7873c800c4840f55ff389936ef1948e4e87645eaf3fccbc8477ac4", size = 2543105, upload-time = "2026-01-02T09:12:49.243Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/54/2e1dd20c8749ff225080d6ba465a0cab4387f5db0d1c5fb1439e2d99923f/pillow-12.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bc11908616c8a283cf7d664f77411a5ed2a02009b0097ff8abbba5e79128ccf2", size = 5268571, upload-time = "2026-01-02T09:12:51.11Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/61/571163a5ef86ec0cf30d265ac2a70ae6fc9e28413d1dc94fa37fae6bda89/pillow-12.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:896866d2d436563fa2a43a9d72f417874f16b5545955c54a64941e87c1376c61", size = 4660426, upload-time = "2026-01-02T09:12:52.865Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/e1/53ee5163f794aef1bf84243f755ee6897a92c708505350dd1923f4afec48/pillow-12.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8e178e3e99d3c0ea8fc64b88447f7cac8ccf058af422a6cedc690d0eadd98c51", size = 6269908, upload-time = "2026-01-02T09:12:54.884Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/0b/b4b4106ff0ee1afa1dc599fde6ab230417f800279745124f6c50bcffed8e/pillow-12.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:079af2fb0c599c2ec144ba2c02766d1b55498e373b3ac64687e43849fbbef5bc", size = 8074733, upload-time = "2026-01-02T09:12:56.802Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/9f/80b411cbac4a732439e629a26ad3ef11907a8c7fc5377b7602f04f6fe4e7/pillow-12.1.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bdec5e43377761c5dbca620efb69a77f6855c5a379e32ac5b158f54c84212b14", size = 6381431, upload-time = "2026-01-02T09:12:58.823Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/b7/d65c45db463b66ecb6abc17c6ba6917a911202a07662247e1355ce1789e7/pillow-12.1.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:565c986f4b45c020f5421a4cea13ef294dde9509a8577f29b2fc5edc7587fff8", size = 7068529, upload-time = "2026-01-02T09:13:00.885Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/96/dfd4cd726b4a45ae6e3c669fc9e49deb2241312605d33aba50499e9d9bd1/pillow-12.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:43aca0a55ce1eefc0aefa6253661cb54571857b1a7b2964bd8a1e3ef4b729924", size = 6492981, upload-time = "2026-01-02T09:13:03.314Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/1c/b5dc52cf713ae46033359c5ca920444f18a6359ce1020dd3e9c553ea5bc6/pillow-12.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0deedf2ea233722476b3a81e8cdfbad786f7adbed5d848469fa59fe52396e4ef", size = 7191878, upload-time = "2026-01-02T09:13:05.276Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/53/26/c4188248bd5edaf543864fe4834aebe9c9cb4968b6f573ce014cc42d0720/pillow-12.1.0-cp314-cp314t-win32.whl", hash = "sha256:b17fbdbe01c196e7e159aacb889e091f28e61020a8abeac07b68079b6e626988", size = 6438703, upload-time = "2026-01-02T09:13:07.491Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/0e/69ed296de8ea05cb03ee139cee600f424ca166e632567b2d66727f08c7ed/pillow-12.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27b9baecb428899db6c0de572d6d305cfaf38ca1596b5c0542a5182e3e74e8c6", size = 7182927, upload-time = "2026-01-02T09:13:09.841Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/f5/68334c015eed9b5cff77814258717dec591ded209ab5b6fb70e2ae873d1d/pillow-12.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:f61333d817698bdcdd0f9d7793e365ac3d2a21c1f1eb02b32ad6aefb8d8ea831", size = 2545104, upload-time = "2026-01-02T09:13:12.068Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -389,11 +413,11 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "pypdf"
|
||||
version = "6.1.1"
|
||||
version = "6.5.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a6/85/4c0f12616db83c2e3ef580c3cfa98bd082e88fc8d02e136bad3bede1e3fa/pypdf-6.1.1.tar.gz", hash = "sha256:10f44d49bf2a82e54c3c5ba3cdcbb118f2a44fc57df8ce51d6fb9b1ed9bfbe8b", size = 5074507, upload-time = "2025-09-28T13:29:16.165Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/4d/9b/db1056a54eda8cd44f9e5128e87e1142cb328295dad92bbec0d39f251641/pypdf-6.5.0.tar.gz", hash = "sha256:9e78950906380ae4f2ce1d9039e9008098ba6366a4d9c7423c4bdbd6e6683404", size = 5277655, upload-time = "2025-12-21T11:07:19.876Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/07/ed/adae13756d9dabdddee483fc7712905bb5585fbf6e922b1a19aca3a29cd1/pypdf-6.1.1-py3-none-any.whl", hash = "sha256:7781f99493208a37a7d4275601d883e19af24e62a525c25844d22157c2e4cde7", size = 323455, upload-time = "2025-09-28T13:29:14.392Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/db/f2e7703791a1f32532618b82789ddddb7173b9e22d97e34cc11950d8e330/pypdf-6.5.0-py3-none-any.whl", hash = "sha256:9cef8002aaedeecf648dfd9ff1ce38f20ae8d88e2534fced6630038906440b25", size = 329560, upload-time = "2025-12-21T11:07:18.173Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -410,40 +434,40 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "pyupgrade"
|
||||
version = "3.21.0"
|
||||
version = "3.21.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "tokenize-rt" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/cb/11/b08f5e4d50575c944e4ea0a86f070a1ba2c0d5a4dc42fac571a605ada78d/pyupgrade-3.21.0.tar.gz", hash = "sha256:3e63a882ec1d16f5621736d938952df3cdc2446501fb049e711415cb8d273960", size = 45210, upload-time = "2025-10-09T19:32:26.448Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/7f/a1/dc63caaeed232b1c58eae1b7a75f262d64ab8435882f696ffa9b58c0c415/pyupgrade-3.21.2.tar.gz", hash = "sha256:1a361bea39deda78d1460f65d9dd548d3a36ff8171d2482298539b9dc11c9c06", size = 45455, upload-time = "2025-11-19T00:39:48.012Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/4a/d18f2d6b2f9b90ec7e10843ea10e4cde0dc36091782443ee923e29d653bb/pyupgrade-3.21.0-py2.py3-none-any.whl", hash = "sha256:3a200cdc537980f371b4867f1d662e37166a7869d7713f309c1c3241c310c754", size = 62638, upload-time = "2025-10-09T19:32:25.086Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/8c/433dac11910989a90c40b10149d07ef7224232236971a562d3976790ec53/pyupgrade-3.21.2-py2.py3-none-any.whl", hash = "sha256:2ac7b95cbd176475041e4dfe8ef81298bd4654a244f957167bd68af37d52be9f", size = 62814, upload-time = "2025-11-19T00:39:46.958Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.14.1"
|
||||
version = "0.14.10"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9e/58/6ca66896635352812de66f71cdf9ff86b3a4f79071ca5730088c0cd0fc8d/ruff-0.14.1.tar.gz", hash = "sha256:1dd86253060c4772867c61791588627320abcb6ed1577a90ef432ee319729b69", size = 5513429, upload-time = "2025-10-16T18:05:41.766Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/57/08/52232a877978dd8f9cf2aeddce3e611b40a63287dfca29b6b8da791f5e8d/ruff-0.14.10.tar.gz", hash = "sha256:9a2e830f075d1a42cd28420d7809ace390832a490ed0966fe373ba288e77aaf4", size = 5859763, upload-time = "2025-12-18T19:28:57.98Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/39/9cc5ab181478d7a18adc1c1e051a84ee02bec94eb9bdfd35643d7c74ca31/ruff-0.14.1-py3-none-linux_armv6l.whl", hash = "sha256:083bfc1f30f4a391ae09c6f4f99d83074416b471775b59288956f5bc18e82f8b", size = 12445415, upload-time = "2025-10-16T18:04:48.227Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/2e/1226961855ccd697255988f5a2474890ac7c5863b080b15bd038df820818/ruff-0.14.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:f6fa757cd717f791009f7669fefb09121cc5f7d9bd0ef211371fad68c2b8b224", size = 12784267, upload-time = "2025-10-16T18:04:52.515Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/ea/fd9e95863124ed159cd0667ec98449ae461de94acda7101f1acb6066da00/ruff-0.14.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d6191903d39ac156921398e9c86b7354d15e3c93772e7dbf26c9fcae59ceccd5", size = 11781872, upload-time = "2025-10-16T18:04:55.396Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/5a/e890f7338ff537dba4589a5e02c51baa63020acfb7c8cbbaea4831562c96/ruff-0.14.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed04f0e04f7a4587244e5c9d7df50e6b5bf2705d75059f409a6421c593a35896", size = 12226558, upload-time = "2025-10-16T18:04:58.166Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/7a/8ab5c3377f5bf31e167b73651841217542bcc7aa1c19e83030835cc25204/ruff-0.14.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5c9e6cf6cd4acae0febbce29497accd3632fe2025c0c583c8b87e8dbdeae5f61", size = 12187898, upload-time = "2025-10-16T18:05:01.455Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/8d/ba7c33aa55406955fc124e62c8259791c3d42e3075a71710fdff9375134f/ruff-0.14.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a6fa2458527794ecdfbe45f654e42c61f2503a230545a91af839653a0a93dbc6", size = 12939168, upload-time = "2025-10-16T18:05:04.397Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/c2/70783f612b50f66d083380e68cbd1696739d88e9b4f6164230375532c637/ruff-0.14.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:39f1c392244e338b21d42ab29b8a6392a722c5090032eb49bb4d6defcdb34345", size = 14386942, upload-time = "2025-10-16T18:05:07.102Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/44/cd7abb9c776b66d332119d67f96acf15830d120f5b884598a36d9d3f4d83/ruff-0.14.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7382fa12a26cce1f95070ce450946bec357727aaa428983036362579eadcc5cf", size = 13990622, upload-time = "2025-10-16T18:05:09.882Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/56/4259b696db12ac152fe472764b4f78bbdd9b477afd9bc3a6d53c01300b37/ruff-0.14.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd0bf2be3ae8521e1093a487c4aa3b455882f139787770698530d28ed3fbb37c", size = 13431143, upload-time = "2025-10-16T18:05:13.46Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/35/266a80d0eb97bd224b3265b9437bd89dde0dcf4faf299db1212e81824e7e/ruff-0.14.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cabcaa9ccf8089fb4fdb78d17cc0e28241520f50f4c2e88cb6261ed083d85151", size = 13132844, upload-time = "2025-10-16T18:05:16.1Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/6e/d31ce218acc11a8d91ef208e002a31acf315061a85132f94f3df7a252b18/ruff-0.14.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:747d583400f6125ec11a4c14d1c8474bf75d8b419ad22a111a537ec1a952d192", size = 13401241, upload-time = "2025-10-16T18:05:19.395Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/b5/dbc4221bf0b03774b3b2f0d47f39e848d30664157c15b965a14d890637d2/ruff-0.14.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5a6e74c0efd78515a1d13acbfe6c90f0f5bd822aa56b4a6d43a9ffb2ae6e56cd", size = 12132476, upload-time = "2025-10-16T18:05:22.163Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/4b/ac99194e790ccd092d6a8b5f341f34b6e597d698e3077c032c502d75ea84/ruff-0.14.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0ea6a864d2fb41a4b6d5b456ed164302a0d96f4daac630aeba829abfb059d020", size = 12139749, upload-time = "2025-10-16T18:05:25.162Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/26/7df917462c3bb5004e6fdfcc505a49e90bcd8a34c54a051953118c00b53a/ruff-0.14.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:0826b8764f94229604fa255918d1cc45e583e38c21c203248b0bfc9a0e930be5", size = 12544758, upload-time = "2025-10-16T18:05:28.018Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/d0/81e7f0648e9764ad9b51dd4be5e5dac3fcfff9602428ccbae288a39c2c22/ruff-0.14.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:cbc52160465913a1a3f424c81c62ac8096b6a491468e7d872cb9444a860bc33d", size = 13221811, upload-time = "2025-10-16T18:05:30.707Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/07/3c45562c67933cc35f6d5df4ca77dabbcd88fddaca0d6b8371693d29fd56/ruff-0.14.1-py3-none-win32.whl", hash = "sha256:e037ea374aaaff4103240ae79168c0945ae3d5ae8db190603de3b4012bd1def6", size = 12319467, upload-time = "2025-10-16T18:05:33.261Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/88/0ee4ca507d4aa05f67e292d2e5eb0b3e358fbcfe527554a2eda9ac422d6b/ruff-0.14.1-py3-none-win_amd64.whl", hash = "sha256:59d599cdff9c7f925a017f6f2c256c908b094e55967f93f2821b1439928746a1", size = 13401123, upload-time = "2025-10-16T18:05:35.984Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/81/4b6387be7014858d924b843530e1b2a8e531846807516e9bea2ee0936bf7/ruff-0.14.1-py3-none-win_arm64.whl", hash = "sha256:e3b443c4c9f16ae850906b8d0a707b2a4c16f8d2f0a7fe65c475c5886665ce44", size = 12436636, upload-time = "2025-10-16T18:05:38.995Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/01/933704d69f3f05ee16ef11406b78881733c186fe14b6a46b05cfcaf6d3b2/ruff-0.14.10-py3-none-linux_armv6l.whl", hash = "sha256:7a3ce585f2ade3e1f29ec1b92df13e3da262178df8c8bdf876f48fa0e8316c49", size = 13527080, upload-time = "2025-12-18T19:29:25.642Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/58/a0349197a7dfa603ffb7f5b0470391efa79ddc327c1e29c4851e85b09cc5/ruff-0.14.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:674f9be9372907f7257c51f1d4fc902cb7cf014b9980152b802794317941f08f", size = 13797320, upload-time = "2025-12-18T19:29:02.571Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/82/36be59f00a6082e38c23536df4e71cdbc6af8d7c707eade97fcad5c98235/ruff-0.14.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d85713d522348837ef9df8efca33ccb8bd6fcfc86a2cde3ccb4bc9d28a18003d", size = 12918434, upload-time = "2025-12-18T19:28:51.202Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/00/45c62a7f7e34da92a25804f813ebe05c88aa9e0c25e5cb5a7d23dd7450e3/ruff-0.14.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6987ebe0501ae4f4308d7d24e2d0fe3d7a98430f5adfd0f1fead050a740a3a77", size = 13371961, upload-time = "2025-12-18T19:29:04.991Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/31/a5906d60f0405f7e57045a70f2d57084a93ca7425f22e1d66904769d1628/ruff-0.14.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:16a01dfb7b9e4eee556fbfd5392806b1b8550c9b4a9f6acd3dbe6812b193c70a", size = 13275629, upload-time = "2025-12-18T19:29:21.381Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/60/61c0087df21894cf9d928dc04bcd4fb10e8b2e8dca7b1a276ba2155b2002/ruff-0.14.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7165d31a925b7a294465fa81be8c12a0e9b60fb02bf177e79067c867e71f8b1f", size = 14029234, upload-time = "2025-12-18T19:29:00.132Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/84/77d911bee3b92348b6e5dab5a0c898d87084ea03ac5dc708f46d88407def/ruff-0.14.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c561695675b972effb0c0a45db233f2c816ff3da8dcfbe7dfc7eed625f218935", size = 15449890, upload-time = "2025-12-18T19:28:53.573Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/36/480206eaefa24a7ec321582dda580443a8f0671fdbf6b1c80e9c3e93a16a/ruff-0.14.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4bb98fcbbc61725968893682fd4df8966a34611239c9fd07a1f6a07e7103d08e", size = 15123172, upload-time = "2025-12-18T19:29:23.453Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/38/68e414156015ba80cef5473d57919d27dfb62ec804b96180bafdeaf0e090/ruff-0.14.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f24b47993a9d8cb858429e97bdf8544c78029f09b520af615c1d261bf827001d", size = 14460260, upload-time = "2025-12-18T19:29:27.808Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/19/9e050c0dca8aba824d67cc0db69fb459c28d8cd3f6855b1405b3f29cc91d/ruff-0.14.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59aabd2e2c4fd614d2862e7939c34a532c04f1084476d6833dddef4afab87e9f", size = 14229978, upload-time = "2025-12-18T19:29:11.32Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/eb/e8dd1dd6e05b9e695aa9dd420f4577debdd0f87a5ff2fedda33c09e9be8c/ruff-0.14.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:213db2b2e44be8625002dbea33bb9c60c66ea2c07c084a00d55732689d697a7f", size = 14338036, upload-time = "2025-12-18T19:29:09.184Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/12/f3e3a505db7c19303b70af370d137795fcfec136d670d5de5391e295c134/ruff-0.14.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b914c40ab64865a17a9a5b67911d14df72346a634527240039eb3bd650e5979d", size = 13264051, upload-time = "2025-12-18T19:29:13.431Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/64/8c3a47eaccfef8ac20e0484e68e0772013eb85802f8a9f7603ca751eb166/ruff-0.14.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1484983559f026788e3a5c07c81ef7d1e97c1c78ed03041a18f75df104c45405", size = 13283998, upload-time = "2025-12-18T19:29:06.994Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/12/84/534a5506f4074e5cc0529e5cd96cfc01bb480e460c7edf5af70d2bcae55e/ruff-0.14.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c70427132db492d25f982fffc8d6c7535cc2fd2c83fc8888f05caaa248521e60", size = 13601891, upload-time = "2025-12-18T19:28:55.811Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/1e/14c916087d8598917dbad9b2921d340f7884824ad6e9c55de948a93b106d/ruff-0.14.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5bcf45b681e9f1ee6445d317ce1fa9d6cba9a6049542d1c3d5b5958986be8830", size = 14336660, upload-time = "2025-12-18T19:29:16.531Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/1c/d7b67ab43f30013b47c12b42d1acd354c195351a3f7a1d67f59e54227ede/ruff-0.14.10-py3-none-win32.whl", hash = "sha256:104c49fc7ab73f3f3a758039adea978869a918f31b73280db175b43a2d9b51d6", size = 13196187, upload-time = "2025-12-18T19:29:19.006Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/9c/896c862e13886fae2af961bef3e6312db9ebc6adc2b156fe95e615dee8c1/ruff-0.14.10-py3-none-win_amd64.whl", hash = "sha256:466297bd73638c6bdf06485683e812db1c00c7ac96d4ddd0294a338c62fdc154", size = 14661283, upload-time = "2025-12-18T19:29:30.16Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/31/b0e29d572670dca3674eeee78e418f20bdf97fa8aa9ea71380885e175ca0/ruff-0.14.10-py3-none-win_arm64.whl", hash = "sha256:e51d046cf6dda98a4633b8a8a771451107413b0f07183b2bef03f075599e44e6", size = 13729839, upload-time = "2025-12-18T19:28:48.636Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -465,12 +489,21 @@ wheels = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sqlparse"
|
||||
version = "0.5.3"
|
||||
name = "sniffio"
|
||||
version = "1.3.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e5/40/edede8dd6977b0d3da179a342c198ed100dd2aba4be081861ee5911e4da4/sqlparse-0.5.3.tar.gz", hash = "sha256:09f67787f56a0b16ecdbde1bfc7f5d9c3371ca683cfeaa8e6ff60b4807ec9272", size = 84999, upload-time = "2024-12-10T12:05:30.728Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/5c/bfd6bd0bf979426d405cc6e71eceb8701b148b16c21d2dc3c261efc61c7b/sqlparse-0.5.3-py3-none-any.whl", hash = "sha256:cf2196ed3418f3ba5de6af7e82c694a9fbdbfecccdfc72e281548517081f16ca", size = 44415, upload-time = "2024-12-10T12:05:27.824Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sqlparse"
|
||||
version = "0.5.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/90/76/437d71068094df0726366574cf3432a4ed754217b436eb7429415cf2d480/sqlparse-0.5.5.tar.gz", hash = "sha256:e20d4a9b0b8585fdf63b10d30066c7c94c5d7a7ec47c889a2d83a3caa93ff28e", size = 120815, upload-time = "2025-12-19T07:17:45.073Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/49/4b/359f28a903c13438ef59ebeee215fb25da53066db67b305c125f1c6d2a25/sqlparse-0.5.5-py3-none-any.whl", hash = "sha256:12a08b3bf3eec877c519589833aed092e2444e68240a3577e8e26148acc7b1ba", size = 46138, upload-time = "2025-12-19T07:17:46.573Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -493,20 +526,20 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "tzdata"
|
||||
version = "2025.2"
|
||||
version = "2025.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload-time = "2025-03-23T13:54:43.652Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772, upload-time = "2025-12-13T17:45:35.667Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "2.5.0"
|
||||
version = "2.6.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1e/24/a2a2ed9addd907787d7aa0355ba36a6cadf1768b934c652ea78acbd59dcd/urllib3-2.6.2.tar.gz", hash = "sha256:016f9c98bb7e98085cb2b4b17b87d2c702975664e4f060c6532e64d1c1a5e797", size = 432930, upload-time = "2025-12-11T15:56:40.252Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/b9/4095b668ea3678bf6a0af005527f39de12fb026516fb3df17495a733b7f8/urllib3-2.6.2-py3-none-any.whl", hash = "sha256:ec21cddfe7724fc7cb4ba4bea7aa8e2ef36f607a4bab81aa6ce42a13dc3f03dd", size = 131182, upload-time = "2025-12-11T15:56:38.584Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
[uwsgi]
|
||||
#uid
|
||||
#gid
|
||||
master = true
|
||||
processes = 3
|
||||
#chown-socket =www-data:www-data
|
||||
module = fet2020.wsgi:application
|
||||
pidfile=/tmp/project-master.pid
|
||||
enable-threads = true
|
||||
venv=/opt/venv
|
||||
socket=/tmp/app.sock
|
||||
22
install.md
22
install.md
@@ -1,22 +0,0 @@
|
||||
== Install System from scratch ==
|
||||
=== Install Docker ===
|
||||
apt-get update
|
||||
|
||||
apt-get install \
|
||||
apt-transport-https \
|
||||
ca-certificates \
|
||||
curl \
|
||||
gnupg-agent \
|
||||
software-properties-common
|
||||
|
||||
curl -fsSL https://download.docker.com/linux/debian/gpg | apt-key add -
|
||||
|
||||
add-apt-repository \
|
||||
"deb [arch=amd64] https://download.docker.com/linux/debian \
|
||||
$(lsb_release -cs) \
|
||||
stable"
|
||||
|
||||
apt-get update
|
||||
apt-get install docker-ce docker-ce-cli containerd.io
|
||||
|
||||
docker run -d -p 8000:8000 -p 9000:9000 --name=portainer --restart=always -v /var/run/docker.sock:/var/run/docker.sock -v portainer_data:/data portainer/portainer-ce
|
||||
@@ -1,7 +0,0 @@
|
||||
#/bin/bash
|
||||
export REGISTRY="docker.triton2.fet.at"
|
||||
git clone https://git.fet.at/bofh/fet2020.git tmp/fet2020
|
||||
# Build a development Image with Theia and all content
|
||||
docker build -f Dockerfile.test -t $REGISTRY/fet2020django:latest .
|
||||
docker push $REGISTRY/fet2020django
|
||||
rm -rf ./tmp/fet2020
|
||||
@@ -1,19 +0,0 @@
|
||||
docker stop fetdjango
|
||||
docker container rm fetdjango
|
||||
docker stop mariadb
|
||||
docker container rm mariadb
|
||||
docker build -t fet2020django .
|
||||
docker network create fet-net
|
||||
docker run --name fetdjango --network fet-net -d -p 8080:8080 \
|
||||
-v /srv/deploy_1/app/fet2020:/app fet2020django
|
||||
docker run --name mariadb \
|
||||
--network fet-net -d -p 3306:3306 \
|
||||
-v mariadb2:/var/lib/mysql \
|
||||
# -e SKIP_INNODB=yes \
|
||||
-e MYSQL_DATABASE=wordpressdb \
|
||||
-e MYSQL_USER=wordpressuser \
|
||||
-e MYSQL_PASSWORD=hguyFt6S95dgfR4ryb \
|
||||
jbergstroem/mariadb-alpine
|
||||
|
||||
docker exec fetdjango ./initdb
|
||||
|
||||
11
runmariadb
11
runmariadb
@@ -1,11 +0,0 @@
|
||||
docker stop mysql
|
||||
docker container rm mysql
|
||||
docker volume rm mariadb2
|
||||
docker run -d --name mysql -p 3308:3306 \
|
||||
-v mariadb2:/var/lib/mysql \
|
||||
-e SKIP_INNODB=no \
|
||||
-e MYSQL_DATABASE=fet2020db \
|
||||
-e MYSQL_USER=user \
|
||||
-e MYSQL_PASSWORD=hgu \
|
||||
-e MYSQL_ROOT_PASSWORD=hgu \
|
||||
jbergstroem/mariadb-alpine
|
||||
Reference in New Issue
Block a user