diff --git a/docs/development.md b/docs/development.md index a98b6de479..282ae4654f 100644 --- a/docs/development.md +++ b/docs/development.md @@ -286,3 +286,14 @@ This will set local REMS instance as the target and start 3 concurrent browser i ## CircleCI CircleCI offers a [command-line tool](https://circleci.com/docs/guides/toolkit/local-cli/) for debugging and validating the [config file](/.circleci/config.yml) (and much more) + + +## Lucene + +The binary release of Lucene includes a GUI tool (Luke) that can be useful for viewing and searching the index. Download the binary from the [Lucene website](https://lucene.apache.org/core/downloads.html), extract it somewhere, `cd` to that directory and launch the tool with +```sh +bin/luke.sh +``` +Then choose the index you want to inspect (`rems/target/search-index-dev`, `...-test`) and pick `NIOFSDirectory` for "Directory implementation". + +The tool is somewhat klunky: any fields indexed as Long need to be set as such (from the default int) under Search > Query Parser > Point range query diff --git a/docs/search.md b/docs/search.md index f0514b7ce9..30234b1aaa 100644 --- a/docs/search.md +++ b/docs/search.md @@ -11,7 +11,7 @@ syntax it's possible to build more specific queries. Wildcards `?` and `*` are supported, plus many more. See the [query syntax documentation][query-syntax] for more details. -[query-syntax]: https://lucene.apache.org/core/8_2_0/queryparser/org/apache/lucene/queryparser/classic/package-summary.html#package.description +[query-syntax]: https://lucene.apache.org/core/9_12_3/queryparser/org/apache/lucene/queryparser/classic/package-summary.html ### Examples @@ -51,4 +51,4 @@ for more details. The catalogue page and admin pages use a simpler search mechanism which only does a substring search of the values shown in the table. -The search tems are joined with the "AND" operation. \ No newline at end of file +The search tems are joined with the "AND" operation. diff --git a/src/clj/rems/application/commands.clj b/src/clj/rems/application/commands.clj index fd0e81a927..f6e88b0f38 100644 --- a/src/clj/rems/application/commands.clj +++ b/src/clj/rems/application/commands.clj @@ -114,7 +114,8 @@ (remove (set catalogue-item-ids)) (map get-catalogue-item) (filter (complement (comp entitled-to-resids :resource-id))) - (map :id)) + (map :id) + (distinct)) catalogue-item-ids)] (when (seq missing) {:errors [{:type :t.applications.errors/missing-top-level-item @@ -450,21 +451,28 @@ :application/licenses (mapv (fn [id] {:license/id id}) (:licenses cmd))}))) (defmethod command-handler :application.command/change-resources - [cmd application {:keys [get-catalogue-item get-workflow] :as injections}] - (let [cat-ids (:catalogue-item-ids cmd) - workflow (when (seq cat-ids) - (get-workflow (-> (first cat-ids) + [{:keys [catalogue-item-ids actor] :as cmd} + application + {:keys [get-catalogue-item get-workflow get-entitlements] :as injections}] + (let [workflow (when (seq catalogue-item-ids) + (get-workflow (-> (first catalogue-item-ids) get-catalogue-item - :wfid)))] + :wfid))) + member-cannot-apply-complementary (->> (:application/members application) + (keep (fn [{:keys [userid]}] + (invalid-catalogue-item-hierarchy-error catalogue-item-ids userid injections))) + (apply merge-with into))] (or (must-not-be-empty cmd :catalogue-item-ids) - (invalid-catalogue-items cat-ids injections) - (unbundlable-catalogue-items-for-actor application cat-ids (:actor cmd) injections) - (changes-original-workflow application cat-ids (:actor cmd) injections) + (invalid-catalogue-items catalogue-item-ids injections) + (unbundlable-catalogue-items-for-actor application catalogue-item-ids actor injections) + (changes-original-workflow application catalogue-item-ids actor injections) + (invalid-catalogue-item-hierarchy-error catalogue-item-ids actor injections) + member-cannot-apply-complementary (add-comment-and-attachments cmd application injections {:event/type :application.event/resources-changed - :application/forms (build-forms-list workflow cat-ids injections) - :application/resources (build-resources-list cat-ids injections) - :application/licenses (build-licenses-list cat-ids injections)})))) + :application/forms (build-forms-list workflow catalogue-item-ids injections) + :application/resources (build-resources-list catalogue-item-ids injections) + :application/licenses (build-licenses-list catalogue-item-ids injections)})))) (defmethod command-handler :application.command/add-member [cmd application injections] @@ -494,15 +502,17 @@ :invitation/token ((getx injections :secure-token))})) (defmethod command-handler :application.command/accept-invitation - [cmd application _injections] + [cmd application injections] (let [{token :token application-id :application-id actor :actor} cmd - invitation (get-in application [:application/invitation-tokens token])] + invitation (get-in application [:application/invitation-tokens token]) + catalogue-item-ids (->> application :application/resources (keep :catalogue-item/id))] (cond (:application/member invitation) (or (already-joined-error application actor :member) (token-used-error invitation token) + (invalid-catalogue-item-hierarchy-error catalogue-item-ids actor injections) (ok-with-data {:application-id application-id} [{:event/type :application.event/member-joined :application/id application-id diff --git a/src/cljs/rems/accept_invitation.cljs b/src/cljs/rems/accept_invitation.cljs index d288f54a91..1eacfede33 100644 --- a/src/cljs/rems/accept_invitation.cljs +++ b/src/cljs/rems/accept_invitation.cljs @@ -16,11 +16,13 @@ ::accept-invitation (fn [[type token]] (let [error-handler (fn [response] - ((flash-message/default-error-handler :top [text :t.accept-invitation/header]) response) + ((flash-message/default-error-handler :top [text :t.accept-invitation/header]) + response) (navigate! "/catalogue"))] (post! "/api/invitations/accept-invitation" {:url-params {:token token} :handler (fn [response] + (js/console.log response) (let [error (first (:errors response))] (cond (:success response) diff --git a/src/cljs/rems/actions/accept_invitation.cljs b/src/cljs/rems/actions/accept_invitation.cljs index 88d8270abb..4637671b98 100644 --- a/src/cljs/rems/actions/accept_invitation.cljs +++ b/src/cljs/rems/actions/accept_invitation.cljs @@ -16,7 +16,7 @@ (fn [token] (let [error-handler (fn [response] ((flash-message/default-error-handler :top [text :t.actions/accept-invitation]) - response) + (update response :errors #(mapv (flash-message/argumentize-some-key :catalogue-item-ids) %))) (navigate! "/catalogue"))] (post! "/api/applications/accept-invitation" {:url-params {:invitation-token token} diff --git a/src/cljs/rems/actions/change_resources.cljs b/src/cljs/rems/actions/change_resources.cljs index 42edca404c..748a1df31f 100644 --- a/src/cljs/rems/actions/change_resources.cljs +++ b/src/cljs/rems/actions/change_resources.cljs @@ -1,12 +1,16 @@ (ns rems.actions.change-resources - (:require [re-frame.core :as rf] - [rems.actions.components :refer [action-button action-form-view comment-field collapse-action-form perform-action-button]] - [rems.globals] + (:require [clojure.set :as set] + [medley.core :refer [distinct-by]] + [re-frame.core :as rf] + [rems.actions.components :refer [action-button action-form-view + collapse-action-form + comment-field + perform-action-button]] [rems.dropdown :as dropdown] [rems.flash-message :as flash-message] - [medley.core :refer [distinct-by]] + [rems.globals] [rems.spinner :as spinner] - [rems.text :refer [text get-localized-title]] + [rems.text :refer [get-localized-title text]] [rems.util :refer [post!]])) (def ^:private action-form-id "change-resources") @@ -15,13 +19,15 @@ ::open-form (fn [{:keys [db]} [_ initial-resources]] - (merge - {:db (assoc db - ::initial-resources (into #{} (map :catalogue-item/id initial-resources)) - ::selected-resources (into #{} (map :catalogue-item/id initial-resources))) - :dispatch-n (concat [[:rems.actions.components/set-comment action-form-id ""]] - (when-not (:rems.catalogue/catalogue db) - [[:rems.catalogue/full-catalogue]]))}))) + {:db (assoc db + ::initial-resources (into #{} (map :catalogue-item/id initial-resources)) + ::selected-resources (into #{} (map :catalogue-item/id initial-resources)) + ::error nil) + :fx [[:dispatch [:rems.actions.components/set-comment action-form-id ""]] + (when-not (:rems.catalogue/catalogue db) + [:dispatch [:rems.catalogue/full-catalogue]]) + (when (:enable-catalogue-hierarchy @rems.globals/config) + [:dispatch [:rems.catalogue/entitlements]])]})) (rf/reg-sub ::catalogue @@ -44,6 +50,9 @@ (rf/reg-sub ::selected-resources (fn [db _] (::selected-resources db))) (rf/reg-event-db ::set-selected-resources (fn [db [_ resources]] (assoc db ::selected-resources (set (map :id resources))))) +(rf/reg-sub ::command-error (fn [db _] some? (::command-error db))) +(rf/reg-event-db ::set-command-error (fn [db [_ error]] (assoc db ::command-error error))) + (def ^:private dropdown-id "change-resources-dropdown") ;; The API allows us to add attachments to this command @@ -57,12 +66,25 @@ :catalogue-item-ids (vec resources)} (when comment {:comment comment})) - :handler (flash-message/default-success-handler - :change-resources - description - (fn [_] - (collapse-action-form action-form-id) - (on-finished))) + :handler (fn [{:keys [success errors] :as response}] + (cond success + (do + ((flash-message/default-success-handler + :change-resources + description + (fn [_] + (collapse-action-form action-form-id) + (on-finished))) + response) + (rf/dispatch [::set-command-error nil])) + errors + (do + (flash-message/show-error! + :change-resources + (->> errors + (mapv (flash-message/argumentize-some-key :catalogue-item-id :catalogue-item-ids)) + flash-message/format-errors)) + (rf/dispatch [::set-command-error errors])))) :error-handler (flash-message/default-error-handler :change-resources description)})) {})) @@ -74,21 +96,31 @@ (defn compatible-item? [item original-workflow-id] (= original-workflow-id (:wfid item))) +(defn compatible-hierarchy? [catalogue-item selected-resources entitlements] + (if-let [top-level-id (-> catalogue-item :part-of :catalogue-item/id)] + (contains? (set/union entitlements selected-resources) top-level-id) + true)) + (defn change-resources-view - [{:keys [application initial-resources selected-resources catalogue can-comment? on-set-resources on-send]}] + [{:keys [application initial-resources selected-resources catalogue entitlements can-comment? on-set-resources on-send]}] (let [original-workflow-id (get-in application [:application/workflow :workflow/id]) compatible-first-sort-fn #(if (compatible-item? % original-workflow-id) -1 1) sorted-selected-catalogue (->> catalogue (sort-by #(get-localized-title %)) (sort-by compatible-first-sort-fn)) - enable-cart? (:enable-cart @rems.globals/config)] + enable-cart? (:enable-cart @rems.globals/config) + enable-hierarchy? (:enable-catalogue-hierarchy @rems.globals/config) + item-disabled? #(or (not (compatible-item? % original-workflow-id)) + (and enable-hierarchy? + (not (compatible-hierarchy? % selected-resources entitlements))))] [action-form-view action-form-id (text :t.actions/change-resources) [[perform-action-button {:id "change-resources" :text (text :t.actions/change-resources) :class "btn-primary" :disabled (or (empty? selected-resources) - (= selected-resources initial-resources)) + (and (not @(rf/subscribe [::command-error])) + (= selected-resources initial-resources))) :on-click on-send}]] (if (empty? catalogue) [spinner/big] @@ -106,7 +138,7 @@ {:id dropdown-id :items (->> sorted-selected-catalogue (mapv #(assoc % ::label (get-localized-title %)))) - :item-disabled? #(not (compatible-item? % original-workflow-id)) + :item-disabled? item-disabled? :item-key :id :item-label ::label :item-selected? #(contains? (set selected-resources) (% :id)) @@ -121,11 +153,13 @@ (let [initial-resources @(rf/subscribe [::initial-resources]) selected-resources @(rf/subscribe [::selected-resources]) catalogue @(rf/subscribe [::catalogue]) + entitlements @(rf/subscribe [:rems.catalogue/entitlements->catalogue-item-ids]) comment @(rf/subscribe [:rems.actions.components/comment action-form-id])] [change-resources-view {:application application :initial-resources initial-resources :selected-resources selected-resources :catalogue catalogue + :entitlements entitlements :can-comment? can-comment? :on-set-resources #(rf/dispatch [::set-selected-resources %]) :on-send #(rf/dispatch [::send-change-resources {:application-id (:application/id application) diff --git a/src/cljs/rems/catalogue.cljs b/src/cljs/rems/catalogue.cljs index f664d0d5f9..4e18608a89 100644 --- a/src/cljs/rems/catalogue.cljs +++ b/src/cljs/rems/catalogue.cljs @@ -22,14 +22,18 @@ ::enter-page (fn [{:keys [db]} _] {:db (dissoc db ::catalogue ::draft-applications) - :dispatch-n [[:rems.table/reset] - [::entitlements] - (when @roles/logged-in? - [::draft-applications]) - (when (:enable-catalogue-tree @rems.globals/config) - [::full-catalogue-tree]) - (when (:enable-catalogue-table @rems.globals/config) - [::full-catalogue])]})) + :dispatch-n (cond-> [[:rems.table/reset]] + @roles/logged-in? + (conj [::draft-applications]) + + (:enable-catalogue-tree @rems.globals/config) + (conj [::full-catalogue-tree]) + + (:enable-catalogue-table @rems.globals/config) + (conj [::full-catalogue]) + + (:enable-catalogue-hierarchy @rems.globals/config) + (conj [::entitlements]))})) (fetcher/reg-fetcher ::full-catalogue "/api/catalogue?join-organization=false") (fetcher/reg-fetcher ::full-catalogue-tree "/api/catalogue/tree?join-organization=false" {:result :roots}) @@ -51,7 +55,7 @@ (rf/reg-sub ::entitlements->catalogue-item-ids :<- [::entitlements] - :<- [::catalogue] + :<- [::full-catalogue] (fn [[entitlements catalogue] _] (let [entitled-to-resources (into #{} (map :resource) entitlements)] (into #{} diff --git a/src/cljs/rems/flash_message.cljs b/src/cljs/rems/flash_message.cljs index d9de671120..f536b7536f 100644 --- a/src/cljs/rems/flash_message.cljs +++ b/src/cljs/rems/flash_message.cljs @@ -124,20 +124,24 @@ (defn argumentize-some-key [& id-keys] (fn argumentize [error] - (assoc error - :args - [(->> error - ((apply some-fn id-keys)) - vector - flatten - (str/join ", "))]))) + (if-not (:args error) + (assoc error + :args + [(->> error + ((apply some-fn id-keys)) + ((fn [x] (cond-> x (not (seqable? x)) list))) + (str/join ", "))]) + error))) (deftest test-argumentize-some-key (are [expected input] (= expected ((argumentize-some-key :a :b) input)) - {:args ["1"] :a 1} {:a 1} - {:args ["1"] :b 1} {:b 1} - {:args ["1"] :a [1]} {:a [1]} - {:args ["2, 3"] :b [2 3]} {:b [2 3]})) + {:args ["1"] :a 1} {:a 1} + {:args ["1"] :b 1} {:b 1} + {:args ["1"] :a [1]} {:a [1]} + {:args ["2, 3"] :b [2 3]} {:b [2 3]} + {:args ["1"] :a 1 :b 2} {:a 1 :b 2} + {:args 1} {:args 1} + {:other-key 1 :args [""]} {:other-key 1})) (defn format-response-error [response] (if (:response response) diff --git a/src/cljs/rems/new_application.cljs b/src/cljs/rems/new_application.cljs index cd51e2f11f..9a3b905129 100644 --- a/src/cljs/rems/new_application.cljs +++ b/src/cljs/rems/new_application.cljs @@ -28,7 +28,6 @@ errors (do (replace-url! "/catalogue") - (js/console.log errors) (flash-message/show-error! :top (->> errors (mapv (flash-message/argumentize-some-key :catalogue-item-id :catalogue-item-ids)) flash-message/format-errors))))) diff --git a/test/clj/rems/application/test_commands.clj b/test/clj/rems/application/test_commands.clj index 4215e93200..f7da445716 100644 --- a/test/clj/rems/application/test_commands.clj +++ b/test/clj/rems/application/test_commands.clj @@ -279,6 +279,19 @@ :application/forms [{:form/id 1}] :workflow/id 1 :workflow/type :workflow/default}) + +(def dummy-created-event-with-complementary-item + {:event/type :application.event/created + :event/time test-time + :event/actor applicant-user-id + :application/id app-id + :application/external-id "2000/123" + :application/forms [{:form/id 1}] + :application/resources [{:catalogue-item/id 11, :resource/ext-id "res-complementary-2"}] + :application/licenses [{:license/id 1}] + :workflow/id 1 + :workflow/type :workflow/default}) + (def dummy-submitted-event {:event/type :application.event/submitted :event/time test-time :event/actor applicant-user-id @@ -789,6 +802,54 @@ :catalogue-item-ids [4]} (build-application-view [dummy-created-event]))))) + (testing "applicant can add hierarchical catalogue items" + (is (= {:event/type :application.event/resources-changed + :event/time test-time + :event/actor applicant-user-id + :application/id app-id + :application/forms [{:form/id 1}] + :application/resources [{:catalogue-item/id 2, :resource/ext-id "res2"} + {:catalogue-item/id 8 :resource/ext-id "res-top-level"} + {:catalogue-item/id 9 :resource/ext-id "res-complementary"}] + :application/licenses [{:license/id 2} {:license/id 1}]} + (ok-command {:type :application.command/change-resources + :actor applicant-user-id + :catalogue-item-ids [2 8 9]} + (build-application-view [dummy-created-event]) + (assoc command-injections :get-config (constantly {:enable-catalogue-hierarchy true})))))) + + (testing "applicant can add complementary item, when they have entitlement to it's top-level item" + (is (= {:event/type :application.event/resources-changed + :event/time test-time + :event/actor applicant-user-id + :application/id app-id + :application/forms [{:form/id 1}] + :application/resources [{:catalogue-item/id 11, :resource/ext-id "res-complementary-2"}] + :application/licenses [{:license/id 1}]} + (ok-command {:type :application.command/change-resources + :actor applicant-user-id + :catalogue-item-ids [11]} + (build-application-view [dummy-created-event]) + (assoc command-injections :get-config (constantly {:enable-catalogue-hierarchy true})))))) + + (testing "applicant cannot change resources in a way that would violate catalogue item hierarchy" + (is (= {:errors [{:type :t.applications.errors/missing-top-level-item :catalogue-item-ids [8]}]} + (fail-command {:type :application.command/change-resources + :actor applicant-user-id + :catalogue-item-ids [1 9]} + (build-application-view [dummy-created-event]) + (assoc command-injections :get-config (constantly {:enable-catalogue-hierarchy true})))))) + + (testing "applicant cannot change resources to include such a complementary item, to whose top-level item another member is not entitled to" + (is (= {:errors [{:type :t.applications.errors/missing-top-level-item :catalogue-item-ids [10]}]} + (fail-command {:type :application.command/change-resources + :actor applicant-user-id + :catalogue-item-ids [11]} + (build-application-view [dummy-created-event + dummy-member-invited-event + dummy-member-joined-event]) + (assoc command-injections :get-config (constantly {:enable-catalogue-hierarchy true})))))) + (testing "applicant can replace resources with different form" (is (= {:event/type :application.event/resources-changed :event/time test-time @@ -1463,7 +1524,17 @@ (build-application-view [dummy-created-event dummy-member-invited-event dummy-submitted-event - dummy-closed-event])))))) + dummy-closed-event]))))) + + (testing "cannot join an application with complementary catalogue items without entitlement to the top-level item" + (is (= {:errors [{:type :t.applications.errors/missing-top-level-item :catalogue-item-ids [10]}]} + (fail-command {:type :application.command/accept-invitation + :actor "somebody" + :token "very-secure"} + (build-application-view [dummy-created-event-with-complementary-item + dummy-member-invited-event]) + (assoc command-injections :get-config (constantly {:enable-catalogue-hierarchy true}))))))) + (testing "invited reviewer" (let [reviewer-invited-event {:event/type :application.event/reviewer-invited :event/time test-time diff --git a/test/clj/rems/test_browser.clj b/test/clj/rems/test_browser.clj index ff5f13ab32..571f8f10b3 100644 --- a/test/clj/rems/test_browser.clj +++ b/test/clj/rems/test_browser.clj @@ -24,6 +24,7 @@ [rems.config] [rems.db.api-key] [rems.db.applications] + [rems.db.entitlements] [rems.db.test-data-helpers :as test-helpers] [rems.db.test-data-users :as test-users] [rems.db.testing :refer [save-cache-statistics!]] @@ -175,6 +176,15 @@ ;;; common functionality +(defn create-and-approve-application! [{:keys [catalogue-item-ids actor time handler] :as args}] + (let [app-id (test-helpers/create-application! args)] + (test-helpers/submit-application (-> args + (dissoc :catalogue-item-ids) + (assoc :application-id app-id))) + (test-helpers/command! {:type :application.command/approve + :application-id app-id + :actor "handler"}))) + (defn login-as [username] (btu/go (btu/get-server-url)) (btu/screenshot "landing-page") @@ -2312,19 +2322,31 @@ :resource-4 (test-helpers/create-resource! {:resource-ext-id (str "test-catalogue-item-hierarchy" (btu/get-seed) " resource-4") :organization {:organization/id (btu/context-getx :organization-id)}}) + :resource-5 (test-helpers/create-resource! + {:resource-ext-id (str "test-catalogue-item-hierarchy" (btu/get-seed) " resource-5") + :organization {:organization/id (btu/context-getx :organization-id)}}) + :resource-6 (test-helpers/create-resource! + {:resource-ext-id (str "test-catalogue-item-hierarchy" (btu/get-seed) " resource-6") + :organization {:organization/id (btu/context-getx :organization-id)}}) + :resource-7 (test-helpers/create-resource! + {:resource-ext-id (str "test-catalogue-item-hierarchy" (btu/get-seed) " resource-7") + :organization {:organization/id (btu/context-getx :organization-id)}}) :form (test-helpers/create-form! {:form/internal-name "test-catalogue-item-hierarchy form" :form/external-title (test-helpers/make-localized "Test Edit Catalogue Item Children Form EN") :form/fields [] :form/organization {:organization/id (btu/context-getx :organization-id)}})) (btu/context-assoc! :parent-1-name (str "test-catalogue-item-hierarchy parent created with form " (btu/get-seed)) - :parent-2-name (str "test-catalogue-item-hierarchy-parent " (btu/get-seed)) + :parent-2-name (str "test-catalogue-item-hierarchy-parent 2 " (btu/get-seed)) + :parent-3-name (str "test-catalogue-item-hierarchy-parent 3 " (btu/get-seed)) :child-1-name (str "test-catalogue-item-hierarchy-child-1 " (btu/get-seed)) :child-2-name (str "test-catalogue-item-hierarchy-child-2 " (btu/get-seed)) :child-3-name (str "test-catalogue-item-hierarchy-child-3 " (btu/get-seed)) - :child-4-name (str "test-catalogue-item-hierarchy-child-4 " (btu/get-seed))) + :child-4-name (str "test-catalogue-item-hierarchy-child-4 " (btu/get-seed)) + :child-5-name (str "test-catalogue-item-hierarchy-child-5 " (btu/get-seed))) (btu/context-assoc! :parent-1-title-en (:en (test-helpers/make-localized (btu/context-getx :parent-1-name))) :parent-2-title-en (:en (test-helpers/make-localized (btu/context-getx :parent-2-name))) + :parent-3-title-en (:en (test-helpers/make-localized (btu/context-getx :parent-3-name))) :child-1-title-en (:en (test-helpers/make-localized (btu/context-getx :child-1-name))) :child-2-title-en (:en (test-helpers/make-localized (btu/context-getx :child-2-name))) :child-3-title-en (:en (test-helpers/make-localized (btu/context-getx :child-3-name))) @@ -2337,22 +2359,36 @@ :organization {:organization/id (btu/context-getx :organization-id)}}) :parent-2-id (test-helpers/create-catalogue-item! {:title (test-helpers/make-localized (btu/context-getx :parent-2-name)) - :resource-id (btu/context-getx :resource-1) + :resource-id (btu/context-getx :resource-3) + :form-id (btu/context-getx :form) + :workflow-id (btu/context-getx :workflow) + :organization {:organization/id (btu/context-getx :organization-id)}}) + :parent-3-id (test-helpers/create-catalogue-item! + {:title (test-helpers/make-localized (btu/context-getx :parent-3-name)) + :resource-id (btu/context-getx :resource-4) :form-id (btu/context-getx :form) :workflow-id (btu/context-getx :workflow) :organization {:organization/id (btu/context-getx :organization-id)}}) :child-2-id (test-helpers/create-catalogue-item! {:title (test-helpers/make-localized (btu/context-getx :child-2-name)) - :resource-id (btu/context-getx :resource-3) + :resource-id (btu/context-getx :resource-5) :form-id (btu/context-getx :form) :workflow-id (btu/context-getx :workflow) :organization {:organization/id (btu/context-getx :organization-id)}}) :child-3-id (test-helpers/create-catalogue-item! {:title (test-helpers/make-localized (btu/context-getx :child-3-name)) - :resource-id (btu/context-getx :resource-3) + :resource-id (btu/context-getx :resource-6) + :form-id (btu/context-getx :form) + :workflow-id (btu/context-getx :workflow) + :organization {:organization/id (btu/context-getx :organization-id)}}) + :child-5-id (test-helpers/create-catalogue-item! + {:title (test-helpers/make-localized (btu/context-getx :child-5-name)) + :resource-id (btu/context-getx :resource-7) :form-id (btu/context-getx :form) :workflow-id (btu/context-getx :workflow) :organization {:organization/id (btu/context-getx :organization-id)}})) + (test-helpers/edit-catalogue-item! {:id (btu/context-getx :parent-3-id) + :children [{:catalogue-item/id (btu/context-getx :child-5-id)}]}) (login-as "owner") (testing "create catalogue item with child" @@ -2532,12 +2568,13 @@ :actor "handler"}) (let [child-4-id (test-helpers/create-catalogue-item! {:title (test-helpers/make-localized (btu/context-getx :child-4-name)) - :resource-id (btu/context-getx :resource-4) + :resource-id (btu/context-getx :resource-5) :form-id (btu/context-getx :form) :workflow-id (btu/context-getx :workflow) :organization {:organization/id (btu/context-getx :organization-id)}})] (test-helpers/edit-catalogue-item! {:id (btu/context-getx :parent-2-id) - :children [{:catalogue-item/id child-4-id}]})) + :children [{:catalogue-item/id child-4-id}]}) + (btu/context-assoc! :child-4-id child-4-id)) (testing "with entitlement" (go-to-catalogue) @@ -2588,7 +2625,67 @@ (into [] (map btu/value-of-el) (-> (btu/query [{:class :application-resources}]) - (btu/children {:fn/has-class :application-resource})))))))) + (btu/children {:fn/has-class :application-resource})))))) + + (btu/context-assoc! :application-id (Integer/parseInt (get-application-id))))) + + (testing "invited user cannot join application of complementary item without also having entitlement to top-level item" + (test-helpers/command! {:type :application.command/invite-member + :actor "alice" + :application-id (btu/context-getx :application-id) + :member {:name "Frank" :email "frank@example.com"}}) + (btu/context-assoc! :invitation-token (-> (btu/context-getx :application-id) + rems.db.applications/get-application-internal + :application/invitation-tokens + first + key)) + (logout) + (login-as "frank") + (btu/go (str (btu/get-server-url) "application/accept-invitation/" (btu/context-getx :invitation-token))) + (is (= ["Accept invitation: Failed" + (str "Missing top-level item: " (btu/context-getx :parent-2-id))] + (get-error-summary :top))) + (is (empty? (rems.db.entitlements/get-entitlements "frank"))) + + (testing "with entitlement granted for invited user" + (create-and-approve-application! {:actor "frank" + :catalogue-item-ids [(btu/context-getx :parent-2-id)]}) + (btu/go (str (btu/get-server-url) "application/accept-invitation/" (btu/context-getx :invitation-token))) + (is (btu/eventually-visible? {:fn/has-string "Frank Roleless joined to the application."}) + "Frank can now join") + (is (= {:event/type :application.event/member-joined + :event/actor "frank"} + (-> (btu/context-getx :application-id) + rems.db.applications/get-application-internal + :application/events + last + (select-keys [:event/actor :event/type]))))) + + (testing "cannot change resource to a complementary item, when invited member doesn't have entitlement to it's parent" + (create-and-approve-application! {:actor "alice" + :catalogue-item-ids [(btu/context-getx :parent-3-id)]}) + (is (match? [{:resourceid (btu/context-getx :resource-4)}] + (filterv (comp #{(btu/context-getx :resource-4)} :resourceid) + (rems.db.entitlements/get-entitlements "alice"))) + "Alice now has entitlement to parent 3, so she could apply for child 5 on it's own.") + (is (= [] + (filterv (comp #{(btu/context-getx :resource-4)} :resourceid) + (rems.db.entitlements/get-entitlements "frank "))) + "Frank doesn't have entitlement, and with him as a member, the application's resources cannot be changed to include child 5.") + (logout) + (login-as "alice") + (go-to-application (btu/context-getx :application-id)) + (btu/scroll-and-click :change-resources-action-button) + (select-option "Resources included in the application:" (btu/context-getx :child-5-name)) + (btu/scroll-and-click :change-resources) + (is (btu/visible? [:flash-message-change-resources + {:tag :p + :fn/has-text (str "Missing top-level item: " (btu/context-getx :parent-3-id))}]))) + + (testing "with parent item present, changing resources becomes possible again" + (select-option "Resources included in the application:" (btu/context-getx :parent-3-name)) + (btu/scroll-and-click :change-resources) + (is (btu/eventually-visible? [:flash-message-change-resources {:id :status-success}])))) (testing "with feature flag off" (try @@ -2721,6 +2818,7 @@ (btu/screenshot "test-update-catalogue-item-before-update-2") (btu/scroll-and-click {:tag :button :fn/text "Update catalogue item"}) (is (btu/eventually-visible? {:css ".alert-success"})) + (btu/wait-for-idle) (btu/wait-disabled {:tag :button :fn/text "Update catalogue item"}) (is (= [{"name" "test-update-catalogue-item 1 EN" "form" "test-update-catalogue-item form 1"