Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,6 @@ node_modules
test.zip
tests/test.zip
venv

.idea
db.sqlite3
4 changes: 4 additions & 0 deletions feincms/static/feincms/item_editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,10 @@
that the submit handlers from FilteredSelectMultiple get
invoked. See Issue #372 */
form_element.find("[type=submit][name=_save]").click()
/* Disable all submit buttons to prevent double submission while page reloads */
form_element
.find("input[type=submit], button")
.attr("disabled", "disabled")
} else {
// Restore original value
form_element.val($(this).data("original_value"))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Generated by Django 6.0.2 on 2026-02-10 09:17

import django.db.models.deletion
from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("page", "0001_initial"),
]

operations = [
migrations.AlterField(
model_name="applicationcontent",
name="parent",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="%(class)s_set",
to="page.page",
),
),
migrations.AlterField(
model_name="mediafilecontent",
name="parent",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="%(class)s_set",
to="page.page",
),
),
migrations.AlterField(
model_name="page",
name="symlinked_page",
field=models.ForeignKey(
blank=True,
help_text="All content is inherited from this page if given.",
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="%(app_label)s_%(class)s_symlinks",
to="page.page",
verbose_name="symlinked page",
),
),
migrations.AlterField(
model_name="page",
name="template_key",
field=models.CharField(
choices=[("", "")],
default="base",
max_length=255,
verbose_name="template",
),
),
migrations.AlterField(
model_name="rawcontent",
name="parent",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="%(class)s_set",
to="page.page",
),
),
migrations.AlterField(
model_name="templatecontent",
name="parent",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="%(class)s_set",
to="page.page",
),
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Generated by Django 6.0.2 on 2026-02-10 09:17

import django.db.models.deletion
from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("testapp", "0001_initial"),
]

operations = [
migrations.AlterField(
model_name="category",
name="level",
field=models.PositiveIntegerField(editable=False),
),
migrations.AlterField(
model_name="category",
name="lft",
field=models.PositiveIntegerField(editable=False),
),
migrations.AlterField(
model_name="category",
name="rght",
field=models.PositiveIntegerField(editable=False),
),
migrations.AlterField(
model_name="customcontenttype",
name="parent",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="%(class)s_set",
to="testapp.mymodel",
),
),
]
85 changes: 85 additions & 0 deletions tests/testapp/tests/test_template_change_fix.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
"""
Tests for the template change double submission fix.

This test verifies that the JavaScript fix is in place and hasn't been accidentally removed.
"""

import os
from django.conf import settings
from django.test import TestCase


class TemplateChangeFixTest(TestCase):
"""Test that the double submission fix is present in the JavaScript file."""

def test_fix_is_present_in_javascript(self):
"""Verify that the fix line exists in item_editor.js"""
# Use BASEDIR from settings to construct the path
js_file_path = os.path.join(
settings.BASEDIR,
"..",
"..",
"feincms",
"static",
"feincms",
"item_editor.js",
)
js_file_path = os.path.normpath(js_file_path)

# Read the file
with open(js_file_path, "r") as f:
content = f.read()

# Check that the fix is present (code is formatted across multiple lines)
self.assertIn(
'.find("input[type=submit], button")',
content,
"The button selector for double submission fix should be present in item_editor.js",
)
self.assertIn(
'.attr("disabled", "disabled")',
content,
"The disabled attribute setter should be present in item_editor.js",
)

# Verify it's in the correct context (after the click() call)
self.assertIn(
'form_element.find("[type=submit][name=_save]").click()',
content,
"The save button click trigger should be present",
)

# Verify the comment is present
self.assertIn(
"Disable all submit buttons to prevent double submission",
content,
"The explanatory comment should be present",
)

def test_fix_order_is_correct(self):
"""Verify that the disable happens AFTER the click, not before."""
base_dir = os.path.dirname(
os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
)
js_file_path = os.path.join(
base_dir, "feincms", "static", "feincms", "item_editor.js"
)
js_file_path = os.path.normpath(js_file_path)

with open(js_file_path, "r") as f:
content = f.read()

# Find positions of both statements
click_pos = content.find(
'form_element.find("[type=submit][name=_save]").click()'
)
# The disable code is formatted across multiple lines, so search for the key part
disable_pos = content.find('.find("input[type=submit], button")')

self.assertGreater(click_pos, 0, "The click() statement should be present")
self.assertGreater(disable_pos, 0, "The disable statement should be present")
self.assertLess(
click_pos,
disable_pos,
"The disable statement should come AFTER the click() statement",
)
Loading