commit
0b0fb50aac
|
@ -0,0 +1,3 @@
|
|||
docker-compose.yml
|
||||
Dockerfile
|
||||
Jenkinsfile
|
|
@ -23,3 +23,7 @@ profiles.clj
|
|||
.idea
|
||||
resources/contracts
|
||||
node_modules
|
||||
/config-prod.edn
|
||||
/config-dev.edn
|
||||
/config-test.edn
|
||||
/src/java
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
FROM clojure as builder
|
||||
|
||||
WORKDIR /tmp
|
||||
|
||||
RUN wget -O /usr/local/bin/solc https://github.com/ethereum/solidity/releases/download/v0.4.15/solc-static-linux
|
||||
RUN chmod +x /usr/local/bin/solc
|
||||
|
||||
RUN wget https://github.com/web3j/web3j/releases/download/v2.3.0/web3j-2.3.0.tar
|
||||
RUN tar -xf web3j-2.3.0.tar
|
||||
RUN cp -r web3j-2.3.0/* /usr/local/
|
||||
|
||||
|
||||
COPY . /usr/src/app
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
ENV LEIN_SNAPSHOTS_IN_RELEASE=1
|
||||
|
||||
|
||||
RUN lein less once
|
||||
RUN lein uberjar
|
||||
|
||||
|
||||
FROM clojure
|
||||
WORKDIR /root/
|
||||
|
||||
COPY --from=builder /usr/src/app/target/uberjar/commiteth.jar .
|
||||
|
||||
CMD [""]
|
||||
ENTRYPOINT ["/usr/bin/java", "-Duser.timezone=UTC", "-Dconf=config-test.edn", "-jar", "/root/commiteth.jar"]
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
#!/usr/bin/env groovy
|
||||
|
||||
node('linux') {
|
||||
checkout scm
|
||||
|
||||
def dockerreponame = "statusim/openbounty-app"
|
||||
|
||||
try {
|
||||
stage('Build & push') {
|
||||
|
||||
GIT_COMMIT_HASH = sh (script: "git rev-parse --short HEAD | tr -d '\n'", returnStdout: true)
|
||||
|
||||
docker.withRegistry('https://index.docker.io/v1/', 'dockerhub-statusvan') {
|
||||
def openbountyApp = docker.build("${dockerreponame}:${env.BUILD_NUMBER}")
|
||||
openbountyApp.push("${env.BRANCH_NAME}")
|
||||
if (env.BRANCH_NAME == 'develop') {
|
||||
openbountyApp.push("develop")
|
||||
} else if (env.BRANCH_NAME == 'master') {
|
||||
openbountyApp.push("master")
|
||||
} else {
|
||||
println "Not named branch have no custom tag"
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
stage('Deploy') {
|
||||
build job: 'status-openbounty/openbounty-cluster', parameters: [[$class: 'StringParameterValue', name: 'DEPLOY_ENVIRONMENT', value: "dev"], [$class: 'StringParameterValue', name: 'BRANCH', value: env.BRANCH_NAME]]
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
// slackSend color: 'bad', message: REPO + ":" + BRANCH_NAME + ' failed to build. ' + env.BUILD_URL
|
||||
throw e
|
||||
}
|
||||
}
|
|
@ -16,7 +16,7 @@ The `develop` branch is automatically deployed here.
|
|||
|
||||
## Prerequisites
|
||||
|
||||
You will need [Leiningen](https://github.com/technomancy/leiningen) 2.0 or above installed.
|
||||
You will need [Leiningen](https://github.com/technomancy/leiningen) 2.0 or above installed. Also, make sure that you have [wkhtmltoimage](https://wkhtmltopdf.org/downloads.html) available in your PATH. On macOS, it can be installed via `brew cask install wkhtmltopdf`.
|
||||
|
||||
### PostgreSQL
|
||||
|
||||
|
@ -55,7 +55,7 @@ Web3j [2.3.0](https://github.com/web3j/web3j/releases/tag/v2.3.0) is required an
|
|||
|
||||
## Application config
|
||||
|
||||
Make sure that `env/dev/resources/config.edn` is correctly populated. Description of config fields is given below:
|
||||
Make sure to create `/config-dev.edn` and populate it correctly, which is based on `env/dev/resources/config.edn`. Description of config fields is given below:
|
||||
|
||||
Key | Description
|
||||
--- | ---
|
||||
|
@ -81,7 +81,7 @@ testnet-token-data | Token data map, useful if there are Geth connectivity probl
|
|||
Open Bounty uses both OAuth App and GitHub App integration.
|
||||
|
||||
### OAuth App
|
||||
Follow the steps [here](https://developer.github.com/apps/building-oauth-apps/creating-an-oauth-app/). Specify the value of `:server-address` as "Homepage URL", and `:server-address` + `/callback` as "Authorization callback URL". Be sure to copy Client ID and Client Secret values to `config.edn`.
|
||||
Follow the steps [here](https://developer.github.com/apps/building-oauth-apps/creating-an-oauth-app/). Specify the value of `:server-address` as "Homepage URL", and `:server-address` + `/callback` as "Authorization callback URL". Be sure to copy Client ID and Client Secret values in the config file.
|
||||
|
||||
### GitHub App
|
||||
Follow the steps [here](https://developer.github.com/apps/building-github-apps/creating-a-github-app/). Be sure to specify `:server-address` + `/webhook-app` as "Webhook URL", and `:webhook-secret` as "Webhook Secret".
|
||||
|
|
|
@ -59,3 +59,11 @@ To remove issue from the Bounties list you can close it in GitHub.
|
|||
All bugs should be reported as issues in the [OpenBounty Github repository](https://github.com/status-im/open-bounty/issues).
|
||||
|
||||
Please first check that there is not already a duplicate issue. Issues should contain exact and minimal step-by-step instructions for reproducing the problem.
|
||||
|
||||
### Status Open Bounty end-to-end tests
|
||||
|
||||
Framework for testing located in: `open-bounty/test/end-to-end`
|
||||
|
||||
Full installation and configuring manual: [Status Open Bounty end-to-end tests](https://wiki.status.im/Status_Open_Bounty_end-to-end_tests)
|
||||
|
||||
Currently supports local and Jenkins environment running (you can find example of JenkinsFile in `open-bounty/test` )
|
||||
|
|
|
@ -11,16 +11,18 @@
|
|||
:eth-password "XXX"
|
||||
|
||||
;; RPC URL to ethereum node to be used
|
||||
:eth-rpc-url "http://localhost:8547"
|
||||
:eth-rpc-url "http://localhost:8545"
|
||||
:eth-wallet-file "/some/location"
|
||||
|
||||
;; address of token registry to be used
|
||||
:tokenreg-addr "0x..."
|
||||
;; this is the default value for ropsten
|
||||
:tokenreg-addr "0x7d127a3e3b5e72cd8f15e7dee650abe4fcced2b9"
|
||||
;; format of tokenreg records' base field, possible values :status, :parity
|
||||
:tokenreg-base-format :parity
|
||||
:tokenreg-base-format :status
|
||||
|
||||
;; address of factory contract used for deploying bounty contracts
|
||||
:contract-factory-addr "0x..."
|
||||
;; this is the default value for ropsten
|
||||
:contract-factory-addr "0x3B9A3c062Bdb640b5039C0cCda4157737d732F95"
|
||||
|
||||
;; commiteth-test-tpatja
|
||||
:github-client-id "CLIENT ID"
|
||||
|
@ -43,5 +45,7 @@
|
|||
;; needeed when :hubspot-contact-create-enabled
|
||||
:hubspot-api-key "xxxxxxx-xxxx-x-xxxx-xxxx"
|
||||
|
||||
:user-whitelist #{}
|
||||
|
||||
;; used for blacklisting tokens from token registry data
|
||||
:token-blacklist #{}}
|
||||
|
|
10
project.clj
10
project.clj
|
@ -69,7 +69,6 @@
|
|||
[lein-auto "0.1.2"]
|
||||
[lein-less "1.7.5"]
|
||||
[lein-shell "0.5.0"]
|
||||
[cider/cider-nrepl "0.15.0-SNAPSHOT"]
|
||||
[lein-sha-version "0.1.1"]]
|
||||
|
||||
|
||||
|
@ -94,7 +93,8 @@
|
|||
|
||||
|
||||
:profiles
|
||||
{:uberjar {:omit-source true
|
||||
{:uberjar {:jvm-opts ["-server" "-Dconf=config-prod.edn"]
|
||||
:omit-source true
|
||||
:prep-tasks ["build-contracts" "javac" "compile" ["cljsbuild" "once" "min"] ["less" "once"]]
|
||||
:cljsbuild
|
||||
{:builds
|
||||
|
@ -116,7 +116,8 @@
|
|||
:uberjar-name "commiteth.jar"
|
||||
:source-paths ["env/prod/clj"]
|
||||
:resource-paths ["env/prod/resources"]}
|
||||
:dev {:dependencies [[prone "1.1.4"]
|
||||
:dev {:jvm-opts ["-server" "-Dconf=config-dev.edn"]
|
||||
:dependencies [[prone "1.1.4"]
|
||||
[ring/ring-mock "0.3.1"]
|
||||
[ring/ring-devel "1.6.2"]
|
||||
[pjstadig/humane-test-output "0.8.3"]
|
||||
|
@ -150,7 +151,8 @@
|
|||
:nrepl-middleware [cemerick.piggieback/wrap-cljs-repl]}
|
||||
:injections [(require 'pjstadig.humane-test-output)
|
||||
(pjstadig.humane-test-output/activate!)]}
|
||||
:test {:resource-paths ["env/dev/resources" "env/test/resources"]
|
||||
:test {:jvm-opts ["-server" "-Dconf=config-test.edn"]
|
||||
:resource-paths ["env/dev/resources" "env/test/resources"]
|
||||
:dependencies [[devcards "0.2.4"]]
|
||||
:cljsbuild
|
||||
{:builds
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20">
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
<rect width="20" height="20" fill="#000" opacity=".5" rx="6"/>
|
||||
<path fill="#57A7ED" d="M10 8.812L6.436 5.247a.839.839 0 0 0-1.19 0 .839.839 0 0 0 .001 1.189L8.812 10l-3.565 3.564a.839.839 0 0 0 0 1.19c.33.33.86.327 1.189-.001L10 11.188l3.564 3.565c.33.33.861.328 1.19 0a.839.839 0 0 0-.001-1.189L11.188 10l3.565-3.564a.839.839 0 0 0 0-1.19.839.839 0 0 0-1.189.001L10 8.812z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 533 B |
Binary file not shown.
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 327 KiB |
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 322 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="7" viewBox="0 0 12 7">
|
||||
<path fill="#8D99A4" fill-rule="evenodd" d="M6 3.828L2.462.291A1.003 1.003 0 0 0 1.05.293a.996.996 0 0 0-.002 1.412l4.247 4.247c.192.192.447.289.702.29a.981.981 0 0 0 .708-.29l4.247-4.247A1.003 1.003 0 0 0 10.95.293.996.996 0 0 0 9.538.29L6 3.828z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 344 B |
|
@ -1,7 +1,7 @@
|
|||
(ns commiteth.core
|
||||
(:require [commiteth.handler :as handler]
|
||||
[clojure.tools.nrepl.server :as nrepl-server]
|
||||
[cider.nrepl :refer [cider-nrepl-handler]]
|
||||
[luminus.repl-server :as repl]
|
||||
[luminus.http-server :as http]
|
||||
[luminus-migrations.core :as migrations]
|
||||
[commiteth.config :refer [env]]
|
||||
|
@ -31,7 +31,7 @@ repl-server
|
|||
(when-let [nrepl-port (env :nrepl-port)]
|
||||
(log/info "Starting NREPL server on port" nrepl-port)
|
||||
(nrepl-server/start-server :port nrepl-port
|
||||
:handler cider-nrepl-handler))
|
||||
:handler nrepl-server/default-handler))
|
||||
:stop
|
||||
(when repl-server
|
||||
(nrepl-server/stop-server repl-server)))
|
||||
|
|
|
@ -29,7 +29,7 @@
|
|||
:confirmation (eth/event-sig->topic-id "Confirmation(address,uint256)")})
|
||||
|
||||
(defn factory-contract-addr []
|
||||
(env :contract-factory-addr "0x47F56FD26EEeCda4FdF5DB5843De1fe75D2A64A6"))
|
||||
(env :contract-factory-addr))
|
||||
|
||||
(defn tokenreg-base-format
|
||||
;; status tokenreg uses eg :base 18, while parity uses :base 1000000000000
|
||||
|
|
|
@ -1,10 +1,15 @@
|
|||
(ns commiteth.bounties
|
||||
(:require [re-frame.core :as rf]
|
||||
[reagent.core :as r]
|
||||
(:require [reagent.core :as r]
|
||||
[re-frame.core :as rf]
|
||||
[commiteth.common :refer [moment-timestamp
|
||||
display-data-page
|
||||
items-per-page
|
||||
issue-url]]))
|
||||
issue-url]]
|
||||
[commiteth.handlers :as handlers]
|
||||
[commiteth.db :as db]
|
||||
[commiteth.ui-model :as ui-model]
|
||||
[commiteth.subscriptions :as subs]
|
||||
[commiteth.util :as util]))
|
||||
|
||||
|
||||
(defn bounty-item [bounty]
|
||||
|
@ -40,22 +45,174 @@
|
|||
[:span.usd-value-label "Value "] [:span.usd-balance-label (str "$" value-usd)]
|
||||
(when (> claim-count 0)
|
||||
[:span.open-claims-label (str claim-count " open claim"
|
||||
(when (> claim-count 1) "s"))]) ]]
|
||||
(when (> claim-count 1) "s"))])]]
|
||||
[:div.open-bounty-item-icon
|
||||
[:div.ui.tiny.circular.image
|
||||
[:img {:src avatar-url}]]]]))
|
||||
|
||||
(defn bounties-filter-tooltip-value-input-view [label tooltip-open? opts]
|
||||
[:div.open-bounties-filter-element-tooltip-value-input-container
|
||||
[:div.:input.open-bounties-filter-element-tooltip-value-input-label
|
||||
label]
|
||||
[:input.open-bounties-filter-element-tooltip-value-input
|
||||
{:type "range"
|
||||
:min (:min opts)
|
||||
:max (:max opts)
|
||||
:step (:step opts)
|
||||
:value (:current-val opts)
|
||||
:on-change (when-let [f (:on-change-val opts)]
|
||||
#(-> % .-target .-value int f))
|
||||
:on-focus #(reset! tooltip-open? true)}]])
|
||||
|
||||
(defmulti bounties-filter-tooltip-view #(-> %2 ::ui-model/bounty-filter-type.category))
|
||||
|
||||
(defmethod bounties-filter-tooltip-view ::ui-model/bounty-filter-type-category|range
|
||||
[filter-type filter-type-def current-filter-value tooltip-open?]
|
||||
(let [default-min (::ui-model/bounty-filter-type.min-val filter-type-def)
|
||||
default-max (::ui-model/bounty-filter-type.max-val filter-type-def)
|
||||
common-range-opts {:min default-min :max default-max}
|
||||
current-min (or (first current-filter-value) default-min)
|
||||
current-max (or (second current-filter-value) default-max)
|
||||
on-change-fn (fn [min-val max-val]
|
||||
(rf/dispatch [::handlers/set-open-bounty-filter-type
|
||||
filter-type
|
||||
[(min min-val default-max)
|
||||
(max max-val default-min)]]))
|
||||
on-min-change-fn (fn [new-min]
|
||||
(let [new-max (max current-max (min default-max new-min))]
|
||||
(on-change-fn new-min new-max)))
|
||||
on-max-change-fn (fn [new-max]
|
||||
(let [new-min (min current-min (max default-min new-max))]
|
||||
(on-change-fn new-min new-max)))]
|
||||
[:div.open-bounties-filter-list
|
||||
(::ui-model/bounty-filter.type.header filter-type-def)
|
||||
[bounties-filter-tooltip-value-input-view "Min" tooltip-open? (merge common-range-opts
|
||||
{:current-val current-min
|
||||
:on-change-val on-min-change-fn})]
|
||||
[bounties-filter-tooltip-value-input-view "Max" tooltip-open? (merge common-range-opts
|
||||
{:current-val current-max
|
||||
:on-change-val on-max-change-fn})]]))
|
||||
|
||||
(defmethod bounties-filter-tooltip-view ::ui-model/bounty-filter-type-category|single-static-option
|
||||
[filter-type filter-type-def current-filter-value tooltip-open?]
|
||||
[:div.open-bounties-filter-list
|
||||
(for [[option-type option-text] (::ui-model/bounty-filter-type.options filter-type-def)]
|
||||
^{:key (str option-type)}
|
||||
[:div.open-bounties-filter-list-option
|
||||
(merge {:on-click #(do (rf/dispatch [::handlers/set-open-bounty-filter-type
|
||||
filter-type
|
||||
option-type])
|
||||
(reset! tooltip-open? false))
|
||||
:tab-index 0
|
||||
:on-focus #(reset! tooltip-open? true)}
|
||||
(when (= option-type current-filter-value)
|
||||
{:class "active"}))
|
||||
option-text])])
|
||||
|
||||
(defmethod bounties-filter-tooltip-view ::ui-model/bounty-filter-type-category|multiple-dynamic-options
|
||||
[filter-type filter-type-def current-filter-value tooltip-open?]
|
||||
(let [options (rf/subscribe [(::ui-model/bounty-filter-type.re-frame-subs-key-for-options filter-type-def)])]
|
||||
[:div.open-bounties-filter-list
|
||||
(for [option @options]
|
||||
(let [active? (boolean (and current-filter-value (current-filter-value option)))]
|
||||
^{:key (str option)}
|
||||
[:div.open-bounties-filter-list-option-checkbox
|
||||
[:label
|
||||
{:on-click #(rf/dispatch [::handlers/set-open-bounty-filter-type
|
||||
filter-type
|
||||
(cond
|
||||
(and active? (= #{option} current-filter-value)) nil
|
||||
active? (disj current-filter-value option)
|
||||
:else (into #{option} current-filter-value))])
|
||||
:tab-index 0
|
||||
:on-focus #(do (.stopPropagation %) (reset! tooltip-open? true))}
|
||||
[:input
|
||||
{:type "checkbox"
|
||||
:checked active?
|
||||
:on-focus #(reset! tooltip-open? true)}]
|
||||
[:div.text option]]]))]))
|
||||
|
||||
(defn bounty-filter-view [filter-type current-filter-value]
|
||||
(let [tooltip-open? (r/atom false)]
|
||||
(fn [filter-type current-filter-value]
|
||||
[:div.open-bounties-filter-element-container
|
||||
{:tab-index 0
|
||||
:on-focus #(reset! tooltip-open? true)
|
||||
:on-blur #(reset! tooltip-open? false)}
|
||||
[:div.open-bounties-filter-element
|
||||
{:on-mouse-down #(swap! tooltip-open? not)
|
||||
:class (when (or current-filter-value @tooltip-open?)
|
||||
"open-bounties-filter-element-active")}
|
||||
[:div.text
|
||||
(if current-filter-value
|
||||
(ui-model/bounty-filter-value->short-text filter-type current-filter-value)
|
||||
(ui-model/bounty-filter-type->name filter-type))]
|
||||
(when current-filter-value
|
||||
[:div.remove-container
|
||||
{:tab-index 0
|
||||
:on-focus #(.stopPropagation %)}
|
||||
[:img.remove
|
||||
{:src "bounty-filter-remove.svg"
|
||||
:on-mouse-down (fn [e]
|
||||
(.stopPropagation e)
|
||||
(rf/dispatch [::handlers/set-open-bounty-filter-type filter-type nil])
|
||||
(reset! tooltip-open? false))}]])]
|
||||
(when @tooltip-open?
|
||||
[:div.open-bounties-filter-element-tooltip
|
||||
[bounties-filter-tooltip-view
|
||||
filter-type
|
||||
(ui-model/bounty-filter-types-def filter-type)
|
||||
current-filter-value
|
||||
tooltip-open?]])])))
|
||||
|
||||
(defn bounty-filters-view []
|
||||
(let [current-filters (rf/subscribe [::subs/open-bounties-filters])]
|
||||
[:div.open-bounties-filter
|
||||
; doall because derefs are not supported in lazy seqs: https://github.com/reagent-project/reagent/issues/18
|
||||
(doall
|
||||
(for [filter-type ui-model/bounty-filter-types]
|
||||
^{:key (str filter-type)}
|
||||
[bounty-filter-view
|
||||
filter-type
|
||||
(get @current-filters filter-type)]))]))
|
||||
|
||||
(defn bounties-sort-view []
|
||||
(let [open? (r/atom false)]
|
||||
(fn []
|
||||
(let [current-sorting (rf/subscribe [::subs/open-bounties-sorting-type])]
|
||||
[:div.open-bounties-sort
|
||||
{:tab-index 0
|
||||
:on-blur #(reset! open? false)}
|
||||
[:div.open-bounties-sort-element
|
||||
{:on-click #(swap! open? not)}
|
||||
(ui-model/bounty-sorting-type->name @current-sorting)
|
||||
[:div.icon-forward-white-box
|
||||
[:img
|
||||
{:src "icon-forward-white.svg"}]]]
|
||||
(when @open?
|
||||
[:div.open-bounties-sort-element-tooltip
|
||||
(for [sorting-type (keys ui-model/bounty-sorting-types-def)]
|
||||
^{:key (str sorting-type)}
|
||||
[:div.open-bounties-sort-type
|
||||
{:on-click #(do
|
||||
(reset! open? false)
|
||||
(rf/dispatch [::handlers/set-open-bounties-sorting-type sorting-type]))}
|
||||
(ui-model/bounty-sorting-type->name sorting-type)])])]))))
|
||||
|
||||
(defn bounties-list [{:keys [items item-count page-number total-count]
|
||||
:as bounty-page-data}
|
||||
container-element]
|
||||
(if (empty? items)
|
||||
[:div.view-no-data-container
|
||||
[:p "No recent activity yet"]]
|
||||
[:p "No matching bounties found."]]
|
||||
[:div
|
||||
(let [left (inc (* (dec page-number) items-per-page))
|
||||
right (dec (+ left item-count))]
|
||||
[:div.item-counts-label-and-sorting-container
|
||||
[:div.item-counts-label
|
||||
[:span (str "Showing " left "-" right " of " total-count)]])
|
||||
[:span (str "Showing " left "-" right " of " total-count)]]
|
||||
(when-not (util/os-windows?)
|
||||
[bounties-sort-view])])
|
||||
(display-data-page bounty-page-data bounty-item container-element)]))
|
||||
|
||||
(defn bounties-page []
|
||||
|
@ -70,5 +227,7 @@
|
|||
[:div.ui.container.open-bounties-container
|
||||
{:ref #(reset! container-element %1)}
|
||||
[:div.open-bounties-header "Bounties"]
|
||||
[bounties-list @bounty-page-data container-element]]))
|
||||
))
|
||||
(when-not (util/os-windows?)
|
||||
[:div.open-bounties-filter-and-sort
|
||||
[bounty-filters-view]])
|
||||
[bounties-list @bounty-page-data container-element]]))))
|
||||
|
|
|
@ -208,12 +208,12 @@
|
|||
[:div.ui.vertical.segment
|
||||
[:div.ui.container
|
||||
[:div.ui.grid.stackable
|
||||
[:div {:class (str (if (show-top-hunters?) "ten" "sixteen")
|
||||
[:div {:class (str (if (show-top-hunters?) "eleven" "sixteen")
|
||||
" wide computer sixteen wide tablet column")}
|
||||
[:div.ui.container
|
||||
[(pages @current-page)]]]
|
||||
(when (show-top-hunters?)
|
||||
[:div.six.wide.column.computer.only
|
||||
[:div.five.wide.column.computer.only
|
||||
[:div.ui.container.top-hunters
|
||||
[:h3.top-hunters-header "Top 5 hunters"]
|
||||
[:div.top-hunters-subheader "All time"]
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
(ns commiteth.db)
|
||||
(ns commiteth.db
|
||||
(:require [commiteth.ui-model :as ui-model]))
|
||||
|
||||
(def default-db
|
||||
{:page :bounties
|
||||
|
@ -9,6 +10,13 @@
|
|||
:open-bounties-loading? false
|
||||
:open-bounties []
|
||||
:page-number 1
|
||||
:bounty-page-number 1
|
||||
:activity-page-number 1
|
||||
::open-bounties-sorting-type ::ui-model/bounty-sorting-type|most-recent
|
||||
::open-bounties-filters {::ui-model/bounty-filter-type|value nil
|
||||
::ui-model/bounty-filter-type|currency nil
|
||||
::ui-model/bounty-filter-type|date nil
|
||||
::ui-model/bounty-filter-type|owner nil}
|
||||
:owner-bounties {}
|
||||
:top-hunters []
|
||||
:activity-feed []})
|
||||
|
|
|
@ -11,7 +11,8 @@
|
|||
[cljs-web3.eth :as web3-eth]
|
||||
[akiroz.re-frame.storage
|
||||
:as rf-storage
|
||||
:refer [reg-co-fx!]]))
|
||||
:refer [reg-co-fx!]]
|
||||
[commiteth.ui-model :as ui-model]))
|
||||
|
||||
|
||||
(rf-storage/reg-co-fx! :commiteth-sob {:fx :store
|
||||
|
@ -66,7 +67,9 @@
|
|||
:set-active-page
|
||||
(fn [db [_ page]]
|
||||
(assoc db :page page
|
||||
:page-number 1)))
|
||||
:page-number 1
|
||||
::db/open-bounties-filters {}
|
||||
::db/open-bounties-sorting-type ::ui-model/bounty-sorting-type|most-recent)))
|
||||
|
||||
(reg-event-db
|
||||
:set-page-number
|
||||
|
@ -457,3 +460,16 @@
|
|||
(fn [db [_]]
|
||||
(.removeEventListener js/window "click" close-dropdown)
|
||||
(assoc db :user-dropdown-open? false)))
|
||||
|
||||
(reg-event-db
|
||||
::set-open-bounties-sorting-type
|
||||
(fn [db [_ sorting-type]]
|
||||
(merge db {::db/open-bounties-sorting-type sorting-type
|
||||
:page-number 1})))
|
||||
|
||||
(reg-event-db
|
||||
::set-open-bounty-filter-type
|
||||
(fn [db [_ filter-type filter-value]]
|
||||
(-> db
|
||||
(assoc-in [::db/open-bounties-filters filter-type] filter-value)
|
||||
(assoc :page-number 1))))
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
(ns commiteth.subscriptions
|
||||
(:require [re-frame.core :refer [reg-sub]]
|
||||
[commiteth.common :refer [items-per-page]]))
|
||||
[commiteth.db :as db]
|
||||
[commiteth.ui-model :as ui-model]
|
||||
[commiteth.common :refer [items-per-page]]
|
||||
[clojure.string :as string]))
|
||||
|
||||
(reg-sub
|
||||
:db
|
||||
|
@ -43,7 +46,7 @@
|
|||
|
||||
(reg-sub
|
||||
:open-bounties-page
|
||||
:<- [:open-bounties]
|
||||
:<- [::filtered-and-sorted-open-bounties]
|
||||
:<- [:page-number]
|
||||
(fn [[open-bounties page-number] _]
|
||||
(let [total-count (count open-bounties)
|
||||
|
@ -118,3 +121,49 @@
|
|||
:user-dropdown-open?
|
||||
(fn [db _]
|
||||
(:user-dropdown-open? db)))
|
||||
|
||||
(reg-sub
|
||||
::open-bounties-sorting-type
|
||||
(fn [db _]
|
||||
(::db/open-bounties-sorting-type db)))
|
||||
|
||||
(reg-sub
|
||||
::open-bounties-filters
|
||||
(fn [db _]
|
||||
(::db/open-bounties-filters db)))
|
||||
|
||||
(reg-sub
|
||||
::open-bounties-owners
|
||||
:<- [:open-bounties]
|
||||
(fn [open-bounties _]
|
||||
(->> open-bounties
|
||||
(map :repo-owner)
|
||||
set)))
|
||||
|
||||
(reg-sub
|
||||
::open-bounties-owners-sorted
|
||||
:<- [::open-bounties-owners]
|
||||
(fn [owners _]
|
||||
(sort-by string/lower-case owners)))
|
||||
|
||||
(reg-sub
|
||||
::open-bounties-currencies
|
||||
:<- [:open-bounties]
|
||||
(fn [open-bounties _]
|
||||
(let [token-ids (->> open-bounties
|
||||
(map :tokens)
|
||||
(mapcat keys)
|
||||
(filter identity)
|
||||
set)]
|
||||
(into #{"ETH"} token-ids))))
|
||||
|
||||
(reg-sub
|
||||
::filtered-and-sorted-open-bounties
|
||||
:<- [:open-bounties]
|
||||
:<- [::open-bounties-filters]
|
||||
:<- [::open-bounties-sorting-type]
|
||||
(fn [[open-bounties filters sorting-type] _]
|
||||
(cond->> open-bounties
|
||||
true (ui-model/filter-bounties filters)
|
||||
sorting-type (ui-model/sort-bounties-by-sorting-type sorting-type)
|
||||
true vec)))
|
||||
|
|
|
@ -0,0 +1,165 @@
|
|||
(ns commiteth.ui-model
|
||||
(:require [clojure.set :as set]
|
||||
[cljs-time.core :as t]
|
||||
[cljs-time.coerce :as t-coerce]
|
||||
[cljs-time.format :as t-format]))
|
||||
|
||||
;;;; bounty sorting types
|
||||
|
||||
(def bounty-sorting-types-def
|
||||
{::bounty-sorting-type|most-recent {::bounty-sorting-type.name "Most recent"
|
||||
::bounty-sorting-type.sort-key-fn (fn [bounty]
|
||||
(:updated bounty))
|
||||
::bounty-sorting-type.sort-comparator-fn (comp - compare)}
|
||||
::bounty-sorting-type|lowest-value {::bounty-sorting-type.name "Lowest value"
|
||||
::bounty-sorting-type.sort-key-fn (fn [bounty]
|
||||
(js/parseFloat (:value-usd bounty)))
|
||||
::bounty-sorting-type.sort-comparator-fn compare}
|
||||
::bounty-sorting-type|highest-value {::bounty-sorting-type.name "Highest value"
|
||||
::bounty-sorting-type.sort-key-fn (fn [bounty]
|
||||
(js/parseFloat (:value-usd bounty)))
|
||||
::bounty-sorting-type.sort-comparator-fn (comp - compare)}
|
||||
::bounty-sorting-type|owner {::bounty-sorting-type.name "Owner"
|
||||
::bounty-sorting-type.sort-key-fn (fn [bounty]
|
||||
(:repo-owner bounty))
|
||||
::bounty-sorting-type.sort-comparator-fn compare}})
|
||||
|
||||
(defn bounty-sorting-type->name [sorting-type]
|
||||
(-> bounty-sorting-types-def (get sorting-type) ::bounty-sorting-type.name))
|
||||
|
||||
(defn sort-bounties-by-sorting-type [sorting-type bounties]
|
||||
(let [keyfn (-> bounty-sorting-types-def
|
||||
sorting-type
|
||||
::bounty-sorting-type.sort-key-fn)
|
||||
comparator (-> bounty-sorting-types-def
|
||||
sorting-type
|
||||
::bounty-sorting-type.sort-comparator-fn)]
|
||||
(sort-by keyfn comparator bounties)))
|
||||
|
||||
;;;; bounty filter types
|
||||
|
||||
(def bounty-filter-type-date-options-def {::bounty-filter-type-date-option|last-week "Last week"
|
||||
::bounty-filter-type-date-option|last-month "Last month"
|
||||
::bounty-filter-type-date-option|last-3-months "Last 3 months"})
|
||||
|
||||
(def bounty-filter-type-date-options (keys bounty-filter-type-date-options-def))
|
||||
|
||||
(defn bounty-filter-type-date-option->name [option]
|
||||
(bounty-filter-type-date-options-def option))
|
||||
|
||||
(def bounty-filter-type-date-pre-predicate-value-processor
|
||||
"It converts an option of the filter type date to a cljs-time interval in which
|
||||
that option is valid, so that you can check `cljs-time.core.within?` against that
|
||||
interval and know if a `cljs-time` date is valid for that filter type date option."
|
||||
(fn [filter-value]
|
||||
(let [filter-from (condp = filter-value
|
||||
::bounty-filter-type-date-option|last-week (t/minus (t/now) (t/weeks 1))
|
||||
::bounty-filter-type-date-option|last-month (t/minus (t/now) (t/months 1))
|
||||
::bounty-filter-type-date-option|last-3-months (t/minus (t/now) (t/months 3)))]
|
||||
(t/interval filter-from (t/now)))))
|
||||
(def bounty-filter-type-date-predicate
|
||||
(fn [filter-value-interval bounty]
|
||||
(when-let [date-inst (:updated bounty)]
|
||||
(let [date (-> date-inst inst-ms t-coerce/from-long)]
|
||||
(t/within? filter-value-interval date)))))
|
||||
|
||||
(def bounty-filter-type-claims-options-def {::bounty-filter-type-claims-option|no-claims "Not claimed yet"})
|
||||
|
||||
(def bounty-filter-type-claims-options (keys bounty-filter-type-claims-options-def))
|
||||
|
||||
(defn bounty-filter-type-claims-option->name [option]
|
||||
(bounty-filter-type-claims-options-def option))
|
||||
|
||||
(def bounty-filter-types-def
|
||||
{::bounty-filter-type|value
|
||||
{::bounty-filter-type.name "Value"
|
||||
::bounty-filter-type.category ::bounty-filter-type-category|range
|
||||
::bounty-filter-type.min-val 0
|
||||
::bounty-filter-type.max-val 10000
|
||||
::bounty-filter.type.header "$0 - $10000+"
|
||||
::bounty-filter-type.predicate (fn [filter-value bounty]
|
||||
(let [min-val (first filter-value)
|
||||
max-val (second filter-value)]
|
||||
(<= min-val (:value-usd bounty) max-val)))}
|
||||
|
||||
::bounty-filter-type|currency
|
||||
{::bounty-filter-type.name "Currency"
|
||||
::bounty-filter-type.category ::bounty-filter-type-category|multiple-dynamic-options
|
||||
::bounty-filter-type.re-frame-subs-key-for-options :commiteth.subscriptions/open-bounties-currencies
|
||||
::bounty-filter-type.predicate (fn [filter-value bounty]
|
||||
(and (or (not-any? #{"ETH"} filter-value)
|
||||
(< 0 (:balance-eth bounty)))
|
||||
(set/subset? (->> filter-value (remove #{"ETH"}) set)
|
||||
(-> bounty :tokens keys set))))}
|
||||
|
||||
::bounty-filter-type|date
|
||||
{::bounty-filter-type.name "Date"
|
||||
::bounty-filter-type.category ::bounty-filter-type-category|single-static-option
|
||||
::bounty-filter-type.options bounty-filter-type-date-options-def
|
||||
::bounty-filter-type.pre-predicate-value-processor bounty-filter-type-date-pre-predicate-value-processor
|
||||
::bounty-filter-type.predicate bounty-filter-type-date-predicate}
|
||||
|
||||
::bounty-filter-type|owner
|
||||
{::bounty-filter-type.name "Owner"
|
||||
::bounty-filter-type.category ::bounty-filter-type-category|multiple-dynamic-options
|
||||
::bounty-filter-type.re-frame-subs-key-for-options :commiteth.subscriptions/open-bounties-owners-sorted
|
||||
::bounty-filter-type.predicate (fn [filter-value bounty]
|
||||
(->> filter-value
|
||||
(some #{(:repo-owner bounty)})
|
||||
boolean))}
|
||||
|
||||
::bounty-filter-type|claims
|
||||
{::bounty-filter-type.name "Claims"
|
||||
::bounty-filter-type.category ::bounty-filter-type-category|single-static-option
|
||||
::bounty-filter-type.options bounty-filter-type-claims-options-def
|
||||
::bounty-filter-type.predicate (fn [filter-value bounty]
|
||||
(condp = filter-value
|
||||
::bounty-filter-type-claims-option|no-claims
|
||||
(= 0 (:claim-count bounty))))}})
|
||||
|
||||
(def bounty-filter-types (keys bounty-filter-types-def))
|
||||
|
||||
(defn bounty-filter-type->name [filter-type]
|
||||
(-> bounty-filter-types-def (get filter-type) ::bounty-filter-type.name))
|
||||
|
||||
(defn bounty-filter-value->short-text [filter-type filter-value]
|
||||
(cond
|
||||
(= filter-type ::bounty-filter-type|date)
|
||||
(bounty-filter-type-date-option->name filter-value)
|
||||
|
||||
(#{::bounty-filter-type|owner
|
||||
::bounty-filter-type|currency} filter-type)
|
||||
(str (bounty-filter-type->name filter-type) " (" (count filter-value) ")")
|
||||
|
||||
(= filter-type ::bounty-filter-type|value)
|
||||
(str "$" (first filter-value) "-$" (second filter-value))
|
||||
|
||||
(= filter-type ::bounty-filter-type|claims)
|
||||
(bounty-filter-type-claims-option->name filter-value)
|
||||
|
||||
:else
|
||||
(str filter-type " with val " filter-value)))
|
||||
|
||||
(defn- bounty-filter-values-by-type->predicates [filters-by-type]
|
||||
"It receives a map with filter types as keys and filter values as values and
|
||||
returns a lazy seq of predicates, one for each pair of filter type and value.
|
||||
Those predicate can receive a bounty and tell whether that bounty passes
|
||||
the filter type with that filter value. It removes filter types with a `nil`
|
||||
filter value."
|
||||
(->> filters-by-type
|
||||
; used `nil?` because a valid filter value can be `false`
|
||||
(remove #(nil? (val %)))
|
||||
(map (fn [[filter-type filter-value]]
|
||||
(let [filter-type-def (bounty-filter-types-def filter-type)
|
||||
pred (::bounty-filter-type.predicate filter-type-def)
|
||||
pre-pred-processor (::bounty-filter-type.pre-predicate-value-processor filter-type-def)
|
||||
filter-value (cond-> filter-value
|
||||
pre-pred-processor pre-pred-processor)]
|
||||
(partial pred filter-value))))))
|
||||
|
||||
(defn filter-bounties [filters-by-type bounties]
|
||||
(let [filter-preds (bounty-filter-values-by-type->predicates filters-by-type)
|
||||
filters-pred (fn [bounty]
|
||||
(every? #(% bounty) filter-preds))]
|
||||
(cond->> bounties
|
||||
(not-empty filter-preds) (filter filters-pred))))
|
|
@ -0,0 +1,5 @@
|
|||
(ns commiteth.util
|
||||
(:require [clojure.string :as string]))
|
||||
|
||||
(defn os-windows? []
|
||||
(string/includes? (-> js/navigator .-platform) "Win"))
|
|
@ -413,6 +413,312 @@
|
|||
font-weight: 500;
|
||||
color: #42505c;
|
||||
}
|
||||
|
||||
.item-counts-label-and-sorting-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.open-bounties-filter-and-sort {
|
||||
margin-top: 24px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.open-bounties-filter-and-sort {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.open-bounties-filter {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.open-bounties-filter-element {
|
||||
font-family: "PostGrotesk-Book";
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
line-height: 1.0;
|
||||
color: #8d99a4;
|
||||
//padding: 8px 12px;
|
||||
border-radius: 8px;
|
||||
border: solid 1px rgba(151, 151, 151, 0.2);
|
||||
margin-right: 10px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
z-index: 1000;
|
||||
|
||||
.text {
|
||||
margin: 8px 12px;
|
||||
}
|
||||
|
||||
.remove-container {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.remove {
|
||||
margin-left: -6px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.open-bounties-filter-element:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.open-bounties-filter-element-active
|
||||
{
|
||||
background-color: #57a7ed;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.open-bounties-filter-element-container:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.open-bounties-filter-element-tooltip {
|
||||
position: absolute;
|
||||
min-width: 227px;
|
||||
margin-top: 12px;
|
||||
border-radius: 10px;
|
||||
background-color: #ffffff;
|
||||
box-shadow: 0 15px 12px 0 rgba(161, 174, 182, 0.53), 0 0 38px 0 rgba(0, 0, 0, 0.05);
|
||||
font-family: "PostGrotesk-Book";
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
z-index: 999;
|
||||
max-height: 300px;
|
||||
overflow: scroll;
|
||||
|
||||
.open-bounties-filter-element-tooltip-value-input-container {
|
||||
display: flex;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.open-bounties-filter-element-tooltip-value-input-label {
|
||||
width: 60px;
|
||||
}
|
||||
|
||||
.open-bounties-filter-element-tooltip-value-input {
|
||||
margin-top: 24px;
|
||||
width: 288.5px;
|
||||
height: 4px;
|
||||
background-color: #55a5ea;
|
||||
}
|
||||
|
||||
// generated with http://danielstern.ca/range.css/#/
|
||||
input[type=range] {
|
||||
-webkit-appearance: none;
|
||||
//width: 100%;
|
||||
margin: 14.5px 0;
|
||||
}
|
||||
input[type=range]:focus {
|
||||
outline: 0;
|
||||
}
|
||||
input[type=range]::-moz-focus-outer {
|
||||
border: 0;
|
||||
}
|
||||
input[type=range]::-webkit-slider-runnable-track {
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
cursor: pointer;
|
||||
box-shadow: 0px 0px 0px #000000, 0px 0px 0px #0d0d0d;
|
||||
background: #55a5ea;
|
||||
border-radius: 0px;
|
||||
border: 0px solid #010101;
|
||||
}
|
||||
input[type=range]::-webkit-slider-thumb {
|
||||
box-shadow: 0px 0px 0px #000000, 0px 0px 0px #0d0d0d;
|
||||
border: 0px solid #000000;
|
||||
height: 33px;
|
||||
width: 33px;
|
||||
border-radius: 50px;
|
||||
background: #55a5ea;
|
||||
cursor: pointer;
|
||||
-webkit-appearance: none;
|
||||
margin-top: -14.5px;
|
||||
}
|
||||
input[type=range]:focus::-webkit-slider-runnable-track {
|
||||
background: #5aa7eb;
|
||||
}
|
||||
input[type=range]::-moz-range-track {
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
cursor: pointer;
|
||||
box-shadow: 0px 0px 0px #000000, 0px 0px 0px #0d0d0d;
|
||||
background: #55a5ea;
|
||||
border-radius: 0px;
|
||||
border: 0px solid #010101;
|
||||
}
|
||||
input[type=range]::-moz-range-thumb {
|
||||
box-shadow: 0px 0px 0px #000000, 0px 0px 0px #0d0d0d;
|
||||
border: 0px solid #000000;
|
||||
height: 33px;
|
||||
width: 33px;
|
||||
border-radius: 50px;
|
||||
background: #55a5ea;
|
||||
cursor: pointer;
|
||||
}
|
||||
input[type=range]::-ms-track {
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
border-color: transparent;
|
||||
color: transparent;
|
||||
}
|
||||
input[type=range]::-ms-fill-lower {
|
||||
background: #50a3e9;
|
||||
border: 0px solid #010101;
|
||||
border-radius: 0px;
|
||||
box-shadow: 0px 0px 0px #000000, 0px 0px 0px #0d0d0d;
|
||||
}
|
||||
input[type=range]::-ms-fill-upper {
|
||||
background: #55a5ea;
|
||||
border: 0px solid #010101;
|
||||
border-radius: 0px;
|
||||
box-shadow: 0px 0px 0px #000000, 0px 0px 0px #0d0d0d;
|
||||
}
|
||||
input[type=range]::-ms-thumb {
|
||||
box-shadow: 0px 0px 0px #000000, 0px 0px 0px #0d0d0d;
|
||||
border: 0px solid #000000;
|
||||
height: 33px;
|
||||
width: 33px;
|
||||
border-radius: 50px;
|
||||
background: #55a5ea;
|
||||
cursor: pointer;
|
||||
height: 4px;
|
||||
}
|
||||
input[type=range]:focus::-ms-fill-lower {
|
||||
background: #55a5ea;
|
||||
}
|
||||
input[type=range]:focus::-ms-fill-upper {
|
||||
background: #5aa7eb;
|
||||
}
|
||||
|
||||
.open-bounties-filter-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
margin: 24px 16px;
|
||||
}
|
||||
|
||||
.open-bounties-filter-list-option {
|
||||
font-family: "PostGrotesk-Book";
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
line-height: 1.0;
|
||||
color: #42505c;
|
||||
padding: 8px 12px;
|
||||
border-radius: 8px;
|
||||
border: solid 1px rgba(151, 151, 151, 0.2);
|
||||
}
|
||||
|
||||
.open-bounties-filter-list-option.active {
|
||||
background-color: #57a7ed;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.open-bounties-filter-list-option:not(:first-child) {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.open-bounties-filter-list-option:hover {
|
||||
cursor: pointer;
|
||||
background-color: #57a7ed;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.open-bounties-filter-list-option-checkbox {
|
||||
label {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
label:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
label:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
input:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.text {
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
line-height: 1.0;
|
||||
margin-left: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.open-bounties-filter-list-option-checkbox:not(:first-child) {
|
||||
margin-top: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.open-bounties-sort {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.open-bounties-sort:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.open-bounties-sort-element {
|
||||
display: flex;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
line-height: 1.0;
|
||||
color: #8d99a4;
|
||||
padding: 16px 12px 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.open-bounties-sort-element:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.open-bounties-sort-element-tooltip {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
//padding: 16px 0;
|
||||
min-width: 200px;
|
||||
border-radius: 8px;
|
||||
background-color: #1e3751;
|
||||
font-family: "PostGrotesk-Book";
|
||||
font-size: 15px;
|
||||
line-height: 1.0;
|
||||
text-align: left;
|
||||
color: #ffffff;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.open-bounties-sort-type {
|
||||
padding: 19px 24px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.open-bounties-sort-type:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.icon-forward-white-box {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.open-bounty-item:first-child {
|
||||
border-top: #eaecee 1px solid !important;
|
||||
}
|
||||
|
||||
.open-bounty-item {
|
||||
|
@ -496,7 +802,7 @@
|
|||
}
|
||||
|
||||
.open-claims-label {
|
||||
padding-left: 5px;
|
||||
padding-left: 15px;
|
||||
font-size: 15px;
|
||||
color: #57a7ed;
|
||||
}
|
||||
|
@ -971,12 +1277,11 @@ body {
|
|||
}
|
||||
|
||||
.item-counts-label {
|
||||
margin: auto;
|
||||
//margin: auto;
|
||||
font-family: "PostGrotesk-Book";
|
||||
font-size: 15px;
|
||||
color: #8d99a4;
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
node ('linux1') {sauce('1be1b688-e0e7-4314-92a0-db11f52d3c00') {
|
||||
checkout([$class: 'GitSCM', branches: [[name: '*/develop']], doGenerateSubmoduleConfigurations: false, extensions: [[$class: 'CleanBeforeCheckout']], submoduleCfg: [], userRemoteConfigs: [[url: 'https://github.com/status-im/open-bounty.git']]])
|
||||
configFileProvider([configFile(fileId: 'sob_automation_test_config', targetLocation: 'test/end-to-end/tests')]) {
|
||||
try {sh 'cd test/end-to-end/tests && python3 -m pytest -m sanity --build=$BUILD_NAME -v -n 1'
|
||||
}
|
||||
finally {
|
||||
saucePublisher()
|
||||
junit testDataPublishers: [[$class: 'SauceOnDemandReportPublisher', jobVisibility: 'public']], testResults: 'test/end-to-end/tests/*.xml' }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -15,3 +15,4 @@ class BasePageObject(object):
|
|||
@property
|
||||
def time_now(self):
|
||||
return datetime.now().strftime('%-m%-d%-H%-M%-S')
|
||||
|
||||
|
|
|
@ -2,6 +2,10 @@ import time, pytest
|
|||
from pages.base_element import *
|
||||
from pages.base_page import BasePageObject
|
||||
from tests import test_data
|
||||
from git import Repo
|
||||
import os
|
||||
import shutil
|
||||
import logging
|
||||
|
||||
|
||||
class EmailEditbox(BaseEditBox):
|
||||
|
@ -106,7 +110,46 @@ class ContractBody(BaseText):
|
|||
super(ContractBody, self).__init__(driver)
|
||||
self.locator = self.Locator.xpath_selector("//tbody//p[contains(text(), "
|
||||
"'Current balance: 0.000000 ETH')]")
|
||||
class IssueId(BaseText):
|
||||
def __init__(self, driver):
|
||||
super(IssueId, self).__init__(driver)
|
||||
self.locator = self.Locator.css_selector(".gh-header-number")
|
||||
|
||||
class ForkButton(BaseButton):
|
||||
def __init__(self, driver):
|
||||
super(ForkButton, self).__init__(driver)
|
||||
self.locator = self.Locator.css_selector("[href='#fork-destination-box']")
|
||||
|
||||
class HeaderInForkPopup(BaseText):
|
||||
def __init__(self, driver):
|
||||
super(HeaderInForkPopup, self).__init__(driver)
|
||||
self.locator = self.Locator.css_selector("#facebox-header")
|
||||
|
||||
class UserAccountInForkPopup(BaseButton):
|
||||
def __init__(self, driver):
|
||||
super(UserAccountInForkPopup, self).__init__(driver)
|
||||
self.locator = self.Locator.css_selector("[value=%s]"%test_data.config['DEV']['gh_username'])
|
||||
|
||||
|
||||
class ForkedRepoText(BaseText):
|
||||
def __init__(self, driver):
|
||||
super(ForkedRepoText, self).__init__(driver)
|
||||
self.locator = self.Locator.css_selector(".commit-tease")
|
||||
|
||||
class DeleteRepo(BaseButton):
|
||||
def __init__(self, driver):
|
||||
super(DeleteRepo, self).__init__(driver)
|
||||
self.locator = self.Locator.xpath_selector("//button[text()[contains(.,' Delete this repository')]]")
|
||||
|
||||
class RepoNameBoxInPopup(BaseEditBox):
|
||||
def __init__(self, driver):
|
||||
super(RepoNameBoxInPopup, self).__init__(driver)
|
||||
self.locator = self.Locator.css_selector("input[aria-label='Type in the name of the repository to confirm that you want to delete this repository.']")
|
||||
|
||||
class ConfirmDeleteButton(BaseButton):
|
||||
def __init__(self, driver):
|
||||
super(ConfirmDeleteButton, self).__init__(driver)
|
||||
self.locator = self.Locator.xpath_selector("//button[text()[contains(.,'I understand the consequences, delete')]]")
|
||||
|
||||
class GithubPage(BasePageObject):
|
||||
def __init__(self, driver):
|
||||
|
@ -133,18 +176,32 @@ class GithubPage(BasePageObject):
|
|||
self.cross_button = LabelsButton.CrossButton(self.driver)
|
||||
self.submit_new_issue_button = SubmitNewIssueButton(self.driver)
|
||||
self.contract_body = ContractBody(self.driver)
|
||||
self.issue_id = IssueId(self.driver)
|
||||
self.fork_button = ForkButton(self.driver)
|
||||
self.header_in_fork_popup = HeaderInForkPopup(self.driver)
|
||||
self.user_account_in_fork_popup = UserAccountInForkPopup(self.driver)
|
||||
self.forked_repo_text = ForkedRepoText(self.driver)
|
||||
|
||||
self.delete_repo = DeleteRepo(self.driver)
|
||||
self.repo_name_confirm_delete = RepoNameBoxInPopup(self.driver)
|
||||
self.confirm_delete = ConfirmDeleteButton(self.driver)
|
||||
|
||||
|
||||
def get_issues_page(self):
|
||||
self.driver.get('https://github.com/Org4/nov13/issues')
|
||||
self.driver.get(test_data.config['ORG']['gh_repo'] + 'issues')
|
||||
|
||||
def get_issue_page(self, issue_id):
|
||||
self.driver.get(test_data.config['ORG']['gh_repo'] + 'issues/' + issue_id)
|
||||
|
||||
def get_sob_plugin_page(self):
|
||||
self.driver.get('http://github.com/apps/status-open-bounty-app-test')
|
||||
self.driver.get(test_data.config['Common']['sob_test_app'])
|
||||
|
||||
def sign_in(self, email, password):
|
||||
self.email_input.send_keys(email)
|
||||
self.password_input.send_keys(password)
|
||||
self.sign_in_button.click()
|
||||
|
||||
|
||||
def install_sob_plugin(self):
|
||||
initial_url = self.driver.current_url
|
||||
self.get_sob_plugin_page()
|
||||
|
@ -164,7 +221,18 @@ class GithubPage(BasePageObject):
|
|||
self.bounty_label.click()
|
||||
self.cross_button.click()
|
||||
self.submit_new_issue_button.click()
|
||||
return test_data.issue['title']
|
||||
test_data.issue['id'] = self.issue_id.text[1:]
|
||||
logging.info("Issue title is %s" % test_data.issue['title'])
|
||||
|
||||
def fork_repo(self, initial_repo, wait=60):
|
||||
self.driver.get(initial_repo)
|
||||
self.fork_button.click()
|
||||
if self.header_in_fork_popup.text == 'Where should we fork this repository?':
|
||||
self.user_account_in_fork_popup.click()
|
||||
self.forked_repo_text.wait_for_element(wait)
|
||||
|
||||
def get_login_page(self):
|
||||
self.driver.get(test_data.config['Common']['gh_login'])
|
||||
|
||||
def get_deployed_contract(self, wait=120):
|
||||
for i in range(wait):
|
||||
|
@ -175,3 +243,34 @@ class GithubPage(BasePageObject):
|
|||
time.sleep(10)
|
||||
pass
|
||||
pytest.fail('Contract is not deployed in %s minutes!' % str(wait/60))
|
||||
|
||||
#cloning via HTTPS
|
||||
def clone_repo(self, initial_repo=None, username=None, repo_name=None, repo_path='git_repo'):
|
||||
os.mkdir(repo_path)
|
||||
os.chdir(repo_path)
|
||||
test_data.local_repo_path = os.getcwd()
|
||||
fork = 'https://github.com/%s/%s.git' % (username, repo_name)
|
||||
logging.info(('Cloning from %s to %s' % (fork, repo_path)))
|
||||
r = Repo.clone_from(fork, repo_path)
|
||||
logging.info(('Successefully cloned to: %s' % test_data.local_repo_path))
|
||||
logging.info('Set upstream to %s'% initial_repo)
|
||||
upstream = r.create_remote('upstream', initial_repo)
|
||||
upstream.fetch()
|
||||
assert upstream.exists()
|
||||
r.heads.master.checkout()
|
||||
|
||||
def clean_repo_local_folder(self):
|
||||
logging.info('Removing %s' % test_data.local_repo_path)
|
||||
if test_data.local_repo_path:
|
||||
shutil.rmtree(test_data.local_repo_path)
|
||||
|
||||
def delete_fork(self):
|
||||
self.get_url(test_data.config['DEV']['gh_forked_repo'] + 'settings')
|
||||
self.delete_repo.click()
|
||||
self.repo_name_confirm_delete.send_keys(test_data.config['ORG']['gh_repo_name'])
|
||||
self.confirm_delete.click()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -16,3 +16,6 @@ selenium==2.53.6
|
|||
six==1.10.0
|
||||
urllib3==1.22
|
||||
yarl==0.12.0
|
||||
gitpython==2.1.8
|
||||
gitdb2==2.0.3
|
||||
smmap2==2.0.3
|
|
@ -7,11 +7,14 @@ class TestData(object):
|
|||
self.config = configparser.ConfigParser()
|
||||
|
||||
# define here path to your config.ini file
|
||||
#example - config_example.ini
|
||||
# example - config_example.ini
|
||||
|
||||
self.config.read('config.ini')
|
||||
self.base_case_issue = dict()
|
||||
self.base_case_issue['title'] = 'Very first auto_test_bounty'
|
||||
|
||||
# self.issue['title'] is set in GithubPage::create_new_bounty
|
||||
# self.issue['id'] is set in GithubPage::create_new_bounty
|
||||
# self.local_repo_path is set in GithubPage::clone_repo
|
||||
|
||||
|
||||
|
||||
test_data = TestData()
|
||||
|
|
|
@ -4,63 +4,25 @@ from selenium.common.exceptions import WebDriverException
|
|||
from tests.postconditions import remove_application, remove_installation
|
||||
from os import environ, path
|
||||
from tests import test_data
|
||||
|
||||
from pages.thirdparty.github import GithubPage
|
||||
from pages.openbounty.landing import LandingPage
|
||||
|
||||
class BaseTestCase:
|
||||
|
||||
@property
|
||||
def sauce_username(self):
|
||||
return environ.get('SAUCE_USERNAME')
|
||||
|
||||
|
||||
@property
|
||||
def sauce_access_key(self):
|
||||
return environ.get('SAUCE_ACCESS_KEY')
|
||||
|
||||
|
||||
@property
|
||||
def executor_sauce_lab(self):
|
||||
return 'http://%s:%s@ondemand.saucelabs.com:80/wd/hub' % (self.sauce_username, self.sauce_access_key)
|
||||
|
||||
def print_sauce_lab_info(self, driver):
|
||||
sys.stdout = sys.stderr
|
||||
print("SauceOnDemandSessionID=%s job-name=%s" % (driver.session_id,
|
||||
pytest.config.getoption('build')))
|
||||
|
||||
@property
|
||||
def capabilities_sauce_lab(self):
|
||||
|
||||
desired_caps = dict()
|
||||
desired_caps['name'] = test_data.test_name
|
||||
desired_caps['build'] = pytest.config.getoption('build')
|
||||
desired_caps['platform'] = "MAC"
|
||||
desired_caps['browserName'] = 'Chrome'
|
||||
desired_caps['screenResolution'] = '2048x1536'
|
||||
desired_caps['captureHtml'] = False
|
||||
return desired_caps
|
||||
|
||||
@property
|
||||
def environment(self):
|
||||
return pytest.config.getoption('env')
|
||||
|
||||
def setup_method(self):
|
||||
|
||||
self.errors = []
|
||||
self.cleanup = None
|
||||
|
||||
if self.environment == 'local':
|
||||
options = webdriver.ChromeOptions()
|
||||
options.add_argument('--start-fullscreen')
|
||||
options.add_extension(
|
||||
path.abspath(test_data.config['Paths']['tests_absolute'] + 'resources/metamask3_12_0.crx'))
|
||||
# for chromedriver 2.35
|
||||
self.driver = webdriver.Chrome(chrome_options=options)
|
||||
if self.environment == 'sauce':
|
||||
self.driver = webdriver.Remote(self.executor_sauce_lab,
|
||||
desired_capabilities=self.capabilities_sauce_lab)
|
||||
self.driver.implicitly_wait(5)
|
||||
|
||||
|
||||
def get_remote_caps(self):
|
||||
sauce_lab_cap = dict()
|
||||
sauce_lab_cap['name'] = test_data.test_name
|
||||
sauce_lab_cap['build'] = pytest.config.getoption('build')
|
||||
sauce_lab_cap['platform'] = "MAC"
|
||||
sauce_lab_cap['browserName'] = 'Chrome'
|
||||
sauce_lab_cap['screenResolution'] = '2048x1536'
|
||||
sauce_lab_cap['captureHtml'] = False
|
||||
return sauce_lab_cap
|
||||
|
||||
def verify_no_errors(self):
|
||||
if self.errors:
|
||||
|
@ -69,13 +31,115 @@ class BaseTestCase:
|
|||
msg += (error + '\n')
|
||||
pytest.fail(msg, pytrace=False)
|
||||
|
||||
def teardown_method(self):
|
||||
if self.cleanup:
|
||||
remove_application(self.driver)
|
||||
remove_installation(self.driver)
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
cls.errors = []
|
||||
cls.environment = pytest.config.getoption('env')
|
||||
|
||||
###################################################################################################################
|
||||
######### Drivers setup
|
||||
###################################################################################################################
|
||||
|
||||
#
|
||||
# Dev Chrome options
|
||||
#
|
||||
cls.capabilities_dev = webdriver.ChromeOptions()
|
||||
cls.capabilities_dev.add_argument('--start-fullscreen')
|
||||
|
||||
#
|
||||
# Org Chrome options
|
||||
#
|
||||
cls.capabilities_org = webdriver.ChromeOptions()
|
||||
# doesn't work on sauce env
|
||||
# cls.capabilities_org.add_extension(path.abspath(test_data.config['Paths']['tests_absolute'] + 'resources/metamask3_12_0.crx'))
|
||||
|
||||
#
|
||||
# SauceLab capabilities
|
||||
#
|
||||
cls.executor_sauce_lab = 'http://%s:%s@ondemand.saucelabs.com:80/wd/hub' % (
|
||||
environ.get('SAUCE_USERNAME'), environ.get('SAUCE_ACCESS_KEY'))
|
||||
drivers = []
|
||||
|
||||
if cls.environment == 'local':
|
||||
for caps in cls.capabilities_dev, cls.capabilities_org:
|
||||
driver = webdriver.Chrome(chrome_options=caps)
|
||||
drivers.append(driver)
|
||||
|
||||
if cls.environment == 'sauce':
|
||||
for caps in cls.capabilities_dev, cls.capabilities_org:
|
||||
cls.get_remote_caps(cls)
|
||||
new_caps = caps.to_capabilities()
|
||||
driver = webdriver.Remote(cls.executor_sauce_lab,
|
||||
desired_capabilities=new_caps)
|
||||
drivers.append(driver)
|
||||
|
||||
for driver in drivers:
|
||||
cls.print_sauce_lab_info(cls, driver)
|
||||
|
||||
cls.driver_dev = drivers[0]
|
||||
cls.driver_org = drivers[1]
|
||||
|
||||
for driver in drivers:
|
||||
driver.implicitly_wait(10)
|
||||
|
||||
###################################################################################################################
|
||||
######### Actions for each driver before class
|
||||
###################################################################################################################
|
||||
|
||||
######ORG
|
||||
landing = LandingPage(cls.driver_org)
|
||||
landing.get_landing_page()
|
||||
|
||||
# Sign Up to SOB
|
||||
cls.github_org = landing.login_button.click()
|
||||
cls.github_org.sign_in(test_data.config['ORG']['gh_login'],
|
||||
test_data.config['ORG']['gh_password'])
|
||||
assert cls.github_org.permission_type.text == 'Personal user data'
|
||||
bounties_page = cls.github_org.authorize_sob.click()
|
||||
|
||||
# SOB Plugin installation and navigate to "Open bounties"
|
||||
cls.github_org.install_sob_plugin()
|
||||
assert bounties_page.bounties_header.text == 'Bounties'
|
||||
assert bounties_page.top_hunters_header.text == 'Top 5 hunters'
|
||||
|
||||
######DEV
|
||||
cls.github_dev = GithubPage(cls.driver_dev)
|
||||
# Sign In to GH as Developer
|
||||
cls.github_dev.get_login_page()
|
||||
cls.github_dev.sign_in(test_data.config['DEV']['gh_login'],
|
||||
test_data.config['DEV']['gh_password'])
|
||||
|
||||
# Fork repo as Developer from Organization
|
||||
cls.github_dev.fork_repo(test_data.config['ORG']['gh_repo'])
|
||||
|
||||
# Cloning repo to local git as Developer and set upstream to Organization (via HTTPS)
|
||||
cls.github_dev.clone_repo(test_data.config['ORG']['gh_repo'],
|
||||
test_data.config['DEV']['gh_username'],
|
||||
test_data.config['ORG']['gh_repo_name'],
|
||||
'git_repo')
|
||||
cls.verify_no_errors(cls)
|
||||
|
||||
|
||||
|
||||
|
||||
@classmethod
|
||||
def teardown_class(cls):
|
||||
|
||||
######ORG
|
||||
|
||||
# SOB Plugin remove installation
|
||||
remove_application(cls.driver_org)
|
||||
remove_installation(cls.driver_org)
|
||||
|
||||
######DEV
|
||||
|
||||
cls.github_dev.delete_fork()
|
||||
cls.github_dev.clean_repo_local_folder()
|
||||
try:
|
||||
self.print_sauce_lab_info(self.driver)
|
||||
self.driver.quit()
|
||||
cls.driver_dev.quit()
|
||||
cls.driver_org.quit()
|
||||
except WebDriverException:
|
||||
pass
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -1,13 +1,30 @@
|
|||
[Common]
|
||||
;app URL
|
||||
url = https://openbounty.status.im:444/
|
||||
;URL to GH openbounty app
|
||||
sob_test_app = http://github.com/apps/status-open-bounty-app-test
|
||||
gh_login = https://github.com/login
|
||||
[Paths]
|
||||
;AbsolutePath to 'tests' folder
|
||||
tests_absolute = /usr/dir/open-bounty/test/end-to-end/tests/
|
||||
;AbsolutePath to 'end-to-end' folder
|
||||
tests_absolute = /usr/dir/open-bounty/test/end-to-end/
|
||||
[ORG]
|
||||
;GitHub credentials for organization
|
||||
|
||||
;GitHub credentials for organization owner
|
||||
gh_login = login
|
||||
gh_password = password
|
||||
;GitHub organization path
|
||||
gh_org_profile = https://github.com/organizations/organization/
|
||||
;GitHub repo path
|
||||
gh_repo = https://github.com/org_name/repo_name/
|
||||
;GitHub repo name
|
||||
gh_repo_name = repo_name
|
||||
|
||||
;MetaMask password for organization
|
||||
mm_password = password
|
||||
|
||||
[DEV]
|
||||
;GitHub credentials for developer
|
||||
gh_login = login
|
||||
gh_password = password
|
||||
gh_username = username
|
||||
gh_forked_repo = https://github.com/username/reponame/
|
|
@ -1,5 +1,6 @@
|
|||
from selenium.webdriver.common.by import By
|
||||
from selenium.common.exceptions import NoSuchElementException
|
||||
from tests import test_data
|
||||
|
||||
|
||||
def remove_application(driver):
|
||||
|
@ -13,7 +14,7 @@ def remove_application(driver):
|
|||
|
||||
def remove_installation(driver):
|
||||
try:
|
||||
driver.get('https://github.com/organizations/Org4/settings/installations')
|
||||
driver.get(test_data.config['ORG']['gh_org_profile'] + 'settings/installations')
|
||||
driver.find_element(By.CSS_SELECTOR, '.iconbutton').click()
|
||||
driver.find_element(By.XPATH, "//a[@class='btn btn-danger']").click()
|
||||
driver.find_element(By.CSS_SELECTOR, '.facebox-popup .btn-danger').click()
|
||||
|
|
|
@ -2,6 +2,7 @@ import pytest
|
|||
from os import environ
|
||||
from pages.openbounty.landing import LandingPage
|
||||
from pages.openbounty.bounties import BountiesPage
|
||||
from pages.thirdparty.github import GithubPage
|
||||
from tests.basetestcase import BaseTestCase
|
||||
from tests import test_data
|
||||
|
||||
|
@ -10,28 +11,13 @@ from tests import test_data
|
|||
class TestLogin(BaseTestCase):
|
||||
|
||||
def test_deploy_new_contract(self):
|
||||
self.cleanup = True
|
||||
landing = LandingPage(self.driver)
|
||||
landing.get_landing_page()
|
||||
|
||||
# Sign Up to SOB
|
||||
github = landing.login_button.click()
|
||||
github.sign_in(test_data.config['ORG']['gh_login'],
|
||||
test_data.config['ORG']['gh_password'])
|
||||
assert github.permission_type.text == 'Personal user data'
|
||||
bounties_page = github.authorize_sob.click()
|
||||
|
||||
# SOB Plugin installation and navigate to "Open bounties"
|
||||
github.install_sob_plugin()
|
||||
assert bounties_page.bounties_header.text == 'Bounties'
|
||||
assert bounties_page.top_hunters_header.text == 'Top 5 hunters'
|
||||
|
||||
# Waiting for deployed contract; test_data.issue created here
|
||||
github.create_new_bounty()
|
||||
github.get_deployed_contract()
|
||||
self.github_org.create_new_bounty()
|
||||
self.github_org.get_deployed_contract()
|
||||
|
||||
# Navigate and check top bounty in "Open bounties"
|
||||
bounties_page = BountiesPage(self.driver)
|
||||
bounties_page = BountiesPage(self.driver_org)
|
||||
bounties_page.get_bounties_page()
|
||||
titles = bounties_page.bounty_titles.find_elements()
|
||||
assert titles[0].text == test_data.issue['title']
|
||||
|
@ -40,3 +26,6 @@ class TestLogin(BaseTestCase):
|
|||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
Loading…
Reference in New Issue