Skip to content

Commit 58d8c2f

Browse files
committed
Updated the check_referential_integrity command, adding a --delete command to remove dangling references to removed records, and also adding colorized success/fail indications for each FK and m2m key inspected.
- Legacy-Id: 17298
1 parent e79eb56 commit 58d8c2f

1 file changed

Lines changed: 88 additions & 24 deletions

File tree

Lines changed: 88 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
1-
# Copyright The IETF Trust 2015-2019, All Rights Reserved
1+
# Copyright The IETF Trust 2015-2020, All Rights Reserved
22
# -*- coding: utf-8 -*-
33

44

55
from __future__ import absolute_import, print_function, unicode_literals
66

7-
import six
8-
97
from tqdm import tqdm
108

119
import django
@@ -14,16 +12,28 @@
1412
from django.apps import apps
1513
from django.core.management.base import BaseCommand #, CommandError
1614
from django.core.exceptions import FieldError
17-
from django.db.models.fields.related import ForeignKey, OneToOneField
15+
from django.db import IntegrityError
16+
from django.db.models.fields.related import ForeignKey, OneToOneField, ManyToManyField
17+
1818

1919
import debug # pyflakes:ignore
2020

2121
class Command(BaseCommand):
2222
help = "Check all models for referential integrity."
2323

24+
def add_arguments(self, parser):
25+
parser.add_argument(
26+
'--delete', action='store_true', default=False,
27+
help="Delete dangling references",
28+
)
29+
30+
2431
def handle(self, *args, **options):
2532
verbosity = options.get("verbosity", 1)
2633
verbose = verbosity > 1
34+
if verbosity > 1:
35+
self.stdout.ending = None
36+
self.stderr.ending = None
2737

2838
def check_field(field):
2939
try:
@@ -32,20 +42,48 @@ def check_field(field):
3242
debug.pprint('dir(field)')
3343
raise
3444
if verbosity > 1:
35-
six.print_(" %s -> %s.%s" % (field.name,foreign_model.__module__,foreign_model.__name__), end=' ')
36-
used = set(field.model.objects.values_list(field.name,flat=True))
45+
self.stdout.write(" [....] %s -> %s.%s" % (
46+
field.name, foreign_model.__module__, foreign_model.__name__))
47+
self.stdout.flush()
48+
used = set(field.model.objects.values_list(field.name, flat=True))
3749
used.discard(None)
38-
exists = set(foreign_model.objects.values_list('pk',flat=True))
50+
exists = set(foreign_model.objects.values_list('pk', flat=True))
51+
dangling = used - exists
3952
if verbosity > 1:
40-
if used - exists:
41-
six.print_(" ** Bad key values:",list(used - exists))
53+
if dangling:
54+
self.stdout.write("\r ["+self.style.ERROR("fail")+"]\n ** Bad key values: %s\n" % sorted(list(dangling)))
4255
else:
43-
six.print_(" ok")
56+
self.stdout.write("\r [ "+self.style.SUCCESS('ok')+" ]\n")
4457
else:
45-
if used - exists:
46-
six.print_("\n%s.%s.%s -> %s.%s ** Bad key values:" % (model.__module__,model.__name__,field.name,foreign_model.__module__,foreign_model.__name__),list(used - exists))
58+
if dangling:
59+
self.stdout.write("\n%s.%s.%s -> %s.%s ** Bad key values:\n %s\n" % (model.__module__, model.__name__, field.name, foreign_model.__module__, foreign_model.__name__, sorted(list(dangling))))
60+
61+
if dangling and options.get('delete'):
62+
if verbosity > 1:
63+
self.stdout.write("Removing dangling values: %s.%s.%s\n" % (model.__module__, model.__name__, field.name, ))
64+
for value in dangling:
65+
kwargs = { field.name: value }
66+
for obj in field.model.objects.filter(**kwargs):
67+
if verbosity > 1:
68+
self.stdout.write('.', ending=None)
69+
self.stdout.flush()
70+
try:
71+
if isinstance(field, (ForeignKey, OneToOneField)):
72+
setattr(obj, field.name, None)
73+
obj.save()
74+
elif isinstance(field, (ManyToManyField, )):
75+
manager = getattr(obj, field.name)
76+
manager.remove(value)
77+
else:
78+
self.stderr.write("\nUnexpected field type: %s\n" % type(field))
79+
except IntegrityError as e:
80+
self.stderr.write('\n')
81+
self.stderr.write("Tried setting %s[%s].%s to %s, but got:\n" % (model.__name__, obj.pk, field.name, None))
82+
self.stderr.write("Exception: %s\n" % e)
83+
if verbosity > 1:
84+
self.stdout.write('\n')
4785

48-
def check_reverse_field(field):
86+
def check_many_to_many_field(field):
4987
try:
5088
foreign_model = field.related_model
5189
except Exception:
@@ -56,35 +94,61 @@ def check_reverse_field(field):
5694
foreign_field_name = field.remote_field.name
5795
foreign_accessor_name = field.remote_field.get_accessor_name()
5896
if verbosity > 1:
59-
six.print_(" %s <- %s -> %s.%s" % (field.model.__name__, field.remote_field.through._meta.db_table, foreign_model.__module__, foreign_model.__name__), end=' ')
97+
self.stdout.write(" [....] %s <- %s ( -> %s.%s)" %
98+
(field.model.__name__, field.remote_field.through._meta.db_table,
99+
foreign_model.__module__, foreign_model.__name__))
100+
self.stdout.flush()
101+
60102
try:
61103
used = set(foreign_model.objects.values_list(foreign_field_name, flat=True))
104+
accessor_name = foreign_field_name
62105
except FieldError:
63106
try:
64107
used = set(foreign_model.objects.values_list(foreign_accessor_name, flat=True))
108+
accessor_name = foreign_accessor_name
65109
except FieldError:
66-
six.print_(" ** Warning: could not find reverse name for %s.%s -> %s.%s" % (field.model.__module__, field.model.__name__, foreign_model.__name__, foreign_field_name), end=' ')
110+
self.stdout.write("\n ** Warning: could not find foreign field name for %s.%s -> %s.%s\n" %
111+
(field.model.__module__, field.model.__name__,
112+
foreign_model.__name__, foreign_field_name))
67113
used.discard(None)
68114
exists = set(field.model.objects.values_list('pk',flat=True))
115+
dangling = used - exists
69116
if verbosity > 1:
70-
if used - exists:
71-
six.print_(" ** Bad key values:\n ",list(used - exists))
117+
if dangling:
118+
self.stdout.write("\r ["+self.style.ERROR("fail")+"]\n ** Bad key values:\n %s\n" % sorted(list(dangling)))
72119
else:
73-
six.print_(" ok")
120+
self.stdout.write("\r [ "+self.style.SUCCESS("ok")+" ]\n")
74121
else:
75-
if used - exists:
76-
six.print_("\n%s.%s <- %s -> %s.%s ** Bad key values:\n " % (field.model.__module__, field.model.__name__, field.remote_field.through._meta.db_table, foreign_model.__module__, foreign_model.__name__), list(used - exists))
122+
if dangling:
123+
self.stdout.write("\n%s.%s <- %s (-> %s.%s) ** Bad target key values:\n %s\n" %
124+
(field.model.__module__, field.model.__name__,
125+
field.remote_field.through._meta.db_table,
126+
foreign_model.__module__, foreign_model.__name__,
127+
sorted(list(dangling))))
128+
129+
if dangling and options.get('delete'):
130+
through = field.remote_field.through
131+
if verbosity > 1:
132+
self.stdout.write("Removing dangling entries from %s.%s\n" % (through._meta.app_label, through.__name__))
133+
134+
kwargs = { accessor_name+'_id__in': dangling }
135+
to_delete = field.remote_field.through.objects.filter(**kwargs)
136+
count = to_delete.count()
137+
to_delete.delete()
138+
if verbosity > 1:
139+
self.stdout.write("Removed %s entries from through table %s.%s\n" % (count, through._meta.app_label, through.__name__))
140+
77141

78-
for conf in tqdm([ c for c in apps.get_app_configs() if c.name.startswith('ietf.')], desc='apps', disable=verbose):
142+
for conf in tqdm([ c for c in apps.get_app_configs() if c.name.startswith('ietf')], desc='apps ', disable=verbose):
79143
if verbosity > 1:
80-
six.print_("Checking", conf.name)
144+
self.stdout.write("\nChecking %s\n" % conf.name)
81145
for model in tqdm(list(conf.get_models()), desc='models', disable=verbose):
82146
if model._meta.proxy:
83147
continue
84148
if verbosity > 1:
85-
six.print_(" %s.%s" % (model.__module__,model.__name__))
149+
self.stdout.write(" %s.%s\n" % (model.__module__,model.__name__))
86150
for field in [f for f in model._meta.fields if isinstance(f, (ForeignKey, OneToOneField)) ]:
87151
check_field(field)
88152
for field in [f for f in model._meta.many_to_many ]:
89153
check_field(field)
90-
check_reverse_field(field)
154+
check_many_to_many_field(field)

0 commit comments

Comments
 (0)