Skip to content

Instantly share code, notes, and snippets.

@pradyunsg
Created August 26, 2020 12:20
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pradyunsg/91060eb4e6b9f424f78b5b990ef466fb to your computer and use it in GitHub Desktop.
Save pradyunsg/91060eb4e6b9f424f78b5b990ef466fb to your computer and use it in GitHub Desktop.
Nicer LimeSurvey form UI

USAGE

  1. Open the "[LimeSurvey]/survey/index.php?r=admin/responses/sa/index/surveyid/[survey-id]".
  2. Get the HTML of the table element containing the responses into a file.
    • Right click the first response, Inspect Element
    • Select "<table class="table-striped table">" in the new window/pane that opened.
    • Copy the HTML for that element (w/ ctrl+c or cmd+c)
    • Paste this into a text editor and save this file (anything works, I'm assuming it's surveys.html).
  3. Install jinja2 and BeautifulSoup4. (pip install jinja2 beautifulsoup4)
  4. Run python nicer-survey-ui.py surveys.html.
  5. Open the generated out.html file in any reasonably modern browser.
  6. Go through the responses in a nicer UI than LimeSurvey. :)
import sys
from pathlib import Path
import jinja2
from bs4 import BeautifulSoup
here = Path(__file__).parent
file = Path(sys.argv[1])
soup = BeautifulSoup(file.read_text(), "lxml")
BREAK_AT = 11 # Initial non response entries in the survey.
def _get_question(heading_cell):
tag = heading_cell.div
if not tag:
return (heading_cell.text, None)
return (tag.attrs["data-content"], tag.attrs["data-original-title"])
heading_cells = soup.find("thead").find_all("th")
questions = [_get_question(cell) for cell in heading_cells[BREAK_AT:]]
entries = []
for row in soup.find("tbody").find_all("tr"):
cells = row.find_all("td")
responses = [tag.text for tag in cells[BREAK_AT:]]
meta = {}
meta["Answers Given"] = str(len(list(filter(None, responses))))
for key_cell, value_cell in zip(heading_cells[:BREAK_AT], cells[:BREAK_AT]):
key = _get_question(key_cell)[0]
if not key:
continue
if key == "completed":
value = "YES" if "text-success" in str(value_cell) else "NO"
else:
value = value_cell.text
meta[key] = value
entries.append({"responses": responses, "meta": meta})
jt = jinja2.Template((here / "template.jinja-html").read_text())
text = jt.render(questions=questions, entries=entries)
(here / "out.html").write_text(text)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LimeSurvey Rendered</title>
<script>
var entries = {{ entries| tojson }};
var current_index = 0;
// Use the id from the heading if possible.
var urlParams = new URLSearchParams(window.location.search);
var id_given = urlParams.get("id");
if (id_given != null) {
for (let i = 0; i < entries.length; i++) {
if (entries[i].meta.id == id_given) {
current_index = i;
break;
}
}
}
// Helpers
function updateQueryStringParameter(uri, key, value) {
var re = new RegExp("([?&])" + key + "=.*?(&|$)", "i");
var separator = uri.indexOf('?') !== -1 ? "&" : "?";
if (uri.match(re)) {
return uri.replace(re, '$1' + key + "=" + value + '$2');
}
else {
return uri + separator + key + "=" + value;
}
}
function hasClass(elem, className) {
return new RegExp(' ' + className + ' ').test(' ' + elem.className + ' ');
}
function addClass(elem, className) {
if (!hasClass(elem, className)) {
elem.className += ' ' + className;
}
}
function removeClass(elem, className) {
var newClass = ' ' + elem.className.replace(/[\t\r\n]/g, ' ') + ' ';
if (hasClass(elem, className)) {
while (newClass.indexOf(' ' + className + ' ') >= 0) {
newClass = newClass.replace(' ' + className + ' ', ' ');
}
elem.className = newClass.replace(/^\s+|\s+$/g, '');
}
}
function escapeHtml(unsafe) {
return unsafe
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
// Actual logic
function updateContent() {
let entry = entries[current_index];
let responses = entry["responses"];
let meta = entry["meta"];
let response_id = meta["id"];
let completed = false;
let response_count_elem = document.getElementById("response-count");
let meta_elem = document.getElementById("meta");
let prev_elem = document.getElementById("prev");
let next_elem = document.getElementById("next");
// Fill the "meta" information about the user.
response_count_elem.innerHTML = response_id;
let metaHTML = "";
for (const [key, value] of Object.entries(meta)) {
if (!key.trim()) continue;
if (key == "completed") {
completed = value == "YES"
}
metaHTML += (
"<tr class='meta-entry'>" +
"<td class='meta-key'>" + key + "</td>" +
"<td class='meta-value'>" + escapeHtml(value) + "</td>" +
"</tr>"
);
}
meta_elem.innerHTML = metaHTML;
if (completed) {
addClass(document.body, "completed");
} else {
removeClass(document.body, "completed");
}
// Enable / Disable the navigation
if (current_index == 0) {
prev_elem.disabled = true;
next_elem.disabled = false;
} else if (current_index == (entries.length - 1)) {
prev_elem.disabled = false;
next_elem.disabled = true;
} else {
next_elem.disabled = prev_elem.disabled = false;
}
// Fill the responses into the document
for (let i = 0; i < responses.length; i++) {
let item = responses[i];
let element = document.getElementById('response-' + i);
element.innerHTML = escapeHtml(item) || "<i class='nothing'></i>";
}
let newurl = (
window.location.protocol + "//" +
window.location.host + window.location.pathname +
"?id=" + response_id
);
window.history.pushState({path: newurl}, '', newurl);
}
function next() {
current_index += 1;
updateContent();
}
function prev() {
current_index -= 1;
updateContent();
}
</script>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, avenir next, avenir,
helvetica neue, helvetica, Ubuntu, roboto, noto, segoe ui,
arial, sans-serif;
margin: 0;
line-height: 1.4;
background: #fff0f0;
}
body.completed {
background: #f0fff0;
}
.container {
margin: 1rem auto;
padding: 1rem;
max-width: 80%;
width: 45rem;
background: white;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
}
button {
appearance: none;
background: #fff;
color: royalblue;
border: 1px solid CornflowerBlue;
border-radius: 2px;
cursor: pointer;
display: inline-block;
font-size: .8rem;
height: 1.8rem;
line-height: 1.2rem;
outline: 0;
padding: .25rem .4rem;
text-align: center;
text-decoration: none;
user-select: none;
vertical-align: middle;
white-space: nowrap;
}
button:disabled {
opacity: 0.5;
cursor: initial;
}
.float-right {
float: right;
}
.item {
padding: 1rem 1rem;
}
.item-question {
font-weight: 500;
margin-bottom: 0.25rem;
}
.item-response {
font-weight: 300;
white-space: pre-wrap;
overflow-x: auto;
}
/* Indent, and look like a supporting comment. */
.item--comment {
padding: 0;
}
.item--comment .item-response {
padding: 0 2rem;
}
.item--comment .item-question {
display: none;
}
/* Hide comments if there's none */
.item--comment .item-response .nothing {
display: none;
}
#meta {
background: white;
white-space: nowrap;
border-collapse: collapse;
}
#meta-wrapper {
margin: 1rem 0.5rem;
padding: 0.5rem;
overflow-x: auto;
}
.container,
#meta-wrapper {
/* Pretty */
border-radius: 4px;
box-shadow: 0 0.2rem 0.5rem rgba(0, 0, 0, 0.05), 0 0 1px rgba(0, 0, 0, 0.1);
}
.nothing::before {
display: block;
content: "no response.";
font-size: 0.75rem;
color: red;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<button id="prev" onClick="prev()">prev</button>
<div>Response <span id="response-count">Either JS crashed or you have it disabled.</span></div>
<button id="next" onClick="next()">next</button>
</div>
<div id="meta-wrapper">
<table id="meta">
</table>
</div>
<div class="items">
{% for question in questions %}
<div class="item{% if question[1].endswith("_comment") %} item--comment{% endif %}">
<div class="item-question">{{ question[0] }}</div>
<div class="item-response" id="response-{{ loop.index - 1 }}">
Enable javascript to see answers.
</div>
</div>
{%- endfor %}
</div>
</div>
<script>
updateContent();
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment