diff --git a/app/models/fel.js b/app/models/fel.js index c4933642..9ec1e38a 100644 --- a/app/models/fel.js +++ b/app/models/fel.js @@ -15,6 +15,16 @@ var FEL = mongoose.Schema({ resample: Number, ci: Boolean, bootstrap: Boolean, + multiple_hits: { + type: String, + enum: ["None", "Double", "Double+Triple"], + default: "None", + }, + site_multihit: { + type: String, + enum: ["Estimate", "Global"], + default: "Estimate", + }, }); FEL.add(AnalysisSchema); @@ -47,7 +57,7 @@ FEL.virtual("original_fn").get(function () { "/../../uploads/msa/" + this._id + "-original." + - this.original_extension + this.original_extension, ); }); @@ -123,7 +133,7 @@ FEL.statics.spawn = function (fn, options, callback) { } else { var move = Msa.removeTreeFromFile( fel_result.filepath, - fel_result.filepath + fel_result.filepath, ); move.then( (val) => { @@ -137,7 +147,7 @@ FEL.statics.spawn = function (fn, options, callback) { }, (reason) => { callback(err, "issue removing tree from file"); - } + }, ); } } diff --git a/app/models/meme.js b/app/models/meme.js index bad9ffcc..c66974d5 100644 --- a/app/models/meme.js +++ b/app/models/meme.js @@ -12,6 +12,10 @@ var MEME = mongoose.Schema({ results: Object, resample: Number, bootstrap: Boolean, + multiple_hits: String, + site_multihit: String, + rates: Number, + impute_states: String, }); MEME.add(AnalysisSchema); @@ -49,50 +53,41 @@ MEME.virtual("url").get(function () { return "http://" + setup.host + "/meme/" + this._id; }); -/** - * Shared API / Web request job spawn - */ + MEME.statics.spawn = function (fn, options, callback) { const Msa = mongoose.model("Msa"); - var meme = new this(); + const meme = new this(); - let gencodeid = options.gencodeid, - datatype = options.datatype; - - meme.mail = options.mail; + // Dynamically add all options to the meme object + Object.keys(options).forEach((key) => { + meme[key] = options[key]; + }); // Check advanced options - if (!_.isNaN(options.resample)) { - meme.resample = options.resample; - meme.bootstrap = true; - } else { - meme.bootstrap = false; - } + meme.bootstrap = !_.isNaN(options.resample); const connect_callback = function (data) { - if (data == "connected") { + if (data === "connected") { logger.log("connected"); } }; - Msa.parseFile(fn, datatype, gencodeid, (err, msa) => { + Msa.parseFile(fn, options.datatype, options.gencodeid, (err, msa) => { if (err) { callback(err); return; } + // Check if msa exceeds limitations if (msa.sites > meme.max_sites) { - const error = - "Site limit exceeded! Sites must be less than " + meme.max_sites; + const error = `Site limit exceeded! Sites must be less than ${meme.max_sites}`; logger.error(error); callback(error); return; } if (msa.sequences > meme.max_sequences) { - var error = - "Sequence limit exceeded! Sequences must be less than " + - meme.max_sequences; + const error = `Sequence limit exceeded! Sequences must be less than ${meme.max_sequences}`; logger.error(error); callback(error); return; @@ -108,32 +103,24 @@ MEME.statics.spawn = function (fn, options, callback) { return; } - function move_cb(err, result) { + function move_cb(err) { if (err) { logger.error( - "meme rename failed" + - " Errored on line 113~ within models/meme.js :: move_cb " + - err + `meme rename failed. Error on line 113~ within models/meme.js :: move_cb ${err}`, ); callback(err, null); } else { - var move = Msa.removeTreeFromFile( - meme_result.filepath, - meme_result.filepath - ); - move.then( - (val) => { - let to_send = meme; - to_send.upload_redirect_path = meme.upload_redirect_path; + Msa.removeTreeFromFile(meme_result.filepath, meme_result.filepath) + .then(() => { this.submitJob(meme_result, connect_callback); callback(null, meme); - }, - (reason) => { - res.json(500, { error: "issue removing tree from file" }); - } - ); + }) + .catch(() => { + callback(new Error("issue removing tree from file")); + }); } } + helpers.moveSafely(fn, meme_result.filepath, move_cb.bind(this)); }); }); diff --git a/app/models/msa.js b/app/models/msa.js index c3dfb6eb..a9cb7d86 100644 --- a/app/models/msa.js +++ b/app/models/msa.js @@ -65,12 +65,12 @@ Msa.virtual("genetic_code").get(function () { }); Msa.virtual("day_created_on").get(function () { - var time = moment(this.timestamp); + var time = moment.unix(this.timestamp); return time.format("YYYY-MMM-DD"); }); Msa.virtual("time_created_on").get(function () { - var time = moment(this.timestamp); + var time = moment.unix(this.timestamp); return time.format("HH:mm"); }); diff --git a/app/routes/fel.js b/app/routes/fel.js index cdd89c3f..1b8e2163 100644 --- a/app/routes/fel.js +++ b/app/routes/fel.js @@ -25,6 +25,7 @@ exports.uploadFile = function (req, res) { } }; + var fn = req.files.files.file, fel = new FEL(), postdata = req.body, @@ -33,9 +34,12 @@ exports.uploadFile = function (req, res) { ds_variation = postdata.ds_variation, resample = parseInt(postdata.resample); + fel.original_extension = path.basename(fn).split(".")[1]; fel.mail = postdata.mail; fel.ci = postdata.confidence_interval == "true"; + fel.multiple_hits = postdata.multiple_hits; + fel.site_multihit = postdata.site_multihit; // Check advanced options if (!_.isNaN(resample)) { @@ -87,7 +91,7 @@ exports.uploadFile = function (req, res) { } else { var move = Msa.removeTreeFromFile( fel_result.filepath, - fel_result.filepath + fel_result.filepath, ); move.then( (val) => { @@ -98,7 +102,7 @@ exports.uploadFile = function (req, res) { }, (reason) => { res.json(500, { error: "issue removing tree from file" }); - } + }, ); } } @@ -116,7 +120,7 @@ exports.uploadFile = function (req, res) { helpers.moveSafely( req.files.files.file, fel_result.filepath, - move_cb + move_cb, ); }); }); diff --git a/app/routes/meme.js b/app/routes/meme.js index a5958010..2ea13707 100644 --- a/app/routes/meme.js +++ b/app/routes/meme.js @@ -18,12 +18,16 @@ exports.form = function (req, res) { exports.invoke = function (req, res) { var fn = req.files.files.file; let postdata = req.body; - //let resample = parseInt(postdata.resample); let options = { datatype: 0, gencodeid: postdata.gencodeid, mail: postdata.mail, + multiple_hits: postdata.multiple_hits, + site_multihit: postdata.site_multihit, + rates: parseInt(postdata.rates), + resample: parseInt(postdata.resample || 0), + impute_states: postdata.impute_states, }; //// Check advanced options diff --git a/app/templates/fel/msa_form.ejs b/app/templates/fel/msa_form.ejs index 550d3e37..8d5eacac 100644 --- a/app/templates/fel/msa_form.ejs +++ b/app/templates/fel/msa_form.ejs @@ -88,6 +88,22 @@ +
+ + +
+ +
+ + +
has-error<% } %>"> diff --git a/app/templates/meme/form.ejs b/app/templates/meme/form.ejs index 420b0e2f..3bf8563e 100644 --- a/app/templates/meme/form.ejs +++ b/app/templates/meme/form.ejs @@ -1,7 +1,7 @@ <%- include("../includes/header.ejs") %>
<%- include("header.ejs") %> - <%- include("../partials/msa/form.ejs") %> + <%- include("./msa_form.ejs") %> <%- include("../includes/modal.ejs") %>
<%- include("../includes/footer.ejs") %> diff --git a/app/templates/meme/msa_form.ejs b/app/templates/meme/msa_form.ejs index 9723a403..bb7cc8e0 100644 --- a/app/templates/meme/msa_form.ejs +++ b/app/templates/meme/msa_form.ejs @@ -1,136 +1,131 @@ -<%- include("../includes/header.ejs") %> -
- <%- include("header.ejs") %> - - - -
- -
> - -
- - -
- -
- - -
- -
has-error<% } %>"> - - -
- -
-
-
-
- -
-
- -
-
-
-

[Advanced setting, will result in MUCH SLOWER run time] Perform parametric bootstrap resampling to derive site-level null LRT distributions up to this many replicates per site. Recommended use for small to medium (<30 sequences) datasets.

- - -
+ + +
+ + > + +
+ + -
-
+
+
+ +
+ +
-
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
has-error<% } %>"> + + +
+ + + + +
-<%- include("../includes/footer.ejs") %> +
+ + + diff --git a/public/assets/js/fel/msa_form.js b/public/assets/js/fel/msa_form.js index c19ec83c..cf5ae502 100644 --- a/public/assets/js/fel/msa_form.js +++ b/public/assets/js/fel/msa_form.js @@ -1,4 +1,24 @@ $(function () { + function toggleSiteMultihit() { + const multipleHitsValue = $("#multiple-hits").val(); + const siteMultihitDropdown = $("#site-multihit"); + + if (multipleHitsValue === "None") { + siteMultihitDropdown.prop("disabled", true); + siteMultihitDropdown.val("Estimate"); // Set to default value if disabled + } else { + siteMultihitDropdown.prop("disabled", false); + } + } + + // Initial check on page load + toggleSiteMultihit(); + + // Bind change event to multiple_hits dropdown + $("#multiple-hits").change(function () { + toggleSiteMultihit(); + }); + $("form").submit(function (e) { e.preventDefault(); @@ -13,6 +33,8 @@ $(function () { formData.append("gencodeid", $("select[name='gencodeid']").val()); formData.append("ds_variation", $("#ds-variation").val()); formData.append("resample", $("#resample").val()); + formData.append("multiple_hits", $("#multiple-hits").val()); + formData.append("site_multihit", $("#site-multihit").val()); formData.append( "receive_mail", diff --git a/public/assets/js/meme/form.js b/public/assets/js/meme/form.js new file mode 100644 index 00000000..51f56d96 --- /dev/null +++ b/public/assets/js/meme/form.js @@ -0,0 +1,114 @@ +$(function () { + function toggleSiteMultihit() { + const multipleHitsValue = $("#multiple-hits").val(); + const siteMultihitDropdown = $("#site-multihit"); + + if (multipleHitsValue === "None") { + siteMultihitDropdown.prop("disabled", true); + siteMultihitDropdown.val("Estimate"); // Default value when disabled + } else { + siteMultihitDropdown.prop("disabled", false); + } + } + + // Initialize dependent dropdown states on page load + toggleSiteMultihit(); + + // Handle changes in multiple hits selection + $("#multiple-hits").change(function () { + toggleSiteMultihit(); + }); + + // Handle form submission + $("form").submit(function (e) { + e.preventDefault(); + + $("#file-progress").removeClass("hidden"); + + const formData = new FormData(); + + // File validation + const fileInput = document.getElementById("seq-file"); + const file = fileInput.files[0]; + + if (!file) { + $("#modal-error-msg").text("Please select a file to upload."); + $("#errorModal").modal(); + return; + } + formData.append("files", file); + + // Gather and validate form data + formData.append("gencodeid", $("select[name='gencodeid']").val()); + formData.append("multiple_hits", $("#multiple-hits").val()); + formData.append("site_multihit", $("#site-multihit").val()); + formData.append("rates", $("#rates").val()); + formData.append("resample", $("#resample").val()); + formData.append("impute_states", $("#impute-states").val()); + + const email = $("input[name='mail']").val(); + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (email && !emailRegex.test(email)) { + $("#modal-error-msg").text("Please enter a valid email address."); + $("#errorModal").modal(); + return; + } + formData.append("mail", email); + + const actionUrl = $("#msa-form").attr("action"); + + // Create XMLHttpRequest + const xhr = new XMLHttpRequest(); + xhr.open("post", actionUrl, true); + + xhr.upload.onprogress = function (e) { + if (e.lengthComputable) { + const percentage = (e.loaded / e.total) * 100; + $(".progress .progress-bar").css("width", percentage + "%"); + } + }; + + xhr.onerror = function () { + $("#modal-error-msg").text("An error occurred during the upload."); + $("#errorModal").modal(); + }; + + xhr.onload = function () { + try { + const result = JSON.parse(this.responseText); + + if (result.error) { + $("#modal-error-msg").text(result.error); + $("#errorModal").modal(); + } else if (result.upload_redirect_path) { + window.location.href = result.upload_redirect_path; + } else { + $("#modal-error-msg").text( + "Unexpected response format from the server." + ); + $("#errorModal").modal(); + } + } catch (err) { + $("#modal-error-msg").text("Error parsing server response."); + $("#errorModal").modal(); + } finally { + $(".progress .progress-bar").css("width", "0%"); + $("#file-progress").addClass("hidden"); + } + }; + + xhr.send(formData); + }); + + // Email validation on input + $("input[name='mail']").on("input", function () { + const email = $(this).val(); + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + + if (email && !emailRegex.test(email)) { + $(this).addClass("is-invalid"); + } else { + $(this).removeClass("is-invalid"); + } + }); +});