For anyone coming across this in the future - Chris' solution is great, and can be implemented like so:
class Player(BasePlayer):
submit_missing = models.IntegerField(initial=0)
class (Page):
@staticmethod
def error_message(player: Player, values):
errors = {f: 'Please fill in this field' for f in values if not values[f]}
if errors:
player.submit_missing += 1
if player.submit_missing < 2:
return errors
@staticmethod # if using method for multiple pages
def before_next_page(player, timeout_happened):
player.submit_missing = 0
One of my particular cases had some optional and some required fields on the same page, with some radio and some integer input items. My javascript solution is below:
@staticmethod
def js_vars(player): # highlights variables/fields that do or do not need to be filled
return dict(optional_fields = Demographics.form_fields[2:4],
required_fields = Demographics.form_fields[0:2])
let required_fields = js_vars.required_fields;
let optional_fields = js_vars.optional_fields;
let times_submitted = 0;
function Validate() {
var items = []
let reqitems = []
let isValid = []
let reqValid = []
let messages = []
let req_messages = []
let checker = arr => arr.every(v => v === true) // this function checks whether all elements in an array are true
//each time the next button is pressed, times submitted increments
times_submitted++
// required variable error flagging
for (i = 0; i < required_fields.length; i++) {
reqValid[i] = false
req_messages[i] = "Please fill out this field"
reqitems.push(Array.from(document.getElementsByName(required_fields[i]))) //Reference the Group of optional questions.
// branches for radio (div) vs. text input (input, probably) items
if (document.getElementById("id_" + required_fields[i]).tagName == "DIV"){
for (var j = 0; j < reqitems[i].length; j++) {
if (reqitems[i][j].checked) {
// if at least one radio is checked for the question, return true and change item's message to blank
reqValid[i] = true;
req_messages[i] = "";
break;
} else {
scroll(0, 0)
}
}
} else {
if (document.getElementById("id_" + required_fields[i]).value) {
reqValid[i] = true;
req_messages[i] = "";
} else {
scroll(0,0)
}
}
}
// export messages to html
age_message.innerHTML = req_messages[0]
gender_message.innerHTML = req_messages[1]
//for optional variables
if (times_submitted >= 2) {
if(checker(reqValid) == true) {
form.submit() //optional submission - if next is hit twice or more, submit
}
} else { // if next not hit more than twice...
for (i = 0; i < optional_fields.length; i++) {
isValid[i] = false
messages[i] = "Please fill out this field"
items.push(Array.from(document.getElementsByName(optional_fields[i]))) //Reference the Group of optional questions.
// branches for radio (div) vs. text input (input, probably) items
if (document.getElementById("id_" + optional_fields[i]).tagName == "DIV"){
for (var j = 0; j < items[i].length; j++) {
if (items[i][j].checked) {
// if at least one radio is checked for the question, return true and change item's message to blank
isValid[i] = true;
messages[i] = "";
break;
} else {
scroll(0, 0)
}
}
} else {
if (document.getElementById("id_" + optional_fields[i]).value) {
isValid[i] = true;
messages[i] = "";
} else {
scroll(0,0)
}
}
}
if(checker(reqValid) == true && checker(isValid) == true) {
form.submit() //optional submission - if next is hit twice or more, submit
}
education_message.innerHTML = messages[0]
income_message.innerHTML = messages[1]
}
}