Update to Django 5.x (#746)

This commit is contained in:
Christopher Charbonneau Wells 2024-01-13 20:22:08 -08:00 committed by GitHub
parent 9b603a9e84
commit 49d8f2b340
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 122 additions and 98 deletions

View File

@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [ '3.8', '3.9', '3.10', '3.11', '3.12' ]
python-version: [ '3.10', '3.11', '3.12' ]
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/setup

View File

@ -6,7 +6,7 @@ name = "pypi"
[packages]
boto3 = "*"
dj-database-url = "*"
django = "~=4.0"
django = "~=5.0"
django-axes = "*"
django-filter = "*"
django-imagekit = "*"

View File

@ -316,8 +316,8 @@ class FeedingAPITestCase(TestBase.BabyBuddyAPITestCaseBase):
{
"id": 3,
"child": 1,
"start": "2017-11-18T19:00:00-05:00",
"end": "2017-11-18T19:15:00-05:00",
"start": "2017-11-18T09:00:00-05:00",
"end": "2017-11-18T09:15:00-05:00",
"duration": "00:15:00",
"type": "formula",
"method": "bottle",
@ -335,7 +335,7 @@ class FeedingAPITestCase(TestBase.BabyBuddyAPITestCaseBase):
def test_get_with_iso_filter(self):
response = self.client.get(
self.endpoint, {"start_min": "2017-11-18T11:30:00-05:00"}
self.endpoint, {"start_min": "2017-11-18T04:00:00-05:00"}
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data["count"], 3)

View File

@ -249,8 +249,8 @@
"fields":
{
"child": 1,
"start": "2017-11-18T14:00:00-05:00",
"end": "2017-11-18T14:30:00-05:00",
"start": "2017-11-18T09:00:00Z",
"end": "2017-11-18T09:30:00Z",
"duration": "00:30:00",
"type": "breast milk",
"method": "left breast",
@ -263,8 +263,8 @@
"fields":
{
"child": 1,
"start": "2017-11-18T16:30:00-05:00",
"end": "2017-11-18T17:00:00-05:00",
"start": "2017-11-18T11:30:00Z",
"end": "2017-11-18T12:00:00Z",
"duration": "00:30:00",
"type": "breast milk",
"method": "right breast",
@ -277,8 +277,8 @@
"fields":
{
"child": 1,
"start": "2017-11-18T19:00:00-05:00",
"end": "2017-11-18T19:15:00-05:00",
"start": "2017-11-18T14:00:00Z",
"end": "2017-11-18T14:15:00Z",
"duration": "00:15:00",
"type": "formula",
"method": "bottle",
@ -292,8 +292,8 @@
"fields":
{
"child": 1,
"start": "2017-11-17T19:00:00-05:00",
"end": "2017-11-17T19:15:00-05:00",
"start": "2017-11-17T14:00:00Z",
"end": "2017-11-17T14:15:00Z",
"duration": "00:15:00",
"type": "formula",
"method": "bottle",
@ -307,8 +307,8 @@
"fields":
{
"child": 1,
"start": "2017-11-11T19:00:00-05:00",
"end": "2017-11-11T19:15:00-05:00",
"start": "2017-11-11T14:00:00Z",
"end": "2017-11-11T14:15:00Z",
"duration": "00:15:00",
"type": "formula",
"method": "bottle",
@ -322,8 +322,8 @@
"fields":
{
"child": 1,
"start": "2017-11-11T00:00:00-05:00",
"end": "2017-11-11T00:15:00-05:00",
"start": "2017-11-11T05:00:00Z",
"end": "2017-11-11T05:15:00Z",
"duration": "00:15:00",
"type": "formula",
"method": "bottle",

View File

@ -2,7 +2,6 @@ from os import getenv
from time import time
from functools import wraps
import pytz
from urllib.parse import urlunsplit, urlsplit
from django.conf import settings
@ -55,8 +54,8 @@ class UserTimezoneMiddleware:
user = request.user
if hasattr(user, "settings") and user.settings.timezone:
try:
timezone.activate(pytz.timezone(user.settings.timezone))
except pytz.UnknownTimeZoneError:
timezone.activate(user.settings.timezone)
except ValueError:
pass
return self.get_response(request)

View File

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
import pytz
import zoneinfo
from django.conf import settings
from django.contrib.auth import get_user_model
@ -63,7 +63,9 @@ class Settings(models.Model):
verbose_name=_("Language"),
)
timezone = models.CharField(
choices=tuple(zip(pytz.common_timezones, pytz.common_timezones)),
choices=sorted(
tuple(zip(zoneinfo.available_timezones(), zoneinfo.available_timezones()))
),
default=timezone.get_default_timezone_name(),
max_length=100,
verbose_name=_("Timezone"),

View File

@ -20,7 +20,7 @@ DEBUG = bool(strtobool(os.environ.get("DEBUG") or "False"))
# Applications
# https://docs.djangoproject.com/en/4.0/ref/applications/
# https://docs.djangoproject.com/en/5.0/ref/applications/
INSTALLED_APPS = [
"api",
@ -48,7 +48,7 @@ INSTALLED_APPS = [
]
# Middleware
# https://docs.djangoproject.com/en/4.0/ref/middleware/
# https://docs.djangoproject.com/en/5.0/ref/middleware/
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
@ -69,13 +69,13 @@ MIDDLEWARE = [
# URL dispatcher
# https://docs.djangoproject.com/en/4.0/topics/http/urls/
# https://docs.djangoproject.com/en/5.0/topics/http/urls/
ROOT_URLCONF = "babybuddy.urls"
# Templates
# https://docs.djangoproject.com/en/4.0/ref/settings/#std:setting-TEMPLATES
# https://docs.djangoproject.com/en/5.0/ref/settings/#std:setting-TEMPLATES
TEMPLATES = [
{
@ -95,7 +95,7 @@ TEMPLATES = [
# Database
# https://docs.djangoproject.com/en/4.0/ref/settings/#databases
# https://docs.djangoproject.com/en/5.0/ref/settings/#databases
config = {
"ENGINE": os.getenv("DB_ENGINE") or "django.db.backends.sqlite3",
@ -118,7 +118,7 @@ DATABASES = {"default": config}
# Cache
# https://docs.djangoproject.com/en/4.0/topics/cache/
# https://docs.djangoproject.com/en/5.0/topics/cache/
CACHES = {
"default": {
@ -129,13 +129,13 @@ CACHES = {
# WGSI
# https://docs.djangoproject.com/en/4.0/howto/deployment/wsgi/
# https://docs.djangoproject.com/en/5.0/howto/deployment/wsgi/
WSGI_APPLICATION = "babybuddy.wsgi.application"
# Authentication
# https://docs.djangoproject.com/en/4.0/topics/auth/default/
# https://docs.djangoproject.com/en/5.0/topics/auth/default/
AUTHENTICATION_BACKENDS = [
"axes.backends.AxesBackend",
@ -158,14 +158,14 @@ if REVERSE_PROXY_AUTH:
# Timezone
# https://docs.djangoproject.com/en/4.0/topics/i18n/timezones/
# https://docs.djangoproject.com/en/5.0/topics/i18n/timezones/
USE_TZ = True
TIME_ZONE = "UTC"
# Internationalization
# https://docs.djangoproject.com/en/4.0/topics/i18n/
# https://docs.djangoproject.com/en/5.0/topics/i18n/
USE_I18N = True
@ -199,18 +199,26 @@ LANGUAGES = [
# Format localization
# https://docs.djangoproject.com/en/4.0/topics/i18n/formatting/
USE_L10N = True
# https://docs.djangoproject.com/en/5.0/topics/i18n/formatting/
FORMAT_MODULE_PATH = ["babybuddy.formats"]
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.0/howto/static-files/
# http://whitenoise.evans.io/en/stable/django.html
# Storage
# https://docs.djangoproject.com/en/5.0/ref/files/storage/
# https://docs.djangoproject.com/en/5.0/ref/settings/#std-setting-STORAGES
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
STORAGES = {
"default": {"BACKEND": "django.core.files.storage.FileSystemStorage"},
"staticfiles": {
"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
},
}
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.0/howto/static-files/
# http://whitenoise.evans.io/en/stable/django.html
STATICFILES_FINDERS = [
"django.contrib.staticfiles.finders.FileSystemFinder",
@ -225,7 +233,7 @@ WHITENOISE_ROOT = os.path.join(BASE_DIR, "static", "babybuddy", "root")
# Media files (User uploaded content)
# https://docs.djangoproject.com/en/4.0/topics/files/
# https://docs.djangoproject.com/en/5.0/topics/files/
MEDIA_ROOT = os.path.join(BASE_DIR, "media")
@ -240,11 +248,11 @@ AWS_SECRET_ACCESS_KEY = os.environ.get("AWS_SECRET_ACCESS_KEY") or None
AWS_S3_ENDPOINT_URL = os.environ.get("AWS_S3_ENDPOINT_URL") or None
if AWS_STORAGE_BUCKET_NAME:
DEFAULT_FILE_STORAGE = "storages.backends.s3boto3.S3Boto3Storage"
STORAGES["default"]["BACKEND"] = "storages.backends.s3boto3.S3Boto3Storage"
# Email
# https://docs.djangoproject.com/en/4.0/topics/email/
# https://docs.djangoproject.com/en/5.0/topics/email/
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
EMAIL_SUBJECT_PREFIX = "[Baby Buddy] "
@ -263,17 +271,17 @@ if os.environ.get("EMAIL_HOST"):
# Security
# https://docs.djangoproject.com/en/4.0/ref/settings/#secure-proxy-ssl-header
# https://docs.djangoproject.com/en/5.0/ref/settings/#secure-proxy-ssl-header
if os.environ.get("SECURE_PROXY_SSL_HEADER"):
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
# https://docs.djangoproject.com/en/4.0/topics/http/sessions/#settings
# https://docs.djangoproject.com/en/5.0/topics/http/sessions/#settings
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SECURE = bool(
strtobool(os.environ.get("SESSION_COOKIE_SECURE") or "False")
)
# https://docs.djangoproject.com/en/4.0/ref/csrf/#settings
# https://docs.djangoproject.com/en/5.0/ref/csrf/#settings
CSRF_COOKIE_HTTPONLY = True
CSRF_COOKIE_SECURE = bool(strtobool(os.environ.get("CSRF_COOKIE_SECURE") or "False"))
CSRF_FAILURE_VIEW = "babybuddy.views.csrf_failure"
@ -282,7 +290,7 @@ CSRF_TRUSTED_ORIGINS = list(
)
# https://docs.djangoproject.com/en/4.0/topics/auth/passwords/
# https://docs.djangoproject.com/en/5.0/topics/auth/passwords/
AUTH_PASSWORD_VALIDATORS = [
{
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
@ -349,7 +357,7 @@ AXES_LOCKOUT_URL = "/login/lock"
ROLLING_SESSION_REFRESH = 86400
# Set default auto field for models.
# See https://docs.djangoproject.com/en/4.0/releases/3.2/#customizing-type-of-auto-created-primary-keys
# See https://docs.djangoproject.com/en/5.0/releases/3.2/#customizing-type-of-auto-created-primary-keys
DEFAULT_AUTO_FIELD = "django.db.models.AutoField"

View File

@ -4,6 +4,8 @@ SECRET_KEY = "CISECRETKEYIGUESS"
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.0/howto/static-files/
# https://docs.djangoproject.com/en/5.0/howto/static-files/
STATICFILES_STORAGE = "django.contrib.staticfiles.storage.StaticFilesStorage"
STORAGES["staticfiles"][
"BACKEND"
] = "django.contrib.staticfiles.storage.StaticFilesStorage"

View File

@ -1,20 +1,22 @@
from .base import *
# Quick-start development settings - unsuitable for production
# https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/
# https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/
SECRET_KEY = os.environ.get("SECRET_KEY") or "DEVELOPMENT!!"
DEBUG = bool(strtobool(os.environ.get("DEBUG") or "True"))
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.0/howto/static-files/
# https://docs.djangoproject.com/en/5.0/howto/static-files/
#
# Comment out STATICFILES_STORAGE and uncomment DEBUG = False to test with
# production static files.
# Comment out STORAGES["staticfiles"]["BACKEND"] and uncomment DEBUG = False to
# test with production static files.
# DEBUG = False
STATICFILES_STORAGE = "django.contrib.staticfiles.storage.StaticFilesStorage"
STORAGES["staticfiles"][
"BACKEND"
] = "django.contrib.staticfiles.storage.StaticFilesStorage"
# Django Rest Framework

View File

@ -1,7 +1,7 @@
from .development import *
# CSRF configuration
# https://docs.djangoproject.com/en/4.0/ref/settings/#std:setting-CSRF_TRUSTED_ORIGINS
# https://docs.djangoproject.com/en/5.0/ref/settings/#std:setting-CSRF_TRUSTED_ORIGINS
# https://www.gitpod.io/docs/environment-variables/#default-environment-variables
CSRF_TRUSTED_ORIGINS = [

View File

@ -8,7 +8,7 @@ SECRET_KEY = ""
ALLOWED_HOSTS = [""]
# Database
# https://docs.djangoproject.com/en/4.0/ref/settings/#databases
# https://docs.djangoproject.com/en/5.0/ref/settings/#databases
DATABASES = {
"default": {
@ -18,7 +18,7 @@ DATABASES = {
}
# Media files
# https://docs.djangoproject.com/en/4.0/topics/files/
# https://docs.djangoproject.com/en/5.0/topics/files/
MEDIA_ROOT = os.path.join(BASE_DIR, "../data/media")
@ -26,8 +26,8 @@ MEDIA_ROOT = os.path.join(BASE_DIR, "../data/media")
# After setting up SSL, uncomment the settings below for enhanced security of
# application cookies.
#
# See https://docs.djangoproject.com/en/4.0/topics/http/sessions/#settings
# See https://docs.djangoproject.com/en/4.0/ref/csrf/#settings
# See https://docs.djangoproject.com/en/5.0/topics/http/sessions/#settings
# See https://docs.djangoproject.com/en/5.0/ref/csrf/#settings
# SESSION_COOKIE_SECURE = True
# CSRF_COOKIE_SECURE = True

View File

@ -3,15 +3,15 @@ from .base import *
SECRET_KEY = "TESTS"
# Password hasher configuration
# See https://docs.djangoproject.com/en/4.0/ref/settings/#password-hashers
# See https://docs.djangoproject.com/en/4.0/topics/testing/overview/#password-hashing
# See https://docs.djangoproject.com/en/5.0/ref/settings/#password-hashers
# See https://docs.djangoproject.com/en/5.0/topics/testing/overview/#password-hashing
PASSWORD_HASHERS = [
"django.contrib.auth.hashers.MD5PasswordHasher",
]
# Email
# https://docs.djangoproject.com/en/4.0/topics/email/
# https://docs.djangoproject.com/en/5.0/topics/email/
EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend"

View File

@ -65,8 +65,7 @@ class FormsTestCase(TestCase):
page = self.c.post("/user/password/", params)
self.assertEqual(page.status_code, 200)
self.assertFormError(
page,
"form",
page.context["form"],
"old_password",
"Your old password was entered incorrectly. " "Please enter it again.",
)
@ -75,7 +74,9 @@ class FormsTestCase(TestCase):
page = self.c.post("/user/password/", params)
self.assertEqual(page.status_code, 200)
self.assertFormError(
page, "form", "new_password2", "The two password fields didnt match."
page.context["form"],
"new_password2",
"The two password fields didnt match.",
)
params["new_password2"] = "mynewpassword"
@ -101,7 +102,7 @@ class FormsTestCase(TestCase):
page = self.c.post("/users/{}/delete/".format(new_user.id))
self.assertEqual(page.status_code, 302)
self.assertQuerysetEqual(
self.assertQuerySetEqual(
get_user_model().objects.filter(username="username"), []
)
@ -212,7 +213,9 @@ class FormsTestCase(TestCase):
page = self.c.post("/user/settings/", params)
self.assertEqual(page.status_code, 200)
self.assertFormError(page, "user_form", "email", "Enter a valid email address.")
self.assertFormError(
page.context["user_form"], "email", "Enter a valid email address."
)
def test_user_settings_language(self):
self.c.login(**self.credentials)

View File

@ -210,7 +210,7 @@ class ChildFormsTestCase(FormsTestCaseBase):
)
self.assertEqual(page.status_code, 200)
self.assertFormError(
page, "form", "confirm_name", "Name does not match child name."
page.context["form"], "confirm_name", "Name does not match child name."
)
params["confirm_name"] = str(self.child)
@ -798,7 +798,9 @@ class ValidationsTestCase(FormsTestCaseBase):
page = self.c.post("/weight/{}/".format(entry.id), params, follow=True)
self.assertEqual(page.status_code, 200)
self.assertFormError(page, "form", "date", "Date can not be in the future.")
self.assertFormError(
page.context["form"], "date", "Date can not be in the future."
)
def test_validate_duration(self):
end = timezone.localtime() - timezone.timedelta(minutes=10)
@ -813,14 +815,14 @@ class ValidationsTestCase(FormsTestCaseBase):
page = self.c.post("/tummy-time/add/", params, follow=True)
self.assertEqual(page.status_code, 200)
self.assertFormError(
page, "form", None, "Start time must come before end time."
page.context["form"], None, "Start time must come before end time."
)
start = end - timezone.timedelta(weeks=53)
params["start"] = self.localtime_string(start)
page = self.c.post("/tummy-time/add/", params, follow=True)
self.assertEqual(page.status_code, 200)
self.assertFormError(page, "form", None, "Duration too long.")
self.assertFormError(page.context["form"], None, "Duration too long.")
def test_validate_time(self):
future = timezone.localtime() + timezone.timedelta(hours=1)
@ -833,7 +835,9 @@ class ValidationsTestCase(FormsTestCaseBase):
page = self.c.post("/tummy-time/add/", params, follow=True)
self.assertEqual(page.status_code, 200)
self.assertFormError(page, "form", "end", "Date/time can not be in the future.")
self.assertFormError(
page.context["form"], "end", "Date/time can not be in the future."
)
def test_validate_unique_period(self):
entry = models.TummyTime.objects.create(
@ -854,7 +858,9 @@ class ValidationsTestCase(FormsTestCaseBase):
page = self.c.post("/tummy-time/add/", params, follow=True)
self.assertEqual(page.status_code, 200)
self.assertFormError(
page, "form", None, "Another entry intersects the specified time period."
page.context["form"],
None,
"Another entry intersects the specified time period.",
)

View File

@ -24,8 +24,9 @@ class ImportTestCase(TestCase):
).save()
def get_dataset(self, model_name):
file = open(self.base_path + model_name + ".csv")
return tablib.Dataset().load(file.read())
with open(self.base_path + model_name + ".csv", "r") as f:
data = f.read()
return tablib.Dataset().load(data)
def import_data(self, model, count):
dataset = self.get_dataset(model._meta.model_name)
@ -86,7 +87,7 @@ class ImportTestCase(TestCase):
]
for pk, tags in tests:
entry = models.Temperature.objects.get(pk=pk)
self.assertQuerysetEqual(entry.tags.names(), tags, ordered=False)
self.assertQuerySetEqual(entry.tags.names(), tags, ordered=False)
def test_temperature(self):
self.import_data(models.Temperature, 23)

View File

@ -1,6 +1,4 @@
# -*- coding: utf-8 -*-
import pytz
from django.contrib.auth import get_user_model
from django.test import TestCase
from django.utils import timezone
@ -28,7 +26,7 @@ class TemplateTagsTestCase(TestCase):
# Ensure timezone matches the one defined by fixtures.
user_timezone = Settings.objects.first().timezone
timezone.activate(pytz.timezone(user_timezone))
timezone.activate(user_timezone)
# Test file data uses a basis date of 2017-11-18.
date = timezone.localtime().strptime("2017-11-18", "%Y-%m-%d")
@ -160,9 +158,11 @@ class TemplateTagsTestCase(TestCase):
def test_card_feeding_recent(self):
data = cards.card_feeding_recent(self.context, self.child, self.date)
self.assertEqual(data["type"], "feeding")
self.assertFalse(data["empty"])
self.assertFalse(data["hide_empty"])
# most recent day
self.assertEqual(data["feedings"][0]["total"], 2.5)
self.assertEqual(data["feedings"][0]["count"], 3)
@ -273,7 +273,7 @@ class TemplateTagsTestCase(TestCase):
},
{
"type": "duration",
"stat": timezone.timedelta(days=1, seconds=46980),
"stat": timezone.timedelta(days=1, seconds=39780),
"title": "Feeding frequency",
},
{

View File

@ -7,7 +7,7 @@ information and steps below to set up a local development environment for Baby B
## Requirements
- Python 3.8+, pip, pipenv
- Python 3.10+, pip, pipenv
- NodeJS 18.x and NPM 8.x (NVM recommended)
- Gulp
- Possibly `libpq-dev`

View File

@ -117,14 +117,14 @@ requirements are Python, a web server, an application server, and a database.
### Requirements
- Python 3.8+, pip, pipenv
- Python 3.10+, pip, pipenv
- Web server ([nginx](http://nginx.org/), [Apache](http://httpd.apache.org/), etc.)
- Application server ([uwsgi](http://projects.unbit.it/uwsgi), [gunicorn](http://gunicorn.org/), etc.)
- Database (See [Django's databases documentation](https://docs.djangoproject.com/en/4.2/ref/databases/)).
### Example deployment
*This example assumes a 1 GB VPS instance with Ubuntu 20.04.* It uses Python 3.8,
*This example assumes a 1 GB VPS instance with Ubuntu 20.04.* It uses Python 3.10,
nginx, uwsgi and sqlite. It should be sufficient for a few users (e.g., two parents
and any number of children).

View File

@ -202,6 +202,7 @@ function test(cb) {
let command = [
'run',
'python',
'-Wa',
'manage.py',
'test',
'--settings=babybuddy.settings.test',

View File

@ -23,8 +23,8 @@ class SleepPatternTestCase(TestCase):
models.Sleep.objects.create(
child=c,
start=dt.datetime(2000, 1, 1, 0, 0, tzinfo=timezone.utc),
end=dt.datetime(2000, 1, 1, 0, 1, tzinfo=timezone.utc),
start=dt.datetime(2000, 1, 1, 0, 0, tzinfo=dt.timezone.utc),
end=dt.datetime(2000, 1, 1, 0, 1, tzinfo=dt.timezone.utc),
)
sleep_pattern(models.Sleep.objects.order_by("start"))

View File

@ -1,25 +1,25 @@
-i https://pypi.python.org/simple
annotated-types==0.6.0; python_version >= '3.8'
asgiref==3.7.2; python_version >= '3.7'
boto3==1.33.10; python_version >= '3.7'
botocore==1.33.10; python_version >= '3.7'
boto3==1.34.14; python_version >= '3.8'
botocore==1.34.14; python_version >= '3.8'
defusedxml==0.7.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
diff-match-patch==20230430; python_version >= '3.7'
dj-database-url==2.1.0
django==4.2.8; python_version >= '3.8'
django==5.0.1; python_version >= '3.10'
django-appconf==1.0.6; python_version >= '3.7'
django-axes==6.1.1; python_version >= '3.7'
django-axes==6.3.0; python_version >= '3.7'
django-dbsettings==1.3.0
django-filter==23.5; python_version >= '3.7'
django-imagekit==5.0.0
django-import-export==3.3.3; python_version >= '3.8'
django-qr-code==3.1.1; python_version >= '3.7'
django-import-export==3.3.5; python_version >= '3.8'
django-qr-code==4.0.0; python_version >= '3.7'
django-storages==1.14.2; python_version >= '3.7'
django-taggit==5.0.1; python_version >= '3.8'
django-widget-tweaks==1.5.0; python_version >= '3.8'
djangorestframework==3.14.0; python_version >= '3.6'
et-xmlfile==1.1.0; python_version >= '3.6'
faker==20.1.0; python_version >= '3.8'
faker==22.0.0; python_version >= '3.8'
gunicorn==21.2.0; python_version >= '3.5'
jmespath==1.0.1; python_version >= '3.7'
markuppy==1.14
@ -27,23 +27,23 @@ odfpy==1.4.1
openpyxl==3.1.2
packaging==23.2; python_version >= '3.7'
pilkit==3.0
pillow==10.1.0; python_version >= '3.8'
pillow==10.2.0; python_version >= '3.8'
plotly==5.18.0; python_version >= '3.6'
psycopg2-binary==2.9.9; python_version >= '3.7'
pydantic==2.5.2; python_version >= '3.7'
pydantic-core==2.14.5; python_version >= '3.7'
pydantic==2.5.3; python_version >= '3.7'
pydantic-core==2.14.6; python_version >= '3.7'
python-dateutil==2.8.2; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
python-dotenv==1.0.0; python_version >= '3.8'
pytz==2023.3.post1
pyyaml==6.0.1; python_version >= '3.6'
s3transfer==0.8.2; python_version >= '3.7'
s3transfer==0.10.0; python_version >= '3.8'
segno==1.6.0; python_version >= '3.5'
setuptools==69.0.2; python_version >= '3.8'
setuptools==69.0.3; python_version >= '3.8'
six==1.16.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
sqlparse==0.4.4; python_version >= '3.5'
tablib[html,ods,xls,xlsx,yaml]==3.5.0; python_version >= '3.8'
tenacity==8.2.3; python_version >= '3.7'
typing-extensions==4.8.0; python_version >= '3.8'
typing-extensions==4.9.0; python_version >= '3.8'
uritemplate==4.1.1; python_version >= '3.6'
urllib3==2.0.7; python_version >= '3.10'
whitenoise==6.6.0; python_version >= '3.8'