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()