Skip to content

Commit 6e19ed1

Browse files
committed
Migration to support Django 5.2.
Phase 1, still on Django 4.2, updated the code to be compatible with 5.2. * askbot/deployment/templates/settings_py.jinja2: - added STORAGES setting, removed DEFAULT_FILE_STORAGE * askbot/deps/group_messaging/signals.py: - removed providing_args kwarg from the custom signal definitions * askbot/deps/group_messaging/urls.py: - import re_path as url, remove legacy try/except url imports * askbot/doc/source/changelog.rst: added entry re: migration to django 5.2 * askbot/doc/source/optional-modules.rst: - described migration of DEFAULT_FILE_STORAGE -> STORAGES config * askbot/importers/stackexchange/{models,parse_models}.py: - replaced usage of NullBooleanField with BooleanField(null=True) * askbot/startup_procedures.py: - extract staticfiles storage from the STORAGES setting * askbot/tests/test_startup_procedures.py: - added tests for the get_staticfiles_backend function to assert that it works with both legacy STATICFILES_STORAGE and the STORAGES configs (i.e. that it works with the old format of Django 4.2 and 5.4 settings.py) * docker/extra_settings.py: minor edit * testproject/testproject/settings.py: - migrated to STORAGES setting - added DEFAULT_AUTO_FIELD setting to use the 32 bit AutoField * testproject/testproject/urls.py: - import urls functions from the modern canonical locations, removed try/except import fallbacks - removed the legacy rosetta app urls
1 parent 5852c98 commit 6e19ed1

13 files changed

Lines changed: 156 additions & 48 deletions

File tree

askbot/__init__.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@
1111

1212
VERSION = (0, 12, 9)
1313

14-
default_app_config = 'askbot.apps.AskbotConfig' #pylint: disable=invalid-name
15-
1614
#keys are module names used by python imports,
1715
#values - the package qualifier to use for pip
1816
REQUIREMENTS = {

askbot/deployment/templates/settings_py.jinja2

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,15 @@ FILE_UPLOAD_HANDLERS = (
119119
)
120120
ASKBOT_ALLOWED_UPLOAD_FILE_TYPES = ('.jpg', '.jpeg', '.gif', '.bmp', '.png', '.tiff')
121121
ASKBOT_MAX_UPLOAD_FILE_SIZE = 1024 * 1024 #result in bytes
122-
DEFAULT_FILE_STORAGE = 'django.core.files.storage.FileSystemStorage'
122+
123+
STORAGES = {
124+
'default': {
125+
'BACKEND': 'django.core.files.storage.FileSystemStorage',
126+
},
127+
'staticfiles': {
128+
'BACKEND': 'django.contrib.staticfiles.storage.StaticFilesStorage',
129+
},
130+
}
123131

124132

125133
#TEMPLATE_DIRS = (,) #template have no effect in askbot, use the variable below
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
import django.dispatch
22

3-
thread_created = django.dispatch.Signal(providing_args=['message',])
4-
response_created = django.dispatch.Signal(providing_args=['message',])
3+
thread_created = django.dispatch.Signal()
4+
response_created = django.dispatch.Signal()

askbot/deps/group_messaging/urls.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
"""url configuration for the group_messaging application"""
2-
try:
3-
from django.conf.urls import url
4-
except ImportError:
5-
from django.conf.urls.defaults import url
2+
from django.urls import re_path as url
63

74
from askbot.deps.group_messaging import views
85

askbot/doc/source/changelog.rst

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
11
Changes in Askbot
22
=================
33

4+
Development (not yet released)
5+
------------------------------
6+
* Django 5.2 LTS preparation (no Django bump yet): removed
7+
``default_app_config``, dropped legacy ``Signal(providing_args=...)``,
8+
modernised vendored ``url()`` imports, replaced ``NullBooleanField`` in
9+
the StackExchange importer, and migrated ``DEFAULT_FILE_STORAGE`` to the
10+
``STORAGES`` dict in deployment templates, the test project, the docker
11+
overrides, and the user-facing docs. The startup check for the static
12+
files backend now reads ``STORAGES`` first and falls back to the legacy
13+
setting.
14+
415
0.12.9 (May 24, 2026)
516
---------------------
617
* Optimized multi-tag question search: replaced per-tag JOINs with a single subquery,

askbot/doc/source/optional-modules.rst

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -230,14 +230,29 @@ with::
230230
AWS_S3_CUSTOM_DOMAIN = '%s.s3.amazonaws.com' % AWS_STORAGE_BUCKET_NAME
231231
MEDIA_ROOT = "https://%s/%s/" % (AWS_S3_CUSTOM_DOMAIN, 'media')
232232
233-
In addition, replace the following::
234-
235-
DEFAULT_FILE_STORAGE = 'django.core.files.storage.FileSystemStorage'
236-
233+
In addition, replace the following ``STORAGES`` entry::
234+
235+
STORAGES = {
236+
'default': {
237+
'BACKEND': 'django.core.files.storage.FileSystemStorage',
238+
},
239+
'staticfiles': {
240+
'BACKEND': 'django.contrib.staticfiles.storage.StaticFilesStorage',
241+
},
242+
}
243+
237244
with::
238-
239-
DEFAULT_FILE_STORAGE = 's3utils.MediaRootS3BotoStorage'
240-
245+
246+
STORAGES = {
247+
'default': {
248+
'BACKEND': 's3utils.MediaRootS3BotoStorage',
249+
},
250+
'staticfiles': {
251+
'BACKEND': 'django.contrib.staticfiles.storage.StaticFilesStorage',
252+
},
253+
}
254+
255+
241256
Last but not least, we would need to update the ``INSTALLED_APPS`` field to let Django know that the storage modules have been installed::
242257
243258
INSTALLED_APPS = (

askbot/importers/stackexchange/models.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ class Badge(models.Model):
44
class_type = models.IntegerField(null=True)
55
name = models.CharField(max_length=50, null=True)
66
description = models.TextField(null=True)
7-
single = models.NullBooleanField(null=True)
8-
secret = models.NullBooleanField(null=True)
9-
tag_based = models.NullBooleanField(null=True)
7+
single = models.BooleanField(null=True)
8+
secret = models.BooleanField(null=True)
9+
tag_based = models.BooleanField(null=True)
1010
command = models.TextField(null=True)
1111
award_frequency = models.IntegerField(null=True)
1212

@@ -32,14 +32,14 @@ class FlatPage(models.Model):
3232
url = models.CharField(max_length=128, null=True)
3333
value = models.TextField(null=True)
3434
content_type = models.CharField(max_length=50, null=True)
35-
active = models.NullBooleanField(null=True)
36-
use_master = models.NullBooleanField(null=True)
35+
active = models.BooleanField(null=True)
36+
use_master = models.BooleanField(null=True)
3737

3838
class Message(models.Model):
3939
id = models.IntegerField(primary_key=True)
4040
user = models.ForeignKey('User', related_name='Message_by_user_set', null=True, on_delete=models.CASCADE)
4141
message_type = models.ForeignKey('MessageType', related_name='Message_by_message_type_set', null=True, on_delete=models.CASCADE)
42-
is_read = models.NullBooleanField(null=True)
42+
is_read = models.BooleanField(null=True)
4343
creation_date = models.DateTimeField(null=True)
4444
text = models.TextField(null=True)
4545
post = models.ForeignKey('Post', related_name='Message_by_post_set', null=True, on_delete=models.CASCADE)
@@ -167,8 +167,8 @@ class Tag(models.Model):
167167
count = models.IntegerField(null=True)
168168
user = models.ForeignKey('User', related_name='Tag_by_user_set', null=True, on_delete=models.CASCADE)
169169
creation_date = models.DateTimeField(null=True)
170-
is_moderator_only = models.NullBooleanField(null=True)
171-
is_required = models.NullBooleanField(null=True)
170+
is_moderator_only = models.BooleanField(null=True)
171+
is_required = models.BooleanField(null=True)
172172
aliases = models.CharField(max_length=200, null=True)
173173

174174
class ThemeResource(models.Model):
@@ -231,10 +231,10 @@ class User(models.Model):
231231
views = models.IntegerField(null=True)
232232
creation_date = models.DateTimeField(null=True)
233233
last_access_date = models.DateTimeField(null=True)
234-
has_replies = models.NullBooleanField(null=True)
235-
has_message = models.NullBooleanField(null=True)
236-
opt_in_email = models.NullBooleanField(null=True)
237-
opt_in_recruit = models.NullBooleanField(null=True)
234+
has_replies = models.BooleanField(null=True)
235+
has_message = models.BooleanField(null=True)
236+
opt_in_email = models.BooleanField(null=True)
237+
opt_in_recruit = models.BooleanField(null=True)
238238
last_login_date = models.DateTimeField(null=True)
239239
last_email_date = models.DateTimeField(null=True)
240240
last_login_ip = models.CharField(max_length=15, null=True)

askbot/importers/stackexchange/parse_models.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
'string':'models.CharField',
2020
'text':'models.TextField',
2121
'int':'models.IntegerField',
22-
'boolean':'models.NullBooleanField',
22+
'boolean':'models.BooleanField',
2323
'dateTime':'models.DateTimeField',
2424
'base64Binary':'models.TextField',
2525
'double':'models.IntegerField',

askbot/startup_procedures.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -474,6 +474,24 @@ def test_new_skins():
474474
)
475475

476476

477+
def get_staticfiles_backend():
478+
"""returns the dotted path of the staticfiles storage backend.
479+
480+
reads from the modern STORAGES dict first (Django 4.2+ shape used
481+
by every supported version) and falls back to the legacy
482+
STATICFILES_STORAGE setting for projects that have not migrated
483+
yet. returns an empty string when neither is configured.
484+
"""
485+
storages = getattr(django_settings, 'STORAGES', None)
486+
if isinstance(storages, dict):
487+
staticfiles = storages.get('staticfiles')
488+
if isinstance(staticfiles, dict):
489+
backend = staticfiles.get('BACKEND')
490+
if backend:
491+
return backend
492+
return getattr(django_settings, 'STATICFILES_STORAGE', '') or ''
493+
494+
477495
def test_staticfiles():
478496
"""tests configuration of the staticfiles app"""
479497
errors = list()
@@ -570,7 +588,7 @@ def test_staticfiles():
570588
'ASKBOT_EXTRA_SKINS_DIR just above STATICFILES_DIRS.'
571589
)
572590

573-
if django_settings.STATICFILES_STORAGE == \
591+
if get_staticfiles_backend() == \
574592
'django.contrib.staticfiles.storage.StaticFilesStorage':
575593
if os.path.dirname(django_settings.STATIC_ROOT) == '':
576594
# static root is needed only for local storoge of
@@ -607,7 +625,7 @@ def test_staticfiles():
607625
)
608626

609627
print_errors(errors)
610-
if django_settings.STATICFILES_STORAGE == \
628+
if get_staticfiles_backend() == \
611629
'django.contrib.staticfiles.storage.StaticFilesStorage':
612630

613631
if not os.path.isdir(django_settings.STATIC_ROOT):
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
"""Tests for askbot.startup_procedures helpers."""
2+
from unittest import mock
3+
4+
from django.test import SimpleTestCase
5+
6+
from askbot import startup_procedures
7+
from askbot.startup_procedures import get_staticfiles_backend
8+
9+
10+
LEGACY_BACKEND = 'django.contrib.staticfiles.storage.StaticFilesStorage'
11+
MODERN_BACKEND = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage'
12+
DEFAULT_FILE_BACKEND = 'django.core.files.storage.FileSystemStorage'
13+
14+
15+
class _StubSettings:
16+
"""A tiny object used in place of django.conf.settings.
17+
18+
getattr falls back to the requested default when an attribute is
19+
missing, so we can model a project that has STATICFILES_STORAGE
20+
set but no STORAGES, or vice versa.
21+
"""
22+
23+
def __init__(self, **attrs):
24+
for key, value in attrs.items():
25+
setattr(self, key, value)
26+
27+
28+
class GetStaticfilesBackendTests(SimpleTestCase):
29+
"""get_staticfiles_backend prefers STORAGES, falls back to legacy."""
30+
31+
def _run_with(self, stub):
32+
with mock.patch.object(startup_procedures,
33+
'django_settings', stub):
34+
return get_staticfiles_backend()
35+
36+
def test_legacy_only(self):
37+
"""returns STATICFILES_STORAGE when no STORAGES dict configured."""
38+
stub = _StubSettings(STATICFILES_STORAGE=LEGACY_BACKEND)
39+
self.assertEqual(self._run_with(stub), LEGACY_BACKEND)
40+
41+
def test_storages_only(self):
42+
"""reads from STORAGES dict without falling back to legacy."""
43+
stub = _StubSettings(STORAGES={
44+
'default': {'BACKEND': DEFAULT_FILE_BACKEND},
45+
'staticfiles': {'BACKEND': MODERN_BACKEND},
46+
})
47+
self.assertEqual(self._run_with(stub), MODERN_BACKEND)
48+
49+
def test_storages_wins_when_both_present(self):
50+
"""STORAGES['staticfiles'] beats legacy STATICFILES_STORAGE."""
51+
stub = _StubSettings(
52+
STATICFILES_STORAGE=LEGACY_BACKEND,
53+
STORAGES={
54+
'default': {'BACKEND': DEFAULT_FILE_BACKEND},
55+
'staticfiles': {'BACKEND': MODERN_BACKEND},
56+
},
57+
)
58+
self.assertEqual(self._run_with(stub), MODERN_BACKEND)

0 commit comments

Comments
 (0)