Merge branch 'develop' of https://github.com/status-im/open-bounty into 193-hide-myself

This commit is contained in:
Ivan Grishaev 2018-02-06 20:13:15 +03:00
commit a0073b295e
42 changed files with 1397 additions and 130 deletions

3
.dockerignore Normal file
View File

@ -0,0 +1,3 @@
docker-compose.yml
Dockerfile
Jenkinsfile

5
.gitignore vendored
View File

@ -23,4 +23,7 @@ profiles.clj
.idea .idea
resources/contracts resources/contracts
node_modules node_modules
.DS_Store /config-prod.edn
/config-dev.edn
/config-test.edn
/src/java

30
Dockerfile Normal file
View File

@ -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"]

35
Jenkinsfile vendored Normal file
View File

@ -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
}
}

View File

@ -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: 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 https://openbounty.status.im
The `master` branch is automatically deployed here. The `master` branch is automatically deployed here.
Live testnet (Ropsten) version: Live testnet (Ropsten) version:
https://openbounty.status.im:444 https://openbounty.status.im:444
The `develop` branch is automatically deployed here. The `develop` branch is automatically deployed here.
@ -17,10 +16,28 @@ The `develop` branch is automatically deployed here.
## Prerequisites ## 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 ### 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: 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. 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: 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. This copies over necessary artifacts to `resources` dir.
### Troubleshooting
See the [Cookbook](doc/cookbook.md).
## License ## License
Licensed under the [Affero General Public License v3.0](https://github.com/status-im/commiteth/blob/master/LICENSE.md) Licensed under the [Affero General Public License v3.0](https://github.com/status-im/commiteth/blob/master/LICENSE.md)

18
doc/cookbook.md Normal file
View File

@ -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`.

View File

@ -10,7 +10,7 @@ For testing you will need:
* a Github account with administrative access to one or more repositories * 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. * 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 ### Signing up
@ -29,13 +29,9 @@ You should now see `Bounties`, `Activity`, `Repositories` and `Manage Payouts` t
### Creating bounty issues ### 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 * Request for your account to be whitelisted. Contact [Riot](https://chat.status.im) for more information
* 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.
* 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` * 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 * 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 ### 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. 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` )

View File

@ -1,13 +1,14 @@
(ns user (ns user
(:require [mount.core :as mount] (:require [mount.core :as mount]
[commiteth.figwheel :refer [start-fw stop-fw cljs]] [commiteth.figwheel :refer [start-fw stop-fw cljs]]))
commiteth.core))
(defn start [] (defn start []
(mount/start-without #'commiteth.core/repl-server)) (require 'commiteth.core)
(mount/start-without (ns-resolve 'commiteth.core 'repl-server)))
(defn stop [] (defn stop []
(mount/stop-except #'commiteth.core/repl-server)) (require 'commiteth.core)
(mount/stop-except (ns-resolve 'commiteth.core 'repl-server)))
(defn restart [] (defn restart []
(stop) (stop)

View File

@ -11,16 +11,18 @@
:eth-password "XXX" :eth-password "XXX"
;; RPC URL to ethereum node to be used ;; 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" :eth-wallet-file "/some/location"
;; address of token registry to be used ;; 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 ;; 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 ;; 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 ;; commiteth-test-tpatja
:github-client-id "CLIENT ID" :github-client-id "CLIENT ID"
@ -43,5 +45,7 @@
;; needeed when :hubspot-contact-create-enabled ;; needeed when :hubspot-contact-create-enabled
:hubspot-api-key "xxxxxxx-xxxx-x-xxxx-xxxx" :hubspot-api-key "xxxxxxx-xxxx-x-xxxx-xxxx"
:user-whitelist #{}
;; used for blacklisting tokens from token registry data ;; used for blacklisting tokens from token registry data
:token-blacklist #{}} :token-blacklist #{}}

View File

@ -33,7 +33,11 @@
<AppenderRef ref="FILE"/> <AppenderRef ref="FILE"/>
</logger> </logger>
<root level="DEBUG"> <root level="DEBUG">
<appender-ref ref="STDOUT"/> <!-- <appender-ref ref="STDOUT"/> -->
<appender-ref ref="FILE"/>
</root>
<root level="INFO">
<!-- <appender-ref ref="STDOUT"/> -->
<appender-ref ref="FILE"/> <appender-ref ref="FILE"/>
</root> </root>
</configuration> </configuration>

View File

@ -93,7 +93,8 @@
:profiles :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"]] :prep-tasks ["build-contracts" "javac" "compile" ["cljsbuild" "once" "min"] ["less" "once"]]
:cljsbuild :cljsbuild
{:builds {:builds
@ -115,11 +116,8 @@
:uberjar-name "commiteth.jar" :uberjar-name "commiteth.jar"
:source-paths ["env/prod/clj"] :source-paths ["env/prod/clj"]
:resource-paths ["env/prod/resources"]} :resource-paths ["env/prod/resources"]}
;; :precomp profile allows to compile classes from commiteth.eth.contracts :dev {:jvm-opts ["-server" "-Dconf=config-dev.edn"]
;; namespace before compiling clojure code. Otherwise ClassNotFound exception :dependencies [[prone "1.1.4"]
;; will be thrown
:precomp {:target-path "target/base+system+user+dev/" }
:dev {:dependencies [[prone "1.1.4"]
[ring/ring-mock "0.3.1"] [ring/ring-mock "0.3.1"]
[ring/ring-devel "1.6.2"] [ring/ring-devel "1.6.2"]
[pjstadig/humane-test-output "0.8.3"] [pjstadig/humane-test-output "0.8.3"]
@ -145,7 +143,7 @@
:optimizations :none :optimizations :none
:pretty-print true}}]} :pretty-print true}}]}
:prep-tasks ["build-contracts" ["with-profile" "precomp" "javac"]] :prep-tasks ["build-contracts" "javac"]
:doo {:build "test"} :doo {:build "test"}
:source-paths ["env/dev/clj" "test/clj"] :source-paths ["env/dev/clj" "test/clj"]
:resource-paths ["env/dev/resources"] :resource-paths ["env/dev/resources"]
@ -153,7 +151,8 @@
:nrepl-middleware [cemerick.piggieback/wrap-cljs-repl]} :nrepl-middleware [cemerick.piggieback/wrap-cljs-repl]}
:injections [(require 'pjstadig.humane-test-output) :injections [(require 'pjstadig.humane-test-output)
(pjstadig.humane-test-output/activate!)]} (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"]] :dependencies [[devcards "0.2.4"]]
:cljsbuild :cljsbuild
{:builds {:builds

File diff suppressed because one or more lines are too long

View File

@ -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

View File

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><title>icon_sl</title><g fill="#000"><path d="M12.632 3.316H7.893a2.365 2.365 0 0 0-2.368 2.369v11.821a2.365 2.365 0 0 0 2.368 2.369 2.366 2.366 0 0 0 2.369-2.369v-2.369h2.37c3.254 0 5.91-2.656 5.91-5.911 0-3.254-2.656-5.91-5.91-5.91z"/><path d="M7.905 19.277a1.771 1.771 0 0 1-1.771-1.771V5.673a1.77 1.77 0 0 1 1.759-1.772h4.739a5.336 5.336 0 0 1 5.325 5.325 5.334 5.334 0 0 1-5.325 5.324H9.676v2.956c0 .967-.794 1.771-1.771 1.771z"/><path d="M7.905 18.68a1.185 1.185 0 0 1-1.185-1.186V5.673c0-.645.517-1.173 1.161-1.185h4.751a4.734 4.734 0 0 1 4.726 4.727 4.74 4.74 0 0 1-4.726 4.726H9.09v3.541c0 .666-.529 1.198-1.185 1.198z"/><path d="M7.905 18.094a.596.596 0 0 1-.598-.6V5.673c0-.322.252-.587.574-.598h4.751a4.144 4.144 0 0 1 4.14 4.14 4.145 4.145 0 0 1-4.14 4.14h-4.13v4.139c0 .334-.264.6-.597.6z"/><path d="M7.905 12.768h4.727a3.556 3.556 0 0 0 3.554-3.552 3.55 3.55 0 0 0-3.554-3.554H7.905v7.106z"/><path d="M8.502 12.182V6.26h4.14a2.953 2.953 0 0 1 2.955 2.956 2.954 2.954 0 0 1-2.955 2.956h-4.14v.01z"/><path d="M9.09 11.595V6.857h3.542a2.366 2.366 0 0 1 2.369 2.369 2.366 2.366 0 0 1-2.369 2.369H9.09z"/><path d="M9.676 11.01V7.444h2.956a1.783 1.783 0 0 1 0 3.566H9.676z"/><path d="M10.262 10.411v-2.38h2.37c.654 0 1.195.53 1.195 1.195 0 .668-.528 1.196-1.195 1.196h-2.37v-.011z"/><path d="M10.861 9.825V8.617h1.771c.333 0 .599.265.599.599a.595.595 0 0 1-.599.598h-1.771v.011z"/><ellipse cx="7.827" cy="5.678" rx="2.369" ry="2.369"/><path d="M18.036 16.137a2.361 2.361 0 1 1-3.863 2.716l-3.542-5.062a2.362 2.362 0 0 1 3.865-2.713l3.54 5.059z"/></g></svg> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><title>icon_sl</title><g fill="#FFF"><path d="M12.632 3.316H7.893a2.365 2.365 0 0 0-2.368 2.369v11.821a2.365 2.365 0 0 0 2.368 2.369 2.366 2.366 0 0 0 2.369-2.369v-2.369h2.37c3.254 0 5.91-2.656 5.91-5.911 0-3.254-2.656-5.91-5.91-5.91z"/><path d="M7.905 19.277a1.771 1.771 0 0 1-1.771-1.771V5.673a1.77 1.77 0 0 1 1.759-1.772h4.739a5.336 5.336 0 0 1 5.325 5.325 5.334 5.334 0 0 1-5.325 5.324H9.676v2.956c0 .967-.794 1.771-1.771 1.771z"/><path d="M7.905 18.68a1.185 1.185 0 0 1-1.185-1.186V5.673c0-.645.517-1.173 1.161-1.185h4.751a4.734 4.734 0 0 1 4.726 4.727 4.74 4.74 0 0 1-4.726 4.726H9.09v3.541c0 .666-.529 1.198-1.185 1.198z"/><path d="M7.905 18.094a.596.596 0 0 1-.598-.6V5.673c0-.322.252-.587.574-.598h4.751a4.144 4.144 0 0 1 4.14 4.14 4.145 4.145 0 0 1-4.14 4.14h-4.13v4.139c0 .334-.264.6-.597.6z"/><path d="M7.905 12.768h4.727a3.556 3.556 0 0 0 3.554-3.552 3.55 3.55 0 0 0-3.554-3.554H7.905v7.106z"/><path d="M8.502 12.182V6.26h4.14a2.953 2.953 0 0 1 2.955 2.956 2.954 2.954 0 0 1-2.955 2.956h-4.14v.01z"/><path d="M9.09 11.595V6.857h3.542a2.366 2.366 0 0 1 2.369 2.369 2.366 2.366 0 0 1-2.369 2.369H9.09z"/><path d="M9.676 11.01V7.444h2.956a1.783 1.783 0 0 1 0 3.566H9.676z"/><path d="M10.262 10.411v-2.38h2.37c.654 0 1.195.53 1.195 1.195 0 .668-.528 1.196-1.195 1.196h-2.37v-.011z"/><path d="M10.861 9.825V8.617h1.771c.333 0 .599.265.599.599a.595.595 0 0 1-.599.598h-1.771v.011z"/><ellipse cx="7.827" cy="5.678" rx="2.369" ry="2.369"/><path d="M18.036 16.137a2.361 2.361 0 1 1-3.863 2.716l-3.542-5.062a2.362 2.362 0 0 1 3.865-2.713l3.54 5.059z"/></g></svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path fill="#57A7ED" fill-rule="evenodd" d="M7.223 11.293l7.068-7.067a.999.999 0 1 1 1.414 1.414l-6.377 6.377 6.377 6.376a.999.999 0 1 1-1.414 1.415L7.223 12.74a1.001 1.001 0 0 1-.288-.827 1 1 0 0 1 .288-.62z"/>
</svg>

After

Width:  |  Height:  |  Size: 307 B

View File

@ -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

View File

@ -72,7 +72,7 @@ height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), (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) 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'); })(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'); ga('send', 'pageview');
</script> </script>
<script type="text/javascript" id="hs-script-loader" async defer src="//js.hs-scripts.com/3954379.js"></script> <script type="text/javascript" id="hs-script-loader" async defer src="//js.hs-scripts.com/3954379.js"></script>

View File

@ -68,7 +68,6 @@
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-WHLZ2RZ" <noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-WHLZ2RZ"
height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript> height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
<!-- End Google Tag Manager (noscript) --> <!-- End Google Tag Manager (noscript) -->
<div class="news"> <div class="news">
<div class="news-inner"> <div class="news-inner">
<strong>$1M</strong> Bounty fund for open source projects <strong>$1M</strong> Bounty fund for open source projects
@ -291,6 +290,25 @@
} catch (e) {} } catch (e) {}
</script> </script>
<!-- /Google Analytics --> <!-- /Google Analytics -->
<!-- Facebook Pixel Code -->
<script>
!function(f,b,e,v,n,t,s)
{if(f.fbq)return;n=f.fbq=function(){n.callMethod?
n.callMethod.apply(n,arguments):n.queue.push(arguments)};
if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';
n.queue=[];t=b.createElement(e);t.async=!0;
t.src=v;s=b.getElementsByTagName(e)[0];
s.parentNode.insertBefore(t,s)}(window, document,'script',
'https://connect.facebook.net/en_US/fbevents.js');
fbq('init', '293089407869419');
fbq('track', 'PageView');
</script>
<noscript><img height="1" width="1" style="display:none"
src="https://www.facebook.com/tr?id=293089407869419&ev=PageView&noscript=1"
/></noscript>
<!-- End Facebook Pixel Code -->
<!-- Start of HubSpot Embed Code --> <!-- Start of HubSpot Embed Code -->
<script type="text/javascript" id="hs-script-loader" async defer src="//js.hs-scripts.com/3954379.js"></script> <script type="text/javascript" id="hs-script-loader" async defer src="//js.hs-scripts.com/3954379.js"></script>
<!-- End of HubSpot Embed Code --> <!-- End of HubSpot Embed Code -->

View File

@ -30,7 +30,6 @@
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f); 'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','GTM-WHLZ2RZ');</script> })(window,document,'script','dataLayer','GTM-WHLZ2RZ');</script>
<!-- End Google Tag Manager --> <!-- End Google Tag Manager -->
</head> </head>
<body> <body>
@ -169,6 +168,10 @@
src="https://www.facebook.com/tr?id=293089407869419&ev=PageView&noscript=1" src="https://www.facebook.com/tr?id=293089407869419&ev=PageView&noscript=1"
/></noscript> /></noscript>
<!-- End Facebook Pixel Code --> <!-- End Facebook Pixel Code -->
<!-- Start of HubSpot Embed Code -->
<script type="text/javascript" id="hs-script-loader" async defer src="//js.hs-scripts.com/3954379.js"></script>
<!-- End of HubSpot Embed Code -->
</body> </body>
</html> </html>

View File

@ -29,7 +29,7 @@
:confirmation (eth/event-sig->topic-id "Confirmation(address,uint256)")}) :confirmation (eth/event-sig->topic-id "Confirmation(address,uint256)")})
(defn factory-contract-addr [] (defn factory-contract-addr []
(env :contract-factory-addr "0x47F56FD26EEeCda4FdF5DB5843De1fe75D2A64A6")) (env :contract-factory-addr))
(defn tokenreg-base-format (defn tokenreg-base-format
;; status tokenreg uses eg :base 18, while parity uses :base 1000000000000 ;; status tokenreg uses eg :base 18, while parity uses :base 1000000000000

View File

@ -53,8 +53,8 @@
new-user?) new-user?)
(try (try
(hubspot/create-hubspot-contact (:email user) (hubspot/create-hubspot-contact (:email user)
(:name user "") (:name user "")
(:login user)) (:login user))
(catch Throwable t (catch Throwable t
(log/error "Failed to create hubspot contact" t)))) (log/error "Failed to create hubspot contact" t))))
(assoc (found (str (env :server-address) "/app")) (assoc (found (str (env :server-address) "/app"))

View File

@ -312,12 +312,13 @@
(defn wrap-in-try-catch [func] (defn wrap-in-try-catch [func]
(try (try
(func)
(catch Throwable t (catch Throwable t
(log/error t)))) (log/error t))))
(defn run-tasks [tasks] (defn run-tasks [tasks]
(doall (doall
(map (fn [func] (wrap-in-try-catch (func))) (map (fn [func] (wrap-in-try-catch func))
tasks))) tasks)))
(defn run-1-min-interval-tasks [time] (defn run-1-min-interval-tasks [time]

View File

@ -2,6 +2,8 @@
(:require [re-frame.core :as rf] (:require [re-frame.core :as rf]
[reagent.core :as r] [reagent.core :as r]
[commiteth.common :refer [moment-timestamp [commiteth.common :refer [moment-timestamp
items-per-page
display-data-page
issue-url]])) issue-url]]))
@ -54,23 +56,29 @@
(str (subs (str tla) 1) " " balance)])]) (str (subs (str tla) 1) " " balance)])])
[:div.time (moment-timestamp updated)]]]]) [:div.time (moment-timestamp updated)]]]])
(defn activity-list [{:keys [items item-count page-number total-count]
:as activity-page-data}
(defn activity-list [activity-items] container-element]
[:div.ui.container.activity-container (if (empty? (:items activity-page-data))
(if (empty? activity-items) [:div.view-no-data-container
[:div.view-no-data-container [:p "No recent activity yet"]]
[:p "No recent activity yet"]] [:div
(into [:div.ui.items] (let [left (inc (* (dec page-number) items-per-page))
(for [item activity-items] right (dec (+ left item-count))]
^{:key item} [activity-item item])))] ) [: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 [] (defn activity-page []
(let [activity-items (rf/subscribe [:activity-feed]) (let [activity-page-data (rf/subscribe [:activities-page])
activity-feed-loading? (rf/subscribe [:get-in [:activity-feed-loading?]])] activity-feed-loading? (rf/subscribe [:get-in [:activity-feed-loading?]])
container-element (atom nil)]
(fn [] (fn []
(if @activity-feed-loading? (if @activity-feed-loading?
[:div.view-loading-container [:div.view-loading-container
[:div.ui.active.inverted.dimmer [:div.ui.active.inverted.dimmer
[:div.ui.text.loader.view-loading-label "Loading"]]] [: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]]))))

View File

@ -1,23 +1,30 @@
(ns commiteth.bounties (ns commiteth.bounties
(:require [re-frame.core :as rf] (:require [reagent.core :as r]
[re-frame.core :as rf]
[commiteth.common :refer [moment-timestamp [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] (defn bounty-item [bounty]
(let [{avatar-url :repo_owner_avatar_url (let [{avatar-url :repo_owner_avatar_url
owner :repo-owner owner :repo-owner
repo-name :repo-name repo-name :repo-name
issue-title :issue-title issue-title :issue-title
issue-number :issue-number issue-number :issue-number
updated :updated updated :updated
tokens :tokens tokens :tokens
balance-eth :balance-eth balance-eth :balance-eth
value-usd :value-usd value-usd :value-usd
claim-count :claim-count} bounty claim-count :claim-count} bounty
full-repo (str owner "/" repo-name) full-repo (str owner "/" repo-name)
repo-url (str "https://github.com/" full-repo) repo-url (str "https://github.com/" full-repo)
repo-link [:a {:href repo-url} full-repo] repo-link [:a {:href repo-url} full-repo]
issue-link [:a issue-link [:a
{:href (issue-url owner repo-name issue-number)} {:href (issue-url owner repo-name issue-number)}
issue-title]] issue-title]]
@ -37,28 +44,187 @@
[:span.usd-value-label "Value "] [:span.usd-balance-label (str "$" value-usd)] [:span.usd-value-label "Value "] [:span.usd-balance-label (str "$" value-usd)]
(when (> claim-count 0) (when (> claim-count 0)
[:span.open-claims-label (str claim-count " open claim" [: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.open-bounty-item-icon
[:div.ui.tiny.circular.image [:div.ui.tiny.circular.image
[:img {:src avatar-url}]]]])) [:img {:src avatar-url}]]]]))
(defn bounties-list [open-bounties] (defn bounties-filter-tooltip-value-input-view [label tooltip-open? opts]
[:div.ui.container.open-bounties-container [:div.open-bounties-filter-element-tooltip-value-input-container
[:div.open-bounties-header "Bounties"] [:div.:input.open-bounties-filter-element-tooltip-value-input-label
(if (empty? open-bounties) label]
[:div.view-no-data-container [:input.open-bounties-filter-element-tooltip-value-input
[:p "No recent activity yet"]] {:type "range"
(into [:div.ui.items] :min (:min opts)
(for [bounty open-bounties] :max (:max opts)
[bounty-item bounty])))]) :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 [] (defn bounties-page []
(let [open-bounties (rf/subscribe [:open-bounties]) (let [bounty-page-data (rf/subscribe [:open-bounties-page])
open-bounties-loading? (rf/subscribe [:get-in [:open-bounties-loading?]])] open-bounties-loading? (rf/subscribe [:get-in [:open-bounties-loading?]])
container-element (atom nil)]
(fn [] (fn []
(if @open-bounties-loading? (if @open-bounties-loading?
[:div.view-loading-container [:div.view-loading-container
[:div.ui.active.inverted.dimmer [:div.ui.active.inverted.dimmer
[:div.ui.text.loader.view-loading-label "Loading"]]] [: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]]))
))

View File

@ -1,6 +1,7 @@
(ns commiteth.common (ns commiteth.common
(:require [reagent.core :as r] (:require [reagent.core :as r]
[re-frame.core :as rf] [re-frame.core :as rf]
[clojure.string :as str]
[cljsjs.moment])) [cljsjs.moment]))
(defn input [val-ratom props] (defn input [val-ratom props]
@ -29,3 +30,96 @@
(defn issue-url [owner repo number] (defn issue-url [owner repo number]
(str "https://github.com/" owner "/" repo "/issues/" 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]]])))

View File

@ -1,13 +1,22 @@
(ns commiteth.db) (ns commiteth.db
(:require [commiteth.ui-model :as ui-model]))
(def default-db (def default-db
{:page :bounties {:page :bounties
:user nil :user nil
:repos-loading? false :repos-loading? false
:repos {} :repos {}
:activity-feed-loading? false :activity-feed-loading? false
:open-bounties-loading? false :open-bounties-loading? false
:open-bounties [] :open-bounties []
:owner-bounties {} :page-number 1
:top-hunters [] :bounty-page-number 1
:activity-feed []}) :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 []})

View File

@ -11,7 +11,8 @@
[cljs-web3.eth :as web3-eth] [cljs-web3.eth :as web3-eth]
[akiroz.re-frame.storage [akiroz.re-frame.storage
:as rf-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 (rf-storage/reg-co-fx! :commiteth-sob {:fx :store
@ -38,7 +39,6 @@
(println "redirecting to" path) (println "redirecting to" path)
(set! (.-pathname js/location) path))) (set! (.-pathname js/location) path)))
(reg-event-fx (reg-event-fx
:initialize-db :initialize-db
[(inject-cofx :store)] [(inject-cofx :store)]
@ -70,7 +70,15 @@
(reg-event-db (reg-event-db
:set-active-page :set-active-page
(fn [db [_ 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 (reg-event-fx
:set-flash-message :set-flash-message
@ -85,7 +93,6 @@
(fn [db _] (fn [db _]
(dissoc db :flash-message))) (dissoc db :flash-message)))
(defn assoc-in-if-not-empty [m path val] (defn assoc-in-if-not-empty [m path val]
(if (seq val) (if (seq val)
(assoc-in m path val) (assoc-in m path val)
@ -459,3 +466,16 @@
(fn [db [_]] (fn [db [_]]
(.removeEventListener js/window "click" close-dropdown) (.removeEventListener js/window "click" close-dropdown)
(assoc db :user-dropdown-open? false))) (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))))

View File

@ -1,9 +1,13 @@
(ns commiteth.subscriptions (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 (reg-sub
:db :db
(fn [db _] db)) (fn [db _] db))
(reg-sub (reg-sub
:page :page
@ -33,7 +37,28 @@
(reg-sub (reg-sub
:open-bounties :open-bounties
(fn [db _] (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 (reg-sub
:owner-bounties :owner-bounties
@ -53,7 +78,23 @@
(reg-sub (reg-sub
:activity-feed :activity-feed
(fn [db _] (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 (reg-sub
:gh-admin-token :gh-admin-token
@ -67,16 +108,62 @@
(get-in db path))) (get-in db path)))
(reg-sub (reg-sub
:usage-metrics :usage-metrics
(fn [db _] (fn [db _]
(:usage-metrics db))) (:usage-metrics db)))
(reg-sub (reg-sub
:metrics-loading? :metrics-loading?
(fn [db _] (fn [db _]
(:metrics-loading? db))) (:metrics-loading? db)))
(reg-sub (reg-sub
:user-dropdown-open? :user-dropdown-open?
(fn [db _] (fn [db _]
(:user-dropdown-open? 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)))

View File

@ -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))))

View File

@ -421,6 +421,304 @@ label[for="input-hidden"] {
font-weight: 500; font-weight: 500;
color: #42505c; 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 { .open-bounty-item {
@ -504,7 +802,7 @@ label[for="input-hidden"] {
} }
.open-claims-label { .open-claims-label {
padding-left: 5px; padding-left: 15px;
font-size: 15px; font-size: 15px;
color: #57a7ed; 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; box-shadow: none!important;
border-radius: 0.3em!important; border-radius: 0.3em!important;
padding: 0.8em 1em 1.1em!important; padding: 0.8em 1em 1.1em!important;
@ -596,9 +894,16 @@ label[for="input-hidden"] {
} }
.activity-container { .activity-container {
background-color: #fff;
transform: translate(0, -45px); transform: translate(0, -45px);
border-radius: 10px; border-radius: 10px;
padding: 24px;
.activity-header {
font-family: "PostGrotesk-Medium";
font-size: 21px;
font-weight: 500;
color: #42505c;
}
} }
.footer-row { .footer-row {
@ -878,3 +1183,106 @@ body {
text-rendering: optimizeLegibility; text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased; -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;
}

View File

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><title>icon_sl</title><g fill="#000"><path d="M12.632 3.316H7.893a2.365 2.365 0 0 0-2.368 2.369v11.821a2.365 2.365 0 0 0 2.368 2.369 2.366 2.366 0 0 0 2.369-2.369v-2.369h2.37c3.254 0 5.91-2.656 5.91-5.911 0-3.254-2.656-5.91-5.91-5.91z"/><path d="M7.905 19.277a1.771 1.771 0 0 1-1.771-1.771V5.673a1.77 1.77 0 0 1 1.759-1.772h4.739a5.336 5.336 0 0 1 5.325 5.325 5.334 5.334 0 0 1-5.325 5.324H9.676v2.956c0 .967-.794 1.771-1.771 1.771z"/><path d="M7.905 18.68a1.185 1.185 0 0 1-1.185-1.186V5.673c0-.645.517-1.173 1.161-1.185h4.751a4.734 4.734 0 0 1 4.726 4.727 4.74 4.74 0 0 1-4.726 4.726H9.09v3.541c0 .666-.529 1.198-1.185 1.198z"/><path d="M7.905 18.094a.596.596 0 0 1-.598-.6V5.673c0-.322.252-.587.574-.598h4.751a4.144 4.144 0 0 1 4.14 4.14 4.145 4.145 0 0 1-4.14 4.14h-4.13v4.139c0 .334-.264.6-.597.6z"/><path d="M7.905 12.768h4.727a3.556 3.556 0 0 0 3.554-3.552 3.55 3.55 0 0 0-3.554-3.554H7.905v7.106z"/><path d="M8.502 12.182V6.26h4.14a2.953 2.953 0 0 1 2.955 2.956 2.954 2.954 0 0 1-2.955 2.956h-4.14v.01z"/><path d="M9.09 11.595V6.857h3.542a2.366 2.366 0 0 1 2.369 2.369 2.366 2.366 0 0 1-2.369 2.369H9.09z"/><path d="M9.676 11.01V7.444h2.956a1.783 1.783 0 0 1 0 3.566H9.676z"/><path d="M10.262 10.411v-2.38h2.37c.654 0 1.195.53 1.195 1.195 0 .668-.528 1.196-1.195 1.196h-2.37v-.011z"/><path d="M10.861 9.825V8.617h1.771c.333 0 .599.265.599.599a.595.595 0 0 1-.599.598h-1.771v.011z"/><ellipse cx="7.827" cy="5.678" rx="2.369" ry="2.369"/><path d="M18.036 16.137a2.361 2.361 0 1 1-3.863 2.716l-3.542-5.062a2.362 2.362 0 0 1 3.865-2.713l3.54 5.059z"/></g></svg> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><title>icon_sl</title><g fill="#FFF"><path d="M12.632 3.316H7.893a2.365 2.365 0 0 0-2.368 2.369v11.821a2.365 2.365 0 0 0 2.368 2.369 2.366 2.366 0 0 0 2.369-2.369v-2.369h2.37c3.254 0 5.91-2.656 5.91-5.911 0-3.254-2.656-5.91-5.91-5.91z"/><path d="M7.905 19.277a1.771 1.771 0 0 1-1.771-1.771V5.673a1.77 1.77 0 0 1 1.759-1.772h4.739a5.336 5.336 0 0 1 5.325 5.325 5.334 5.334 0 0 1-5.325 5.324H9.676v2.956c0 .967-.794 1.771-1.771 1.771z"/><path d="M7.905 18.68a1.185 1.185 0 0 1-1.185-1.186V5.673c0-.645.517-1.173 1.161-1.185h4.751a4.734 4.734 0 0 1 4.726 4.727 4.74 4.74 0 0 1-4.726 4.726H9.09v3.541c0 .666-.529 1.198-1.185 1.198z"/><path d="M7.905 18.094a.596.596 0 0 1-.598-.6V5.673c0-.322.252-.587.574-.598h4.751a4.144 4.144 0 0 1 4.14 4.14 4.145 4.145 0 0 1-4.14 4.14h-4.13v4.139c0 .334-.264.6-.597.6z"/><path d="M7.905 12.768h4.727a3.556 3.556 0 0 0 3.554-3.552 3.55 3.55 0 0 0-3.554-3.554H7.905v7.106z"/><path d="M8.502 12.182V6.26h4.14a2.953 2.953 0 0 1 2.955 2.956 2.954 2.954 0 0 1-2.955 2.956h-4.14v.01z"/><path d="M9.09 11.595V6.857h3.542a2.366 2.366 0 0 1 2.369 2.369 2.366 2.366 0 0 1-2.369 2.369H9.09z"/><path d="M9.676 11.01V7.444h2.956a1.783 1.783 0 0 1 0 3.566H9.676z"/><path d="M10.262 10.411v-2.38h2.37c.654 0 1.195.53 1.195 1.195 0 .668-.528 1.196-1.195 1.196h-2.37v-.011z"/><path d="M10.861 9.825V8.617h1.771c.333 0 .599.265.599.599a.595.595 0 0 1-.599.598h-1.771v.011z"/><ellipse cx="7.827" cy="5.678" rx="2.369" ry="2.369"/><path d="M18.036 16.137a2.361 2.361 0 1 1-3.863 2.716l-3.542-5.062a2.362 2.362 0 0 1 3.865-2.713l3.54 5.059z"/></g></svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -290,6 +290,25 @@
} catch (e) {} } catch (e) {}
</script> </script>
<!-- /Google Analytics --> <!-- /Google Analytics -->
<!-- Facebook Pixel Code -->
<script>
!function(f,b,e,v,n,t,s)
{if(f.fbq)return;n=f.fbq=function(){n.callMethod?
n.callMethod.apply(n,arguments):n.queue.push(arguments)};
if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';
n.queue=[];t=b.createElement(e);t.async=!0;
t.src=v;s=b.getElementsByTagName(e)[0];
s.parentNode.insertBefore(t,s)}(window, document,'script',
'https://connect.facebook.net/en_US/fbevents.js');
fbq('init', '293089407869419');
fbq('track', 'PageView');
</script>
<noscript><img height="1" width="1" style="display:none"
src="https://www.facebook.com/tr?id=293089407869419&ev=PageView&noscript=1"
/></noscript>
<!-- End Facebook Pixel Code -->
<!-- Start of HubSpot Embed Code --> <!-- Start of HubSpot Embed Code -->
<script type="text/javascript" id="hs-script-loader" async defer src="//js.hs-scripts.com/3954379.js"></script> <script type="text/javascript" id="hs-script-loader" async defer src="//js.hs-scripts.com/3954379.js"></script>
<!-- End of HubSpot Embed Code --> <!-- End of HubSpot Embed Code -->

View File

@ -168,6 +168,10 @@
src="https://www.facebook.com/tr?id=293089407869419&ev=PageView&noscript=1" src="https://www.facebook.com/tr?id=293089407869419&ev=PageView&noscript=1"
/></noscript> /></noscript>
<!-- End Facebook Pixel Code --> <!-- End Facebook Pixel Code -->
<!-- Start of HubSpot Embed Code -->
<script type="text/javascript" id="hs-script-loader" async defer src="//js.hs-scripts.com/3954379.js"></script>
<!-- End of HubSpot Embed Code -->
</body> </body>
</html> </html>

11
test/Jenkinsfile vendored Normal file
View File

@ -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' }
}
}
}

View File

@ -1,5 +1,6 @@
from pages.base_page import BasePageObject from pages.base_page import BasePageObject
from pages.base_element import * from pages.base_element import *
from tests import test_data
class BountiesHeader(BaseText): class BountiesHeader(BaseText):
@ -15,11 +16,38 @@ class TopHuntersHeader(BaseText):
super(TopHuntersHeader, self).__init__(driver) super(TopHuntersHeader, self).__init__(driver)
self.locator = self.Locator.css_selector('.top-hunters-header') 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): class BountiesPage(BasePageObject):
def __init__(self, driver): def __init__(self, driver):
super(BountiesPage, self).__init__(driver) super(BountiesPage, self).__init__(driver)
self.driver = driver self.driver = driver
self.bounties_header = BountiesHeader(self.driver) self.bounties_header = BountiesHeader(self.driver)
self.top_hunters_header = TopHuntersHeader(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')

View File

@ -1,5 +1,6 @@
from pages.base_page import BasePageObject from pages.base_page import BasePageObject
from pages.base_element import * from pages.base_element import *
from tests import test_data
class LoginButton(BaseButton): class LoginButton(BaseButton):
@ -20,4 +21,4 @@ class LandingPage(BasePageObject):
self.login_button = LoginButton(self.driver) self.login_button = LoginButton(self.driver)
def get_landing_page(self): def get_landing_page(self):
self.driver.get('https://openbounty.status.im:444/') self.driver.get(test_data.config['Common']['url'])

View File

@ -1,6 +1,7 @@
import time, pytest import time, pytest
from pages.base_element import * from pages.base_element import *
from pages.base_page import BasePageObject from pages.base_page import BasePageObject
from tests import test_data
class EmailEditbox(BaseEditBox): class EmailEditbox(BaseEditBox):
@ -84,7 +85,7 @@ class LabelsButton(BaseButton):
class BountyLabel(BaseButton): class BountyLabel(BaseButton):
def __init__(self, driver): def __init__(self, driver):
super(LabelsButton.BountyLabel, self).__init__(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): class CrossButton(BaseButton):
def __init__(self, driver): def __init__(self, driver):
@ -156,11 +157,14 @@ class GithubPage(BasePageObject):
def create_new_bounty(self): def create_new_bounty(self):
self.get_issues_page() self.get_issues_page()
self.new_issue_button.click() 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.labels_button.click()
self.bounty_label.click() self.bounty_label.click()
self.cross_button.click() self.cross_button.click()
self.submit_new_issue_button.click() self.submit_new_issue_button.click()
return test_data.issue['title']
def get_deployed_contract(self, wait=120): def get_deployed_contract(self, wait=120):
for i in range(wait): for i in range(wait):

View File

@ -1,8 +1,17 @@
import configparser
class TestData(object): class TestData(object):
def __init__(self): def __init__(self):
self.test_name = None 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() test_data = TestData()

View File

@ -39,18 +39,29 @@ class BaseTestCase:
desired_caps['captureHtml'] = False desired_caps['captureHtml'] = False
return desired_caps return desired_caps
@property
def environment(self):
return pytest.config.getoption('env')
def setup_method(self): def setup_method(self):
self.errors = [] self.errors = []
self.cleanup = None
# options = webdriver.ChromeOptions() if self.environment == 'local':
# options.add_argument('--start-fullscreen') options = webdriver.ChromeOptions()
# options.add_extension(path.abspath('/resources/metamask3_12_0.crx')) options.add_argument('--start-fullscreen')
options.add_extension(
self.driver = webdriver.Remote(self.executor_sauce_lab, path.abspath(test_data.config['Paths']['tests_absolute'] + 'resources/metamask3_12_0.crx'))
desired_capabilities=self.capabilities_sauce_lab) # 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) self.driver.implicitly_wait(5)
def verify_no_errors(self): def verify_no_errors(self):
if self.errors: if self.errors:
msg = '' msg = ''
@ -59,9 +70,9 @@ class BaseTestCase:
pytest.fail(msg, pytrace=False) pytest.fail(msg, pytrace=False)
def teardown_method(self): def teardown_method(self):
if self.cleanup:
remove_application(self.driver) remove_application(self.driver)
remove_installation(self.driver) remove_installation(self.driver)
try: try:
self.print_sauce_lab_info(self.driver) self.print_sauce_lab_info(self.driver)

View File

@ -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

View File

@ -11,6 +11,10 @@ def pytest_addoption(parser):
action='store', action='store',
default=True, default=True,
help='Display each test step in terminal as plain text: True/False') 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): def pytest_configure(config):

View File

@ -1,22 +1,42 @@
import pytest import pytest
from os import environ from os import environ
from pages.openbounty.landing import LandingPage from pages.openbounty.landing import LandingPage
from pages.openbounty.bounties import BountiesPage
from tests.basetestcase import BaseTestCase from tests.basetestcase import BaseTestCase
from tests import test_data
@pytest.mark.sanity @pytest.mark.sanity
class TestLogin(BaseTestCase): class TestLogin(BaseTestCase):
def test_deploy_new_contract(self): def test_deploy_new_contract(self):
self.cleanup = True
landing = LandingPage(self.driver) landing = LandingPage(self.driver)
landing.get_landing_page() landing.get_landing_page()
# Sign Up to SOB
github = landing.login_button.click() github = landing.login_button.click()
github.sign_in('anna04test', github.sign_in(test_data.config['ORG']['gh_login'],
'f@E23D3H15Rd') test_data.config['ORG']['gh_password'])
assert github.permission_type.text == 'Personal user data' assert github.permission_type.text == 'Personal user data'
bounties_page = github.authorize_sob.click() bounties_page = github.authorize_sob.click()
# SOB Plugin installation and navigate to "Open bounties"
github.install_sob_plugin() github.install_sob_plugin()
assert bounties_page.bounties_header.text == 'Bounties' 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.create_new_bounty()
github.get_deployed_contract() 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']