Why is Django Machina query altered with nonexistent table name

I have a django-machina forum where I can't delete posts because of an error where the delete process apparently looks for a table "forum_conversation_forumpost" This is not a table name in the package. There is a table "forum_conversation_post."

I have another test forum installed and I am not getting this error when I delete a post. While I have overridden some sections of the standard machina install in the site with a problem, I still get the error when I revert back to the standard install.

I can't figure out where the query is being overridden or why.

Error:

ProgrammingError at /forum/moderation/queue/31/disapprove/

(1146, "Table 'site.forum_conversation_forumpost' doesn't exist")

Request Method:     POST
Request URL:    http://localhost:8000/forum/moderation/queue/31/disapprove/
Django Version:     3.2.23
Exception Type:     ProgrammingError
Exception Value:    

(1146, "Table 'site.forum_conversation_forumpost' doesn't exist")

Exception Location:     /Users/user1/.local/share/virtualenvs/labororg-RtI6P7d4/lib/python3.9/site-packages/MySQLdb/connections.py, line 265, in query
Python Executable:  /Users/user1/.local/share/virtualenvs/labororg-RtI6P7d4/bin/python
Python Version:     3.9.6
Python Path:    

['/Users/user1/Documents/labororg',
 '/Users/user1/.pyenv/versions/3.9.6/lib/python39.zip',
 '/Users/user1/.pyenv/versions/3.9.6/lib/python3.9',
 '/Users/user1/.pyenv/versions/3.9.6/lib/python3.9/lib-dynload',
 '/Users/user1/.local/share/virtualenvs/labororg-RtI6P7d4/lib/python3.9/site-packages']

Server time:    Sat, 04 Jan 2025 18:45:46 +0000

Traceback:

Environment:


Request Method: POST
Request URL: http://localhost:8000/forum/moderation/queue/31/disapprove/

Django Version: 3.2.23
Python Version: 3.9.6
Installed Applications:
['django.contrib.admin',
 'django.contrib.auth',
 'django.contrib.contenttypes',
 'django.contrib.sessions',
 'django.contrib.messages',
 'whitenoise.runserver_nostatic',
 'django.contrib.staticfiles',
 'haystack',
 'mptt',
 'widget_tweaks',
 'ckeditor',
 'ckeditor_uploader',
 'compressor',
 'crispy_forms',
 'crispy_bootstrap4',
 'allauth',
 'allauth.account',
 'allauth.socialaccount',
 'captcha',
 'machina',
 'machina.apps.forum_conversation.forum_attachments',
 'machina.apps.forum_conversation.forum_polls',
 'machina.apps.forum_feeds',
 'machina.apps.forum_moderation',
 'machina.apps.forum_search',
 'machina.apps.forum_tracking',
 'machina.apps.forum_member',
 'machina.apps.forum_permission',
 'apps.forum',
 'apps.forum_conversation',
 'accounts',
 'pages',
 'company_reviews',
 'imagecap',
 'flaggedword']
Installed Middleware:
['django.middleware.security.SecurityMiddleware',
 'django.contrib.sessions.middleware.SessionMiddleware',
 'whitenoise.middleware.WhiteNoiseMiddleware',
 'django.middleware.common.CommonMiddleware',
 'django.middleware.csrf.CsrfViewMiddleware',
 'django.contrib.auth.middleware.AuthenticationMiddleware',
 'django.contrib.messages.middleware.MessageMiddleware',
 'django.middleware.clickjacking.XFrameOptionsMiddleware',
 'machina.apps.forum_permission.middleware.ForumPermissionMiddleware',
 'allauth.account.middleware.AccountMiddleware']



Traceback (most recent call last):
  File "/Users/user1/.local/share/virtualenvs/labororg-RtI6P7d4/lib/python3.9/site-packages/django/db/backends/utils.py", line 84, in _execute
    return self.cursor.execute(sql, params)
  File "/Users/user1/.local/share/virtualenvs/labororg-RtI6P7d4/lib/python3.9/site-packages/django/db/backends/mysql/base.py", line 73, in execute
    return self.cursor.execute(query, args)
  File "/Users/user1/.local/share/virtualenvs/labororg-RtI6P7d4/lib/python3.9/site-packages/MySQLdb/cursors.py", line 179, in execute
    res = self._query(mogrified_query)
  File "/Users/user1/.local/share/virtualenvs/labororg-RtI6P7d4/lib/python3.9/site-packages/MySQLdb/cursors.py", line 330, in _query
    db.query(q)
  File "/Users/user1/.local/share/virtualenvs/labororg-RtI6P7d4/lib/python3.9/site-packages/MySQLdb/connections.py", line 265, in query
    _mysql.connection.query(self, query)

The above exception ((1146, "Table 'undergroundm.forum_conversation_forumpost' doesn't exist")) was the direct cause of the following exception:
  File "/Users/user1/.local/share/virtualenvs/labororg-RtI6P7d4/lib/python3.9/site-packages/django/core/handlers/exception.py", line 47, in inner
    response = get_response(request)
  File "/Users/user1/.local/share/virtualenvs/labororg-RtI6P7d4/lib/python3.9/site-packages/django/core/handlers/base.py", line 181, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "/Users/user1/.local/share/virtualenvs/labororg-RtI6P7d4/lib/python3.9/site-packages/django/views/generic/base.py", line 70, in view
    return self.dispatch(request, *args, **kwargs)
  File "/Users/user1/.local/share/virtualenvs/labororg-RtI6P7d4/lib/python3.9/site-packages/machina/apps/forum_permission/viewmixins.py", line 114, in dispatch
    return super().dispatch(request, *args, **kwargs)
  File "/Users/user1/.local/share/virtualenvs/labororg-RtI6P7d4/lib/python3.9/site-packages/django/views/generic/base.py", line 98, in dispatch
    return handler(request, *args, **kwargs)
  File "/Users/user1/.local/share/virtualenvs/labororg-RtI6P7d4/lib/python3.9/site-packages/machina/apps/forum_moderation/views.py", line 454, in post
    return self.disapprove(request, *args, **kwargs)
  File "/Users/user1/.local/share/virtualenvs/labororg-RtI6P7d4/lib/python3.9/site-packages/machina/apps/forum_moderation/views.py", line 448, in disapprove
    self.object.delete()
  File "/Users/user1/.local/share/virtualenvs/labororg-RtI6P7d4/lib/python3.9/site-packages/machina/apps/forum_conversation/abstract_models.py", line 346, in delete
    super(AbstractPost, self).delete(using)
  File "/Users/user1/.local/share/virtualenvs/labororg-RtI6P7d4/lib/python3.9/site-packages/django/db/models/base.py", line 966, in delete
    collector.collect([self], keep_parents=keep_parents)
  File "/Users/user1/.local/share/virtualenvs/labororg-RtI6P7d4/lib/python3.9/site-packages/django/db/models/deletion.py", line 295, in collect
    if sub_objs:
  File "/Users/user1/.local/share/virtualenvs/labororg-RtI6P7d4/lib/python3.9/site-packages/django/db/models/query.py", line 284, in __bool__
    self._fetch_all()
  File "/Users/user1/.local/share/virtualenvs/labororg-RtI6P7d4/lib/python3.9/site-packages/django/db/models/query.py", line 1324, in _fetch_all
    self._result_cache = list(self._iterable_class(self))
  File "/Users/user1/.local/share/virtualenvs/labororg-RtI6P7d4/lib/python3.9/site-packages/django/db/models/query.py", line 51, in __iter__
    results = compiler.execute_sql(chunked_fetch=self.chunked_fetch, chunk_size=self.chunk_size)
  File "/Users/user1/.local/share/virtualenvs/labororg-RtI6P7d4/lib/python3.9/site-packages/django/db/models/sql/compiler.py", line 1175, in execute_sql
    cursor.execute(sql, params)
  File "/Users/user1/.local/share/virtualenvs/labororg-RtI6P7d4/lib/python3.9/site-packages/django/db/backends/utils.py", line 98, in execute
    return super().execute(sql, params)
  File "/Users/user1/.local/share/virtualenvs/labororg-RtI6P7d4/lib/python3.9/site-packages/django/db/backends/utils.py", line 66, in execute
    return self._execute_with_wrappers(sql, params, many=False, executor=self._execute)
  File "/Users/user1/.local/share/virtualenvs/labororg-RtI6P7d4/lib/python3.9/site-packages/django/db/backends/utils.py", line 75, in _execute_with_wrappers
    return executor(sql, params, many, context)
  File "/Users/user1/.local/share/virtualenvs/labororg-RtI6P7d4/lib/python3.9/site-packages/django/db/backends/utils.py", line 84, in _execute
    return self.cursor.execute(sql, params)
  File "/Users/user1/.local/share/virtualenvs/labororg-RtI6P7d4/lib/python3.9/site-packages/django/db/utils.py", line 90, in __exit__
    raise dj_exc_value.with_traceback(traceback) from exc_value
  File "/Users/user1/.local/share/virtualenvs/labororg-RtI6P7d4/lib/python3.9/site-packages/django/db/backends/utils.py", line 84, in _execute
    return self.cursor.execute(sql, params)
  File "/Users/user1/.local/share/virtualenvs/labororg-RtI6P7d4/lib/python3.9/site-packages/django/db/backends/mysql/base.py", line 73, in execute
    return self.cursor.execute(query, args)
  File "/Users/user1/.local/share/virtualenvs/labororg-RtI6P7d4/lib/python3.9/site-packages/MySQLdb/cursors.py", line 179, in execute
    res = self._query(mogrified_query)
  File "/Users/user1/.local/share/virtualenvs/labororg-RtI6P7d4/lib/python3.9/site-packages/MySQLdb/cursors.py", line 330, in _query
    db.query(q)
  File "/Users/user1/.local/share/virtualenvs/labororg-RtI6P7d4/lib/python3.9/site-packages/MySQLdb/connections.py", line 265, in query
    _mysql.connection.query(self, query)

Exception Type: ProgrammingError at /forum/moderation/queue/31/disapprove/
Exception Value: (1146, "Table 'undergroundm.forum_conversation_forumpost' doesn't exist")

Relevant (abstract) models

"""
    Forum conversation abstract models
    ==================================

    This module defines abstract models provided by the ``forum_conversation`` application.

"""

from django.conf import settings
from django.core.exceptions import ValidationError
from django.db import models
from django.db.models import Q
from django.utils.encoding import force_str
from django.utils.text import slugify
from django.utils.translation import gettext_lazy as _

from machina.conf import settings as machina_settings
from machina.core import validators
from machina.core.loading import get_class
from machina.models.abstract_models import DatedModel
from machina.models.fields import MarkupTextField


ApprovedManager = get_class('forum_conversation.managers', 'ApprovedManager')


class AbstractTopic(models.Model):
    """ Represents a forum topic. """

    forum = models.ForeignKey(
        'forum.Forum', related_name='topics', on_delete=models.CASCADE,
        verbose_name=_('Topic forum'),
    )
    poster = models.ForeignKey(
        settings.AUTH_USER_MODEL, blank=True, null=True, on_delete=models.CASCADE,
        verbose_name=_('Poster'),
    )

    # The subject of the thread should correspond to the one associated with the first post.
    subject = models.CharField(max_length=255, verbose_name=_('Subject'))
    slug = models.SlugField(max_length=255, verbose_name=_('Slug'))

    # Sticky, Announce, Global topic or Default topic ; that's what a topic can be.
    TOPIC_POST, TOPIC_STICKY, TOPIC_ANNOUNCE = 0, 1, 2
    TYPE_CHOICES = (
        (TOPIC_POST, _('Default topic')),
        (TOPIC_STICKY, _('Sticky')),
        (TOPIC_ANNOUNCE, _('Announce')),
    )
    type = models.PositiveSmallIntegerField(
        choices=TYPE_CHOICES, db_index=True, verbose_name=_('Topic type'),
    )

    # A topic can be locked, unlocked or moved.
    TOPIC_UNLOCKED, TOPIC_LOCKED, TOPIC_MOVED = 0, 1, 2
    STATUS_CHOICES = (
        (TOPIC_UNLOCKED, _('Topic unlocked')),
        (TOPIC_LOCKED, _('Topic locked')),
        (TOPIC_MOVED, _('Topic moved')),
    )
    status = models.PositiveIntegerField(
        choices=STATUS_CHOICES, db_index=True, verbose_name=_('Topic status'),
    )

    # A topic can be approved before publishing ; defaults to True. The value of this flag
    # should correspond to the one associated with the first post.
    approved = models.BooleanField(default=True, db_index=True, verbose_name=_('Approved'))

    # The number of posts included in this topic (only those that are approved).
    posts_count = models.PositiveIntegerField(
        editable=False, blank=True, default=0, verbose_name=_('Posts count'),
    )

    # The number of time the topic has been viewed.
    views_count = models.PositiveIntegerField(
        editable=False, blank=True, default=0, verbose_name=_('Views count'),
    )

    # The date of the latest post.
    last_post_on = models.DateTimeField(
        blank=True,
        null=True,
        db_index=True,
        verbose_name=_('Last post added on')
    )

    # The first post and the last post of the topic. The first post can be unnaproved. The last post
    # must be approved.
    first_post = models.ForeignKey(
        'forum_conversation.Post', editable=False, related_name='+', blank=True, null=True,
        on_delete=models.SET_NULL, verbose_name=_('First post'),
    )
    last_post = models.ForeignKey(
        'forum_conversation.Post', editable=False, related_name='+', blank=True, null=True,
        on_delete=models.SET_NULL, verbose_name=_('Last post'),
    )

    # Many users can subscribe to this topic
    subscribers = models.ManyToManyField(
        settings.AUTH_USER_MODEL, related_name='topic_subscriptions', blank=True,
        verbose_name=_('Subscribers'),
    )

    created = models.DateTimeField(
        auto_now_add=True,
        db_index=True,
        verbose_name=_('Creation date')
    )
    updated = models.DateTimeField(
        auto_now=True,
        db_index=True,
        verbose_name=_('Update date')
    )

    objects = models.Manager()
    approved_objects = ApprovedManager()

    class Meta:
        abstract = True
        app_label = 'forum_conversation'
        indexes = [models.Index(fields=['type', 'last_post_on']), ]
        ordering = ['-type', '-last_post_on', ]
        get_latest_by = 'last_post_on'
        verbose_name = _('Topic')
        verbose_name_plural = _('Topics')

    def __str__(self):
        return self.first_post.subject if self.first_post is not None else self.subject

    @property
    def is_topic(self):
        """ Returns ``True`` if the topic is a default topic. """
        return self.type == self.TOPIC_POST

    @property
    def is_sticky(self):
        """ Returns ``True`` if the topic is a sticky topic. """
        return self.type == self.TOPIC_STICKY

    @property
    def is_announce(self):
        """ Returns ``True`` if the topic is an announce. """
        return self.type == self.TOPIC_ANNOUNCE

    @property
    def is_locked(self):
        """ Returns ``True`` if the topic is locked. """
        return self.status == self.TOPIC_LOCKED

    def has_subscriber(self, user):
        """ Returns ``True`` if the given user is a subscriber of this topic. """
        if not hasattr(self, '_subscribers'):
            self._subscribers = list(self.subscribers.all())
        return user in self._subscribers

    def clean(self):
        """ Validates the topic instance. """
        super().clean()
        if self.forum.is_category or self.forum.is_link:
            raise ValidationError(
                _('A topic can not be associated with a category or a link forum')
            )

    def save(self, *args, **kwargs):
        """ Saves the topic instance. """
        # It is vital to track the changes of the forum associated with a topic in order to
        # maintain counters up-to-date.
        old_instance = None
        if self.pk:
            old_instance = self.__class__._default_manager.get(pk=self.pk)

        # Update the slug field
        self.slug = slugify(force_str(self.subject), allow_unicode=True) or 'topic'

        # Do the save
        super().save(*args, **kwargs)

        # If any change has been made to the parent forum, trigger the update of the counters
        if old_instance and old_instance.forum != self.forum:
            self.update_trackers()
            # The previous parent forum counters should also be updated
            if old_instance.forum:
                old_forum = old_instance.forum
                old_forum.refresh_from_db()
                old_forum.update_trackers()

    def _simple_save(self, *args, **kwargs):
        """ Simple wrapper around the standard save method.

        Calls the parent save method in order to avoid the checks for topic forum changes which can
        result in triggering a new update of the counters associated with the current topic.

        This allow the database to not be hit by such checks during very common and regular
        operations such as those provided by the update_trackers function; indeed these operations
        will never result in an update of a topic's forum.

        """
        super().save(*args, **kwargs)

    def delete(self, using=None):
        """ Deletes the forum instance. """
        super().delete(using)
        self.forum.update_trackers()

    def update_trackers(self):
        """ Updates the denormalized trackers associated with the topic instance. """
        self.posts_count = self.posts.filter(approved=True).count()
        first_post = self.posts.all().order_by('created').first()
        last_post = self.posts.filter(approved=True).order_by('-created').first()
        self.first_post = first_post
        self.last_post = last_post
        self.last_post_on = last_post.created if last_post else None
        self._simple_save()
        # Trigger the forum-level trackers update
        self.forum.update_trackers()


class AbstractPost(DatedModel):
    """ Represents a forum post. A forum post is always linked to a topic. """

    topic = models.ForeignKey(
        'forum_conversation.Topic', related_name='posts', on_delete=models.CASCADE,
        verbose_name=_('Topic'),
    )
    poster = models.ForeignKey(
        settings.AUTH_USER_MODEL, related_name='posts',
        blank=True, null=True, on_delete=models.CASCADE, verbose_name=_('Poster'),
    )
    anonymous_key = models.CharField(
        max_length=100, blank=True, null=True, verbose_name=_('Anonymous user forum key'),
    )

    # Each post can have its own subject. The subject of the thread corresponds to the
    # one associated with the first post
    subject = models.CharField(verbose_name=_('Subject'), max_length=255)

    # Content
    content = MarkupTextField(
        validators=[
            validators.MarkupMaxLengthValidator(machina_settings.POST_CONTENT_MAX_LENGTH),
        ],
        verbose_name=_('Content'),
    )

    # Username: if the user creating a topic post is not authenticated, he must enter a username
    username = models.CharField(max_length=155, blank=True, null=True, verbose_name=_('Username'))

    # A post can be approved before publishing ; defaults to True
    approved = models.BooleanField(default=True, db_index=True, verbose_name=_('Approved'))

    # The user can choose if they want to display their signature with the content of the post
    enable_signature = models.BooleanField(
        default=True, db_index=True, verbose_name=_('Attach a signature'),
    )

    # A post can be edited for several reason (eg. moderation) ; the reason why it has been
    # updated can be specified
    update_reason = models.CharField(
        max_length=255, blank=True, null=True, verbose_name=_('Update reason'),
    )

    # Tracking data
    updated_by = models.ForeignKey(
        settings.AUTH_USER_MODEL, editable=False, blank=True, null=True, on_delete=models.SET_NULL,
        verbose_name=_('Lastly updated by'),
    )
    updates_count = models.PositiveIntegerField(
        editable=False, blank=True, default=0, verbose_name=_('Updates count'),
    )

    objects = models.Manager()
    approved_objects = ApprovedManager()

    class Meta:
        abstract = True
        app_label = 'forum_conversation'
        ordering = ['created', ]
        get_latest_by = 'created'
        verbose_name = _('Post')
        verbose_name_plural = _('Posts')

    def __str__(self):
        return self.subject

    @property
    def is_topic_head(self):
        """ Returns ``True`` if the post is the first post of the topic. """
        return self.topic.first_post.id == self.id if self.topic.first_post else False

    @property
    def is_topic_tail(self):
        """ Returns ``True`` if the post is the last post of the topic. """
        return self.topic.last_post.id == self.id if self.topic.last_post else False

    @property
    def is_alone(self):
        """ Returns ``True`` if the post is the only single post of the topic. """
        return self.topic.posts.count() == 1

    @property
    def position(self):
        """ Returns an integer corresponding to the position of the post in the topic. """
        position = self.topic.posts.filter(Q(created__lt=self.created) | Q(id=self.id)).count()
        return position

    def clean(self):
        """ Validates the post instance. """
        super().clean()

        # At least a poster (user) or a session key must be associated with
        # the post.
        if self.poster is None and self.anonymous_key is None:
            raise ValidationError(
                _('A user id or an anonymous key must be associated with a post.'),
            )
        if self.poster and self.anonymous_key:
            raise ValidationError(
                _('A user id or an anonymous key must be associated with a post, but not both.'),
            )

        if self.anonymous_key and not self.username:
            raise ValidationError(_('A username must be specified if the poster is anonymous'))

    def save(self, *args, **kwargs):
        """ Saves the post instance. """
        new_post = self.pk is None
        super().save(*args, **kwargs)

        # Ensures that the subject of the thread corresponds to the one associated
        # with the first post. Do the same with the 'approved' flag.
        if (new_post and self.topic.first_post is None) or self.is_topic_head:
            if self.subject != self.topic.subject or self.approved != self.topic.approved:
                self.topic.subject = self.subject
                self.topic.approved = self.approved

        # Trigger the topic-level trackers update
        self.topic.update_trackers()

    def delete(self, using=None):
        """ Deletes the post instance. """
        if self.is_alone:
            # The default way of operating is to trigger the deletion of the associated topic
            # only if the considered post is the only post embedded in the topic
            self.topic.delete()
        else:
            super(AbstractPost, self).delete(using)
            self.topic.update_trackers()
Вернуться на верх