1
1
import logging
2
2
from contextlib import contextmanager
3
3
4
+ import django
4
5
from django .conf import settings
5
6
from django .contrib .contenttypes .fields import GenericForeignKey
6
7
from django .contrib .contenttypes .models import ContentType
7
8
from django .contrib .sites .models import Site
8
9
from django .core .cache import cache
10
+ from django .core .exceptions import ObjectDoesNotExist
9
11
from django .db import models
10
12
from django .db .models .constraints import UniqueConstraint
11
13
from django .template .loader import render_to_string
22
24
from openwisp_notifications .exceptions import NotificationRenderException
23
25
from openwisp_notifications .types import (
24
26
NOTIFICATION_CHOICES ,
27
+ get_notification_choices ,
25
28
get_notification_configuration ,
26
29
)
27
30
from openwisp_notifications .utils import _get_absolute_url , _get_object_link
@@ -53,6 +56,15 @@ def notification_render_attributes(obj, **attrs):
53
56
for target_attr , source_attr in defaults .items ():
54
57
setattr (obj , target_attr , getattr (obj , source_attr ))
55
58
59
+ # In Django 5.1+, GenericForeignKey fields defined in parent models can no longer
60
+ # be overridden in child models (https://code.djangoproject.com/ticket/36295).
61
+ # To avoid multiple database queries, we explicitly set these attributes here
62
+ # using our cached _related_object method instead of relying on the default
63
+ # GenericForeignKey accessor which would bypass our caching mechanism.
64
+ setattr (obj , 'actor' , obj ._related_object ('actor' ))
65
+ setattr (obj , 'action_object' , obj ._related_object ('action_object' ))
66
+ setattr (obj , 'target' , obj ._related_object ('target' ))
67
+
56
68
yield obj
57
69
58
70
for attr in defaults .keys ():
@@ -61,7 +73,21 @@ def notification_render_attributes(obj, **attrs):
61
73
62
74
class AbstractNotification (UUIDModel , BaseNotification ):
63
75
CACHE_KEY_PREFIX = 'ow-notifications-'
64
- type = models .CharField (max_length = 30 , null = True , choices = NOTIFICATION_CHOICES )
76
+ type = models .CharField (
77
+ max_length = 30 ,
78
+ null = True ,
79
+ # TODO: Remove when dropping support for Django 4.2
80
+ choices = (
81
+ NOTIFICATION_CHOICES
82
+ if django .VERSION < (5 , 0 )
83
+ # In Django 5.0+, choices are normalized at model definition,
84
+ # creating a static list of tuples that doesn't update when notification
85
+ # types are dynamically registered or unregistered. Using a callable
86
+ # ensures we always get the current choices from the registry.
87
+ else get_notification_choices
88
+ ),
89
+ verbose_name = _ ('Notification Type' ),
90
+ )
65
91
_actor = BaseNotification .actor
66
92
_action_object = BaseNotification .action_object
67
93
_target = BaseNotification .target
@@ -117,7 +143,7 @@ def _get_related_object_url(self, field):
117
143
return url_callable (self , field = field , absolute_url = True )
118
144
except ImportError :
119
145
return url
120
- return _get_object_link (self , field = field , absolute_url = True )
146
+ return _get_object_link (obj = self . _related_object ( field ) , absolute_url = True )
121
147
122
148
@property
123
149
def actor_url (self ):
@@ -200,7 +226,19 @@ def _related_object(self, field):
200
226
cache_key = self ._cache_key (obj_content_type_id , obj_id )
201
227
obj = cache .get (cache_key )
202
228
if not obj :
203
- obj = getattr (self , f'_{ field } ' )
229
+ try :
230
+ obj = getattr (self , f'_{ field } ' )
231
+ except AttributeError :
232
+ # Django 5.1+ no longer respects overridden GenericForeignKey fields in model definitions.
233
+ # Using `_actor = BaseNotification.actor` doesn't work as expected.
234
+ # We must manually fetch the related object using content type and object ID.
235
+ # See: https://code.djangoproject.com/ticket/36295
236
+ try :
237
+ obj = ContentType .objects .get_for_id (
238
+ obj_content_type_id
239
+ ).get_object_for_this_type (pk = obj_id )
240
+ except ObjectDoesNotExist :
241
+ obj = None
204
242
cache .set (
205
243
cache_key ,
206
244
obj ,
@@ -246,8 +284,17 @@ class AbstractNotificationSetting(UUIDModel):
246
284
type = models .CharField (
247
285
max_length = 30 ,
248
286
null = True ,
249
- choices = NOTIFICATION_CHOICES ,
250
- verbose_name = 'Notification Type' ,
287
+ # TODO: Remove when dropping support for Django 4.2
288
+ choices = (
289
+ NOTIFICATION_CHOICES
290
+ if django .VERSION < (5 , 0 )
291
+ # In Django 5.0+, choices are normalized at model definition,
292
+ # creating a static list of tuples that doesn't update when notification
293
+ # types are dynamically registered or unregistered. Using a callable
294
+ # ensures we always get the current choices from the registry.
295
+ else get_notification_choices
296
+ ),
297
+ verbose_name = _ ('Notification Type' ),
251
298
)
252
299
organization = models .ForeignKey (
253
300
get_model_name ('openwisp_users' , 'Organization' ),
0 commit comments