I am working on a Flask application that manages therapy sessions for children. Each session can have multiple goals, and I recently updated my models and form handling. However, I’m encountering a 400 Bad Request error when submitting the form to update a session.
Problem:
The error message indicates that “At least one goal and its rating must be provided,” but I believe I am submitting the goals correctly.
Error logs :
2024-07-26T18:05:27.998440+00:00 app[web.1]: DEBUG:root:Full form data: ImmutableMultiDict([('session_id', '65'), ('therapy_type', 'Therapy 1'), ('session_date', '2024-08-03'), ('progress_rating[]', '8'), ('progress_update', 'asd'), ('activities_performed', 'asdad'), ('notes_comments', 'asd')])
2024-07-26T18:05:28.000630+00:00 app[web.1]: ERROR:root:Invalid form data: At least one goal and its rating must be provided.
2024-07-26T18:05:28.008888+00:00 heroku[router]: at=info method=POST path="/update_session/65" host=ams-chat-80db21a7820d.herokuapp.com request_id=cff68d03-618b-4b2c-8b5d-606f66332ce2 fwd="77.127.190.215" dyno=web.1 connect=0ms service=16ms status=400 bytes=236 protocol=https
this is the backed code :
# Models
class SessionDetail(db.Model):
id = db.Column(db.Integer, primary_key=True)
child_id = db.Column(db.Integer, db.ForeignKey('child.id'), nullable=False)
therapy_type = db.Column(db.String(150), nullable=False)
session_date = db.Column(db.String(20), nullable=False)
progress_update = db.Column(db.Text, nullable=True)
activities_performed = db.Column(db.Text, nullable=True)
notes_comments = db.Column(db.Text, nullable=True)
goals = db.relationship('Goal', backref='session', lazy=True)
class Goal(db.Model):
id = db.Column(db.Integer, primary_key=True)
session_id = db.Column(db.Integer, db.ForeignKey('session_detail.id'), nullable=False)
description = db.Column(db.Text, nullable=False)
rating = db.Column(db.Integer, nullable=False)
# Route to update session
@app.route('/update_session/<int:session_id>', methods=['POST'])
def update_session(session_id):
session_detail = SessionDetail.query.get(session_id)
if not session_detail:
logging.error(f"Session with ID {session_id} not found.")
return 'Session not found', 404
try:
logging.debug(f"Full form data: {request.form}")
session_detail.therapy_type = request.form['therapy_type']
session_detail.session_date = request.form['session_date']
session_detail.progress_update = request.form['progress_update']
session_detail.activities_performed = request.form.get('activities_performed')
session_detail.notes_comments = request.form.get('notes_comments')
# Delete existing goals
Goal.query.filter_by(session_id=session_id).delete()
# Add new goals
setting_goals = request.form.getlist('setting_goals[]')
progress_ratings = request.form.getlist('progress_rating[]')
if not setting_goals or not progress_ratings:
raise ValueError("At least one goal and its rating must be provided.")
for goal, rating in zip(setting_goals, progress_ratings):
new_goal = Goal(session_id=session_id, description=goal, rating=int(rating))
db.session.add(new_goal)
db.session.commit()
logging.info(f"Session ID {session_id} successfully updated with {len(setting_goals)} goals.")
except KeyError as e:
logging.error(f"Missing form field: {e}")
return f'Error updating session: Missing form field {e}', 400
except ValueError as e:
logging.error(f"Invalid form data: {e}")
return f'Error updating session: {e}', 400
except Exception as e:
logging.error(f"Error updating session: {e}")
return f'Error updating session: {e}', 500
return redirect(url_for('child_sessions', child_id=session_detail.child_id))
This is the fronted code which contain the sessions Table and JS (which the exception is captured from ×ĒMore precisely in the ‘unction submitForm(form)’ function :
<body>
<div class="profile-container">
<h2>Sessions for {{ child.first_name }} {{ child.surname }}</h2>
<!-- Back Button -->
<div class="back-button-container">
<a href="{{ url_for('children_progress') }}" class="btn-back">Back to Children's Progress</a>
</div>
<div class="table-container">
<table>
<thead>
<tr>
<th>Therapy Type</th>
<th>Session Date</th>
<th>Goals</th>
<th>Progress Update</th>
<th>Activities Performed</th>
<th>Notes/Comments</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for session in sessions %}
<tr>
<form method="post" action="{{ url_for('update_session', session_id=session.id) }}" onsubmit="return submitForm(this);">
<input type="hidden" name="session_id" value="{{ session.id }}">
<td>
<select name="therapy_type">
<option value="Therapy 1" {% if session.therapy_type == 'Therapy 1' %}selected{% endif %}>Therapy 1</option>
<option value="Therapy 2" {% if session.therapy_type == 'Therapy 2' %}selected{% endif %}>Therapy 2</option>
<option value="Therapy 3" {% if session.therapy_type == 'Therapy 3' %}selected{% endif %}>Therapy 3</option>
<option value="Therapy 4" {% if session.therapy_type == 'Therapy 4' %}selected{% endif %}>Therapy 4</option>
</select>
</td>
<td><input type="date" name="session_date" value="{{ session.session_date }}"></td>
<td>
<div class="goal-container">
{% for goal in session.goals %}
<div class="goal-item">
<textarea class="expandable-textarea" name="setting_goals[]">{{ goal.description }}</textarea>
<select name="progress_rating[]">
<option value="" selected disabled>Choose rating</option>
{% for i in range(1, 11) %}
<option value="{{ i }}" {% if goal.rating == i %}selected{% endif %}>{{ i }}</option>
{% endfor %}
</select>
<div class="goal-item-buttons">
<button type="button" onclick="removeGoal(this)">Remove</button>
</div>
</div>
{% endfor %}
<button type="button" class="add-goal-button" onclick="addGoal(this)">Add Goal</button>
</div>
</td>
<td><textarea class="expandable-textarea" name="progress_update">{{ session.progress_update }}</textarea></td>
<td><textarea class="expandable-textarea" name="activities_performed">{{ session.activities_performed }}</textarea></td>
<td><textarea class="expandable-textarea" name="notes_comments">{{ session.notes_comments }}</textarea></td>
<td>
<button type="submit" name="update_session">Update</button>
</td>
</form>
<td>
<form method="post" action="{{ url_for('delete_session', session_id=session.id) }}" style="display:inline;">
<button type="submit" name="delete_session">Delete</button>
</form>
</td>
</tr>
{% endfor %}
<!-- Form for adding a new session -->
<form method="post" action="{{ url_for('add_session', child_id=child.id) }}" onsubmit="submitForm(this); return false;">
<td>
<select name="therapy_type" required>
<option value="Therapy 1">Therapy 1</option>
<option value="Therapy 2">Therapy 2</option>
<option value="Therapy 3">Therapy 3</option>
<option value="Therapy 4">Therapy 4</option>
</select>
</td>
<td><input type="date" name="session_date" required></td>
<td>
<div class="goal-container">
<div class="goal-item">
<textarea class="expandable-textarea" name="setting_goals_new[]" required></textarea>
<select name="progress_rating_new[]" required>
<option value="" selected disabled>Choose rating</option>
{% for i in range(1, 11) %}
<option value="{{ i }}">{{ i }}</option>
{% endfor %}
</select>
<div class="goal-item-buttons">
<button type="button" onclick="removeGoal(this)">Remove</button>
</div>
</div>
<button type="button" class="add-goal-button" onclick="addGoal(this, 'setting_goals_new[]', 'progress_rating_new[]')">Add Goal</button>
</div>
</td>
<td><textarea class="expandable-textarea" name="progress_update" required></textarea></td>
<td><textarea class="expandable-textarea" name="activities_performed"></textarea></td>
<td><textarea class="expandable-textarea" name="notes_comments"></textarea></td>
<td><button type="submit" name="add_session">Add</button></td>
</form>
</tbody>
</table>
</div>
<!-- Chart Container -->
<div class="chart-container">
<canvas id="progressChart"></canvas>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const formInputs = document.querySelectorAll('.login-form input[type="text"], .login-form input[type="password"], .profile-container form input[type="text"], .profile-container form input[type="tel"], .profile-container form select');
formInputs.forEach(input => {
input.addEventListener('focus', function() {
this.style.borderColor = '#007bff';
});
input.addEventListener('blur', function() {
this.style.borderColor = '#ddd';
});
});
const submitButtons = document.querySelectorAll('.login-form input[type="submit"], .profile-container form button[type="submit"]');
submitButtons.forEach(button => {
button.addEventListener('mousedown', function() {
this.style.backgroundColor = '#004080';
});
button.addEventListener('mouseup', function() {
this.style.backgroundColor = '#007bff';
});
});
});
function addGoal(button, goalsName = 'setting_goals[]', ratingsName = 'progress_rating[]') {
const goalContainer = button.closest('.goal-container');
const goalCount = goalContainer.querySelectorAll('.goal-item').length;
const newGoalItem = document.createElement('div');
newGoalItem.className = 'goal-item';
newGoalItem.innerHTML = `
<textarea class="expandable-textarea" name="${goalsName.replace('[]', `[${goalCount}]`)}"></textarea>
<select name="${ratingsName.replace('[]', `[${goalCount}]`)}">
<option value="" selected disabled>Choose rating</option>
${[...Array(10).keys()].map(i => `<option value="${i+1}">${i+1}</option>`).join('')}
</select>
<div class="goal-item-buttons">
<button type="button" onclick="removeGoal(this)">Remove</button>
</div>
`;
goalContainer.insertBefore(newGoalItem, button);
}
function removeGoal(button) {
const goalItem = button.closest('.goal-item');
goalItem.remove();
}
function submitForm(form) {
console.log("Submitting form");
const goalItems = form.querySelectorAll('.goal-item');
const formData = new FormData(form);
// Remove existing goals and ratings
for (let key of formData.keys()) {
if (key.startsWith('setting_goals') || key.startsWith('progress_rating')) {
formData.delete(key);
}
}
goalItems.forEach((goalItem, index) => {
const goalText = goalItem.querySelector('textarea[name^="setting_goals"]').value;
const goalRating = goalItem.querySelector('select[name^="progress_rating"]').value;
if (goalText && goalRating) {
formData.append(`setting_goals[${index}]`, goalText);
formData.append(`progress_rating[${index}]`, goalRating);
}
});
// Log form data for debugging
console.log("Form data before submission:");
for (let pair of formData.entries()) {
console.log(pair[0] + ': ' + pair[1]);
}
// Use fetch to send the data
fetch(form.action, {
method: 'POST',
body: formData
})
.then(response => {
if (!response.ok) {
return response.text().then(text => {
throw new Error(`Network response was not ok: ${text}`);
});
}
return response.text();
})
.then(data => {
console.log("Response:", data);
window.location.reload();
})
.catch(error => {
console.error('Error:', error);
});
return false; // Prevent default form submission
}
</script>
</body>
What I’ve Tried:
Ensured that the form data includes setting_goals[] and progress_rating[].(apperantly it still has a problem )
Added debugging statements to log the form data before submission.
Checked the backend logs to verify the form data received.
Network Console Logs (playlod) output aftersending the post request:
session_id: 65
therapy_type: Therapy 1
session_date: 2024-08-03
progress_rating[]: 8
progress_update: asd
activities_performed: asdad
notes_comments: asd