     (go
       (swap! app-state #(assoc % :loading? true))
       (let [query (render-template (<! query-channel) :data data)]
-        ;(.log js/console query)
-        (>! sparql-results (<! (sparql-query-channel query))))
+        ; I wonder why cljs.pipe/pipe doesn't block.
+        (>! sparql-results (<! (sparql-query-channel sparql-endpoint query))))
       (swap! app-state #(assoc % :loading? false)))
     sparql-results))
 
-(defn sparql-autocomplete
-  "Autocomplete from SPARQL endpoint.
-  request: jQuery UI autocomplete's request object
-  response: channel to put the response to"
-  [request response]
-  (let [fragment (lower-case (.-term request))
+(def load-labels
+  (let [{{classes :default} :classes
+         :keys [page-size endpoint]} (get-in config [:data :sparql])
+        render-fn (fn [offset] (render-template load-labels-template
+                                                :data {:classes classes
+                                                       :limit page-size
+                                                       :offset offset}))
+        query-fn (fn [offset] (sparql-query-channel endpoint (render-fn offset)))
         sparql-results (sparql-results-channel)
-        query (render-template autocomplete-template
-                               :data {:classes (get-in config [:data :sparql :classes :default])
-                                      :fragment fragment})
-        sparql-query (sparql-query-channel query)]
-    (go (let [results (map :label (<! (wrap-load sparql-query sparql-results)))]
-          (response (clj->js results))))))
-
-(defn wikipedia-autocomplete
-  "Autocomplete from Wikipedia API.
-  request: jQuery UI autocomplete's request object
-  response: channel to put the response to"
-  [request response]
-  (let [fragment (lower-case (.-term request))
-        query (http/jsonp "http://cs.wikipedia.org/w/api.php"
-                          {:query-params {:action "opensearch"
-                                          :format "json"
-                                          :search fragment}})]
-    (go (-> query <! :body second clj->js response))))
+        results (atom [])]
+    (fn []
+      (go-loop [offset 0]
+               (swap! app-state #(assoc % :loading? true))
+               (let [results (<! (query-fn offset))]
+                 (if results
+                   (do (js/console.log (clj->js results))
+                       (>! sparql-results results)
+                       (recur (+ offset page-size)))
+                   (js/console.log "Wut, no results?"))) 
+               (swap! app-state #(assoc % :loading? false)))
+      (go (let [labels (<! sparql-results)]
+            (swap! results #(conj % labels)))) 
+      results)))
+
+; (defn sparql-autocomplete
+;   "Autocomplete from SPARQL endpoint.
+;   request: jQuery UI autocomplete's request object
+;   response: channel to put the response to"
+;   [request response]
+;   (let [fragment (lower-case (.-term request))
+;         sparql-results (sparql-results-channel)
+;         query (render-template autocomplete-template
+;                                :data {:classes (get-in config [:data :sparql :classes :default])
+;                                       :fragment fragment})
+;         sparql-query (sparql-query-channel query)]
+;     (go (let [results (map :label (<! (wrap-load sparql-query sparql-results)))]
+;           (response (clj->js results))))))
+
+(defn spreadsheet-url-to-id
+  "Extract ID from Google Spreadsheet URL"
+  [url]
+  (let [a (.createElement js/document "a")
+        _ (set! (.-href a) url)
+        path-name (.-pathname a)
+        start "/spreadsheets/d/"]
+    (when (zero? (.indexOf path-name start))
+      (-> path-name
+          (replace start "")
+          (split #"/" 2)
+          first))))
 
 (defn load-gdocs-items
   "Load items from Google Spreadsheet's worksheet."
-  [spreadsheet-id worksheet-id]
-  (let [url (str "https://spreadsheets.google.com/feeds/list/"
+  [spreadsheet-url]
+  (let [worksheet-id "od6" ; FIXME: Is this value used every time?
+        spreadsheet-id (spreadsheet-url-to-id spreadsheet-url)
+        url (str "https://spreadsheets.google.com/feeds/list/"
                  spreadsheet-id "/" worksheet-id "/public/full")
-        results (chan 1 (map (comp :entry :feed :body)))]
-    (wrap-load (http/jsonp url {:query-params {:alt "json-in-script"}})
-               results)))
+        transform-row (fn [{{label :$t} :gsx$label
+                           {description :$t} :gsx$description}]
+                            {:label label
+                             :abbreviation (abbreviate label)
+                             :description description})
+        raw-results-chan (wrap-load (http/jsonp url {:query-params {:alt "json-in-script"}})
+                                (chan 1 (map (comp (partial map transform-row) :entry :feed :body))))
+        results-chan (chan)]
+    (if spreadsheet-id
+      (do (go (let [results (<! raw-results-chan)
+                    results-count (count results)]
+                (cond (not= results-count number-of-fields)
+                        (reagent-modals/modal! (modals/invalid-spreadsheet-rows results-count))
+                      ; TODO: More validation rules
+                      :else (>! results-chan results))))
+          results-chan)
+      (reagent-modals/modal! (modals/invalid-google-spreadsheet-url spreadsheet-url)))))
+
+(defn- merge-board-with-data
+  "Merge data with questions into the initialized board."
+  [board data]
+  (into {}
+        (map (fn [[k v] result] [k (merge v result)])
+             board
+             data)))
+
+(defmulti load-board-data (fn [] (get-in @app-state [:options :data-source])))
+
+(defmethod load-board-data :dbpedia
+  [board callback]
+  (let [{{:keys [classes difficulty language]} :options} @app-state
+        [endpoint query-file] (case language
+                                    :czech ["http://cs.dbpedia.org/sparql" "sparql/cs_dbpedia.mustache"]
+                                    :english ["http://dbpedia.org/sparql" "sparql/en_dbpedia.mustache"])
+        offset (+ (case difficulty
+                        :easy 0
+                        :normal 3750
+                        :hard 8750)
+                  (rand-int 2500))
+        query-channel (sparql-query endpoint
+                                    query-file 
+                                    :data {:classes classes
+                                           :limit number-of-fields
+                                           :offset offset})]
+    (go (let [results (map despoilerify (<! query-channel))]
+          (swap! app-state #(assoc % :board (merge-board-with-data board results)))
+          (callback)))))
+
+(defmethod load-board-data :gdrive
+  [board callback]
+  (let [spreadsheet-url (get-in @app-state [:options :doc])]
+    (go (let [results (<! (load-gdocs-items spreadsheet-url))]
+          (swap! app-state #(assoc % :board (merge-board-with-data board results)))
+          (callback)))))

                  (split description #"\.\s+"))
          :else description)))

(defn clear-label
  [label]
  (-> label
      delete-parenthesized-parts 
      trim))

(defn clear-tokens
  [tokens]
  ; Filter out roman numerals
  (filter (fn [token]
            (not (re-matches #"^M{0,4}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})\.?$" token)))
          tokens))

      (replace #"\s*\[[^\]]+\]" "")))

(defn clear-description
  "TODO: Implement cleaning"
  [description]
  description)

(defn truncate-description
  "Truncate description to the configured maximum length.
  Cuts the description on a complete sentence."
  [description]
  (let [maximum-length (:max-question-length config)]
    (cond (> (count description) maximum-length)
          (reduce (fn [a b]
                    (if (> (count a) maximum-length)
