diff --git a/src/mtgcoll/client/components/cards.cljs b/src/mtgcoll/client/components/cards.cljs index 15502f5..67fdefa 100644 --- a/src/mtgcoll/client/components/cards.cljs +++ b/src/mtgcoll/client/components/cards.cljs @@ -6,6 +6,7 @@ [webtools.reagent.bootstrap :as bs] [webtools.reagent.components :refer [raw-html]] [mtgcoll.common :refer [max-search-results]] + [mtgcoll.client.auth :as auth] [mtgcoll.client.components.utils :refer [set-short-label symboled-markup th-sortable]] [mtgcoll.client.components.inventory :refer [inventory]] [mtgcoll.client.utils :refer [format-currency]])) @@ -49,13 +50,13 @@ (> num-results max-search-results)) (defvc card-list-table - [filters pager & [{:keys [no-owned-highlight?] :as options}]] + [list-id filters pager & [{:keys [no-owned-highlight?] :as options}]] (let [sort-settings (r/atom {:sort-by :name :ascending? true})] - (fn [filters pager] - (let [cards (view-cursor :cards filters (:sort-by @sort-settings) (:ascending? @sort-settings) (:page @pager) (:page-size @pager)) - card-count (view-cursor :count-of-cards filters) + (fn [list-id filters pager] + (let [cards (view-cursor :cards list-id (auth/get-username) filters (:sort-by @sort-settings) (:ascending? @sort-settings) (:page @pager) (:page-size @pager)) + card-count (view-cursor :count-of-cards list-id (auth/get-username) filters) num-pages (min (js/Math.ceil (/ @card-count (:page-size @pager))) (max-pages @pager))] (if (vc/loading? cards) @@ -76,8 +77,9 @@ [th-sortable sort-settings :inventory "Inventory"]]] [:tbody (map - (fn [{:keys [id name set_code set_name mana_cost type power toughness rarity paper_price online_price owned_count] :as card}] - (let [owned? (> owned_count 0)] + (fn [{:keys [id name set_code set_name mana_cost type power toughness rarity paper_price online_price quantity] :as card}] + (let [quantity (or quantity 0) + owned? (> quantity 0)] ^{:key id} [:tr {:class (if (and (not no-owned-highlight?) owned?) "warning")} [:td [card-link id name :block-element? true]] @@ -89,8 +91,8 @@ [:td rarity] [:td (format-currency paper_price true)] [:td (format-currency online_price true)] - [:td [inventory id - {:num-owned owned_count + [:td [inventory id list-id + {:num-owned quantity :button-size "xsmall" :button-style (if owned? "primary")}]]])) @cards)]] diff --git a/src/mtgcoll/client/components/inventory.cljs b/src/mtgcoll/client/components/inventory.cljs index 759cabe..bc4decb 100644 --- a/src/mtgcoll/client/components/inventory.cljs +++ b/src/mtgcoll/client/components/inventory.cljs @@ -13,15 +13,15 @@ ["online" "near mint" "lightly played" "moderately played" "heavily played" "damaged"]) (defn on-add-card - [card-id quality foil?] + [card-id quality foil? list-id] (ajax/POST (->url "/collection/add") - :params {:card-id card-id :quality quality :foil foil?} + :params {:card-id card-id :quality quality :foil foil? :list-id list-id} :on-error #(set-error! "Server error while adding card to inventory."))) (defn on-remove-card - [card-id quality foil?] + [card-id quality foil? list-id] (ajax/POST (->url "/collection/remove") - :params {:card-id card-id :quality quality :foil foil?} + :params {:card-id card-id :quality quality :foil foil? :list-id list-id} :on-error #(set-error! "Server error while adding card to inventory."))) (defn can-modify-inventory? @@ -30,7 +30,7 @@ (auth/authenticated?))) (defvc inventory-management - [card-id] + [card-id list-id] (let [inventory (view-cursor :owned-card card-id) inventory (group-by :quality @inventory) colspan (if (can-modify-inventory?) 2 1) @@ -72,10 +72,10 @@ [:td.col-sm-3 [bs/ButtonGroup {:justified true} [bs/ButtonGroup - [bs/Button {:bsStyle "success" :on-click #(on-add-card card-id quality false)} + [bs/Button {:bsStyle "success" :on-click #(on-add-card card-id quality false list-id)} [bs/Glyphicon {:glyph "plus"}]]] [bs/ButtonGroup - [bs/Button {:bsStyle "danger" :disabled (= 0 non-foil-quantity) :on-click #(on-remove-card card-id quality false)} + [bs/Button {:bsStyle "danger" :disabled (= 0 non-foil-quantity) :on-click #(on-remove-card card-id quality false list-id)} [bs/Glyphicon {:glyph "minus"}]]]]]) ;; foil [:td {:class quantity-class} @@ -87,21 +87,21 @@ [:td.col-sm-3 [bs/ButtonGroup {:justified true} [bs/ButtonGroup - [bs/Button {:bsStyle "success" :on-click #(on-add-card card-id quality true)} + [bs/Button {:bsStyle "success" :on-click #(on-add-card card-id quality true list-id)} [bs/Glyphicon {:glyph "plus"}]]] [bs/ButtonGroup - [bs/Button {:bsStyle "danger" :disabled (= 0 foil-quantity) :on-click #(on-remove-card card-id quality true)} + [bs/Button {:bsStyle "danger" :disabled (= 0 foil-quantity) :on-click #(on-remove-card card-id quality true list-id)} [bs/Glyphicon {:glyph "minus"}]]]]])])) qualities))]]])) (defn inventory - [card-id & [{:keys [num-owned owned? button-size button-style] :as opts}]] + [card-id list-id & [{:keys [num-owned owned? button-size button-style] :as opts}]] [bs/OverlayTrigger {:placement "bottom" :trigger "click" :root-close true :overlay (r/as-component [bs/Popover {:class "inventory" :title "Card Inventory"} - [inventory-management card-id]])} + [inventory-management card-id list-id]])} [bs/Button (merge {:block true} diff --git a/src/mtgcoll/client/core.cljs b/src/mtgcoll/client/core.cljs index 1e001bd..f85d612 100644 --- a/src/mtgcoll/client/core.cljs +++ b/src/mtgcoll/client/core.cljs @@ -16,7 +16,7 @@ (defroute "/all" [] (page/page [collection/all-cards-list])) (defroute "/sets" [] (page/page [sets/sets-list])) (defroute "/set/:code" [code] (page/page [sets/set-details code])) -(defroute "/card/:id" [id] (page/page [cards/card-details id])) +(defroute "/card/:id" [id] (page/page [cards/card-details id 0])) (defroute "/stats" [] (page/page [stats/stats-page])) (defroute "*" [] (page/barebones-page [:div "not found"])) diff --git a/src/mtgcoll/client/routes/cards.cljs b/src/mtgcoll/client/routes/cards.cljs index 414109b..a64ad23 100644 --- a/src/mtgcoll/client/routes/cards.cljs +++ b/src/mtgcoll/client/routes/cards.cljs @@ -6,6 +6,7 @@ [webtools.cljs.utils :refer [->url pprint-json]] [webtools.reagent.bootstrap :as bs] [webtools.reagent.components :refer [->keyed-comps raw-html]] + [mtgcoll.client.auth :as auth] [mtgcoll.client.components.cards :refer [card-image card-link]] [mtgcoll.client.components.utils :refer [symbol-image set-heading set-label symboled-markup]] [mtgcoll.client.components.inventory :refer [inventory]] @@ -27,8 +28,8 @@ (->keyed-comps components)]) (defvc card-details - [id] - (let [card (view-cursor :full-card-info id) + [id list-id] + (let [card (view-cursor :full-card-info id list-id (auth/get-username)) variations (view-cursor :card-variations id) pricing (view-cursor :card-pricing id) printings (view-cursor :card-printings (:card_name @card))] @@ -48,10 +49,11 @@ [:a {:href (->url "#/set/" (:set_code @card))} [:small [set-heading (:set_code @card) (:set_name @card)]]] [:div.pull-right - [inventory id - {:num-owned (:owned_count @card) - :button-size "large" - :button-style (if (> (:owned_count @card) 0) "primary")}]]] + (let [quantity (or (:quantity @card) 0)] + [inventory id list-id + {:num-owned quantity + :button-size "large" + :button-style (if (> quantity 0) "primary")}])]] [bs/Grid {:class "card" :fluid true} [bs/Col {:sm 6 :class "card-image"} diff --git a/src/mtgcoll/client/routes/collection.cljs b/src/mtgcoll/client/routes/collection.cljs index 0ecf6e7..15382f5 100644 --- a/src/mtgcoll/client/routes/collection.cljs +++ b/src/mtgcoll/client/routes/collection.cljs @@ -16,13 +16,14 @@ (defn all-cards-list [] (let [active-search-filters (r/cursor all-cards-search-filters [:active-filters]) - pager (r/cursor all-cards-search-filters [:pager])] + pager (r/cursor all-cards-search-filters [:pager]) + list-id 0] (fn [] (set-active-breadcrumb! :all) [:div [bs/PageHeader "All Cards"] [s/search-filter-selector all-cards-search-filters] - [card-list-table @active-search-filters pager]]))) + [card-list-table list-id @active-search-filters pager]]))) ;;; @@ -33,11 +34,12 @@ [] (let [fixed-filters [{:field :owned? :value true :comparison :=}] active-search-filters (r/cursor owned-cards-search-filters [:active-filters]) - pager (r/cursor owned-cards-search-filters [:pager])] + pager (r/cursor owned-cards-search-filters [:pager]) + list-id 0] (s/apply-search-filters! owned-cards-search-filters fixed-filters) (fn [] (set-active-breadcrumb! :owned) [:div [bs/PageHeader "Owned Cards"] [s/search-filter-selector owned-cards-search-filters {:fixed-active-filters fixed-filters}] - [card-list-table @active-search-filters pager {:no-owned-highlight? true}]]))) + [card-list-table list-id @active-search-filters pager {:no-owned-highlight? true}]]))) diff --git a/src/mtgcoll/client/routes/sets.cljs b/src/mtgcoll/client/routes/sets.cljs index e87b275..ed2a31c 100644 --- a/src/mtgcoll/client/routes/sets.cljs +++ b/src/mtgcoll/client/routes/sets.cljs @@ -61,12 +61,13 @@ [set-code] (let [fixed-filters [{:field :set-code :value set-code :comparison :=}] active-search-filters (r/cursor set-cards-search-filters [:active-filters]) - pager (r/cursor set-cards-search-filters [:pager])] + pager (r/cursor set-cards-search-filters [:pager]) + list-id 0] (s/apply-search-filters! set-cards-search-filters fixed-filters) (fn [] [:div.set-cards-list [s/search-filter-selector set-cards-search-filters {:fixed-active-filters fixed-filters}] - [card-list-table @active-search-filters pager]]))) + [card-list-table list-id @active-search-filters pager]]))) (defvc set-details [set-code] diff --git a/src/mtgcoll/client/routes/stats.cljs b/src/mtgcoll/client/routes/stats.cljs index c28c1da..e6bf77b 100644 --- a/src/mtgcoll/client/routes/stats.cljs +++ b/src/mtgcoll/client/routes/stats.cljs @@ -6,6 +6,7 @@ [webtools.reagent.bootstrap :as bs] [webtools.reagent.components :refer [->keyed-comps]] [webtools.cljs.utils :refer [->url]] + [mtgcoll.client.auth :as auth] [mtgcoll.client.page :refer [set-active-breadcrumb!]] [mtgcoll.client.utils :refer [format-number format-currency get-field-value]] [mtgcoll.client.components.utils :refer [set-short-label]] @@ -84,8 +85,8 @@ :backgroundColor (vec bg-colors)}]}) (defvc widget-color-totals - [online? & [{:keys [width] :as options}]] - (let [color-totals (view-cursor :stats/color-totals online?)] + [online? list-id & [{:keys [width] :as options}]] + (let [color-totals (view-cursor :stats/color-totals online? list-id (auth/get-username))] (if-not (vc/loading? color-totals) [bs/Col {:sm (or width 12) :class "widget"} [:div.title "Card Colors"] @@ -97,8 +98,8 @@ {:vertical-legend? true}]]))) (defvc widget-basic-type-totals - [online? & [{:keys [width] :as options}]] - (let [basic-type-totals (view-cursor :stats/basic-type-totals online?)] + [online? list-id & [{:keys [width] :as options}]] + (let [basic-type-totals (view-cursor :stats/basic-type-totals online? list-id (auth/get-username))] (if-not (vc/loading? basic-type-totals) [bs/Col {:sm (or width 12) :class "widget"} [:div.title "Basic Card Types"] @@ -133,8 +134,8 @@ data)]]])) (defvc widget-most-owned-sets - [online? & [options]] - (let [data (view-cursor :stats/most-owned-sets online?)] + [online? list-id & [options]] + (let [data (view-cursor :stats/most-owned-sets online? list-id (auth/get-username))] (if-not (vc/loading? data) [table-statistics "Cards Owned By Set" @@ -145,8 +146,8 @@ options]))) (defvc widget-most-copies-of-card - [online? & [options]] - (let [data (view-cursor :stats/most-copies-of-card online?)] + [online? list-id & [options]] + (let [data (view-cursor :stats/most-copies-of-card online? list-id (auth/get-username))] (if-not (vc/loading? data) [table-statistics "Most Copies Owned" @@ -158,8 +159,8 @@ options]))) (defvc widget-most-nonland-copies-of-card - [online? & [options]] - (let [data (view-cursor :stats/most-nonland-copies-of-card online?)] + [online? list-id & [options]] + (let [data (view-cursor :stats/most-nonland-copies-of-card online? list-id (auth/get-username))] (if-not (vc/loading? data) [table-statistics "Most Non-land Copies Owned" @@ -171,8 +172,8 @@ options]))) (defvc widget-most-valuable-cards - [online? pricing-source & [options]] - (let [data (view-cursor :stats/most-valuable-cards online? pricing-source)] + [online? list-id pricing-source & [options]] + (let [data (view-cursor :stats/most-valuable-cards online? pricing-source list-id (auth/get-username))] (if-not (vc/loading? data) [table-statistics "Most Valuable Cards" @@ -184,8 +185,8 @@ options]))) (defvc widget-rarity-totals - [online? & [options]] - (let [data (view-cursor :stats/card-rarity-totals online?)] + [online? list-id & [options]] + (let [data (view-cursor :stats/card-rarity-totals online? list-id (auth/get-username))] (if-not (vc/loading? data) [table-statistics "Cards Owned By Rarity" @@ -195,8 +196,8 @@ options]))) (defvc widget-agg-price-stats - [online? pricing-source & [{:keys [width] :as options}]] - (let [agg-price-stats (view-cursor :stats/agg-price-stats online? pricing-source) + [online? list-id pricing-source & [{:keys [width] :as options}]] + (let [agg-price-stats (view-cursor :stats/agg-price-stats online? pricing-source list-id (auth/get-username)) min-price (:min_price @agg-price-stats) max-price (:max_price @agg-price-stats) avg-price (:avg_price @agg-price-stats) @@ -209,15 +210,16 @@ [big-currency-statistic "Median Card Value" median-price]]))) (defvc widget-summary-stats - [online? pricing-source & [{:keys [width] :as options}]] - (let [agg-price-stats (view-cursor :stats/agg-price-stats online? pricing-source) - owned-total (view-cursor :stats/owned-total online?) - owned-foil-total (view-cursor :stats/owned-foil-total online?) - distinct-owned-total (view-cursor :stats/distinct-owned-total online?) - total-sets-owned-from (view-cursor :stats/total-sets-owned-from online?) - total-sets-owned-all-from (view-cursor :stats/total-sets-owned-all-from online?) - total-price (view-cursor :stats/total-price online? pricing-source) - num-worth-over-1-dollar (view-cursor :stats/num-cards-worth-over-1-dollar online? pricing-source)] + [online? list-id pricing-source & [{:keys [width] :as options}]] + (let [username (auth/get-username) + agg-price-stats (view-cursor :stats/agg-price-stats online? pricing-source list-id username) + owned-total (view-cursor :stats/owned-total online? list-id username) + owned-foil-total (view-cursor :stats/owned-foil-total online? list-id username) + distinct-owned-total (view-cursor :stats/distinct-owned-total online? list-id username) + total-sets-owned-from (view-cursor :stats/total-sets-owned-from online? list-id username) + total-sets-owned-all-from (view-cursor :stats/total-sets-owned-all-from online? list-id username) + total-price (view-cursor :stats/total-price online? pricing-source list-id username) + num-worth-over-1-dollar (view-cursor :stats/num-cards-worth-over-1-dollar online? pricing-source list-id username)] [bs/Col {:sm (or width 12) :class "widget"} [widget-row [big-number-statistic "Owned" @owned-total]] [widget-row [big-number-statistic "Unique" @distinct-owned-total]] @@ -239,7 +241,8 @@ [] (let [pricing-sources (view-cursor :pricing-sources) online? (:online? @settings) - pricing-source (:pricing-source @settings)] + pricing-source (:pricing-source @settings) + list-id 0] (set-active-breadcrumb! :stats) (if (and (not (vc/loading? pricing-sources)) (nil? (:pricing-source @settings))) @@ -268,12 +271,12 @@ [bs/Grid {:fluid true :class "statistics"} [widget-row [bs/Col {:sm 4} - [widget-row [widget-summary-stats online? pricing-source]] - [widget-row [widget-rarity-totals online?]] - [widget-row [widget-most-owned-sets online?]]] + [widget-row [widget-summary-stats online? list-id pricing-source]] + [widget-row [widget-rarity-totals online? list-id]] + [widget-row [widget-most-owned-sets online? list-id]]] [bs/Col {:sm 8} - [widget-row [widget-color-totals online?]] - [widget-row [widget-basic-type-totals online?]] - [widget-row [widget-most-valuable-cards online? pricing-source {:width 10}]] - [widget-row [widget-most-copies-of-card online? {:width 10}]] - [widget-row [widget-most-nonland-copies-of-card online? {:width 10}]]]]]])) + [widget-row [widget-color-totals online? list-id]] + [widget-row [widget-basic-type-totals online? list-id]] + [widget-row [widget-most-valuable-cards online? list-id pricing-source {:width 10}]] + [widget-row [widget-most-copies-of-card online? list-id {:width 10}]] + [widget-row [widget-most-nonland-copies-of-card online? list-id {:width 10}]]]]]])) diff --git a/src/mtgcoll/models/collection.clj b/src/mtgcoll/models/collection.clj index 33201cc..4789e2b 100644 --- a/src/mtgcoll/models/collection.clj +++ b/src/mtgcoll/models/collection.clj @@ -1,37 +1,43 @@ (ns mtgcoll.models.collection (:require + [clojure.java.jdbc :as jdbc] [views.sql.core :refer [vexec! with-view-transaction]] [mtgcoll.db :refer [db]] [mtgcoll.views.core :refer [view-system]])) (defn update-collection! - [card-id quality foil? quantity-change] + [card-id quality foil? list-id user-id quantity-change] ;; written assuming postgresql server is _not_ 9.5+ (so, without access to UPSERT functionality) - (with-view-transaction - view-system - [dt @db] - (let [num-updates (first - (vexec! view-system dt - ["update collection - set quantity = quantity + ? - where card_id = ? and - quality = ? and - foil = ?" - quantity-change card-id quality foil?]))] - (if (= 0 num-updates) - (first - (vexec! view-system dt - ["insert into collection - (card_id, quality, quantity, foil) - values - (?, ?, ?, ?)" - card-id quality quantity-change foil?])) - num-updates)))) + (let [list-id (int list-id) + public-only? (nil? user-id)] + (with-view-transaction + view-system + [dt @db] + (if-not (first (jdbc/query dt ["select count(*) from lists where id = ? and is_public in (true, ?)" list-id public-only?])) + (throw (new Exception (str "Not authorized to update list:" list-id))) + (let [num-updates (first + (vexec! view-system dt + ["update collection + set quantity = quantity + ? + where card_id = ? and + quality = ? and + foil = ? and + list_id = ?" + quantity-change card-id quality foil? list-id]))] + (if (= 0 num-updates) + (first + (vexec! view-system dt + ["insert into collection + (card_id, quality, quantity, foil, list_id) + values + (?, ?, ?, ?, ?)" + card-id quality quantity-change foil? list-id])) + num-updates)))))) (defn add-to-collection! - [card-id quality foil?] - (update-collection! card-id quality foil? 1)) + [card-id quality foil? list-id user-id] + (update-collection! card-id quality foil? list-id user-id 1)) (defn remove-from-collection! - [card-id quality foil?] - (update-collection! card-id quality foil? -1)) + [card-id quality foil? list-id user-id] + (update-collection! card-id quality foil? list-id user-id -1)) diff --git a/src/mtgcoll/routes/collection.clj b/src/mtgcoll/routes/collection.clj index 15caaec..6dd6822 100644 --- a/src/mtgcoll/routes/collection.clj +++ b/src/mtgcoll/routes/collection.clj @@ -9,11 +9,13 @@ (def collection-routes (wrap-middleware (routes - (POST "/collection/add" [card-id quality foil :as request] - (collection/add-to-collection! card-id quality foil) - (response/json {:status "ok"})) + (POST "/collection/add" [card-id quality foil list-id :as request] + (let [username (get-in request [:session :user :username]) + result (collection/add-to-collection! card-id quality foil list-id username)] + (response/json {:status "ok"}))) - (POST "/collection/remove" [card-id quality foil :as request] - (collection/remove-from-collection! card-id quality foil) - (response/json {:status "ok"}))) + (POST "/collection/remove" [card-id quality foil list-id :as request] + (let [username (get-in request [:session :user :username]) + result (collection/remove-from-collection! card-id quality foil list-id username)] + (response/json {:status "ok"})))) (wrap-authenticated))) diff --git a/src/mtgcoll/views/core.clj b/src/mtgcoll/views/core.clj index 8fcf392..df34fcf 100644 --- a/src/mtgcoll/views/core.clj +++ b/src/mtgcoll/views/core.clj @@ -1,10 +1,12 @@ (ns mtgcoll.views.core (:require + [clojure.tools.logging :as log] [views.reagent.sente.server :as vr] [views.core :as views] [views.sql.view :refer [view]] [honeysql.format :as fmt] [mtgcoll.db :refer [db]] + [mtgcoll.auth :as auth] [mtgcoll.views.sente :refer [sente-socket]] [mtgcoll.views.functions.cards :as cards] [mtgcoll.views.functions.sets :as sets] @@ -56,11 +58,42 @@ #_(views/add-views! view-system views) +(defn view-auth-fn + [{:keys [view-id parameters] :as view-sig} subscriber-key context] + (let [request context + username (get-in request [:session :user :username])] + (if-not (auth/using-authorization?) + true + + (case view-id + ; views where the user-id parameter is always last + (:stats/owned-total :stats/owned-foil-total :stats/distinct-owned-total :stats/color-totals + :stats/basic-type-totals :stats/most-common-types :stats/total-sets-owned-from + :stats/total-sets-owned-all-from :stats/most-owned-sets :stats/most-copies-of-card + :stats/most-nonland-copies-of-card :stats/total-price :stats/agg-price-stats :stats/most-valuable-cards + :stats/num-cards-worth-over-1-dollar :stats/card-rarity-totals :full-card-info) + (= username (last parameters)) + + ; views where the user-id parameter is second + (:cards :count-of-cards) + (= username (second parameters)) + + ; assume otherwise that the view is not one that requires an auth check (no user-id parameter) + true)))) + +(defn view-on-unauth-fn + [{:keys [view-id parameters] :as view-sig} subscriber-key context] + (let [request context + user-profile (get-in request [:session :user])] + (log/warn "Unauthorized view subscription attempt: " view-id ", " parameters " - user profile: " user-profile))) + (defn init! [] (vr/init! view-system @sente-socket {:views views - :use-default-sente-router? true})) + :use-default-sente-router? true + :auth-fn view-auth-fn + :on-unauth-fn view-on-unauth-fn})) (defn shutdown! [] diff --git a/src/mtgcoll/views/functions/cards.clj b/src/mtgcoll/views/functions/cards.clj index 3dfead3..42cfd62 100644 --- a/src/mtgcoll/views/functions/cards.clj +++ b/src/mtgcoll/views/functions/cards.clj @@ -24,53 +24,59 @@ where c.id = ?" id]) (defn full-card-info - [id] - ["select c.id, - c.set_code, - c.layout, - c.name as card_name, - c.mana_cost, - c.converted_mana_cost, - c.colors, - c.color_identity, - c.type, - c.supertypes, - c.types, - c.subtypes, - c.rarity, - c.text, - c.flavor, - c.artist, - c.number, - c.power, - c.toughness, - c.loyalty, - c.multiverseid, - c.image_name, - c.watermark, - c.border, - s.border as set_border, - c.timeshifted, - c.hand, - c.life, - c.reserved, - c.release_date, - c.starter, - s.name as set_name, - s.gatherer_code, - s.old_code, - s.magic_cards_info_code, - s.release_date as set_release_date, - s.type as set_type, - s.block, - s.online_only, - c.paper_price, - c.online_price, - c.owned_count, - c.owned_foil_count - from cards c - join sets s on c.set_code = s.code - where c.id = ?" id]) + [id list-id user-id] + (let [list-id (int list-id) + public-only? (nil? user-id)] + ["select c.id, + c.set_code, + c.layout, + c.name as card_name, + c.mana_cost, + c.converted_mana_cost, + c.colors, + c.color_identity, + c.type, + c.supertypes, + c.types, + c.subtypes, + c.rarity, + c.text, + c.flavor, + c.artist, + c.number, + c.power, + c.toughness, + c.loyalty, + c.multiverseid, + c.image_name, + c.watermark, + c.border, + s.border as set_border, + c.timeshifted, + c.hand, + c.life, + c.reserved, + c.release_date, + c.starter, + s.name as set_name, + s.gatherer_code, + s.old_code, + s.magic_cards_info_code, + s.release_date as set_release_date, + s.type as set_type, + s.block, + s.online_only, + c.paper_price, + c.online_price, + lc.quantity, + lc.foil_quantity + from cards c + join sets s on c.set_code = s.code + join lists l on l.id = ? + left join lists_card_quantities lc on (lc.card_id = c.id and lc.list_id = l.id) + where c.id = ? + and l.is_public in (true, ?)" + list-id id public-only?])) (defn card-names [id] @@ -164,20 +170,24 @@ :number (text-comparison-fn [:number] true) :power (text-comparison-fn [:power] true) :toughness (text-comparison-fn [:toughness] true) - :owned-count (numeric-comparison-fn [:owned_count]) - :owned-foil-count (numeric-comparison-fn [:owned_foil_count]) + :owned-count (numeric-comparison-fn [:quantity]) + :owned-foil-count (numeric-comparison-fn [:foil_quantity]) :paper-price (numeric-comparison-fn [:paper_price]) :online-price (numeric-comparison-fn [:online_price]) :owned? (fn [value comparison] (assert (= := comparison)) (case value - true [:> :owned_count 0] - false [:= :owned_count 0])) + true [:> :quantity 0] + false [:or + [:is :quantity nil] + [:= :quantity 0]])) :owned-foil? (fn [value comparison] (assert (= := comparison)) (case value - true [:> :owned_foil_count 0] - false [:= :owned_foil_count 0]))}) + true [:> :foil_quantity 0] + false [:or + [:is :foil_quantity nil] + [:= :foil_quantity 0]]))}) (defn- filter->hsql-where-criteria [{:keys [field value comparison]}] @@ -185,32 +195,38 @@ (f value comparison))) (defn- base-card-search-query - [hsql-filters & [order-by]] + [list-id public-only? hsql-filters & [order-by]] (let [q {:from [[(merge - {:select [:c.id - :c.name - :c.normalized_name - :c.set_code - [:s.name :set_name] - :c.mana_cost - :c.converted_mana_cost - :c.colors - :c.color_identity - :c.type - :c.rarity - :c.text - :c.flavor - :c.artist - :c.number - :c.power - :c.toughness - :c.loyalty - :c.paper_price - :c.online_price - :c.owned_count - :c.owned_foil_count] - :from [[:cards :c]] - :join [[:sets :s] [:= :c.set_code :s.code]]} + {:select [:c.id + :c.name + :c.normalized_name + :c.set_code + [:s.name :set_name] + :c.mana_cost + :c.converted_mana_cost + :c.colors + :c.color_identity + :c.type + :c.rarity + :c.text + :c.flavor + :c.artist + :c.number + :c.power + :c.toughness + :c.loyalty + :c.paper_price + :c.online_price + :lc.quantity + :lc.foil_quantity] + :from [[:cards :c]] + :join [[:sets :s] [:= :c.set_code :s.code] + [:lists :l] [:and + [:= :l.id list-id] + [:in :l.is_public [true public-only?]]]] + :left-join [[:lists_card_quantities :lc] [:and + [:= :c.id :lc.card_id] + [:= :lc.list_id list-id]]]} (if order-by {:order-by [order-by [:c.id]]})) :cards_list]]}] @@ -219,8 +235,9 @@ q))) (defn cards - [filters & [sort-by ascending? page-num page-size]] - (let [sort-by (case sort-by + [list-id user-id filters & [sort-by ascending? page-num page-size]] + (let [public-only? (nil? user-id) + sort-by (case sort-by :name :name :set :set_code :mana-cost :converted_mana_cost @@ -228,10 +245,10 @@ :rarity :rarity :paper-price :paper_price :online-price :online_price - :inventory :owned_count + :inventory :quantity :name) sort-dir (if ascending? :asc :desc) - nulls (if (some #{sort-by} #{:converted_mana_cost :paper_price :online_price :owned_count}) + nulls (if (some #{sort-by} #{:converted_mana_cost :paper_price :online_price :quantity}) (if-not ascending? :nulls-last :nulls-first)) page-size (or page-size 25) page-size (if (> page-size 200) 200 page-size) @@ -241,6 +258,8 @@ page-num) hsql-filters (mapv filter->hsql-where-criteria filters) q (base-card-search-query + list-id + public-only? hsql-filters (if nulls [sort-by sort-dir nulls] @@ -251,10 +270,11 @@ (hsql/format q))) (defn count-of-cards - [filters] - (let [hsql-filters (mapv filter->hsql-where-criteria filters) - q (base-card-search-query hsql-filters) + [list-id user-id filters] + (let [public-only? (nil? user-id) + hsql-filters (mapv filter->hsql-where-criteria filters) + q (base-card-search-query list-id public-only? hsql-filters) q (assoc q :select [:%count.*])] (hsql/format q))) -#_(cards [{:field :name :value "z" :comparison :like}] :name true 10000 20) \ No newline at end of file +#_(cards 0 nil [{:field :name :value "z" :comparison :like}] :name true 10000 20) \ No newline at end of file diff --git a/src/mtgcoll/views/functions/collection.clj b/src/mtgcoll/views/functions/collection.clj index 0c15f57..34bf8db 100644 --- a/src/mtgcoll/views/functions/collection.clj +++ b/src/mtgcoll/views/functions/collection.clj @@ -4,12 +4,12 @@ [card-id] ["select quality, quantity, foil from collection - where card_id = ?" + where card_id = ? AND list_id = 0" card-id]) (defn total-owned-of-card [card-id] ["select count(*) from collection - where card_id = ?" + where card_id = ? AND list_id = 0" card-id]) diff --git a/src/mtgcoll/views/functions/sets.clj b/src/mtgcoll/views/functions/sets.clj index c492d5e..fff210f 100644 --- a/src/mtgcoll/views/functions/sets.clj +++ b/src/mtgcoll/views/functions/sets.clj @@ -18,7 +18,7 @@ select count(*) from collection cl join cards c on cl.card_id = c.id - where c.set_code = s.code and cl.quantity > 0 + where c.set_code = s.code and cl.quantity > 0 and cl.list_id = 0 ) as owned_count from sets s where s.code = ?" code]) @@ -47,6 +47,6 @@ select count(*) from collection cl join cards c on cl.card_id = c.id - where c.set_code = s.code and cl.quantity > 0 + where c.set_code = s.code and cl.quantity > 0 and cl.list_id = 0 ) as owned_count from sets s"]) diff --git a/src/mtgcoll/views/functions/statistics.clj b/src/mtgcoll/views/functions/statistics.clj index f07875c..0bd3059 100644 --- a/src/mtgcoll/views/functions/statistics.clj +++ b/src/mtgcoll/views/functions/statistics.clj @@ -1,228 +1,390 @@ (ns mtgcoll.views.functions.statistics) (defn owned-total - [online?] - ["select coalesce(sum(quantity), 0) as total - from collection - where online = ?" - (boolean online?)]) + [online? list-id user-id] + (let [online? (boolean online?) + list-id (int list-id) + public-only? (nil? user-id)] + ["select coalesce(sum(cl.quantity), 0) as total + from collection cl + join lists l on cl.list_id = l.id + where cl.online = ? + and cl.list_id = ? + and (l.is_public in (true, ?))" + online? list-id public-only?])) (defn distinct-owned-total - [online?] - ["select count(distinct c.id) - from cards c - join collection cl on c.id = cl.card_id - where cl.quantity > 0 and cl.online = ?" - (boolean online?)]) + [online? list-id user-id] + (let [online? (boolean online?) + list-id (int list-id) + public-only? (nil? user-id)] + ["select count(distinct c.id) + from cards c + join collection cl on c.id = cl.card_id + join lists l on cl.list_id = l.id + where cl.quantity > 0 + and cl.online = ? + and cl.list_id = ? + and (l.is_public in (true, ?))" + online? list-id public-only?])) (defn owned-foil-total - [online?] - ["select coalesce(sum(quantity), 0) as total - from collection - where online = ? and foil = true" - (boolean online?)]) + [online? list-id user-id] + (let [online? (boolean online?) + list-id (int list-id) + public-only? (nil? user-id)] + ["select coalesce(sum(cl.quantity), 0) as total + from collection cl + join lists l on cl.list_id = l.id + where cl.online = ? + and cl.foil = true + and cl.list_id = ? + and (l.is_public in (true, ?))" + online? list-id public-only?])) (defn color-totals - [online?] - (let [online? (boolean online?)] + [online? list-id user-id] + ;; TODO: i really dislike how this query is written ... try to clean this up at some point maybe? + (let [online? (boolean online?) + list-id (int list-id) + public-only? (nil? user-id)] ["select ( select sum(cl.quantity) from cards c join collection cl on c.id = cl.card_id - where c.color_identity like '%B%' and cl.quantity > 0 and cl.online = ? + join lists l on cl.list_id = l.id + where c.color_identity like '%B%' + and cl.quantity > 0 + and cl.online = ? + and cl.list_id = ? + and (l.is_public in (true, ?)) ) as black, ( select sum(cl.quantity) from cards c join collection cl on c.id = cl.card_id - where c.color_identity like '%U%' and cl.quantity > 0 and cl.online = ? + join lists l on cl.list_id = l.id + where c.color_identity like '%U%' + and cl.quantity > 0 + and cl.online = ? + and cl.list_id = ? + and (l.is_public in (true, ?)) ) as blue, ( select sum(cl.quantity) from cards c join collection cl on c.id = cl.card_id - where c.color_identity like '%G%' and cl.quantity > 0 and cl.online = ? + join lists l on cl.list_id = l.id + where c.color_identity like '%G%' + and cl.quantity > 0 + and cl.online = ? + and cl.list_id = ? + and (l.is_public in (true, ?)) ) as green, ( select sum(cl.quantity) from cards c join collection cl on c.id = cl.card_id - where c.color_identity like '%R%' and cl.quantity > 0 and cl.online = ? + join lists l on cl.list_id = l.id + where c.color_identity like '%R%' + and cl.quantity > 0 + and cl.online = ? + and cl.list_id = ? + and (l.is_public in (true, ?)) ) as red, ( select sum(cl.quantity) from cards c join collection cl on c.id = cl.card_id - where c.color_identity like '%W%' and cl.quantity > 0 and cl.online = ? + join lists l on cl.list_id = l.id + where c.color_identity like '%W%' + and cl.quantity > 0 + and cl.online = ? + and cl.list_id = ? + and (l.is_public in (true, ?)) ) as white, ( select sum(cl.quantity) from cards c join collection cl on c.id = cl.card_id - where c.color_identity = '' and cl.quantity > 0 and cl.online = ? + join lists l on cl.list_id = l.id + where c.color_identity = '' + and cl.quantity > 0 + and cl.online = ? + and cl.list_id = ? + and (l.is_public in (true, ?)) ) as colorless" - online? online? online? online? online? online?])) + online? list-id public-only? + online? list-id public-only? + online? list-id public-only? + online? list-id public-only? + online? list-id public-only? + online? list-id public-only?])) (defn basic-type-totals - [online?] - (let [online? (boolean online?)] + [online? list-id user-id] + ;; TODO: i really dislike how this query is written ... try to clean this up at some point maybe? + (let [online? (boolean online?) + list-id (int list-id) + public-only? (nil? user-id)] ["select ( select coalesce(sum(cl.quantity), 0) from cards c join collection cl on c.id = cl.card_id - where c.types like '%Artifact%' and cl.quantity > 0 and cl.online = ? + join lists l on cl.list_id = l.id + where c.types like '%Artifact%' + and cl.quantity > 0 + and cl.online = ? + and cl.list_id = ? + and (l.is_public in (true, ?)) ) as artifacts, ( select coalesce(sum(cl.quantity), 0) from cards c join collection cl on c.id = cl.card_id - where c.types like '%Creature%' and cl.quantity > 0 and cl.online = ? + join lists l on cl.list_id = l.id + where c.types like '%Creature%' + and cl.quantity > 0 + and cl.online = ? + and cl.list_id = ? + and (l.is_public in (true, ?)) ) as creatures, ( select coalesce(sum(cl.quantity), 0) from cards c join collection cl on c.id = cl.card_id - where c.types like '%Enchantment%' and cl.quantity > 0 and cl.online = ? + join lists l on cl.list_id = l.id + where c.types like '%Enchantment%' + and cl.quantity > 0 + and cl.online = ? + and cl.list_id = ? + and (l.is_public in (true, ?)) ) as enchantments, ( select coalesce(sum(cl.quantity), 0) from cards c join collection cl on c.id = cl.card_id - where c.types like '%Instant%' and cl.quantity > 0 and cl.online = ? + join lists l on cl.list_id = l.id + where c.types like '%Instant%' + and cl.quantity > 0 + and cl.online = ? + and cl.list_id = ? + and (l.is_public in (true, ?)) ) as instants, ( select coalesce(sum(cl.quantity), 0) from cards c join collection cl on c.id = cl.card_id - where c.types like '%Land%' and cl.quantity > 0 and cl.online = ? + join lists l on cl.list_id = l.id + where c.types like '%Land%' + and cl.quantity > 0 + and cl.online = ? + and cl.list_id = ? + and (l.is_public in (true, ?)) ) as lands, ( select coalesce(sum(cl.quantity), 0) from cards c join collection cl on c.id = cl.card_id - where c.types like '%Planeswalker%' and cl.quantity > 0 and cl.online = ? + join lists l on cl.list_id = l.id + where c.types like '%Planeswalker%' + and cl.quantity > 0 + and cl.online = ? + and cl.list_id = ? + and (l.is_public in (true, ?)) ) as planeswalkers, ( select coalesce(sum(cl.quantity), 0) from cards c join collection cl on c.id = cl.card_id - where c.types like '%Tribal%' and cl.quantity > 0 and cl.online = ? + join lists l on cl.list_id = l.id + where c.types like '%Tribal%' + and cl.quantity > 0 + and cl.online = ? + and cl.list_id = ? + and (l.is_public in (true, ?)) ) as tribals, ( select coalesce(sum(cl.quantity), 0) from cards c join collection cl on c.id = cl.card_id - where c.types like '%Sorcery%' and cl.quantity > 0 and cl.online = ? + join lists l on cl.list_id = l.id + where c.types like '%Sorcery%' + and cl.quantity > 0 + and cl.online = ? + and cl.list_id = ? + and (l.is_public in (true, ?)) ) as sorcerys" - online? online? online? online? online? online? online? online?])) + online? list-id public-only? + online? list-id public-only? + online? list-id public-only? + online? list-id public-only? + online? list-id public-only? + online? list-id public-only? + online? list-id public-only? + online? list-id public-only?])) (defn most-common-types - [online?] - ["select * - from ( - select sum(cl.quantity) as quantity, c.types - from cards c - join collection cl on c.id = cl.card_id - where cl.quantity > 0 and cl.online = ? - group by c.types - ) types_totals - order by quantity desc - limit 10" - (boolean online?)]) + [online? list-id user-id] + (let [online? (boolean online?) + list-id (int list-id) + public-only? (nil? user-id)] + ["select * + from ( + select sum(cl.quantity) as quantity, c.types + from cards c + join collection cl on c.id = cl.card_id + join lists l on cl.list_id = l.id + where cl.quantity > 0 + and cl.online = ? + and cl.list_id = ? + and (l.is_public in (true, ?)) + group by c.types + ) types_totals + order by quantity desc + limit 10" + online? list-id public-only?])) (defn total-sets-owned-from - [online?] - ["select count(distinct c.set_code) - from cards c - join collection cl on c.id = cl.card_id - where cl.quantity > 0 and cl.online = ?" - (boolean online?)]) + [online? list-id user-id] + (let [online? (boolean online?) + list-id (int list-id) + public-only? (nil? user-id)] + ["select count(distinct c.set_code) + from cards c + join collection cl on c.id = cl.card_id + join lists l on cl.list_id = l.id + where cl.quantity > 0 + and cl.online = ? + and cl.list_id = ? + and (l.is_public in (true, ?))" + online? list-id public-only?])) (defn total-sets-owned-all-from - [online?] - ["select count(*) - from ( - select - ( - select count(*) - from cards - where set_code = s.code - ) as num_in_set, - ( - select coalesce(sum(cl.quantity), 0) - from cards c - join collection cl on c.id = cl.card_id - where cl.quantity > 0 and cl.online = ? and c.set_code = s.code - ) as owned_in_set, - s.code - from sets s - ) as set_owned_counts - where set_owned_counts.num_in_set = set_owned_counts.owned_in_set" - (boolean online?)]) + [online? list-id user-id] + (let [online? (boolean online?) + list-id (int list-id) + public-only? (nil? user-id)] + ["select count(*) + from ( + select + ( + select count(*) + from cards + where set_code = s.code + ) as num_in_set, + ( + select coalesce(sum(cl.quantity), 0) + from cards c + join collection cl on c.id = cl.card_id + join lists l on cl.list_id = l.id + where cl.quantity > 0 + and cl.online = ? + and c.set_code = s.code + and cl.list_id = ? + and (l.is_public in (true, ?)) + ) as owned_in_set, + s.code + from sets s + ) as set_owned_counts + where set_owned_counts.num_in_set = set_owned_counts.owned_in_set" + online? list-id public-only?])) (defn most-owned-sets - [online?] - ["select * - from ( - select sum(cl.quantity) as quantity, c.set_code - from cards c - join sets s on c.set_code = s.code - join collection cl on c.id = cl.card_id - where cl.quantity > 0 and cl.online = ? - group by c.set_code - ) sets_totals - order by quantity desc - limit 10" - (boolean online?)]) + [online? list-id user-id] + (let [online? (boolean online?) + list-id (int list-id) + public-only? (nil? user-id)] + ["select * + from ( + select sum(cl.quantity) as quantity, c.set_code + from cards c + join sets s on c.set_code = s.code + join collection cl on c.id = cl.card_id + join lists l on cl.list_id = l.id + where cl.quantity > 0 + and cl.online = ? + and cl.list_id = ? + and (l.is_public in (true, ?)) + group by c.set_code + ) sets_totals + order by quantity desc + limit 10" + online? list-id public-only?])) (defn most-copies-of-card - [online?] - ["select quantity, c.id, c.name, c.set_code - from ( - select sum(cl.quantity) as quantity, c2.id as card_id - from cards c2 - join collection cl on c2.id = cl.card_id - where cl.quantity > 0 and cl.online = ? - group by c2.id - ) copies_of_cards - join cards c on c.id = card_id - order by quantity desc - limit 10" - (boolean online?)]) + [online? list-id user-id] + (let [online? (boolean online?) + list-id (int list-id) + public-only? (nil? user-id)] + ["select quantity, c.id, c.name, c.set_code + from ( + select sum(cl.quantity) as quantity, c2.id as card_id + from cards c2 + join collection cl on c2.id = cl.card_id + join lists l on cl.list_id = l.id + where cl.quantity > 0 + and cl.online = ? + and cl.list_id = ? + and (l.is_public in (true, ?)) + group by c2.id + ) copies_of_cards + join cards c on c.id = card_id + order by quantity desc + limit 10" + online? list-id public-only?])) (defn most-nonland-copies-of-card - [online?] - ["select quantity, c.id, c.name, c.set_code - from ( - select sum(cl.quantity) as quantity, c2.id as card_id - from cards c2 - join collection cl on c2.id = cl.card_id - where cl.quantity > 0 and cl.online = ? and c2.type not like '%Land%' - group by c2.id - ) copies_of_cards - join cards c on c.id = card_id - order by quantity desc - limit 10" - (boolean online?)]) + [online? list-id user-id] + (let [online? (boolean online?) + list-id (int list-id) + public-only? (nil? user-id)] + ["select quantity, c.id, c.name, c.set_code + from ( + select sum(cl.quantity) as quantity, c2.id as card_id + from cards c2 + join collection cl on c2.id = cl.card_id + join lists l on cl.list_id = l.id + where cl.quantity > 0 + and cl.online = ? + and c2.type not like '%Land%' + and cl.list_id = ? + and (l.is_public in (true, ?)) + group by c2.id + ) copies_of_cards + join cards c on c.id = card_id + order by quantity desc + limit 10" + online? list-id public-only?])) (defn total-price - [online? price-source] - (let [online? (boolean online?)] + [online? price-source list-id user-id] + (let [online? (boolean online?) + list-id (int list-id) + public-only? (nil? user-id)] ["select cast(coalesce(sum(sub_total), 0) as decimal(10, 2)) as total from ( select (cl.quantity * cp.price) as sub_total from cards c join card_prices cp on c.id = cp.card_id join collection cl on c.id = cl.card_id - where cl.quantity > 0 and cl.online = ? and cp.online = ? and cp.source = ? + join lists l on cl.list_id = l.id + where cl.quantity > 0 + and cl.online = ? + and cp.online = ? + and cp.source = ? + and cl.list_id = ? + and (l.is_public in (true, ?)) ) as sub_totals" - online? online? price-source])) + online? online? price-source list-id public-only?])) (defn agg-price-stats - [online? price-source] - (let [online? (boolean online?)] + [online? price-source list-id user-id] + (let [online? (boolean online?) + list-id (int list-id) + public-only? (nil? user-id)] ["select cast(coalesce(min(cp.price), 0) as decimal(10, 2)) as min_price, cast(coalesce(max(cp.price), 0) as decimal(10, 2)) as max_price, cast(coalesce(avg(cp.price), 0) as decimal(10, 2)) as avg_price, @@ -230,37 +392,67 @@ from cards c join card_prices cp on c.id = cp.card_id join collection cl on c.id = cl.card_id - where cl.quantity > 0 and cl.online = ? and cp.online = ? and cp.source = ?" - online? online? price-source])) + join lists l on cl.list_id = l.id + where cl.quantity > 0 + and cl.online = ? + and cp.online = ? + and cp.source = ? + and cl.list_id = ? + and (l.is_public in (true, ?))" + online? online? price-source list-id public-only?])) (defn most-valuable-cards - [online? price-source] - (let [online? (boolean online?)] + [online? price-source list-id user-id] + (let [online? (boolean online?) + list-id (int list-id) + public-only? (nil? user-id)] ["select c.id, c.name, c.set_code, cp.price from cards c join card_prices cp on c.id = cp.card_id join collection cl on c.id = cl.card_id - where cl.quantity > 0 and cl.online = ? and cp.online = ? and cp.source = ? + join lists l on cl.list_id = l.id + where cl.quantity > 0 + and cl.online = ? + and cp.online = ? + and cp.source = ? + and cl.list_id = ? + and (l.is_public in (true, ?)) order by cp.price desc limit 10" - online? online? price-source])) + online? online? price-source list-id public-only?])) (defn num-cards-worth-over-1-dollar - [online? price-source] - (let [online? (boolean online?)] + [online? price-source list-id user-id] + (let [online? (boolean online?) + list-id (int list-id) + public-only? (nil? user-id)] ["select coalesce(sum(cl.quantity), 0) as count from cards c join card_prices cp on c.id = cp.card_id join collection cl on c.id = cl.card_id - where cl.quantity > 0 and cl.online = ? and cp.online = ? and cp.source = ? and cp.price >= 1.0" - online? online? price-source])) + join lists l on cl.list_id = l.id + where cl.quantity > 0 + and cl.online = ? + and cp.online = ? + and cp.source = ? + and cp.price >= 1.0 + and cl.list_id = ? + and (l.is_public in (true, ?))" + online? online? price-source list-id public-only?])) (defn card-rarity-totals - [online?] - ["select coalesce(sum(cl.quantity), 0) as total, c.rarity - from cards c - join collection cl on c.id = cl.card_id - where cl.quantity > 0 and cl.online = ? - group by c.rarity - order by total desc" - (boolean online?)]) \ No newline at end of file + [online? list-id user-id] + (let [online? (boolean online?) + list-id (int list-id) + public-only? (nil? user-id)] + ["select coalesce(sum(cl.quantity), 0) as total, c.rarity + from cards c + join collection cl on c.id = cl.card_id + join lists l on cl.list_id = l.id + where cl.quantity > 0 + and cl.online = ? + and cl.list_id = ? + and (l.is_public in (true, ?)) + group by c.rarity + order by total desc" + online? list-id public-only?])) \ No newline at end of file