Skip to content

Commit 9c2bf60

Browse files
authored
Fixes issue 451 (#452)
1 parent 12b55af commit 9c2bf60

File tree

13 files changed

+124
-22
lines changed

13 files changed

+124
-22
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Change Log
22

3+
## [2.10.3] - 2025-02-28
4+
5+
* Fixes [issue 451](https://github.com/danirus/django-comments-xtd/issues/451): Exceeding the `COMMENT_MAX_LENGTH` does not reflect in the UI, either when using django templates or when using the JavaScript plugin.
6+
37
## [2.10.2] - 2025-01-07
48

59
* Add reminder about template loading order to the documentation.

django_comments_xtd/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ def get_form():
1717
return import_string(settings.COMMENTS_XTD_FORM_CLASS)
1818

1919

20-
VERSION = (2, 10, 2, 'f', 0) # following PEP 440
20+
VERSION = (2, 10, 3, 'f', 0) # following PEP 440
2121

2222

2323
def get_version():

django_comments_xtd/api/frontend.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ def get_props(cls, obj, user, request=None):
124124
"default_followup": settings.COMMENTS_XTD_DEFAULT_FOLLOWUP,
125125
"html_id_suffix": get_html_id_suffix(obj),
126126
"max_thread_level": max_thread_level_for_content_type(ctype),
127+
"comment_max_length": settings.COMMENT_MAX_LENGTH,
127128
}
128129
try:
129130
user_is_authenticated = user.is_authenticated()

django_comments_xtd/forms.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,3 +66,15 @@ def get_comment_create_data(self, site_id=None):
6666
'followup': self.cleaned_data['followup'],
6767
'content_object': target})
6868
return data
69+
70+
def clean(self):
71+
cleaned_data = super().clean()
72+
for field_name in self.errors.keys():
73+
if 'class' not in self.fields[field_name].widget.attrs.keys():
74+
continue
75+
widget_classes = self.fields[field_name].widget.attrs['class']
76+
widget_classes = widget_classes.split(" ")
77+
widget_classes.append("is-invalid")
78+
widget_classes = " ".join(widget_classes)
79+
self.fields[field_name].widget.attrs['class'] = widget_classes
80+
return cleaned_data

django_comments_xtd/static/django_comments_xtd/js/src/commentform.jsx

Lines changed: 45 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@ import { getCookie } from './lib.js';
66
import { InitContext } from './context';
77

88

9-
export function FieldIsRequired({replyTo}) {
9+
export function FieldIsRequired({replyTo, message}) {
1010
return (
1111
<span
1212
className="form-text small invalid-feedback"
1313
{...((replyTo > 0) && {style: {"fontSize": "0.71rem"}})}
1414
>
15-
{django.gettext("This field is required.")}
15+
{django.gettext(message)}
1616
</span>
1717
);
1818
}
@@ -67,6 +67,7 @@ export function PreviewComment({avatar, name, url, comment, replyTo}) {
6767

6868
export function CommentForm({ replyTo, onCommentCreated }) {
6969
const {
70+
comment_max_length,
7071
default_followup,
7172
default_form,
7273
is_authenticated,
@@ -82,36 +83,59 @@ export function CommentForm({ replyTo, onCommentCreated }) {
8283
avatar: undefined, name: "", email: "", url: "", comment: "",
8384
followup: default_followup,
8485
errors: { name: false, email: false, comment: false },
85-
alert: { message: "", cssc: "" }
86+
alert: { message: "", cssc: "" },
87+
comment_field_error: "",
8688
});
8789

90+
const default_error = "This field is required.";
91+
92+
const get_comment_length_error_msg = () => {
93+
return (
94+
`Ensure this value has at most ${comment_max_length} `
95+
+ `character (it has ${lstate.comment.length}).`
96+
);
97+
}
98+
8899
const handle_input_change = (event) => {
89100
const target = event.target;
90101
const value = target.type === 'checkbox' ? target.checked : target.value;
91102
const iname = target.name;
92103

93-
setLstate({ ...lstate, [iname]: value });
104+
let comment_field_error = "";
105+
if (lstate.comment_field_error.length > 0) {
106+
comment_field_error = get_comment_length_error_msg();
107+
}
108+
setLstate({
109+
...lstate,
110+
[iname]: value,
111+
comment_field_error: comment_field_error
112+
});
94113
}
95114

96115
const is_valid_data = () => {
97-
let is_valid_name = true, is_valid_email = true;
116+
let is_valid_name = true, is_valid_email = true, comment_field_error = "";
98117

99118
if (!is_authenticated || request_name)
100119
is_valid_name = (/^\s*$/.test(lstate.name)) ? false : true;
101120

102121
if (!is_authenticated || request_email_address)
103122
is_valid_email = (/\S+@\S+\.\S+/.test(lstate.email)) ? true : false;
104123

105-
const is_valid_comment = (/^\s*$/.test(lstate.comment)) ? false : true;
124+
let is_valid_comment = (/^\s*$/.test(lstate.comment)) ? false : true;
125+
if (lstate.comment.length >= comment_max_length) {
126+
is_valid_comment = false;
127+
comment_field_error = get_comment_length_error_msg();
128+
}
106129

107130
setLstate({
108131
...lstate,
109132
errors: {
110133
...lstate.errors,
111134
name: !is_valid_name,
112135
email: !is_valid_email,
113-
comment: !is_valid_comment
114-
}
136+
comment: !is_valid_comment,
137+
},
138+
comment_field_error: comment_field_error
115139
});
116140

117141
return is_valid_name && is_valid_email && is_valid_comment;
@@ -253,12 +277,17 @@ export function CommentForm({ replyTo, onCommentCreated }) {
253277
<div className={(replyTo > 0) ? "col-12" : "col-10"}>
254278
<textarea
255279
required name="comment" id="id_comment"
256-
value={lstate.comment} maxLength={3000}
280+
value={lstate.comment}
257281
placeholder={django.gettext("Your comment")}
258282
className={get_input_css_classes("comment")}
259283
onChange={handle_input_change}
260284
/>
261-
{lstate.errors.comment && <FieldIsRequired replyTo={replyTo} />}
285+
{lstate.errors.comment && (
286+
<FieldIsRequired
287+
replyTo={replyTo}
288+
message={lstate.comment_field_error || default_error}
289+
/>
290+
)}
262291
</div>
263292
</div>
264293
);
@@ -285,7 +314,12 @@ export function CommentForm({ replyTo, onCommentCreated }) {
285314
onChange={handle_input_change}
286315
className={get_input_css_classes("name")}
287316
/>
288-
{lstate.errors.name && <FieldIsRequired replyTo={replyTo} />}
317+
{lstate.errors.name && (
318+
<FieldIsRequired
319+
replyTo={replyTo}
320+
message="This field is required."
321+
/>
322+
)}
289323
</div>
290324
</div>
291325
);

django_comments_xtd/static/django_comments_xtd/js/src/context.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ export const init_context_default = {
4242
},
4343
default_followup: false,
4444
html_ud_suffix: "",
45-
max_thread_level: -1
45+
max_thread_level: -1,
46+
comment_max_length: 3000,
4647
}
4748

4849
export const InitContext = React.createContext(init_context_default);

django_comments_xtd/static/django_comments_xtd/js/tests/commentform.test.jsx

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { InitContext, init_context_default } from '../src/context.js';
1616
describe("Test <FieldIsRequired />", () => {
1717
it("Renders a <span> with 'This field is required.' text", () => {
1818
const { container } = render(
19-
<FieldIsRequired replyTo={0} />
19+
<FieldIsRequired replyTo={0} message={"This field is required."} />
2020
);
2121

2222
const span_elem = container.querySelector("span");
@@ -30,7 +30,7 @@ describe("Test <FieldIsRequired />", () => {
3030

3131
it("Renders a <span> with a explicit style attribute", () => {
3232
const { container } = render(
33-
<FieldIsRequired replyTo={1} /> // If replyTo > 0.
33+
<FieldIsRequired replyTo={1} message={"This field is required."} />
3434
);
3535

3636
const span_elem = container.querySelector("span");
@@ -191,6 +191,7 @@ describe("Test <CommentForm />", () => {
191191
};
192192
props = {
193193
...init_context_default,
194+
comment_max_length: 140,
194195
default_form
195196
};
196197
});
@@ -293,6 +294,49 @@ describe("Test <CommentForm />", () => {
293294
expect(field_is_required.textContent).toEqual("This field is required.");
294295
});
295296

297+
it("Submits with comment field too long displays error", async () => {
298+
global.fetch = jest.fn();
299+
const { container } = render(
300+
<InitContext.Provider value={props}>
301+
<CommentForm
302+
replyTo={0}
303+
onCommentCreated={() => commentCreatedHandler()}
304+
/>
305+
</InitContext.Provider>
306+
);
307+
308+
const long_comment = (
309+
"Cras faucibus vitae nisi sit amet semper. Aenean varius, neque sit"
310+
+ " amet porta malesuada, odio est laoreet tellus, non fermentum nunc"
311+
+ " ex et est. Sed consequat sit amet turpis ut congue. Aenean"
312+
+ " convallis quis ex a porta."
313+
);
314+
315+
const comment_error_msg = (
316+
`Ensure this value has at most 140 character`
317+
+ ` (it has ${long_comment.length}).`
318+
);
319+
320+
const comment_field = container.querySelector("[name=comment]");
321+
fireEvent.change(comment_field, {target: {value: long_comment}});
322+
const name_field = container.querySelector("[name=name]");
323+
fireEvent.change(name_field, {target: {value: "Fulanito de Tal"}});
324+
const email_field = container.querySelector("[name=email]");
325+
fireEvent.change(email_field, {target: {value: "[email protected]"}});
326+
327+
const post_button = container.querySelector("[name=post]");
328+
expect(post_button).toBeInTheDocument();
329+
await act(async () => {
330+
fireEvent.click(post_button);
331+
});
332+
333+
expect(global.fetch).not.toHaveBeenCalled();
334+
expect(comment_field).toBeInTheDocument();
335+
const field_is_required = comment_field.nextSibling;
336+
expect(field_is_required.nodeName).toEqual("SPAN");
337+
expect(field_is_required.textContent).toEqual(comment_error_msg);
338+
});
339+
296340
it("Submits with name field empty does not call fetch", async () => {
297341
global.fetch = jest.fn();
298342
const { container } = render(

django_comments_xtd/templates/comments/form.html

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,11 @@
1010
{% if field.is_hidden %}<div>{{ field }}</div>{% endif %}
1111
{% endfor %}
1212
<div style="display:none">{{ form.honeypot }}</div>
13-
<div class="row justify-content-center my-3 form-group{% if 'comment' in form.errors %} has-danger{% endif %}">
14-
<div class="col-10">{{ form.comment }}</div>
13+
<div class="row justify-content-center my-3 form-group">
14+
<div class="col-10">
15+
{{ form.comment }}
16+
{% if 'comment' in form.errors %}<span class="form-text small invalid-feedback">{{ form.errors.comment.0 }}</span>{% endif %}
17+
</div>
1518
</div>
1619

1720
{% if not request.user.is_authenticated or not request.user.get_full_name %}

django_comments_xtd/tests/test_frontend.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from django.contrib.auth.models import AnonymousUser, User
44
from django.test import RequestFactory, TestCase
55

6-
from django_comments_xtd.api.frontend import commentbox_props_response
6+
from django_comments_xtd.api.frontend import commentbox_props_response, settings
77
from django_comments_xtd.models import XtdComment
88
from django_comments_xtd.tests.models import Diary, UUIDDiary
99

@@ -28,6 +28,7 @@ def test_comment_box_props_response(self):
2828
self.assertEqual(d['count_url'], '/comments/api/tests-diary/1/count/')
2929
self.assertEqual(d['list_url'], '/comments/api/tests-diary/1/')
3030
self.assertEqual(d['current_user'], "1:bob")
31+
self.assertEqual(d['comment_max_length'], settings.COMMENT_MAX_LENGTH)
3132

3233
def test_comment_box_props_response_anonymous(self):
3334
request_factory = RequestFactory()

docs/conf.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@
6767
# The short X.Y version.
6868
version = '2.10'
6969
# The full version, including alpha/beta/rc tags.
70-
release = '2.10.2'
70+
release = '2.10.3'
7171

7272
releases = [
7373
"latest",

0 commit comments

Comments
 (0)