77
88class Command (BaseCommand ):
99 help = (
10- 'Remove duplicate NotificationSubscription records, keeping only '
11- 'the highest-id record per (user, content_type, object_id, notification_type).'
10+ 'Remove duplicate NotificationSubscription records, keeping only the highest-id record: '
11+ 'Default uniqueness: (user, content_type, object_id, notification_type, is_digest); '
12+ 'Optional uniqueness with --exclude-is-digest: (user, content_type, object_id, notification_type).'
1213 )
1314
1415 def add_arguments (self , parser ):
@@ -17,39 +18,53 @@ def add_arguments(self, parser):
1718 action = 'store_true' ,
1819 help = 'Show how many rows would be deleted without deleting anything.' ,
1920 )
21+ parser .add_argument (
22+ '--exclude-is-digest' ,
23+ action = 'store_true' ,
24+ default = False ,
25+ help = 'Whether to exclude _is_digest field in unique_together' )
2026
2127 def handle (self , * args , ** options ):
28+
2229 self .stdout .write ('Finding duplicate NotificationSubscription records…' )
2330
24- to_remove = NotificationSubscription .objects .filter (
25- Exists (
26- NotificationSubscription .objects .filter (
27- user_id = OuterRef ('user_id' ),
28- content_type_id = OuterRef ('content_type_id' ),
29- object_id = OuterRef ('object_id' ),
30- notification_type_id = OuterRef ('notification_type_id' ),
31- _is_digest = OuterRef ('_is_digest' ),
32- id__gt = OuterRef ('id' ), # keep most recent record
31+ if options ['exclude_is_digest' ]:
32+ to_remove = NotificationSubscription .objects .filter (
33+ Exists (
34+ NotificationSubscription .objects .filter (
35+ user_id = OuterRef ('user_id' ),
36+ content_type_id = OuterRef ('content_type_id' ),
37+ object_id = OuterRef ('object_id' ),
38+ notification_type_id = OuterRef ('notification_type_id' ),
39+ id__gt = OuterRef ('id' ), # keep most recent record
40+ )
41+ )
42+ )
43+ else :
44+ to_remove = NotificationSubscription .objects .filter (
45+ Exists (
46+ NotificationSubscription .objects .filter (
47+ user_id = OuterRef ('user_id' ),
48+ content_type_id = OuterRef ('content_type_id' ),
49+ object_id = OuterRef ('object_id' ),
50+ notification_type_id = OuterRef ('notification_type_id' ),
51+ _is_digest = OuterRef ('_is_digest' ),
52+ id__gt = OuterRef ('id' ), # keep most recent record
53+ )
3354 )
3455 )
35- )
3656
3757 count = to_remove .count ()
3858 self .stdout .write (f"Duplicates to remove: { count } " )
3959
40- if options ['dry' ]:
41- self .stdout .write (
42- self .style .WARNING ('Dry run enabled — no records were deleted.' )
43- )
44- return
45-
4660 if count == 0 :
4761 self .stdout .write (self .style .SUCCESS ('No duplicates found.' ))
48- return
4962
50- with transaction .atomic ():
51- deleted , _ = to_remove .delete ()
63+ if options ['dry' ]:
64+ self .stdout .write (self .style .WARNING ('Dry run enabled — no records were deleted.' ))
65+ return
5266
53- self .stdout .write (
54- self .style .SUCCESS (f"Successfully removed { deleted } duplicate records." )
55- )
67+ if count > 0 :
68+ with transaction .atomic ():
69+ deleted , _ = to_remove .delete ()
70+ self .stdout .write (self .style .SUCCESS (f"Successfully removed { deleted } duplicate records." ))
0 commit comments