forked from beba/foerderbarometer
simplified application views
This commit is contained in:
parent
84ef809705
commit
a7d3df7b39
|
|
@ -21,15 +21,9 @@
|
||||||
|
|
||||||
<strong>Serviceleistungen</strong>
|
<strong>Serviceleistungen</strong>
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="{% url 'bibliotheksstipendium' %}">Bibliotheksstipendium</a></li>
|
{% for info in types %}
|
||||||
<li><a href="{% url 'eliteraturstipendium' %}">eLiteraturstipendium</a></li>
|
<li><a href="{% url 'extern' type=info.path %}">{{ info.label|striptags }}</a></li>
|
||||||
<li><a href="{% url 'email' %}">E-Mail-Adresse</a></li>
|
{% endfor %}
|
||||||
<li><a href="{% url 'ifg' %}">Kostenübernahme IFG-Anfrage</a></li>
|
|
||||||
<li><a href="{% url 'literatur' %}">Literaturstipendium</a></li>
|
|
||||||
<li><a href="{% url 'mailingliste' %}">Mailingliste</a></li>
|
|
||||||
<li><a href="{% url 'reisekosten' %}">Reisekosten</a></li>
|
|
||||||
<li><a href="{% url 'softwarestipendium' %}">Softwarestipendium</a></li>
|
|
||||||
<li><a href="{% url 'visitenkarten' %}">Visitenkarten</a></li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
{{ form.media }}
|
{{ form.media }}
|
||||||
|
|
||||||
<div class="page-centered">
|
<div class="page-centered">
|
||||||
<p>Du hast {{ typestring }} ausgewählt.</p>
|
<p>Du hast {{ type_label }} ausgewählt.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form method="post" class="wm-form" {% if form.is_multipart %}enctype="multipart/form-data"{% endif %}>
|
<form method="post" class="wm-form" {% if form.is_multipart %}enctype="multipart/form-data"{% endif %}>
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ Hallo Team Communitys und Engagement,
|
||||||
<br><br>
|
<br><br>
|
||||||
es gab einen neuen Antrag von {{data.realname}}.
|
es gab einen neuen Antrag von {{data.realname}}.
|
||||||
<br><br>
|
<br><br>
|
||||||
Der Nutzer mit dem Username {{data.username}} ({{data.email}}) fragt ein_e {{data.typestring|striptags}} an.<br>
|
Der Nutzer mit dem Username {{data.username}} ({{data.email}}) fragt ein_e {{data.type_label|striptags}} an.<br>
|
||||||
{% if data.choice in data.grant %}<br>
|
{% if data.choice in data.grant %}<br>
|
||||||
Vorraussichtliche Kosten: {{data.cost}}<br>
|
Vorraussichtliche Kosten: {{data.cost}}<br>
|
||||||
Anmerkungen: {{data.notes}} {% endif %} {% if data.choice in data.domain %}<br>
|
Anmerkungen: {{data.notes}} {% endif %} {% if data.choice in data.domain %}<br>
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ Hallo Team Communitys und Engagement,
|
||||||
|
|
||||||
es gab einen neuen Antrag von {{data.realname}}.
|
es gab einen neuen Antrag von {{data.realname}}.
|
||||||
|
|
||||||
Der Nutzer mit dem Username {{data.username}} ({{data.email}}) fragt ein_e {{data.typestring|striptags}} an.
|
Der Nutzer mit dem Username {{data.username}} ({{data.email}}) fragt ein_e {{data.type_label|striptags}} an.
|
||||||
{% if data.choice in data.grant %}
|
{% if data.choice in data.grant %}
|
||||||
Vorraussichtliche Kosten: {{data.cost}}
|
Vorraussichtliche Kosten: {{data.cost}}
|
||||||
Anmerkungen: {{data.notes}} {% endif %} {% if data.choice in data.domain %}
|
Anmerkungen: {{data.notes}} {% endif %} {% if data.choice in data.domain %}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
<body>
|
<body>
|
||||||
Hallo {{data.realname}},
|
Hallo {{data.realname}},
|
||||||
<br><br>
|
<br><br>
|
||||||
wir haben Deine Anfrage ({{data.typestring|striptags}}) erhalten.<br>
|
wir haben Deine Anfrage ({{data.type_label|striptags}}) erhalten.<br>
|
||||||
{% if data.choice in data.grant %}<br>
|
{% if data.choice in data.grant %}<br>
|
||||||
Vorraussichtliche Kosten: {{data.cost}}<br>
|
Vorraussichtliche Kosten: {{data.cost}}<br>
|
||||||
Anmerkungen: {{data.notes}} {% endif %} {% if data.choice in data.domain %}<br>
|
Anmerkungen: {{data.notes}} {% endif %} {% if data.choice in data.domain %}<br>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
Hallo {{data.realname}},
|
Hallo {{data.realname}},
|
||||||
|
|
||||||
wir haben Deine Anfrage ({{data.typestring|striptags}}) erhalten.
|
wir haben Deine Anfrage ({{data.type_label|striptags}}) erhalten.
|
||||||
{% if data.choice in data.grant %}
|
{% if data.choice in data.grant %}
|
||||||
Vorraussichtliche Kosten: {{data.cost}}
|
Vorraussichtliche Kosten: {{data.cost}}
|
||||||
Anmerkungen: {{data.notes}} {% endif %} {% if data.choice in data.domain %}
|
Anmerkungen: {{data.notes}} {% endif %} {% if data.choice in data.domain %}
|
||||||
|
|
|
||||||
|
|
@ -1,41 +1,28 @@
|
||||||
from django.urls import path
|
from django.urls import path, include
|
||||||
from django.views.generic import TemplateView
|
|
||||||
from django.views.i18n import JavaScriptCatalog
|
from django.views.i18n import JavaScriptCatalog
|
||||||
|
|
||||||
from .views import (
|
from .views import (
|
||||||
index, done, export, authorize, deny,
|
index,
|
||||||
TravelApplicationView, IFGApplicationView, EmailApplicationView,
|
done,
|
||||||
LiteratureApplicationView, ListApplicationView, BusinessCardApplicationView,
|
export,
|
||||||
LibraryApplicationView, ELiteratureApplicationView, SoftwareApplicationView,
|
authorize,
|
||||||
|
deny,
|
||||||
|
ApplicationView,
|
||||||
|
ApplicationStartView,
|
||||||
|
ProjectInfoView,
|
||||||
)
|
)
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('', index, name='index'),
|
path('', index, name='index'),
|
||||||
path(
|
|
||||||
'extern/',
|
|
||||||
TemplateView.as_view(template_name='input/forms/extern.html'),
|
|
||||||
name='extern',
|
|
||||||
),
|
|
||||||
path('saved', done, name='done'),
|
path('saved', done, name='done'),
|
||||||
path('export', export, name='export'),
|
path('export', export, name='export'),
|
||||||
path('authorize/<str:choice>/<int:pk>', authorize, name='authorize'),
|
path('authorize/<str:choice>/<int:pk>', authorize, name='authorize'),
|
||||||
path('deny/<str:choice>/<int:pk>', deny, name='deny'),
|
path('deny/<str:choice>/<int:pk>', deny, name='deny'),
|
||||||
|
path('extern/', include([
|
||||||
# Static info page for project funding above 1000 EUR
|
path('', ApplicationStartView.as_view(), name='extern'),
|
||||||
path('extern/info/projektfoerderung-ab-1000/',
|
path('info/projektfoerderung-ab-1000/', ProjectInfoView.as_view(), name='info-foerderprojekt-ab-1000'),
|
||||||
TemplateView.as_view(template_name='input/info_project_funding_gt_1000.html'),
|
path('<slug:type>/', ApplicationView.as_view(), name='extern'),
|
||||||
name='info-foerderprojekt-ab-1000'),
|
])),
|
||||||
|
|
||||||
# New single-page application views
|
|
||||||
path('extern/reisekosten/', TravelApplicationView.as_view(), name='reisekosten'),
|
|
||||||
path('extern/ifg/', IFGApplicationView.as_view(), name='ifg'),
|
|
||||||
path('extern/email/', EmailApplicationView.as_view(), name='email'),
|
|
||||||
path('extern/literaturstipendium/', LiteratureApplicationView.as_view(), name='literatur'),
|
|
||||||
path('extern/mailingliste/', ListApplicationView.as_view(), name='mailingliste'),
|
|
||||||
path('extern/visitenkarten/', BusinessCardApplicationView.as_view(), name='visitenkarten'),
|
|
||||||
path('extern/bibliotheksstipendium/', LibraryApplicationView.as_view(), name='bibliotheksstipendium'),
|
|
||||||
path('extern/eliteraturstipendium/', ELiteratureApplicationView.as_view(), name='eliteraturstipendium'),
|
|
||||||
path('extern/softwarestipendium/', SoftwareApplicationView.as_view(), name='softwarestipendium'),
|
|
||||||
|
|
||||||
# JavaScript translations for date widgets, etc.
|
# JavaScript translations for date widgets, etc.
|
||||||
path('jsi18n/', JavaScriptCatalog.as_view(), name='jsi18n'),
|
path('jsi18n/', JavaScriptCatalog.as_view(), name='jsi18n'),
|
||||||
]
|
]
|
||||||
|
|
|
||||||
166
input/views.py
166
input/views.py
|
|
@ -1,17 +1,19 @@
|
||||||
from smtplib import SMTPException
|
from smtplib import SMTPException
|
||||||
|
from typing import NamedTuple
|
||||||
|
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse, Http404
|
||||||
|
from django.utils.functional import cached_property
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from formtools.wizard.views import CookieWizardView
|
|
||||||
from django.core.mail import BadHeaderError, EmailMultiAlternatives
|
from django.core.mail import BadHeaderError, EmailMultiAlternatives
|
||||||
from django.template.loader import get_template
|
from django.template.loader import get_template
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.views.generic import TemplateView
|
||||||
from django.views.generic.edit import FormView
|
from django.views.generic.edit import FormView
|
||||||
|
|
||||||
from .forms import (
|
from .forms import (
|
||||||
ExternForm,
|
BaseApplicationForm,
|
||||||
LibraryForm,
|
LibraryForm,
|
||||||
ELiteratureForm,
|
ELiteratureForm,
|
||||||
SoftwareForm,
|
SoftwareForm,
|
||||||
|
|
@ -22,31 +24,38 @@ from .forms import (
|
||||||
ListForm,
|
ListForm,
|
||||||
BusinessCardForm,
|
BusinessCardForm,
|
||||||
)
|
)
|
||||||
from .models import TYPE_CHOICES, MODELS, TYPE_BIB, TYPE_ELIT, TYPE_SOFT
|
from .models import (
|
||||||
|
MODELS,
|
||||||
LIBRARY_FORMS = {
|
LIBRARY_TYPES,
|
||||||
TYPE_BIB: LibraryForm,
|
TYPE_CHOICES,
|
||||||
TYPE_ELIT: ELiteratureForm,
|
TYPE_BIB,
|
||||||
TYPE_SOFT: SoftwareForm,
|
TYPE_ELIT,
|
||||||
}
|
TYPE_IFG,
|
||||||
|
TYPE_LIT,
|
||||||
|
TYPE_LIST,
|
||||||
|
TYPE_MAIL,
|
||||||
|
TYPE_SOFT,
|
||||||
|
TYPE_TRAV,
|
||||||
|
TYPE_VIS,
|
||||||
|
)
|
||||||
|
|
||||||
HELP_TEXTS = {
|
HELP_TEXTS = {
|
||||||
'IFG': {
|
TYPE_IFG: {
|
||||||
'notes': (
|
'notes': (
|
||||||
'Bitte gib an, wie die gewonnenen Informationen den<br>'
|
'Bitte gib an, wie die gewonnenen Informationen den<br>'
|
||||||
'Wikimedia-Projekten zugute kommen sollen.'
|
'Wikimedia-Projekten zugute kommen sollen.'
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
'MAIL': {
|
TYPE_MAIL: {
|
||||||
'domain': (
|
'domain': (
|
||||||
'Mit welcher Domain, bzw. für welches Wikimedia-Projekt,<br>'
|
'Mit welcher Domain, bzw. für welches Wikimedia-Projekt,<br>'
|
||||||
'möchtest du eine Mailadresse beantragen?'
|
'möchtest du eine Mailadresse beantragen?'
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
'LIT': {
|
TYPE_LIT: {
|
||||||
'notes': 'Bitte gib an, wofür du die Literatur verwenden möchtest.'
|
'notes': 'Bitte gib an, wofür du die Literatur verwenden möchtest.'
|
||||||
},
|
},
|
||||||
'LIST': {
|
TYPE_LIST: {
|
||||||
'domain': (
|
'domain': (
|
||||||
'Mit welcher Domain, bzw. für welches Wikimedia-Projekt,<br>'
|
'Mit welcher Domain, bzw. für welches Wikimedia-Projekt,<br>'
|
||||||
'möchtest du eine Mailingliste beantragen?'
|
'möchtest du eine Mailingliste beantragen?'
|
||||||
|
|
@ -55,6 +64,37 @@ HELP_TEXTS = {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class ApplicationType(NamedTuple):
|
||||||
|
code: str
|
||||||
|
path: str
|
||||||
|
form_class: type[BaseApplicationForm]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def label(self):
|
||||||
|
return TYPE_CHOICES[self.code]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def model(self):
|
||||||
|
return MODELS[self.code]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def help_texts(self):
|
||||||
|
return HELP_TEXTS.get(self.code)
|
||||||
|
|
||||||
|
|
||||||
|
TYPES = [
|
||||||
|
ApplicationType(TYPE_BIB, 'bibliotheksstipendium', LibraryForm),
|
||||||
|
ApplicationType(TYPE_ELIT, 'eliteraturstipendium', ELiteratureForm),
|
||||||
|
ApplicationType(TYPE_MAIL, 'email', EmailForm),
|
||||||
|
ApplicationType(TYPE_IFG, 'ifg', IFGForm),
|
||||||
|
ApplicationType(TYPE_LIT, 'literaturstipendium', LiteratureForm),
|
||||||
|
ApplicationType(TYPE_LIST, 'mailingliste', ListForm),
|
||||||
|
ApplicationType(TYPE_TRAV, 'reisekosten', TravelForm),
|
||||||
|
ApplicationType(TYPE_SOFT, 'softwarestipendium', SoftwareForm),
|
||||||
|
ApplicationType(TYPE_VIS, 'visitenkarten', BusinessCardForm),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def auth_deny(choice, pk, auth):
|
def auth_deny(choice, pk, auth):
|
||||||
if choice not in MODELS:
|
if choice not in MODELS:
|
||||||
return HttpResponse(f'ERROR! UNKNOWN CHOICE TYPE! {choice}')
|
return HttpResponse(f'ERROR! UNKNOWN CHOICE TYPE! {choice}')
|
||||||
|
|
@ -99,11 +139,19 @@ def index(request):
|
||||||
return render(request, 'input/index.html')
|
return render(request, 'input/index.html')
|
||||||
|
|
||||||
|
|
||||||
class BaseApplicationView(FormView):
|
class ApplicationStartView(TemplateView):
|
||||||
"""
|
template_name = 'input/forms/extern.html'
|
||||||
Base view for all application types.
|
extra_context = {'types': TYPES}
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectInfoView(TemplateView):
|
||||||
|
template_name = 'input/info_project_funding_gt_1000.html'
|
||||||
|
|
||||||
|
|
||||||
|
class ApplicationView(FormView):
|
||||||
|
"""
|
||||||
|
View for all application types.
|
||||||
|
|
||||||
- Each application type (travel, literature, email, etc.) gets its own subclass.
|
|
||||||
- Renders the generic form template.
|
- Renders the generic form template.
|
||||||
- Handles saving the submitted form to the database.
|
- Handles saving the submitted form to the database.
|
||||||
- Adds extra fields from the session or request type if needed.
|
- Adds extra fields from the session or request type if needed.
|
||||||
|
|
@ -112,22 +160,37 @@ class BaseApplicationView(FormView):
|
||||||
- Sends notification mail to the internal IF address.
|
- Sends notification mail to the internal IF address.
|
||||||
- Returns the "done" response after successful processing.
|
- Returns the "done" response after successful processing.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
template_name = 'input/forms/form_generic.html'
|
template_name = 'input/forms/form_generic.html'
|
||||||
type_code: str = ''
|
|
||||||
|
@cached_property
|
||||||
|
def type_info(self) -> ApplicationType:
|
||||||
|
type_path = self.kwargs['type']
|
||||||
|
|
||||||
|
for type_info in TYPES:
|
||||||
|
if type_path == type_info.path:
|
||||||
|
return type_info
|
||||||
|
|
||||||
|
raise Http404(f'"{type_path}" existiert nicht.')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def type_code(self):
|
||||||
|
return self.type_info.code
|
||||||
|
|
||||||
|
@property
|
||||||
|
def form_class(self):
|
||||||
|
return self.type_info.form_class
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
"""Add the human-readable type string (from TYPE_CHOICES) to the template context."""
|
return super().get_context_data(**kwargs, type_label=self.type_info.label)
|
||||||
ctx = super().get_context_data(**kwargs)
|
|
||||||
ctx['typestring'] = TYPE_CHOICES.get(self.type_code, self.type_code)
|
|
||||||
return ctx
|
|
||||||
|
|
||||||
def get_form(self, form_class=None):
|
def get_form(self, form_class=None):
|
||||||
"""Return the form instance and inject custom help_texts if defined for this type."""
|
"""Return the form instance and inject custom help_texts if defined for this type."""
|
||||||
form = super().get_form(form_class)
|
form = super().get_form(form_class)
|
||||||
|
|
||||||
# Apply help_text overrides if defined for this type_code
|
# Apply help_text overrides if defined for this type_code
|
||||||
if self.type_code in HELP_TEXTS:
|
if help_texts := self.type_info.help_texts:
|
||||||
for field, text in HELP_TEXTS[self.type_code].items():
|
for field, text in help_texts.items():
|
||||||
if field in form.fields:
|
if field in form.fields:
|
||||||
form.fields[field].help_text = mark_safe(text)
|
form.fields[field].help_text = mark_safe(text)
|
||||||
|
|
||||||
|
|
@ -147,7 +210,7 @@ class BaseApplicationView(FormView):
|
||||||
data['choice'] = self.type_code
|
data['choice'] = self.type_code
|
||||||
|
|
||||||
# Special rule for literature applications
|
# Special rule for literature applications
|
||||||
if self.type_code == 'LIT' and data.get('selfbuy') == 'TRUE':
|
if self.type_code == TYPE_LIT and data.get('selfbuy') == 'TRUE':
|
||||||
data['selfbuy_give_data'] = 'False'
|
data['selfbuy_give_data'] = 'False'
|
||||||
|
|
||||||
# Save model instance
|
# Save model instance
|
||||||
|
|
@ -164,11 +227,11 @@ class BaseApplicationView(FormView):
|
||||||
modell.email = data['email']
|
modell.email = data['email']
|
||||||
|
|
||||||
# Set model.type for specific request types
|
# Set model.type for specific request types
|
||||||
if self.type_code in ('BIB', 'ELIT', 'SOFT'):
|
if self.type_code in LIBRARY_TYPES:
|
||||||
modell.type = self.type_code
|
modell.type = self.type_code
|
||||||
|
|
||||||
# Literature-specific extra field
|
# Literature-specific extra field
|
||||||
if self.type_code == 'LIT' and 'selfbuy_give_data' in data:
|
if self.type_code == TYPE_LIT and 'selfbuy_give_data' in data:
|
||||||
modell.selfbuy_give_data = data['selfbuy_give_data']
|
modell.selfbuy_give_data = data['selfbuy_give_data']
|
||||||
|
|
||||||
modell.save()
|
modell.save()
|
||||||
|
|
@ -178,7 +241,7 @@ class BaseApplicationView(FormView):
|
||||||
# Prepare minimal mail context and send mails
|
# Prepare minimal mail context and send mails
|
||||||
data['pk'] = modell.pk
|
data['pk'] = modell.pk
|
||||||
data['url_prefix'] = settings.EMAIL_URL_PREFIX
|
data['url_prefix'] = settings.EMAIL_URL_PREFIX
|
||||||
data['typestring'] = TYPE_CHOICES.get(self.type_code, self.type_code)
|
data['type_label'] = self.type_info.label
|
||||||
context = {'data': data}
|
context = {'data': data}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
@ -208,48 +271,3 @@ class BaseApplicationView(FormView):
|
||||||
return HttpResponse('Error in sending mails (probably wrong adress?). Data not saved!')
|
return HttpResponse('Error in sending mails (probably wrong adress?). Data not saved!')
|
||||||
|
|
||||||
return done(self.request)
|
return done(self.request)
|
||||||
|
|
||||||
|
|
||||||
class TravelApplicationView(BaseApplicationView):
|
|
||||||
form_class = TravelForm
|
|
||||||
type_code = 'TRAV'
|
|
||||||
|
|
||||||
|
|
||||||
class LibraryApplicationView(BaseApplicationView):
|
|
||||||
form_class = LibraryForm
|
|
||||||
type_code = 'BIB'
|
|
||||||
|
|
||||||
|
|
||||||
class ELiteratureApplicationView(BaseApplicationView):
|
|
||||||
form_class = ELiteratureForm
|
|
||||||
type_code = 'ELIT'
|
|
||||||
|
|
||||||
|
|
||||||
class SoftwareApplicationView(BaseApplicationView):
|
|
||||||
form_class = SoftwareForm
|
|
||||||
type_code = 'SOFT'
|
|
||||||
|
|
||||||
|
|
||||||
class IFGApplicationView(BaseApplicationView):
|
|
||||||
form_class = IFGForm
|
|
||||||
type_code = 'IFG'
|
|
||||||
|
|
||||||
|
|
||||||
class EmailApplicationView(BaseApplicationView):
|
|
||||||
form_class = EmailForm
|
|
||||||
type_code = 'MAIL'
|
|
||||||
|
|
||||||
|
|
||||||
class LiteratureApplicationView(BaseApplicationView):
|
|
||||||
form_class = LiteratureForm
|
|
||||||
type_code = 'LIT'
|
|
||||||
|
|
||||||
|
|
||||||
class ListApplicationView(BaseApplicationView):
|
|
||||||
form_class = ListForm
|
|
||||||
type_code = 'LIST'
|
|
||||||
|
|
||||||
|
|
||||||
class BusinessCardApplicationView(BaseApplicationView):
|
|
||||||
form_class = BusinessCardForm
|
|
||||||
type_code = 'VIS'
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
Authlib==1.6.1
|
Authlib==1.6.1
|
||||||
Django==5.2.5
|
Django==5.2.5
|
||||||
django-formtools==2.5.1
|
|
||||||
gunicorn==23.0.0
|
gunicorn==23.0.0
|
||||||
mysqlclient==2.2.7
|
mysqlclient==2.2.7
|
||||||
python-dotenv==1.1.1
|
python-dotenv==1.1.1
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue