diff --git a/django/core/management.py b/django/core/management.py index 88c4c3c251..f303742bfd 100644 --- a/django/core/management.py +++ b/django/core/management.py @@ -524,7 +524,7 @@ def install(app): # First, try validating the models. s = StringIO() - num_errors = get_validation_errors(s) + num_errors = get_validation_errors(s, app) if num_errors: sys.stderr.write("Error: %s couldn't be installed, because there were errors in your model:\n" % app_name) s.seek(0) @@ -559,7 +559,7 @@ def reset(app): # First, try validating the models. s = StringIO() - num_errors = get_validation_errors(s) + num_errors = get_validation_errors(s, app) if num_errors: sys.stderr.write("Error: %s couldn't be installed, because there were errors in your model:\n" % app_name) s.seek(0) @@ -819,13 +819,17 @@ class ModelErrorCollection: self.errors.append((opts, error)) self.outfile.write("%s.%s: %s\n" % (opts.app_label, opts.module_name, error)) -def get_validation_errors(outfile): - "Validates all installed models. Writes errors, if any, to outfile. Returns number of errors." +def get_validation_errors(outfile, app=None): + """ + Validates all models that are part of the specified app. If no app name is provided, + validates all models of all installed apps. Writes errors, if any, to outfile. + Returns number of errors. + """ from django.db import models from django.db.models.fields.related import RelatedObject e = ModelErrorCollection(outfile) - for cls in models.get_models(): + for cls in models.get_models(app): opts = cls._meta # Do field-specific validation. @@ -864,6 +868,9 @@ def get_validation_errors(outfile): # existing fields, m2m fields, m2m related objects or related objects if f.rel: rel_opts = f.rel.to._meta + if f.rel.to not in models.get_models(): + e.add(opts, "'%s' field: relates to uninstalled model %s" % (f.name, rel_opts.object_name)) + rel_name = RelatedObject(f.rel.to, cls, f).OLD_get_accessor_name() if rel_name in [r.name for r in rel_opts.fields]: e.add(opts, "'%s.%s' related field: Clashes with field on '%s.%s'" % (opts.object_name, f.name, rel_opts.object_name, rel_name)) @@ -879,6 +886,9 @@ def get_validation_errors(outfile): # existing fields, m2m fields, m2m related objects or related objects if f.rel: rel_opts = f.rel.to._meta + if f.rel.to not in models.get_models(): + e.add(opts, "'%s' field: has m2m relation with uninstalled model %s" % (f.name, rel_opts.object_name)) + rel_name = RelatedObject(f.rel.to, cls, f).OLD_get_accessor_name() if rel_name in [r.name for r in rel_opts.fields]: e.add(opts, "'%s.%s' related m2m field: Clashes with field on '%s.%s'" % (opts.object_name, f.name, rel_opts.object_name, rel_name)) diff --git a/tests/modeltests/invalid_models/__init__.py b/tests/modeltests/invalid_models/__init__.py new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/tests/modeltests/invalid_models/__init__.py @@ -0,0 +1 @@ + diff --git a/tests/modeltests/invalid_models/models.py b/tests/modeltests/invalid_models/models.py new file mode 100644 index 0000000000..02f7288dbf --- /dev/null +++ b/tests/modeltests/invalid_models/models.py @@ -0,0 +1,26 @@ +""" +26. A test to check that the model validator works can correctly identify errors in a model. +""" + +from django.db import models + +class FieldErrors(models.Model): + charfield = models.CharField() + floatfield = models.FloatField() + filefield = models.FileField() + prepopulate = models.CharField(maxlength=10, prepopulate_from='bad') + choices = models.CharField(maxlength=10, choices='bad') + choices2 = models.CharField(maxlength=10, choices=[(1,2,3),(1,2,3)]) + index = models.CharField(maxlength=10, db_index='bad') + + +error_log = """invalid_models.fielderrors: "charfield" field: CharFields require a "maxlength" attribute. +invalid_models.fielderrors: "floatfield" field: FloatFields require a "decimal_places" attribute. +invalid_models.fielderrors: "floatfield" field: FloatFields require a "max_digits" attribute. +invalid_models.fielderrors: "filefield" field: FileFields require an "upload_to" attribute. +invalid_models.fielderrors: "prepopulate" field: prepopulate_from should be a list or tuple. +invalid_models.fielderrors: "choices" field: "choices" should be either a tuple or list. +invalid_models.fielderrors: "choices2" field: "choices" should be a sequence of two-tuples. +invalid_models.fielderrors: "choices2" field: "choices" should be a sequence of two-tuples. +invalid_models.fielderrors: "index" field: "db_index" should be either None, True or False. +""" diff --git a/tests/runtests.py b/tests/runtests.py index 9e96a9229e..ba387502d0 100755 --- a/tests/runtests.py +++ b/tests/runtests.py @@ -82,6 +82,14 @@ class TestRunner: # Determine which models we're going to test. test_models = get_test_models() + if 'othertests' in self.which_tests: + self.which_tests.remove('othertests') + run_othertests = True + if not self.which_tests: + test_models = [] + else: + run_othertests = not self.which_tests + if self.which_tests: # Only run the specified tests. bad_models = [m for m in self.which_tests if m not in test_models] @@ -138,21 +146,39 @@ class TestRunner: except Exception, e: log_error(model_name, "Error while importing", ''.join(traceback.format_exception(*sys.exc_info())[1:])) continue - self.output(1, "%s model: Installing" % model_name) - management.install(mod) + + if not getattr(mod, 'error_log', None): + # Model is not marked as an invalid model + self.output(1, "%s model: Installing" % model_name) + management.install(mod) - # Run the API tests. - p = doctest.DocTestParser() - test_namespace = dict([(m._meta.object_name, m) \ - for m in django.db.models.get_models(mod)]) - dtest = p.get_doctest(mod.API_TESTS, test_namespace, model_name, None, None) - # Manually set verbose=False, because "-v" command-line parameter - # has side effects on doctest TestRunner class. - runner = DjangoDoctestRunner(verbosity_level=verbosity_level, verbose=False) - self.output(1, "%s model: Running tests" % model_name) - runner.run(dtest, clear_globs=True, out=sys.stdout.write) + # Run the API tests. + p = doctest.DocTestParser() + test_namespace = dict([(m._meta.object_name, m) \ + for m in django.db.models.get_models(mod)]) + dtest = p.get_doctest(mod.API_TESTS, test_namespace, model_name, None, None) + # Manually set verbose=False, because "-v" command-line parameter + # has side effects on doctest TestRunner class. + runner = DjangoDoctestRunner(verbosity_level=verbosity_level, verbose=False) + self.output(1, "%s model: Running tests" % model_name) + runner.run(dtest, clear_globs=True, out=sys.stdout.write) + else: + # Check that model known to be invalid is invalid for the right reasons. + self.output(1, "%s model: Validating" % model_name) + + from cStringIO import StringIO + s = StringIO() + count = management.get_validation_errors(s, mod) + s.seek(0) + error_log = s.read() + expected = len(mod.error_log.split('\n')) - 1 + if error_log != mod.error_log: + log_error(model_name, + "Validator found %d validation errors, %d expected" % (count, expected), + "Expected errors:\n%s\n\nActual errors:\n%s" % (mod.error_log, error_log)) - if not self.which_tests: + + if run_othertests: # Run the non-model tests in the other tests dir self.output(1, "Running other tests") other_tests_dir = os.path.join(os.path.dirname(__file__), OTHER_TESTS_DIR)