Logo for the UKRI Digital Research Skills Catalyst Logo for the UKRI Digital Research Skills Catalyst
  • Home
  • Consult an Expert
  • News & Events
  • About
  • Contact

Welcome to the UKRI Digital Research Skills Catalyst

Accelerate your research with digital skills - discover 50+ learning resources, events, and expert-led training.

Code
# This cell loads the CSV data directly from the Google sheet via its published CSV-specific URL.

# We need to load in Python, as OJS doesn't have a caching system. Once loaded, we "publish" the variable globally to OJS.
# This block is run in Python as we need Pandas for the necessary caching functionality.
import pandas as pd
course_data_cached = pd.read_csv("https://docs.google.com/spreadsheets/d/e/2PACX-1vRSrvsfFfVwokza_WP9JIzd4Wfg6OKPBJcwelLTqYn1SgigZXnfcU6_apN5gWTMF79n4CRQFNOJ5w6M/pub?gid=1012757406&single=true&output=csv")
ojs_define(course_data_cached = course_data_cached)
Code
// This cell transposes the Python-loaded data and populates the `time_hours` field.

// We loaded the data from Google sheets via Python Pandas to allow for caching.
// This cell transposes the data presented from Python, as numpy & OJS expect data in opposite order.
// We do this in OJS to make conversion easier.
// We also convert the "XX hours" / "XX days" `TimeRequired` field to a more useful `XX` hours `time_hours` field.
course_data = transpose(course_data_cached).map(row => {
  const time_match = String(row.TimeRequired).match(/([\d\.]+)\s*([a-z]+)/i)
  let time_hours = null;
  if (time_match) {
    const time_amount = parseFloat(time_match[1]);
    const time_unit = time_match[2].toLowerCase();
    if (time_unit.startsWith("day")) {
      time_hours = time_amount * 24;
    } else if (time_unit.startsWith("hours")) {
      time_hours = time_amount;
    }
  }
  return {
    ...row,
    time_hours: time_hours
  };
})
Code
function csv_column_set(ds, col, inject_null) {
  let all_values = ds.flatMap(d =>
    d[col]
      ? d[col].split(",").map(s => s.trim())
      : []
  );
  all_values = Array.from(new Set(all_values)).sort()
  if(inject_null) {
    all_values.unshift(null)
  }
  return all_values
}
audiences = csv_column_set(course_data, "audience", true);
resource_types = csv_column_set(course_data, "learningResourceType", true);
providers = csv_column_set(course_data, "provider", true);
times = [null, "1–2 hours", "3–8 hours", "9–23 hours", "≥24 hours"];
Code
// This cell trims the search query and makes it lowercase.
search_query = (search_terms ?? "").toLowerCase().trim()
Code
// This cell contains the main search bar and both buttons.

viewof search_terms = Inputs.text({placeholder: "Search Training Resources..."})
Code
// Create button container
{
  const buttonContainer = document.createElement("div");
  buttonContainer.className = "button-container";
  
  // Create Show All button
  const showAllBtn = document.createElement("button");
  showAllBtn.textContent = "show all";
  showAllBtn.className = "primary-search-button";
  showAllBtn.addEventListener("click", () => {
    viewof search_terms.value = "*";
    viewof search_terms.dispatchEvent(new Event("input", {bubbles: true}));
    viewof reset_filters.value += 1;
    viewof reset_filters.dispatchEvent(new Event("input", {bubbles: true}));
  });
  
  // Create Show Filters button
  const toggleLink = document.createElement("a");
  toggleLink.id = "toggle-filters";
  toggleLink.href = "#";
  toggleLink.textContent = "show filters";
  toggleLink.className = "primary-search-button";
  toggleLink.addEventListener("click", (event) => {
    event.preventDefault();
    const filter_div = document.getElementById("search-filters");
    if (filter_div.style.display === "none") {
      toggleLink.textContent = "hide filters";
      filter_div.style.display = "block"
    } else {
      toggleLink.textContent = "show filters";
      filter_div.style.display = "none"
    }
  });
  
  buttonContainer.appendChild(showAllBtn);
  buttonContainer.appendChild(toggleLink);
  
  return buttonContainer;
}
Code
function is_any_filtering() {
  return (
    selected_audience ||
    selected_resource_type ||
    selected_provider ||
    selected_time ||
    search_query
  )
}

// A function that filters the course_data rows by the active (ie non-null) search dropdowns:
function filter_course_row_by_filters(data_row) {
  // 1: Check the "simple" selection dropdown options:
  const audience_match = !selected_audience || String(data_row.audience).includes(selected_audience);
  const resource_type_match = !selected_resource_type || String(data_row.learningResourceType).includes(selected_resource_type);
  const provider_match = !selected_provider || String(data_row.provider).includes(selected_provider);
  // 2: Check that the row has a time_hours value in range:
  // NB: This section requires that the selected_time values are in a specifiec order (set by the value of `times`)!
  //     If you change the values for the times, you must update this selection.
  let time_match = true;
  if (selected_time === times[1]) {
    // Selected 1–2 hours
    time_match = data_row.time_hours >= 1 && data_row.time_hours <= 2;
  } else if (selected_time === times[2]) {
    // Selected "3–8 hours"
    time_match = data_row.time_hours >= 3 && data_row.time_hours <= 8;
  } else if (selected_time === times[3]) {
    // Selected "9–23 hours"
    time_match = data_row.time_hours >= 9 && data_row.time_hours <= 23;
  } else if (selected_time === times[4]) {
    // Selected "≥24 hours"
    time_match = data_row.time_hours >= 24;
  }
  // 3: Return rows that match all filters:
  return (
    audience_match &&
    resource_type_match &&
    provider_match &&
    time_match
  )
}

// A function that filters course_data rows by the main search bar:
// This has 2 possibilities:
// 1: Search bar is empty OR "*" => Return everything
// 2: Search bar has text => Return rows matching text
function filter_course_row_by_search(data_row) {
  // Return everything if search_query is "*" or blank:
  if (
    (search_query === "*") ||
    (!search_query) ||
    (search_query === "")
  ) {
    return true;
  }
  // Otherwise, search in headline, description, funding and identifier:
  return (
    (data_row.headline && data_row.headline.toLowerCase().includes(search_query)) ||
    (data_row.description && data_row.description.toLowerCase().includes(search_query)) ||
    (data_row.projectFunding && data_row.projectFunding.toLowerCase().includes(search_query)) ||
    (data_row.identifier && data_row.identifier.toLowerCase().includes(search_query))
  );
}

// A function that first checks if there is no filtering set at all (and returns featured rows)
// or returns matching rows
function filter_course_row(data_row) {
  // If everything is blank, then we show the featured rows only:
  if (!is_any_filtering()) {
    return (
      data_row.featuredText != null &&
      data_row.featuredText !== ""
    );
  }
  // Otherwise, filter as normal:
  return (
    filter_course_row_by_filters(data_row) &&
    filter_course_row_by_search(data_row)
  )
}

// Filter the commplete data:
course_filtered = course_data.filter(filter_course_row)
Code
viewof selected_audience = Inputs.select(audiences, {label: "Audience", value: null})
viewof selected_resource_type = Inputs.select(resource_types, {label: "Type", value: null})
viewof selected_time = Inputs.select(times, {label: "Time Required", value: null})
viewof selected_provider = Inputs.select(providers, {label: "Provider", value: null})
// The reset filters button allows simple resetting to null:
viewof reset_filters = Inputs.button("reset filters")
Code
// This cell iterates through the the filtered course results, and builds the displayed output.

// If there are no selected results, we show a placeholder <div>.
// All filtered courses are displayed, sorted first by `featured` status then in the CSV file order.
{
  const search_results = document.getElementById("primary-search-results");
  const empty_results = document.getElementById("empty-primary-search-results");
  if (course_filtered.length === 0) {
    return html`
      <div class="course-data-empty">
        <p>No results to display</p>
      </div>
    `
  } else {
    return html`
      ${course_filtered.slice().sort((a, b) => {
        if(a.featuredText != "" && b.featuredText === "") return -1;
        if(a.featured === "" && b.featured != "") return 1;
        return 0;
      }).map((row, row_index) => html `
        <div class="course-data" id="course_data_${row_index}" ${row.featuredText ? `featured_text="${row.featuredText}"` : ""}>
          <h3>
              <a href=${row.url} target="_blank" rel="noopener noreferrer">${row.headline}</a>
          </h3>
          <p>${row.description}</p>
          <input type="checkbox" class="toggle-state" id="toggle-state-${row_index}">
          <label for="toggle-state-${row_index}" class="toggle-button"></label>
          <div class="course-data-extras" id="course_data_extras_${row_index}">
            <table>
              <tr>
                <th scope="row">Pre-requisites</th>
                <td class="pre-reqs">${row.competencyRequired?.split(/,(?=(?:(?:[^"]*"){2})*[^"]*$)/).map(prereq => html`<p>${prereq.trim().replace(/^"|"$/g, "")}</p>`)}</td>
              </tr>
              <tr>
                <th scope="row">Teaches</th>
                <td class="teaches">${row.teaches?.split(/,(?=(?:(?:[^"]*"){2})*[^"]*$)/).map(teach_p => html`<p>${teach_p.trim().replace(/^"|"$/g, "")}</p>`)}</td>
              </tr>
              <tr>
                <th scope="row">Time required</th>
                <td class="required">${row.TimeRequired}</td>
              </tr>
              <tr>
                <th scope="row">Credit</th>
                <td class="credit">${md`${row.creditText}`}</td>
              </tr>
              <tr>
                <th scope="row">Provider</th>
                <td>${row.projectFunding}</td>
              </tr>
            </table>
          </div>
        </div>
    `)}`
  }
}
Code
// This cell fires whenever the "reset filters" button is pressed.
{
  reset_filters; // We need this line to trigger on an update to the "reset filters" button.
  function resetInput(view, value) {
    const el = view.querySelector("select, input");
    if (!el) return;
    el.value = value;
    view.value = value;
    el.dispatchEvent(new Event("input", { bubbles: true }));
  }
  resetInput(viewof selected_audience, null);
  resetInput(viewof selected_resource_type, null);
  resetInput(viewof selected_time, null);
  resetInput(viewof selected_provider, null);
}
Code
// This cell displays a table of all `course_data` for debugging only.
Inputs.table(course_data)

Funded by the UKRI

  • contact

Cookie Preferences