Repository: CSCfi/rems
Pull request #3035 (2022-09-12 14:33:06)

Fix attachment field validation
relates #2955 

`rems.application/format-validations` formats hiccup warning messages that link to field elements (allowing user to jumpselect that field), but attachment had prefixed `id`. special case handling was added for `:field/type :attachment` when formatting validations.

also refactored `rems.form-validation` and unified `save-draft` and `submit` field validation functions, as api now returns `:warnings` and `:errors`. save draft and submit commands still handle validations separately.

also refactored ip address field validation code (more concise regex names and replaced `cond` with more commonly used `or`).

# Checklist for author

Remove items that aren't applicable, check items that are done.

## Reviewability
- [x] Link to issue
- [x] Consider adding screenshots for ease of review

## Documentation
- [x] Update changelog if necessary

## Testing
- [x] Valuable features are integration / browser / acceptance tested automatically

## Follow-up
- [ ] New tasks are created for pending or remaining tasks

---
## Screenshots
![Screenshot 2022-09-13 at 18 23 58](https://user-images.githubusercontent.com/794087/189942031-58bb8f27-da8c-4716-bd53-3e9b60522e5b.png)
![Screenshot 2022-09-13 at 18 24 05](https://user-images.githubusercontent.com/794087/189942039-17268ddf-afbb-4665-bb7c-32b0fc79d878.png)
![Screenshot 2022-09-13 at 18 24 13](https://user-images.githubusercontent.com/794087/189942043-dd748b22-7adc-45b6-aa4b-40d89b4913ca.png)



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index beaaca149..7ce16d79b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -21,6 +21,7 @@ Changes since v2.29
 ### Fixes
 - License, create/edit license and create/edit catalogue item administrator views have been updated to display localized fields the same way other administrator views do. (#1334)
 - Don't needlessly complain about the config keys that are passed automatically from system properties and the environment. (#2935)
+- Application warning and error links were not functioning correctly for attachment fields. This is now fixed. (#2955)
 
 ## v2.29 "Länsisatamankatu" 2022-09-12
 
diff --git a/src/clj/rems/application/commands.clj b/src/clj/rems/application/commands.clj
index 208ca8c12..12d7c4c9d 100644
--- a/src/clj/rems/application/commands.clj
+++ b/src/clj/rems/application/commands.clj
@@ -259,14 +259,14 @@
 
 (defn- form-validation-warnings [forms]
   (let [warnings (for [form forms
-                       warning (form-validation/validate-fields-for-draft (:form/fields form))]
+                       warning (form-validation/validate-fields (:form/fields form))]
                    (assoc warning :form-id (:form/id form)))]
     (when (seq warnings)
       {:warnings warnings})))
 
 (defn- form-validation-errors [forms]
   (let [errors (for [form forms
-                     error (form-validation/validate-fields-for-submit (:form/fields form))]
+                     error (form-validation/validate-fields (:form/fields form))]
                  (assoc error :form-id (:form/id form)))]
     (when (seq errors)
       {:errors errors})))
diff --git a/src/clj/rems/form_validation.clj b/src/clj/rems/form_validation.clj
index d078cb116..691aecfd3 100644
--- a/src/clj/rems/form_validation.clj
+++ b/src/clj/rems/form_validation.clj
@@ -4,38 +4,27 @@
             [rems.common.form :as form]
             [rems.common.util :refer [+email-regex+
                                       +phone-number-regex+
-                                      +valid-ip-address-regex+
-                                      +valid-ip-address-regex-version-six+
-                                      +reserved-ip-address-range-regex+
-                                      +reserved-ip-address-range-regex-version-six+]]))
-
-(defn- all-columns-set? [field]
-  (let [valid-row? #(not-any? str/blank? (map :value %))]
-    (every? valid-row? (:field/value field))))
+                                      +ipv4-regex+
+                                      +ipv6-regex+
+                                      +reserved-ipv4-range-regex+
+                                      +reserved-ipv6-range-regex+]]))
 
 (defn- required-error [field]
-  (when (:field/visible field)
-    (case (:field/type field)
-      (:header :label)
-      nil
-
-      :table
-      (or
-       ;; a non-optional table must have at least one row
-       (when-not (:field/optional field)
-         (when (empty? (:field/value field))
-           {:field-id (:field/id field)
-            :type     :t.form.validation/required}))
-       ;; all tables must have all columns set for all rows
-       (when-not (all-columns-set? field)
-         {:field-id (:field/id field)
-          :type     :t.form.validation/column-values-missing}))
-
-      ;; default:
-      (when-not (:field/optional field)
-        (when (str/blank? (:field/value field))
+  (let [field-value (:field/value field)]
+    (when (and (:field/visible field)
+               (not (:field/optional field)))
+      (case (:field/type field)
+        (:header :label) nil
+        :table ;; a non-optional table must have at least one row
+        (when (empty? field-value)
+          {:field-id (:field/id field)
+           :type :t.form.validation/required})
+        ;; default:
+        (when (cond
+                (string? field-value) (str/blank? field-value)
+                :else (nil? field-value))
           {:field-id (:field/id field)
-           :type     :t.form.validation/required})))))
+           :type :t.form.validation/required})))))
 
 (defn- too-long-error [field]
   (when-let [limit (:field/max-length field)]
@@ -60,20 +49,14 @@
 (defn- invalid-ip-address-error [field]
   (when (and (= (:field/type field) :ip-address)
              (not (str/blank? (:field/value field))))
-    (cond
-      (or
-       (and (first (re-matches +valid-ip-address-regex+ (:field/value field)))
-            (first (re-matches +reserved-ip-address-range-regex+ (:field/value field))))
-       (and (first (re-matches +valid-ip-address-regex-version-six+ (:field/value field)))
-            (first (re-matches +reserved-ip-address-range-regex-version-six+ (:field/value field)))))
-      {:field-id (:field/id field)
-       :type     :t.form.validation/invalid-ip-address-private}
-      (and
-       (not (first (re-matches +valid-ip-address-regex+ (:field/value field))))
-       (not (first (re-matches +valid-ip-address-regex-version-six+ (:field/value field)))))
-      {:field-id (:field/id field)
-       :type     :t.form.validation/invalid-ip-address}
-      :else nil)))
+    (let [matches #(first (re-matches % (:field/value field)))
+          invalid-ip? (not-any? matches [+ipv4-regex+ +ipv6-regex+])
+          private-ip? (or (every? matches [+ipv4-regex+ +reserved-ipv4-range-regex+])
+                          (every? matches [+ipv6-regex+ +reserved-ipv6-range-regex+]))]
+      (or (when invalid-ip? {:field-id (:field/id field)
+                             :type :t.form.validation/invalid-ip-address})
+          (when private-ip? {:field-id (:field/id field)
+                             :type :t.form.validation/invalid-ip-address-private})))))
 
 (defn- option-value-valid? [field]
   (let [allowed-values (set (conj (map :key (:field/options field)) ""))]
@@ -99,14 +82,17 @@
   (when (= (:field/type field) :table)
     (let [columns (set (map :key (:field/columns field)))
           row-ok? (fn [row] (= columns (set (map :column row))))
+          columns-set? (fn [row] (not-any? (comp str/blank? :value) row))
           value (:field/value field)]
       ;; Schema validation guarantees that it's either a s/Str or
       ;; a [[{:column s/Str :value s/Str}]], and we've ruled out s/Str
       ;; in wrong-value-type-error
-      (when-not (every? row-ok? value)
-        ;; TODO more specific error?
-        {:field-id (:field/id field)
-         :type     :t.form.validation/invalid-value}))))
+      (or (when-not (every? row-ok? value)
+            {:field-id (:field/id field)
+             :type :t.form.validation/invalid-value})
+          (when-not (every? columns-set? value)
+            {:field-id (:field/id field)
+             :type :t.form.validation/column-values-missing})))))
 
 ;; TODO: validate that attachments are actually valid?
 (defn- invalid-attachment-error [field]
@@ -140,21 +126,12 @@
       (missing-columns-error field)
       (invalid-attachment-error field)))
 
-(defn- validate-field-submit [field]
+(defn- validate-field [field]
   (or (required-error field)
       (validate-field-content field)))
 
-(defn- validate-draft-field [field]
-  (validate-field-content field))
-
-(defn validate-fields-for-draft [fields]
+(defn validate-fields [fields]
   (->> (sort-by :field/id fields)
-       (map validate-draft-field)
-       (remove nil?)
-       (seq)))
+       (keep validate-field)
+       not-empty))
 
-(defn validate-fields-for-submit [fields]
-  (->> (sort-by :field/id fields)
-       (map validate-field-submit)
-       (remove nil?)
-       (seq)))
diff --git a/src/cljc/rems/common/util.cljc b/src/cljc/rems/common/util.cljc
index 413518602..b2071d679 100644
--- a/src/cljc/rems/common/util.cljc
+++ b/src/cljc/rems/common/util.cljc
@@ -20,9 +20,9 @@
          (re-matches +phone-number-regex+ "+35845000000000000000000000000000"))))
 
 ;; regex from https://stackoverflow.com/questions/5284147/validating-ipv4-addresses-with-regexp
-(def +valid-ip-address-regex+ #"(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$")
+(def +ipv4-regex+ #"(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$")
 
-(def +valid-ip-address-regex-version-six+ #"(([0-9]|[a-f]){1,4}(\:)){7}(([0-9]|[a-f]){1,4})$")
+(def +ipv6-regex+ #"(([0-9]|[a-f]){1,4}(\:)){7}(([0-9]|[a-f]){1,4})$")
 
 ;; https://stackoverflow.com/questions/2814002/private-ip-address-identifier-in-regular-expression
 ;; for now, this test should test for the following private IPv4 address pattaerns
@@ -40,7 +40,7 @@
 ;; 203.0.113.x - Documentation, Assigned as TEST-NET-3, documentation and examples.
 ;; 224.x.x.x - In use for IP multicast.[11] (Former Class D network).
 ;; 240.x.x.x–255.x.x.x - multicast
-(def +reserved-ip-address-range-regex+
+(def +reserved-ipv4-range-regex+
   (re-pattern
    (str
     "((0|10|127|224)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$)|"
@@ -56,129 +56,129 @@
 
 ;; https://simpledns.plus/private-ipv6
 ;; https://serverfault.com/questions/546606/what-are-the-ipv6-public-and-private-and-reserved-ranges
-(def +reserved-ip-address-range-regex-version-six+
+(def +reserved-ipv6-range-regex+
   (re-pattern
    (str "(fdce|fc00|fd00)\\:(([0-9]|[a-f]){1,4}(\\:)){6}(([0-9]|[a-f]){1,4})$")))
 
 (deftest test-ip-address-regex
   (is (= "0.0.0.0"
-         (first (re-matches +valid-ip-address-regex+ "0.0.0.0"))))
+         (first (re-matches +ipv4-regex+ "0.0.0.0"))))
   (is (= "255.255.255.255"
-         (first (re-matches +valid-ip-address-regex+ "255.255.255.255"))))
+         (first (re-matches +ipv4-regex+ "255.255.255.255"))))
   (is (= "127.0.0.1"
-         (first (re-matches +valid-ip-address-regex+ "127.0.0.1"))))
+         (first (re-matches +ipv4-regex+ "127.0.0.1"))))
   (is (= "192.168.0.0"
-         (first (re-matches +valid-ip-address-regex+ "192.168.0.0"))))
+         (first (re-matches +ipv4-regex+ "192.168.0.0"))))
   (is (= "142.250.74.110"
-         (first (re-matches +valid-ip-address-regex+ "142.250.74.110"))))
+         (first (re-matches +ipv4-regex+ "142.250.74.110"))))
   (is (= "10.255.255.255"
-         (first (re-matches +valid-ip-address-regex+ "10.255.255.255"))))
+         (first (re-matches +ipv4-regex+ "10.255.255.255"))))
   (is (= nil
-         (first (re-matches +valid-ip-address-regex+ "256.255.255.255"))))
+         (first (re-matches +ipv4-regex+ "256.255.255.255"))))
   (is (= nil
-         (first (re-matches +valid-ip-address-regex+ "10.foo.bar.255"))))
+         (first (re-matches +ipv4-regex+ "10.foo.bar.255"))))
   ;; 10.x.x.x - Private network, local communications
   (is (= "10.0.0.0"
-         (first (re-matches +reserved-ip-address-range-regex+ "10.0.0.0"))))
+         (first (re-matches +reserved-ipv4-range-regex+ "10.0.0.0"))))
   (is (= "10.26.167.0"
-         (first (re-matches +reserved-ip-address-range-regex+ "10.26.167.0"))))
+         (first (re-matches +reserved-ipv4-range-regex+ "10.26.167.0"))))
   (is (= "10.0.255.255"
-         (first (re-matches +reserved-ip-address-range-regex+ "10.0.255.255"))))
+         (first (re-matches +reserved-ipv4-range-regex+ "10.0.255.255"))))
   (is (= "192.0.0.255"
-         (first (re-matches +reserved-ip-address-range-regex+ "192.0.0.255"))))
+         (first (re-matches +reserved-ipv4-range-regex+ "192.0.0.255"))))
   (is (= "192.0.2.255"
-         (first (re-matches +reserved-ip-address-range-regex+ "192.0.2.255"))))
+         (first (re-matches +reserved-ipv4-range-regex+ "192.0.2.255"))))
   (is (= "192.168.10.255"
-         (first (re-matches +reserved-ip-address-range-regex+ "192.168.10.255"))))
+         (first (re-matches +reserved-ipv4-range-regex+ "192.168.10.255"))))
   (is (= "172.16.0.255"
-         (first (re-matches +reserved-ip-address-range-regex+ "172.16.0.255"))))
+         (first (re-matches +reserved-ipv4-range-regex+ "172.16.0.255"))))
 
   ;; 100.64.x.x–100.127.x.x - Private network, communications between a service provider and its subscribers
   (is (= "100.71.0.255"
-         (first (re-matches +reserved-ip-address-range-regex+ "100.71.0.255"))))
+         (first (re-matches +reserved-ipv4-range-regex+ "100.71.0.255"))))
   (is (= "100.64.0.255"
-         (first (re-matches +reserved-ip-address-range-regex+ "100.64.0.255"))))
+         (first (re-matches +reserved-ipv4-range-regex+ "100.64.0.255"))))
   (is (= "100.100.0.255"
-         (first (re-matches +reserved-ip-address-range-regex+ "100.100.0.255"))))
+         (first (re-matches +reserved-ipv4-range-regex+ "100.100.0.255"))))
   (is (= "100.127.78.10"
-         (first (re-matches +reserved-ip-address-range-regex+ "100.127.78.10"))))
+         (first (re-matches +reserved-ipv4-range-regex+ "100.127.78.10"))))
   (is (= nil
-         (first (re-matches +reserved-ip-address-range-regex+ "100.255.78.10"))))
+         (first (re-matches +reserved-ipv4-range-regex+ "100.255.78.10"))))
   (is (= nil
-         (first (re-matches +reserved-ip-address-range-regex+ "100.198.78.10"))))
+         (first (re-matches +reserved-ipv4-range-regex+ "100.198.78.10"))))
 
   ;; 169.254.x.x - Subnet, Used for link-local addresses[7] between two hosts on a single link when no IP address is otherwise specified
   (is (= "169.254.1.1"
-         (first (re-matches +reserved-ip-address-range-regex+ "169.254.1.1"))))
+         (first (re-matches +reserved-ipv4-range-regex+ "169.254.1.1"))))
   (is (= nil
-         (first (re-matches +reserved-ip-address-range-regex+ "16.254.1.1"))))
+         (first (re-matches +reserved-ipv4-range-regex+ "16.254.1.1"))))
   (is (= nil
-         (first (re-matches +reserved-ip-address-range-regex+ "169.25.1.1"))))
+         (first (re-matches +reserved-ipv4-range-regex+ "169.25.1.1"))))
   (is (= nil
-         (first (re-matches +reserved-ip-address-range-regex+ "169.25.1.1"))))
+         (first (re-matches +reserved-ipv4-range-regex+ "169.25.1.1"))))
 
   ;; 172.16.x.x – 172.31.x.x - Private network, local communications within a private network
   (is (= "172.16.0.0"
-         (first (re-matches +reserved-ip-address-range-regex+ "172.16.0.0"))))
+         (first (re-matches +reserved-ipv4-range-regex+ "172.16.0.0"))))
   (is (= nil
-         (first (re-matches +reserved-ip-address-range-regex+ "172.1.0.0"))))
+         (first (re-matches +reserved-ipv4-range-regex+ "172.1.0.0"))))
   (is (= "172.30.0.0"
-         (first (re-matches +reserved-ip-address-range-regex+ "172.30.0.0"))))
+         (first (re-matches +reserved-ipv4-range-regex+ "172.30.0.0"))))
   (is (= "172.31.0.0"
-         (first (re-matches +reserved-ip-address-range-regex+ "172.31.0.0"))))
+         (first (re-matches +reserved-ipv4-range-regex+ "172.31.0.0"))))
   (is (= nil
-         (first (re-matches +reserved-ip-address-range-regex+ "172.11.0.0"))))
+         (first (re-matches +reserved-ipv4-range-regex+ "172.11.0.0"))))
 
   ;; 192.0.0.x - Private network, IETF Protocol Assignments
   ;; 192.0.2.x - Documentation, Assigned as TEST-NET-1, documentation and examples.
   ;; 192.168.x.x - Private network, local communications within a private network
 
   (is (= "192.0.0.0"
-         (first (re-matches +reserved-ip-address-range-regex+ "192.0.0.0"))))
+         (first (re-matches +reserved-ipv4-range-regex+ "192.0.0.0"))))
   (is (= "192.0.0.189"
-         (first (re-matches +reserved-ip-address-range-regex+ "192.0.0.189"))))
+         (first (re-matches +reserved-ipv4-range-regex+ "192.0.0.189"))))
   (is (= "192.0.2.189"
-         (first (re-matches +reserved-ip-address-range-regex+ "192.0.2.189"))))
+         (first (re-matches +reserved-ipv4-range-regex+ "192.0.2.189"))))
   (is (= nil
-         (first (re-matches +reserved-ip-address-range-regex+ "192.0.25.189"))))
+         (first (re-matches +reserved-ipv4-range-regex+ "192.0.25.189"))))
   (is (= nil
-         (first (re-matches +reserved-ip-address-range-regex+ "192.0.1.10"))))
+         (first (re-matches +reserved-ipv4-range-regex+ "192.0.1.10"))))
   (is (= nil
-         (first (re-matches +reserved-ip-address-range-regex+ "19.0.1.10"))))
+         (first (re-matches +reserved-ipv4-range-regex+ "19.0.1.10"))))
   (is (= "192.168.1.10"
-         (first (re-matches +reserved-ip-address-range-regex+ "192.168.1.10"))))
+         (first (re-matches +reserved-ipv4-range-regex+ "192.168.1.10"))))
   (is (= nil
-         (first (re-matches +reserved-ip-address-range-regex+ "192.16.1.10"))))
+         (first (re-matches +reserved-ipv4-range-regex+ "192.16.1.10"))))
 
   ;; 198.18.x.x - 198.19.x.x - Private network, benchmark testing of inter-network communications between two separate subnets
   ;; 198.51.100.x - Documentation, Assigned as TEST-NET-2, documentation and examples.
 
   (is (= nil
-         (first (re-matches +reserved-ip-address-range-regex+ "198.1.100.78"))))
+         (first (re-matches +reserved-ipv4-range-regex+ "198.1.100.78"))))
   (is (= "198.18.100.78"
-         (first (re-matches +reserved-ip-address-range-regex+ "198.18.100.78"))))
+         (first (re-matches +reserved-ipv4-range-regex+ "198.18.100.78"))))
   (is (= "198.51.100.78"
-         (first (re-matches +reserved-ip-address-range-regex+ "198.51.100.78"))))
+         (first (re-matches +reserved-ipv4-range-regex+ "198.51.100.78"))))
   (is (= nil
-         (first (re-matches +reserved-ip-address-range-regex+ "198.50.100.78"))))
+         (first (re-matches +reserved-ipv4-range-regex+ "198.50.100.78"))))
 
   ;; 203.0.113.x - Documentation, Assigned as TEST-NET-3, documentation and examples.
   (is (= "203.0.113.89"
-         (first (re-matches +reserved-ip-address-range-regex+ "203.0.113.89"))))
+         (first (re-matches +reserved-ipv4-range-regex+ "203.0.113.89"))))
   (is (= nil
-         (first (re-matches +reserved-ip-address-range-regex+ "203.0.111.89"))))
+         (first (re-matches +reserved-ipv4-range-regex+ "203.0.111.89"))))
 
   (is (= nil
-         (first (re-matches +reserved-ip-address-range-regex+ "142.250.74.110"))))
+         (first (re-matches +reserved-ipv4-range-regex+ "142.250.74.110"))))
 
   (is (= "2001:db8:1870:999:128:7648:3849:688"
-         (first (re-matches +valid-ip-address-regex-version-six+ "2001:db8:1870:999:128:7648:3849:688"))))
+         (first (re-matches +ipv6-regex+ "2001:db8:1870:999:128:7648:3849:688"))))
   (is (= nil
-         (first (re-matches +valid-ip-address-regex-version-six+ "2001:db8:1g70:999:128:7648:3849:688"))))
+         (first (re-matches +ipv6-regex+ "2001:db8:1g70:999:128:7648:3849:688"))))
   (is (= nil
-         (first (re-matches +reserved-ip-address-range-regex-version-six+ "2001:db8:1g70:999:128:7648:3849:688"))))
+         (first (re-matches +reserved-ipv6-range-regex+ "2001:db8:1g70:999:128:7648:3849:688"))))
   (is (= "fdce:de09:d25d:b23e:de8:d0e8:de8:de8"
-         (first (re-matches +reserved-ip-address-range-regex-version-six+ "fdce:de09:d25d:b23e:de8:d0e8:de8:de8")))))
+         (first (re-matches +reserved-ipv6-range-regex+ "fdce:de09:d25d:b23e:de8:d0e8:de8:de8")))))
 
 ;; TODO remove separate clj and cljs implementations of getx and getx-in
 (defn getx
diff --git a/src/cljs/rems/application.cljs b/src/cljs/rems/application.cljs
index 6fb9dab3a..b7f15fd4a 100644
--- a/src/cljs/rems/application.cljs
+++ b/src/cljs/rems/application.cljs
@@ -84,7 +84,10 @@
                   [:a {:href "#"
                        :on-click (case (:field/type field)
                                    ;; workaround for tables: there's no single input to focus
-                                   :table #(focus/focus-selector (str "#container-" (fields/field-name field)))
+                                   :table #(focus/focus-selector (str "#container-"
+                                                                      (fields/field-name field)))
+                                   :attachment (focus-input-field (str "upload-"
+                                                                       (fields/field-name field)))
                                    (focus-input-field (fields/field-name field)))}
                    (text-format type (localized (:field/title field)))]
                   (text type))]))])
diff --git a/src/cljs/rems/config.cljs b/src/cljs/rems/config.cljs
index 3059b8400..350143437 100644
--- a/src/cljs/rems/config.cljs
+++ b/src/cljs/rems/config.cljs
@@ -29,10 +29,10 @@
   (let [config @(rf/subscribe [::config])]
     (boolean (:dev config))))
 
-(defn ^:export set-config!
-  [js-config]
-  (rf/dispatch-sync [::loaded-config (merge @(rf/subscribe [::config])
-                                            (js->clj js-config :keywordize-keys true))]))
+(defn ^:export set-config! [js-config]
+  (when (dev-environment?)
+    (rf/dispatch-sync [::loaded-config (merge @(rf/subscribe [::config])
+                                              (js->clj js-config :keywordize-keys true))])))
 
 ;; organizations
 


Test patch:
diff --git a/test/clj/rems/api/test_applications.clj b/test/clj/rems/api/test_applications.clj
index 29ca3806e..08c5872b9 100644
--- a/test/clj/rems/api/test_applications.clj
+++ b/test/clj/rems/api/test_applications.clj
@@ -1032,7 +1032,9 @@
                                                   :actor user-id})]
 
     (testing "set value of optional field"
-      (is (= {:success true}
+      (is (= {:success true
+              :warnings [{:field-id "req1" :form-id form-id :type "t.form.validation/required"}
+                         {:field-id "req2" :form-id form-id2 :type "t.form.validation/required"}]}
              (send-command user-id {:type :application.command/save-draft
                                     :application-id app-id
                                     :field-values [{:form form-id :field "opt1" :value "opt"}]})
@@ -1158,7 +1160,8 @@
         (is (= [] (get-in app [:application/forms 0 :form/fields 0 :field/value])))
         (is (= [] (get-in app [:application/forms 0 :form/fields 1 :field/value])))))
     (testing "save a draft"
-      (is (= {:success true}
+      (is (= {:success true
+              :warnings [{:field-id "req" :form-id form-id :type "t.form.validation/required"}]}
              (send-command user-id {:type :application.command/save-draft
                                     :application-id app-id
                                     :field-values [{:form form-id :field "opt"
@@ -1198,7 +1201,8 @@
              (send-command user-id {:type :application.command/submit
                                     :application-id app-id}))))
     (testing "save a new draft"
-      (is (= {:success true}
+      (is (= {:success true
+              :warnings [{:type "t.form.validation/column-values-missing" :form-id form-id :field-id "opt"}]}
              (send-command user-id {:type :application.command/save-draft
                                     :application-id app-id
                                     :field-values [{:form form-id :field "opt"
diff --git a/test/clj/rems/browser_test_util.clj b/test/clj/rems/browser_test_util.clj
index 8dcecdee6..172c01e09 100644
--- a/test/clj/rems/browser_test_util.clj
+++ b/test/clj/rems/browser_test_util.clj
@@ -536,10 +536,12 @@
         (when (seq @violations)
           (throw (Exception. (str "\n\n\nThere are accessibility violations: " (count @violations) "\n\n\n"))))))))
 
-(defn set-client-config
-  [config]
-  (js-execute "return window.rems.config.set_config_BANG_(arguments[0]);" config)
-  (fn []
-    (js-async "var args = arguments;
-               var callback = args[args.length - 1];
-               window.rems.config.fetch_config_BANG_(callback);")))
+(defmacro with-client-config [config & body]
+  `(do
+     (rems.browser-test-util/js-execute
+      "return window.rems.config.set_config_BANG_(arguments[0]);" ~config)
+     ~@body
+     (rems.browser-test-util/js-async
+      "var args = arguments;
+       var callback = args[args.length - 1];
+       return window.rems.config.fetch_config_BANG_(callback);")))
diff --git a/test/clj/rems/test_browser.clj b/test/clj/rems/test_browser.clj
index 12637a7bc..ee1af9d19 100644
--- a/test/clj/rems/test_browser.clj
+++ b/test/clj/rems/test_browser.clj
@@ -321,6 +321,7 @@
   (let [el (first (btu/query-all [{:css ".fields"}
                                   {:tag :label :fn/has-text label}]))
         id (btu/get-element-attr-el el :for)]
+    (btu/wait-visible {:id (str id "-error")})
     (btu/get-element-text {:id (str id "-error")})))
 
 (defn fill-license-fields [{:keys [title external-links inline-text attachments]}]
@@ -472,15 +473,14 @@
           (btu/scroll-and-click-el (last (btu/query-all {:css (str "button.remove-attachment-" attachment-field-id)}))))
 
         (testing "uploading oversized attachment should display error"
-          (let [reset-config (btu/set-client-config {:attachment-max-size 900})]
+          (btu/with-client-config {:attachment-max-size 900}
             (btu/upload-file attachment-field-upload-selector "resources/public/img/rems_logo_fi.png")
             (is (btu/eventually-visible? :status-failed))
             (is (= ["Upload an attachment: Failed"
                     (str/join "\n" ["The attachment is too large."
                                     "rems_logo_fi.png 10.34 KB"
                                     "Allowed maximum size of an attachment: 0.9 KB."])]
-                   (get-error-summary)))
-            (reset-config)))
+                   (get-error-summary)))))
 
         (is (not (btu/field-visible? "Conditional field"))
             "Conditional field is not visible before selecting option")
@@ -517,38 +517,37 @@
 
         (fill-form-field "Simple text field" "Private field answer before autosave")
 
-        (btu/scroll-and-click :save)
-        (is (btu/eventually-visible? {:id :status-success}))
-
-        ;; let's also try autosave
-        (let [reset-config (btu/set-client-config {:enable-autosave true})]
-          (clear-form-field "Simple text field")
-          (fill-form-field "Simple text field" "Private field answer")
-          (is (btu/eventually-visible? {:id :status-success :fn/text "Application is saved."}))
-          (reset-config))
-
-        (testing "add invalid value for field, try to save"
+        (testing "save draft succesfully, but show validation warnings"
           (fill-form-field "Email field" "user")
-
           (btu/scroll-and-click :save)
-
           (is (btu/eventually-visible? :status-warning))
-          (is (= ["Invalid email address."]
+          (is (= ["Field \"Text field\" is required."
+                  "Invalid email address."]
                  (get-validation-summary)))
           (is (= "Invalid email address."
                  (get-validation-for-field "Email field"))))
 
-        (fill-form-field "Email field" "@example.com")
+        ;; let's also try autosave
+        (btu/with-client-config {:enable-autosave true}
+          (clear-form-field "Simple text field")
+          (fill-form-field "Simple text field" "Private field answer")
+          (is (btu/eventually-visible? {:id :status-success :fn/text "Application is saved."}))
+          (is (btu/eventually-visible? :status-warning))
+          (is (= ["Field \"Text field\" is required."
+                  "Invalid email address."]
+                 (get-validation-summary))))
 
         (testing "try to submit without accepting licenses or filling in a mandatory field"
           (btu/scroll-and-click :submit)
           (is (btu/eventually-visible? {:id "status-failed" :fn/has-text "Send application"}))
           (is (= ["Terms of use not accepted."
-                  "Field \"Text field\" is required."]
+                  "Field \"Text field\" is required."
+                  "Invalid email address."]
                  (get-validation-summary)))
           (is (= "Field \"Text field\" is required."
                  (get-validation-for-field "Text field"))))
 
+        (fill-form-field "Email field" "@example.com") ; to complete email field value
         (fill-form-field "Text field" "Test")
 
         (accept-licenses)
diff --git a/test/clj/rems/test_form_validation.clj b/test/clj/rems/test_form_validation.clj
index 6d7a74a9a..47c786365 100644
--- a/test/clj/rems/test_form_validation.clj
+++ b/test/clj/rems/test_form_validation.clj
@@ -1,234 +1,232 @@
 (ns rems.test-form-validation
-  (:refer-clojure :exclude [validate-fields])
-  (:require [clojure.test :refer :all]
-            [rems.form-validation :refer [validate-fields-for-submit validate-fields-for-draft]]))
+  (:require [clojure.test :refer [deftest testing is]]
+            [rems.form-validation :refer [validate-fields]]))
 
-(deftest test-validate-fields-for-submit
+(deftest test-validate-fields
   (testing "all fields filled"
-    (is (nil? (validate-fields-for-submit [{:field/title    "A"
-                                            :field/optional false
-                                            :field/visible  true
-                                            :field/value    "a"}]))))
-
+    (is (nil? (validate-fields [{:field/title    "A"
+                                 :field/optional false
+                                 :field/visible  true
+                                 :field/value    "a"}]))))
 
   (testing "optional fields do not need to be filled"
-    (is (nil? (validate-fields-for-submit [{:field/title    "A"
-                                            :field/optional true
-                                            :field/visible  true
-                                            :field/value    nil}
-                                           {:field/title    "B"
-                                            :field/optional true
-                                            :field/visible  true
-                                            :field/value    ""}]))))
+    (is (nil? (validate-fields [{:field/title    "A"
+                                 :field/optional true
+                                 :field/visible  true
+                                 :field/value    nil}
+                                {:field/title    "B"
+                                 :field/optional true
+                                 :field/visible  true
+                                 :field/value    ""}]))))
 
   (testing "invisible fields do not need to be filled"
-    (is (nil? (validate-fields-for-submit [{:field/title    "A"
-                                            :field/optional true
-                                            :field/visible  false
-                                            :field/value    nil}
-                                           {:field/title    "B"
-                                            :field/optional true
-                                            :field/visible  false
-                                            :field/value    ""}
-                                           {:field/title    "C"
-                                            :field/optional false
-                                            :field/visible  false
-                                            :field/value    ""}]))))
+    (is (nil? (validate-fields [{:field/title    "A"
+                                 :field/optional true
+                                 :field/visible  false
+                                 :field/value    nil}
+                                {:field/title    "B"
+                                 :field/optional true
+                                 :field/visible  false
+                                 :field/value    ""}
+                                {:field/title    "C"
+                                 :field/optional false
+                                 :field/visible  false
+                                 :field/value    ""}]))))
 
   (testing "labels and headers are always effectively optional"
-    (is (nil? (validate-fields-for-submit [{:field/type     :label
-                                            :field/optional false
-                                            :field/visible  true
-                                            :field/value    ""}
-                                           {:field/type     :label
-                                            :field/optional true
-                                            :field/visible  true
-                                            :field/value    ""}
-                                           {:field/type     :header
-                                            :field/optional false
-                                            :field/visible  true
-                                            :field/value    ""}
-                                           {:field/type     :header
-                                            :field/optional true
-                                            :field/visible  true
-                                            :field/value    ""}]))))
+    (is (nil? (validate-fields [{:field/type     :label
+                                 :field/optional false
+                                 :field/visible  true
+                                 :field/value    ""}
+                                {:field/type     :label
+                                 :field/optional true
+                                 :field/visible  true
+                                 :field/value    ""}
+                                {:field/type     :header
+                                 :field/optional false
+                                 :field/visible  true
+                                 :field/value    ""}
+                                {:field/type     :header
+                                 :field/optional true
+                                 :field/visible  true
+                                 :field/value    ""}]))))
 
   (testing "error: field required"
     (is (= [{:type :t.form.validation/required :field-id "2"}
             {:type :t.form.validation/required :field-id "3"}
             {:type :t.form.validation/required :field-id "4"}]
-           (validate-fields-for-submit [{:field/id       "1"
-                                         :field/optional true
-                                         :field/visible  true
-                                         :field/value    nil}
-                                        {:field/id       "2"
-                                         :field/optional false
-                                         :field/visible  true
-                                         :field/value    nil}
-                                        {:field/id       "3"
-                                         :field/optional false
-                                         :field/visible  true
-                                         :field/value    ""}
-                                        {:field/id       "4"
-                                         :field/optional false
-                                         :field/visible  true
-                                         :field/value    "    "}]))))
+           (validate-fields [{:field/id       "1"
+                              :field/optional true
+                              :field/visible  true
+                              :field/value    nil}
+                             {:field/id       "2"
+                              :field/optional false
+                              :field/visible  true
+                              :field/value    nil}
+                             {:field/id       "3"
+                              :field/optional false
+                              :field/visible  true
+                              :field/value    ""}
+                             {:field/id       "4"
+                              :field/optional false
+                              :field/visible  true
+                              :field/value    "    "}]))))
 
   (testing "error: field input too long"
     (is (= [{:type :t.form.validation/toolong :field-id "2"}]
-           (validate-fields-for-submit [{:field/id         "1"
-                                         :field/max-length 5
-                                         :field/visible    true
-                                         :field/value      "abcde"}
-                                        {:field/id         "2"
-                                         :field/max-length 5
-                                         :field/visible    true
-                                         :field/value      "abcdef"}]))))
+           (validate-fields [{:field/id         "1"
+                              :field/max-length 5
+                              :field/visible    true
+                              :field/value      "abcde"}
+                             {:field/id         "2"
+                              :field/max-length 5
+                              :field/visible    true
+                              :field/value      "abcdef"}]))))
 
   (testing "error: field input selected option is invalid"
     (is (= [{:type :t.form.validation/invalid-value :field-id "1"}]
-           (validate-fields-for-submit [{:field/id       "1"
-                                         :field/title    {:en "Option list."
-                                                          :fi "Valintalista."}
-                                         :field/type     :option
-                                         :field/options  [{:key   "Option1"
-                                                           :label {:en "First"
-                                                                   :fi "Ensimmäinen"}}
-                                                          {:key   "Option2"
-                                                           :label {:en "Second"
-                                                                   :fi "Toinen"}}
-                                                          {:key   "Option3"
-                                                           :label {:en "Third"
-                                                                   :fi "Kolmas "}}]
-                                         :field/optional true
-                                         :field/visible  true
-                                         :field/value    "foobar"}
-                                        {:field/id       "2"
-                                         :field/title    {:en "Option list."
-                                                          :fi "Valintalista."}
-                                         :field/type     :option
-                                         :field/options  [{:key   "Option1"
-                                                           :label {:en "First"
-                                                                   :fi "Ensimmäinen"}}
-                                                          {:key   "Option2"
-                                                           :label {:en "Second"
-                                                                   :fi "Toinen"}}
-                                                          {:key   "Option3"
-                                                           :label {:en "Third"
-                                                                   :fi "Kolmas "}}]
-                                         :field/optional true
-                                         :field/visible  true
-                                         :field/value    "Option1"}]))))
+           (validate-fields [{:field/id       "1"
+                              :field/title    {:en "Option list."
+                                               :fi "Valintalista."}
+                              :field/type     :option
+                              :field/options  [{:key   "Option1"
+                                                :label {:en "First"
+                                                        :fi "Ensimmäinen"}}
+                                               {:key   "Option2"
+                                                :label {:en "Second"
+                                                        :fi "Toinen"}}
+                                               {:key   "Option3"
+                                                :label {:en "Third"
+                                                        :fi "Kolmas "}}]
+                              :field/optional true
+                              :field/visible  true
+                              :field/value    "foobar"}
+                             {:field/id       "2"
+                              :field/title    {:en "Option list."
+                                               :fi "Valintalista."}
+                              :field/type     :option
+                              :field/options  [{:key   "Option1"
+                                                :label {:en "First"
+                                                        :fi "Ensimmäinen"}}
+                                               {:key   "Option2"
+                                                :label {:en "Second"
+                                                        :fi "Toinen"}}
+                                               {:key   "Option3"
+                                                :label {:en "Third"
+                                                        :fi "Kolmas "}}]
+                              :field/optional true
+                              :field/visible  true
+                              :field/value    "Option1"}]))))
 
   (testing "error: field input option can be left empty when optional"
     (is (= nil
-           (validate-fields-for-submit [{:field/id       "1"
-                                         :field/title    {:en "Option list."
-                                                          :fi "Valintalista."}
-                                         :field/type     :option
-                                         :field/options  [{:key   "Option1"
-                                                           :label {:en "First"
-                                                                   :fi "Ensimmäinen"}}
-                                                          {:key   "Option2"
-                                                           :label {:en "Second"
-                                                                   :fi "Toinen"}}
-                                                          {:key   "Option3"
-                                                           :label {:en "Third"
-                                                                   :fi "Kolmas "}}]
-                                         :field/optional true
-                                         :field/visible  true
-                                         :field/value    ""}]))))
+           (validate-fields [{:field/id       "1"
+                              :field/title    {:en "Option list."
+                                               :fi "Valintalista."}
+                              :field/type     :option
+                              :field/options  [{:key   "Option1"
+                                                :label {:en "First"
+                                                        :fi "Ensimmäinen"}}
+                                               {:key   "Option2"
+                                                :label {:en "Second"
+                                                        :fi "Toinen"}}
+                                               {:key   "Option3"
+                                                :label {:en "Third"
+                                                        :fi "Kolmas "}}]
+                              :field/optional true
+                              :field/visible  true
+                              :field/value    ""}]))))
 
   (testing "error: multiselect validation"
     (is (= nil
-           (validate-fields-for-submit [{:field/id       "1"
-                                         :field/type     :multiselect
-                                         :field/visible  true
-                                         :field/optional true
-                                         :field/options  [{:key   "ye"}
-                                                          {:key   "no"}
-                                                          {:key   "xx"}]
-                                         :field/value    ""}])
-           (validate-fields-for-submit [{:field/id       "1"
-                                         :field/type     :multiselect
-                                         :field/visible  true
-                                         :field/optional true
-                                         :field/options  [{:key   "ye"}
-                                                          {:key   "no"}
-                                                          {:key   "xx"}]
-                                         :field/value    "ye"}])
-           (validate-fields-for-submit [{:field/id       "1"
-                                         :field/type     :multiselect
-                                         :field/visible  true
-                                         :field/optional true
-                                         :field/options  [{:key   "ye"}
-                                                          {:key   "no"}
-                                                          {:key   "xx"}]
-                                         :field/value    "no"}])
-           (validate-fields-for-submit [{:field/id       "1"
-                                         :field/type     :multiselect
-                                         :field/visible  true
-                                         :field/optional true
-                                         :field/options  [{:key   "ye"}
-                                                          {:key   "no"}
-                                                          {:key   "xx"}]
-                                         :field/value    "ye no xx"}])))
+           (validate-fields [{:field/id       "1"
+                              :field/type     :multiselect
+                              :field/visible  true
+                              :field/optional true
+                              :field/options  [{:key   "ye"}
+                                               {:key   "no"}
+                                               {:key   "xx"}]
+                              :field/value    ""}])
+           (validate-fields [{:field/id       "1"
+                              :field/type     :multiselect
+                              :field/visible  true
+                              :field/optional true
+                              :field/options  [{:key   "ye"}
+                                               {:key   "no"}
+                                               {:key   "xx"}]
+                              :field/value    "ye"}])
+           (validate-fields [{:field/id       "1"
+                              :field/type     :multiselect
+                              :field/visible  true
+                              :field/optional true
+                              :field/options  [{:key   "ye"}
+                                               {:key   "no"}
+                                               {:key   "xx"}]
+                              :field/value    "no"}])
+           (validate-fields [{:field/id       "1"
+                              :field/type     :multiselect
+                              :field/visible  true
+                              :field/optional true
+                              :field/options  [{:key   "ye"}
+                                               {:key   "no"}
+                                               {:key   "xx"}]
+                              :field/value    "ye no xx"}])))
     (is (= [{:type :t.form.validation/invalid-value :field-id "1"}]
-           (validate-fields-for-submit [{:field/id       "1"
-                                         :field/type     :multiselect
-                                         :field/visible  true
-                                         :field/optional true
-                                         :field/options  [{:key   "ye"}
-                                                          {:key   "no"}
-                                                          {:key   "xx"}]
-                                         :field/value    "foo"}])
-           (validate-fields-for-submit [{:field/id       "1"
-                                         :field/type     :multiselect
-                                         :field/visible  true
-                                         :field/optional true
-                                         :field/options  [{:key   "ye"}
-                                                          {:key   "no"}
-                                                          {:key   "xx"}]
-                                         :field/value    "ye foo xx"}])))
+           (validate-fields [{:field/id       "1"
+                              :field/type     :multiselect
+                              :field/visible  true
+                              :field/optional true
+                              :field/options  [{:key   "ye"}
+                                               {:key   "no"}
+                                               {:key   "xx"}]
+                              :field/value    "foo"}])
+           (validate-fields [{:field/id       "1"
+                              :field/type     :multiselect
+                              :field/visible  true
+                              :field/optional true
+                              :field/options  [{:key   "ye"}
+                                               {:key   "no"}
+                                               {:key   "xx"}]
+                              :field/value    "ye foo xx"}])))
     (is (= [{:type :t.form.validation/required :field-id "1"}]
-           (validate-fields-for-submit [{:field/id       "1"
-                                         :field/type     :multiselect
-                                         :field/visible  true
-                                         :field/optional false
-                                         :field/options  [{:key   "ye"}
-                                                          {:key   "no"}
-                                                          {:key   "xx"}]
-                                         :field/value    ""}]))))
+           (validate-fields [{:field/id       "1"
+                              :field/type     :multiselect
+                              :field/visible  true
+                              :field/optional false
+                              :field/options  [{:key   "ye"}
+                                               {:key   "no"}
+                                               {:key   "xx"}]
+                              :field/value    ""}]))))
 
   (testing "error: field input invalid email address"
     (is (= [{:type :t.form.validation/required :field-id "1"}
             {:type :t.form.validation/invalid-email :field-id "2"}
             {:type :t.form.validation/invalid-email :field-id "5"}]
-           (validate-fields-for-submit [{:field/id       "1"
-                                         :field/type     :email
-                                         :field/optional false
-                                         :field/visible  true
-                                         :field/value    ""}
-                                        {:field/id       "2"
-                                         :field/type     :email
-                                         :field/optional false
-                                         :field/visible  true
-                                         :field/value    "invalid.email"}
-                                        {:field/id       "3"
-                                         :field/type     :email
-                                         :field/optional false
-                                         :field/visible  true
-                                         :field/value    "valid.email@example.com"}
-                                        {:field/id       "4"
-                                         :field/type     :email
-                                         :field/optional true
-                                         :field/visible  true
-                                         :field/value    ""}
-                                        {:field/id       "5"
-                                         :field/type     :email
-                                         :field/optional true
-                                         :field/visible  true
-                                         :field/value    "invalid.email"}]))))
+           (validate-fields [{:field/id       "1"
+                              :field/type     :email
+                              :field/optional false
+                              :field/visible  true
+                              :field/value    ""}
+                             {:field/id       "2"
+                              :field/type     :email
+                              :field/optional false
+                              :field/visible  true
+                              :field/value    "invalid.email"}
+                             {:field/id       "3"
+                              :field/type     :email
+                              :field/optional false
+                              :field/visible  true
+                              :field/value    "valid.email@example.com"}
+                             {:field/id       "4"
+                              :field/type     :email
+                              :field/optional true
+                              :field/visible  true
+                              :field/value    ""}
+                             {:field/id       "5"
+                              :field/type     :email
+                              :field/optional true
+                              :field/visible  true
+                              :field/value    "invalid.email"}]))))
 
   (testing "table validation"
     (let [fields [{:field/id "tbl"
@@ -237,324 +235,102 @@
                    :field/optional true
                    :field/columns [{:key "col1"} {:key "col2"}]}]]
       (testing "optional"
-        (is (nil? (validate-fields-for-submit (assoc-in fields [0 :field/value] []))))
-        (is (nil? (validate-fields-for-submit (assoc-in fields [0 :field/value] [[{:column "col1" :value "1"}
-                                                                                  {:column "col2" :value "2"}]
-                                                                                 [{:column "col1" :value "1"}
-                                                                                  {:column "col2" :value "2"}]])))))
+        (is (nil? (validate-fields (assoc-in fields [0 :field/value] []))))
+        (is (nil? (validate-fields (assoc-in fields [0 :field/value] [[{:column "col1" :value "1"}
+                                                                       {:column "col2" :value "2"}]
+                                                                      [{:column "col1" :value "1"}
+                                                                       {:column "col2" :value "2"}]])))))
       (testing "required"
         (let [required (assoc-in fields [0 :field/optional] false)]
           (is (= [{:field-id "tbl", :type :t.form.validation/required}]
-                 (validate-fields-for-submit (assoc-in required [0 :field/value] ""))))
+                 (validate-fields (assoc-in required [0 :field/value] ""))))
           (is (= [{:field-id "tbl", :type :t.form.validation/required}]
-                 (validate-fields-for-submit (assoc-in required [0 :field/value] []))))
-          (is (nil? (validate-fields-for-submit (assoc-in required [0 :field/value] [[{:column "col1" :value "1"}
-                                                                                      {:column "col2" :value "2"}]])))))))))
-
-(deftest test-validate-fields-for-draft
-  (testing "required fields do not need to be filled at draft stage"
-    (is (nil? (validate-fields-for-draft [{:field/title    "A"
-                                           :field/optional false
-                                           :field/visible  true
-                                           :field/value    nil}
-                                          {:field/title    "B"
-                                           :field/optional false
-                                           :field/visible  true
-                                           :field/value    ""}]))))
-
-
-  (testing "optional fields do not need to be filled"
-    (is (nil? (validate-fields-for-draft [{:field/title    "A"
-                                           :field/optional true
-                                           :field/visible  true
-                                           :field/value    nil}
-                                          {:field/title    "B"
-                                           :field/optional true
-                                           :field/visible  true
-                                           :field/value    ""}]))))
-
-  (testing "invisible fields do not need to be filled"
-    (is (nil? (validate-fields-for-draft [{:field/title    "A"
-                                           :field/optional true
-                                           :field/visible  false
-                                           :field/value    nil}
-                                          {:field/title    "B"
-                                           :field/optional true
-                                           :field/visible  false
-                                           :field/value    ""}]))))
-
-  (testing "labels and headers are always effectively optional"
-    (is (nil? (validate-fields-for-draft [{:field/type     :label
-                                           :field/optional false
-                                           :field/visible  true
-                                           :field/value    ""}
-                                          {:field/type     :label
-                                           :field/optional true
-                                           :field/visible  true
-                                           :field/value    ""}
-                                          {:field/type     :header
-                                           :field/optional false
-                                           :field/visible  true
-                                           :field/value    ""}
-                                          {:field/type     :header
-                                           :field/optional true
-                                           :field/visible  true
-                                           :field/value    ""}]))))
+                 (validate-fields (assoc-in required [0 :field/value] []))))
+          (is (nil? (validate-fields (assoc-in required [0 :field/value] [[{:column "col1" :value "1"}
+                                                                           {:column "col2" :value "2"}]]))))))))
 
-  (testing "error: field input too long"
-    (is (= [{:type :t.form.validation/toolong :field-id "2"}]
-           (validate-fields-for-draft [{:field/id         "1"
-                                        :field/max-length 5
-                                        :field/visible    true
-                                        :field/value      "abcde"}
-                                       {:field/id         "2"
-                                        :field/max-length 5
-                                        :field/visible    true
-                                        :field/value      "abcdef"}]))))
+  (testing "error: field input invalid ip address"
+    (is (= [{:type :t.form.validation/required :field-id "empty ip address"}
+            {:type :t.form.validation/invalid-ip-address :field-id "invalid ip address"}
+            {:type :t.form.validation/invalid-ip-address-private :field-id "private ip address"}]
+           (validate-fields [{:field/id       "empty ip address"
+                              :field/type     :ip-address
+                              :field/optional false
+                              :field/visible  true
+                              :field/value    ""}
+                             {:field/id       "invalid ip address"
+                              :field/type     :ip-address
+                              :field/optional false
+                              :field/visible  true
+                              :field/value    "+058450000100"}
+                             {:field/id       "correct ip address"
+                              :field/type     :ip-address
+                              :field/optional false
+                              :field/visible  true
+                              :field/value    "142.250.74.110"}
+                             {:field/id       "private ip address"
+                              :field/type     :ip-address
+                              :field/optional true
+                              :field/visible  true
+                              :field/value    "192.0.2.255"}]))))
 
   (testing "attachment field"
-    (let [field {:field/id         "fld1"
-                 :field/type       :attachment
-                 :field/visible    true}]
+    (let [field {:field/id "fld1"
+                 :field/type :attachment
+                 :field/visible true}]
       (is (= [{:type :t.form.validation/invalid-value :field-id "fld1"}]
-             (validate-fields-for-draft [(assoc field :field/value "1;2")])))
+             (validate-fields [(assoc field :field/value "1;2")])))
       (is (= [{:type :t.form.validation/invalid-value :field-id "fld1"}]
-             (validate-fields-for-draft [(assoc field :field/value "1 2")])))
+             (validate-fields [(assoc field :field/value "1 2")])))
       (is (= [{:type :t.form.validation/invalid-value :field-id "fld1"}]
-             (validate-fields-for-draft [(assoc field :field/value "1,a,2")])))
-      (is (nil? (validate-fields-for-draft [(assoc field :field/value "1")])))
-      (is (nil? (validate-fields-for-draft [(assoc field :field/value "1,2,3")])))))
+             (validate-fields [(assoc field :field/value "1,a,2")])))
+      (is (nil? (validate-fields [(assoc field :field/value "1")])))
+      (is (nil? (validate-fields [(assoc field :field/value "1,2,3")])))))
 
   (testing "string required for"
     (doseq [type [:text :texta :date :email :attachment :description]]
       (testing type
         (is (= [{:type :t.form.validation/invalid-value :field-id "fld1"}]
-               (validate-fields-for-draft [{:field/id "fld1" :field/visible true :field/type type :field/value 1}])))
+               (validate-fields [{:field/id "fld1" :field/visible true :field/type type :field/value 1}])))
         (is (= [{:type :t.form.validation/invalid-value :field-id "fld1"}]
-               (validate-fields-for-draft [{:field/id "fld1" :field/visible true :field/type type :field/value {:foo "bar"}}])))
+               (validate-fields [{:field/id "fld1" :field/visible true :field/type type :field/value {:foo "bar"}}])))
         (is (= [{:type :t.form.validation/invalid-value :field-id "fld1"}]
-               (validate-fields-for-draft [{:field/id "fld1" :field/visible true :field/type type :field/value [{:foo "bar"}]}]))))))
-
-  (testing "error: field input selected option is invalid"
-    (is (= [{:type :t.form.validation/invalid-value :field-id "1"}]
-           (validate-fields-for-draft [{:field/id       "1"
-                                        :field/title    {:en "Option list."
-                                                         :fi "Valintalista."}
-                                        :field/type     :option
-                                        :field/options  [{:key   "Option1"
-                                                          :label {:en "First"
-                                                                  :fi "Ensimmäinen"}}
-                                                         {:key   "Option2"
-                                                          :label {:en "Second"
-                                                                  :fi "Toinen"}}
-                                                         {:key   "Option3"
-                                                          :label {:en "Third"
-                                                                  :fi "Kolmas "}}]
-                                        :field/optional true
-                                        :field/visible  true
-                                        :field/value    "foobar"}
-                                       {:field/id       "2"
-                                        :field/title    {:en "Option list."
-                                                         :fi "Valintalista."}
-                                        :field/type     :option
-                                        :field/options  [{:key   "Option1"
-                                                          :label {:en "First"
-                                                                  :fi "Ensimmäinen"}}
-                                                         {:key   "Option2"
-                                                          :label {:en "Second"
-                                                                  :fi "Toinen"}}
-                                                         {:key   "Option3"
-                                                          :label {:en "Third"
-                                                                  :fi "Kolmas "}}]
-                                        :field/optional true
-                                        :field/visible  true
-                                        :field/value    "Option1"}]))))
-
-  (testing "error: field input option can be left empty when optional"
-    (is (= nil
-           (validate-fields-for-draft [{:field/id       "1"
-                                        :field/title    {:en "Option list."
-                                                         :fi "Valintalista."}
-                                        :field/type     :option
-                                        :field/options  [{:key   "Option1"
-                                                          :label {:en "First"
-                                                                  :fi "Ensimmäinen"}}
-                                                         {:key   "Option2"
-                                                          :label {:en "Second"
-                                                                  :fi "Toinen"}}
-                                                         {:key   "Option3"
-                                                          :label {:en "Third"
-                                                                  :fi "Kolmas "}}]
-                                        :field/optional true
-                                        :field/visible  true
-                                        :field/value    ""}]))))
-
-  (testing "error: multiselect validation"
-    (is (= nil
-           (validate-fields-for-draft [{:field/id       "1"
-                                        :field/type     :multiselect
-                                        :field/options  [{:key   "ye"}
-                                                         {:key   "no"}
-                                                         {:key   "xx"}]
-                                        :field/value    ""}])
-           (validate-fields-for-draft [{:field/id       "1"
-                                        :field/type     :multiselect
-                                        :field/options  [{:key   "ye"}
-                                                         {:key   "no"}
-                                                         {:key   "xx"}]
-                                        :field/value    "ye"}])
-           (validate-fields-for-draft [{:field/id       "1"
-                                        :field/type     :multiselect
-                                        :field/options  [{:key   "ye"}
-                                                         {:key   "no"}
-                                                         {:key   "xx"}]
-                                        :field/value    "no"}])
-           (validate-fields-for-draft [{:field/id       "1"
-                                        :field/type     :multiselect
-                                        :field/options  [{:key   "ye"}
-                                                         {:key   "no"}
-                                                         {:key   "xx"}]
-                                        :field/value    "ye no xx"}])))
-    (is (= [{:type :t.form.validation/invalid-value :field-id "1"}]
-           (validate-fields-for-draft [{:field/id       "1"
-                                        :field/type     :multiselect
-                                        :field/options  [{:key   "ye"}
-                                                         {:key   "no"}
-                                                         {:key   "xx"}]
-                                        :field/value    "foo"}])
-           (validate-fields-for-draft [{:field/id       "1"
-                                        :field/type     :multiselect
-                                        :field/options  [{:key   "ye"}
-                                                         {:key   "no"}
-                                                         {:key   "xx"}]
-                                        :field/value    "ye foo xx"}]))))
-
-  (testing "error: field input invalid email address"
-    (is (= [{:type :t.form.validation/invalid-email :field-id "2"}
-            {:type :t.form.validation/invalid-email :field-id "5"}]
-           (validate-fields-for-draft [{:field/id       "1"
-                                        :field/type     :email
-                                        :field/optional false
-                                        :field/visible  true
-                                        :field/value    ""}
-                                       {:field/id       "2"
-                                        :field/type     :email
-                                        :field/optional false
-                                        :field/visible  true
-                                        :field/value    "invalid.email"}
-                                       {:field/id       "3"
-                                        :field/type     :email
-                                        :field/optional false
-                                        :field/visible  true
-                                        :field/value    "valid.email@example.com"}
-                                       {:field/id       "4"
-                                        :field/type     :email
-                                        :field/optional true
-                                        :field/visible  true
-                                        :field/value    ""}
-                                       {:field/id       "5"
-                                        :field/type     :email
-                                        :field/optional true
-                                        :field/visible  true
-                                        :field/value    "invalid.email"}]))))
+               (validate-fields [{:field/id "fld1" :field/visible true :field/type type :field/value [{:foo "bar"}]}]))))))
 
   (testing "error: field input invalid phone number"
-    (is (= [{:type :t.form.validation/invalid-phone-number :field-id "less than 5 symbols is not allowed"}
+    (is (= [{:type :t.form.validation/required :field-id "empty phone number"}
+            {:type :t.form.validation/invalid-phone-number :field-id "less than 5 symbols is not allowed"}
             {:type :t.form.validation/invalid-phone-number :field-id "more than 25 symbols is not allowed"}
             {:type :t.form.validation/invalid-phone-number :field-id "number has to start with a + prefix"}
             {:type :t.form.validation/invalid-phone-number :field-id "prefix 0 is not allowed"}]
-           (validate-fields-for-draft [{:field/id       "empty phone number"
-                                        :field/type     :phone-number
-                                        :field/optional false
-                                        :field/visible  true
-                                        :field/value    ""}
-                                       {:field/id       "prefix 0 is not allowed"
-                                        :field/type     :phone-number
-                                        :field/optional false
-                                        :field/visible  true
-                                        :field/value    "+058450000100"}
-                                       {:field/id       "more than 25 symbols is not allowed"
-                                        :field/type     :phone-number
-                                        :field/optional false
-                                        :field/visible  true
-                                        :field/value    "+15005000010000000000000000000000"}
-                                       {:field/id       "less than 5 symbols is not allowed"
-                                        :field/type     :phone-number
-                                        :field/optional true
-                                        :field/visible  true
-                                        :field/value    "+3500"}
-                                       {:field/id       "number has to start with a + prefix"
-                                        :field/type     :phone-number
-                                        :field/optional true
-                                        :field/visible  true
-                                        :field/value    "35000000000000"}
-                                       {:field/id       "valid phone number"
-                                        :field/type     :phone-number
-                                        :field/optional true
-                                        :field/visible  true
-                                        :field/value    "+358450000100"}]))))
+           (validate-fields [{:field/id       "empty phone number"
+                              :field/type     :phone-number
+                              :field/optional false
+                              :field/visible  true
+                              :field/value    ""}
+                             {:field/id       "prefix 0 is not allowed"
+                              :field/type     :phone-number
+                              :field/optional false
+                              :field/visible  true
+                              :field/value    "+058450000100"}
+                             {:field/id       "more than 25 symbols is not allowed"
+                              :field/type     :phone-number
+                              :field/optional false
+                              :field/visible  true
+                              :field/value    "+15005000010000000000000000000000"}
+                             {:field/id       "less than 5 symbols is not allowed"
+                              :field/type     :phone-number
+                              :field/optional true
+                              :field/visible  true
+                              :field/value    "+3500"}
+                             {:field/id       "number has to start with a + prefix"
+                              :field/type     :phone-number
+                              :field/optional true
+                              :field/visible  true
+                              :field/value    "35000000000000"}
+                             {:field/id       "valid phone number"
+                              :field/type     :phone-number
+                              :field/optional true
+                              :field/visible  true
+                              :field/value    "+358450000100"}])))))
 
-  (testing "error: field input invalid ip address"
-    (is (= [{:type :t.form.validation/invalid-ip-address :field-id "invalid ip address"}
-            {:type :t.form.validation/invalid-ip-address-private :field-id "private ip address"}]
-           (validate-fields-for-draft [{:field/id       "empty ip address"
-                                        :field/type     :ip-address
-                                        :field/optional false
-                                        :field/visible  true
-                                        :field/value    ""}
-                                       {:field/id       "invalid ip address"
-                                        :field/type     :ip-address
-                                        :field/optional false
-                                        :field/visible  true
-                                        :field/value    "+058450000100"}
-                                       {:field/id       "correct ip address"
-                                        :field/type     :ip-address
-                                        :field/optional false
-                                        :field/visible  true
-                                        :field/value    "142.250.74.110"}
-                                       {:field/id       "private ip address"
-                                        :field/type     :ip-address
-                                        :field/optional true
-                                        :field/visible  true
-                                        :field/value    "192.0.2.255"}]))))
-
-  (testing "table validation"
-    (let [fields [{:field/id "tbl"
-                   :field/type :table
-                   :field/visible true
-                   :field/optional true
-                   :field/columns [{:key "col1"} {:key "col2"}]}]]
-      (testing "valid cases"
-        (is (nil? (validate-fields-for-draft (assoc-in fields [0 :field/value] []))))
-        (is (nil? (validate-fields-for-draft
-                   (assoc-in fields [0 :field/value] [[{:column "col1" :value "val11"} {:column "col2" :value "val21"}]
-                                                      [{:column "col1" :value "val12"} {:column "col2" :value "val22"}]])))))
-      (testing "invalid cases"
-        ;; we don't need to test cases here that violate the Field
-        ;; schema, schema checking catches those
-        (is (= [{:field-id "tbl", :type :t.form.validation/invalid-value}]
-               (validate-fields-for-draft (assoc-in fields [0 :field/value] ""))))
-        (is (= [{:field-id "tbl", :type :t.form.validation/invalid-value}]
-               (validate-fields-for-draft (assoc-in fields [0 :field/value] "string"))))
-        (is (= [{:field-id "tbl", :type :t.form.validation/invalid-value}]
-               (validate-fields-for-draft
-                (assoc-in fields [0 :field/value] [[{:column "colx" :value "val22"}]]))
-               (validate-fields-for-draft
-                (assoc-in fields [0 :field/value] [[{:column "col1" :value "val11"} {:column "col2" :value "val21"}]
-                                                   [{:column "col1" :value "val12"} {:column "colx" :value "val22"}]])))
-            "unknown columns")
-        (is (= [{:field-id "tbl", :type :t.form.validation/invalid-value}]
-               (validate-fields-for-draft (assoc-in fields [0 :field/value] [[]]))
-               (validate-fields-for-submit (assoc-in fields [0 :field/value] [[{:column "col1" :value "1"}]]))
-               (validate-fields-for-submit (assoc-in fields [0 :field/value] [[{:column "col1" :value "1"}
-                                                                               {:column "col2" :value "1"}
-                                                                               {:column "col3" :value "1"}]])))
-            "missing columns")
-        (is (= [{:field-id "tbl", :type :t.form.validation/column-values-missing}]
-               (validate-fields-for-submit (assoc-in fields [0 :field/value] [[{:column "col1" :value "1"}
-                                                                               {:column "col2" :value ""}]]))
-               (validate-fields-for-submit (assoc-in fields [0 :field/value] [[{:column "col1" :value "1"}
-                                                                               {:column "col2" :value "2"}]
-                                                                              [{:column "col1" :value ""}
-                                                                               {:column "col2" :value "2"}]])))
-            "empty column values")))))