275dbe07f66da071cc6ab506054b76be0cf3519b — Phil Hagelberg 11 days ago 084995e
Document survey code.
5 files changed, 41 insertions(+), 6 deletions(-)

A survey/README.md
M survey/chart.fnl
M survey/save.fnl
M survey/summary.fnl
M survey/survey.fnl
A survey/README.md => survey/README.md +15 -0
@@ 0,0 1,15 @@
# Fennel Programming Language Survey

This code powers the (hopefully yearly) survey where we ask Fennel
programmers what's up!

Rather than a typical database-backed web application, this is done
using almost exclusively static HTML; the one exception is an 7-line
CGI script which writes responses to disk. The `survey.fnl` code
creates a page with a form on it based on the questions in
`questions.fnl`. The form sends its contents to the CGI script. The
responses are then rsync'd back to the local checkout and
`summary.fnl` generates static HTML summarizing them, including SVG
charts from `chart.fnl`, along with some plain-text commentary in the
`commentary/` directory. Then that output is uploaded back to the web

M survey/chart.fnl => survey/chart.fnl +3 -2
@@ 1,5 1,3 @@
(local fennel (require :fennel))

(fn bar-rect [answer count i]
  (let [width (* count 10)
        y (* 21 (- i 1))]

@@ 9,6 7,9 @@
      (.. answer " (" count ")")]]))

(fn bar [i data ?sorter]
  ;; by default, sort in descending order of count of responses, but
  ;; allow sorting to be overridden, for example with the age question
  ;; the answers should be ordered by the age, not response count.
  (fn count-sorter [k1 k2]
    (let [v1 (. data k1) v2 (. data k2)]
      (if (= v1 v2) (< k1 k2) (< v2 v1))))

M survey/save.fnl => survey/save.fnl +4 -0
@@ 1,5 1,9 @@
;; This is the only piece of code which runs server-side.
;; It takes HTTP POST data on stdin and saves it to disk for later analysis.
(error "Survey is closed!")
(let [contents (io.read "*all")
      ;; we could use os.time but it only has second-level resolution which
      ;; means there is a very slight chance of conflict for two requests.
      date (io.popen "date --rfc-3339=ns")
      id (: (date:read "*a") :sub 1 -2)]
  (with-open [raw (io.open (.. "responses/" id ".raw") :w)]

M survey/summary.fnl => survey/summary.fnl +9 -0
@@ 1,3 1,6 @@
;; this file spits out the HTML file summarizing the results using a
;; combination of SVG bar graphs and commentary text. it reads the results
;; written by the save.fnl file in raw HTTP form encoded format.
(local html (require :html))
(local foot (require :foot))
(local questions (require :questions))

@@ 32,6 35,7 @@
          n (tset sums v (+ n 1)))))

;; workaround for not having :into yet; see comment in survey.fnl
(fn prepend [tbl pre]
  (while (. pre 1)
    (table.insert tbl 1 (table.remove pre))))

@@ 70,6 74,7 @@
  (match response
    [r] [(or (. aliases r) r)]))

;; turn URLs in parens into [:a {:href "https://..."} "https://..."] tables
(fn link [text]
  (var last 1)
  (let [out []]

@@ 91,6 96,8 @@
                          [:p {:class "commentary quote"} (link (p:sub 3))]
                          [:p {:class "commentary"} (link p)])))))

;; almost all the question types will get a bar graph, but sometimes
;; the less quantitative data can't be summarized this way.
(fn html-for [i q]
  [:div {:class :question}
   [(if (= q.type :section) :h2 :h4) {} (if (not= :submit q.type) q.question)]

@@ 112,6 119,8 @@
     :textarea [:span {}])
   (commentary-for i)])

;; by default we emit the HTML summary but we can also display different
;; forms of summaries of the data to help write the commentary.
(match ...
  "--view" (print ((require :fennel.view) responses))
  "--others" (print ((require :fennel.view) (others questions)))

M survey/survey.fnl => survey/survey.fnl +10 -4
@@ 3,10 3,17 @@
(local questions (require :questions))
(local year 2021)

;; this function is a tacky workaround for the fact that fennel-lang.org is
;; generated using the current stable (at the time of this writing, 0.10.0)
;; version of fennel which does not have the :into clause for
;; icollect. replace it with :into once the next version is released!
(fn prepend [tbl pre]
  (while (. pre 1)
    (table.insert tbl 1 (table.remove pre))))

;; each type of question gets its own HTML representation. section headers are
;; "questions" for the purposes of the data even though they don't have answers.
;; or questions.
(fn section-html [{: question}]
  [:h2 {} question])

@@ 16,9 23,9 @@
   (doto (icollect [ia answer (ipairs answers)]
           (let [id (.. i "-" ia)]
             [:li {:class :answer}
              ;; every checkbox gets an "other" text input
              (if (= "Other" answer)
                  [:input {:type :text :name (.. id "-other")
                           :placeholder "Other"}]
                  [:input {:type :text :name (.. i "-other") :placeholder "Other"}]
                  [:input {:type :checkbox :value answer :id id :name question}])
              (if (not= "Other" answer)
                  [:label {:for id} answer])]))

@@ 34,14 41,13 @@
   [:label {:for i} question]
   [:input {:name question :type :text}]])

;; again, not a question, but represented as such.
(fn submit-html [{: question}]
  [:input {:type :submit :value question}])

(local out [:html {:lang "en"}
            [:head {}
             [:meta {:charset "UTF-8"}]
             ;; [:script {:src "/fengari-web.js"}]
             ;; [:script {:type "application/lua" :src "/init.lua" :async true}]
             [:link {:rel "stylesheet" :href "/fennel.css"}]
             [:link {:rel "stylesheet"
                     :href "https://code.cdn.mozilla.net/fonts/fira.css"}]