Skip to content

Commit

Permalink
Built out functionality. Still lacking validation
Browse files Browse the repository at this point in the history
  • Loading branch information
aanunez committed Oct 12, 2022
1 parent 29ea039 commit e4c7f06
Show file tree
Hide file tree
Showing 3 changed files with 239 additions and 25 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.vscode
113 changes: 105 additions & 8 deletions AddValidationTypes.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,37 +9,134 @@ class AddValidationTypes extends AbstractExternalModule
{
public function redcap_control_center()
{
// Custom Config page
if ($this->isPage('ControlCenter/validation_type_setup.php')) {
$settings = json_encode($this->loadSettings());
echo "<script>ExternalModules.addValTypes = {$settings}</script>";
echo "<style>#val_table { display:none; }</style>";
echo "<script src={$this->getUrl("main.js")}></script>";
}
}

public function process()
{
global $Proj; // Can we remove this?
$request = RestUtility::processRequest(false);
$params = $request->getRequestVars();

// Run core code
$result = ["error" => "No valid action"];
$result = ["errors" => ["No valid action"]];
if ($params["action"] == "add") {
$this->addType($params["display"], $params["internal"], $params["php"], $params["js"]);
$result = $this->addType($params["display"], $params["internal"], $params["php"], $params["js"], $params["dataType"]);
} elseif ($params["action"] == "remove") {
$this->removeType($params["internal"]);
$result = $this->removeType($params["internal"]);
}
return json_encode($result);
}

private function addType($display, $internal, $phpRegex, $jsRegex)
private function loadSettings()
{
return [
"validationTypes" => $this->emValidationTypes(),
"dataTypes" => $this->allDataTypes(),
"csrf" => $this->getCSRFToken(),
"router" => $this->getUrl('router.php')
];
}

private function emValidationTypes()
{
return array_filter(array_map('trim', explode(",", $this->getSystemSetting('typesAdded'))));
}

private function allValidationTypes()
{
$result = [];
$sql = $this->query("SELECT * FROM redcap_validation_types", []);
while ($row = $sql->fetch_assoc()) {
$result[$row["validation_name"]] = [
"internal" => $row["validation_name"],
"display" => $row["validation_label"],
"phpRegex" => $row["regex_js"],
"jsRegex" => $row["regex_php"],
"dataType" => $row["data_type"],
"visible" => $row["visible"] == 1

];
}
return $result;
}

private function allDataTypes()
{
$sql = $this->query("SHOW COLUMNS FROM redcap_validation_types LIKE 'data_type'", []);
$enum = $sql->fetch_assoc()["Type"];
preg_match('/enum\((.*)\)$/', $enum, $matches);
return array_map(function ($value) {
return trim($value, "'");
}, explode(',', $matches[1]));
}

private function addType($display, $internal, $phpRegex, $jsRegex, $dataType)
{
// Validate everything, make sure name is unique
$errors = [];
// Display Name (alpha, numeric, space, limited special chars ()-:/.)
if (!preg_match("^[a-zA-Z0-9 ()-:/.]+$", $display)) {
$errors[] = "Incorrectly formatted display name";
}

// Internal Name (lower alpha, undersocre, numeric)
if (!preg_match("^[a-z0-9_ ]+$", $internal)) {
$errors[] = "Incorrectly formatted internal name";
}

// TODO PHP Regex (Make sure that \ is escaped)
// We can validate the PHP regex via var_dump(preg_match('~Valid(Regular)Expression~', '') === false);
// TODO JS Regex (Make sure that \ is escaped)

// Make sure that display name isn't in use
$allTypes = $this->allValidationTypes();
$displayNames = array_map(function ($key) use ($allTypes) {
return $allTypes[$key]["display"];
}, $allTypes);
if (in_array($display, $displayNames)) {
$errors[] = "Display name too similar to existing name";
}

// Make sure that internal name isn't in use
if (in_array($internal, array_keys($allTypes))) {
$errors[] = "Internal name is already in use";
}

// Make sure the data type is real
if (!in_array($dataType, $this->allDataTypes())) {
$errors[] = "Invalid data type";
}

// Perform the DB Update and update the EM's setting
if (count($errors) == 0) {
// TODO do the db update
// INSERT INTO redcap_validation_types (validation_name, validation_label, regex_js, regex_php, data_type, legacy_value, visible)
// VALUES (?, ?, ?, ?, ?, NULL, 0);
$types = $this->emValidationTypes();
$types[] = $internal;
$this->setSystemSetting("typesAdded", implode(",", $types));
}

return ["errors" => $errors];
}

private function removeType($name)
{
// We can remove only those types that we added
$types = $this->emValidationTypes();
if (!in_array($name, $types)) {
return ["errors" => ["Bad validation type or type was not added by EM"]];
}
$sql = "
DELETE from redcap_validation_types
WHERE validation_name = ?";
$this->query($sql, [$name]);
$types = array_diff($types, [$name]);
$this->setSystemSetting('typesAdded', implode(",", $types));
return ["errors" => []];
}
}
150 changes: 133 additions & 17 deletions main.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,31 @@
$(document).ready(() => {
(() => {

let html = `
// Small amount of css just for the case sensative toggle
const css = `
.custom-switch.custom-switch-lg {
padding-bottom: 1rem;
padding-left: 2.25rem;
}
.custom-switch.custom-switch-lg .custom-control-label {
padding-left: 0.75rem;
padding-top: 0.15rem;
}
.custom-switch.custom-switch-lg .custom-control-label::before {
border-radius: 1rem;
height: 1.5rem;
width: 2.5rem;
}
.custom-switch.custom-switch-lg .custom-control-label::after {
border-radius: 0.65rem;
height: calc(1.5rem - 4px);
width: calc(1.5rem - 4px);
}
.custom-switch.custom-switch-lg .custom-control-input:checked ~ .custom-control-label::after {
transform: translateX(1rem);
}`

// Form to add new validation types
const html = `
<div id="add_validation_form" class="p-4 border rounded mb-4">
<div class="form-group">
<label class="font-weight-bold mb-0" for="displayName">Display Name</label>
Expand Down Expand Up @@ -35,34 +60,125 @@ $(document).ready(() => {
</div>
</div>
<div class="form-group row mb-0">
<label class="font-weight-bold col-3 mb-0">Case Sensative?</label>
<div class="col-7">
<div class="custom-control custom-checkbox custom-control-inline">
<input name="caseSensative" id="caseSensative_0" type="checkbox" class="custom-control-input" value="">
<label for="caseSensative_0" class="custom-control-label"></label>
<div class="col-4">
<label class="font-weight-bold mb-0" for="dataType">Data Type</label>
<div>
<select id="dataType" name="dataType" class="custom-select">
</select>
</div>
</div>
<div class="col-4">
<label class="font-weight-bold mb-0" for="caseSensative">Case Sensative?</label>
<div class="custom-control custom-switch custom-switch-lg">
<input name="caseSensative" id="caseSensative" type="checkbox" class="custom-control-input" value="">
<label for="caseSensative" class="custom-control-label"></label>
</div>
</div>
<div class="col-2 text-right">
<button name="submit" type="submit" class="btn btn-primary">Add</button>
<div class="col-4 text-right">
<button id="validationAdd" class="btn btn-primary">Add</button>
</div>
</div>
</div>`

const getForm = () => {
// Grab all used values
const display = $("#displayName").val()
const internal = $("#internalName").val()
let phpRegex = $("#phpRegex").val()
let jsRegex = $("#jsRegex").val()
const dataType = $("#dataType").val()
const caseSensative = $("#caseSensative").is(":checked")

if (!display || !internal || !phpRegex || !jsRegex) return false

// TODO check each validation

phpRegex = `/^${phpRegex}$/`
jsRegex = `/^${jsRegex}$/`

if (!caseSensative) {
phpRegex += "i"
jsRegex += "i"
}

return {
display, internal, phpRegex, jsRegex, dataType
}
}

// Insert the new form and style the old table
$("head").append(`<style>${css}</style>`)
$("#val_table tr td").first().attr("colspan", "4")
$("#val_table tr:not(:first)").append(`
<td class="data2" style="text-align:center;font-size:13px"><a><i class="fa-solid fa-trash-can hidden"></i></a></td>
<td class="data2" style="text-align:center;font-size:13px">
<a class="validationRemove hidden">
<i class="fa-solid fa-trash-can"></i>
</a>
</td>
`)
$("#val_table").before(html)
$("#val_table").before(html).show()
ExternalModules.addValTypes.dataTypes.forEach((el) => $("#dataType").append(new Option(el)))
$("#dataType").val("text") // Default

// Setup form. Validate on server and client
// Remove hidden class on rows that were added by EM
ExternalModules.addValTypes.validationTypes.forEach((el) => $(`#${el} a`).removeClass('hidden'))

// Validations TODO
// Display Name (alpha, numeric, space, limited special chars ()-:/.)
// Internal Name (lower alpha, undersocre, numeric)
// Internal Name (lower alpha, undersocre, numeric)
// PHP Regex (Make sure that \ is escaped)
// JS Regex (Make sure that \ is escaped)

// Make sure that display name isn't in use
// Make sure that internal name isn't in use
// Track which internal names we have added so we know what can be reomved
// Update the table on screen on add

});
// Setup Add button on new form
$("#validationAdd").on("click", () => {
const settings = getForm();
if (!settings) return;
$("#validationAdd").prop("disabled", true)
$.ajax({
method: 'POST',
url: ExternalModules.addValTypes.router,
data: {
...settings,
action: 'add',
redcap_csrf_token: ExternalModules.addValTypes.csrf
},
// Only occurs on network or technical issue
error: (jqXHR, textStatus, errorThrown) => console.log(`${JSON.stringify(jqXHR)}\n${textStatus}\n${errorThrown}`),
// Response returned from server (possible 500 error still)
success: (data) => {
console.log(data);
if ((typeof data == "string" && data.length === 0) || data.errors.length) {
// TODO show an error
}
location.reload()
}
})
})

// Setup old table Interactivity (delete)
$("#val_table").on("click", ".validationRemove", (el) => {
const $el = $(el.currentTarget)
const $row = $el.closest('tr')
const name = $row.prop("id")
if (!$el.is(":visible") || name == "") return
$.ajax({
method: 'POST',
url: ExternalModules.addValTypes.router,
data: {
name: name,
action: 'remove',
redcap_csrf_token: ExternalModules.addValTypes.csrf
},
// Only occurs on network or technical issue
error: (jqXHR, textStatus, errorThrown) => console.log(`${JSON.stringify(jqXHR)}\n${textStatus}\n${errorThrown}`),
// Response returned from server (possible 500 error still)
success: (data) => {
console.log(data);
if ((typeof data == "string" && data.length === 0) || data.errors.length) return;
$row.remove()
}
})
})
})();

0 comments on commit e4c7f06

Please sign in to comment.