").addClass("alert alert-danger").text(data.error))
+ else
+ recipients.append(data.result)
+ form[0].reset()
+ false
+ error: ->
+ form.append($("
").addClass("alert alert-danger").text("An error occured."))
+ false
+
+ $(".commit-autocomplete[data-project-id]").each ->
+ input = $(this)
+ projectId = input.data("project-id")
+ source = new Bloodhound
+ datumTokenizer: Bloodhound.tokenizers.obj.whitespace("sha")
+ queryTokenizer: Bloodhound.tokenizers.whitespace
+ remote: "/projects/#{projectId}/commit_suggestions.json?query=%QUERY"
+
+ source.initialize()
+
+ input.typeahead {minLength: 2, highlight: true},
+ displayKey: "sha"
+ source: source.ttAdapter()
+ templates:
+ suggestion: (object) ->
+ object.description
+
+ if input.data("submit")
+ input.bind "typeahead:selected", ->
+ setTimeout ->
+ input.closest("form").submit()
+ input.typeahead('val', '')
+ , 100
+
+ $(".github-user-autocomplete[data-project-id]").each ->
+ input = $(this)
+ projectId = input.data("project-id")
+ source = new Bloodhound
+ datumTokenizer: Bloodhound.tokenizers.obj.whitespace("login")
+ queryTokenizer: Bloodhound.tokenizers.whitespace
+ remote: "/projects/#{projectId}/github_user_suggestions.json?query=%QUERY"
+
+ source.initialize()
+
+ input.typeahead {minLength: 1, highlight: true},
+ displayKey: "login"
+ source: source.ttAdapter()
+ templates:
+ suggestion: (object) ->
+ object.description
+
+ if input.data("submit")
+ input.bind "typeahead:selected", ->
+ setTimeout ->
+ input.closest("form").submit()
+ input.typeahead('val', '')
+ , 100
+
+ $(".user-autocomplete").each ->
+ input = $(this)
+ source = new Bloodhound
+ datumTokenizer: Bloodhound.tokenizers.obj.whitespace("identifier")
+ queryTokenizer: Bloodhound.tokenizers.whitespace
+ remote: "/users/suggestions.json?query=%QUERY"
+
+ source.initialize()
+
+ input.typeahead {minLength: 1, highlight: true},
+ displayKey: "identifier"
+ source: source.ttAdapter()
+ templates:
+ suggestion: (object) ->
+ object.description
+
+ if input.data("submit")
+ input.bind "typeahead:selected", ->
+ setTimeout ->
+ input.closest("form").submit()
+ input.typeahead('val', '')
+ , 100
diff --git a/app/assets/javascripts/projects.js.coffee b/app/assets/javascripts/projects.js.coffee
index 124a2fc9..e69de29b 100644
--- a/app/assets/javascripts/projects.js.coffee
+++ b/app/assets/javascripts/projects.js.coffee
@@ -1,10 +0,0 @@
-# Place all the behaviors and hooks related to the matching controller here.
-# All this logic will automatically be available in application.js.
-# You can use CoffeeScript in this file: http://coffeescript.org/
-
-init = () ->
- $('.qrcode').each () ->
- $(this).qrcode($(this).attr('data-qrcode'));
-
-$ init
-$(document).on 'page:load', init
\ No newline at end of file
diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css
index 82b772b1..f1b6dba4 100644
--- a/app/assets/stylesheets/application.css
+++ b/app/assets/stylesheets/application.css
@@ -8,6 +8,7 @@
* You're free to add application-wide styles to this file and they'll appear at the top of the
* compiled file, but it's generally better to create a new file per style scope.
*
+ *= require rails_bootstrap_forms
*= require_self
*= require_tree .
*/
@@ -21,4 +22,10 @@
-webkit-box-sizing: content-box;
-moz-box-sizing: content-box;
box-sizing: content-box;
-}
\ No newline at end of file
+}
+.form-devise {
+ max-width: 430px;
+ padding: 15px;
+ margin: 0 auto;
+}
+
diff --git a/app/assets/stylesheets/bootstrap_and_overrides.css.less b/app/assets/stylesheets/bootstrap_and_overrides.css.less
index 9c3ec0d1..4a84e392 100644
--- a/app/assets/stylesheets/bootstrap_and_overrides.css.less
+++ b/app/assets/stylesheets/bootstrap_and_overrides.css.less
@@ -27,4 +27,10 @@
//
// Example:
// @linkColor: #ff0000;
-.qrcode {text-align:center}
\ No newline at end of file
+
+.thread_new_comment {
+ input {
+ &:extend(.btn, .btn-default);
+ }
+}
+
diff --git a/app/assets/stylesheets/distribution.css.sass b/app/assets/stylesheets/distribution.css.sass
new file mode 100644
index 00000000..10155531
--- /dev/null
+++ b/app/assets/stylesheets/distribution.css.sass
@@ -0,0 +1,9 @@
+@import compass/css3
+
+#distribution-form
+ td.amount
+ width: 170px
+ input
+ text-align: right
+.distribution-action
+ +inline-block
diff --git a/app/assets/stylesheets/flash_message.css.sass b/app/assets/stylesheets/flash_message.css.sass
new file mode 100644
index 00000000..37c3c9c2
--- /dev/null
+++ b/app/assets/stylesheets/flash_message.css.sass
@@ -0,0 +1,2 @@
+.flash-message
+ margin-top: 20px
diff --git a/app/assets/stylesheets/home.css.sass b/app/assets/stylesheets/home.css.sass
new file mode 100644
index 00000000..6c68e889
--- /dev/null
+++ b/app/assets/stylesheets/home.css.sass
@@ -0,0 +1,195 @@
+// Place all the styles related to the home controller here.
+// They will automatically be included in application.css.
+// You can use Sass (SCSS) here: http://sass-lang.com/
+
+@import "compass/css3"
+@import "compass/typography/text/replacement"
+@import "oldsansblack"
+@import "same-height-columns"
+
+=verdana
+ font-family: Verdana, Geneva, sans-serif
+
+=oldsansblack
+ font-family: oldsansblack, Verdana, Geneva, sans-serif
+
+td.money, th.money
+ text-align: right
+
+h1:first-child
+ margin-top: 0
+
+
+=button($top-color, $bottom-color, $border, $text, $text-hover, $hover)
+ background: $bottom-color
+ +background(linear-gradient(180deg, $top-color, $bottom-color))
+ border-color: $border
+ color: $text
+
+ &:hover, &:active
+ +background($hover)
+ border-color: $hover
+ color: $text-hover
+
+.btn-default, .btn-primary, .btn-success
+ +button(#aadb44, #609c2a, #86ab46, white, #406f0e, #98cc3d)
+
+.btn-danger
+ +button(#d2322d, #ac2925, #ac2925, white, white, #ac2925)
+
+#main-logo
+ +replace-text-with-dimensions("logo.png")
+ +inline-block(middle)
+ margin: 10px 0
+ text-decoration: none
+
+#top-bar
+ background: #0a0a0a
+ +background(linear-gradient(180deg, #2c2c2c, #0a0a0a))
+ $link-color: white
+ $link-color-active: #94d317
+
+ a:not(.btn):not(#main-logo)
+ color: $link-color
+ &:hover, &:active
+ color: $link-color-active
+ background: transparent
+
+ #main-menu
+ text-align: right
+ padding-left: 210px
+ li
+ +inline-block(middle)
+ padding: 0 30px
+ &.active a
+ color: $link-color-active
+
+ #session-menu
+ float: right
+ padding-top: 16px
+ a
+ +inline-block(middle)
+ margin-left: 6px
+
+#main-menu
+ +inline-block(middle)
+
+body
+ +verdana
+ background: #f9f9f9
+ a
+ $base: #457f05
+ color: $base
+ &:hover, &:active
+ color: $base
+
+.jumbotron
+ background: #1c191f
+ background: #1c191f image-url("blackboard.jpg") center top repeat-y
+ text-align: center
+ color: white
+ padding: 50px 0
+ h1
+ text-transform: uppercase
+ font-size: 48px
+ font-weight: bold
+ +oldsansblack
+ margin: 50px 0
+
+#main-container
+ padding-top: 30px
+
+h1
+ +oldsansblack
+ font-size: 36px
+ color: #8bc139
+
+h2
+ +verdana
+ color: #111d04
+ font-size: 30px
+
+h3
+ +verdana
+ color: #599400
+ font-size: 18px
+
+h4
+ +verdana
+ color: #363636
+ font-size: 14px
+
+#home-page
+ .main-panel
+ .btn-primary
+ height: 60px
+ font-size: 18px
+ padding-top: 15px
+ +verdana
+
+ &:before
+ content: ""
+ display: block
+ width: 100px
+ height: 100px
+ +border-radius(100px)
+ margin: 10px auto
+
+ &.how-it-works:before
+ background: #98cc3d image-url("picto-books.png") center center no-repeat
+
+ &.raise:before
+ background: #98cc3d image-url("picto-plane.png") 44% 52% no-repeat
+
+ &.donate:before
+ background: #ffc000 image-url("picto-peerheart.png") 50% 54% no-repeat
+
+ &.contribute:before
+ background: #98cc3d image-url("picto-tasks.png") 50% 48% no-repeat
+
+ h2
+ text-align: center
+ font-size: 24px
+ +verdana
+ margin-top: 10px
+ p
+ font-size: 14px
+ text-align: center
+
+.panel.note-panel
+ background: image-url("panel-top.png") left top no-repeat
+ padding: 35px 14px 0px 3px
+ width: 334px
+ margin: 0px
+ border: none
+ margin-bottom: 10px
+ float: right
+ margin-right: -14px
+ +box-shadow(none)
+ .panel-heading
+ background: white
+ border: none
+ .panel-title
+ +verdana
+ font-size: 18px
+ .panel-body
+ background: white
+
+@media(min-width:992px)
+ #home-page
+ .main-panel
+ .panel-content
+ +box-sizing(border-box)
+ height: 160px
+ position: relative
+ padding-bottom: 70px
+ .button-container
+ position: absolute
+ bottom: 0
+ left: 15px
+ right: 15px
+
+#project-index
+ td, th
+ &.actions, &.amount
+ text-align: right
diff --git a/app/assets/stylesheets/home.css.scss b/app/assets/stylesheets/home.css.scss
deleted file mode 100644
index f0ddc684..00000000
--- a/app/assets/stylesheets/home.css.scss
+++ /dev/null
@@ -1,3 +0,0 @@
-// Place all the styles related to the home controller here.
-// They will automatically be included in application.css.
-// You can use Sass (SCSS) here: http://sass-lang.com/
diff --git a/app/assets/stylesheets/justified-nav.css b/app/assets/stylesheets/justified-nav.css
index 35e84503..a51fdd0b 100644
--- a/app/assets/stylesheets/justified-nav.css
+++ b/app/assets/stylesheets/justified-nav.css
@@ -1,7 +1,3 @@
-body {
- padding-top: 20px;
-}
-
.footer {
border-top: 1px solid #eee;
margin-top: 40px;
@@ -9,73 +5,6 @@ body {
padding-bottom: 40px;
}
-/* Main marketing message and sign up button */
-.jumbotron {
- text-align: center;
- background-color: transparent;
-}
-.jumbotron .btn {
- font-size: 21px;
- padding: 14px 24px;
-}
-
-/* Customize the nav-justified links to be fill the entire space of the .navbar */
-
-.nav-justified {
- background-color: #eee;
- border-radius: 5px;
- border: 1px solid #ccc;
-}
-.nav-justified > li > a {
- padding-top: 15px;
- padding-bottom: 15px;
- color: #777;
- font-weight: bold;
- text-align: center;
- border-bottom: 1px solid #d5d5d5;
- background-color: #e5e5e5; /* Old browsers */
- background-repeat: repeat-x; /* Repeat the gradient */
- background-image: -moz-linear-gradient(top, #f5f5f5 0%, #e5e5e5 100%); /* FF3.6+ */
- background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#f5f5f5), color-stop(100%,#e5e5e5)); /* Chrome,Safari4+ */
- background-image: -webkit-linear-gradient(top, #f5f5f5 0%,#e5e5e5 100%); /* Chrome 10+,Safari 5.1+ */
- background-image: -ms-linear-gradient(top, #f5f5f5 0%,#e5e5e5 100%); /* IE10+ */
- background-image: -o-linear-gradient(top, #f5f5f5 0%,#e5e5e5 100%); /* Opera 11.10+ */
- filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#f5f5f5', endColorstr='#e5e5e5',GradientType=0 ); /* IE6-9 */
- background-image: linear-gradient(top, #f5f5f5 0%,#e5e5e5 100%); /* W3C */
-}
-.nav-justified > .active > a,
-.nav-justified > .active > a:hover,
-.nav-justified > .active > a:focus {
- background-color: #ddd;
- background-image: none;
- box-shadow: inset 0 3px 7px rgba(0,0,0,.15);
-}
-.nav-justified > li:first-child > a {
- border-radius: 5px 5px 0 0;
-}
-.nav-justified > li:last-child > a {
- border-bottom: 0;
- border-radius: 0 0 5px 5px;
-}
-
-@media (min-width: 768px) {
- .nav-justified {
- max-height: 52px;
- }
- .nav-justified > li > a {
- border-left: 1px solid #fff;
- border-right: 1px solid #d5d5d5;
- }
- .nav-justified > li:first-child > a {
- border-left: 0;
- border-radius: 5px 0 0 5px;
- }
- .nav-justified > li:last-child > a {
- border-radius: 0 5px 5px 0;
- border-right: 0;
- }
-}
-
/* Responsive: Portrait tablets and up */
@media screen and (min-width: 768px) {
/* Remove the padding we set earlier */
@@ -85,4 +14,4 @@ body {
padding-left: 0;
padding-right: 0;
}
-}
\ No newline at end of file
+}
diff --git a/app/assets/stylesheets/oldsansblack-webfont.eot b/app/assets/stylesheets/oldsansblack-webfont.eot
new file mode 100644
index 00000000..17a103f7
Binary files /dev/null and b/app/assets/stylesheets/oldsansblack-webfont.eot differ
diff --git a/app/assets/stylesheets/oldsansblack-webfont.svg b/app/assets/stylesheets/oldsansblack-webfont.svg
new file mode 100644
index 00000000..0293653f
--- /dev/null
+++ b/app/assets/stylesheets/oldsansblack-webfont.svg
@@ -0,0 +1,1433 @@
+
+
+
\ No newline at end of file
diff --git a/app/assets/stylesheets/oldsansblack-webfont.ttf b/app/assets/stylesheets/oldsansblack-webfont.ttf
new file mode 100644
index 00000000..e25e5952
Binary files /dev/null and b/app/assets/stylesheets/oldsansblack-webfont.ttf differ
diff --git a/app/assets/stylesheets/oldsansblack-webfont.woff b/app/assets/stylesheets/oldsansblack-webfont.woff
new file mode 100644
index 00000000..1553cfb6
Binary files /dev/null and b/app/assets/stylesheets/oldsansblack-webfont.woff differ
diff --git a/app/assets/stylesheets/oldsansblack.sass b/app/assets/stylesheets/oldsansblack.sass
new file mode 100644
index 00000000..af931fe7
--- /dev/null
+++ b/app/assets/stylesheets/oldsansblack.sass
@@ -0,0 +1,8 @@
+/* Generated by Font Squirrel (http://www.fontsquirrel.com) on June 13, 2014
+
+@font-face
+ font-family: 'oldsansblack'
+ src: asset-url('oldsansblack-webfont.eot')
+ src: asset-url('oldsansblack-webfont.eot?#iefix') format("embedded-opentype"), asset-url('oldsansblack-webfont.woff') format("woff"), asset-url('oldsansblack-webfont.ttf') format("truetype"), asset-url('oldsansblack-webfont.svg#oldsansblackregular') format("svg")
+ font-weight: normal
+ font-style: normal
diff --git a/app/assets/stylesheets/projects.css.sass b/app/assets/stylesheets/projects.css.sass
new file mode 100644
index 00000000..708a8cca
--- /dev/null
+++ b/app/assets/stylesheets/projects.css.sass
@@ -0,0 +1,20 @@
+.commit-sha
+ font-family: monospace
+
+.qrcode
+ text-align: center
+
+.project-panel
+ overflow: auto
+
+ .bitcoin-address
+ word-wrap: break-word
+
+.donor-list
+ .amount
+ text-align: right
+
+ .txid abbr
+ font-variant: none
+ text-decoration: none
+ border-bottom: none
diff --git a/app/assets/stylesheets/projects.css.scss b/app/assets/stylesheets/projects.css.scss
deleted file mode 100644
index d0192666..00000000
--- a/app/assets/stylesheets/projects.css.scss
+++ /dev/null
@@ -1,3 +0,0 @@
-// Place all the styles related to the projects controller here.
-// They will automatically be included in application.css.
-// You can use Sass (SCSS) here: http://sass-lang.com/
diff --git a/app/assets/stylesheets/same-height-columns.sass b/app/assets/stylesheets/same-height-columns.sass
new file mode 100644
index 00000000..11abf445
--- /dev/null
+++ b/app/assets/stylesheets/same-height-columns.sass
@@ -0,0 +1,53 @@
+.container-xs-height
+ display: table
+ padding-left: 0px
+ padding-right: 0px
+
+.row-xs-height
+ display: table-row
+
+.col-xs-height
+ display: table-cell
+ float: none
+
+@media (min-width: 768px)
+ .container-sm-height
+ display: table
+ padding-left: 0px
+ padding-right: 0px
+ .row-sm-height
+ display: table-row
+ .col-sm-height
+ display: table-cell
+ float: none
+
+@media (min-width: 992px)
+ .container-md-height
+ display: table
+ padding-left: 0px
+ padding-right: 0px
+ .row-md-height
+ display: table-row
+ .col-md-height
+ display: table-cell
+ float: none
+
+@media (min-width: 1200px)
+ .container-lg-height
+ display: table
+ padding-left: 0px
+ padding-right: 0px
+ .row-lg-height
+ display: table-row
+ .col-lg-height
+ display: table-cell
+ float: none
+
+.col-top
+ vertical-align: top
+
+.col-middle
+ vertical-align: middle
+
+.col-bottom
+ vertical-align: bottom
diff --git a/app/assets/stylesheets/sessions.css.scss b/app/assets/stylesheets/sessions.css.sass
similarity index 100%
rename from app/assets/stylesheets/sessions.css.scss
rename to app/assets/stylesheets/sessions.css.sass
diff --git a/app/assets/stylesheets/typeahead.css.sass b/app/assets/stylesheets/typeahead.css.sass
new file mode 100644
index 00000000..d2105a3c
--- /dev/null
+++ b/app/assets/stylesheets/typeahead.css.sass
@@ -0,0 +1,41 @@
+.twitter-typeahead
+ .tt-query, .tt-hint
+ margin-bottom: 0
+
+.tt-dropdown-menu
+ min-width: 160px
+ margin-top: 2px
+ padding: 5px 0
+ background-color: #fff
+ border: 1px solid #ccc
+ border: 1px solid rgba(0, 0, 0, 0.2)
+ *border-right-width: 2px
+ *border-bottom-width: 2px
+ -webkit-border-radius: 6px
+ -moz-border-radius: 6px
+ border-radius: 6px
+ -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2)
+ -moz-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2)
+ box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2)
+ -webkit-background-clip: padding-box
+ -moz-background-clip: padding
+ background-clip: padding-box
+
+.tt-suggestion
+ display: block
+ padding: 3px 20px
+ cursor: pointer
+ &.tt-is-under-cursor, &:hover
+ color: #fff
+ background-color: #0081c2
+ background-image: -moz-linear-gradient(top, #0088cc, #0077b3)
+ background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0077b3))
+ background-image: -webkit-linear-gradient(top, #0088cc, #0077b3)
+ background-image: -o-linear-gradient(top, #0088cc, #0077b3)
+ background-image: linear-gradient(to bottom, #0088cc, #0077b3)
+ background-repeat: repeat-x
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc', endColorstr='#ff0077b3', GradientType=0)
+ a
+ color: #fff
+ p
+ margin: 0
diff --git a/app/assets/stylesheets/users.css.scss b/app/assets/stylesheets/users.css.sass
similarity index 79%
rename from app/assets/stylesheets/users.css.scss
rename to app/assets/stylesheets/users.css.sass
index 31a2eacb..1d6284eb 100644
--- a/app/assets/stylesheets/users.css.scss
+++ b/app/assets/stylesheets/users.css.sass
@@ -1,3 +1,7 @@
// Place all the styles related to the Users controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/
+
+#error_explanation
+ h2
+ font-size: 16px
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index d83690e1..1cfaa08e 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -2,4 +2,33 @@ class ApplicationController < ActionController::Base
# Prevent CSRF attacks by raising an exception.
# For APIs, you may want to use :null_session instead.
protect_from_forgery with: :exception
+
+ rescue_from CanCan::AccessDenied do |exception|
+ if request.xhr?
+ raise exception
+ else
+ redirect_to root_path, :alert => "Access denied"
+ end
+ end
+
+ before_filter :configure_permitted_parameters, if: :devise_controller?
+
+ before_filter :closed
+
+ protected
+ def after_sign_in_path_for(user)
+ params[:return_url].presence ||
+ session["user_return_to"].presence ||
+ root_path
+ end
+
+ def configure_permitted_parameters
+ devise_parameter_sanitizer.permit(:account_update, keys: [:email, :name, :bitcoin_address, :current_password, :password, :password_confirmation])
+ end
+
+ def closed
+ return if controller_name == "home" and action_name == "audit"
+
+ render "layouts/closed", status: 404
+ end
end
diff --git a/app/controllers/distributions_controller.rb b/app/controllers/distributions_controller.rb
new file mode 100644
index 00000000..da9d3829
--- /dev/null
+++ b/app/controllers/distributions_controller.rb
@@ -0,0 +1,100 @@
+class DistributionsController < ApplicationController
+ load_and_authorize_resource :project
+ load_and_authorize_resource :distribution, :through => :project
+
+ def index
+ @distributions = @distributions.order(created_at: :desc).page(params[:page]).per(30)
+ end
+
+ def new
+ end
+
+ def create
+ @distribution.project = @project
+ finalize_distribution
+ if @distribution.save
+ redirect_to [@project, @distribution], notice: "Distribution created"
+ else
+ render "new"
+ end
+ end
+
+ def edit
+ end
+
+ def update
+ @distribution.attributes = distribution_params
+ finalize_distribution
+ if @distribution.save
+ redirect_to [@project, @distribution], notice: "Distribution updated"
+ else
+ render "edit"
+ end
+ end
+
+ def show
+ commontator_thread_show(@distribution)
+ end
+
+ def send_transaction
+ @distribution.send_transaction!
+ redirect_to [@project, @distribution], flash: {notice: "Transaction sent"}
+ rescue RuntimeError => e
+ redirect_to [@project, @distribution], flash: {error: e.message}
+ end
+
+ def new_recipient_form
+ @tips = []
+ if params[:user] and params[:user][:nickname].present?
+ user = User.enabled.where(nickname: params[:user][:nickname]).first_or_initialize
+ if user.new_record?
+ raise "Invalid GitHub user" unless user.valid_github_user?
+ user.confirm
+ user.save!
+ end
+ @tips << Tip.new(user: user)
+ elsif params[:user] and params[:user][:identifier].present?
+ user = User.enabled.find_by(identifier: params[:user][:identifier])
+ @tips << Tip.new(user: user)
+ elsif params[:not_rewarded_commits]
+ @project.commits.each do |commit|
+ next if Tip.where(reason: commit).any?
+ next if Tip.where(commit: commit.sha).where.not(amount: nil).any?
+ tip = Tip.build_from_commit(commit)
+ @tips << tip if tip
+ end
+ elsif params[:commit] and sha = params[:commit][:sha]
+ commits = @project.commits.where("sha LIKE ?", "#{sha}%")
+ count = commits.count
+ raise "Commit not found" if count == 0
+ raise "Multiple commits match this prefix" if count > 1
+ commit = commits.first
+ @tips << Tip.build_from_commit(commit)
+ else
+ raise "Unrecognized recipient"
+ end
+ result = render_to_string(layout: false)
+ render json: {result: result}
+ rescue RuntimeError => e
+ render json: {error: e.message}
+ end
+
+ private
+
+ def distribution_params
+ if params[:distribution]
+ params.require(:distribution).permit(tips_attributes: [:id, :coin_amount, :user_id, :comment, :reason_type, :reason_id, :_destroy])
+ else
+ {}
+ end
+ end
+
+ def finalize_distribution
+ @distribution.tips.each do |tip|
+ tip.project = @project
+ if tip.user.new_record?
+ tip.user.skip_confirmation_notification!
+ end
+ end
+ end
+end
diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb
index 65a6314d..95f29929 100644
--- a/app/controllers/home_controller.rb
+++ b/app/controllers/home_controller.rb
@@ -1,48 +1,4 @@
class HomeController < ApplicationController
def index
end
-
- def blockchain_info_callback
- # todo: check if remote IP address belongs to blockchain.info
-
- if (params[:secret]!=CONFIG["blockchain_info"]["callback_secret"])
- render :text => "Invalid secret #{params}!"
- return
- end
-
- test = params[:test]
-
- if (params[:value].to_i < 0) || Sendmany.find_by_txid(params[:transaction_hash])
- render :text => "*ok*";
- return
- end
-
- if deposit = Deposit.find_by_txid(params[:transaction_hash])
- deposit.update_attribute(:confirmations, confirmations = params[:confirmations]) if !test
- if confirmations.to_i > 6
- render :text => "*ok*"
- else
- render :text => "Deposit #{deposit.id} updated!"
- end
- return
- end
-
- if project = Project.find_by_bitcoin_address(params[:input_address])
- (
- deposit = Deposit.create({
- project_id: project.id,
- txid: params[:transaction_hash],
- confirmations: params[:confirmations],
- amount: params[:value].to_i,
- duration: 30.days.to_i,
- paid_out: 0,
- paid_out_at: Time.now
- })
- ) if !test
- render :text => "Deposit #{deposit[:txid]} has been created!"
- else
- render :text => "Project with deposit address #{params[:input_address]} is not found!"
- end
- end
-
end
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 18c28caf..5481f80e 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -1,38 +1,152 @@
require 'net/http'
class ProjectsController < ApplicationController
+
+ before_action :load_project, only: [:qrcode, :edit, :update, :decide_tip_amounts]
+
+ load_and_authorize_resource only: [:commit_suggestions, :donate, :donors]
+
def index
- @projects = Project.order(available_amount_cache: :desc, watchers_count: :desc, full_name: :asc).page(params[:page]).per(30)
+ @projects = Project.enabled.order(available_amount_cache: :desc, watchers_count: :desc, full_name: :asc).page(params[:page]).per(30)
end
def show
- @project = Project.find params[:id]
- if @project && @project.bitcoin_address.nil?
- uri = URI("https://blockchain.info/merchant/#{CONFIG["blockchain_info"]["guid"]}/new_address")
- params = { password: CONFIG["blockchain_info"]["password"], label:"#{@project.full_name}@tip4commit" }
- uri.query = URI.encode_www_form(params)
- res = Net::HTTP.get_response(uri)
- if res.is_a?(Net::HTTPSuccess) && (bitcoin_address = JSON.parse(res.body)["address"])
- @project.update_attribute :bitcoin_address, bitcoin_address
- end
+ @project = Project.where(id: params[:id]).first
+ unless @project
+ redirect_to root_path, alert: "Project not found"
+ return
+ end
+ commontator_thread_show(@project)
+ end
+
+ def new
+ unless user_signed_in?
+ redirect_to new_user_session_path(return_url: request.original_url), flash: {info: "You must be logged in to create a new project"}
+ return
+ end
+ @project = Project.new(params[:project])
+ end
+
+ def edit
+ authorize! :update, @project
+ end
+
+ def update
+ authorize! :update, @project
+ @project.attributes = project_params
+ if @project.tipping_policies_text.try(:text_changed?)
+ @project.tipping_policies_text.user = current_user
+ end
+ if @project.save
+ redirect_to project_path(@project), notice: "The project has been updated"
+ else
+ render 'edit'
+ end
+ end
+
+ def decide_tip_amounts
+ authorize! :decide_tip_amounts, @project
+ if request.patch?
+ @project.attributes = params.require(:project).permit(tips_attributes: [:id, :decided_amount_percentage, :decided_free_amount])
+ @project.tips.each do |tip|
+ next if tip.decided?
+ if tip.decided_amount_percentage.present?
+ tip.amount = @project.available_amount * (tip.decided_amount_percentage.to_f / 100)
+ elsif tip.decided_free_amount.present?
+ tip.amount = tip.decided_free_amount.to_d * COIN
+ end
+ end
+ if @project.available_amount < 0
+ flash.now[:error] = "The project has insufficient funds"
+ return
+ end
+ if @project.save
+ message = "The tip amounts have been defined"
+ if @project.has_undecided_tips?
+ redirect_to decide_tip_amounts_project_path(@project), notice: message
+ else
+ redirect_to @project, notice: message
+ end
+ end
+ end
+ end
+
+ def qrcode
+ respond_to do |format|
+ format.svg { render qrcode: @project.bitcoin_address, level: :l, unit: 4 }
end
end
def create
- project_name = params[:full_name].
- gsub(/https?\:\/\/github.com\//, '').
- gsub(/\#.+$/, '').
- gsub(' ', '')
- client = Octokit::Client.new \
- :client_id => CONFIG['github']['key'],
- :client_secret => CONFIG['github']['secret']
- begin
- repo = client.repo project_name
- @project = Project.find_or_create_by full_name: repo.full_name
- @project.update_github_info repo
- redirect_to @project
- rescue Octokit::NotFound
- redirect_to projects_path, alert: "Project not found"
+ @project = Project.new(project_params)
+ @project.hold_tips = true
+ @project.collaborators.build(user: current_user)
+ authorize! :create, @project
+
+ if @project.save
+ redirect_to @project, notice: "The project was created"
+ else
+ render "new"
+ end
+ end
+
+ def commit_suggestions
+ respond_to do |format|
+ format.json do
+ query = params[:query]
+ commits = @project.commits.where('sha LIKE ? OR username LIKE ? OR message LIKE ?', "#{query}%", "%#{query}%", "%#{query}%")
+ commits = commits.map do |commit|
+ {
+ sha: commit.sha,
+ description: [
+ commit.sha[0,10],
+ commit.username.present? ? "@#{commit.username}" : nil,
+ ERB::Util.html_escape(commit.message).truncate(30),
+ ].reject(&:blank?).join(" "),
+ }
+ end
+ render json: commits
+ end
+ end
+ end
+
+ def github_user_suggestions
+ respond_to do |format|
+ format.json do
+ query = params[:query]
+ users = User.enabled.where.not(nickname: nil).where.not(nickname: '').where('nickname LIKE ? OR name LIKE ?', "%#{query}%", "%#{query}%")
+ users = users.map do |user|
+ {
+ login: user.nickname,
+ description: [
+ user.nickname,
+ user.name.present? ? "(#{user.name})" : nil,
+ ].reject(&:blank?).join(" "),
+ }
+ end
+ render json: users
+ end
+ end
+ end
+
+ def donate
+ if params[:donation_address]
+ sender_address = params[:donation_address][:sender_address].strip
+ @donation_address = @project.donation_addresses.where(sender_address: sender_address).first_or_create
+ else
+ @donation_address = @project.donation_addresses.build
+ end
+ end
+
+ private
+ def project_params
+ params.require(:project).permit(:name, :description, :detailed_description, :full_name, :auto_tip_commits, :hold_tips, tipping_policies_text_attributes: [:text])
+ end
+
+ def load_project
+ @project = Project.enabled.where(id: params[:id]).first
+ unless @project
+ redirect_to root_path, alert: "Project not found"
end
end
end
diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb
new file mode 100644
index 00000000..e0d95db9
--- /dev/null
+++ b/app/controllers/registrations_controller.rb
@@ -0,0 +1,22 @@
+class RegistrationsController < Devise::RegistrationsController
+ def update
+ @user = User.enabled.find(current_user.id)
+
+ user_params = devise_parameter_sanitizer.sanitize(:account_update)
+
+ successfully_updated = if @user.has_password?
+ @user.update_with_password(user_params)
+ else
+ @user.update(user_params)
+ end
+
+ if successfully_updated
+ set_flash_message :notice, :updated
+ # Sign in the user bypassing validation in case their password changed
+ bypass_sign_in @user
+ redirect_to after_update_path_for(@user)
+ else
+ render "edit"
+ end
+ end
+end
diff --git a/app/controllers/users/omniauth_callbacks_controller.rb b/app/controllers/users/omniauth_callbacks_controller.rb
index a3ffad82..7864b71d 100644
--- a/app/controllers/users/omniauth_callbacks_controller.rb
+++ b/app/controllers/users/omniauth_callbacks_controller.rb
@@ -2,21 +2,34 @@ class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
def github
# render text: "#{request.env["omniauth.auth"].to_json}"
info = request.env["omniauth.auth"]["info"]
- @user = User.find_by :email => info["email"]
+ @user = User.enabled.find_by :nickname => info["nickname"]
+ if @user.nil? and info["verified_emails"].any?
+ @user = User.enabled.find_by :email => info["verified_emails"]
+ end
unless @user
- generated_password = Devise.friendly_token.first(8)
- @user = User.create!(
- :email => info['email'],
- :password => generated_password,
- :nickname => info['nickname']
- )
+ if info['primary_email']
+ @user = User.new(
+ :email => info['primary_email'],
+ :nickname => info['nickname']
+ )
+ @user.confirm
+ @user.save!
+ else
+ set_flash_message(:error, :failure, kind: 'GitHub', reason: 'your primary email address should be verified.')
+ redirect_to new_user_session_path and return
+ end
end
- @user.name = info['name']
- @user.image = info['image']
+ @user.name ||= info['name']
+ @user.image ||= info['image']
@user.save
+
+ Collaborator.where(login: @user.nickname, user_id: nil).each do |collaborator|
+ collaborator.update(user: @user)
+ end
- sign_in_and_redirect @user, :event => :authentication
- set_flash_message(:notice, :success, :kind => "Github") if is_navigational_format?
+ sign_in(@user)
+ redirect_to request.env["omniauth.origin"].presence || after_sign_in_path_for(@user)
+ set_flash_message(:notice, :success, :kind => "GitHub") if is_navigational_format?
end
-end
\ No newline at end of file
+end
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index a1546bbd..8f17e192 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -1,40 +1,93 @@
class UsersController < ApplicationController
- before_action except: [:login, :index] do
- @user = User.find params[:id]
- unless current_user && current_user == @user
- redirect_to root_path
+ before_action except: [:show, :login, :index, :set_password_and_address, :suggestions] do
+ @user = User.enabled.where(id: params[:id]).first
+ if current_user
+ if current_user != @user
+ redirect_to root_path, alert: "Access denied"
+ end
+ else
+ redirect_to new_user_session_path(return_url: request.url)
end
end
def show
+ @user = User.find(params[:id])
+ commontator_thread_show(@user)
end
def index
- @users = User.order(:withdrawn_amount => :desc, :commits_count => :desc).where('commits_count > 0').page(params[:page]).per(30)
+ @users = User.order(withdrawn_amount: :desc, commits_count: :desc).where('commits_count > 0').page(params[:page]).per(30)
end
def update
- if @user.update_attributes(users_params)
- redirect_to @user, notice: 'Your information saved!'
+ if @user.update(users_params)
+ redirect_to @user, notice: 'Your information was saved.'
else
- render :show, alert: 'Error updating bitcoin address'
+ render :show, alert: 'Error updating peercoin address'
end
end
def login
- @user = User.find_by(login_token: params[:token])
+ @user = User.where(login_token: params[:token]).first
if @user
- sign_in_and_redirect @user, :event => :authentication
if params[:unsubscribe]
@user.update unsubscribed: true
- flash[:alert] = 'You unsubscribed! Sorry for bothering you. Although, you still can leave us your bitcoin address to get your tips.'
+ flash[:alert] = 'You unsubscribed! Sorry for bothering you. Although, you still can leave us your peercoin address to get your tips.'
end
+ sign_in_and_redirect @user, event: :authentication
else
redirect_to root_url, alert: 'User not found'
end
end
+ def send_tips_back
+ @user.tips.not_sent.non_refunded.each do |tip|
+ tip.touch :refunded_at
+ end
+ redirect_to @user, notice: 'All your tips have been refunded to their project'
+ end
+
+ def set_password_and_address
+ @user = User.enabled.find(params[:id])
+ raise "Blank token" if params[:token].blank?
+
+ if @user.confirmed?
+ redirect_to new_session_path(User), notice: "Your account is already confirmed. Please sign in to set your Peercoin address."
+ return
+ end
+
+ raise "Invalid token" unless Devise.secure_compare(params[:token], @user.confirmation_token)
+ if params[:user]
+ @user.attributes = params.require(:user).permit(:password, :password_confirmation, :bitcoin_address)
+ if @user.password.present? and @user.bitcoin_address.present? and @user.save
+ @user.confirm!
+ redirect_to root_path, notice: "Information saved"
+ else
+ flash.now[:alert] = "Please fill all the information"
+ end
+ end
+ end
+
+ def suggestions
+ respond_to do |format|
+ format.json do
+ query = params[:query]
+ users = User.enabled.where('identifier LIKE ? OR name LIKE ?', "%#{query}%", "%#{query}%")
+ users = users.map do |user|
+ {
+ identifier: user.identifier,
+ description: [
+ user.name,
+ "(#{user.identifier})",
+ ].reject(&:blank?).join(" "),
+ }
+ end
+ render json: users
+ end
+ end
+ end
+
private
def users_params
params.require(:user).permit(:bitcoin_address)
diff --git a/app/controllers/withdrawals_controller.rb b/app/controllers/withdrawals_controller.rb
deleted file mode 100644
index 8c9168e5..00000000
--- a/app/controllers/withdrawals_controller.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-class WithdrawalsController < ApplicationController
- def index
- @sendmanies = Sendmany.order(created_at: :desc).page(params[:page]).per(30)
- end
-end
\ No newline at end of file
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index d4b6fb29..b71b2a84 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -1,14 +1,73 @@
module ApplicationHelper
def btc_human amount, options = {}
+ return nil unless amount
+ options = (@default_btc_human_options || {}).merge(options)
nobr = options.has_key?(:nobr) ? options[:nobr] : true
currency = options[:currency] || false
- btc = "%.8f Ƀ" % to_btc(amount)
+ precision = options[:precision] || 2
+ display_currency = options.fetch(:display_currency, true)
+ btc = "%.#{precision}f" % to_btc(amount)
+ btc += " PPC" if display_currency
btc = "
#{btc}" if currency
btc = "
#{btc}" if nobr
btc.html_safe
end
+ def with_btc_human_defaults(defaults)
+ @old_btc_human_defaults ||= []
+ @old_btc_human_defaults << @default_btc_human_options
+ @default_btc_human_options = defaults.dup
+ yield
+ @default_btc_human_options = @old_btc_human_defaults.pop
+ end
+
def to_btc satoshies
- (1.0*satoshies.to_i/1e8)
+ satoshies.to_d / COIN if satoshies
+ end
+
+ def transaction_url(txid)
+ "https://peercoin.mintr.org/tx/#{txid}"
+ end
+
+ def address_explorers
+ [:mintr, :blockr]
+ end
+
+ def address_url(address, explorer = address_explorers.first)
+ case explorer
+ when :blockr then "http://ppc.blockr.io/address/info/#{address}"
+ when :mintr then "https://peercoin.mintr.org/address/#{address}"
+ else raise "Unknown provider: #{provider.inspect}"
+ end
+ end
+
+ def truncate_commit(sha1)
+ truncate(sha1, length: 10, omission: "")
+ end
+
+ def commit_tag(sha1)
+ content_tag(:span, truncate_commit(sha1), class: "commit-sha")
+ end
+
+ def render_flash_message
+ html = []
+ flash.each do |type, message|
+ alert_type = case type
+ when :notice then :success
+ when :alert, :error then :danger
+ else type
+ end
+ html << content_tag(:div, message, class: "flash-message text-center alert alert-#{alert_type}")
+ end
+ html.join("\n").html_safe
+ end
+
+ def render_markdown(source)
+ return nil unless source
+
+ markdown = Redcarpet::Markdown.new(Redcarpet::Render::HTML.new(safe_links_only: true, filter_html: true), autolink: true)
+ html = markdown.render(source)
+ clean = Sanitize.clean(html, Sanitize::Config::RELAXED)
+ clean.html_safe
end
end
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 091f154e..2de77218 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -2,7 +2,7 @@ module ProjectsHelper
def shield_btc_amount amount
btc_amount = to_btc amount
- "%.#{9 - btc_amount.to_i.to_s.length}f Ƀ" % btc_amount
+ "%.#{6 - btc_amount.to_i.to_s.length}f PPC" % btc_amount
end
def shield_color project
diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb
index e21ded23..6d4bb0da 100644
--- a/app/mailers/user_mailer.rb
+++ b/app/mailers/user_mailer.rb
@@ -7,4 +7,16 @@ def new_tip user, tip
mail to: user.email, subject: "You received a tip for your commit"
end
+
+ def security_issue(user)
+ @user = user
+ mail to: user.email, subject: "Security issue on peer4commit.com"
+ end
+
+ def address_request(tip, collaborator)
+ @collaborator = collaborator
+ @tip = tip
+ @user = tip.user
+ mail to: @user.email, subject: "[#{tip.project.name}] Provide an address to get your reward"
+ end
end
diff --git a/app/models/ability.rb b/app/models/ability.rb
new file mode 100644
index 00000000..a8fab4aa
--- /dev/null
+++ b/app/models/ability.rb
@@ -0,0 +1,18 @@
+class Ability
+ include CanCan::Ability
+
+ def initialize(user)
+ can [:read, :donate, :donors], Project
+ can :read, Distribution
+
+ if user
+ can [:update, :decide_tip_amounts, :commit_suggestions, :github_user_suggestions], Project, {collaborators: {user_id: user.id}}
+ can [:create], Project, {collaborators: {user_id: user.id}}
+ can [:create], Distribution, project: {collaborators: {user_id: user.id}}
+ can [:update, :new_recipient_form], Distribution, project: {collaborators: {user_id: user.id}}, txid: nil, sent_at: nil
+ can [:send_transaction], Distribution do |distribution|
+ distribution.can_be_sent?
+ end
+ end
+ end
+end
diff --git a/app/models/cold_storage_transfer.rb b/app/models/cold_storage_transfer.rb
new file mode 100644
index 00000000..73ca8ee7
--- /dev/null
+++ b/app/models/cold_storage_transfer.rb
@@ -0,0 +1,7 @@
+class ColdStorageTransfer < ActiveRecord::Base
+ belongs_to :project
+
+ def confirmed?
+ confirmations and confirmations >= 1
+ end
+end
diff --git a/app/models/collaborator.rb b/app/models/collaborator.rb
new file mode 100644
index 00000000..0bf3805d
--- /dev/null
+++ b/app/models/collaborator.rb
@@ -0,0 +1,4 @@
+class Collaborator < ActiveRecord::Base
+ belongs_to :project
+ belongs_to :user
+end
diff --git a/app/models/commit.rb b/app/models/commit.rb
new file mode 100644
index 00000000..b7e43f9d
--- /dev/null
+++ b/app/models/commit.rb
@@ -0,0 +1,5 @@
+class Commit < ActiveRecord::Base
+ belongs_to :project
+
+ validates :sha, uniqueness: {scope: :project_id}
+end
diff --git a/app/models/deposit.rb b/app/models/deposit.rb
index 237128f5..97ee2d8d 100644
--- a/app/models/deposit.rb
+++ b/app/models/deposit.rb
@@ -1,5 +1,6 @@
class Deposit < ActiveRecord::Base
belongs_to :project
+ belongs_to :donation_address, inverse_of: :deposits
def fee
(amount * CONFIG["our_fee"]).to_i
@@ -9,4 +10,4 @@ def available_amount
[amount - fee, 0].max
end
-end
\ No newline at end of file
+end
diff --git a/app/models/distribution.rb b/app/models/distribution.rb
new file mode 100644
index 00000000..1f06a5c6
--- /dev/null
+++ b/app/models/distribution.rb
@@ -0,0 +1,71 @@
+class Distribution < ActiveRecord::Base
+ belongs_to :project, inverse_of: :distributions
+ has_many :tips
+ accepts_nested_attributes_for :tips, allow_destroy: true
+
+ record_changes(include: :tips)
+
+ acts_as_commontable
+
+ validate :validate_funds
+
+ scope :to_send, -> { where(txid: nil) }
+ scope :error, -> { where(is_error: true) }
+
+ def sent?
+ sent_at or txid
+ end
+
+ def total_amount
+ tips.map(&:amount).compact.sum
+ end
+
+ def send_transaction!
+ Distribution.transaction do
+ lock!
+ raise "Already sent" if sent?
+ raise "Transaction already sent and failed" if is_error?
+ raise "Project disabled" if project.disabled?
+
+ update!(sent_at: Time.now, is_error: true) # it's a lock to prevent duplicates
+ end
+
+ data = generate_data
+ update_attribute(:data, data)
+
+ raise "Not enough funds on Distribution##{id}" if Project.find(project.id).available_amount < 0
+
+ txid = BitcoinDaemon.instance.send_many(project.address_label, JSON.parse(data))
+
+ update!(txid: txid, is_error: false)
+ end
+
+ def generate_data
+ outs = Hash.new { 0.to_d }
+ tips.each do |tip|
+ outs[tip.user.bitcoin_address] += tip.amount.to_d / COIN if tip.amount > 0
+ end
+ outs.to_json
+ end
+
+ def all_addresses_known?
+ tips.all? { |tip| tip.user.try(:bitcoin_address).present? }
+ end
+
+ def can_be_sent?
+ !sent? and all_addresses_known? and tips.any? and tips.all?(&:decided?)
+ end
+
+ def to_label
+ "##{id} on project #{project.to_label}"
+ end
+
+ def validate_funds
+ tips = project.tips.to_a
+ tips -= self.tips
+ tips += self.tips
+ if project.total_deposited < tips.reject(&:refunded?).map(&:amount).compact.sum
+ errors.add(:base, "Not enough funds")
+ end
+ end
+end
diff --git a/app/models/donation_address.rb b/app/models/donation_address.rb
new file mode 100644
index 00000000..c3c50381
--- /dev/null
+++ b/app/models/donation_address.rb
@@ -0,0 +1,15 @@
+class DonationAddress < ActiveRecord::Base
+ belongs_to :project, inverse_of: :donation_addresses
+ has_many :deposits, inverse_of: :donation_address
+
+ validates :sender_address, bitcoin_address: true, presence: true
+ validates :donation_address, bitcoin_address: true
+
+ before_create :generate_donation_address
+
+ def generate_donation_address
+ return if donation_address.present?
+ raise "The project has no address label" if project.address_label.blank?
+ self.donation_address = BitcoinDaemon.instance.get_new_address(project.address_label)
+ end
+end
diff --git a/app/models/project.rb b/app/models/project.rb
index 2e5db9cd..62168c70 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -1,53 +1,95 @@
class Project < ActiveRecord::Base
has_many :deposits # todo: only confirmed deposits that have amount > paid_out
- has_many :tips
+ has_many :tips, inverse_of: :project
+ accepts_nested_attributes_for :tips
+ has_many :collaborators
+ has_many :distributions, inverse_of: :project
+ has_many :donation_addresses, inverse_of: :project
+ has_many :users, through: :collaborators
- def update_github_info repo
- self.name = repo.name
- self.full_name = repo.full_name
- self.source_full_name = repo.source.full_name rescue ''
- self.description = repo.description
- self.watchers_count = repo.watchers_count
- self.language = repo.language
- self.save!
- end
+ has_many :cold_storage_transfers
+
+ has_one :tipping_policies_text, inverse_of: :project
+ accepts_nested_attributes_for :tipping_policies_text
+
+ has_many :commits
+
+ record_changes(except: [:available_amount_cache, :last_commit, :updated_at])
+
+ acts_as_commontable
+
+ validates :name, presence: true
+
+ before_validation :strip_full_name
+ after_create :generate_address!
+
+ scope :enabled, -> { where(disabled: false) }
+ scope :disabled, -> { where(disabled: true) }
def github_url
- "https://github.com/#{full_name}"
+ "https://github.com/#{full_name}" if full_name.present?
end
def source_github_url
"https://github.com/#{source_full_name}"
end
- def new_commits
+ def get_commits
+ return [] if full_name.blank? or full_name !~ %r{\A[-\w.]+/[-\w.]+\Z}
begin
commits = Timeout::timeout(90) do
client = Octokit::Client.new \
:client_id => CONFIG['github']['key'],
:client_secret => CONFIG['github']['secret'],
:per_page => 100
- client.commits(full_name).
- # Filter merge request
- select{|c| !(c.commit.message =~ /^(Merge\s|auto\smerge)/)}.
- # Filter fake emails
- select{|c| c.commit.author.email =~ Devise::email_regexp }.
- # Filter commited after t4c project creation
- select{|c| c.commit.committer.date > self.deposits.first.created_at }.
- to_a
+ client.commits(full_name)
end
rescue Octokit::BadGateway, Octokit::NotFound, Octokit::InternalServerError,
- Errno::ETIMEDOUT, Net::ReadTimeout, Faraday::Error::ConnectionFailed => e
+ Errno::ETIMEDOUT, Faraday::Error::ConnectionFailed, Octokit::Forbidden,
+ Octokit::Conflict, Octokit::ClientError => e
Rails.logger.info "Project ##{id}: #{e.class} happened"
rescue StandardError => e
- Airbrake.notify(e)
+ if CONFIG["airbrake"]
+ Airbrake.notify(e)
+ else
+ raise
+ end
end
sleep(1)
commits || []
end
+ def update_commits
+ commits = get_commits
+
+ commits.each do |commit|
+ Commit.where(
+ project: self,
+ sha: commit.sha,
+ ).first_or_create!(
+ message: commit.commit.message,
+ username: commit.author.try(:login),
+ email: commit.commit.author.email,
+ )
+ end
+ end
+
def tip_commits
- new_commits.each do |commit|
+ return unless self.deposits.any?
+ return if available_amount == 0
+
+ commits = get_commits
+
+ commits.each do |commit|
+ next if Tip.where(project_id: id, commit: commit.sha).any?
+
+ # Filter merge request
+ next if commit.commit.message =~ /^(Merge\s|auto\smerge)/
+ # Filter fake emails
+ next unless commit.commit.author.email =~ Devise::email_regexp
+ # Filter commited after t4c project creation
+ next unless commit.commit.committer.date > self.deposits.first.created_at
+
Project.transaction do
tip_for commit
update_attribute :last_commit, commit.sha
@@ -57,44 +99,42 @@ def tip_commits
def tip_for commit
email = commit.commit.author.email
- user = User.find_by email: email
-
- if (next_tip_amount > 0) &&
- Tip.find_by_commit(commit.sha).nil?
-
- # create user
- unless user
- generated_password = Devise.friendly_token.first(8)
- user = User.create({
- email: email,
- password: generated_password,
- name: commit.commit.author.name
- })
+ if nickname = commit.author.try(:login)
+ user = User.enabled.find_by(nickname: nickname)
+ end
+ user ||= User.enabled.find_by(email: email)
+
+ if (next_tip_amount > 0) and
+ Tip.find_by(commit: commit.sha).nil? and
+ user
+
+ if hold_tips
+ amount = nil
+ else
+ amount = next_tip_amount
end
# create tip
- tip = Tip.create({
- project: self,
+ tip = tips.create!({
user: user,
- amount: next_tip_amount,
- commit: commit.sha
+ amount: amount,
+ commit: commit.sha,
+ commit_message: ActionController::Base.helpers.truncate(commit.commit.message, length: 100),
})
- # notify user
- if tip && user.bitcoin_address.blank? && !user.unsubscribed
- if !user.notified_at || (user.notified_at < (Time.now - 30.days))
- UserMailer.new_tip(user, tip).deliver
- user.touch :notified_at
- end
- end
+ tip.notify_user
Rails.logger.info " Tip created #{tip.inspect}"
end
end
+
+ def total_deposited
+ self.deposits.where("confirmations > 0").map(&:available_amount).sum
+ end
def available_amount
- self.deposits.where("confirmations > 0").map(&:available_amount).sum - tips_paid_amount
+ total_deposited - tips_paid_amount
end
def unconfirmed_amount
@@ -102,7 +142,7 @@ def unconfirmed_amount
end
def tips_paid_amount
- self.tips.non_refunded.sum(:amount)
+ self.tips.select(&:decided?).reject(&:refunded?).sum(&:amount)
end
def tips_paid_unclaimed_amount
@@ -119,4 +159,73 @@ def self.update_cache
end
end
+ def tips_to_pay
+ tips.select(&:to_pay?)
+ end
+
+ def amount_to_pay
+ tips_to_pay.sum(&:amount)
+ end
+
+ def has_undecided_tips?
+ tips.undecided.any?
+ end
+
+ def commit_url(commit)
+ "https://github.com/#{full_name}/commit/#{commit}"
+ end
+
+ def cold_storage_amount
+ cold_storage_transfers.to_a.select(&:confirmed?).sum(&:amount)
+ end
+
+ def send_to_cold_storage!(amount, address_index = 0)
+ address = CONFIG["cold_storage"].try(:[], "addresses").try(:[], address_index)
+ raise "No cold storage address" if address.blank?
+ BitcoinDaemon.instance.send_many(address_label, {address => amount.to_f})
+ end
+
+ def paid_fee
+ [
+ distributions.map(&:fee),
+ cold_storage_transfers.map(&:fee),
+ ].flatten.compact.sum
+ end
+
+ def strip_full_name
+ if full_name_changed? and full_name.present?
+ self.full_name = full_name.gsub(/https?\:\/\/github.com\//, '')
+ end
+ end
+
+ def github?
+ full_name.present?
+ end
+
+ def auto_tip_commits
+ !hold_tips
+ end
+ alias_method :auto_tip_commits?, :auto_tip_commits
+
+ def auto_tip_commits=(value)
+ self.hold_tips = case value
+ when false, nil, "0" then true
+ else false
+ end
+ end
+
+ def generate_address!
+ return if bitcoin_address.present? or address_label.present?
+ self.address_label = "peer4commit-#{id}"
+ self.bitcoin_address = BitcoinDaemon.instance.get_new_address(address_label)
+ save(validate: false)
+ end
+
+ def to_label
+ name.presence || id.to_s
+ end
+
+ def not_sent_distributions_amount
+ distributions.select { |d| d.is_error or !d.sent? }.map(&:tips).flatten.map(&:amount).compact.sum
+ end
end
diff --git a/app/models/record_change.rb b/app/models/record_change.rb
new file mode 100644
index 00000000..569aee96
--- /dev/null
+++ b/app/models/record_change.rb
@@ -0,0 +1,3 @@
+class RecordChange < ActiveRecord::Base
+ belongs_to :record, polymorphic: true
+end
diff --git a/app/models/sendmany.rb b/app/models/sendmany.rb
deleted file mode 100644
index a0f46092..00000000
--- a/app/models/sendmany.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-class Sendmany < ActiveRecord::Base
- def send_transaction
- return if txid || is_error
-
- update_attribute :is_error, true # it's a lock to prevent duplicates
- uri = URI("https://blockchain.info/merchant/#{CONFIG["blockchain_info"]["guid"]}/sendmany")
- params = { password: CONFIG["blockchain_info"]["password"], recipients: data }
- uri.query = URI.encode_www_form(params)
- res = Net::HTTP.get_response(uri)
- if res.is_a?(Net::HTTPSuccess) && (json = JSON.parse(res.body))
- Rails.logger.info res.body
- update_attribute :result, json
- if !(txid = json["tx_hash"]).blank?
- update_attribute :is_error, false
- update_attribute :txid, json["tx_hash"]
- end
- else
- Rails.logger.error "Failed to get correct response from blockchain.info"
- end
- end
-end
diff --git a/app/models/tip.rb b/app/models/tip.rb
index 06ca7fe6..c1226f37 100644
--- a/app/models/tip.rb
+++ b/app/models/tip.rb
@@ -1,23 +1,72 @@
class Tip < ActiveRecord::Base
belongs_to :user
- belongs_to :sendmany
- belongs_to :project
+ belongs_to :distribution, touch: true
+ belongs_to :project, inverse_of: :tips
+ belongs_to :reason, polymorphic: true
- validates :amount, :numericality => { :greater_than => 0 }
+ validate :validate_amount_is_positive
+ validate :validate_reason
- scope :unpaid, -> { non_refunded.
- where(sendmany_id: nil) }
+ scope :not_sent, -> { where(distribution_id: nil) }
+ def not_sent?
+ distribution_id.nil?
+ end
+
+ scope :unpaid, -> { non_refunded.not_sent }
+ def unpaid?
+ non_refunded? and not_sent?
+ end
+
+ scope :to_pay, -> { unpaid.decided.not_free.with_address }
+ def to_pay?
+ unpaid? and decided? and !free? and with_address?
+ end
- scope :paid, -> { where('sendmany_id is not ?', nil) }
+ scope :free, -> { where('amount = 0') }
+ scope :not_free, -> { where('amount > 0') }
+ def free?
+ amount == 0
+ end
- scope :refunded, -> { where('refunded_at is not ?', nil) }
+ scope :paid, -> { where.not(distribution_id: nil) }
+ def paid?
+ !!distribution_id
+ end
+ scope :refunded, -> { where.not(refunded_at: nil) }
scope :non_refunded, -> { where(refunded_at: nil) }
+ def refunded?
+ !!refunded_at
+ end
+ def non_refunded?
+ !refunded?
+ end
scope :unclaimed, -> { joins(:user).
unpaid.
where('users.bitcoin_address' => ['', nil]) }
+ scope :with_address, -> { joins(:user).where.not('users.bitcoin_address' => [nil, ""]) }
+ def with_address?
+ user.present? and user.bitcoin_address.present?
+ end
+
+ scope :decided, -> { where.not(amount: nil) }
+ scope :undecided, -> { where(amount: nil) }
+ def decided?
+ !!amount
+ end
+ def undecided?
+ !decided?
+ end
+ def was_undecided?
+ amount_was.nil?
+ end
+
+
+ after_save :notify_user_if_just_decided
+
+
def self.refund_unclaimed
unclaimed.non_refunded.
where('tips.created_at < ?', Time.now - 1.month).
@@ -25,4 +74,64 @@ def self.refund_unclaimed
tip.touch :refunded_at
end
end
+
+ def commit_url
+ project.commit_url(commit)
+ end
+
+ attr_accessor :decided_amount_percentage
+ attr_accessor :decided_free_amount
+
+ def notify_user
+ if amount and amount > 0 and user and user.bitcoin_address.blank? and !user.unsubscribed
+ if user.notified_at.nil? or user.notified_at < 30.days.ago
+ UserMailer.new_tip(user, self).deliver_now
+ user.touch :notified_at
+ end
+ end
+ end
+
+ def notify_user_if_just_decided
+ return if distribution_id
+ notify_user if amount_was.nil? and amount
+ end
+
+ def coin_amount
+ amount.to_f / COIN if amount
+ end
+
+ def coin_amount=(coin_amount)
+ if coin_amount.present?
+ self.amount = (coin_amount.to_f * COIN).round
+ else
+ self.amount = nil
+ end
+ end
+
+ def self.build_from_commit(commit)
+ if commit.username.present?
+ user = User.enabled.where(nickname: commit.username).first
+ elsif commit.email =~ Devise::email_regexp
+ user = User.enabled.where(email: commit.email).first
+ end
+ return nil unless user
+ new(user_id: user.id, reason: commit)
+ end
+
+ def validate_reason
+ case reason_type
+ when nil, ""
+ errors.add(:reason_id, :present) unless reason_id.blank?
+ when "Commit"
+ errors.add(:reason_id, :invalid) unless project.commits.include?(reason)
+ else
+ errors.add(:reason_type, :invalid)
+ end
+ end
+
+ def validate_amount_is_positive
+ if amount and amount < 0
+ errors.add(:amount, "must be positive")
+ end
+ end
end
diff --git a/app/models/tipping_policies_text.rb b/app/models/tipping_policies_text.rb
new file mode 100644
index 00000000..eaf490f2
--- /dev/null
+++ b/app/models/tipping_policies_text.rb
@@ -0,0 +1,4 @@
+class TippingPoliciesText < ActiveRecord::Base
+ belongs_to :project
+ belongs_to :user
+end
diff --git a/app/models/user.rb b/app/models/user.rb
index 940d185c..b7c05860 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -5,40 +5,135 @@ class User < ActiveRecord::Base
:rememberable, :trackable, :validatable
devise :omniauthable, :omniauth_providers => [:github]
+ devise :confirmable, reconfirmable: true
- validates :bitcoin_address, :bitcoin_address => true
+ validates :bitcoin_address, bitcoin_address: true
+ validates :password, confirmation: true
has_many :tips
+ has_many :collaborators
+ has_many :projects, through: :collaborators
+
+ has_many :tipping_policies_texts
+ has_many :record_changes
+
+ before_create :generate_login_token!, unless: :login_token?
+ before_create :assign_random_identifier, unless: :identifier?
+
+ acts_as_commontator
+ acts_as_commontable
+
+ scope :enabled, -> { where(disabled: false) }
+ scope :disabled, -> { where(disabled: true) }
+
def github_url
"https://github.com/#{nickname}"
end
def balance
- tips.unpaid.sum(:amount)
+ tips.unpaid.sum(:amount)
end
- after_create :generate_login_token!
- def generate_login_token!
- if login_token.blank?
- self.update login_token: SecureRandom.urlsafe_base64
+ def full_name
+ name.presence || nickname.presence || email
+ end
+
+ def self.update_cache
+ commits_counts = Tip.group(:user_id).count
+ paid_sums = Tip.paid.group(:user_id).sum(:amount)
+
+ find_each do |user|
+ user.commits_count = commits_counts[user.id] || 0
+ user.withdrawn_amount = paid_sums[user.id] || 0
+ user.save
end
end
- def full_name
- if !name.blank?
- name
- elsif !nickname.blank?
+ def has_password?
+ encrypted_password_was.present?
+ end
+
+ def password_required?
+ false
+ end
+
+ def email_required?
+ false
+ end
+
+ def recipient_label
+ if nickname.present?
nickname
+ elsif identifier.present?
+ identifier
+ elsif email.present?
+ if new_record?
+ "#{email} (new user)"
+ else
+ email
+ end
else
- email
+ "Unknown user"
end
end
- def self.update_cache
- find_each do |user|
- user.update commits_count: user.tips.count
- user.update withdrawn_amount: user.tips.paid.sum(:amount)
+ def valid_github_user?
+ return false unless nickname.present?
+
+ client = Octokit::Client.new(client_id: CONFIG['github']['key'], client_secret: CONFIG['github']['secret'])
+ begin
+ client.user(nickname)
+ true
+ rescue Octokit::NotFound
+ false
+ end
+ end
+
+ def reset_confirmation_token!
+ generate_confirmation_token!
+ end
+
+ def merge_into!(other)
+ raise unless id
+ raise unless other.id
+
+ self.class.transaction do
+ logger.info "Merging #{inspect} into user #{other.inspect}"
+ [
+ :collaborators,
+ :tipping_policies_texts,
+ :record_changes,
+ :tips,
+ ].each do |association|
+ send(association).each do |record|
+ logger.info "Updating user id from #{record.user_id} to #{other.id} on #{record.inspect}"
+ record.update_columns(user_id: other.id)
+ end
+ end
+ update_attribute(:disabled, true)
end
end
+
+ def active_for_authentication?
+ super and !disabled?
+ end
+
+ private
+
+ def generate_login_token!
+ loop do
+ self.login_token = SecureRandom.urlsafe_base64
+ break unless User.exists?(login_token: login_token)
+ end
+ end
+
+ def assign_random_identifier
+ charset = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'.split(//)
+ loop do
+ self.identifier = (0...12).map { charset.sample }.join
+ break unless User.exists?(identifier: identifier)
+ end
+ end
+
end
diff --git a/app/views/common/_menu.html.haml b/app/views/common/_menu.html.haml
index aefe1c39..f4ecc24d 100644
--- a/app/views/common/_menu.html.haml
+++ b/app/views/common/_menu.html.haml
@@ -1,9 +1,10 @@
-%ul.nav.nav-justified
- %li{class: controller_name == 'home' ? 'active' : ''}
- %a{href: root_path} Home
- %li{class: controller_name == 'projects' || @project ? 'active' : ''}
- %a{href: projects_path} Supported Projects
- / %li
- / %a{href: "#"} About
- / %li
- / %a{href: "#"} Contact
\ No newline at end of file
+%nav#main-menu
+ %ul.nav
+ %li{class: controller_name == 'home' ? 'active' : ''}
+ %a{href: root_path} Home
+ %li{class: controller_name == 'projects' || @project ? 'active' : ''}
+ %a{href: projects_path} Projects
+ / %li
+ / %a{href: "#"} About
+ / %li
+ / %a{href: "#"} Contact
diff --git a/app/views/devise/confirmations/new.html.haml b/app/views/devise/confirmations/new.html.haml
new file mode 100644
index 00000000..2c52b90c
--- /dev/null
+++ b/app/views/devise/confirmations/new.html.haml
@@ -0,0 +1,7 @@
+= twitter_bootstrap_form_for(resource, :as => resource_name, :url => confirmation_path(resource_name), :html => { :method => :post, class: 'form-devise' }) do |f|
+ %h2 Resend confirmation instructions
+ = devise_error_messages!
+ = f.email_field :email, :autofocus => true
+ = f.submit "Resend confirmation instructions"
+ %p
+ = render "devise/shared/links"
diff --git a/app/views/devise/mailer/confirmation_instructions.html.haml b/app/views/devise/mailer/confirmation_instructions.html.haml
new file mode 100644
index 00000000..dfa5989a
--- /dev/null
+++ b/app/views/devise/mailer/confirmation_instructions.html.haml
@@ -0,0 +1,4 @@
+%p
+ Welcome #{@email}!
+%p You can confirm your account email through the link below:
+%p= link_to 'Confirm my account', confirmation_url(@resource, :confirmation_token => @token)
diff --git a/app/views/devise/mailer/reset_password_instructions.html.haml b/app/views/devise/mailer/reset_password_instructions.html.haml
new file mode 100644
index 00000000..eda27ee3
--- /dev/null
+++ b/app/views/devise/mailer/reset_password_instructions.html.haml
@@ -0,0 +1,6 @@
+%p
+ Hello #{@resource.email}!
+%p Someone has requested a link to change your password. You can do this through the link below.
+%p= link_to 'Change my password', edit_password_url(@resource, :reset_password_token => @token)
+%p If you didn't request this, please ignore this email.
+%p Your password won't change until you access the link above and create a new one.
diff --git a/app/views/devise/mailer/unlock_instructions.html.haml b/app/views/devise/mailer/unlock_instructions.html.haml
new file mode 100644
index 00000000..47f55a54
--- /dev/null
+++ b/app/views/devise/mailer/unlock_instructions.html.haml
@@ -0,0 +1,5 @@
+%p
+ Hello #{@resource.email}!
+%p Your account has been locked due to an excessive number of unsuccessful sign in attempts.
+%p Click the link below to unlock your account:
+%p= link_to 'Unlock my account', unlock_url(@resource, :unlock_token => @token)
diff --git a/app/views/devise/passwords/edit.html.haml b/app/views/devise/passwords/edit.html.haml
new file mode 100644
index 00000000..6dbf4a25
--- /dev/null
+++ b/app/views/devise/passwords/edit.html.haml
@@ -0,0 +1,9 @@
+= twitter_bootstrap_form_for(resource, :as => resource_name, :url => password_path(resource_name), :html => { :method => :put, class: 'form-devise' }) do |f|
+ %h2 Change your password
+ = devise_error_messages!
+ = f.hidden_field :reset_password_token
+ = f.password_field :password, :autofocus => true
+ = f.password_field :password_confirmation
+ = f.submit "Change my password"
+ %p
+ = render "devise/shared/links"
diff --git a/app/views/devise/passwords/new.html.haml b/app/views/devise/passwords/new.html.haml
new file mode 100644
index 00000000..24dbea80
--- /dev/null
+++ b/app/views/devise/passwords/new.html.haml
@@ -0,0 +1,7 @@
+= twitter_bootstrap_form_for(resource, :as => resource_name, :url => password_path(resource_name), :html => { :method => :post, class: 'form-devise', role: 'form' }) do |f|
+ %h2 Forgot your password?
+ = devise_error_messages!
+ = f.email_field :email, :autofocus => true
+ = f.submit "Send me reset password instructions"
+ %p
+ = render "devise/shared/links"
diff --git a/app/views/devise/registrations/edit.html.haml b/app/views/devise/registrations/edit.html.haml
new file mode 100644
index 00000000..049139c1
--- /dev/null
+++ b/app/views/devise/registrations/edit.html.haml
@@ -0,0 +1,33 @@
+.form-devise
+ = bootstrap_form_for(resource, :as => resource_name, :url => registration_path(resource_name), :html => { :method => :put, class: 'form-devise' }) do |f|
+ %h2
+ Edit #{resource_name.to_s.humanize}
+ = devise_error_messages!
+ = f.static_control :identifier
+ = f.text_field :name
+ = f.email_field :email
+ - if devise_mapping.confirmable? && resource.pending_reconfirmation?
+ %div
+ Currently waiting confirmation for: #{resource.unconfirmed_email}
+
+ = f.text_field :bitcoin_address, placeholder: 'Your peercoin address'
+ %div
+ = f.password_field :password, autocomplete: 'off', help: "(leave blank if you don't want to change it)"
+ %div
+ = f.password_field :password_confirmation, autocomplete: 'off'
+ - if f.object.has_password?
+ %div
+ = f.password_field :current_password, help: "(we need your current password to confirm your changes)"
+ %div= f.submit "Update", class: 'btn btn-primary btn-block'
+
+ - if @user.balance > 0
+ %h3 Send tips back
+ .send-tips-back-block
+ %p
+ If you don't want the tips, you can send the funds back to the supported projects:
+ = button_to "Send all my tips back to their project", send_tips_back_user_path(@user), class: "btn btn-danger btn-block", confirm: "All the #{to_btc @user.balance} peercoins you received will be sent back to their project. Are you sure?"
+
+ %h3 Cancel my account
+ %p
+ Unhappy? #{button_to "Cancel my account", registration_path(resource_name), :data => { :confirm => "Are you sure?" }, :method => :delete, class: 'btn btn-danger btn-block'}
+ = link_to "Back", :back
diff --git a/app/views/devise/registrations/new.html.haml b/app/views/devise/registrations/new.html.haml
new file mode 100644
index 00000000..90e17fd6
--- /dev/null
+++ b/app/views/devise/registrations/new.html.haml
@@ -0,0 +1,9 @@
+= twitter_bootstrap_form_for(resource, :as => resource_name, :url => registration_path(resource_name), html: { class: 'form-devise'}) do |f|
+ %h2 Sign up
+ = devise_error_messages!
+ = f.email_field :email, :autofocus => true
+ = f.password_field :password
+ = f.password_field :password_confirmation
+ = f.submit "Sign up"
+ %p
+ = render "devise/shared/links"
diff --git a/app/views/devise/sessions/new.html.haml b/app/views/devise/sessions/new.html.haml
new file mode 100644
index 00000000..a38ec0bd
--- /dev/null
+++ b/app/views/devise/sessions/new.html.haml
@@ -0,0 +1,20 @@
+#sign-in-form
+ .row
+ .col-md-4
+ %h4 Sign in with your email
+ = twitter_bootstrap_form_for(resource, :as => resource_name, :url => session_path(resource_name)) do |f|
+ = hidden_field_tag :return_url, params[:return_url]
+ = f.email_field :email, :autofocus => true
+ = f.password_field :password
+ - if devise_mapping.rememberable?
+ %div
+ = f.check_box :remember_me, "Remember me"
+ = f.submit "Sign in", class: 'btn btn-primary btn-block'
+ .col-md-4
+ - if devise_mapping.omniauthable?
+ %h4 Sign in with a provider
+ - resource_class.omniauth_providers.each do |provider|
+ = link_to "Sign in with #{provider.to_s.titleize}", omniauth_authorize_path(resource_name, provider, origin: params[:return_url]), class: "btn btn-primary btn-block", method: :post
+ .col-md-4
+ %h4 Other options
+ = render "devise/shared/links"
diff --git a/app/views/devise/shared/_links.haml b/app/views/devise/shared/_links.haml
new file mode 100644
index 00000000..1573dbe5
--- /dev/null
+++ b/app/views/devise/shared/_links.haml
@@ -0,0 +1,15 @@
+- if controller_name != 'sessions'
+ = link_to "Sign in", new_session_path(resource_name)
+ %br/
+- if devise_mapping.registerable? && controller_name != 'registrations'
+ = link_to "Sign up", new_registration_path(resource_name)
+ %br/
+- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations'
+ = link_to "Forgot your password?", new_password_path(resource_name)
+ %br/
+- if devise_mapping.confirmable? && controller_name != 'confirmations'
+ = link_to "Didn't receive confirmation instructions?", new_confirmation_path(resource_name)
+ %br/
+- if devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) && controller_name != 'unlocks'
+ = link_to "Didn't receive unlock instructions?", new_unlock_path(resource_name)
+ %br/
diff --git a/app/views/devise/unlocks/new.html.haml b/app/views/devise/unlocks/new.html.haml
new file mode 100644
index 00000000..d3c87d07
--- /dev/null
+++ b/app/views/devise/unlocks/new.html.haml
@@ -0,0 +1,9 @@
+%h2 Resend unlock instructions
+= form_for(resource, :as => resource_name, :url => unlock_path(resource_name), :html => { :method => :post }) do |f|
+ = devise_error_messages!
+ %div
+ = f.label :email
+ %br/
+ = f.email_field :email, :autofocus => true
+ %div= f.submit "Resend unlock instructions"
+= render "devise/shared/links"
diff --git a/app/views/distributions/_form.html.haml b/app/views/distributions/_form.html.haml
new file mode 100644
index 00000000..16abe353
--- /dev/null
+++ b/app/views/distributions/_form.html.haml
@@ -0,0 +1,52 @@
+.row
+ .col-md-12
+ = bootstrap_form_for [@project, @distribution], html: {id: "distribution-form"} do |f|
+ - if (errors = f.object.errors[:base]).any?
+ .alert.alert-danger
+ Unable to save the distribution: #{errors.join(", ")}
+ %table.table
+ %thead
+ %th Recipient
+ %th Reason
+ %th Amount
+ %th
+ %tbody#recipients
+ - f.object.tips.each do |tip|
+ = render "tip_form", tip: tip, form: f
+ .text-center
+ = f.submit "Save the distribution", class: 'btn btn-primary'
+#add-recipient-panels
+ .row
+ .col-md-12
+ %label{for: "add-recipients-input"} Add recipient(s)
+ .row
+ .col-md-3
+ .panel.panel-default
+ .panel-heading
+ %h3.panel-title Peer4commit user
+ .panel-body
+ .input-group
+ = bootstrap_form_for User.new, url: new_recipient_form_project_distributions_path(@project) do |f|
+ = f.text_field :identifier, hide_label: true, append: content_tag(:button, "Add", class: "btn btn-default add-recipient-button"), class: "user-autocomplete", data: {submit: true}
+ .col-md-3
+ .panel.panel-default
+ .panel-heading
+ %h3.panel-title GitHub user
+ .panel-body
+ = bootstrap_form_for User.new, url: new_recipient_form_project_distributions_path(@project) do |f|
+ = f.text_field :nickname, hide_label: true, append: content_tag(:button, "Add", class: "btn btn-default add-recipient-button"), class: "github-user-autocomplete", data: {project_id: @project.id, submit: true}
+ .col-md-3
+ .panel.panel-default
+ .panel-heading
+ %h3.panel-title Author of a commit
+ .panel-body
+ = bootstrap_form_for Commit.new, url: new_recipient_form_project_distributions_path(@project) do |f|
+ = f.text_field :sha, hide_label: true, append: content_tag(:button, "Add", class: "btn btn-default add-recipient-button"), class: "commit-autocomplete", data: {project_id: @project.id, submit: true}
+ .col-md-3
+ .panel.panel-default
+ .panel-heading
+ %h3.panel-title Authors of commits
+ .panel-body
+ = bootstrap_form_for User.new, url: new_recipient_form_project_distributions_path(@project) do |f|
+ = hidden_field_tag :not_rewarded_commits, "1"
+ %button.btn-block.btn.btn-default Commits not rewarded
diff --git a/app/views/distributions/_reason.html.haml b/app/views/distributions/_reason.html.haml
new file mode 100644
index 00000000..5fa9f93a
--- /dev/null
+++ b/app/views/distributions/_reason.html.haml
@@ -0,0 +1,15 @@
+- case tip.reason
+- when Commit
+ - commit = tip.reason
+ Commit #{link_to truncate_commit(commit.sha), "https://github.com/#{commit.project.full_name}/commit/#{commit.sha}"}:
+ %pre= commit.message
+- when nil
+ - if tip.commit.present?
+ - if tip.project
+ Commit #{link_to truncate_commit(tip.commit), "https://github.com/#{tip.project.full_name}/commit/#{tip.commit}"}:
+ - else
+ Commit #{tip.commit}
+ - if tip.commit_message.present?
+ %pre= tip.commit_message
+ - else
+ = render_markdown tip.comment
diff --git a/app/views/distributions/_tip_form.html.haml b/app/views/distributions/_tip_form.html.haml
new file mode 100644
index 00000000..af03720a
--- /dev/null
+++ b/app/views/distributions/_tip_form.html.haml
@@ -0,0 +1,24 @@
+- index = "#{(Time.now.to_f * 1000000).round}#{SecureRandom.random_number(1_000_000).to_s.rjust(6, '0')}"
+= form.fields_for :tips, tip, child_index: index do |fields|
+ - user = tip.user
+ - raise "An user is required" unless user
+ %tr
+ %td.recipient
+ = fields.hidden_field :id unless tip.new_record?
+ - if user.new_record?
+ = fields.fields_for :user do |user_fields|
+ = user_fields.hidden_field :email
+ = user_fields.hidden_field :nickname
+ = user.recipient_label
+ - else
+ = fields.hidden_field :user_id
+ = link_to user.recipient_label, user
+ %td.reason
+ - if tip.reason
+ = fields.hidden_field :reason_type
+ = fields.hidden_field :reason_id
+ = render "reason", tip: tip
+ - else
+ = fields.text_area :comment, hide_label: true, rows: 2
+ %td.amount= fields.text_field :coin_amount, hide_label: true, append: "PPC"
+ %td.remove= fields.check_box :_destroy
diff --git a/app/views/distributions/edit.html.haml b/app/views/distributions/edit.html.haml
new file mode 100644
index 00000000..b1bc3ba0
--- /dev/null
+++ b/app/views/distributions/edit.html.haml
@@ -0,0 +1 @@
+= render "form"
diff --git a/app/views/distributions/index.html.haml b/app/views/distributions/index.html.haml
new file mode 100644
index 00000000..974d7590
--- /dev/null
+++ b/app/views/distributions/index.html.haml
@@ -0,0 +1,22 @@
+%h1 Distributions
+- if @project
+ %h2= @project.name
+%p
+ %table.table
+ %thead
+ %tr
+ %th Created At
+ %th Sent at
+ %th.text-right Amount
+ %th Transaction
+ %th Result
+ %th
+ %tbody
+ - @distributions.each do |distribution|
+ %tr
+ %td= l distribution.created_at
+ %td= l distribution.sent_at if distribution.sent_at
+ %td.text-right= btc_human distribution.total_amount
+ %td= link_to truncate(distribution.txid), transaction_url(distribution.txid), target: '_blank' if distribution.txid.present?
+ %td= distribution.is_error ? "Error" : "Success" if distribution.sent?
+ %td= link_to "Details", [distribution.project, distribution], class: "btn btn-success"
diff --git a/app/views/distributions/new.html.haml b/app/views/distributions/new.html.haml
new file mode 100644
index 00000000..b1bc3ba0
--- /dev/null
+++ b/app/views/distributions/new.html.haml
@@ -0,0 +1 @@
+= render "form"
diff --git a/app/views/distributions/new_recipient_form.html.haml b/app/views/distributions/new_recipient_form.html.haml
new file mode 100644
index 00000000..98d7da52
--- /dev/null
+++ b/app/views/distributions/new_recipient_form.html.haml
@@ -0,0 +1,6 @@
+- output = nil
+- bootstrap_form_for Distribution.new do |f|
+ - output = capture_haml do
+ - @tips.each do |tip|
+ = render "tip_form", form: f, tip: tip
+= output
diff --git a/app/views/distributions/show.html.haml b/app/views/distributions/show.html.haml
new file mode 100644
index 00000000..bf99a0f8
--- /dev/null
+++ b/app/views/distributions/show.html.haml
@@ -0,0 +1,58 @@
+#distribution-show-page
+ - total = @distribution.tips.map(&:amount).sum if @distribution.tips.all?(&:amount)
+ %table.table
+ %thead
+ %tr
+ %th Recipient
+ %th Reason
+ %th Address
+ %th Amount
+ %th Percentage
+ %tbody
+ - @distribution.tips.each do |tip|
+ %tr
+ %td.recipient
+ - if tip.user
+ - if tip.user.new_record?
+ = tip.user.recipient_label
+ - else
+ = link_to tip.user.recipient_label, tip.user
+ - else
+ Nobody
+ %td.reason= render "reason", tip: tip
+ %td.address
+ - if tip.user.try(:bitcoin_address).present?
+ = tip.user.bitcoin_address
+ %td.amount
+ - if tip.amount
+ = btc_human tip.amount
+ - else
+ %em Undecided
+ %td.percentage= number_to_percentage(tip.amount.to_f * 100 / total, precision: 1) if total and tip.amount and total > 0
+
+ - if total
+ %p
+ %strong
+ Total amount: #{btc_human total}
+
+ - if @distribution.is_error?
+ %p.alert.alert-danger
+ The transaction failed.
+ - elsif @distribution.sent?
+ %p.alert.alert-success
+ Transaction sent
+ - if @distribution.sent_at
+ on #{l(@distribution.sent_at)}
+ - elsif !@distribution.all_addresses_known?
+ %p.alert.alert-warning
+ The transaction cannot be sent because some addresses are missing. Ask the recipients to sign in and provide an address.
+ .distribution-actions
+ - if can? :update, @distribution
+ .distribution-action
+ = link_to "Edit the distribution", edit_project_distribution_path(@project, @distribution), class: "btn btn-default"
+ - if total and can? :send_transaction, @distribution
+ .distribution-action
+ = button_to "Send the transaction", send_transaction_project_distribution_path(@project, @distribution), class: "btn btn-danger", data: {confirm: "#{total.to_f / COIN} peercoins will be sent. Are you sure?"}
+
+%hr
+= commontator_thread(@distribution)
diff --git a/app/views/home/audit.html.haml b/app/views/home/audit.html.haml
new file mode 100644
index 00000000..8a6254ac
--- /dev/null
+++ b/app/views/home/audit.html.haml
@@ -0,0 +1,81 @@
+- cache_time = 1.minute
+- cache("audit-#{Time.now.to_i / cache_time}") do
+ %h1 Audit
+ %p
+ Some cells may take up to #{distance_of_time_in_words eval CONFIG['tipper_delay']} to be updated.
+
+ - projects = Project.order(disabled: :asc, account_balance: :desc).includes(:cold_storage_transfers, :distributions, :tips, :deposits)
+ - with_btc_human_defaults(precision: 0, display_currency: false) do
+ %p
+ %table.table
+ - projects.each_slice(15) do |project_slice|
+ %thead
+ %tr
+ %th Project
+ %th Address
+ %th.money
+ %abbr{title: "Total amount that was donated to the project."} Donated
+ %th.money
+ %abbr{title: "#{CONFIG["our_fee"]*100}% Peer4commit maintenance fee, used to pay maintenance, hosting and transaction fees."} Peer4commit Fee
+ %th.money
+ %abbr{title: "The balance displayed on the website. The tip amounts are based on this value. This balance includes tips refunded to the project."} Available balance
+ %th.money
+ %abbr{title: "Tips attributed but not sent because they have not been processed yet or because the author did not set an address."} Tips not sent
+ %th.money
+ %abbr{title: "Transaction fee paid when tips are sent."} Transaction fee
+ %th.money
+ %abbr{title: "Amount currently in cold storage (see below)."} Amount in cold storage
+ %th.money
+ %abbr{title: "Distributions that were not actually processed, either because they failed or because they were not validated"} Not sent
+ %th.money
+ %abbr{title: "Available balance + tips not sent - amount in cold storage + fee - transaction fee."} Expected account balance
+ %th.money
+ %abbr{title: "The balance of the project account as reported by the Peercoin daemon."} Account balance
+ %th.money
+ %abbr{title: "If it is different than 0 there is an issue."} Difference
+ %tbody
+ - project_slice.each do |project|
+ %tr{class: project.disabled ? "text-muted" : nil}
+ %td
+ %strong= link_to project.name, project
+ %td
+ - address_explorers.each_with_index do |explorer, i|
+ = link_to "[#{i + 1}]", address_url(project.bitcoin_address, explorer)
+ %td.money= btc_human(donated = project.deposits.map(&:amount).sum)
+ %td.money= btc_human(fee = donated - project.deposits.map(&:available_amount).sum)
+ %td.money= btc_human(available = project.available_amount_cache)
+ %td.money= btc_human(unpaid = project.tips.select(&:unpaid?).map(&:amount).compact.sum)
+ %td.money= btc_human(txfee = project.paid_fee, precision: 2)
+ %td.money= btc_human(cold = project.cold_storage_amount)
+ %td.money= btc_human(not_sent = project.not_sent_distributions_amount)
+ %td.money= btc_human(expected = available + unpaid - cold + fee - txfee + not_sent) if available and unpaid and cold and fee and txfee
+ %td.money= btc_human(account = project.account_balance - (project.stake_mint_amount || 0))
+ %td.money= btc_human(account - expected, precision: 2) if account and expected
+ %tbody
+ %tr
+ %th{colspan: 2} Total
+ %td.money= btc_human(donated = projects.map(&:deposits).flatten.map(&:amount).sum)
+ %td.money= btc_human(fee = donated - projects.map(&:deposits).flatten.map(&:available_amount).sum)
+ %td.money= btc_human(available = projects.map(&:available_amount_cache).compact.sum)
+ %td.money= btc_human(unpaid = projects.map(&:tips).flatten.select(&:unpaid?).map(&:amount).compact.sum)
+ %td.money= btc_human(txfee = projects.map(&:paid_fee).compact.sum, precision: 2)
+ %td.money= btc_human(cold = projects.map(&:cold_storage_amount).compact.sum)
+ %td.money= btc_human(not_sent = projects.map(&:not_sent_distributions_amount).compact.sum)
+ %td.money= btc_human(expected = available + unpaid - cold + fee - txfee + not_sent) if available and unpaid and cold and fee and txfee
+ %td.money= btc_human(account = projects.map(&:account_balance).compact.sum - projects.map(&:stake_mint_amount).compact.sum)
+ %td.money= btc_human(account - expected, precision: 2) if account and expected
+
+ %h2 Cold storage
+ %p
+ %table.table
+ %thead
+ %tr
+ %th Address
+ %th Explorers
+ %tbody
+ - (CONFIG["cold_storage"].try(:[], "addresses") || []).each do |address|
+ %tr
+ %td= address
+ %td
+ - address_explorers.each_with_index do |explorer, i|
+ = link_to "[#{i + 1}]", address_url(address, explorer)
diff --git a/app/views/home/faq.html.md b/app/views/home/faq.html.md
new file mode 100644
index 00000000..ed706427
--- /dev/null
+++ b/app/views/home/faq.html.md
@@ -0,0 +1,163 @@
+Peer4commit FAQ
+===============
+
+What is Peer4commit?
+--------------------
+Peer4commit is a website where people raise funds for any kind of project.
+
+Anyone can start collecting funds in a few clicks. You just have to explain what you're going to do with the funds and why people can trust you.
+
+People can easily donate to the projects they want to support by sending cryptocurrencies. The fundraiser can then distribute the funds to the people who make the project happen.
+
+How is it different than other crowdfunding sites?
+--------------------------------------------------
+On Peer4commit the fundraiser is not expected to do the actual work. His job is to collect funds and distribute them to the people who work on the project. He may for example reward commits on an open source project or pay a lobbyist.
+
+Why?
+----
+The goal is give the initiative to the people, and not only to those who are able to achieve projects.
+
+For example imagine you want a feature in an open source software but the developers don't care about it and you can't implement it yourself. You can start a project on Peer4commit to make it happen. If you get enough funds you can pay someone to implement the feature. There are many freelance developers and companies who will be happy to do that for you.
+
+Or imagine you favorite band has given up. You can raise funds to pay them to work on a new album.
+
+
+Why give control to a fundraiser and not directly to the people?
+----------------------------------------------------------------
+Giving direct control to multiple persons is a complex task with no ideal solution. For example are decisions made at the majority? Is there a quorum? Can people vote for multiple choices? Are votes proportional to the amount you gave?
+
+And there is a big risk. For example if all decisions are made proportional to the amount given, then someone can easily take over the funds by sending more than everybody else. He can then send the whole funds to himself.
+
+So we decided all these choices will be made by the fundraiser. He can start a project where he decides everything as a benevolent dictator, but he can also create more democratic projects where important decisions will be taken at the majority among choices he selects.
+
+The fundraiser will also have the responsibility of deciding what can and cannot be changed. For example the main goal of the project is not likely to change. Even if you give a small amount and have an insignificant weight in the decisions you can rest assured that your money will still be used for the same goal. Someone who gave 90% of the funds won't be able to change that.
+
+
+Why use Peer4commit and not send funds directly to the fundraiser?
+------------------------------------------------------------------
+
+Peer4commit provides tools to the donors and the fundraisers.
+
+The donors can browse the fundraiser history: what projects he managed, how he distributed funds, what comments he received, etc. The fundraisers are forced to distribute the funds through Peer4commit so that anyone can see the details. For example if the fundraiser decided to send money to a GitHub account, then the GitHub account name will be displayed, not only the payment address.
+
+The fundraisers have tools to easily distribute money. They can for example send funds to an email address. Peer4commit will take charge of getting payment information from the owner of this email address. They can also send funds to a GitHub users or to the author of a particular commit. More options will be added later (sending to a Reddit user, to a forum member, etc.).
+
+We also provide tools to identify donors. When they donate they can provide a address. This address can be used whenever Peer4commit, the fundraiser or anyone wants to identify a donor. Anyone able to sign with this address will be considered the sender of the associated money. This can be used to return the funds, to organize votes, to send rewards, etc.
+
+
+Can I trust the fundraisers?
+----------------------------
+
+Not blindly. You should do some researches before you give money.
+
+For example the fact a project has a lot of donations is not an indication that many people trust the fundraiser. He may have sent the money himself.
+
+Peer4commit provides some tools to help you check the fundraiser. We keep track of all the projects he managed and all the funds he distributed. You can browse that and see how he managed previous projects. Anyone can comment the projects, distributions and users so if he did something wrong then there are good chances he received bad comments.
+
+The fundraiser should also explain in the project description why you can trust him. If he doesn't do that you should be skeptical. Then you'll have to evaluate what he says. It's particularly important to check the identity of the fundraiser. He should provide proofs he really is who he claims to be.
+
+But an important point is that the fundraisers will actually compete on trust. Since anyone can raise funds a big part of the difference will be made on trustworthiness.
+
+Some other skills may be important too. For example the project may require some technical skills to evaluate the work made by others. You should ensure the fundraiser has these abilities.
+
+Refund
+------
+When you make a donation you explicitly give to the fundraiser the full control of the money you send. He may have committed himself to refund the donations under certain circumstances but you still need to evaluate whether you can trust him on that.
+
+When you donate Peer4commit asks you for a return address. This address will be used if the fundraiser wants to send funds back to you. It may also be used for other things where you need to prove you are the sender (a vote, a reward, etc.). All the donation addresses will be displayed publicly so you should use a newly generated address without history. And you must keep the associated private key in a safe place.
+
+Does it work?
+-------------
+Peer4commit is still young but yes. Some projects were successfully managed.
+
+We initially started as a [tip4commit](http://tip4commit.com/) clone where GitHub commits were automatically rewarded 1% of the balance. We moved recently to the more generic system described here.
+
+Examples of successful projects:
+* [Peer4commit](http://peer4commit.com/projects/1) itself
+* [A Peercoin marketing video](http://peer4commit.com/projects/68)
+* [Peerunity](http://peer4commit.com/projects/74)
+
+Currency
+--------
+For now the only supported currency is [Peercoin](http://peercoin.net/). Other currencies will be added later.
+
+
+How can I raise funds?
+----------------------
+Click on the <%= link_to '"Create a project"', new_project_path %> button. You'll have to fill a detailed description. Here are some recommendations:
+
+* State the main goal of your peer4commit project, and decide what can and cannot be changed for the project. The main goal cannot be changed.
+* Provide your identity and convince donators why they should trust you.
+* Decide and state if you will be a benevolent dictator (more efficient), or create a more democratic project (finer control for stakeholders).
+* Describe your techical skills and other relevant qualifications if they are needed to evaluate the work made by others.
+* State policy of tipping for potential developers. Use other projects as templates or references.
+
+The project will be visible on Peer4commit but it will not be particularly highlighted. You will have to communicate about it, that's part of your job.
+
+
+Can fundraisers get paid for their work?
+----------------------------------------
+Yes. A fundraisers can send money to himself for his raising and distributing jobs. He can also reward himself to do actual work. That's up to him.
+
+Fundraisers should explain in the project description whether they intend to pay themselves and how much.
+
+
+How can I get paid to do the actual work?
+-----------------------------------------
+Check the policies of the projects. Fundraiser can chose to raise bounty or a specific percentage of the donated funds for specific tasks or assign a percentage of the fund for commits in case of a development. If unclear ask the fundraiser. Disputes should be resolved between the developer and the fundraiser.
+
+
+How do I donate to a project I like?
+------------------------------------
+Browse the project list and click on the "Donate" button. You will be asked for a personal address that will be used if Peer4commit or the fundraiser ever needs to identify you. Then Peer4commit will give you an address to which you just have to send Peercoins. 99% of your donation will be available to the fundraiser. 1% will be kept to host Peer4commit and pay the transaction fees.
+
+You can also donate without providing an address. But the fundraiser won't be able to return you the funds if he ever wants to. And if the fundraiser organizes a vote or send rewards, you won't be able to participate.
+
+What's going to happen next?
+----------------------------
+There are many features planed. Their achievement depends on the willingness of donors, fundraisers and developers.
+
+### Bitcoin support
+Adding support for Bitcoin is an important step. We can easily change Peer4commit to support projects either in Bitcoin or in Peercoin. But supporting multiple currecies in the same project will require more work and an external service to automatically convert currencies.
+
+### Multi-signature
+The most important imminent change is the introduction of multi-signature donation addresses:
+
+When you donate, your money will be sent to a multi-signature address:
+* 1 key will be owned by Peer4commit,
+* 1 key will be owned by the fundraiser and
+* 1 key will be owned by yourself (if you want to).
+
+And 2 keys will be required to use the funds.
+
+To distribute coins the fundraiser will use the website and fill some forms. The website will generate a transaction and ask the fundraiser to provide a signature for it. Then the website will sign the transaction too and propagate it.
+
+So if the website is hacked the funds can't be stolen. And the fundraiser cannot spend the funds outside the website.
+
+Also if a fundraiser is clearly misbehaving the website and the supporter can decide to return the funds. If the website itself is misbehaving (a very bad policy change, abandoned, hacked...) the fundraiser and the supporter can decide to move the funds elsewhere.
+
+### Decentralization
+Peer4commit can probably run completely decentralized, maybe on its own blockchain, maybe as a [Peershares](http://peershares.net/) implementation. But this will require a lot of thoughts. For now we focus on more practical things, but decentralization is certainly an ultimate goal.
+
+Multi-signature will already be a big step toward decentralization.
+
+### Other
+We will also improve the various tools provided by the website:
+* add new recipients the fundraisers can distribute funds to: all the people involved in a GitHub issue, another Peer4commit project, a Reddit user, etc.
+* project categorization, tags, sorting, filtering, etc.
+* browsing the history of projects (description changes, distribution changes, etc). The data are already there but just not displayed.
+* etc.
+
+We may also add some kind of discussion boards, unless the community thinks this is better kept externalized.
+
+
+What measures have been taken to secure the funds on Peer4commit?
+-----------------------------------------------------------------
+The project funds are isolated in different accounts in the wallet, so if someone ever finds a way to distribute more funds than the project balance, Peercoin will not take the funds from another project and will refuse the transaction. Projects with a high balance have a part of its funds moved to cold storage. The website runs in an isolated virtual server running only this service. There's an <%= link_to "audit page", audit_path %> that shows the status of all project accounts.
+
+When multisignatures are implemented Peer4commit will not have direct the control over the funds (see above).
+
+Contact
+-------
+If you have any question send a message to <%= mail_to "contact@peer4commit.com" %> or [open an issue on GitHub](https://github.com/sigmike/peer4commit/issues/new).
+
diff --git a/app/views/home/index.html.haml b/app/views/home/index.html.haml
index 8b8862a7..1455191f 100644
--- a/app/views/home/index.html.haml
+++ b/app/views/home/index.html.haml
@@ -1,49 +1,41 @@
-/ Jumbotron
-.jumbotron
- %h1 Contribute to Open Source
- %p.lead Donate bitcoins to open source projects or make commits and get tips for it.
- / - if current_user
- / You are logged in as
- / - unless current_user.image.blank?
- / = image_tag(current_user.image, width: 26)
- / = link_to current_user.name, current_user.github_url
- / %p
- / \/
- / Your balance
- / = btc_human current_user.balance
- / \/
- / = link_to current_user.bitcoin_address.blank? ? 'Please set your Bitcoin address to receive tips!' : 'Change bitcoin address', current_user
- / \/
- / = link_to 'Sign Out', destroy_user_session_path, method: :delete
- / \/
- / - else
- / / %a.btn.btn-lg.btn-success{href: user_omniauth_authorize_path(:github)} See projects
+- content_for :main_content do
+ #home-page
+ / Jumbotron
+ .jumbotron
+ .container
+ %h1 Make anything happen
+ %p.lead
- %a.btn.btn-lg.btn-success{href: projects_path} See projects
+ %a.btn.btn-lg.btn-success{href: projects_path} See projects
-/ Example row of columns
-.row
- .col-lg-4
- %h2 How it works?
- %p People donate bitcoins to projects. When someone's commit is accepted to the project repository, we automatically tip the author.
- %p
- %a.btn.btn-primary{href: "https://weusecoins.com/", target: '_blank'} Learn about Bitcoin »
- .col-lg-4
- %h2 Donate
- %p
- Find a project you like and deposit bitcoins to it. Your money will be accumulated with money of other donators to tip for new commits.
- %p
- %a.btn.btn-primary{href: projects_path} Find or add a project »
- .col-lg-4
- %h2 Contribute
- %p
- Go and fix something! If your commit is accepted by project maintainer, you will get a tip!
- - if !current_user
- Just check your email or
- %a{href: user_omniauth_authorize_path(:github)} Sign In.
- %p
- %a.btn.btn-primary{href: projects_path} Supported projects »
-/ - if current_user
-/ %a.btn.btn-primary{href: user_path(current_user)} Change your bitcoin address »
-/ - else
-/ %a.btn.btn-primary{href: user_omniauth_authorize_path(:github)} Sign In »
\ No newline at end of file
+ .container.container-md-height
+ / Example row of columns
+ .row.row-md-height
+ .col-lg-3.col-md-height.col-top.main-panel.donate
+ .panel-content
+ %h2 Donate
+ %p
+ Donate to projects to make them happen.
+ .button-container
+ %a.btn.btn-primary.btn-block{href: projects_path} See projects
+ .col-lg-3.col-md-height.col-top.main-panel.contribute
+ .panel-content
+ %h2 Contribute
+ %p
+ Get paid to contribute to a project.
+ .button-container
+ %a.btn.btn-primary.btn-block{href: projects_path} See projects
+ .col-lg-3.col-md-height.col-top.main-panel.raise
+ .panel-content
+ %h2 Raise funds
+ %p
+ Make something happen by raising funds and distributing them.
+ .button-container
+ %a.btn.btn-primary.btn-block{href: new_project_path} Create a project
+ .col-lg-3.col-md-height.col-top.main-panel.how-it-works
+ .panel-content
+ %h2 How it works?
+ %p
+ Fundraisers collect funds and distribute them to contributors.
+ .button-container
+ %a.btn.btn-primary.btn-block{href: faq_path} FAQ
diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml
index 64202c07..2cad8c64 100644
--- a/app/views/layouts/application.html.haml
+++ b/app/views/layouts/application.html.haml
@@ -5,58 +5,55 @@
%meta{content: "width=device-width, initial-scale=1.0", name: "viewport"}/
%meta{content: "", name: "description"}/
%meta{content: "", name: "author"}/
- %link{href: "/favicon.png", rel: "shortcut icon"}/
-
- %title= "Tip4Commit — " + (content_for?(:title) ? yield(:title) : "Contribute to Open Source")
+ %link{href: image_path("ppcoin.png"), rel: "shortcut icon"}/
- %meta{:name => 'description', :content => (content_for?(:title) ? yield(:title) : "Donate bitcoins to open source projects or make commits and get tips for it.")}
- %meta{:name => 'keywords', :content => 'open source,cotribute,github,community,git,bitcoin,tips,perks'}
+ %title= content_for?(:title) ? yield(:title) : "Peer4commit"
+
+ %meta{name: 'description', content: (content_for?(:title) ? yield(:title) : "Donate peercoins to open source projects or make commits and get tips for it.")}
+ %meta{name: 'keywords', content: 'open source,contribute,github,community,git,bitcoin,peercoin,ppc,tips,perks'}
/ %meta{:property => 'og:image', :content => asset_path('logo.png')}
/ %link{:rel => 'image_src', :type => 'image/png', :href => asset_path('logo.png')}
- = stylesheet_link_tag "application", media: "all", "data-turbolinks-track" => true
- = javascript_include_tag "application", "data-turbolinks-track" => true
-
+ = stylesheet_link_tag "application", media: "all", data: { "turbolinks-track" => true }
+ = javascript_include_tag "application", data: { "turbolinks-track" => true }
+
+ /[if lt IE 9]
+ %script{:src => "https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"}
+ %script{:src => "https://oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js"}
+
+
+
= csrf_meta_tags
- %body
- :javascript
- (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
- (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','//www.google-analytics.com/analytics.js','ga');
+ %body{data: {environment: Rails.env}}
+ - if Rails.env.production?
+ :javascript
+ (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
+ (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','//www.google-analytics.com/analytics.js','ga');
- ga('create', 'UA-1115214-16', 'tip4commit.com');
- ga('send', 'pageview');
- .container
- .masthead
- %div.pull-right
- %small
- - if current_user
- = current_user.full_name
- \/
- = link_to btc_human(current_user.balance), current_user
- \/
- = link_to 'Sign Out', destroy_user_session_path, method: :delete
- - else
- %a{href: user_omniauth_authorize_path(:github)} Sign in
- %h3.text-muted.code-pro Tip4Commit
- = render 'common/menu'
- - if flash[:alert]
- %br
- .alert= flash[:alert]
- - if flash[:notice]
- %br
- .alert.alert-info= flash[:notice]
- = yield
- / Site footer
- .footer
- %p
- ©
- = link_to 'Tip4commit', 'http://tip4commit.com/', target: '_blank'
- 2013-2014. Source code is available at #{link_to('github', 'https://github.com/tip4commit/tip4commit', target: '_blank')}, you can
- = link_to('support', 'http://tip4commit.com/projects/307')
- its development.
- = link_to 'Follow @tip4commit', 'https://twitter.com/tip4commit', target: '_blank'
+ ga('create', 'UA-11108334-6', 'peer4commit.com');
+ ga('send', 'pageview');
+ #top-bar
+ .container
+ %a#main-logo{href: root_path}
+ %h3 Peer4commit
+ = render_flash_message
+ #main-content
+ - if content_for?(:main_content)
+ = yield :main_content
+ - else
+ .container#main-container
+ = yield
+ #footer
+ .container
+ / Site footer
+ .footer
+ %p
+ ©
+ = link_to 'Peer4commit', 'http://peer4commit.com/', target: '_blank'
+ 2014-2019. Source code is available at #{link_to('github', 'https://github.com/sigmike/peer4commit', target: '_blank')},
+ based on #{link_to "Tip4commit", "http://tip4commit.com/"}.
/ /container
/
Bootstrap core JavaScript
diff --git a/app/views/layouts/closed.html.haml b/app/views/layouts/closed.html.haml
new file mode 100644
index 00000000..fd971da0
--- /dev/null
+++ b/app/views/layouts/closed.html.haml
@@ -0,0 +1,5 @@
+%h1 Closed
+
+%p
+ Peer4commit is now closed. All the remaining funds have been sent to #{link_to "the Peercoin Foundation", "https://peercoin.net/foundation.html"}.
+ See #{link_to "this post", "https://talk.peercoin.net/t/notice-the-peercoin-team-will-be-closing-peer4commit-please-withdraw-all-coins-before-sept-21st/9759"} for more information and discussion.
diff --git a/app/views/projects/_form.html.haml b/app/views/projects/_form.html.haml
new file mode 100644
index 00000000..8fd3c9d1
--- /dev/null
+++ b/app/views/projects/_form.html.haml
@@ -0,0 +1,12 @@
+= bootstrap_form_for @project, layout: :horizontal do |f|
+ = f.alert_message "Please fix the errors below."
+ = f.text_field :name
+ = f.text_field :description
+ = f.text_area :detailed_description, rows: 10
+ = f.fields_for :tipping_policies_text, @project.tipping_policies_text || @project.build_tipping_policies_text do |fields|
+ = fields.text_area :text, rows: 10, label: "Tipping policies"
+ = f.text_field :full_name, label: "GitHub URL (optional)"
+ = f.form_group do
+ = f.check_box :auto_tip_commits, {label: "Automatically send 1% of the balance to each commit added to the default branch of the GitHub project. It only works if the user has registered an account on this site."}
+ = f.form_group do
+ = f.primary "Save"
diff --git a/app/views/projects/decide_tip_amounts.html.haml b/app/views/projects/decide_tip_amounts.html.haml
new file mode 100644
index 00000000..e5a9a7cd
--- /dev/null
+++ b/app/views/projects/decide_tip_amounts.html.haml
@@ -0,0 +1,29 @@
+%h1 Decide tip amounts
+%p
+ Project balance: #{btc_human @project.available_amount}
+= bootstrap_form_for @project, url: decide_tip_amounts_project_path(@project) do |f|
+ %table.table.table-hover.decide-tip-amounts-table
+ %thead
+ %tr
+ %th Commit
+ %th Author
+ %th Message
+ %th Percentage of balance
+ %th Free amount
+ %tbody
+ = f.fields_for(:tips, @project.tips.select(&:was_undecided?)) do |tip_fields|
+ = tip_fields.hidden_field :id
+ - tip = tip_fields.object
+ - next unless tip.user
+ %tr
+ %td= link_to commit_tag(tip.commit), tip.commit_url
+ %td= tip.user.nickname
+ %td= simple_format tip.commit_message
+ %td.col-sm-2
+ - options = [["Free: 0%", "0"], ["Tiny: 0.1%", "0.1"], ["Small: 0.5%", "0.5"], ["Normal: 1%", "1"], ["Big: 2%", "2"], ["Huge: 5%", "5"]]
+ = tip_fields.select :decided_amount_percentage, options, hide_label: true, include_blank: true
+ %td.col-sm-2
+ = tip_fields.text_field :decided_free_amount, inline: true, hide_label: true, append: "PPC"
+
+ .text-center
+ = f.submit 'Send the selected tip amounts'
diff --git a/app/views/projects/donate.html.haml b/app/views/projects/donate.html.haml
new file mode 100644
index 00000000..c35201b6
--- /dev/null
+++ b/app/views/projects/donate.html.haml
@@ -0,0 +1,25 @@
+.row
+ .col-md-12
+ %h1 Donate to #{@project.name}
+ - fundraisers = @project.users
+ - if fundraisers.any?
+ %p
+ - if fundraisers.size > 1
+ Before you donate make sure the project collaborators #{fundraisers.map { |user| link_to user.full_name, user }.join(", ").html_safe} are trustworthy and able to achieve what they promised.
+ - else
+ - user = fundraisers.first
+ Before you donate make sure the fundraiser #{link_to user.full_name, user} is trustworthy and able to achieve what he promised.
+
+ - if @donation_address.persisted? and @donation_address.donation_address.present?
+ %p
+ To donate to #{link_to @project.name, @project} send Peercoins to this address: #{@donation_address.donation_address}
+ - else
+ To donate to this project you can provide a return address. This address will be used if Peer4Commit or the fundraiser ever need to send funds back to you. In the future, this address may also be used to cast a vote. For a vote to be valid, it will need to be signed by the address that the donation was sent from.
+
+ = bootstrap_form_for @donation_address, url: donate_project_path(@project) do |f|
+ = f.text_field :sender_address, label: 'Return address'
+ = f.submit "Generate my donation address"
+
+
+ %hr
+ %p If you want to donate without providing a return address, you can just send Peercoins to this address: #{@project.bitcoin_address}
diff --git a/app/views/projects/donors.html.haml b/app/views/projects/donors.html.haml
new file mode 100644
index 00000000..7c8c4298
--- /dev/null
+++ b/app/views/projects/donors.html.haml
@@ -0,0 +1,23 @@
+- content_for :title do
+ = "#{@project.name} - Donor list"
+
+.row
+ .col-md-12
+ %h1
+ = content_for(:title)
+ %table.table.table-hover.donor-list
+ %thead
+ %tr
+ %th.date Date
+ %th.amount Amount
+ %th.sender-address Sender address
+ %th.transactions Transaction
+ %tbody
+ - @project.deposits.includes(:donation_address).order(created_at: :desc).each do |deposit|
+ %tr.donor-row
+ %td.date= l(deposit.created_at)
+ %td.amount= btc_human deposit.amount
+ %td.sender-address= deposit.donation_address.try(:sender_address).presence || 'No address provided'
+ %td.transactions.txid
+ = link_to transaction_url(deposit.txid) do
+ %abbr{title: deposit.txid}= truncate(deposit.txid, length: 10)
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
new file mode 100644
index 00000000..12988975
--- /dev/null
+++ b/app/views/projects/edit.html.haml
@@ -0,0 +1,7 @@
+- content_for :title do
+ = @project.name
+
+%h1= @project.name
+.row
+ .col-md-12
+ = render "form"
diff --git a/app/views/projects/index.html.haml b/app/views/projects/index.html.haml
index d9345199..0182a3bd 100644
--- a/app/views/projects/index.html.haml
+++ b/app/views/projects/index.html.haml
@@ -1,34 +1,24 @@
-%h1 Supported Projects
-%p
- = form_tag projects_path, html: {role: 'form', class: 'form-inline"'}, method: :post do |f|
- .form-group
- .row
- .col-lg-10
- = text_field_tag :full_name, '', class: 'form-control', placeholder: 'Enter GitHub project URL to find or add a project e.g. rails/rails'
- .col-lg-2
- %button(type="submit" class="btn form-control btn-default") Find or add project
-%p
- %table.table
- %thead
- %tr
- %th Repository
- %th Description
- %th Watchers
- %th Balance
- %th
- %tbody
- - @projects.each do |project|
+#project-index
+ .row
+ .col-md-10
+ %h1 Projects
+ .col-md-2
+ .text-right
+ = link_to "Create a project", new_project_path, class: 'btn btn-default btn-block'
+ %p
+ %table.table.table-hover
+ %thead
%tr
- %td
- %strong= link_to project.full_name, project
- - if !project.source_full_name.blank?
- %br
- %nobr
- %small
- forked from
- = link_to project.source_full_name, project.source_github_url, target: '_blank'
- %td= project.description
- %td= project.watchers_count
- %td= btc_human project.available_amount_cache
- %td= link_to 'Support', project, class: 'btn btn-success'
- = paginate @projects
+ %th.name Name
+ %th.description Description
+ %th.amount Funds
+ %th.actions
+ %tbody
+ - @projects.each do |project|
+ %tr
+ %td.name
+ %strong= link_to project.name, project
+ %td.description= project.description
+ %td.amount= btc_human project.available_amount_cache
+ %td.actions= link_to 'Details', project, class: 'btn btn-default btn-sm'
+ = paginate @projects
diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml
new file mode 100644
index 00000000..4f1bf18b
--- /dev/null
+++ b/app/views/projects/new.html.haml
@@ -0,0 +1,7 @@
+- content_for :title do
+ New project
+
+%h1 New project
+.row
+ .col-md-12
+ = render "form"
diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml
index 29c8bb9a..09c175cb 100644
--- a/app/views/projects/show.html.haml
+++ b/app/views/projects/show.html.haml
@@ -1,77 +1,98 @@
- content_for :title do
- Contribute to
= @project.name
- content_for :description do
= @project.description
-%h1
- = @project.full_name
- %small= link_to glyph(:github), @project.github_url, target: '_blank'
-
.row
- .col-md-4
- .panel.panel-default
- .panel-heading
- %h4.panel-title
- Project Sponsors
- .panel-body
- %iframe{:src => "http://coingiving.com/project_sponsors?url=#{project_url @project.id}", :scrolling => "no", :style => 'width:100%; height:500px; border:0px; padding:0;overflow:hidden'}
- .hidden
- %span(data-coingiving="title")= "[tip4commit] " + @project.full_name
- %span(data-coingiving="description")= @project.description
- %span(data-coingiving="bitcoin-address")= @project.bitcoin_address
- %p #{100-(CONFIG["our_fee"]*100).round}% of deposited funds will be used to tip for new commits.
.col-md-8
+ %h1
+ = @project.name
+ - if (url = @project.github_url).present?
+ %small= link_to glyph(:github), url, target: '_blank'
- unless @project.description.blank?
- .well.well-sm= @project.description
- %h4 Balance
- = btc_human @project.available_amount
- (each new commit receives #{(CONFIG["tip"]*100).round}% of available balance)
- - if (unconfirmed_amount = @project.unconfirmed_amount) > 0
- (#{btc_human unconfirmed_amount} unconfirmed)
-
- %h4 Tips Paid
- = btc_human @project.tips_paid_amount
- - if (tips_paid_unclaimed_amount = @project.tips_paid_unclaimed_amount) > 0
- (#{btc_human tips_paid_unclaimed_amount} of this is unclaimed, and will be refunded to the project after being unclaimed for 1 month.)
+ %h3= @project.description
+ - unless @project.detailed_description.blank?
+ = render_markdown @project.detailed_description
- - if @project.tips.count > 0
- %h4
- Last Tips
- - if @project.tips.count > 5
- = link_to 'see all', project_tips_path(@project)
- %ul
- - @project.tips.includes(:user).order(created_at: :desc).first(5).each do |tip|
- %li
- = l tip.created_at, format: :short
- - if tip.user.nickname.blank?
- = tip.user.full_name
- - else
- = link_to tip.user.full_name, "https://github.com/#{tip.user.nickname}", target: '_blank'
- received
- = btc_human tip.amount
- for commit
- = link_to tip.commit[0..6], "https://github.com/#{@project.full_name}/commit/#{tip.commit}", target: :blank
+ - if @project.tipping_policies_text.try(:text).present?
+ %h4 Tipping policies
+ = render_markdown @project.tipping_policies_text.text
+ %small
+ %em
+ - user = @project.tipping_policies_text.user
+ - name = user.nickname.presence || user.name if user
+ - date = l(@project.tipping_policies_text.updated_at)
+ - if name.present?
+ = "(Last updated by #{name} on #{date})"
+ - else
+ = "(Last updated on #{date})"
- - if @project.next_tip_amount > 0
- %h4 Next Tip
- = btc_human @project.next_tip_amount
-
- %h4 Contribute and Earn
- Donate bitcoins to this project or
- = link_to 'make commits', @project.github_url, target: '_blank'
- and get tips for it. If your commit is accepted by project maintainer and there are bitcoins on its balance, you will get a tip!
+ .col-md-4
+ - if @project.disabled?
+ .panel.panel-danger.note-panel
+ .panel-heading
+ %h4.panel-title
+ Project Disabled
+ .panel-body.text-center
+ %p
+ This project has been disabled.
+ It doesn't accept donation and it will not distribute tips.
+ - if (reason = @project.disabled_reason).present?
+ %p Reason: #{reason}
- - if current_user
- - if current_user.bitcoin_address.blank?
- Just
- = link_to 'tell us', current_user
- your bitcoin address.
- else
- Just check your email or
- %a{href: user_omniauth_authorize_path(:github)} Sign In.
+ .panel.panel-default.project-panel.note-panel
+ .panel-heading
+ %h4.panel-title
+ Project informations
+ .panel-body.text-center
+ %table.table.text-left
+ %tr
+ %td Fundraiser
+ %td
+ - @project.users.each do |user|
+ .fundraiser= link_to user.full_name, user
+ %tr
+ %td Funds
+ %td
+ = btc_human @project.available_amount
+ - if @project.deposits.any?
+ .list-of-donors
+ = link_to "List of donors", donors_project_path(@project)
+ %tr
+ %td Distributions
+ %td
+ - if @project.distributions.empty?
+ None
+ - else
+ %ul.list-unstyled#distribution-list
+ - @project.distributions.order(created_at: :desc).limit(5).each do |distribution|
+ %li.distribution-link
+ - label = btc_human(distribution.total_amount)
+ - if distribution.is_error?
+ - label << " failed"
+ - elsif distribution.sent?
+ - if distribution.sent_at
+ - label << " sent #{time_ago_in_words(distribution.sent_at)} ago"
+ - else
+ - label << " sent"
+ - else
+ - label << " not sent"
+ = link_to label, [@project, distribution]
+ = link_to "All distributions", project_distributions_path(@project)
- %h4 Promote #{@project.full_name}
+ = link_to "Donate", donate_project_path(@project), class: "btn btn-primary btn-block"
+ - if can? :update, @project
+ = link_to "Edit project", edit_project_path(@project), class: "btn btn-default btn-sm btn-block"
+ - if can? :decide_tip_amounts, @project and @project.has_undecided_tips?
+ = link_to "Decide tip amounts", decide_tip_amounts_project_path(@project), class: "btn btn-default btn-sm btn-block"
+ - if can? :create, @project.distributions.build
+ = link_to "New distribution", new_project_distribution_path(@project), class: "btn btn-default btn-sm btn-block"
+
+.row
+ .col-md-8
+ %hr
+ %h4 Promote #{@project.name}
%p
/ AddThis Button BEGIN
.addthis_toolbox.addthis_default_style.addthis_32x32_style(addthis:data_track_clickback="false" addthis:data_track_addressbar="false")
@@ -84,6 +105,10 @@
/ AddThis Button END
%h4 Embed in README.md
- %p= link_to image_tag(project_url(@project, format: :svg), alt: 'Tip4Commit'), project_url(@project)
+ %p= link_to image_tag(project_url(@project, format: :svg), alt: 'Peer4Commit'), project_url(@project)
%p
%input.form-control{type: 'text', value: "[})](#{project_url(@project)})"}
+
+ %hr
+ = commontator_thread(@project)
+
diff --git a/app/views/projects/show.svg.erb b/app/views/projects/show.svg.erb
index acabfbc3..8dda8b3e 100644
--- a/app/views/projects/show.svg.erb
+++ b/app/views/projects/show.svg.erb
@@ -1,7 +1,7 @@
\ No newline at end of file
+
diff --git a/app/views/tips/index.html.haml b/app/views/tips/index.html.haml
index 0b7093ad..ca10a0ed 100644
--- a/app/views/tips/index.html.haml
+++ b/app/views/tips/index.html.haml
@@ -20,23 +20,33 @@
%tr
%td= l tip.created_at, format: :short
%td
- - if tip.user.nickname.blank?
- = tip.user.full_name
- - else
- = link_to tip.user.full_name, "https://github.com/#{tip.user.nickname}", target: '_blank'
+ - if tip.user
+ - if tip.user.nickname.blank?
+ = tip.user.full_name
+ - else
+ = link_to tip.user.full_name, "https://github.com/#{tip.user.nickname}", target: '_blank'
- unless @project
%td= link_to tip.project.full_name, tip.project
- %td= link_to tip.commit[0..6], "https://github.com/#{tip.project.full_name}/commit/#{tip.commit}", target: :blank
- %td= btc_human tip.amount
%td
- - if tip.sendmany.nil?
+ - if tip.commit.present?
+ = link_to tip.commit[0..6], "https://github.com/#{tip.project.full_name}/commit/#{tip.commit}", target: :blank
+ %td= btc_human tip.amount
+ %td{class: tip.distribution.try(:is_error?) ? "danger" : nil}
+ - if tip.distribution.nil?
- if tip.refunded_at
Refunded to project's deposit
+ - elsif tip.undecided?
+ The amount of the tip has not been decided yet
+ - elsif tip.free?
+ - elsif tip.user and tip.user.bitcoin_address.blank?
+ User didn't specify withdrawal address
+ - elsif tip.project.amount_to_pay < CONFIG["min_payout"].to_d * COIN
+ The amount of tips for this project is below withdrawal threshold
- else
- - if tip.user.bitcoin_address.blank?
- User didn't specify withdrawal address
- - else
- User's balance is below withdrawal threshold
+ Waiting for withdrawal
- else
- = link_to tip.sendmany.txid, "https://blockchain.info/tx/#{tip.sendmany.txid}", target: :blank
- = paginate @tips
\ No newline at end of file
+ - if tip.distribution.is_error?
+ Transaction failed
+ - if tip.distribution.txid.present?
+ = link_to tip.distribution.txid, transaction_url(tip.distribution.txid), target: :blank
+ = paginate @tips
diff --git a/app/views/user_mailer/address_request.html.haml b/app/views/user_mailer/address_request.html.haml
new file mode 100644
index 00000000..87c52cce
--- /dev/null
+++ b/app/views/user_mailer/address_request.html.haml
@@ -0,0 +1,22 @@
+%h4 Hello,
+
+%p
+ #{@collaborator.nickname} wants to reward you #{@tip.amount ? btc_human(@tip.amount) : ""} on his project #{@tip.project.name}.
+ = link_to "More details on the distribution", project_distribution_url(@tip.project, @tip.distribution)
+
+%p
+ To get your reward you must provide a Peercoin address.
+
+- if @user.confirmed?
+ %p= link_to "Set your Peercoin address", edit_registration_url(@user)
+- else
+ %p= link_to 'Set your password and Peercoin address', set_password_and_address_user_url(@user, token: @user.confirmation_token)
+
+%p Thank you.
+
+%p= link_to "peer4commit.com", "http://peer4commit.com/"
+
+%p
+ %small
+ = link_to "Don't notify me anymore.", login_users_url(token: @user.login_token, unsubscribe: true)
+
diff --git a/app/views/user_mailer/new_tip.html.haml b/app/views/user_mailer/new_tip.html.haml
index 52e21d40..b417a42f 100644
--- a/app/views/user_mailer/new_tip.html.haml
+++ b/app/views/user_mailer/new_tip.html.haml
@@ -1,16 +1,14 @@
%h4 Hello, #{@user.full_name}!
-%p You were tipped #{btc_human @tip.amount} for your commit on Project #{@tip.project.full_name}. Please, log in and tell us your bitcoin address to get it.
+%p You were tipped #{btc_human @tip.amount} for your commit on Project #{@tip.project.full_name}. Please, log in and tell us your peercoin address to get it.
-%p Your current balance is #{btc_human @user.balance}. If you don't enter a bitcoin address your tips will be returned to the project in 30 days.
+%p Your current balance is #{btc_human @user.balance}. If you don't enter a peercoin address your tips will be returned to the project in 30 days.
-%p If you don't need bitcoins you can redirect your funds to any charity by using its address which you can find at #{link_to 'coingiving.com', 'http://coingiving.com/'}.
-
-%p= link_to 'Sign In', login_users_url(token: @user.login_token)
+%p= link_to 'Set your password and Peercoin address', set_password_and_address_user_url(@user, token: @user.confirmation_token)
%p Thanks for contributing to Open Source!
-%p= link_to "tip4commit.com", "http://tip4commit.com/"
+%p= link_to "peer4commit.com", "http://peer4commit.com/"
%p
%small
diff --git a/app/views/user_mailer/security_issue.html.haml b/app/views/user_mailer/security_issue.html.haml
new file mode 100644
index 00000000..d503374c
--- /dev/null
+++ b/app/views/user_mailer/security_issue.html.haml
@@ -0,0 +1,18 @@
+%h4 Hello #{@user.full_name},
+
+%p We recently discovered a security issue on Peer4commit. This issue allowed someone to change the Peercoin address of other users.
+
+%p
+ The problem is now fixed. To ensure our database is clean we decided to clear all the addresses.
+ Please set your Peercoin address again:
+ = link_to('Sign in', login_users_url(token: @user.login_token)) + "."
+
+%p We think only one tip was stolen. It will be sent again to its owner when he sets his address.
+
+%p Sorry for this inconvenience.
+
+%p= link_to "peer4commit.com", "http://peer4commit.com/"
+
+%p
+ %small
+ = link_to "Don't notify me anymore.", login_users_url(token: @user.login_token, unsubscribe: true)
diff --git a/app/views/users/set_password_and_address.html.haml b/app/views/users/set_password_and_address.html.haml
new file mode 100644
index 00000000..a6b916e9
--- /dev/null
+++ b/app/views/users/set_password_and_address.html.haml
@@ -0,0 +1,5 @@
+= bootstrap_form_for @user, url: request.url do |f|
+ = f.password_field :password
+ = f.password_field :password_confirmation
+ = f.text_field :bitcoin_address
+ = f.submit "Save"
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index 886bab48..a3194140 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -1,19 +1,18 @@
%h1= @user.name
%p
- %strong Balance
-%p
- = btc_human @user.balance
-%p
- %small
- You will get your money when your balance hits the threshold of
- = btc_human CONFIG["min_payout"]
-%p
- %strong E-mail
-%p= @user.email
-= form_for @user, html: {role: 'form', class: 'form-inline"'} do |f|
- - if @user.errors.size > 0
- .alert.alert-danger Bitcoin address is invalid.
- .form-group
- = f.label :bitcoin_address
- = f.text_field :bitcoin_address, class: 'form-control', placeholder: 'Your bitcoin address'
- = f.button :update, class: 'btn btn-default'
\ No newline at end of file
+ %strong Identifier:
+ = @user.identifier
+
+- if @user.nickname.present?
+ %p
+ %strong GitHub account:
+ = link_to @user.nickname, "https://github.com/#{@user.nickname}"
+
+- if (projects = @user.projects).any?
+ %h2 Projects
+ %ul
+ - projects.each do |project|
+ %li= link_to project.to_label, project
+
+%hr
+= commontator_thread(@user)
diff --git a/app/views/withdrawals/index.html.haml b/app/views/withdrawals/index.html.haml
deleted file mode 100644
index d44a09df..00000000
--- a/app/views/withdrawals/index.html.haml
+++ /dev/null
@@ -1,14 +0,0 @@
-%h1 Last Withdrawals
-%p
- %table.table
- %thead
- %tr
- %th Created At
- %th Transaction
- %th Result
- %tbody
- - @sendmanies.each do |sendmany|
- %tr
- %td= l sendmany.created_at, format: :short
- %td= link_to sendmany.txid, "https://blockchain.info/tx/#{sendmany.txid}", target: '_blank'
- %td= sendmany.is_error ? "Error" : "Success"
diff --git a/config.ru b/config.ru
index 5bc2a619..f3c77d8b 100644
--- a/config.ru
+++ b/config.ru
@@ -1,4 +1,9 @@
# This file is used by Rack-based servers to start the application.
require ::File.expand_path('../config/environment', __FILE__)
+
+if host = CONFIG["canonical_host"]
+ use Rack::CanonicalHost, host
+end
+
run Rails.application
diff --git a/config/application.rb b/config/application.rb
index 6495eac6..98b32389 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -8,21 +8,27 @@
CONFIG ||= YAML::load(File.open("config/config.yml"))
+COIN = 1000000 # ppcoin/src/util.h
+
module T4c
class Application < Rails::Application
- # Settings in config/environments/* take precedence over those specified here.
- # Application configuration should go into files in config/initializers
- # -- all .rb files in that directory are automatically loaded.
+ # Settings in config/environments/* take precedence over those specified here.
+ # Application configuration should go into files in config/initializers
+ # -- all .rb files in that directory are automatically loaded.
+
+ # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone.
+ # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC.
+ # config.time_zone = 'Central Time (US & Canada)'
+
+ # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
+ # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
+ # config.i18n.default_locale = :de
- # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone.
- # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC.
- # config.time_zone = 'Central Time (US & Canada)'
+ config.autoload_paths += %W(#{config.root}/lib)
- # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
- # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
- # config.i18n.default_locale = :de
+ I18n.enforce_available_locales = false
- config.autoload_paths += %W(#{config.root}/lib)
+ config.active_record.raise_in_transactional_callbacks = true
end
end
diff --git a/config/config.yml.sample b/config/config.yml.sample
index 992b3052..433bdd96 100644
--- a/config/config.yml.sample
+++ b/config/config.yml.sample
@@ -2,10 +2,12 @@ github:
key: "111111111111"
secret: "111111111111"
-blockchain_info:
- guid: "111111111111"
- password: "111111111111"
- callback_secret: "111111111111"
+daemon:
+ username: rpcuser
+ password: rpcpassword
+ host: localhost
+ port: 9904
+ path: /path/to/ppcoin/src/ppcoind
devise:
secret: "111111111111"
@@ -22,6 +24,10 @@ smtp_settings:
authentication: plain
enable_starttls_auto: true
+default_from: contact@example.com
+send_all_emails_to: # put an email here if you're testing and you don't want any email sent to others
+exception_email: admin@example.com # an email will be sent to this address if an unhandled exception occurs
+
# Uncomment to use airbrake/errbit
# airbrake:
@@ -29,5 +35,12 @@ smtp_settings:
# host: errbit.tip4commit.com
tip: 0.01
-min_payout: 100000
-our_fee: 0.05
\ No newline at end of file
+min_payout: 1.0 # in PPC
+our_fee: 0.05
+tipper_delay: "1.hour"
+
+address_versions: # 55/117 for peercoin, 111/196 for testnet, see base58.h
+ - 111
+ - 196
+
+# canonical_host: peer4commit.example.com # will redirect all other hostnames to this one
diff --git a/config/cucumber.yml b/config/cucumber.yml
new file mode 100644
index 00000000..19b288df
--- /dev/null
+++ b/config/cucumber.yml
@@ -0,0 +1,8 @@
+<%
+rerun = File.file?('rerun.txt') ? IO.read('rerun.txt') : ""
+rerun_opts = rerun.to_s.strip.empty? ? "--format #{ENV['CUCUMBER_FORMAT'] || 'progress'} features" : "--format #{ENV['CUCUMBER_FORMAT'] || 'pretty'} #{rerun}"
+std_opts = "--format #{ENV['CUCUMBER_FORMAT'] || 'pretty'} --strict --tags ~@wip"
+%>
+default: <%= std_opts %> features
+wip: --tags @wip:3 --wip features
+rerun: <%= rerun_opts %> --format rerun --out rerun.txt --strict --tags ~@wip
diff --git a/config/database.yml.sample b/config/database.yml.sample
index d41fe63b..35ef0e07 100644
--- a/config/database.yml.sample
+++ b/config/database.yml.sample
@@ -21,7 +21,7 @@ test:
production:
adapter: mysql2
encoding: utf8
- database: tip4commit
+ database: peer4commit
username: root
password:
- socket: /var/run/mysqld/mysqld.sock
\ No newline at end of file
+ socket: /var/run/mysqld/mysqld.sock
diff --git a/config/deploy.rb b/config/deploy.rb
index cd5541a0..85a44849 100644
--- a/config/deploy.rb
+++ b/config/deploy.rb
@@ -1,9 +1,9 @@
set :application, 't4c'
-set :repo_url, 'git@github.com:tip4commit/tip4commit.git'
+set :repo_url, 'git@github.com:sigmike/peer4commit.git'
# ask :branch, proc { `git rev-parse --abbrev-ref HEAD`.chomp }
-set :deploy_to, "/home/apps/t4c"
+set :deploy_to, "/home/apps/p4c"
set :scm, :git
set :rvm_type, :user
@@ -40,4 +40,4 @@
after :finishing, 'deploy:cleanup'
-end
\ No newline at end of file
+end
diff --git a/config/environments/development.rb b/config/environments/development.rb
index 42389197..898d5bcf 100644
--- a/config/environments/development.rb
+++ b/config/environments/development.rb
@@ -13,10 +13,14 @@
config.consider_all_requests_local = true
config.action_controller.perform_caching = false
- # Don't care if the mailer can't send.
- config.action_mailer.raise_delivery_errors = false
+ config.action_mailer.default_url_options = { :host => "localhost:3000" }
- config.action_mailer.default_url_options = { :host => "localhost:3000" }
+ config.action_mailer.delivery_method = :smtp
+ config.action_mailer.smtp_settings = CONFIG['smtp_settings'].to_options
+
+ config.action_mailer.perform_deliveries = true
+ config.action_mailer.raise_delivery_errors = true
+ config.action_mailer.default_options = {from: 'no-reply@' + CONFIG['smtp_settings']['domain'] }
# Print deprecation notices to the Rails logger.
config.active_support.deprecation = :log
diff --git a/config/environments/production.rb b/config/environments/production.rb
index d1485d96..c05d1f00 100644
--- a/config/environments/production.rb
+++ b/config/environments/production.rb
@@ -20,7 +20,7 @@
# config.action_dispatch.rack_cache = true
# Disable Rails's static asset server (Apache or nginx will already do this).
- config.serve_static_assets = false
+ config.serve_static_files = false
# Compress JavaScripts and CSS.
config.assets.js_compressor = :uglifier
@@ -61,14 +61,14 @@
# application.js, application.css, and all non-JS/CSS in app/assets folder are already added.
# config.assets.precompile += %w( search.js )
- config.action_mailer.default_url_options = { :host => CONFIG['smtp_settings']['domain'] }
+ config.action_mailer.default_url_options = { :host => default_hostname = CONFIG['smtp_settings']['domain'] }
config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = CONFIG['smtp_settings'].to_options
config.action_mailer.perform_deliveries = true
config.action_mailer.raise_delivery_errors = true
- config.action_mailer.default_options = {from: 'no-reply@' + CONFIG['smtp_settings']['domain'] }
+ config.action_mailer.default_options = {from: default_from = CONFIG['default_from'].presence || ('no-reply@' + default_hostname) }
# Enable locale fallbacks for I18n (makes lookups for any locale fall back to
# the I18n.default_locale when a translation can not be found).
@@ -82,4 +82,13 @@
# Use default logging formatter so that PID and timestamp are not suppressed.
config.log_formatter = ::Logger::Formatter.new
+
+ if (exception_email = CONFIG["exception_email"]).present?
+ T4c::Application.config.middleware.use ExceptionNotification::Rack,
+ :email => {
+ :email_prefix => "[#{default_hostname}] ",
+ :sender_address => default_from,
+ :exception_recipients => exception_email,
+ }
+ end
end
diff --git a/config/environments/test.rb b/config/environments/test.rb
index 799d7195..459f7c38 100644
--- a/config/environments/test.rb
+++ b/config/environments/test.rb
@@ -13,7 +13,7 @@
config.eager_load = false
# Configure static asset server for tests with Cache-Control for performance.
- config.serve_static_assets = true
+ config.serve_static_files = true
config.static_cache_control = "public, max-age=3600"
# Show full error reports and disable caching.
@@ -31,6 +31,9 @@
# ActionMailer::Base.deliveries array.
config.action_mailer.delivery_method = :test
+ config.action_mailer.default_url_options = {host: "www.example.com"}
+ config.action_mailer.default_options = {from: 'no-reply@example.com'}
+
# Print deprecation notices to the stderr.
config.active_support.deprecation = :stderr
end
diff --git a/config/initializers/commontator.rb b/config/initializers/commontator.rb
new file mode 100644
index 00000000..a86eb06b
--- /dev/null
+++ b/config/initializers/commontator.rb
@@ -0,0 +1,238 @@
+# Change the settings below to suit your needs
+# All settings are initially set to their default values
+
+# Note: Do not "return" from a Proc, use "next" instead if necessary
+# "return" in a lambda is OK
+Commontator.configure do |config|
+ # Engine Configuration
+
+ # current_user_proc
+ # Type: Proc
+ # Arguments: the current controller (ActionController::Base)
+ # Returns: the current user (acts_as_commontator)
+ # The default works for Devise and similar authentication plugins
+ # Default: lambda { |controller| controller.current_user }
+ config.current_user_proc = lambda { |controller| controller.current_user }
+
+ # javascript_proc
+ # Type: Proc
+ # Arguments: a view (ActionView::Base)
+ # Returns: a String that is appended to Commontator JS views
+ # Can be used, for example, to display/clear Rails error messages
+ # Objects visible in view templates can be accessed
+ # through the view object (for example, view.flash)
+ # However, the view does not include the main application's helpers
+ # Default: lambda { |view| '$("#error_explanation").remove();' }
+ config.javascript_proc = lambda { |view|
+ '$("#error_explanation").remove();' }
+
+
+
+ # User (acts_as_commontator) Configuration
+
+ # user_name_proc
+ # Type: Proc
+ # Arguments: a user (acts_as_commontator)
+ # Returns: the user's name (String)
+ # Default: lambda { |user| I18n.t('commontator.anonymous') } (all users are anonymous)
+ config.user_name_proc = lambda { |user| user.try(:full_name) || I18n.t('commontator.anonymous') }
+
+ # user_link_proc
+ # Type: Proc
+ # Arguments: a user (acts_as_commontator),
+ # the app_routes (ActionDispatch::Routing::RoutesProxy)
+ # Returns: a path to the user's `show` page (String)
+ # If anything non-blank is returned, the user's name in comments
+ # comments will become a hyperlink pointing to this path
+ # The main application's routes can be accessed through the app_routes object
+ # Default: lambda { |user, app_routes| '' } (no link)
+ config.user_link_proc = lambda { |user, app_routes| user ? Rails.application.routes.url_helpers.user_path(user) : nil }
+
+ # user_avatar_proc
+ # Type: Proc
+ # Arguments: a user (acts_as_commontator), a view (ActionView::Base)
+ # Returns: a String containing a HTML
![]()
tag pointing to the user's avatar image
+ # The commontator_gravatar_image_tag helper takes a user object,
+ # a border size and an options hash for Gravatar, and produces a Gravatar image tag
+ # See available options at http://en.gravatar.com/site/implement/images/)
+ # Note: Gravatar has several security implications for your users
+ # It makes your users trackable across different sites and
+ # allows de-anonymization attacks against their email addresses
+ # If you absolutely want to keep users' email addresses or identities secret,
+ # do not use Gravatar or similar services
+ # Default: lambda { |user, view|
+ # view.commontator_gravatar_image_tag(
+ # user, 1, :s => 60, :d => 'mm') }
+ config.user_avatar_proc = lambda { |user, view|
+ view.commontator_gravatar_image_tag(
+ user, 1, :s => 60, :d => 'mm') }
+
+ # user_email_proc
+ # Type: Proc
+ # Arguments: a user (acts_as_commontator), a mailer (ActionMailer::Base)
+ # Returns: the user's email address (String)
+ # The default works for Devise's defaults
+ # If the mailer argument is nil, Commontator intends to hash the email and send the hash
+ # to Gravatar, so you should always return the user's email address (if using Gravatar)
+ # If the mailer argument is not nil, then Commontator intends to send an email to
+ # the address returned; you can prevent it from being sent by returning a blank String
+ # Default: lambda { |user, mailer| user.try(:email) || '' }
+ config.user_email_proc = lambda { |user, mailer| user.try(:email) || '' }
+
+
+
+ # Thread/Commontable (acts_as_commontable) Configuration
+
+ # comment_filter
+ # Type: Arel node (Arel::Nodes::Node) or nil
+ # Arel that filters visible comments
+ # If specified, visible comments will be filtered according to this Arel node
+ # A value of nil will cause no filtering to be done
+ # Moderators can manually override this filter for themselves
+ # Example: Commontator::Comment.arel_table[:deleted_at].eq(nil) (hides deleted comments)
+ # This is not recommended, as it can cause confusion over deleted comments
+ # If using pagination, it can also cause comments to change pages
+ # Default: nil (no filtering - all comments are visible)
+ config.comment_filter = nil
+
+ # thread_read_proc
+ # Type: Proc
+ # Arguments: a thread (Commontator::Thread), a user (acts_as_commontator)
+ # Returns: a Boolean, true iif the user should be allowed to read that thread
+ # Note: can be called with a user object that is nil (if they are not logged in)
+ # Default: lambda { |thread, user| true } (anyone can read any thread)
+ config.thread_read_proc = lambda { |thread, user| true }
+
+ # thread_moderator_proc
+ # Type: Proc
+ # Arguments: a thread (Commontator::Thread), a user (acts_as_commontator)
+ # Returns: a Boolean, true iif the user is a moderator for that thread
+ # If you want global moderators, make this proc true for them regardless of thread
+ # Default: lambda { |thread, user| false } (no moderators)
+ config.thread_moderator_proc = lambda { |thread, user| false }
+
+ # comment_editing
+ # Type: Symbol
+ # Whether users can edit their own comments
+ # Valid options:
+ # :a (always)
+ # :l (only if it's the latest comment)
+ # :n (never)
+ # Default: :l
+ config.comment_editing = :l
+
+ # comment_deletion
+ # Type: Symbol
+ # Whether users can delete their own comments
+ # Valid options:
+ # :a (always)
+ # :l (only if it's the latest comment)
+ # :n (never)
+ # Note: For moderators, see the next option
+ # Default: :l
+ config.comment_deletion = :l
+
+ # moderator_permissions
+ # Type: Symbol
+ # What permissions moderators have
+ # Valid options:
+ # :e (delete and edit comments and close threads)
+ # :d (delete comments and close threads)
+ # :c (close threads only)
+ # Default: :d
+ config.moderator_permissions = :d
+
+ # comment_voting
+ # Type: Symbol
+ # Whether users can vote on other users' comments
+ # Valid options:
+ # :n (no voting)
+ # :l (likes - requires acts_as_votable gem)
+ # :ld (likes/dislikes - requires acts_as_votable gem)
+ # Not yet implemented:
+ # :s (star ratings)
+ # :r (reputation system)
+ # Default: :n
+ config.comment_voting = :n
+
+ # vote_count_proc
+ # Type: Proc
+ # Arguments: a thread (Commontator::Thread), pos (Fixnum), neg (Fixnum)
+ # Returns: vote count to be displayed (String)
+ # pos is the number of likes, or the rating, or the reputation
+ # neg is the number of dislikes, if applicable, or 0 otherwise
+ # Default: lambda { |thread, pos, neg| "%+d" % (pos - neg) }
+ config.vote_count_proc = lambda { |thread, pos, neg| "%+d" % (pos - neg) }
+
+ # comment_order
+ # Type: Symbol
+ # What order to use for comments
+ # Valid options:
+ # :e (earliest comment first)
+ # :l (latest comment first)
+ # :ve (highest voted first; earliest first if tied)
+ # :vl (highest voted first; latest first if tied)
+ # Notes:
+ # :e is usually used in forums (discussions)
+ # :l is usually used in blogs (opinions)
+ # :ve and :vl are usually used where it makes sense to rate comments
+ # based on usefulness (q&a, reviews, guides, etc.)
+ # If :l is selected, the "reply to thread" form will appear before the comments
+ # Otherwise, it will appear after the comments
+ # Default: :e
+ config.comment_order = :e
+
+ # comments_per_page
+ # Type: Fixnum or nil
+ # Number of comments to display in each page
+ # Set to nil to disable pagination
+ # Any other value requires the will_paginate gem
+ # Default: nil (no pagination)
+ config.comments_per_page = nil
+
+ # thread_subscription
+ # Type: Symbol
+ # Whether users can subscribe to threads to receive activity email notifications
+ # Valid options:
+ # :n (no subscriptions)
+ # :a (automatically subscribe when you comment; cannot do it manually)
+ # :m (manual subscriptions only)
+ # :b (both automatic, when commenting, and manual)
+ # Default: :n
+ config.thread_subscription = :n
+
+ # email_from_proc
+ # Type: Proc
+ # Arguments: a thread (Commontator::Thread)
+ # Returns: the address emails are sent "from" (String)
+ # Important: If using subscriptions, change this to at least match your domain name
+ # Default: lambda { |thread|
+ # "no-reply@#{Rails.application.class.parent.to_s.downcase}.com" }
+ config.email_from_proc = lambda { |thread|
+ CONFIG["default_from"].presence || "no-reply@#{Rails.application.class.parent.to_s.downcase}.com" }
+
+ # commontable_name_proc
+ # Type: Proc
+ # Arguments: a thread (Commontator::Thread)
+ # Returns: a name that refers to the commontable object (String)
+ # If you have multiple commontable models, you can also pass this
+ # configuration value as an argument to acts_as_commontable for each one
+ # Default: lambda { |thread|
+ # "#{thread.commontable.class.name} ##{thread.commontable.id}" }
+ config.commontable_name_proc = lambda { |thread|
+ "#{thread.commontable.class.name} ##{thread.commontable.to_label}" }
+
+ # commontable_url_proc
+ # Type: Proc
+ # Arguments: a thread (Commontator::Thread),
+ # the app_routes (ActionDispatch::Routing::RoutesProxy)
+ # Returns: a String containing the url of the view that displays the given thread
+ # This usually is the commontable's "show" page
+ # The main application's routes can be accessed through the app_routes object
+ # Default: lambda { |commontable, app_routes|
+ # app_routes.polymorphic_url(commontable) }
+ # (defaults to the commontable's show url)
+ config.commontable_url_proc = lambda { |thread, app_routes|
+ app_routes.polymorphic_url(thread.commontable) }
+end
+
diff --git a/config/initializers/cookies_serializer.rb b/config/initializers/cookies_serializer.rb
new file mode 100644
index 00000000..84bbfa8b
--- /dev/null
+++ b/config/initializers/cookies_serializer.rb
@@ -0,0 +1 @@
+Rails.application.config.action_dispatch.cookies_serializer = :hybrid
diff --git a/config/initializers/intercept_emails.rb b/config/initializers/intercept_emails.rb
new file mode 100644
index 00000000..2f1e12fd
--- /dev/null
+++ b/config/initializers/intercept_emails.rb
@@ -0,0 +1,10 @@
+if (SEND_ALL_EMAILS_TO = CONFIG["send_all_emails_to"]).present? and !Rails.env.test?
+ class MailInterceptor
+ def self.delivering_email(message)
+ message.subject = "[#{CONFIG['smtp_settings']['domain']} to #{message.to.join(", ")}] #{message.subject}"
+ message.to = SEND_ALL_EMAILS_TO
+ end
+ end
+
+ ActionMailer::Base.register_interceptor(MailInterceptor)
+end
diff --git a/config/initializers/mime_types.rb b/config/initializers/mime_types.rb
index d39c83c4..72aca7e4 100644
--- a/config/initializers/mime_types.rb
+++ b/config/initializers/mime_types.rb
@@ -3,4 +3,3 @@
# Add new mime types for use in respond_to blocks:
# Mime::Type.register "text/richtext", :rtf
# Mime::Type.register_alias "text/html", :iphone
-Mime::Type.register "image/svg+xml", :svg
\ No newline at end of file
diff --git a/config/initializers/record_changes.rb b/config/initializers/record_changes.rb
new file mode 100644
index 00000000..6d727943
--- /dev/null
+++ b/config/initializers/record_changes.rb
@@ -0,0 +1,13 @@
+class ActiveRecord::Base
+ def self.record_changes(options)
+ has_many :record_changes, as: :record
+
+ after_save do
+ state = to_json(options)
+ last_state = RecordChange.where(record: self).order(created_at: :desc).first.try(:raw_state)
+ if state != last_state
+ RecordChange.create!(record: self, raw_state: state)
+ end
+ end
+ end
+end
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 06539571..5eb77741 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -21,3 +21,15 @@
en:
hello: "Hello world"
+
+ activerecord:
+ attributes:
+ user:
+ bitcoin_address: Peercoin address
+ project:
+ github_url: GitHub URL
+ detailed_description: Detailed description
+ tip:
+ coin_amount: Amount
+ description: Comment
+ _destroy: Remove
diff --git a/config/routes.rb b/config/routes.rb
index cc205fcd..1c64af15 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -1,72 +1,45 @@
T4c::Application.routes.draw do
+ mount Commontator::Engine => '/commontator'
root 'home#index'
- get '/blockchain_info_callback' => "home#blockchain_info_callback", :as => "blockchain_info_callback"
+ get 'audit' => 'home#audit'
+ get 'faq' => 'home#faq'
+
+ devise_for :users,
+ controllers: {
+ omniauth_callbacks: "users/omniauth_callbacks",
+ registrations: "registrations",
+ }
+
resources :users, :only => [:show, :update, :index] do
collection do
get :login
+ get :suggestions
+ end
+ member do
+ post :send_tips_back
+ get :set_password_and_address
+ patch :set_password_and_address
end
end
- resources :projects, :only => [:show, :index, :create] do
+ resources :projects, :only => [:new, :show, :index, :create, :edit, :update] do
resources :tips, :only => [:index]
+ resources :distributions, :only => [:new, :create, :show, :index, :edit, :update] do
+ get :new_recipient_form, on: :collection
+ post :send_transaction, on: :member
+ end
+ member do
+ get :qrcode
+ get :decide_tip_amounts
+ patch :decide_tip_amounts
+ get :commit_suggestions
+ get :github_user_suggestions
+ get :donate
+ post :donate
+ get :donors
+ end
end
resources :tips, :only => [:index]
- resources :withdrawals, :only => [:index]
-
- devise_for :users,
- :controllers => {
- :omniauth_callbacks => "users/omniauth_callbacks"
- }
- # The priority is based upon order of creation: first created -> highest priority.
- # See how all your routes lay out with "rake routes".
-
- # Example of regular route:
- # get 'products/:id' => 'catalog#view'
-
- # Example of named route that can be invoked with purchase_url(id: product.id)
- # get 'products/:id/purchase' => 'catalog#purchase', as: :purchase
-
- # Example resource route (maps HTTP verbs to controller actions automatically):
- # resources :products
-
- # Example resource route with options:
- # resources :products do
- # member do
- # get 'short'
- # post 'toggle'
- # end
- #
- # collection do
- # get 'sold'
- # end
- # end
-
- # Example resource route with sub-resources:
- # resources :products do
- # resources :comments, :sales
- # resource :seller
- # end
-
- # Example resource route with more complex sub-resources:
- # resources :products do
- # resources :comments
- # resources :sales do
- # get 'recent', on: :collection
- # end
- # end
-
- # Example resource route with concerns:
- # concern :toggleable do
- # post 'toggle'
- # end
- # resources :posts, concerns: :toggleable
- # resources :photos, concerns: :toggleable
-
- # Example resource route within a namespace:
- # namespace :admin do
- # # Directs /admin/products/* to Admin::ProductsController
- # # (app/controllers/admin/products_controller.rb)
- # resources :products
- # end
+ resources :distributions, :only => [:index]
end
diff --git a/config/schedule.rb b/config/schedule.rb
new file mode 100644
index 00000000..16f96783
--- /dev/null
+++ b/config/schedule.rb
@@ -0,0 +1,34 @@
+# Use this file to easily define all of your cron jobs.
+#
+# It's helpful, but not entirely necessary to understand cron before proceeding.
+# http://en.wikipedia.org/wiki/Cron
+
+# Example:
+#
+# set :output, "/path/to/my/cron_log.log"
+#
+# every 2.hours do
+# command "/usr/bin/some_great_command"
+# runner "MyModel.some_method"
+# rake "some:great:rake:task"
+# end
+#
+# every 4.days do
+# runner "AnotherModel.prune_old_records"
+# end
+
+# Learn more: http://github.com/javan/whenever
+
+require File.expand_path('../../config/environment', __FILE__)
+every :reboot do
+ if daemon = CONFIG['daemon']['path']
+ command daemon
+ end
+end
+
+if delay = CONFIG['tipper_delay']
+ delay = eval(delay)
+ every delay do
+ runner "BalanceUpdater.work; BitcoinTipper.work; BalanceUpdater.work"
+ end
+end
diff --git a/db/migrate/20140207061855_add_github_id_to_projects.rb b/db/migrate/20140207061855_add_github_id_to_projects.rb
new file mode 100644
index 00000000..de43248e
--- /dev/null
+++ b/db/migrate/20140207061855_add_github_id_to_projects.rb
@@ -0,0 +1,5 @@
+class AddGithubIdToProjects < ActiveRecord::Migration
+ def change
+ add_column :projects, :github_id, :string
+ end
+end
\ No newline at end of file
diff --git a/db/migrate/20140209022632_change_projects_description.rb b/db/migrate/20140209022632_change_projects_description.rb
new file mode 100644
index 00000000..a02592e2
--- /dev/null
+++ b/db/migrate/20140209022632_change_projects_description.rb
@@ -0,0 +1,8 @@
+class ChangeProjectsDescription < ActiveRecord::Migration
+ def up
+ change_column :projects, :description, :text, :limit => nil
+ end
+ def down
+ change_column :projects, :description, :string
+ end
+end
diff --git a/db/migrate/20140209041123_create_indexes_for_projects.rb b/db/migrate/20140209041123_create_indexes_for_projects.rb
new file mode 100644
index 00000000..6f1cb519
--- /dev/null
+++ b/db/migrate/20140209041123_create_indexes_for_projects.rb
@@ -0,0 +1,6 @@
+class CreateIndexesForProjects < ActiveRecord::Migration
+ def change
+ add_index :projects, :full_name, :unique => true
+ add_index :projects, :github_id, :unique => true
+ end
+end
\ No newline at end of file
diff --git a/db/migrate/20140215062842_add_project_to_sendmany.rb b/db/migrate/20140215062842_add_project_to_sendmany.rb
new file mode 100644
index 00000000..41af2662
--- /dev/null
+++ b/db/migrate/20140215062842_add_project_to_sendmany.rb
@@ -0,0 +1,6 @@
+class AddProjectToSendmany < ActiveRecord::Migration
+ def change
+ add_column :sendmanies, :project_id, :integer
+ add_index :sendmanies, :project_id
+ end
+end
diff --git a/db/migrate/20140215094135_add_addres_label_to_project.rb b/db/migrate/20140215094135_add_addres_label_to_project.rb
new file mode 100644
index 00000000..a38b2834
--- /dev/null
+++ b/db/migrate/20140215094135_add_addres_label_to_project.rb
@@ -0,0 +1,5 @@
+class AddAddresLabelToProject < ActiveRecord::Migration
+ def change
+ add_column :projects, :address_label, :string
+ end
+end
diff --git a/db/migrate/20140215094549_initialize_project_address_label.rb b/db/migrate/20140215094549_initialize_project_address_label.rb
new file mode 100644
index 00000000..8bcc6116
--- /dev/null
+++ b/db/migrate/20140215094549_initialize_project_address_label.rb
@@ -0,0 +1,5 @@
+class InitializeProjectAddressLabel < ActiveRecord::Migration
+ def up
+ execute "UPDATE projects SET address_label=(full_name || '@peer4commit') WHERE address_label IS NULL"
+ end
+end
diff --git a/db/migrate/20140309161105_change_project_amount_cache_to_big_int.rb b/db/migrate/20140309161105_change_project_amount_cache_to_big_int.rb
new file mode 100644
index 00000000..ef13e88e
--- /dev/null
+++ b/db/migrate/20140309161105_change_project_amount_cache_to_big_int.rb
@@ -0,0 +1,5 @@
+class ChangeProjectAmountCacheToBigInt < ActiveRecord::Migration
+ def change
+ change_column :projects, :available_amount_cache, :integer, limit: 8
+ end
+end
diff --git a/db/migrate/20140309192616_create_collaborators.rb b/db/migrate/20140309192616_create_collaborators.rb
new file mode 100644
index 00000000..c0d734dc
--- /dev/null
+++ b/db/migrate/20140309192616_create_collaborators.rb
@@ -0,0 +1,10 @@
+class CreateCollaborators < ActiveRecord::Migration
+ def change
+ create_table :collaborators do |t|
+ t.belongs_to :project, index: true
+ t.string :login
+
+ t.timestamps
+ end
+ end
+end
diff --git a/db/migrate/20140323072851_add_hold_tips_to_project.rb b/db/migrate/20140323072851_add_hold_tips_to_project.rb
new file mode 100644
index 00000000..bb932ba1
--- /dev/null
+++ b/db/migrate/20140323072851_add_hold_tips_to_project.rb
@@ -0,0 +1,5 @@
+class AddHoldTipsToProject < ActiveRecord::Migration
+ def change
+ add_column :projects, :hold_tips, :boolean, default: false
+ end
+end
diff --git a/db/migrate/20140323165816_add_commit_message_to_tip.rb b/db/migrate/20140323165816_add_commit_message_to_tip.rb
new file mode 100644
index 00000000..aad73bee
--- /dev/null
+++ b/db/migrate/20140323165816_add_commit_message_to_tip.rb
@@ -0,0 +1,5 @@
+class AddCommitMessageToTip < ActiveRecord::Migration
+ def change
+ add_column :tips, :commit_message, :string
+ end
+end
diff --git a/db/migrate/20140323173320_create_tipping_policies_texts.rb b/db/migrate/20140323173320_create_tipping_policies_texts.rb
new file mode 100644
index 00000000..898e3428
--- /dev/null
+++ b/db/migrate/20140323173320_create_tipping_policies_texts.rb
@@ -0,0 +1,11 @@
+class CreateTippingPoliciesTexts < ActiveRecord::Migration
+ def change
+ create_table :tipping_policies_texts do |t|
+ t.belongs_to :project, index: true
+ t.belongs_to :user, index: true
+ t.text :text
+
+ t.timestamps
+ end
+ end
+end
diff --git a/db/migrate/20140330165138_create_cold_storage_transfers.rb b/db/migrate/20140330165138_create_cold_storage_transfers.rb
new file mode 100644
index 00000000..29b2ec13
--- /dev/null
+++ b/db/migrate/20140330165138_create_cold_storage_transfers.rb
@@ -0,0 +1,13 @@
+class CreateColdStorageTransfers < ActiveRecord::Migration
+ def change
+ create_table :cold_storage_transfers do |t|
+ t.belongs_to :project, index: true
+ t.integer :amount, limit: 8
+ t.string :address
+ t.string :txid
+ t.integer :confirmations
+
+ t.timestamps
+ end
+ end
+end
diff --git a/db/migrate/20140401174927_add_cold_storage_withdrawal_address_to_project.rb b/db/migrate/20140401174927_add_cold_storage_withdrawal_address_to_project.rb
new file mode 100644
index 00000000..c19c5a49
--- /dev/null
+++ b/db/migrate/20140401174927_add_cold_storage_withdrawal_address_to_project.rb
@@ -0,0 +1,5 @@
+class AddColdStorageWithdrawalAddressToProject < ActiveRecord::Migration
+ def change
+ add_column :projects, :cold_storage_withdrawal_address, :string
+ end
+end
diff --git a/db/migrate/20140402111051_add_disabled_to_project.rb b/db/migrate/20140402111051_add_disabled_to_project.rb
new file mode 100644
index 00000000..631e6e86
--- /dev/null
+++ b/db/migrate/20140402111051_add_disabled_to_project.rb
@@ -0,0 +1,5 @@
+class AddDisabledToProject < ActiveRecord::Migration
+ def change
+ add_column :projects, :disabled, :boolean, default: false
+ end
+end
diff --git a/db/migrate/20140403062826_add_account_balance_to_project.rb b/db/migrate/20140403062826_add_account_balance_to_project.rb
new file mode 100644
index 00000000..6017bdeb
--- /dev/null
+++ b/db/migrate/20140403062826_add_account_balance_to_project.rb
@@ -0,0 +1,5 @@
+class AddAccountBalanceToProject < ActiveRecord::Migration
+ def change
+ add_column :projects, :account_balance, :integer, limit: 8
+ end
+end
diff --git a/db/migrate/20140405084351_add_disabled_reason_to_project.rb b/db/migrate/20140405084351_add_disabled_reason_to_project.rb
new file mode 100644
index 00000000..15c3b1ea
--- /dev/null
+++ b/db/migrate/20140405084351_add_disabled_reason_to_project.rb
@@ -0,0 +1,5 @@
+class AddDisabledReasonToProject < ActiveRecord::Migration
+ def change
+ add_column :projects, :disabled_reason, :string
+ end
+end
diff --git a/db/migrate/20140406064344_add_fee_to_sendmany.rb b/db/migrate/20140406064344_add_fee_to_sendmany.rb
new file mode 100644
index 00000000..8704b4b2
--- /dev/null
+++ b/db/migrate/20140406064344_add_fee_to_sendmany.rb
@@ -0,0 +1,5 @@
+class AddFeeToSendmany < ActiveRecord::Migration
+ def change
+ add_column :sendmanies, :fee, :integer
+ end
+end
diff --git a/db/migrate/20140406071705_add_fee_to_cold_storage_transfer.rb b/db/migrate/20140406071705_add_fee_to_cold_storage_transfer.rb
new file mode 100644
index 00000000..f6a6c308
--- /dev/null
+++ b/db/migrate/20140406071705_add_fee_to_cold_storage_transfer.rb
@@ -0,0 +1,5 @@
+class AddFeeToColdStorageTransfer < ActiveRecord::Migration
+ def change
+ add_column :cold_storage_transfers, :fee, :integer
+ end
+end
diff --git a/db/migrate/20140529135156_add_detailed_descrition_to_project.rb b/db/migrate/20140529135156_add_detailed_descrition_to_project.rb
new file mode 100644
index 00000000..48724d59
--- /dev/null
+++ b/db/migrate/20140529135156_add_detailed_descrition_to_project.rb
@@ -0,0 +1,5 @@
+class AddDetailedDescritionToProject < ActiveRecord::Migration
+ def change
+ add_column :projects, :detailed_description, :text
+ end
+end
diff --git a/db/migrate/20140530132209_rename_sendmany_to_distribution.rb b/db/migrate/20140530132209_rename_sendmany_to_distribution.rb
new file mode 100644
index 00000000..d7dc0abe
--- /dev/null
+++ b/db/migrate/20140530132209_rename_sendmany_to_distribution.rb
@@ -0,0 +1,6 @@
+class RenameSendmanyToDistribution < ActiveRecord::Migration
+ def change
+ rename_table :sendmanies, :distributions
+ rename_column :tips, :sendmany_id, :distribution_id
+ end
+end
diff --git a/db/migrate/20140531080839_add_sent_at_to_distribution.rb b/db/migrate/20140531080839_add_sent_at_to_distribution.rb
new file mode 100644
index 00000000..dc47d0fa
--- /dev/null
+++ b/db/migrate/20140531080839_add_sent_at_to_distribution.rb
@@ -0,0 +1,5 @@
+class AddSentAtToDistribution < ActiveRecord::Migration
+ def change
+ add_column :distributions, :sent_at, :datetime
+ end
+end
diff --git a/db/migrate/20140601072522_add_devise_confirmable.rb b/db/migrate/20140601072522_add_devise_confirmable.rb
new file mode 100644
index 00000000..6a497175
--- /dev/null
+++ b/db/migrate/20140601072522_add_devise_confirmable.rb
@@ -0,0 +1,13 @@
+class AddDeviseConfirmable < ActiveRecord::Migration
+ def change
+ change_table :users do |t|
+ t.string :confirmation_token
+ t.datetime :confirmed_at
+ t.datetime :confirmation_sent_at
+ t.string :unconfirmed_email
+ end
+
+ # Existing users with a GitHub nickname are confirmed
+ execute "UPDATE users SET confirmed_at='#{Time.now.strftime('%Y-%m-%d %H:%M:%S')}' WHERE nickname IS NOT NULL"
+ end
+end
diff --git a/db/migrate/20140601103950_new_default_to_hold_tips.rb b/db/migrate/20140601103950_new_default_to_hold_tips.rb
new file mode 100644
index 00000000..9781d645
--- /dev/null
+++ b/db/migrate/20140601103950_new_default_to_hold_tips.rb
@@ -0,0 +1,5 @@
+class NewDefaultToHoldTips < ActiveRecord::Migration
+ def change
+ change_column :projects, :hold_tips, :boolean, default: true
+ end
+end
diff --git a/db/migrate/20140601104108_remove_unique_constraint_to_project_full_name.rb b/db/migrate/20140601104108_remove_unique_constraint_to_project_full_name.rb
new file mode 100644
index 00000000..4ed12259
--- /dev/null
+++ b/db/migrate/20140601104108_remove_unique_constraint_to_project_full_name.rb
@@ -0,0 +1,9 @@
+class RemoveUniqueConstraintToProjectFullName < ActiveRecord::Migration
+ def up
+ remove_index "projects", "full_name"
+ end
+
+ def down
+ add_index "projects", ["full_name"], name: "index_projects_on_full_name", unique: true
+ end
+end
diff --git a/db/migrate/20140601144116_add_comment_to_tip.rb b/db/migrate/20140601144116_add_comment_to_tip.rb
new file mode 100644
index 00000000..26aaa97c
--- /dev/null
+++ b/db/migrate/20140601144116_add_comment_to_tip.rb
@@ -0,0 +1,5 @@
+class AddCommentToTip < ActiveRecord::Migration
+ def change
+ add_column :tips, :comment, :string
+ end
+end
diff --git a/db/migrate/20140601145337_create_versions.rb b/db/migrate/20140601145337_create_versions.rb
new file mode 100644
index 00000000..23be970c
--- /dev/null
+++ b/db/migrate/20140601145337_create_versions.rb
@@ -0,0 +1,13 @@
+class CreateVersions < ActiveRecord::Migration
+ def change
+ create_table :versions do |t|
+ t.string :item_type, :null => false
+ t.integer :item_id, :null => false
+ t.string :event, :null => false
+ t.string :whodunnit
+ t.text :object
+ t.datetime :created_at
+ end
+ add_index :versions, [:item_type, :item_id]
+ end
+end
diff --git a/db/migrate/20140601145338_add_object_changes_to_versions.rb b/db/migrate/20140601145338_add_object_changes_to_versions.rb
new file mode 100644
index 00000000..2d723e06
--- /dev/null
+++ b/db/migrate/20140601145338_add_object_changes_to_versions.rb
@@ -0,0 +1,5 @@
+class AddObjectChangesToVersions < ActiveRecord::Migration
+ def change
+ add_column :versions, :object_changes, :text
+ end
+end
diff --git a/db/migrate/20140602210025_create_commits.rb b/db/migrate/20140602210025_create_commits.rb
new file mode 100644
index 00000000..ab4029ee
--- /dev/null
+++ b/db/migrate/20140602210025_create_commits.rb
@@ -0,0 +1,13 @@
+class CreateCommits < ActiveRecord::Migration
+ def change
+ create_table :commits do |t|
+ t.belongs_to :project, index: true
+ t.string :sha
+ t.text :message
+ t.string :username
+ t.string :email
+
+ t.timestamps
+ end
+ end
+end
diff --git a/db/migrate/20140607100342_add_origin_to_tip.rb b/db/migrate/20140607100342_add_origin_to_tip.rb
new file mode 100644
index 00000000..37cce1f7
--- /dev/null
+++ b/db/migrate/20140607100342_add_origin_to_tip.rb
@@ -0,0 +1,5 @@
+class AddOriginToTip < ActiveRecord::Migration
+ def change
+ add_reference :tips, :origin, index: true, polymorphic: true
+ end
+end
diff --git a/db/migrate/20140608120038_install_commontator.commontator.rb b/db/migrate/20140608120038_install_commontator.commontator.rb
new file mode 100644
index 00000000..17cd44c7
--- /dev/null
+++ b/db/migrate/20140608120038_install_commontator.commontator.rb
@@ -0,0 +1,49 @@
+# This migration comes from commontator (originally 0)
+class InstallCommontator < ActiveRecord::Migration
+ def change
+ create_table 'commontator_comments' do |t|
+ t.string 'creator_type'
+ t.integer 'creator_id'
+ t.string 'editor_type'
+ t.integer 'editor_id'
+ t.integer 'thread_id', :null => false
+ t.text 'body', :null => false
+ t.datetime 'deleted_at'
+
+ t.integer :cached_votes_up, :default => 0
+ t.integer :cached_votes_down, :default => 0
+
+ t.timestamps
+ end
+
+ add_index :commontator_comments, [:creator_id, :creator_type, :thread_id], :name => 'index_commontator_comments_on_c_id_and_c_type_and_t_id'
+ add_index :commontator_comments, :thread_id
+
+ add_index :commontator_comments, :cached_votes_up
+ add_index :commontator_comments, :cached_votes_down
+
+ create_table 'commontator_subscriptions' do |t|
+ t.string 'subscriber_type', :null => false
+ t.integer 'subscriber_id', :null => false
+ t.integer 'thread_id', :null => false
+
+ t.timestamps
+ end
+
+ add_index :commontator_subscriptions, [:subscriber_id, :subscriber_type, :thread_id], :unique => true, :name => 'index_commontator_subscriptions_on_s_id_and_s_type_and_t_id'
+ add_index :commontator_subscriptions, :thread_id
+
+ create_table 'commontator_threads' do |t|
+ t.string 'commontable_type'
+ t.integer 'commontable_id'
+ t.datetime 'closed_at'
+ t.string 'closer_type'
+ t.integer 'closer_id'
+
+ t.timestamps
+ end
+
+ add_index :commontator_threads, [:commontable_id, :commontable_type], :unique => true, :name => 'index_commontator_threads_on_c_id_and_c_type'
+ end
+end
+
diff --git a/db/migrate/20140608131519_rename_origin_to_reason.rb b/db/migrate/20140608131519_rename_origin_to_reason.rb
new file mode 100644
index 00000000..654d381c
--- /dev/null
+++ b/db/migrate/20140608131519_rename_origin_to_reason.rb
@@ -0,0 +1,6 @@
+class RenameOriginToReason < ActiveRecord::Migration
+ def change
+ rename_column :tips, :origin_type, :reason_type
+ rename_column :tips, :origin_id, :reason_id
+ end
+end
diff --git a/db/migrate/20140609122234_drop_version.rb b/db/migrate/20140609122234_drop_version.rb
new file mode 100644
index 00000000..ec3a2b58
--- /dev/null
+++ b/db/migrate/20140609122234_drop_version.rb
@@ -0,0 +1,19 @@
+class DropVersion < ActiveRecord::Migration
+ def change
+ drop_table :versions
+ end
+
+ def down
+ create_table "versions", force: true do |t|
+ t.string "item_type", null: false
+ t.integer "item_id", null: false
+ t.string "event", null: false
+ t.string "whodunnit"
+ t.text "object"
+ t.datetime "created_at"
+ t.text "object_changes"
+ end
+
+ add_index "versions", ["item_type", "item_id"], name: "index_versions_on_item_type_and_item_id"
+ end
+end
diff --git a/db/migrate/20140609122440_create_record_changes.rb b/db/migrate/20140609122440_create_record_changes.rb
new file mode 100644
index 00000000..e848c33a
--- /dev/null
+++ b/db/migrate/20140609122440_create_record_changes.rb
@@ -0,0 +1,11 @@
+class CreateRecordChanges < ActiveRecord::Migration
+ def change
+ create_table :record_changes do |t|
+ t.belongs_to :record, index: true, polymorphic: true
+ t.belongs_to :user
+ t.text :raw_state, limit: 1.megabyte
+
+ t.timestamps
+ end
+ end
+end
diff --git a/db/migrate/20140615122107_create_donation_addresses.rb b/db/migrate/20140615122107_create_donation_addresses.rb
new file mode 100644
index 00000000..1a501499
--- /dev/null
+++ b/db/migrate/20140615122107_create_donation_addresses.rb
@@ -0,0 +1,11 @@
+class CreateDonationAddresses < ActiveRecord::Migration
+ def change
+ create_table :donation_addresses do |t|
+ t.belongs_to :project, index: true
+ t.string :sender_address
+ t.string :donation_address
+
+ t.timestamps
+ end
+ end
+end
diff --git a/db/migrate/20140615124857_add_donation_address_to_deposit.rb b/db/migrate/20140615124857_add_donation_address_to_deposit.rb
new file mode 100644
index 00000000..1d46b02f
--- /dev/null
+++ b/db/migrate/20140615124857_add_donation_address_to_deposit.rb
@@ -0,0 +1,5 @@
+class AddDonationAddressToDeposit < ActiveRecord::Migration
+ def change
+ add_reference :deposits, :donation_address, index: true
+ end
+end
diff --git a/db/migrate/20140616055815_add_user_to_collaborator.rb b/db/migrate/20140616055815_add_user_to_collaborator.rb
new file mode 100644
index 00000000..5b508145
--- /dev/null
+++ b/db/migrate/20140616055815_add_user_to_collaborator.rb
@@ -0,0 +1,5 @@
+class AddUserToCollaborator < ActiveRecord::Migration
+ def change
+ add_reference :collaborators, :user, index: true
+ end
+end
diff --git a/db/migrate/20140616060504_convert_collaborator_nick_names_to_user.rb b/db/migrate/20140616060504_convert_collaborator_nick_names_to_user.rb
new file mode 100644
index 00000000..66417375
--- /dev/null
+++ b/db/migrate/20140616060504_convert_collaborator_nick_names_to_user.rb
@@ -0,0 +1,8 @@
+class ConvertCollaboratorNickNamesToUser < ActiveRecord::Migration
+ def up
+ execute("UPDATE collaborators SET user_id = (SELECT id FROM users WHERE users.nickname = collaborators.login LIMIT 1)")
+ end
+
+ def down
+ end
+end
diff --git a/db/migrate/20140704060602_add_disabled_to_user.rb b/db/migrate/20140704060602_add_disabled_to_user.rb
new file mode 100644
index 00000000..ef6551db
--- /dev/null
+++ b/db/migrate/20140704060602_add_disabled_to_user.rb
@@ -0,0 +1,6 @@
+class AddDisabledToUser < ActiveRecord::Migration
+ def change
+ add_column :users, :disabled, :boolean, default: false
+ add_index :users, :disabled
+ end
+end
diff --git a/db/migrate/20140706075813_remove_git_hub_id_from_project.rb b/db/migrate/20140706075813_remove_git_hub_id_from_project.rb
new file mode 100644
index 00000000..82ed3630
--- /dev/null
+++ b/db/migrate/20140706075813_remove_git_hub_id_from_project.rb
@@ -0,0 +1,5 @@
+class RemoveGitHubIdFromProject < ActiveRecord::Migration
+ def change
+ remove_column :projects, :github_id, :string
+ end
+end
diff --git a/db/migrate/20140714074128_add_identifier_to_user.rb b/db/migrate/20140714074128_add_identifier_to_user.rb
new file mode 100644
index 00000000..74e4afa3
--- /dev/null
+++ b/db/migrate/20140714074128_add_identifier_to_user.rb
@@ -0,0 +1,17 @@
+class AddIdentifierToUser < ActiveRecord::Migration
+ def up
+ add_column :users, :identifier, :string
+ execute("SELECT id FROM users WHERE identifier IS NULL").each do |row|
+ id = row["id"]
+ charset = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'.split(//)
+ identifier = (0...12).map { charset.sample }.join
+ execute "UPDATE users SET identifier='#{identifier}' WHERE id = #{id}"
+ end
+ change_column :users, :identifier, :string, null: false
+ add_index :users, :identifier, unique: true
+ end
+
+ def down
+ remove_column :users, :identifier
+ end
+end
diff --git a/db/migrate/20180121184454_add_stake_mint_to_project.rb b/db/migrate/20180121184454_add_stake_mint_to_project.rb
new file mode 100644
index 00000000..a4fd76e7
--- /dev/null
+++ b/db/migrate/20180121184454_add_stake_mint_to_project.rb
@@ -0,0 +1,5 @@
+class AddStakeMintToProject < ActiveRecord::Migration
+ def change
+ add_column :projects, :stake_mint_amount, :integer, limit: 8
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index aa2be27e..f47ff206 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,86 +11,227 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20140102095035) do
+ActiveRecord::Schema.define(version: 20180121184454) do
- create_table "deposits", force: true do |t|
+ # These are extensions that must be enabled in order to support this database
+ enable_extension "plpgsql"
+
+ create_table "cold_storage_transfers", force: :cascade do |t|
t.integer "project_id"
- t.string "txid"
+ t.integer "amount", limit: 8
+ t.string "address", limit: 255
+ t.string "txid", limit: 255
t.integer "confirmations"
- t.integer "duration", default: 2592000
- t.integer "paid_out", limit: 8
- t.datetime "paid_out_at"
t.datetime "created_at"
t.datetime "updated_at"
- t.integer "amount", limit: 8
+ t.integer "fee"
end
- add_index "deposits", ["project_id"], name: "index_deposits_on_project_id"
+ add_index "cold_storage_transfers", ["project_id"], name: "index_cold_storage_transfers_on_project_id", using: :btree
- create_table "projects", force: true do |t|
- t.string "url"
- t.string "bitcoin_address"
+ create_table "collaborators", force: :cascade do |t|
+ t.integer "project_id"
+ t.string "login", limit: 255
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.integer "user_id"
+ end
+
+ add_index "collaborators", ["project_id"], name: "index_collaborators_on_project_id", using: :btree
+ add_index "collaborators", ["user_id"], name: "index_collaborators_on_user_id", using: :btree
+
+ create_table "commits", force: :cascade do |t|
+ t.integer "project_id"
+ t.string "sha", limit: 255
+ t.text "message"
+ t.string "username", limit: 255
+ t.string "email", limit: 255
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ end
+
+ add_index "commits", ["project_id"], name: "index_commits_on_project_id", using: :btree
+
+ create_table "commontator_comments", force: :cascade do |t|
+ t.string "creator_type", limit: 255
+ t.integer "creator_id"
+ t.string "editor_type", limit: 255
+ t.integer "editor_id"
+ t.integer "thread_id", null: false
+ t.text "body", null: false
+ t.datetime "deleted_at"
+ t.integer "cached_votes_up", default: 0
+ t.integer "cached_votes_down", default: 0
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ end
+
+ add_index "commontator_comments", ["cached_votes_down"], name: "index_commontator_comments_on_cached_votes_down", using: :btree
+ add_index "commontator_comments", ["cached_votes_up"], name: "index_commontator_comments_on_cached_votes_up", using: :btree
+ add_index "commontator_comments", ["creator_id", "creator_type", "thread_id"], name: "index_commontator_comments_on_c_id_and_c_type_and_t_id", using: :btree
+ add_index "commontator_comments", ["thread_id"], name: "index_commontator_comments_on_thread_id", using: :btree
+
+ create_table "commontator_subscriptions", force: :cascade do |t|
+ t.string "subscriber_type", limit: 255, null: false
+ t.integer "subscriber_id", null: false
+ t.integer "thread_id", null: false
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ end
+
+ add_index "commontator_subscriptions", ["subscriber_id", "subscriber_type", "thread_id"], name: "index_commontator_subscriptions_on_s_id_and_s_type_and_t_id", unique: true, using: :btree
+ add_index "commontator_subscriptions", ["thread_id"], name: "index_commontator_subscriptions_on_thread_id", using: :btree
+
+ create_table "commontator_threads", force: :cascade do |t|
+ t.string "commontable_type", limit: 255
+ t.integer "commontable_id"
+ t.datetime "closed_at"
+ t.string "closer_type", limit: 255
+ t.integer "closer_id"
t.datetime "created_at"
t.datetime "updated_at"
- t.string "name"
- t.string "full_name"
- t.string "source_full_name"
- t.string "description"
- t.integer "watchers_count"
- t.string "language"
- t.string "last_commit"
- t.integer "available_amount_cache"
end
- create_table "sendmanies", force: true do |t|
- t.string "txid"
+ add_index "commontator_threads", ["commontable_id", "commontable_type"], name: "index_commontator_threads_on_c_id_and_c_type", unique: true, using: :btree
+
+ create_table "deposits", force: :cascade do |t|
+ t.integer "project_id"
+ t.string "txid", limit: 255
+ t.integer "confirmations"
+ t.integer "duration", default: 2592000
+ t.integer "paid_out", limit: 8
+ t.datetime "paid_out_at"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.integer "amount", limit: 8
+ t.integer "donation_address_id"
+ end
+
+ add_index "deposits", ["donation_address_id"], name: "index_deposits_on_donation_address_id", using: :btree
+ add_index "deposits", ["project_id"], name: "index_deposits_on_project_id", using: :btree
+
+ create_table "distributions", force: :cascade do |t|
+ t.string "txid", limit: 255
t.text "data"
- t.string "result"
+ t.string "result", limit: 255
t.boolean "is_error"
t.datetime "created_at"
t.datetime "updated_at"
+ t.integer "project_id"
+ t.integer "fee"
+ t.datetime "sent_at"
+ end
+
+ add_index "distributions", ["project_id"], name: "index_distributions_on_project_id", using: :btree
+
+ create_table "donation_addresses", force: :cascade do |t|
+ t.integer "project_id"
+ t.string "sender_address", limit: 255
+ t.string "donation_address", limit: 255
+ t.datetime "created_at"
+ t.datetime "updated_at"
end
- create_table "tips", force: true do |t|
+ add_index "donation_addresses", ["project_id"], name: "index_donation_addresses_on_project_id", using: :btree
+
+ create_table "projects", force: :cascade do |t|
+ t.string "url", limit: 255
+ t.string "bitcoin_address", limit: 255
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.string "name", limit: 255
+ t.string "full_name", limit: 255
+ t.string "source_full_name", limit: 255
+ t.text "description"
+ t.integer "watchers_count"
+ t.string "language", limit: 255
+ t.string "last_commit", limit: 255
+ t.integer "available_amount_cache", limit: 8
+ t.string "address_label", limit: 255
+ t.boolean "hold_tips", default: true
+ t.string "cold_storage_withdrawal_address", limit: 255
+ t.boolean "disabled", default: false
+ t.integer "account_balance", limit: 8
+ t.string "disabled_reason", limit: 255
+ t.text "detailed_description"
+ t.integer "stake_mint_amount", limit: 8
+ end
+
+ create_table "record_changes", force: :cascade do |t|
+ t.integer "record_id"
+ t.string "record_type", limit: 255
+ t.integer "user_id"
+ t.text "raw_state"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ end
+
+ add_index "record_changes", ["record_id", "record_type"], name: "index_record_changes_on_record_id_and_record_type", using: :btree
+
+ create_table "tipping_policies_texts", force: :cascade do |t|
+ t.integer "project_id"
+ t.integer "user_id"
+ t.text "text"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ end
+
+ add_index "tipping_policies_texts", ["project_id"], name: "index_tipping_policies_texts_on_project_id", using: :btree
+ add_index "tipping_policies_texts", ["user_id"], name: "index_tipping_policies_texts_on_user_id", using: :btree
+
+ create_table "tips", force: :cascade do |t|
t.integer "user_id"
- t.integer "amount", limit: 8
- t.integer "sendmany_id"
+ t.integer "amount", limit: 8
+ t.integer "distribution_id"
t.datetime "created_at"
t.datetime "updated_at"
- t.string "commit"
+ t.string "commit", limit: 255
t.integer "project_id"
t.datetime "refunded_at"
+ t.string "commit_message", limit: 255
+ t.string "comment", limit: 255
+ t.integer "reason_id"
+ t.string "reason_type", limit: 255
end
- add_index "tips", ["project_id"], name: "index_tips_on_project_id"
- add_index "tips", ["sendmany_id"], name: "index_tips_on_sendmany_id"
- add_index "tips", ["user_id"], name: "index_tips_on_user_id"
+ add_index "tips", ["distribution_id"], name: "index_tips_on_distribution_id", using: :btree
+ add_index "tips", ["project_id"], name: "index_tips_on_project_id", using: :btree
+ add_index "tips", ["reason_id", "reason_type"], name: "index_tips_on_reason_id_and_reason_type", using: :btree
+ add_index "tips", ["user_id"], name: "index_tips_on_user_id", using: :btree
- create_table "users", force: true do |t|
- t.string "email", default: "", null: false
- t.string "encrypted_password", default: "", null: false
- t.string "reset_password_token"
+ create_table "users", force: :cascade do |t|
+ t.string "email", limit: 255, default: "", null: false
+ t.string "encrypted_password", limit: 255, default: "", null: false
+ t.string "reset_password_token", limit: 255
t.datetime "reset_password_sent_at"
t.datetime "remember_created_at"
- t.integer "sign_in_count", default: 0, null: false
+ t.integer "sign_in_count", default: 0, null: false
t.datetime "current_sign_in_at"
t.datetime "last_sign_in_at"
- t.string "current_sign_in_ip"
- t.string "last_sign_in_ip"
+ t.string "current_sign_in_ip", limit: 255
+ t.string "last_sign_in_ip", limit: 255
t.datetime "created_at"
t.datetime "updated_at"
- t.string "nickname"
- t.string "name"
- t.string "image"
- t.string "bitcoin_address"
- t.string "login_token"
+ t.string "nickname", limit: 255
+ t.string "name", limit: 255
+ t.string "image", limit: 255
+ t.string "bitcoin_address", limit: 255
+ t.string "login_token", limit: 255
t.boolean "unsubscribed"
t.datetime "notified_at"
- t.integer "commits_count", default: 0
- t.integer "withdrawn_amount", limit: 8, default: 0
+ t.integer "commits_count", default: 0
+ t.integer "withdrawn_amount", limit: 8, default: 0
+ t.string "confirmation_token", limit: 255
+ t.datetime "confirmed_at"
+ t.datetime "confirmation_sent_at"
+ t.string "unconfirmed_email", limit: 255
+ t.boolean "disabled", default: false
+ t.string "identifier", limit: 255, null: false
end
- add_index "users", ["email"], name: "index_users_on_email", unique: true
- add_index "users", ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
+ add_index "users", ["disabled"], name: "index_users_on_disabled", using: :btree
+ add_index "users", ["email"], name: "index_users_on_email", unique: true, using: :btree
+ add_index "users", ["identifier"], name: "index_users_on_identifier", unique: true, using: :btree
+ add_index "users", ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true, using: :btree
end
diff --git a/features/change_github.feature b/features/change_github.feature
new file mode 100644
index 00000000..401be459
--- /dev/null
+++ b/features/change_github.feature
@@ -0,0 +1,38 @@
+Feature: Fundraiser can change the GitHub repository linked to a project
+ Scenario: A project not holding tips changes github repository
+ Given a project
+ And the project does not hold tips
+ And the project GitHub name is "foo/bar"
+ And the commits on GitHub for project "foo/bar" are
+ | sha | author | email |
+ | 123 | bob | bobby@example.com |
+ | abc | alice | alicia@example.com |
+ | 333 | bob | bobby@example.com |
+ And our fee is "0"
+ And a deposit of "500"
+ Given a GitHub user "bob" who has set his address to "mxWfjaZJTNN5QKeZZYQ5HW3vgALFBsnuG1"
+ And a GitHub user "alice" who has set his address to "mi9SLroAgc8eUNuLwnZmdyqWdShbNtvr3n"
+
+ When the project tips are built from commits
+ Then the project should have these tips:
+ | commit | amount |
+ | 123 | 5.0 |
+ | abc | 4.95 |
+ | 333 | 4.9005 |
+
+ When the project GitHub name is "baz/foo"
+ And the commits on GitHub for project "baz/foo" are
+ | sha | author | email |
+ | aaa | bob | bobby@example.com |
+ | bbb | alice | alicia@example.com |
+ | ccc | bob | bobby@example.com |
+ And the project tips are built from commits
+ Then the project should have these tips:
+ | commit | amount |
+ | 123 | 5.0 |
+ | abc | 4.95 |
+ | 333 | 4.9005 |
+ | aaa | 4.851495 |
+ | bbb | 4.802981 |
+ | ccc | 4.754951 |
+
diff --git a/features/cold_storage.feature b/features/cold_storage.feature
new file mode 100644
index 00000000..9e8c132d
--- /dev/null
+++ b/features/cold_storage.feature
@@ -0,0 +1,51 @@
+Feature: Some funds are transfered to cold storage
+ Background:
+ Given a project
+ And our fee is "0.01"
+ And the project address is "mqEtf1CcGtAmoVRHENBVmBRpYppoEcA8LH"
+ And the project cold storage withdrawal address is "n1g6mxaEpMb6cERcS4bGhmJPjxKc3msvni"
+ And the cold storage addresses are
+ | mpjDVmvCgsi2WW9qZJDQN6WgpDTP5iGbpD |
+ | mr6HkUBp3iUqH6JuvD33banN4vZkifvTGD |
+
+ Scenario: A project receives funds to its non cold storage address
+ When there's a new incoming transaction of "50" to address "mqEtf1CcGtAmoVRHENBVmBRpYppoEcA8LH" on the project account
+ And the project balance is updated
+ Then the project balance should be "49.5"
+ And the project amount in cold storage should be "0"
+
+ Scenario: A project receives funds to its cold storage address
+ When there's a new incoming transaction of "50" to address "n1g6mxaEpMb6cERcS4bGhmJPjxKc3msvni" on the project account
+ And the project balance is updated
+ Then the project balance should be "0"
+ And the project amount in cold storage should be "-50"
+
+ Scenario: A project receives funds to an unknown address
+ When there's a new incoming transaction of "50" to address "mmoS6KKr4Q4v6VQcTQmSWGPgBS8mhJ9f74" on the project account
+ Then updating the project balance should raise an error
+ And the project balance should be "0"
+ And the project amount in cold storage should be "0"
+
+ Scenario: Some funds are sent to cold storage
+ When there's a new outgoing transaction of "50" to address "mpjDVmvCgsi2WW9qZJDQN6WgpDTP5iGbpD" on the project account
+ And the project balance is updated
+ Then the project balance should be "0"
+ And the project amount in cold storage should be "50"
+
+ Scenario: Unconfirmed transactions are not counted
+ When there's a new incoming transaction of "50" to address "mqEtf1CcGtAmoVRHENBVmBRpYppoEcA8LH" on the project account with 0 confirmations
+ And there's a new incoming transaction of "10" to address "n1g6mxaEpMb6cERcS4bGhmJPjxKc3msvni" on the project account with 0 confirmations
+ And there's a new outgoing transaction of "20" to address "mpjDVmvCgsi2WW9qZJDQN6WgpDTP5iGbpD" on the project account with 0 confirmations
+ And the project balance is updated
+ Then the project balance should be "0"
+ And the project amount in cold storage should be "0"
+
+ Scenario: Sending funds to cold storage
+ When "50" coins of the project funds are sent to cold storage
+ Then there should be an outgoing transaction of "50" to address "mpjDVmvCgsi2WW9qZJDQN6WgpDTP5iGbpD" on the project account
+
+ Scenario: Cold storage withdrawal address is created at balance update time if it doesn't exist
+ Given the project has no cold storage withdrawal address
+ When the project balance is updated
+ Then the project should have a cold storage withdrawal address
+ And the project cold storage withdrawal address should be linked to its account
diff --git a/features/commit_from_known_nickname.feature b/features/commit_from_known_nickname.feature
new file mode 100644
index 00000000..4365848f
--- /dev/null
+++ b/features/commit_from_known_nickname.feature
@@ -0,0 +1,16 @@
+Feature: A commit with an identified GitHub nickname should be sent to the right user if he exists
+ Scenario:
+ Given a project "a"
+ And our fee is "0"
+ And a deposit of "500"
+ And an user "yugo"
+ And the email of "yugo" is "yugo1@example.com"
+ And the last known commit is "A"
+ And a new commit "B" with parent "A"
+ And the author of commit "B" is "yugo"
+ And the email of commit "B" is "yugo2@example.com"
+
+ When the new commits are read
+ Then there should be a tip of "5" for commit "B"
+ And the tip for commit "B" is for user "yugo"
+ And there should be no user with email "yugo2@example.com"
diff --git a/features/create_project.feature b/features/create_project.feature
new file mode 100644
index 00000000..df05546a
--- /dev/null
+++ b/features/create_project.feature
@@ -0,0 +1,124 @@
+Feature: An user can create a project, linked with GitHub or not.
+ Scenario: Create a project simple project
+ Given I'm logged in on GitHub as "seldon"
+
+ When I visit the home page
+ And I click on "Create a project"
+ And I click on "Sign in with Github"
+ Then I should see "New project"
+
+ When I fill "Name" with "Project Foo"
+ And I fill "Description" with "The foo project"
+ And I click on "Save"
+ Then I should see "The project was created"
+ And there should be a project "Project Foo"
+ And the description of the project should be
+ """
+ The foo project
+ """
+ And the project should hold tips
+ And the project single collaborators should be "seldon"
+ And the project address label should be "peer4commit-1"
+ And the project donation address should be the same as account "peer4commit-1"
+ And I should be on the project page
+
+ Scenario: Create a project without name
+ Given I'm logged in on GitHub as "seldon"
+
+ When I visit the home page
+ And I click on "Create a project"
+ And I click on "Sign in with Github"
+ Then I should see "New project"
+
+ And I click on "Save"
+ Then I should see "Please fix"
+ And there should be no project
+
+ Scenario: Create a project without name
+ Given I'm logged in on GitHub as "seldon"
+
+ When I visit the home page
+ And I click on "Create a project"
+ And I click on "Sign in with Github"
+ Then I should see "New project"
+
+ And I click on "Save"
+ Then I should see "Please fix"
+ And there should be no project
+
+ Scenario: Create a project linked to a GitHub project
+ Given I'm logged in on GitHub as "seldon"
+
+ When I visit the home page
+ And I click on "Create a project"
+ And I click on "Sign in with Github"
+ Then I should see "New project"
+
+ When I fill "Name" with "Project Foo"
+ And I fill "Description" with "The foo project"
+ And I fill "GitHub URL" with "http://github.com/sigmike/peer4commit"
+ And I click on "Save"
+ Then I should see "The project was created"
+ And there should be a project "Project Foo"
+ And the GitHub name of the project should be "sigmike/peer4commit"
+ And the project should hold tips
+ And the project single collaborators should be "seldon"
+
+ Scenario: Create multiple projects linked to the same GitHub project
+ Given I'm logged in on GitHub as "seldon"
+
+ When I visit the home page
+ And I click on "Create a project"
+ And I click on "Sign in with Github"
+ Then I should see "New project"
+
+ When I fill "Name" with "Project Foo"
+ And I fill "Description" with "The foo project"
+ And I fill "GitHub URL" with "http://github.com/sigmike/peer4commit"
+ And I click on "Save"
+ Then I should see "The project was created"
+
+ When I visit the home page
+ And I click on "Create a project"
+ Then I should see "New project"
+
+ When I fill "Name" with "Project Bar"
+ And I fill "Description" with "The bar project"
+ And I fill "GitHub URL" with "http://github.com/sigmike/peer4commit"
+ And I click on "Save"
+ Then I should see "The project was created"
+
+ Scenario: Create a project as an email user
+ When I visit the home page
+ And I click on "Create a project"
+ And I click on "Sign up"
+ And I fill "Email" with "bob@example.com"
+ And I fill "Password" with "password"
+ And I fill "Password confirmation" with "password"
+ And I click on "Sign up"
+ Then I should see "A message with a confirmation link has been sent to your email address"
+ And an email should have been sent to "bob@example.com"
+ When I click on the "Confirm my account" link in the email
+ Then I should see "Your account was successfully confirmed"
+ When I fill "Email" with "bob@example.com"
+ And I fill "Password" with "password"
+ And I click on "Sign in" in the sign in form
+ Then I should see "Signed in successfully"
+ When I click on "Create a project"
+ Then I should see "New project"
+
+ When I fill "Name" with "Project Foo"
+ And I fill "Description" with "The foo project"
+ And I click on "Save"
+ Then I should see "The project was created"
+ And there should be a project "Project Foo"
+ And the description of the project should be
+ """
+ The foo project
+ """
+ And the project should hold tips
+ And the project single collaborators should be "bob@example.com"
+ And the project address label should be "peer4commit-1"
+ And the project donation address should be the same as account "peer4commit-1"
+ And I should be on the project page
+
diff --git a/features/distribute_to_commits.feature b/features/distribute_to_commits.feature
new file mode 100644
index 00000000..8a2ff98e
--- /dev/null
+++ b/features/distribute_to_commits.feature
@@ -0,0 +1,107 @@
+Feature: A project collaborator distribute to commit authors
+ @javascript
+ Scenario:
+ Given a project "a"
+ And the project collaborators are:
+ | seldon |
+ | daneel |
+ And our fee is "0"
+ And a deposit of "500"
+ And the last known commit is "AAA"
+ And a new commit "BBB" with parent "AAA"
+ And a new commit "CCC" with parent "BBB"
+ And the author of commit "BBB" is "yugo"
+ And the message of commit "BBB" is "Tiny change"
+ And the author of commit "CCC" is "gaal"
+ And a GitHub user "yugo" who has set his address to "mxWfjaZJTNN5QKeZZYQ5HW3vgALFBsnuG1"
+ And a GitHub user "gaal" who has set his address to "mi9SLroAgc8eUNuLwnZmdyqWdShbNtvr3n"
+
+ Given I'm logged in as "seldon"
+ And I go to the project page
+ And I click on "Edit project"
+ And I uncheck "Automatically send 1% of the balance to each commit added to the default branch of the GitHub project"
+ And I click on "Save"
+ Then I should see "The project has been updated"
+
+ When the new commits are read
+
+ When I go to the project page
+ And I click on "New distribution"
+ And I select the commit recipients "Commits not rewarded"
+ Then the distribution form should have these recipients:
+ | recipient | reason | amount |
+ | yugo | Commit BBB: Tiny change | |
+ | gaal | Commit CCC: Some changes | |
+
+ And I fill the amount to "yugo" with "0.5"
+ And I remove the recipient "gaal"
+ And I click on "Save"
+
+ Then I should see these distribution lines:
+ | recipient | reason | address | amount | percentage |
+ | yugo | Commit BBB: Tiny change | mxWfjaZJTNN5QKeZZYQ5HW3vgALFBsnuG1 | 0.5 | 100 |
+ And I should see "Total amount: 0.50 PPC"
+ When the new commits are read
+
+ When I go to the project page
+ And I click on "New distribution"
+ And I select the commit recipients "Commits not rewarded"
+ Then the distribution form should have these recipients:
+ | recipient | reason | amount |
+ | gaal | Commit CCC: Some changes | |
+
+ @javascript
+ Scenario: Distribute to commits not linked to a GitHub account
+ Given a project "a" holding tips
+ And the project single collaborator is "seldon"
+ And our fee is "0"
+ And a deposit of "500"
+ And the last known commit is "AAA"
+ And a new commit "BBB" with parent "AAA"
+ And the author of commit "BBB" is the non identified email "yugo@example.com"
+ And the message of commit "BBB" is "Tiny change"
+
+ Given I'm logged in as "seldon"
+ When the new commits are read
+ And I go to the project page
+ And I click on "New distribution"
+ And I select the commit recipients "Commits not rewarded"
+ Then the distribution form should have these recipients:
+ | recipient | reason | amount |
+
+ @javascript
+ Scenario: Distribute to a single commit
+ Given a project "a" holding tips
+ And the project single collaborator is "seldon"
+ And our fee is "0"
+ And a deposit of "500"
+ And the last known commit is "529a8eec77e455781eed81b0b2f351ec65d8eb95"
+ And a new commit "170ed604f287b9fec397389d0b1b3f7d15b82276" with parent "529a8eec77e455781eed81b0b2f351ec65d8eb95"
+ And a new commit "1329394df2595739d652528d48fe6db66c67e1e8" with parent "170ed604f287b9fec397389d0b1b3f7d15b82276"
+ And the author of commit "170ed604f287b9fec397389d0b1b3f7d15b82276" is "yugo"
+ And the message of commit "170ed604f287b9fec397389d0b1b3f7d15b82276" is "Tiny change"
+ And the author of commit "1329394df2595739d652528d48fe6db66c67e1e8" is "gaal"
+ And a GitHub user "yugo" who has set his address to "mxWfjaZJTNN5QKeZZYQ5HW3vgALFBsnuG1"
+ And a GitHub user "gaal" who has set his address to "mi9SLroAgc8eUNuLwnZmdyqWdShbNtvr3n"
+
+ Given I'm logged in as "seldon"
+ When the new commits are read
+ And I go to the project page
+ And I click on "New distribution"
+ And I add the commit "170ed604f287b9fec397389d0b1b3f7d15b82276" to the recipients
+ Then the distribution form should have these recipients:
+ | recipient | reason | amount |
+ | yugo | Commit 170ed604f2: Tiny change | |
+ When I add the commit "1329394df" to the recipients
+ Then the distribution form should have these recipients:
+ | recipient | reason | amount |
+ | yugo | Commit 170ed604f2: Tiny change | |
+ | gaal | Commit 1329394df2: Some changes | |
+
+ And I fill the amount to "yugo" with "0.5"
+ And I click on "Save"
+
+ Then I should see these distribution lines:
+ | recipient | reason | address | amount | percentage |
+ | yugo | Commit 170ed604f2: Tiny change | mxWfjaZJTNN5QKeZZYQ5HW3vgALFBsnuG1 | 0.5 | |
+ | gaal | Commit 1329394df2: Some changes | mi9SLroAgc8eUNuLwnZmdyqWdShbNtvr3n | Undecided | |
diff --git a/features/distribute_to_user_identifier.feature b/features/distribute_to_user_identifier.feature
new file mode 100644
index 00000000..fba6437c
--- /dev/null
+++ b/features/distribute_to_user_identifier.feature
@@ -0,0 +1,28 @@
+Feature: Distribute funds to an user identifier
+
+ @javascript
+ Scenario:
+ Given an user with email "bob@example.com"
+ And the user with email "bob@example.com" has set his address to "mi9SLroAgc8eUNuLwnZmdyqWdShbNtvr3n"
+
+ Given a project managed by "alice"
+ And our fee is "0"
+ And a deposit of "500"
+
+ Given I'm logged in as "alice"
+ And I go to the project page
+ And I click on "New distribution"
+ And I add the user with email "bob@example.com" through his identifier to the recipients
+ And I fill the amount to "
" with "10"
+ And I save the distribution
+
+ Then I should see these distribution lines:
+ | recipient | address | amount | percentage |
+ | | mi9SLroAgc8eUNuLwnZmdyqWdShbNtvr3n | 10 | 100.0 |
+
+ When I click on "Send the transaction"
+ Then I should see "Transaction sent"
+ And these amounts should have been sent from the account of the project:
+ | address | amount |
+ | mi9SLroAgc8eUNuLwnZmdyqWdShbNtvr3n | 10.0 |
+ And the project balance should be "490.00"
diff --git a/features/distribution.feature b/features/distribution.feature
new file mode 100644
index 00000000..e45b883b
--- /dev/null
+++ b/features/distribution.feature
@@ -0,0 +1,398 @@
+Feature: Fundraisers can distribute funds
+ @javascript
+ Scenario: Send distribution to a single user who has set his address
+ Given a GitHub user "bob" who has set his address to "mxWfjaZJTNN5QKeZZYQ5HW3vgALFBsnuG1"
+
+ Given a project managed by "alice"
+ And our fee is "0"
+ And a deposit of "500"
+
+ Given I'm logged in as "alice"
+ And I go to the project page
+ And I click on "New distribution"
+ And I add the GitHub user "bob" to the recipients
+ And I fill the amount to "bob" with "10"
+ And I save the distribution
+
+ Then I should see these distribution lines:
+ | recipient | address | amount | percentage |
+ | bob | mxWfjaZJTNN5QKeZZYQ5HW3vgALFBsnuG1 | 10 | 100 |
+ And I should see "Total amount: 10.00 PPC"
+
+ When the tipper is started
+ Then no coins should have been sent
+
+ Given the current time is "2014-03-01 12:35:02 UTC"
+ When I click on "Send the transaction"
+ Then I should see "Transaction sent"
+ And I should see "Transaction sent on Sat, 01 Mar 2014 12:35:02 +0000"
+ And these amounts should have been sent from the account of the project:
+ | address | amount |
+ | mxWfjaZJTNN5QKeZZYQ5HW3vgALFBsnuG1 | 10.0 |
+ And the project balance should be "490"
+
+ @javascript
+ Scenario: Send distribution to multiple users
+ Given a GitHub user "bob" who has set his address to "mxWfjaZJTNN5QKeZZYQ5HW3vgALFBsnuG1"
+ And a GitHub user "carol" who has set his address to "mi9SLroAgc8eUNuLwnZmdyqWdShbNtvr3n"
+
+ Given a project managed by "alice"
+ And our fee is "0"
+ And a deposit of "500"
+
+ Given I'm logged in as "alice"
+ And I go to the project page
+ And I click on "New distribution"
+ And I add the GitHub user "bob" to the recipients
+ And I add the GitHub user "carol" to the recipients
+ And I fill the amount to "bob" with "10"
+ And I fill the amount to "carol" with "13.56"
+ And I save the distribution
+
+ Then I should see these distribution lines:
+ | recipient | address | amount | percentage |
+ | bob | mxWfjaZJTNN5QKeZZYQ5HW3vgALFBsnuG1 | 10 | 42.4 |
+ | carol | mi9SLroAgc8eUNuLwnZmdyqWdShbNtvr3n | 13.56 | 57.6 |
+ And I should see "Total amount: 23.56 PPC"
+
+ When the tipper is started
+ Then no coins should have been sent
+
+ When I click on "Send the transaction"
+ Then I should see "Transaction sent"
+ And these amounts should have been sent from the account of the project:
+ | address | amount |
+ | mxWfjaZJTNN5QKeZZYQ5HW3vgALFBsnuG1 | 10.0 |
+ | mi9SLroAgc8eUNuLwnZmdyqWdShbNtvr3n | 13.56 |
+ And the project balance should be "476.44"
+
+ @javascript
+ Scenario: Send to an unknown GitHub user
+ Given "bob" is an user registered on GitHub
+
+ Given a project managed by "alice"
+ And our fee is "0"
+ And a deposit of "500"
+
+ Given I'm logged in as "alice"
+ And I go to the project page
+ And I click on "New distribution"
+ And I add the GitHub user "bob" to the recipients
+ And I fill the amount to "bob" with "10"
+ And I save the distribution
+
+ Then I should see these distribution lines:
+ | recipient | address | amount | percentage |
+ | bob | | 10 | 100.0 |
+ And I should see "Total amount: 10.00 PPC"
+ And I should not see "Send the transaction"
+ And I should see "The transaction cannot be sent because some addresses are missing"
+
+ And no email should have been sent
+
+ When the tipper is started
+ Then no coins should have been sent
+
+ When I log out
+ And I log in as "bob"
+ And I set my address to "mnVba8qrpy5uxYD7dV4NZMQPWjgdt2QC1i"
+
+ When I log out
+ And I log in as "alice"
+ And I go to the project page
+ And I click on the last distribution
+ Then I should see these distribution lines:
+ | recipient | address | amount | percentage |
+ | bob | mnVba8qrpy5uxYD7dV4NZMQPWjgdt2QC1i | 10 | 100.0 |
+
+ When I click on "Send the transaction"
+ Then I should see "Transaction sent"
+ And these amounts should have been sent from the account of the project:
+ | address | amount |
+ | mnVba8qrpy5uxYD7dV4NZMQPWjgdt2QC1i | 10.0 |
+ And the project balance should be "490.00"
+
+ @javascript
+ Scenario: Send to an invalid GitHub user
+ Given a project managed by "alice"
+ And our fee is "0"
+ And a deposit of "500"
+
+ Given I'm logged in as "alice"
+ And I go to the project page
+ And I click on "New distribution"
+ And I add the GitHub user "bob" to the recipients
+ Then I should see "Invalid GitHub user"
+
+ When I save the distribution
+ Then I should see these distribution lines:
+ | recipient | address | amount | percentage |
+ And I should see "Total amount: 0.00 PPC"
+
+ @javascript
+ Scenario: Send to an user without an address
+ Given a GitHub user "bob"
+
+ Given a project managed by "alice"
+ And our fee is "0"
+ And a deposit of "500"
+
+ Given I'm logged in as "alice"
+ And I go to the project page
+ And I click on "New distribution"
+ And I add the GitHub user "bob" to the recipients
+ And I fill the amount to "bob" with "10"
+ And I save the distribution
+
+ Then I should see these distribution lines:
+ | recipient | address | amount | percentage |
+ | bob | | 10 | 100.0 |
+ And I should see "Total amount: 10.00 PPC"
+ And I should not see "Send the transaction"
+ And I should see "The transaction cannot be sent because some addresses are missing"
+
+ And no email should have been sent
+
+ When the tipper is started
+ Then no coins should have been sent
+
+ When I log out
+ And I log in as "bob"
+ And I set my address to "mnVba8qrpy5uxYD7dV4NZMQPWjgdt2QC1i"
+
+ When I log out
+ And I log in as "alice"
+ And I go to the project page
+ And I click on the last distribution
+ Then I should see these distribution lines:
+ | recipient | address | amount | percentage |
+ | bob | mnVba8qrpy5uxYD7dV4NZMQPWjgdt2QC1i | 10 | 100.0 |
+
+ When I click on "Send the transaction"
+ Then I should see "Transaction sent"
+ And these amounts should have been sent from the account of the project:
+ | address | amount |
+ | mnVba8qrpy5uxYD7dV4NZMQPWjgdt2QC1i | 10.0 |
+ And the project balance should be "490.00"
+
+ Scenario: Send to someone who doesn't want to be notified
+ Then pending
+
+ Scenario: Cannot login from email link if a password has already been set
+ Then pending
+
+ Scenario: Cannot login from an old email link
+ Then pending
+
+ @javascript
+ Scenario: Edit a distribution
+ Given a GitHub user "bob" who has set his address to "mxWfjaZJTNN5QKeZZYQ5HW3vgALFBsnuG1"
+
+ Given a project managed by "alice"
+ And our fee is "0"
+ And a deposit of "500"
+
+ Given I'm logged in as "alice"
+ And I go to the project page
+ And I click on "New distribution"
+ And I add the GitHub user "bob" to the recipients
+ And I fill the amount to "bob" with "10"
+ And I save the distribution
+
+ Then I should see these distribution lines:
+ | recipient | address | amount | percentage |
+ | bob | mxWfjaZJTNN5QKeZZYQ5HW3vgALFBsnuG1 | 10 | 100 |
+ And I should see "Total amount: 10.00 PPC"
+
+ Given a GitHub user "carol" who has set his address to "mi9SLroAgc8eUNuLwnZmdyqWdShbNtvr3n"
+
+ And I click on "Edit"
+ And I fill the amount to "bob" with "15"
+ And I add the GitHub user "carol" to the recipients
+ And I fill the amount to "carol" with "5"
+ And I save the distribution
+
+ Then I should see these distribution lines:
+ | recipient | address | amount | percentage |
+ | bob | mxWfjaZJTNN5QKeZZYQ5HW3vgALFBsnuG1 | 15 | 75.0 |
+ | carol | mi9SLroAgc8eUNuLwnZmdyqWdShbNtvr3n | 5 | 25.0 |
+
+ When I click on "Send the transaction"
+ Then I should see "Transaction sent"
+ And these amounts should have been sent from the account of the project:
+ | address | amount |
+ | mxWfjaZJTNN5QKeZZYQ5HW3vgALFBsnuG1 | 15.0 |
+ | mi9SLroAgc8eUNuLwnZmdyqWdShbNtvr3n | 5.0 |
+ And the project balance should be "480"
+
+ @javascript
+ Scenario: Send distribution with a comment
+ Given a GitHub user "bob" who has set his address to "mxWfjaZJTNN5QKeZZYQ5HW3vgALFBsnuG1"
+
+ Given a project managed by "alice"
+ And our fee is "0"
+ And a deposit of "500"
+
+ When I'm logged in as "alice"
+ And I go to the project page
+ And I click on "New distribution"
+ And I add the GitHub user "bob" to the recipients
+ And I fill the amount to "bob" with "10"
+ And I fill the comment to "bob" with "Great idea"
+ And I save the distribution
+
+ Then I should see these distribution lines:
+ | recipient | address | reason | amount | percentage |
+ | bob | mxWfjaZJTNN5QKeZZYQ5HW3vgALFBsnuG1 | Great idea | 10 | 100 |
+
+
+ Scenario: Send multiple times to the same recipient
+ Then pending
+
+ @javascript
+ Scenario: Remove a distribution line
+ Given a GitHub user "bob"
+ And a GitHub user "carol"
+
+ Given a project managed by "alice"
+ And our fee is "0"
+ And a deposit of "500"
+
+ When I'm logged in as "alice"
+ And I go to the project page
+ And I click on "New distribution"
+ And I add the GitHub user "bob" to the recipients
+ And I add the GitHub user "carol" to the recipients
+ And I remove the recipient "bob"
+ Then the distribution form should have these recipients:
+ | recipient |
+ | bob |
+ | carol |
+
+ When I save the distribution
+ Then I should see these distribution lines:
+ | recipient |
+ | carol |
+
+ When I click on "Edit the distribution"
+ And I remove the recipient "carol"
+ And I save the distribution
+ Then I should see these distribution lines:
+ | recipient |
+
+ @javascript
+ Scenario: Create distribution line without an amount
+ Given a GitHub user "bob" who has set his address to "mxWfjaZJTNN5QKeZZYQ5HW3vgALFBsnuG1"
+
+ Given a project managed by "alice"
+ And our fee is "0"
+ And a deposit of "500"
+
+ When I'm logged in as "alice"
+ And I go to the project page
+ And I click on "New distribution"
+ And I add the GitHub user "bob" to the recipients
+ And I save the distribution
+
+ Then I should see these distribution lines:
+ | recipient | amount | percentage |
+ | bob | Undecided | |
+ And I should not see the button "Send the transaction"
+
+ @javascript
+ Scenario: Send too much funds
+ Given a GitHub user "bob" who has set his address to "mxWfjaZJTNN5QKeZZYQ5HW3vgALFBsnuG1"
+
+ Given a project managed by "alice"
+ And our fee is "0"
+ And a deposit of "500"
+
+ When I'm logged in as "alice"
+ And I go to the project page
+ And I click on "New distribution"
+ And I add the GitHub user "bob" to the recipients
+ And I fill the amount to "bob" with "500.01"
+ And I click on "Save"
+ Then I should see "Not enough funds"
+
+ @javascript
+ Scenario: Send all the funds
+ Given a GitHub user "bob" who has set his address to "mxWfjaZJTNN5QKeZZYQ5HW3vgALFBsnuG1"
+
+ Given a project managed by "alice"
+ And our fee is "0"
+ And a deposit of "500"
+
+ When I'm logged in as "alice"
+ And I go to the project page
+ And I click on "New distribution"
+ And I add the GitHub user "bob" to the recipients
+ And I fill the amount to "bob" with "500.00"
+ And I click on "Save"
+ Then I should not see "Not enough funds"
+
+ When I click on "Send the transaction"
+ Then I should see "Transaction sent"
+ And these amounts should have been sent from the account of the project:
+ | address | amount |
+ | mxWfjaZJTNN5QKeZZYQ5HW3vgALFBsnuG1 | 500.0 |
+ And the project balance should be "0.00"
+
+ @javascript
+ Scenario: Send all the funds while some tips were refunded
+ Given a GitHub user "bob" who has set his address to "mxWfjaZJTNN5QKeZZYQ5HW3vgALFBsnuG1"
+
+ Given a project managed by "alice"
+ And our fee is "0"
+ And a deposit of "500"
+ And a refunded tip of "50"
+
+ When I'm logged in as "alice"
+ And I go to the project page
+ And I click on "New distribution"
+ And I add the GitHub user "bob" to the recipients
+ And I fill the amount to "bob" with "500.00"
+ And I click on "Save"
+ Then I should not see "Not enough funds"
+
+ When I click on "Send the transaction"
+ Then I should see "Transaction sent"
+ And these amounts should have been sent from the account of the project:
+ | address | amount |
+ | mxWfjaZJTNN5QKeZZYQ5HW3vgALFBsnuG1 | 500.0 |
+ And the project balance should be "0.00"
+
+ @javascript
+ Scenario: Send 0 amount in a distribution
+ Given a GitHub user "bob" who has set his address to "mxWfjaZJTNN5QKeZZYQ5HW3vgALFBsnuG1"
+ And a GitHub user "carol" who has set his address to "mi9SLroAgc8eUNuLwnZmdyqWdShbNtvr3n"
+
+ Given a project managed by "alice"
+ And our fee is "0"
+ And a deposit of "500"
+
+ Given I'm logged in as "alice"
+ And I go to the project page
+ And I click on "New distribution"
+ And I add the GitHub user "bob" to the recipients
+ And I add the GitHub user "carol" to the recipients
+ And I fill the amount to "bob" with "10"
+ And I fill the amount to "carol" with "0"
+ And I save the distribution
+
+ Then I should see these distribution lines:
+ | recipient | address | amount | percentage |
+ | bob | mxWfjaZJTNN5QKeZZYQ5HW3vgALFBsnuG1 | 10 | 100.0 |
+ | carol | mi9SLroAgc8eUNuLwnZmdyqWdShbNtvr3n | 0 | 0.0 |
+ And I should see "Total amount: 10.00 PPC"
+
+ When the tipper is started
+ Then no coins should have been sent
+
+ When I click on "Send the transaction"
+ Then I should see "Transaction sent"
+ And these amounts should have been sent from the account of the project:
+ | address | amount |
+ | mxWfjaZJTNN5QKeZZYQ5HW3vgALFBsnuG1 | 10.0 |
+ And the project balance should be "490"
+
diff --git a/features/donate_to_project.feature b/features/donate_to_project.feature
new file mode 100644
index 00000000..53213036
--- /dev/null
+++ b/features/donate_to_project.feature
@@ -0,0 +1,70 @@
+Feature: A visitor can donate to a project
+ Background:
+ Given a project
+ And our fee is "0.01"
+
+ Scenario: A visitor sends coins to a project
+ When I visit the project page
+ And I click on "Donate"
+ And I fill "Return address" with "mmGen7mZTGi9bciEaEa2W1DLsx3HjaFvcd"
+ And I click on "Generate my donation address"
+ Then I should see the project donation address associated with "mmGen7mZTGi9bciEaEa2W1DLsx3HjaFvcd"
+
+ Given there's a new incoming transaction of "50" to the donation address associated with "mmGen7mZTGi9bciEaEa2W1DLsx3HjaFvcd"
+ And the project balance is updated
+
+ When I visit the project page
+ Then I should see the project balance is "49.5"
+
+ When I click on "List of donors"
+ Then I should see the donor "mmGen7mZTGi9bciEaEa2W1DLsx3HjaFvcd" sent "50"
+
+ Scenario: Sending twice with the same return address
+ Given the project has a donation address "mfbDMySWmo4p31waWE4bUGFqK47V4comdq" associated with "mpbkNzunFtBmu3JYENE62UTLtKyvwrSUfx"
+ When I visit the project page
+ And I click on "Donate"
+ And I fill "Return address" with "mpbkNzunFtBmu3JYENE62UTLtKyvwrSUfx"
+ And I click on "Generate my donation address"
+ Then I should see "mfbDMySWmo4p31waWE4bUGFqK47V4comdq"
+
+ Scenario: Sending with an invalid return address
+ When I visit the project page
+ And I click on "Donate"
+ And I click on "Generate my donation address"
+ Then I should see "can't be blank"
+ When I fill "Return address" with "mpbkNzunFtBmu3JYENE62UTLtKyvwrSUfy"
+ And I click on "Generate my donation address"
+ Then I should see "invalid"
+
+ Scenario: Sending without a return address
+ When I visit the project page
+ And I click on "Donate"
+ Then I should see the project donation address
+
+ Given there's a new incoming transaction of "50" to the project donation address
+ And the project balance is updated
+
+ When I visit the project page
+ Then I should see the project balance is "49.5"
+
+ When I click on "List of donors"
+ Then I should see the donor "No address provided" sent "50"
+
+ Scenario: Multiple donations in a single transaction
+ Given a project "A" with a donation address "mpD1oHHQqAWWrrfxzAg1gEWbHh2teQjYsU" associated with "mpR1otQoiJo8dfXzxyTmvPLg42n7RFJMMh"
+ And a project "B" with a donation address "mpD2oDRevAtG5Q24jRQx1GKpaZfxM8wh3y" associated with "mpR1otQoiJo8dfXzxyTmvPLg42n7RFJMMh"
+ And there's a new incoming transaction of "50" to "mpD1oHHQqAWWrrfxzAg1gEWbHh2teQjYsU" in transaction "tx1"
+ And there's a new incoming transaction of "75" to "mpD2oDRevAtG5Q24jRQx1GKpaZfxM8wh3y" in transaction "tx1"
+ And the project balances are updated
+
+ When I visit the project "A" page
+ Then I should see the project balance is "49.50"
+
+ When I click on "List of donors"
+ Then I should see the donor "mpR1otQoiJo8dfXzxyTmvPLg42n7RFJMMh" sent "50"
+
+ When I visit the project "B" page
+ Then I should see the project balance is "74.25"
+
+ When I click on "List of donors"
+ Then I should see the donor "mpR1otQoiJo8dfXzxyTmvPLg42n7RFJMMh" sent "75"
diff --git a/features/project_detailed_description.feature b/features/project_detailed_description.feature
new file mode 100644
index 00000000..2737f121
--- /dev/null
+++ b/features/project_detailed_description.feature
@@ -0,0 +1,40 @@
+Feature: Project detailed description is markdown formatted
+ Background:
+ Given a project
+ And the project single collaborator is "bob"
+ And I'm logged in as "bob"
+ And I go to the project page
+ And I click on "Edit project"
+
+ Scenario: Standard markdown
+ When I fill "Detailed description" with:
+ """
+ foo [bar](http://foo.example.com/)
+ """
+ And I click on "Save"
+ Then I should see a link "bar" to "http://foo.example.com/"
+
+ Scenario: XSS attempt
+ When I fill "Detailed description" with:
+ """
+ foo [bar](javascript:alert('xss'))
+ """
+ And I click on "Save"
+ Then I should not see a link "bar" to "javascript:alert('xss')"
+
+ Scenario: Embeded HTML
+ When I fill "Detailed description" with:
+ """
+ foo bar
+ """
+ And I click on "Save"
+ Then I should not see a link "bar" to "javascript:alert('xss')"
+
+ Scenario: Inline external image
+ When I fill "Detailed description" with:
+ """
+ 
+ """
+ And I click on "Save"
+ Then I should not see the image "http://example.com/img.jpg"
+
diff --git a/features/step_definitions/cold_storage.rb b/features/step_definitions/cold_storage.rb
new file mode 100644
index 00000000..ced4006c
--- /dev/null
+++ b/features/step_definitions/cold_storage.rb
@@ -0,0 +1,71 @@
+Given(/^the cold storage addresses are$/) do |table|
+ CONFIG["cold_storage"] ||= {}
+ CONFIG["cold_storage"]["addresses"] = table.raw.map(&:first)
+end
+
+Given(/^the project address is "(.*?)"$/) do |arg1|
+ @project.update(bitcoin_address: arg1)
+end
+
+Given(/^the project cold storage withdrawal address is "(.*?)"$/) do |arg1|
+ @project.update(cold_storage_withdrawal_address: arg1)
+end
+
+When(/^there's a new incoming transaction of "([^"]*?)" on the project account$/) do |arg1|
+ BitcoinDaemon.instance.add_transaction(account: @project.address_label, amount: arg1.to_d, address: @project.bitcoin_address)
+end
+
+When(/^there's a new incoming transaction of "(.*?)" to address "(.*?)" on the project account$/) do |arg1, arg2|
+ BitcoinDaemon.instance.add_transaction(account: @project.address_label, amount: arg1.to_d, address: arg2)
+end
+
+When(/^there's a new incoming transaction of "(.*?)" to address "(.*?)" on the project account with (\d+) confirmations$/) do |arg1, arg2, arg3|
+ BitcoinDaemon.instance.add_transaction(account: @project.address_label, amount: arg1.to_d, address: arg2, confirmations: arg3.to_i)
+end
+
+When(/^there's a new outgoing transaction of "(.*?)" to address "(.*?)" on the project account$/) do |arg1, arg2|
+ BitcoinDaemon.instance.add_transaction(category: "send", account: @project.address_label, amount: -arg1.to_d, address: arg2)
+end
+
+When(/^there's a new outgoing transaction of "(.*?)" to address "(.*?)" on the project account with (\d+) confirmations$/) do |arg1, arg2, arg3|
+ BitcoinDaemon.instance.add_transaction(category: "send", account: @project.address_label, amount: -arg1.to_d, address: arg2, confirmations: arg3.to_i)
+end
+
+When(/^the project (?:balance is|balances are) updated$/) do
+ BalanceUpdater.work
+end
+
+Then(/^updating the project balance should raise an error$/) do
+ expect { BalanceUpdater.work }.to raise_error(RuntimeError)
+end
+
+Then(/^the project balance should be "(.*?)"$/) do |arg1|
+ expect(@project.reload.available_amount.to_d / COIN).to eq(arg1.to_d)
+end
+
+Then(/^the project amount in cold storage should be "(.*?)"$/) do |arg1|
+ expect(@project.reload.cold_storage_amount / COIN).to eq(arg1.to_d)
+end
+
+When(/^"(.*?)" coins of the project funds are sent to cold storage$/) do |arg1|
+ @project.send_to_cold_storage!((arg1.to_d * COIN).to_i)
+end
+
+Then(/^there should be an outgoing transaction of "(.*?)" to address "(.*?)" on the project account$/) do |arg1, arg2|
+ transactions = BitcoinDaemon.instance.list_transactions(@project.address_label)
+ expect(transactions.map { |t| t["category"] }).to eq(["send"])
+ expect(transactions.map { |t| t["address"] }).to eq([arg2])
+ expect(transactions.map { |t| -t["amount"].to_d / COIN }).to eq([arg1.to_d])
+end
+
+Given(/^the project has no cold storage withdrawal address$/) do
+ @project.update(cold_storage_withdrawal_address: nil)
+end
+
+Then(/^the project should have a cold storage withdrawal address$/) do
+ expect(@project.reload.cold_storage_withdrawal_address).not_to be_blank
+end
+
+Then(/^the project cold storage withdrawal address should be linked to its account$/) do
+ expect(BitcoinDaemon.instance.get_addresses_by_account(@project.address_label)).to include(@project.reload.cold_storage_withdrawal_address)
+end
diff --git a/features/step_definitions/commit_from_known_nickname.rb b/features/step_definitions/commit_from_known_nickname.rb
new file mode 100644
index 00000000..28d32a99
--- /dev/null
+++ b/features/step_definitions/commit_from_known_nickname.rb
@@ -0,0 +1,16 @@
+Given(/^an user "(.*?)"$/) do |arg1|
+ create(:user, nickname: arg1, email: "#{arg1}@example.com")
+end
+
+Given(/^the email of "(.*?)" is "(.*?)"$/) do |arg1, arg2|
+ User.find_by_nickname!(arg1).update(email: arg2)
+end
+
+Then(/^the tip for commit "(.*?)" is for user "(.*?)"$/) do |arg1, arg2|
+ expect(Tip.find_by_commit!(arg1).user.nickname).to eq(arg2)
+end
+
+Then(/^there should be no user with email "(.*?)"$/) do |arg1|
+ expect(User.where(email: arg1).size).to eq(0)
+end
+
diff --git a/features/step_definitions/common.rb b/features/step_definitions/common.rb
new file mode 100644
index 00000000..e857a5b5
--- /dev/null
+++ b/features/step_definitions/common.rb
@@ -0,0 +1,205 @@
+Before do
+ ActionMailer::Base.deliveries.clear
+end
+
+Then(/^there should be (\d+) email sent$/) do |arg1|
+ begin
+ expect(ActionMailer::Base.deliveries.size).to eq(arg1.to_i)
+ rescue
+ p ActionMailer::Base.deliveries
+ raise
+ end
+ @email = ActionMailer::Base.deliveries.first
+end
+
+Then(/^(\d+) email should have been sent$/) do |arg1|
+ step "there should be #{arg1} email sent"
+end
+
+Then(/^no email should have been sent$/) do
+ expect(ActionMailer::Base.deliveries).to eq([])
+end
+
+When(/^the email counters are reset$/) do
+ ActionMailer::Base.deliveries.clear
+end
+
+Given(/^the tip for commit is "(.*?)"$/) do |arg1|
+ CONFIG["tip"] = arg1.to_f
+end
+
+Given(/^our fee is "(.*?)"$/) do |arg1|
+ CONFIG["our_fee"] = arg1.to_f
+end
+
+Given(/^a project$/) do
+ @project = Project.create!(name: "test", full_name: "example/test", bitcoin_address: 'mq4NtnmQoQoPfNWEPbhSvxvncgtGo6L8WY', address_label: "example_project_account", hold_tips: false)
+end
+
+Given(/^a project "([^"]*?)"$/) do |arg1|
+ @project = Project.create!(name: "test", full_name: "example/#{arg1}", bitcoin_address: 'mq4NtnmQoQoPfNWEPbhSvxvncgtGo6L8WY', hold_tips: false)
+end
+
+Given(/^a project "(.*?)" holding tips$/) do |arg1|
+ @project = Project.create!(name: "test", full_name: "example/#{arg1}", bitcoin_address: 'mq4NtnmQoQoPfNWEPbhSvxvncgtGo6L8WY', hold_tips: true)
+end
+
+Given(/^a deposit of "(.*?)"$/) do |arg1|
+ Deposit.create!(project: @project, amount: arg1.to_d * COIN, confirmations: 1, created_at: 2.minutes.ago)
+end
+
+Given(/^a refunded tip of "([^"]*)"$/) do |arg1|
+ Tip.create!(project: @project, amount: arg1.to_d * COIN, created_at: 2.minutes.ago, refunded_at: 1.minute.ago)
+end
+
+Given(/^the last known commit is "(.*?)"$/) do |arg1|
+ @project.update!(last_commit: arg1)
+end
+
+def add_new_commit(id, params = {})
+ @commits ||= {}
+ defaults = {
+ sha: id,
+ commit: {
+ message: "Some changes",
+ author: {
+ email: "anonymous@example.com",
+ },
+ committer: {
+ date: Time.now,
+ }
+ },
+ }
+ @commits[id] = defaults.deep_merge(params)
+end
+
+def find_new_commit(id)
+ @commits[id]
+end
+
+Given(/^a new commit "(.*?)" with parent "([^"]*?)"$/) do |arg1, arg2|
+ add_new_commit(arg1, parents: [{sha: arg2}])
+end
+
+Given(/^a new commit "(.*?)" with parent "(.*?)" and "(.*?)"$/) do |arg1, arg2, arg3|
+ add_new_commit(arg1, parents: [{sha: arg2}, {sha: arg3}], commit: {message: "Merge #{arg2} and #{arg3}"})
+end
+
+Given(/^(\d+) new commits$/) do |arg1|
+ arg1.to_i.times do
+ add_new_commit(Digest::SHA1.hexdigest(SecureRandom.hex))
+ end
+end
+
+Given(/^(\d+) new commits by "([^"]*)"$/) do |arg1, arg2|
+ arg1.to_i.times do
+ add_new_commit(Digest::SHA1.hexdigest(SecureRandom.hex), author: {login: arg2}, commit: {author: {email: "#{arg2}@example.com"}})
+ end
+end
+
+Given(/^a new commit "([^"]*?)"$/) do |arg1|
+ add_new_commit(arg1)
+end
+
+Given(/^a new commit "([^"]*?)" by "([^"]*)"$/) do |arg1, arg2|
+ add_new_commit(arg1, author: {login: arg2}, commit: {author: {email: "#{arg2}@example.com"}})
+end
+
+Given(/^the project holds tips$/) do
+ @project.update(hold_tips: true)
+end
+
+Given(/^the message of commit "(.*?)" is "(.*?)"$/) do |arg1, arg2|
+ find_new_commit(arg1).deep_merge!(commit: {message: arg2})
+end
+
+Given(/^the email of commit "(.*?)" is "(.*?)"$/) do |arg1, arg2|
+ find_new_commit(arg1).deep_merge!(commit: {author: {email: arg2}})
+end
+
+When(/^the new commits are read$/) do
+ @project.reload
+ expect(@project).to receive(:get_commits).and_return(@commits.values.map(&:to_ostruct))
+ @project.update_commits
+ expect(@project).to receive(:get_commits).and_return(@commits.values.map(&:to_ostruct))
+ @project.tip_commits
+end
+
+Then(/^there should be no tip for commit "(.*?)"$/) do |arg1|
+ expect(Tip.where(commit: arg1).to_a).to eq([])
+end
+
+Then(/^there should be a tip of "(.*?)" for commit "(.*?)"$/) do |arg1, arg2|
+ amount = Tip.find_by(commit: arg2).amount
+ expect(amount).not_to be_nil
+ expect((amount.to_d / COIN)).to eq(arg1.to_d)
+end
+
+Then(/^the tip amount for commit "(.*?)" should be undecided$/) do |arg1|
+ expect(Tip.find_by(commit: arg1).undecided?).to be true
+end
+
+Then(/^the new last known commit should be "(.*?)"$/) do |arg1|
+ expect(@project.reload.last_commit).to eq(arg1)
+end
+
+Given(/^the project collaborators are:$/) do |table|
+ @project.reload
+ @project.collaborators.each(&:destroy)
+ table.raw.each do |name,|
+ @project.collaborators.create!(login: name)
+ end
+end
+
+Given(/^the project single collaborator is "(.*?)"$/) do |arg1|
+ @project.reload
+ @project.collaborators.each(&:destroy)
+ @project.collaborators.create!(login: arg1)
+end
+
+Given(/^a project managed by "(.*?)"$/) do |arg1|
+ user = create(:user, email: "#{arg1}@example.com", nickname: arg1)
+ user.confirm
+ user.save!
+ @project = Project.create!(name: "#{arg1} project", bitcoin_address: 'mq4NtnmQoQoPfNWEPbhSvxvncgtGo6L8WY', address_label: "example_project_account")
+ @project.collaborators.create!(login: arg1)
+end
+
+Given(/^the author of commit "(.*?)" is "(.*?)"$/) do |arg1, arg2|
+ find_new_commit(arg1).deep_merge!(author: {login: arg2}, commit: {author: {email: "#{arg2}@example.com"}})
+end
+
+Given(/^the author of commit "(.*?)" is the non identified email "(.*?)"$/) do |arg1, arg2|
+ find_new_commit(arg1).deep_merge!(commit: {author: {email: arg2}})
+end
+
+Given(/^an illustration of the history is:$/) do |string|
+ # not checked
+end
+
+Given(/^the current time is "(.*?)"$/) do |arg1|
+ Timecop.travel(Time.parse(arg1))
+end
+
+After do
+ Timecop.return
+end
+
+Then(/^pending$/) do
+ pending
+end
+
+Then(/^these amounts should have been sent from the account of the project:$/) do |table|
+ expect(BitcoinDaemon.instance.list_transactions(@project.address_label).map do |tx|
+ if tx["category"] == "send"
+ {
+ "address" => tx["address"],
+ "amount" => (-tx["amount"]).to_s,
+ }
+ end
+ end.compact.sort_by { |x| x["address"] }).to eq(table.hashes.sort_by { |x| x["address"] })
+end
+
+When(/^the transaction history is cleared$/) do
+ BitcoinDaemon.instance.clear_transaction_history
+end
diff --git a/features/step_definitions/create_project.rb b/features/step_definitions/create_project.rb
new file mode 100644
index 00000000..6d32f4cd
--- /dev/null
+++ b/features/step_definitions/create_project.rb
@@ -0,0 +1,38 @@
+
+Then(/^there should be a project "(.*?)"$/) do |arg1|
+ expect(Project.pluck(:name)).to include(arg1)
+ @project = Project.where(name: arg1).first
+end
+
+Then(/^the description of the project should be$/) do |string|
+ expect(@project.description).to eq(string)
+end
+
+Then(/^I should be on the project page$/) do
+ expect(current_url).to eq(project_url(@project))
+end
+
+Then(/^there should be no project$/) do
+ expect(Project.all).to be_empty
+end
+
+Then(/^the GitHub name of the project should be "(.*?)"$/) do |arg1|
+ expect(@project.full_name).to eq(arg1)
+end
+
+Then(/^the project single collaborators should be "(.*?)"$/) do |arg1|
+ if arg1 =~ /@/
+ expect(@project.collaborators.map(&:user).map(&:email)).to eq([arg1])
+ else
+ expect(@project.collaborators.map(&:user).map(&:nickname)).to eq([arg1])
+ end
+end
+
+Then(/^the project address label should be "(.*?)"$/) do |arg1|
+ expect(@project.address_label).to eq(arg1)
+end
+
+Then(/^the project donation address should be the same as account "(.*?)"$/) do |arg1|
+ expect(@project.bitcoin_address).to eq(BitcoinDaemon.instance.get_addresses_by_account(arg1).first)
+end
+
diff --git a/features/step_definitions/distribution.rb b/features/step_definitions/distribution.rb
new file mode 100644
index 00000000..e9653b7b
--- /dev/null
+++ b/features/step_definitions/distribution.rb
@@ -0,0 +1,210 @@
+
+Given(/^a GitHub user "(.*?)" who has set his address to "(.*?)"$/) do |arg1, arg2|
+ create(:user, email: "#{arg1}@example.com", nickname: arg1, bitcoin_address: arg2)
+end
+
+Given(/^the user with email "(.*?)" has set his address to "(.*?)"$/) do |arg1, arg2|
+ User.find_by(email: arg1).update(bitcoin_address: arg2)
+end
+
+Given(/^a GitHub user "([^"]*?)"$/) do |arg1|
+ create(:user, email: "#{arg1}@example.com", nickname: arg1, bitcoin_address: nil)
+end
+
+Given(/^an user with email "(.*?)"$/) do |arg1|
+ create(:user, email: arg1, nickname: nil, bitcoin_address: nil)
+end
+
+Given(/^an user with email "(.*?)" and without password nor confirmation token$/) do |arg1|
+ user = create(:user, email: arg1, nickname: nil, bitcoin_address: nil, password: nil, confirmation_token: nil)
+ user.update(confirmed_at: nil)
+end
+
+Given(/^I add the GitHub user "(.*?)" to the recipients$/) do |arg1|
+ within ".panel", text: "GitHub user" do
+ find("input:enabled[name=\"user[nickname]\"]").set(arg1)
+ click_on "Add"
+ end
+end
+
+Given(/^I add the email address "(.*?)" to the recipients$/) do |arg1|
+ within ".panel", text: "email address" do
+ find("input").set(arg1)
+ click_on "Add"
+ end
+end
+
+
+Given(/^I add the user with email "(.*?)" through his identifier to the recipients$/) do |arg1|
+ user = User.find_by(email: arg1)
+ within ".panel", text: "Peer4commit user" do
+ find("input:enabled[name=\"user[identifier]\"]").set(user.identifier)
+ click_on "Add"
+ end
+end
+
+When(/^I select the commit recipients "(.*?)"$/) do |arg1|
+ within ".panel", text: "Authors of commits" do
+ click_on arg1
+ end
+end
+
+When(/^I add the commit "(.*?)" to the recipients$/) do |arg1|
+ within ".panel", text: "Author of a commit" do
+ find("input:enabled[name=\"commit[sha]\"]").set(arg1)
+ click_on "Add"
+ end
+end
+
+def parse_recipient(recipient)
+ case recipient
+ when /\A<(.+) identifier>\Z/
+ user = User.find_by(email: $1)
+ user.identifier
+ else
+ recipient
+ end
+end
+
+def within_recipient_row(recipient)
+ within "#recipients tr", text: /^#{Regexp.escape parse_recipient(recipient)}/ do
+ yield
+ end
+end
+
+Given(/^I fill the amount to "(.*?)" with "(.*?)"$/) do |arg1, arg2|
+ begin
+ within_recipient_row(arg1) do
+ fill_in "Amount", with: arg2
+ end
+ rescue
+ p all("#recipients tr").map(&:text)
+ p errors: all(".alert.alert-danger").map(&:text)
+ raise
+ end
+end
+
+Given(/^I fill the comment to "(.*?)" with "(.*?)"$/) do |arg1, arg2|
+ within_recipient_row(arg1) do
+ fill_in "Comment", with: arg2
+ end
+end
+
+When(/^I remove the recipient "(.*?)"$/) do |arg1|
+ within_recipient_row(arg1) do
+ check "Remove"
+ end
+end
+
+Then(/^I should see these distribution lines:$/) do |table|
+ table.hashes.each do |row|
+ recipient = parse_recipient(row["recipient"])
+ begin
+ tr = find("#distribution-show-page tbody tr", text: /^#{Regexp.escape recipient}/)
+ rescue
+ puts "Rows: " + all("#distribution-show-page tbody tr").map(&:text).inspect
+ raise
+ end
+ expect(tr.find(".recipient").text).to eq(recipient)
+ expect(tr.find(".address").text).to eq(row["address"]) if row["address"]
+ expect(tr.find(".reason").text).to eq(row["reason"]) if row["reason"]
+ if row["amount"]
+ text = tr.find(".amount").text
+ if row["amount"] =~ /\A[0-9.]+\Z/
+ expect(text.to_d).to eq(row["amount"].to_d)
+ else
+ expect(text).to eq(row["amount"])
+ end
+ end
+ if row["percentage"]
+ text = tr.find(".percentage").text
+ if row["percentage"] =~ /\A[0-9.]+\Z/
+ expect(text.to_d).to eq(row["percentage"].to_d)
+ else
+ expect(text).to eq(row["percentage"])
+ end
+ end
+ expect(tr.find(".tip-comment").text).to eq(row["comment"]) if row["comment"]
+ end
+ expect(table.hashes.size).to eq(all("#distribution-show-page tbody tr").size)
+end
+
+Then(/^the distribution form should have these recipients:$/) do |table|
+ table.hashes.each do |row|
+ begin
+ tr = find("#distribution-form #recipients tr", text: row["recipient"])
+ rescue
+ p rows: all("#distribution-form #recipients tr").map(&:text)
+ p errors: all(".alert.alert-danger").map(&:text)
+ raise
+ end
+ expect(tr.find(".recipient").text).to eq(row["recipient"])
+ expect(tr.find(".reason").text).to eq(row["reason"]) if row["reason"]
+ if row["amount"]
+ text = tr.find_field("Amount").value
+ if row["amount"] =~ /\A[0-9.]+\Z/
+ expect(text.to_d).to eq(row["amount"].to_d)
+ else
+ expect(text).to eq(row["amount"])
+ end
+ end
+ if row["comment"]
+ text = tr.find_field("Comment").value
+ expect(text).to eq(row["comment"])
+ end
+ end
+ expect(all("#distribution-form tbody tr").size).to eq(table.hashes.size)
+end
+
+When(/^the tipper is started$/) do
+ BitcoinTipper.work
+end
+
+Then(/^no coins should have been sent$/) do
+ expect(BitcoinDaemon.instance.list_transactions("*")).to eq([])
+end
+
+When(/^I set my address to "(.*?)"$/) do |arg1|
+ step 'I go to edit my profile'
+ fill_in "Peercoin address", with: arg1
+ if has_field?("Current password")
+ fill_in "Current password", with: "password"
+ end
+ click_on "Update"
+ expect(page).to have_content "You updated your account successfully"
+end
+
+When(/^I click on the last distribution$/) do
+ find("#distribution-list .distribution-link:first-child").click
+end
+
+Then(/^an email should have been sent to "(.*?)"$/) do |arg1|
+ expect(ActionMailer::Base.deliveries.map(&:to)).to include([arg1])
+ @email = ActionMailer::Base.deliveries.detect { |email| email.to == [arg1] }
+end
+
+Then(/^the email should include "(.*?)"$/) do |arg1|
+ expect(@email.body).to include(arg1)
+end
+
+Then(/^the email should include a link to the last distribution$/) do
+ distribution = Distribution.last
+ expect(@email.body).to include(project_distribution_url(distribution.project, distribution))
+end
+
+When(/^I visit the link to set my password and address from the email$/) do
+ step "I click on the \"Set your password and Peercoin address\" link in the email"
+end
+
+Then(/^the user with email "(.*?)" should have "(.*?)" as password$/) do |arg1, arg2|
+ expect(User.find_by(email: arg1).valid_password?(arg2)).to eq(true)
+end
+
+Then(/^the user with email "(.*?)" should have "(.*?)" as peercoin address$/) do |arg1, arg2|
+ expect(User.find_by(email: arg1).bitcoin_address).to eq(arg2)
+end
+
+Given(/^I save the distribution$/) do
+ click_on "Save"
+ expect(page).to have_content(/Distribution (created|updated)/)
+end
diff --git a/features/step_definitions/donate_to_project.rb b/features/step_definitions/donate_to_project.rb
new file mode 100644
index 00000000..fdf810e5
--- /dev/null
+++ b/features/step_definitions/donate_to_project.rb
@@ -0,0 +1,49 @@
+Given(/^a project "([^"]*)" with a donation address "([^"]*)" associated with "([^"]*)"$/) do |arg1, arg2, arg3|
+ @project = Project.create!(
+ name: arg1,
+ full_name: "example/#{arg1}",
+ bitcoin_address: 'mq4NtnmQoQoPfNWEPbhSvxvncgtGo6L8WY',
+ hold_tips: false,
+ address_label: "project-#{arg1}",
+ )
+ step %Q[the project has a donation address "#{arg2}" associated with "#{arg3}"]
+end
+
+
+Then(/^I should see the project donation address associated with "(.*?)"$/) do |arg1|
+ address = @project.donation_addresses.find_by(sender_address: arg1).donation_address
+ expect(address).not_to be_blank
+ expect(page).to have_content(address)
+end
+
+Given(/^there's a new incoming transaction of "(.*?)" to the donation address associated with "(.*?)"$/) do |arg1, arg2|
+ address = @project.donation_addresses.find_by(sender_address: arg2).donation_address
+ expect(address).not_to be_blank
+ BitcoinDaemon.instance.add_transaction(account: @project.address_label, amount: arg1.to_d, address: address)
+end
+
+Given(/^there's a new incoming transaction of "([^"]*)" to "([^"]*)" in transaction "([^"]*)"$/) do |arg1, arg2, arg3|
+ donation_address = DonationAddress.find_by!(donation_address: arg2)
+ project = donation_address.project
+ BitcoinDaemon.instance.add_transaction(
+ account: project.address_label,
+ amount: arg1.to_d,
+ address: donation_address.donation_address,
+ txid: arg3,
+ )
+end
+
+Then(/^I should see the donor "(.*?)" sent "(.*?)"$/) do |arg1, arg2|
+ within ".donor-row", text: arg1 do
+ expect(find(".amount").text.to_d).to eq(arg2.to_d)
+ end
+end
+
+Given(/^the project has a donation address "(.*?)" associated with "(.*?)"$/) do |arg1, arg2|
+ @project.donation_addresses.create!(sender_address: arg2, donation_address: arg1)
+end
+
+When(/^there's a new incoming transaction of "([^"]*?)" to the project donation address$/) do |arg1|
+ BitcoinDaemon.instance.add_transaction(account: @project.address_label, amount: arg1.to_d, address: @project.bitcoin_address)
+end
+
diff --git a/features/step_definitions/github.rb b/features/step_definitions/github.rb
new file mode 100644
index 00000000..65494303
--- /dev/null
+++ b/features/step_definitions/github.rb
@@ -0,0 +1,5 @@
+
+Given(/^"(.*?)" is an user registered on GitHub$/) do |arg1|
+ GITHUB_USERS[arg1] = {}
+end
+
diff --git a/features/step_definitions/tip_for_commit.rb b/features/step_definitions/tip_for_commit.rb
new file mode 100644
index 00000000..d06f8efe
--- /dev/null
+++ b/features/step_definitions/tip_for_commit.rb
@@ -0,0 +1,48 @@
+
+Given(/^the project does not hold tips$/) do
+ @project.update(hold_tips: false)
+end
+
+Given(/^the project GitHub name is "(.*?)"$/) do |arg1|
+ @project.update(full_name: arg1)
+end
+
+Given(/^the commits on GitHub for project "(.*?)" are$/) do |arg1, table|
+ @project.reload
+ expect(@project.full_name).to eq(arg1)
+ commits = []
+ table.hashes.each do |row|
+ commit = OpenStruct.new(
+ sha: row["sha"],
+ author: OpenStruct.new(
+ login: row["author"],
+ ),
+ commit: OpenStruct.new(
+ message: row["message"] || "Some changes",
+ author: OpenStruct.new(
+ email: row["email"] || "author@example.com",
+ ),
+ committer: OpenStruct.new(
+ date: Time.now,
+ ),
+ ),
+ )
+ commits << commit
+ end
+
+ expect(@project).to receive(:get_commits).and_return(commits)
+end
+
+When(/^the project tips are built from commits$/) do
+ @project.tip_commits
+end
+
+Then(/^the project should have these tips:$/) do |table|
+ tips = @project.tips.map do |tip|
+ {
+ commit: tip.commit,
+ amount: tip.amount ? (tip.amount.to_f / COIN).to_s : "",
+ }.with_indifferent_access
+ end
+ expect(tips).to eq(table.hashes)
+end
diff --git a/features/step_definitions/tip_modifier_interface.rb b/features/step_definitions/tip_modifier_interface.rb
new file mode 100644
index 00000000..47415667
--- /dev/null
+++ b/features/step_definitions/tip_modifier_interface.rb
@@ -0,0 +1,93 @@
+When(/^I choose the amount "(.*?)" on commit "(.*?)"$/) do |arg1, arg2|
+ within find(".decide-tip-amounts-table tbody tr", text: arg2) do
+ select arg1
+ end
+end
+
+When(/^I fill the free amount with "(.*?)" on commit "(.*?)"$/) do |arg1, arg2|
+ within find(".decide-tip-amounts-table tbody tr", text: arg2) do
+ fill_in "Decided free amount", with: arg1
+ end
+end
+
+When(/^I choose the amount "(.*?)" on all commits$/) do |arg1|
+ all(".decide-tip-amounts-table tbody tr").each do |tr|
+ within tr do
+ select arg1
+ end
+ end
+end
+
+When(/^I go to the edit page of the project$/) do
+ visit edit_project_path(@project)
+end
+
+When(/^I send a forged request to enable tip holding on the project$/) do
+ page.driver.browser.process_and_follow_redirects(:patch, project_path(@project), project: {hold_tips: "1"})
+end
+
+Then(/^I should see an access denied$/) do
+ expect(page).to have_content("Access denied")
+end
+
+Then(/^the project should not hold tips$/) do
+ expect(@project.reload.hold_tips).to be false
+end
+
+Then(/^the project should hold tips$/) do
+ expect(@project.reload.hold_tips).to be true
+end
+
+Given(/^the project has undedided tips$/) do
+ create(:undecided_tip, project: @project)
+ expect(@project.reload).to have_undecided_tips
+end
+
+Given(/^the project has (\d+) undecided tip$/) do |arg1|
+ @project.tips.undecided.each(&:destroy)
+ create(:undecided_tip, project: @project)
+ expect(@project.reload).to have_undecided_tips
+end
+
+Given(/^I send a forged request to set the amount of the first undecided tip of the project$/) do
+ tip = @project.tips.undecided.first
+ expect(tip).not_to be_nil
+ params = {
+ project: {
+ tips_attributes: {
+ "0" => {
+ id: tip.id,
+ decided_amount_percentage: "5",
+ },
+ },
+ },
+ }
+
+ page.driver.browser.process_and_follow_redirects(:patch, decide_tip_amounts_project_path(@project), params)
+end
+
+When(/^I send a forged request to change the percentage of commit "(.*?)" on project "(.*?)" to "(.*?)"$/) do |arg1, arg2, arg3|
+ project = find_project(arg2)
+ tip = project.tips.detect { |t| t.commit == arg1 }
+ expect(tip).not_to be_nil
+ params = {
+ project: {
+ tips_attributes: {
+ "0" => {
+ id: tip.id,
+ decided_amount_percentage: arg3,
+ },
+ },
+ },
+ }
+
+ page.driver.browser.process_and_follow_redirects(:patch, decide_tip_amounts_project_path(project), params)
+end
+
+Then(/^the project should have (\d+) undecided tips$/) do |arg1|
+ expect(@project.tips.undecided.size).to eq(arg1.to_i)
+end
+
+Then(/^there should be (\d+) tip$/) do |arg1|
+ expect(@project.reload.tips.size).to eq(arg1.to_i)
+end
diff --git a/features/step_definitions/user_identifier.rb b/features/step_definitions/user_identifier.rb
new file mode 100644
index 00000000..cb098b90
--- /dev/null
+++ b/features/step_definitions/user_identifier.rb
@@ -0,0 +1,5 @@
+Then(/^I should see the identifier of "(.*?)"$/) do |arg1|
+ identifier = User.find_by(email: arg1).identifier
+ expect(identifier).to be_present
+ expect(page).to have_content identifier
+end
diff --git a/features/step_definitions/web.rb b/features/step_definitions/web.rb
new file mode 100644
index 00000000..b2aef649
--- /dev/null
+++ b/features/step_definitions/web.rb
@@ -0,0 +1,160 @@
+Before '@javascript' do
+ # https://github.com/teampoltergeist/poltergeist/issues/754#issuecomment-228433228
+ page.driver.clear_memory_cache
+end
+
+Given(/^I'm logged in as "(.*?)"$/) do |arg1|
+ OmniAuth.config.test_mode = true
+ OmniAuth.config.mock_auth[:github] = {
+ "info" => {
+ "nickname" => arg1,
+ "primary_email" => "#{arg1.gsub(/\s+/,'')}@example.com",
+ "verified_emails" => [],
+ },
+ }
+ visit root_path
+ click_on "Sign in"
+ click_on "Sign in with Github"
+ expect(page).to have_content("Successfully authenticated")
+ OmniAuth.config.mock_auth[:github] = nil
+ @current_user = User.find_by(nickname: arg1)
+end
+
+Given(/^I'm logged in on GitHub as "(.*?)"$/) do |arg1|
+ OmniAuth.config.test_mode = true
+ OmniAuth.config.mock_auth[:github] = {
+ "info" => {
+ "nickname" => arg1,
+ "primary_email" => "#{arg1.gsub(/\s+/,'')}@example.com",
+ "verified_emails" => [],
+ },
+ }
+end
+
+Given(/^I'm not logged in$/) do
+ visit root_path
+ if page.has_content?("Sign Out")
+ click_on "Sign Out"
+ expect(page).to have_content("Signed out successfully")
+ else
+ expect(page).to have_content("Sign in")
+ end
+end
+
+When(/^I log out$/) do
+ click_on "Sign Out"
+ expect(page).to have_content "Signed out successfully"
+end
+
+When(/^I log in as "(.*?)"$/) do |arg1|
+ step "I'm logged in as \"#{arg1}\""
+end
+
+
+When(/^I visit the home page$/) do
+ visit '/'
+end
+
+When(/^I fill "(.*?)" with "(.*?)"$/) do |arg1, arg2|
+ fill_in arg1, with: arg2
+end
+
+Given(/^I go to the project page$/) do
+ visit project_path(@project)
+end
+
+Given(/^I click on "(.*?)"$/) do |arg1|
+ click_on(arg1)
+end
+
+Given(/^I click on "(.*?)" in the sign in form$/) do |arg1|
+ within "#sign-in-form" do
+ click_on(arg1)
+ end
+end
+
+Given(/^I check "(.*?)"$/) do |arg1|
+ check(arg1)
+end
+
+Given(/^I uncheck "(.*?)"$/) do |arg1|
+ uncheck(arg1)
+end
+
+Then(/^I should see "(.*?)"$/) do |arg1|
+ expect(page).to have_content(arg1)
+end
+
+Then(/^I should not see "(.*?)"$/) do |arg1|
+ expect(page).to have_no_content(arg1)
+end
+
+Then(/^I should not see the button "(.*?)"$/) do |arg1|
+ expect(page).to have_no_button(arg1)
+end
+
+Given(/^I fill "(.*?)" with:$/) do |arg1, string|
+ fill_in arg1, with: string
+end
+
+When(/^I visit the project page$/) do
+ visit project_path(@project)
+end
+
+When(/^I visit the project "([^"]*)" page$/) do |arg1|
+ @project = Project.find_by!(name: arg1)
+ step 'I visit the project page'
+end
+
+Then(/^I should see the project donation address$/) do
+ address = @project.bitcoin_address
+ expect(address).not_to be_blank
+ expect(page).to have_content(address)
+end
+
+Then(/^I should see the project balance is "(.*?)"$/) do |arg1|
+ begin
+ expect(page).to have_content("Funds #{arg1}")
+ rescue RSpec::Expectations::ExpectationNotMetError
+ ap Project.all.reduce({}) { |h, project| h.merge(project.name => project.deposits) }
+ raise
+ end
+end
+
+Then(/^I should see a link "(.*?)" to "(.*?)"$/) do |arg1, arg2|
+ link = find("a", text: arg1)
+ expect(link["href"]).to eq(arg2)
+end
+
+Then(/^I should not see a link "(.*?)" to "(.*?)"$/) do |arg1, arg2|
+ link = all("a", text: arg1).first
+ expect((link.nil? or link["href"] != arg2)).to be true
+end
+
+Then(/^I should not see the image "(.*?)"$/) do |arg1|
+ find("img[src=\"#{arg1}\"]")
+end
+
+When(/^I click on the "(.*?)" link in the email$/) do |arg1|
+ begin
+ link = Nokogiri::HTML.parse(@email.body.decoded).css("a").detect { |el| el.text == arg1 }
+ expect(link).not_to be_nil
+ rescue
+ puts @email.body
+ raise
+ end
+ url = URI.parse(link["href"]).request_uri
+ visit url
+end
+
+When(/^I click on "(.*?)" in the email$/) do |arg1|
+ step "I click on the \"#{arg1}\" link in the email"
+end
+
+Then(/^the user with email "(.*?)" should have his email confirmed$/) do |arg1|
+ expect(User.find_by(email: arg1).confirmed?).to be true
+end
+
+When(/^I go to edit my profile$/) do
+ find(".edit-profile-link").click
+end
diff --git a/features/support/big_decimal_inspect.rb b/features/support/big_decimal_inspect.rb
new file mode 100644
index 00000000..97c4d459
--- /dev/null
+++ b/features/support/big_decimal_inspect.rb
@@ -0,0 +1,5 @@
+class BigDecimal
+ def inspect
+ ""
+ end
+end
diff --git a/features/support/bitcoin_daemon_mock.rb b/features/support/bitcoin_daemon_mock.rb
new file mode 100644
index 00000000..75e3c0c3
--- /dev/null
+++ b/features/support/bitcoin_daemon_mock.rb
@@ -0,0 +1,72 @@
+class BitcoinDaemonMock
+ def initialize
+ @transactions = []
+ @addresses_by_account = Hash.new
+ end
+
+ def random_address
+ "random_address"
+ end
+
+ def add_transaction(options)
+ transaction = {
+ "account" => "",
+ "address" => random_address,
+ "category" => "receive",
+ "amount" => 10.0,
+ "confirmations" => 10,
+ "blockhash" => SecureRandom.hex(64),
+ "blockindex" => 3,
+ "txid" => SecureRandom.hex(64),
+ "time" => Time.now.to_i,
+ }.merge(options.stringify_keys)
+ @transactions << transaction
+ end
+
+ def list_transactions(account = "", count = 10, from = 0)
+ @transactions.select { |t| account == "*" ? true : (t["account"] == account) }[from, count]
+ end
+
+ def clear_transaction_history
+ @transactions.clear
+ end
+
+ def send_many(account, recipients, minconf = 1)
+ txid = SecureRandom.hex(64)
+ recipients.each do |recipient, amount|
+ @transactions << {
+ "account" => account,
+ "address" => recipient,
+ "category" => "send",
+ "amount" => -amount.to_f,
+ "confirmations" => 10,
+ "blockhash" => SecureRandom.hex(64),
+ "blockindex" => 3,
+ "txid" => txid,
+ "time" => Time.now.to_i,
+ }
+ end
+ txid
+ end
+
+ def get_new_address(account)
+ @addresses_by_account[account] ||= []
+ address = SecureRandom.hex(10)
+ @addresses_by_account[account] << address
+ address
+ end
+
+ def get_addresses_by_account(account)
+ @addresses_by_account[account] || []
+ end
+
+ def get_balance(account)
+ 0
+ end
+end
+
+Before do
+ BitcoinDaemon.instance_eval do
+ @bitcoin_daemon = BitcoinDaemonMock.new
+ end
+end
diff --git a/features/support/env.rb b/features/support/env.rb
new file mode 100644
index 00000000..501084dd
--- /dev/null
+++ b/features/support/env.rb
@@ -0,0 +1,69 @@
+# IMPORTANT: This file is generated by cucumber-rails - edit at your own peril.
+# It is recommended to regenerate this file in the future when you upgrade to a
+# newer version of cucumber-rails. Consider adding your own code to a new file
+# instead of editing this one. Cucumber will automatically load all features/**/*.rb
+# files.
+
+require 'cucumber/rails'
+
+# Capybara defaults to CSS3 selectors rather than XPath.
+# If you'd prefer to use XPath, just uncomment this line and adjust any
+# selectors in your step definitions to use the XPath syntax.
+# Capybara.default_selector = :xpath
+
+# By default, any exception happening in your Rails application will bubble up
+# to Cucumber so that your scenario will fail. This is a different from how
+# your application behaves in the production environment, where an error page will
+# be rendered instead.
+#
+# Sometimes we want to override this default behaviour and allow Rails to rescue
+# exceptions and display an error page (just like when the app is running in production).
+# Typical scenarios where you want to do this is when you test your error pages.
+# There are two ways to allow Rails to rescue exceptions:
+#
+# 1) Tag your scenario (or feature) with @allow-rescue
+#
+# 2) Set the value below to true. Beware that doing this globally is not
+# recommended as it will mask a lot of errors for you!
+#
+ActionController::Base.allow_rescue = false
+
+# Remove/comment out the lines below if your app doesn't have a database.
+# For some databases (like MongoDB and CouchDB) you may need to use :truncation instead.
+begin
+ DatabaseCleaner.strategy = :transaction
+rescue NameError
+ raise "You need to add database_cleaner to your Gemfile (in the :test group) if you wish to use it."
+end
+
+# You may also want to configure DatabaseCleaner to use different strategies for certain features and scenarios.
+# See the DatabaseCleaner documentation for details. Example:
+#
+# Before('@no-txn,@selenium,@culerity,@celerity,@javascript') do
+# # { :except => [:widgets] } may not do what you expect here
+# # as Cucumber::Rails::Database.javascript_strategy overrides
+# # this setting.
+# DatabaseCleaner.strategy = :truncation
+# end
+#
+# Before('~@no-txn', '~@selenium', '~@culerity', '~@celerity', '~@javascript') do
+# DatabaseCleaner.strategy = :transaction
+# end
+#
+
+# Possible values are :truncation and :transaction
+# The :transaction strategy is faster, but might give you threading problems.
+# See https://github.com/cucumber/cucumber-rails/blob/master/features/choose_javascript_database_strategy.feature
+Cucumber::Rails::Database.javascript_strategy = :truncation
+
+require 'capybara/poltergeist'
+if ENV["FIREFOX"]
+ Capybara.javascript_driver = :selenium
+else
+ Capybara.register_driver :poltergeist do |app|
+ Capybara::Poltergeist::Driver.new(app, inspector: true)
+ end
+ Capybara.javascript_driver = :poltergeist
+end
+
+require 'capybara-screenshot/cucumber'
diff --git a/features/support/factory_girl.rb b/features/support/factory_girl.rb
new file mode 100644
index 00000000..139fbe01
--- /dev/null
+++ b/features/support/factory_girl.rb
@@ -0,0 +1 @@
+World(FactoryGirl::Syntax::Methods)
diff --git a/features/support/finders.rb b/features/support/finders.rb
new file mode 100644
index 00000000..47334b17
--- /dev/null
+++ b/features/support/finders.rb
@@ -0,0 +1,4 @@
+def find_project(name)
+ project = Project.where(full_name: "example/#{name}").first
+ project or raise "Project #{name.inspect} not found"
+end
diff --git a/features/support/octokit_mock.rb b/features/support/octokit_mock.rb
new file mode 100644
index 00000000..718fab0c
--- /dev/null
+++ b/features/support/octokit_mock.rb
@@ -0,0 +1,20 @@
+class Octokit::Client
+ def initialize(*args)
+ end
+
+ def commits(*args)
+ []
+ end
+
+ def user(login)
+ GITHUB_USERS.fetch(login) do
+ raise Octokit::NotFound
+ end
+ end
+end
+
+GITHUB_USERS = {}
+
+Before do
+ GITHUB_USERS.clear
+end
diff --git a/features/support/rspec_doubles.rb b/features/support/rspec_doubles.rb
new file mode 100644
index 00000000..6476fc19
--- /dev/null
+++ b/features/support/rspec_doubles.rb
@@ -0,0 +1 @@
+require 'cucumber/rspec/doubles'
diff --git a/features/support/to_ostruct.rb b/features/support/to_ostruct.rb
new file mode 100644
index 00000000..bbb9cbc8
--- /dev/null
+++ b/features/support/to_ostruct.rb
@@ -0,0 +1,23 @@
+require 'ostruct'
+
+class Hash
+ def to_ostruct
+ o = OpenStruct.new(self)
+ each do |k,v|
+ o.send(:"#{k}=", v.to_ostruct) if v.respond_to? :to_ostruct
+ end
+ o
+ end
+end
+
+class Array
+ def to_ostruct
+ map do |item|
+ if item.respond_to? :to_ostruct
+ item.to_ostruct
+ else
+ item
+ end
+ end
+ end
+end
diff --git a/features/tip_for_commit.feature b/features/tip_for_commit.feature
new file mode 100644
index 00000000..221494aa
--- /dev/null
+++ b/features/tip_for_commit.feature
@@ -0,0 +1,48 @@
+Feature: On projects not holding tips, a tip is created for each new commit
+ Scenario: A project not holding tips
+ Given a project
+ And the project does not hold tips
+ And the project GitHub name is "foo/bar"
+ And the commits on GitHub for project "foo/bar" are
+ | sha | author | email |
+ | 123 | bob | bobby@example.com |
+ | abc | alice | alicia@example.com |
+ | 333 | bob | bobby@example.com |
+ And our fee is "0"
+ And a deposit of "500"
+ Given a GitHub user "bob" who has set his address to "mxWfjaZJTNN5QKeZZYQ5HW3vgALFBsnuG1"
+
+ When the project tips are built from commits
+ Then the project should have these tips:
+ | commit | amount |
+ | 123 | 5.0 |
+ | 333 | 4.95 |
+
+ When the tipper is started
+ Then these amounts should have been sent from the account of the project:
+ | address | amount |
+ | mxWfjaZJTNN5QKeZZYQ5HW3vgALFBsnuG1 | 9.95 |
+
+ And no email should have been sent
+
+ Scenario: A project holding tips
+ Given a project
+ And the project holds tips
+ And the project GitHub name is "foo/bar"
+ And the commits on GitHub for project "foo/bar" are
+ | sha | author | email |
+ | 123 | bob | bobby@example.com |
+ | abc | alice | alicia@example.com |
+ | 333 | bob | bobby@example.com |
+ And our fee is "0"
+ And a deposit of "500"
+ And a GitHub user "bob" who has set his address to "mxWfjaZJTNN5QKeZZYQ5HW3vgALFBsnuG1"
+
+ When the project tips are built from commits
+ Then the project should have these tips:
+ | commit | amount |
+ | 123 | |
+ | 333 | |
+
+ When the tipper is started
+ Then no coins should have been sent
diff --git a/features/tip_modifier_interface.feature b/features/tip_modifier_interface.feature
new file mode 100644
index 00000000..51589c0d
--- /dev/null
+++ b/features/tip_modifier_interface.feature
@@ -0,0 +1,200 @@
+Feature: A project collaborator can change the tips of commits
+ Background:
+ Given a project "a"
+ And the project collaborators are:
+ | seldon |
+ | daneel |
+ And our fee is "0"
+ And a deposit of "500"
+ And the last known commit is "AAA"
+ And a new commit "BBB" with parent "AAA"
+ And a new commit "CCC" with parent "BBB"
+ And the author of commit "BBB" is "yugo"
+ And the message of commit "BBB" is "Tiny change"
+ And the author of commit "CCC" is "gaal"
+
+ Scenario: Without anything modified and known users
+ Given a GitHub user "yugo" who has set his address to "mxWfjaZJTNN5QKeZZYQ5HW3vgALFBsnuG1"
+ And a GitHub user "gaal" who has set his address to "mi9SLroAgc8eUNuLwnZmdyqWdShbNtvr3n"
+ When the new commits are read
+ Then there should be a tip of "5" for commit "BBB"
+ And there should be a tip of "4.95" for commit "CCC"
+ And there should be 0 email sent
+
+ Scenario: Without anything modified and unknown users
+ When the new commits are read
+ Then there should be 0 tip
+ And there should be 0 email sent
+
+ Scenario: A collaborator wants to alter the tips
+ Given I'm logged in as "seldon"
+ And I go to the project page
+ And I click on "Edit project"
+ And I uncheck "Automatically send 1% of the balance to each commit added to the default branch of the GitHub project"
+ And I click on "Save"
+ Then I should see "The project has been updated"
+
+ Given a GitHub user "yugo" who has set his address to "mxWfjaZJTNN5QKeZZYQ5HW3vgALFBsnuG1"
+ And a GitHub user "gaal" who has set his address to "mi9SLroAgc8eUNuLwnZmdyqWdShbNtvr3n"
+
+ When the new commits are read
+ Then the tip amount for commit "BBB" should be undecided
+ And the tip amount for commit "CCC" should be undecided
+ And there should be 0 email sent
+
+ When I go to the project page
+ And I click on "Decide tip amounts"
+ Then I should see "BBB"
+ And I should see "Tiny change"
+ And I should see "CCC"
+ And I should not see "AAA"
+
+ When I choose the amount "Tiny: 0.1%" on commit "BBB"
+ And I click on "Send the selected tip amounts"
+ Then there should be a tip of "0.5" for commit "BBB"
+ And the tip amount for commit "CCC" should be undecided
+ And there should be 0 email sent
+
+ When the email counters are reset
+ And I choose the amount "Free: 0%" on commit "CCC"
+ And I click on "Send the selected tip amounts"
+ Then there should be a tip of "0.5" for commit "BBB"
+ And there should be a tip of "0" for commit "CCC"
+ And there should be 0 email sent
+
+ Scenario: A collaborator wants to alter the tips without known users
+ Given I'm logged in as "seldon"
+ And I go to the project page
+ And I click on "Edit project"
+ And I uncheck "Automatically send 1% of the balance to each commit added to the default branch of the GitHub project"
+ And I click on "Save"
+ Then I should see "The project has been updated"
+
+ When the new commits are read
+ Then there should be 0 tip
+ And there should be 0 email sent
+
+ When I go to the project page
+ And I should not see "Decide tip amounts"
+
+ Scenario: A non collaborator does not see the settings button
+ Given I'm logged in as "yugo"
+ And I go to the project page
+ Then I should not see "Edit project"
+
+ Scenario: A non collaborator does not see the decide tip amounts button
+ Given the project has undedided tips
+ And I'm logged in as "yugo"
+ And I go to the project page
+ Then I should not see "Decide tip amounts"
+
+ Scenario: A non collaborator goes to the edit page of a project
+ Given I'm logged in as "yugo"
+ When I go to the edit page of the project
+ Then I should see an access denied
+
+ Scenario: A non collaborator sends a forged update on a project
+ Given I'm logged in as "yugo"
+ When I send a forged request to enable tip holding on the project
+ Then I should see an access denied
+ And the project should not hold tips
+
+ Scenario: A collaborator sends a forged update on a project
+ Given I'm logged in as "daneel"
+ When I send a forged request to enable tip holding on the project
+ Then the project should hold tips
+
+ Scenario Outline: A user sends a forged request to set a tip amount
+ Given the project has 1 undecided tip
+ And I'm logged in as ""
+ And I go to the project page
+ And I send a forged request to set the amount of the first undecided tip of the project
+ Then the project should have undecided tips
+
+ Examples:
+ | user | remaining undecided tips |
+ | seldon | 0 |
+ | yugo | 1 |
+
+ Scenario: A collaborator sends large amounts in tips
+ Given a GitHub user "yugo" who has set his address to "mxWfjaZJTNN5QKeZZYQ5HW3vgALFBsnuG1"
+ Given 20 new commits by "yugo"
+ And a new commit "last" by "yugo"
+ And the project holds tips
+ When the new commits are read
+ And I'm logged in as "seldon"
+ And I go to the project page
+ And I click on "Decide tip amounts"
+ And I choose the amount "Huge: 5%" on all commits
+ And I click on "Send the selected tip amounts"
+ Then there should be a tip of "25" for commit "BBB"
+ And there should be a tip of "8.51404" for commit "last"
+
+ Scenario Outline: A collaborator changes the amount of a tip on another project
+ Given the project holds tips
+ Given a GitHub user "yugo" who has set his address to "mxWfjaZJTNN5QKeZZYQ5HW3vgALFBsnuG1"
+ And the new commits are read
+ And a project "fake"
+ And a deposit of "500"
+ And the project collaborators are:
+ | bad guy |
+ And a new commit "fake commit"
+ And the project holds tips
+ When the new commits are read
+ And I'm logged in as ""
+ And I send a forged request to change the percentage of commit "BBB" on project "a" to "5"
+ Then
+
+ Examples:
+ | user | consequences |
+ | seldon | there should be a tip of "25" for commit "BBB" |
+ | bad guy | the tip amount for commit "BBB" should be undecided |
+
+ Scenario: A collaborator sends a free amount as tip
+ Given the project holds tips
+ And a GitHub user "yugo" who has set his address to "mxWfjaZJTNN5QKeZZYQ5HW3vgALFBsnuG1"
+ And a GitHub user "gaal" who has set his address to "mi9SLroAgc8eUNuLwnZmdyqWdShbNtvr3n"
+ And the new commits are read
+ And I'm logged in as "seldon"
+ And I go to the project page
+ And I click on "Decide tip amounts"
+ When I fill the free amount with "10" on commit "BBB"
+ And I click on "Send the selected tip amounts"
+ Then there should be a tip of "10" for commit "BBB"
+ And the tip amount for commit "CCC" should be undecided
+
+ Scenario: A collaborator sends too big free amounts
+ Given the project holds tips
+ And a GitHub user "yugo" who has set his address to "mxWfjaZJTNN5QKeZZYQ5HW3vgALFBsnuG1"
+ And a GitHub user "gaal" who has set his address to "mi9SLroAgc8eUNuLwnZmdyqWdShbNtvr3n"
+ And the new commits are read
+ And I'm logged in as "seldon"
+ And I go to the project page
+ And I click on "Decide tip amounts"
+ When I choose the amount "Tiny: 0.1%" on commit "BBB"
+ And I fill the free amount with "499.500001" on commit "CCC"
+ And I click on "Send the selected tip amounts"
+ Then I should see "The project has insufficient funds"
+ And the tip amount for commit "BBB" should be undecided
+ And the tip amount for commit "CCC" should be undecided
+
+ When I fill the free amount with "499.5" on commit "CCC"
+ And I click on "Send the selected tip amounts"
+ Then there should be a tip of "0.5" for commit "BBB"
+ And there should be a tip of "499.5" for commit "CCC"
+ And the project balance should be "0"
+
+ Scenario: A collaborator changes the amount of an already decided tip
+ Given the project holds tips
+ And a GitHub user "yugo" who has set his address to "mxWfjaZJTNN5QKeZZYQ5HW3vgALFBsnuG1"
+ And a GitHub user "gaal" who has set his address to "mi9SLroAgc8eUNuLwnZmdyqWdShbNtvr3n"
+ And the new commits are read
+ And I'm logged in as "seldon"
+ And I go to the project page
+ And I click on "Decide tip amounts"
+ When I fill the free amount with "10" on commit "BBB"
+ And I click on "Send the selected tip amounts"
+ Then there should be a tip of "10" for commit "BBB"
+ And the tip amount for commit "CCC" should be undecided
+ And I send a forged request to change the percentage of commit "BBB" on project "a" to "5"
+ Then there should be a tip of "10" for commit "BBB"
diff --git a/features/tipping_policies.feature b/features/tipping_policies.feature
new file mode 100644
index 00000000..c8f6ed09
--- /dev/null
+++ b/features/tipping_policies.feature
@@ -0,0 +1,25 @@
+Feature: A project collaborator can display the tipping policies of the project
+ Background:
+ Given a project
+ And the project collaborators are:
+ | seldon |
+ | daneel |
+
+ Scenario: A collaborator changes the tipping policies
+ Given I'm logged in as "seldon"
+ And I go to the project page
+ And I click on "Edit project"
+ And I fill "Tipping policies" with:
+ """
+ All commits are huge!
+
+ Blah blah
+ """
+ And I click on "Save"
+ Then I should see "The project has been updated"
+
+ Given I'm not logged in
+ And I go to the project page
+ Then I should see "All commits are huge!"
+ And I should see "Blah blah"
+ And I should see "seldon"
diff --git a/features/user_identifier.feature b/features/user_identifier.feature
new file mode 100644
index 00000000..9921872d
--- /dev/null
+++ b/features/user_identifier.feature
@@ -0,0 +1,20 @@
+Feature: Each user has an unique identifier
+ Scenario: New email user gets an unique identifier
+ When I visit the home page
+ And I click on "Sign in"
+ And I click on "Sign up"
+ And I fill "Email" with "bob@example.com"
+ And I fill "Password" with "password"
+ And I fill "Password confirmation" with "password"
+ And I click on "Sign up"
+ Then I should see "confirmation link"
+
+ And an email should have been sent to "bob@example.com"
+ When I click on "Confirm my account" in the email
+ Then I should see "confirmed"
+
+ And I fill "Email" with "bob@example.com"
+ And I fill "Password" with "password"
+ And I click on "Sign in" in the sign in form
+ When I go to edit my profile
+ Then I should see the identifier of "bob@example.com"
diff --git a/lib/balance_updater.rb b/lib/balance_updater.rb
new file mode 100644
index 00000000..c51c1d99
--- /dev/null
+++ b/lib/balance_updater.rb
@@ -0,0 +1,131 @@
+module BalanceUpdater
+ def self.work(projects = Project.all)
+ projects.each do |project|
+ start = 0
+ count = 10
+
+ raise "Project without address label: #{project.inspect}" if project.address_label.blank?
+
+ project.update(account_balance: (BitcoinDaemon.instance.get_balance(project.address_label) * COIN).to_i)
+
+ if project.cold_storage_withdrawal_address.blank?
+ new_address = BitcoinDaemon.instance.get_new_address(project.address_label)
+ project.update!(cold_storage_withdrawal_address: new_address)
+ end
+
+ stake_mint = 0
+
+ loop do
+ transactions = BitcoinDaemon.instance.list_transactions(project.address_label, count, start)
+ break if transactions.empty?
+
+ transactions.each do |transaction|
+ txid = transaction["txid"]
+ confirmations = transaction["confirmations"]
+ category = transaction["category"]
+ fee = transaction["fee"]
+
+ if category == "move"
+ next
+ end
+
+ if category == "send" and distribution = Distribution.where(txid: txid).first
+ raise "No fee on distribution #{distribution.inspect}" unless fee
+ distribution.update(fee: -fee * COIN)
+ next
+ end
+
+ if deposit = project.deposits.find_by_txid(txid)
+ deposit.update_attribute(:confirmations, confirmations)
+ next
+ end
+
+ if cold_storage_transfer = ColdStorageTransfer.find_by_txid(txid)
+ cold_storage_transfer.confirmations = confirmations
+ cold_storage_transfer.fee = -fee * COIN if fee
+ cold_storage_transfer.save!
+ next
+ end
+
+ address = transaction["address"]
+ if address.blank?
+ raise "Invalid transaction: #{transaction.inspect}"
+ end
+
+ cold_storage_addresses = CONFIG["cold_storage"].try(:[], "addresses") || []
+ cold_storage_withdrawal_address = project.cold_storage_withdrawal_address
+
+ amount = (transaction["amount"].to_d * COIN).to_i
+
+ if %w( stake-mint stake ).include?(category)
+ # When a block is found, peercoin seems to debit the default account ("") and credit the account associated with the address.
+ # So we record these false amounts to remove them in the audit page
+ stake_mint += amount
+ next
+ end
+
+ if category == "stake-orphan"
+ next
+ end
+
+ if address == cold_storage_withdrawal_address
+ if category != "receive"
+ raise "Unexpected cold storage withdrawal: #{transaction.inspect}"
+ end
+
+ project.cold_storage_transfers.create!(
+ address: address,
+ txid: txid,
+ confirmations: confirmations,
+ amount: -amount,
+ )
+ next
+ end
+
+ if cold_storage_addresses.include?(address)
+ if category != "send"
+ raise "Unexpected cold storage transaction: #{transaction.inspect}"
+ end
+
+ project.cold_storage_transfers.create!(
+ address: address,
+ txid: txid,
+ confirmations: confirmations,
+ amount: -amount,
+ fee: fee,
+ )
+ next
+ end
+
+ if category == "receive"
+ if address == project.bitcoin_address
+ donation_address = nil
+ elsif donation_address = project.donation_addresses.detect { |da| da.donation_address == address}
+ else
+ raise "Funds received to unexpected address: #{transaction.inspect}"
+ end
+
+ deposit = Deposit.create(
+ project_id: project.id,
+ txid: txid,
+ confirmations: confirmations,
+ amount: amount,
+ duration: 30.days.to_i,
+ paid_out: 0,
+ paid_out_at: Time.now,
+ donation_address: donation_address,
+ )
+ next
+ end
+
+ raise "Unexpected transaction: #{transaction.inspect}"
+ end
+
+ project.update(stake_mint_amount: stake_mint)
+
+ break if transactions.size < count
+ start += count
+ end
+ end
+ end
+end
diff --git a/lib/bitcoin_address_validator.rb b/lib/bitcoin_address_validator.rb
index 82cc184c..7332bd7a 100644
--- a/lib/bitcoin_address_validator.rb
+++ b/lib/bitcoin_address_validator.rb
@@ -3,7 +3,7 @@
class BitcoinAddressValidator < ActiveModel::EachValidator
def validate_each(record, field, value)
unless value.blank? || valid_bitcoin_address?(value)
- record.errors[field] << "Bitcoin address is invalid"
+ record.errors[field] << "Peercoin address is invalid"
end
end
@@ -13,7 +13,15 @@ def validate_each(record, field, value)
B58Base = B58Chars.length
def valid_bitcoin_address?(address)
- (address =~ /^[a-zA-Z1-9]{33,35}$/) and version(address)
+ if (address =~ /^[a-zA-Z1-9]{33,35}$/) and version = version(address)
+ if (expected_versions = CONFIG["address_versions"]).present?
+ expected_versions.include?(version.ord)
+ else
+ true
+ end
+ else
+ false
+ end
end
def version(address)
@@ -52,4 +60,4 @@ def b58_decode(value, length)
result
end
-end
\ No newline at end of file
+end
diff --git a/lib/bitcoin_daemon.rb b/lib/bitcoin_daemon.rb
new file mode 100644
index 00000000..b37f082a
--- /dev/null
+++ b/lib/bitcoin_daemon.rb
@@ -0,0 +1,64 @@
+class BitcoinDaemon
+ def self.instance
+ @bitcoin_daemon ||= BitcoinDaemon.new(CONFIG['daemon'])
+ end
+
+ class RPCError < StandardError
+ attr_accessor :code
+ def initialize(code, message)
+ @code = code
+ super(message)
+ end
+ end
+
+ attr_reader :config
+
+ def initialize(config)
+ @config = config || {}
+ end
+
+ def rpc(command, *params)
+ %w( username password port host ).each do |field|
+ raise "No #{field} provided in daemon config" if config[field].blank?
+ end
+
+ uri = URI::HTTP.build(host: config['host'], port: config['port'].to_i)
+
+ auth = config.slice('username', 'password').symbolize_keys
+
+ data = {
+ method: command,
+ params: params,
+ id: 1,
+ }
+
+ Rails.logger.info "RPC Command: #{data.inspect}"
+ response = HTTParty.post(uri.to_s, body: data.to_json, basic_auth: auth)
+
+ result = JSON.parse(response.body)
+ if error = result["error"]
+ raise RPCError.new(error["code"], error["message"])
+ end
+ result["result"]
+ end
+
+ def get_new_address(account = "")
+ rpc('getnewaddress', account)
+ end
+
+ def list_transactions(account = "", count = 10, from = 0)
+ rpc('listtransactions', account, count, from)
+ end
+
+ def send_many(account, recipients, minconf = 1)
+ recipients = recipients.dup
+ recipients.each do |address, amount|
+ recipients[address] = amount.to_f
+ end
+ rpc('sendmany', account, recipients, minconf)
+ end
+
+ def get_balance(account = "")
+ rpc('getbalance', account).to_f
+ end
+end
diff --git a/lib/bitcoin_tipper.rb b/lib/bitcoin_tipper.rb
index da10ccb2..4dd1ca7d 100644
--- a/lib/bitcoin_tipper.rb
+++ b/lib/bitcoin_tipper.rb
@@ -7,29 +7,26 @@ def self.work_forever
def self.work
Rails.logger.info "Traversing projects..."
- Project.find_each do |project|
- if project.available_amount > 0
- Rails.logger.info " Project #{project.id} #{project.full_name}"
- project.tip_commits
- end
+ Project.enabled.find_each do |project|
+ Rails.logger.info " Project #{project.id} #{project.full_name}"
+ project.update_commits
+ project.tip_commits
end
- Rails.logger.info "Traversing users..."
- is_sendmany_needed = false
- User.find_each do |user|
- if user.bitcoin_address.present? && user.balance > CONFIG["min_payout"]
- is_sendmany_needed = true
- Rails.logger.info "Sendmany is needed"
+ Rails.logger.info "Sending tips to commits..."
+ Project.enabled.find_each do |project|
+ tips = project.tips_to_pay
+ amount = tips.sum(&:amount).to_d
+ if amount > CONFIG["min_payout"].to_d * COIN
+ distribution = Distribution.create(project_id: project.id)
+ tips.each do |tip|
+ tip.update_attribute :distribution_id, distribution.id
+ end
+ distribution.reload.send_transaction!
+ Rails.logger.info " #{distribution.inspect}"
end
end
- self.create_sendmany if is_sendmany_needed
-
- Rails.logger.info "Traversing sendmanies..."
- Sendmany.where(txid: nil).each do |sendmany|
- sendmany.send_transaction
- end
-
Rails.logger.info "Refunding unclaimed tips..."
Tip.refund_unclaimed
@@ -40,22 +37,10 @@ def self.work
User.update_cache
end
- def self.create_sendmany
- Rails.logger.info "Creating sendmany"
+ def self.create_distributions
+ Rails.logger.info "Creating distribution"
ActiveRecord::Base.transaction do
- sendmany = Sendmany.create
- outs = {}
- User.find_each do |user|
- if user.bitcoin_address.present? && user.balance > CONFIG["min_payout"]
- user.tips.unpaid.each do |tip|
- tip.update_attribute :sendmany_id, sendmany.id
- outs[user.bitcoin_address] = outs[user.bitcoin_address].to_i + tip.amount
- end
- end
- end
- sendmany.update_attribute :data, outs.to_json
- Rails.logger.info " #{sendmany.inspect}"
end
end
-end
\ No newline at end of file
+end
diff --git a/lib/tasks/cucumber.rake b/lib/tasks/cucumber.rake
new file mode 100644
index 00000000..9f53ce49
--- /dev/null
+++ b/lib/tasks/cucumber.rake
@@ -0,0 +1,65 @@
+# IMPORTANT: This file is generated by cucumber-rails - edit at your own peril.
+# It is recommended to regenerate this file in the future when you upgrade to a
+# newer version of cucumber-rails. Consider adding your own code to a new file
+# instead of editing this one. Cucumber will automatically load all features/**/*.rb
+# files.
+
+
+unless ARGV.any? {|a| a =~ /^gems/} # Don't load anything when running the gems:* tasks
+
+vendored_cucumber_bin = Dir["#{Rails.root}/vendor/{gems,plugins}/cucumber*/bin/cucumber"].first
+$LOAD_PATH.unshift(File.dirname(vendored_cucumber_bin) + '/../lib') unless vendored_cucumber_bin.nil?
+
+begin
+ require 'cucumber/rake/task'
+
+ namespace :cucumber do
+ Cucumber::Rake::Task.new({:ok => 'test:prepare'}, 'Run features that should pass') do |t|
+ t.binary = vendored_cucumber_bin # If nil, the gem's binary is used.
+ t.fork = true # You may get faster startup if you set this to false
+ t.profile = 'default'
+ end
+
+ Cucumber::Rake::Task.new({:wip => 'test:prepare'}, 'Run features that are being worked on') do |t|
+ t.binary = vendored_cucumber_bin
+ t.fork = true # You may get faster startup if you set this to false
+ t.profile = 'wip'
+ end
+
+ Cucumber::Rake::Task.new({:rerun => 'test:prepare'}, 'Record failing features and run only them if any exist') do |t|
+ t.binary = vendored_cucumber_bin
+ t.fork = true # You may get faster startup if you set this to false
+ t.profile = 'rerun'
+ end
+
+ desc 'Run all features'
+ task :all => [:ok, :wip]
+
+ task :statsetup do
+ require 'rails/code_statistics'
+ ::STATS_DIRECTORIES << %w(Cucumber\ features features) if File.exist?('features')
+ ::CodeStatistics::TEST_TYPES << "Cucumber features" if File.exist?('features')
+ end
+ end
+ desc 'Alias for cucumber:ok'
+ task :cucumber => 'cucumber:ok'
+
+ task :default => :cucumber
+
+ task :features => :cucumber do
+ STDERR.puts "*** The 'features' task is deprecated. See rake -T cucumber ***"
+ end
+
+ # In case we don't have the generic Rails test:prepare hook, append a no-op task that we can depend upon.
+ task 'test:prepare' do
+ end
+
+ task :stats => 'cucumber:statsetup'
+rescue LoadError
+ desc 'cucumber rake task not available (cucumber not installed)'
+ task :cucumber do
+ abort 'Cucumber rake task is not available. Be sure to install cucumber as a gem or plugin'
+ end
+end
+
+end
diff --git a/lib/tasks/reassign_noreply_tips.rake b/lib/tasks/reassign_noreply_tips.rake
new file mode 100644
index 00000000..d562a2ee
--- /dev/null
+++ b/lib/tasks/reassign_noreply_tips.rake
@@ -0,0 +1,37 @@
+task :reassign_noreply_tips => :environment do
+ logger = Rails.logger
+ logger.info "Reassigning noreply tips"
+
+ User.transaction do
+ User.where("email like '%@users.noreply.github.com'").each do |user|
+ next unless user.nickname.present?
+
+ all = User.where(nickname: user.nickname)
+ users_with_address = all.select(&:bitcoin_address)
+ next if users_with_address.size != 1
+
+ real_user = users_with_address.first
+ logger.info "Real user: #{real_user.inspect}"
+
+ all.each do |other|
+ next if other == real_user
+ logger.info "Reassigning tips from user #{other.inspect}"
+ other.tips.each do |tip|
+ if tip.project.disabled?
+ logger.info "Skipping disabled project on tip #{tip.inspect}"
+ next
+ end
+ logger.info "Reassigning tip #{tip.inspect}"
+ tip.user = real_user
+ if tip.refunded?
+ logger.info "Canceling refunded state"
+ tip.refunded_at = nil
+ end
+ if ENV["PROCEED"] == "yes"
+ tip.save!
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/tasks/send_security_issue.rake b/lib/tasks/send_security_issue.rake
new file mode 100644
index 00000000..68720afd
--- /dev/null
+++ b/lib/tasks/send_security_issue.rake
@@ -0,0 +1,5 @@
+task :send_security_issue => :environment do
+ User.where(unsubscribed: nil).each do |user|
+ UserMailer.security_issue(user).deliver
+ end
+end
diff --git a/lib/templates/haml/scaffold/_form.html.haml b/lib/templates/haml/scaffold/_form.html.haml
new file mode 100644
index 00000000..ac3aa7bc
--- /dev/null
+++ b/lib/templates/haml/scaffold/_form.html.haml
@@ -0,0 +1,10 @@
+= simple_form_for(@<%= singular_table_name %>) do |f|
+ = f.error_notification
+
+ .form-inputs
+ <%- attributes.each do |attribute| -%>
+ = f.<%= attribute.reference? ? :association : :input %> :<%= attribute.name %>
+ <%- end -%>
+
+ .form-actions
+ = f.button :submit
diff --git a/public/favicon.ico b/public/favicon.ico
deleted file mode 100644
index e69de29b..00000000
diff --git a/script/cucumber b/script/cucumber
new file mode 100755
index 00000000..7fa5c920
--- /dev/null
+++ b/script/cucumber
@@ -0,0 +1,10 @@
+#!/usr/bin/env ruby
+
+vendored_cucumber_bin = Dir["#{File.dirname(__FILE__)}/../vendor/{gems,plugins}/cucumber*/bin/cucumber"].first
+if vendored_cucumber_bin
+ load File.expand_path(vendored_cucumber_bin)
+else
+ require 'rubygems' unless ENV['NO_RUBYGEMS']
+ require 'cucumber'
+ load Cucumber::BINARY
+end
diff --git a/test/factories/tips.rb b/test/factories/tips.rb
new file mode 100644
index 00000000..84eb127c
--- /dev/null
+++ b/test/factories/tips.rb
@@ -0,0 +1,13 @@
+# Read about factories at https://github.com/thoughtbot/factory_girl
+
+FactoryGirl.define do
+ factory :tip do
+ association :user
+ amount 2
+ commit { Digest::SHA1.hexdigest(SecureRandom.hex) }
+
+ factory :undecided_tip do
+ amount nil
+ end
+ end
+end
diff --git a/test/factories/users.rb b/test/factories/users.rb
new file mode 100644
index 00000000..6f680e2a
--- /dev/null
+++ b/test/factories/users.rb
@@ -0,0 +1,9 @@
+# Read about factories at https://github.com/thoughtbot/factory_girl
+
+FactoryGirl.define do
+ factory :user do
+ sequence(:email) { |n| "user#{n}@example.com" }
+ password "password"
+ confirmed_at { Time.now }
+ end
+end
diff --git a/test/fixtures/commits.yml b/test/fixtures/commits.yml
new file mode 100644
index 00000000..181f87da
--- /dev/null
+++ b/test/fixtures/commits.yml
@@ -0,0 +1,15 @@
+# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
+
+one:
+ project_id:
+ sha: MyString
+ message: MyText
+ username: MyString
+ email: MyString
+
+two:
+ project_id:
+ sha: MyString
+ message: MyText
+ username: MyString
+ email: MyString
diff --git a/test/fixtures/donation_addresses.yml b/test/fixtures/donation_addresses.yml
new file mode 100644
index 00000000..e8a15301
--- /dev/null
+++ b/test/fixtures/donation_addresses.yml
@@ -0,0 +1,11 @@
+# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
+
+one:
+ project_id:
+ sender_address: MyString
+ donation_address: MyString
+
+two:
+ project_id:
+ sender_address: MyString
+ donation_address: MyString
diff --git a/test/fixtures/tipping_policies_texts.yml b/test/fixtures/tipping_policies_texts.yml
new file mode 100644
index 00000000..3b5a5fa9
--- /dev/null
+++ b/test/fixtures/tipping_policies_texts.yml
@@ -0,0 +1,11 @@
+# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
+
+one:
+ project_id:
+ user_id:
+ text: MyText
+
+two:
+ project_id:
+ user_id:
+ text: MyText
diff --git a/test/fixtures/tips.yml b/test/fixtures/tips.yml
index 1f9726c4..07b0ad88 100644
--- a/test/fixtures/tips.yml
+++ b/test/fixtures/tips.yml
@@ -3,11 +3,11 @@
one:
user_id:
amount: 1
- sendmany_id:
+ distribution_id:
refunded_at: false
two:
user_id:
amount: 1
- sendmany_id:
+ distribution_id:
refunded_at: false
diff --git a/test/models/sendmany_test.rb b/test/models/commit_test.rb
similarity index 63%
rename from test/models/sendmany_test.rb
rename to test/models/commit_test.rb
index 13c4f4e3..2424af32 100644
--- a/test/models/sendmany_test.rb
+++ b/test/models/commit_test.rb
@@ -1,6 +1,6 @@
require 'test_helper'
-class SendmanyTest < ActiveSupport::TestCase
+class CommitTest < ActiveSupport::TestCase
# test "the truth" do
# assert true
# end
diff --git a/test/models/donation_address_test.rb b/test/models/donation_address_test.rb
new file mode 100644
index 00000000..be2ed6b4
--- /dev/null
+++ b/test/models/donation_address_test.rb
@@ -0,0 +1,7 @@
+require 'test_helper'
+
+class DonationAddressTest < ActiveSupport::TestCase
+ # test "the truth" do
+ # assert true
+ # end
+end
diff --git a/test/models/record_change_test.rb b/test/models/record_change_test.rb
new file mode 100644
index 00000000..52c4dd4f
--- /dev/null
+++ b/test/models/record_change_test.rb
@@ -0,0 +1,7 @@
+require 'test_helper'
+
+class RecordChangeTest < ActiveSupport::TestCase
+ # test "the truth" do
+ # assert true
+ # end
+end
diff --git a/test/models/tipping_policies_text_test.rb b/test/models/tipping_policies_text_test.rb
new file mode 100644
index 00000000..22743b43
--- /dev/null
+++ b/test/models/tipping_policies_text_test.rb
@@ -0,0 +1,7 @@
+require 'test_helper'
+
+class TippingPoliciesTextTest < ActiveSupport::TestCase
+ # test "the truth" do
+ # assert true
+ # end
+end