Compare commits

..

No commits in common. "main" and "0.15.1" have entirely different histories.
main ... 0.15.1

12 changed files with 1035 additions and 1276 deletions

View file

@ -1,8 +1,8 @@
ARG DEBIAN_RELEASE=bookworm ARG DEBIAN_RELEASE=buster
FROM debian:$DEBIAN_RELEASE AS builder FROM debian:$DEBIAN_RELEASE AS builder
ARG GVAAPP=gva ARG GVAAPP=gva
ARG POETRY_VERSION=1.7.1 ARG POETRY_VERSION=1.3.1
ENV LC_ALL=C.UTF-8 ENV LC_ALL=C.UTF-8
ENV LANG=C.UTF-8 ENV LANG=C.UTF-8
@ -26,7 +26,7 @@ WORKDIR /srv/$GVAAPP
COPY poetry.lock pyproject.toml /srv/$GVAAPP/ COPY poetry.lock pyproject.toml /srv/$GVAAPP/
RUN /root/.local/bin/poetry install --only=main --no-root RUN /root/.local/bin/poetry install --only=main
FROM debian:$DEBIAN_RELEASE FROM debian:$DEBIAN_RELEASE
LABEL maintainer="Jan Dittberner <jan@dittberner.info>" LABEL maintainer="Jan Dittberner <jan@dittberner.info>"

View file

@ -19,7 +19,7 @@ services:
volumes: volumes:
- "redis_data:/var/lib/redis" - "redis_data:/var/lib/redis"
gva: gva:
image: gnuviech/gva:bookworm image: gnuviech/gva:buster
build: build:
context: . context: .
args: args:
@ -67,7 +67,7 @@ services:
volumes: volumes:
- "../gvaldap/gvaldap:/srv/gvaldap/gvaldap" - "../gvaldap/gvaldap:/srv/gvaldap/gvaldap"
file: file:
image: gnuviech/gvafile:bookworm image: gnuviech/gvafile:buster
build: build:
context: ../gvafile context: ../gvafile
args: args:

View file

@ -6,12 +6,9 @@ Common settings and globals.
""" """
from os.path import abspath, basename, dirname, join, normpath from os.path import abspath, basename, dirname, join, normpath
from environs import Env
from django.contrib.messages import constants as messages from django.contrib.messages import constants as messages
from gvacommon.settings_utils import get_env_variable
env = Env()
env.read_env()
# ######### PATH CONFIGURATION # ######### PATH CONFIGURATION
# Absolute filesystem path to the Django project directory: # Absolute filesystem path to the Django project directory:
@ -20,14 +17,12 @@ DJANGO_ROOT = dirname(dirname(abspath(__file__)))
# Absolute filesystem path to the top-level project folder: # Absolute filesystem path to the top-level project folder:
SITE_ROOT = dirname(DJANGO_ROOT) SITE_ROOT = dirname(DJANGO_ROOT)
ROOT_DIR = dirname(DJANGO_ROOT)
# Site name: # Site name:
SITE_NAME = basename(DJANGO_ROOT) SITE_NAME = basename(DJANGO_ROOT)
# ######### END PATH CONFIGURATION # ######### END PATH CONFIGURATION
GVA_ENVIRONMENT = env.str("GVA_ENVIRONMENT", default="prod") GVA_ENVIRONMENT = get_env_variable("GVA_ENVIRONMENT", default="prod")
# ######### DEBUG CONFIGURATION # ######### DEBUG CONFIGURATION
# See: https://docs.djangoproject.com/en/dev/ref/settings/#debug # See: https://docs.djangoproject.com/en/dev/ref/settings/#debug
@ -39,8 +34,8 @@ DEBUG = GVA_ENVIRONMENT == "local"
# See: https://docs.djangoproject.com/en/dev/ref/settings/#admins # See: https://docs.djangoproject.com/en/dev/ref/settings/#admins
ADMINS = ( ADMINS = (
( (
env.str("GVA_ADMIN_NAME", default="Admin"), get_env_variable("GVA_ADMIN_NAME", default="Admin"),
env.str("GVA_ADMIN_EMAIL", default="admin@example.org"), get_env_variable("GVA_ADMIN_EMAIL", default="admin@example.org"),
), ),
) )
@ -52,7 +47,14 @@ MANAGERS = ADMINS
# ######### DATABASE CONFIGURATION # ######### DATABASE CONFIGURATION
# See: https://docs.djangoproject.com/en/dev/ref/settings/#databases # See: https://docs.djangoproject.com/en/dev/ref/settings/#databases
DATABASES = { DATABASES = {
"default": env.dj_db_url("GVA_DATABASE_URL"), "default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": get_env_variable("GVA_PGSQL_DATABASE", default="gnuviechadmin"),
"USER": get_env_variable("GVA_PGSQL_USER", default="gnuviechadmin"),
"PASSWORD": get_env_variable("GVA_PGSQL_PASSWORD"),
"HOST": get_env_variable("GVA_PGSQL_HOSTNAME", default="db"),
"PORT": get_env_variable("GVA_PGSQL_PORT", int, default=5432),
}
} }
DEFAULT_AUTO_FIELD = "django.db.models.AutoField" DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
@ -68,8 +70,8 @@ LANGUAGE_CODE = "en-us"
# See: https://docs.djangoproject.com/en/dev/ref/settings/#site-id # See: https://docs.djangoproject.com/en/dev/ref/settings/#site-id
SITE_ID = 1 SITE_ID = 1
SITES_DOMAIN_NAME = env.str("GVA_DOMAIN_NAME") SITES_DOMAIN_NAME = get_env_variable("GVA_DOMAIN_NAME")
SITES_SITE_NAME = env.str("GVA_SITE_NAME") SITES_SITE_NAME = get_env_variable("GVA_SITE_NAME")
# See: https://docs.djangoproject.com/en/dev/ref/settings/#use-i18n # See: https://docs.djangoproject.com/en/dev/ref/settings/#use-i18n
USE_I18N = True USE_I18N = True
@ -107,7 +109,7 @@ STATICFILES_FINDERS = (
# ######### SECRET CONFIGURATION # ######### SECRET CONFIGURATION
# See: https://docs.djangoproject.com/en/dev/ref/settings/#secret-key # See: https://docs.djangoproject.com/en/dev/ref/settings/#secret-key
# Note: This key should only be used for development and testing. # Note: This key should only be used for development and testing.
SECRET_KEY = env.str("GVA_SITE_SECRET") SECRET_KEY = get_env_variable("GVA_SITE_SECRET")
# ######### END SECRET CONFIGURATION # ######### END SECRET CONFIGURATION
@ -159,9 +161,9 @@ MIDDLEWARE = [
"django.contrib.sessions.middleware.SessionMiddleware", "django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.csrf.CsrfViewMiddleware", "django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware",
"allauth.account.middleware.AccountMiddleware",
"django.middleware.locale.LocaleMiddleware", "django.middleware.locale.LocaleMiddleware",
"django.contrib.messages.middleware.MessageMiddleware", "django.contrib.messages.middleware.MessageMiddleware",
# uncomment next line to enable translation to browser locale
"django.middleware.clickjacking.XFrameOptionsMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware",
] ]
# ######### END MIDDLEWARE CONFIGURATION # ######### END MIDDLEWARE CONFIGURATION
@ -306,7 +308,7 @@ LOGGING = {
"logfile": { "logfile": {
"level": "INFO", "level": "INFO",
"class": "logging.FileHandler", "class": "logging.FileHandler",
"filename": env.str("GVA_LOG_FILE", default="gva.log"), "filename": get_env_variable("GVA_LOG_FILE", default="gva.log"),
"formatter": "verbose", "formatter": "verbose",
}, },
"mail_admins": { "mail_admins": {
@ -349,7 +351,7 @@ WSGI_APPLICATION = "%s.wsgi.application" % SITE_NAME
# ######### CELERY CONFIGURATION # ######### CELERY CONFIGURATION
BROKER_URL = env.str( BROKER_URL = get_env_variable(
"GVA_BROKER_URL", default="amqp://gnuviechadmin:gnuviechadmin@mq/gnuviechadmin" "GVA_BROKER_URL", default="amqp://gnuviechadmin:gnuviechadmin@mq/gnuviechadmin"
) )
BROKER_TRANSPORT_OPTIONS = { BROKER_TRANSPORT_OPTIONS = {
@ -358,7 +360,7 @@ BROKER_TRANSPORT_OPTIONS = {
"interval_step": 0.2, "interval_step": 0.2,
"interval_max": 0.2, "interval_max": 0.2,
} }
CELERY_RESULT_BACKEND = env.str( CELERY_RESULT_BACKEND = get_env_variable(
"GVA_RESULTS_REDIS_URL", default="redis://:gnuviechadmin@redis:6379/0" "GVA_RESULTS_REDIS_URL", default="redis://:gnuviechadmin@redis:6379/0"
) )
CELERY_TASK_RESULT_EXPIRES = None CELERY_TASK_RESULT_EXPIRES = None
@ -372,28 +374,32 @@ CELERY_RESULT_SERIALIZER = "json"
# ######### CUSTOM APP CONFIGURATION # ######### CUSTOM APP CONFIGURATION
OSUSER_MINUID = env.int("GVA_MIN_OS_UID", default=10000) OSUSER_MINUID = get_env_variable("GVA_MIN_OS_UID", int, default=10000)
OSUSER_MINGID = env.int("GVA_MIN_OS_GID", default=10000) OSUSER_MINGID = get_env_variable("GVA_MIN_OS_GID", int, default=10000)
OSUSER_USERNAME_PREFIX = env.str("GVA_OSUSER_PREFIX", default="usr") OSUSER_USERNAME_PREFIX = get_env_variable("GVA_OSUSER_PREFIX", default="usr")
OSUSER_HOME_BASEPATH = env.str("GVA_OSUSER_HOME_BASEPATH", default="/home") OSUSER_HOME_BASEPATH = get_env_variable("GVA_OSUSER_HOME_BASEPATH", default="/home")
OSUSER_DEFAULT_SHELL = env.str("GVA_OSUSER_DEFAULT_SHELL", default="/usr/bin/rssh") OSUSER_DEFAULT_SHELL = get_env_variable(
"GVA_OSUSER_DEFAULT_SHELL", default="/usr/bin/rssh"
)
OSUSER_SFTP_GROUP = "sftponly" OSUSER_SFTP_GROUP = "sftponly"
OSUSER_SSH_GROUP = "sshusers" OSUSER_SSH_GROUP = "sshusers"
OSUSER_DEFAULT_GROUPS = [OSUSER_SFTP_GROUP] OSUSER_DEFAULT_GROUPS = [OSUSER_SFTP_GROUP]
OSUSER_UPLOAD_SERVER = env.str("GVA_OSUSER_UPLOADSERVER", default="file") OSUSER_UPLOAD_SERVER = get_env_variable("GVA_OSUSER_UPLOADSERVER", default="file")
GVA_LINK_WEBMAIL = env.str("GVA_WEBMAIL_URL", default="https://webmail.example.org/") GVA_LINK_WEBMAIL = get_env_variable(
GVA_LINK_PHPMYADMIN = env.str( "GVA_WEBMAIL_URL", default="https://webmail.example.org/"
)
GVA_LINK_PHPMYADMIN = get_env_variable(
"GVA_PHPMYADMIN_URL", default="https://phpmyadmin.example.org/" "GVA_PHPMYADMIN_URL", default="https://phpmyadmin.example.org/"
) )
GVA_LINK_PHPPGADMIN = env.str( GVA_LINK_PHPPGADMIN = get_env_variable(
"GVA_PHPPGADMIN_URL", default="https://phppgadmin.example.org/" "GVA_PHPPGADMIN_URL", default="https://phppgadmin.example.org/"
) )
# ######### END CUSTOM APP CONFIGURATION # ######### END CUSTOM APP CONFIGURATION
# ######### STATIC FILE CONFIGURATION # ######### STATIC FILE CONFIGURATION
# See: https://docs.djangoproject.com/en/dev/ref/settings/#static-root # See: https://docs.djangoproject.com/en/dev/ref/settings/#static-root
STATIC_ROOT = env.str("GVA_STATIC_PATH", default=normpath(join(ROOT_DIR, "static"))) STATIC_ROOT = "/srv/gva/static/"
def show_debug_toolbar(request): def show_debug_toolbar(request):
@ -488,10 +494,12 @@ else:
EMAIL_SUBJECT_PREFIX = "[%s] " % SITE_NAME EMAIL_SUBJECT_PREFIX = "[%s] " % SITE_NAME
# See: https://docs.djangoproject.com/en/dev/ref/settings/#default-from-email # See: https://docs.djangoproject.com/en/dev/ref/settings/#default-from-email
DEFAULT_FROM_EMAIL = env.str("GVA_SITE_ADMINMAIL", default="admin@example.org") DEFAULT_FROM_EMAIL = get_env_variable(
"GVA_SITE_ADMINMAIL", default="admin@example.org"
)
# See: https://docs.djangoproject.com/en/dev/ref/settings/#server-email # See: https://docs.djangoproject.com/en/dev/ref/settings/#server-email
SERVER_EMAIL = env.str("GVA_SITE_ADMINMAIL", default="admin@example.org") SERVER_EMAIL = get_env_variable("GVA_SITE_ADMINMAIL", default="admin@example.org")
# ######### END EMAIL CONFIGURATION # ######### END EMAIL CONFIGURATION
# ######### CACHE CONFIGURATION # ######### CACHE CONFIGURATION

View file

@ -38,7 +38,6 @@ urlpatterns = [
path("contact/", include("contact_form.urls")), path("contact/", include("contact_form.urls")),
path("impressum/", views.flatpage, {"url": "/impressum/"}, name="imprint"), path("impressum/", views.flatpage, {"url": "/impressum/"}, name="imprint"),
path("datenschutz/", views.flatpage, {"url": "/datenschutz/"}, name="privacy"), path("datenschutz/", views.flatpage, {"url": "/datenschutz/"}, name="privacy"),
path("issues/", views.flatpage, {"url": "/issues/"}, name="support"),
] ]
# Uncomment the next line to serve media files in dev. # Uncomment the next line to serve media files in dev.

View file

@ -10,7 +10,7 @@ PASSWORD_MISMATCH_ERROR = _("Passwords don't match")
class ReadOnlyPasswordHashWidget(forms.Widget): class ReadOnlyPasswordHashWidget(forms.Widget):
def render(self, name, value, attrs, renderer=None): def render(self, name, value, attrs):
final_attrs = self.build_attrs(attrs) final_attrs = self.build_attrs(attrs)
summary = format_html("<strong>{0}</strong>: {1} ", _("Hash"), value) summary = format_html("<strong>{0}</strong>: {1} ", _("Hash"), value)
return format_html("<div{0}>{1}</div>", flatatt(final_attrs), summary) return format_html("<div{0}>{1}</div>", flatatt(final_attrs), summary)

View file

@ -119,13 +119,13 @@ class ActivationChangeMixinTest(TestCase):
querysetmock = Mock() querysetmock = Mock()
activationchange = ActivationChangeMixin() activationchange = ActivationChangeMixin()
activationchange.activate(Mock(), querysetmock) activationchange.activate(Mock(), querysetmock)
querysetmock.update.assert_called_with(active=True) querysetmock.update.called_with(active=True)
def test_deactivate(self): def test_deactivate(self):
querysetmock = Mock() querysetmock = Mock()
activationchange = ActivationChangeMixin() activationchange = ActivationChangeMixin()
activationchange.deactivate(Mock(), querysetmock) activationchange.deactivate(Mock(), querysetmock)
querysetmock.update.assert_called_with(active=False) querysetmock.update.called_with(active=False)
class MailBoxAdminTest(CustomerTestCase): class MailBoxAdminTest(CustomerTestCase):

View file

@ -35,8 +35,8 @@ class FetchTaskResultsCommandTest(TestCase):
Command().handle(verbosity=0) Command().handle(verbosity=0)
tr = TaskResult.objects.get(task_id=TEST_TASK_UUID) tr = TaskResult.objects.get(task_id=TEST_TASK_UUID)
asyncresult.assert_called_with(TEST_TASK_UUID) self.assertTrue(asyncresult.called_with(TEST_TASK_UUID))
aresult.ready.assert_called_with() self.assertTrue(aresult.ready.called_with())
self.assertFalse(tr.finished) self.assertFalse(tr.finished)
self.assertEqual(tr.result, "") self.assertEqual(tr.result, "")
self.assertEqual(tr.state, "PENDING") self.assertEqual(tr.state, "PENDING")
@ -58,9 +58,9 @@ class FetchTaskResultsCommandTest(TestCase):
Command().handle(verbosity=0) Command().handle(verbosity=0)
tr = TaskResult.objects.get(task_id=TEST_TASK_UUID) tr = TaskResult.objects.get(task_id=TEST_TASK_UUID)
asyncresult.assert_called_with(TEST_TASK_UUID) self.assertTrue(asyncresult.called_with(TEST_TASK_UUID))
aresult.ready.assert_called_with() self.assertTrue(aresult.ready.called_with())
aresult.get.assert_called_with(propagate=False, timeout=5) self.assertTrue(aresult.get.called_with())
self.assertTrue(tr.finished) self.assertTrue(tr.finished)
self.assertEqual(tr.result, TEST_TASK_RESULT) self.assertEqual(tr.result, TEST_TASK_RESULT)
self.assertEqual(tr.state, "SUCCESS") self.assertEqual(tr.state, "SUCCESS")

View file

@ -41,7 +41,7 @@ class TaskResultTest(TestCase):
aresult.ready.return_value = True aresult.ready.return_value = True
aresult.get.return_value = TEST_TASK_RESULT aresult.get.return_value = TEST_TASK_RESULT
tr.fetch_result() tr.fetch_result()
aresult.get.assert_called_with(propagate=False, timeout=5) self.assertTrue(aresult.get.called_with())
self.assertEqual(aresult.get.call_count, 1) self.assertEqual(aresult.get.call_count, 1)
self.assertTrue(tr.finished) self.assertTrue(tr.finished)
self.assertEqual(tr.result, str(TEST_TASK_RESULT)) self.assertEqual(tr.result, str(TEST_TASK_RESULT))

View file

@ -24,9 +24,11 @@ class DatabaseUserCreationFormTest(TestCase):
mockuser = Mock(name="osuser") mockuser = Mock(name="osuser")
form.cleaned_data = {"osuser": mockuser, "db_type": DB_TYPES.pgsql} form.cleaned_data = {"osuser": mockuser, "db_type": DB_TYPES.pgsql}
retval = form.save() retval = form.save()
create_database_user.assert_called_with( self.assertTrue(
create_database_user.called_with(
osuser=mockuser, db_type=DB_TYPES.pgsql, commit=True osuser=mockuser, db_type=DB_TYPES.pgsql, commit=True
) )
)
self.assertEqual(retval, create_database_user.return_value) self.assertEqual(retval, create_database_user.return_value)
def test_save_m2m_returns_none(self): def test_save_m2m_returns_none(self):
@ -42,7 +44,7 @@ class UserDatabaseCreationFormTest(TestCase):
mockuser = Mock(name="mockuser") mockuser = Mock(name="mockuser")
form.cleaned_data = {"db_user": mockuser} form.cleaned_data = {"db_user": mockuser}
retval = form.save() retval = form.save()
create_userdatabase.assert_called_with(db_user=mockuser, commit=True) self.assertTrue(create_userdatabase.called_with(db_user=mockuser, commit=True))
self.assertEqual(retval, create_userdatabase.return_value) self.assertEqual(retval, create_userdatabase.return_value)
def test_save_m2m_returns_none(self): def test_save_m2m_returns_none(self):
@ -77,20 +79,20 @@ class DatabaseUserAdminTest(TestCase):
def test_save_model_change(self): def test_save_model_change(self):
objmock = Mock() objmock = Mock()
self.dbuadmin.save_model(Mock(name="request"), objmock, Mock(), True) self.dbuadmin.save_model(Mock(name="request"), objmock, Mock(), True)
objmock.create_in_database.assert_not_called() self.assertTrue(objmock.create_in_database.not_called())
def test_save_model_no_change(self): def test_save_model_no_change(self):
objmock = Mock() objmock = Mock()
self.dbuadmin.save_model(Mock(name="request"), objmock, Mock(), False) self.dbuadmin.save_model(Mock(name="request"), objmock, Mock(), False)
objmock.create_in_database.assert_called_with() self.assertTrue(objmock.create_in_database.called_with())
def test_perform_delete_selected(self): def test_perform_delete_selected(self):
usermock = Mock() usermock = Mock()
selected = Mock() selected = Mock()
selected.all.return_value = [usermock] selected.all.return_value = [usermock]
self.dbuadmin.perform_delete_selected(Mock(name="request"), selected) self.dbuadmin.perform_delete_selected(Mock(name="request"), selected)
selected.all.assert_called_with() self.assertTrue(selected.all.called_with())
usermock.delete.assert_called_with() self.assertTrue(usermock.delete.called_with())
def test_get_actions(self): def test_get_actions(self):
requestmock = MagicMock(name="request") requestmock = MagicMock(name="request")
@ -125,20 +127,20 @@ class UserDatabaseAdminTest(TestCase):
def test_save_model_change(self): def test_save_model_change(self):
objmock = Mock() objmock = Mock()
self.udbadmin.save_model(Mock(name="request"), objmock, Mock(), True) self.udbadmin.save_model(Mock(name="request"), objmock, Mock(), True)
objmock.create_in_database.assert_not_called() self.assertTrue(objmock.create_in_database.not_called())
def test_save_model_no_change(self): def test_save_model_no_change(self):
objmock = Mock() objmock = Mock()
self.udbadmin.save_model(Mock(name="request"), objmock, Mock(), False) self.udbadmin.save_model(Mock(name="request"), objmock, Mock(), False)
objmock.create_in_database.assert_called_with() self.assertTrue(objmock.create_in_database.called_with())
def test_perform_delete_selected(self): def test_perform_delete_selected(self):
userdbmock = Mock() userdbmock = Mock()
selected = Mock() selected = Mock()
selected.all.return_value = [userdbmock] selected.all.return_value = [userdbmock]
self.udbadmin.perform_delete_selected(Mock(name="request"), selected) self.udbadmin.perform_delete_selected(Mock(name="request"), selected)
selected.all.assert_called_with() self.assertTrue(selected.all.called_with())
userdbmock.delete.assert_called_with() self.assertTrue(userdbmock.delete.called_with())
def test_get_actions(self): def test_get_actions(self):
requestmock = MagicMock(name="request") requestmock = MagicMock(name="request")

View file

@ -82,12 +82,14 @@ class AddUserDatabaseFormTest(TestCase):
) )
form.cleaned_data = {"db_type": DB_TYPES.pgsql, "password1": "secret"} form.cleaned_data = {"db_type": DB_TYPES.pgsql, "password1": "secret"}
form.save() form.save()
create_userdatabase_with_user.assert_called_with( self.assertTrue(
create_userdatabase_with_user.called_with(
DB_TYPES.pgsql, DB_TYPES.pgsql,
self.hostingpackage.osuser, self.hostingpackage.osuser,
password="secret", password="secret",
commit=True, commit=True,
) )
)
class ChangeDatabaseUserPasswordFormTest(TestCase): class ChangeDatabaseUserPasswordFormTest(TestCase):
@ -129,4 +131,4 @@ class ChangeDatabaseUserPasswordFormTest(TestCase):
) )
form.cleaned_data = {"password1": "secret"} form.cleaned_data = {"password1": "secret"}
form.save() form.save()
instance.set_password.assert_called_with("secret") self.assertTrue(instance.set_password.called_with("secret"))

2166
poetry.lock generated

File diff suppressed because it is too large Load diff

View file

@ -4,30 +4,28 @@ version = "0.15.1"
description = "gnuviechadmin web interface" description = "gnuviechadmin web interface"
authors = ["Jan Dittberner <jan@dittberner.info>"] authors = ["Jan Dittberner <jan@dittberner.info>"]
license = "AGPL-3+" license = "AGPL-3+"
readme = "README.rst" readme = "README.md"
package-mode = false
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.8" python = "^3.8"
django = "^4.2" django = "^4.2"
psycopg2-binary = "^2.9" psycopg2-binary = "^2.9"
celery = "^5.2.7" celery = "^5.2.7"
django-allauth = "^0.60.0" django-allauth = "^0.52.0"
django-crispy-forms = "^2.0" django-crispy-forms = "^2.0"
django-debug-toolbar = "^4.2" django-debug-toolbar = "^3.8"
django-model-utils = "^4.1" django-model-utils = "^4.1"
gvacommon = {version = "^0.7.0", source = "gnuviech"} gvacommon = {version = "^0.7.0", source = "gnuviech"}
passlib = "^1.7.4" passlib = "^1.7.4"
redis = "^5.0.1" redis = "^4.5.1"
requests-oauthlib = "^1.3.1" requests-oauthlib = "^1.3.1"
django-impersonate = "^1.9.1" django-impersonate = "^1.9.1"
djangorestframework = "^3.14.0" djangorestframework = "^3.14.0"
markdown = "^3.4.3" markdown = "^3.4.3"
django-filter = "^23.1" django-filter = "^23.1"
crispy-bootstrap5 = "^2023.10" crispy-bootstrap5 = "^0.7"
python-magic = "^0.4.27" python-magic = "^0.4.27"
isort = "^5.12.0" isort = "^5.12.0"
environs = {extras = ["django"], version = "^11.0.0"}
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]