diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..f10a6b0 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +docker-compose.yml +Dockerfile +Jenkinsfile diff --git a/.gitignore b/.gitignore index 0e3eb68..39c20db 100644 --- a/.gitignore +++ b/.gitignore @@ -23,4 +23,7 @@ profiles.clj .idea resources/contracts node_modules -.DS_Store +/config-prod.edn +/config-dev.edn +/config-test.edn +/src/java diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..ce2947a --- /dev/null +++ b/Dockerfile @@ -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"] + diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 0000000..ec6b860 --- /dev/null +++ b/Jenkinsfile @@ -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 + } +} \ No newline at end of file diff --git a/README.md b/README.md index eedcafd..98216f5 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,14 @@ -# Commiteth +# Status Open Bounty -Allows you to set bounties for Github issues, paid out in Ether. +Allows you to set bounties for Github issues, paid out in Ether or any ERC-20 token. More information: -http://wiki.status.im/proposals/commiteth/ +https://wiki.status.im/Status_Open_Bounty -Live beta version: +Live production version: https://openbounty.status.im The `master` branch is automatically deployed here. - Live testnet (Ropsten) version: https://openbounty.status.im:444 The `develop` branch is automatically deployed here. @@ -17,10 +16,28 @@ 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 +Make sure you install [PostgreSQL](https://www.postgresql.org/) and properly set it up: + +``` +sudo -u postgres psql -c "CREATE USER commiteth WITH PASSWORD 'commiteth';" +sudo -u postgres createdb commiteth +``` + +## Running + +Launch following commands each in its own shell: + +``` +lein run +lein figwheel +lein less auto +``` + + Make sure you install [PostgreSQL](https://www.postgresql.org/) and properly set it up: ``` @@ -36,9 +53,40 @@ Solidity compiler [0.4.15](https://github.com/ethereum/solidity/releases/tag/v0. Web3j [2.3.0](https://github.com/web3j/web3j/releases/tag/v2.3.0) is required and the command line tools need to be in $PATH. -## Running +## Application config -Make sure `env/dev/resources/config.edn` is correctly populated. +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 +--- | --- +dev | Currently specifies whether Swagger UI endpoints should be added to routes +port | HTTP port for the Ring web app +nrepl-port | nREPL port for development +jdbc-database-url | PostgreSQL database URL. For instance, URL to local db would be `jdbc:postgresql://localhost/commiteth?user=commiteth&password=commiteth` +server-address | URL and port of local server that can be resolved from public internet. It will be used as a redirect URI during GitHub OAuth authorization process +eth-account | Ethereum account ID for the bot +eth-password | Ethereum account password for the bot +eth-rpc-url | RPC URL to Ethereum node, e.g. Geth. Either local or remote +eth-wallet-file | Location of wallet file. If Geth is run with the parameters as given below, it will reside under `$HOME/.ropsten/keystore` +tokenreg-base-format | Should be set to `:status` +github-client-id | Related to OAuth. Copied from GitHub account Settings->Developer settings->OAuth Apps +github-client-secret | Related to OAuth. Copied from GitHub account Settings->Developer settings->OAuth Apps +github-user | GitHub username for bot account. It is used for posting bounty comments +github-password | GitHub password for bot account +webhook-secret | Secret string to be used when creating a GitHub App +user-whitelist | Set of GitHub user/org IDs to be whitelisted. E.g. `#{"status-im" "your_org"}` +testnet-token-data | Token data map, useful if there are Geth connectivity problems + +## GitHub integration +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 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". + +## Running Lauch a local geth node with the bot account unlocked: @@ -121,6 +169,10 @@ Landing page is static and different CSS and JS due to time constraints. This copies over necessary artifacts to `resources` dir. + +### Troubleshooting +See the [Cookbook](doc/cookbook.md). + ## License Licensed under the [Affero General Public License v3.0](https://github.com/status-im/commiteth/blob/master/LICENSE.md) diff --git a/doc/cookbook.md b/doc/cookbook.md new file mode 100644 index 0000000..e76ad48 --- /dev/null +++ b/doc/cookbook.md @@ -0,0 +1,18 @@ +# Cookbook + +Here some common tasks/issues are listed along with their solutions. + +## Change config and restart the service + - ssh to the host running commiteth + - go to `/opt/commiteth` for prod or `/opt/commiteth-test` for test environment. + - there edit `config.edn` + - restart the service with `sudo service commiteth-test restart` + +## Manually add a GitHub repo to SOB app +Sometimes SOB will not + - connect to Postgres instance. Get DB name and username/password from respective `config.edn` (see previous answer). These are set in `:jdbc-database-url` field. + - execute the query, e.g. +``` +insert into repositories(repo_id,user_id,owner,repo,hook_id,state,hook_secret,owner_avatar_url) values(116971984,447328, 'aragon', 'aragon-monthly', 0, 2, '', 'https://avatars1.githubusercontent.com/u/24612534?v=4'); +``` +Note that the value for repo_id field is GitHub's internal repo ID. One way of obtaining it is navigating to `api.github.com/repos/:owner/:repo_name`, e.g. `https://api.github.com/repos/aragon/aragon-monthly`. diff --git a/doc/testing.md b/doc/testing.md index a3e59d0..f9dc7a1 100644 --- a/doc/testing.md +++ b/doc/testing.md @@ -10,7 +10,7 @@ For testing you will need: * a Github account with administrative access to one or more repositories * for approving bounty payouts you will additionally need access to an Ethereum wallet. So far, Mist and [MetaMask](https://metamask.io/) have been used, but anything that provides the web3 javascript interface should work. -The developers can be reached on the `#commiteth` channel in the [Status slack](http://slack.status.im/). +The developers can be reached on the `#openbounty` channel in the [Status slack](http://slack.status.im/). ### Signing up @@ -29,13 +29,9 @@ You should now see `Bounties`, `Activity`, `Repositories` and `Manage Payouts` t ### Creating bounty issues -Before you can create bounties, you need to have administrative access to one or more GitHub repositories. These can be either in the scope of your personal user account or in the scope of a Github orgnazation. +Before you can create bounties, you need to add Open Bounty GitHub App to your account or repos. Go to https://github.com/apps/status-open-bounty-app-test (or link to another GitHub App you've created for testing, as described in the [README](README.md) and click Install. Specify whether access to all org repos or specific repos is granted. This will install webhooks for SOB in your repos. -* click the `Repositories` tab -* click on the button `Enable Github Account` -* If you have 1 or more Organisation repositories then grant Organisation access to each of them by clicking on the button `Grant` -* grant Status Open Bounty the needed addtional permissions for managing repository webhooks, adding and modifying comments by clicking on the button `Authorize status-open-bounty` -* now you should see your repositories on the `Repositories` tab, click `Add` on one. If your account isn't whitelisted you will see instructions how to request an access. If your account is whitelisted then new `bounty` label will become available in the GitHub repository's labels and a new webhook should now exist for the repository. +* Request for your account to be whitelisted. Contact [Riot](https://chat.status.im) for more information * now, add the `bounty` label to a new or an existing issue. This should cause Status Open Bounty to post a new comment for the issue containing an image with text `Deploying contract, please wait` * once the contract has been mined, the comment will be updated to contain the bounty contract's address and a QR code @@ -60,6 +56,14 @@ To remove issue from the Bounties list you can close it in GitHub. ### Reporting bugs -All bugs should be reported as issues in the [CommitETH Github repository](https://github.com/status-im/commiteth/issues). +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` ) diff --git a/env/dev/clj/user.clj b/env/dev/clj/user.clj index e45e943..b318a86 100644 --- a/env/dev/clj/user.clj +++ b/env/dev/clj/user.clj @@ -1,13 +1,14 @@ (ns user (:require [mount.core :as mount] - [commiteth.figwheel :refer [start-fw stop-fw cljs]] - commiteth.core)) + [commiteth.figwheel :refer [start-fw stop-fw cljs]])) (defn start [] - (mount/start-without #'commiteth.core/repl-server)) + (require 'commiteth.core) + (mount/start-without (ns-resolve 'commiteth.core 'repl-server))) (defn stop [] - (mount/stop-except #'commiteth.core/repl-server)) + (require 'commiteth.core) + (mount/stop-except (ns-resolve 'commiteth.core 'repl-server))) (defn restart [] (stop) diff --git a/env/dev/resources/config.edn b/env/dev/resources/config.edn index 1ee9264..571a6ae 100644 --- a/env/dev/resources/config.edn +++ b/env/dev/resources/config.edn @@ -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 #{}} diff --git a/env/dev/resources/logback.xml b/env/dev/resources/logback.xml index f569fce..a4f8cc5 100644 --- a/env/dev/resources/logback.xml +++ b/env/dev/resources/logback.xml @@ -33,7 +33,11 @@ - + + + + + diff --git a/project.clj b/project.clj index fb6d054..ccb500f 100644 --- a/project.clj +++ b/project.clj @@ -93,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 @@ -115,11 +116,8 @@ :uberjar-name "commiteth.jar" :source-paths ["env/prod/clj"] :resource-paths ["env/prod/resources"]} - ;; :precomp profile allows to compile classes from commiteth.eth.contracts - ;; namespace before compiling clojure code. Otherwise ClassNotFound exception - ;; will be thrown - :precomp {:target-path "target/base+system+user+dev/" } - :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"] @@ -145,7 +143,7 @@ :optimizations :none :pretty-print true}}]} - :prep-tasks ["build-contracts" ["with-profile" "precomp" "javac"]] + :prep-tasks ["build-contracts" "javac"] :doo {:build "test"} :source-paths ["env/dev/clj" "test/clj"] :resource-paths ["env/dev/resources"] @@ -153,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 diff --git a/resources/contracts/wallet.data b/resources/contracts/wallet.data deleted file mode 100644 index fca12fc..0000000 --- a/resources/contracts/wallet.data +++ /dev/null @@ -1 +0,0 @@ -0x606060405234156200001057600080fd5b60405162000d5938038062000d59833981016040528080518201919060200180519150505b6000808083116200004557600080fd5b8351915060328211156200005857600080fd5b828210156200006657600080fd5b5060005b818110156200013257600260008583815181106200008457fe5b90602001906020020151600160a060020a0316815260208101919091526040016000205460ff16158015620000d75750838181518110620000c157fe5b90602001906020020151600160a060020a031615155b1515620000e357600080fd5b600160026000868481518110620000f657fe5b90602001906020020151600160a060020a031681526020810191909152604001600020805460ff19169115159190911790555b6001016200006a565b60038480516200014792916020019062000158565b5060048390555b50505050620001f3565b828054828255906000526020600020908101928215620001b2579160200282015b82811115620001b25782518254600160a060020a031916600160a060020a03919091161782556020929092019160019091019062000179565b5b50620001c1929150620001c5565b5090565b620001f091905b80821115620001c1578054600160a060020a0319168155600101620001cc565b5090565b90565b610b5680620002036000396000f300606060405236156100e35763ffffffff7c0100000000000000000000000000000000000000000000000000000000600035041663025e7c27811461013e5780632f54bf6e146101705780633411c81c146101a35780634f64b2be146101d9578063523fba7f1461020b578063547415251461023c578063784547a71461026b5780638b51d13f146102955780639ace38c2146102bd578063a0e67e2b1461037c578063a8abe69a146103e3578063add2c5cc1461045a578063b5dc40c31461048d578063b77bf600146104f7578063d74f8edd1461051c578063dc8452cd14610541575b5b73f5f6853e0eba27074a804358eedf4e89efaebc98600036816040516020015260405180838380828437820191505092505050602060405180830381856102c65a03f4151561013257600080fd5b5050604051805150505b005b341561014957600080fd5b610154600435610566565b604051600160a060020a03909116815260200160405180910390f35b341561017b57600080fd5b61018f600160a060020a0360043516610598565b604051901515815260200160405180910390f35b34156101ae57600080fd5b61018f600435600160a060020a03602435166105ad565b604051901515815260200160405180910390f35b34156101e457600080fd5b6101546004356105cd565b604051600160a060020a03909116815260200160405180910390f35b341561021657600080fd5b61022a600160a060020a03600435166105ff565b60405190815260200160405180910390f35b341561024757600080fd5b61022a60043515156024351515610611565b60405190815260200160405180910390f35b341561027657600080fd5b61018f600435610680565b604051901515815260200160405180910390f35b34156102a057600080fd5b61022a600435610714565b60405190815260200160405180910390f35b34156102c857600080fd5b6102d3600435610793565b604051600160a060020a03851681526020810184905281151560608201526080604082018181528454600260001961010060018416150201909116049183018290529060a08301908590801561036a5780601f1061033f5761010080835404028352916020019161036a565b820191906000526020600020905b81548152906001019060200180831161034d57829003601f168201915b50509550505050505060405180910390f35b341561038757600080fd5b61038f6107c7565b60405160208082528190810183818151815260200191508051906020019060200280838360005b838110156103cf5780820151818401525b6020016103b6565b505050509050019250505060405180910390f35b34156103ee57600080fd5b61038f60043560243560443515156064351515610830565b60405160208082528190810183818151815260200191508051906020019060200280838360005b838110156103cf5780820151818401525b6020016103b6565b505050509050019250505060405180910390f35b341561046557600080fd5b61018f600160a060020a036004351661095e565b604051901515815260200160405180910390f35b341561049857600080fd5b61038f600435610973565b60405160208082528190810183818151815260200191508051906020019060200280838360005b838110156103cf5780820151818401525b6020016103b6565b505050509050019250505060405180910390f35b341561050257600080fd5b61022a610af5565b60405190815260200160405180910390f35b341561052757600080fd5b61022a610afb565b60405190815260200160405180910390f35b341561054c57600080fd5b61022a610b00565b60405190815260200160405180910390f35b600380548290811061057457fe5b906000526020600020900160005b915054906101000a9004600160a060020a031681565b60026020526000908152604090205460ff1681565b600160209081526000928352604080842090915290825290205460ff1681565b600880548290811061057457fe5b906000526020600020900160005b915054906101000a9004600160a060020a031681565b60066020526000908152604090205481565b6000805b6005548110156106785783801561063e575060008181526020819052604090206003015460ff16155b806106625750828015610662575060008181526020819052604090206003015460ff165b5b1561066f576001820191505b5b600101610615565b5b5092915050565b600080805b60035481101561070c57600084815260016020526040812060038054919291849081106106ae57fe5b906000526020600020900160005b9054600160a060020a036101009290920a900416815260208101919091526040016000205460ff16156106f0576001820191505b600454821415610703576001925061070c565b5b600101610685565b5b5050919050565b6000805b60035481101561078c576000838152600160205260408120600380549192918490811061074157fe5b906000526020600020900160005b9054600160a060020a036101009290920a900416815260208101919091526040016000205460ff1615610783576001820191505b5b600101610718565b5b50919050565b6000602081905290815260409020805460018201546003830154600160a060020a0390921692909160029091019060ff1684565b6107cf610b06565b600380548060200260200160405190810160405280929190818152602001828054801561082557602002820191906000526020600020905b8154600160a060020a03168152600190910190602001808311610807575b505050505090505b90565b610838610b06565b610840610b06565b6000806005546040518059106108535750595b908082528060200260200182016040525b50925060009150600090505b6005548110156108eb57858015610899575060008181526020819052604090206003015460ff16155b806108bd57508480156108bd575060008181526020819052604090206003015460ff165b5b156108e257808383815181106108d057fe5b60209081029091010152600191909101905b5b600101610870565b8787036040518059106108fb5750595b908082528060200260200182016040525b5093508790505b868110156109525782818151811061092757fe5b90602001906020020151848983038151811061093f57fe5b602090810290910101525b600101610913565b5b505050949350505050565b60076020526000908152604090205460ff1681565b61097b610b06565b610983610b06565b60035460009081906040518059106109985750595b908082528060200260200182016040525b50925060009150600090505b600354811015610a7b57600085815260016020526040812060038054919291849081106109de57fe5b906000526020600020900160005b9054600160a060020a036101009290920a900416815260208101919091526040016000205460ff1615610a72576003805482908110610a2757fe5b906000526020600020900160005b9054906101000a9004600160a060020a0316838381518110610a5357fe5b600160a060020a03909216602092830290910190910152600191909101905b5b6001016109b5565b81604051805910610a895750595b908082528060200260200182016040525b509350600090505b81811015610aec57828181518110610ab657fe5b90602001906020020151848281518110610acc57fe5b600160a060020a039092166020928302909101909101525b600101610aa2565b5b505050919050565b60055481565b603281565b60045481565b60206040519081016040526000815290565b602060405190810160405260008152905600a165627a7a723058207d9d3f76961e4749c98ecb2cbf1de9ef5bbd27b040625b73dc9454d14908f7df0029000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002 \ No newline at end of file diff --git a/resources/public/bounty-filter-remove.svg b/resources/public/bounty-filter-remove.svg new file mode 100644 index 0000000..b4e091f --- /dev/null +++ b/resources/public/bounty-filter-remove.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/resources/public/dest/img/new-site/icon_rt_black.svg b/resources/public/dest/img/new-site/icon_rt_black.svg index 835f3d5..7a31f8b 100644 --- a/resources/public/dest/img/new-site/icon_rt_black.svg +++ b/resources/public/dest/img/new-site/icon_rt_black.svg @@ -1 +1 @@ -icon_sl \ No newline at end of file +icon_sl \ No newline at end of file diff --git a/resources/public/icon-forward-gray.svg b/resources/public/icon-forward-gray.svg new file mode 100644 index 0000000..a80ff29 --- /dev/null +++ b/resources/public/icon-forward-gray.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/public/icon-forward-white.svg b/resources/public/icon-forward-white.svg new file mode 100644 index 0000000..9842677 --- /dev/null +++ b/resources/public/icon-forward-white.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/templates/home.html b/resources/templates/home.html index 76e60f3..590156e 100644 --- a/resources/templates/home.html +++ b/resources/templates/home.html @@ -72,7 +72,7 @@ height="0" width="0" style="display:none;visibility:hidden"> (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) })(window,document,'script','https://www.google-analytics.com/analytics.js','ga'); - ga('create', 'UA-79146816-2', 'auto'); + ga('create', 'UA-79146816-1', 'auto'); ga('send', 'pageview'); diff --git a/resources/templates/index.html b/resources/templates/index.html index 8e9e660..ba31209 100644 --- a/resources/templates/index.html +++ b/resources/templates/index.html @@ -68,7 +68,6 @@ -
$1M Bounty fund for open source projects @@ -291,6 +290,25 @@ } catch (e) {} + + + + + + diff --git a/resources/templates/welcome.html b/resources/templates/welcome.html index 06cc9be..7f2c631 100644 --- a/resources/templates/welcome.html +++ b/resources/templates/welcome.html @@ -30,7 +30,6 @@ 'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f); })(window,document,'script','dataLayer','GTM-WHLZ2RZ'); - @@ -169,6 +168,10 @@ src="https://www.facebook.com/tr?id=293089407869419&ev=PageView&noscript=1" /> + + + + diff --git a/src/clj/commiteth/eth/multisig_wallet.clj b/src/clj/commiteth/eth/multisig_wallet.clj index 67bbb06..f5393ab 100644 --- a/src/clj/commiteth/eth/multisig_wallet.clj +++ b/src/clj/commiteth/eth/multisig_wallet.clj @@ -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 diff --git a/src/clj/commiteth/routes/redirect.clj b/src/clj/commiteth/routes/redirect.clj index 1560aa0..01018fa 100644 --- a/src/clj/commiteth/routes/redirect.clj +++ b/src/clj/commiteth/routes/redirect.clj @@ -53,8 +53,8 @@ new-user?) (try (hubspot/create-hubspot-contact (:email user) - (:name user "") - (:login user)) + (:name user "") + (:login user)) (catch Throwable t (log/error "Failed to create hubspot contact" t)))) (assoc (found (str (env :server-address) "/app")) diff --git a/src/clj/commiteth/scheduler.clj b/src/clj/commiteth/scheduler.clj index fd84030..0c35df3 100644 --- a/src/clj/commiteth/scheduler.clj +++ b/src/clj/commiteth/scheduler.clj @@ -312,12 +312,13 @@ (defn wrap-in-try-catch [func] (try + (func) (catch Throwable t (log/error t)))) (defn run-tasks [tasks] (doall - (map (fn [func] (wrap-in-try-catch (func))) + (map (fn [func] (wrap-in-try-catch func)) tasks))) (defn run-1-min-interval-tasks [time] diff --git a/src/cljs/commiteth/activity.cljs b/src/cljs/commiteth/activity.cljs index 36f4162..887b8c7 100644 --- a/src/cljs/commiteth/activity.cljs +++ b/src/cljs/commiteth/activity.cljs @@ -2,6 +2,8 @@ (:require [re-frame.core :as rf] [reagent.core :as r] [commiteth.common :refer [moment-timestamp + items-per-page + display-data-page issue-url]])) @@ -54,23 +56,29 @@ (str (subs (str tla) 1) " " balance)])]) [:div.time (moment-timestamp updated)]]]]) - - -(defn activity-list [activity-items] - [:div.ui.container.activity-container - (if (empty? activity-items) - [:div.view-no-data-container - [:p "No recent activity yet"]] - (into [:div.ui.items] - (for [item activity-items] - ^{:key item} [activity-item item])))] ) +(defn activity-list [{:keys [items item-count page-number total-count] + :as activity-page-data} + container-element] + (if (empty? (:items activity-page-data)) + [:div.view-no-data-container + [:p "No recent activity yet"]] + [:div + (let [left (inc (* (dec page-number) items-per-page)) + right (dec (+ left item-count))] + [:div.item-counts-label + [:span (str "Showing " left "-" right " of " total-count)]]) + (display-data-page activity-page-data activity-item container-element)])) (defn activity-page [] - (let [activity-items (rf/subscribe [:activity-feed]) - activity-feed-loading? (rf/subscribe [:get-in [:activity-feed-loading?]])] + (let [activity-page-data (rf/subscribe [:activities-page]) + activity-feed-loading? (rf/subscribe [:get-in [:activity-feed-loading?]]) + container-element (atom nil)] (fn [] (if @activity-feed-loading? [:div.view-loading-container [:div.ui.active.inverted.dimmer [:div.ui.text.loader.view-loading-label "Loading"]]] - [activity-list @activity-items])))) + [:div.ui.container.activity-container + {:ref #(reset! container-element %1)} + [:div.activity-header "Activities"] + [activity-list @activity-page-data container-element]])))) diff --git a/src/cljs/commiteth/bounties.cljs b/src/cljs/commiteth/bounties.cljs index f7ba984..34137bb 100644 --- a/src/cljs/commiteth/bounties.cljs +++ b/src/cljs/commiteth/bounties.cljs @@ -1,23 +1,30 @@ (ns commiteth.bounties - (:require [re-frame.core :as rf] + (:require [reagent.core :as r] + [re-frame.core :as rf] [commiteth.common :refer [moment-timestamp - issue-url]])) + display-data-page + items-per-page + issue-url]] + [commiteth.handlers :as handlers] + [commiteth.db :as db] + [commiteth.ui-model :as ui-model] + [commiteth.subscriptions :as subs])) (defn bounty-item [bounty] - (let [{avatar-url :repo_owner_avatar_url - owner :repo-owner - repo-name :repo-name - issue-title :issue-title + (let [{avatar-url :repo_owner_avatar_url + owner :repo-owner + repo-name :repo-name + issue-title :issue-title issue-number :issue-number - updated :updated - tokens :tokens - balance-eth :balance-eth - value-usd :value-usd - claim-count :claim-count} bounty - full-repo (str owner "/" repo-name) - repo-url (str "https://github.com/" full-repo) - repo-link [:a {:href repo-url} full-repo] + updated :updated + tokens :tokens + balance-eth :balance-eth + value-usd :value-usd + claim-count :claim-count} bounty + full-repo (str owner "/" repo-name) + repo-url (str "https://github.com/" full-repo) + repo-link [:a {:href repo-url} full-repo] issue-link [:a {:href (issue-url owner repo-name issue-number)} issue-title]] @@ -37,28 +44,187 @@ [: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-list [open-bounties] - [:div.ui.container.open-bounties-container - [:div.open-bounties-header "Bounties"] - (if (empty? open-bounties) - [:div.view-no-data-container - [:p "No recent activity yet"]] - (into [:div.ui.items] - (for [bounty open-bounties] - [bounty-item bounty])))]) +(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 matching bounties found."]] + [:div + (let [left (inc (* (dec page-number) items-per-page)) + right (dec (+ left item-count))] + [:div.item-counts-label + [:span (str "Showing " left "-" right " of " total-count)]]) + (display-data-page bounty-page-data bounty-item container-element)])) (defn bounties-page [] - (let [open-bounties (rf/subscribe [:open-bounties]) - open-bounties-loading? (rf/subscribe [:get-in [:open-bounties-loading?]])] + (let [bounty-page-data (rf/subscribe [:open-bounties-page]) + open-bounties-loading? (rf/subscribe [:get-in [:open-bounties-loading?]]) + container-element (atom nil)] (fn [] (if @open-bounties-loading? [:div.view-loading-container [:div.ui.active.inverted.dimmer [:div.ui.text.loader.view-loading-label "Loading"]]] - [bounties-list @open-bounties])))) + [:div.ui.container.open-bounties-container + {:ref #(reset! container-element %1)} + [:div.open-bounties-header "Bounties"] + [:div.open-bounties-filter-and-sort + [bounty-filters-view] + [bounties-sort-view]] + [bounties-list @bounty-page-data container-element]])) + )) diff --git a/src/cljs/commiteth/common.cljs b/src/cljs/commiteth/common.cljs index 9d302f0..1213395 100644 --- a/src/cljs/commiteth/common.cljs +++ b/src/cljs/commiteth/common.cljs @@ -1,6 +1,7 @@ (ns commiteth.common (:require [reagent.core :as r] [re-frame.core :as rf] + [clojure.string :as str] [cljsjs.moment])) (defn input [val-ratom props] @@ -29,3 +30,96 @@ (defn issue-url [owner repo number] (str "https://github.com/" owner "/" repo "/issues/" number)) + +(def items-per-page 15) + +(defn draw-page-numbers [page-number page-count container-element] + "Draw page numbers for the pagination component. + Inserts ellipsis when list is too long, by default + max 6 items are allowed" + (let [draw-page-num-fn (fn [current? i] + ^{:key i} + [:div.rectangle-rounded + (if current? + {:class "page-num-active"} + {:class "grayed-out-page-num" + :on-click #(do + (rf/dispatch [:set-page-number i]) + (when @container-element + (.scrollIntoView @container-element)))}) + i]) + max-page-nums 6] + [:div.page-nums-container + (cond (<= page-count max-page-nums) + (for [i (map inc (range page-count))] + (draw-page-num-fn (= i page-number) i)) + (<= page-number (- max-page-nums 3)) + (concat + (for [i (map inc (range (- max-page-nums 2)))] + (draw-page-num-fn (= i page-number) i)) + [^{:key (dec max-page-nums)} + [:div.page-nav-text [:span "..."]]] + [(draw-page-num-fn false page-count)]) + (>= page-number (- page-count (- max-page-nums 4))) + (concat + [(draw-page-num-fn false 1) + ^{:key 2} + [:div.page-nav-text [:span "..."]]] + (for [i (map inc (range (- page-count 4) page-count))] + (draw-page-num-fn (= i page-number) i)) + ) + :else + (concat + [(draw-page-num-fn false 1) + ^{:key 2} [:div.page-nav-text [:span "..."]]] + (for [i [(dec page-number) page-number (inc page-number)]] + (draw-page-num-fn (= i page-number) i)) + [^{:key (dec page-count)} [:div.page-nav-text [:span "..."]] + (draw-page-num-fn false page-count)]))])) + +(defn display-data-page [{:keys [items + item-count + total-count + page-number + page-count]} + draw-item-fn + container-element] + "Draw data items along with pagination controls" + (let [draw-items (fn [] + (into [:div.ui.items] + (for [item items] + ^{:key item} [draw-item-fn item]))) + on-direction-click (fn [forward?] + #(when (or (and (< page-number page-count) + forward?) + (and (< 1 page-number) + (not forward?))) + (rf/dispatch [:set-page-number + (if forward? + (inc page-number) + (dec page-number))]) + (when @container-element + (.scrollIntoView @container-element)))) + draw-rect (fn [direction] + (let [forward? (= direction :forward) + gray-out? (or (and forward? (= page-number page-count)) + (and (not forward?) (= page-number 1)))] + [:div.rectangle-rounded + (cond-> {:on-click (on-direction-click forward?)} + gray-out? (assoc :class "grayed-out-direction")) + [:img.icon-forward-gray + (cond-> {:src "icon-forward-gray.svg"} + forward? (assoc :class "flip-horizontal"))]]))] + (cond (<= total-count items-per-page) + [draw-items] + :else + [:div + [draw-items] + [:div.page-nav-container + [:div.page-direction-container + [draw-rect :backward] + [draw-rect :forward]] + [:div.page-nav-text [:span (str "Page " page-number " of " page-count)]] + [draw-page-numbers page-number page-count container-element]]]))) + + diff --git a/src/cljs/commiteth/db.cljs b/src/cljs/commiteth/db.cljs index 2ee532f..c0ebfb8 100644 --- a/src/cljs/commiteth/db.cljs +++ b/src/cljs/commiteth/db.cljs @@ -1,13 +1,22 @@ -(ns commiteth.db) +(ns commiteth.db + (:require [commiteth.ui-model :as ui-model])) (def default-db - {:page :bounties - :user nil - :repos-loading? false - :repos {} - :activity-feed-loading? false - :open-bounties-loading? false - :open-bounties [] - :owner-bounties {} - :top-hunters [] - :activity-feed []}) + {:page :bounties + :user nil + :repos-loading? false + :repos {} + :activity-feed-loading? false + :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 []}) diff --git a/src/cljs/commiteth/handlers.cljs b/src/cljs/commiteth/handlers.cljs index e9c6d4a..549d22b 100644 --- a/src/cljs/commiteth/handlers.cljs +++ b/src/cljs/commiteth/handlers.cljs @@ -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 @@ -38,7 +39,6 @@ (println "redirecting to" path) (set! (.-pathname js/location) path))) - (reg-event-fx :initialize-db [(inject-cofx :store)] @@ -70,7 +70,15 @@ (reg-event-db :set-active-page (fn [db [_ page]] - (assoc db :page page))) + (assoc db :page page + :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 + (fn [db [_ page]] + (assoc db :page-number page))) (reg-event-fx :set-flash-message @@ -85,7 +93,6 @@ (fn [db _] (dissoc db :flash-message))) - (defn assoc-in-if-not-empty [m path val] (if (seq val) (assoc-in m path val) @@ -459,3 +466,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)))) diff --git a/src/cljs/commiteth/subscriptions.cljs b/src/cljs/commiteth/subscriptions.cljs index 6e18eb1..bb2a82a 100644 --- a/src/cljs/commiteth/subscriptions.cljs +++ b/src/cljs/commiteth/subscriptions.cljs @@ -1,9 +1,13 @@ (ns commiteth.subscriptions - (:require [re-frame.core :refer [reg-sub]])) + (:require [re-frame.core :refer [reg-sub]] + [commiteth.db :as db] + [commiteth.ui-model :as ui-model] + [commiteth.common :refer [items-per-page]] + [clojure.string :as string])) (reg-sub - :db - (fn [db _] db)) + :db + (fn [db _] db)) (reg-sub :page @@ -33,7 +37,28 @@ (reg-sub :open-bounties (fn [db _] - (:open-bounties db))) + (vec (:open-bounties db)))) + +(reg-sub + :page-number + (fn [db _] + (:page-number db))) + +(reg-sub + :open-bounties-page + :<- [::filtered-and-sorted-open-bounties] + :<- [:page-number] + (fn [[open-bounties page-number] _] + (let [total-count (count open-bounties) + start (* (dec page-number) items-per-page) + end (min total-count (+ items-per-page start)) + items (subvec open-bounties start end)] + {:items items + :item-count (count items) + :total-count total-count + :page-number page-number + :page-count (Math/ceil (/ total-count items-per-page))}))) + (reg-sub :owner-bounties @@ -53,7 +78,23 @@ (reg-sub :activity-feed (fn [db _] - (:activity-feed db))) + (vec (:activity-feed db)))) + +(reg-sub + :activities-page + :<- [:activity-feed] + :<- [:page-number] + (fn [[activities page-number] _] + (let [total-count (count activities) + start (* (dec page-number) items-per-page) + end (min total-count (+ items-per-page start)) + items (subvec activities start end)] + {:items items + :item-count (count items) + :total-count total-count + :page-number page-number + :page-count (Math/ceil (/ total-count items-per-page))}))) + (reg-sub :gh-admin-token @@ -67,16 +108,62 @@ (get-in db path))) (reg-sub - :usage-metrics - (fn [db _] - (:usage-metrics db))) + :usage-metrics + (fn [db _] + (:usage-metrics db))) (reg-sub - :metrics-loading? - (fn [db _] - (:metrics-loading? db))) + :metrics-loading? + (fn [db _] + (:metrics-loading? db))) (reg-sub - :user-dropdown-open? + :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))) diff --git a/src/cljs/commiteth/ui_model.cljs b/src/cljs/commiteth/ui_model.cljs new file mode 100644 index 0000000..fd7d41c --- /dev/null +++ b/src/cljs/commiteth/ui_model.cljs @@ -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)))) diff --git a/src/less/style.less b/src/less/style.less index 516c60c..53b85cc 100644 --- a/src/less/style.less +++ b/src/less/style.less @@ -421,6 +421,304 @@ label[for="input-hidden"] { font-weight: 500; color: #42505c; } + + .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; + + .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: none; + } + 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 { + background: green; + transform: scale(1.3); + } + + 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: 8px 12px; + 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 { @@ -504,7 +802,7 @@ label[for="input-hidden"] { } .open-claims-label { - padding-left: 5px; + padding-left: 15px; font-size: 15px; color: #57a7ed; } @@ -531,7 +829,7 @@ label[for="input-hidden"] { } } - border: #e7e7e7 solid 0.1em!important; + border-bottom: #eaecee 1px solid !important; box-shadow: none!important; border-radius: 0.3em!important; padding: 0.8em 1em 1.1em!important; @@ -596,9 +894,16 @@ label[for="input-hidden"] { } .activity-container { + background-color: #fff; transform: translate(0, -45px); border-radius: 10px; - + padding: 24px; + .activity-header { + font-family: "PostGrotesk-Medium"; + font-size: 21px; + font-weight: 500; + color: #42505c; + } } .footer-row { @@ -878,3 +1183,106 @@ body { text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; } + +.rectangle-rounded { + width: 45px; + height: 45px; + color: #57a7ed; + /*opacity: 0.2;*/ + border-radius: 22.5px; + /*background-color: #57a7ed;*/ + background-color: rgba(87,167,237,.2); + font-family: "PostGrotesk-Medium"; + font-weight: 500; + font-size: 13px; + display: flex; + margin: 0 6px; + flex: none; + align-items: center; + justify-content: center; + cursor: pointer; +} + +.page-num-active { + cursor: auto; +} + +.grayed-out-page-num { + color: #8d99a4; + background-color: #f2f5f8; + opacity: 1.0; +} + +.grayed-out-direction { + color: #8d99a4; + background-color: #f2f5f8; + opacity: 0.4; + cursor: auto +} + +.flip-horizontal { + -moz-transform: scaleX(-1); + -webkit-transform: scaleX(-1); + -o-transform: scaleX(-1); + transform: scaleX(-1); + -ms-filter: fliph; /*IE*/ + filter: fliph; +} + +.icon-forward-gray { + width: 24px; + height: 24px; + object-fit: contain; +} + +.page-nav-container { + display: flex; + margin: 0 -6px; + @media (max-width: 767px) { + align-items: center; + flex-direction: column; + } +} + +.page-direction-container { + display: flex; + @media (max-width: 767px) { + flex-direction: row; + } +} + +.page-nums-container { + display: flex; + @media (max-width: 767px) { + display: none; + } + margin-left: auto; + justify-content: space-between; +} + +.page-nav-text { + font-family: PostGrotesk-Book; + font-size: 15px; + color: #8d99a4; + + display: flex; + margin: 0 6px; + flex: none; + align-items: center; + justify-content: center; + + @media (max-width: 767px) { + margin-top: 12px; + } +} + +.item-counts-label { + margin: auto; + font-family: "PostGrotesk-Book"; + font-size: 15px; + color: #8d99a4; + padding-top: 8px; + padding-bottom: 8px; +} + + diff --git a/static_langing_page/dest/img/new-site/icon_rt_black.svg b/static_langing_page/dest/img/new-site/icon_rt_black.svg index 835f3d5..7a31f8b 100644 --- a/static_langing_page/dest/img/new-site/icon_rt_black.svg +++ b/static_langing_page/dest/img/new-site/icon_rt_black.svg @@ -1 +1 @@ -icon_sl \ No newline at end of file +icon_sl \ No newline at end of file diff --git a/static_langing_page/index.html b/static_langing_page/index.html index 9310dac..ba31209 100644 --- a/static_langing_page/index.html +++ b/static_langing_page/index.html @@ -290,6 +290,25 @@ } catch (e) {} + + + + + + diff --git a/static_langing_page/welcome.html b/static_langing_page/welcome.html index 96d2c01..7f2c631 100644 --- a/static_langing_page/welcome.html +++ b/static_langing_page/welcome.html @@ -168,6 +168,10 @@ src="https://www.facebook.com/tr?id=293089407869419&ev=PageView&noscript=1" /> + + + + diff --git a/test/Jenkinsfile b/test/Jenkinsfile new file mode 100644 index 0000000..8169c6f --- /dev/null +++ b/test/Jenkinsfile @@ -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' } + } + } +} \ No newline at end of file diff --git a/test/end-to-end/pages/openbounty/bounties.py b/test/end-to-end/pages/openbounty/bounties.py index 91415fd..24a47bc 100644 --- a/test/end-to-end/pages/openbounty/bounties.py +++ b/test/end-to-end/pages/openbounty/bounties.py @@ -1,5 +1,6 @@ from pages.base_page import BasePageObject from pages.base_element import * +from tests import test_data class BountiesHeader(BaseText): @@ -15,11 +16,38 @@ class TopHuntersHeader(BaseText): super(TopHuntersHeader, self).__init__(driver) self.locator = self.Locator.css_selector('.top-hunters-header') +class BountyTitles(BaseText): + + def __init__(self, driver): + super(BountyTitles, self).__init__(driver) + self.locator = self.Locator.css_selector('.open-bounty-item-content .header') + + +class BountyItemRows(BaseText): + + def __init__(self, driver): + super(BountyItemRows, self).__init__(driver) + self.locator = self.Locator.css_selector('.open-bounty-item-content .bounty-item-row') + +class BountyFooters(BaseText): + + def __init__(self, driver): + super(BountyFooters, self).__init__(driver) + self.locator = self.Locator.css_selector('.open-bounty-item-content .footer-row') + class BountiesPage(BasePageObject): def __init__(self, driver): super(BountiesPage, self).__init__(driver) + self.driver = driver self.bounties_header = BountiesHeader(self.driver) self.top_hunters_header = TopHuntersHeader(self.driver) + self.bounty_titles = BountyTitles(self.driver) + self.bounty_item_rows = BountyItemRows(self.driver) + self.bounty_footers = BountyFooters(self.driver) + + def get_bounties_page(self): + self.driver.get(test_data.config['Common']['url'] + 'app') + diff --git a/test/end-to-end/pages/openbounty/landing.py b/test/end-to-end/pages/openbounty/landing.py index 7886f90..71f47f1 100644 --- a/test/end-to-end/pages/openbounty/landing.py +++ b/test/end-to-end/pages/openbounty/landing.py @@ -1,5 +1,6 @@ from pages.base_page import BasePageObject from pages.base_element import * +from tests import test_data class LoginButton(BaseButton): @@ -20,4 +21,4 @@ class LandingPage(BasePageObject): self.login_button = LoginButton(self.driver) def get_landing_page(self): - self.driver.get('https://openbounty.status.im:444/') + self.driver.get(test_data.config['Common']['url']) diff --git a/test/end-to-end/pages/thirdparty/github.py b/test/end-to-end/pages/thirdparty/github.py index 2585c91..0d00335 100644 --- a/test/end-to-end/pages/thirdparty/github.py +++ b/test/end-to-end/pages/thirdparty/github.py @@ -1,6 +1,7 @@ import time, pytest from pages.base_element import * from pages.base_page import BasePageObject +from tests import test_data class EmailEditbox(BaseEditBox): @@ -84,7 +85,7 @@ class LabelsButton(BaseButton): class BountyLabel(BaseButton): def __init__(self, driver): super(LabelsButton.BountyLabel, self).__init__(driver) - self.locator = self.Locator.css_selector("[data-name='bounty']") + self.locator = self.Locator.css_selector("[data-name='748942015']") class CrossButton(BaseButton): def __init__(self, driver): @@ -156,11 +157,14 @@ class GithubPage(BasePageObject): def create_new_bounty(self): self.get_issues_page() self.new_issue_button.click() - self.issue_title_input.send_keys('auto_test_bounty_%s' % self.time_now) + test_data.issue = dict() + test_data.issue['title'] = 'auto_test_bounty_%s' % self.time_now + self.issue_title_input.send_keys(test_data.issue['title']) self.labels_button.click() self.bounty_label.click() self.cross_button.click() self.submit_new_issue_button.click() + return test_data.issue['title'] def get_deployed_contract(self, wait=120): for i in range(wait): diff --git a/test/end-to-end/tests/__init__.py b/test/end-to-end/tests/__init__.py index d9e141d..41ae6be 100644 --- a/test/end-to-end/tests/__init__.py +++ b/test/end-to-end/tests/__init__.py @@ -1,8 +1,17 @@ - +import configparser class TestData(object): def __init__(self): self.test_name = None + self.config = configparser.ConfigParser() + + # define here path to your config.ini file + #example - config_example.ini + + self.config.read('config.ini') + self.base_case_issue = dict() + self.base_case_issue['title'] = 'Very first auto_test_bounty' + test_data = TestData() diff --git a/test/end-to-end/tests/basetestcase.py b/test/end-to-end/tests/basetestcase.py index 398673b..9ba9409 100644 --- a/test/end-to-end/tests/basetestcase.py +++ b/test/end-to-end/tests/basetestcase.py @@ -39,18 +39,29 @@ class BaseTestCase: 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 - # options = webdriver.ChromeOptions() - # options.add_argument('--start-fullscreen') - # options.add_extension(path.abspath('/resources/metamask3_12_0.crx')) - - self.driver = webdriver.Remote(self.executor_sauce_lab, - desired_capabilities=self.capabilities_sauce_lab) + 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 verify_no_errors(self): if self.errors: msg = '' @@ -59,9 +70,9 @@ class BaseTestCase: pytest.fail(msg, pytrace=False) def teardown_method(self): - - remove_application(self.driver) - remove_installation(self.driver) + if self.cleanup: + remove_application(self.driver) + remove_installation(self.driver) try: self.print_sauce_lab_info(self.driver) diff --git a/test/end-to-end/tests/config_example.ini b/test/end-to-end/tests/config_example.ini new file mode 100644 index 0000000..4b52a24 --- /dev/null +++ b/test/end-to-end/tests/config_example.ini @@ -0,0 +1,13 @@ +[Common] +;app URL +url = https://openbounty.status.im:444/ +[Paths] +;AbsolutePath to 'tests' folder +tests_absolute = /usr/dir/open-bounty/test/end-to-end/tests/ +[ORG] +;GitHub credentials for organization +gh_login = login +gh_password = password +;MetaMask password for organization +mm_password = password + diff --git a/test/end-to-end/tests/conftest.py b/test/end-to-end/tests/conftest.py index 62f296e..c0abbae 100644 --- a/test/end-to-end/tests/conftest.py +++ b/test/end-to-end/tests/conftest.py @@ -11,6 +11,10 @@ def pytest_addoption(parser): action='store', default=True, help='Display each test step in terminal as plain text: True/False') + parser.addoption('--env', + action='store', + default='sauce', + help='Specify environment, sauce or local') def pytest_configure(config): diff --git a/test/end-to-end/tests/test_contracts.py b/test/end-to-end/tests/test_contracts.py index 68bb4cb..5564b7b 100644 --- a/test/end-to-end/tests/test_contracts.py +++ b/test/end-to-end/tests/test_contracts.py @@ -1,22 +1,42 @@ import pytest from os import environ from pages.openbounty.landing import LandingPage +from pages.openbounty.bounties import BountiesPage from tests.basetestcase import BaseTestCase +from tests import test_data @pytest.mark.sanity 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('anna04test', - 'f@E23D3H15Rd') + 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 hunters' + 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() + + # Navigate and check top bounty in "Open bounties" + bounties_page = BountiesPage(self.driver) + bounties_page.get_bounties_page() + titles = bounties_page.bounty_titles.find_elements() + assert titles[0].text == test_data.issue['title'] + + + + +