vendredi 8 janvier 2016

Django raises a `ProgrammingError` when testing — but only with the MySQL backend

In a current Django project I get a django.db.utils.ProgrammingError every time I run test trough django-admin test — but only if I use the MySQL backend (using mysqlclient for Python 3); the tests run fine if I use the SQLite backend:

django.db.utils.ProgrammingError: (1064, "You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'None, None) NOT NULL)' at line 1")

See below for a full traceback.

The project itself and the admin are running fine — with both backends, despite the tests bailing and with the build in server, as well as a WSGI app. The error only happens when running the tests.

I have a models.py in my core package, that defines some abstract models and mixins that are used by my Django apps. There's also a test.py in that package, in which some models are defined, that use those abstract classes and mixins, so I can test them. I guess the error is there… somewhere; but the I can't figure it out from the exception.

Since the "real" models are only in test.py and not in models.py, I have no clue on how to inspect the raw SQL with django-admin sql or similar…

Full Traceback

$ django-admin test blog -v 3
Creating test database for alias 'default' ('yogalessontv_test')...
Got an error creating the test database: (1007, "Can't create database 'yogalessontv_test'; database exists")
Type 'yes' if you would like to try deleting the test database 'yogalessontv_test', or 'no' to cancel: yes
Destroying old test database 'default'...
Operations to perform:
  Synchronize unmigrated apps: streambox, profiles, core, suit, contact, allauth, messages, staticfiles, debug_toolbar
  Apply all migrations: admin, auth, carousel, sites, pages, socialaccount, blog, contenttypes, account, sessions, videos
Synchronizing apps without migrations:
Running pre-migrate handlers for application core
Running pre-migrate handlers for application pages
Running pre-migrate handlers for application blog
Running pre-migrate handlers for application videos
Running pre-migrate handlers for application carousel
Running pre-migrate handlers for application suit
Running pre-migrate handlers for application admin
Running pre-migrate handlers for application auth
Running pre-migrate handlers for application contenttypes
Running pre-migrate handlers for application sessions
Running pre-migrate handlers for application sites
Running pre-migrate handlers for application allauth
Running pre-migrate handlers for application account
Running pre-migrate handlers for application socialaccount
Running pre-migrate handlers for application debug_toolbar
  Creating tables...
    Creating table core_trimstringsmixintestmodel
    Creating table core_playtimemixintestmodel
Traceback (most recent call last):
  File "/home/xxx/virtualenv/myproject/lib/python3.4/site-packages/django/db/backends/utils.py", line 62, in execute
    return self.cursor.execute(sql)
  File "/home/xxx/virtualenv/myproject/lib/python3.4/site-packages/django/db/backends/mysql/base.py", line 124, in execute
    return self.cursor.execute(query, args)
  File "/home/xxx/virtualenv/myproject/lib/python3.4/site-packages/MySQLdb/cursors.py", line 226, in execute
    self.errorhandler(self, exc, value)
  File "/home/xxx/virtualenv/myproject/lib/python3.4/site-packages/MySQLdb/connections.py", line 36, in defaulterrorhandler
    raise errorvalue
  File "/home/xxx/virtualenv/myproject/lib/python3.4/site-packages/MySQLdb/cursors.py", line 217, in execute
    res = self._query(query)
  File "/home/xxx/virtualenv/myproject/lib/python3.4/site-packages/MySQLdb/cursors.py", line 378, in _query
    rowcount = self._do_query(q)
  File "/home/xxx/virtualenv/myproject/lib/python3.4/site-packages/MySQLdb/cursors.py", line 341, in _do_query
    db.query(q)
  File "/home/xxx/virtualenv/myproject/lib/python3.4/site-packages/MySQLdb/connections.py", line 280, in query
    _mysql.connection.query(self, query)
_mysql_exceptions.ProgrammingError: (1064, "You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'None, None) NOT NULL)' at line 1")

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/home/xxx/virtualenv/myproject/bin/django-admin", line 11, in <module>
    sys.exit(execute_from_command_line())
  File "/home/xxx/virtualenv/myproject/lib/python3.4/site-packages/django/core/management/__init__.py", line 338, in execute_from_command_line
    utility.execute()
  File "/home/xxx/virtualenv/myproject/lib/python3.4/site-packages/django/core/management/__init__.py", line 330, in execute
    self.fetch_command(subcommand).run_from_argv(self.argv)
  File "/home/xxx/virtualenv/myproject/lib/python3.4/site-packages/django/core/management/commands/test.py", line 30, in run_from_argv
    super(Command, self).run_from_argv(argv)
  File "/home/xxx/virtualenv/myproject/lib/python3.4/site-packages/django/core/management/base.py", line 390, in run_from_argv
    self.execute(*args, **cmd_options)
  File "/home/xxx/virtualenv/myproject/lib/python3.4/site-packages/django/core/management/commands/test.py", line 74, in execute
    super(Command, self).execute(*args, **options)
  File "/home/xxx/virtualenv/myproject/lib/python3.4/site-packages/django/core/management/base.py", line 441, in execute
    output = self.handle(*args, **options)
  File "/home/xxx/virtualenv/myproject/lib/python3.4/site-packages/django/core/management/commands/test.py", line 90, in handle
    failures = test_runner.run_tests(test_labels)
  File "/home/xxx/virtualenv/myproject/lib/python3.4/site-packages/django/test/runner.py", line 210, in run_tests
    old_config = self.setup_databases()
  File "/home/xxx/virtualenv/myproject/lib/python3.4/site-packages/django/test/runner.py", line 166, in setup_databases
    **kwargs
  File "/home/xxx/virtualenv/myproject/lib/python3.4/site-packages/django/test/runner.py", line 370, in setup_databases
    serialize=connection.settings_dict.get("TEST", {}).get("SERIALIZE", True),
  File "/home/xxx/virtualenv/myproject/lib/python3.4/site-packages/django/db/backends/base/creation.py", line 368, in create_test_db
    test_flush=True,
  File "/home/xxx/virtualenv/myproject/lib/python3.4/site-packages/django/core/management/__init__.py", line 120, in call_command
    return command.execute(*args, **defaults)
  File "/home/xxx/virtualenv/myproject/lib/python3.4/site-packages/django/core/management/base.py", line 441, in execute
    output = self.handle(*args, **options)
  File "/home/xxx/virtualenv/myproject/lib/python3.4/site-packages/django/core/management/commands/migrate.py", line 179, in handle
    created_models = self.sync_apps(connection, executor.loader.unmigrated_apps)
  File "/home/xxx/virtualenv/myproject/lib/python3.4/site-packages/django/core/management/commands/migrate.py", line 309, in sync_apps
    editor.create_model(model)
  File "/home/xxx/virtualenv/myproject/lib/python3.4/site-packages/django/db/backends/base/schema.py", line 282, in create_model
    self.execute(sql, params or None)
  File "/home/xxx/virtualenv/myproject/lib/python3.4/site-packages/django/db/backends/base/schema.py", line 107, in execute
    cursor.execute(sql, params)
  File "/home/xxx/virtualenv/myproject/lib/python3.4/site-packages/django/db/backends/utils.py", line 64, in execute
    return self.cursor.execute(sql, params)
  File "/home/xxx/virtualenv/myproject/lib/python3.4/site-packages/django/db/utils.py", line 97, in __exit__
    six.reraise(dj_exc_type, dj_exc_value, traceback)
  File "/home/xxx/virtualenv/myproject/lib/python3.4/site-packages/django/utils/six.py", line 658, in reraise
    raise value.with_traceback(tb)
  File "/home/xxx/virtualenv/myproject/lib/python3.4/site-packages/django/db/backends/utils.py", line 62, in execute
    return self.cursor.execute(sql)
  File "/home/xxx/virtualenv/myproject/lib/python3.4/site-packages/django/db/backends/mysql/base.py", line 124, in execute
    return self.cursor.execute(query, args)
  File "/home/xxx/virtualenv/myproject/lib/python3.4/site-packages/MySQLdb/cursors.py", line 226, in execute
    self.errorhandler(self, exc, value)
  File "/home/xxx/virtualenv/myproject/lib/python3.4/site-packages/MySQLdb/connections.py", line 36, in defaulterrorhandler
    raise errorvalue
  File "/home/xxx/virtualenv/myproject/lib/python3.4/site-packages/MySQLdb/cursors.py", line 217, in execute
    res = self._query(query)
  File "/home/xxx/virtualenv/myproject/lib/python3.4/site-packages/MySQLdb/cursors.py", line 378, in _query
    rowcount = self._do_query(q)
  File "/home/xxx/virtualenv/myproject/lib/python3.4/site-packages/MySQLdb/cursors.py", line 341, in _do_query
    db.query(q)
  File "/home/xxx/virtualenv/myproject/lib/python3.4/site-packages/MySQLdb/connections.py", line 280, in query
    _mysql.connection.query(self, query)
django.db.utils.ProgrammingError: (1064, "You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'None, None) NOT NULL)' at line 1")

Core models file

# -*- coding: UTF-8 -*-
# /project_root/project_base/core/models.py


"""
Abstract models and common mixins for Django models.

"""

from __future__ import absolute_import
from __future__ import unicode_literals

from django.db import models
from django.utils.safestring import mark_safe
from django.utils.html import format_html
from django.template.defaultfilters import (
  slugify,
  filesizeformat,
)
from django.core.validators import (
  MinLengthValidator,
  MaxLengthValidator,
)

from utils.timespans import seconds_to_string
from utils.markup import parse_markdown


MARKDOWN_CONTENT_HELP = (
  "…"
  )

MARKDOWN_CONTENT_HELP_SHORT = (
  "Inhalt des Artikels oder der Seite. Hier kann sowohl HTML direkt genutzt werden (z.B. um Videos einzubinden), als auch Markdown (<a href=\" http://ift.tt/22Qc9rz\">Spickzettel</a>)."
)


class TrimStringsMixin(object):

  """
  Mixin that hooks into :meth:`clean_fields` to strip whithespace from
  the content of text fields before cleaning them.

  """

  def strip_whitespace(self):
    """
    Strips whithespace from text-fields.

    """
    for field in self._meta.fields:
      value = getattr(self, field.name)
      if value:
        try:
          setattr(self, field.name, value.strip())
        except AttributeError:
          pass

  def clean_fields(self, *args, **kwargs):
    """
    Calls :meth:`strip_whitespace` and then `super`.

    """
    self.strip_whitespace()
    super(TrimStringsMixin, self).clean_fields(*args, **kwargs)


class PlaytimeMixin(object):

  """
  Adds a :attr:`playtime` property that fromats the playtime using
  :func:`seconds_to_string`.

  For the property a :attr:`seconds` attribute needs to be present on the
  model, as field or trough the query set.

  """

  def get_playtime(self):
    """
    Returns the :attr:`seconds` as ``h:mm:ss`` string.

    Or ``?`` if :attr:`seconds` is missing.

    """
    try:
      seconds = self.seconds if self.seconds else 0
      return seconds_to_string(seconds, force_minutes=True)
    except AttributeError:
      return '?'

  playtime = property(get_playtime)


class OrderedListMixin(object):

  """
  Mixin with some methods to automatically arrange ordered items.

  Saved objects are put at the end of the list if no *position* is set.

  .. note::

    "End of the list" means to the highest *position* + 1.

    "Start of the list" means to the lowest *position* - 1.

  .. important::

    Models need to have a **position** attribute, e.g. inherit from
    :cls:`OrderedObject`.

  Ordered
  -------

  Per default, the standard manager ``objects`` is used to get the
  objects on which :meth:`last()` and :meth:`first()` operate.

  In most cases those objects are sorted by ``position`` because of the meta
  settings. You can implement :meth:`get_ordered_objects` in subclasses to
  return a :cls:`django.db.models.QuerySet` that is used instead.

  Ordered Group
  ~~~~~~~~~~~~~

  So you can have more that one **ordered group** in a model, e.g. journal
  enties sorted by position but based on a FK to a journal…

  """

  def save(self, move_to_end=True, *args, **kwargs):
    """
    Sets *position* to end if not set.

    """
    if move_to_end and not self.position:
      self.move_to_end(save=False)
    super(OrderedListMixin, self).save(*args, **kwargs)

  def get_ordered_objects(self, manager):
    """
    Returns a :cls:`django.db.models.QuerySet`.

    Uses all relevant objects (e.g. all objects in the same **ordered group**
    as *self*) in a given order.

    The supplied *manager* should be the default manager for the class.

    """
    raise NotImplementedError

  def get_objects(self):
    """
    Returns a :cls:`django.db.models.QuerySet` that contains all
    :cls:`OrderedObject` objects that belong to the same **ordered group**
    as *self*.

    If no **ordered group** is returned by :meth:`get_ordered_objects`, then
    all :cls:`OrderedObject` are used (as supplied by the default manager
    ``objects``).

    """
    manager = self.__class__.objects
    try:
      return self.get_ordered_objects(manager).all()
    except NotImplementedError:
      return manager.all()

  def move_to_end(self, save=True):
    """
    Sets the *position* to the end of the list.

    .. note::

      *position* is set to `0` if there are no other instances.

    """
    objects = self.get_objects()
    last = objects.last()
    if last is None:
      self.position = 0
    elif last.pk == self.pk:
      return
    else:
      self.position = last.position + 1
    if save:
      self.save(move_to_end=False)

  def move_to_start(self, save=True):
    """
    Sets the *position* to the start of the list.

    .. note::

      *position* is set to `0` if there are no other instances.

    .. important::

      **Sideffect**: Even if you don't call :meth:`save`, the *position* of
      all other objects might be increased by one.

    """
    objects = self.get_objects()
    first = objects.first()
    if first is None:
      self.position = 0
    elif first.pk == self.pk:
      return
    else:
      self.position = first.position - 1
      if self.position < 0:
        for obj in objects.order_by('-position'):
          obj.position += 1
          obj.save()
        self.position = 0
    if save:
      self.save(move_to_end=False)

  def get_number(self):
    """
    Returns `position + 1` (so that `0` becomes `1`).

    """
    return self.position + 1

  number = property(get_number)


class ImageMixin(object):

  """
  Some common properties for models that include an image field.

  .. important::

    Models need to have a **image** attribute.

  """

  def get_name(self):
    return self.image.name
  get_name.short_description = 'Datei Name'
  get_name.admin_order_field = 'image__name'

  def get_url(self):
    return self.image.url
  get_url.short_description = 'URL'
  get_url.admin_order_field = 'image__url'

  def get_link(self):
    return '<a href="{}">{}</a>'.format(self.url, self.name)
  get_link.short_description = 'URL'
  get_link.admin_order_field = 'image__url'
  get_link.allow_tags = True

  def get_size(self):
    return filesizeformat(self.image.size)
  get_size.short_description = 'Größe'
  get_size.admin_order_field = 'image__size'

  name = property(get_name)
  url = property(get_url)
  link = property(get_link)
  size = property(get_size)


class ShortDescriptionMixin(object):

  """
  Adds a `short_description` property.

  .. important::

    Models need to have a **description** attribute.

  """

  def get_short_description(self, max_length=60):
    """
    Returns the (shorted) *description*.

    """
    if len(self.description) > max_length:
      return self.description[:max_length] + '…'
    else:
      return self.description
  get_short_description.short_description = 'Kurzbeschreibung'
  get_short_description.admin_order_field = 'description'

  short_description = property(get_short_description)


class OrderedObject(models.Model):

  """
  Abstract model that adds a *position* field.

  """

  class Meta:
    abstract = True
    ordering = ['position']
    get_latest_by = 'position'

  position = models.PositiveSmallIntegerField(
    default=0,
    help_text="Position in der Reihenfolge."
  )


class TitleSlug(models.Model):

  """
  Abstract model that adds a *title* and a *slug* field.

  The *slug* is automatically updated on :meth:`save` calls.

  """

  class Meta:
    abstract = True

  title = models.CharField(
    'Titel', max_length=150,
    validators=[MinLengthValidator(4), MaxLengthValidator(150)],
    help_text="Ein beliebiger Title für den Eintrag."
  )

  slug = models.SlugField(max_length=150)

  def save(self, *args, **kwargs):
    self.slug = slugify(self.title)
    super(TitleSlug, self).save(*args, **kwargs)

  def get_short_title(self, max_length=60):
    """
    Returns the (shorted) *title*.

    """
    if len(self.title) > max_length:
      return self.title[:max_length] + '…'
    else:
      return self.title
  get_short_title.short_description = 'Title'
  get_short_title.admin_order_field = 'title'

  short_title = property(get_short_title)


class MarkdownContent(models.Model):

  """
  Abstract model that adds a *content* and a *html* field.

  The *content* field supports Markdown: the *html* field is automatically
  updated on :meth:`save` calls to contain the HTML version of *content*.

  """

  class Meta:
    abstract = True

  content = models.TextField(
    'Inhalt', max_length=35000,
    validators=[MinLengthValidator(6), MaxLengthValidator(35000)],
    help_text=MARKDOWN_CONTENT_HELP
  )

  html = models.TextField(max_length=40000)

  def save(self, *args, **kwargs):
    self.html = parse_markdown(self.content)
    super(MarkdownContent, self).save(*args, **kwargs)

  def get_short_content(self, max_length=80):
    """
    Returns the (shorted) *content*.

    """
    if len(self.content) > max_length:
      return self.content[:max_length] + '…'
    else:
      return self.content
  get_short_content.short_description = 'Inhalt'
  get_short_content.admin_order_field = 'content'

  def get_html_safe(self):
    """
    Returns the :attr:`html` attribute as safe string.

    """
    return mark_safe(self.html)

  def get_preview(self):
    """
    Returns HTML "marked save for preview" for the Markdown in *content*.

    The HTML is contained in a DIV with the id `preview`.

    """
    preview = (
      '<div id="md-preview">'
      '<header>'
      '<h1>{title}</h1>'
      '</header>'
      '{content}'
      '</div>'
    )
    return format_html(
      preview, title=self.title, content=self.get_html_safe()
    )
  get_preview.short_description = 'Preview'
  get_preview.admin_order_field = 'content'

  short_content = property(get_short_content)
  preview = property(get_preview)

Test file

# -*- coding: UTF-8 -*-
# /project_root/project_base/core/test.py

"""
Test models for *core*.

"""

from __future__ import absolute_import
from __future__ import unicode_literals

from django.db import models

from .managers import (
  FeatureManager
)
from .models import (
  TrimStringsMixin,
  PlaytimeMixin,
  OrderedListMixin,
  OrderedObject,
  TitleSlug,
  MarkdownContent,
)


class TrimStringsMixinTestModel(TrimStringsMixin, models.Model):

  """
  Test–Model for the `TrimStringsMixin` mixin.

  """

  title = models.CharField(max_length=50)
  description = models.TextField()


class PlaytimeMixinTestModel(PlaytimeMixin, models.Model):

  """
  Test–Model for the `PlaytimeMixin` mixin.

  """

  title = models.CharField(max_length=50)
  seconds = models.DecimalField()


class OrderedObjectTestModel(OrderedObject):

  """
  Test–Model for the `OrderedObject` abstract class.

  """

  title = models.CharField(max_length=50)


class OrderedGroupTestModel(OrderedListMixin, OrderedObject):

  """
  Test–Model for the `OrderedObject` abstract class — with `OrderedListMixin`.

  """

  class Meta(OrderedObject.Meta):
    unique_together = ('position', 'gidx')

  title = models.CharField(max_length=50)
  gidx = models.IntegerField()

  def get_ordered_objects(self, manager):
    """
    Returns a :cls:`django.db.models.QuerySet` for the objects in the same
    **ordered group** as *self*.

    The supplied *manager* should be the default manager for the class.

    """
    return manager.filter(gidx=self.gidx)


class FeatureTestModel(models.Model):

  """
  Test–Model for the `FeatureManager` manager class.

  """

  title = models.CharField(max_length=50)

  featured = models.BooleanField(
    default=False,
    help_text="Soll dieses Objekt angezeigt werden?"
  )

  objects = FeatureManager()


class StripTestModel(models.Model):

  """
  Test–Model for the `FormTrimStringsMixin` form class.

  """

  title = models.CharField(max_length=12)
  extra = models.CharField(max_length=12, blank=True)


class TitleSlugTestModel(TitleSlug):

  """
  Test–Model for the `TitleSlug` abstract class.

  """

  pass


class MarkdownContentTestModel(MarkdownContent):

  """
  Test–Model for the `MarkdownContent` abstract class.

  """

  pass

Aucun commentaire:

Enregistrer un commentaire