diff --git a/CHANGELOG.md b/CHANGELOG.md index 79a0a0e831..72e12ced6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,12 +8,21 @@ have notable changes. Changes since v2.11 -### Additions -- Event notifications over HTTP. See [docs/event-notification.md](docs/event-notification.md) for details. (#2095) +### Changes +- Improvements to PDFs (#2114) + - show attachment file names + - list instead of table for events + - hide draft-saved events + - vertical space around form fields + - PDF button moved to Actions pane ### Fixes - Long attachment filenames are now truncated in the UI (#2118) +### Additions +- `api/applications/:id/attachments` api for downloading all attachments as a zip file (#2075) +- Event notifications over HTTP. See [docs/event-notification.md](docs/event-notification.md) for details. (#2095) + ## v2.11 "Kotitontuntie" 2020-04-07 ### Additions diff --git a/dev-config.edn b/dev-config.edn index 019dfad74a..88a1cd9021 100644 --- a/dev-config.edn +++ b/dev-config.edn @@ -23,7 +23,9 @@ :translations {:fi {:title "Info" :filename "about-fi.md"} :en {:title "About" - :filename "about-en.md"}}}] + :filename "about-en.md"} + :sv {:title "Info" + :filename "about-sv.md"}}}] :extra-pages-path "./test-data/extra-pages" :application-deadline-days 4 :enable-pdf-api true diff --git a/docs/architecture/010-transactions.md b/docs/architecture/010-transactions.md index 633ffc795e..4a8faa8246 100644 --- a/docs/architecture/010-transactions.md +++ b/docs/architecture/010-transactions.md @@ -1,6 +1,6 @@ # 010: Database transactions -Authors: @opqdonut +Authors: @opqdonut @Macroz ## Introduction diff --git a/docs/architecture/011-api-key-access-control.md b/docs/architecture/011-api-key-access-control.md new file mode 100644 index 0000000000..b3e8819fe3 --- /dev/null +++ b/docs/architecture/011-api-key-access-control.md @@ -0,0 +1,92 @@ +# 011: API Key Access Control + +Authors: @opqdonut @Macroz @foxlynx + +## Background + +REMS access control has always been user-based. For example all +application commands must have an actor, and the code checks whether +this actor is allowed to perform this command. API authentication has +been done with two headers: `x-rems-api-key` which must be a valid api +key, and `x-rems-user-id`, which specifies which user is being +impersonated. + +Some users have had a need to limit the access available with API +keys. In response, we added a list of allowed roles to each API key. +The roles were a whitelist, with which the roles of the impersonated +user were filtered. + +This approach works fine for so-called _explicit roles_ that are +granted using the `role` table in the database. However, we also have +_implicit roles_ that are granted per application, based on the +application state. For example, a user has the `:reviewer` role for an +application if they have been invited to review the application. When +the API key roles were implemented, they were (accidentally?) only +implemented for explicit roles. Extending the implementation for +implicit roles would require more code. + +As a concrete example, an API key with only the role `:logged-in` +would still be able to impersonate the handler of an application and +perform handler commands like approving the application. + +In order to support the use cases at the end of this document, we need +to either fix the interaction between API key roles & implicit roles, +or choose a new approach. + +## Proposed approach + +Let's dismantle the API key role support, and instead associate with +each API key: + +- an optional whitelist of HTTP paths and methods that the API key is allowed to access +- an optional whitelist of users that the API key is allowed to impersonate + +## Use cases + +Here are some use cases for API key access control, and a comparison +of the current approach with the proposed approach. + +These are from the NKR project, see +[Issue #1910](https://github.com/CSCfi/rems/issues/1910). + +### Creating users and applications + +Use case: a proxy service creates a user, and and applies for a +resource as the created user. + +Current approach: API key with the `:user-owner` and `:applicant` +roles. Additional work would be required to actually support the +`:applicant` role for an API key (it's an implicit role). + +Proposed approach: Two API keys: + +- One API key for creating users. It can only impersonate a fixed user + with the `:user-owner` role. (Can also be limited to the + `/api/users/create` endpoint.) +- Another API key for creating applications. It can impersonate + anyone, but is limited to the `/api/application/create`, + `/api/application/save-draft` and `/api/application/submit` + endpoints. + +### Fetching entitlements and applications + +Use case: fetching entitlements and applications for any applicant. + +Current approach: API key with the `:reporter` role. + +Proposed approach: API key associated with a user with the `:reporter` +role. (Additional path restrictions possible if needed.) + +### Closing applications + +Use case: closing applications (and thus ending entitlements) that are no longer needed. + +Current approach: A new `:application-closer` role, and an API key +associated with it. Also needs fixes to the handling of implicit +roles. + +Proposed approach: An API key associated with a user that is a handler +for suitable workflows. The API key is limited to the +`/api/applications/close` POST request, and some additional GET +requests as needed. + diff --git a/docs/configuration.md b/docs/configuration.md index 5ddadf8d6a..c5f40ca7ed 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -81,7 +81,7 @@ create the following en.edn file to the new translations folder. Custom themes can be used by creating a file, for example `my-custom-theme.edn`, and specifying its location in the `:theme-path` configuration parameter. The theme file can override some or all of the theme attributes (see `:theme` in [config-defaults.edn](https://github.com/CSCfi/rems/blob/master/resources/config-defaults.edn)). Static resources can be placed in a `public` directory next to the theme configuration file. See [example-theme/theme.edn](https://github.com/CSCfi/rems/blob/master/example-theme/theme.edn) for an example. -To quickly validate that all UI components look right navigate to `/guide`. See it in action at . +To quickly validate that all UI components look right navigate to `/guide`. See it in action at . ## Extra pages diff --git a/docs/linking.md b/docs/linking.md index 5af8cec6d1..9283f93cb0 100644 --- a/docs/linking.md +++ b/docs/linking.md @@ -5,25 +5,25 @@ REMS supports linking users directly to application forms, pre-existing applicat ## Linking into catalogue ``` -https://rems2demo.csc.fi/catalogue +https://rems-demo.rahtiapp.fi/catalogue ``` ## Linking into a new application -This application has items with catalogue item ids 2 and 3. +This creates a draft for an application for catalogue item ids 2 and 3. ``` -https://rems2demo.csc.fi/application?items=2,3 +https://rems-demo.rahtiapp.fi/application?items=2,3 ``` If only the resource ID is known, this will find out which catalogue item matches it and will redirect to the new application page for it. ``` -https://rems2demo.csc.fi/apply-for?resource=urn:nbn:fi:lb-123456789 +https://rems-demo.rahtiapp.fi/apply-for?resource=urn:nbn:fi:lb-123456789 ``` ## Linking into an existing application ``` -https://rems2demo.csc.fi/application/2 +https://rems-demo.rahtiapp.fi/application/2 ``` diff --git a/docs/usingtheapi.md b/docs/usingtheapi.md index 06ccd6661b..357ea1b14f 100644 --- a/docs/usingtheapi.md +++ b/docs/usingtheapi.md @@ -3,7 +3,7 @@ These examples assume that the REMS instance you want to talk to is running locally at `localhost:3000`. ## Authentication - + To call the API programmatically, you will first need to add an API key to the `api_key` database table. The API key must be provided in the `x-rems-api-key` header when calling the API. Some API endpoints also require `x-rems-user-id` header to contain the REMS user ID for the user that is being represented, i.e. the user which applies for a resource or approves an application. @@ -52,6 +52,6 @@ Returns the list of catalogue items as a JSON response: ## Learn More -See the [REMS API documentation](https://rems2demo.csc.fi/swagger-ui) for a list of all available operations. +See the [REMS API documentation](https://rems-demo.rahtiapp.fi/swagger-ui) for a list of all available operations. -You may also inspect what API request the REMS UI does using your web browser's developer tools. The REMS UI does its requests using the `application/transit+json` content-type, but all the APIs work also using `application/json` (which is the default). +You may also inspect what API request the REMS UI does using your web browser's developer tools. The REMS UI does its requests using the `application/transit+json` content-type, but all the APIs work also using `application/json` (which is the default). diff --git a/example-theme/theme.edn b/example-theme/theme.edn index 0505cba190..b0d06391f7 100644 --- a/example-theme/theme.edn +++ b/example-theme/theme.edn @@ -35,6 +35,8 @@ :logo-name-sm-fi "rems_logo_fi.png" :logo-name-en "rems_logo_en.png" :logo-name-sm-en "rems_logo_en.png" + :logo-name-sv "rems_logo_sv.png" + :logo-name-sm-sv "rems_logo_sv.png" :logo-content-origin "initial" :phase-color "#111" :phase-bgcolor "#f8f8f8" diff --git a/mkdocs.yml b/mkdocs.yml deleted file mode 100644 index cc7f3f356d..0000000000 --- a/mkdocs.yml +++ /dev/null @@ -1,13 +0,0 @@ -site_name: REMS2 Documentation -pages: - - Home: index.md - - User guide: - - Linking to REMS: linking.md - - Dictionary: dictionary.md - - Developer guide: - - Using the API: usingtheapi.md - - Architecture: architecture.md - - Configuration: configuration.md - - Hooks: hooks.md -theme: - name: readthedocs diff --git a/project.clj b/project.clj index 5b80e06941..f48b1efe63 100644 --- a/project.clj +++ b/project.clj @@ -7,7 +7,7 @@ [ch.qos.logback/logback-classic "1.2.3"] [clj-commons/secretary "1.2.4"] [clj-http "3.10.0"] - [clj-pdf "2.4.0"] + [clj-pdf "2.4.3"] [clj-time "0.15.2"] [cljs-ajax "0.8.0"] [cljsjs/react "16.9.0-1"] diff --git a/resources/public/img/rems_logo_sv.png b/resources/public/img/rems_logo_sv.png new file mode 100644 index 0000000000..87cb869aad Binary files /dev/null and b/resources/public/img/rems_logo_sv.png differ diff --git a/resources/translations/en.edn b/resources/translations/en.edn index a7b40c1a6f..d999220db6 100644 --- a/resources/translations/en.edn +++ b/resources/translations/en.edn @@ -319,15 +319,14 @@ :alert-disabled-resources "Resources no longer available:" :application "Application" :attachment-remove "Remove" + :attachments "Attachments" :checkbox-checked "Yes" :checkbox-unchecked "No" :comment "Comment" :copy-as-new "Copy as a new application" :current-value "Current" - :date "Time" :diff-hide "Hide changes" :diff-show "Show changes" - :event "Event" :events "Events" :failed "Failed" :has-accepted-licenses "Terms of use accepted." diff --git a/resources/translations/fi.edn b/resources/translations/fi.edn index afe107bace..bd2980cc52 100644 --- a/resources/translations/fi.edn +++ b/resources/translations/fi.edn @@ -65,7 +65,7 @@ :create-workflow "Uusi työvuo" :created "Luotu" :disable "Poista käytöstä" - :disabled-and-archived-explanation "Käytöstä poistaminen piilottaa kielivaran käyttäjiltä. Arkistoiminen piilottaa sen myös ylläpitonäkymästä." + :disabled-and-archived-explanation "Käytöstä poistaminen piilottaa asian käyttäjiltä. Arkistoiminen piilottaa sen myös ylläpitonäkymästä." :display-archived "Näytä arkistoidut" :edit "Muokkaa" :edit-catalogue-item "Muokkaa aineistoa" @@ -317,15 +317,14 @@ :alert-disabled-resources "Resurssit eivät ole enää saatavilla:" :application "Tiedot" :attachment-remove "Poista" + :attachments "Liitteet" :checkbox-checked "Kyllä" :checkbox-unchecked "Ei" :comment "Kommentti" :copy-as-new "Kopioi uudeksi hakemukseksi" :current-value "Nykyinen" - :date "Aika" :diff-hide "Piilota muutokset" :diff-show "Näytä muutokset" - :event "Tapahtuma" :events "Tapahtumat" :failed "Epäonnistui" :has-accepted-licenses "Käyttöehdot hyväksytty." diff --git a/resources/translations/sv.edn b/resources/translations/sv.edn index 324ab8c66e..7081a54e8b 100644 --- a/resources/translations/sv.edn +++ b/resources/translations/sv.edn @@ -65,7 +65,7 @@ :create-workflow "Nytt arbetsflöde" :created "Skapad" :disable "Avaktivera" - :disabled-and-archived-explanation "Avaktiveringen döljer resursen för användarna. Arkiveringen döljer den också från administrationsvyn." + :disabled-and-archived-explanation "Avaktiveringen döljer föremål för användarna. Arkiveringen döljer den också från administrationsvyn." :display-archived "Visa arkiverade" :edit "Ändra" :edit-catalogue-item "Ändra katalogpost" @@ -317,15 +317,14 @@ :alert-disabled-resources "Resurser som inte längre är tillgängliga:" :application "Ansökning" :attachment-remove "Ta bort" + :attachments "Bilagor" :checkbox-checked "Ja" :checkbox-unchecked "Nej" :comment "Komment" :copy-as-new "Kopiera som nytt" :current-value "Nuvarande" - :date "Datum" :diff-hide "Dölja ändringar" :diff-show "Visa ändringar" - :event "Händelse" :events "Händelser" :failed "Misslyckades" :has-accepted-licenses "Har accepterat licenserna." diff --git a/src/clj/rems/api/applications.clj b/src/clj/rems/api/applications.clj index cfe7955924..851ae4ef96 100644 --- a/src/clj/rems/api/applications.clj +++ b/src/clj/rems/api/applications.clj @@ -266,6 +266,16 @@ (ok app) (api-util/not-found-json-response))) + (GET "/:application-id/attachments" [] + :summary "Get all attachments as a zip file" + :roles #{:logged-in} + :path-params [application-id :- (describe s/Int "application id")] + :responses {200 {} + 404 {:schema s/Str :description "Not found"}} + (if-let [app (applications/get-application-for-user (getx-user-id) application-id)] + (attachment/zip-attachments app) + (api-util/not-found-json-response))) + (GET "/:application-id/experimental/pdf" request :summary "PDF export of application (EXPERIMENTAL)" :roles #{:logged-in :api-key} diff --git a/src/clj/rems/api/services/attachment.clj b/src/clj/rems/api/services/attachment.clj index 90081d949f..221e76b875 100644 --- a/src/clj/rems/api/services/attachment.clj +++ b/src/clj/rems/api/services/attachment.clj @@ -1,13 +1,16 @@ (ns rems.api.services.attachment (:require [clojure.set :as set] - [clojure.test :refer :all] + [clojure.tools.logging :as log] [rems.application.commands :as commands] [rems.common.application-util :as application-util] [rems.auth.util :refer [throw-forbidden]] [rems.db.applications :as applications] [rems.db.attachments :as attachments] - [ring.util.http-response :refer [ok content-type header]]) - (:import [java.io ByteArrayInputStream])) + [rems.util :refer [getx]] + [ring.util.http-response :refer [ok content-type header]] + [ring.util.io :as ring-io]) + (:import [java.io ByteArrayInputStream ByteArrayOutputStream] + [java.util.zip ZipOutputStream ZipEntry ZipException])) (defn download [attachment] (-> (ok (ByteArrayInputStream. (:attachment/data attachment))) @@ -41,3 +44,23 @@ (:application/permissions application)) (throw-forbidden)) (attachments/save-attachment! file user-id application-id))) + +(defn zip-attachments [application] + (let [zip-input (ring-io/piped-input-stream + (fn [out] + (with-open [zip (ZipOutputStream. out)] + (doseq [metadata (getx application :application/attachments)] + (let [id (getx metadata :attachment/id) + attachment (attachments/get-attachment id)] + ;; we deduplicate filenames when uploading, but here's a + ;; failsafe in case we have duplicate filenames in old + ;; applications + (try + (.putNextEntry zip (ZipEntry. (getx attachment :attachment/filename))) + (.write zip (getx attachment :attachment/data)) + (.closeEntry zip) + (catch ZipException e + (log/warn "Ignoring attachment" (pr-str metadata) "when generating zip. Cause:" e))))))))] + (-> (ok zip-input) + (header "Content-Disposition" (str "attachment;filename=attachments-" (getx application :application/id) ".zip")) + (content-type "application/zip")))) diff --git a/src/clj/rems/db/applications.clj b/src/clj/rems/db/applications.clj index 4303a9e589..c76b4daa2d 100644 --- a/src/clj/rems/db/applications.clj +++ b/src/clj/rems/db/applications.clj @@ -4,6 +4,7 @@ [clojure.java.jdbc :as jdbc] [clojure.set :as set] [clojure.test :refer [deftest is]] + [clojure.tools.logging :as log] [conman.core :as conman] [medley.core :refer [map-vals]] [mount.core :as mount] @@ -259,10 +260,12 @@ (csv/applications-to-csv filtered-applications user-id))) (defn reload-cache! [] + (log/info "Start rems.db.applications/reload-cache!") (empty-injections-cache!) ;; TODO: Here is a small chance that a user will experience a cache miss. Consider rebuilding the cache asynchronously and then `reset!` the cache. (events-cache/empty! all-applications-cache) - (refresh-all-applications-cache!)) + (refresh-all-applications-cache!) + (log/info "Finished rems.db.applications/reload-cache!")) ;; empty the cache occasionally in case some of the injected entities are changed (mount/defstate all-applications-cache-reloader diff --git a/src/clj/rems/db/attachments.clj b/src/clj/rems/db/attachments.clj index 7963dda814..82e6c394cf 100644 --- a/src/clj/rems/db/attachments.clj +++ b/src/clj/rems/db/attachments.clj @@ -1,5 +1,7 @@ (ns rems.db.attachments - (:require [rems.common.application-util :refer [form-fields-editable?]] + (:require [clojure.string :as str] + [clojure.test :refer :all] + [rems.common.application-util :refer [form-fields-editable?]] [rems.common.attachment-types :as attachment-types] [rems.auth.util :refer [throw-forbidden]] [rems.db.core :as db] @@ -11,18 +13,6 @@ (when-not (attachment-types/allowed-extension? filename) (throw (InvalidRequestException. (str "Unsupported extension: " filename))))) -(defn save-attachment! - [{:keys [tempfile filename content-type]} user-id application-id] - (check-allowed-attachment filename) - (let [byte-array (file-to-bytes tempfile) - id (:id (db/save-attachment! {:application application-id - :user user-id - :filename filename - :type content-type - :data byte-array}))] - {:id id - :success true})) - (defn get-attachment [attachment-id] (when-let [{:keys [modifieruserid type appid filename data]} (db/get-attachment {:id attachment-id})] (check-allowed-attachment filename) @@ -47,6 +37,49 @@ :attachment/filename filename :attachment/type type}))) +(defn- add-postfix [filename postfix] + (if-let [i (str/last-index-of filename \.)] + (str (subs filename 0 i) postfix (subs filename i)) + (str filename postfix))) + +(deftest test-add-postfix + (is (= "foo (1).txt" + (add-postfix "foo.txt" " (1)"))) + (is (= "foo_bar_quux (1)" + (add-postfix "foo_bar_quux" " (1)"))) + (is (= "foo.bar!.quux" + (add-postfix "foo.bar.quux" "!"))) + (is (= "!" + (add-postfix "" "!")))) + +(defn- fix-filename [filename existing-filenames] + (let [exists? (set existing-filenames) + versions (cons filename + (map #(add-postfix filename (str " (" (inc %) ")")) + (range)))] + (first (remove exists? versions)))) + +(deftest test-fix-filename + (is (= "file.txt" + (fix-filename "file.txt" ["file.pdf" "picture.gif"]))) + (is (= "file (1).txt" + (fix-filename "file.txt" ["file.txt" "boing.txt"]))) + (is (= "file (2).txt" + (fix-filename "file.txt" ["file.txt" "file (1).txt" "file (3).txt"])))) + +(defn save-attachment! + [{:keys [tempfile filename content-type]} user-id application-id] + (check-allowed-attachment filename) + (let [byte-array (file-to-bytes tempfile) + filename (fix-filename filename (mapv :attachment/filename (get-attachments-for-application application-id))) + id (:id (db/save-attachment! {:application application-id + :user user-id + :filename filename + :type content-type + :data byte-array}))] + {:id id + :success true})) + (defn copy-attachment! [new-application-id attachment-id] (let [attachment (db/get-attachment {:id attachment-id})] (:id (db/save-attachment! {:application new-application-id diff --git a/src/clj/rems/db/test_data.clj b/src/clj/rems/db/test_data.clj index 5d9fa23eb5..acfe7f24e5 100644 --- a/src/clj/rems/db/test_data.clj +++ b/src/clj/rems/db/test_data.clj @@ -244,7 +244,7 @@ :actor actor :time (or time (time/now))}) -(defn fill-form! [{:keys [application-id actor field-value optional-fields] :as command}] +(defn fill-form! [{:keys [application-id actor field-value optional-fields attachment] :as command}] (let [app (applications/get-application-for-user actor application-id)] (command! (assoc (base-command command) :type :application.command/save-draft @@ -258,7 +258,7 @@ (:header :label) "" :date "2002-03-04" :email "user@example.com" - :attachment "" ;; don't know what to do for these + :attachment (str attachment) (:option :multiselect) (:key (first (:field/options field))) (or field-value "x"))}))))) diff --git a/src/clj/rems/pdf.clj b/src/clj/rems/pdf.clj index d51b90b9e3..b09a90e9bb 100644 --- a/src/clj/rems/pdf.clj +++ b/src/clj/rems/pdf.clj @@ -2,6 +2,7 @@ "Rendering applications as pdf" (:require [clj-pdf.core :refer :all] [clj-time.core :as time] + [clojure.string :as str] [rems.common.util :refer [build-index]] [rems.text :refer [localized localize-event localize-state localize-time text with-language]] [rems.util :refer [getx getx-in]]) @@ -18,75 +19,98 @@ (defn- render-header [application] (let [state (getx application :application/state) resources (getx application :application/resources)] - (concat - (list - [:heading heading-style - (str (text :t.applications/application) - " " - (get application :application/external-id - (getx application :application/id)) - (when-let [description (get application :application/description)] - (str ": " description)))] - [:paragraph - (text :t.pdf/generated) - " " - (localize-time (time/now))] - [:paragraph - (text :t.applications/state) - (when state [:phrase ": " (localize-state state)])] - [:heading heading-style (text :t.applicant-info/applicants)] - [:paragraph (text :t.applicant-info/applicant) ": " (render-user (getx application :application/applicant))]) - (seq + (list + [:heading heading-style + (str (text :t.applications/application) + " " + (get application :application/external-id + (getx application :application/id)) + (when-let [description (get application :application/description)] + (str ": " description)))] + [:paragraph + (text :t.pdf/generated) + " " + (localize-time (time/now))] + [:paragraph + (text :t.applications/state) + (when state [:phrase ": " (localize-state state)])] + [:heading heading-style (text :t.applicant-info/applicants)] + [:paragraph (text :t.applicant-info/applicant) ": " (render-user (getx application :application/applicant))] + (doall (for [member (getx application :application/members)] [:paragraph (text :t.applicant-info/member) ": " (render-user member)])) - (list - [:heading heading-style (text :t.form/resources)] - (into - [:list] + [:heading heading-style (text :t.form/resources)] + [:list + (doall (for [resource resources] [:phrase (localized (:catalogue-item/title resource)) - " (" (:resource/ext-id resource) ")"])))))) + " (" (:resource/ext-id resource) ")"]))]))) + +(defn- attachment-filenames [application] + (build-index [:attachment/id] :attachment/filename (:application/attachments application))) (defn- render-events [application] - (let [events (getx application :application/events)] + (let [filenames (attachment-filenames application) + events (getx application :application/events)] (list [:heading heading-style (text :t.form/events)] (if (empty? events) [:paragraph "–"] - (into - [:table {:header [(text :t.form/date) - (text :t.form/event) - (text :t.form/comment)]}] - (for [event events] - [(localize-time (:event/time event)) - (localize-event event) - (get event :application/comment "")])))))) - -(defn- field-value [field] - (case (:field/type field) - (:option :multiselect) - (localized (get (build-index [:key] :label (:field/options field)) - (:field/value field))) - - (:field/value field))) - -(defn- render-field [field] + [:list + (doall + (for [event events + :when (not (#{:application.event/draft-saved} (:event/type event)))] + [:phrase + (localize-time (:event/time event)) + " " + (localize-event event) + (let [comment (get event :application/comment)] + (when-not (empty? comment) + (str "\n" + (text :t.form/comment) + ": " + comment))) + (when-let [attachments (seq (get event :event/attachments))] + (str "\n" + (text :t.form/attachments) + ": " + (str/join ", " (map (comp filenames :attachment/id) attachments))))]))])))) + +(defn- field-value [filenames field] + (let [value (:field/value field)] + (case (:field/type field) + (:option :multiselect) + (localized (get (build-index [:key] :label (:field/options field)) value)) + + :attachment + (if (empty? value) + value + (get filenames (Integer/parseInt value))) + + (:field/value field)))) + +(def label-field-style {:spacing-before 8}) +(def header-field-style {:spacing-before 8 :style :bold :size 15}) +(def field-style {:spacing-before 8 :style :bold}) + +(defn- render-field [filenames field] (when (:field/visible field) (list [:paragraph (case (:field/type field) - :label {} - :header {:style :bold :size 15} - {:style :bold}) + :label label-field-style + :header header-field-style + field-style) (localized (:field/title field))] - [:paragraph (field-value field)]))) + [:paragraph (field-value filenames field)]))) (defn- render-fields [application] - (apply concat - (list [:heading heading-style (text :t.form/application)]) - (for [form (getx application :application/forms) - field (getx form :form/fields)] - (render-field field)))) + (let [filenames (attachment-filenames application)] + (list [:heading heading-style (text :t.form/application)] + (doall + (for [form (getx application :application/forms) + field (getx form :form/fields)] + (render-field filenames field)))))) (defn- render-license [license] ;; TODO license text? @@ -95,9 +119,10 @@ (localized (:license/title license))]) (defn- render-licenses [application] - (concat (list [:heading heading-style (text :t.form/licenses)]) - (for [license (getx application :application/licenses)] - (render-license license)))) + (list [:heading heading-style (text :t.form/licenses)] + (doall + (for [license (getx application :application/licenses)] + (render-license license))))) (defn- render-application [application] [{} diff --git a/src/cljs/rems/application.cljs b/src/cljs/rems/application.cljs index a1c0fcb60e..2341c07510 100644 --- a/src/cljs/rems/application.cljs +++ b/src/cljs/rems/application.cljs @@ -685,9 +685,11 @@ :application.command/assign-external-id [assign-external-id-button] :application.command/close [close-action-button] :application.command/copy-as-new [copy-as-new-button]]] - (distinct (for [[command action] (partition 2 commands-and-actions) - :when (contains? (:application/permissions application) command)] - action)))) + (concat (distinct (for [[command action] (partition 2 commands-and-actions) + :when (contains? (:application/permissions application) command)] + action)) + (list [pdf-button (:application/id application)])))) + (defn- actions-form [application] (let [app-id (:application/id application) @@ -795,7 +797,6 @@ attachment-success @(rf/subscribe [::attachment-success]) userid (:userid @(rf/subscribe [:user]))] [:div.container-fluid - [:div {:class "float-right"} [pdf-button (:application/id application)]] [document-title (str (text :t.applications/application) (when application (str " " (application-list/format-application-id config application))) diff --git a/test-config.edn b/test-config.edn index a3fe06976f..b6a73b0c72 100644 --- a/test-config.edn +++ b/test-config.edn @@ -8,6 +8,7 @@ :test-database-url "postgresql://localhost/rems_test?user=rems_test" :search-index-path "target/search-index-test" :authentication :fake-shibboleth + :languages [:en :fi :sv] :enable-pdf-api true ;; list all organizations used in test-data :organizations ["default" "perf" "thl" "nbn" "organization1" "organization2"]} diff --git a/test-data/extra-pages/about-en.md b/test-data/extra-pages/about-en.md index db8e6d7bed..10d88a2b21 100644 --- a/test-data/extra-pages/about-en.md +++ b/test-data/extra-pages/about-en.md @@ -1,67 +1 @@ -This is a demo environment for testing the REMS software. The demo environment has a couple of fictional datasets to which you can apply for access. For each dataset, there is an application form that the applicant fills in and submits and a workflow that determines how and to whom the submitted application is circulated for approval. - -You can use the demo usernames and passwords below to log in with various roles. The demo environment does not control access to any real datasets and does not send any email notifications to the applicants, reviewers or approvers of the applications. - -**The demo environment might get cleaned occasionally**, which will make all applications and access rights disappear. However, please notice that the applications or configurations you make may be visible to the other persons using the demo. - -[More information on the REMS tool.](http://www.csc.fi/rems) - -REMS support: rems@csc.fi - -## User Accounts and Roles - -Following test user accounts (roles) can be used when signing in using Haka test IdP: - -**1. Applicant 1**
-Username: RDapplicant1
-Password: RDapplicant1
-Description: Use this account to apply for access rights to a dataset. - -**2. Applicant 2**
-Username: RDapplicant2
-Password: RDapplicant2
-Description: Another account for applying for access rights. For instance, RDapplicant1 can add this user to an application as a member to whom equal access rights are applied as well. - -**3. Reviewer**
-Username: RDreview
-Password: RDreview
-Description: A reviewer can provide comments on an application to the approver but cannot approve or reject the application. - -**4. Approver 1**
-Username: RDapprover1
-Password: RDapprover1
-Description: An approver can approve, reject, close and return applications - -**5. Approver 2**
-Username: RDapprover2
-Password: RDapprover2
-Description: Some datasets in the demo have several parallel or alternative approvers. - -**6. Dataset Owner**
-Username: RDowner
-Password: RDowner
-Description: A dataset owner can add new datasets and change properties of a dataset, such as its application form, approval workflow and approvers. - -## Datasets - -**ELFA Corpus, direct approval**
-Description: This dataset has a minimal workflow. The user just commits to licence terms and receives an entitlement. - -**ELFA Corpus, one approval**
-Description: This dataset has a simple workflow. The application is sent to a single person (RDapprover1) for approval. - -**ELFA Corpus, with review**
-Description: This dataset has a simple workflow with a reviewer (RDreview), who can just provide comments to the approver (RDapprover1) on the application. - -**ELFA Corpus, two rounds of approval by different approvers**
-Description: This dataset has two approval phases. The application must be first approved by RDapprover1 and then by RDapprover2. - -## Terms of Use - -You agree not to do anything that infringes any laws or regulations or the rights of another (such as copyright) or is obscene, threatening, violent, abusive, hateful, harassing or otherwise objectionable. - -You agree not to introduce to REMS any information that relates to an identified or identifiable natural person. - -## Acknowledgements - -CSC’s work for REMS has been supported by the Ministry of Education and Culture of Finland and by Academy of Finland grants 271642 and 263164 to construct Biomedinfra, the Finnish consortium for ELIXIR, BBMRI and EATRIS ESFRI. +This is a dummy About page for REMS. diff --git a/test-data/extra-pages/about-fi.md b/test-data/extra-pages/about-fi.md index 138930236c..322cf806a4 100644 --- a/test-data/extra-pages/about-fi.md +++ b/test-data/extra-pages/about-fi.md @@ -1,67 +1 @@ -Tämä on demo-ympäristö, joka on tarkoitettu REMS-ohjelmiston kokeilemiseen. Demo-ympäristöön on luotu valmiiksi joitain kuvitteellisia tietoaineistoja, joihin voit hakea käyttöoikeutta. Kullekin tietoaineistolle on luotu hakulomake, jonka hakija täyttää ja lähettää, sekä työnkulku, joka määrittelee, minkälaisen käsittelypolun lähetetty lomake kulkee. - -Demo-ympäristöön voi kirjautua eri rooleissa alla olevilla käyttäjätunnuksilla ja salasanoilla. REMS-demo ei kontrolloi minkään todellisen tietoaineiston käyttöoikeuksia. Demo-ympäristöstä ei myöskään lähetetä sähköposti-ilmoituksia käyttöoikeuden hakijoille, kommentoijille tai hyväksyjille. - -**Demoympäristö saatetaan nollata ajoittain**, jolloin kaikki tehdyt hakemukset ja käyttöoikeudet pyyhkiytyvät. Huomioi kuitenkin, että demotunnuksilla tekemäsi hakemukset ja muutokset saattavat sitä ennen näkyä muille kokeilijoille. - -[Lisätietoa REMS-ohjelmistosta](http://www.csc.fi/rems). - -REMS-tuki: rems@csc.fi - -## Testitilit ja roolit - -Hakan testi-IdP:ltä löytyy seuraavia testitunnuksia: - -**1. Käyttöluvanhakija 1**
-Käyttäjätunnus: RDapplicant1
-Salasana: RDapplicant1
-Kuvaus: Kirjaudu tällä tunnuksella hakeaksesi tietoaineiston käyttöoikeutta. - -**2. Käyttöluvanhakija 2**
-Käyttäjätunnus: RDapplicant2
-Salasana: RDapplicant2
-Kuvaus: Toinen tunnus tietoaineiston käyttöoikeuden hakemiseen. RDapplicant1 voi vaikkapa kutsua tämän käyttäjän käyttölupahakemuksensa toiseksi jäseneksi, jolloin käyttöoikeuksia anotaan samalla myös hänelle. - -**3. Reviewer**
-Käyttäjätunnus: RDreview
-Salasana: RDreview
-Kuvaus: Katselmoija voi kommentoida saapunutta käyttölupahakemusta mutta ei voi hyväksyä tai hylätä sitä. - -**4. Approver 1**
-Käyttäjätunnus: RDapprover1
-Salasana: RDapprover1
-Kuvaus: Hyväksyjä hyväksyy tai hylkää saapuneen käyttölupahakemuksen tai palauttaa sen täydennettäväksi. - -**5. Approver 2**
-Käyttäjätunnus: RDapprover2
-Salasana: RDapprover2
-Kuvaus: Demon eräillä tietoaineistoilla on useita rinnakkaisia tai vaihtoehtoisia hyväksyjiä. - -**6. Dataset Owner**
-Käyttäjätunnus: RDowner
-Salasana: RDowner
-Kuvaus: Aineiston omistaja voi lisätä uusia tietoaineistoja ja muuttaa niiden ominaisuuksia, kuten hakulomaketta, hyväksymisprosessia ja hyväksyjiä. - -## Aineistot - -**ELFA-korpus, suora hyväksyntä**
-Kuvaus: Tässä aineistossa on minimi työkulku. Käyttäjä sitoutuu lisenssin ehtoihin ja saa käyttöluvan. - -**ELFA-korpus, yksi hyväksyntä**
-Kuvaus: Tässä aineistossa on yksinkertainen työkulku. Hakemus lähetetään yhdelle henkilölle (RDapprover1) hyväksyttäväksi. - -**ELFA-korpus, katselmoinnilla**
-Kuvaus: Tässä aineistossa on yksinkertainen työkulku. Katselmoija (RDreview) voi vain kommentoida hyväksyjälle (RDapprover1) menevää hakemusta. - -**ELFA-korpus, kaksi hyväksyntäkierrosta eri hyväksyjillä**
-Kuvaus: Tässä aineistossa on kaksivaiheinen hyväksyntä. Hakemuksen hyväksyy ensin käyttäjä RDapprover1 ja sitten käyttäjä RDapprover2. - -## Käyttöehdot - -Sitoudut siihen, että et tee REMS-demossa mitään sellaista joka loukkaa lakeja tai asetuksia tai kolmannen osapuolen oikeuksia (kuten tekijänoikeuksia) tai on säädytöntä, uhkaavaa, väkivaltaista, herjaavaa, vihamielistä, häiritsevää tai muulla tavalla paheksuttavaa. - -Sitoudut siihen, että et syötä REMS-demoon mitään tunnistettua tai tunnistettavissa olevaa luonnollista henkilöä koskevaa tietoa. - -## Rahoittaja - -CSC:n REMS-tuote on saanut rahoitusta Opetus- ja kulttuuriministeriöltä ja Suomen akatemialta (apurahat 271642 ja 263164 Biomedinfraa varten, joka on Suomen ELIXIR, BBMRI ja EATRIS-tutkimusinfrastruktuurisolmujen muodostama konsortio). +Tämä on REMSin info-sivun tynkä. diff --git a/test-data/extra-pages/about-sv.md b/test-data/extra-pages/about-sv.md new file mode 100644 index 0000000000..417a0d24ca --- /dev/null +++ b/test-data/extra-pages/about-sv.md @@ -0,0 +1 @@ +Den här är en infosida på svenska. diff --git a/test/clj/rems/api/test_applications.clj b/test/clj/rems/api/test_applications.clj index 6db1082fd1..7fca60b099 100644 --- a/test/clj/rems/api/test_applications.clj +++ b/test/clj/rems/api/test_applications.clj @@ -1,5 +1,6 @@ (ns ^:integration rems.api.test-applications - (:require [clojure.string :as str] + (:require [clojure.java.io :as io] + [clojure.string :as str] [clojure.test :refer :all] [rems.api.services.catalogue :as catalogue] [rems.api.testing :refer :all] @@ -11,7 +12,9 @@ [rems.handler :refer [handler]] [rems.json] [rems.testing-util :refer [with-user]] - [ring.mock.request :refer :all])) + [ring.mock.request :refer :all]) + (:import java.io.ByteArrayOutputStream + java.util.zip.ZipInputStream)) (use-fixtures :once @@ -688,9 +691,9 @@ read-ok-body)] (is (= (count (str/split exported #"\n")) 2))))) -(def testfile (clojure.java.io/file "./test-data/test.txt")) +(def testfile (io/file "./test-data/test.txt")) -(def malicious-file (clojure.java.io/file "./test-data/malicious_test.html")) +(def malicious-file (io/file "./test-data/malicious_test.html")) (def filecontent {:tempfile testfile :content-type "text/plain" @@ -744,6 +747,20 @@ assert-response-is-ok)] (is (= "attachment;filename=\"test.txt\"" (get-in response [:headers "Content-Disposition"]))) (is (= (slurp testfile) (slurp (:body response)))))) + (testing "and uploading an attachment with the same name" + (let [id (-> (upload-request filecontent) + (authenticate api-key user-id) + handler + read-ok-body + :id)] + (is (number? id)) + (testing "and retrieving it" + (let [response (-> (read-request id) + (authenticate api-key user-id) + handler + assert-response-is-ok)] + (is (= "attachment;filename=\"test (1).txt\"" (get-in response [:headers "Content-Disposition"]))) + (is (= (slurp testfile) (slurp (:body response)))))))) (testing "and retrieving it as non-applicant" (let [response (-> (read-request id) (authenticate api-key "carl") @@ -927,9 +944,9 @@ :comment "see attachment" :attachments [{:attachment/id attachment-id}]}))))) - (testing "handler closes with two attachments" - (let [id1 (add-attachment handler-id (file "handler-close1.txt")) - id2 (add-attachment handler-id (file "handler-close2.txt"))] + (testing "handler closes with two attachments (with the same name)" + (let [id1 (add-attachment handler-id (file "handler-close.txt")) + id2 (add-attachment handler-id (file "handler-close.txt"))] (is (number? id1)) (is (number? id2)) (is (= {:success true} (send-command handler-id @@ -960,18 +977,106 @@ (testing "applicant" (is (= ["handler-public-remark.txt" "handler-approve.txt" - "handler-close1.txt" - "handler-close2.txt"] + "handler-close.txt" + "handler-close (1).txt"] (mapv :attachment/filename (:application/attachments (get-application-for-user application-id applicant-id)))))) (testing "handler" (is (= ["handler-public-remark.txt" "reviewer-review.txt" "handler-private-remark.txt" "handler-approve.txt" - "handler-close1.txt" - "handler-close2.txt"] + "handler-close.txt" + "handler-close (1).txt"] (mapv :attachment/filename (:application/attachments (get-application-for-user application-id handler-id))))))))) +(deftest test-application-attachment-zip + (let [api-key "42" + applicant-id "alice" + handler-id "handler" + reporter-id "reporter" + workflow-id (test-data/create-workflow! {:handlers [handler-id]}) + form-id (test-data/create-form! {:form/fields [{:field/id "attach1" + :field/title {:en "some attachment" + :fi "joku liite"} + :field/type :attachment + :field/optional true} + {:field/id "attach2" + :field/title {:en "another attachment" + :fi "toinen liite"} + :field/type :attachment + :field/optional true}]}) + cat-id (test-data/create-catalogue-item! {:workflow-id workflow-id + :form-id form-id}) + app-id (test-data/create-application! {:catalogue-item-ids [cat-id] + :actor applicant-id}) + add-attachment (fn [user file] + (-> (request :post (str "/api/applications/add-attachment?application-id=" app-id)) + (authenticate api-key user) + (assoc :params {"file" file}) + (assoc :multipart-params {"file" file}) + handler + read-ok-body + :id)) + file #(assoc filecontent :filename %) + fetch-zip (fn [user-id] + (with-open [zip (-> (api-response :get (str "/api/applications/" app-id "/attachments") nil + api-key user-id) + :body + ZipInputStream.)] + (loop [files {}] + (if-let [entry (.getNextEntry zip)] + (let [buf (ByteArrayOutputStream.)] + (io/copy zip buf) + (recur (assoc files (.getName entry) (.toString buf "UTF-8")))) + files))))] + (testing "save a draft" + (let [id (add-attachment applicant-id (file "invisible.txt"))] + (is (= {:success true} + (send-command applicant-id {:type :application.command/save-draft + :application-id app-id + :field-values [{:form form-id :field "attach1" :value (str id)}]}))))) + (testing "save a new draft" + (let [blue-id (add-attachment applicant-id (file "blue.txt")) + red-id (add-attachment applicant-id (file "red.txt"))] + (is (= {:success true} + (send-command applicant-id {:type :application.command/save-draft + :application-id app-id + :field-values [{:form form-id :field "attach1" :value (str blue-id)} + {:form form-id :field "attach2" :value (str red-id)}]}))))) + (testing "fetch zip as applicant" + (is (= {"blue.txt" (slurp testfile) + "red.txt" (slurp testfile)} + (fetch-zip applicant-id)))) + (testing "submit" + (is (= {:success true} + (send-command applicant-id {:type :application.command/submit + :application-id app-id})))) + (testing "remark with attachments" + (let [blue-comment-id (add-attachment handler-id (file "blue.txt")) + yellow-comment-id (add-attachment handler-id (file "yellow.txt"))] + (is (= {:success true} (send-command handler-id + {:type :application.command/remark + :public true + :application-id app-id + :comment "see attachment" + :attachments [{:attachment/id blue-comment-id} + {:attachment/id yellow-comment-id}]})))) + (testing "fetch zip as applicant, handler and reporter" + (is (= {"blue.txt" (slurp testfile) + "red.txt" (slurp testfile) + "blue (1).txt" (slurp testfile) + "yellow.txt" (slurp testfile)} + (fetch-zip applicant-id) + (fetch-zip handler-id) + (fetch-zip reporter-id)))) + (testing "fetch zip as third party" + (is (response-is-forbidden? (api-response :get (str "/api/applications/" app-id "/attachments") nil + api-key "malice")))) + (testing "fetch zip for nonexisting application" + (is (response-is-not-found? (api-response :get "/api/applications/99999999/attachments" nil + api-key "malice"))))))) + + (deftest test-application-api-license-attachments (let [api-key "42" applicant "alice" diff --git a/test/clj/rems/api/test_public.clj b/test/clj/rems/api/test_public.clj index 1d570754e4..070b64dd17 100644 --- a/test/clj/rems/api/test_public.clj +++ b/test/clj/rems/api/test_public.clj @@ -16,7 +16,7 @@ handler read-body) languages (keys data)] - (is (= [:en :fi] (sort languages)))))) + (is (= [:en :fi :sv] (sort languages)))))) (deftest test-config-api-smoke (let [config (-> (request :get "/api/config") diff --git a/test/clj/rems/test_pdf.clj b/test/clj/rems/test_pdf.clj index 77668e9ff4..d08e1adc1e 100644 --- a/test/clj/rems/test_pdf.clj +++ b/test/clj/rems/test_pdf.clj @@ -2,6 +2,7 @@ (:require [clj-time.core :as time] [clojure.test :refer :all] [rems.db.applications :as applications] + [rems.db.core :as db] [rems.db.test-data :as test-data] [rems.db.testing :refer [test-db-fixture rollback-db-fixture]] [rems.pdf :as pdf] @@ -41,11 +42,24 @@ :time (time/date-time 2000)}) handler "developer"] (testing "fill and submit" - (test-data/fill-form! {:time (time/date-time 2000) - :actor applicant - :application-id application-id - :field-value "pdf test" - :optional-fields true}) + (let [attachment (:id (db/save-attachment! {:application application-id + :user handler + :filename "attachment.pdf" + :type "application/pdf" + :data (byte-array 0)}))] + ;; two draft-saved events + (test-data/fill-form! {:time (time/date-time 2000) + :actor applicant + :application-id application-id + :field-value "pdf test" + :attachment attachment + :optional-fields true}) + (test-data/fill-form! {:time (time/date-time 2000) + :actor applicant + :application-id application-id + :field-value "pdf test" + :attachment attachment + :optional-fields true})) (test-data/accept-licenses! {:time (time/date-time 2000) :actor applicant :application-id application-id}) @@ -60,11 +74,22 @@ :member {:userid "beth"} :actor handler})) (testing "approve" - (test-data/command! {:time (time/date-time 2003) - :application-id application-id - :type :application.command/approve - :comment "approved" - :actor handler})) + (let [att1 (:id (db/save-attachment! {:application application-id + :user handler + :filename "file1.txt" + :type "text/plain" + :data (byte-array 0)})) + att2 (:id (db/save-attachment! {:application application-id + :user handler + :filename "file2.pdf" + :type "application/pdf" + :data (byte-array 0)}))] + (test-data/command! {:time (time/date-time 2003) + :application-id application-id + :type :application.command/approve + :comment "approved" + :attachments [{:attachment/id att1} {:attachment/id att2}] + :actor handler}))) (testing "pdf contents" (is (= [{} [[:heading pdf/heading-style "Application 2000/1: pdf test"] @@ -72,50 +97,51 @@ [:paragraph "State" [:phrase ": " "Approved"]] [:heading pdf/heading-style "Applicants"] [:paragraph "Applicant" ": " "Alice Applicant (alice) "] - [:paragraph "Member" ": " "Beth Applicant (beth) "] + [[:paragraph "Member" ": " "Beth Applicant (beth) "]] [:heading pdf/heading-style "Resources"] - [:list [:phrase "Catalogue item" " (" "pdf-resource-ext" ")"]]] + [:list [[:phrase "Catalogue item" " (" "pdf-resource-ext" ")"]]]] [[:heading pdf/heading-style "Terms of use"] - [:paragraph "Google license"] - [:paragraph "Text license"]] + [[:paragraph "Google license"] + [:paragraph "Text license"]]] [[:heading pdf/heading-style "Application"] - [:paragraph {} "This form demonstrates all possible field types. (This text itself is a label field.)"] - [:paragraph ""] - [:paragraph {:style :bold} "Application title field"] - [:paragraph "pdf test"] - [:paragraph {:style :bold} "Text field"] - [:paragraph "pdf test"] - [:paragraph {:style :bold} "Text area"] - [:paragraph "pdf test"] - [:paragraph {:style :bold :size 15} "Header"] - [:paragraph ""] - [:paragraph {:style :bold} "Date field"] - [:paragraph "2002-03-04"] - [:paragraph {:style :bold} "Email field"] - [:paragraph "user@example.com"] - [:paragraph {:style :bold} "Attachment"] - [:paragraph ""] - [:paragraph {:style :bold} "Option list. Choose the first option to reveal a new field."] - [:paragraph "First option"] - [:paragraph {:style :bold} "Conditional field. Shown only if first option is selected above."] - [:paragraph "pdf test"] - [:paragraph {:style :bold} "Multi-select list"] - [:paragraph "First option"] - [:paragraph {} "The following field types can have a max length."] - [:paragraph ""] - [:paragraph {:style :bold} "Text field with max length"] - [:paragraph "pdf test"] - [:paragraph {:style :bold} "Text area with max length"] - [:paragraph "pdf test"]] + [[[:paragraph pdf/label-field-style + "This form demonstrates all possible field types. (This text itself is a label field.)"] + [:paragraph ""]] + [[:paragraph pdf/field-style "Application title field"] + [:paragraph "pdf test"]] + [[:paragraph pdf/field-style "Text field"] + [:paragraph "pdf test"]] + [[:paragraph pdf/field-style "Text area"] + [:paragraph "pdf test"]] + [[:paragraph pdf/header-field-style "Header"] + [:paragraph ""]] + [[:paragraph pdf/field-style "Date field"] + [:paragraph "2002-03-04"]] + [[:paragraph pdf/field-style "Email field"] + [:paragraph "user@example.com"]] + [[:paragraph pdf/field-style "Attachment"] + [:paragraph "attachment.pdf"]] + [[:paragraph pdf/field-style "Option list. Choose the first option to reveal a new field."] + [:paragraph "First option"]] + [[:paragraph pdf/field-style "Conditional field. Shown only if first option is selected above."] + [:paragraph "pdf test"]] + [[:paragraph pdf/field-style "Multi-select list"] + [:paragraph "First option"]] + [[:paragraph pdf/label-field-style "The following field types can have a max length."] + [:paragraph ""]] + [[:paragraph pdf/field-style "Text field with max length"] + [:paragraph "pdf test"]] + [[:paragraph pdf/field-style "Text area with max length"] + [:paragraph "pdf test"]]]] [[:heading pdf/heading-style "Events"] - [:table - {:header ["Time" "Event" "Comment"]} - ["2000-01-01 00:00" "Alice Applicant created a new application." ""] - ["2000-01-01 00:00" "Alice Applicant saved the application as a draft." ""] - ["2000-01-01 00:00" "Alice Applicant accepted the terms of use." ""] - ["2001-01-01 00:00" "Alice Applicant submitted the application for review." ""] - ["2002-01-01 00:00" "Developer added Beth Applicant to the application." ""] - ["2003-01-01 00:00" "Developer approved the application." "approved"]]]] + [:list + [[:phrase "2000-01-01 00:00" " " "Alice Applicant created a new application." nil nil] + [:phrase "2000-01-01 00:00" " " "Alice Applicant accepted the terms of use." nil nil] + [:phrase "2001-01-01 00:00" " " "Alice Applicant submitted the application for review." nil nil] + [:phrase "2002-01-01 00:00" " " "Developer added Beth Applicant to the application." nil nil] + [:phrase "2003-01-01 00:00" " " "Developer approved the application." + "\nComment: approved" + "\nAttachments: file1.txt, file2.pdf"]]]]] (with-language :en (fn [] (with-fixed-time (time/date-time 2010) @@ -124,4 +150,7 @@ (testing "pdf rendering succeeds" (is (some? (with-language :en - #(pdf/application-to-pdf-bytes (applications/get-application-for-user handler application-id)))))))) + #(do + ;; uncomment this to get a pdf file to look at + #_(pdf/application-to-pdf (applications/get-application-for-user handler application-id) "/tmp/example-application.pdf") + (pdf/application-to-pdf-bytes (applications/get-application-for-user handler application-id)))))))))