Repository: CSCfi/rems
Pull request #3172 (2023-07-05 15:27:38)

Display if event is shown to applicant in event log
relates #3156 

NB: contains migration (renamed event `:application/public` -> `:event/public`)

events are enriched with new attribute `:event/visibility`, where value can be:
- `:visibility/public` -> shown to all users
- `:visibility/handling-users` -> shown only to users with `see-everything` permission (currently)
- `:event/public` and `:event/visibility` are sensitive information and removed from personalized application without `:see-everything` permission
- events have extra information rendered (eye open / eye closed icon) when `:event/visibility` is present

**other notable changes**

event highlighting feature is removed
added applying/handling user roles to application-util namespace

# 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

## Follow-up
- [x] New tasks are created for pending or remaining tasks #3196

## Screenshots
### Handler
<img width="914" alt="Screenshot 2023-09-15 at 17 02 13" src="https://github.com/CSCfi/rems/assets/794087/8dd3b9d1-aa9c-4c68-84f6-3213f22febac">

#### Tooltip on mouse-over
<img width="356" alt="Screenshot 2023-09-15 at 17 28 50" src="https://github.com/CSCfi/rems/assets/794087/09de5d8e-006e-4261-aced-513076f8f230">

#### Tooltip on mouse-over
<img width="328" alt="Screenshot 2023-09-15 at 17 28 41" src="https://github.com/CSCfi/rems/assets/794087/07fb156c-4075-44fc-86db-ee4cc2a515de">

### Applicant
<img width="1040" alt="Screenshot 2023-09-15 at 16 05 40" src="https://github.com/CSCfi/rems/assets/794087/8fd3d74c-308c-4b92-95cd-8e12637af3cc">



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f2fd800ba..15b223dad 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,11 +8,18 @@ have notable changes.
 
 Changes since v2.33
 
+**NB: This release contains migrations!**
+**NB: `:application/public` is renamed to `:event/public` (in `application_event` table `eventdata` column).**
+
+### Changes
+- "Show related events" has been removed from events. (#3156)
+
 ### Additions
 - (Experimental) Workflow can be configured to enable voting for the approval. Currently all handlers can vote (including bots). Use `:enable-voting`. (#3174)
 - There is now a Danish language translation (#3176). We are considering supporting a limited set of languages officially, and improving support for community maintained translations (see #3179).
 - Added experimental support for named format parameters in translations. (#3183)
 - Cache reloading can be configured using the new `:buzy-hours` config. REMS will then try to avoid reloading during the specified time spans. (#3194)
+- The handling users can now see whether an event is shown to the applicant from a small eye icon in the event history. (#3156)
 
 ### Fixes
 - Email template parameters for `:application-expiration-notification` event are now documented. The parameters are different from standard event email parameters, which may have caused confusion.
diff --git a/project.clj b/project.clj
index 632be1c7f..2e803fc9b 100644
--- a/project.clj
+++ b/project.clj
@@ -24,7 +24,7 @@
                  [javax.xml.bind/jaxb-api "2.4.0-b180830.0359"]
                  [lambdaisland/deep-diff "0.0-47"]
                  [luminus-jetty "0.2.3"]
-                 [luminus-migrations "0.7.1"] ; 0.7.3 switches to next.jdbc, 0.7.5 fails: No such var: prepare/statement
+                 [luminus-migrations "0.7.2"] ; 0.7.3 switches to next.jdbc, 0.7.5 fails: No such var: prepare/statement
                  [luminus-nrepl "0.1.7"]
                  [luminus/ring-ttl-session "0.3.3"]
                  [macroz/hiccup-find "0.6.1"]
diff --git a/resources/migrations/20230912072334-event-public.edn b/resources/migrations/20230912072334-event-public.edn
new file mode 100644
index 000000000..554555617
--- /dev/null
+++ b/resources/migrations/20230912072334-event-public.edn
@@ -0,0 +1,3 @@
+{:ns rems.migrations.event-public
+ :up-fn migrate-up
+ :down-fn migrate-down}
diff --git a/resources/translations/da.edn b/resources/translations/da.edn
index cde88fc46..58cdd76ef 100644
--- a/resources/translations/da.edn
+++ b/resources/translations/da.edn
@@ -196,6 +196,8 @@
                  :empty "Ingen ansøgninger."
                  ;; %1 - date
                  :entitlement-end "Udløbsdato for rettighederne: %1."
+                 :event {:not-shown-to-applicant "Skjult for ansøgere"
+                         :shown-to-applicant "Synlig for ansøgere"}
                  ;; Format parameters for all events:
                  ;; %1 - name of event actor
                  :events {:applicant-changed "%1 ændrede ansøger til %2."
@@ -246,7 +248,6 @@
                           :resources "Ressourcer"
                           :state "Tilstand"}
                  :handlers "Sagsbehandler"
-                 :highlight-related-events "Vis relaterede hændelser."
                  :id "id"
                  :intro [:div.intro [:p "Udfyld og indsend ansøgningen. Du kan invitere andre til at deltage i ansøgningen. Alle deltagere skal acceptere brugsvilkår senest når ansøgningen godkendes."]]
                  :last-activity "Ændret"
diff --git a/resources/translations/en.edn b/resources/translations/en.edn
index a47eec7e3..89458663b 100644
--- a/resources/translations/en.edn
+++ b/resources/translations/en.edn
@@ -196,6 +196,8 @@
                  :empty "No applications."
                  ;; %1 - date
                  :entitlement-end "Access rights end %1."
+                 :event {:not-shown-to-applicant "Not shown to applicant"
+                         :shown-to-applicant "Shown to applicant"}
                  ;; Format parameters for all events:
                  ;; %1 - name of event actor
                  :events {:applicant-changed "%1 changed the applicant to %2."
@@ -246,7 +248,6 @@
                           :resources "Resources"
                           :state "State"}
                  :handlers "Handlers"
-                 :highlight-related-events "Show related events."
                  :id "Id"
                  :intro [:div.intro [:p "Fill out and submit the application. Several applicants can be invited as members of the application, and each applicant has to accept the terms of use to gain access."]]
                  :last-activity "Last activity"
diff --git a/resources/translations/fi.edn b/resources/translations/fi.edn
index 2873f8385..90b6d74b4 100644
--- a/resources/translations/fi.edn
+++ b/resources/translations/fi.edn
@@ -189,6 +189,8 @@
                  :duos {:validation {:missing-duo-codes "DUO-koodit ovat määritetty resursseihin, mutta puuttuvat hakemuksesta:"}}
                  :empty "Ei hakemuksia."
                  :entitlement-end "Pääsyoikeudet loppuvat %1."
+                 :event {:not-shown-to-applicant "Ei näytetä hakijalle"
+                         :shown-to-applicant "Näytetään hakijalle"}
                  :events {:applicant-changed "%1 vaihtoi hakijan henkilöksi %2."
                           :approved "%1 hyväksyi hakemuksen."
                           :attachments-redacted "%1 poisti liitetiedostoja. %2"
@@ -230,7 +232,6 @@
                           :resources "Resurssit"
                           :state "Tila"}
                  :handlers "Käsittelijät"
-                 :highlight-related-events "Näytä liittyvät tapahtumat."
                  :id "Tunniste"
                  :intro [:div.intro [:p "Täytä hakemus ja lähetä se käsiteltäväksi. Hakemukseen voi kutsua jäseniksi useampia hakijoita, joista jokaisen on hyväksyttävä käyttöehdot viimeistään kun hakemus on hyväksytty."]]
                  :last-activity "Muokattu"
diff --git a/resources/translations/sv.edn b/resources/translations/sv.edn
index 5a730f947..31cef5e05 100644
--- a/resources/translations/sv.edn
+++ b/resources/translations/sv.edn
@@ -189,6 +189,8 @@
                  :duos {:validation {:missing-duo-codes "DUO koder anges i resurser, men de saknas från ansökan:"}}
                  :empty "Inga ansökningar."
                  :entitlement-end "Upphörandedatum för rättigheterna: %1."
+                 :event {:not-shown-to-applicant "Dold för sökande"
+                         :shown-to-applicant "Synlig för sökande"}
                  :events {:applicant-changed "%1 bytte sökande till %2." ; TODO check
                           :approved "%1 godkände ansökan."
                           :attachments-redacted "%1 tog bort bilagor. %2"
@@ -230,7 +232,6 @@
                           :resources "Resurser"
                           :state "Tillstånd"}
                  :handlers "Handläggare"
-                 :highlight-related-events "Visa relaterade händelser."
                  :id "id"
                  :intro [:div.intro [:p "Fyll i ansökan och skicka den för behandling. Du kan bjuda in andra att delta i din ansökan. Alla deltagare måste godkänna användarvillkoren senast när ansökan godkänts."]]
                  :last-activity "Ändrat"
diff --git a/src/clj/rems/application/commands.clj b/src/clj/rems/application/commands.clj
index 376dfabdf..10d2330f6 100644
--- a/src/clj/rems/application/commands.clj
+++ b/src/clj/rems/application/commands.clj
@@ -459,7 +459,7 @@
       (add-comment-and-attachments cmd application injections
                                    {:event/type :application.event/attachments-redacted
                                     :event/redacted-attachments (vec (:redacted-attachments cmd))
-                                    :application/public (:public cmd)})))
+                                    :event/public (:public cmd)})))
 
 (defmethod command-handler :application.command/reject
   [cmd application injections]
@@ -535,7 +535,7 @@
   [cmd application injections]
   (add-comment-and-attachments cmd application injections
                                {:event/type :application.event/remarked
-                                :application/public (:public cmd)}))
+                                :event/public (:public cmd)}))
 
 (defmethod command-handler :application.command/add-licenses
   [cmd application injections]
diff --git a/src/clj/rems/application/events.clj b/src/clj/rems/application/events.clj
index b6a060fcb..5dc0e6e50 100644
--- a/src/clj/rems/application/events.clj
+++ b/src/clj/rems/application/events.clj
@@ -121,7 +121,7 @@
   (assoc EventWithComment
          :event/type (s/enum :application.event/attachments-redacted)
          :event/redacted-attachments [EventAttachment]
-         :application/public s/Bool))
+         :event/public s/Bool))
 (s/defschema ApplicantChangedEvent
   (assoc EventWithComment
          :event/type (s/enum :application.event/applicant-changed)
@@ -156,7 +156,7 @@
 (s/defschema RemarkedEvent
   (assoc EventWithComment
          :event/type (s/enum :application.event/remarked)
-         :application/public s/Bool))
+         :event/public s/Bool))
 (s/defschema ResourcesChangedEvent
   (assoc EventWithComment
          :event/type (s/enum :application.event/resources-changed)
diff --git a/src/clj/rems/application/model.clj b/src/clj/rems/application/model.clj
index 3f083eabd..5e90552b9 100644
--- a/src/clj/rems/application/model.clj
+++ b/src/clj/rems/application/model.clj
@@ -5,7 +5,7 @@
             [medley.core :refer [assoc-some distinct-by find-first map-vals update-existing update-existing-in]]
             [rems.application.events :as events]
             [rems.application.master-workflow :as master-workflow]
-            [rems.common.application-util :as application-util]
+            [rems.common.application-util :refer [applicant-and-members can-redact-attachment can-see-everything? is-applying-user?]]
             [rems.common.form :as form]
             [rems.common.util :refer [assoc-some-in conj-vec getx getx-in into-vec]]
             [rems.permissions :as permissions]
@@ -557,10 +557,65 @@
                            (sort-by :license/id))]
     (merge-lists-by :license/id rich-licenses app-licenses)))
 
+(def ^:private sensitive-events #{:application.event/review-requested
+                                  :application.event/reviewed
+                                  :application.event/reviewer-invited
+                                  :application.event/reviewer-joined
+                                  :application.event/decided
+                                  :application.event/decider-invited
+                                  :application.event/decider-joined
+                                  :application.event/decision-requested
+                                  :application.event/voted})
+
+(deftest test-sensitive-events
+  (let [public-events #{:application.event/attachments-redacted
+                        :application.event/applicant-changed
+                        :application.event/approved
+                        :application.event/closed
+                        :application.event/copied-from
+                        :application.event/copied-to
+                        :application.event/created
+                        :application.event/deleted
+                        :application.event/draft-saved
+                        :application.event/external-id-assigned
+                        :application.event/expiration-notifications-sent
+                        :application.event/licenses-accepted
+                        :application.event/licenses-added
+                        :application.event/member-added
+                        :application.event/member-invited
+                        :application.event/member-joined
+                        :application.event/member-removed
+                        :application.event/member-uninvited
+                        :application.event/rejected
+                        :application.event/remarked
+                        :application.event/resources-changed
+                        :application.event/returned
+                        :application.event/revoked
+                        :application.event/submitted}]
+    (is (= #{}
+           (set/intersection sensitive-events public-events)))
+    (is (= #{}
+           (set/difference (set events/event-types)
+                           (set/union public-events sensitive-events)))
+        "seems like a new event has been added; is public or sensitive?")))
+
+(defn get-event-visibility [event]
+  (let [event-public (:event/public event)
+        event-type (:event/type event)]
+    (case event-public
+      true :visibility/public
+      false :visibility/handling-users
+      (cond
+        (contains? sensitive-events event-type)
+        :visibility/handling-users
+
+        :else :visibility/public))))
+
 (defn enrich-event [event get-user get-catalogue-item]
   (let [event-type (:event/type event)]
     (merge event
-           {:event/actor-attributes (get-user (:event/actor event))}
+           {:event/actor-attributes (get-user (:event/actor event))
+            :event/visibility (get-event-visibility event)}
            (case event-type
              :application.event/resources-changed
              {:application/resources (enrich-resources (:application/resources event) get-catalogue-item)}
@@ -613,7 +668,7 @@
            (classify-attachments application)))))
 
 (defn- get-blacklist [application blacklisted?]
-  (let [all-members (application-util/applicant-and-members application)
+  (let [all-members (applicant-and-members application)
         all-resources (distinct (map :resource/ext-id (:application/resources application)))]
     (vec
      (for [member all-members
@@ -773,51 +828,9 @@
 
 ;;;; Authorization
 
-(def ^:private sensitive-events #{:application.event/review-requested
-                                  :application.event/reviewed
-                                  :application.event/reviewer-invited
-                                  :application.event/reviewer-joined
-                                  :application.event/decided
-                                  :application.event/decider-invited
-                                  :application.event/decider-joined
-                                  :application.event/decision-requested
-                                  :application.event/voted})
-(deftest test-sensitive-events
-  (let [public-events #{:application.event/attachments-redacted
-                        :application.event/applicant-changed
-                        :application.event/approved
-                        :application.event/closed
-                        :application.event/copied-from
-                        :application.event/copied-to
-                        :application.event/created
-                        :application.event/deleted
-                        :application.event/draft-saved
-                        :application.event/external-id-assigned
-                        :application.event/expiration-notifications-sent
-                        :application.event/licenses-accepted
-                        :application.event/licenses-added
-                        :application.event/member-added
-                        :application.event/member-invited
-                        :application.event/member-joined
-                        :application.event/member-removed
-                        :application.event/member-uninvited
-                        :application.event/rejected
-                        :application.event/remarked
-                        :application.event/resources-changed
-                        :application.event/returned
-                        :application.event/revoked
-                        :application.event/submitted}]
-    (is (= #{}
-           (set/intersection sensitive-events public-events)))
-    (is (= #{}
-           (set/difference (set events/event-types)
-                           (set/union public-events sensitive-events)))
-        "seems like a new event has been added; is public or sensitive?")))
-
 (defn- hide-sensitive-events [events]
   (->> events
-       (remove (comp sensitive-events :event/type))
-       (remove (comp false? :application/public)))) ; :application/public might not be set
+       (filterv #(= :visibility/public (:event/visibility %)))))
 
 (defn- censor-user [user]
   (select-keys user [:userid :name :email :organizations :notification-email]))
@@ -847,7 +860,8 @@
 (defn- hide-sensitive-information [application]
   (-> application
       (dissoc :application/blacklist)
-      (update :application/events hide-sensitive-events)
+      (update :application/events hide-sensitive-events) ; uses :event/public :event/visibility
+      (update :application/events (partial mapv #(dissoc % :event/public :event/visibility)))
       (update :application/workflow dissoc :workflow.dynamic/handlers)
       (update :application/workflow dissoc :workflow/voting)
       (dissoc :application/votes)
@@ -899,7 +913,7 @@
 
 (defn apply-attachment-permissions [attachment roles userid]
   (-> attachment
-      (assoc-some :attachment/can-redact (application-util/can-redact-attachment attachment roles userid))
+      (assoc-some :attachment/can-redact (can-redact-attachment attachment roles userid))
       (dissoc :attachment/redact-roles)))
 
 (defn hide-non-public-information [application]
@@ -941,15 +955,17 @@
         visible? (comp visible-ids :attachment/id)]
     (update application :application/attachments #(filterv visible? %))))
 
-(defn user-is-applicant-or-member [roles]
-  (or (contains? roles :applicant)
-      (contains? roles :member)))
-
 (defn see-application? [application userid]
-  (let [roles (permissions/user-roles application userid)]
-    (if (= :application.state/draft (:application/state application))
-      (user-is-applicant-or-member roles)
-      (not= #{:everyone-else} roles))))
+  (let [state (:application/state application)
+        roles (permissions/user-roles application userid)]
+    (cond
+      (= :application.state/draft state)
+      (is-applying-user? {:application/roles roles})
+
+      (= #{:everyone-else} roles)
+      false
+
+      :else true)))
 
 (defn apply-user-permissions [application userid]
   (let [roles (permissions/user-roles application userid)
diff --git a/src/clj/rems/email/template.clj b/src/clj/rems/email/template.clj
index 0fb8d4360..31fc7a5c7 100644
--- a/src/clj/rems/email/template.clj
+++ b/src/clj/rems/email/template.clj
@@ -159,7 +159,7 @@
 
 (defmethod event-to-emails :application.event/remarked [event application]
   (emails-to-recipients (concat (handlers application)
-                                (when (:application/public event)
+                                (when (:event/public event)
                                   ;; no need to email members on non-actionable things
                                   [(:application/applicant application)]))
                         event application
diff --git a/src/clj/rems/migrations/event_public.clj b/src/clj/rems/migrations/event_public.clj
new file mode 100644
index 000000000..a52d21e55
--- /dev/null
+++ b/src/clj/rems/migrations/event_public.clj
@@ -0,0 +1,48 @@
+(ns ^{:added "2.34"} rems.migrations.event-public
+  "Migrates event :application/public to :event/public."
+  (:require [clojure.set]
+            [hugsql.core :as hugsql]
+            [rems.json :as json]
+            [rems.db.core]))
+
+(def migration-id 20230912072334)
+
+(declare get-events set-event!)
+(hugsql/def-db-fns-from-string
+  "
+-- :name get-events :? :*
+SELECT id, eventdata::TEXT
+FROM application_event;
+
+-- :name set-event! :!
+UPDATE application_event
+SET eventdata = :eventdata::jsonb
+WHERE id = :id;
+")
+
+(def attributes-up {:application/public :event/public})
+(def attributes-down {:event/public :application/public})
+
+(defn migrate-event [event rename-kmap]
+  (update event :eventdata clojure.set/rename-keys rename-kmap))
+
+(def event-from-db #(update % :eventdata json/parse-string))
+
+(defn migrate-events! [conn xf attributes]
+  (doseq [event (sequence (comp (map event-from-db) xf) (get-events conn))
+          :let [new-event (migrate-event event attributes)]]
+    (set-event! conn
+                (update new-event :eventdata json/generate-string))))
+
+(def old-event? #(boolean? (get-in % [:eventdata :application/public])))
+(def new-event? #(boolean? (get-in % [:eventdata :event/public])))
+
+(defn migrate-up [{:keys [conn]}]
+  (migrate-events! conn (filter old-event?) attributes-up))
+
+(defn migrate-down [{:keys [conn]}]
+  (migrate-events! conn (filter new-event?) attributes-down))
+
+(comment
+  (migrate-up {:conn rems.db.core/*db*})
+  (migrate-down {:conn rems.db.core/*db*}))
diff --git a/src/cljc/rems/common/application_util.cljc b/src/cljc/rems/common/application_util.cljc
index 2838eb2b8..d9310482d 100644
--- a/src/cljc/rems/common/application_util.cljc
+++ b/src/cljc/rems/common/application_util.cljc
@@ -1,5 +1,6 @@
 (ns rems.common.application-util
-  (:require [clojure.test :refer [deftest is]]
+  (:require [clojure.set]
+            [clojure.test :refer [deftest is]]
             [rems.common.util :as util]))
 
 (def states
@@ -37,9 +38,40 @@
 (defn workflow-handlers [application]
   (set (mapv :userid (get-in application [:application/workflow :workflow.dynamic/handlers]))))
 
-(defn is-handler? [application userid]
+(defn is-handler?
+  "Returns true if `userid` is currently workflow handler in `application`."
+  [application userid]
   (contains? (workflow-handlers application) userid))
 
+(def +applying-user-roles+ #{:applicant :member})
+(def +handling-user-roles+ #{:handler :reviewer :decider :past-reviewer :past-decider})
+
+(defn is-applying-user?
+  "Returns true if current user is applying for `application`.
+
+   See `rems.common.application-util/+applying-user-roles+`"
+  [application]
+  (let [roles (set (:application/roles application))
+        applying-roles (clojure.set/intersection +applying-user-roles+ roles)]
+    (some? (seq applying-roles))))
+
+(defn is-handling-user?
+  "Returns true if current user is taking part in handling process of `application` .
+
+   See `rems.common.application-util/+handling-user-roles+`"
+  [application]
+  (let [roles (set (:application/roles application))
+        handling-roles (clojure.set/intersection +handling-user-roles+ roles)]
+    (some? (seq handling-roles))))
+
+(defn can-see-everything?
+  "Returns true if current user has special `:see-everything` permission in `application`.
+
+   See `docs/glossary.md`"
+  [application]
+  (let [permissions (set (:application/permissions application))]
+    (contains? permissions :see-everything)))
+
 (defn- parse-int-maybe [x]
   (if (re-matches #"\d+" x)
     (util/parse-int x)
@@ -50,7 +82,7 @@
   (is (= 100 (parse-int-maybe "100")))
   (is (= "/" (parse-int-maybe "/")))
   (is (= "THL/" (parse-int-maybe "THL/")))
-  (is (= ["THL/" 54 "/" 14  "." 01 "." 00 "/" 2002 "-rems/" 21] (mapv parse-int-maybe (re-seq #"\d+|[^\d]+" "THL/54/14.01.00/2002-rems/21")))))
+  (is (= ["THL/" 54 "/" 14 "." 01 "." 00 "/" 2002 "-rems/" 21] (mapv parse-int-maybe (re-seq #"\d+|[^\d]+" "THL/54/14.01.00/2002-rems/21")))))
 
 (defn parse-sortable-external-id [external-id]
   (when external-id
@@ -59,14 +91,14 @@
 
 (deftest test-parse-sortable-external-id
   (is (= [2020 "/" 10] (parse-sortable-external-id "2020/10")))
-  (is (= ["THL/" 54 "/" 14  "." 01 "." 00 "/" 2002 "-rems/" 21] (parse-sortable-external-id "THL/54/14.01.00/2002-rems/21")))
+  (is (= ["THL/" 54 "/" 14 "." 01 "." 00 "/" 2002 "-rems/" 21] (parse-sortable-external-id "THL/54/14.01.00/2002-rems/21")))
   (is (= ["ABC/" 2000 "." 1] (parse-sortable-external-id "ABC/2000.1")))
   (is (= ["abGtmk"] (parse-sortable-external-id "abGtmk")))
   (is (= nil (parse-sortable-external-id nil)))
   (is (= [[2020 "/" 1] [2020 "/" 2] [2020 "/" 10] [2020 "/" 11] [2020 "/" 12]]
          (sort (mapv parse-sortable-external-id ["2020/10" "2020/1" "2020/11" "2020/12" "2020/2"]))))
-  (is (= [["ABC/" 2000 "." 1] ["ABC/" 2000 "." 2] ["ABC/" 2000 "." 3] ["ABC/" 2002 "." 0] ["ABC/" 2002 "." 2]  ["ABC/" 2302 "." 0]]
-         (sort (mapv parse-sortable-external-id ["ABC/2000.1" "ABC/2002.0" "ABC/2002.2"  "ABC/2302.0" "ABC/2000.2" "ABC/2000.3"])))))
+  (is (= [["ABC/" 2000 "." 1] ["ABC/" 2000 "." 2] ["ABC/" 2000 "." 3] ["ABC/" 2002 "." 0] ["ABC/" 2002 "." 2] ["ABC/" 2302 "." 0]]
+         (sort (mapv parse-sortable-external-id ["ABC/2000.1" "ABC/2002.0" "ABC/2002.2" "ABC/2302.0" "ABC/2000.2" "ABC/2000.3"])))))
 
 (defn can-redact-attachment [attachment roles userid]
   (when-not (:attachment/redacted attachment)
diff --git a/src/cljs/rems/application.cljs b/src/cljs/rems/application.cljs
index 53e51a1aa..daa3e7769 100644
--- a/src/cljs/rems/application.cljs
+++ b/src/cljs/rems/application.cljs
@@ -3,7 +3,7 @@
             [clojure.set :refer [union]]
             [goog.string]
             [re-frame.core :as rf]
-            [medley.core :refer [assoc-some find-first filter-keys filter-vals update-existing]]
+            [medley.core :refer [find-first update-existing]]
             [cljs-time.core :as time-core]
             [rems.actions.accept-licenses :refer [accept-licenses-action-button]]
             [rems.actions.components :refer [button-wrapper]]
@@ -29,7 +29,7 @@
             [rems.actions.vote :refer [vote-action-button vote-form votes-summary]]
             [rems.application-list :as application-list]
             [rems.administration.duo :refer [duo-field duo-info-field]]
-            [rems.common.application-util :as application-util :refer [accepted-licenses? form-fields-editable? get-member-name is-handler?]]
+            [rems.common.application-util :refer [accepted-licenses? can-see-everything? form-fields-editable? get-member-name is-handler? is-handling-user?]]
             [rems.common.attachment-util :as attachment-util]
             [rems.atoms :refer [external-link expander file-download info-field readonly-checkbox document-title success-symbol make-empty-symbol]]
             [rems.common.catalogue-util :refer [catalogue-item-more-info-url]]
@@ -54,19 +54,6 @@
 (defn reload! [application-id & [full-reload?]]
   (rf/dispatch [::fetch-application application-id full-reload?]))
 
-(defn- disabled-items-warning [application]
-  (when (contains? (:application/permissions application) :see-everything) ; don't show to applicants
-    (when-some [resources (->> (:application/resources application)
-                               (filter #(or (not (:catalogue-item/enabled %))
-                                            (:catalogue-item/expired %)
-                                            (:catalogue-item/archived %)))
-                               seq)]
-      [:div.alert.alert-warning
-       (text :t.form/alert-disabled-resources)
-       (into [:ul]
-             (for [resource resources]
-               [:li (localized (:catalogue-item/title resource))]))])))
-
 (defn- blacklist-warning [application]
   (let [resources-by-id (group-by :resource/ext-id (:application/resources application))
         blacklist (:application/blacklist application)]
@@ -125,8 +112,7 @@
             (dissoc ::application
                     ::edit-application
                     ::duo-codes
-                    ::autosaving
-                    ::highlight-event-ids))
+                    ::autosaving))
     :dispatch-n (if (-> db :config :enable-duo)
                   [[::fetch-application id true] [::duo-codes]]
                   [[::fetch-application id true]])}))
@@ -177,45 +163,12 @@
                                   :validation nil
                                   :attachment-status {}})))
 
-(defn- set-highlightables-by-request-id [events]
-  (let [related-events-by-request-id (->> events
-                                          (group-by :application/request-id)
-                                          (filter-keys some?)
-                                          (filter-vals #(<= 2 (count %))))]
-    (for [event events
-          :let [highlightable-event-ids (->> event
-                                             :application/request-id
-                                             (get related-events-by-request-id)
-                                             (into #{} (map :event/id)))]]
-      (update event :highlight-event-ids into highlightable-event-ids))))
-
-(defn- set-highlightables-by-redacted-attachments [events attachments]
-  (let [attachments (build-index {:keys [:attachment/id]} attachments)
-        related-events (for [event events
-                             id (map :attachment/id (:event/redacted-attachments event))
-                             :let [added-in-event-id (get-in attachments [id :attachment/event :event/id])]]
-                         #{added-in-event-id (:event/id event)})]
-    (for [event events
-          :let [highlightable-event-ids (->> related-events
-                                             (filter #(contains? % (:event/id event)))
-                                             (reduce into #{}))]]
-      (update event :highlight-event-ids into highlightable-event-ids))))
-
-(defn- set-application-event-highlightables
-  "Updates events with event ids that are related to the event so that they can
-   be highlighted within the UI."
-  [application]
-  (-> application
-      (update :application/events set-highlightables-by-request-id)
-      (update :application/events set-highlightables-by-redacted-attachments (:application/attachments application))))
-
 (rf/reg-event-fx
  ::fetch-application-result
  (fn [{:keys [db]} [_ application full-reload?]]
    (let [initial-fetch? (not (:initialized? (::application db)))]
      {:db (-> db
               (update ::application fetcher/finished application)
-              (update-in [::application :data] set-application-event-highlightables)
               (cond-> (or initial-fetch? full-reload?) (initialize-edit-application)))})))
 
 (rf/reg-event-db
@@ -489,16 +442,6 @@
      :searching? searching?
      nil (filterv #(not= application-id (:application/id %)) data))))
 
-(rf/reg-event-db
- ::set-highlight-event-ids
- (fn [db [_ event-ids]]
-   (assoc db ::highlight-event-ids event-ids)))
-
-(rf/reg-sub
- ::highlight-event-ids
- (fn [db _]
-   (set (::highlight-event-ids db))))
-
 ;;;; UI components
 
 (defn- pdf-button [app-id]
@@ -714,9 +657,26 @@
        (str prefix " "))
      (application-list/format-application-id config application)]))
 
-(defn- event-view [{:keys [attachments highlight set-highlights]} event]
-  (let [event-text (localize-event event)
-        decision (localize-decision event)
+(defn- event-description [event]
+  (case (:event/visibility event)
+    :visibility/public
+    [:div.row.no-gutters.gap-1
+     [:div.col-sm-auto
+      [:i.fas.fa-eye {:title (text :t.applications.event/shown-to-applicant)}
+       [:span.sr-only (text :t.applications.event/shown-to-applicant)]]]
+     [:b.col-sm (localize-event event)]]
+
+    :visibility/handling-users
+    [:div.row.no-gutters.gap-1
+     [:div.col-sm-auto
+      [:i.fas.fa-eye-slash {:title (text :t.applications.event/not-shown-to-applicant)}
+       [:span.sr-only (text :t.applications.event/not-shown-to-applicant)]]]
+     [:b.col-sm (localize-event event)]]
+
+    [:b (localize-event event)]))
+
+(defn- event-view [{:keys [attachments]} event]
+  (let [decision (localize-decision event)
         comment (case (:event/type event)
                   :application.event/copied-from
                   [application-link (:application/copied-from event) (text :t.applications/application)]
@@ -727,44 +687,24 @@
                   (not-empty (:application/comment event)))
         time (localize-time (:event/time event))]
     [:div.row.event
-     {:class (when highlight
-               "border rounded border-primary")}
      [:label.col-sm-2.col-form-label time]
      [:div.col-sm-10
-      [:div.event-description.d-flex.justify-content-between.col-form-label
-       [:b.col.pl-0 event-text]
-       (when (fn? set-highlights)
-         [:div.d-inline-flex.align-items-start
-          [:a {:href "#" :on-click set-highlights}
-           (text :t.applications/highlight-related-events)]])]
+      [:div.event-description.col-form-label
+       [event-description event]]
       (when decision
         [:div.event-decision decision])
       (when comment
         [:div.form-control.event-comment comment])
       (when (seq attachments)
-        [:div.event-attachments.pt-2
+        [:div.event-attachments.py-2
          [fields/render-attachments attachments]])]]))
 
-(rf/reg-sub
- ::enrich-event-by-id
- :<- [::application]
- :<- [::highlight-event-ids]
- (fn [[application ids] [_ event-id]]
-   {:highlight (contains? ids event-id)
-    :attachments (for [att (:application/attachments application)
-                       :when (= event-id (get-in att [:attachment/event :event/id]))]
-                   att)}))
-
-(defn- render-event [event]
-  (let [opts (merge {:set-highlights (when-some [event-ids (seq (:highlight-event-ids event))]
-                                       (fn []
-                                         (rf/dispatch [::set-highlight-event-ids event-ids])))}
-                    @(rf/subscribe [::enrich-event-by-id (:event/id event)]))]
-    [event-view opts event]))
-
-(defn- render-events [events]
-  (for [e events]
-    [render-event e]))
+(defn- render-events [application events]
+  (let [attachments-by-event-id (->> (:application/attachments application)
+                                     (group-by #(get-in % [:attachment/event :event/id])))]
+    (for [event events]
+      [event-view {:attachments (get attachments-by-event-id (:event/id event))}
+       event])))
 
 (defn- get-application-phases [state]
   (cond (contains? #{:application.state/rejected} state)
@@ -843,54 +783,36 @@
     (localize-time (:application/last-activity application))
     {:inline? true}]])
 
-(defn- application-events [events]
+(defn- application-events [application events]
   (when (seq events)
     (into [:<>
            [:h3.mt-3 (text :t.form/events)]]
-          (render-events events))))
-
-(rf/reg-sub
- ::application-events
- :<- [::application]
- (fn [application _]
-   (->> application
-        :application/events
-        (sort-by :event/time)
-        (reverse))))
+          (render-events application events))))
 
 (defn- application-state [{:keys [application config userid]}]
   (let [state (:application/state application)
-        events @(rf/subscribe [::application-events])
-        [events-show-always events-collapse] (split-at 3 events)]
-    [collapsible/component
-     (-> {:id "header"
-          :title (text :t.applications/state)
-          :always [:div
-                   [:div.mb-3
-                    [phases state (get-application-phases state)]]
-
-                   (when (is-handler? application userid)
-                     [:<>
-                      [application-state-details application config]
-
-                      (when (seq (:application/votes application))
-                        [votes-summary application])
-
-                      [application-events events-show-always]])]
-          :collapse-hidden (when (and (not (is-handler? application userid))
-                                      (seq events))
-                             [:div.my-3
-                              [render-event (first events)]])
-          :collapse (if (is-handler? application userid)
-                      (when (seq events-collapse)
-                        (into [:div]
-                              (render-events events-collapse)))
-
-                      [:<>
-                       [application-state-details application config]
-                       [application-events (concat events-show-always events-collapse)]])
-          :on-close #(rf/dispatch [::set-highlight-event-ids []])} ; remove highlights on toggle close
-         (assoc-some :open? (seq @(rf/subscribe [::highlight-event-ids]))))])) ; show all events on highlight
+        events (sort-by :event/time > (:application/events application))]
+    (if (is-handler? application userid)
+      [collapsible/component {:id "header"
+                              :title (text :t.applications/state)
+                              :always [:div
+                                       [:div.mb-3
+                                        [phases state (get-application-phases state)]]
+                                       [application-state-details application config]
+                                       (when (seq (:application/votes application))
+                                         [votes-summary application])
+                                       [application-events application (take 3 events)]]
+                              :collapse (into [:<>]
+                                              (render-events application (drop 3 events)))}]
+      [collapsible/component {:id "header"
+                              :title (text :t.applications/state)
+                              :always [phases state (get-application-phases state)]
+                              :collapse-hidden (when (seq events)
+                                                 (into [:div.my-3]
+                                                       (render-events application (take 1 events))))
+                              :collapse [:div
+                                         [application-state-details application config]
+                                         [application-events application events]]}])))
 
 (defn member-info
   "Renders a applicant, member or invited member of an application
@@ -1069,10 +991,7 @@
 
 (defn- actions-form [application config]
   (let [app-id (:application/id application)
-        ;; The :see-everything permission is used to determine whether the user
-        ;; is allowed to see all comments. It would not make sense for the user
-        ;; to be able to write a comment which he then cannot see.
-        show-comment-field? (contains? (:application/permissions application) :see-everything)
+        show-comment-field? (is-handling-user? application)
         actions (action-buttons application config)
         reload (partial reload! app-id)
         go-to-catalogue #(do (flash-message/show-default-success! :top [text :t.actions/delete])
@@ -1269,10 +1188,24 @@
                   (mapcat #(get-in % [:resource/duo :duo/codes])))]
     duos))
 
+(defn- disabled-items-warning [application]
+  (when-some [resources (->> (:application/resources application)
+                             (filter #(or (not (:catalogue-item/enabled %))
+                                          (:catalogue-item/expired %)
+                                          (:catalogue-item/archived %)))
+                             seq)]
+    [:div.alert.alert-warning
+     (text :t.form/alert-disabled-resources)
+     (into [:ul]
+           (for [resource resources]
+             [:li (localized (:catalogue-item/title resource))]))]))
+
 (defn- render-application [{:keys [application config userid language]}]
   [:<>
-   [disabled-items-warning application]
-   [blacklist-warning application]
+   (when (can-see-everything? application) ; XXX: should these be shown only to handling users?
+     [disabled-items-warning application])
+   (when (can-see-everything? application) ; XXX: should these be shown only to handling users?
+     [blacklist-warning application])
    (text :t.applications/intro)
    [:div.row
     [:div.col-lg-8.application-content
@@ -1287,7 +1220,7 @@
        (if @(rf/subscribe [::readonly?])
          [:div.mt-3 [application-duo-codes]]
          [:div.mt-3 [edit-application-duo-codes]]))
-     (when (contains? (:application/permissions application) :see-everything)
+     (when (can-see-everything? application) ; XXX: should these be shown only to handling users?
        [:div.mt-3 [previous-applications (get-in application [:application/applicant :userid])]])
      [:div.my-3 [application-licenses application userid]]
      [:div.mt-3 [application-fields application]]]
@@ -1615,20 +1548,6 @@
                              :event/actor-attributes {:name "Hannah Handler"}
                              :application/decision :rejected
                              :application/comment "This application is bad."}])
-   (example "event with comment & decision, highlighted"
-            (let [opts {:highlight true}]
-              [event-view opts {:event/time #inst "2020-01-01T08:35"
-                                :event/type :application.event/decided
-                                :event/actor-attributes {:name "Hannah Handler"}
-                                :application/decision :rejected
-                                :application/comment "This application is bad."}]))
-   (example "event that triggers highlight on other events"
-            (let [opts {:set-highlights #(println "set highlights")}]
-              [event-view opts {:event/time #inst "2020-01-01T08:37"
-                                :event/type :application.event/review-requested
-                                :event/actor-attributes {:name "Hannah Handler"}
-                                :application/reviewers [{:name "Carl Reviewer"}]
-                                :application/comment "Please take a look"}]))
    (example "event with comment & attachment"
             (let [opts {:attachments [{:attachment/filename "verylongfilename_loremipsum_dolorsitamet.pdf"}]}]
               [event-view opts {:event/time #inst "2020-01-01T08:35"


Test patch:
diff --git a/test/clj/rems/api/test_applications.clj b/test/clj/rems/api/test_applications.clj
index 2379bce6a..07d0fe15d 100644
--- a/test/clj/rems/api/test_applications.clj
+++ b/test/clj/rems/api/test_applications.clj
@@ -2,10 +2,10 @@
   (:require [clj-time.core :as time]
             [clojure.java.io :as io]
             [clojure.string :as str]
-            [clojure.test :refer :all]
+            [clojure.test :refer [deftest is testing use-fixtures]]
             [rems.service.attachment :as attachment]
             [rems.service.catalogue :as catalogue]
-            [rems.api.testing :refer :all]
+            [rems.api.testing :refer [api-call api-fixture api-response assert-response-is-ok authenticate get-csrf-token login-with-cookies read-body read-ok-body response-is-forbidden? response-is-not-found? response-is-ok? response-is-payload-too-large? response-is-unauthorized? response-is-unsupported-media-type? transit-body]]
             [rems.config]
             [rems.db.applications]
             [rems.db.attachments]
@@ -16,7 +16,7 @@
             [rems.handler :refer [handler]]
             [rems.json]
             [rems.testing-util :refer [with-fixed-time with-user]]
-            [ring.mock.request :refer :all])
+            [ring.mock.request :refer [header json-body request]])
   (:import java.io.ByteArrayOutputStream
            java.util.zip.ZipInputStream))
 
@@ -1278,7 +1278,6 @@
              (get-in (get-application-for-user app-id user-id)
                      [:application/forms 0 :form/fields 1 :field/previous-value]))))))
 
-
 (deftest test-decider-workflow
   (let [applicant "alice"
         handler "handler"
@@ -1450,7 +1449,6 @@
                  (first (:application/members application))
                  (get-in application [:application/events 2 :application/member]))))))))
 
-
 (deftest test-application-vote
   (let [api-key "42"
         applicant-id "alice"
@@ -2208,7 +2206,6 @@
                    (fetch-zip handler-id "?all=false")
                    (fetch-zip reporter-id "?all=false")))))))))
 
-
 (deftest test-application-api-license-attachments
   (let [api-key "42"
         applicant "alice"
@@ -2741,7 +2738,8 @@
                                     :event/actor "alice"
                                     :event/type "application.event/created"
                                     :event/id 100
-                                    :application/licenses []}
+                                    :application/licenses []
+                                    :event/visibility "visibility/public"}
                                    {:event/id 100
                                     :event/type "application.event/draft-saved"
                                     :event/time "2010-01-01T00:00:00.000Z"
@@ -2749,7 +2747,8 @@
                                     :application/id app-id
                                     :event/actor-attributes {:userid "alice" :name "Alice Applicant" :nickname "In Wonderland" :email "alice@example.com" :organizations [{:organization/id "default"}] :researcher-status-by "so"}
                                     :application/field-values [{:form form-id :field "field-1" :value "raw test"}
-                                                               {:form form-id :field "att" :value (str att-id)}]}]
+                                                               {:form form-id :field "att" :value (str att-id)}]
+                                    :event/visibility "visibility/public"}]
               :application/duo {:duo/matches []}}
              (-> (api-call :get (str "/api/applications/" app-id "/raw") nil
                            api-key reporter)
diff --git a/test/clj/rems/application/test_commands.clj b/test/clj/rems/application/test_commands.clj
index 6d0ab1e4a..f07580d55 100644
--- a/test/clj/rems/application/test_commands.clj
+++ b/test/clj/rems/application/test_commands.clj
@@ -304,7 +304,7 @@
                            :event/actor handler-user-id
                            :application/id app-id
                            :application/comment "handler's remark"
-                           :application/public false
+                           :event/public false
                            :event/attachments [{:attachment/id 1}]})
 
 ;;; Tests
@@ -1699,7 +1699,7 @@
             :event/actor handler-user-id
             :application/id app-id
             :application/comment "handler's remark"
-            :application/public false
+            :event/public false
             :event/attachments [{:attachment/id 1}]}
            (ok-command {:type :application.command/remark
                         :actor handler-user-id
@@ -1722,7 +1722,7 @@
             :event/actor reviewer-user-id
             :application/id app-id
             :application/comment ""
-            :application/public false}
+            :event/public false}
            (ok-command {:type :application.command/remark
                         :actor reviewer-user-id
                         :comment ""
@@ -1736,7 +1736,7 @@
             :event/actor reviewer-user-id
             :application/id app-id
             :application/comment ""
-            :application/public false}
+            :event/public false}
            (ok-command {:type :application.command/remark
                         :actor reviewer-user-id
                         :comment ""
@@ -1902,7 +1902,7 @@
                                          {:attachment/id 5}]
             :application/id app-id
             :application/comment "i've got the power"
-            :application/public false}
+            :event/public false}
            (ok-command {:type :application.command/redact-attachments
                         :actor handler-user-id
                         :comment "i've got the power"
@@ -1944,7 +1944,7 @@
             :event/redacted-attachments [{:attachment/id 3}]
             :application/id app-id
             :application/comment "accidental upload"
-            :application/public false}
+            :event/public false}
            (ok-command {:type :application.command/redact-attachments
                         :actor reviewer-user-id
                         :comment "accidental upload"
@@ -1985,7 +1985,7 @@
             :event/redacted-attachments [{:attachment/id 5}]
             :application/id app-id
             :application/comment ""
-            :application/public false}
+            :event/public false}
            (ok-command {:type :application.command/redact-attachments
                         :actor decider-user-id
                         :comment ""
diff --git a/test/clj/rems/application/test_model.clj b/test/clj/rems/application/test_model.clj
index 42a9f27fa..d9737101a 100644
--- a/test/clj/rems/application/test_model.clj
+++ b/test/clj/rems/application/test_model.clj
@@ -1,7 +1,7 @@
 (ns rems.application.test-model
   (:require [clojure.set :as set]
             [clojure.string :as str]
-            [clojure.test :refer :all]
+            [clojure.test :refer [deftest is testing use-fixtures]]
             [hiccup.core :as hiccup]
             [medley.core :refer [map-vals]]
             [rems.api.schema :as schema]
@@ -491,7 +491,6 @@
 (deftest test-application-view-saved
   (is (= saved-application (recreate saved-application))))
 
-
 (def licenses-accepted-event {:event/type :application.event/licenses-accepted
                               :event/time (DateTime. 2500)
                               :event/actor "applicant"
@@ -1254,7 +1253,8 @@
                                 :application/forms [{:form/id 40}]
                                 :workflow/id 50
                                 :workflow/type :workflow/master
-                                :application/licenses [{:license/id 30} {:license/id 31} {:license/id 32}]}
+                                :application/licenses [{:license/id 30} {:license/id 31} {:license/id 32}]
+                                :event/visibility :visibility/public}
                                {:event/type :application.event/draft-saved
                                 :application/id 1
                                 :event/time (DateTime. 2000)
@@ -1263,24 +1263,28 @@
                                                            {:form 40 :field "42" :value "bar"}
                                                            {:form 40 :field "43" :value "private answer"}
                                                            {:form 40 :field "field-does-not-exist" :value "something"}]
-                                :event/actor-attributes {:userid "applicant" :email "applicant@example.com" :name "Applicant" :secret "secret"}}
+                                :event/actor-attributes {:userid "applicant" :email "applicant@example.com" :name "Applicant" :secret "secret"}
+                                :event/visibility :visibility/public}
                                {:event/type :application.event/licenses-accepted
                                 :application/id 1
                                 :event/time (DateTime. 2500)
                                 :event/actor "applicant"
                                 :application/accepted-licenses #{30 31 32}
-                                :event/actor-attributes {:userid "applicant" :email "applicant@example.com" :name "Applicant" :secret "secret"}}
+                                :event/actor-attributes {:userid "applicant" :email "applicant@example.com" :name "Applicant" :secret "secret"}
+                                :event/visibility :visibility/public}
                                {:event/type :application.event/submitted
                                 :application/id 1
                                 :event/time (DateTime. 3000)
                                 :event/actor "applicant"
-                                :event/actor-attributes {:userid "applicant" :email "applicant@example.com" :name "Applicant" :secret "secret"}}
+                                :event/actor-attributes {:userid "applicant" :email "applicant@example.com" :name "Applicant" :secret "secret"}
+                                :event/visibility :visibility/public}
                                {:event/type :application.event/approved
                                 :application/id 1
                                 :event/time (DateTime. 4000)
                                 :event/actor "handler"
                                 :application/comment "looks good"
-                                :event/actor-attributes {:userid "handler" :email "handler@example.com" :name "Handler" :secret "secret"}}]
+                                :event/actor-attributes {:userid "handler" :email "handler@example.com" :name "Handler" :secret "secret"}
+                                :event/visibility :visibility/public}]
           :application/user-roles {"handler" #{:handler}, "reporter1" #{:reporter}}
           :application/role-permissions nil
           :application/description "foo"
@@ -1331,6 +1335,7 @@
             :event/time (DateTime. 1)
             :event/actor "applicant"
             :event/actor-attributes {:userid "applicant" :email "applicant@example.com" :name "Applicant" :secret "secret"}
+            :event/visibility :visibility/public
             :application/id 1
             :application/resources [{:catalogue-item/id 10
                                      :resource/id 11
@@ -1366,6 +1371,7 @@
             :event/time (DateTime. 1)
             :event/actor "handler"
             :event/actor-attributes {:userid "handler" :email "handler@example.com" :name "Handler" :secret "secret"}
+            :event/visibility :visibility/handling-users
             :application/id 1
             :application/deciders [{:userid "decider" :email "decider@example.com" :name "Decider" :secret "secret"}
                                    {:userid "reviewer" :email "reviewer@example.com" :name "Reviewer" :secret "secret"}]}
@@ -1380,6 +1386,7 @@
             :event/time (DateTime. 1)
             :event/actor "handler"
             :event/actor-attributes {:userid "handler" :email "handler@example.com" :name "Handler" :secret "secret"}
+            :event/visibility :visibility/handling-users
             :application/id 1
             :application/reviewers [{:userid "decider" :email "decider@example.com" :name "Decider" :secret "secret"}
                                     {:userid "reviewer" :email "reviewer@example.com" :name "Reviewer" :secret "secret"}]}
@@ -1394,6 +1401,7 @@
             :event/time (DateTime. 1)
             :event/actor "handler"
             :event/actor-attributes {:userid "handler" :email "handler@example.com" :name "Handler" :secret "secret"}
+            :event/visibility :visibility/public
             :application/id 1
             :application/member {:userid "member" :email "member@example.com" :name "Member" :secret "secret"}}
            (model/enrich-event {:event/type :application.event/member-added
@@ -1407,6 +1415,7 @@
             :event/time (DateTime. 1)
             :event/actor "handler"
             :event/actor-attributes {:userid "handler" :email "handler@example.com" :name "Handler" :secret "secret"}
+            :event/visibility :visibility/public
             :application/id 1
             :application/member {:userid "member" :email "member@example.com" :name "Member" :secret "secret"}}
            (model/enrich-event {:event/type :application.event/member-removed
@@ -1635,11 +1644,11 @@
                           (model/application-view {:event/type :application.event/remarked
                                                    :application/comment "this is public"
                                                    :event/actor "bob"
-                                                   :application/public true})
+                                                   :event/public true})
                           (model/application-view {:event/type :application.event/remarked
                                                    :application/comment "this is private"
                                                    :event/actor "smith"
-                                                   :application/public false}))
+                                                   :event/public false}))
           enriched (-> application
                        (permissions/update-role-permissions {:role-1 #{:see-everything}})
                        (model/enrich-with-injections injections))]
diff --git a/test/clj/rems/email/test_template.clj b/test/clj/rems/email/test_template.clj
index 432e2b4e1..07cded975 100644
--- a/test/clj/rems/email/test_template.clj
+++ b/test/clj/rems/email/test_template.clj
@@ -205,7 +205,7 @@
   (let [mails (emails base-events {:application/id 7
                                    :event/type :application.event/remarked
                                    :event/actor "remarker"
-                                   :application/public true
+                                   :event/public true
                                    :application/comment "remark!"})]
     (is (= #{"assistant" "handler" "applicant"} (email-recipients mails)))
     (is (= {:to-user "applicant"
diff --git a/test/clj/rems/test_browser.clj b/test/clj/rems/test_browser.clj
index 542e28323..51075204c 100644
--- a/test/clj/rems/test_browser.clj
+++ b/test/clj/rems/test_browser.clj
@@ -909,7 +909,7 @@
              (-> (get-application-from-api (btu/context-getx :application-id) "developer")
                  :application/events
                  last
-                 (dissoc :event/id :event/time :event/attachments :event/actor-attributes)))))))
+                 (dissoc :event/id :event/time :event/attachments :event/actor-attributes :event/visibility)))))))
 
 (deftest test-invite-decider
   (testing "create test data"
@@ -1116,7 +1116,7 @@
              (-> (get-application-from-api (btu/context-getx :application-id) "developer")
                  :application/events
                  last
-                 (dissoc :event/id :event/time :event/attachments :event/actor-attributes)))))))
+                 (dissoc :event/id :event/time :event/attachments :event/actor-attributes :event/visibility)))))))
 
 (deftest test-guide-page
   (btu/with-postmortem
@@ -1282,7 +1282,6 @@
       (is (some #(= (btu/context-getx :resid) (get % "title"))
                 (slurp-rows :resources))))))
 
-
 (defn create-form []
   (testing "create form"
     (btu/with-postmortem
diff --git a/test/clj/rems/test_migrations.clj b/test/clj/rems/test_migrations.clj
new file mode 100644
index 000000000..fe250aec6
--- /dev/null
+++ b/test/clj/rems/test_migrations.clj
@@ -0,0 +1,81 @@
+(ns ^:integration rems.test-migrations
+  (:require [clojure.test :refer [deftest is testing use-fixtures]]
+            [hugsql.core :as hugsql]
+            [luminus-migrations.core]
+            [luminus-migrations.util]
+            [migratus.core]
+            [rems.db.core]
+            [rems.db.testing :refer [test-db-fixture reset-db-fixture rollback-db-fixture]]
+            [rems.config]
+            [rems.json :as json]
+            [rems.migrations.event-public :as event-public]))
+
+(use-fixtures
+  :once
+  test-db-fixture
+  reset-db-fixture)
+
+(use-fixtures
+  :each
+  rollback-db-fixture)
+
+(defn- migratus-config []
+  (let [db-url (:test-database-url rems.config/env)]
+    {:store :database
+     :db {:connection-uri (luminus-migrations.util/to-jdbc-uri db-url)}}))
+
+(defn- rollback-until-just-before [migration-id]
+  (let [config (migratus-config)]
+    (migratus.core/migrate-until-just-before config migration-id)
+    (migratus.core/rollback-until-just-after config migration-id)
+    (migratus.core/rollback config)))
+
+;; XXX: db-fn always returns seq unlike def-db-fns-from-string ?
+(defn create-db-fn [sql]
+  (partial (hugsql/db-fn sql) rems.db.core/*db*))
+
+(deftest test-event-public
+  (let [create-application!
+        (create-db-fn
+         "-- :name create-application! :insert
+          INSERT INTO catalogue_item_application (id)
+          VALUES (nextval ('catalogue_item_application_id_seq'))
+          RETURNING id;
+          ")
+        add-application-event!
+        (create-db-fn
+         "-- :name add-application-event! :returning-execute :1
+          INSERT INTO application_event (appId, eventData)
+          VALUES (:application, :eventdata::jsonb)
+          RETURNING id, eventData::TEXT;
+          ")
+        get-events #(->> (event-public/get-events rems.db.core/*db*)
+                         (sort-by :id))]
+    (testing "create test data"
+      (rollback-until-just-before event-public/migration-id)
+      (let [app (first (create-application!))
+            _ (is (:id app))
+            create-event #(do {:application (:id app) :eventdata (json/generate-string %)})]
+        (is (:id (first (add-application-event! (create-event {:application/public true})))))
+        (is (:id (first (add-application-event! (create-event {:application/public false})))))
+        (is (:id (first (add-application-event! (create-event {:event/actor "alice"})))))
+        (is (= [{:application/public true}
+                {:application/public false}
+                {:event/actor "alice"}]
+               (->> (get-events)
+                    (mapv (comp json/parse-string :eventdata)))))))
+    (testing "migrate up"
+      (event-public/migrate-up {:conn rems.db.core/*db*})
+      (is (= [{:event/public true}
+              {:event/public false}
+              {:event/actor "alice"}]
+             (->> (get-events)
+                  (mapv (comp json/parse-string :eventdata))))))
+    (testing "migrate down"
+      (event-public/migrate-down {:conn rems.db.core/*db*})
+      (is (= [{:application/public true}
+              {:application/public false}
+              {:event/actor "alice"}]
+             (->> (get-events)
+                  (mapv (comp json/parse-string :eventdata))))))))
+