Dev/Safety Form

/*


function AjaxError(message) { this.name = "AjaxError"; this.message = message; this.stack = (new Error()).stack; } AjaxError.prototype = Object.create(Error.prototype);

function PerlError(message) { this.name = "PerlError"; this.message = message; this.stack = (new Error()).stack; } PerlError.prototype = Object.create(Error.prototype);

function FormError(message) { this.name = "FormError"; this.message = message; this.stack = (new Error()).stack; } FormError.prototype = Object.create(Error.prototype);


// ===================================================== // BLOCK // URL parser and String.startsWith // =====================================================


//This function parses a URL (or the current page's URL, if no argument is passed in) //and returns an object full of useful properties. //Reference: // http://www.abeautifulsite.net/parsing-urls-in-javascript/ // https://gist.github.com/jlong/2428561

function parseURL(url) { if (typeof url == "undefined") { parser = window.location; } else { var parser = document.createElement('a'); // Let the browser do the work parser.href = url; }

var searchObject = {}, queries, split, i;

// Convert query string to object queries = parser.search.replace(/^\?/, ).split('\u0026'); for( i = 0; i < queries.length; i++ ) { split = queries[i].split('='); searchObject[split[0]] = split[1]; } return { protocol: parser.protocol, host: parser.host, hostname: parser.hostname, port: parser.port, pathname: parser.pathname, search: parser.search, searchObject: searchObject, hash: parser.hash }; }



if (typeof String.prototype.startsWith != "function") { String.prototype.startsWith = function(str) { return this.slice(0, str.length) == str; } }


//VARIABLES

var default_messages = { kicking_to_view_mode: "Returning to View mode.", not_kicking_to_view_mode: "Please return to View mode.", user_cannot_edit: "You are not a member of the selected team.\nIf this is an error, make sure you are logged in and your account is included on your team's roster.", user_cannot_submit: "You do not have permission to submit this form.", user_cannot_use_admin_mode: "You are not an administrator.", user_cannot_edit_admin_fields: "That field is only for use by iGEM Headquarters.",

//used by get_user_info for the case of a user-owned form only (right now, those can't be made public) not_logged_in: "You are not logged in. Please log in (or <a href='http:\/\/igem.org/Account_Apply.cgi'>sign up for an account</a>) to complete this form.", //to be used by OIC if the user is mid-editing and the silent logout bug happens. silent_logout_bug: "Oops, the silent logout bug happened!",

form_validation_fail_beginning: "Some required answers are missing:", unparseable_ajax_response: "There was an error in FORM.CGI. Please inform us of this error by emailing hq (at) igem (dot) org.", please_confirm_submission: "Please check the box to confirm your submission.", confirm_deletion: "Are you sure you want to delete this form?\nYou cannot undo this action.", field_validation_fail_beginning: "Answer missing:", invalid_email_address: "Invalid email address" };


var messages = jQuery.extend({}, default_messages, form_info.messages);

var non_admin_DFFs = "[data-form-field]:not([data-form-field^='admin'])"; //convenient selector for non-admin DFFs

var valid_modes = ["view", "edit", "admin"]; var valid_owner_types = ["user", "judge", "team", "lab", "course"];

//this is where the valid user/viewer categories are defined var valid_permission_groups = ["author", "group_members", "super_users", "any_users", "public"]; var valid_roles = ["Primary Contact", "Primary PI", "Secondary PI", "Student Leader", "Student", "Instructor", "Advisor", "Other", "None"]; var valid_required = ["required", "optional"];

//Explicitly initialize global variables var mode; var team_id; var author; var owner_info; var user_info; var form_displayed = false; var form_submitted = false; //sigh, it looks like I can't get away without this one


var permissions = {view: false, edit: false, submit: false, admin: false};

var parsed_query_string = parseURL().searchObject; mode = parsed_query_string["mode"];

if (form_info.owner_type == "team") { if (typeof team_id == "undefined") { team_id = parseInt(parsed_query_string["team_id"]); } } else if (form_info.owner_type == "user") { author = parsed_query_string["author"]; } else { throw new FormError("Judge, Lab, and Course owned forms are not implemented yet"); }


// ===================================================== // BLOCK // VALIDATING INDIVIDUAL FORMS CONFIG // =====================================================

function validate_form_info() { assert(typeof form_info == "object", "form_info is not an object"); assert(typeof form_info.name == "string", "form_info.name is not a string"); assert(typeof form_info.display_title == "string", "form_info.display_title is not a string"); assert(valid_modes.indexOf(form_info.default_mode) > -1, "form_info.default_mode is not valid"); assert(valid_owner_types.indexOf(form_info.owner_type) > -1, "form_info.owner_type is not valid"); assert(form_info.permissions instanceof Object, "form_info.permissions is not an object"); ["view", "edit", "submit", "admin"].forEach(function(action, index, array) { assert(form_info.permissions[action] instanceof Array, "form_info.permissions." + action + " is not an array"); form_info.permissions[action].forEach(function(perm_group, index, array) { assert(valid_permission_groups.indexOf(perm_group) > -1 || valid_roles.indexOf(perm_group) > -1 , "invalid permission group in form_info.permissions." + action); }); }); assert(typeof form_info.ajax_URL == "string", "form_info.ajax_URL is not a string"); assert(valid_required.indexOf(form_info.validate_unspecified_fields) > -1, "form_info.validate_unspecified_fields is not valid"); }

validate_form_info();

// ===================================================== // BLOCK // SETUP ON DOCUMENT.READY (yes it is the kitchen sink) // ===================================================== function document_ready_handler() {


//Validate mode and default to something if invalid check_mode();

//NOW A WHOLE BUNCH OF RANDOM ELEMENT SETUP

//Display form title $("#form_title_display").text(form_info.display_title).show();

//read the nokick checkbox and bind it to change nokick nokick = $("#nokick").prop("checked"); $("#nokick").change(function(event) { nokick = $(this).prop("checked"); });

//bind author-select input on enter key $("#author_select").keyup(function(event) { if (event.which == 13) { change_author($(this).val()); } });

//apply wrapper divs for feedback function $("[data-form-field]:not(textarea)").wrap("<\/span>");

$("textarea[data-form-field]").wrap("
<\/DIV>");

$("p").find(".wrapper").has("[type=radio], [type=checkbox]").css("margin-left", "2em");

//force create AJ log and error divs

//$("#formbody").before("
").after("
");

//display log div //(it will be hidden from non-admin users by default, but if you're debugging for such a user //then you can go into the console and show it -- you do want it to EXIST for all users. //So don't move this into an if-user-is-admin clause!) aj_display_log_div();

//bind click events for .change_mode(team,author) links //needs to delegate from #bodyContent because not all .change_mode links exist //at document.ready $("#bodyContent").on("click", "a.change_mode", function(e) { var new_action = $(this).attr("data-mode"); change_mode(new_action); }); $("#bodyContent").on("click", "a.change_team", function(e) { var new_team = $(this).attr("data-team"); change_team(new_team); }); $("#bodyContent").on("click", "a.change_author", function(e) { var new_author = $(this).attr("data-author"); change_author(new_author); });

//bind hide_admin $("#hide_admin").click(function(event) { event.preventDefault(); $(".admins_only").hide(); });

//The event handlers for data-form-fields used to be bound here. //But because some of them depend on user_info, I've moved them into their own function //which is called by get_user_info when it's done. //**This change was dumb. Revert it. But it's tangled, so that will take some doing.

refresh_form();

//Start the chain by getting user info get_user_info();

$("#team_list_dropdown").on("change", function(e) { var team_id_chosen = $(this).val(); change_team(parseInt(team_id_chosen)); });


if (form_info.owner_type == "team") { //put teams in the team list dropdown jQuery.ajax({ url: "https://igem.org/aj/team_list.cgi", type: "GET", timeout: 30000, dataType: "json", data: {command: "get_team_list", year: form_info.year}, error: function(jqxhr, textStatus, errorThrown) { general_ajax_error( jqxhr, textStatus, errorThrown, "Failed to get list of teams"); aj_log("Failed to get list of teams"); }, success: function(data, textStatus, jqxhr) { put_teams_in_selector(data); } }); } else { $("#team_list_dropdown").hide(); }


//if it's a user-owned form and no author is specified, it defaults to self. //but that happens later, in get_user_info success //(because it can't default to self if you're not logged in) }


function put_teams_in_selector(team_list) { //Each element of team_list is an object like //{ region: "Europe", name: "Aachen", id: "1585" } var optgroups = { "Africa": "<optgroup label='Africa'>", "Asia": "<optgroup label='Asia'>", "Europe": "<optgroup label='Europe'>", "Latin America": "<optgroup label='Latin America'>", "North America": "<optgroup label='North America'>" }; team_list.forEach(function(team) { optgroups[team.region] += "<option value='" + team.id + "'>" + team.name + "</option>"; }); //I'm not using a for..in loop for this because order is important $("#team_list_dropdown").append(optgroups["Africa"] + "</optgroup>"); $("#team_list_dropdown").append(optgroups["Asia"] + "</optgroup>"); $("#team_list_dropdown").append(optgroups["Europe"] + "</optgroup>"); $("#team_list_dropdown").append(optgroups["Latin America"] + "</optgroup>"); $("#team_list_dropdown").append(optgroups["North America"] + "</optgroup>"); }

function bind_DFF_event_handlers() { //bind change handler to all data-form-field elements //except admin fields and submit-family buttons //and validate-on-change fields $(non_admin_DFFs).not("[type='submit']").not(".validate-on-change").change(change_handler_for_most_DFFs); $(non_admin_DFFs).filter(".validate-on-change").change(DFF_change_handler_with_validation);

//bind handler to admin data-form-fields according to whether user is admin if(user_info.is_super) { $("[data-form-field^='admin']").change(admin_DFF_change_handler); } else { $("[data-form-field^='admin']").click(admin_lockout_click_handler); }

//bind click handlers for submit/delete/return buttons //These are bound regardless of permissions, BUT //one_input_changed will reject these changes if the user doesn't have submit permissions $("[data-form-field=submit][type='submit']").click(click_handler_for_submit); $("[data-form-field=delete_form][type='submit']").click(click_handler_for_delete); $("[data-form-field=return_form][type='submit']").click(click_handler_for_return); $("#popwin_unsubmit").on("click", click_handler_for_return);

$("#popwin_dismiss").on("click", function(event) { event.preventDefault(); $("#submitted_warning").hide(); });

//bind click handler for submit confirmation box $("#" + $("[data-form-field=submit]").attr("data-confirmation")).on("click", click_handler_for_confirm_submit);

//bind handler for custom new subform event //this event is right now triggered by code that's inline in the form page, //which also handles validation of new sub form fields //(eventually should be moved into here, though) $("[data-form-field=sub_form]").on("kforms:newsubform", newsf_handler); }

$(document).ready(document_ready_handler);

function change_handler_for_most_DFFs(event) { //console.log("change_handler_for_most_DFFs"); if ($(this).attr("data-form-field") == "sub_form") { console.log("change handler on sub_form"); var offon = ($(this).val() == "") ? "on" : "off"; $("[data-form-field^='sub_form_']").each(function(index, field) { dffswitch(field, offon); }); one_input_changed(this, 'store', , false, false); } else if ($(this).attr("data-form-field").startsWith("sub_form_")) { if ($("[data-form-field=sub_form]").val() == "") { return; } one_input_changed(this, 'store', , false, true); } else { one_input_changed(this, 'store', , false, true); } }


function DFF_change_handler_with_validation(event) { var validation_result = validate_one_field(this); if (validation_result == "") { one_input_changed(this, 'store', , false, true); } }

function admin_DFF_change_handler(event) { //console.log("admin_DFF_change_handler"); one_input_changed(this, 'store', , false, true); }


function admin_lockout_click_handler(event) { //console.log("admin_lockout_click_handler"); event.preventDefault(); alert(messages.user_cannot_edit_admin_fields); }


function click_handler_for_submit(event) { event.preventDefault();

//if there are any validation error messages, show them and don't submit var errors = validate_form_entries(); if(errors.length > 0) { alert(messages.form_validation_fail_beginning + "\n\n" + errors.join("\n")); return; }

//moved confirmation-box-polling to OIC so it happens AFTER permission checking //finally, actually submit the form one_input_changed(this, 'submit', , false, true); }

function click_handler_for_confirm_submit(event) { if (!permissions.submit) { alert(messages.user_cannot_submit); event.preventDefault(); } }

function click_handler_for_delete(event) {

//console.log("starting delete click handler"); event.preventDefault(); var confirmed = window.confirm(messages.confirm_deletion); if (confirmed) { one_input_changed(this, 'delete_form', , false, true); } }

function click_handler_for_return(event) { event.preventDefault(); one_input_changed(this, 'return_form', , false, true); }

function newsf_handler(event, newname) { console.log("newsf_handler, value " + newname); make_new_sub_form(newname, false, true); one_input_changed(this, 'store', , true, true); }

// ===================================================== // BLOCK // USER AND OWNER INFO // =====================================================

//Get information about the logged-in user //(nb: it looks at the login cookie, not at wgUserName) function get_user_info() { aj_log("Getting user info"); var request = jQuery.ajax({ url: form_info.ajax_URL, type: "POST", timeout: 30000, data: {command: 'get_user_info'}, dataType: "json", error: function(jqxhr, textStatus, errorThrown) { general_ajax_error( jqxhr, textStatus, errorThrown, "Failed to get information about the user"); }, success: function(data, textStatus, jqxhr) { aj_log("Received user info: " + jqxhr.responseText); if(try_parse_received_info(jqxhr.responseText)) { user_info = jqxhr.responseJSON; bind_DFF_event_handlers();

//Show hide admin stuff if (user_info.is_super == "true") { $(".admins_only").show(); } else { $(".admins_only").hide(); }

//now, if it's a user-owned form, we can handle some special cases at this point if (form_info.owner_type == "user") {

//If they're not logged in, //Yell at them and stop the setup chain. //**eventually I may want to change this, if I want to allow the public to view //any user-owned forms if (user_info.user_name == "") { aj_display_error(messages.not_logged_in); return; }

//Now we know they must be logged in, and we can default to self-author if no author specified. else if (typeof author == "undefined") { author = user_info.user_name; } }

//allow individual forms to have a custom extra response at this point if (typeof custom_get_user_info_response == "function") { custom_get_user_info_response(data, textStatus, jqxhr); } get_owner_info(); } else { aj_display_error(messages.unparseable_ajax_response); } } }); }


//Get information about the form owner //This calls one of several specialized ajax getters, and then triggers the whole //rest of form setup upon success of the request. function get_owner_info(query_param) { aj_log("Getting owner info"); owner_info = undefined; //refresh_form(); var ajax_getter;

if (form_info.owner_type == "team") {ajax_getter = get_team_info;} else if (form_info.owner_type == "user") {ajax_getter = get_author_info;} else if (form_info.owner_type == "judge") {ajax_getter = get_judge_info;} else if (form_info.owner_type == "lab") {ajax_getter = get_lab_info;} else if (form_info.owner_type == "course") {ajax_getter = get_course_info;}

var request = ajax_getter(query_param); request.done( function (data, textStatus, jqxhr) { if(try_parse_received_info(jqxhr.responseText)) { if (data.error) { //we did the query wrong, oh no //this case will catch invalid team/lab/course IDs respond_to_error_in_owner_info(); } else { //special case: invalid usernames //will return empty strings for user_name and full_name if (form_info.owner_type == "user" && data.user_name == "") { respond_to_error_in_owner_info(); } else { owner_info = data; //put owner info into global object new_form_setup(); //this used to be set_up_form_and_mode } } if (typeof custom_get_owner_info_response == "function") { custom_get_owner_info_response(data, textStatus, jqxhr); } } else { aj_display_error(messages.unparseable_ajax_response); } }); }


//**These get_foo_info functions are kind of repetitive. Maybe rewrite later.

//Get information about a user of a specified username. //If none is specified, the function will look at global var author. function get_author_info(query_author) { query_author = (typeof query_author == "undefined") ? author : query_author return jQuery.ajax({ url: form_info.ajax_URL, type: "POST", timeout: 30000, dataType: "json", data: {command: 'get_user_info', username: query_author}, error: function(jqxhr, textStatus, errorThrown) { general_ajax_error( jqxhr, textStatus, errorThrown, "Failed to get information about author " + query_author); aj_log("Failed to get author info"); }, success: function(data, textStatus, jqxhr) { aj_log("Received author info: " + jqxhr.responseText); } }); }

function get_judge_info(query_judge_username) { query_judge_username = (typeof query_judge_username == "undefined") ? judge_username : query_judge_username; return jQuery.ajax({ url: form_info.ajax_URL, type: 'POST', timeout: 30000, dataType: "json", data: {command: "get_judge_info", judge_username: query_judge_username}, error: function(jqxhr, textStatus, errorThrown) { general_ajax_error(jqxhr, textStatus, errorThrown, "Failed to get judge information"); aj_log("Failed to get judge info"); }, success: function(data, textStatus, jqxhr) { aj_log("Received judge info: " + jqxhr.responseText); } }); }

// Get information about a team (and about the user w/r/t that team) function get_team_info(query_team_id) { query_team_id = (typeof query_team_id == "undefined") ? team_id : query_team_id; return jQuery.ajax({ url: form_info.ajax_URL, type: 'POST', timeout: 30000, dataType: "json", data: {command: 'get_info', form_name: form_info.name, team_id: parseInt(query_team_id)}, error: function(jqxhr, textStatus, errorThrown) { general_ajax_error(jqxhr, textStatus, errorThrown, "Failed to get team information"); aj_log("Failed to get team info"); }, success: function(data, textStatus, jqxhr) { aj_log("Received team info: " + jqxhr.responseText); } }); }

function get_lab_info(query_lab_id) { query_lab_id = (typeof query_lab_id == "undefined") ? lab_id : query_lab_id; return jQuery.ajax({ url: form_info.ajax_URL, type: 'POST', timeout: 30000, dataType: "json", data: {command: 'get_info', form_name: form_info.name, lab_id: parseInt(query_lab_id)}, error: function(jqxhr, textStatus, errorThrown) { general_ajax_error(jqxhr, textStatus, errorThrown, "Failed to get lab information"); aj_log("Failed to get lab info"); }, success: function(data, textStatus, jqxhr) { aj_log("Received lab info: " + jqxhr.responseText); } }); }

function get_course_info(query_course_id) { query_course_id = (typeof query_course_id == "undefined") ? course_id : query_course_id; return jQuery.ajax({ url: form_info.ajax_URL, type: 'POST', timeout: 30000, dataType: "json", data: {command: 'get_info', form_name: form_info.name, course_id: parseInt(query_course_id)}, error: function(jqxhr, textStatus, errorThrown) { general_ajax_error(jqxhr, textStatus, errorThrown, "Failed to get course information"); aj_log("Failed to get course info"); }, success: function(data, textStatus, jqxhr) { aj_log("Received course info: " + jqxhr.responseText); } }); }

function get_sub_form_list() { aj_log("Getting sub form list"); return jQuery.ajax({ url: form_info.ajax_URL, type: 'POST', timeout: 30000, dataType: "json", data: {command: 'get_sub_form_list', form_name: form_info.name, team_id: team_id}, //**THIS DATA DOES NOT WORK FOR ANY OWNER TYPE EXCEPT TEAMS. error: function(jqxhr, textStatus, errorThrown) { general_ajax_error(jqxhr, textStatus, errorThrown, "Failed to get subforms"); aj_log("Failed to get sub form list"); }, success: function(data, textStatus, jqxhr) { aj_log("Received sub_form list: " + jqxhr.responseText); } }); }



function respond_to_error_in_owner_info() { aj_display_error("Invalid " + form_info.owner_type + " selected"); }



//**Moe some of this down in to subform utility functions function display_form() { if ($("[data-form-field='sub_form']").length == 1) { var sfreq = get_sub_form_list(); sfreq.done(function(data, textStatus, jqxhr){ data.sub_forms.forEach(function(sf) { if (sf.sub_form == "") { return; } make_new_sub_form(sf.sub_form, sf.submitted == "1", false); }); }); initially_get_old_answers(); } else { initially_get_old_answers(); } }




//**Okay so this needs MAJOR testing. //The problem is that the interaction of form_submitted, form_displayed, display_form, and OIC //may create an infinite loop. If we're gonna call this on initial setup AND every time someone hits //the submit button... can we not call it twice? Problem is OIC calls this when someone hits the su function new_form_setup() { determine_permissions();

//special-case because it's common to poke around a bunch of teams, then go back to your team //and be confused that you can't edit if (mode == "view" && form_info.default_mode == "edit" && permissions["edit"]) { mode = "edit"; }

show_owner(); show_mode();

if (!permissions[mode]) { deny_permission(); }

//Here's where we handle the visibility of all the accessory crap that ISN'T a dff. if (form_info.owner_type == "team") { $("#team_select_inner").slideUp(); //this won't get called unless a team has been picked } if (form_submitted) { $("#submitted_warning").show(); } else { $("#submitted_warning").hide(); }

if (!form_displayed) {display_form();}

set_up_dffs_for_mode(); }

function set_up_dffs_for_mode(mymode) { mymode = (typeof mymode == "undefined") ? mode : mymode; if (!permissions[mode]) { mymode = "view"; }

//**Carefully review all these conditions against what they should be. Oh god. $("[data-form-field]").each(function(index, field) { field = $(field); var dff = field.attr("data-form-field"); if (dff == "sub_form") { //the subform select dffswitch(field, "on"); } else if (dff.startsWith("sub_form")) { //subform accessory fields dffswitch(field, ($("[data-form-field='sub_form']").val() == "" && mode != "view") ? "on" : "off"); } else if (dff.startsWith("admin")) { //admin dffs dffswitch(field, (mode == "admin" && user_info.is_super == "true") ? "on" : "off"); } else if (dff == "submit") { //submit button dffswitch(field, (mode != "view" && permissions.submit && !form_submitted) ? "on" : "off"); } else if (dff == "return_form") { //unsubmit button dffswitch(field, (mode != "view" && permissions.edit && form_submitted) ? "on" : "off"); } else if (dff == "delete_form") { //delete button dffswitch(field, (mode != "view" && permissions.edit) ? "on" : "off"); } else { //Regular DFF //This will ALSO check for an empty sub_form (value ""). //Note that a NONEXISTENT sub_form would have val() undefined. dffswitch(field, (mode != "view" && !form_submitted && $("[data-form-field=sub_form]").val() != "") ? "on" : "off"); } }); }

// ===================================================== // BLOCK // OWNER/MODE SHOWING/CHANGING // =====================================================

//modes! //All this does is reconcile the view/edit/admin mode indicator at the top of the page function show_mode() { //console.log("show_mode"); var m = $("#modes"); if (mode == "view") { m.html(jQuery.parseHTML("Mode: View <a class='change_mode' data-mode='edit'>(click for edit mode)</a>")); } else if (mode == "edit") { m.html(jQuery.parseHTML("Mode: Edit <a class='change_mode' data-mode='view'>(click for view mode)</a>")); } else if (mode == "admin") { m.html(jQuery.parseHTML("Mode: Admin <a class='change_mode' data-mode='view'>(view)</a> <a class='change_mode' data-mode='edit'>(edit)</a>")); }

//if the user can't edit, color edit links gray if (!permissions.edit) { $("[data-mode='edit']").css("color", "gray"); } }

function show_owner() { var owner_name; var display_text = "[none]";

//case team if (form_info.owner_type == "team") { $("#prompt_to_choose_team").hide(); owner_name = owner_info.team_name; var this_teams_profile_href = "https://igem.org/Team.cgi?id=" + team_id; display_html = "for Team <a target='_blank' href='" + this_teams_profile_href + "'>" + owner_name + "</a>"; } //case author else if (form_info.owner_type == "user") { owner_name = owner_info.full_name; display_html = "for user " + owner_name; } //case lab, course, judge else { //owner_name = display_html = "oops, this form is owned by a lab, course, or judge"; throw new FormError("Can't show_owner for lab/course/judge: not implemented yet"); }

$("#owner_name_display").html(display_html).show(); }

function prompt_to_choose_team() { //console.log("prompt_to_choose_team"); $("#owner_name_display").hide(); $("#prompt_to_choose_team").show(); $("#team_select_inner").show(); }

function change_mode(new_mode) { if (form_info.owner_type == "team" && typeof owner_info == "undefined") { alert("Please choose a team first!"); //**unify? or make team-choosing better return; } //console.log("change_mode"); refresh_form(false); mode = new_mode; check_mode(); //set_up_form_and_mode(); new_form_setup(); }

function change_team(new_team_id) { //console.log("change_team"); refresh_form(); team_id = new_team_id; get_owner_info(); }

//Change to a different user's form function change_author(new_author) { refresh_form(); author = new_author; get_owner_info(); }

//Checks the mode and sets to default_mode if it's invalid or unspecified function check_mode() { if (valid_modes.indexOf(mode) == -1) { mode = form_info.default_mode; } }

//This function goes into the team_select_container and replaces all the //links with a link to the current page / query string with the selected team_id //and the current mode. function replace_links_in_team_selector() { $("#team_select_inner").find("a").each(function() { var the_link = $(this); var existing_href = the_link.attr("href"); var team_id_from_this_link = existing_href.split("?id=")[1]; the_link.removeAttr("href").addClass("change_team").attr("data-team", team_id_from_this_link); }); }

// ===================================================== // BLOCK // SENDING AND RECEIVING FORM ANSWERS // =====================================================

function one_input_changed( input_element, command, effective_date, send_sub_form_accessories, check_for_silent_logout ) { send_sub_form_accessories = (typeof send_sub_form_accessories == "undefined") ? false : send_sub_form_accessories; check_for_silent_logout = (typeof check_for_silent_logout == "undefined") ? true : check_for_silent_logout; var input_element = $( input_element );

//permissions if (command == "submit" || command == "delete_form") { if (!permissions["submit"] && !permissions["admin"]) { alert(messages.user_cannot_submit); if (typeof custom_refuse_submission == "function") { custom_refuse_submission(input_element, command, effective_date); } return; } }

if (command == "submit") { //check if it has a confirmation box and if it's checked //if not, refuse submission //(currently unable to handle multiple confirmation boxes) var confirmation_box_id = input_element.attr("data-confirmation"); if(typeof confirmation_box_id != "undefined") { var confirmed = $("#" + confirmation_box_id).prop("checked"); if(!confirmed) { submission_not_confirmed(); return; } } }

//warning on unsubmitting a signed form if (command == "return_form") { if (permissions.edit && !permissions.submit) { //**can't unify this msg yet var confirm_string = "Are you sure you want to unsubmit this form?\n\n"; confirm_string += "You will need a " + form_info.permissions.submit.join(" / ") + " to submit the form again." if (!confirm(confirm_string)) { return; } } }

feedback(input_element, "sending");

if (owner_info == undefined) { aj_log( "Error: no form owner specified"); return; }

//Uncheck all confirmation boxes, //except on submit, return, and the initial "store" request when input_element is if (command != "submit" && command != "return_form" && input_element.length != 0) { uncheck_all_confirmations(); }

// Step 1: Format information about the element that changed and send it to the server // If a sub_form element exists, get its value var sub_form = $("[data-form-field=sub_form]").val() || ; var eff_date = ( typeof effective_date !== 'undefined' ) ? effective_date : '2004-01-01'; if (send_sub_form_accessories) { //in this condition we want to package all the values of the subform accessory fields into a single entry_list var entry_list = $("[data-form-field^=sub_form_]").get().reduce(function(previous, current, index, array) { var tmp = get_one_entry($(current)); if (typeof tmp == "undefined") { return previous; } else { return previous.concat(tmp); } }, []); } else { var entry_list = get_one_entry( input_element ); } var json_entries = JSON.stringify( entry_list ); aj_log( "OIC: " + command + ' '+ json_entries );

// When the sub_form changes, we clear all the inputs and they are reloaded by ajax if ( input_element.attr('data-form-field') == 'sub_form' ) { clear_form_but_keep_sub_form(); } // When the sub-form is deleted, we clear all the inputs and they are reloaded by ajax if ( input_element.attr('data-form-field') == 'delete_form') { //and delete the corresponding subform, unless it's the empty one $("[data-form-field=sub_form]").find(":selected").not("[value=]").remove(); clear_form(); }

var data = {command: command, form_name: form_info.name, sub_form: sub_form, eff_date: eff_date, entry_list: json_entries}; if (form_info.owner_type == "team") { data.team_id = team_id; } else if (form_info.owner_type == "user") { data.author_username = author; } else { throw new FormError("OIC can't deal with labs, courses, or judges"); }

var stor_req = jQuery.ajax({url: form_info.ajax_URL, type: 'POST', timeout: 30000, data: data, dataType: "json", error: function(jqxhr, textStatus, errorThrown) { feedback(input_element, "invalid"); general_ajax_error(jqxhr, textStatus, errorThrown, "Failed to save entry " + input_element.attr("name")); aj_log("Failed one_input_changed on entry " + input_element.attr("name")); }, success: function(data, textStatus, jqxhr) { aj_log( "Received: " +jqxhr.responseText); if (check_for_silent_logout && data.return_error == "Not logged in") { respond_to_silent_logout(input_element); return; } form_submitted = parseInt(data.submitted) > 0; //console.log("Done with OIC! Command was " + command); if (command == "submit" || command == "return_form") { //console.log(typeof data.submitted); //show_hide_submitted_status( data.submitted ); new_form_setup(); if (command == "submit") { mark_current_sub_form_as_submitted(); } else { mark_current_sub_form_as_unsubmitted(); } } if (input_element.get(0) == $("[data-form-field=sub_form]").get(0)) { //set_up_dffs_for_mode(); new_form_setup(); } process_received_data( jqxhr.responseText );

// Removes any warnings from other elements of the same name (radio buttons) unwarn($("[name='" + input_element.attr("name") + "']").not(input_element)); feedback(input_element, "sent"); aj_clear_error(); if(typeof custom_one_input_changed_response == "function") { //Allow individual forms to have a custom response custom_one_input_changed_response(input_element, command, effective_date, data, textStatus, jqxhr); } } });

stor_req.done(function(data, textStatus, jqxhr) { jQuery.noop(); }); }

// Step 1: Format up one entry function get_one_entry(input_element) { //console.log("get_one_entry"); var name = input_element.attr("data-form-field"); var answer = input_element.val();

//if the answer is an array, as it is from a select multiple, //then preemptively JSON.stringify it if (answer instanceof Array) { // console.log("Found an Array answer, converting to string"); answer = answer.toString(); // console.log(answer); }

if (input_element.attr('type') == 'checkbox') { answer = input_element.prop('checked'); } else if (input_element.attr('type') == 'radio') { if (!input_element.prop('checked')) { return; } answer = input_element.prop('value'); } else if (input_element.attr('type') == 'submit') { answer = ; }

return [ {'field_name': name, 'answer':answer } ]; }

// Step: 2 Process the received data for all the inputs // The received data looks like { ... , : [ { input_name: 'name', answers: 'answer'}, .... ] function process_received_data( data ) { //console.log("process_received_data"); try { data = JSON.parse( data ); //**Rewrite this to use try_parse_received_data } catch (err) { aj_log( 'JSON.parse error: ' + err ); return; } aj_display_error(data.error);

var entry_list = data.entry_list || []; for (var i = 0; i < entry_list.length; i++) { process_one_received_entry( entry_list[i] ); } // if (!form_displayed) { // new_form_setup(); // } form_displayed = true; return; }

//**Make this not fill in admin fields if you're not an admin! Or only some of them. Or something. function process_one_received_entry(entry) { var field_name = entry["field_name"] || ; var answer = entry["answer"] || ; var input_element = $("[data-form-field='"+ field_name +"']"); var input_type = input_element.attr('type'); if (input_type == 'submit') { return; }

//Special case for select-multiples if (input_element.is('select') && input_element.prop("multiple")) { // console.log("Hey, this element is a select-multiple, I'm gonna split the answer"); // console.log("The old answer is " + answer); answer = answer.split(","); // console.log("The new answer is " + answer); } aj_log("Processing " + field_name + " = '" + answer + "' (Found: " +input_element.length+ " element Type: "+input_type+")");

// Radio buttons are identified by form-field and value if (input_type == 'radio') { input_element = input_element.filter("[value='"+answer+"']"); input_element.prop('checked', true); } else if (input_type == "checkbox") { if (answer == 'false' || answer == '0') { answer = false; } input_element.prop("checked", answer ); } else { input_element.val(answer); } }


//Helper function for initially loading in the answers from a new form function initially_get_old_answers() { one_input_changed(, 'store', , false, false); //Store nothing, process all returned answers feedback($("[data-form-field]"), "none"); //Clear green/yellow feedback divs //form_displayed = true; //moved to OIC.done }


function respond_to_silent_logout(input_element) { input_element = (input_element instanceof jQuery) ? input_element : $(input_element); var msg = "Your login session has expired.\n Answer to question \"" + input_element.attr("name") + "\" was not saved.\n Please log out and then log in to continue."; warn(input_element, "Answer not saved. Please log out and log back in."); alert(msg); aj_display_error(msg); }


// // =========================================================================================================================================== // DETERMINE PERMISSIONS

//ana

//Check the view/edit/submit/admin permissions of the current user on the current form. //Puts them in global variable permissions. function determine_permissions() {

var perm_group; $.each(form_info.permissions, function(perm_name, groups_having_this_perm) { console.log("Permission to " + perm_name); perm_group = groups_having_this_perm[index];

for (index in groups_having_this_perm) { switch(perm_group){

case "public": { return true; break; }

case "any_users": { permissions.view = true; return user_info.user_name != ""; break; }

case "super_users": { permissions.admin = true; permissions.submit = true; permissions.edit = true; permissions.view = true;

return user_info.is_super == "true"; break; }

case "group_members": {

permissions.edit = true; permissions.view = true; return (typeof owner_info.role == "string" && owner_info.role != "None"); break; }

case "author": {

permissions.submit = true; permissions.edit = true; permissions.view = true; return user_info.user_name.toLowerCase() == author.toLowerCase(); break; } }}

});

return permissions; }




//Determine whether the current user falls into a given permission group on the current form. //groups are ["individualOwner", "group_members", "super_users", "any_users", "public"] function user_is_in_permission_group(perm_group) {

if (perm_group == "public") { return true; }


else if (perm_group == "any_users") {

permissions.view = true; return user_info.user_name != ""; }

else if (perm_group == "super_users") {

permissions.admin = true; permissions.submit = true; permissions.edit = true; permissions.view = true;

return user_info.is_super == "true";

}


else if (perm_group == "group_members") {

permissions.edit = true; permissions.view = true; return (typeof owner_info.role == "string" && owner_info.role != "None"); }

else if (perm_group == "author") {

permissions.submit = true; permissions.edit = true; permissions.view = true; return user_info.user_name.toLowerCase() == author.toLowerCase(); }


else if (valid_roles.indexOf(perm_group) > -1) { return owner_info.role == perm_group; } }


//when the user doesn't have permission to view/ edit the form, display the appropiate message function deny_permission() {

// alert message to give to user var alert_string = "Sorry, you don't have permission to " + mode + " that form.";

//because I got rid of the no kick checkbox nokick = false;

//if the user is not a super user, hide admin view if (!user_info.is_super) { $(".admins_only").hide(); }


//**figure out how to unify this string because it depends on current mode if (permissions.view) { if (nokick) { alert_string += "\n" + messages.not_kicking_to_view_mode; } else { alert_string += "\n" + messages.kicking_to_view_mode; } }

if (nokick) { aj_display_error(alert_string); } else { alert(alert_string); change_mode("view"); } }


// // =========================================================================================================================================== //








// ===================================================== // BLOCK // AJAX HELPERS // =====================================================

//General helper for ajax errors. //In every case, it will display an alert and an aj_display_error. //Not unifying these because form writers shouldn't change them. function general_ajax_error(jqxhr, textStatus, errorThrown, custom_msg) { //case: timeout error if (textStatus == "timeout") { alert("Timeout error"); aj_display_error("Timeout error: there could be a problem with your internet connection, or the server could be under a heavy load. Please try again later."); } //case: internet probably unplugged or something else if (jqxhr.status == 0) { alert("Unable to connect to server. Please check your Internet connection and try again."); aj_display_error("Unable to connect to server. Please check your Internet connection and try again."); } //case: 4xx/5xx error else if (400 <= jqxhr.status <= 599) { alert("Server error [" + jqxhr.status + " " + jqxhr.statusText + "]"); aj_display_error("Server error [" + jqxhr.status + " " + jqxhr.statusText + "]. Please report this error to iGEM HQ. In your message, please include the URL of this page, and the date/time of the error."); }

//other error else { alert("Error in ajax\n[" + textStatus + "]"); custom_msg = (typeof custom_msg == "undefined") ? "general AJAX error" : custom_msg; aj_display_error(custom_msg); } }

//All our ajax requests will receive both a responseJSON and a responseText. //Here, we attempt parsing the responseText. If there is an error in between //"ajax request fail" and "you gave wrong parameters to form.cgi", the responseText //will not be parseable as JSON. function try_parse_received_info(responseText) { try { JSON.parse(responseText); return true; } catch (err) { aj_log("JSON.parse error: " + err); aj_display_error(unparseable_ajax_response); return false; } }


// ===================================================== // BLOCK // SUBFORM STUFF // =====================================================

//Accepts a string and makes a new choice in the subform dropdown. //Boolean gotoit (default false) causes that choice to then be selected immediately. function make_new_sub_form(name, submitted, gotoit) { gotoit = (typeof gotoit == "undefined") ? false : gotoit;

var option = $("<option>").text(name).attr("value", name); option.appendTo( submitted ? $("#sub_forms_submitted") : $("#sub_forms_unsubmitted") ); if (gotoit) { option.prop("selected", true);//.trigger("change"); } }


//Function and wrappers for moving subforms around. // - option should be a jQuery object containing an option, or a string equaling the value of an option // - move_to should be "submitted" or "unsubmitted" function move_sub_form(option, move_to) { if (typeof option == "string") { option = $("[data-form-field=sub_form]").find("[value='" + option + "']"); } option.detach().appendTo($("#sub_forms_" + move_to)); } function mark_current_sub_form_as_submitted() { move_sub_form($("[data-form-field=sub_form]").find(":selected"), "submitted"); } function mark_current_sub_form_as_unsubmitted() { move_sub_form($("[data-form-field=sub_form]").find(":selected"), "unsubmitted"); }

function empty_sub_form_list() { $("[data-form-field=sub_form]").children("optgroup").empty(); }


// ===================================================== // BLOCK // MISC UTILITY FUNCTIONS // =====================================================

//Fake "assert" function for assertion testing. //It is best practice to strip out assertions before code goes to real users. function assert(condition, message) { if (!condition) { message = message || "Assertion failed"; if (typeof FormError !== "undefined") { throw new FormError(message); } throw new Error(message); } }

function clear_form_but_keep_sub_form() { $('[data-form-field][type!=checkbox][type!=radio][type!=submit][data-form-field!=sub_form]').val(); $('[data-form-field][type=checkbox]').prop('checked', false); $("[data-form-field][type=radio]").prop('checked', false); feedback($("[data-form-field]"), "none"); }

function clear_form() { $("[data-form-field][type!=checkbox][type!=radio][type!=submit][data-form-field!='sub_form']").val(); $('[data-form-field][type=checkbox]').prop('checked', false); $("[data-form-field][type=radio]").prop('checked', false); $("[data-form-field='sub_form']").val(""); feedback($("[data-form-field]"), "none"); }

//Upon first page load, or upon changing owner, we must clear the form to keep the browser from //autofilling fields, and to keep the last owner's answers from leaving droppings in the new form //also we readonly all DFFs -- they will be made un-readonly upon successful entry into //Edit or Admin mode function refresh_form(also_clear_answers) { also_clear_answers = (typeof also_clear_answers == "undefined") ? true : false; if (also_clear_answers) { empty_sub_form_list(); clear_form(); form_displayed = false; } readonly_all_DFFs(); aj_clear_error(); }


//function simply unchecks all confirmation boxes (radio or checkbox) with //class="confirmation" function uncheck_all_confirmations() { $(".confirmation").prop("checked", false); }

function submission_not_confirmed() { if (typeof custom_submission_not_confirmed == "function") { custom_submission_not_confirmed(); } else { alert(messages.please_confirm_submission); } }

//gets all sets of radio buttons that have a data-form-field attribute //returns them as an array of "name"s function get_all_sets_of_radios() { var names_list = [];

$("[data-form-field][type='radio']").each(function(index) { //get the name var this_radio_name = $(this).attr("name");

//check if the name is already in the list. if not, add it if(names_list.indexOf(this_radio_name) == -1) { names_list.push(this_radio_name); } //else skip it }); return names_list; }


// ===================================================== // BLOCK // READONLY/DISABLE FUNCTIONS // =====================================================

//Simply disables everything with a data-form-field function disable_all_DFFs() { $('[data-form-field]').prop('disabled', true); }

//Function makes all DFF elements either readonly (for text boxes) or disabled (everything else). //It does not change any of their event bindings. function readonly_all_DFFs() { $('[data-form-field]').each(function() { var field = $(this); if (field.is("textarea") || field.is("input[type='text']")) { field.prop('readonly', true); } else { field.prop('disabled', true); } }); }

//undisable all DFFs except admin ones and static ones function undisable_all_DFFs() { $(non_admin_DFFs).not(".static").prop('disabled', false).prop('readonly', false); }

//undisable admin DFFs, except static ones function undisable_admin_DFFs() { $('[data-form-field^="admin"]').not(".static").prop("disabled", false).prop("readonly", false); }

//switch one field to "off" or "on" (tested, works) function dffswitch(field, offon) { field = (field instanceof jQuery) ? field : $(field); if (offon == "off") { if (field.is("textarea") || field.is("input[type='text']")) { field.prop('readonly', true); } else { field.prop('disabled', true); } } else if (offon == "on") { field.prop("disabled", false).attr("readonly", false); } else { throw new FormError("I can't switch field " + field.attr("data-form-field") + " to state " + offon); } }


// ===================================================== // BLOCK // LOG ENTRIES AND ERROR MESSAGES // =====================================================

// If you wish to Display a Log of Transactions add a div with id= 'aj_form_log' function aj_display_log_div() { var div = $('#aj_form_log'); div.addClass("admins_only");

div.html("
"

+ "<\/DIV>" ); jq_versions(); aj_display_log_buttons(); }


function aj_display_log_buttons() { var div = $('#aj_form_log_inner_div'); div.before("<button type='button' id='clear_log'>Clear Log<\/button>"); div.before("<button type='button' id='toggle_log'>Toggle Log<\/button>"); $("#clear_log").on("click", aj_display_log_div); $("#toggle_log").on("click", function(event) { $("#aj_form_log_inner_div").toggle(); }); }


function aj_log(text) { var div = $('#aj_form_log_inner_div'); if (div.length == 0) { return; } var old_text = div.html();

div.html(old_text + "

" + text + "<\/P>"); } // Put the jQuery versions in the log box function jq_versions() { var jq_v='None'; try { jq_v = jQuery.fn.jquery || ; } catch (err) {} var ui_v = 'None'; try { ui_v= jQuery.ui ? jQuery.ui.version || 'pre 1.6' : ; } catch (err) {} aj_log( 'AJ Form Log: [ jQuery v '+ jq_v + '   jQuery UI v ' + ui_v + ' ]' ); } function aj_display_error(error_text){ if (!error_text) { return; } var div = $('#aj_form_errors'); div.html("

<img src='https://static.igem.org/mediawiki/2017/d/d6/Info_demo_icon-13.svg'>
Software error:
" + error_text + "
");

div.show(); }



function aj_clear_error(){ //aj_display_error_text(""); $("#aj_form_errors").text("").hide(); }


// ===================================================== // BLOCK Hey I know you don't like this but don't get rid of it // SHIELDING It could be useful for sign/lock/submit! // =====================================================

//All these shield-manipulating functions expect a jQuery object as an argument. //But if none is given, they default to the single shield on a form, which is assumed //to have id="shield". //Shield divs should have class "shield-active" or "shield-inactive" as defined in //Template:CSS/SafetyForms function toggle_shield(s) { s = (typeof s == "undefined") ? $("#shield") : s; if(s.hasClass("shield-inactive")) { add_shield(s); } else { remove_shield(s); } }


function add_shield(s) { s = (typeof s == "undefined") ? $("#shield") : s; s.removeClass("shield-inactive"); s.addClass("shield-active"); }


function remove_shield(s) { s = (typeof s == "undefined") ? $("#shield") : s; s.removeClass("shield-active"); s.addClass("shield-inactive"); }


// ===================================================== // BLOCK // VALIDATION // =====================================================

function validate_form_entries() { var fields_to_validate; var results = [];

$(non_admin_DFFs).each(function(index, field){ field = $(field); var this_result = ""; var what_the_field_says = field.attr("data-validation"); if (what_the_field_says == "required") { this_result = validate_one_field(field); } else if (what_the_field_says == "optional") { return; } else if (typeof what_the_field_says == "undefined") { if (form_info.validate_unspecified_fields == "required") { this_result = validate_one_field(field); } }

//If we reach this case, data-validation is set to something custom //It should be a selector (NOT JUST AN ID) for a radio or checkbox input elsewhere on the page. //We will require field to be nonempty if the indicated radio/checkbox is checked. else { //console.log("Custom validating field " + field.attr("data-form-field") + " with respect to " + what_the_field_says); var other_field = $(what_the_field_says); if (other_field.length == 0) { throw new FormError("Can't validate field " + field.attr("data-form-field") + " with respect to selector '" + what_the_field_says + "'; that field does not exist"); } else if (other_field.attr('type') != "radio" && other_field.attr('type') != "checkbox") { throw new FormError("Can't validate field " + field.attr("data-form-field") + " with respect to selector '" + what_the_field_says + "'; that field is not a radio or checkbox"); } else { if (other_field.prop("checked")) { this_result = validate_one_field(field); } } }

//Only add nonempty, nonduplicate strings if (this_result != "" && results.indexOf(this_result) == -1) { results.push(this_result); } });

return results; }



function validate_one_field(fld) { fld = (fld instanceof jQuery) ? fld : $(fld); var type = fld.attr("type"); var name = fld.attr("name"); var result = ""; var isValid;

if (fld.attr("data-validate-as") == "email") { return validate_email(fld); }

if (type == "radio" || type == "checkbox") { isValid = $("[name='"+name+"']:checked").length > 0; } else { isValid = (fld.val() != "" && fld.val() != null && typeof fld.val() != "undefined"); }

if (!isValid) { result = messages.field_validation_fail_beginning + " " + name; feedback(fld, "invalid"); } return result }



var email_regex = /^.+@.+\..+/; function validate_email(fld) { var result; if (email_regex.test(fld.val())) { result = ""; unwarn(fld); } else { result = messages.invalid_email_address; feedback(fld, "invalid"); warn(fld, messages.invalid_email_address); } return result; }


// ===================================================== // YELLOW-AND-GREEN FEEDBACK // =====================================================

var wrapper_none_color = ""; var wrapper_sending_color = "#FFFF99"; var wrapper_sent4 = "#B3FF66"; var wrapper_sent3 = "#C6FF8C"; var wrapper_sent2 = "#D9FFB3"; var wrapper_sent1 = "#ECFFD9"; var wrapper_invalid_color = "pink"; var wrapper_readonly_color = "#EEEEEE";

function feedback(element, command) { element = (element instanceof jQuery) ? element : $(element);


var wrapper = element.parent(".wrapper");

if(command == "none") { wrapper.css("background-color", wrapper_none_color); } else if(command == "sending") { wrapper.children(".dff-warning").remove(); wrapper.css("background-color", wrapper_sending_color); } else if(command == "sent") { wrapper.children(".dff-warning").remove(); fade_sent(wrapper); } else if(command == "invalid") { wrapper.css("background-color", wrapper_invalid_color); } else if(command == "readonly") { wrapper.css("background-color", wrapper_readonly_color); } else { throw new FormError("Invalid command " + command + " for feedback on element #" + element.attr("id")); } }

//Fades a wrapper from fresh green thru pale green back to white. //**We should really be using the jQuery Color() plugin, or jQuery UI, for this. //(because native jQuery cannot animate a non-numeric CSS property like background-color) //ORRRRR just fade the OPACITY function fade_sent(wrapper) { var interval = 100; wrapper.css("background-color", wrapper_sent4); setTimeout(function() { wrapper.css("background-color", wrapper_sent3); setTimeout(function() { wrapper.css("background-color", wrapper_sent2); setTimeout(function() { wrapper.css("background-color", wrapper_sent1); setTimeout(function() { wrapper.css("background-color", wrapper_none_color); }, interval); }, interval); }, interval); }, 3 * interval); }

function warn(element, warning_text) { if (!warning_text || typeof warning_text != "string") { unwarn(element); } element = (element instanceof jQuery) ? element : $(element); var wrapper = element.parent(".wrapper"); if (wrapper.length == 0) { return; }

//First, remove any warnings that exist already wrapper.children(".dff-warning").remove();

if (wrapper.is("span") && element.attr("type") != "text") { //everything except textarea, select, and input type="text" wrapper.prepend($("" + warning_text + "")); } else if (element.attr("type") == "text") { wrapper.append($("" + warning_text + "")); } else { //textarea, select

wrapper.prepend($("<p class='dff-warning'>" + warning_text + "

"));

}

feedback(element, "invalid"); }

function unwarn(element) { element = (element instanceof jQuery) ? element : $(element); var wrapper = element.parent(".wrapper"); if (wrapper.length == 0) { return; } wrapper.children(".dff-warning").remove(); feedback(element, "none");

}