From c9eb0498fc39e0a9a1b23fa09ac536ac89677cbf Mon Sep 17 00:00:00 2001 From: Aleksandr Zykov Date: Tue, 11 Feb 2014 08:19:16 +0700 Subject: [PATCH 001/372] changed tip4commit project id --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 87254791..643d8225 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ Tip4commit ========== -[![tip for next commit](http://tip4commit.com/projects/307.svg)](http://tip4commit.com/projects/307) +[![tip for next commit](http://tip4commit.com/projects/560.svg)](http://tip4commit.com/projects/560) Donate bitcoins to open source projects or make commits and get tips for it. From 84a06cdceacf4f020f372a9b93004916585cf6be Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Fri, 14 Feb 2014 16:02:33 +0100 Subject: [PATCH 002/372] updated readme to new project peer4commit --- README.md | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 643d8225..148b0a8b 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,15 @@ -Tip4commit +Peer4commit ========== [![tip for next commit](http://tip4commit.com/projects/560.svg)](http://tip4commit.com/projects/560) -Donate bitcoins to open source projects or make commits and get tips for it. +Donate peercoins to open source projects or make commits and get tips for it. -Official site: http://tip4commit.com/ +Official site: http://peer4commit.com/ -Forum thread: https://bitcointalk.org/index.php?topic=315802 - -FAQ: https://github.com/tip4commit/tip4commit/wiki/FAQ - -ToDo: https://github.com/tip4commit/tip4commit/issues +Based on [tip4commit](https://github.com/tip4commit) License ======= -[MIT License](https://github.com/tip4commit/tip4commit/blob/master/LICENSE) +[MIT License](https://github.com/sigmike/peer4commit/blob/master/LICENSE) From e4274dbb86da9c1e6f67d33c2e7e18f6aaa83508 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Fri, 14 Feb 2014 16:24:52 +0100 Subject: [PATCH 003/372] changed site names and copyright --- LICENSE | 2 +- README.md | 3 ++- app/views/layouts/application.html.haml | 17 +++++++++-------- app/views/projects/show.svg.erb | 2 +- app/views/user_mailer/new_tip.html.haml | 2 +- config/database.yml.sample | 4 ++-- config/deploy.rb | 6 +++--- 7 files changed, 19 insertions(+), 17 deletions(-) diff --git a/LICENSE b/LICENSE index 5707826a..7d06a610 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2014 tip4commit +Copyright (c) 2014 sigmike Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in diff --git a/README.md b/README.md index 148b0a8b..c0580ce0 100644 --- a/README.md +++ b/README.md @@ -7,9 +7,10 @@ Donate peercoins to open source projects or make commits and get tips for it. Official site: http://peer4commit.com/ -Based on [tip4commit](https://github.com/tip4commit) License ======= [MIT License](https://github.com/sigmike/peer4commit/blob/master/LICENSE) + +Based on [Tip4commit](http://tip4commit.com/), [MIT License](https://github.com/tip4commit/tip4commit/blob/master/LICENSE), copyright (c) 2013-2014 tip4commit diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 64202c07..66ddadff 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -7,9 +7,9 @@ %meta{content: "", name: "author"}/ %link{href: "/favicon.png", rel: "shortcut icon"}/ - %title= "Tip4Commit — " + (content_for?(:title) ? yield(:title) : "Contribute to Open Source") + %title= "Peer4Commit — " + (content_for?(:title) ? yield(:title) : "Contribute to Open Source") - %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 => '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,cotribute,github,community,git,bitcoin,tips,perks'} / %meta{:property => 'og:image', :content => asset_path('logo.png')} / %link{:rel => 'image_src', :type => 'image/png', :href => asset_path('logo.png')} @@ -25,7 +25,7 @@ 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('create', 'UA-11108334-6', 'peer4commit.com'); ga('send', 'pageview'); .container .masthead @@ -39,7 +39,7 @@ = 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 + %h3.text-muted.code-pro Peer4Commit = render 'common/menu' - if flash[:alert] %br @@ -52,11 +52,12 @@ .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') + = link_to 'Peer4commit', 'http://peer4commit.com/', target: '_blank' + 2014. Source code is available at #{link_to('github', 'https://github.com/sigmike/peer4commit', target: '_blank')}, + based on #{link_to "Tip4commit", "http://tip4commit.com/"}. + You can + = link_to('support', 'http://tip4commit.com/projects/560') its development. - = link_to 'Follow @tip4commit', 'https://twitter.com/tip4commit', target: '_blank' / /container / Bootstrap core JavaScript diff --git a/app/views/projects/show.svg.erb b/app/views/projects/show.svg.erb index acabfbc3..3d8947c1 100644 --- a/app/views/projects/show.svg.erb +++ b/app/views/projects/show.svg.erb @@ -84,4 +84,4 @@ <%= shield_btc_amount @project.next_tip_amount %> - \ No newline at end of file + diff --git a/app/views/user_mailer/new_tip.html.haml b/app/views/user_mailer/new_tip.html.haml index 52e21d40..487f6e31 100644 --- a/app/views/user_mailer/new_tip.html.haml +++ b/app/views/user_mailer/new_tip.html.haml @@ -10,7 +10,7 @@ %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/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 From 384986874869dee5f9de5753571ea9e14a457cad Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Fri, 14 Feb 2014 16:36:17 +0100 Subject: [PATCH 004/372] changed bitcoin mentions to peercoin --- app/controllers/users_controller.rb | 4 ++-- app/views/home/index.html.haml | 14 +++++++------- app/views/layouts/application.html.haml | 2 +- app/views/projects/show.html.haml | 6 +++--- app/views/user_mailer/new_tip.html.haml | 6 +++--- app/views/users/show.html.haml | 4 ++-- 6 files changed, 18 insertions(+), 18 deletions(-) diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index a1546bbd..02496d2b 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -18,7 +18,7 @@ def update if @user.update_attributes(users_params) redirect_to @user, notice: 'Your information saved!' else - render :show, alert: 'Error updating bitcoin address' + render :show, alert: 'Error updating peercoin address' end end @@ -28,7 +28,7 @@ def login 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 else redirect_to root_url, alert: 'User not found' diff --git a/app/views/home/index.html.haml b/app/views/home/index.html.haml index 8b8862a7..d33c4960 100644 --- a/app/views/home/index.html.haml +++ b/app/views/home/index.html.haml @@ -1,7 +1,7 @@ / Jumbotron .jumbotron %h1 Contribute to Open Source - %p.lead Donate bitcoins to open source projects or make commits and get tips for it. + %p.lead Donate peercoins 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? @@ -12,7 +12,7 @@ / 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 current_user.bitcoin_address.blank? ? 'Please set your Peercoin address to receive tips!' : 'Change peercoin address', current_user / \/ / = link_to 'Sign Out', destroy_user_session_path, method: :delete / \/ @@ -25,13 +25,13 @@ .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 People donate peercoins 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 » + %a.btn.btn-primary{href: "http://peercoin.net/", target: '_blank'} Learn about Peercoin » .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. + Find a project you like and deposit peercoins 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 @@ -44,6 +44,6 @@ %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 » +/ %a.btn.btn-primary{href: user_path(current_user)} Change your peercoin address » / - else -/ %a.btn.btn-primary{href: user_omniauth_authorize_path(:github)} Sign In » \ No newline at end of file +/ %a.btn.btn-primary{href: user_omniauth_authorize_path(:github)} Sign In » diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 66ddadff..3e15b83c 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -10,7 +10,7 @@ %title= "Peer4Commit — " + (content_for?(:title) ? yield(:title) : "Contribute to Open Source") %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,cotribute,github,community,git,bitcoin,tips,perks'} + %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')} diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index 29c8bb9a..c043bbf1 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -58,15 +58,15 @@ = btc_human @project.next_tip_amount %h4 Contribute and Earn - Donate bitcoins to this project or + Donate peercoins 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! + and get tips for it. If your commit is accepted by project maintainer and there are peercoins on its balance, you will get a tip! - if current_user - if current_user.bitcoin_address.blank? Just = link_to 'tell us', current_user - your bitcoin address. + your peercoin address. - else Just check your email or %a{href: user_omniauth_authorize_path(:github)} Sign In. diff --git a/app/views/user_mailer/new_tip.html.haml b/app/views/user_mailer/new_tip.html.haml index 487f6e31..55ac43b9 100644 --- a/app/views/user_mailer/new_tip.html.haml +++ b/app/views/user_mailer/new_tip.html.haml @@ -1,10 +1,10 @@ %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 If you don't need peercoins 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) diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index 886bab48..91bc5b84 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -15,5 +15,5 @@ .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 + = f.text_field :bitcoin_address, class: 'form-control', placeholder: 'Your peercoin address' + = f.button :update, class: 'btn btn-default' From ec56985ea9ffb748188a07363b79e5380e1c66b1 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Fri, 14 Feb 2014 16:39:40 +0100 Subject: [PATCH 005/372] removed coingiving iframe --- app/views/projects/show.html.haml | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index c043bbf1..cf5d54b1 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -15,11 +15,9 @@ %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 + To give to this project, send peercoins to this address: + .well + = @project.bitcoin_address %p #{100-(CONFIG["our_fee"]*100).round}% of deposited funds will be used to tip for new commits. .col-md-8 - unless @project.description.blank? From 5583550d54d200c2e47c7332bdefa9548935b4c5 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Fri, 14 Feb 2014 16:45:10 +0100 Subject: [PATCH 006/372] changed unit --- app/helpers/application_helper.rb | 2 +- app/helpers/projects_helper.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index d4b6fb29..27b88867 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -2,7 +2,7 @@ module ApplicationHelper def btc_human amount, options = {} nobr = options.has_key?(:nobr) ? options[:nobr] : true currency = options[:currency] || false - btc = "%.8f Ƀ" % to_btc(amount) + btc = "%.8f Ᵽ" % to_btc(amount) btc = "#{btc}" if currency btc = "#{btc}" if nobr btc.html_safe diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 091f154e..9d05265b 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 + "%.#{9 - btc_amount.to_i.to_s.length}f Ᵽ" % btc_amount end def shield_color project From 42cf70ab53a5667742eed19a4b299af858d0a5c4 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Fri, 14 Feb 2014 16:48:29 +0100 Subject: [PATCH 007/372] updated shield for peercoin --- app/helpers/projects_helper.rb | 2 +- app/views/projects/show.svg.erb | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 9d05265b..03951aa9 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 + "%.#{8 - btc_amount.to_i.to_s.length}f Ᵽ" % btc_amount end def shield_color project diff --git a/app/views/projects/show.svg.erb b/app/views/projects/show.svg.erb index 3d8947c1..a7c8ff78 100644 --- a/app/views/projects/show.svg.erb +++ b/app/views/projects/show.svg.erb @@ -71,9 +71,9 @@ - + - tip4commit + peer4commit From 869c83f5e6c5e32da480049bbb42d8d43a3727cd Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Fri, 14 Feb 2014 17:24:14 +0100 Subject: [PATCH 008/372] module to call peercoin rpc command --- Gemfile | 4 ++- Gemfile.lock | 5 +++ app/controllers/projects_controller.rb | 8 +---- config/config.yml.sample | 8 ++++- lib/peercoin_daemon.rb | 43 ++++++++++++++++++++++++++ 5 files changed, 59 insertions(+), 9 deletions(-) create mode 100644 lib/peercoin_daemon.rb diff --git a/Gemfile b/Gemfile index 8f72fdae..b2d6e124 100644 --- a/Gemfile +++ b/Gemfile @@ -63,4 +63,6 @@ group :development do gem 'capistrano-rails' end -gem 'airbrake' \ No newline at end of file +gem 'airbrake' + +gem 'httparty' diff --git a/Gemfile.lock b/Gemfile.lock index 30bc5d1a..ca61a713 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -89,6 +89,9 @@ GEM railties (>= 4.0.1) hashie (2.0.5) hike (1.2.3) + httparty (0.12.0) + json (~> 1.8) + multi_xml (>= 0.5.2) httpauth (0.2.0) i18n (0.6.9) jbuilder (1.5.3) @@ -115,6 +118,7 @@ GEM mime-types (1.25.1) minitest (4.7.5) multi_json (1.8.4) + multi_xml (0.5.5) multipart-post (1.2.0) mysql2 (0.3.14) net-scp (1.1.2) @@ -218,6 +222,7 @@ DEPENDENCIES coffee-rails (~> 4.0.0) devise haml-rails + httparty jbuilder (~> 1.2) jquery-rails kaminari diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 18c28caf..44083474 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -8,13 +8,7 @@ def index 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.update_attribute :bitcoin_address, PeercoinDaemon.get_new_address end end diff --git a/config/config.yml.sample b/config/config.yml.sample index 992b3052..0e6c1856 100644 --- a/config/config.yml.sample +++ b/config/config.yml.sample @@ -7,6 +7,12 @@ blockchain_info: password: "111111111111" callback_secret: "111111111111" +peercoin: + username: rpcuser + password: rpcpassword + host: localhost + port: 9904 + devise: secret: "111111111111" @@ -30,4 +36,4 @@ smtp_settings: tip: 0.01 min_payout: 100000 -our_fee: 0.05 \ No newline at end of file +our_fee: 0.05 diff --git a/lib/peercoin_daemon.rb b/lib/peercoin_daemon.rb new file mode 100644 index 00000000..5493df7f --- /dev/null +++ b/lib/peercoin_daemon.rb @@ -0,0 +1,43 @@ +class PeercoinDaemon + def self.instance + @peercoin_daemon ||= PeercoinDaemon.new(CONFIG['peercoin']) + 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 peercoin config" if config[field].blank? + end + + uri = URI::HTTP.build(config.slice('host', 'port').symbolize_keys) + + auth = config.slice('username', 'password').symbolize_keys + + data = { + method: command, + params: params, + id: 1, + } + + 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 +end From 89697cd6db0da73ef4f71c0013a4d38006ed93c2 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Fri, 14 Feb 2014 17:26:30 +0100 Subject: [PATCH 009/372] generate a peercoin address on project creation --- app/controllers/projects_controller.rb | 2 +- lib/peercoin_daemon.rb | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 44083474..41b21f4b 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -8,7 +8,7 @@ def index def show @project = Project.find params[:id] if @project && @project.bitcoin_address.nil? - @project.update_attribute :bitcoin_address, PeercoinDaemon.get_new_address + @project.update_attribute :bitcoin_address, PeercoinDaemon.instance.get_new_address end end diff --git a/lib/peercoin_daemon.rb b/lib/peercoin_daemon.rb index 5493df7f..21f10b93 100644 --- a/lib/peercoin_daemon.rb +++ b/lib/peercoin_daemon.rb @@ -40,4 +40,8 @@ def rpc(command, *params) end result["result"] end + + def get_new_address + rpc('getnewaddress') + end end From 29c5fa07b525d23981f6d21417da19f03a48d9e6 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Fri, 14 Feb 2014 17:59:04 +0100 Subject: [PATCH 010/372] works on ruby 1.9 --- Gemfile | 2 -- 1 file changed, 2 deletions(-) diff --git a/Gemfile b/Gemfile index b2d6e124..ea4b5ef0 100644 --- a/Gemfile +++ b/Gemfile @@ -1,7 +1,5 @@ source 'https://rubygems.org' -ruby '2.0.0' - # Bundle edge Rails instead: gem 'rails', github: 'rails/rails' gem 'rails', '4.0.2' From 9b7f3956798dda61ddb2c55432d3ba55f3f079f8 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Fri, 14 Feb 2014 18:10:01 +0100 Subject: [PATCH 011/372] added postgresql gem --- Gemfile | 3 ++- Gemfile.lock | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index ea4b5ef0..eb49960b 100644 --- a/Gemfile +++ b/Gemfile @@ -5,7 +5,8 @@ gem 'rails', '4.0.2' # Databases gem 'sqlite3', group: :development -gem 'mysql2', group: :production +gem 'mysql2', group: :mysql +gem 'pg', group: :postgresql # Use SCSS for stylesheets gem 'sass-rails', '~> 4.0.0' diff --git a/Gemfile.lock b/Gemfile.lock index ca61a713..bd9e8c2f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -142,6 +142,7 @@ GEM oauth2 (~> 0.8.0) omniauth (~> 1.0) orm_adapter (0.5.0) + pg (0.17.1) polyglot (0.3.3) rack (1.5.2) rack-test (0.6.2) @@ -231,6 +232,7 @@ DEPENDENCIES octokit omniauth omniauth-github + pg rails (= 4.0.2) sass-rails (~> 4.0.0) sdoc From 109b9dd63c4913809c3fe7bbbdc3e1dbd109baf7 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Fri, 14 Feb 2014 18:57:44 +0100 Subject: [PATCH 012/372] convert port to integer --- lib/peercoin_daemon.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/peercoin_daemon.rb b/lib/peercoin_daemon.rb index 21f10b93..bba908f2 100644 --- a/lib/peercoin_daemon.rb +++ b/lib/peercoin_daemon.rb @@ -22,7 +22,7 @@ def rpc(command, *params) raise "No #{field} provided in peercoin config" if config[field].blank? end - uri = URI::HTTP.build(config.slice('host', 'port').symbolize_keys) + uri = URI::HTTP.build(host: config['host'], port: config['port'].to_i) auth = config.slice('username', 'password').symbolize_keys From 81f875ba6f0f9553de99184edabd1f63fb5e6699 Mon Sep 17 00:00:00 2001 From: Dan Bartram Date: Fri, 14 Feb 2014 21:53:56 +0000 Subject: [PATCH 013/372] Fixed margins in nav bar on mobile Removed the margin-bottom rule from: .nav-justified>li>a of the /assets/application-25b8d7709fc3c19033d1292decf2d537.css:3892 (line 3892) to remove the empty space at mobile resolutions. ### Before ![Before](http://i.imgur.com/2egBnON.png) ### After ![After](http://i.imgur.com/YHL4TV8.png) It would obviously be better to remove the original rule which sets the margin-bottom value to "5px" in the first place, depending on whether you prefer this style. --- app/assets/stylesheets/justified-nav.css | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/justified-nav.css b/app/assets/stylesheets/justified-nav.css index 35e84503..0bfb4ac9 100644 --- a/app/assets/stylesheets/justified-nav.css +++ b/app/assets/stylesheets/justified-nav.css @@ -27,6 +27,7 @@ body { border: 1px solid #ccc; } .nav-justified > li > a { + margin-bottom: 0; padding-top: 15px; padding-bottom: 15px; color: #777; @@ -85,4 +86,4 @@ body { padding-left: 0; padding-right: 0; } -} \ No newline at end of file +} From aa2b20a6f7e8505247ae376e5bfa05548d6a1eee Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sat, 15 Feb 2014 06:27:25 +0100 Subject: [PATCH 014/372] give a label to project addresses --- app/controllers/projects_controller.rb | 2 +- app/models/project.rb | 3 +++ lib/peercoin_daemon.rb | 4 ++-- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 41b21f4b..11262e52 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -8,7 +8,7 @@ def index def show @project = Project.find params[:id] if @project && @project.bitcoin_address.nil? - @project.update_attribute :bitcoin_address, PeercoinDaemon.instance.get_new_address + @project.update_attribute :bitcoin_address, PeercoinDaemon.instance.get_new_address(@project.address_label) end end diff --git a/app/models/project.rb b/app/models/project.rb index 2e5db9cd..47417525 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -119,4 +119,7 @@ def self.update_cache end end + def address_label + full_name + "@peer4commit" + end end diff --git a/lib/peercoin_daemon.rb b/lib/peercoin_daemon.rb index bba908f2..4ecdabab 100644 --- a/lib/peercoin_daemon.rb +++ b/lib/peercoin_daemon.rb @@ -41,7 +41,7 @@ def rpc(command, *params) result["result"] end - def get_new_address - rpc('getnewaddress') + def get_new_address(account = "") + rpc('getnewaddress', account) end end From f4cdd7ce0a1bc1f3c2470aed53155f68681c9cc9 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sat, 15 Feb 2014 07:12:56 +0100 Subject: [PATCH 015/372] get project deposits --- lib/peercoin_balance_updater.rb | 38 +++++++++++++++++++++++++++++++++ lib/peercoin_daemon.rb | 4 ++++ 2 files changed, 42 insertions(+) create mode 100644 lib/peercoin_balance_updater.rb diff --git a/lib/peercoin_balance_updater.rb b/lib/peercoin_balance_updater.rb new file mode 100644 index 00000000..1cd6a87b --- /dev/null +++ b/lib/peercoin_balance_updater.rb @@ -0,0 +1,38 @@ +module PeercoinBalanceUpdater + COIN = 1000000 # ppcoin/src/util.h + + def self.work + Project.all.each do |project| + start = 0 + count = 10 + loop do + transactions = PeercoinDaemon.instance.list_transactions(project.address_label, count, start) + break if transactions.empty? + + transactions.each do |transaction| + if (transaction["category"] == "send") || Sendmany.find_by_txid(transaction["txid"]) + next + end + + if deposit = Deposit.find_by_txid(transaction["txid"]) + deposit.update_attribute(:confirmations, transaction["confirmations"]) + next + end + + deposit = Deposit.create({ + project_id: project.id, + txid: transaction["txid"], + confirmations: transaction["confirmations"], + amount: (transaction["amount"].to_d * COIN).to_i, + duration: 30.days.to_i, + paid_out: 0, + paid_out_at: Time.now + }) + end + + break if transactions.size < count + start += count + end + end + end +end diff --git a/lib/peercoin_daemon.rb b/lib/peercoin_daemon.rb index 4ecdabab..fead1f33 100644 --- a/lib/peercoin_daemon.rb +++ b/lib/peercoin_daemon.rb @@ -44,4 +44,8 @@ def rpc(command, *params) def get_new_address(account = "") rpc('getnewaddress', account) end + + def list_transactions(account = "", count = 10, from = 0) + rpc('listtransactions', account, count, from) + end end From fd24f2fbda2562ef3daaed5075cf4bfdda6cddf4 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sat, 15 Feb 2014 07:13:17 +0100 Subject: [PATCH 016/372] display peercoin with the right decimals --- app/helpers/application_helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 27b88867..abe96166 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -9,6 +9,6 @@ def btc_human amount, options = {} end def to_btc satoshies - (1.0*satoshies.to_i/1e8) + satoshies.to_d / PeercoinBalanceUpdater::COIN end end From 006e9cf6a5edf2dc6ebb214e4643a93ed23cdea5 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sat, 15 Feb 2014 07:29:49 +0100 Subject: [PATCH 017/372] added project to sendmany --- app/models/sendmany.rb | 2 ++ db/migrate/20140215062842_add_project_to_sendmany.rb | 6 ++++++ db/schema.rb | 5 ++++- 3 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 db/migrate/20140215062842_add_project_to_sendmany.rb diff --git a/app/models/sendmany.rb b/app/models/sendmany.rb index a0f46092..da541583 100644 --- a/app/models/sendmany.rb +++ b/app/models/sendmany.rb @@ -1,4 +1,6 @@ class Sendmany < ActiveRecord::Base + belongs_to :project + def send_transaction return if txid || is_error 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/schema.rb b/db/schema.rb index aa2be27e..b08cd7c1 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20140102095035) do +ActiveRecord::Schema.define(version: 20140215062842) do create_table "deposits", force: true do |t| t.integer "project_id" @@ -49,8 +49,11 @@ t.boolean "is_error" t.datetime "created_at" t.datetime "updated_at" + t.integer "project_id" end + add_index "sendmanies", ["project_id"], name: "index_sendmanies_on_project_id" + create_table "tips", force: true do |t| t.integer "user_id" t.integer "amount", limit: 8 From 6415b9a79b40a1deeaf04acf58ae9c0d21facc55 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sat, 15 Feb 2014 07:44:35 +0100 Subject: [PATCH 018/372] send tips (the min_payout is now per project, not per user) --- app/models/sendmany.rb | 25 ++++++++----------------- app/models/tip.rb | 4 ++++ lib/bitcoin_tipper.rb | 29 +++++++++++------------------ lib/peercoin_daemon.rb | 4 ++++ 4 files changed, 27 insertions(+), 35 deletions(-) diff --git a/app/models/sendmany.rb b/app/models/sendmany.rb index da541583..75602e92 100644 --- a/app/models/sendmany.rb +++ b/app/models/sendmany.rb @@ -1,23 +1,14 @@ class Sendmany < ActiveRecord::Base belongs_to :project - def send_transaction + 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 + update_attribute :is_error, true # it's a lock to prevent duplicates + + txid = PeercoinDaemon.send_many(project.address_label, JSON.parse(data)) + + update_attribute :is_error, false + update_attribute :txid, txid + end end diff --git a/app/models/tip.rb b/app/models/tip.rb index 06ca7fe6..1a00ae02 100644 --- a/app/models/tip.rb +++ b/app/models/tip.rb @@ -18,6 +18,10 @@ class Tip < ActiveRecord::Base unpaid. where('users.bitcoin_address' => ['', nil]) } + scope :on_project, -> (project) { where(project_id: project.id) } + + scope :with_address, -> { joins(:user).where('users.bitcoin_address IS NOT NULL') } + def self.refund_unclaimed unclaimed.non_refunded. where('tips.created_at < ?', Time.now - 1.month). diff --git a/lib/bitcoin_tipper.rb b/lib/bitcoin_tipper.rb index da10ccb2..96efb5ac 100644 --- a/lib/bitcoin_tipper.rb +++ b/lib/bitcoin_tipper.rb @@ -14,16 +14,7 @@ def self.work end 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" - end - end - - self.create_sendmany if is_sendmany_needed + self.create_sendmany Rails.logger.info "Traversing sendmanies..." Sendmany.where(txid: nil).each do |sendmany| @@ -43,19 +34,21 @@ def self.work def self.create_sendmany Rails.logger.info "Creating sendmany" 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| + Project.find_each do |project| + tips = project.tips.unpaid.with_address + amount = tips.sum(:amount) + if amount > CONFIG["min_payout"] + sendmany = Sendmany.create(project_id: project.id) + outs = {} + tips.each do |tip| tip.update_attribute :sendmany_id, sendmany.id outs[user.bitcoin_address] = outs[user.bitcoin_address].to_i + tip.amount end + sendmany.update_attribute :data, outs.to_json + Rails.logger.info " #{sendmany.inspect}" 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/peercoin_daemon.rb b/lib/peercoin_daemon.rb index fead1f33..ec22537c 100644 --- a/lib/peercoin_daemon.rb +++ b/lib/peercoin_daemon.rb @@ -48,4 +48,8 @@ def get_new_address(account = "") def list_transactions(account = "", count = 10, from = 0) rpc('listtransactions', account, count, from) end + + def send_many(account, recipients, minconf = 1) + rpc('sendmany', recipients.to_json, minconf) + end end From 74f36d3deae86fc1996d5042d966c5bfd39dec29 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sat, 15 Feb 2014 08:21:39 +0100 Subject: [PATCH 019/372] fixed sendmany --- app/models/sendmany.rb | 2 +- lib/bitcoin_tipper.rb | 6 +++--- lib/peercoin_daemon.rb | 6 +++++- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/app/models/sendmany.rb b/app/models/sendmany.rb index 75602e92..9fc927c1 100644 --- a/app/models/sendmany.rb +++ b/app/models/sendmany.rb @@ -6,7 +6,7 @@ def send_transaction update_attribute :is_error, true # it's a lock to prevent duplicates - txid = PeercoinDaemon.send_many(project.address_label, JSON.parse(data)) + txid = PeercoinDaemon.instance.send_many(project.address_label, JSON.parse(data)) update_attribute :is_error, false update_attribute :txid, txid diff --git a/lib/bitcoin_tipper.rb b/lib/bitcoin_tipper.rb index 96efb5ac..0e557b93 100644 --- a/lib/bitcoin_tipper.rb +++ b/lib/bitcoin_tipper.rb @@ -35,14 +35,14 @@ def self.create_sendmany Rails.logger.info "Creating sendmany" ActiveRecord::Base.transaction do Project.find_each do |project| - tips = project.tips.unpaid.with_address - amount = tips.sum(:amount) + tips = project.tips.unpaid.with_address.readonly(false) + amount = tips.sum(:amount).to_d / PeercoinBalanceUpdater::COIN if amount > CONFIG["min_payout"] sendmany = Sendmany.create(project_id: project.id) outs = {} tips.each do |tip| tip.update_attribute :sendmany_id, sendmany.id - outs[user.bitcoin_address] = outs[user.bitcoin_address].to_i + tip.amount + outs[tip.user.bitcoin_address] = outs[tip.user.bitcoin_address].to_i + tip.amount.to_d / PeercoinBalanceUpdater::COIN end sendmany.update_attribute :data, outs.to_json Rails.logger.info " #{sendmany.inspect}" diff --git a/lib/peercoin_daemon.rb b/lib/peercoin_daemon.rb index ec22537c..d78ce6d3 100644 --- a/lib/peercoin_daemon.rb +++ b/lib/peercoin_daemon.rb @@ -50,6 +50,10 @@ def list_transactions(account = "", count = 10, from = 0) end def send_many(account, recipients, minconf = 1) - rpc('sendmany', recipients.to_json, minconf) + recipients = recipients.dup + recipients.each do |address, amount| + recipients[address] = amount.to_f + end + rpc('sendmany', account, recipients, minconf) end end From ea561932a70315434cfe361acccf212695a10482 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sat, 15 Feb 2014 08:22:50 +0100 Subject: [PATCH 020/372] whenever gem --- Gemfile | 2 ++ Gemfile.lock | 5 +++++ config/schedule.rb | 20 ++++++++++++++++++++ 3 files changed, 27 insertions(+) create mode 100644 config/schedule.rb diff --git a/Gemfile b/Gemfile index eb49960b..26504159 100644 --- a/Gemfile +++ b/Gemfile @@ -65,3 +65,5 @@ end gem 'airbrake' gem 'httparty' + +gem 'whenever' diff --git a/Gemfile.lock b/Gemfile.lock index bd9e8c2f..4d3c9d54 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -62,6 +62,7 @@ GEM capistrano-rails (1.1.0) capistrano (>= 3.0.0) capistrano-bundler (>= 1.0.0) + chronic (0.10.2) coffee-rails (4.0.1) coffee-script (>= 2.2.0) railties (>= 4.0.0, < 5.0) @@ -210,6 +211,9 @@ GEM json (>= 1.8.0) warden (1.2.3) rack (>= 1.0) + whenever (0.9.0) + activesupport (>= 2.3.4) + chronic (>= 0.6.3) PLATFORMS ruby @@ -241,3 +245,4 @@ DEPENDENCIES turbolinks twitter-bootstrap-rails! uglifier (>= 1.3.0) + whenever diff --git a/config/schedule.rb b/config/schedule.rb new file mode 100644 index 00000000..de75cf9d --- /dev/null +++ b/config/schedule.rb @@ -0,0 +1,20 @@ +# 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 From 0efa80f9bb5d1fa429a218d291098f58e551752a Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sat, 15 Feb 2014 08:28:36 +0100 Subject: [PATCH 021/372] run daemon at boot --- config/config.yml.sample | 1 + config/schedule.rb | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/config/config.yml.sample b/config/config.yml.sample index 0e6c1856..57b33444 100644 --- a/config/config.yml.sample +++ b/config/config.yml.sample @@ -12,6 +12,7 @@ peercoin: password: rpcpassword host: localhost port: 9904 + daemon: /path/to/ppcoin/src/ppcoind devise: secret: "111111111111" diff --git a/config/schedule.rb b/config/schedule.rb index de75cf9d..0c68796e 100644 --- a/config/schedule.rb +++ b/config/schedule.rb @@ -18,3 +18,11 @@ # end # Learn more: http://github.com/javan/whenever + +require File.expand_path('../../config/environment', __FILE__) +every :reboot do + if daemon = CONFIG['peercoin']['daemon'] + command daemon + end +end + From 83e975572b1b3e5c85b4e015c69df461fbfa639e Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sat, 15 Feb 2014 08:32:42 +0100 Subject: [PATCH 022/372] run the tipper at tipper_delay interval --- config/config.yml.sample | 2 ++ config/schedule.rb | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/config/config.yml.sample b/config/config.yml.sample index 57b33444..f52cd597 100644 --- a/config/config.yml.sample +++ b/config/config.yml.sample @@ -38,3 +38,5 @@ smtp_settings: tip: 0.01 min_payout: 100000 our_fee: 0.05 +tipper_delay: "1.hour" + diff --git a/config/schedule.rb b/config/schedule.rb index 0c68796e..476def76 100644 --- a/config/schedule.rb +++ b/config/schedule.rb @@ -26,3 +26,9 @@ end end +if delay = CONFIG['tipper_delay'] + delay = eval(delay) + every delay do + runner "BitcoinTipper.work" + end +end From e9bd90031b994467e6945daf54a40193fc1ea80a Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sat, 15 Feb 2014 08:36:02 +0100 Subject: [PATCH 023/372] removed unused scope with invalid syntax on ruby 1.9 --- app/models/tip.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/models/tip.rb b/app/models/tip.rb index 1a00ae02..05c8a9be 100644 --- a/app/models/tip.rb +++ b/app/models/tip.rb @@ -18,8 +18,6 @@ class Tip < ActiveRecord::Base unpaid. where('users.bitcoin_address' => ['', nil]) } - scope :on_project, -> (project) { where(project_id: project.id) } - scope :with_address, -> { joins(:user).where('users.bitcoin_address IS NOT NULL') } def self.refund_unclaimed From 632fd3b84d645d299780fa32c941d39c2b4edb56 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sat, 15 Feb 2014 08:39:10 +0100 Subject: [PATCH 024/372] btc_human returns nil on nil amount --- app/helpers/application_helper.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index abe96166..b5009d01 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -1,5 +1,6 @@ module ApplicationHelper def btc_human amount, options = {} + return nil unless amount nobr = options.has_key?(:nobr) ? options[:nobr] : true currency = options[:currency] || false btc = "%.8f Ᵽ" % to_btc(amount) @@ -9,6 +10,6 @@ def btc_human amount, options = {} end def to_btc satoshies - satoshies.to_d / PeercoinBalanceUpdater::COIN + satoshies.to_d / PeercoinBalanceUpdater::COIN if satoshies end end From a53f5f457493f2526ec8aeee16c6d547e9e4d1d7 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sat, 15 Feb 2014 08:53:56 +0100 Subject: [PATCH 025/372] update balance before tipper work --- config/schedule.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/schedule.rb b/config/schedule.rb index 476def76..0085776c 100644 --- a/config/schedule.rb +++ b/config/schedule.rb @@ -29,6 +29,6 @@ if delay = CONFIG['tipper_delay'] delay = eval(delay) every delay do - runner "BitcoinTipper.work" + runner "PeercoinBalanceUpdater.work; BitcoinTipper.work" end end From a0b5deb31b6edcc0ceb8b3d13a5532cabed417af Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sat, 15 Feb 2014 09:02:34 +0100 Subject: [PATCH 026/372] removed coingiving link because they only accept bitcoins --- app/views/user_mailer/new_tip.html.haml | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/views/user_mailer/new_tip.html.haml b/app/views/user_mailer/new_tip.html.haml index 55ac43b9..ddbaad6b 100644 --- a/app/views/user_mailer/new_tip.html.haml +++ b/app/views/user_mailer/new_tip.html.haml @@ -4,8 +4,6 @@ %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 peercoins 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 Thanks for contributing to Open Source! From b32cdd5e49f0df61bcfb64d2423676b1a620c846 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sat, 15 Feb 2014 09:13:59 +0100 Subject: [PATCH 027/372] replace symbol with PPC --- app/helpers/application_helper.rb | 2 +- app/helpers/projects_helper.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index b5009d01..3a957250 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -3,7 +3,7 @@ def btc_human amount, options = {} return nil unless amount nobr = options.has_key?(:nobr) ? options[:nobr] : true currency = options[:currency] || false - btc = "%.8f Ᵽ" % to_btc(amount) + btc = "%.8f PPC" % to_btc(amount) btc = "#{btc}" if currency btc = "#{btc}" if nobr btc.html_safe diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 03951aa9..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 - "%.#{8 - 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 From eb0a6489fa2b3b9de9e3d90e96f0a61484f206f8 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sat, 15 Feb 2014 09:27:03 +0100 Subject: [PATCH 028/372] display project address qrcode --- Gemfile | 3 +++ Gemfile.lock | 4 ++++ app/controllers/projects_controller.rb | 7 +++++++ app/views/projects/show.html.haml | 8 +++++--- config/routes.rb | 3 +++ 5 files changed, 22 insertions(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index 26504159..63e1f52f 100644 --- a/Gemfile +++ b/Gemfile @@ -67,3 +67,6 @@ gem 'airbrake' gem 'httparty' gem 'whenever' + +gem 'rqrcode-rails3' + diff --git a/Gemfile.lock b/Gemfile.lock index 4d3c9d54..47d5aea0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -165,6 +165,9 @@ GEM rdoc (4.1.1) json (~> 1.4) ref (1.0.5) + rqrcode (0.4.2) + rqrcode-rails3 (0.1.7) + rqrcode (>= 0.4.2) sass (3.2.13) sass-rails (4.0.1) railties (>= 4.0.0, < 5.0) @@ -238,6 +241,7 @@ DEPENDENCIES omniauth-github pg rails (= 4.0.2) + rqrcode-rails3 sass-rails (~> 4.0.0) sdoc sqlite3 diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 11262e52..63b65b80 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -12,6 +12,13 @@ def show end end + def qrcode + @project = Project.find params[:id] + 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\//, ''). diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index cf5d54b1..88835fbb 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -14,10 +14,12 @@ .panel-heading %h4.panel-title Project Sponsors - .panel-body - To give to this project, send peercoins to this address: - .well + .panel-body.text-center + %p To give to this project, send peercoins to this address: + %p = @project.bitcoin_address + %p + = image_tag qrcode_project_path(@project, format: :svg), alt: @project.bitcoin_address, class: "project qrcode" %p #{100-(CONFIG["our_fee"]*100).round}% of deposited funds will be used to tip for new commits. .col-md-8 - unless @project.description.blank? diff --git a/config/routes.rb b/config/routes.rb index cc205fcd..b12275d1 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -10,6 +10,9 @@ end resources :projects, :only => [:show, :index, :create] do resources :tips, :only => [:index] + member do + get :qrcode + end end resources :tips, :only => [:index] resources :withdrawals, :only => [:index] From 306380c14903f85d2ef70410227d0a44df0b19da Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sat, 15 Feb 2014 09:35:25 +0100 Subject: [PATCH 029/372] link to peercoin transaction --- app/helpers/application_helper.rb | 4 ++++ app/views/tips/index.html.haml | 6 +++--- app/views/withdrawals/index.html.haml | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 3a957250..bc5315e0 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -12,4 +12,8 @@ def btc_human amount, options = {} def to_btc satoshies satoshies.to_d / PeercoinBalanceUpdater::COIN if satoshies end + + def transaction_url(txid) + "http://bkchain.org/ppc/tx/#{txid}" + end end diff --git a/app/views/tips/index.html.haml b/app/views/tips/index.html.haml index 0b7093ad..2a49c135 100644 --- a/app/views/tips/index.html.haml +++ b/app/views/tips/index.html.haml @@ -26,7 +26,7 @@ = 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= 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? @@ -38,5 +38,5 @@ - else User's balance is below withdrawal threshold - else - = link_to tip.sendmany.txid, "https://blockchain.info/tx/#{tip.sendmany.txid}", target: :blank - = paginate @tips \ No newline at end of file + = link_to tip.sendmany.txid, transaction_url(tip.sendmany.txid), target: :blank + = paginate @tips diff --git a/app/views/withdrawals/index.html.haml b/app/views/withdrawals/index.html.haml index d44a09df..5e049f3a 100644 --- a/app/views/withdrawals/index.html.haml +++ b/app/views/withdrawals/index.html.haml @@ -10,5 +10,5 @@ - @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= link_to sendmany.txid, transaction_url(sendmany.txid), target: '_blank' %td= sendmany.is_error ? "Error" : "Success" From 21439d1c476324245e7dd726d9694d69d918ab58 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sat, 15 Feb 2014 09:38:49 +0100 Subject: [PATCH 030/372] removed all blockchain mention --- app/controllers/home_controller.rb | 44 ------------------------------ config/config.yml.sample | 5 ---- config/routes.rb | 1 - 3 files changed, 50 deletions(-) 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/config/config.yml.sample b/config/config.yml.sample index f52cd597..79bd2dcd 100644 --- a/config/config.yml.sample +++ b/config/config.yml.sample @@ -2,11 +2,6 @@ github: key: "111111111111" secret: "111111111111" -blockchain_info: - guid: "111111111111" - password: "111111111111" - callback_secret: "111111111111" - peercoin: username: rpcuser password: rpcpassword diff --git a/config/routes.rb b/config/routes.rb index b12275d1..ab7f59f3 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -2,7 +2,6 @@ root 'home#index' - get '/blockchain_info_callback' => "home#blockchain_info_callback", :as => "blockchain_info_callback" resources :users, :only => [:show, :update, :index] do collection do get :login From 6e2c3e9801978bb52aeac0533df3bdd494ce6069 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sat, 15 Feb 2014 09:45:02 +0100 Subject: [PATCH 031/372] exception notification --- Gemfile | 5 +---- Gemfile.lock | 4 ++++ config/config.yml.sample | 2 ++ config/environments/production.rb | 13 +++++++++++-- 4 files changed, 18 insertions(+), 6 deletions(-) diff --git a/Gemfile b/Gemfile index 63e1f52f..245972db 100644 --- a/Gemfile +++ b/Gemfile @@ -63,10 +63,7 @@ group :development do end gem 'airbrake' - gem 'httparty' - gem 'whenever' - gem 'rqrcode-rails3' - +gem 'exception_notification' diff --git a/Gemfile.lock b/Gemfile.lock index 47d5aea0..96ab48af 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -78,6 +78,9 @@ GEM thread_safe (~> 0.1) warden (~> 1.2.3) erubis (2.7.0) + exception_notification (4.0.1) + actionmailer (>= 3.0.4) + activesupport (>= 3.0.4) execjs (2.0.2) faraday (0.8.9) multipart-post (~> 1.2.0) @@ -229,6 +232,7 @@ DEPENDENCIES capistrano-rvm! coffee-rails (~> 4.0.0) devise + exception_notification haml-rails httparty jbuilder (~> 1.2) diff --git a/config/config.yml.sample b/config/config.yml.sample index 79bd2dcd..9e3cb7cb 100644 --- a/config/config.yml.sample +++ b/config/config.yml.sample @@ -30,6 +30,8 @@ smtp_settings: # api_key: 111111111111 # host: errbit.tip4commit.com +exception_email: admin@example.com + tip: 0.01 min_payout: 100000 our_fee: 0.05 diff --git a/config/environments/production.rb b/config/environments/production.rb index d1485d96..c1eddcd8 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -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 = '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 From 77441b07519e95c326423e15d82c79f8be0a064c Mon Sep 17 00:00:00 2001 From: Aleksandr Zykov Date: Sat, 15 Feb 2014 15:37:17 +0700 Subject: [PATCH 032/372] used github id instead of full_name to identify repo. solves issue with renamed repositories --- app/models/project.rb | 26 +++++++++++++++++++ ...0140207061855_add_github_id_to_projects.rb | 5 ++++ lib/bitcoin_tipper.rb | 6 +++++ 3 files changed, 37 insertions(+) create mode 100644 db/migrate/20140207061855_add_github_id_to_projects.rb diff --git a/app/models/project.rb b/app/models/project.rb index 47417525..3717777a 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -2,7 +2,11 @@ class Project < ActiveRecord::Base has_many :deposits # todo: only confirmed deposits that have amount > paid_out has_many :tips + validates :full_name, uniqueness: true, presence: true + validates :github_id, uniqueness: true, presence: true + def update_github_info repo + self.github_id = repo.id self.name = repo.name self.full_name = repo.full_name self.source_full_name = repo.source.full_name rescue '' @@ -119,6 +123,28 @@ def self.update_cache end end + def github_info + client = Octokit::Client.new \ + :client_id => CONFIG['github']['key'], + :client_secret => CONFIG['github']['secret'] + if github_id.present? + client.get("/repositories/#{github_id}") + else + client.repo(full_name) + end + end + + def update_info + begin + update_github_info(github_info) + rescue Octokit::BadGateway, Octokit::NotFound, Octokit::InternalServerError, + Errno::ETIMEDOUT, Net::ReadTimeout, Faraday::Error::ConnectionFailed => e + Rails.logger.info "Project ##{id}: #{e.class} happened" + rescue StandardError => e + Airbrake.notify(e) + end + end + def address_label full_name + "@peer4commit" 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/lib/bitcoin_tipper.rb b/lib/bitcoin_tipper.rb index 0e557b93..79f475c3 100644 --- a/lib/bitcoin_tipper.rb +++ b/lib/bitcoin_tipper.rb @@ -14,6 +14,12 @@ def self.work end end + Rails.logger.info "Updating projects info..." + Project.order(:updated_at => :desc).last(10).each do |project| + Rails.logger.info " Project #{project.id} #{project.full_name}" + project.update_info + end + self.create_sendmany Rails.logger.info "Traversing sendmanies..." From 4340f96c017f632d3a23e85a36ec7c596b91a515 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sat, 15 Feb 2014 10:53:19 +0100 Subject: [PATCH 033/372] use github is as address label and keep it in database --- app/controllers/projects_controller.rb | 6 ++++-- app/models/project.rb | 4 ---- db/migrate/20140215094135_add_addres_label_to_project.rb | 5 +++++ .../20140215094549_initialize_project_address_label.rb | 5 +++++ db/schema.rb | 4 +++- 5 files changed, 17 insertions(+), 7 deletions(-) create mode 100644 db/migrate/20140215094135_add_addres_label_to_project.rb create mode 100644 db/migrate/20140215094549_initialize_project_address_label.rb diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 63b65b80..2e8a945b 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -7,8 +7,10 @@ def index def show @project = Project.find params[:id] - if @project && @project.bitcoin_address.nil? - @project.update_attribute :bitcoin_address, PeercoinDaemon.instance.get_new_address(@project.address_label) + if @project and @project.bitcoin_address.nil? and (github_id = @project.github_id).present? + label = "#{github_id}@peer4commit" + address = PeercoinDaemon.instance.get_new_address(label) + @project.update_attributes(bitcoin_address: address, address_label: label) end end diff --git a/app/models/project.rb b/app/models/project.rb index 3717777a..0bad4104 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -144,8 +144,4 @@ def update_info Airbrake.notify(e) end end - - def address_label - full_name + "@peer4commit" - 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/schema.rb b/db/schema.rb index b08cd7c1..500127d2 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20140215062842) do +ActiveRecord::Schema.define(version: 20140215094549) do create_table "deposits", force: true do |t| t.integer "project_id" @@ -40,6 +40,8 @@ t.string "language" t.string "last_commit" t.integer "available_amount_cache" + t.string "github_id" + t.string "address_label" end create_table "sendmanies", force: true do |t| From 7bc433be03e7a81e39542c2c570f969336c4cbfa Mon Sep 17 00:00:00 2001 From: Aleksandr Zykov Date: Thu, 13 Feb 2014 23:29:17 +0700 Subject: [PATCH 034/372] fixed label for tips that will be withdrawn soon --- app/views/tips/index.html.haml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/app/views/tips/index.html.haml b/app/views/tips/index.html.haml index 2a49c135..be01b343 100644 --- a/app/views/tips/index.html.haml +++ b/app/views/tips/index.html.haml @@ -32,11 +32,12 @@ - if tip.sendmany.nil? - if tip.refunded_at Refunded to project's deposit + - elsif tip.user.bitcoin_address.blank? + User didn't specify withdrawal address + - elsif tip.user.balance < CONFIG["min_payout"] + User's balance 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, transaction_url(tip.sendmany.txid), target: :blank = paginate @tips From 6ccb91971d7320c566aab941b18e9ca9921bd005 Mon Sep 17 00:00:00 2001 From: Aleksandr Zykov Date: Sat, 15 Feb 2014 17:24:49 +0700 Subject: [PATCH 035/372] fixes creation of projects with a long description + some minor fixes --- app/models/sendmany.rb | 1 + db/migrate/20140209022632_change_projects_description.rb | 8 ++++++++ db/migrate/20140209041123_create_indexes_for_projects.rb | 6 ++++++ db/schema.rb | 5 ++++- 4 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 db/migrate/20140209022632_change_projects_description.rb create mode 100644 db/migrate/20140209041123_create_indexes_for_projects.rb diff --git a/app/models/sendmany.rb b/app/models/sendmany.rb index 9fc927c1..06b7b240 100644 --- a/app/models/sendmany.rb +++ b/app/models/sendmany.rb @@ -1,5 +1,6 @@ class Sendmany < ActiveRecord::Base belongs_to :project + has_many :tips def send_transaction return if txid || is_error 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/schema.rb b/db/schema.rb index 500127d2..9f88f66c 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -35,7 +35,7 @@ t.string "name" t.string "full_name" t.string "source_full_name" - t.string "description" + t.text "description" t.integer "watchers_count" t.string "language" t.string "last_commit" @@ -44,6 +44,9 @@ t.string "address_label" end + add_index "projects", ["full_name"], name: "index_projects_on_full_name", unique: true + add_index "projects", ["github_id"], name: "index_projects_on_github_id", unique: true + create_table "sendmanies", force: true do |t| t.string "txid" t.text "data" From 3214c97f47790fc20843aaf18671ea6d67ea8638 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sat, 15 Feb 2014 13:10:37 +0100 Subject: [PATCH 036/372] fixed texts about min_payout. It's per projet now, not per user --- app/models/project.rb | 8 ++++++++ app/views/tips/index.html.haml | 4 ++-- app/views/users/show.html.haml | 4 ++-- config/config.yml.sample | 2 +- lib/bitcoin_tipper.rb | 6 +++--- 5 files changed, 16 insertions(+), 8 deletions(-) diff --git a/app/models/project.rb b/app/models/project.rb index 0bad4104..f501102c 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -144,4 +144,12 @@ def update_info Airbrake.notify(e) end end + + def tips_to_pay + tips.unpaid.with_address + end + + def amount_to_pay + tips_to_pay.sum(:amount) + end end diff --git a/app/views/tips/index.html.haml b/app/views/tips/index.html.haml index be01b343..dca6a51a 100644 --- a/app/views/tips/index.html.haml +++ b/app/views/tips/index.html.haml @@ -34,8 +34,8 @@ Refunded to project's deposit - elsif tip.user.bitcoin_address.blank? User didn't specify withdrawal address - - elsif tip.user.balance < CONFIG["min_payout"] - User's balance is below withdrawal threshold + - elsif tip.project.amount_to_pay < CONFIG["min_payout"].to_d * PeercoinBalanceUpdater::COIN + The amount of tips for this project is below withdrawal threshold - else Waiting for withdrawal - else diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index 91bc5b84..8d7ef1b7 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -5,8 +5,8 @@ = btc_human @user.balance %p %small - You will get your money when your balance hits the threshold of - = btc_human CONFIG["min_payout"] + You will get the money of your tips when the project they belong hits the threshold of + = btc_human CONFIG["min_payout"].to_d * PeercoinBalanceUpdater::COIN %p %strong E-mail %p= @user.email diff --git a/config/config.yml.sample b/config/config.yml.sample index 9e3cb7cb..bbcc90fd 100644 --- a/config/config.yml.sample +++ b/config/config.yml.sample @@ -33,7 +33,7 @@ smtp_settings: exception_email: admin@example.com tip: 0.01 -min_payout: 100000 +min_payout: 1.0 # in PPC our_fee: 0.05 tipper_delay: "1.hour" diff --git a/lib/bitcoin_tipper.rb b/lib/bitcoin_tipper.rb index 79f475c3..6dbc1f2a 100644 --- a/lib/bitcoin_tipper.rb +++ b/lib/bitcoin_tipper.rb @@ -41,9 +41,9 @@ def self.create_sendmany Rails.logger.info "Creating sendmany" ActiveRecord::Base.transaction do Project.find_each do |project| - tips = project.tips.unpaid.with_address.readonly(false) - amount = tips.sum(:amount).to_d / PeercoinBalanceUpdater::COIN - if amount > CONFIG["min_payout"] + tips = project.tips_to_pay.readonly(false) + amount = tips.sum(:amount).to_d + if amount > CONFIG["min_payout"].to_d * PeercoinBalanceUpdater::COIN sendmany = Sendmany.create(project_id: project.id) outs = {} tips.each do |tip| From 0f44a3554767f8dd02c98fafab1874e00c5172cb Mon Sep 17 00:00:00 2001 From: Aleksandr Zykov Date: Sat, 15 Feb 2014 22:37:30 +0700 Subject: [PATCH 037/372] updates github nickname using commit information --- app/models/project.rb | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/models/project.rb b/app/models/project.rb index f501102c..3b086b40 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -72,10 +72,15 @@ def tip_for commit user = User.create({ email: email, password: generated_password, - name: commit.commit.author.name + name: commit.commit.author.name, + nickname: (commit.author.login rescue nil) }) end + if commit.author && commit.author.login + user.update nickname: commit.author.login + end + # create tip tip = Tip.create({ project: self, From edc132b8805373fe2e3ee1b0725d7ec435bb2571 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sat, 15 Feb 2014 17:45:32 +0100 Subject: [PATCH 038/372] intercept emails --- config/config.yml.sample | 5 +++-- config/initializers/intercept_emails.rb | 10 ++++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) create mode 100644 config/initializers/intercept_emails.rb diff --git a/config/config.yml.sample b/config/config.yml.sample index bbcc90fd..1f1adf84 100644 --- a/config/config.yml.sample +++ b/config/config.yml.sample @@ -24,14 +24,15 @@ smtp_settings: authentication: plain enable_starttls_auto: true +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: # api_key: 111111111111 # host: errbit.tip4commit.com -exception_email: admin@example.com - tip: 0.01 min_payout: 1.0 # in PPC our_fee: 0.05 diff --git a/config/initializers/intercept_emails.rb b/config/initializers/intercept_emails.rb new file mode 100644 index 00000000..5f8ed8bc --- /dev/null +++ b/config/initializers/intercept_emails.rb @@ -0,0 +1,10 @@ +if (SEND_ALL_EMAILS_TO = CONFIG["send_all_emails_to"]).present? + 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 From ba7219bf849619edb6601d04558d1494cdc3e480 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sat, 15 Feb 2014 18:24:23 +0100 Subject: [PATCH 039/372] use nickname to link github account to user --- app/controllers/users/omniauth_callbacks_controller.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/controllers/users/omniauth_callbacks_controller.rb b/app/controllers/users/omniauth_callbacks_controller.rb index a3ffad82..125a01b0 100644 --- a/app/controllers/users/omniauth_callbacks_controller.rb +++ b/app/controllers/users/omniauth_callbacks_controller.rb @@ -2,7 +2,8 @@ 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.find_by :nickname => info["nickname"] + @user ||= User.find_by :email => info["email"] unless @user generated_password = Devise.friendly_token.first(8) @user = User.create!( @@ -19,4 +20,4 @@ def github sign_in_and_redirect @user, :event => :authentication set_flash_message(:notice, :success, :kind => "Github") if is_navigational_format? end -end \ No newline at end of file +end From 7e61a4c86cff2bce24e66233aa28a59e6f92fc4f Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sat, 15 Feb 2014 19:06:12 +0100 Subject: [PATCH 040/372] fixed address label --- app/views/users/show.html.haml | 2 +- config/locales/en.yml | 5 +++++ lib/bitcoin_address_validator.rb | 4 ++-- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index 8d7ef1b7..84eb0421 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -12,7 +12,7 @@ %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. + .alert.alert-danger Peercoin address is invalid. .form-group = f.label :bitcoin_address = f.text_field :bitcoin_address, class: 'form-control', placeholder: 'Your peercoin address' diff --git a/config/locales/en.yml b/config/locales/en.yml index 06539571..e618cddd 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -21,3 +21,8 @@ en: hello: "Hello world" + + activerecord: + attributes: + user: + bitcoin_address: Peercoin address diff --git a/lib/bitcoin_address_validator.rb b/lib/bitcoin_address_validator.rb index 82cc184c..3a534483 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 @@ -52,4 +52,4 @@ def b58_decode(value, length) result end -end \ No newline at end of file +end From 41e62cb191272893dbd2cb5ccbf2172897984800 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sat, 15 Feb 2014 19:29:34 +0100 Subject: [PATCH 041/372] verify address version --- config/config.yml.sample | 4 ++++ lib/bitcoin_address_validator.rb | 10 +++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/config/config.yml.sample b/config/config.yml.sample index 1f1adf84..503521d9 100644 --- a/config/config.yml.sample +++ b/config/config.yml.sample @@ -38,3 +38,7 @@ 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 + diff --git a/lib/bitcoin_address_validator.rb b/lib/bitcoin_address_validator.rb index 3a534483..7332bd7a 100644 --- a/lib/bitcoin_address_validator.rb +++ b/lib/bitcoin_address_validator.rb @@ -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) From d1d14f431bb59c03541bdce6a8adce3a77708997 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sat, 15 Feb 2014 20:03:10 +0100 Subject: [PATCH 042/372] fixed decimal truncation --- lib/bitcoin_tipper.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/bitcoin_tipper.rb b/lib/bitcoin_tipper.rb index 6dbc1f2a..fe548163 100644 --- a/lib/bitcoin_tipper.rb +++ b/lib/bitcoin_tipper.rb @@ -45,10 +45,10 @@ def self.create_sendmany amount = tips.sum(:amount).to_d if amount > CONFIG["min_payout"].to_d * PeercoinBalanceUpdater::COIN sendmany = Sendmany.create(project_id: project.id) - outs = {} + outs = Hash.new { 0.to_d } tips.each do |tip| tip.update_attribute :sendmany_id, sendmany.id - outs[tip.user.bitcoin_address] = outs[tip.user.bitcoin_address].to_i + tip.amount.to_d / PeercoinBalanceUpdater::COIN + outs[tip.user.bitcoin_address] += tip.amount.to_d / PeercoinBalanceUpdater::COIN end sendmany.update_attribute :data, outs.to_json Rails.logger.info " #{sendmany.inspect}" From 635e83137ced2f5bd6633a4a75841bafd1e9d759 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 16 Feb 2014 07:02:01 +0100 Subject: [PATCH 043/372] canonical hostname --- Gemfile | 1 + Gemfile.lock | 4 ++++ config.ru | 5 +++++ config/config.yml.sample | 1 + 4 files changed, 11 insertions(+) diff --git a/Gemfile b/Gemfile index 245972db..1f0a132e 100644 --- a/Gemfile +++ b/Gemfile @@ -67,3 +67,4 @@ gem 'httparty' gem 'whenever' gem 'rqrcode-rails3' gem 'exception_notification' +gem 'rack-canonical-host' diff --git a/Gemfile.lock b/Gemfile.lock index 96ab48af..e0441aa1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -149,6 +149,9 @@ GEM pg (0.17.1) polyglot (0.3.3) rack (1.5.2) + rack-canonical-host (0.0.8) + addressable + rack (~> 1.0) rack-test (0.6.2) rack (>= 1.0) rails (4.0.2) @@ -244,6 +247,7 @@ DEPENDENCIES omniauth omniauth-github pg + rack-canonical-host rails (= 4.0.2) rqrcode-rails3 sass-rails (~> 4.0.0) 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/config.yml.sample b/config/config.yml.sample index 503521d9..49a50dc1 100644 --- a/config/config.yml.sample +++ b/config/config.yml.sample @@ -42,3 +42,4 @@ 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 From 4a4314da5e159dfa4b27354964a8d90d9a207a2e Mon Sep 17 00:00:00 2001 From: rbsec Date: Mon, 17 Feb 2014 17:23:40 +0000 Subject: [PATCH 044/372] Fixed alt text on peer4commit svg --- app/views/projects/show.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index 88835fbb..adc6647e 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -84,6 +84,6 @@ / 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: "[![tip for next commit](#{project_url(@project, format: :svg)})](#{project_url(@project)})"} From 3edfa146f8c75e1ab2f2b25a5e455ac76163db03 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Thu, 20 Feb 2014 13:55:23 +0100 Subject: [PATCH 045/372] upgraded rails to 4.0.3 --- Gemfile | 2 +- Gemfile.lock | 46 +++++++++++++++++++++++----------------------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/Gemfile b/Gemfile index 1f0a132e..9d18d3e4 100644 --- a/Gemfile +++ b/Gemfile @@ -1,7 +1,7 @@ source 'https://rubygems.org' # Bundle edge Rails instead: gem 'rails', github: 'rails/rails' -gem 'rails', '4.0.2' +gem 'rails', '~> 4.0.2' # Databases gem 'sqlite3', group: :development diff --git a/Gemfile.lock b/Gemfile.lock index e0441aa1..80ef624b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -20,25 +20,25 @@ GIT GEM remote: https://rubygems.org/ specs: - actionmailer (4.0.2) - actionpack (= 4.0.2) + actionmailer (4.0.3) + actionpack (= 4.0.3) mail (~> 2.5.4) - actionpack (4.0.2) - activesupport (= 4.0.2) + actionpack (4.0.3) + activesupport (= 4.0.3) builder (~> 3.1.0) erubis (~> 2.7.0) rack (~> 1.5.2) rack-test (~> 0.6.2) - activemodel (4.0.2) - activesupport (= 4.0.2) + activemodel (4.0.3) + activesupport (= 4.0.3) builder (~> 3.1.0) - activerecord (4.0.2) - activemodel (= 4.0.2) + activerecord (4.0.3) + activemodel (= 4.0.3) activerecord-deprecated_finders (~> 1.0.2) - activesupport (= 4.0.2) + activesupport (= 4.0.3) arel (~> 4.0.0) activerecord-deprecated_finders (1.0.3) - activesupport (4.0.2) + activesupport (4.0.3) i18n (~> 0.6, >= 0.6.4) minitest (~> 4.2) multi_json (~> 1.3) @@ -48,7 +48,7 @@ GEM airbrake (3.1.15) builder multi_json - arel (4.0.1) + arel (4.0.2) atomic (1.1.14) bcrypt-ruby (3.1.2) builder (3.1.4) @@ -147,24 +147,24 @@ GEM omniauth (~> 1.0) orm_adapter (0.5.0) pg (0.17.1) - polyglot (0.3.3) + polyglot (0.3.4) rack (1.5.2) rack-canonical-host (0.0.8) addressable rack (~> 1.0) rack-test (0.6.2) rack (>= 1.0) - rails (4.0.2) - actionmailer (= 4.0.2) - actionpack (= 4.0.2) - activerecord (= 4.0.2) - activesupport (= 4.0.2) + rails (4.0.3) + actionmailer (= 4.0.3) + actionpack (= 4.0.3) + activerecord (= 4.0.3) + activesupport (= 4.0.3) bundler (>= 1.3.0, < 2.0) - railties (= 4.0.2) + railties (= 4.0.3) sprockets-rails (~> 2.0.0) - railties (4.0.2) - actionpack (= 4.0.2) - activesupport (= 4.0.2) + railties (4.0.3) + actionpack (= 4.0.3) + activesupport (= 4.0.3) rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) rake (10.1.1) @@ -185,7 +185,7 @@ GEM sdoc (0.4.0) json (~> 1.8) rdoc (~> 4.0, < 5.0) - sprockets (2.10.1) + sprockets (2.11.0) hike (~> 1.2) multi_json (~> 1.0) rack (~> 1.0) @@ -248,7 +248,7 @@ DEPENDENCIES omniauth-github pg rack-canonical-host - rails (= 4.0.2) + rails (~> 4.0.2) rqrcode-rails3 sass-rails (~> 4.0.0) sdoc From bf801048c1dc44d3694c0d31f891e804997b82c8 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Thu, 20 Feb 2014 13:55:53 +0100 Subject: [PATCH 046/372] svg is already defined by rqrcode --- config/initializers/mime_types.rb | 1 - 1 file changed, 1 deletion(-) 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 From 81fbc41528412dcb6311de77e2d47a0288ede3a7 Mon Sep 17 00:00:00 2001 From: Fuzzybear Date: Fri, 21 Feb 2014 02:23:31 +0000 Subject: [PATCH 047/372] Support Peer4commit project reference --- app/views/layouts/application.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 3e15b83c..c365fe2d 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -56,7 +56,7 @@ 2014. Source code is available at #{link_to('github', 'https://github.com/sigmike/peer4commit', target: '_blank')}, based on #{link_to "Tip4commit", "http://tip4commit.com/"}. You can - = link_to('support', 'http://tip4commit.com/projects/560') + = link_to('support', 'http://peer4commit.com/projects/1') its development. / /container / From 6d52291b5cb4d29fa105ed1822d0cc6fd10ab29a Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Fri, 21 Feb 2014 07:24:03 +0100 Subject: [PATCH 048/372] Support in both peercoin and bitcoin --- README.md | 3 ++- app/views/layouts/application.html.haml | 7 ++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index c0580ce0..8edf630a 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,8 @@ Peer4commit ========== -[![tip for next commit](http://tip4commit.com/projects/560.svg)](http://tip4commit.com/projects/560) +[![peercoin tip for next commit](http://peer4commit.com/projects/1.svg)](http://peer4commit.com/projects/1) +[![bitcoin tip for next commit](http://tip4commit.com/projects/560.svg)](http://tip4commit.com/projects/560) Donate peercoins to open source projects or make commits and get tips for it. diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index c365fe2d..95489bce 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -55,9 +55,10 @@ = link_to 'Peer4commit', 'http://peer4commit.com/', target: '_blank' 2014. Source code is available at #{link_to('github', 'https://github.com/sigmike/peer4commit', target: '_blank')}, based on #{link_to "Tip4commit", "http://tip4commit.com/"}. - You can - = link_to('support', 'http://peer4commit.com/projects/1') - its development. + You can support its development with + = link_to('peercoins', 'http://peer4commit.com/projects/1') + or + = link_to('bitcoins', 'http://tip4commit.com/projects/560') / /container / Bootstrap core JavaScript From 0009dbd713629acc13239b9a0a07a40d6e00bfb1 Mon Sep 17 00:00:00 2001 From: Bharat Gupta Date: Fri, 21 Feb 2014 23:03:18 +0530 Subject: [PATCH 049/372] optimize User.update_cache to remove extra queries --- app/models/tip.rb | 4 ++++ app/models/user.rb | 7 ++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/app/models/tip.rb b/app/models/tip.rb index 05c8a9be..5a110c6b 100644 --- a/app/models/tip.rb +++ b/app/models/tip.rb @@ -20,6 +20,10 @@ class Tip < ActiveRecord::Base scope :with_address, -> { joins(:user).where('users.bitcoin_address IS NOT NULL') } + def paid? + !!sendmany_id + end + def self.refund_unclaimed unclaimed.non_refunded. where('tips.created_at < ?', Time.now - 1.month). diff --git a/app/models/user.rb b/app/models/user.rb index 940d185c..543e85aa 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -36,9 +36,10 @@ def full_name 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) + includes(:tips).find_each do |user| + user.update( commits_count: user.tips.size, + withdrawn_amount: user.tips.select(&:paid?).sum(:amount) ) end end + end From 8e165c144125b023a5bb8509c90612dd9c16f088 Mon Sep 17 00:00:00 2001 From: aditya-kapoor Date: Sun, 23 Feb 2014 02:11:44 +0530 Subject: [PATCH 050/372] add boostrap classed for flash messages --- app/helpers/application_helper.rb | 12 ++++++++++++ app/views/layouts/application.html.haml | 7 +------ 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index bc5315e0..fd51b31c 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -16,4 +16,16 @@ def to_btc satoshies def transaction_url(txid) "http://bkchain.org/ppc/tx/#{txid}" end + + def render_flash_message + html = [] + flash.each do |_type, _message| + alert_type = case _type + when :notice then :success + when :alert then :danger + end + html << content_tag(:div, class: "text-center alert alert-#{alert_type}"){ _message } + end + html.join("\n").html_safe + end end diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 95489bce..aea55b76 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -41,12 +41,7 @@ %a{href: user_omniauth_authorize_path(:github)} Sign in %h3.text-muted.code-pro Peer4Commit = render 'common/menu' - - if flash[:alert] - %br - .alert= flash[:alert] - - if flash[:notice] - %br - .alert.alert-info= flash[:notice] + = render_flash_message = yield / Site footer .footer From 4917b6df10d02d578e05499f1d191b7218dd6e4a Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 23 Feb 2014 08:44:48 +0100 Subject: [PATCH 051/372] fixed User.update_cache broken sum and removed tip instantiations --- app/models/user.rb | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/app/models/user.rb b/app/models/user.rb index 543e85aa..1ab2fa99 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -36,9 +36,13 @@ def full_name end def self.update_cache - includes(:tips).find_each do |user| - user.update( commits_count: user.tips.size, - withdrawn_amount: user.tips.select(&:paid?).sum(:amount) ) + 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 From 16d78d544943ef694e4a8632a58da32384988f34 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Mon, 24 Feb 2014 22:34:31 +0100 Subject: [PATCH 052/372] fixed bug when address is blank --- app/models/tip.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/tip.rb b/app/models/tip.rb index 5a110c6b..4ce54477 100644 --- a/app/models/tip.rb +++ b/app/models/tip.rb @@ -18,7 +18,7 @@ class Tip < ActiveRecord::Base unpaid. where('users.bitcoin_address' => ['', nil]) } - scope :with_address, -> { joins(:user).where('users.bitcoin_address IS NOT NULL') } + scope :with_address, -> { joins(:user).where('users.bitcoin_address IS NOT NULL AND users.bitcoin_address != ?', "") } def paid? !!sendmany_id From 66493058d715df181012eeaef8eb50493a48fb22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Witrant?= Date: Thu, 27 Feb 2014 13:09:01 +0100 Subject: [PATCH 053/372] updated readme for primecoin --- README.md | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 8edf630a..341943dc 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,19 @@ -Peer4commit +Prime4commit ========== -[![peercoin tip for next commit](http://peer4commit.com/projects/1.svg)](http://peer4commit.com/projects/1) -[![bitcoin tip for next commit](http://tip4commit.com/projects/560.svg)](http://tip4commit.com/projects/560) +[![peercoin tip for next commit](http://peer4commit.com/projects/16.svg)](http://peer4commit.com/projects/16) -Donate peercoins to open source projects or make commits and get tips for it. -Official site: http://peer4commit.com/ +Donate primecoins to open source projects or make commits and get tips for it. + +Official site: http://prime4commit.com/ License ======= -[MIT License](https://github.com/sigmike/peer4commit/blob/master/LICENSE) +[MIT License](https://github.com/sigmike/prime4commit/blob/master/LICENSE) + +Based on [peer4commit](http://peer4commit.com/), [MIT License](https://github.com/sigmike/peer4commit/blob/master/LICENSE), copyright (c) 2014 sigmike -Based on [Tip4commit](http://tip4commit.com/), [MIT License](https://github.com/tip4commit/tip4commit/blob/master/LICENSE), copyright (c) 2013-2014 tip4commit +Which is based on [Tip4commit](http://tip4commit.com/), [MIT License](https://github.com/tip4commit/tip4commit/blob/master/LICENSE), copyright (c) 2013-2014 tip4commit From dbcea4bdca917299af25e730b129269e67421e0d Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Tue, 4 Mar 2014 21:12:18 +0100 Subject: [PATCH 054/372] do not use the free email field of github to authenticate users --- Gemfile | 2 +- Gemfile.lock | 18 ++++++++++++------ .../users/omniauth_callbacks_controller.rb | 4 +++- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/Gemfile b/Gemfile index 9d18d3e4..95de1e3a 100644 --- a/Gemfile +++ b/Gemfile @@ -42,7 +42,7 @@ end gem 'devise' gem 'omniauth' -gem 'omniauth-github' +gem 'omniauth-github', git: 'https://github.com/sigmike/omniauth-github.git', branch: 'provide_emails' gem 'octokit' diff --git a/Gemfile.lock b/Gemfile.lock index 80ef624b..6da9129e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -17,6 +17,15 @@ GIT rails (>= 3.1) railties (>= 3.1) +GIT + remote: https://github.com/sigmike/omniauth-github.git + revision: a6a3a48b5bad1f7b677e07340edee95c070da94d + branch: provide_emails + specs: + omniauth-github (1.1.0) + omniauth (~> 1.0) + omniauth-oauth2 (~> 1.1) + GEM remote: https://rubygems.org/ specs: @@ -96,7 +105,7 @@ GEM httparty (0.12.0) json (~> 1.8) multi_xml (>= 0.5.2) - httpauth (0.2.0) + httpauth (0.2.1) i18n (0.6.9) jbuilder (1.5.3) activesupport (>= 3.0.0) @@ -105,7 +114,7 @@ GEM railties (>= 3.0, < 5.0) thor (>= 0.14, < 2.0) json (1.8.1) - jwt (0.1.10) + jwt (0.1.11) multi_json (>= 1.5) kaminari (0.15.0) actionpack (>= 3.0.0) @@ -139,9 +148,6 @@ GEM omniauth (1.1.4) hashie (>= 1.2, < 3) rack - omniauth-github (1.1.1) - omniauth (~> 1.0) - omniauth-oauth2 (~> 1.1) omniauth-oauth2 (1.1.1) oauth2 (~> 0.8.0) omniauth (~> 1.0) @@ -245,7 +251,7 @@ DEPENDENCIES mysql2 octokit omniauth - omniauth-github + omniauth-github! pg rack-canonical-host rails (~> 4.0.2) diff --git a/app/controllers/users/omniauth_callbacks_controller.rb b/app/controllers/users/omniauth_callbacks_controller.rb index 125a01b0..6529c38a 100644 --- a/app/controllers/users/omniauth_callbacks_controller.rb +++ b/app/controllers/users/omniauth_callbacks_controller.rb @@ -3,7 +3,9 @@ def github # render text: "#{request.env["omniauth.auth"].to_json}" info = request.env["omniauth.auth"]["info"] @user = User.find_by :nickname => info["nickname"] - @user ||= User.find_by :email => info["email"] + if @user.nil? and info["emails"].any? + @user = User.find_by :email => info["emails"] + end unless @user generated_password = Devise.friendly_token.first(8) @user = User.create!( From c4b9f9330192146b94b524c8b5064de57d15ee44 Mon Sep 17 00:00:00 2001 From: Aleksandr Zykov Date: Wed, 5 Mar 2014 13:46:11 +0700 Subject: [PATCH 055/372] use verified emails only --- Gemfile | 2 +- Gemfile.lock | 18 +++++++-------- .../users/omniauth_callbacks_controller.rb | 23 +++++++++++-------- 3 files changed, 24 insertions(+), 19 deletions(-) diff --git a/Gemfile b/Gemfile index 95de1e3a..56bb5d09 100644 --- a/Gemfile +++ b/Gemfile @@ -42,7 +42,7 @@ end gem 'devise' gem 'omniauth' -gem 'omniauth-github', git: 'https://github.com/sigmike/omniauth-github.git', branch: 'provide_emails' +gem 'omniauth-github', github: 'alexandrz/omniauth-github', branch: 'provide_emails' gem 'octokit' diff --git a/Gemfile.lock b/Gemfile.lock index 6da9129e..36f2d127 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,3 +1,12 @@ +GIT + remote: git://github.com/alexandrz/omniauth-github.git + revision: 37a030aa37659831ef80af21b5c7270fe1384b3c + branch: provide_emails + specs: + omniauth-github (1.1.0) + omniauth (~> 1.0) + omniauth-oauth2 (~> 1.1) + GIT remote: git://github.com/capistrano/rvm.git revision: 19e8d15ae3d705499c610370f159d523bbedbd94 @@ -17,15 +26,6 @@ GIT rails (>= 3.1) railties (>= 3.1) -GIT - remote: https://github.com/sigmike/omniauth-github.git - revision: a6a3a48b5bad1f7b677e07340edee95c070da94d - branch: provide_emails - specs: - omniauth-github (1.1.0) - omniauth (~> 1.0) - omniauth-oauth2 (~> 1.1) - GEM remote: https://rubygems.org/ specs: diff --git a/app/controllers/users/omniauth_callbacks_controller.rb b/app/controllers/users/omniauth_callbacks_controller.rb index 6529c38a..4b828708 100644 --- a/app/controllers/users/omniauth_callbacks_controller.rb +++ b/app/controllers/users/omniauth_callbacks_controller.rb @@ -3,16 +3,21 @@ def github # render text: "#{request.env["omniauth.auth"].to_json}" info = request.env["omniauth.auth"]["info"] @user = User.find_by :nickname => info["nickname"] - if @user.nil? and info["emails"].any? - @user = User.find_by :email => info["emails"] + if @user.nil? and info["verified_emails"].any? + @user = User.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'] + generated_password = Devise.friendly_token.first(8) + @user = User.create!( + :email => info['primary_email'], + :password => generated_password, + :nickname => info['nickname'] + ) + else + set_flash_message(:error, :failure, kind: 'GitHub', reason: 'your promary email address should be verified.') + redirect_to new_user_session_path and return + end end @user.name = info['name'] @@ -20,6 +25,6 @@ def github @user.save sign_in_and_redirect @user, :event => :authentication - set_flash_message(:notice, :success, :kind => "Github") if is_navigational_format? + set_flash_message(:notice, :success, :kind => "GitHub") if is_navigational_format? end end From e56b8b0bbfe7821ae7f44cb52ac586c928e4bca4 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Thu, 6 Mar 2014 20:39:26 +0100 Subject: [PATCH 056/372] send security issue email --- app/mailers/user_mailer.rb | 5 +++++ app/views/user_mailer/security_issue.html.haml | 18 ++++++++++++++++++ config/environments/development.rb | 10 +++++++--- lib/tasks/send_security_issue.rake | 5 +++++ 4 files changed, 35 insertions(+), 3 deletions(-) create mode 100644 app/views/user_mailer/security_issue.html.haml create mode 100644 lib/tasks/send_security_issue.rake diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb index e21ded23..f585efb9 100644 --- a/app/mailers/user_mailer.rb +++ b/app/mailers/user_mailer.rb @@ -7,4 +7,9 @@ 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 end 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/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/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 From 469af0b95bfe22484f4cef5b476adac2476d55d8 Mon Sep 17 00:00:00 2001 From: Chris Morgan Date: Fri, 7 Mar 2014 16:45:34 +1100 Subject: [PATCH 057/372] Stop PPCoin addresses overflowing the panel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This was a problem on the Project Sponsors panel as demonstrated at http://peer4commit.com/projects/1. This change makes the entire block gain a scroll bar if necessary, so that the entire value is still accessible, but it is not in the way, overflowing onto the top of the main content. This was mainly an issue for a viewport width in the range 992–1199px, though with a significant percentage of wider characters (e.g. lots of W's, not many i's) it could still appear outside that range. --- app/assets/stylesheets/bootstrap_and_overrides.css.less | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/bootstrap_and_overrides.css.less b/app/assets/stylesheets/bootstrap_and_overrides.css.less index 9c3ec0d1..b60cf8df 100644 --- a/app/assets/stylesheets/bootstrap_and_overrides.css.less +++ b/app/assets/stylesheets/bootstrap_and_overrides.css.less @@ -27,4 +27,5 @@ // // Example: // @linkColor: #ff0000; -.qrcode {text-align:center} \ No newline at end of file +.qrcode {text-align:center} +.panel-body {overflow:auto} From b469f0b3206c5c00e158c8838d6caf8a5b4f87a4 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 9 Mar 2014 17:11:57 +0100 Subject: [PATCH 058/372] increase project amount cache capacity --- .../20140309161105_change_project_amount_cache_to_big_int.rb | 5 +++++ db/schema.rb | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 db/migrate/20140309161105_change_project_amount_cache_to_big_int.rb 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/schema.rb b/db/schema.rb index 9f88f66c..acf3beb9 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20140215094549) do +ActiveRecord::Schema.define(version: 20140309161105) do create_table "deposits", force: true do |t| t.integer "project_id" @@ -39,7 +39,7 @@ t.integer "watchers_count" t.string "language" t.string "last_commit" - t.integer "available_amount_cache" + t.integer "available_amount_cache", limit: 8 t.string "github_id" t.string "address_label" end From 851fe837e981cddef5027add71614728050fdbcd Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sat, 22 Mar 2014 15:37:34 +0100 Subject: [PATCH 059/372] added a button to immediatly send tips back to their project (closes #47) --- app/assets/stylesheets/users.css.scss | 4 ++++ app/controllers/users_controller.rb | 7 +++++++ app/models/tip.rb | 5 +++-- app/views/users/show.html.haml | 6 ++++++ config/routes.rb | 3 +++ 5 files changed, 23 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/users.css.scss b/app/assets/stylesheets/users.css.scss index 31a2eacb..9c4ccae2 100644 --- a/app/assets/stylesheets/users.css.scss +++ b/app/assets/stylesheets/users.css.scss @@ -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/ + +.send-tips-back-block { + margin-top: 50px; +} diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 02496d2b..54be8c89 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -35,6 +35,13 @@ def login 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 + private def users_params params.require(:user).permit(:bitcoin_address) diff --git a/app/models/tip.rb b/app/models/tip.rb index 4ce54477..a611567f 100644 --- a/app/models/tip.rb +++ b/app/models/tip.rb @@ -5,8 +5,9 @@ class Tip < ActiveRecord::Base validates :amount, :numericality => { :greater_than => 0 } - scope :unpaid, -> { non_refunded. - where(sendmany_id: nil) } + scope :not_sent, -> { where(sendmany_id: nil) } + + scope :unpaid, -> { non_refunded.not_sent } scope :paid, -> { where('sendmany_id is not ?', nil) } diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index 84eb0421..c3790b69 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -17,3 +17,9 @@ = f.label :bitcoin_address = f.text_field :bitcoin_address, class: 'form-control', placeholder: 'Your peercoin address' = f.button :update, class: 'btn btn-default' + +- if @user.balance > 0 + .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", confirm: "All the #{to_btc @user.balance} peercoins you received will be sent back to their project. Are you sure?" diff --git a/config/routes.rb b/config/routes.rb index ab7f59f3..073b4f39 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -6,6 +6,9 @@ collection do get :login end + member do + post :send_tips_back + end end resources :projects, :only => [:show, :index, :create] do resources :tips, :only => [:index] From 3baa6de837c35dd55435757466d5f8bb7ed15ba2 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 23 Mar 2014 13:29:54 +0100 Subject: [PATCH 060/372] quiet assets --- Gemfile | 1 + Gemfile.lock | 3 +++ 2 files changed, 4 insertions(+) diff --git a/Gemfile b/Gemfile index 56bb5d09..c7c7daf6 100644 --- a/Gemfile +++ b/Gemfile @@ -60,6 +60,7 @@ group :development do gem 'capistrano-rvm', github: 'capistrano/rvm' gem 'capistrano-bundler', '>= 1.1.0' gem 'capistrano-rails' + gem 'quiet_assets' end gem 'airbrake' diff --git a/Gemfile.lock b/Gemfile.lock index 36f2d127..8961a7c5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -154,6 +154,8 @@ GEM orm_adapter (0.5.0) pg (0.17.1) polyglot (0.3.4) + quiet_assets (1.0.2) + railties (>= 3.1, < 5.0) rack (1.5.2) rack-canonical-host (0.0.8) addressable @@ -253,6 +255,7 @@ DEPENDENCIES omniauth omniauth-github! pg + quiet_assets rack-canonical-host rails (~> 4.0.2) rqrcode-rails3 From a0a52d9b93ae391d89b42dbc057585f3cf326b60 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 9 Mar 2014 19:47:37 +0100 Subject: [PATCH 061/372] added cucumber --- Gemfile | 7 +++++ Gemfile.lock | 42 ++++++++++++++++++++++++++ config/cucumber.yml | 8 +++++ features/support/env.rb | 58 ++++++++++++++++++++++++++++++++++++ lib/tasks/cucumber.rake | 65 +++++++++++++++++++++++++++++++++++++++++ script/cucumber | 10 +++++++ 6 files changed, 190 insertions(+) create mode 100644 config/cucumber.yml create mode 100644 features/support/env.rb create mode 100644 lib/tasks/cucumber.rake create mode 100755 script/cucumber diff --git a/Gemfile b/Gemfile index c7c7daf6..1ff39b47 100644 --- a/Gemfile +++ b/Gemfile @@ -69,3 +69,10 @@ gem 'whenever' gem 'rqrcode-rails3' gem 'exception_notification' gem 'rack-canonical-host' + +group :test do + gem 'cucumber-rails', :require => false + # database_cleaner is not required, but highly recommended + gem 'database_cleaner' + gem 'rspec-rails' +end diff --git a/Gemfile.lock b/Gemfile.lock index 8961a7c5..b163a211 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -71,6 +71,12 @@ GEM capistrano-rails (1.1.0) capistrano (>= 3.0.0) capistrano-bundler (>= 1.0.0) + capybara (2.2.1) + mime-types (>= 1.16) + nokogiri (>= 1.3.3) + rack (>= 1.0.0) + rack-test (>= 0.5.4) + xpath (~> 2.0) chronic (0.10.2) coffee-rails (4.0.1) coffee-script (>= 2.2.0) @@ -80,12 +86,25 @@ GEM execjs coffee-script-source (1.6.3) commonjs (0.2.7) + cucumber (1.3.10) + builder (>= 2.1.2) + diff-lcs (>= 1.1.3) + gherkin (~> 2.12) + multi_json (>= 1.7.5, < 2.0) + multi_test (>= 0.0.2) + cucumber-rails (1.4.0) + capybara (>= 1.1.2) + cucumber (>= 1.2.0) + nokogiri (>= 1.5.0) + rails (>= 3.0.0) + database_cleaner (1.2.0) devise (3.2.2) bcrypt-ruby (~> 3.0) orm_adapter (~> 0.1) railties (>= 3.2.6, < 5) thread_safe (~> 0.1) warden (~> 1.2.3) + diff-lcs (1.2.5) erubis (2.7.0) exception_notification (4.0.1) actionmailer (>= 3.0.4) @@ -93,6 +112,8 @@ GEM execjs (2.0.2) faraday (0.8.9) multipart-post (~> 1.2.0) + gherkin (2.12.2) + multi_json (~> 1.3) haml (4.0.5) tilt haml-rails (0.5.3) @@ -129,14 +150,18 @@ GEM mime-types (~> 1.16) treetop (~> 1.4.8) mime-types (1.25.1) + mini_portile (0.5.2) minitest (4.7.5) multi_json (1.8.4) + multi_test (0.0.3) multi_xml (0.5.5) multipart-post (1.2.0) mysql2 (0.3.14) net-scp (1.1.2) net-ssh (>= 2.6.5) net-ssh (2.7.0) + nokogiri (1.6.1) + mini_portile (~> 0.5.0) oauth2 (0.8.1) faraday (~> 0.8) httpauth (~> 0.1) @@ -182,6 +207,18 @@ GEM rqrcode (0.4.2) rqrcode-rails3 (0.1.7) rqrcode (>= 0.4.2) + rspec-core (2.14.7) + rspec-expectations (2.14.5) + diff-lcs (>= 1.1.3, < 2.0) + rspec-mocks (2.14.6) + rspec-rails (2.14.1) + actionpack (>= 3.0) + activemodel (>= 3.0) + activesupport (>= 3.0) + railties (>= 3.0) + rspec-core (~> 2.14.0) + rspec-expectations (~> 2.14.0) + rspec-mocks (~> 2.14.0) sass (3.2.13) sass-rails (4.0.1) railties (>= 4.0.0, < 5.0) @@ -231,6 +268,8 @@ GEM whenever (0.9.0) activesupport (>= 2.3.4) chronic (>= 0.6.3) + xpath (2.0.0) + nokogiri (~> 1.3) PLATFORMS ruby @@ -242,6 +281,8 @@ DEPENDENCIES capistrano-rails capistrano-rvm! coffee-rails (~> 4.0.0) + cucumber-rails + database_cleaner devise exception_notification haml-rails @@ -259,6 +300,7 @@ DEPENDENCIES rack-canonical-host rails (~> 4.0.2) rqrcode-rails3 + rspec-rails sass-rails (~> 4.0.0) sdoc sqlite3 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/features/support/env.rb b/features/support/env.rb new file mode 100644 index 00000000..9f3b86d4 --- /dev/null +++ b/features/support/env.rb @@ -0,0 +1,58 @@ +# 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 + 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/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 From 7ae0f0fc250bb6e36ed03b69ce4c1709c6e37b7c Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 9 Mar 2014 21:00:59 +0100 Subject: [PATCH 062/372] projects have a list of collaborators --- app/models/collaborator.rb | 3 ++ app/models/project.rb | 28 +++++++++++++++++++ .../20140309192616_create_collaborators.rb | 10 +++++++ db/schema.rb | 11 +++++++- 4 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 app/models/collaborator.rb create mode 100644 db/migrate/20140309192616_create_collaborators.rb diff --git a/app/models/collaborator.rb b/app/models/collaborator.rb new file mode 100644 index 00000000..3c0e6f15 --- /dev/null +++ b/app/models/collaborator.rb @@ -0,0 +1,3 @@ +class Collaborator < ActiveRecord::Base + belongs_to :project +end diff --git a/app/models/project.rb b/app/models/project.rb index 3b086b40..97593120 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -1,6 +1,7 @@ class Project < ActiveRecord::Base has_many :deposits # todo: only confirmed deposits that have amount > paid_out has_many :tips + has_many :collaborators validates :full_name, uniqueness: true, presence: true validates :github_id, uniqueness: true, presence: true @@ -16,6 +17,25 @@ def update_github_info repo self.save! end + def update_github_collaborators(github_collaborators) + github_logins = github_collaborators.map(&:login) + existing_logins = collaborators.map(&:login) + + collaborators.each do |collaborator| + unless github_logins.include?(collaborator.login) + collaborator.mark_for_destruction + end + end + + github_collaborators.each do |github_collaborator| + unless existing_logins.include?(github_collaborator.login) + collaborators.build(login: github_collaborator.login) + end + end + + save! + end + def github_url "https://github.com/#{full_name}" end @@ -139,9 +159,17 @@ def github_info end end + def github_collaborators + client = Octokit::Client.new \ + :client_id => CONFIG['github']['key'], + :client_secret => CONFIG['github']['secret'] + client.get("/repos/#{full_name}/collaborators") + end + def update_info begin update_github_info(github_info) + update_github_collaborators(github_collaborators) rescue Octokit::BadGateway, Octokit::NotFound, Octokit::InternalServerError, Errno::ETIMEDOUT, Net::ReadTimeout, Faraday::Error::ConnectionFailed => e Rails.logger.info "Project ##{id}: #{e.class} happened" 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/schema.rb b/db/schema.rb index acf3beb9..be9e0a25 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,16 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20140309161105) do +ActiveRecord::Schema.define(version: 20140309192616) do + + create_table "collaborators", force: true do |t| + t.integer "project_id" + t.string "login" + t.datetime "created_at" + t.datetime "updated_at" + end + + add_index "collaborators", ["project_id"], name: "index_collaborators_on_project_id" create_table "deposits", force: true do |t| t.integer "project_id" From 7ef2e1cb07e34b1686012f3a7fba08263b3d374e Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sat, 22 Mar 2014 17:39:44 +0100 Subject: [PATCH 063/372] configured test environment --- config/environments/test.rb | 3 + features/step_definitions/common.rb | 83 +++++++++++++++++++++++++ features/support/big_decimal_inspect.rb | 5 ++ features/support/rspec_doubles.rb | 1 + features/support/to_ostruct.rb | 23 +++++++ 5 files changed, 115 insertions(+) create mode 100644 features/step_definitions/common.rb create mode 100644 features/support/big_decimal_inspect.rb create mode 100644 features/support/rspec_doubles.rb create mode 100644 features/support/to_ostruct.rb diff --git a/config/environments/test.rb b/config/environments/test.rb index 799d7195..ec5a2086 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -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/features/step_definitions/common.rb b/features/step_definitions/common.rb new file mode 100644 index 00000000..72fc8a19 --- /dev/null +++ b/features/step_definitions/common.rb @@ -0,0 +1,83 @@ + +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!(full_name: "example/test", github_id: 123) +end + +Given(/^a deposit of "(.*?)"$/) do |arg1| + Deposit.create!(project: @project, amount: arg1.to_d * PeercoinBalanceUpdater::COIN, confirmations: 1) +end + +Given(/^the last known commit is "(.*?)"$/) do |arg1| + @project.update!(last_commit: arg1) +end + +def add_new_commit(id, params = {}) + @new_commits ||= {} + defaults = { + sha: id, + commit: { + message: "Some changes", + author: { + email: "anonymous@example.com", + }, + }, + } + @new_commits[id] = defaults.deep_merge(params) +end + +def find_new_commit(id) + @new_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(/^the message of commit "(.*?)" is "(.*?)"$/) do |arg1, arg2| + find_new_commit(arg1).deep_merge!(commit: {message: arg2}) +end + +When(/^the new commits are read$/) do + @project.should_receive(:new_commits).and_return(@new_commits.values.map(&:to_ostruct)) + @project.tip_commits +end + +Then(/^there should be no tip for commit "(.*?)"$/) do |arg1| + Tip.where(commit: arg1).to_a.should eq([]) +end + +Then(/^there should be a tip of "(.*?)" for commit "(.*?)"$/) do |arg1, arg2| + (Tip.find_by(commit: arg2).amount.to_d / PeercoinBalanceUpdater::COIN).should eq(arg1.to_d) +end + +Then(/^the new last known commit should be "(.*?)"$/) do |arg1| + @project.reload.last_commit.should 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 author of commit "(.*?)" is "(.*?)"$/) do |arg1, arg2| + find_new_commit(arg1).deep_merge!(author: {login: arg2}) +end + +Given(/^an illustration of the history is:$/) do |string| + # not checked +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/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 From f52daf7ed69290bcf8b0a3dbac50ee5ed22e3157 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sat, 22 Mar 2014 17:44:51 +0100 Subject: [PATCH 064/372] project collaborators can put tips on hold --- Gemfile | 1 + Gemfile.lock | 2 ++ app/controllers/projects_controller.rb | 19 ++++++++++++ app/models/project.rb | 10 +++++-- app/models/tip.rb | 6 +++- app/views/projects/edit.html.haml | 7 +++++ app/views/projects/show.html.haml | 1 + config/routes.rb | 2 +- ...20140323072851_add_hold_tips_to_project.rb | 5 ++++ db/schema.rb | 3 +- features/step_definitions/common.rb | 14 ++++++++- .../tip_modifier_interface.rb | 0 features/step_definitions/web.rb | 30 +++++++++++++++++++ features/tip_modifier_interface.feature | 29 ++++++++++++++++++ 14 files changed, 123 insertions(+), 6 deletions(-) create mode 100644 app/views/projects/edit.html.haml create mode 100644 db/migrate/20140323072851_add_hold_tips_to_project.rb create mode 100644 features/step_definitions/tip_modifier_interface.rb create mode 100644 features/step_definitions/web.rb create mode 100644 features/tip_modifier_interface.feature diff --git a/Gemfile b/Gemfile index 1ff39b47..853db02d 100644 --- a/Gemfile +++ b/Gemfile @@ -69,6 +69,7 @@ gem 'whenever' gem 'rqrcode-rails3' gem 'exception_notification' gem 'rack-canonical-host' +gem 'bootstrap_forms' group :test do gem 'cucumber-rails', :require => false diff --git a/Gemfile.lock b/Gemfile.lock index b163a211..9fccff62 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -60,6 +60,7 @@ GEM arel (4.0.2) atomic (1.1.14) bcrypt-ruby (3.1.2) + bootstrap_forms (4.0.1) builder (3.1.4) capistrano (3.0.1) i18n @@ -276,6 +277,7 @@ PLATFORMS DEPENDENCIES airbrake + bootstrap_forms capistrano (~> 3.0) capistrano-bundler (>= 1.1.0) capistrano-rails diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 2e8a945b..3bd23bdc 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -14,6 +14,20 @@ def show end end + def edit + @project = Project.find params[:id] + end + + def update + @project = Project.find params[:id] + @project.attributes = project_params + if @project.save + redirect_to project_path(@project), notice: "The project settings have been updated" + else + render 'edit' + end + end + def qrcode @project = Project.find params[:id] respond_to do |format| @@ -38,4 +52,9 @@ def create redirect_to projects_path, alert: "Project not found" end end + + private + def project_params + params.require(:project).permit(:hold_tips) + end end diff --git a/app/models/project.rb b/app/models/project.rb index 97593120..251ee04a 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -101,16 +101,22 @@ def tip_for commit user.update nickname: commit.author.login end + if hold_tips + amount = nil + else + amount = next_tip_amount + end + # create tip tip = Tip.create({ project: self, user: user, - amount: next_tip_amount, + amount: amount, commit: commit.sha }) # notify user - if tip && user.bitcoin_address.blank? && !user.unsubscribed + if tip && tip.amount && 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 diff --git a/app/models/tip.rb b/app/models/tip.rb index a611567f..63ae2e12 100644 --- a/app/models/tip.rb +++ b/app/models/tip.rb @@ -3,7 +3,7 @@ class Tip < ActiveRecord::Base belongs_to :sendmany belongs_to :project - validates :amount, :numericality => { :greater_than => 0 } + validates :amount, numericality: {greater_than: 0, allow_nil: true} scope :not_sent, -> { where(sendmany_id: nil) } @@ -25,6 +25,10 @@ def paid? !!sendmany_id end + def amount_undecided? + amount.nil? + end + def self.refund_unclaimed unclaimed.non_refunded. where('tips.created_at < ?', Time.now - 1.month). diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml new file mode 100644 index 00000000..4bcce566 --- /dev/null +++ b/app/views/projects/edit.html.haml @@ -0,0 +1,7 @@ +- content_for :title do + = @project.name + settings + += bootstrap_form_for @project do |f| + = f.check_box :hold_tips, label: "Do not send the tips immediatly. Give collaborators the ability to modify the tips before they're sent" + = f.submit 'Save the project settings' diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index adc6647e..db634e0c 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -7,6 +7,7 @@ %h1 = @project.full_name %small= link_to glyph(:github), @project.github_url, target: '_blank' + = link_to "Change project settings", edit_project_path(@project), class: "btn btn-default" .row .col-md-4 diff --git a/config/routes.rb b/config/routes.rb index 073b4f39..bb16e470 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -10,7 +10,7 @@ post :send_tips_back end end - resources :projects, :only => [:show, :index, :create] do + resources :projects, :only => [:show, :index, :create, :edit, :update] do resources :tips, :only => [:index] member do get :qrcode 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/schema.rb b/db/schema.rb index be9e0a25..12834de6 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20140309192616) do +ActiveRecord::Schema.define(version: 20140323072851) do create_table "collaborators", force: true do |t| t.integer "project_id" @@ -51,6 +51,7 @@ t.integer "available_amount_cache", limit: 8 t.string "github_id" t.string "address_label" + t.boolean "hold_tips", default: false end add_index "projects", ["full_name"], name: "index_projects_on_full_name", unique: true diff --git a/features/step_definitions/common.rb b/features/step_definitions/common.rb index 72fc8a19..a3a924b8 100644 --- a/features/step_definitions/common.rb +++ b/features/step_definitions/common.rb @@ -1,3 +1,10 @@ +Before do + ActionMailer::Base.deliveries.clear +end + +Then(/^there should be (\d+) email sent$/) do |arg1| + ActionMailer::Base.deliveries.size.should eq(arg1.to_i) +end Given(/^the tip for commit is "(.*?)"$/) do |arg1| CONFIG["tip"] = arg1.to_f @@ -8,7 +15,7 @@ end Given(/^a project$/) do - @project = Project.create!(full_name: "example/test", github_id: 123) + @project = Project.create!(full_name: "example/test", github_id: 123, bitcoin_address: 'mq4NtnmQoQoPfNWEPbhSvxvncgtGo6L8WY') end Given(/^a deposit of "(.*?)"$/) do |arg1| @@ -50,6 +57,7 @@ def find_new_commit(id) end When(/^the new commits are read$/) do + @project.reload @project.should_receive(:new_commits).and_return(@new_commits.values.map(&:to_ostruct)) @project.tip_commits end @@ -62,6 +70,10 @@ def find_new_commit(id) (Tip.find_by(commit: arg2).amount.to_d / PeercoinBalanceUpdater::COIN).should eq(arg1.to_d) end +Then(/^the tip amount for commit "(.*?)" should be undecided$/) do |arg1| + Tip.find_by(commit: arg1).amount_undecided?.should be_true +end + Then(/^the new last known commit should be "(.*?)"$/) do |arg1| @project.reload.last_commit.should eq(arg1) 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..e69de29b diff --git a/features/step_definitions/web.rb b/features/step_definitions/web.rb new file mode 100644 index 00000000..c3ee3013 --- /dev/null +++ b/features/step_definitions/web.rb @@ -0,0 +1,30 @@ +Given(/^I'm logged in as "(.*?)"$/) do |arg1| + OmniAuth.config.test_mode = true + OmniAuth.config.mock_auth[:github] = { + "info" => { + "nickname" => arg1, + "primary_email" => "#{arg1}@example.com", + "verified_emails" => [], + }, + } + visit root_path + click_on "Sign in" + page.should have_content("Successfully authenticated") +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 check "(.*?)"$/) do |arg1| + check(arg1) +end + +Then(/^I should see "(.*?)"$/) do |arg1| + page.should have_content(arg1) +end + diff --git a/features/tip_modifier_interface.feature b/features/tip_modifier_interface.feature new file mode 100644 index 00000000..4be55301 --- /dev/null +++ b/features/tip_modifier_interface.feature @@ -0,0 +1,29 @@ +Feature: A project collaborator can change the tips of commits + Background: + Given a project + And the project collaborators are: + | seldon | + | daneel | + And our fee is "0" + And a deposit of "500" + And the last known commit is "A" + And a new commit "B" with parent "A" + And the author of commit "B" is "yugo" + + Scenario: Without anything modified + When the new commits are read + Then there should be a tip of "5" for commit "B" + And there should be 1 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 "Change project settings" + And I check "Do not send the tips immediatly. Give collaborators the ability to modify the tips before they're sent" + And I click on "Save the project settings" + Then I should see "The project settings have been updated" + + When the new commits are read + Then the tip amount for commit "B" should be undecided + And there should be 0 email sent + From a84ce0876c0a5daa3255f89664446b1d609a3252 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 23 Mar 2014 11:12:59 +0100 Subject: [PATCH 065/372] project collaborators can decide tip amount of undecided tips --- Gemfile | 2 +- Gemfile.lock | 10 ++++- app/controllers/projects_controller.rb | 15 +++++++ app/models/project.rb | 21 +++++----- app/models/tip.rb | 40 +++++++++++++++++-- .../projects/decide_tip_amounts.html.haml | 25 ++++++++++++ app/views/projects/show.html.haml | 2 + app/views/tips/index.html.haml | 2 + config/routes.rb | 2 + features/step_definitions/common.rb | 10 ++++- .../tip_modifier_interface.rb | 5 +++ features/step_definitions/web.rb | 4 ++ features/tip_modifier_interface.feature | 33 ++++++++++++--- 13 files changed, 149 insertions(+), 22 deletions(-) create mode 100644 app/views/projects/decide_tip_amounts.html.haml diff --git a/Gemfile b/Gemfile index 853db02d..17f26b7c 100644 --- a/Gemfile +++ b/Gemfile @@ -69,7 +69,7 @@ gem 'whenever' gem 'rqrcode-rails3' gem 'exception_notification' gem 'rack-canonical-host' -gem 'bootstrap_forms' +gem 'bootstrap_forms', github: 'sigmike/bootstrap_forms', branch: 'sanitize_value_in_radio_label_for' group :test do gem 'cucumber-rails', :require => false diff --git a/Gemfile.lock b/Gemfile.lock index 9fccff62..c66fe8cd 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -26,6 +26,13 @@ GIT rails (>= 3.1) railties (>= 3.1) +GIT + remote: git://github.com/sigmike/bootstrap_forms.git + revision: e69b6834d36b198d36a8f4a892d1bac6633e7545 + branch: sanitize_value_in_radio_label_for + specs: + bootstrap_forms (4.0.1) + GEM remote: https://rubygems.org/ specs: @@ -60,7 +67,6 @@ GEM arel (4.0.2) atomic (1.1.14) bcrypt-ruby (3.1.2) - bootstrap_forms (4.0.1) builder (3.1.4) capistrano (3.0.1) i18n @@ -277,7 +283,7 @@ PLATFORMS DEPENDENCIES airbrake - bootstrap_forms + bootstrap_forms! capistrano (~> 3.0) capistrano-bundler (>= 1.1.0) capistrano-rails diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 3bd23bdc..01758224 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -28,6 +28,21 @@ def update end end + def decide_tip_amounts + @project = Project.find params[:id] + if request.patch? + @project.attributes = params.require(:project).permit(tips_attributes: [:id, :amount_percentage]) + 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 @project = Project.find params[:id] respond_to do |format| diff --git a/app/models/project.rb b/app/models/project.rb index 251ee04a..50eb2a45 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -1,6 +1,7 @@ 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 validates :full_name, uniqueness: true, presence: true @@ -115,13 +116,7 @@ def tip_for commit commit: commit.sha }) - # notify user - if tip && tip.amount && 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 @@ -185,10 +180,18 @@ def update_info end def tips_to_pay - tips.unpaid.with_address + tips.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 end diff --git a/app/models/tip.rb b/app/models/tip.rb index 63ae2e12..792a4b51 100644 --- a/app/models/tip.rb +++ b/app/models/tip.rb @@ -1,14 +1,16 @@ class Tip < ActiveRecord::Base belongs_to :user belongs_to :sendmany - belongs_to :project + belongs_to :project, inverse_of: :tips - validates :amount, numericality: {greater_than: 0, allow_nil: true} + validates :amount, numericality: {greater_or_equal_than: 0, allow_nil: true} scope :not_sent, -> { where(sendmany_id: nil) } scope :unpaid, -> { non_refunded.not_sent } + scope :to_pay, -> { unpaid.decided.with_address } + scope :paid, -> { where('sendmany_id is not ?', nil) } scope :refunded, -> { where('refunded_at is not ?', nil) } @@ -21,11 +23,16 @@ class Tip < ActiveRecord::Base scope :with_address, -> { joins(:user).where('users.bitcoin_address IS NOT NULL AND users.bitcoin_address != ?', "") } + scope :undecided, -> { where(amount: nil) } + scope :decided, -> { where.not(amount: nil) } + + after_save :notify_user_if_just_decided + def paid? !!sendmany_id end - def amount_undecided? + def undecided? amount.nil? end @@ -36,4 +43,31 @@ def self.refund_unclaimed tip.touch :refunded_at end end + + def commit_url + project.commit_url(commit) + end + + def amount_percentage + nil + end + + def amount_percentage=(percentage) + if undecided? and percentage.present? + self.amount = project.available_amount * (percentage.to_f / 100) + end + end + + def notify_user + if amount and amount > 0 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 + user.touch :notified_at + end + end + end + + def notify_user_if_just_decided + notify_user if amount_was.nil? and amount + end end 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..f37d3e70 --- /dev/null +++ b/app/views/projects/decide_tip_amounts.html.haml @@ -0,0 +1,25 @@ += bootstrap_form_for @project, url: decide_tip_amounts_project_path(@project) do |f| + %table.table + %thead + %tr + %th Commit + %th Tip (relative to the project balance) + %tbody + = f.fields_for(:tips, @project.tips.undecided) do |tip_fields| + = tip_fields.hidden_field :id + - tip = tip_fields.object + %tr + %td + = link_to tip.commit, tip.commit_url + %td + - radios = {} + - radios["Undecided"] = "" + - radios["Free: 0%"] = "0" + - radios["Tiny: 0.1%"] = "0.1" + - radios["Small: 0.5%"] = "0.5" + - radios["Normal: 1%"] = "1" + - radios["Big: 2%"] = "2" + - radios["Huge: 5%"] = "5" + = tip_fields.radio_buttons(:amount_percentage, radios, inline: true, label: false) + .text-center + = f.submit 'Send the selected tip amounts' diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index db634e0c..dd79d504 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -8,6 +8,8 @@ = @project.full_name %small= link_to glyph(:github), @project.github_url, target: '_blank' = link_to "Change project settings", edit_project_path(@project), class: "btn btn-default" + - if @project.has_undecided_tips? + = link_to "Decide tip amounts", decide_tip_amounts_project_path(@project), class: "btn btn-primary" .row .col-md-4 diff --git a/app/views/tips/index.html.haml b/app/views/tips/index.html.haml index dca6a51a..df75befd 100644 --- a/app/views/tips/index.html.haml +++ b/app/views/tips/index.html.haml @@ -32,6 +32,8 @@ - if tip.sendmany.nil? - if tip.refunded_at Refunded to project's deposit + - elsif tip.undecided? + The amount of tips has not been decided yet - elsif tip.user.bitcoin_address.blank? User didn't specify withdrawal address - elsif tip.project.amount_to_pay < CONFIG["min_payout"].to_d * PeercoinBalanceUpdater::COIN diff --git a/config/routes.rb b/config/routes.rb index bb16e470..6ee4ad84 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -14,6 +14,8 @@ resources :tips, :only => [:index] member do get :qrcode + get :decide_tip_amounts + patch :decide_tip_amounts end end resources :tips, :only => [:index] diff --git a/features/step_definitions/common.rb b/features/step_definitions/common.rb index a3a924b8..8e10884a 100644 --- a/features/step_definitions/common.rb +++ b/features/step_definitions/common.rb @@ -6,6 +6,10 @@ ActionMailer::Base.deliveries.size.should eq(arg1.to_i) 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 @@ -67,11 +71,13 @@ def find_new_commit(id) end Then(/^there should be a tip of "(.*?)" for commit "(.*?)"$/) do |arg1, arg2| - (Tip.find_by(commit: arg2).amount.to_d / PeercoinBalanceUpdater::COIN).should eq(arg1.to_d) + amount = Tip.find_by(commit: arg2).amount + amount.should_not be_nil + (amount.to_d / PeercoinBalanceUpdater::COIN).should eq(arg1.to_d) end Then(/^the tip amount for commit "(.*?)" should be undecided$/) do |arg1| - Tip.find_by(commit: arg1).amount_undecided?.should be_true + Tip.find_by(commit: arg1).undecided?.should be_true end Then(/^the new last known commit should be "(.*?)"$/) do |arg1| diff --git a/features/step_definitions/tip_modifier_interface.rb b/features/step_definitions/tip_modifier_interface.rb index e69de29b..ad2bbb05 100644 --- a/features/step_definitions/tip_modifier_interface.rb +++ b/features/step_definitions/tip_modifier_interface.rb @@ -0,0 +1,5 @@ +When(/^I choose the amount "(.*?)" on commit "(.*?)"$/) do |arg1, arg2| + within find("tr", text: arg2) do + choose arg1 + end +end diff --git a/features/step_definitions/web.rb b/features/step_definitions/web.rb index c3ee3013..78fbe4a9 100644 --- a/features/step_definitions/web.rb +++ b/features/step_definitions/web.rb @@ -28,3 +28,7 @@ page.should have_content(arg1) end +Then(/^I should not see "(.*?)"$/) do |arg1| + page.should have_no_content(arg1) +end + diff --git a/features/tip_modifier_interface.feature b/features/tip_modifier_interface.feature index 4be55301..b250b2a0 100644 --- a/features/tip_modifier_interface.feature +++ b/features/tip_modifier_interface.feature @@ -6,13 +6,16 @@ Feature: A project collaborator can change the tips of commits | daneel | And our fee is "0" And a deposit of "500" - 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 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 author of commit "CCC" is "seldon" Scenario: Without anything modified When the new commits are read - Then there should be a tip of "5" for commit "B" + 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 1 email sent Scenario: A collaborator wants to alter the tips @@ -24,6 +27,26 @@ Feature: A project collaborator can change the tips of commits Then I should see "The project settings have been updated" When the new commits are read - Then the tip amount for commit "B" should be undecided + 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 "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 1 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 From 688a776bf18a019a19d218e9d5f6638675680a7b Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 23 Mar 2014 15:35:19 +0100 Subject: [PATCH 066/372] only collaborators can change projects and set tip amounts --- Gemfile | 2 + Gemfile.lock | 8 +++ app/controllers/application_controller.rb | 4 ++ app/controllers/projects_controller.rb | 3 ++ app/models/ability.rb | 9 ++++ app/views/projects/show.html.haml | 5 +- .../tip_modifier_interface.rb | 53 +++++++++++++++++++ features/support/factory_girl.rb | 1 + features/tip_modifier_interface.feature | 39 ++++++++++++++ test/factories/tips.rb | 13 +++++ test/factories/users.rb | 8 +++ 11 files changed, 143 insertions(+), 2 deletions(-) create mode 100644 app/models/ability.rb create mode 100644 features/support/factory_girl.rb create mode 100644 test/factories/tips.rb create mode 100644 test/factories/users.rb diff --git a/Gemfile b/Gemfile index 17f26b7c..70ec1488 100644 --- a/Gemfile +++ b/Gemfile @@ -43,6 +43,7 @@ end gem 'devise' gem 'omniauth' gem 'omniauth-github', github: 'alexandrz/omniauth-github', branch: 'provide_emails' +gem 'cancancan' gem 'octokit' @@ -76,4 +77,5 @@ group :test do # database_cleaner is not required, but highly recommended gem 'database_cleaner' gem 'rspec-rails' + gem 'factory_girl_rails' end diff --git a/Gemfile.lock b/Gemfile.lock index c66fe8cd..b5c7dbf3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -68,6 +68,7 @@ GEM atomic (1.1.14) bcrypt-ruby (3.1.2) builder (3.1.4) + cancancan (1.7.1) capistrano (3.0.1) i18n rake (>= 10.0.0) @@ -117,6 +118,11 @@ GEM actionmailer (>= 3.0.4) activesupport (>= 3.0.4) execjs (2.0.2) + factory_girl (4.4.0) + activesupport (>= 3.0.0) + factory_girl_rails (4.4.1) + factory_girl (~> 4.4.0) + railties (>= 3.0.0) faraday (0.8.9) multipart-post (~> 1.2.0) gherkin (2.12.2) @@ -284,6 +290,7 @@ PLATFORMS DEPENDENCIES airbrake bootstrap_forms! + cancancan capistrano (~> 3.0) capistrano-bundler (>= 1.1.0) capistrano-rails @@ -293,6 +300,7 @@ DEPENDENCIES database_cleaner devise exception_notification + factory_girl_rails haml-rails httparty jbuilder (~> 1.2) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index d83690e1..99564543 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -2,4 +2,8 @@ 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| + redirect_to root_path, :alert => "Access denied" + end end diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 01758224..aeb4ce78 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -16,10 +16,12 @@ def show def edit @project = Project.find params[:id] + authorize! :update, @project end def update @project = Project.find params[:id] + authorize! :update, @project @project.attributes = project_params if @project.save redirect_to project_path(@project), notice: "The project settings have been updated" @@ -30,6 +32,7 @@ def update def decide_tip_amounts @project = Project.find params[:id] + authorize! :decide_tip_amounts, @project if request.patch? @project.attributes = params.require(:project).permit(tips_attributes: [:id, :amount_percentage]) if @project.save diff --git a/app/models/ability.rb b/app/models/ability.rb new file mode 100644 index 00000000..63f6fd78 --- /dev/null +++ b/app/models/ability.rb @@ -0,0 +1,9 @@ +class Ability + include CanCan::Ability + + def initialize(user) + if user and user.nickname.present? + can [:update, :decide_tip_amounts], Project, collaborators: {login: user.nickname} + end + end +end diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index dd79d504..788d68cf 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -7,8 +7,9 @@ %h1 = @project.full_name %small= link_to glyph(:github), @project.github_url, target: '_blank' - = link_to "Change project settings", edit_project_path(@project), class: "btn btn-default" - - if @project.has_undecided_tips? + - if can? :update, @project + = link_to "Change project settings", edit_project_path(@project), class: "btn btn-default" + - 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-primary" .row diff --git a/features/step_definitions/tip_modifier_interface.rb b/features/step_definitions/tip_modifier_interface.rb index ad2bbb05..73bd9430 100644 --- a/features/step_definitions/tip_modifier_interface.rb +++ b/features/step_definitions/tip_modifier_interface.rb @@ -3,3 +3,56 @@ choose arg1 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 + page.should have_content("Access denied") +end + +Then(/^the project should not hold tips$/) do + @project.reload.hold_tips.should be_false +end + +Then(/^the project should hold tips$/) do + @project.reload.hold_tips.should be_true +end + +Given(/^the project has undedided tips$/) do + create(:undecided_tip, project: @project) + @project.reload.should have_undecided_tips +end + +Given(/^the project has (\d+) undecided tip$/) do |arg1| + @project.tips.undecided.each(&:destroy) + create(:undecided_tip, project: @project) + @project.reload.should 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 + tip.should_not be_nil + params = { + project: { + tips_attributes: { + "0" => { + id: tip.id, + amount_percentage: "5", + }, + }, + }, + } + + 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| + @project.tips.undecided.size.should eq(arg1.to_i) +end + 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/tip_modifier_interface.feature b/features/tip_modifier_interface.feature index b250b2a0..851786f5 100644 --- a/features/tip_modifier_interface.feature +++ b/features/tip_modifier_interface.feature @@ -50,3 +50,42 @@ Feature: A project collaborator can change the tips of commits And there should be a tip of "0" for commit "CCC" And there should be 0 email sent + 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 "Change project settings" + + 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 | + 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..9303c680 --- /dev/null +++ b/test/factories/users.rb @@ -0,0 +1,8 @@ +# 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" + end +end From 83439f4eda7d44a0e1c6ecf91aba5ccf8bb6f9d8 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 23 Mar 2014 17:57:19 +0100 Subject: [PATCH 067/372] added author and message to the tip decision table --- app/assets/stylesheets/projects.css.scss | 4 ++++ app/helpers/application_helper.rb | 4 ++++ app/models/project.rb | 3 ++- app/views/projects/decide_tip_amounts.html.haml | 9 ++++++--- db/migrate/20140323165816_add_commit_message_to_tip.rb | 5 +++++ db/schema.rb | 5 +++-- features/tip_modifier_interface.feature | 2 ++ 7 files changed, 26 insertions(+), 6 deletions(-) create mode 100644 db/migrate/20140323165816_add_commit_message_to_tip.rb diff --git a/app/assets/stylesheets/projects.css.scss b/app/assets/stylesheets/projects.css.scss index d0192666..6d95023e 100644 --- a/app/assets/stylesheets/projects.css.scss +++ b/app/assets/stylesheets/projects.css.scss @@ -1,3 +1,7 @@ // 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/ + +.commit-sha { + font-family: monospace; +} diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index bc5315e0..ff1b6ea9 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -16,4 +16,8 @@ def to_btc satoshies def transaction_url(txid) "http://bkchain.org/ppc/tx/#{txid}" end + + def commit_tag(sha1) + content_tag(:span, truncate(sha1, length: 10, omission: ""), class: "commit-sha") + end end diff --git a/app/models/project.rb b/app/models/project.rb index 50eb2a45..f8426c88 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -113,7 +113,8 @@ def tip_for commit project: self, user: user, amount: amount, - commit: commit.sha + commit: commit.sha, + commit_message: commit.commit.message, }) tip.notify_user diff --git a/app/views/projects/decide_tip_amounts.html.haml b/app/views/projects/decide_tip_amounts.html.haml index f37d3e70..9ef349ff 100644 --- a/app/views/projects/decide_tip_amounts.html.haml +++ b/app/views/projects/decide_tip_amounts.html.haml @@ -1,16 +1,19 @@ = bootstrap_form_for @project, url: decide_tip_amounts_project_path(@project) do |f| - %table.table + %table.table.table-hover %thead %tr %th Commit + %th Author + %th Message %th Tip (relative to the project balance) %tbody = f.fields_for(:tips, @project.tips.undecided) do |tip_fields| = tip_fields.hidden_field :id - tip = tip_fields.object %tr - %td - = link_to tip.commit, tip.commit_url + %td= link_to commit_tag(tip.commit), tip.commit_url + %td= tip.user.nickname + %td= simple_format tip.commit_message %td - radios = {} - radios["Undecided"] = "" 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/schema.rb b/db/schema.rb index 12834de6..e0ba7d89 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20140323072851) do +ActiveRecord::Schema.define(version: 20140323165816) do create_table "collaborators", force: true do |t| t.integer "project_id" @@ -71,13 +71,14 @@ create_table "tips", force: true do |t| t.integer "user_id" - t.integer "amount", limit: 8 + t.integer "amount", limit: 8 t.integer "sendmany_id" t.datetime "created_at" t.datetime "updated_at" t.string "commit" t.integer "project_id" t.datetime "refunded_at" + t.string "commit_message" end add_index "tips", ["project_id"], name: "index_tips_on_project_id" diff --git a/features/tip_modifier_interface.feature b/features/tip_modifier_interface.feature index 851786f5..5794ee52 100644 --- a/features/tip_modifier_interface.feature +++ b/features/tip_modifier_interface.feature @@ -10,6 +10,7 @@ Feature: A project collaborator can change the tips of commits 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 "seldon" Scenario: Without anything modified @@ -34,6 +35,7 @@ Feature: A project collaborator can change the tips of commits 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" From 3611013080f7c16536c86589102f35e9f50a09cc Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 23 Mar 2014 18:13:35 +0100 Subject: [PATCH 068/372] fixed feature, there should be one email per commit --- features/step_definitions/common.rb | 2 +- features/tip_modifier_interface.feature | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/features/step_definitions/common.rb b/features/step_definitions/common.rb index 8e10884a..eb16cb7e 100644 --- a/features/step_definitions/common.rb +++ b/features/step_definitions/common.rb @@ -93,7 +93,7 @@ def find_new_commit(id) end Given(/^the author of commit "(.*?)" is "(.*?)"$/) do |arg1, arg2| - find_new_commit(arg1).deep_merge!(author: {login: arg2}) + find_new_commit(arg1).deep_merge!(author: {login: arg2}, commit: {author: {email: "#{arg2}@example.com"}}) end Given(/^an illustration of the history is:$/) do |string| diff --git a/features/tip_modifier_interface.feature b/features/tip_modifier_interface.feature index 5794ee52..ac9c30fe 100644 --- a/features/tip_modifier_interface.feature +++ b/features/tip_modifier_interface.feature @@ -11,13 +11,13 @@ Feature: A project collaborator can change the tips of commits 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 "seldon" + And the author of commit "CCC" is "gaal" Scenario: Without anything modified 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 1 email sent + And there should be 2 email sent Scenario: A collaborator wants to alter the tips Given I'm logged in as "seldon" From f95dcf85f1b2906461c0c625b0cb5b5076d1c46a Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 23 Mar 2014 19:12:51 +0100 Subject: [PATCH 069/372] collaborators can display tipping policies on the project page --- app/controllers/projects_controller.rb | 5 +++- app/models/project.rb | 3 +++ app/models/tipping_policies_text.rb | 4 +++ app/views/projects/edit.html.haml | 17 ++++++++++--- app/views/projects/show.html.haml | 13 ++++++++++ ...323173320_create_tipping_policies_texts.rb | 11 ++++++++ db/schema.rb | 13 +++++++++- features/step_definitions/web.rb | 14 +++++++++++ features/tipping_policies.feature | 25 +++++++++++++++++++ test/fixtures/tipping_policies_texts.yml | 11 ++++++++ test/models/tipping_policies_text_test.rb | 7 ++++++ 11 files changed, 118 insertions(+), 5 deletions(-) create mode 100644 app/models/tipping_policies_text.rb create mode 100644 db/migrate/20140323173320_create_tipping_policies_texts.rb create mode 100644 features/tipping_policies.feature create mode 100644 test/fixtures/tipping_policies_texts.yml create mode 100644 test/models/tipping_policies_text_test.rb diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index aeb4ce78..57b01d49 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -23,6 +23,9 @@ def update @project = Project.find params[:id] 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 settings have been updated" else @@ -73,6 +76,6 @@ def create private def project_params - params.require(:project).permit(:hold_tips) + params.require(:project).permit(:hold_tips, tipping_policies_text_attributes: [:text]) end end diff --git a/app/models/project.rb b/app/models/project.rb index f8426c88..a494f97e 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -4,6 +4,9 @@ class Project < ActiveRecord::Base accepts_nested_attributes_for :tips has_many :collaborators + has_one :tipping_policies_text, inverse_of: :project + accepts_nested_attributes_for :tipping_policies_text + validates :full_name, uniqueness: true, presence: true validates :github_id, uniqueness: true, presence: true 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/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 4bcce566..0d57695d 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -2,6 +2,17 @@ = @project.name settings -= bootstrap_form_for @project do |f| - = f.check_box :hold_tips, label: "Do not send the tips immediatly. Give collaborators the ability to modify the tips before they're sent" - = f.submit 'Save the project settings' +%h1 #{@project.full_name} project settings +.row + .col-md-12 + = form_for @project, html: {role: "form"} do |f| + = f.fields_for :tipping_policies_text, @project.tipping_policies_text || @project.build_tipping_policies_text do |fields| + .form-group + = fields.label :text, "Tipping policies" + = fields.text_area :text, rows: 10, class: "form-control" + + .checkbox + %label + = f.check_box :hold_tips + Do not send the tips immediatly. Give collaborators the ability to modify the tips before they're sent + = f.submit 'Save the project settings', class: "btn btn-default" diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index 788d68cf..44b98b4c 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -34,6 +34,19 @@ - if (unconfirmed_amount = @project.unconfirmed_amount) > 0 (#{btc_human unconfirmed_amount} unconfirmed) + - if @project.tipping_policies_text.try(:text).present? + %h4 Tipping policies + = simple_format @project.tipping_policies_text.text + %small + %em + - user = @project.tipping_policies_text.user + - name = user.name.presence || user.nickname if user + - date = l(@project.tipping_policies_text.updated_at) + - if name.present? + = "(Last updated by #{name} on #{date})" + - else + = "(Last updated on #{date})" + %h4 Tips Paid = btc_human @project.tips_paid_amount - if (tips_paid_unclaimed_amount = @project.tips_paid_unclaimed_amount) > 0 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/schema.rb b/db/schema.rb index e0ba7d89..a9abc408 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20140323165816) do +ActiveRecord::Schema.define(version: 20140323173320) do create_table "collaborators", force: true do |t| t.integer "project_id" @@ -69,6 +69,17 @@ add_index "sendmanies", ["project_id"], name: "index_sendmanies_on_project_id" + create_table "tipping_policies_texts", force: true 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" + add_index "tipping_policies_texts", ["user_id"], name: "index_tipping_policies_texts_on_user_id" + create_table "tips", force: true do |t| t.integer "user_id" t.integer "amount", limit: 8 diff --git a/features/step_definitions/web.rb b/features/step_definitions/web.rb index 78fbe4a9..f1fc2cc0 100644 --- a/features/step_definitions/web.rb +++ b/features/step_definitions/web.rb @@ -12,6 +12,16 @@ page.should have_content("Successfully authenticated") end +Given(/^I'm not logged in$/) do + visit root_path + if page.has_content?("Sign Out") + click_on "Sign Out" + page.should have_content("Signed out successfully") + else + page.should have_content("Sign in") + end +end + Given(/^I go to the project page$/) do visit project_path(@project) end @@ -32,3 +42,7 @@ page.should have_no_content(arg1) end +Given(/^I fill "(.*?)" with:$/) do |arg1, string| + fill_in arg1, with: string +end + diff --git a/features/tipping_policies.feature b/features/tipping_policies.feature new file mode 100644 index 00000000..3b3c3e8a --- /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 "Change project settings" + And I fill "Tipping policies" with: + """ + All commits are huge! + + Blah blah + """ + And I click on "Save the project settings" + Then I should see "The project settings have 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/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/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 From 93277d5f2fc1b52b8cb427affa0ca7e1f2f4d75c Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 23 Mar 2014 19:47:58 +0100 Subject: [PATCH 070/372] switched to the new rails-bootstrap-forms gem --- Gemfile | 2 +- Gemfile.lock | 10 +++++----- app/assets/stylesheets/application.css | 3 ++- app/views/projects/decide_tip_amounts.html.haml | 17 ++++++++--------- app/views/projects/edit.html.haml | 14 ++++---------- 5 files changed, 20 insertions(+), 26 deletions(-) diff --git a/Gemfile b/Gemfile index 70ec1488..ff43a892 100644 --- a/Gemfile +++ b/Gemfile @@ -70,7 +70,7 @@ gem 'whenever' gem 'rqrcode-rails3' gem 'exception_notification' gem 'rack-canonical-host' -gem 'bootstrap_forms', github: 'sigmike/bootstrap_forms', branch: 'sanitize_value_in_radio_label_for' +gem 'bootstrap_form', github: 'sigmike/rails-bootstrap-forms', branch: 'removed_for_on_radio_label' group :test do gem 'cucumber-rails', :require => false diff --git a/Gemfile.lock b/Gemfile.lock index b5c7dbf3..2ae3c8d5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -27,11 +27,11 @@ GIT railties (>= 3.1) GIT - remote: git://github.com/sigmike/bootstrap_forms.git - revision: e69b6834d36b198d36a8f4a892d1bac6633e7545 - branch: sanitize_value_in_radio_label_for + remote: git://github.com/sigmike/rails-bootstrap-forms.git + revision: a1c8420ab999df56b13d3de8a097528b77a02217 + branch: removed_for_on_radio_label specs: - bootstrap_forms (4.0.1) + bootstrap_form (2.0.1) GEM remote: https://rubygems.org/ @@ -289,7 +289,7 @@ PLATFORMS DEPENDENCIES airbrake - bootstrap_forms! + bootstrap_form! cancancan capistrano (~> 3.0) capistrano-bundler (>= 1.1.0) diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index 82b772b1..dfd74b4f 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,4 @@ -webkit-box-sizing: content-box; -moz-box-sizing: content-box; box-sizing: content-box; -} \ No newline at end of file +} diff --git a/app/views/projects/decide_tip_amounts.html.haml b/app/views/projects/decide_tip_amounts.html.haml index 9ef349ff..12a95f03 100644 --- a/app/views/projects/decide_tip_amounts.html.haml +++ b/app/views/projects/decide_tip_amounts.html.haml @@ -15,14 +15,13 @@ %td= tip.user.nickname %td= simple_format tip.commit_message %td - - radios = {} - - radios["Undecided"] = "" - - radios["Free: 0%"] = "0" - - radios["Tiny: 0.1%"] = "0.1" - - radios["Small: 0.5%"] = "0.5" - - radios["Normal: 1%"] = "1" - - radios["Big: 2%"] = "2" - - radios["Huge: 5%"] = "5" - = tip_fields.radio_buttons(:amount_percentage, radios, inline: true, label: false) + = tip_fields.radio_button :amount_percentage, "", inline: true, label: "Undecided" + = tip_fields.radio_button :amount_percentage, "0", inline: true, label: "Free: 0%" + = tip_fields.radio_button :amount_percentage, "0.1", inline: true, label: "Tiny: 0.1%" + = tip_fields.radio_button :amount_percentage, "0.5", inline: true, label: "Small: 0.5%" + = tip_fields.radio_button :amount_percentage, "1", inline: true, label: "Normal: 1%" + = tip_fields.radio_button :amount_percentage, "2", inline: true, label: "Big: 2%" + = tip_fields.radio_button :amount_percentage, "5", inline: true, label: "Huge: 5%" + .text-center = f.submit 'Send the selected tip amounts' diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 0d57695d..8d085f9c 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -5,14 +5,8 @@ %h1 #{@project.full_name} project settings .row .col-md-12 - = form_for @project, html: {role: "form"} do |f| + = bootstrap_form_for @project do |f| = f.fields_for :tipping_policies_text, @project.tipping_policies_text || @project.build_tipping_policies_text do |fields| - .form-group - = fields.label :text, "Tipping policies" - = fields.text_area :text, rows: 10, class: "form-control" - - .checkbox - %label - = f.check_box :hold_tips - Do not send the tips immediatly. Give collaborators the ability to modify the tips before they're sent - = f.submit 'Save the project settings', class: "btn btn-default" + = fields.text_area :text, rows: 10, label: "Tipping policies" + = f.check_box :hold_tips, label: "Do not send the tips immediatly. Give collaborators the ability to modify the tips before they're sent" + = f.submit 'Save the project settings', class: "btn btn-default" From e0fea5df147b01219644b9890f5a7becddca658f Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 23 Mar 2014 20:14:28 +0100 Subject: [PATCH 071/372] better display of undecided tips --- app/models/tip.rb | 4 ++++ app/views/projects/show.html.haml | 9 +++++++-- app/views/tips/index.html.haml | 2 +- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/app/models/tip.rb b/app/models/tip.rb index 792a4b51..0e3e0947 100644 --- a/app/models/tip.rb +++ b/app/models/tip.rb @@ -36,6 +36,10 @@ def undecided? amount.nil? end + def decided? + !undecided? + end + def self.refund_unclaimed unclaimed.non_refunded. where('tips.created_at < ?', Time.now - 1.month). diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index 44b98b4c..f1c232cd 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -65,10 +65,15 @@ = tip.user.full_name - else = link_to tip.user.full_name, "https://github.com/#{tip.user.nickname}", target: '_blank' - received - = btc_human tip.amount + - if tip.decided? + received + = btc_human tip.amount + - else + will receive a tip for commit = link_to tip.commit[0..6], "https://github.com/#{@project.full_name}/commit/#{tip.commit}", target: :blank + - if tip.undecided? + when its amount is decided - if @project.next_tip_amount > 0 %h4 Next Tip diff --git a/app/views/tips/index.html.haml b/app/views/tips/index.html.haml index df75befd..272ced74 100644 --- a/app/views/tips/index.html.haml +++ b/app/views/tips/index.html.haml @@ -33,7 +33,7 @@ - if tip.refunded_at Refunded to project's deposit - elsif tip.undecided? - The amount of tips has not been decided yet + The amount of the tip has not been decided yet - elsif tip.user.bitcoin_address.blank? User didn't specify withdrawal address - elsif tip.project.amount_to_pay < CONFIG["min_payout"].to_d * PeercoinBalanceUpdater::COIN From 04060b0b1a5a87dbcc37a7781eb9beca96ffed89 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Wed, 26 Mar 2014 07:07:12 +0100 Subject: [PATCH 072/372] amount given diminishes after each decision (fixes #68) --- app/controllers/projects_controller.rb | 1 + app/models/project.rb | 5 ++-- app/models/tip.rb | 28 +++++++++++-------- .../projects/decide_tip_amounts.html.haml | 2 +- features/step_definitions/common.rb | 14 ++++++++++ .../tip_modifier_interface.rb | 10 ++++++- features/tip_modifier_interface.feature | 12 ++++++++ 7 files changed, 55 insertions(+), 17 deletions(-) diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 57b01d49..ca4b8a30 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -37,6 +37,7 @@ def decide_tip_amounts @project = Project.find params[:id] authorize! :decide_tip_amounts, @project if request.patch? + @project.available_amount # preload anything required to get the amount, otherwise it's loaded during the assignation and there are undesirable consequences @project.attributes = params.require(:project).permit(tips_attributes: [:id, :amount_percentage]) if @project.save message = "The tip amounts have been defined" diff --git a/app/models/project.rb b/app/models/project.rb index a494f97e..2ae2c66e 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -112,8 +112,7 @@ def tip_for commit end # create tip - tip = Tip.create({ - project: self, + tip = tips.create({ user: user, amount: amount, commit: commit.sha, @@ -136,7 +135,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 diff --git a/app/models/tip.rb b/app/models/tip.rb index 0e3e0947..36ec1476 100644 --- a/app/models/tip.rb +++ b/app/models/tip.rb @@ -12,10 +12,18 @@ class Tip < ActiveRecord::Base scope :to_pay, -> { unpaid.decided.with_address } scope :paid, -> { where('sendmany_id is not ?', nil) } + def paid? + !!sendmany_id + end scope :refunded, -> { where('refunded_at is not ?', nil) } - scope :non_refunded, -> { where(refunded_at: nil) } + def refunded? + !!refunded_at + end + def non_refunded? + !refunded? + end scope :unclaimed, -> { joins(:user). unpaid. @@ -23,22 +31,18 @@ class Tip < ActiveRecord::Base scope :with_address, -> { joins(:user).where('users.bitcoin_address IS NOT NULL AND users.bitcoin_address != ?', "") } - scope :undecided, -> { where(amount: nil) } scope :decided, -> { where.not(amount: nil) } - - after_save :notify_user_if_just_decided - - def paid? - !!sendmany_id + scope :undecided, -> { where(amount: nil) } + def decided? + !!amount end - def undecided? - amount.nil? + !decided? end - def decided? - !undecided? - end + + after_save :notify_user_if_just_decided + def self.refund_unclaimed unclaimed.non_refunded. diff --git a/app/views/projects/decide_tip_amounts.html.haml b/app/views/projects/decide_tip_amounts.html.haml index 12a95f03..b138ce55 100644 --- a/app/views/projects/decide_tip_amounts.html.haml +++ b/app/views/projects/decide_tip_amounts.html.haml @@ -1,5 +1,5 @@ = bootstrap_form_for @project, url: decide_tip_amounts_project_path(@project) do |f| - %table.table.table-hover + %table.table.table-hover.decide-tip-amounts-table %thead %tr %th Commit diff --git a/features/step_definitions/common.rb b/features/step_definitions/common.rb index eb16cb7e..fd67ac54 100644 --- a/features/step_definitions/common.rb +++ b/features/step_definitions/common.rb @@ -56,6 +56,20 @@ def find_new_commit(id) 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(/^a new commit "([^"]*?)"$/) do |arg1| + add_new_commit(arg1) +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 diff --git a/features/step_definitions/tip_modifier_interface.rb b/features/step_definitions/tip_modifier_interface.rb index 73bd9430..4fb52888 100644 --- a/features/step_definitions/tip_modifier_interface.rb +++ b/features/step_definitions/tip_modifier_interface.rb @@ -1,9 +1,17 @@ When(/^I choose the amount "(.*?)" on commit "(.*?)"$/) do |arg1, arg2| - within find("tr", text: arg2) do + within find(".decide-tip-amounts-table tbody tr", text: arg2) do choose 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 + choose arg1 + end + end +end + When(/^I go to the edit page of the project$/) do visit edit_project_path(@project) end diff --git a/features/tip_modifier_interface.feature b/features/tip_modifier_interface.feature index ac9c30fe..6cc7cc7f 100644 --- a/features/tip_modifier_interface.feature +++ b/features/tip_modifier_interface.feature @@ -91,3 +91,15 @@ Feature: A project collaborator can change the tips of commits | seldon | 0 | | yugo | 1 | + Scenario: A collaborator sends large amounts in tips + Given 20 new commits + And a new commit "last" + 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.088338" for commit "last" From dab576e8fdd105fde3fc661a0cbaa3d06de0f6db Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Wed, 26 Mar 2014 07:23:53 +0100 Subject: [PATCH 073/372] ensure a collaborator cannot change the tips of another project --- features/step_definitions/common.rb | 4 ++++ .../tip_modifier_interface.rb | 18 ++++++++++++++++ features/step_definitions/web.rb | 2 +- features/support/finders.rb | 4 ++++ features/tip_modifier_interface.feature | 21 ++++++++++++++++++- 5 files changed, 47 insertions(+), 2 deletions(-) create mode 100644 features/support/finders.rb diff --git a/features/step_definitions/common.rb b/features/step_definitions/common.rb index fd67ac54..a3b06567 100644 --- a/features/step_definitions/common.rb +++ b/features/step_definitions/common.rb @@ -22,6 +22,10 @@ @project = Project.create!(full_name: "example/test", github_id: 123, bitcoin_address: 'mq4NtnmQoQoPfNWEPbhSvxvncgtGo6L8WY') end +Given(/^a project "(.*?)"$/) do |arg1| + @project = Project.create!(full_name: "example/#{arg1}", github_id: Digest::SHA1.hexdigest(arg1), bitcoin_address: 'mq4NtnmQoQoPfNWEPbhSvxvncgtGo6L8WY') +end + Given(/^a deposit of "(.*?)"$/) do |arg1| Deposit.create!(project: @project, amount: arg1.to_d * PeercoinBalanceUpdater::COIN, confirmations: 1) end diff --git a/features/step_definitions/tip_modifier_interface.rb b/features/step_definitions/tip_modifier_interface.rb index 4fb52888..deb9dc60 100644 --- a/features/step_definitions/tip_modifier_interface.rb +++ b/features/step_definitions/tip_modifier_interface.rb @@ -60,6 +60,24 @@ 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 } + tip.should_not be_nil + params = { + project: { + tips_attributes: { + "0" => { + id: tip.id, + 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| @project.tips.undecided.size.should eq(arg1.to_i) end diff --git a/features/step_definitions/web.rb b/features/step_definitions/web.rb index f1fc2cc0..0d86695b 100644 --- a/features/step_definitions/web.rb +++ b/features/step_definitions/web.rb @@ -3,7 +3,7 @@ OmniAuth.config.mock_auth[:github] = { "info" => { "nickname" => arg1, - "primary_email" => "#{arg1}@example.com", + "primary_email" => "#{arg1.gsub(/\s+/,'')}@example.com", "verified_emails" => [], }, } 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/tip_modifier_interface.feature b/features/tip_modifier_interface.feature index 6cc7cc7f..26b2fbaa 100644 --- a/features/tip_modifier_interface.feature +++ b/features/tip_modifier_interface.feature @@ -1,6 +1,6 @@ Feature: A project collaborator can change the tips of commits Background: - Given a project + Given a project "a" And the project collaborators are: | seldon | | daneel | @@ -103,3 +103,22 @@ Feature: A project collaborator can change the tips of 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.088338" for commit "last" + + Scenario Outline: A collaborator changes the amount of a tip on another project + Given the project holds tips + And the new commits are read + And a project "fake" + 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 | + From f379fc7c7891d245cfe0a3f84d57d920c98d0403 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 23 Mar 2014 20:30:21 +0100 Subject: [PATCH 074/372] update all projects --- lib/bitcoin_tipper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/bitcoin_tipper.rb b/lib/bitcoin_tipper.rb index fe548163..ec4cfb15 100644 --- a/lib/bitcoin_tipper.rb +++ b/lib/bitcoin_tipper.rb @@ -15,7 +15,7 @@ def self.work end Rails.logger.info "Updating projects info..." - Project.order(:updated_at => :desc).last(10).each do |project| + Project.all.each do |project| Rails.logger.info " Project #{project.id} #{project.full_name}" project.update_info end From d5a146c1af31ccecd1fb455bb1b318944ae9bdf4 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Mon, 24 Mar 2014 08:36:37 +0100 Subject: [PATCH 075/372] display nickname first --- app/views/projects/show.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index f1c232cd..3f5cfff4 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -40,7 +40,7 @@ %small %em - user = @project.tipping_policies_text.user - - name = user.name.presence || user.nickname if 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})" From 5b1564cf06acd658f2498764f8a058ad6f961d4a Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Fri, 28 Mar 2014 23:51:58 +0100 Subject: [PATCH 076/372] free tips are not to pay --- app/models/tip.rb | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/app/models/tip.rb b/app/models/tip.rb index 36ec1476..228a43cb 100644 --- a/app/models/tip.rb +++ b/app/models/tip.rb @@ -9,7 +9,16 @@ class Tip < ActiveRecord::Base scope :unpaid, -> { non_refunded.not_sent } - scope :to_pay, -> { unpaid.decided.with_address } + scope :to_pay, -> { unpaid.decided.not_free.with_address } + def to_pay? + unpaid? and decided? and !free? and with_address? + end + + scope :free, -> { where('amount = 0') } + scope :not_free, -> { where('amount > 0') } + def free? + amount == 0 + end scope :paid, -> { where('sendmany_id is not ?', nil) } def paid? From 3c9fbe21e8ddd115507012ed33d1d66b493167c4 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Thu, 20 Mar 2014 21:08:52 +0000 Subject: [PATCH 077/372] do not catch Net::ReadTimeout because it's undefined --- app/models/project.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/project.rb b/app/models/project.rb index 2ae2c66e..187ad0dc 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -65,7 +65,7 @@ def new_commits to_a end rescue Octokit::BadGateway, Octokit::NotFound, Octokit::InternalServerError, - Errno::ETIMEDOUT, Net::ReadTimeout, Faraday::Error::ConnectionFailed => e + Errno::ETIMEDOUT, Faraday::Error::ConnectionFailed => e Rails.logger.info "Project ##{id}: #{e.class} happened" rescue StandardError => e Airbrake.notify(e) @@ -175,7 +175,7 @@ def update_info update_github_info(github_info) update_github_collaborators(github_collaborators) rescue Octokit::BadGateway, Octokit::NotFound, Octokit::InternalServerError, - Errno::ETIMEDOUT, Net::ReadTimeout, Faraday::Error::ConnectionFailed => e + Errno::ETIMEDOUT, Faraday::Error::ConnectionFailed => e Rails.logger.info "Project ##{id}: #{e.class} happened" rescue StandardError => e Airbrake.notify(e) From e183a48f6d534e30e95ac47f7623471be195a625 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Wed, 26 Mar 2014 07:36:29 +0100 Subject: [PATCH 078/372] truncate commit message to fit in database --- app/models/project.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/project.rb b/app/models/project.rb index 187ad0dc..6ff10698 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -116,7 +116,7 @@ def tip_for commit user: user, amount: amount, commit: commit.sha, - commit_message: commit.commit.message, + commit_message: ActionController::Base.helpers.truncate(commit.commit.message, length: 100), }) tip.notify_user From 1d462854d90b3aaa0ffa131aebb1f4c88792e930 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Wed, 26 Mar 2014 07:51:30 +0100 Subject: [PATCH 079/372] extra checks to ensure we never give more tips than the available amount --- app/models/project.rb | 12 ++++++++++-- app/models/sendmany.rb | 6 ++++++ app/models/tip.rb | 20 ++++++++++++++++++++ 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/app/models/project.rb b/app/models/project.rb index 6ff10698..027aadfa 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -10,6 +10,8 @@ class Project < ActiveRecord::Base validates :full_name, uniqueness: true, presence: true validates :github_id, uniqueness: true, presence: true + before_save :check_tips_to_pay_against_avaiable_amount + def update_github_info repo self.github_id = repo.id self.name = repo.name @@ -183,11 +185,11 @@ def update_info end def tips_to_pay - tips.to_pay + tips.select(&:to_pay?) end def amount_to_pay - tips_to_pay.sum(:amount) + tips_to_pay.sum(&:amount) end def has_undecided_tips? @@ -197,4 +199,10 @@ def has_undecided_tips? def commit_url(commit) "https://github.com/#{full_name}/commit/#{commit}" end + + def check_tips_to_pay_against_avaiable_amount + if amount_to_pay > available_amount + raise "Not enough funds to pay the pending tips on #{inspect} (#{amount_to_pay} > #{available_amount}" + end + end end diff --git a/app/models/sendmany.rb b/app/models/sendmany.rb index 06b7b240..ad917b58 100644 --- a/app/models/sendmany.rb +++ b/app/models/sendmany.rb @@ -2,11 +2,17 @@ class Sendmany < ActiveRecord::Base belongs_to :project has_many :tips + def total_amount + JSON.parse(data).values.map(&:to_d).sum if data + end + def send_transaction return if txid || is_error update_attribute :is_error, true # it's a lock to prevent duplicates + raise "Not enough funds on Sendmany##{id}" if total_amount > project.available_amount + txid = PeercoinDaemon.instance.send_many(project.address_label, JSON.parse(data)) update_attribute :is_error, false diff --git a/app/models/tip.rb b/app/models/tip.rb index 228a43cb..6c02d44b 100644 --- a/app/models/tip.rb +++ b/app/models/tip.rb @@ -6,8 +6,14 @@ class Tip < ActiveRecord::Base validates :amount, numericality: {greater_or_equal_than: 0, allow_nil: true} scope :not_sent, -> { where(sendmany_id: nil) } + def not_sent? + sendmany_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? @@ -39,6 +45,9 @@ def non_refunded? where('users.bitcoin_address' => ['', nil]) } scope :with_address, -> { joins(:user).where('users.bitcoin_address IS NOT NULL AND users.bitcoin_address != ?', "") } + def with_address? + user.bitcoin_address.present? + end scope :decided, -> { where.not(amount: nil) } scope :undecided, -> { where(amount: nil) } @@ -50,6 +59,7 @@ def undecided? end + before_save :check_amount_against_project after_save :notify_user_if_just_decided @@ -87,4 +97,14 @@ def notify_user def notify_user_if_just_decided notify_user if amount_was.nil? and amount end + + def check_amount_against_project + if amount + available_amount = project.available_amount + available_amount -= amount_was if amount_was + if amount > available_amount + raise "Not enough funds on project to save #{inspect} (available: #{available_amount})" + end + end + end end From 15519dae12f7cdc367bc6fbc550b90cc084ba959 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Wed, 26 Mar 2014 08:07:08 +0100 Subject: [PATCH 080/372] warn about unsent transactions --- app/models/project.rb | 1 + app/models/sendmany.rb | 4 +++- app/views/projects/show.html.haml | 6 ++++++ app/views/tips/index.html.haml | 7 +++++-- 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/app/models/project.rb b/app/models/project.rb index 027aadfa..35b2320c 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -3,6 +3,7 @@ class Project < ActiveRecord::Base has_many :tips, inverse_of: :project accepts_nested_attributes_for :tips has_many :collaborators + has_many :sendmanies, inverse_of: :project has_one :tipping_policies_text, inverse_of: :project accepts_nested_attributes_for :tipping_policies_text diff --git a/app/models/sendmany.rb b/app/models/sendmany.rb index ad917b58..229f077b 100644 --- a/app/models/sendmany.rb +++ b/app/models/sendmany.rb @@ -1,7 +1,9 @@ class Sendmany < ActiveRecord::Base - belongs_to :project + belongs_to :project, inverse_of: :sendmanies has_many :tips + scope :error, -> { where(is_error: true) } + def total_amount JSON.parse(data).values.map(&:to_d).sum if data end diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index 3f5cfff4..7f03fbbc 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -49,6 +49,12 @@ %h4 Tips Paid = btc_human @project.tips_paid_amount + + - if (failed_sendmanies = @project.sendmanies.error).any? + .alert.alert-danger + %strong Some tip transactions failed. + A total of #{btc_human failed_sendmanies.map(&:tips).flatten.sum(&:amount)} may not have been sent. + - 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.) diff --git a/app/views/tips/index.html.haml b/app/views/tips/index.html.haml index 272ced74..969790c5 100644 --- a/app/views/tips/index.html.haml +++ b/app/views/tips/index.html.haml @@ -28,7 +28,7 @@ %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 + %td{class: tip.sendmany.try(:is_error?) ? "danger" : nil} - if tip.sendmany.nil? - if tip.refunded_at Refunded to project's deposit @@ -41,5 +41,8 @@ - else Waiting for withdrawal - else - = link_to tip.sendmany.txid, transaction_url(tip.sendmany.txid), target: :blank + - if tip.sendmany.is_error? + Transaction failed + - if tip.sendmany.txid.present? + = link_to tip.sendmany.txid, transaction_url(tip.sendmany.txid), target: :blank = paginate @tips From 68a5abe4d3f1bbc2f59e5c5d6e3af34b50ad461d Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Wed, 26 Mar 2014 08:35:10 +0100 Subject: [PATCH 081/372] raise an error if the tip is invalid --- app/models/project.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/project.rb b/app/models/project.rb index 35b2320c..44657ac6 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -115,7 +115,7 @@ def tip_for commit end # create tip - tip = tips.create({ + tip = tips.create!({ user: user, amount: amount, commit: commit.sha, From 189f1d9438bda74efb468997bcd9e5f76d11201b Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Wed, 26 Mar 2014 09:04:53 +0100 Subject: [PATCH 082/372] fixed sendmany with new tips_to_pay --- lib/bitcoin_tipper.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/bitcoin_tipper.rb b/lib/bitcoin_tipper.rb index ec4cfb15..bf136872 100644 --- a/lib/bitcoin_tipper.rb +++ b/lib/bitcoin_tipper.rb @@ -41,8 +41,8 @@ def self.create_sendmany Rails.logger.info "Creating sendmany" ActiveRecord::Base.transaction do Project.find_each do |project| - tips = project.tips_to_pay.readonly(false) - amount = tips.sum(:amount).to_d + tips = project.tips_to_pay + amount = tips.sum(&:amount).to_d if amount > CONFIG["min_payout"].to_d * PeercoinBalanceUpdater::COIN sendmany = Sendmany.create(project_id: project.id) outs = Hash.new { 0.to_d } From 1e8aa850a6c59583ee0f941a2b0783b4211852bb Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Wed, 26 Mar 2014 12:41:23 +0100 Subject: [PATCH 083/372] disabled check on project, those with negative balance fails --- app/models/project.rb | 8 -------- 1 file changed, 8 deletions(-) diff --git a/app/models/project.rb b/app/models/project.rb index 44657ac6..aceb9ad1 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -11,8 +11,6 @@ class Project < ActiveRecord::Base validates :full_name, uniqueness: true, presence: true validates :github_id, uniqueness: true, presence: true - before_save :check_tips_to_pay_against_avaiable_amount - def update_github_info repo self.github_id = repo.id self.name = repo.name @@ -200,10 +198,4 @@ def has_undecided_tips? def commit_url(commit) "https://github.com/#{full_name}/commit/#{commit}" end - - def check_tips_to_pay_against_avaiable_amount - if amount_to_pay > available_amount - raise "Not enough funds to pay the pending tips on #{inspect} (#{amount_to_pay} > #{available_amount}" - end - end end From efea8ae7b6c8a81569ddbc8bf018b61d47352721 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sat, 29 Mar 2014 00:03:04 +0100 Subject: [PATCH 084/372] do not display failed sendmanies if the amount is 0 --- app/views/projects/show.html.haml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index 7f03fbbc..9117686c 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -50,10 +50,10 @@ %h4 Tips Paid = btc_human @project.tips_paid_amount - - if (failed_sendmanies = @project.sendmanies.error).any? + - if (failed_sendmanies = @project.sendmanies.error).any? and (amount = failed_sendmanies.map(&:tips).flatten.sum(&:amount)) > 0 .alert.alert-danger %strong Some tip transactions failed. - A total of #{btc_human failed_sendmanies.map(&:tips).flatten.sum(&:amount)} may not have been sent. + A total of #{btc_human amount} may not have been sent. - 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.) From 14a4e364ecece11c80e55263c93d45ed92e2c38e Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sat, 29 Mar 2014 00:04:42 +0100 Subject: [PATCH 085/372] no withdrawal if the tip is free --- app/views/tips/index.html.haml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/views/tips/index.html.haml b/app/views/tips/index.html.haml index 969790c5..1c95da3c 100644 --- a/app/views/tips/index.html.haml +++ b/app/views/tips/index.html.haml @@ -34,6 +34,7 @@ Refunded to project's deposit - elsif tip.undecided? The amount of the tip has not been decided yet + - elsif tip.free? - elsif tip.user.bitcoin_address.blank? User didn't specify withdrawal address - elsif tip.project.amount_to_pay < CONFIG["min_payout"].to_d * PeercoinBalanceUpdater::COIN From 18715643bbf2e2ea81f917290bd6fa5bd511df92 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 30 Mar 2014 19:31:51 +0200 Subject: [PATCH 086/372] give to project feature --- features/donate_to_project.feature | 14 ++++++++++++++ features/step_definitions/web.rb | 14 ++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 features/donate_to_project.feature diff --git a/features/donate_to_project.feature b/features/donate_to_project.feature new file mode 100644 index 00000000..ede5fd1c --- /dev/null +++ b/features/donate_to_project.feature @@ -0,0 +1,14 @@ +Feature: A visitor can donate to a project + Scenario: A visitor sends coins to a project + Given a project + And our fee is "0.01" + + When I visit the project page + Then I should see the project donation address + + Given there's a new incoming transaction of "50" on the project account + And the project balance is updated + + When I visit the project page + Then I should see the project balance is "49.5" + diff --git a/features/step_definitions/web.rb b/features/step_definitions/web.rb index 0d86695b..e58ca113 100644 --- a/features/step_definitions/web.rb +++ b/features/step_definitions/web.rb @@ -46,3 +46,17 @@ fill_in arg1, with: string end +When(/^I visit the project page$/) do + visit project_path(@project) +end + +Then(/^I should see the project donation address$/) do + address = @project.bitcoin_address + address.should_not be_blank + page.should have_content(address) +end + +Then(/^I should see the project balance is "(.*?)"$/) do |arg1| + page.should have_content("Balance #{arg1}") +end + From 7e5250fea25526dd2592916b3d65a0a5e7369467 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Mon, 31 Mar 2014 21:06:56 +0200 Subject: [PATCH 087/372] log rpc commands --- lib/peercoin_daemon.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/peercoin_daemon.rb b/lib/peercoin_daemon.rb index d78ce6d3..4f785403 100644 --- a/lib/peercoin_daemon.rb +++ b/lib/peercoin_daemon.rb @@ -32,6 +32,7 @@ def rpc(command, *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) From b69e0455805efcd598c32efffdda3168c952c426 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 30 Mar 2014 17:30:26 +0200 Subject: [PATCH 088/372] enable cold storage transfers --- app/models/cold_storage_transfer.rb | 7 ++ app/models/project.rb | 12 +++ ...330165138_create_cold_storage_transfers.rb | 13 +++ ...d_storage_withdrawal_address_to_project.rb | 5 ++ db/schema.rb | 19 +++- features/cold_storage.feature | 51 +++++++++++ features/step_definitions/cold_storage.rb | 72 ++++++++++++++++ features/step_definitions/common.rb | 2 +- features/support/peercoin_daemon_mock.rb | 64 ++++++++++++++ lib/peercoin_balance_updater.rb | 86 ++++++++++++++++--- 10 files changed, 315 insertions(+), 16 deletions(-) create mode 100644 app/models/cold_storage_transfer.rb create mode 100644 db/migrate/20140330165138_create_cold_storage_transfers.rb create mode 100644 db/migrate/20140401174927_add_cold_storage_withdrawal_address_to_project.rb create mode 100644 features/cold_storage.feature create mode 100644 features/step_definitions/cold_storage.rb create mode 100644 features/support/peercoin_daemon_mock.rb 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/project.rb b/app/models/project.rb index aceb9ad1..b4a55c2b 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -5,6 +5,8 @@ class Project < ActiveRecord::Base has_many :collaborators has_many :sendmanies, inverse_of: :project + has_many :cold_storage_transfers + has_one :tipping_policies_text, inverse_of: :project accepts_nested_attributes_for :tipping_policies_text @@ -198,4 +200,14 @@ def has_undecided_tips? 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 = CONFIG["cold_storage"].try(:[], "addresses").try(:first) + raise "No cold storage address" if address.blank? + PeercoinDaemon.instance.send_many(address_label, {address => amount.to_f}) + 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/schema.rb b/db/schema.rb index a9abc408..34ab74da 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,19 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20140323173320) do +ActiveRecord::Schema.define(version: 20140401174927) do + + create_table "cold_storage_transfers", force: true do |t| + t.integer "project_id" + t.integer "amount", limit: 8 + t.string "address" + t.string "txid" + t.integer "confirmations" + t.datetime "created_at" + t.datetime "updated_at" + end + + add_index "cold_storage_transfers", ["project_id"], name: "index_cold_storage_transfers_on_project_id" create_table "collaborators", force: true do |t| t.integer "project_id" @@ -48,10 +60,11 @@ t.integer "watchers_count" t.string "language" t.string "last_commit" - t.integer "available_amount_cache", limit: 8 + t.integer "available_amount_cache", limit: 8 t.string "github_id" t.string "address_label" - t.boolean "hold_tips", default: false + t.boolean "hold_tips", default: false + t.string "cold_storage_withdrawal_address" end add_index "projects", ["full_name"], name: "index_projects_on_full_name", unique: true diff --git a/features/cold_storage.feature b/features/cold_storage.feature new file mode 100644 index 00000000..10b986a5 --- /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" peercoins 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/step_definitions/cold_storage.rb b/features/step_definitions/cold_storage.rb new file mode 100644 index 00000000..875043a6 --- /dev/null +++ b/features/step_definitions/cold_storage.rb @@ -0,0 +1,72 @@ +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| + PeercoinDaemon.instance.add_transaction(account: @project.address_label, amount: arg1.to_d) +end + +When(/^there's a new incoming transaction of "(.*?)" to address "(.*?)" on the project account$/) do |arg1, arg2| + PeercoinDaemon.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| + PeercoinDaemon.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| + PeercoinDaemon.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| + PeercoinDaemon.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 updated$/) do + PeercoinBalanceUpdater.work +end + +Then(/^updating the project balance should raise an error$/) do + expect { PeercoinBalanceUpdater.work }.to raise_error +end + +Then(/^the project balance should be "(.*?)"$/) do |arg1| + (@project.reload.available_amount.to_d / PeercoinBalanceUpdater::COIN).should eq(arg1.to_d) +end + +Then(/^the project amount in cold storage should be "(.*?)"$/) do |arg1| + (@project.reload.cold_storage_amount / PeercoinBalanceUpdater::COIN).should eq(arg1.to_d) +end + +When(/^"(.*?)" peercoins of the project funds are sent to cold storage$/) do |arg1| + @project.send_to_cold_storage!((arg1.to_d * PeercoinBalanceUpdater::COIN).to_i) +end + +Then(/^there should be an outgoing transaction of "(.*?)" to address "(.*?)" on the project account$/) do |arg1, arg2| + transactions = PeercoinDaemon.instance.list_transactions(@project.address_label) + transactions.map { |t| t["category"] }.should eq(["send"]) + transactions.map { |t| t["address"] }.should eq([arg2]) + transactions.map { |t| -t["amount"].to_d / PeercoinBalanceUpdater::COIN }.should 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 + @project.reload.cold_storage_withdrawal_address.should_not be_blank +end + +Then(/^the project cold storage withdrawal address should be linked to its account$/) do + PeercoinDaemon.instance.get_addresses_by_account(@project.address_label).should include(@project.reload.cold_storage_withdrawal_address) +end + diff --git a/features/step_definitions/common.rb b/features/step_definitions/common.rb index a3b06567..bbf0a0c3 100644 --- a/features/step_definitions/common.rb +++ b/features/step_definitions/common.rb @@ -19,7 +19,7 @@ end Given(/^a project$/) do - @project = Project.create!(full_name: "example/test", github_id: 123, bitcoin_address: 'mq4NtnmQoQoPfNWEPbhSvxvncgtGo6L8WY') + @project = Project.create!(full_name: "example/test", github_id: 123, bitcoin_address: 'mq4NtnmQoQoPfNWEPbhSvxvncgtGo6L8WY', address_label: "example_project_account") end Given(/^a project "(.*?)"$/) do |arg1| diff --git a/features/support/peercoin_daemon_mock.rb b/features/support/peercoin_daemon_mock.rb new file mode 100644 index 00000000..da9a65a6 --- /dev/null +++ b/features/support/peercoin_daemon_mock.rb @@ -0,0 +1,64 @@ +class PeercoinDaemonMock + 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| t["account"] == account }[from, count] + 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 +end + +Before do + PeercoinDaemon.instance_eval do + @peercoin_daemon = PeercoinDaemonMock.new + end +end diff --git a/lib/peercoin_balance_updater.rb b/lib/peercoin_balance_updater.rb index 1cd6a87b..b138519a 100644 --- a/lib/peercoin_balance_updater.rb +++ b/lib/peercoin_balance_updater.rb @@ -5,29 +5,91 @@ def self.work Project.all.each do |project| start = 0 count = 10 + + raise "Project without address label: #{project.inspect}" if project.address_label.blank? + + if project.cold_storage_withdrawal_address.blank? + new_address = PeercoinDaemon.instance.get_new_address(project.address_label) + project.update!(cold_storage_withdrawal_address: new_address) + end + loop do transactions = PeercoinDaemon.instance.list_transactions(project.address_label, count, start) break if transactions.empty? transactions.each do |transaction| - if (transaction["category"] == "send") || Sendmany.find_by_txid(transaction["txid"]) + txid = transaction["txid"] + confirmations = transaction["confirmations"] + category = transaction["category"] + + next if Sendmany.where(txid: txid).any? + + if deposit = Deposit.find_by_txid(txid) + deposit.update_attribute(:confirmations, confirmations) + next + end + + if cold_storage_transfer = ColdStorageTransfer.find_by_txid(txid) + cold_storage_transfer.update_attribute(:confirmations, confirmations) + 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 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 deposit = Deposit.find_by_txid(transaction["txid"]) - deposit.update_attribute(:confirmations, transaction["confirmations"]) + 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, + ) + next + end + + if category == "receive" + if address != project.bitcoin_address + 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, + ) next end - deposit = Deposit.create({ - project_id: project.id, - txid: transaction["txid"], - confirmations: transaction["confirmations"], - amount: (transaction["amount"].to_d * COIN).to_i, - duration: 30.days.to_i, - paid_out: 0, - paid_out_at: Time.now - }) + raise "Unexpected transaction: #{transaction.inspect}" end break if transactions.size < count From 6ff6c6350ce53f53fe944b08a7d9c8a535cdff94 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Tue, 1 Apr 2014 20:58:42 +0200 Subject: [PATCH 089/372] moved COIN to root object --- app/helpers/application_helper.rb | 2 +- app/views/tips/index.html.haml | 2 +- app/views/users/show.html.haml | 2 +- config/application.rb | 2 ++ features/step_definitions/cold_storage.rb | 8 ++++---- features/step_definitions/common.rb | 4 ++-- lib/bitcoin_tipper.rb | 4 ++-- lib/peercoin_balance_updater.rb | 2 -- 8 files changed, 13 insertions(+), 13 deletions(-) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index ff1b6ea9..84f6aecb 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -10,7 +10,7 @@ def btc_human amount, options = {} end def to_btc satoshies - satoshies.to_d / PeercoinBalanceUpdater::COIN if satoshies + satoshies.to_d / COIN if satoshies end def transaction_url(txid) diff --git a/app/views/tips/index.html.haml b/app/views/tips/index.html.haml index 1c95da3c..cbe6636a 100644 --- a/app/views/tips/index.html.haml +++ b/app/views/tips/index.html.haml @@ -37,7 +37,7 @@ - elsif tip.free? - elsif tip.user.bitcoin_address.blank? User didn't specify withdrawal address - - elsif tip.project.amount_to_pay < CONFIG["min_payout"].to_d * PeercoinBalanceUpdater::COIN + - elsif tip.project.amount_to_pay < CONFIG["min_payout"].to_d * COIN The amount of tips for this project is below withdrawal threshold - else Waiting for withdrawal diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index c3790b69..1c751f2b 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -6,7 +6,7 @@ %p %small You will get the money of your tips when the project they belong hits the threshold of - = btc_human CONFIG["min_payout"].to_d * PeercoinBalanceUpdater::COIN + = btc_human CONFIG["min_payout"].to_d * COIN %p %strong E-mail %p= @user.email diff --git a/config/application.rb b/config/application.rb index 6495eac6..e06ec41b 100644 --- a/config/application.rb +++ b/config/application.rb @@ -8,6 +8,8 @@ CONFIG ||= YAML::load(File.open("config/config.yml")) +COIN = 1000000 # ppcoin/src/util.h + module T4c class Application < Rails::Application diff --git a/features/step_definitions/cold_storage.rb b/features/step_definitions/cold_storage.rb index 875043a6..490c1614 100644 --- a/features/step_definitions/cold_storage.rb +++ b/features/step_definitions/cold_storage.rb @@ -40,22 +40,22 @@ end Then(/^the project balance should be "(.*?)"$/) do |arg1| - (@project.reload.available_amount.to_d / PeercoinBalanceUpdater::COIN).should eq(arg1.to_d) + (@project.reload.available_amount.to_d / COIN).should eq(arg1.to_d) end Then(/^the project amount in cold storage should be "(.*?)"$/) do |arg1| - (@project.reload.cold_storage_amount / PeercoinBalanceUpdater::COIN).should eq(arg1.to_d) + (@project.reload.cold_storage_amount / COIN).should eq(arg1.to_d) end When(/^"(.*?)" peercoins of the project funds are sent to cold storage$/) do |arg1| - @project.send_to_cold_storage!((arg1.to_d * PeercoinBalanceUpdater::COIN).to_i) + @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 = PeercoinDaemon.instance.list_transactions(@project.address_label) transactions.map { |t| t["category"] }.should eq(["send"]) transactions.map { |t| t["address"] }.should eq([arg2]) - transactions.map { |t| -t["amount"].to_d / PeercoinBalanceUpdater::COIN }.should eq([arg1.to_d]) + transactions.map { |t| -t["amount"].to_d / COIN }.should eq([arg1.to_d]) end Given(/^the project has no cold storage withdrawal address$/) do diff --git a/features/step_definitions/common.rb b/features/step_definitions/common.rb index bbf0a0c3..095f7247 100644 --- a/features/step_definitions/common.rb +++ b/features/step_definitions/common.rb @@ -27,7 +27,7 @@ end Given(/^a deposit of "(.*?)"$/) do |arg1| - Deposit.create!(project: @project, amount: arg1.to_d * PeercoinBalanceUpdater::COIN, confirmations: 1) + Deposit.create!(project: @project, amount: arg1.to_d * COIN, confirmations: 1) end Given(/^the last known commit is "(.*?)"$/) do |arg1| @@ -91,7 +91,7 @@ def find_new_commit(id) Then(/^there should be a tip of "(.*?)" for commit "(.*?)"$/) do |arg1, arg2| amount = Tip.find_by(commit: arg2).amount amount.should_not be_nil - (amount.to_d / PeercoinBalanceUpdater::COIN).should eq(arg1.to_d) + (amount.to_d / COIN).should eq(arg1.to_d) end Then(/^the tip amount for commit "(.*?)" should be undecided$/) do |arg1| diff --git a/lib/bitcoin_tipper.rb b/lib/bitcoin_tipper.rb index bf136872..1d080362 100644 --- a/lib/bitcoin_tipper.rb +++ b/lib/bitcoin_tipper.rb @@ -43,12 +43,12 @@ def self.create_sendmany Project.find_each do |project| tips = project.tips_to_pay amount = tips.sum(&:amount).to_d - if amount > CONFIG["min_payout"].to_d * PeercoinBalanceUpdater::COIN + if amount > CONFIG["min_payout"].to_d * COIN sendmany = Sendmany.create(project_id: project.id) outs = Hash.new { 0.to_d } tips.each do |tip| tip.update_attribute :sendmany_id, sendmany.id - outs[tip.user.bitcoin_address] += tip.amount.to_d / PeercoinBalanceUpdater::COIN + outs[tip.user.bitcoin_address] += tip.amount.to_d / COIN end sendmany.update_attribute :data, outs.to_json Rails.logger.info " #{sendmany.inspect}" diff --git a/lib/peercoin_balance_updater.rb b/lib/peercoin_balance_updater.rb index b138519a..6d114380 100644 --- a/lib/peercoin_balance_updater.rb +++ b/lib/peercoin_balance_updater.rb @@ -1,6 +1,4 @@ module PeercoinBalanceUpdater - COIN = 1000000 # ppcoin/src/util.h - def self.work Project.all.each do |project| start = 0 From e235a4f8cfec068ffc530396a30cba385f47ad37 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Wed, 2 Apr 2014 13:15:37 +0200 Subject: [PATCH 090/372] hide disabled projects from list and allow show with a warning --- app/controllers/projects_controller.rb | 10 +++--- app/models/project.rb | 5 ++- app/models/sendmany.rb | 1 + app/views/projects/show.html.haml | 32 ++++++++++++------- .../20140402111051_add_disabled_to_project.rb | 5 +++ db/schema.rb | 3 +- lib/bitcoin_tipper.rb | 6 ++-- lib/peercoin_balance_updater.rb | 2 +- 8 files changed, 42 insertions(+), 22 deletions(-) create mode 100644 db/migrate/20140402111051_add_disabled_to_project.rb diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index ca4b8a30..9c60a912 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -2,7 +2,7 @@ class ProjectsController < ApplicationController 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 @@ -15,12 +15,12 @@ def show end def edit - @project = Project.find params[:id] + @project = Project.enabled.find params[:id] authorize! :update, @project end def update - @project = Project.find params[:id] + @project = Project.enabled.find params[:id] authorize! :update, @project @project.attributes = project_params if @project.tipping_policies_text.try(:text_changed?) @@ -34,7 +34,7 @@ def update end def decide_tip_amounts - @project = Project.find params[:id] + @project = Project.enabled.find params[:id] authorize! :decide_tip_amounts, @project if request.patch? @project.available_amount # preload anything required to get the amount, otherwise it's loaded during the assignation and there are undesirable consequences @@ -51,7 +51,7 @@ def decide_tip_amounts end def qrcode - @project = Project.find params[:id] + @project = Project.enabled.find params[:id] respond_to do |format| format.svg { render :qrcode => @project.bitcoin_address, level: :l, unit: 4 } end diff --git a/app/models/project.rb b/app/models/project.rb index b4a55c2b..f25d1073 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -13,6 +13,9 @@ class Project < ActiveRecord::Base validates :full_name, uniqueness: true, presence: true validates :github_id, uniqueness: true, presence: true + scope :enabled, -> { where(disabled: false) } + scope :disabled, -> { where(disabled: true) } + def update_github_info repo self.github_id = repo.id self.name = repo.name @@ -150,7 +153,7 @@ def next_tip_amount end def self.update_cache - find_each do |project| + enabled.find_each do |project| project.update available_amount_cache: project.available_amount end end diff --git a/app/models/sendmany.rb b/app/models/sendmany.rb index 229f077b..4fbd0d37 100644 --- a/app/models/sendmany.rb +++ b/app/models/sendmany.rb @@ -10,6 +10,7 @@ def total_amount def send_transaction return if txid || is_error + return if project.disabled? update_attribute :is_error, true # it's a lock to prevent duplicates diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index 9117686c..4f16795b 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -14,17 +14,27 @@ .row .col-md-4 - .panel.panel-default - .panel-heading - %h4.panel-title - Project Sponsors - .panel-body.text-center - %p To give to this project, send peercoins to this address: - %p - = @project.bitcoin_address - %p - = image_tag qrcode_project_path(@project, format: :svg), alt: @project.bitcoin_address, class: "project qrcode" - %p #{100-(CONFIG["our_fee"]*100).round}% of deposited funds will be used to tip for new commits. + - if @project.disabled? + .panel.panel-danger + .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. + - else + .panel.panel-default + .panel-heading + %h4.panel-title + Project Sponsors + .panel-body.text-center + %p To give to this project, send peercoins to this address: + %p + = @project.bitcoin_address + %p + = image_tag qrcode_project_path(@project, format: :svg), alt: @project.bitcoin_address, class: "project qrcode" + %p #{100-(CONFIG["our_fee"]*100).round}% of deposited funds will be used to tip for new commits. .col-md-8 - unless @project.description.blank? .well.well-sm= @project.description 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/schema.rb b/db/schema.rb index 34ab74da..892b026e 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20140401174927) do +ActiveRecord::Schema.define(version: 20140402111051) do create_table "cold_storage_transfers", force: true do |t| t.integer "project_id" @@ -65,6 +65,7 @@ t.string "address_label" t.boolean "hold_tips", default: false t.string "cold_storage_withdrawal_address" + t.boolean "disabled", default: false end add_index "projects", ["full_name"], name: "index_projects_on_full_name", unique: true diff --git a/lib/bitcoin_tipper.rb b/lib/bitcoin_tipper.rb index 1d080362..5b04e49c 100644 --- a/lib/bitcoin_tipper.rb +++ b/lib/bitcoin_tipper.rb @@ -7,7 +7,7 @@ def self.work_forever def self.work Rails.logger.info "Traversing projects..." - Project.find_each do |project| + Project.enabled.find_each do |project| if project.available_amount > 0 Rails.logger.info " Project #{project.id} #{project.full_name}" project.tip_commits @@ -15,7 +15,7 @@ def self.work end Rails.logger.info "Updating projects info..." - Project.all.each do |project| + Project.enabled.each do |project| Rails.logger.info " Project #{project.id} #{project.full_name}" project.update_info end @@ -40,7 +40,7 @@ def self.work def self.create_sendmany Rails.logger.info "Creating sendmany" ActiveRecord::Base.transaction do - Project.find_each do |project| + 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 diff --git a/lib/peercoin_balance_updater.rb b/lib/peercoin_balance_updater.rb index 6d114380..0d2f3fbe 100644 --- a/lib/peercoin_balance_updater.rb +++ b/lib/peercoin_balance_updater.rb @@ -1,6 +1,6 @@ module PeercoinBalanceUpdater def self.work - Project.all.each do |project| + Project.enabled.each do |project| start = 0 count = 10 From fb5587ee4560f28ab982c598386a7d169656be9a Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Thu, 3 Apr 2014 08:33:47 +0200 Subject: [PATCH 091/372] save account balance of each project --- db/migrate/20140403062826_add_account_balance_to_project.rb | 5 +++++ db/schema.rb | 3 ++- lib/peercoin_balance_updater.rb | 2 ++ lib/peercoin_daemon.rb | 4 ++++ 4 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 db/migrate/20140403062826_add_account_balance_to_project.rb 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/schema.rb b/db/schema.rb index 892b026e..1ed44b6f 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20140402111051) do +ActiveRecord::Schema.define(version: 20140403062826) do create_table "cold_storage_transfers", force: true do |t| t.integer "project_id" @@ -66,6 +66,7 @@ t.boolean "hold_tips", default: false t.string "cold_storage_withdrawal_address" t.boolean "disabled", default: false + t.integer "account_balance", limit: 8 end add_index "projects", ["full_name"], name: "index_projects_on_full_name", unique: true diff --git a/lib/peercoin_balance_updater.rb b/lib/peercoin_balance_updater.rb index 0d2f3fbe..89484ee4 100644 --- a/lib/peercoin_balance_updater.rb +++ b/lib/peercoin_balance_updater.rb @@ -11,6 +11,8 @@ def self.work project.update!(cold_storage_withdrawal_address: new_address) end + project.update(account_balance: (PeercoinDaemon.instance.get_balance(project.address_label) * COIN).to_i) + loop do transactions = PeercoinDaemon.instance.list_transactions(project.address_label, count, start) break if transactions.empty? diff --git a/lib/peercoin_daemon.rb b/lib/peercoin_daemon.rb index 4f785403..c1187771 100644 --- a/lib/peercoin_daemon.rb +++ b/lib/peercoin_daemon.rb @@ -57,4 +57,8 @@ def send_many(account, recipients, minconf = 1) end rpc('sendmany', account, recipients, minconf) end + + def get_balance(account = "") + rpc('getbalance', account).to_f + end end From 3230c99007c84303e8da6458a4799594dd2397fe Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Thu, 3 Apr 2014 08:45:18 +0200 Subject: [PATCH 092/372] added audit page --- app/assets/stylesheets/home.css.scss | 4 ++++ app/helpers/application_helper.rb | 4 ++++ app/views/home/audit.html.haml | 36 ++++++++++++++++++++++++++++ config/routes.rb | 2 ++ 4 files changed, 46 insertions(+) create mode 100644 app/views/home/audit.html.haml diff --git a/app/assets/stylesheets/home.css.scss b/app/assets/stylesheets/home.css.scss index f0ddc684..af3318d2 100644 --- a/app/assets/stylesheets/home.css.scss +++ b/app/assets/stylesheets/home.css.scss @@ -1,3 +1,7 @@ // 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/ + +td.money, th.money { + text-align: right; +} diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 84f6aecb..9681b625 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -17,6 +17,10 @@ def transaction_url(txid) "http://bkchain.org/ppc/tx/#{txid}" end + def address_url(address) + "http://bkchain.org/ppc/address/#{address}" + end + def commit_tag(sha1) content_tag(:span, truncate(sha1, length: 10, omission: ""), class: "commit-sha") end diff --git a/app/views/home/audit.html.haml b/app/views/home/audit.html.haml new file mode 100644 index 00000000..bb28eae7 --- /dev/null +++ b/app/views/home/audit.html.haml @@ -0,0 +1,36 @@ +%h1 Audit +- projects = Project.order(disabled: :asc, account_balance: :desc).includes(:cold_storage_transfers) +%p + %table.table + %thead + %tr + %th Project + %th Address + %th.money Website balance + %th.money Amount in cold storage + %th.money Peercoin balance + %tbody + - projects.each do |project| + %tr + %td + %strong= link_to project.full_name, project + %td= link_to project.bitcoin_address, address_url(project.bitcoin_address) + %td.money= btc_human project.available_amount_cache + %td.money= btc_human project.cold_storage_amount + %td.money= btc_human project.account_balance + %tr + %th{colspan: 2} Total + %td.money= btc_human projects.map(&:available_amount_cache).compact.sum + %td.money= btc_human projects.map(&:cold_storage_amount).compact.sum + %td.money= btc_human projects.map(&:account_balance).compact.sum + +%h2 Cold storage +%p + %table.table + %thead + %tr + %th Address + %tbody + - (CONFIG["cold_storage"].try(:[], "addresses") || []).each do |address| + %tr + %td= link_to address, address_url(address) diff --git a/config/routes.rb b/config/routes.rb index 6ee4ad84..c0f577cd 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -2,6 +2,8 @@ root 'home#index' + get 'audit' => 'home#audit' + resources :users, :only => [:show, :update, :index] do collection do get :login From 62fe137ed1c29d3361c1e080cc71ce66807de545 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Thu, 3 Apr 2014 09:36:50 +0200 Subject: [PATCH 093/372] display difference in audit --- app/views/home/audit.html.haml | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/app/views/home/audit.html.haml b/app/views/home/audit.html.haml index bb28eae7..b70fe4f0 100644 --- a/app/views/home/audit.html.haml +++ b/app/views/home/audit.html.haml @@ -9,20 +9,23 @@ %th.money Website balance %th.money Amount in cold storage %th.money Peercoin balance + %th.money Difference %tbody - projects.each do |project| %tr %td %strong= link_to project.full_name, project %td= link_to project.bitcoin_address, address_url(project.bitcoin_address) - %td.money= btc_human project.available_amount_cache - %td.money= btc_human project.cold_storage_amount - %td.money= btc_human project.account_balance + %td.money= btc_human(available = project.available_amount_cache) + %td.money= btc_human(cold = project.cold_storage_amount) + %td.money= btc_human(account = project.account_balance) + %td.money= btc_human(account + cold - available) if account and cold and available %tr %th{colspan: 2} Total - %td.money= btc_human projects.map(&:available_amount_cache).compact.sum - %td.money= btc_human projects.map(&:cold_storage_amount).compact.sum - %td.money= btc_human projects.map(&:account_balance).compact.sum + %td.money= btc_human(available = projects.map(&:available_amount_cache).compact.sum) + %td.money= btc_human(cold = projects.map(&:cold_storage_amount).compact.sum) + %td.money= btc_human(account = projects.map(&:account_balance).compact.sum) + %td.money= btc_human(account + cold - available) %h2 Cold storage %p From 405f08093f607e5a76a62a4eaf2eb4baa84b9616 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Thu, 3 Apr 2014 09:39:54 +0200 Subject: [PATCH 094/372] update account balance of disabled projects --- lib/peercoin_balance_updater.rb | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/peercoin_balance_updater.rb b/lib/peercoin_balance_updater.rb index 89484ee4..cfa1ada2 100644 --- a/lib/peercoin_balance_updater.rb +++ b/lib/peercoin_balance_updater.rb @@ -1,18 +1,20 @@ module PeercoinBalanceUpdater def self.work - Project.enabled.each do |project| + Project.all.each do |project| start = 0 count = 10 raise "Project without address label: #{project.inspect}" if project.address_label.blank? + project.update(account_balance: (PeercoinDaemon.instance.get_balance(project.address_label) * COIN).to_i) + + next if project.disabled? + if project.cold_storage_withdrawal_address.blank? new_address = PeercoinDaemon.instance.get_new_address(project.address_label) project.update!(cold_storage_withdrawal_address: new_address) end - project.update(account_balance: (PeercoinDaemon.instance.get_balance(project.address_label) * COIN).to_i) - loop do transactions = PeercoinDaemon.instance.list_transactions(project.address_label, count, start) break if transactions.empty? From 0072ac06edc5bf89a54f5127e82b8791fe6f59a6 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Thu, 3 Apr 2014 09:46:31 +0200 Subject: [PATCH 095/372] added audit column explanations --- app/views/home/audit.html.haml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/app/views/home/audit.html.haml b/app/views/home/audit.html.haml index b70fe4f0..6cec3369 100644 --- a/app/views/home/audit.html.haml +++ b/app/views/home/audit.html.haml @@ -6,10 +6,14 @@ %tr %th Project %th Address - %th.money Website balance - %th.money Amount in cold storage - %th.money Peercoin balance - %th.money Difference + %th.money + %abbr{title: "The balance displayed on the website. The amount of the tips is based on this value."} Available balance + %th.money + %abbr{title: "Amount currently in cold storage (see below)."} Amount in cold storage + %th.money + %abbr{title: "The balance of the project account as reported by the Peercoin daemon."} Account balance + %th.money + %abbr{title: "Account balance + cold storage - available balance. If this is negative there is a problem."} Difference %tbody - projects.each do |project| %tr From 4545e90958c4b65003682cb809141cc404fee1a1 Mon Sep 17 00:00:00 2001 From: Aleksandr Zykov Date: Wed, 2 Apr 2014 15:13:49 +0800 Subject: [PATCH 096/372] fixed too long commit_message error and loading colloborators from organization owners list --- app/models/project.rb | 3 ++- db/migrate/20140402071216_change_commit_message_type.rb | 8 ++++++++ db/schema.rb | 2 +- 3 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 db/migrate/20140402071216_change_commit_message_type.rb diff --git a/app/models/project.rb b/app/models/project.rb index f25d1073..a4781365 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -173,7 +173,8 @@ def github_collaborators client = Octokit::Client.new \ :client_id => CONFIG['github']['key'], :client_secret => CONFIG['github']['secret'] - client.get("/repos/#{full_name}/collaborators") + client.get("/repos/#{full_name}/collaborators") + + (client.get("/orgs/#{full_name.split('/').first}/members") rescue []) end def update_info diff --git a/db/migrate/20140402071216_change_commit_message_type.rb b/db/migrate/20140402071216_change_commit_message_type.rb new file mode 100644 index 00000000..546a1090 --- /dev/null +++ b/db/migrate/20140402071216_change_commit_message_type.rb @@ -0,0 +1,8 @@ +class ChangeCommitMessageType < ActiveRecord::Migration + def up + change_column :tips, :commit_message, :text, limit: nil + end + def down + change_column :tips, :commit_message, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index 1ed44b6f..d3b2fd70 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -104,7 +104,7 @@ t.string "commit" t.integer "project_id" t.datetime "refunded_at" - t.string "commit_message" + t.text "commit_message" end add_index "tips", ["project_id"], name: "index_tips_on_project_id" From 13bd6b98770e9fab77c3c3bd9749ae5b969444ab Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sat, 5 Apr 2014 08:25:48 +0200 Subject: [PATCH 097/372] reverted change of commit message from string to text --- db/migrate/20140402071216_change_commit_message_type.rb | 8 -------- db/schema.rb | 2 +- 2 files changed, 1 insertion(+), 9 deletions(-) delete mode 100644 db/migrate/20140402071216_change_commit_message_type.rb diff --git a/db/migrate/20140402071216_change_commit_message_type.rb b/db/migrate/20140402071216_change_commit_message_type.rb deleted file mode 100644 index 546a1090..00000000 --- a/db/migrate/20140402071216_change_commit_message_type.rb +++ /dev/null @@ -1,8 +0,0 @@ -class ChangeCommitMessageType < ActiveRecord::Migration - def up - change_column :tips, :commit_message, :text, limit: nil - end - def down - change_column :tips, :commit_message, :string - end -end diff --git a/db/schema.rb b/db/schema.rb index d3b2fd70..1ed44b6f 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -104,7 +104,7 @@ t.string "commit" t.integer "project_id" t.datetime "refunded_at" - t.text "commit_message" + t.string "commit_message" end add_index "tips", ["project_id"], name: "index_tips_on_project_id" From 0ab01295ad1402c66e685cf363bd6ae712bc6db5 Mon Sep 17 00:00:00 2001 From: Aleksandr Zykov Date: Thu, 6 Mar 2014 10:24:58 +0700 Subject: [PATCH 098/372] typo --- app/controllers/users/omniauth_callbacks_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/users/omniauth_callbacks_controller.rb b/app/controllers/users/omniauth_callbacks_controller.rb index 4b828708..e96482be 100644 --- a/app/controllers/users/omniauth_callbacks_controller.rb +++ b/app/controllers/users/omniauth_callbacks_controller.rb @@ -15,7 +15,7 @@ def github :nickname => info['nickname'] ) else - set_flash_message(:error, :failure, kind: 'GitHub', reason: 'your promary email address should be verified.') + 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 From 483a3bbb55ff35d9b5d69d2108f26c07d109aae9 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sat, 5 Apr 2014 09:00:59 +0200 Subject: [PATCH 099/372] display total donated to each project --- app/views/home/audit.html.haml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/views/home/audit.html.haml b/app/views/home/audit.html.haml index 6cec3369..535b03f2 100644 --- a/app/views/home/audit.html.haml +++ b/app/views/home/audit.html.haml @@ -6,6 +6,8 @@ %tr %th Project %th Address + %th.money + %abbr{title: "Total amount that was donated to the project."} Donated %th.money %abbr{title: "The balance displayed on the website. The amount of the tips is based on this value."} Available balance %th.money @@ -20,12 +22,14 @@ %td %strong= link_to project.full_name, project %td= link_to project.bitcoin_address, address_url(project.bitcoin_address) + %td.money= btc_human(donated = project.deposits.map(&:amount).sum) %td.money= btc_human(available = project.available_amount_cache) %td.money= btc_human(cold = project.cold_storage_amount) %td.money= btc_human(account = project.account_balance) %td.money= btc_human(account + cold - available) if account and cold and available %tr %th{colspan: 2} Total + %td.money= btc_human(donated = projects.map(&:deposits).flatten.map(&:amount).sum) %td.money= btc_human(available = projects.map(&:available_amount_cache).compact.sum) %td.money= btc_human(cold = projects.map(&:cold_storage_amount).compact.sum) %td.money= btc_human(account = projects.map(&:account_balance).compact.sum) From eee8dfe7d729be671daedd954db0f8a9bfd32c28 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sat, 5 Apr 2014 09:03:20 +0200 Subject: [PATCH 100/372] display less decimals --- app/helpers/application_helper.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 9681b625..367fff12 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -3,7 +3,8 @@ def btc_human amount, options = {} return nil unless amount nobr = options.has_key?(:nobr) ? options[:nobr] : true currency = options[:currency] || false - btc = "%.8f PPC" % to_btc(amount) + precision = options[:precision] || 2 + btc = "%.#{precision}f PPC" % to_btc(amount) btc = "#{btc}" if currency btc = "#{btc}" if nobr btc.html_safe From 4577e6bc89cc1c5cf36f3bb4226d69437df15427 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sat, 5 Apr 2014 09:16:00 +0200 Subject: [PATCH 101/372] display unsent tips and expected account balance --- app/views/home/audit.html.haml | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/app/views/home/audit.html.haml b/app/views/home/audit.html.haml index 535b03f2..744cb08b 100644 --- a/app/views/home/audit.html.haml +++ b/app/views/home/audit.html.haml @@ -10,12 +10,16 @@ %abbr{title: "Total amount that was donated to the project."} Donated %th.money %abbr{title: "The balance displayed on the website. The amount of the tips is based on this value."} Available balance + %th.money + %abbr{title: "Tips attributed but not sent because they have not been processed yet or the author did not set an address."} Tips not sent %th.money %abbr{title: "Amount currently in cold storage (see below)."} Amount in cold storage + %th.money + %abbr{title: "Available balance + tips not sent - amount in cold storage."} 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: "Account balance + cold storage - available balance. If this is negative there is a problem."} Difference + %abbr{title: "Account balance - expected account balance. If this is negative there is a problem."} Difference %tbody - projects.each do |project| %tr @@ -24,16 +28,20 @@ %td= link_to project.bitcoin_address, address_url(project.bitcoin_address) %td.money= btc_human(donated = project.deposits.map(&:amount).sum) %td.money= btc_human(available = project.available_amount_cache) + %td.money= btc_human(not_sent = project.tips.select(&:not_sent?).map(&:amount).compact.sum) %td.money= btc_human(cold = project.cold_storage_amount) + %td.money= btc_human(expected = available + not_sent - cold) if available and not_sent and cold %td.money= btc_human(account = project.account_balance) - %td.money= btc_human(account + cold - available) if account and cold and available + %td.money= btc_human(account - expected) if account and expected %tr %th{colspan: 2} Total %td.money= btc_human(donated = projects.map(&:deposits).flatten.map(&:amount).sum) %td.money= btc_human(available = projects.map(&:available_amount_cache).compact.sum) + %td.money= btc_human(not_sent = projects.map(&:tips).flatten.select(&:not_sent?).map(&:amount).compact.sum) %td.money= btc_human(cold = projects.map(&:cold_storage_amount).compact.sum) + %td.money= btc_human(expected = available + not_sent - cold) if available and not_sent and cold %td.money= btc_human(account = projects.map(&:account_balance).compact.sum) - %td.money= btc_human(account + cold - available) + %td.money= btc_human(account - expected) if account and expected %h2 Cold storage %p From 3909e78a635bf2575ca8f54bc73fa7961152539d Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sat, 5 Apr 2014 09:30:39 +0200 Subject: [PATCH 102/372] repeat audit header --- app/views/home/audit.html.haml | 75 +++++++++++++++++----------------- 1 file changed, 38 insertions(+), 37 deletions(-) diff --git a/app/views/home/audit.html.haml b/app/views/home/audit.html.haml index 744cb08b..50f4e5b9 100644 --- a/app/views/home/audit.html.haml +++ b/app/views/home/audit.html.haml @@ -2,46 +2,47 @@ - projects = Project.order(disabled: :asc, account_balance: :desc).includes(:cold_storage_transfers) %p %table.table - %thead - %tr - %th Project - %th Address - %th.money - %abbr{title: "Total amount that was donated to the project."} Donated - %th.money - %abbr{title: "The balance displayed on the website. The amount of the tips is based on this value."} Available balance - %th.money - %abbr{title: "Tips attributed but not sent because they have not been processed yet or the author did not set an address."} Tips not sent - %th.money - %abbr{title: "Amount currently in cold storage (see below)."} Amount in cold storage - %th.money - %abbr{title: "Available balance + tips not sent - amount in cold storage."} 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: "Account balance - expected account balance. If this is negative there is a problem."} Difference - %tbody - - projects.each do |project| + - 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: "The balance displayed on the website. The amount of the tips is based on this value."} Available balance + %th.money + %abbr{title: "Tips attributed but not sent because they have not been processed yet or the author did not set an address."} Tips not sent + %th.money + %abbr{title: "Amount currently in cold storage (see below)."} Amount in cold storage + %th.money + %abbr{title: "Available balance + tips not sent - amount in cold storage."} 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: "Account balance - expected account balance. If this is negative there is a problem."} Difference + %tbody + - project_slice.each do |project| + %tr + %td + %strong= link_to project.full_name, project + %td= link_to project.bitcoin_address, address_url(project.bitcoin_address) + %td.money= btc_human(donated = project.deposits.map(&:amount).sum) + %td.money= btc_human(available = project.available_amount_cache) + %td.money= btc_human(not_sent = project.tips.select(&:not_sent?).map(&:amount).compact.sum) + %td.money= btc_human(cold = project.cold_storage_amount) + %td.money= btc_human(expected = available + not_sent - cold) if available and not_sent and cold + %td.money= btc_human(account = project.account_balance) + %td.money= btc_human(account - expected) if account and expected %tr - %td - %strong= link_to project.full_name, project - %td= link_to project.bitcoin_address, address_url(project.bitcoin_address) - %td.money= btc_human(donated = project.deposits.map(&:amount).sum) - %td.money= btc_human(available = project.available_amount_cache) - %td.money= btc_human(not_sent = project.tips.select(&:not_sent?).map(&:amount).compact.sum) - %td.money= btc_human(cold = project.cold_storage_amount) + %th{colspan: 2} Total + %td.money= btc_human(donated = projects.map(&:deposits).flatten.map(&:amount).sum) + %td.money= btc_human(available = projects.map(&:available_amount_cache).compact.sum) + %td.money= btc_human(not_sent = projects.map(&:tips).flatten.select(&:not_sent?).map(&:amount).compact.sum) + %td.money= btc_human(cold = projects.map(&:cold_storage_amount).compact.sum) %td.money= btc_human(expected = available + not_sent - cold) if available and not_sent and cold - %td.money= btc_human(account = project.account_balance) + %td.money= btc_human(account = projects.map(&:account_balance).compact.sum) %td.money= btc_human(account - expected) if account and expected - %tr - %th{colspan: 2} Total - %td.money= btc_human(donated = projects.map(&:deposits).flatten.map(&:amount).sum) - %td.money= btc_human(available = projects.map(&:available_amount_cache).compact.sum) - %td.money= btc_human(not_sent = projects.map(&:tips).flatten.select(&:not_sent?).map(&:amount).compact.sum) - %td.money= btc_human(cold = projects.map(&:cold_storage_amount).compact.sum) - %td.money= btc_human(expected = available + not_sent - cold) if available and not_sent and cold - %td.money= btc_human(account = projects.map(&:account_balance).compact.sum) - %td.money= btc_human(account - expected) if account and expected %h2 Cold storage %p From 74b094fb416ffe6cd671a66bec352ebb4bf76497 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sat, 5 Apr 2014 09:39:57 +0200 Subject: [PATCH 103/372] display multiple explorer links --- app/helpers/application_helper.rb | 13 +++++++++++-- app/views/home/audit.html.haml | 8 ++++++-- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 367fff12..48023df7 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -18,8 +18,17 @@ def transaction_url(txid) "http://bkchain.org/ppc/tx/#{txid}" end - def address_url(address) - "http://bkchain.org/ppc/address/#{address}" + def address_explorers + [:bkchain, :blockr, :cryptocoin] + end + + def address_url(address, explorer = address_explorers.first) + case explorer + when :blockr then "http://ppc.blockr.io/address/info/#{address}" + when :bkchain then "http://bkchain.org/ppc/address/#{address}" + when :cryptocoin then "http://ppc.cryptocoinexplorer.com/address/#{address}" + else raise "Unknown provider: #{provider.inspect}" + end end def commit_tag(sha1) diff --git a/app/views/home/audit.html.haml b/app/views/home/audit.html.haml index 50f4e5b9..60a60316 100644 --- a/app/views/home/audit.html.haml +++ b/app/views/home/audit.html.haml @@ -26,7 +26,9 @@ %tr %td %strong= link_to project.full_name, project - %td= link_to project.bitcoin_address, address_url(project.bitcoin_address) + %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(available = project.available_amount_cache) %td.money= btc_human(not_sent = project.tips.select(&:not_sent?).map(&:amount).compact.sum) @@ -53,4 +55,6 @@ %tbody - (CONFIG["cold_storage"].try(:[], "addresses") || []).each do |address| %tr - %td= link_to address, address_url(address) + %td + - address_explorers.each_with_index do |explorer, i| + = link_to "[#{i + 1}]", address_url(address, explorer) From 0675d8255a735ff28eb3531739fdb9bcec15a0bd Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sat, 5 Apr 2014 09:44:29 +0200 Subject: [PATCH 104/372] default precision of 0 on audit page --- app/helpers/application_helper.rb | 10 ++- app/views/home/audit.html.haml | 103 +++++++++++++++--------------- 2 files changed, 61 insertions(+), 52 deletions(-) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 48023df7..4e96e90d 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -3,13 +3,21 @@ def btc_human amount, options = {} return nil unless amount nobr = options.has_key?(:nobr) ? options[:nobr] : true currency = options[:currency] || false - precision = options[:precision] || 2 + precision = options[:precision] || @default_precision || 2 btc = "%.#{precision}f PPC" % to_btc(amount) btc = "#{btc}" if currency btc = "#{btc}" if nobr btc.html_safe end + def with_default_precision(precision) + @old_default_precisions ||= [] + @old_default_precisions << @default_precision + @default_precision = precision + yield + @default_precision = @old_default_precisions.pop + end + def to_btc satoshies satoshies.to_d / COIN if satoshies end diff --git a/app/views/home/audit.html.haml b/app/views/home/audit.html.haml index 60a60316..f48fa64c 100644 --- a/app/views/home/audit.html.haml +++ b/app/views/home/audit.html.haml @@ -1,60 +1,61 @@ %h1 Audit - projects = Project.order(disabled: :asc, account_balance: :desc).includes(:cold_storage_transfers) -%p - %table.table - - projects.each_slice(15) do |project_slice| +- with_default_precision(0) 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: "The balance displayed on the website. The amount of the tips is based on this value."} Available balance + %th.money + %abbr{title: "Tips attributed but not sent because they have not been processed yet or the author did not set an address."} Tips not sent + %th.money + %abbr{title: "Amount currently in cold storage (see below)."} Amount in cold storage + %th.money + %abbr{title: "Available balance + tips not sent - amount in cold storage."} 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: "Account balance - expected account balance. If this is negative there is a problem."} Difference + %tbody + - project_slice.each do |project| + %tr + %td + %strong= link_to project.full_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(available = project.available_amount_cache) + %td.money= btc_human(not_sent = project.tips.select(&:not_sent?).map(&:amount).compact.sum) + %td.money= btc_human(cold = project.cold_storage_amount) + %td.money= btc_human(expected = available + not_sent - cold) if available and not_sent and cold + %td.money= btc_human(account = project.account_balance) + %td.money= btc_human(account - expected) if account and expected + %tr + %th{colspan: 2} Total + %td.money= btc_human(donated = projects.map(&:deposits).flatten.map(&:amount).sum) + %td.money= btc_human(available = projects.map(&:available_amount_cache).compact.sum) + %td.money= btc_human(not_sent = projects.map(&:tips).flatten.select(&:not_sent?).map(&:amount).compact.sum) + %td.money= btc_human(cold = projects.map(&:cold_storage_amount).compact.sum) + %td.money= btc_human(expected = available + not_sent - cold) if available and not_sent and cold + %td.money= btc_human(account = projects.map(&:account_balance).compact.sum) + %td.money= btc_human(account - expected) if account and expected + + %h2 Cold storage + %p + %table.table %thead %tr - %th Project %th Address - %th.money - %abbr{title: "Total amount that was donated to the project."} Donated - %th.money - %abbr{title: "The balance displayed on the website. The amount of the tips is based on this value."} Available balance - %th.money - %abbr{title: "Tips attributed but not sent because they have not been processed yet or the author did not set an address."} Tips not sent - %th.money - %abbr{title: "Amount currently in cold storage (see below)."} Amount in cold storage - %th.money - %abbr{title: "Available balance + tips not sent - amount in cold storage."} 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: "Account balance - expected account balance. If this is negative there is a problem."} Difference %tbody - - project_slice.each do |project| + - (CONFIG["cold_storage"].try(:[], "addresses") || []).each do |address| %tr - %td - %strong= link_to project.full_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(available = project.available_amount_cache) - %td.money= btc_human(not_sent = project.tips.select(&:not_sent?).map(&:amount).compact.sum) - %td.money= btc_human(cold = project.cold_storage_amount) - %td.money= btc_human(expected = available + not_sent - cold) if available and not_sent and cold - %td.money= btc_human(account = project.account_balance) - %td.money= btc_human(account - expected) if account and expected - %tr - %th{colspan: 2} Total - %td.money= btc_human(donated = projects.map(&:deposits).flatten.map(&:amount).sum) - %td.money= btc_human(available = projects.map(&:available_amount_cache).compact.sum) - %td.money= btc_human(not_sent = projects.map(&:tips).flatten.select(&:not_sent?).map(&:amount).compact.sum) - %td.money= btc_human(cold = projects.map(&:cold_storage_amount).compact.sum) - %td.money= btc_human(expected = available + not_sent - cold) if available and not_sent and cold - %td.money= btc_human(account = projects.map(&:account_balance).compact.sum) - %td.money= btc_human(account - expected) if account and expected - -%h2 Cold storage -%p - %table.table - %thead - %tr - %th Address - %tbody - - (CONFIG["cold_storage"].try(:[], "addresses") || []).each do |address| - %tr - %td - - address_explorers.each_with_index do |explorer, i| - = link_to "[#{i + 1}]", address_url(address, explorer) + = link_to "[#{i + 1}]", address_url(address, explorer) From 32d2dad96cec825755ecfb942a8f697141f9a76d Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sat, 5 Apr 2014 09:46:49 +0200 Subject: [PATCH 105/372] do not display total after each slice --- app/views/home/audit.html.haml | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/app/views/home/audit.html.haml b/app/views/home/audit.html.haml index f48fa64c..3042dcb6 100644 --- a/app/views/home/audit.html.haml +++ b/app/views/home/audit.html.haml @@ -37,15 +37,16 @@ %td.money= btc_human(expected = available + not_sent - cold) if available and not_sent and cold %td.money= btc_human(account = project.account_balance) %td.money= btc_human(account - expected) if account and expected - %tr - %th{colspan: 2} Total - %td.money= btc_human(donated = projects.map(&:deposits).flatten.map(&:amount).sum) - %td.money= btc_human(available = projects.map(&:available_amount_cache).compact.sum) - %td.money= btc_human(not_sent = projects.map(&:tips).flatten.select(&:not_sent?).map(&:amount).compact.sum) - %td.money= btc_human(cold = projects.map(&:cold_storage_amount).compact.sum) - %td.money= btc_human(expected = available + not_sent - cold) if available and not_sent and cold - %td.money= btc_human(account = projects.map(&:account_balance).compact.sum) - %td.money= btc_human(account - expected) 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(available = projects.map(&:available_amount_cache).compact.sum) + %td.money= btc_human(not_sent = projects.map(&:tips).flatten.select(&:not_sent?).map(&:amount).compact.sum) + %td.money= btc_human(cold = projects.map(&:cold_storage_amount).compact.sum) + %td.money= btc_human(expected = available + not_sent - cold) if available and not_sent and cold + %td.money= btc_human(account = projects.map(&:account_balance).compact.sum) + %td.money= btc_human(account - expected) if account and expected %h2 Cold storage %p From cfb7482ec3b2b4fdcd380431ae7f72d731bbe52c Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sat, 5 Apr 2014 10:02:56 +0200 Subject: [PATCH 106/372] do not count refunded tips in the not sent --- app/views/home/audit.html.haml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/views/home/audit.html.haml b/app/views/home/audit.html.haml index 3042dcb6..98593d4f 100644 --- a/app/views/home/audit.html.haml +++ b/app/views/home/audit.html.haml @@ -32,9 +32,9 @@ = link_to "[#{i + 1}]", address_url(project.bitcoin_address, explorer) %td.money= btc_human(donated = project.deposits.map(&:amount).sum) %td.money= btc_human(available = project.available_amount_cache) - %td.money= btc_human(not_sent = project.tips.select(&:not_sent?).map(&:amount).compact.sum) + %td.money= btc_human(unpaid = project.tips.select(&:unpaid?).map(&:amount).compact.sum) %td.money= btc_human(cold = project.cold_storage_amount) - %td.money= btc_human(expected = available + not_sent - cold) if available and not_sent and cold + %td.money= btc_human(expected = available + unpaid - cold) if available and unpaid and cold %td.money= btc_human(account = project.account_balance) %td.money= btc_human(account - expected) if account and expected %tbody @@ -42,9 +42,9 @@ %th{colspan: 2} Total %td.money= btc_human(donated = projects.map(&:deposits).flatten.map(&:amount).sum) %td.money= btc_human(available = projects.map(&:available_amount_cache).compact.sum) - %td.money= btc_human(not_sent = projects.map(&:tips).flatten.select(&:not_sent?).map(&:amount).compact.sum) + %td.money= btc_human(unpaid = projects.map(&:tips).flatten.select(&:unpaid?).map(&:amount).compact.sum) %td.money= btc_human(cold = projects.map(&:cold_storage_amount).compact.sum) - %td.money= btc_human(expected = available + not_sent - cold) if available and not_sent and cold + %td.money= btc_human(expected = available + unpaid - cold) if available and unpaid and cold %td.money= btc_human(account = projects.map(&:account_balance).compact.sum) %td.money= btc_human(account - expected) if account and expected From 9f628aab6c9d377fddf7249962184671767be673 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sat, 5 Apr 2014 10:23:14 +0200 Subject: [PATCH 107/372] disabled projects are grey --- app/views/home/audit.html.haml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/home/audit.html.haml b/app/views/home/audit.html.haml index 98593d4f..7ae57525 100644 --- a/app/views/home/audit.html.haml +++ b/app/views/home/audit.html.haml @@ -13,7 +13,7 @@ %th.money %abbr{title: "The balance displayed on the website. The amount of the tips is based on this value."} Available balance %th.money - %abbr{title: "Tips attributed but not sent because they have not been processed yet or the author did not set an address."} Tips not sent + %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: "Amount currently in cold storage (see below)."} Amount in cold storage %th.money @@ -24,7 +24,7 @@ %abbr{title: "Account balance - expected account balance. If this is negative there is a problem."} Difference %tbody - project_slice.each do |project| - %tr + %tr{class: project.disabled ? "text-muted" : nil} %td %strong= link_to project.full_name, project %td From be9b4f35117ede5a88fe021799476c4524166e0a Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sat, 5 Apr 2014 10:30:04 +0200 Subject: [PATCH 108/372] update cache of disabled projects --- app/models/project.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/project.rb b/app/models/project.rb index a4781365..3b6d7661 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -153,7 +153,7 @@ def next_tip_amount end def self.update_cache - enabled.find_each do |project| + find_each do |project| project.update available_amount_cache: project.available_amount end end From 6edbfb0a154636962237eb89ecc3df2c61ca4d04 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sat, 5 Apr 2014 10:37:05 +0200 Subject: [PATCH 109/372] can specify cold storage address --- app/models/project.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/project.rb b/app/models/project.rb index 3b6d7661..28c16d43 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -209,8 +209,8 @@ def cold_storage_amount cold_storage_transfers.to_a.select(&:confirmed?).sum(&:amount) end - def send_to_cold_storage!(amount) - address = CONFIG["cold_storage"].try(:[], "addresses").try(:first) + 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? PeercoinDaemon.instance.send_many(address_label, {address => amount.to_f}) end From c9511cfbd106049aa4fe992eda38ef74ed27cb33 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sat, 5 Apr 2014 10:44:57 +0200 Subject: [PATCH 110/372] added disabled reason --- app/views/projects/show.html.haml | 3 +++ db/migrate/20140405084351_add_disabled_reason_to_project.rb | 5 +++++ db/schema.rb | 3 ++- 3 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 db/migrate/20140405084351_add_disabled_reason_to_project.rb diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index 4f16795b..3976bbeb 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -23,6 +23,9 @@ %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} + - else .panel.panel-default .panel-heading 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/schema.rb b/db/schema.rb index 1ed44b6f..4ed0b72c 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20140403062826) do +ActiveRecord::Schema.define(version: 20140405084351) do create_table "cold_storage_transfers", force: true do |t| t.integer "project_id" @@ -67,6 +67,7 @@ t.string "cold_storage_withdrawal_address" t.boolean "disabled", default: false t.integer "account_balance", limit: 8 + t.string "disabled_reason" end add_index "projects", ["full_name"], name: "index_projects_on_full_name", unique: true From 2e55fe301d28848201b70f0421d956d05b1ea7e3 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sat, 5 Apr 2014 10:59:17 +0200 Subject: [PATCH 111/372] note about cache --- app/views/home/audit.html.haml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/views/home/audit.html.haml b/app/views/home/audit.html.haml index 7ae57525..2d71b069 100644 --- a/app/views/home/audit.html.haml +++ b/app/views/home/audit.html.haml @@ -48,6 +48,9 @@ %td.money= btc_human(account = projects.map(&:account_balance).compact.sum) %td.money= btc_human(account - expected) if account and expected + %p + Note that some cells are updated in real time and others are cached (#{distance_of_time_in_words eval CONFIG['tipper_delay']}) + %h2 Cold storage %p %table.table From b3e30a6926a732267291bd0413a4110c58cc1576 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 6 Apr 2014 08:40:21 +0200 Subject: [PATCH 112/372] removed our fee from difference --- app/views/home/audit.html.haml | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/app/views/home/audit.html.haml b/app/views/home/audit.html.haml index 2d71b069..7280260f 100644 --- a/app/views/home/audit.html.haml +++ b/app/views/home/audit.html.haml @@ -11,17 +11,19 @@ %th.money %abbr{title: "Total amount that was donated to the project."} Donated %th.money - %abbr{title: "The balance displayed on the website. The amount of the tips is based on this value."} Available balance + %abbr{title: "#{CONFIG["our_fee"]*100}% Peer4commit maintenance fee, used to pay maintenance, hosting and transaction fees."} 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: "Amount currently in cold storage (see below)."} Amount in cold storage %th.money - %abbr{title: "Available balance + tips not sent - amount in cold storage."} Expected account balance + %abbr{title: "Available balance + tips not sent - amount in cold storage + 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: "Account balance - expected account balance. If this is negative there is a problem."} Difference + %abbr{title: "It is either transaction fees or a real issue."} Difference %tbody - project_slice.each do |project| %tr{class: project.disabled ? "text-muted" : nil} @@ -31,20 +33,22 @@ - 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(cold = project.cold_storage_amount) - %td.money= btc_human(expected = available + unpaid - cold) if available and unpaid and cold + %td.money= btc_human(expected = available + unpaid - cold + fee) if available and unpaid and cold and fee %td.money= btc_human(account = project.account_balance) %td.money= btc_human(account - expected) 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(cold = projects.map(&:cold_storage_amount).compact.sum) - %td.money= btc_human(expected = available + unpaid - cold) if available and unpaid and cold + %td.money= btc_human(expected = available + unpaid - cold + fee) if available and unpaid and cold and fee %td.money= btc_human(account = projects.map(&:account_balance).compact.sum) %td.money= btc_human(account - expected) if account and expected From 2eb34725f97a8ff0f113564d3cad76f37e9a292b Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 6 Apr 2014 08:51:58 +0200 Subject: [PATCH 113/372] removed transaction fee from difference --- app/views/home/audit.html.haml | 18 +++++++++++------- .../20140406064344_add_fee_to_sendmany.rb | 5 +++++ db/schema.rb | 3 ++- lib/peercoin_balance_updater.rb | 5 ++++- 4 files changed, 22 insertions(+), 9 deletions(-) create mode 100644 db/migrate/20140406064344_add_fee_to_sendmany.rb diff --git a/app/views/home/audit.html.haml b/app/views/home/audit.html.haml index 7280260f..9b7d25e9 100644 --- a/app/views/home/audit.html.haml +++ b/app/views/home/audit.html.haml @@ -11,19 +11,21 @@ %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."} Fee + %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: "Available balance + tips not sent - amount in cold storage + fee."} Expected account balance + %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: "It is either transaction fees or a real issue."} Difference + %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} @@ -36,10 +38,11 @@ %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.sendmanies.map(&:fee).compact.sum, precision: 2) %td.money= btc_human(cold = project.cold_storage_amount) - %td.money= btc_human(expected = available + unpaid - cold + fee) if available and unpaid and cold and fee + %td.money= btc_human(expected = available + unpaid - cold + fee - txfee) if available and unpaid and cold and fee and txfee %td.money= btc_human(account = project.account_balance) - %td.money= btc_human(account - expected) if account and expected + %td.money= btc_human(account - expected, precision: 2) if account and expected %tbody %tr %th{colspan: 2} Total @@ -47,10 +50,11 @@ %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(&:sendmanies).flatten.map(&:fee).compact.sum, precision: 2) %td.money= btc_human(cold = projects.map(&:cold_storage_amount).compact.sum) - %td.money= btc_human(expected = available + unpaid - cold + fee) if available and unpaid and cold and fee + %td.money= btc_human(expected = available + unpaid - cold + fee - txfee) if available and unpaid and cold and fee and txfee %td.money= btc_human(account = projects.map(&:account_balance).compact.sum) - %td.money= btc_human(account - expected) if account and expected + %td.money= btc_human(account - expected, precision: 2) if account and expected %p Note that some cells are updated in real time and others are cached (#{distance_of_time_in_words eval CONFIG['tipper_delay']}) 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/schema.rb b/db/schema.rb index 4ed0b72c..1ad80bca 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20140405084351) do +ActiveRecord::Schema.define(version: 20140406064344) do create_table "cold_storage_transfers", force: true do |t| t.integer "project_id" @@ -81,6 +81,7 @@ t.datetime "created_at" t.datetime "updated_at" t.integer "project_id" + t.integer "fee" end add_index "sendmanies", ["project_id"], name: "index_sendmanies_on_project_id" diff --git a/lib/peercoin_balance_updater.rb b/lib/peercoin_balance_updater.rb index cfa1ada2..d1176a32 100644 --- a/lib/peercoin_balance_updater.rb +++ b/lib/peercoin_balance_updater.rb @@ -24,7 +24,10 @@ def self.work confirmations = transaction["confirmations"] category = transaction["category"] - next if Sendmany.where(txid: txid).any? + if sendmany = Sendmany.where(txid: txid).first + sendmany.update(fee: -transaction["fee"] * COIN) + next + end if deposit = Deposit.find_by_txid(txid) deposit.update_attribute(:confirmations, confirmations) From d6084c0a56fff87bbdb81f3cc5bcdac7f7c0a0d5 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 6 Apr 2014 08:55:00 +0200 Subject: [PATCH 114/372] display cold storage address in its own column --- app/views/home/audit.html.haml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/views/home/audit.html.haml b/app/views/home/audit.html.haml index 9b7d25e9..e1e33414 100644 --- a/app/views/home/audit.html.haml +++ b/app/views/home/audit.html.haml @@ -65,9 +65,11 @@ %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) From 3d59b7e8cdaa42e2e231f547a6190f5fd58232e7 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 6 Apr 2014 09:00:39 +0200 Subject: [PATCH 115/372] removed units in audit table --- app/helpers/application_helper.rb | 17 ++++++++++------- app/views/home/audit.html.haml | 2 +- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 4e96e90d..2dfa936d 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -1,21 +1,24 @@ 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 - precision = options[:precision] || @default_precision || 2 - btc = "%.#{precision}f PPC" % 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_default_precision(precision) - @old_default_precisions ||= [] - @old_default_precisions << @default_precision - @default_precision = precision + 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_precision = @old_default_precisions.pop + @default_btc_human_options = @old_btc_human_defaults.pop end def to_btc satoshies diff --git a/app/views/home/audit.html.haml b/app/views/home/audit.html.haml index e1e33414..12ae0b8a 100644 --- a/app/views/home/audit.html.haml +++ b/app/views/home/audit.html.haml @@ -1,6 +1,6 @@ %h1 Audit - projects = Project.order(disabled: :asc, account_balance: :desc).includes(:cold_storage_transfers) -- with_default_precision(0) do +- with_btc_human_defaults(precision: 0, display_currency: false) do %p %table.table - projects.each_slice(15) do |project_slice| From e72fb67b5eaf1c5301e6e31b8b3c20658f7249ed Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 6 Apr 2014 09:04:37 +0200 Subject: [PATCH 116/372] raise error when no fee --- lib/peercoin_balance_updater.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/peercoin_balance_updater.rb b/lib/peercoin_balance_updater.rb index d1176a32..d09e85d2 100644 --- a/lib/peercoin_balance_updater.rb +++ b/lib/peercoin_balance_updater.rb @@ -25,6 +25,7 @@ def self.work category = transaction["category"] if sendmany = Sendmany.where(txid: txid).first + raise "No fee on sendmany #{sendmany.inspect}" unless transaction["fee"] sendmany.update(fee: -transaction["fee"] * COIN) next end From 3bd0e906123be71bd528319c48a49b962831ff57 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 6 Apr 2014 09:09:21 +0200 Subject: [PATCH 117/372] count sendmany sent to itself as deposit --- lib/peercoin_balance_updater.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/peercoin_balance_updater.rb b/lib/peercoin_balance_updater.rb index d09e85d2..51c55ea6 100644 --- a/lib/peercoin_balance_updater.rb +++ b/lib/peercoin_balance_updater.rb @@ -24,7 +24,7 @@ def self.work confirmations = transaction["confirmations"] category = transaction["category"] - if sendmany = Sendmany.where(txid: txid).first + if category == "send" and sendmany = Sendmany.where(txid: txid).first raise "No fee on sendmany #{sendmany.inspect}" unless transaction["fee"] sendmany.update(fee: -transaction["fee"] * COIN) next From 5e2f06e47a820743e295862f483fed14c0e1a1bc Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 6 Apr 2014 09:16:08 +0200 Subject: [PATCH 118/372] count cold storage transfer fee in txfee --- app/views/home/audit.html.haml | 4 ++-- .../20140406071705_add_fee_to_cold_storage_transfer.rb | 5 +++++ db/schema.rb | 3 ++- lib/peercoin_balance_updater.rb | 8 +++++--- 4 files changed, 14 insertions(+), 6 deletions(-) create mode 100644 db/migrate/20140406071705_add_fee_to_cold_storage_transfer.rb diff --git a/app/views/home/audit.html.haml b/app/views/home/audit.html.haml index 12ae0b8a..461e7ea5 100644 --- a/app/views/home/audit.html.haml +++ b/app/views/home/audit.html.haml @@ -38,7 +38,7 @@ %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.sendmanies.map(&:fee).compact.sum, precision: 2) + %td.money= btc_human(txfee = project.sendmanies.map(&:fee).compact.sum + project.cold_storage_transfers.map(&:fee).sum, precision: 2) %td.money= btc_human(cold = project.cold_storage_amount) %td.money= btc_human(expected = available + unpaid - cold + fee - txfee) if available and unpaid and cold and fee and txfee %td.money= btc_human(account = project.account_balance) @@ -50,7 +50,7 @@ %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(&:sendmanies).flatten.map(&:fee).compact.sum, precision: 2) + %td.money= btc_human(txfee = projects.map(&:sendmanies).flatten.map(&:fee).compact.sum + projects.map(&:cold_storage_transfers).flatten.map(&:fee).compact.sum, precision: 2) %td.money= btc_human(cold = projects.map(&:cold_storage_amount).compact.sum) %td.money= btc_human(expected = available + unpaid - cold + fee - txfee) if available and unpaid and cold and fee and txfee %td.money= btc_human(account = projects.map(&:account_balance).compact.sum) 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/schema.rb b/db/schema.rb index 1ad80bca..0e6075e3 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20140406064344) do +ActiveRecord::Schema.define(version: 20140406071705) do create_table "cold_storage_transfers", force: true do |t| t.integer "project_id" @@ -21,6 +21,7 @@ t.integer "confirmations" t.datetime "created_at" t.datetime "updated_at" + t.integer "fee" end add_index "cold_storage_transfers", ["project_id"], name: "index_cold_storage_transfers_on_project_id" diff --git a/lib/peercoin_balance_updater.rb b/lib/peercoin_balance_updater.rb index 51c55ea6..a350c859 100644 --- a/lib/peercoin_balance_updater.rb +++ b/lib/peercoin_balance_updater.rb @@ -23,10 +23,11 @@ def self.work txid = transaction["txid"] confirmations = transaction["confirmations"] category = transaction["category"] + fee = transaction["fee"] if category == "send" and sendmany = Sendmany.where(txid: txid).first - raise "No fee on sendmany #{sendmany.inspect}" unless transaction["fee"] - sendmany.update(fee: -transaction["fee"] * COIN) + raise "No fee on sendmany #{sendmany.inspect}" unless fee + sendmany.update(fee: -fee * COIN) next end @@ -36,7 +37,7 @@ def self.work end if cold_storage_transfer = ColdStorageTransfer.find_by_txid(txid) - cold_storage_transfer.update_attribute(:confirmations, confirmations) + cold_storage_transfer.update(confirmations: confirmations, fee: fee) next end @@ -74,6 +75,7 @@ def self.work txid: txid, confirmations: confirmations, amount: -amount, + fee: fee, ) next end From 053d803254a7cc1d6ccb163ae2fbdf418cc3a74b Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 6 Apr 2014 09:22:05 +0200 Subject: [PATCH 119/372] fixed tx fee when some are nil --- app/models/project.rb | 7 +++++++ app/views/home/audit.html.haml | 4 ++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/app/models/project.rb b/app/models/project.rb index 28c16d43..8755bf68 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -214,4 +214,11 @@ def send_to_cold_storage!(amount, address_index = 0) raise "No cold storage address" if address.blank? PeercoinDaemon.instance.send_many(address_label, {address => amount.to_f}) end + + def paid_fee + [ + sendmanies.map(&:fee), + cold_storage_transfers.map(&:fee), + ].flatten.compact.sum + end end diff --git a/app/views/home/audit.html.haml b/app/views/home/audit.html.haml index 461e7ea5..564d0c2d 100644 --- a/app/views/home/audit.html.haml +++ b/app/views/home/audit.html.haml @@ -38,7 +38,7 @@ %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.sendmanies.map(&:fee).compact.sum + project.cold_storage_transfers.map(&:fee).sum, precision: 2) + %td.money= btc_human(txfee = project.paid_fee, precision: 2) %td.money= btc_human(cold = project.cold_storage_amount) %td.money= btc_human(expected = available + unpaid - cold + fee - txfee) if available and unpaid and cold and fee and txfee %td.money= btc_human(account = project.account_balance) @@ -50,7 +50,7 @@ %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(&:sendmanies).flatten.map(&:fee).compact.sum + projects.map(&:cold_storage_transfers).flatten.map(&:fee).compact.sum, precision: 2) + %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(expected = available + unpaid - cold + fee - txfee) if available and unpaid and cold and fee and txfee %td.money= btc_human(account = projects.map(&:account_balance).compact.sum) From 657cc203ce94bd44d0c98fd73d7058539d38f949 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 6 Apr 2014 09:23:44 +0200 Subject: [PATCH 120/372] fixed fee amount --- lib/peercoin_balance_updater.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/peercoin_balance_updater.rb b/lib/peercoin_balance_updater.rb index a350c859..c1936bfb 100644 --- a/lib/peercoin_balance_updater.rb +++ b/lib/peercoin_balance_updater.rb @@ -37,7 +37,7 @@ def self.work end if cold_storage_transfer = ColdStorageTransfer.find_by_txid(txid) - cold_storage_transfer.update(confirmations: confirmations, fee: fee) + cold_storage_transfer.update(confirmations: confirmations, fee: -fee * COIN) next end From d93631cee6a06a22186b5be467b0b884b705f284 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 6 Apr 2014 09:25:51 +0200 Subject: [PATCH 121/372] do not set fee on received cold storage transfers --- lib/peercoin_balance_updater.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/peercoin_balance_updater.rb b/lib/peercoin_balance_updater.rb index c1936bfb..d9829afb 100644 --- a/lib/peercoin_balance_updater.rb +++ b/lib/peercoin_balance_updater.rb @@ -37,7 +37,9 @@ def self.work end if cold_storage_transfer = ColdStorageTransfer.find_by_txid(txid) - cold_storage_transfer.update(confirmations: confirmations, fee: -fee * COIN) + cold_storage_transfer.confirmations = confirmations + cold_storage_transfer.fee = -fee * COIN if fee + cold_storage_transfer.save! next end From 80072618d7fdc94d2b0f4a7ad6e5e0b4f1533e40 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 6 Apr 2014 10:05:17 +0200 Subject: [PATCH 122/372] cache the audit page for 1 minute --- app/views/home/audit.html.haml | 140 +++++++++++++++++---------------- 1 file changed, 71 insertions(+), 69 deletions(-) diff --git a/app/views/home/audit.html.haml b/app/views/home/audit.html.haml index 564d0c2d..1d6933bf 100644 --- a/app/views/home/audit.html.haml +++ b/app/views/home/audit.html.haml @@ -1,75 +1,77 @@ -%h1 Audit -- projects = Project.order(disabled: :asc, account_balance: :desc).includes(:cold_storage_transfers) -- with_btc_human_defaults(precision: 0, display_currency: false) do - %p - %table.table - - projects.each_slice(15) do |project_slice| +- cache_time = 1.minute +- cache("audit-#{Time.now.to_i / cache_time}") do + %h1 Audit + - projects = Project.order(disabled: :asc, account_balance: :desc).includes(:cold_storage_transfers, :sendmanies, :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: "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.full_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(expected = available + unpaid - cold + fee - txfee) if available and unpaid and cold and fee and txfee + %td.money= btc_human(account = project.account_balance) + %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(expected = available + unpaid - cold + fee - txfee) if available and unpaid and cold and fee and txfee + %td.money= btc_human(account = projects.map(&:account_balance).compact.sum) + %td.money= btc_human(account - expected, precision: 2) if account and expected + + %p + Some cells are updated at different intervals. + + %h2 Cold storage + %p + %table.table %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: "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 + %th Explorers %tbody - - project_slice.each do |project| - %tr{class: project.disabled ? "text-muted" : nil} - %td - %strong= link_to project.full_name, project + - (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(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(expected = available + unpaid - cold + fee - txfee) if available and unpaid and cold and fee and txfee - %td.money= btc_human(account = project.account_balance) - %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(expected = available + unpaid - cold + fee - txfee) if available and unpaid and cold and fee and txfee - %td.money= btc_human(account = projects.map(&:account_balance).compact.sum) - %td.money= btc_human(account - expected, precision: 2) if account and expected - - %p - Note that some cells are updated in real time and others are cached (#{distance_of_time_in_words eval CONFIG['tipper_delay']}) - - %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) + = link_to "[#{i + 1}]", address_url(address, explorer) From 50af57ea989d131774e8f8aec85f6f1648f6417c Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 6 Apr 2014 10:28:19 +0200 Subject: [PATCH 123/372] updated amount message when tips are hold --- app/views/projects/show.html.haml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index 3976bbeb..74283d49 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -43,7 +43,10 @@ .well.well-sm= @project.description %h4 Balance = btc_human @project.available_amount - (each new commit receives #{(CONFIG["tip"]*100).round}% of available balance) + - if @project.hold_tips? + (each new commit receives a percentage of available balance) + - else + (each new commit receives #{(CONFIG["tip"]*100).round}% of available balance) - if (unconfirmed_amount = @project.unconfirmed_amount) > 0 (#{btc_human unconfirmed_amount} unconfirmed) From 4d4854f28c28338b9f768fdf8de3ca73168f6b67 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 6 Apr 2014 10:32:15 +0200 Subject: [PATCH 124/372] sync style with tip4commit --- app/views/projects/show.html.haml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index 74283d49..0c63b59c 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -7,10 +7,11 @@ %h1 = @project.full_name %small= link_to glyph(:github), @project.github_url, target: '_blank' - - if can? :update, @project - = link_to "Change project settings", edit_project_path(@project), class: "btn btn-default" - - 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-primary" + .pull-right + - if can? :update, @project + = link_to "Change project settings", edit_project_path(@project), class: "btn btn-primary" + - 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-warning" .row .col-md-4 From e2def1ec6319c99f03fcbcecb08600940548df18 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 6 Apr 2014 10:37:10 +0200 Subject: [PATCH 125/372] better warning about cache --- app/views/home/audit.html.haml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/views/home/audit.html.haml b/app/views/home/audit.html.haml index 1d6933bf..54e6f0fa 100644 --- a/app/views/home/audit.html.haml +++ b/app/views/home/audit.html.haml @@ -1,6 +1,9 @@ - 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, :sendmanies, :tips, :deposits) - with_btc_human_defaults(precision: 0, display_currency: false) do %p @@ -58,9 +61,6 @@ %td.money= btc_human(account = projects.map(&:account_balance).compact.sum) %td.money= btc_human(account - expected, precision: 2) if account and expected - %p - Some cells are updated at different intervals. - %h2 Cold storage %p %table.table From 179982059d3341419782745d577e241979c69107 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 6 Apr 2014 10:40:02 +0200 Subject: [PATCH 126/372] update balances after tipping too --- config/schedule.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/schedule.rb b/config/schedule.rb index 0085776c..269226b7 100644 --- a/config/schedule.rb +++ b/config/schedule.rb @@ -29,6 +29,6 @@ if delay = CONFIG['tipper_delay'] delay = eval(delay) every delay do - runner "PeercoinBalanceUpdater.work; BitcoinTipper.work" + runner "PeercoinBalanceUpdater.work; BitcoinTipper.work; PeercoinBalanceUpdater.work" end end From e573bf9061c2cf193a9011032968b53fe7cfb406 Mon Sep 17 00:00:00 2001 From: aditya-kapoor Date: Sun, 23 Feb 2014 01:26:20 +0530 Subject: [PATCH 127/372] use new rails syntax along with Arel and other query optimizations Conflicts: app/models/tip.rb app/models/user.rb --- Gemfile | 2 +- app/models/project.rb | 5 ++--- app/models/tip.rb | 6 +++--- app/models/user.rb | 7 +++---- 4 files changed, 9 insertions(+), 11 deletions(-) diff --git a/Gemfile b/Gemfile index ff43a892..436cd95e 100644 --- a/Gemfile +++ b/Gemfile @@ -4,7 +4,6 @@ source 'https://rubygems.org' gem 'rails', '~> 4.0.2' # Databases -gem 'sqlite3', group: :development gem 'mysql2', group: :mysql gem 'pg', group: :postgresql @@ -57,6 +56,7 @@ gem 'octokit' # gem 'debugger', group: [:development, :test] group :development do + gem 'sqlite3' gem 'capistrano', '~> 3.0' gem 'capistrano-rvm', github: 'capistrano/rvm' gem 'capistrano-bundler', '>= 1.1.0' diff --git a/app/models/project.rb b/app/models/project.rb index 8755bf68..2f4c9e5e 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -10,8 +10,7 @@ class Project < ActiveRecord::Base has_one :tipping_policies_text, inverse_of: :project accepts_nested_attributes_for :tipping_policies_text - validates :full_name, uniqueness: true, presence: true - validates :github_id, uniqueness: true, presence: true + validates :full_name, :github_id, uniqueness: true, presence: true scope :enabled, -> { where(disabled: false) } scope :disabled, -> { where(disabled: true) } @@ -94,7 +93,7 @@ def tip_for commit user = User.find_by email: email if (next_tip_amount > 0) && - Tip.find_by_commit(commit.sha).nil? + Tip.find_by(commit: commit.sha).nil? # create user unless user diff --git a/app/models/tip.rb b/app/models/tip.rb index 6c02d44b..9c35e96d 100644 --- a/app/models/tip.rb +++ b/app/models/tip.rb @@ -26,12 +26,12 @@ def free? amount == 0 end - scope :paid, -> { where('sendmany_id is not ?', nil) } + scope :paid, -> { where.not(sendmany_id: nil) } def paid? !!sendmany_id end - scope :refunded, -> { where('refunded_at is not ?', nil) } + scope :refunded, -> { where.not(refunded_at: nil) } scope :non_refunded, -> { where(refunded_at: nil) } def refunded? !!refunded_at @@ -44,7 +44,7 @@ def non_refunded? unpaid. where('users.bitcoin_address' => ['', nil]) } - scope :with_address, -> { joins(:user).where('users.bitcoin_address IS NOT NULL AND users.bitcoin_address != ?', "") } + scope :with_address, -> { joins(:user).where.not('users.bitcoin_address' => [nil, ""]) } def with_address? user.bitcoin_address.present? end diff --git a/app/models/user.rb b/app/models/user.rb index 1ab2fa99..d38613f6 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -2,11 +2,9 @@ class User < ActiveRecord::Base # Include default devise modules. Others available are: # :confirmable, :lockable, :timeoutable devise :database_authenticatable, :registerable, :recoverable, - :rememberable, :trackable, :validatable + :rememberable, :trackable, :validatable, :omniauthable, :omniauth_providers => [:github] - devise :omniauthable, :omniauth_providers => [:github] - - validates :bitcoin_address, :bitcoin_address => true + validates :bitcoin_address, bitcoin_address: true has_many :tips @@ -19,6 +17,7 @@ def balance end after_create :generate_login_token! + def generate_login_token! if login_token.blank? self.update login_token: SecureRandom.urlsafe_base64 From 96039186ba97bcbf4c67f72dbe8f51effad47318 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 6 Apr 2014 13:03:45 +0200 Subject: [PATCH 128/372] revert some changes I prefer the other way --- Gemfile | 2 +- app/models/project.rb | 3 ++- app/models/user.rb | 4 +++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index 436cd95e..ff43a892 100644 --- a/Gemfile +++ b/Gemfile @@ -4,6 +4,7 @@ source 'https://rubygems.org' gem 'rails', '~> 4.0.2' # Databases +gem 'sqlite3', group: :development gem 'mysql2', group: :mysql gem 'pg', group: :postgresql @@ -56,7 +57,6 @@ gem 'octokit' # gem 'debugger', group: [:development, :test] group :development do - gem 'sqlite3' gem 'capistrano', '~> 3.0' gem 'capistrano-rvm', github: 'capistrano/rvm' gem 'capistrano-bundler', '>= 1.1.0' diff --git a/app/models/project.rb b/app/models/project.rb index 2f4c9e5e..d4b51da7 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -10,7 +10,8 @@ class Project < ActiveRecord::Base has_one :tipping_policies_text, inverse_of: :project accepts_nested_attributes_for :tipping_policies_text - validates :full_name, :github_id, uniqueness: true, presence: true + validates :full_name, uniqueness: true, presence: true + validates :github_id, uniqueness: true, presence: true scope :enabled, -> { where(disabled: false) } scope :disabled, -> { where(disabled: true) } diff --git a/app/models/user.rb b/app/models/user.rb index d38613f6..7c66c97b 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -2,7 +2,9 @@ class User < ActiveRecord::Base # Include default devise modules. Others available are: # :confirmable, :lockable, :timeoutable devise :database_authenticatable, :registerable, :recoverable, - :rememberable, :trackable, :validatable, :omniauthable, :omniauth_providers => [:github] + :rememberable, :trackable, :validatable + + devise :omniauthable, :omniauth_providers => [:github] validates :bitcoin_address, bitcoin_address: true From 449e3783e060423c2de83f921569958d42a943f8 Mon Sep 17 00:00:00 2001 From: aditya-kapoor Date: Sun, 23 Feb 2014 02:44:53 +0530 Subject: [PATCH 129/372] rename generate_login_token and refactor full_name --- app/models/user.rb | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/app/models/user.rb b/app/models/user.rb index 7c66c97b..a94d639b 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -18,22 +18,14 @@ def balance tips.unpaid.sum(:amount) end - after_create :generate_login_token! + after_create :generate_login_token, unless: :login_token - def generate_login_token! - if login_token.blank? - self.update login_token: SecureRandom.urlsafe_base64 - end + def generate_login_token + self.update login_token: SecureRandom.urlsafe_base64 end def full_name - if !name.blank? - name - elsif !nickname.blank? - nickname - else - email - end + name.presence || nickname.presence || email end def self.update_cache From 74603b6a9f519ec2d9fe42c547a2729459201efc Mon Sep 17 00:00:00 2001 From: aditya-kapoor Date: Sun, 23 Feb 2014 03:07:46 +0530 Subject: [PATCH 130/372] use single quotes and new ruby syntax in views Conflicts: app/views/projects/show.html.haml --- app/views/layouts/application.html.haml | 8 ++++---- app/views/projects/index.html.haml | 4 ++-- app/views/projects/show.html.haml | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 95489bce..381d4e53 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -9,13 +9,13 @@ %title= "Peer4Commit — " + (content_for?(:title) ? yield(:title) : "Contribute to Open Source") - %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{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 } = csrf_meta_tags %body diff --git a/app/views/projects/index.html.haml b/app/views/projects/index.html.haml index d9345199..4d12f265 100644 --- a/app/views/projects/index.html.haml +++ b/app/views/projects/index.html.haml @@ -1,12 +1,12 @@ %h1 Supported Projects %p - = form_tag projects_path, html: {role: 'form', class: 'form-inline"'}, method: :post do |f| + = 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 + = submit_tag "Find or add project", class: 'btn form-control btn-default' %p %table.table %thead diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index 0c63b59c..c42f63e4 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -37,7 +37,7 @@ %p = @project.bitcoin_address %p - = image_tag qrcode_project_path(@project, format: :svg), alt: @project.bitcoin_address, class: "project qrcode" + = image_tag qrcode_project_path(@project, format: :svg), alt: @project.bitcoin_address, class: 'project qrcode' %p #{100-(CONFIG["our_fee"]*100).round}% of deposited funds will be used to tip for new commits. .col-md-8 - unless @project.description.blank? From d7f7b9ebc3058b50a82ebb89affc91ec5cc6490e Mon Sep 17 00:00:00 2001 From: aditya-kapoor Date: Sun, 23 Feb 2014 03:18:31 +0530 Subject: [PATCH 131/372] remove clutter Conflicts: config/routes.rb --- config/routes.rb | 51 ------------------------------------------------ 1 file changed, 51 deletions(-) diff --git a/config/routes.rb b/config/routes.rb index c0f577cd..e962f7f3 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -27,55 +27,4 @@ :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 end From 5c995867749b1fa720002314bbf5cb325b41a2f9 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 6 Apr 2014 13:15:05 +0200 Subject: [PATCH 132/372] updated tests --- features/step_definitions/cold_storage.rb | 2 +- features/support/peercoin_daemon_mock.rb | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/features/step_definitions/cold_storage.rb b/features/step_definitions/cold_storage.rb index 490c1614..431bbba4 100644 --- a/features/step_definitions/cold_storage.rb +++ b/features/step_definitions/cold_storage.rb @@ -12,7 +12,7 @@ end When(/^there's a new incoming transaction of "([^"]*?)" on the project account$/) do |arg1| - PeercoinDaemon.instance.add_transaction(account: @project.address_label, amount: arg1.to_d) + PeercoinDaemon.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| diff --git a/features/support/peercoin_daemon_mock.rb b/features/support/peercoin_daemon_mock.rb index da9a65a6..04c72bb6 100644 --- a/features/support/peercoin_daemon_mock.rb +++ b/features/support/peercoin_daemon_mock.rb @@ -55,6 +55,10 @@ def get_new_address(account) def get_addresses_by_account(account) @addresses_by_account[account] || [] end + + def get_balance(account) + 0 + end end Before do From defba57900d0474299584db6d8aad9ba9a4ceb71 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 6 Apr 2014 13:27:46 +0200 Subject: [PATCH 133/372] better flash messages --- app/assets/stylesheets/flash_message.css.sass | 2 ++ app/helpers/application_helper.rb | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) create mode 100644 app/assets/stylesheets/flash_message.css.sass 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/helpers/application_helper.rb b/app/helpers/application_helper.rb index 544eb785..c00ec64c 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -48,12 +48,12 @@ def commit_tag(sha1) def render_flash_message html = [] - flash.each do |_type, _message| - alert_type = case _type + flash.each do |type, message| + alert_type = case type when :notice then :success when :alert then :danger end - html << content_tag(:div, class: "text-center alert alert-#{alert_type}"){ _message } + html << content_tag(:div, message, class: "flash-message text-center alert alert-#{alert_type}") end html.join("\n").html_safe end From 5f351b44ed35de3861dcc5ce537d46291524b2d0 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 13 Apr 2014 16:11:23 +0200 Subject: [PATCH 134/372] generic balance updater --- config/schedule.rb | 2 +- features/step_definitions/cold_storage.rb | 4 ++-- lib/{peercoin_balance_updater.rb => balance_updater.rb} | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) rename lib/{peercoin_balance_updater.rb => balance_updater.rb} (99%) diff --git a/config/schedule.rb b/config/schedule.rb index 269226b7..6a3ed10f 100644 --- a/config/schedule.rb +++ b/config/schedule.rb @@ -29,6 +29,6 @@ if delay = CONFIG['tipper_delay'] delay = eval(delay) every delay do - runner "PeercoinBalanceUpdater.work; BitcoinTipper.work; PeercoinBalanceUpdater.work" + runner "BalanceUpdater.work; BitcoinTipper.work; BalanceUpdater.work" end end diff --git a/features/step_definitions/cold_storage.rb b/features/step_definitions/cold_storage.rb index 431bbba4..3b214ed8 100644 --- a/features/step_definitions/cold_storage.rb +++ b/features/step_definitions/cold_storage.rb @@ -32,11 +32,11 @@ end When(/^the project balance is updated$/) do - PeercoinBalanceUpdater.work + BalanceUpdater.work end Then(/^updating the project balance should raise an error$/) do - expect { PeercoinBalanceUpdater.work }.to raise_error + expect { BalanceUpdater.work }.to raise_error end Then(/^the project balance should be "(.*?)"$/) do |arg1| diff --git a/lib/peercoin_balance_updater.rb b/lib/balance_updater.rb similarity index 99% rename from lib/peercoin_balance_updater.rb rename to lib/balance_updater.rb index d9829afb..fef069ac 100644 --- a/lib/peercoin_balance_updater.rb +++ b/lib/balance_updater.rb @@ -1,4 +1,4 @@ -module PeercoinBalanceUpdater +module BalanceUpdater def self.work Project.all.each do |project| start = 0 From 2be278c7f2b3923c994d038864af0c664dd59ca8 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 13 Apr 2014 16:15:52 +0200 Subject: [PATCH 135/372] generic bitcoin daemon --- app/controllers/projects_controller.rb | 2 +- app/models/project.rb | 2 +- app/models/sendmany.rb | 2 +- features/step_definitions/cold_storage.rb | 14 +++++++------- ...rcoin_daemon_mock.rb => bitcoin_daemon_mock.rb} | 6 +++--- lib/balance_updater.rb | 6 +++--- lib/{peercoin_daemon.rb => bitcoin_daemon.rb} | 4 ++-- 7 files changed, 18 insertions(+), 18 deletions(-) rename features/support/{peercoin_daemon_mock.rb => bitcoin_daemon_mock.rb} (93%) rename lib/{peercoin_daemon.rb => bitcoin_daemon.rb} (94%) diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 9c60a912..417f9e96 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -9,7 +9,7 @@ def show @project = Project.find params[:id] if @project and @project.bitcoin_address.nil? and (github_id = @project.github_id).present? label = "#{github_id}@peer4commit" - address = PeercoinDaemon.instance.get_new_address(label) + address = BitcoinDaemon.instance.get_new_address(label) @project.update_attributes(bitcoin_address: address, address_label: label) end end diff --git a/app/models/project.rb b/app/models/project.rb index d4b51da7..105d958d 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -212,7 +212,7 @@ def cold_storage_amount 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? - PeercoinDaemon.instance.send_many(address_label, {address => amount.to_f}) + BitcoinDaemon.instance.send_many(address_label, {address => amount.to_f}) end def paid_fee diff --git a/app/models/sendmany.rb b/app/models/sendmany.rb index 4fbd0d37..69e47486 100644 --- a/app/models/sendmany.rb +++ b/app/models/sendmany.rb @@ -16,7 +16,7 @@ def send_transaction raise "Not enough funds on Sendmany##{id}" if total_amount > project.available_amount - txid = PeercoinDaemon.instance.send_many(project.address_label, JSON.parse(data)) + txid = BitcoinDaemon.instance.send_many(project.address_label, JSON.parse(data)) update_attribute :is_error, false update_attribute :txid, txid diff --git a/features/step_definitions/cold_storage.rb b/features/step_definitions/cold_storage.rb index 3b214ed8..66a070f8 100644 --- a/features/step_definitions/cold_storage.rb +++ b/features/step_definitions/cold_storage.rb @@ -12,23 +12,23 @@ end When(/^there's a new incoming transaction of "([^"]*?)" on the project account$/) do |arg1| - PeercoinDaemon.instance.add_transaction(account: @project.address_label, amount: arg1.to_d, address: @project.bitcoin_address) + 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| - PeercoinDaemon.instance.add_transaction(account: @project.address_label, amount: arg1.to_d, address: 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| - PeercoinDaemon.instance.add_transaction(account: @project.address_label, amount: arg1.to_d, address: arg2, confirmations: arg3.to_i) + 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| - PeercoinDaemon.instance.add_transaction(category: "send", account: @project.address_label, amount: -arg1.to_d, address: 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| - PeercoinDaemon.instance.add_transaction(category: "send", account: @project.address_label, amount: -arg1.to_d, address: arg2, confirmations: arg3.to_i) + 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 updated$/) do @@ -52,7 +52,7 @@ end Then(/^there should be an outgoing transaction of "(.*?)" to address "(.*?)" on the project account$/) do |arg1, arg2| - transactions = PeercoinDaemon.instance.list_transactions(@project.address_label) + transactions = BitcoinDaemon.instance.list_transactions(@project.address_label) transactions.map { |t| t["category"] }.should eq(["send"]) transactions.map { |t| t["address"] }.should eq([arg2]) transactions.map { |t| -t["amount"].to_d / COIN }.should eq([arg1.to_d]) @@ -67,6 +67,6 @@ end Then(/^the project cold storage withdrawal address should be linked to its account$/) do - PeercoinDaemon.instance.get_addresses_by_account(@project.address_label).should include(@project.reload.cold_storage_withdrawal_address) + BitcoinDaemon.instance.get_addresses_by_account(@project.address_label).should include(@project.reload.cold_storage_withdrawal_address) end diff --git a/features/support/peercoin_daemon_mock.rb b/features/support/bitcoin_daemon_mock.rb similarity index 93% rename from features/support/peercoin_daemon_mock.rb rename to features/support/bitcoin_daemon_mock.rb index 04c72bb6..12cd8e66 100644 --- a/features/support/peercoin_daemon_mock.rb +++ b/features/support/bitcoin_daemon_mock.rb @@ -1,4 +1,4 @@ -class PeercoinDaemonMock +class BitcoinDaemonMock def initialize @transactions = [] @addresses_by_account = Hash.new @@ -62,7 +62,7 @@ def get_balance(account) end Before do - PeercoinDaemon.instance_eval do - @peercoin_daemon = PeercoinDaemonMock.new + BitcoinDaemon.instance_eval do + @bitcoin_daemon = BitcoinDaemonMock.new end end diff --git a/lib/balance_updater.rb b/lib/balance_updater.rb index fef069ac..4c0e6686 100644 --- a/lib/balance_updater.rb +++ b/lib/balance_updater.rb @@ -6,17 +6,17 @@ def self.work raise "Project without address label: #{project.inspect}" if project.address_label.blank? - project.update(account_balance: (PeercoinDaemon.instance.get_balance(project.address_label) * COIN).to_i) + project.update(account_balance: (BitcoinDaemon.instance.get_balance(project.address_label) * COIN).to_i) next if project.disabled? if project.cold_storage_withdrawal_address.blank? - new_address = PeercoinDaemon.instance.get_new_address(project.address_label) + new_address = BitcoinDaemon.instance.get_new_address(project.address_label) project.update!(cold_storage_withdrawal_address: new_address) end loop do - transactions = PeercoinDaemon.instance.list_transactions(project.address_label, count, start) + transactions = BitcoinDaemon.instance.list_transactions(project.address_label, count, start) break if transactions.empty? transactions.each do |transaction| diff --git a/lib/peercoin_daemon.rb b/lib/bitcoin_daemon.rb similarity index 94% rename from lib/peercoin_daemon.rb rename to lib/bitcoin_daemon.rb index c1187771..45c930df 100644 --- a/lib/peercoin_daemon.rb +++ b/lib/bitcoin_daemon.rb @@ -1,6 +1,6 @@ -class PeercoinDaemon +class BitcoinDaemon def self.instance - @peercoin_daemon ||= PeercoinDaemon.new(CONFIG['peercoin']) + @bitcoin_daemon ||= BitcoinDaemon.new(CONFIG['peercoin']) end class RPCError < StandardError From 0ebf20a75aa75e6316b8c230f81fa102866bd796 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 13 Apr 2014 16:19:14 +0200 Subject: [PATCH 136/372] generic daemon config --- config/config.yml.sample | 4 ++-- config/schedule.rb | 2 +- lib/bitcoin_daemon.rb | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/config/config.yml.sample b/config/config.yml.sample index 49a50dc1..69de3046 100644 --- a/config/config.yml.sample +++ b/config/config.yml.sample @@ -2,12 +2,12 @@ github: key: "111111111111" secret: "111111111111" -peercoin: +daemon: username: rpcuser password: rpcpassword host: localhost port: 9904 - daemon: /path/to/ppcoin/src/ppcoind + path: /path/to/ppcoin/src/ppcoind devise: secret: "111111111111" diff --git a/config/schedule.rb b/config/schedule.rb index 6a3ed10f..16f96783 100644 --- a/config/schedule.rb +++ b/config/schedule.rb @@ -21,7 +21,7 @@ require File.expand_path('../../config/environment', __FILE__) every :reboot do - if daemon = CONFIG['peercoin']['daemon'] + if daemon = CONFIG['daemon']['path'] command daemon end end diff --git a/lib/bitcoin_daemon.rb b/lib/bitcoin_daemon.rb index 45c930df..05cbed8e 100644 --- a/lib/bitcoin_daemon.rb +++ b/lib/bitcoin_daemon.rb @@ -1,6 +1,6 @@ class BitcoinDaemon def self.instance - @bitcoin_daemon ||= BitcoinDaemon.new(CONFIG['peercoin']) + @bitcoin_daemon ||= BitcoinDaemon.new(CONFIG['daemon']) end class RPCError < StandardError From 91453f2df6fd8d9a17f3073ceee0a81df6efb05c Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 13 Apr 2014 16:22:15 +0200 Subject: [PATCH 137/372] generic daemon --- lib/bitcoin_daemon.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/bitcoin_daemon.rb b/lib/bitcoin_daemon.rb index 05cbed8e..b37f082a 100644 --- a/lib/bitcoin_daemon.rb +++ b/lib/bitcoin_daemon.rb @@ -19,7 +19,7 @@ def initialize(config) def rpc(command, *params) %w( username password port host ).each do |field| - raise "No #{field} provided in peercoin config" if config[field].blank? + raise "No #{field} provided in daemon config" if config[field].blank? end uri = URI::HTTP.build(host: config['host'], port: config['port'].to_i) From 768196bc35f3b7ad639a287cf3bf7d223dfc12b4 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 13 Apr 2014 16:28:44 +0200 Subject: [PATCH 138/372] generic features --- features/cold_storage.feature | 2 +- features/step_definitions/cold_storage.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/features/cold_storage.feature b/features/cold_storage.feature index 10b986a5..9e8c132d 100644 --- a/features/cold_storage.feature +++ b/features/cold_storage.feature @@ -41,7 +41,7 @@ Feature: Some funds are transfered to cold storage And the project amount in cold storage should be "0" Scenario: Sending funds to cold storage - When "50" peercoins of the project funds are sent 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 diff --git a/features/step_definitions/cold_storage.rb b/features/step_definitions/cold_storage.rb index 66a070f8..841ce22d 100644 --- a/features/step_definitions/cold_storage.rb +++ b/features/step_definitions/cold_storage.rb @@ -47,7 +47,7 @@ (@project.reload.cold_storage_amount / COIN).should eq(arg1.to_d) end -When(/^"(.*?)" peercoins of the project funds are sent to cold storage$/) do |arg1| +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 From 84efd3e32c92f93c9b20d5eeff5e3ecb31d967d5 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 13 Apr 2014 16:52:41 +0200 Subject: [PATCH 139/372] no canonical host on sample --- config/config.yml.sample | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/config.yml.sample b/config/config.yml.sample index 69de3046..71db0e52 100644 --- a/config/config.yml.sample +++ b/config/config.yml.sample @@ -42,4 +42,4 @@ 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 +# canonical_host: peer4commit.example.com # will redirect all other hostnames to this one From fa0b601f03bfe565a84a738a3ac829df792a5a39 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 13 Apr 2014 16:19:39 +0200 Subject: [PATCH 140/372] primecoin address versions --- config/config.yml.sample | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/config.yml.sample b/config/config.yml.sample index 71db0e52..88ba88ec 100644 --- a/config/config.yml.sample +++ b/config/config.yml.sample @@ -38,7 +38,7 @@ 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 +address_versions: # 23/83 for primecoin, 111/196 for testnet, see base58.h - 111 - 196 From e75e827eb759a763c2389869f002988c02fefd78 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 13 Apr 2014 16:28:03 +0200 Subject: [PATCH 141/372] renamed all peercoin to primecoin --- app/controllers/users_controller.rb | 4 ++-- app/views/home/audit.html.haml | 2 +- app/views/home/index.html.haml | 12 ++++++------ app/views/layouts/application.html.haml | 8 ++++---- app/views/projects/show.html.haml | 8 ++++---- app/views/user_mailer/new_tip.html.haml | 4 ++-- app/views/user_mailer/security_issue.html.haml | 4 ++-- app/views/users/show.html.haml | 6 +++--- config/locales/en.yml | 2 +- lib/bitcoin_address_validator.rb | 2 +- 10 files changed, 26 insertions(+), 26 deletions(-) diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 54be8c89..a0f24674 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -18,7 +18,7 @@ def update if @user.update_attributes(users_params) redirect_to @user, notice: 'Your information saved!' else - render :show, alert: 'Error updating peercoin address' + render :show, alert: 'Error updating primecoin address' end end @@ -28,7 +28,7 @@ def login 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 peercoin address to get your tips.' + flash[:alert] = 'You unsubscribed! Sorry for bothering you. Although, you still can leave us your primecoin address to get your tips.' end else redirect_to root_url, alert: 'User not found' diff --git a/app/views/home/audit.html.haml b/app/views/home/audit.html.haml index 54e6f0fa..1e22d9cb 100644 --- a/app/views/home/audit.html.haml +++ b/app/views/home/audit.html.haml @@ -28,7 +28,7 @@ %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 + %abbr{title: "The balance of the project account as reported by the Primecoin daemon."} Account balance %th.money %abbr{title: "If it is different than 0 there is an issue."} Difference %tbody diff --git a/app/views/home/index.html.haml b/app/views/home/index.html.haml index d33c4960..9fc2c121 100644 --- a/app/views/home/index.html.haml +++ b/app/views/home/index.html.haml @@ -1,7 +1,7 @@ / Jumbotron .jumbotron %h1 Contribute to Open Source - %p.lead Donate peercoins to open source projects or make commits and get tips for it. + %p.lead Donate primecoins 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? @@ -12,7 +12,7 @@ / Your balance / = btc_human current_user.balance / \/ - / = link_to current_user.bitcoin_address.blank? ? 'Please set your Peercoin address to receive tips!' : 'Change peercoin address', current_user + / = link_to current_user.bitcoin_address.blank? ? 'Please set your Primecoin address to receive tips!' : 'Change primecoin address', current_user / \/ / = link_to 'Sign Out', destroy_user_session_path, method: :delete / \/ @@ -25,13 +25,13 @@ .row .col-lg-4 %h2 How it works? - %p People donate peercoins to projects. When someone's commit is accepted to the project repository, we automatically tip the author. + %p People donate primecoins to projects. When someone's commit is accepted to the project repository, we automatically tip the author. %p - %a.btn.btn-primary{href: "http://peercoin.net/", target: '_blank'} Learn about Peercoin » + %a.btn.btn-primary{href: "http://primecoin.io/", target: '_blank'} Learn about Primecoin » .col-lg-4 %h2 Donate %p - Find a project you like and deposit peercoins to it. Your money will be accumulated with money of other donators to tip for new commits. + Find a project you like and deposit primecoins 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 @@ -44,6 +44,6 @@ %p %a.btn.btn-primary{href: projects_path} Supported projects » / - if current_user -/ %a.btn.btn-primary{href: user_path(current_user)} Change your peercoin address » +/ %a.btn.btn-primary{href: user_path(current_user)} Change your primecoin address » / - else / %a.btn.btn-primary{href: user_omniauth_authorize_path(:github)} Sign In » diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 516d1c25..b9da5b6a 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -9,8 +9,8 @@ %title= "Peer4Commit — " + (content_for?(:title) ? yield(:title) : "Contribute to Open Source") - %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{name: 'description', content: (content_for?(:title) ? yield(:title) : "Donate primecoins to open source projects or make commits and get tips for it.")} + %meta{name: 'keywords', content: 'open source,contribute,github,community,git,bitcoin,primecoin,xpm,tips,perks'} / %meta{:property => 'og:image', :content => asset_path('logo.png')} / %link{:rel => 'image_src', :type => 'image/png', :href => asset_path('logo.png')} @@ -51,9 +51,9 @@ 2014. Source code is available at #{link_to('github', 'https://github.com/sigmike/peer4commit', target: '_blank')}, based on #{link_to "Tip4commit", "http://tip4commit.com/"}. You can support its development with - = link_to('peercoins', 'http://peer4commit.com/projects/1') + = link_to('peercoins', 'http://peer4commit.com/projects/16') or - = link_to('bitcoins', 'http://tip4commit.com/projects/560') + = link_to('bitcoins', 'http://tip4commit.com/projects/615') / /container / Bootstrap core JavaScript diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index c42f63e4..27aa39fb 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -33,7 +33,7 @@ %h4.panel-title Project Sponsors .panel-body.text-center - %p To give to this project, send peercoins to this address: + %p To give to this project, send primecoins to this address: %p = @project.bitcoin_address %p @@ -103,15 +103,15 @@ = btc_human @project.next_tip_amount %h4 Contribute and Earn - Donate peercoins to this project or + Donate primecoins 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 peercoins on its balance, you will get a tip! + and get tips for it. If your commit is accepted by project maintainer and there are primecoins on its balance, you will get a tip! - if current_user - if current_user.bitcoin_address.blank? Just = link_to 'tell us', current_user - your peercoin address. + your primecoin address. - else Just check your email or %a{href: user_omniauth_authorize_path(:github)} Sign In. diff --git a/app/views/user_mailer/new_tip.html.haml b/app/views/user_mailer/new_tip.html.haml index ddbaad6b..304a2d65 100644 --- a/app/views/user_mailer/new_tip.html.haml +++ b/app/views/user_mailer/new_tip.html.haml @@ -1,8 +1,8 @@ %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 peercoin 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 primecoin address to get it. -%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 Your current balance is #{btc_human @user.balance}. If you don't enter a primecoin address your tips will be returned to the project in 30 days. %p= link_to 'Sign In', login_users_url(token: @user.login_token) diff --git a/app/views/user_mailer/security_issue.html.haml b/app/views/user_mailer/security_issue.html.haml index d503374c..8be51317 100644 --- a/app/views/user_mailer/security_issue.html.haml +++ b/app/views/user_mailer/security_issue.html.haml @@ -1,10 +1,10 @@ %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 We recently discovered a security issue on Peer4commit. This issue allowed someone to change the Primecoin 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: + Please set your Primecoin 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. diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index 1c751f2b..001509ee 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -12,14 +12,14 @@ %p= @user.email = form_for @user, html: {role: 'form', class: 'form-inline"'} do |f| - if @user.errors.size > 0 - .alert.alert-danger Peercoin address is invalid. + .alert.alert-danger Primecoin address is invalid. .form-group = f.label :bitcoin_address - = f.text_field :bitcoin_address, class: 'form-control', placeholder: 'Your peercoin address' + = f.text_field :bitcoin_address, class: 'form-control', placeholder: 'Your primecoin address' = f.button :update, class: 'btn btn-default' - if @user.balance > 0 .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", confirm: "All the #{to_btc @user.balance} peercoins you received will be sent back to their project. Are you sure?" + = button_to "Send all my tips back to their project", send_tips_back_user_path(@user), class: "btn", confirm: "All the #{to_btc @user.balance} primecoins you received will be sent back to their project. Are you sure?" diff --git a/config/locales/en.yml b/config/locales/en.yml index e618cddd..fe9eb005 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -25,4 +25,4 @@ en: activerecord: attributes: user: - bitcoin_address: Peercoin address + bitcoin_address: Primecoin address diff --git a/lib/bitcoin_address_validator.rb b/lib/bitcoin_address_validator.rb index 7332bd7a..67140a4d 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] << "Peercoin address is invalid" + record.errors[field] << "Primecoin address is invalid" end end From 2a412b1cc284050faf4aab93b984a06bf723809b Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 13 Apr 2014 16:37:39 +0200 Subject: [PATCH 142/372] renamed all peer4commit to prime4commit --- app/controllers/projects_controller.rb | 2 +- app/mailers/user_mailer.rb | 2 +- app/views/home/audit.html.haml | 2 +- app/views/layouts/application.html.haml | 10 +++++----- app/views/projects/show.html.haml | 2 +- app/views/projects/show.svg.erb | 2 +- app/views/user_mailer/new_tip.html.haml | 2 +- app/views/user_mailer/security_issue.html.haml | 4 ++-- config/config.yml.sample | 2 +- config/database.yml.sample | 2 +- config/deploy.rb | 2 +- .../20140215094549_initialize_project_address_label.rb | 2 +- 12 files changed, 17 insertions(+), 17 deletions(-) diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 417f9e96..17902af0 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -8,7 +8,7 @@ def index def show @project = Project.find params[:id] if @project and @project.bitcoin_address.nil? and (github_id = @project.github_id).present? - label = "#{github_id}@peer4commit" + label = "#{github_id}@prime4commit" address = BitcoinDaemon.instance.get_new_address(label) @project.update_attributes(bitcoin_address: address, address_label: label) end diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb index f585efb9..8233c207 100644 --- a/app/mailers/user_mailer.rb +++ b/app/mailers/user_mailer.rb @@ -10,6 +10,6 @@ def new_tip user, tip def security_issue(user) @user = user - mail to: user.email, subject: "Security issue on peer4commit.com" + mail to: user.email, subject: "Security issue on prime4commit.com" end end diff --git a/app/views/home/audit.html.haml b/app/views/home/audit.html.haml index 1e22d9cb..23e3b6ce 100644 --- a/app/views/home/audit.html.haml +++ b/app/views/home/audit.html.haml @@ -16,7 +16,7 @@ %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 + %abbr{title: "#{CONFIG["our_fee"]*100}% Prime4commit maintenance fee, used to pay maintenance, hosting and transaction fees."} Prime4commit 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 diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index b9da5b6a..3adae997 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -7,7 +7,7 @@ %meta{content: "", name: "author"}/ %link{href: "/favicon.png", rel: "shortcut icon"}/ - %title= "Peer4Commit — " + (content_for?(:title) ? yield(:title) : "Contribute to Open Source") + %title= "Prime4commit — " + (content_for?(:title) ? yield(:title) : "Contribute to Open Source") %meta{name: 'description', content: (content_for?(:title) ? yield(:title) : "Donate primecoins to open source projects or make commits and get tips for it.")} %meta{name: 'keywords', content: 'open source,contribute,github,community,git,bitcoin,primecoin,xpm,tips,perks'} @@ -25,7 +25,7 @@ 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-11108334-6', 'peer4commit.com'); + ga('create', 'UA-11108334-7', 'prime4commit.com'); ga('send', 'pageview'); .container .masthead @@ -39,7 +39,7 @@ = 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 Peer4Commit + %h3.text-muted.code-pro Prime4commit = render 'common/menu' = render_flash_message = yield @@ -47,8 +47,8 @@ .footer %p © - = link_to 'Peer4commit', 'http://peer4commit.com/', target: '_blank' - 2014. Source code is available at #{link_to('github', 'https://github.com/sigmike/peer4commit', target: '_blank')}, + = link_to 'Prime4commit', 'http://prime4commit.com/', target: '_blank' + 2014. Source code is available at #{link_to('github', 'https://github.com/sigmike/prime4commit', target: '_blank')}, based on #{link_to "Tip4commit", "http://tip4commit.com/"}. You can support its development with = link_to('peercoins', 'http://peer4commit.com/projects/16') diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index 27aa39fb..0264ae32 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -129,6 +129,6 @@ / AddThis Button END %h4 Embed in README.md - %p= link_to image_tag(project_url(@project, format: :svg), alt: 'Peer4Commit'), project_url(@project) + %p= link_to image_tag(project_url(@project, format: :svg), alt: 'Prime4commit'), project_url(@project) %p %input.form-control{type: 'text', value: "[![tip for next commit](#{project_url(@project, format: :svg)})](#{project_url(@project)})"} diff --git a/app/views/projects/show.svg.erb b/app/views/projects/show.svg.erb index a7c8ff78..38f35d2b 100644 --- a/app/views/projects/show.svg.erb +++ b/app/views/projects/show.svg.erb @@ -73,7 +73,7 @@ - peer4commit + prime4commit diff --git a/app/views/user_mailer/new_tip.html.haml b/app/views/user_mailer/new_tip.html.haml index 304a2d65..1c3aa531 100644 --- a/app/views/user_mailer/new_tip.html.haml +++ b/app/views/user_mailer/new_tip.html.haml @@ -8,7 +8,7 @@ %p Thanks for contributing to Open Source! -%p= link_to "peer4commit.com", "http://peer4commit.com/" +%p= link_to "prime4commit.com", "http://prime4commit.com/" %p %small diff --git a/app/views/user_mailer/security_issue.html.haml b/app/views/user_mailer/security_issue.html.haml index 8be51317..313c869a 100644 --- a/app/views/user_mailer/security_issue.html.haml +++ b/app/views/user_mailer/security_issue.html.haml @@ -1,6 +1,6 @@ %h4 Hello #{@user.full_name}, -%p We recently discovered a security issue on Peer4commit. This issue allowed someone to change the Primecoin address of other users. +%p We recently discovered a security issue on Prime4commit. This issue allowed someone to change the Primecoin address of other users. %p The problem is now fixed. To ensure our database is clean we decided to clear all the addresses. @@ -11,7 +11,7 @@ %p Sorry for this inconvenience. -%p= link_to "peer4commit.com", "http://peer4commit.com/" +%p= link_to "prime4commit.com", "http://prime4commit.com/" %p %small diff --git a/config/config.yml.sample b/config/config.yml.sample index 88ba88ec..522a8464 100644 --- a/config/config.yml.sample +++ b/config/config.yml.sample @@ -42,4 +42,4 @@ address_versions: # 23/83 for primecoin, 111/196 for testnet, see base58.h - 111 - 196 -# canonical_host: peer4commit.example.com # will redirect all other hostnames to this one +# canonical_host: prime4commit.example.com # will redirect all other hostnames to this one diff --git a/config/database.yml.sample b/config/database.yml.sample index 35ef0e07..13f30c76 100644 --- a/config/database.yml.sample +++ b/config/database.yml.sample @@ -21,7 +21,7 @@ test: production: adapter: mysql2 encoding: utf8 - database: peer4commit + database: prime4commit username: root password: socket: /var/run/mysqld/mysqld.sock diff --git a/config/deploy.rb b/config/deploy.rb index 85a44849..05be758d 100644 --- a/config/deploy.rb +++ b/config/deploy.rb @@ -1,5 +1,5 @@ set :application, 't4c' -set :repo_url, 'git@github.com:sigmike/peer4commit.git' +set :repo_url, 'git@github.com:sigmike/prime4commit.git' # ask :branch, proc { `git rev-parse --abbrev-ref HEAD`.chomp } diff --git a/db/migrate/20140215094549_initialize_project_address_label.rb b/db/migrate/20140215094549_initialize_project_address_label.rb index 8bcc6116..ee5ee560 100644 --- a/db/migrate/20140215094549_initialize_project_address_label.rb +++ b/db/migrate/20140215094549_initialize_project_address_label.rb @@ -1,5 +1,5 @@ class InitializeProjectAddressLabel < ActiveRecord::Migration def up - execute "UPDATE projects SET address_label=(full_name || '@peer4commit') WHERE address_label IS NULL" + execute "UPDATE projects SET address_label=(full_name || '@prime4commit') WHERE address_label IS NULL" end end From 0b347b10a6f98feaf30d6e0aa72d2bbb5f91b586 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 13 Apr 2014 16:43:32 +0200 Subject: [PATCH 143/372] PPC to XPM --- app/helpers/application_helper.rb | 2 +- app/helpers/projects_helper.rb | 2 +- config/application.rb | 2 +- config/config.yml.sample | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index c00ec64c..ea8ed9e3 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -7,7 +7,7 @@ def btc_human amount, options = {} precision = options[:precision] || 2 display_currency = options.fetch(:display_currency, true) btc = "%.#{precision}f" % to_btc(amount) - btc += " PPC" if display_currency + btc += " XPM" if display_currency btc = "#{btc}" if currency btc = "#{btc}" if nobr btc.html_safe diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 2de77218..12d288ca 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 - "%.#{6 - btc_amount.to_i.to_s.length}f PPC" % btc_amount + "%.#{6 - btc_amount.to_i.to_s.length}f XPM" % btc_amount end def shield_color project diff --git a/config/application.rb b/config/application.rb index e06ec41b..12e69025 100644 --- a/config/application.rb +++ b/config/application.rb @@ -8,7 +8,7 @@ CONFIG ||= YAML::load(File.open("config/config.yml")) -COIN = 1000000 # ppcoin/src/util.h +COIN = 100000000 # primecoin/src/util.h module T4c class Application < Rails::Application diff --git a/config/config.yml.sample b/config/config.yml.sample index 522a8464..db07b9af 100644 --- a/config/config.yml.sample +++ b/config/config.yml.sample @@ -7,7 +7,7 @@ daemon: password: rpcpassword host: localhost port: 9904 - path: /path/to/ppcoin/src/ppcoind + path: /path/to/primecoin/src/primecoind devise: secret: "111111111111" @@ -34,7 +34,7 @@ exception_email: admin@example.com # an email will be sent to this address if an # host: errbit.tip4commit.com tip: 0.01 -min_payout: 1.0 # in PPC +min_payout: 1.0 # in XPM our_fee: 0.05 tipper_delay: "1.hour" From e3afb1d195c69d33e99f54bd9c8923c49f31731f Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 13 Apr 2014 16:43:38 +0200 Subject: [PATCH 144/372] primcoin COIN --- app/helpers/application_helper.rb | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index ea8ed9e3..b7397704 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -26,18 +26,16 @@ def to_btc satoshies end def transaction_url(txid) - "http://bkchain.org/ppc/tx/#{txid}" + "https://coinplorer.com/XPM/Transactions/#{txid}" end def address_explorers - [:bkchain, :blockr, :cryptocoin] + [:coinplorer] end def address_url(address, explorer = address_explorers.first) case explorer - when :blockr then "http://ppc.blockr.io/address/info/#{address}" - when :bkchain then "http://bkchain.org/ppc/address/#{address}" - when :cryptocoin then "http://ppc.cryptocoinexplorer.com/address/#{address}" + when :coinplorer then "https://coinplorer.com/XPM/Addresses/#{address}" else raise "Unknown provider: #{provider.inspect}" end end From c1396a6e48158e71e7511b17a6c3e7747edb8088 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 13 Apr 2014 16:54:55 +0200 Subject: [PATCH 145/372] primecoin default port --- config/config.yml.sample | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/config.yml.sample b/config/config.yml.sample index db07b9af..c69c3f40 100644 --- a/config/config.yml.sample +++ b/config/config.yml.sample @@ -6,7 +6,7 @@ daemon: username: rpcuser password: rpcpassword host: localhost - port: 9904 + port: 9914 path: /path/to/primecoin/src/primecoind devise: From baaeae58f5cfb5e79827f28717825f9d5268477e Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 13 Apr 2014 16:55:04 +0200 Subject: [PATCH 146/372] prime is bigger than peer --- app/helpers/projects_helper.rb | 2 +- app/views/projects/show.svg.erb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 12d288ca..8843dfe8 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 - "%.#{6 - btc_amount.to_i.to_s.length}f XPM" % btc_amount + "%.#{5 - btc_amount.to_i.to_s.length}f XPM" % btc_amount end def shield_color project diff --git a/app/views/projects/show.svg.erb b/app/views/projects/show.svg.erb index 38f35d2b..5966f962 100644 --- a/app/views/projects/show.svg.erb +++ b/app/views/projects/show.svg.erb @@ -71,7 +71,7 @@ - + prime4commit From c71871e66c68b4c79fb9691f347a10e03519c46e Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Mon, 14 Apr 2014 08:14:38 +0200 Subject: [PATCH 147/372] started faq page --- Gemfile | 1 + Gemfile.lock | 8 ++++++++ app/views/home/faq.html.md | 10 ++++++++++ app/views/home/index.html.haml | 2 +- config/routes.rb | 1 + 5 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 app/views/home/faq.html.md diff --git a/Gemfile b/Gemfile index ff43a892..cc683417 100644 --- a/Gemfile +++ b/Gemfile @@ -71,6 +71,7 @@ gem 'rqrcode-rails3' gem 'exception_notification' gem 'rack-canonical-host' gem 'bootstrap_form', github: 'sigmike/rails-bootstrap-forms', branch: 'removed_for_on_radio_label' +gem 'html_pipeline_rails' group :test do gem 'cucumber-rails', :require => false diff --git a/Gemfile.lock b/Gemfile.lock index 2ae3c8d5..fc1e9e2c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -127,6 +127,7 @@ GEM multipart-post (~> 1.2.0) gherkin (2.12.2) multi_json (~> 1.3) + github-markdown (0.6.5) haml (4.0.5) tilt haml-rails (0.5.3) @@ -136,6 +137,12 @@ GEM railties (>= 4.0.1) hashie (2.0.5) hike (1.2.3) + html-pipeline (1.8.0) + activesupport (>= 2) + nokogiri (~> 1.4) + html_pipeline_rails (0.1.0) + github-markdown + html-pipeline httparty (0.12.0) json (~> 1.8) multi_xml (>= 0.5.2) @@ -302,6 +309,7 @@ DEPENDENCIES exception_notification factory_girl_rails haml-rails + html_pipeline_rails httparty jbuilder (~> 1.2) jquery-rails diff --git a/app/views/home/faq.html.md b/app/views/home/faq.html.md new file mode 100644 index 00000000..da235d6d --- /dev/null +++ b/app/views/home/faq.html.md @@ -0,0 +1,10 @@ +Peer4commit FAQ +=============== + +What is Peer4Commit? +-------------------- +With Peer4Commit you can add projects from GitHub and donate Peercoins to the ones that interest you the most. Anyone that submits code changes and has them accepted will receive Peercoin tips. This helps in providing an incentive for developers to work on important projects that will benefit Peercoin in the future. Peer4commit was adapted by Sigmike from Tip4commit. + +What is a Commit? +----------------- +Each time someone adds changes to the source code of a supported project, he receives 1% of the project balance. A set of changes is called a "commit". Here is an example commit for Peercoin v0.4: https://github.com/ppcoin/ppcoin/commit/5941effd0085dd26ce9b793ec09dcaffae8e5678 diff --git a/app/views/home/index.html.haml b/app/views/home/index.html.haml index d33c4960..e26975de 100644 --- a/app/views/home/index.html.haml +++ b/app/views/home/index.html.haml @@ -27,7 +27,7 @@ %h2 How it works? %p People donate peercoins to projects. When someone's commit is accepted to the project repository, we automatically tip the author. %p - %a.btn.btn-primary{href: "http://peercoin.net/", target: '_blank'} Learn about Peercoin » + %a.btn.btn-primary{href: faq_path} Frequently Asked Questions » .col-lg-4 %h2 Donate %p diff --git a/config/routes.rb b/config/routes.rb index e962f7f3..294468d4 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -3,6 +3,7 @@ root 'home#index' get 'audit' => 'home#audit' + get 'faq' => 'home#faq' resources :users, :only => [:show, :update, :index] do collection do From a1223f26c10428df921804f31f5ec3e09d23737e Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Fri, 18 Apr 2014 08:23:47 +0200 Subject: [PATCH 148/372] imported sentinelrv's faq --- app/views/home/faq.html.md | 58 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/app/views/home/faq.html.md b/app/views/home/faq.html.md index da235d6d..0c666521 100644 --- a/app/views/home/faq.html.md +++ b/app/views/home/faq.html.md @@ -8,3 +8,61 @@ With Peer4Commit you can add projects from GitHub and donate Peercoins to the on What is a Commit? ----------------- Each time someone adds changes to the source code of a supported project, he receives 1% of the project balance. A set of changes is called a "commit". Here is an example commit for Peercoin v0.4: https://github.com/ppcoin/ppcoin/commit/5941effd0085dd26ce9b793ec09dcaffae8e5678 + +How do I Receive a Tip for my Commit? +------------------------------------- +We use the email address included in the commit to identify the author and notify him. To receive the tip, the author must follow the link in the email he received and set his Peercoin address. If he doesn't do that within 1 month, the tip goes back to the project balance. + +How do I Donate to a Project I Like? +------------------------------------ +You can see all supported projects here: http://peer4commit.com/projects. To donate to a specific project, open up the page for that project and just send Peercoins to the address that is displayed. For an example, check out Peercoin's main project page: http://peer4commit.com/projects/19. If the project you want to donate to is not supported yet, go to the supported projects page and just copy/paste its GitHub URL (For example: https://github.com/ppcoin/ppcoin) into the input box above the list. Anyone can add a project to Peer4commit, even if you are not the project maintainer. 99% of your donation will be given as tips. 1% will be kept to host the website and pay the transaction fees. + +How do I Push my Commits? +------------------------- +Getting write access to the "Master" of a project involves that the project maintainer provides access to you in Github. This type of access would only be given to people trusted by the maintainer. If you already have write access to the project, just push your commits to the default branch of the project as usual. Otherwise, you'll have to fork the project (i.e. Start your own project based on the supported project), make some changes and create a pull request to propose your changes. If your pull request is accepted (Merged), you'll receive one tip per commit. If the changes are simple enough, you can do them in your browser by editing the files on GitHub. Otherwise, you'll have to use Git to clone your fork, make some changes, commit them and push the commits to GitHub. You can find a lot of information about that on GitHub help and on the web. + +Make Sure You Read the Project Charter & Tipping Policies Before Starting +------------------------------------------------------------------------- +Project maintainers can refuse your commits for several reasons. It is important to read the "Charter" of the project on its GitHub page, which usually provides guidance on which commits and under what rules they would be accepted. For example, there are very strict rules for contributing to the official ppcoin/ppcoin project. A good way to ensure the maintainer is willing to merge your changes is to first create an issue explaining what you're going to do and ask if they would merge a pull request. Wait for an answer before starting. The project owner can also edit the Tipping Policies section on their Peer4commit project page to include more information on what kind of commits will be tipped. So it's important to read both the project charter on GitHub and the tipping policies that are listed on Peer4commit. + +Can Project Owners Change the Amount Donated to Each Commit? +------------------------------------------------------------ +Yes, they have a new button "Change project settings" on the project page along the project name. In this screen they can change 2 things (for now): + +* A text describing their tipping policies that will be displayed on the project page on peer4commit. +* A checkbox that will put all new tips on hold when commits are found. + +When the checkbox is active, each new commit generates an "Undecided" tip and the authors are not notified. The project owners can then click on a new button on the project page to decide the tip amounts. They have these choices: + +* Leave undecided (To decide later) +* Free (The commit won't get any tip and the author won't be notified) +* Tiny: 0.1% of the project balance. +* Small: 0.5% of the project balance. +* Normal: 1% +* Big: 2% +* Huge: 5% + +The authors are notified when the tip amount is decided (Unless they have recently been notified already, or if they said they don't want any more notification, or if they have configured their Peercoin address). The 2 buttons are only available to project collaborators (Those who can push changes to the supported repository). There should be more options in the future. Your ideas are welcome. + +Do You Have an Audit Page Setup for Peer4commit? +------------------------------------------------ +Yes, Peer4commit does have an audit page. It shows different information, such as amount donated, available balance, transaction fees, amount in cold storage and includes addresses for each project. You can view the page here: http://peer4commit.com/audit. + +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 get more tips 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. This is still a manual operation, but will soon be automated. The website runs in an isolated virtual server running only this service. + +Conclusion +----------- +The commit may not be the best item to identify the value of a contribution, but it's a very convenient way to identify contributors and send them donations. The maintainer of the project doesn't even have to do anything. Supporters can add a project from GitHub and start donating without any extra work on the project side (Except setting their address if they want the tips). A commit can include very important changes that took a very long time to build (Like the v0.4 changes) or a very small change like adding a comma. + +Contact +------- +If you have any questions, either post them in this thread, message Sigmike on PeercoinTalk.org: http://www.peercointalk.org/index.php?action=profile;u=30141 on Reddit: http://www.reddit.com/user/sigmike or open an issue on GitHub: https://github.com/sigmike/peer4commit/issues. + +References: +----------- +Fork: https://help.github.com/articles/fork-a-repo +Pull Request: https://help.github.com/articles/using-pull-requests +Git: https://help.github.com/articles/set-up-git +Github Help: https://help.github.com/ From 9d5e6629da0d87ab6ce717699234321a5ee8943e Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Mon, 21 Apr 2014 13:15:26 +0200 Subject: [PATCH 149/372] Give tips to the existing GitHub user if he is known --- app/models/project.rb | 11 +++++++---- features/commit_from_known_nickname.feature | 16 ++++++++++++++++ .../commit_from_known_nickname.rb | 16 ++++++++++++++++ features/step_definitions/common.rb | 4 ++++ 4 files changed, 43 insertions(+), 4 deletions(-) create mode 100644 features/commit_from_known_nickname.feature create mode 100644 features/step_definitions/commit_from_known_nickname.rb diff --git a/app/models/project.rb b/app/models/project.rb index 105d958d..e3e704b8 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -91,7 +91,10 @@ def tip_commits def tip_for commit email = commit.commit.author.email - user = User.find_by email: email + if nickname = commit.author.try(:login) + user = User.find_by(nickname: nickname) + end + user ||= User.find_by(email: email) if (next_tip_amount > 0) && Tip.find_by(commit: commit.sha).nil? @@ -103,12 +106,12 @@ def tip_for commit email: email, password: generated_password, name: commit.commit.author.name, - nickname: (commit.author.login rescue nil) + nickname: nickname, }) end - if commit.author && commit.author.login - user.update nickname: commit.author.login + if nickname + user.update nickname: nickname end if hold_tips 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/step_definitions/commit_from_known_nickname.rb b/features/step_definitions/commit_from_known_nickname.rb new file mode 100644 index 00000000..e4700b82 --- /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| + Tip.find_by_commit!(arg1).user.nickname.should eq(arg2) +end + +Then(/^there should be no user with email "(.*?)"$/) do |arg1| + User.where(email: arg1).size.should eq(0) +end + diff --git a/features/step_definitions/common.rb b/features/step_definitions/common.rb index 095f7247..632b3f27 100644 --- a/features/step_definitions/common.rb +++ b/features/step_definitions/common.rb @@ -78,6 +78,10 @@ def find_new_commit(id) 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 @project.should_receive(:new_commits).and_return(@new_commits.values.map(&:to_ostruct)) From 3505d6bc79137b9dad24f266af8c7796afec6ef5 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Mon, 21 Apr 2014 13:36:41 +0200 Subject: [PATCH 150/372] Task to reassign tips sent to noreply GitHub addresses --- lib/tasks/reassign_noreply_tips.rake | 37 ++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 lib/tasks/reassign_noreply_tips.rake 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 From 8b73932043b68a5ea1da38c628c1e4a853a3c1c0 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Mon, 21 Apr 2014 13:54:48 +0200 Subject: [PATCH 151/372] Fixed rounding in tests --- features/tip_modifier_interface.feature | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/tip_modifier_interface.feature b/features/tip_modifier_interface.feature index 26b2fbaa..571047bc 100644 --- a/features/tip_modifier_interface.feature +++ b/features/tip_modifier_interface.feature @@ -102,7 +102,7 @@ Feature: A project collaborator can change the tips of commits 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.088338" for commit "last" + And there should be a tip of "8.08833862" for commit "last" Scenario Outline: A collaborator changes the amount of a tip on another project Given the project holds tips From 11694223e1d790ee7733b874908c313e15d52bb3 Mon Sep 17 00:00:00 2001 From: aditya-kapoor Date: Sun, 23 Feb 2014 02:17:47 +0530 Subject: [PATCH 152/372] add before_filter and use where instead of find Conflicts: app/controllers/projects_controller.rb --- app/controllers/projects_controller.rb | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 417f9e96..9745eef7 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -1,13 +1,15 @@ require 'net/http' class ProjectsController < ApplicationController + + before_action :load_project, only: [:show, :qrcode] + def index @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 and @project.bitcoin_address.nil? and (github_id = @project.github_id).present? + if @project.bitcoin_address.nil? and (github_id = @project.github_id).present? label = "#{github_id}@peer4commit" address = BitcoinDaemon.instance.get_new_address(label) @project.update_attributes(bitcoin_address: address, address_label: label) @@ -51,9 +53,8 @@ def decide_tip_amounts end def qrcode - @project = Project.enabled.find params[:id] respond_to do |format| - format.svg { render :qrcode => @project.bitcoin_address, level: :l, unit: 4 } + format.svg { render qrcode: @project.bitcoin_address, level: :l, unit: 4 } end end @@ -79,4 +80,11 @@ def create def project_params params.require(:project).permit(: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 From 7f224d1a3f9052b459c67f223418339fe1ec2606 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 4 May 2014 10:18:55 +0200 Subject: [PATCH 153/372] Use load_project on other actions --- app/controllers/projects_controller.rb | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 9745eef7..4ac08907 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -2,7 +2,7 @@ class ProjectsController < ApplicationController - before_action :load_project, only: [:show, :qrcode] + before_action :load_project, only: [:show, :qrcode, :edit, :update, :decide_tip_amounts] def index @projects = Project.enabled.order(available_amount_cache: :desc, watchers_count: :desc, full_name: :asc).page(params[:page]).per(30) @@ -17,12 +17,10 @@ def show end def edit - @project = Project.enabled.find params[:id] authorize! :update, @project end def update - @project = Project.enabled.find params[:id] authorize! :update, @project @project.attributes = project_params if @project.tipping_policies_text.try(:text_changed?) @@ -36,7 +34,6 @@ def update end def decide_tip_amounts - @project = Project.enabled.find params[:id] authorize! :decide_tip_amounts, @project if request.patch? @project.available_amount # preload anything required to get the amount, otherwise it's loaded during the assignation and there are undesirable consequences From 2e5cfa14a8de264e0216e8c967802757cbc1f29b Mon Sep 17 00:00:00 2001 From: aditya-kapoor Date: Sun, 23 Feb 2014 02:30:35 +0530 Subject: [PATCH 154/372] use new ruby syntax and remove find in favor for where --- app/controllers/users_controller.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 54be8c89..8ed0650e 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -1,7 +1,7 @@ class UsersController < ApplicationController before_action except: [:login, :index] do - @user = User.find params[:id] + @user = User.where(id: params[:id]).first unless current_user && current_user == @user redirect_to root_path end @@ -11,11 +11,11 @@ def show 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) + if @user.update(users_params) redirect_to @user, notice: 'Your information saved!' else render :show, alert: 'Error updating peercoin address' @@ -23,13 +23,13 @@ def update 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 peercoin address to get your tips.' end + sign_in_and_redirect @user, event: :authentication else redirect_to root_url, alert: 'User not found' end From 5620289fe46bbfba575d388cb6a53dd657b2dfb8 Mon Sep 17 00:00:00 2001 From: Kuldeep Aggarwal Date: Sun, 23 Feb 2014 21:34:22 +0530 Subject: [PATCH 155/372] - create unique login token for each user - optimize login_token generation Conflicts: app/models/user.rb --- app/models/user.rb | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/app/models/user.rb b/app/models/user.rb index a94d639b..b4ca7dde 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -10,18 +10,15 @@ class User < ActiveRecord::Base has_many :tips + # Callbacks + before_create :generate_login_token!, unless: :login_token? + def github_url "https://github.com/#{nickname}" end def balance - tips.unpaid.sum(:amount) - end - - after_create :generate_login_token, unless: :login_token - - def generate_login_token - self.update login_token: SecureRandom.urlsafe_base64 + tips.unpaid.sum(:amount) end def full_name @@ -39,4 +36,13 @@ def self.update_cache end end + private + + def generate_login_token! + loop do + self.login_token = SecureRandom.urlsafe_base64 + break login_token unless User.exists?(login_token: login_token) + end + end + end From 2e2934bf92971b6e8e124f546a0e36427e3be9fe Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 4 May 2014 10:33:53 +0200 Subject: [PATCH 156/372] Removed useless parts --- app/models/user.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/models/user.rb b/app/models/user.rb index b4ca7dde..4a6f3f26 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -10,7 +10,6 @@ class User < ActiveRecord::Base has_many :tips - # Callbacks before_create :generate_login_token!, unless: :login_token? def github_url @@ -41,7 +40,7 @@ def self.update_cache def generate_login_token! loop do self.login_token = SecureRandom.urlsafe_base64 - break login_token unless User.exists?(login_token: login_token) + break unless User.exists?(login_token: login_token) end end From 6cd64ddb58671a9019dfb226fcfb61befac1a176 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 4 May 2014 15:18:03 +0200 Subject: [PATCH 157/372] Fixed indentation --- app/models/project.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/project.rb b/app/models/project.rb index e3e704b8..0d350f6f 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -102,12 +102,12 @@ def tip_for commit # create user unless user generated_password = Devise.friendly_token.first(8) - user = User.create({ + user = User.create( email: email, password: generated_password, name: commit.commit.author.name, nickname: nickname, - }) + ) end if nickname From 3d016ea13039dae3a583e76d00a43d11dd3ce1e9 Mon Sep 17 00:00:00 2001 From: Kuldeep Aggarwal Date: Tue, 25 Feb 2014 10:24:33 +0530 Subject: [PATCH 158/372] indent comments - remove I18n.enforce_available_locales depricated warning. --- config/application.rb | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/config/application.rb b/config/application.rb index e06ec41b..59da21f9 100644 --- a/config/application.rb +++ b/config/application.rb @@ -13,18 +13,20 @@ 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)' + # 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 + # 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 - config.autoload_paths += %W(#{config.root}/lib) + config.autoload_paths += %W(#{config.root}/lib) + + I18n.enforce_available_locales = false end end From f418eef034993fc7b532f4a937938506d6aad94a Mon Sep 17 00:00:00 2001 From: Kuldeep Aggarwal Date: Tue, 25 Feb 2014 10:51:07 +0530 Subject: [PATCH 159/372] Fix- generate proper HTML Previously html attributes is put inside the "html" option which was generating wrong html
Now I removed the html options and passed html attributes directly:
Conflicts: app/views/projects/index.html.haml --- app/views/projects/index.html.haml | 2 +- app/views/users/show.html.haml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/projects/index.html.haml b/app/views/projects/index.html.haml index 4d12f265..0ec8db44 100644 --- a/app/views/projects/index.html.haml +++ b/app/views/projects/index.html.haml @@ -1,6 +1,6 @@ %h1 Supported Projects %p - = form_tag projects_path, html: {role: 'form', class: 'form-inline'}, method: :post do |f| + = form_tag projects_path, role: 'form', class: 'form-inline', method: :post do |f| .form-group .row .col-lg-10 diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index 1c751f2b..e126f892 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -10,7 +10,7 @@ %p %strong E-mail %p= @user.email -= form_for @user, html: {role: 'form', class: 'form-inline"'} do |f| += form_for @user, html: {role: 'form', class: 'form-inline'} do |f| - if @user.errors.size > 0 .alert.alert-danger Peercoin address is invalid. .form-group From f670e238d61319fc6196a7b6dce5a68b5e680b2a Mon Sep 17 00:00:00 2001 From: Hibero Date: Thu, 27 Feb 2014 21:06:51 -0600 Subject: [PATCH 160/372] Make the peer4commit tags more readable I personally find the peer4commit and the tip4commit tags very small and hard to read. It is hard for me to test but I think this code will help out. --- app/views/projects/show.svg.erb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/views/projects/show.svg.erb b/app/views/projects/show.svg.erb index a7c8ff78..cbe9bd1f 100644 --- a/app/views/projects/show.svg.erb +++ b/app/views/projects/show.svg.erb @@ -1,7 +1,7 @@ @@ -71,16 +71,16 @@ - - + + peer4commit - + - + <%= shield_btc_amount @project.next_tip_amount %> From fb0b332abe69d87aeab93b82a47beb5e9e538e45 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 4 May 2014 15:27:06 +0200 Subject: [PATCH 161/372] Better tag alignment and upcased the P --- app/views/projects/show.svg.erb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/views/projects/show.svg.erb b/app/views/projects/show.svg.erb index cbe9bd1f..38dd757c 100644 --- a/app/views/projects/show.svg.erb +++ b/app/views/projects/show.svg.erb @@ -71,16 +71,16 @@ - - - peer4commit + + + Peer4commit - + <%= shield_btc_amount @project.next_tip_amount %> From f3974d7c3c3e48c16c575b1e14ec7c73f9716dbb Mon Sep 17 00:00:00 2001 From: Jacob Wright Date: Thu, 27 Feb 2014 21:23:59 -0600 Subject: [PATCH 162/372] Changed favicon I changed the favicon to the Peercoin one. --- public/favicon.ico | Bin 0 -> 1150 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/public/favicon.ico b/public/favicon.ico index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..2994337f63a9ecdafe1e69d6cf0c0a06b896e9e1 100644 GIT binary patch literal 1150 zcmcgnu?@mN47>;=N>V`y6@mdWL6%_)*oKB7=x7)sBc!B2Jm-WH)+_l03E{{&+vhtc zK*lG}!S@>bDX;*rWpczxJ0}3-{uRsHGq~K1z3Vhy-_%1MDXq9Z5AnkA4)q?pJUnl; zBA%;{Jv0}7qnE~`^g_>B?eE8&^@`1@2*tT Date: Sun, 4 May 2014 15:41:37 +0200 Subject: [PATCH 163/372] Converted SCSS to SASS and moved stylesheet put in wrong place --- app/assets/stylesheets/bootstrap_and_overrides.css.less | 2 -- app/assets/stylesheets/{home.css.scss => home.css.sass} | 5 ++--- app/assets/stylesheets/projects.css.sass | 8 ++++++++ app/assets/stylesheets/projects.css.scss | 7 ------- .../stylesheets/{sessions.css.scss => sessions.css.sass} | 0 app/assets/stylesheets/{users.css.scss => users.css.sass} | 5 ++--- 6 files changed, 12 insertions(+), 15 deletions(-) rename app/assets/stylesheets/{home.css.scss => home.css.sass} (80%) create mode 100644 app/assets/stylesheets/projects.css.sass delete mode 100644 app/assets/stylesheets/projects.css.scss rename app/assets/stylesheets/{sessions.css.scss => sessions.css.sass} (100%) rename app/assets/stylesheets/{users.css.scss => users.css.sass} (79%) diff --git a/app/assets/stylesheets/bootstrap_and_overrides.css.less b/app/assets/stylesheets/bootstrap_and_overrides.css.less index b60cf8df..b7a52835 100644 --- a/app/assets/stylesheets/bootstrap_and_overrides.css.less +++ b/app/assets/stylesheets/bootstrap_and_overrides.css.less @@ -27,5 +27,3 @@ // // Example: // @linkColor: #ff0000; -.qrcode {text-align:center} -.panel-body {overflow:auto} diff --git a/app/assets/stylesheets/home.css.scss b/app/assets/stylesheets/home.css.sass similarity index 80% rename from app/assets/stylesheets/home.css.scss rename to app/assets/stylesheets/home.css.sass index af3318d2..8f94af0d 100644 --- a/app/assets/stylesheets/home.css.scss +++ b/app/assets/stylesheets/home.css.sass @@ -2,6 +2,5 @@ // They will automatically be included in application.css. // You can use Sass (SCSS) here: http://sass-lang.com/ -td.money, th.money { - text-align: right; -} +td.money, th.money + text-align: right diff --git a/app/assets/stylesheets/projects.css.sass b/app/assets/stylesheets/projects.css.sass new file mode 100644 index 00000000..e24db110 --- /dev/null +++ b/app/assets/stylesheets/projects.css.sass @@ -0,0 +1,8 @@ +.commit-sha + font-family: monospace + +.qrcode + text-align: center + +.panel-body + overflow: auto diff --git a/app/assets/stylesheets/projects.css.scss b/app/assets/stylesheets/projects.css.scss deleted file mode 100644 index 6d95023e..00000000 --- a/app/assets/stylesheets/projects.css.scss +++ /dev/null @@ -1,7 +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/ - -.commit-sha { - font-family: monospace; -} 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/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 9c4ccae2..9d985b83 100644 --- a/app/assets/stylesheets/users.css.scss +++ b/app/assets/stylesheets/users.css.sass @@ -2,6 +2,5 @@ // They will automatically be included in application.css. // You can use Sass (SCSS) here: http://sass-lang.com/ -.send-tips-back-block { - margin-top: 50px; -} +.send-tips-back-block + margin-top: 50px From 677fa537744e623164fe35e85d5ee47341935173 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 4 May 2014 15:46:44 +0200 Subject: [PATCH 164/372] Break address to new line if necessary --- app/assets/stylesheets/projects.css.sass | 5 ++++- app/views/projects/show.html.haml | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/app/assets/stylesheets/projects.css.sass b/app/assets/stylesheets/projects.css.sass index e24db110..e06f5961 100644 --- a/app/assets/stylesheets/projects.css.sass +++ b/app/assets/stylesheets/projects.css.sass @@ -4,5 +4,8 @@ .qrcode text-align: center -.panel-body +.project-panel overflow: auto + + .bitcoin-address + word-wrap: break-word diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index c42f63e4..77840868 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -28,13 +28,13 @@ %p Reason: #{reason} - else - .panel.panel-default + .panel.panel-default.project-panel .panel-heading %h4.panel-title Project Sponsors .panel-body.text-center %p To give to this project, send peercoins to this address: - %p + %p.bitcoin-address = @project.bitcoin_address %p = image_tag qrcode_project_path(@project, format: :svg), alt: @project.bitcoin_address, class: 'project qrcode' From 28f32186a51971db912b16783f51d6c29ff268f1 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 4 May 2014 16:01:19 +0200 Subject: [PATCH 165/372] Email sender is taken from config --- config/config.yml.sample | 1 + config/environments/production.rb | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/config/config.yml.sample b/config/config.yml.sample index 71db0e52..433bdd96 100644 --- a/config/config.yml.sample +++ b/config/config.yml.sample @@ -24,6 +24,7 @@ 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 diff --git a/config/environments/production.rb b/config/environments/production.rb index c1eddcd8..d12d4fd2 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -68,7 +68,7 @@ config.action_mailer.perform_deliveries = true config.action_mailer.raise_delivery_errors = true - config.action_mailer.default_options = {from: default_from = 'no-reply@' + default_hostname } + config.action_mailer.default_options = {from: default_from = CONFIG['default_from'] || ('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). From 85e87f27ac9a48fe9324d1a1d49a1b0a4255d0a6 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 4 May 2014 16:04:49 +0200 Subject: [PATCH 166/372] Only use config if non empty --- config/environments/production.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/environments/production.rb b/config/environments/production.rb index d12d4fd2..d7e4b501 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -68,7 +68,7 @@ config.action_mailer.perform_deliveries = true config.action_mailer.raise_delivery_errors = true - config.action_mailer.default_options = {from: default_from = CONFIG['default_from'] || ('no-reply@' + default_hostname) } + 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). From 7207a85c6079c55fd5d37bab1a10a97e16ab66a7 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 4 May 2014 16:22:26 +0200 Subject: [PATCH 167/372] Allow show on disabled projects --- app/controllers/projects_controller.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 4ac08907..7b0f1ae3 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -9,6 +9,11 @@ def index end def show + @project = Project.where(id: params[:id]).first + unless @project + redirect_to root_path, alert: "Project not found" + return + end if @project.bitcoin_address.nil? and (github_id = @project.github_id).present? label = "#{github_id}@peer4commit" address = BitcoinDaemon.instance.get_new_address(label) From 464391b674b85cbd6adc79344fa2331890e183d2 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 4 May 2014 16:22:26 +0200 Subject: [PATCH 168/372] Do not use standard load_project on show --- app/controllers/projects_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 7b0f1ae3..adb77030 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -2,7 +2,7 @@ class ProjectsController < ApplicationController - before_action :load_project, only: [:show, :qrcode, :edit, :update, :decide_tip_amounts] + before_action :load_project, only: [:qrcode, :edit, :update, :decide_tip_amounts] def index @projects = Project.enabled.order(available_amount_cache: :desc, watchers_count: :desc, full_name: :asc).page(params[:page]).per(30) From ebe90896d59047802e21fd0a9c687859bc86f01e Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 4 May 2014 16:29:50 +0200 Subject: [PATCH 169/372] Auto link in project policies --- Gemfile | 1 + Gemfile.lock | 3 +++ app/views/projects/show.html.haml | 2 +- 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index cc683417..4ea14802 100644 --- a/Gemfile +++ b/Gemfile @@ -72,6 +72,7 @@ gem 'exception_notification' gem 'rack-canonical-host' gem 'bootstrap_form', github: 'sigmike/rails-bootstrap-forms', branch: 'removed_for_on_radio_label' gem 'html_pipeline_rails' +gem 'rails_autolink' group :test do gem 'cucumber-rails', :require => false diff --git a/Gemfile.lock b/Gemfile.lock index fc1e9e2c..43d1b201 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -215,6 +215,8 @@ GEM bundler (>= 1.3.0, < 2.0) railties (= 4.0.3) sprockets-rails (~> 2.0.0) + rails_autolink (1.1.5) + rails (> 3.1) railties (4.0.3) actionpack (= 4.0.3) activesupport (= 4.0.3) @@ -323,6 +325,7 @@ DEPENDENCIES quiet_assets rack-canonical-host rails (~> 4.0.2) + rails_autolink rqrcode-rails3 rspec-rails sass-rails (~> 4.0.0) diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index 77840868..b8caa95a 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -53,7 +53,7 @@ - if @project.tipping_policies_text.try(:text).present? %h4 Tipping policies - = simple_format @project.tipping_policies_text.text + = auto_link simple_format @project.tipping_policies_text.text %small %em - user = @project.tipping_policies_text.user From b319534b2feda3752607b3362862b1e4d4b74263 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 4 May 2014 17:20:47 +0200 Subject: [PATCH 170/372] Primecoin favicon --- public/favicon.ico | Bin 1150 -> 96929 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/public/favicon.ico b/public/favicon.ico index 2994337f63a9ecdafe1e69d6cf0c0a06b896e9e1..517623b4591e04cc54bc84d5e17697b2f0e2e58f 100644 GIT binary patch literal 96929 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
89%=k7WH>=5PeWkeBw>XWpTfS1d4uP$K0 zBhiZSt|4s?lz=iv1f}wm0RB}hg};8+KgB{>e=Pi8uahe~fTe?Sz9mQp@XE)3*Z#o^ zJ_z{BSP;>e!15UO0c!*Uy`_z}z$xqqBQ96AhuI!prM>@h!p5sCox zuwuv0S7G0|<;RKWo(i*krMh>dAA zu}JUG@LfYCY1vu0QxJ~Ic4JhrNA^YF1gAuQVlDZUH|>E(00>f6%DM+{fyCfpI%F** ziiw5a7EwkIMvgnFLC4luoDl-A1ALwdFP}h4d1y#;u^6kv0JE6|5H~u7f&g9(%b*Yh zupOWiAR|$$Lv#QnnW%@>$>=qx8((A{06ilSiZkVd27oC^ZKVVnwgT9(@Q)edJ0v%l zG?nV9wIp3wN9G_}Y4_MGegeSTaTxmdJO9@1zb2$T*!^*Z8bE)vK(70GFo+g_AJ;sX%}2}>@)>; zA=P^fI#RS#+<?Eu!hf9C(| z;h!}n+&!lN`TbTDum^A^ResFfe-RG0IZ{W!9CB9J{!<}_+oLUmm=VbQ@8F{l`8-2P z5{M>e8ox)KqFonbwsTgFvoD?RNkdjwo`>hOUF7Rd9y-(k&>)ZKF1~0gipKfPJxOHR zBhhL$4Ur;*^ps{FzmSMTL)CRCfRC-pEZ}EYt4DA|no&iDkgdkICVfbUyb1oe1}D(o zXW?GV5IjO=>4y$UnD3a$#aI)9od~-e7YmZr1Yt%5#^>!Q9$+%_fCJai-cxAF*?o-- zqau~H3=v9$Ew)U)#QV$f1z}i&(kvdhM<=J9m=A$+4dX|N>i<&o)f55ARaatQ3LgBE zfVbMgfJSJ5bdWa|<|*+X1Mq#F@%PpKbp+6d{}=B5=#2R)$GYXzlZM^^VX0pM;E(w4 ze#L+0@V80b1*r!3s~UekD7|$6hH_4xo$bI~OCaMkP-IzMbp+J60(!>&ZY$OWbaIY~ zyEefU)#8+Rg2x;;mckK)?`MVmUR97IE;Z%)-zA^;SF#W;kn>c;#Y#>$+AYdV~Ht4 zD03G03fJAz-*uSP;PXecnQGfLJV{1Fi?Go_SL=%eJbl~KX(%hhW;2SENXZ={p&C|JUk?vl7ZoXBZyUMY402%711GwEi^rI9|xc?|%tKhgEN6mBEp^@{q2yu;4 z1g^djWy@2=3^or@(2bNK8J}SMc*|=25M3pv4Fr7|Y$#0W_7x%pW^6%9)LOiZQ48Xy z=Eg)JS|x5kzzJ5M!HrwM-~*)06!nBb>=qnn6b@;D($do*-XL0zsP6#qpLT0%EBEj7 z|GS0%2gm)?0Q#@%0ig-NUcc^%$$#rPKkFF&O|L<^V0R$6#W(VRXj&k~5=uGlc%%0d zQE=I2f=AUiihFG>WOF_G($gsiDJ1Ce>cD(FHJNKVp?wt8vTDr*q+Z~KIsqgXt$}8A zzMtf!0yyvddLFhdoxuE?-k7d(@B7b8qdLQ)2bYduy%5gzfH&UvZqI!EZ(FV4JeQnF zK>Yu{`?oE2qRSllyA4|Eb8adclcHc-4zi*Z=fvD+bw=Sy+nAD(EjD}e zoRrdrujZeytAN2t++i9+Cqqb+vA9T^bwH-T;)nlgf)cT-mqF$yeRIn%&L=X@pq$ZsbihxQZp>)@b^E|BYTVt&?{yp|S=j{KS{f=?@_Ve6v-SNIm z=WK|4#gJ=$ZCT>Fl*(37w*_~tS|#tHT&G;AI~M$WlC*Ttlc7iY`7#)S}|)E8!NzFlk~htEJ3_ z)2&C}LstSv%nmY*7})z&VZBzd76Vr1~?5#DasN|`$8tjOHcwjoi% zvp>u;XBi`c>*-Q;ho{JXj$6(C<_ctqpjQM&0?q6X*geX-rL7%Ju5viVOHQ-v}Hq}uGfn&@QeE1IhpL~CC^ zHlE$y4wL9-cN6%n)h%7y=9sFlJ0v|JVn1whFUQ?)QRMDfu0~Ow#-A#rG%s`;7Gxx= zF=NHvQ`oad6TeC099TyrX?VNz)AH==x9sGp^=73IB+VY=e`GeKhr?>iNlPYIui1+i zCGwwIqMXn2r`TXLL=J0#kJgbpIcpHViDip;&#iHVpTUN1<^7eEy6ARwQI5=4qp@vr zv(2wl3WrMBB@bH=6JZHHxQX?E*~aZvS{pke#PehV;gerx^5>Sg@d7yOe*E3L80IHK zSMq2OO$?VjR>>3Y@DsTanaiHP7I8)V9j1vVYq{xVuD~P0OvL`p6d!@2j!x^jl}5t2s26))2rxYW6qod`&rrd!cX12DFm5rdfeMwAsy@p z6bQBE>*cS{a-25&#gdx*mPU{)l=h0m`En~a4MRp(iJw2t>h=YmG-V=C54OSYtNC58 zx?1Ut+gY77!rA(RXr4scFcBB|v|l~H9p#Mikq?rW}d%E|E%7HGPh<`w=urk9=( zNFcALu9cfF?$M)vcKUHf9`09c>IDm2{c1$``SVDZ&<7e+g-r9=S2c>pU&)^MB^}uo zz`{aXoRThRn(Ab}{!@XQ*oIU(U#fLSyUA&#bLj{6sJ}`8|8ikqi^+L4xWrHG)1KaP z7fl4xU%>m0#dj08-{O3Jdt}soGm^LGsm|Y>s*t=7{=ULEoNvZdS}ywIq6bfoEIP>X zf((05_#B0Lo-k?M_=?4V;ZtQ1CA08z4LPQDs<~45foC47Ppvsa>@e0Nevc| zZHe3m1h>wsNr??j>RIGZMK^N=^W{Ynh0--~1F=jtRMQF0e7?a@5_oIzirX-2d2u4RI#;HirrCk5ro78#6_xcGwRVrxbc4yWn`Mhm+q zjU*>RCY8e^cuje5lU^n^GnW@eBuEF8@ngjSv;DJIl@SLo2}9$!;$LyNTc34|MmLWr z7)3GTLf=^b(KTCTH%tuw6?8l2( zPo<)w75Oh5XdptUW4;ClnHd#Zac5V*o4 zCc4j}xn?Z0XQ{XX+-RC*G;X~iyT^26zr3p%zdB>=^y53l8Ir=!qilz%3{Osxj616H?8G`sXp&HyP{9~r&GlaKc*aUw(6*f%VxKRlLn=!6WVblXzwY( zuYhaXcc#}gAHKaG@=DQ`ZGC*`#sE8otC^Q%4zcJddx{;75X;L%hI-X0kJ#jq&UYDY zcwHK+)kIP*Ua4-J!tB?hHw4Fwvc>66(Ds~A#avY2IV|zP7LSJ%9|j0@9(d^>u;GiW zj;ln3kK@-DRq=@17!~#Sp4RnGh8oXopANBR;SBUj$X=~ZlT}|s#F-MaiM3z;9WW-6 zy?ky`YWv4IrC!vl;bwB#8)dg{w+M!xL<);EacnP7x zu20962T!gtS^D-__Xnc*ijt2q)y|Vn_4Hpd@Z&${jgF}>*!?cVylys1E=`)^$R%@$ zV8OQ}+@7!a_EHYX3mbIoBdX?;hFNqZ#?`{9{CowW$+w9=so&?+lcN#yGhtP?s2WKVJmJp{`7idiS@1Da?x(BM&;i~8o%RbAE z@g}J6V#>pLiWPjAa$0z~A250iUe*@iNsJMe{jzj8eRF=lWN1+?ec2+FUdME+q08Hq z>4ZCx#P^z|#sbkQ98SYv!L5G8ivu@&)ZM;^?H}F873$hK{ZS^9-`GR2?2douNa^_? zMH~9{G4}&46Wws4)Sf@GV@|!_^D68Oj`WFzKUi-_g-h05$&aAFG2(oe#?Zsiiw0eh z{JC=)y|>i3p6WAww%nZ~cn$F(?>^~*b)Dq|c5u9R>FjX*pW?B7no#9U5gvTT()w4X z$k@*JB)^7r$gYL^@bZBlb}{TG`zoU~Bj3WNpBhC>EUPEVc)k;(n0mE4GpIW_t)1&) z^LO@}qf}3jDU1janf(5{hVi;q_UzrmA4@dvHcm~hJiXfCjUjcxSoa@|Uf&%FM2-0Q z)+>1|^lcE(swZ~nFg#L{4gWl}TFM?y@SRdwgZop;R_Di44A0945-;D_U=PtH&~y}f zPBS7F=1^op@gRMMSj?W3!oS2w&62x$;JS@in7JJN_dg`y-*N+_X<8C;20@AIO>I|2Xl;pQMoX#M>G~ zSbECQLj;vZ`25?HV5KfK;Z2Ipv!&QmStbt+wzgfo-ZnJMOK4hwucT138^KQc_XEW` zCh(t1R+55%OO`PB z-?Bb-REaB@ZPT~sqH_W`?i5(WX%t8A#~>Bve!UFqRFn6mDiMlH=t!Z>OxO*ztfb{M zw|&J>Y)j4ZaY@Y8+%)IPOX(L4tem#8TpS@;1lwCE>sxAj+s}FKQ>569%)TSdNecd& zj~9){X_IwQrN2~m&zFjPK;Y?RZ)}z6pyb)9NuWYTT1tqaGWAZ8xz>2EPkmHC_|>UV zZ}X=Zt2|<#{9AN78Rg>iuUJx|mBnuzYRKczmW#d~-JvgS1A zJXtzV(*4w!KUd$$kv_Y1`8ErdgLg6Y_blQ`>a%3&HudjKJ!B>O33KwvIJqQRPn8<~ z9f66C(csdJo%bCP@rcU0T@F?cy3NjKYNv9hVnU~ySs5fv$((5yU+{WTajTbh=8Fps zQH>)^gzRse<*P4O3Os}M5kZd8FS31`Y6=@PF}+vn@zfw)Taa0n{-$>C^A5VSUgUbU zne7EhU)y<~_nS()HAZcBGq95ne zFse%v_$?*+-sYAfg>~Cw#}`7Fy?&+cfDr}f}P z3RHg_%k#Z3`Y($oqZyNl1%cJQ5sA>Gm~`cw{jE@4I&E}Nd9r@aVaYMA(XTIv^b?YI-P^aI4ha>&ua z^9o2!GRMI?VRs~kcJMvKYIYrWZG-j#4#s+(cX-*3r0ym2a9Uq_b^pah1$EL%`I}m% z>3I)p%V^LijeR&iMHOBuv5SX!)MRdRcUQ z3USSwl&SQo^kk;z-7o>^UJXjMbsskh$4q1?+m`$$o!qsXVaq+Kug`KR-8h`^F$lo*xl>sJ{%3Zn;NBU22&?UyA>$?(L> zJD(y$JEr+pSQne*y(WElALkR1Rgf%?^)D2c?2YcRY9mIFM1R(<*OnF$>bcWOhOS)> zIur3{#^>^x!VQBLegPNb?#>2~r)F^pQn&FsyWQe{Z;)2&8Qz(g@`RNp_ke0+qiP!G zjc5#?Wy8y1p7V~&;u1(|UwYH4hk$N~bGqpv#de?lz)0|n+wPGR=8lOUt#n=Q>kl6n z&X+Jb&AEw<6GRI}Sur`*SyYmNPwvpC*podzeQnt=zNB}~X7sd{IeK>k-6e!xxRPU$ zVruj*>bp_lPM<_f&Y|ntT3h|ux`e_NYvU;ibYDHUb^D|;8u#+7sOUPGy&Ria%*xImV5XSOuaI<^;QVDoaL*5p{+$oMYfij9BthN?U@!6#sfB&`w*Xp`N+HhE_ zH5YP8N^1R@QD8NV>(j7XgJJ`cq+3&0B8V#(>)iR|S3EjZGeTvm%IQKsj0gXyUKuC- zK;}}I%0Xy;&t(4Hx*XR8XX?UM^27G^fdSc1mQ<5UJ{K1DO z+O_vjWnGv-Q1Sh^8Dsm}_kxiM`8VFzamdnhyI1#k*^4t(*X6{kSxl=>AvM3y6$o+n zZi|HlK55u=>RPG-^{@}TGa8m?T$;MC;O~?b@8p`V7xBGqNjTJ+%od60FP{u*;)$ne z{PS@l{Pb(mhSbOmy&HXeub$f!DCE74q$onXR)4PG@jXT_--M{uUBUj$lqXA3nY?o@ zQ-Sinv7Xt}3%Z&Og@R>q@@-a#b-z2m$?*;Zw|@=s$BI~XnK|;lvQpO-GK}O`*KscC zxqtbyHswSmmg))l;ivvOqJ&F9DYNLM1u}exRN>WP>V}NVX&2oq)(X>WJe|yKqRDFL zKYlPS%L+<784@(>&M{+*Y|%@w)x2x@L;DhU&S%)YW>rLk69Sw`O+` z-V8HuC^qzUcnGJ2auXC4a7`PtIqHQbsRg0+H-#=S*+z|dm$`qn4BGDSh+XS%o>Y)LrUxE z6%?d=Et>%Z3OcdNoO?DQ$}H4Mbd|R+fiM0Iyp*1x_>s^$#Yqth9!5lnF7pHldVGtu zAOCKwWjt;cMCh7IlIgj}PmxYh685S`E|L6HLcMw3U_MFZQSq&#`{bPNK1RR-+18mGrKHB#UUB7#rOm_%Q=>^F?8vQfBBm zvewt8j&5)Es+2mY6xde#AMu6Av-o<-5HN|L5PB{pR@amsF;ooRi|W_%suAux;hcBv zmIb3<`}JRs#ykv+Zmkbl7toi<7Bl>jlb2qy=<=(+F4z>&{P3!i$g_FGtCZbtg9CT% zw=eW|UX&nEO%f@wTSS!X*EiQEDcKjwG!X^;zYOlP_0Imdjut=IdSXib?J+T-LnUwh zGso!twDq^^6Jnzl_a->SiCJ@3D7epV( zm`C4Z<1{g)&Y)K1ta~{~-y69fFGJwbbAEtf(3$*>2BG+MG8+nK7Sd&-B~2cjiH+p4 z5o|eK-4EPQMjr`ByKQCft3<~IGoV?2UChFXlQxWx-?|qON5sna?b5dELErFUdB(Nq zymG=zF0Ilm)wdSA!W?#srtdikDsB_g3^V!PE!{zIyJ| z6D_Qq#7=iYdhMx@=`+2>57uu`{d7r!YP1HKrqN3oj0(|4)xBkLt|{MHT5ZNi)kGt- zs*``2-52#P&GvDUluYq#ceFlhc_yJEjM%Q9J@r+%PsBc(%0W??j{bZ7eAK}DRRojz z1@<vq4t!eIr zE6%rW$VFz0kv|lm$m}u``2LpcxnfH%qxZRUN74yq+1!(sj&Vd^kj`$e^Jg29M6+MJ zs-}498ZhKh%M&HYRv+p2b-NnBvn+@S^J7^QEInv?qMLLlIQ@fC7MV&+)cOUc-_0AB zm^mW<6s95dRc=gvd3$55KV{qdw412$l^grSt-g`mL@mK7wpS>Iyqz;Fl!Q)Dh|N_# z#f$kZ4$9$*yBgqoJ-aH5zxmq$?(Lf1jW!R&66NBF5g&mOOkKlHxs-QP6%r?AK$c@j z)TPN)=}7C1zIHw1I^!vn=OX3S+>TtS!z87`ORyJ z)SN8lc*XSBTbK`wDc7HV!|oUIUqI4rd>?OndzAm`M^%?5&w&Y7e(V}Gp8WP2Zb__h z;&kPmxgZ`R7kZDS(Pq+Z?I!tZ5`@+y=B?U1ZR3<3!xOu6Z9Vp1n6ZL|+g~0$;#%W7 zbU-Xq4bVsP5vidXP7p;~EjC*x8q=+pXV9{V^7Fr3W6g1Uz+HaUxtEmwzJRLc^O-ac z2X31>=FeJ4t8VR>B;?{~kwUshd8hnurq+rJ7r3$s^(tT2Wp%KH?{Kh~1PGfdoH1fB ze`wvVp!&4Y{*)uz#0rReoT^Ohy;<;4HoqjwM4MV^ODkvR z47&W$b90RtAzMEGn*c__R`ws%>#{yrwUS{Md3~OL zX89P3j3$toA9*EXiD(s_3S^xU?qfwrG5o4~c1mIQ%tOP+CZUpph_m-OPMkm8HszJ8 zl;rx=|Nb0Sb5Yh;iOBqqoUfsSDJj1f>ih3$XKt~Xi1(iM?8elYoxOIy{aL*n4s?xbnRy>vwKra1}l*M zx~YvT^k2n|S|oHH7gvry<+NeK(AnI>tlaqa&1zXoRL7u8%KW@ep_n^r-BhZKNnB25 z@m!K7DeJpYn(S%91TM?&dnL*U(zi^jJY=sjyE7Ck4A%W(Y-%sQSH`Pn-G)%f&==`zcRQCzXb%37SRiR8q z2w$7Y&6<$Kbsl{ut#D_X^GnMln#1f)K@M4woDrthgL%pGbHn||ZSufqJD&XDnb-R-K|ECjL#?bmpdT$t(? zX}dVN1IS{QEl8`Gxj2Gyk18MF`R5i%?_khhJCH?E+Zl|b=PrHZw4K=g{gdFp5S?`+ z&c5mwbF;X#KAkdAMyoJCG3wXN>((OPaW|@5Y>{N=#iH2l{JLj`9_2-A>~7YeF9*;e zxMW@n2kq{;QoRwhHgQAk zoH}tJF4xP|aAm^f!BJ|;pT$rn@>||Y)5}S|>nm>`l%B-sIHwmF`J4-UJD^M;+j85{ znuTVA5pm{6jod_N!Q4q=_RFeI`lrc}4VSJ4NcT^k+83_#>DgzT2)MnVCd`RfPvr6S zR}9_J=4m0bStJ5IOkk?!L@an%EE8!ydx;}k{!}!nB;v?QO8CY@6FyRs&_+Us4AqBr z>e6|Sw$p2)FQ83NI5cxx1zWMC5ui;kT%binpzS5qQ90fO&qO*aA`3%v-VS^+%{VPj zc za(hN|a?piV;t^Vs{1d)tSUGli8-)tACmVhI(U`8#&0gd>GjH>K!5`f+U98O?d>b*Lbt> zpm{Q8YVyvhF>z`sG~J-1sezd2k)=!ap+@)be2?)7>oSP-HdlHQR6$+O%N!cY{$505iUa%~ z>V=?(e5zIJ&p%|4>3awC(P%}AV9hJ9XgqV-zv+kVup^%uSGc|9*5$oZjSFrr(LJzb z+9fn9WAy!`i#XV^x@bSny`2A!d*n>e49~OQc292@mOia5%TXm{S-Tn682h+nJyVwd)D3CPvI~slA=ZROx;15HR3Ai)btAN z!zk9^;S_E{M2?_YcVrY-js1MmdnB=hkZJpk7?*;9Bx&at)(O3NF2dw10)~&;-AS^p z2dkZs&bPmXw*7&oyHXIprGj%(10Ou;5ba5--yS=fJK0)ZXml~IuW;Q;zQG_%Qx1`Lo@VLl(Og^YcTnqGL0be&epVW zMqd8qa3*KuE$zB)cgLEmv*+tJ^Y;G~?N2d$t& z5C1RSsaJPe>glFjXg>Z7eX!23KBYuzEw#aJAcc8xc&OV*KJ>?$vUb-Ge4Ep7T97Mq zmRAG5LMC|XHz)Gf-739o5v#FVZQCyo?)vqJs8M!Q6+YIT;{F=SF&2D^vwvF6NYC$eyKD@aZM5 z)bL*?1LEctljQ~)XiDGHavfFm)b-(CFo(O$t5^rFt~6GhKkM>nV#>R@&xJYZie+5q zJ0h|CV!C8|Qk{WV+e6B0N@X@@T~CThohiK|O+2FRK&oBHDEc@f*gW>9z$&g7=r$QY zE71@<;=WXP6zp2zldNfxm7fT{RdS7tH#vbnOg=c#!aw(W*IAmG2c71 z)MESd%rnwbL+~n6l>FsXven%`_8~v54tDL)8(ME_2pPkh-VnIx>J{nV7(HLWPK#tF zsi2#Fe7G4Kr}Hp{COg{1fV5ESu71n0Ntmeh(yh6SD+czTO z=$y2gr#E??jHnz6!@u*AJYznh!j^9hxy!oJ;7@Mvtfie|_iM~zX{QamseqWOW~4iL zrn0dH{x`k|c65Yqe3nNgr8mOkQ2L388d_&w)H9Dn7V`^EV(p`Ss~TXI)fP5goeA2f!P+KG3t z$oaZ%EaZy(iO{Qg#_PPRXyvihk6dToA(X<^RoAsJSIJ&AzvtPor-)vA%qBXHTn$_ZPXCh%_7m@YRv0r9)BDmoC#p(LF8;u3+S?yb|@89+tz>&Cn zkyTzfowal|OQOVYJ31kx*y7;wTY}yx{$dli=jfOBHS=74iZVGCO1$jS(0OXiehU*} z!pPfQ^)OTPis3MckyQ-uGdeQoG6sg>D6G5C5zWci3-dpsQ#>ce2W9AQ@=3?s%Fl@> zX+#jdd8 zYzuI)W+9wo4s6DOGmMmFbZ?jU`oj#rjwSL^9gXwNrZ9_DzVFpcAu>+a5qZ4aayuYk ziQ;rFC9Ph3XwV&>s~43L&r!%!_T;|fW0JV*B4PY1y{ehL%j%;3mQ4f&ebi0cD{1xU zYth3(-_ivb^ftfZa)?x{X)e+G%9E0HAeY1ZBX0eeNF2LG<^1;Hn7nyN%jHt!6ERj!9>}2o_*b`q%qZ}oU zG^a4&a?~8kKl{U2#@2#_h4C|aru^?{+s778Kf}W25mV1!lcHy=9NJSac?9-nJWY%2 zCs?H@mRDk$&@&5*MTB;aJMD|z_%uZ`7~i^B^}>=h_W{v`=ClrbF)U+q?Sdcm>spei zMY@U!b_e6=&%_sqzTOhQOh^`6A#Cd#HWH+zCUj%iku2fGgf0S;(Xcv;r=elb_rGzV zeGn`)SbrLGpF60tvF+DZg&c0eqai)HXP9`^f^+eOdT_`+d_&2Bh(LG9;;NLU-h-+a z;s+~8#3|=9I=4^~Z)8P$225bxuepvbeGn87?$Q%aTnj#-9C>3Onw*`sVqzAlvVV zlugD%FPaQA*O)dHSs-B6dkZVl~by(o6EkW z%V|jAGydn4zDe~;D2sx0g`M|p9hQeQxdcfZVsV-YK2bF&zMAhH=fjycqO$rQ%y4$j z$I*RcdLGP1z8>V+U-HrhHTt+UXaBJPGXOLqBYuG;_M5qvT>9fD=^-7)!6zkL)hnYul zklKFuDV{<$d1)tuvFe4n_===@}KAR_d;F*9Zg0 zzBVG(uZ&t|c}t8WeXvo8F7iX#~Gql&Z9kjX|gqRRcw3bvPO-p_J zaIhfuIWtq?x?ppxtyu?Ii_@ZLg4@>E8#cbM=Mn1dn6nmCZ~fn0FnmD49%<|lO2GCQ z6^a>a=4(^ZbX9YjqQ0>bmoN|(uO{D-N_@3%!gVcE3a_}KpRMBe9i!jql{W214Sc#8eIsW-xqrjfjC3YYKh zB8_M?E=c|O5g4iwNSoy?ot4v-!%OJUnOiDKQ%D^)n7p;YVQfSAis-66?UV5@?-`>= zHQB#%AoZ6wHJ2womImEyN->k;Czv=P#b9F6r^n$6k~H;Cyr0)Qzo+Uo#|yhsNAzl5AS5%5eRtm6M(jR91pJiM zr?U5pgn_EA+Mk}^-fqfd2;Cy0na9tQn1%Hfl{!&;+LSGAQ%ymhkBBAA9elb)H)ikf z)yrAj_?iyEX?az3gtUb5Ys_i1WSoV_$TbzgyHUNAekDIh5eLLPzZQ19l5r9zYuC$A zYVVG$L1NGA_h@uqQOWbh;(XsIhwjKvNN|kmQ+}bp{hEw)P`zPyEnow06jJw7+5%n}u7!fM#vBwjc7T%Vlie59g@QThW zT2>$JEx^pyjWmCLiO#eVN?DZ2QLCpEYb5jF-S@ajs%a3x@&T(nma?q!}0-syd z*Ew80^_$l8hWTz+>^3_oP6W|dFu6V&lH^jAb+;iaa zQ%k%SPGrsuaq=ZOwLpR%XGJ)(k>dsls0c1a?_}s$y>QS;fBsM>qg5zZJyV8|E&epA z{hfjO77^0<>|ZQGmW~Q1PZ$#RX9Zm=NaF1?i(of){6y>Dymg_Tm`9H*mz-;6AlWRw zEMv{xl^u7z8gua=T5aE_j4j9=vlNW`lpoT&U*&zIYHO9eqB;vauZ^^sw?Gxg_5dU~FLi0qRZ2G9Ai2Q>t zUZ&ZPN#x;dp<=|`D-A?_oqBY}#g8Zybh>53>^Hjyl!YNVoqgpb zES{iqQqtXjdBkI4F@V=djUu>rf32wDrFUamNIA{U!ncL@K0#CLju`ZlED-j47ILNB zZ*B8QSQW!RYA)tQKM>SG)E+*QI;dgae}W8rM>e4s`Rv>rl`7-dtT>x&;4XnV`gLxv z+?y*z9O`U7F*7WM2FCP5imeK!CIuf42D}53A3L%;8tb|fc*dKC)hN9=h4@z1Gm}GY zJgyO~QmA~ABsCf#CG-f1eybWEG#?m_@TX{^iDp!Ap__2$Zxj~CyZX(>OoX-k{@lBg zyt5fFyXLd?vB}PPf5|vRr3C+AZ087k%Qdp71e+nCz(QYyYO?V6_;C1?Mgad_F}-{; zuqy;H^$c0phH}?Y#42@!+M>>hKPRr`7a9BBL$y!!4QWmiyUNElt)ct))YmPpw^2mK z?bPL*`u3E83o<4olUZK7|~ZKBt&=yn)W+dj+AL6+z7{_I;7jI zD#Cw>oxhsfy7T0U37_D)$IEB&!2_>wS@XDB5n+4`VWWA&L0MyB8+y1FYmw1-8h7eB z>Gth<|LRo>2k=97d+exHcT~R;g4?rc&+;x&y~*Cqb38)JryJf!skDDPdZOI8k-OG1 zVP~%I7j=a3jK$t%JXkQBOK#GqpRNdwig~3$)xq07Ykuy9o`(gh?a6PY9d{dAwS`FqI)9Foo%@BU%7FJhw5$|S-FKOCd%L?4eYmC%%T1$LK zyD<_~U(D4Qv;Mwu*Bl@GWRhZQt!;f|QY|m(Ito2~wq!mwI$%ki zh6${X0uMFSyOP&OpS+1&X6rVu-(RTrvb9tkCi%ixUX&R8;^*F^k%HFR4yeivUQE){ zbxmuQYvrrcCH?Pwu2_%kxZFLvgpgE~oFyA>Yc6JTaO+%338~uHQhI9DIQg`!3Xj>L ze79w5*KeLA|L!x(sjn8nAxTf@I}!q2TPAo88b3C+4zp^UvB~_kHUF++#+C-79$8abNJo*NwL| zu}&T3G*h=aw3D)Ft44IH+nKi`^p?n`bJvctq-J&+pHU2Wak;iAJznCuhgfmfx7vvN!FSK}^ zr#fJCGQvMeo50!uV&k6ympE94RZ+u`<%%c^=F#&z(6v&z2qeW}Z`%PcmObQZCB zG@(8cT|38B86RzpJ&<@`FOkXBoL@>nB1X2AP_?LwXNx{1UlDyrNlzY&;nRRCEs!1cYhe+IWC)0B-;n08;=+ zh!X&u0MM?a0RJc0;a~M;1u{VyH36&%=$O1n6?77)9D{P{Bj46AWVH6P6#e+l`eA04 zwRC2U<^A*ybNS2;bLGrBOVRW%wl^bV%xR6^=!0@vC@noJNFV3QfU(j6A^=Co6*NIHqDuF(x@I7nsk8d1J+skb2m_yDEJf0nZ zs~e9;ea7KAJ27~UE)1USGZxR%iN&)uVewq8;CwqCoWtWd+HnV5=`sV$3p-g02n_C06YMY4=^sEO#fdja2_}xEdb=X1|bLEb?Tc@Pnh40cCt3_ zu5)zaaGXF_j$`1!8lVU0{TYwv{({4E^@IHZJf5u=hiB~o+W;s4&UUcfiNmvZfouAJ zt@?pYdZ9gbH(2s}zfoJ=Y5=?{0&_kFfOfb9AOisN4DU3ffeCj`(5=+6Ok;v58A_yTkXwqOI-utOX5 z0zJV#7reI*+y^-VY=iWXBNs2`lF=rPKFlr~ke>zk>~B4x>>L2Bz#c5n1_12c7(C;X z+6jiFl1k?H-!fS1mf~5_8!;Ts8_^ui>(LxdtC?K=*iD|VxMPlR0y}UH0DygtA-D#1 z0DOS<;OxijGG(<6k;IF^N>dzO)D!jw<21R6x1*#k^me4PrPnJ9Z!P8U^%&GC>Y=LL2<0JLD}F0SSq{wSzs&!04@N?(c|Sb(4XbQ7eS71*jaX3f#*B{QVD3{DT*q;2awq6LJCCU;vM0DI6RnIU!#R27CeF1^{gUdGP-<7NG7h zr}B{7_}DOhTI)Z~XCTfY-?o1u(z&0B=#@3I;PzOEp z1>=bVz}gLYb-eHa{h86f_*jRqGw@nga1XqX1B?&#gSiim%L$Lc^CSPbh83KH{NjMQ z;tL+fUNbdLDyvcf28A|&Jb|92M1OUNB|atAL%mbQ&PCj6*)aOCG>a z$W54=V1DN6+B#xNDf-IttmZv?^>`Xb$7(EhFUW5_8__Jin=vdM8<8}LB~TtpfPeD| z=Qi+v=Q173(4i-D|6UYx_f|A#*G4pF=SDPJ`(`v-$2!bWsZgN8XA^wUJ79h{D z9U#|$eb_%ujgy=_<^hZa7;7*m!uW;${9j$5y%n z!Kr`b2-d%Rzz*bsvBvcc#AnIiC?VHzK7ct9)&+h5$mRc)d7*rigaXnMH2G6+>Ct!` zO*0Np4M5w9!!v?Dg$?AnfAlaQM&X=LrnbdhVo|AA;9jWz-|_vou5kVL_Ww+VzxDpN zdzf;XL^(ixIF5^Boxz-t1K)wqzXLx2fBfUz@o_BU@ub+y;PsBJ7p_tGU79;{~3hA@_Y^MAy1M!vyj?3+T~UfWKKl zjbjGe44{{z0evbRIL8G0SCH?Ib9VFM9+BXQQgHu009}C70H^*1vS1rNCvX7#SG_9K z9mXj)0F2pxz4sKQv8^fU%kFP(;BTG@z&QZ6zj6>4h(i?EhTu2`$d%wauI`l;vNIZ0 zV62A#Fb?4J!awUS*#6h$*!P5|)CFihEtfF>{aAs%u$O~17X^6E3Dz(NFoNUszzz_M zz&5O)7BMv}>=W{dqQQXA0U`kc0Q~+1USQh^Km`ESC75IWUjF7Cj2j(*n*gxC|L6V0 zlBXYV^sTOQ&jA0=0^NQB0AKTfT+9o=2lV9~1?S)~zy&S<6xe5d-8%GFuMEdi0D!gU zzv95L@2Rp{uQPUGx8bu1FR-OBu=9y&9A0u3hZmg!xess-=6zV-*@11CfIXN%zsJzJ zg`tjkIY%3i_LDv=W0)p9bBH=JbBHPetRtR&r47#LSn^>IuOE zZ2-UnwmD%=0A_Hp?y7rA`=+9$L0UN^c1c1&W zKLEyYc%eyP6JQTPAOjCL#tyj03}S!@0M=!805&i`BLEYSk9iy%13--(;i#;fYrs6t z05Cs58IBh%Q2!ap0-y8#271i{eSi!w-~B^(7z6+4 zFW7;skn_j3fVuL4S)rV!wh6${es!2xa*JmS+8pT4@fRQAF<1wV>oAZF1>%%v0j#IN zF(}(G7DMZo6b|Ntb%+t*pZQM~=#Ofd1jut-_rNvKHpev)*a2R1Y?Hy=by6j5*c(Rx!1swz2Dm`|DVQ4; z!r*fi8`K}|rbJ~^Q;0M{WWVLs*_#$r(;M?0vW zhx2?tc7Jk?>^87{*{@|C*sG?0@DMs0t0PISY^OW%2#QfDiEE zaFFvTV0YLD@Jxc-2<*=dzyWLo{S9*xvxM&0v7_x z#80Q`Td;HR-3ur1FU)^TKz|nCf5`XaI0ECb1Hf&VQ<%ZHtUv~i-t`5J@_|f_$VWOf zq9ifHpBHc^=OUziBZc^C!a z0|j*lIzryV9sqIz#tMuLxIT`b{+$&PS>?B2U~d3e2jD_TT{LOS*o!-abq?Y<#{Y_W z$aQA;yan|IW5av`>m1uC7Ry%qy_??I%ZiYI0QPk7ea?US7U}|P3ygKxdqe!^HQ>-c zm;dTocnpq%kahD#yaQ{&?s*ZO?}vq`aoiEC30!}19rna1ARF{G7u*K^;s$&;-Ur7) zPjEnG?+J52IKVjoI4%i8!)g@tIn3<>;QC|zVXpft)>)v=P=5ee-#JIHIQD|J2J#cK zFrHZfAg_;Q1Z%j!ZKyK^06hSF_JHvQ`#T1J|GXX?I<^z+iy?<#4-Bt|+pt#un{9<= z@d%09VLGAF{l};=9EST}V;<y#G7iVNY|+`;I?rR5~UR;9gmP|9qZ;y2HB4izFb>B;n%qppa93 zNO#fl9i8>{5@J4~ByjE$z`xahu#cd2520tuEU{&F3wl7Ippi?=%9#zu_`jO_4#29b z>;G@|UT=KIo7o^BqNvz9KuZw^APy8%L~&55)!I5xTO&x(QU?mutq3S6LuAPgNgyGM zgzNwz2_%d#Ldbk0`F}q5IJ|Az~ulVpoa&2 z++9+?+@lx6F76lefb_#2(k@y#C%W}No_+*;2s`!-@}|A}d?Iny=IK+?y}jCu711Ze#)NfDC~%S&XNoWfV`uQo^9(WmmvIOjT(2~1IlOLtW&p@AGQ=6 zYqOP~>9ka~xx?8!R^Iyxo)gJ@qixtF$8V< z&$zIdizxZ{+vY498n>^xI_?RL%V zzyf_XL1yaKs@C93u3%e^Jp|A*?`ETI(%dh@Y!!};X4nDx-WH4<3)*d>+FVIP@KC0Pd?)&`!|J{GtR7m524Fk6O?p8{{l3dFb3nse3}s_ z=ezbPnES?V5PKN#fc`AkQ!~I)=In}|^d$9nA|MXHx!?2HUwhwTFK;Wb!p6iNB_0?b zGPFY%1us1M->dvb0S_7aZzcU`Lw}$C9QB_-`n7chr~M5I<#rE%Ya{KTc&$lm~9$#Vyu zF$n0U-(1+z88vFcw`>PEXMC_#N?esS#J1~=@)z>?T{(=80PGwj^Kfpe}l0f1keWb@SpRifA*2lu~lwIBKC`M*f*J} zN6>q(??)erezTHs}hnTzA`&lSM*ka! zZ?tYY+K&WCL01e5l5;y2d*}zg(0kfE?qz8UjOa&3d->e>HvQcm`X^%i*sMciu}^bS z=ehSK{ha%pdz{0Ty|8~5p#L{$uSfhttAPCn-NI&IOgm^N{lr}@L;UCh?QdZFmH3lQ zu%8LX!8h1F4R%l1L*PKWAnYRWkiXu!Tk`XJ5%^z2`tkS}ceTC3xQ7vY9j~9`@$n&d z&`(}MZ@lq+`ex_Qe;3BLX4Q?wKFrDa%b155x~p}s$rk9e`A6vY&VMuHZ$(|&ERVfL zN!nP5M-_Hgbq=47JX2OmZJiTV%vb#uSo0Q{!{1_7iYz}>jBX`FFS zr$hMrnC}Yrd0hV~d;ShMOa4EgpZaIcts9FNm6Pi(V^sp+12A^O7>50zqcd{CkKhmc zuL6Hlx4kogcLWRd31El)jJN>(cVc`6>!-a2^2hvVe3*1o2gn2N(WyhArz8A{muW-q z1tgIEfoN;kRyP3q{6ky=>FaxFhR&cpV@c$Ll6hhI--QeC?%jUggZA30tmK5l9{F=`ta08@ z{^)}~0A;8Yv^E4?J90M2=G^C=Ou8R!W9F}oQ1`et$lUA0_Z2Z*tos)74}9Yskau2~ ze&VmB*Nx?Vl(=#YrtfL#wqvwqmVKE27m%Nbe}aB9==b_Q8s-7#k7qvMXXt;(1?K-F z7~i@nD>?Zj;ziIk(#_bMup{`HdqC2lY}#9bIPfzdn|lfRP~v$P?Tz^z&l|Ds@oZJ> z8M$6!VG}+e+x`gsSEIcl?a)}v8K)icitS#%NH;;mZGnI21>{FRM1lM)Rj_~Xo9y(R z2wX$JNAg1b5&vll+S|53{te(Cd>*gvP!Grh+CHo+o%(mbUIBu=x=;U2Ja)8?Nk4vt zzNqu4v0MJa|0n&>8({}PzrW{v^9S>v{=2s0&{)JuoQ#ps_KBDv#-{CK4BQO)U&#Nv z9_{tZvyv6qKaFi5Gp^k%Rq8JIZ!U-aUqJr1qrJKo`g72AP{F)WxYnT*djZ-L{HzwY zoeI8nJazv!00W?B{Thb$k#kpv>Iz)T^oV=NjC;Ti5WMFKw0{?(pX;|u`pY_Hz%z3(A9LA->AwZ-W#~WWzsKHtbcp&-|1pmK z^wEyaunE1>KN#(!U_bQGA0zD*^qn#g0N!hruJ(vO|BZXz-vSgskL+SJ;2<?Z;Kd~6-`e|+!laqNQgvmJeuGUyLk zw=s2pk)Ji5<5KovfOBMz-^CM$ehm4ox5E!Q!?i%r4tvO469e)){F4VRMn&$y;{fc3 zJ}4nT+fp{pcEIf<-D2+s*_$W_&~5=P9{=Z|EvWlAU;u#j*V{hO@ZRzP)&tH%+&`Fs zH*H4zG3;edqvU?_0OJQ2ZXYvm`2eNVd5CL0=N$be`Vv@ETOy{-X1wQm0QZeQkUc&V zPe0Hfy<$%o&Y>35?$iIJKVXCn1pRIPNduOl;#2^A$BO`zBO%f7{_?5zTDMcgq=7r* zFk-9ANs)WM`#sohsF9yev^LiLEcop-A^%5XX)=SF4?Q>@ws>s!2{Fo)0g3VXTL`W z5dVP==+h3j`^R6j5tZHs{24F>@Mt$Y68Xf7^RfSMQGTBJ?~$X3&3EG;cH6xCq*AYS6=%Ub8+KI&;A+q4s>haBiB9+?PF_U|L(YO`#`i;3mZ?8*R;3XcgTg! zhePgplYMFrg|Xw^UmY4qB1(l2r$ zP-34X_#x;9J!bNYG=LV?_vlMEZCFF-6VV5uUUN*Yshaqiyg+^U9P~TLKP9hmcgQ_c z7$ZvpFh={6=X`Grx)?lAb5CDvs&Hp8jzXW*>r;#Mjrien6(#4Z3c6*03L4GO2{Yx{ zEpPhfR_x6g8|FOm#`(m0#Xd+E=%L)jnhD-=4H5jNPlz!LDCrN2{VnLF4hsLCwCA7B z4}JK#`#?t`fVpJ<7VTbsaF49zol53hz!^K{D#pbeVE!F(;`sD+gg!$48t62EM(P0h zK|au~Q6D@y3?4$xlt1Yu??iv>7j*RKIp-4jfpO@Ei@iC=^vWNy5NDFWN!itq8v4*P zqk%Kemi}$IdujKU@lM!-w0e`J*f}42qE-=4?bbQU&*O7>=AYQZL*|?ZD(1Xc$3Tk_ zbQ(Ed$OrJr#CcB|(GTqj`6qmK=n=nD$s5WTw2Qq6`Orf@eg?l_8`|_+tCt4fI^hQ3 zssXsh|DyTsrS(Fkr44@m>f%tjuzd>fsS-9x=%PmlJo6JYkVfdRkRxc~84^LGUGLTl z&I|rddZ|Yq`tdV<7c!^Z1%L4wY%b#$8ssj{XFxY~P%CObs9&1(ihsXLnJ>w6=0L#D zvhx?B^+KPUF&BJCJ?LZ1J_R4i^#$b7%(*D$BTCL;(odW0{aoM<*;^n}%GFHU06U

PC3|&b>&WDY(w&4O&D<~(u# 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 Date: Sat, 17 May 2014 18:56:01 +0200 Subject: [PATCH 177/372] Project collaborators can set a free amount on undecided tips --- app/controllers/projects_controller.rb | 15 ++++++- app/helpers/application_helper.rb | 2 +- app/models/tip.rb | 22 ++++------ .../projects/decide_tip_amounts.html.haml | 21 +++++----- .../tip_modifier_interface.rb | 15 ++++--- features/tip_modifier_interface.feature | 42 +++++++++++++++++++ 6 files changed, 84 insertions(+), 33 deletions(-) diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index adb77030..e590dc46 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -41,8 +41,19 @@ def update def decide_tip_amounts authorize! :decide_tip_amounts, @project if request.patch? - @project.available_amount # preload anything required to get the amount, otherwise it's loaded during the assignation and there are undesirable consequences - @project.attributes = params.require(:project).permit(tips_attributes: [:id, :amount_percentage]) + @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? diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index c00ec64c..fc116e7a 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -51,7 +51,7 @@ def render_flash_message flash.each do |type, message| alert_type = case type when :notice then :success - when :alert then :danger + when :alert, :error then :danger end html << content_tag(:div, message, class: "flash-message text-center alert alert-#{alert_type}") end diff --git a/app/models/tip.rb b/app/models/tip.rb index 9c35e96d..0c6f4888 100644 --- a/app/models/tip.rb +++ b/app/models/tip.rb @@ -57,6 +57,9 @@ def decided? def undecided? !decided? end + def was_undecided? + amount_was.nil? + end before_save :check_amount_against_project @@ -75,15 +78,8 @@ def commit_url project.commit_url(commit) end - def amount_percentage - nil - end - - def amount_percentage=(percentage) - if undecided? and percentage.present? - self.amount = project.available_amount * (percentage.to_f / 100) - end - end + attr_accessor :decided_amount_percentage + attr_accessor :decided_free_amount def notify_user if amount and amount > 0 and user.bitcoin_address.blank? and !user.unsubscribed @@ -99,12 +95,8 @@ def notify_user_if_just_decided end def check_amount_against_project - if amount - available_amount = project.available_amount - available_amount -= amount_was if amount_was - if amount > available_amount - raise "Not enough funds on project to save #{inspect} (available: #{available_amount})" - end + if project.available_amount < 0 + raise "Not enough funds on project to save #{inspect} (available: #{available_amount})" end end end diff --git a/app/views/projects/decide_tip_amounts.html.haml b/app/views/projects/decide_tip_amounts.html.haml index b138ce55..54f59609 100644 --- a/app/views/projects/decide_tip_amounts.html.haml +++ b/app/views/projects/decide_tip_amounts.html.haml @@ -1,3 +1,6 @@ +%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 @@ -5,23 +8,21 @@ %th Commit %th Author %th Message - %th Tip (relative to the project balance) + %th Percentage of balance + %th Free amount %tbody - = f.fields_for(:tips, @project.tips.undecided) do |tip_fields| + = f.fields_for(:tips, @project.tips.select(&:was_undecided?)) do |tip_fields| = tip_fields.hidden_field :id - tip = tip_fields.object %tr %td= link_to commit_tag(tip.commit), tip.commit_url %td= tip.user.nickname %td= simple_format tip.commit_message - %td - = tip_fields.radio_button :amount_percentage, "", inline: true, label: "Undecided" - = tip_fields.radio_button :amount_percentage, "0", inline: true, label: "Free: 0%" - = tip_fields.radio_button :amount_percentage, "0.1", inline: true, label: "Tiny: 0.1%" - = tip_fields.radio_button :amount_percentage, "0.5", inline: true, label: "Small: 0.5%" - = tip_fields.radio_button :amount_percentage, "1", inline: true, label: "Normal: 1%" - = tip_fields.radio_button :amount_percentage, "2", inline: true, label: "Big: 2%" - = tip_fields.radio_button :amount_percentage, "5", inline: true, label: "Huge: 5%" + %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/features/step_definitions/tip_modifier_interface.rb b/features/step_definitions/tip_modifier_interface.rb index deb9dc60..00313066 100644 --- a/features/step_definitions/tip_modifier_interface.rb +++ b/features/step_definitions/tip_modifier_interface.rb @@ -1,13 +1,19 @@ When(/^I choose the amount "(.*?)" on commit "(.*?)"$/) do |arg1, arg2| within find(".decide-tip-amounts-table tbody tr", text: arg2) do - choose arg1 + 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 - choose arg1 + select arg1 end end end @@ -51,7 +57,7 @@ tips_attributes: { "0" => { id: tip.id, - amount_percentage: "5", + decided_amount_percentage: "5", }, }, }, @@ -69,7 +75,7 @@ tips_attributes: { "0" => { id: tip.id, - amount_percentage: arg3, + decided_amount_percentage: arg3, }, }, }, @@ -81,4 +87,3 @@ Then(/^the project should have (\d+) undecided tips$/) do |arg1| @project.tips.undecided.size.should eq(arg1.to_i) end - diff --git a/features/tip_modifier_interface.feature b/features/tip_modifier_interface.feature index 26b2fbaa..16044470 100644 --- a/features/tip_modifier_interface.feature +++ b/features/tip_modifier_interface.feature @@ -122,3 +122,45 @@ Feature: A project collaborator can change the tips of commits | 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 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 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 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" From 79978ba8347c5039aa98ca88ac4f04f83e5e0bdc Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Thu, 29 May 2014 10:44:52 +0200 Subject: [PATCH 178/372] Users can create project not linked to GitHub --- app/controllers/application_controller.rb | 6 ++ app/controllers/projects_controller.rb | 33 +++++----- .../users/omniauth_callbacks_controller.rb | 3 +- app/helpers/application_helper.rb | 1 + app/models/ability.rb | 1 + app/models/project.rb | 11 +++- app/views/home/index.html.haml | 26 ++++---- app/views/home/login.html.haml | 7 +++ app/views/projects/new.html.haml | 12 ++++ config/locales/en.yml | 2 + config/routes.rb | 4 +- features/create_project.feature | 63 +++++++++++++++++++ features/step_definitions/common.rb | 4 +- features/step_definitions/create_project.rb | 30 +++++++++ features/step_definitions/web.rb | 19 ++++++ 15 files changed, 190 insertions(+), 32 deletions(-) create mode 100644 app/views/home/login.html.haml create mode 100644 app/views/projects/new.html.haml create mode 100644 features/create_project.feature create mode 100644 features/step_definitions/create_project.rb diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 99564543..629bc2b5 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -6,4 +6,10 @@ class ApplicationController < ActionController::Base rescue_from CanCan::AccessDenied do |exception| redirect_to root_path, :alert => "Access denied" end + + protected + def after_sign_in_path_for(user) + params[:return_url].presence || + root_path + end end diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index e590dc46..97f8c532 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -21,6 +21,14 @@ def show end end + def new + unless user_signed_in? + redirect_to login_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 @@ -72,26 +80,21 @@ def qrcode 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(login: current_user.nickname) + authorize! :create, @project + + if @project.save + redirect_to @project, notice: "The project was created" + else + render "new" end end private def project_params - params.require(:project).permit(:hold_tips, tipping_policies_text_attributes: [:text]) + params.require(:project).permit(:name, :description, :full_name, :hold_tips, tipping_policies_text_attributes: [:text]) end def load_project diff --git a/app/controllers/users/omniauth_callbacks_controller.rb b/app/controllers/users/omniauth_callbacks_controller.rb index e96482be..e634d9f0 100644 --- a/app/controllers/users/omniauth_callbacks_controller.rb +++ b/app/controllers/users/omniauth_callbacks_controller.rb @@ -24,7 +24,8 @@ def github @user.image = info['image'] @user.save - sign_in_and_redirect @user, :event => :authentication + 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 diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index fc116e7a..be060ef3 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -52,6 +52,7 @@ def render_flash_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 diff --git a/app/models/ability.rb b/app/models/ability.rb index 63f6fd78..65b8a499 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -4,6 +4,7 @@ class Ability def initialize(user) if user and user.nickname.present? can [:update, :decide_tip_amounts], Project, collaborators: {login: user.nickname} + can :create, Project end end end diff --git a/app/models/project.rb b/app/models/project.rb index 0d350f6f..91bcbf6f 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -10,8 +10,9 @@ class Project < ActiveRecord::Base has_one :tipping_policies_text, inverse_of: :project accepts_nested_attributes_for :tipping_policies_text - validates :full_name, uniqueness: true, presence: true - validates :github_id, uniqueness: true, presence: true + validates :name, presence: true + + before_validation :strip_full_name scope :enabled, -> { where(disabled: false) } scope :disabled, -> { where(disabled: true) } @@ -224,4 +225,10 @@ def paid_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 end diff --git a/app/views/home/index.html.haml b/app/views/home/index.html.haml index e26975de..3513ee24 100644 --- a/app/views/home/index.html.haml +++ b/app/views/home/index.html.haml @@ -23,26 +23,30 @@ / Example row of columns .row - .col-lg-4 + .col-lg-3 %h2 How it works? - %p People donate peercoins to projects. When someone's commit is accepted to the project repository, we automatically tip the author. + %p + Fundraisers create projects and collect funds to make something happen. %p %a.btn.btn-primary{href: faq_path} Frequently Asked Questions » - .col-lg-4 + .col-lg-3 %h2 Donate %p - Find a project you like and deposit peercoins to it. Your money will be accumulated with money of other donators to tip for new commits. + Donate to projects to make them happen. %p - %a.btn.btn-primary{href: projects_path} Find or add a project » - .col-lg-4 + %a.btn.btn-primary{href: projects_path} See projects » + .col-lg-3 %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. + Get paid to contribute to a project. + %p + %a.btn.btn-primary{href: projects_path} See projects » + .col-lg-3 + %h2 Raise funds + %p + Make something great happen by raising funds and distributing them. %p - %a.btn.btn-primary{href: projects_path} Supported projects » + %a.btn.btn-primary{href: new_project_path} Create a project » / - if current_user / %a.btn.btn-primary{href: user_path(current_user)} Change your peercoin address » / - else diff --git a/app/views/home/login.html.haml b/app/views/home/login.html.haml new file mode 100644 index 00000000..763f1e05 --- /dev/null +++ b/app/views/home/login.html.haml @@ -0,0 +1,7 @@ +- content_for :title do + Log in + +.row + .col-md-12 + %p.text-center + %a.btn.btn-default{href: user_omniauth_authorize_path(:github, origin: params[:return_url])} Log in with GitHub diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml new file mode 100644 index 00000000..78d20e70 --- /dev/null +++ b/app/views/projects/new.html.haml @@ -0,0 +1,12 @@ +- content_for :title do + New project + +%h1 New project +.row + .col-md-12 + = bootstrap_form_for @project do |f| + = f.alert_message "Please fix the errors below." + = f.text_field :name + = f.text_area :description, rows: 10 + = f.url_field :full_name, label: "GitHub URL (optional)" + = f.submit "Save" diff --git a/config/locales/en.yml b/config/locales/en.yml index e618cddd..7adec857 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -26,3 +26,5 @@ en: attributes: user: bitcoin_address: Peercoin address + project: + github_url: GitHub URL diff --git a/config/routes.rb b/config/routes.rb index 294468d4..2f024535 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -5,6 +5,8 @@ get 'audit' => 'home#audit' get 'faq' => 'home#faq' + get 'login' => 'home#login' + resources :users, :only => [:show, :update, :index] do collection do get :login @@ -13,7 +15,7 @@ post :send_tips_back end end - resources :projects, :only => [:show, :index, :create, :edit, :update] do + resources :projects, :only => [:new, :show, :index, :create, :edit, :update] do resources :tips, :only => [:index] member do get :qrcode diff --git a/features/create_project.feature b/features/create_project.feature new file mode 100644 index 00000000..65128318 --- /dev/null +++ b/features/create_project.feature @@ -0,0 +1,63 @@ +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 "Log 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 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 "Log 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 "Log 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 "Log 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" diff --git a/features/step_definitions/common.rb b/features/step_definitions/common.rb index 632b3f27..bcec6cdd 100644 --- a/features/step_definitions/common.rb +++ b/features/step_definitions/common.rb @@ -19,11 +19,11 @@ end Given(/^a project$/) do - @project = Project.create!(full_name: "example/test", github_id: 123, bitcoin_address: 'mq4NtnmQoQoPfNWEPbhSvxvncgtGo6L8WY', address_label: "example_project_account") + @project = Project.create!(name: "test", full_name: "example/test", github_id: 123, bitcoin_address: 'mq4NtnmQoQoPfNWEPbhSvxvncgtGo6L8WY', address_label: "example_project_account") end Given(/^a project "(.*?)"$/) do |arg1| - @project = Project.create!(full_name: "example/#{arg1}", github_id: Digest::SHA1.hexdigest(arg1), bitcoin_address: 'mq4NtnmQoQoPfNWEPbhSvxvncgtGo6L8WY') + @project = Project.create!(name: "test", full_name: "example/#{arg1}", github_id: Digest::SHA1.hexdigest(arg1), bitcoin_address: 'mq4NtnmQoQoPfNWEPbhSvxvncgtGo6L8WY') end Given(/^a deposit of "(.*?)"$/) do |arg1| diff --git a/features/step_definitions/create_project.rb b/features/step_definitions/create_project.rb new file mode 100644 index 00000000..e4c16737 --- /dev/null +++ b/features/step_definitions/create_project.rb @@ -0,0 +1,30 @@ + +Then(/^there should be a project "(.*?)"$/) do |arg1| + Project.pluck(:name).should include(arg1) + @project = Project.where(name: arg1).first +end + +Then(/^the description of the project should be$/) do |string| + @project.description.should eq(string) +end + +Then(/^I should be on the project page$/) do + current_url.should eq(project_url(@project)) +end + +Then(/^there should be no project$/) do + Project.all.should be_empty +end + +Then(/^the GitHub name of the project should be "(.*?)"$/) do |arg1| + @project.full_name.should eq(arg1) +end + +Then(/^the project GitHub ID should be "(.*?)"$/) do |arg1| + @project.github_id.should eq(arg1) +end + +Then(/^the project single collaborators should be "(.*?)"$/) do |arg1| + @project.collaborators.map(&:login).should eq([arg1]) +end + diff --git a/features/step_definitions/web.rb b/features/step_definitions/web.rb index e58ca113..c26f57cc 100644 --- a/features/step_definitions/web.rb +++ b/features/step_definitions/web.rb @@ -12,6 +12,17 @@ page.should have_content("Successfully authenticated") 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") @@ -22,6 +33,14 @@ end 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 From 56a5332edf5000045c3857a90299d871a17d0364 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Thu, 29 May 2014 13:05:26 +0200 Subject: [PATCH 179/372] Check that the old tip for commit still works on projects not holding tips --- app/models/project.rb | 22 ++++++----- features/step_definitions/common.rb | 13 ++++--- features/step_definitions/tip_for_commit.rb | 43 +++++++++++++++++++++ features/tip_for_commit.feature | 38 ++++++++++++++++++ features/tip_modifier_interface.feature | 1 + 5 files changed, 102 insertions(+), 15 deletions(-) create mode 100644 features/step_definitions/tip_for_commit.rb create mode 100644 features/tip_for_commit.feature diff --git a/app/models/project.rb b/app/models/project.rb index 91bcbf6f..48e7f040 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -55,21 +55,14 @@ def source_github_url "https://github.com/#{source_full_name}" end - def new_commits + def get_commits 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, Faraday::Error::ConnectionFailed => e @@ -82,7 +75,16 @@ def new_commits end def tip_commits - new_commits.each do |commit| + return unless self.deposits.any? + + get_commits.each do |commit| + # 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 diff --git a/features/step_definitions/common.rb b/features/step_definitions/common.rb index bcec6cdd..3fae3fe8 100644 --- a/features/step_definitions/common.rb +++ b/features/step_definitions/common.rb @@ -27,7 +27,7 @@ end Given(/^a deposit of "(.*?)"$/) do |arg1| - Deposit.create!(project: @project, amount: arg1.to_d * COIN, confirmations: 1) + Deposit.create!(project: @project, amount: arg1.to_d * COIN, confirmations: 1, created_at: 2.minutes.ago) end Given(/^the last known commit is "(.*?)"$/) do |arg1| @@ -35,7 +35,7 @@ end def add_new_commit(id, params = {}) - @new_commits ||= {} + @commits ||= {} defaults = { sha: id, commit: { @@ -43,13 +43,16 @@ def add_new_commit(id, params = {}) author: { email: "anonymous@example.com", }, + committer: { + date: Time.now, + } }, } - @new_commits[id] = defaults.deep_merge(params) + @commits[id] = defaults.deep_merge(params) end def find_new_commit(id) - @new_commits[id] + @commits[id] end Given(/^a new commit "(.*?)" with parent "([^"]*?)"$/) do |arg1, arg2| @@ -84,7 +87,7 @@ def find_new_commit(id) When(/^the new commits are read$/) do @project.reload - @project.should_receive(:new_commits).and_return(@new_commits.values.map(&:to_ostruct)) + @project.should_receive(:get_commits).and_return(@commits.values.map(&:to_ostruct)) @project.tip_commits 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..008ebb22 --- /dev/null +++ b/features/step_definitions/tip_for_commit.rb @@ -0,0 +1,43 @@ + +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| + commits = [] + table.hashes.each do |row| + commit = OpenStruct.new( + sha: row["sha"], + commit: OpenStruct.new( + message: row["message"] || "Some changes", + author: OpenStruct.new( + email: "author@example.com", + ), + committer: OpenStruct.new( + date: Time.now, + ), + ), + ) + commits << commit + end + + @project.should_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 + tips.should eq(table.hashes) +end diff --git a/features/tip_for_commit.feature b/features/tip_for_commit.feature new file mode 100644 index 00000000..8fa257e5 --- /dev/null +++ b/features/tip_for_commit.feature @@ -0,0 +1,38 @@ +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 | + | 123 | + | abc | + | 333 | + And our fee is "0" + And a deposit of "500" + + 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 | + + 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 | + | 123 | + | abc | + | 333 | + And our fee is "0" + And a deposit of "500" + + When the project tips are built from commits + Then the project should have these tips: + | commit | amount | + | 123 | | + | abc | | + | 333 | | diff --git a/features/tip_modifier_interface.feature b/features/tip_modifier_interface.feature index 16044470..d83b5809 100644 --- a/features/tip_modifier_interface.feature +++ b/features/tip_modifier_interface.feature @@ -108,6 +108,7 @@ Feature: A project collaborator can change the tips of commits Given the project holds tips 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" From 42b9972ec303c945aaaed3ed416b6f0a19d3f6cb Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Thu, 29 May 2014 13:25:53 +0200 Subject: [PATCH 180/372] Common form in edit and new --- app/controllers/projects_controller.rb | 2 +- app/views/projects/_form.html.haml | 11 +++++++++++ app/views/projects/edit.html.haml | 9 ++------- app/views/projects/new.html.haml | 7 +------ features/tip_modifier_interface.feature | 4 ++-- features/tipping_policies.feature | 4 ++-- 6 files changed, 19 insertions(+), 18 deletions(-) create mode 100644 app/views/projects/_form.html.haml diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 97f8c532..190fb67c 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -40,7 +40,7 @@ def update @project.tipping_policies_text.user = current_user end if @project.save - redirect_to project_path(@project), notice: "The project settings have been updated" + redirect_to project_path(@project), notice: "The project has been updated" else render 'edit' end diff --git a/app/views/projects/_form.html.haml b/app/views/projects/_form.html.haml new file mode 100644 index 00000000..225822fb --- /dev/null +++ b/app/views/projects/_form.html.haml @@ -0,0 +1,11 @@ += bootstrap_form_for @project, layout: :horizontal do |f| + = f.alert_message "Please fix the errors below." + = f.text_field :name + = f.text_area :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.url_field :full_name, label: "GitHub URL (optional)" + = f.form_group do + = f.check_box :hold_tips, label: "Do not send the tips immediatly. Give collaborators the ability to modify the tips before they're sent" + = f.form_group do + = f.primary "Save" diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 8d085f9c..12988975 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -1,12 +1,7 @@ - content_for :title do = @project.name - settings -%h1 #{@project.full_name} project settings +%h1= @project.name .row .col-md-12 - = bootstrap_form_for @project do |f| - = 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.check_box :hold_tips, label: "Do not send the tips immediatly. Give collaborators the ability to modify the tips before they're sent" - = f.submit 'Save the project settings', class: "btn btn-default" + = render "form" diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index 78d20e70..4f1bf18b 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -4,9 +4,4 @@ %h1 New project .row .col-md-12 - = bootstrap_form_for @project do |f| - = f.alert_message "Please fix the errors below." - = f.text_field :name - = f.text_area :description, rows: 10 - = f.url_field :full_name, label: "GitHub URL (optional)" - = f.submit "Save" + = render "form" diff --git a/features/tip_modifier_interface.feature b/features/tip_modifier_interface.feature index d83b5809..dc0f34f3 100644 --- a/features/tip_modifier_interface.feature +++ b/features/tip_modifier_interface.feature @@ -24,8 +24,8 @@ Feature: A project collaborator can change the tips of commits And I go to the project page And I click on "Change project settings" And I check "Do not send the tips immediatly. Give collaborators the ability to modify the tips before they're sent" - And I click on "Save the project settings" - Then I should see "The project settings have been updated" + And I click on "Save" + Then I should see "The project has been updated" When the new commits are read Then the tip amount for commit "BBB" should be undecided diff --git a/features/tipping_policies.feature b/features/tipping_policies.feature index 3b3c3e8a..c07c6a4a 100644 --- a/features/tipping_policies.feature +++ b/features/tipping_policies.feature @@ -15,8 +15,8 @@ Feature: A project collaborator can display the tipping policies of the project Blah blah """ - And I click on "Save the project settings" - Then I should see "The project settings have been updated" + 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 From e4b4d66cdd4c722e497c28c2a0311ae19afc28e3 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Thu, 29 May 2014 13:27:57 +0200 Subject: [PATCH 181/372] Better project show --- app/models/project.rb | 2 +- app/views/projects/show.html.haml | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/models/project.rb b/app/models/project.rb index 48e7f040..fd2d27d0 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -48,7 +48,7 @@ def update_github_collaborators(github_collaborators) end def github_url - "https://github.com/#{full_name}" + "https://github.com/#{full_name}" if full_name.present? end def source_github_url diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index 5422573b..c3ee72de 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -7,8 +7,9 @@ .row .col-md-12 %h1 - = @project.full_name - %small= link_to glyph(:github), @project.github_url, target: '_blank' + = @project.name + - if (url = @project.github_url).present? + %small= link_to glyph(:github), url, target: '_blank' .pull-right - if can? :update, @project = link_to "Change project settings", edit_project_path(@project), class: "btn btn-primary" From cba15b22aa4c0bb6c7aad62cba837689176c359b Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Thu, 29 May 2014 13:42:53 +0200 Subject: [PATCH 182/372] Inverted wording of hold checkbox --- app/controllers/projects_controller.rb | 2 +- app/models/project.rb | 11 +++++++++++ app/views/projects/_form.html.haml | 2 +- features/step_definitions/web.rb | 4 ++++ features/tip_modifier_interface.feature | 2 +- 5 files changed, 18 insertions(+), 3 deletions(-) diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 190fb67c..431e46ea 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -94,7 +94,7 @@ def create private def project_params - params.require(:project).permit(:name, :description, :full_name, :hold_tips, tipping_policies_text_attributes: [:text]) + params.require(:project).permit(:name, :description, :full_name, :auto_tip_commits, :hold_tips, tipping_policies_text_attributes: [:text]) end def load_project diff --git a/app/models/project.rb b/app/models/project.rb index fd2d27d0..d3bb388d 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -233,4 +233,15 @@ def strip_full_name self.full_name = full_name.gsub(/https?\:\/\/github.com\//, '') end end + + def auto_tip_commits + !hold_tips + end + + def auto_tip_commits=(value) + self.hold_tips = case value + when false, nil, "0" then true + else false + end + end end diff --git a/app/views/projects/_form.html.haml b/app/views/projects/_form.html.haml index 225822fb..7878894b 100644 --- a/app/views/projects/_form.html.haml +++ b/app/views/projects/_form.html.haml @@ -6,6 +6,6 @@ = fields.text_area :text, rows: 10, label: "Tipping policies" = f.url_field :full_name, label: "GitHub URL (optional)" = f.form_group do - = f.check_box :hold_tips, label: "Do not send the tips immediatly. Give collaborators the ability to modify the tips before they're sent" + = 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"} = f.form_group do = f.primary "Save" diff --git a/features/step_definitions/web.rb b/features/step_definitions/web.rb index c26f57cc..5cd9e525 100644 --- a/features/step_definitions/web.rb +++ b/features/step_definitions/web.rb @@ -53,6 +53,10 @@ check(arg1) end +Given(/^I uncheck "(.*?)"$/) do |arg1| + uncheck(arg1) +end + Then(/^I should see "(.*?)"$/) do |arg1| page.should have_content(arg1) end diff --git a/features/tip_modifier_interface.feature b/features/tip_modifier_interface.feature index dc0f34f3..dc550ea3 100644 --- a/features/tip_modifier_interface.feature +++ b/features/tip_modifier_interface.feature @@ -23,7 +23,7 @@ Feature: A project collaborator can change the tips of commits Given I'm logged in as "seldon" And I go to the project page And I click on "Change project settings" - And I check "Do not send the tips immediatly. Give collaborators the ability to modify the tips before they're sent" + 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" From a2459ccbfca120a20f8a407496c945d6966ebd8b Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Thu, 29 May 2014 13:43:07 +0200 Subject: [PATCH 183/372] Allow non url full_name --- app/views/projects/_form.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/_form.html.haml b/app/views/projects/_form.html.haml index 7878894b..8b115018 100644 --- a/app/views/projects/_form.html.haml +++ b/app/views/projects/_form.html.haml @@ -4,7 +4,7 @@ = f.text_area :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.url_field :full_name, label: "GitHub URL (optional)" + = 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"} = f.form_group do From e3372951f2fcd5f333ffcc60c387c099868409cb Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Thu, 29 May 2014 13:46:57 +0200 Subject: [PATCH 184/372] Better project show --- app/models/project.rb | 1 + app/views/projects/show.html.haml | 7 +++---- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/models/project.rb b/app/models/project.rb index d3bb388d..80031015 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -237,6 +237,7 @@ def strip_full_name 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 diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index c3ee72de..f0bcde7d 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -44,12 +44,11 @@ %p #{100-(CONFIG["our_fee"]*100).round}% of deposited funds will be used to tip for new commits. .col-md-8 - unless @project.description.blank? - .well.well-sm= @project.description + %h4 Project description + %p= @project.description %h4 Balance = btc_human @project.available_amount - - if @project.hold_tips? - (each new commit receives a percentage of available balance) - - else + - if @project.auto_tip_commits? (each new commit receives #{(CONFIG["tip"]*100).round}% of available balance) - if (unconfirmed_amount = @project.unconfirmed_amount) > 0 (#{btc_human unconfirmed_amount} unconfirmed) From ad75feb9d2f5d755babd5ef12772bf7863d02d2e Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Thu, 29 May 2014 15:44:44 +0200 Subject: [PATCH 185/372] Do not update project info from GitHub anymore --- app/models/project.rb | 61 ---------------------------------- app/views/home/index.html.haml | 2 +- lib/bitcoin_tipper.rb | 6 ---- 3 files changed, 1 insertion(+), 68 deletions(-) diff --git a/app/models/project.rb b/app/models/project.rb index 80031015..d14bf485 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -17,36 +17,6 @@ class Project < ActiveRecord::Base scope :enabled, -> { where(disabled: false) } scope :disabled, -> { where(disabled: true) } - def update_github_info repo - self.github_id = repo.id - 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 - - def update_github_collaborators(github_collaborators) - github_logins = github_collaborators.map(&:login) - existing_logins = collaborators.map(&:login) - - collaborators.each do |collaborator| - unless github_logins.include?(collaborator.login) - collaborator.mark_for_destruction - end - end - - github_collaborators.each do |github_collaborator| - unless existing_logins.include?(github_collaborator.login) - collaborators.build(login: github_collaborator.login) - end - end - - save! - end - def github_url "https://github.com/#{full_name}" if full_name.present? end @@ -164,37 +134,6 @@ def self.update_cache end end - def github_info - client = Octokit::Client.new \ - :client_id => CONFIG['github']['key'], - :client_secret => CONFIG['github']['secret'] - if github_id.present? - client.get("/repositories/#{github_id}") - else - client.repo(full_name) - end - end - - def github_collaborators - client = Octokit::Client.new \ - :client_id => CONFIG['github']['key'], - :client_secret => CONFIG['github']['secret'] - client.get("/repos/#{full_name}/collaborators") + - (client.get("/orgs/#{full_name.split('/').first}/members") rescue []) - end - - def update_info - begin - update_github_info(github_info) - update_github_collaborators(github_collaborators) - rescue Octokit::BadGateway, Octokit::NotFound, Octokit::InternalServerError, - Errno::ETIMEDOUT, Faraday::Error::ConnectionFailed => e - Rails.logger.info "Project ##{id}: #{e.class} happened" - rescue StandardError => e - Airbrake.notify(e) - end - end - def tips_to_pay tips.select(&:to_pay?) end diff --git a/app/views/home/index.html.haml b/app/views/home/index.html.haml index 3513ee24..8c2bb9e5 100644 --- a/app/views/home/index.html.haml +++ b/app/views/home/index.html.haml @@ -26,7 +26,7 @@ .col-lg-3 %h2 How it works? %p - Fundraisers create projects and collect funds to make something happen. + Fundraisers collect funds and distribute them to make something happen. %p %a.btn.btn-primary{href: faq_path} Frequently Asked Questions » .col-lg-3 diff --git a/lib/bitcoin_tipper.rb b/lib/bitcoin_tipper.rb index 5b04e49c..12804e23 100644 --- a/lib/bitcoin_tipper.rb +++ b/lib/bitcoin_tipper.rb @@ -14,12 +14,6 @@ def self.work end end - Rails.logger.info "Updating projects info..." - Project.enabled.each do |project| - Rails.logger.info " Project #{project.id} #{project.full_name}" - project.update_info - end - self.create_sendmany Rails.logger.info "Traversing sendmanies..." From 164e6e6443bc8bee827d043d818f9a0513854bd0 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Thu, 29 May 2014 15:48:43 +0200 Subject: [PATCH 186/372] Cleaned up project list --- app/views/projects/index.html.haml | 22 ++++------------------ 1 file changed, 4 insertions(+), 18 deletions(-) diff --git a/app/views/projects/index.html.haml b/app/views/projects/index.html.haml index e07e8f3a..dd579a14 100644 --- a/app/views/projects/index.html.haml +++ b/app/views/projects/index.html.haml @@ -1,17 +1,9 @@ -%h1 Supported Projects -%p - = form_tag projects_path, role: 'form', 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 - = submit_tag "Find or add project", class: 'btn form-control btn-default' +%h1 Projects %p %table.table %thead %tr - %th Repository + %th Name %th Description %th Watchers %th Balance @@ -20,15 +12,9 @@ - @projects.each do |project| %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' + %strong= link_to project.name, project %td= project.description %td= project.watchers_count %td= btc_human project.available_amount_cache - %td= link_to 'Support', project, class: 'btn btn-success' + %td= link_to 'Details', project, class: 'btn btn-success' = paginate @projects From 5b6a551b3272652bda86f309246ff7425463ea11 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Thu, 29 May 2014 16:05:20 +0200 Subject: [PATCH 187/372] Added detailed description --- app/controllers/projects_controller.rb | 2 +- app/views/projects/_form.html.haml | 3 ++- app/views/projects/show.html.haml | 5 +++-- .../20140529135156_add_detailed_descrition_to_project.rb | 5 +++++ db/schema.rb | 3 ++- 5 files changed, 13 insertions(+), 5 deletions(-) create mode 100644 db/migrate/20140529135156_add_detailed_descrition_to_project.rb diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 431e46ea..678bab25 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -94,7 +94,7 @@ def create private def project_params - params.require(:project).permit(:name, :description, :full_name, :auto_tip_commits, :hold_tips, tipping_policies_text_attributes: [:text]) + params.require(:project).permit(:name, :description, :detailed_description, :full_name, :auto_tip_commits, :hold_tips, tipping_policies_text_attributes: [:text]) end def load_project diff --git a/app/views/projects/_form.html.haml b/app/views/projects/_form.html.haml index 8b115018..f1d65160 100644 --- a/app/views/projects/_form.html.haml +++ b/app/views/projects/_form.html.haml @@ -1,7 +1,8 @@ = bootstrap_form_for @project, layout: :horizontal do |f| = f.alert_message "Please fix the errors below." = f.text_field :name - = f.text_area :description, rows: 10 + = 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)" diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index f0bcde7d..f073cca4 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -44,8 +44,9 @@ %p #{100-(CONFIG["our_fee"]*100).round}% of deposited funds will be used to tip for new commits. .col-md-8 - unless @project.description.blank? - %h4 Project description - %p= @project.description + %h3= @project.description + - unless @project.detailed_description.blank? + %p= @project.detailed_description %h4 Balance = btc_human @project.available_amount - if @project.auto_tip_commits? 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/schema.rb b/db/schema.rb index 0e6075e3..5252adb5 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20140406071705) do +ActiveRecord::Schema.define(version: 20140529135156) do create_table "cold_storage_transfers", force: true do |t| t.integer "project_id" @@ -69,6 +69,7 @@ t.boolean "disabled", default: false t.integer "account_balance", limit: 8 t.string "disabled_reason" + t.text "detailed_description" end add_index "projects", ["full_name"], name: "index_projects_on_full_name", unique: true From 7070b6a6e7e058aba02043e185ba663c2d7b8921 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Thu, 29 May 2014 16:15:27 +0200 Subject: [PATCH 188/372] Render detailed description in markdown --- Gemfile | 1 + Gemfile.lock | 2 ++ app/helpers/application_helper.rb | 5 +++++ app/views/projects/show.html.haml | 4 ++-- config/locales/en.yml | 1 + features/project_detailed_description.feature | 14 ++++++++++++++ features/step_definitions/common.rb | 6 ++++++ features/step_definitions/web.rb | 5 +++++ 8 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 features/project_detailed_description.feature diff --git a/Gemfile b/Gemfile index 4ea14802..e9357be4 100644 --- a/Gemfile +++ b/Gemfile @@ -73,6 +73,7 @@ gem 'rack-canonical-host' gem 'bootstrap_form', github: 'sigmike/rails-bootstrap-forms', branch: 'removed_for_on_radio_label' gem 'html_pipeline_rails' gem 'rails_autolink' +gem 'redcarpet' group :test do gem 'cucumber-rails', :require => false diff --git a/Gemfile.lock b/Gemfile.lock index 43d1b201..e9bb835b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -225,6 +225,7 @@ GEM rake (10.1.1) rdoc (4.1.1) json (~> 1.4) + redcarpet (3.1.2) ref (1.0.5) rqrcode (0.4.2) rqrcode-rails3 (0.1.7) @@ -326,6 +327,7 @@ DEPENDENCIES rack-canonical-host rails (~> 4.0.2) rails_autolink + redcarpet rqrcode-rails3 rspec-rails sass-rails (~> 4.0.0) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index be060ef3..d04ded31 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -58,4 +58,9 @@ def render_flash_message end html.join("\n").html_safe end + + def render_markdown(source) + markdown = Redcarpet::Markdown.new(Redcarpet::Render::HTML) + markdown.render(source).html_safe + end end diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index f073cca4..cb42689c 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -12,7 +12,7 @@ %small= link_to glyph(:github), url, target: '_blank' .pull-right - if can? :update, @project - = link_to "Change project settings", edit_project_path(@project), class: "btn btn-primary" + = link_to "Edit project", edit_project_path(@project), class: "btn btn-primary" - 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-warning" @@ -46,7 +46,7 @@ - unless @project.description.blank? %h3= @project.description - unless @project.detailed_description.blank? - %p= @project.detailed_description + = render_markdown @project.detailed_description %h4 Balance = btc_human @project.available_amount - if @project.auto_tip_commits? diff --git a/config/locales/en.yml b/config/locales/en.yml index 7adec857..5514099d 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -28,3 +28,4 @@ en: bitcoin_address: Peercoin address project: github_url: GitHub URL + detailed_description: Detailed description diff --git a/features/project_detailed_description.feature b/features/project_detailed_description.feature new file mode 100644 index 00000000..427e1fb1 --- /dev/null +++ b/features/project_detailed_description.feature @@ -0,0 +1,14 @@ +Feature: Project detailed description is markdown formatted + Scenario: + 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" + And 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/" + diff --git a/features/step_definitions/common.rb b/features/step_definitions/common.rb index 3fae3fe8..42724733 100644 --- a/features/step_definitions/common.rb +++ b/features/step_definitions/common.rb @@ -117,6 +117,12 @@ def find_new_commit(id) end end +Given(/^the project single collaborator is "(.*?)"$/) do |arg1| + @project.reload + @project.collaborators.each(&:destroy) + @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 diff --git a/features/step_definitions/web.rb b/features/step_definitions/web.rb index 5cd9e525..f162cab9 100644 --- a/features/step_definitions/web.rb +++ b/features/step_definitions/web.rb @@ -83,3 +83,8 @@ page.should have_content("Balance #{arg1}") end +Then(/^I should see a link "(.*?)" to "(.*?)"$/) do |arg1, arg2| + link = find("a", text: arg1, exact: true) + link["href"].should eq(arg2) +end + From d80297ac762085e92d2a8b911252107f43396c00 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Thu, 29 May 2014 16:23:45 +0200 Subject: [PATCH 189/372] Filter XSS attempt --- app/helpers/application_helper.rb | 2 +- features/project_detailed_description.feature | 14 ++++++++++++-- features/step_definitions/web.rb | 5 +++++ 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index d04ded31..1d8548c2 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -60,7 +60,7 @@ def render_flash_message end def render_markdown(source) - markdown = Redcarpet::Markdown.new(Redcarpet::Render::HTML) + markdown = Redcarpet::Markdown.new(Redcarpet::Render::HTML.new(safe_links_only: true)) markdown.render(source).html_safe end end diff --git a/features/project_detailed_description.feature b/features/project_detailed_description.feature index 427e1fb1..a6813773 100644 --- a/features/project_detailed_description.feature +++ b/features/project_detailed_description.feature @@ -1,14 +1,24 @@ Feature: Project detailed description is markdown formatted - Scenario: + 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" - And I fill "Detailed description" with: + + 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')" + diff --git a/features/step_definitions/web.rb b/features/step_definitions/web.rb index f162cab9..248ca15d 100644 --- a/features/step_definitions/web.rb +++ b/features/step_definitions/web.rb @@ -88,3 +88,8 @@ link["href"].should eq(arg2) end +Then(/^I should not see a link "(.*?)" to "(.*?)"$/) do |arg1, arg2| + link = all("a", text: arg1, exact: true).first + (link.nil? or link["href"] != arg2).should be_true +end + From 45ee3824c012d49b641f850dfd9d51559d0f0d4a Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Thu, 29 May 2014 16:25:37 +0200 Subject: [PATCH 190/372] Filter HTML --- app/helpers/application_helper.rb | 2 +- features/project_detailed_description.feature | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 1d8548c2..ffc3aa5a 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -60,7 +60,7 @@ def render_flash_message end def render_markdown(source) - markdown = Redcarpet::Markdown.new(Redcarpet::Render::HTML.new(safe_links_only: true)) + markdown = Redcarpet::Markdown.new(Redcarpet::Render::HTML.new(safe_links_only: true, filter_html: true)) markdown.render(source).html_safe end end diff --git a/features/project_detailed_description.feature b/features/project_detailed_description.feature index a6813773..f1ac18e2 100644 --- a/features/project_detailed_description.feature +++ b/features/project_detailed_description.feature @@ -22,3 +22,11 @@ Feature: Project detailed description is markdown formatted 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')" + From 455de383178ba25f6ed7865eec5a4b2435c1adb9 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Thu, 29 May 2014 16:29:47 +0200 Subject: [PATCH 191/372] Added extra sanitizer on markdown --- Gemfile | 1 + Gemfile.lock | 3 +++ app/helpers/application_helper.rb | 4 +++- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index e9357be4..2aeb8554 100644 --- a/Gemfile +++ b/Gemfile @@ -74,6 +74,7 @@ gem 'bootstrap_form', github: 'sigmike/rails-bootstrap-forms', branch: 'removed_ gem 'html_pipeline_rails' gem 'rails_autolink' gem 'redcarpet' +gem 'sanitize' group :test do gem 'cucumber-rails', :require => false diff --git a/Gemfile.lock b/Gemfile.lock index e9bb835b..cbe3de23 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -242,6 +242,8 @@ GEM rspec-core (~> 2.14.0) rspec-expectations (~> 2.14.0) rspec-mocks (~> 2.14.0) + sanitize (2.1.0) + nokogiri (>= 1.4.4) sass (3.2.13) sass-rails (4.0.1) railties (>= 4.0.0, < 5.0) @@ -330,6 +332,7 @@ DEPENDENCIES redcarpet rqrcode-rails3 rspec-rails + sanitize sass-rails (~> 4.0.0) sdoc sqlite3 diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index ffc3aa5a..47b1ed98 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -61,6 +61,8 @@ def render_flash_message def render_markdown(source) markdown = Redcarpet::Markdown.new(Redcarpet::Render::HTML.new(safe_links_only: true, filter_html: true)) - markdown.render(source).html_safe + html = markdown.render(source) + clean = Sanitize.clean(html, Sanitize::Config::RELAXED) + clean.html_safe end end From f8c75260bf6212e655ee3ef99694cba6bb1e7748 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Thu, 29 May 2014 16:33:46 +0200 Subject: [PATCH 192/372] Allow images in detailed description --- features/project_detailed_description.feature | 8 ++++++++ features/step_definitions/web.rb | 3 +++ 2 files changed, 11 insertions(+) diff --git a/features/project_detailed_description.feature b/features/project_detailed_description.feature index f1ac18e2..2737f121 100644 --- a/features/project_detailed_description.feature +++ b/features/project_detailed_description.feature @@ -30,3 +30,11 @@ Feature: Project detailed description is markdown formatted 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: + """ + ![foo](http://example.com/img.jpg) + """ + And I click on "Save" + Then I should not see the image "http://example.com/img.jpg" + diff --git a/features/step_definitions/web.rb b/features/step_definitions/web.rb index 248ca15d..ca696208 100644 --- a/features/step_definitions/web.rb +++ b/features/step_definitions/web.rb @@ -93,3 +93,6 @@ (link.nil? or link["href"] != arg2).should be_true end +Then(/^I should not see the image "(.*?)"$/) do |arg1| + find("img[src=\"#{arg1}\"]") +end From d422702a70d032af1103b6c08b6f5bfc55cdd771 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Thu, 29 May 2014 17:05:46 +0200 Subject: [PATCH 193/372] Fixed features --- features/tip_modifier_interface.feature | 4 ++-- features/tipping_policies.feature | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/features/tip_modifier_interface.feature b/features/tip_modifier_interface.feature index dc550ea3..87a42071 100644 --- a/features/tip_modifier_interface.feature +++ b/features/tip_modifier_interface.feature @@ -22,7 +22,7 @@ Feature: A project collaborator can change the tips of commits 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 "Change project settings" + 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" @@ -55,7 +55,7 @@ Feature: A project collaborator can change the tips of commits 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 "Change project settings" + 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 diff --git a/features/tipping_policies.feature b/features/tipping_policies.feature index c07c6a4a..c8f6ed09 100644 --- a/features/tipping_policies.feature +++ b/features/tipping_policies.feature @@ -8,7 +8,7 @@ Feature: A project collaborator can display the tipping policies of the project 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 "Change project settings" + And I click on "Edit project" And I fill "Tipping policies" with: """ All commits are huge! From 7ea6694f7901990f6792f2bc40dfee34ae3ebe17 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Thu, 29 May 2014 17:22:44 +0200 Subject: [PATCH 194/372] Generate donation address at project creation --- app/controllers/projects_controller.rb | 5 ----- app/models/project.rb | 8 ++++++++ features/create_project.feature | 2 ++ features/step_definitions/create_project.rb | 8 ++++++++ 4 files changed, 18 insertions(+), 5 deletions(-) diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 678bab25..3684aa79 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -14,11 +14,6 @@ def show redirect_to root_path, alert: "Project not found" return end - if @project.bitcoin_address.nil? and (github_id = @project.github_id).present? - label = "#{github_id}@peer4commit" - address = BitcoinDaemon.instance.get_new_address(label) - @project.update_attributes(bitcoin_address: address, address_label: label) - end end def new diff --git a/app/models/project.rb b/app/models/project.rb index d14bf485..c0405373 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -13,6 +13,7 @@ class Project < ActiveRecord::Base validates :name, presence: true before_validation :strip_full_name + after_create :generate_address! scope :enabled, -> { where(disabled: false) } scope :disabled, -> { where(disabled: true) } @@ -184,4 +185,11 @@ def auto_tip_commits=(value) 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 end diff --git a/features/create_project.feature b/features/create_project.feature index 65128318..7ea1be45 100644 --- a/features/create_project.feature +++ b/features/create_project.feature @@ -18,6 +18,8 @@ Feature: An user can create a project, linked with GitHub or not. """ 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 diff --git a/features/step_definitions/create_project.rb b/features/step_definitions/create_project.rb index e4c16737..f3ba6a8e 100644 --- a/features/step_definitions/create_project.rb +++ b/features/step_definitions/create_project.rb @@ -28,3 +28,11 @@ @project.collaborators.map(&:login).should eq([arg1]) end +Then(/^the project address label should be "(.*?)"$/) do |arg1| + @project.address_label.should eq(arg1) +end + +Then(/^the project donation address should be the same as account "(.*?)"$/) do |arg1| + @project.bitcoin_address.should eq(BitcoinDaemon.instance.get_addresses_by_account(arg1).first) +end + From 3a5a6943d8beb6a937a44218ed6c3490a46cd01b Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Fri, 30 May 2014 15:52:59 +0200 Subject: [PATCH 195/372] Renamed sendmany to distribution --- app/controllers/withdrawals_controller.rb | 4 +-- app/models/{sendmany.rb => distribution.rb} | 6 ++-- app/models/project.rb | 4 +-- app/models/tip.rb | 10 +++--- app/views/home/audit.html.haml | 2 +- app/views/projects/show.html.haml | 2 +- app/views/tips/index.html.haml | 10 +++--- app/views/withdrawals/index.html.haml | 8 ++--- ...0132209_rename_sendmany_to_distribution.rb | 6 ++++ db/schema.rb | 34 +++++++++---------- lib/balance_updater.rb | 6 ++-- lib/bitcoin_tipper.rb | 18 +++++----- test/fixtures/tips.yml | 4 +-- test/models/sendmany_test.rb | 7 ---- 14 files changed, 60 insertions(+), 61 deletions(-) rename app/models/{sendmany.rb => distribution.rb} (71%) create mode 100644 db/migrate/20140530132209_rename_sendmany_to_distribution.rb delete mode 100644 test/models/sendmany_test.rb diff --git a/app/controllers/withdrawals_controller.rb b/app/controllers/withdrawals_controller.rb index 8c9168e5..a0515ea5 100644 --- a/app/controllers/withdrawals_controller.rb +++ b/app/controllers/withdrawals_controller.rb @@ -1,5 +1,5 @@ class WithdrawalsController < ApplicationController def index - @sendmanies = Sendmany.order(created_at: :desc).page(params[:page]).per(30) + @distributions = Distribution.order(created_at: :desc).page(params[:page]).per(30) end -end \ No newline at end of file +end diff --git a/app/models/sendmany.rb b/app/models/distribution.rb similarity index 71% rename from app/models/sendmany.rb rename to app/models/distribution.rb index 69e47486..6dcf6486 100644 --- a/app/models/sendmany.rb +++ b/app/models/distribution.rb @@ -1,5 +1,5 @@ -class Sendmany < ActiveRecord::Base - belongs_to :project, inverse_of: :sendmanies +class Distribution < ActiveRecord::Base + belongs_to :project, inverse_of: :distributions has_many :tips scope :error, -> { where(is_error: true) } @@ -14,7 +14,7 @@ def send_transaction update_attribute :is_error, true # it's a lock to prevent duplicates - raise "Not enough funds on Sendmany##{id}" if total_amount > project.available_amount + raise "Not enough funds on Distribution##{id}" if total_amount > project.available_amount txid = BitcoinDaemon.instance.send_many(project.address_label, JSON.parse(data)) diff --git a/app/models/project.rb b/app/models/project.rb index c0405373..106215f4 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -3,7 +3,7 @@ class Project < ActiveRecord::Base has_many :tips, inverse_of: :project accepts_nested_attributes_for :tips has_many :collaborators - has_many :sendmanies, inverse_of: :project + has_many :distributions, inverse_of: :project has_many :cold_storage_transfers @@ -163,7 +163,7 @@ def send_to_cold_storage!(amount, address_index = 0) def paid_fee [ - sendmanies.map(&:fee), + distributions.map(&:fee), cold_storage_transfers.map(&:fee), ].flatten.compact.sum end diff --git a/app/models/tip.rb b/app/models/tip.rb index 0c6f4888..e13118e2 100644 --- a/app/models/tip.rb +++ b/app/models/tip.rb @@ -1,13 +1,13 @@ class Tip < ActiveRecord::Base belongs_to :user - belongs_to :sendmany + belongs_to :distribution belongs_to :project, inverse_of: :tips validates :amount, numericality: {greater_or_equal_than: 0, allow_nil: true} - scope :not_sent, -> { where(sendmany_id: nil) } + scope :not_sent, -> { where(distribution_id: nil) } def not_sent? - sendmany_id.nil? + distribution_id.nil? end scope :unpaid, -> { non_refunded.not_sent } @@ -26,9 +26,9 @@ def free? amount == 0 end - scope :paid, -> { where.not(sendmany_id: nil) } + scope :paid, -> { where.not(distribution_id: nil) } def paid? - !!sendmany_id + !!distribution_id end scope :refunded, -> { where.not(refunded_at: nil) } diff --git a/app/views/home/audit.html.haml b/app/views/home/audit.html.haml index 54e6f0fa..029d0bdb 100644 --- a/app/views/home/audit.html.haml +++ b/app/views/home/audit.html.haml @@ -4,7 +4,7 @@ %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, :sendmanies, :tips, :deposits) + - 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 diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index cb42689c..1cc87fe6 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -70,7 +70,7 @@ %h4 Tips Paid = btc_human @project.tips_paid_amount - - if (failed_sendmanies = @project.sendmanies.error).any? and (amount = failed_sendmanies.map(&:tips).flatten.sum(&:amount)) > 0 + - if (failed_distributions = @project.distributions.error).any? and (amount = failed_distributions.map(&:tips).flatten.sum(&:amount)) > 0 .alert.alert-danger %strong Some tip transactions failed. A total of #{btc_human amount} may not have been sent. diff --git a/app/views/tips/index.html.haml b/app/views/tips/index.html.haml index cbe6636a..1fb23669 100644 --- a/app/views/tips/index.html.haml +++ b/app/views/tips/index.html.haml @@ -28,8 +28,8 @@ %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{class: tip.sendmany.try(:is_error?) ? "danger" : nil} - - if tip.sendmany.nil? + %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? @@ -42,8 +42,8 @@ - else Waiting for withdrawal - else - - if tip.sendmany.is_error? + - if tip.distribution.is_error? Transaction failed - - if tip.sendmany.txid.present? - = link_to tip.sendmany.txid, transaction_url(tip.sendmany.txid), target: :blank + - if tip.distribution.txid.present? + = link_to tip.distribution.txid, transaction_url(tip.distribution.txid), target: :blank = paginate @tips diff --git a/app/views/withdrawals/index.html.haml b/app/views/withdrawals/index.html.haml index 5e049f3a..fd564a4c 100644 --- a/app/views/withdrawals/index.html.haml +++ b/app/views/withdrawals/index.html.haml @@ -7,8 +7,8 @@ %th Transaction %th Result %tbody - - @sendmanies.each do |sendmany| + - @distributions.each do |distribution| %tr - %td= l sendmany.created_at, format: :short - %td= link_to sendmany.txid, transaction_url(sendmany.txid), target: '_blank' - %td= sendmany.is_error ? "Error" : "Success" + %td= l distribution.created_at, format: :short + %td= link_to distribution.txid, transaction_url(distribution.txid), target: '_blank' + %td= distribution.is_error ? "Error" : "Success" 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/schema.rb b/db/schema.rb index 5252adb5..c04232a3 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20140529135156) do +ActiveRecord::Schema.define(version: 20140530132209) do create_table "cold_storage_transfers", force: true do |t| t.integer "project_id" @@ -49,6 +49,19 @@ add_index "deposits", ["project_id"], name: "index_deposits_on_project_id" + create_table "distributions", force: true do |t| + t.string "txid" + t.text "data" + t.string "result" + t.boolean "is_error" + t.datetime "created_at" + t.datetime "updated_at" + t.integer "project_id" + t.integer "fee" + end + + add_index "distributions", ["project_id"], name: "index_distributions_on_project_id" + create_table "projects", force: true do |t| t.string "url" t.string "bitcoin_address" @@ -75,19 +88,6 @@ add_index "projects", ["full_name"], name: "index_projects_on_full_name", unique: true add_index "projects", ["github_id"], name: "index_projects_on_github_id", unique: true - create_table "sendmanies", force: true do |t| - t.string "txid" - t.text "data" - t.string "result" - t.boolean "is_error" - t.datetime "created_at" - t.datetime "updated_at" - t.integer "project_id" - t.integer "fee" - end - - add_index "sendmanies", ["project_id"], name: "index_sendmanies_on_project_id" - create_table "tipping_policies_texts", force: true do |t| t.integer "project_id" t.integer "user_id" @@ -101,8 +101,8 @@ create_table "tips", force: true 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" @@ -111,8 +111,8 @@ t.string "commit_message" end + add_index "tips", ["distribution_id"], name: "index_tips_on_distribution_id" 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" create_table "users", force: true do |t| diff --git a/lib/balance_updater.rb b/lib/balance_updater.rb index 4c0e6686..1f8277b4 100644 --- a/lib/balance_updater.rb +++ b/lib/balance_updater.rb @@ -25,9 +25,9 @@ def self.work category = transaction["category"] fee = transaction["fee"] - if category == "send" and sendmany = Sendmany.where(txid: txid).first - raise "No fee on sendmany #{sendmany.inspect}" unless fee - sendmany.update(fee: -fee * COIN) + 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 diff --git a/lib/bitcoin_tipper.rb b/lib/bitcoin_tipper.rb index 12804e23..ffbd0a9f 100644 --- a/lib/bitcoin_tipper.rb +++ b/lib/bitcoin_tipper.rb @@ -14,11 +14,11 @@ def self.work end end - self.create_sendmany + self.create_distributions Rails.logger.info "Traversing sendmanies..." - Sendmany.where(txid: nil).each do |sendmany| - sendmany.send_transaction + Distribution.where(txid: nil).each do |distribution| + distribution.send_transaction end Rails.logger.info "Refunding unclaimed tips..." @@ -31,21 +31,21 @@ 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 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 - sendmany = Sendmany.create(project_id: project.id) + distribution = Distribution.create(project_id: project.id) outs = Hash.new { 0.to_d } tips.each do |tip| - tip.update_attribute :sendmany_id, sendmany.id + tip.update_attribute :distribution_id, distribution.id outs[tip.user.bitcoin_address] += tip.amount.to_d / COIN end - sendmany.update_attribute :data, outs.to_json - Rails.logger.info " #{sendmany.inspect}" + distribution.update_attribute :data, outs.to_json + Rails.logger.info " #{distribution.inspect}" end end end 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/sendmany_test.rb deleted file mode 100644 index 13c4f4e3..00000000 --- a/test/models/sendmany_test.rb +++ /dev/null @@ -1,7 +0,0 @@ -require 'test_helper' - -class SendmanyTest < ActiveSupport::TestCase - # test "the truth" do - # assert true - # end -end From cc07c781d0161233d4f6792ce5435f9f04208501 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Fri, 30 May 2014 15:58:06 +0200 Subject: [PATCH 196/372] Renamed withdrawals controller to distributions controller --- .../{withdrawals_controller.rb => distributions_controller.rb} | 2 +- app/views/{withdrawals => distributions}/index.html.haml | 0 config/routes.rb | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename app/controllers/{withdrawals_controller.rb => distributions_controller.rb} (66%) rename app/views/{withdrawals => distributions}/index.html.haml (100%) diff --git a/app/controllers/withdrawals_controller.rb b/app/controllers/distributions_controller.rb similarity index 66% rename from app/controllers/withdrawals_controller.rb rename to app/controllers/distributions_controller.rb index a0515ea5..9d6822d1 100644 --- a/app/controllers/withdrawals_controller.rb +++ b/app/controllers/distributions_controller.rb @@ -1,4 +1,4 @@ -class WithdrawalsController < ApplicationController +class DistributionsController < ApplicationController def index @distributions = Distribution.order(created_at: :desc).page(params[:page]).per(30) end diff --git a/app/views/withdrawals/index.html.haml b/app/views/distributions/index.html.haml similarity index 100% rename from app/views/withdrawals/index.html.haml rename to app/views/distributions/index.html.haml diff --git a/config/routes.rb b/config/routes.rb index 2f024535..a99ed6b0 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -24,7 +24,7 @@ end end resources :tips, :only => [:index] - resources :withdrawals, :only => [:index] + resources :distributions, :only => [:index] devise_for :users, :controllers => { From ad91380ee44166e4fa236f0fa805290b79c97e5f Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Fri, 30 May 2014 17:11:03 +0200 Subject: [PATCH 197/372] Removed unused client side QR code generation --- app/assets/javascripts/projects.js.coffee | 10 ---------- 1 file changed, 10 deletions(-) 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 From e9973fc86097d0897b5a22463feeff0e99a13827 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Fri, 30 May 2014 16:14:32 +0200 Subject: [PATCH 198/372] Only send error to Airbrake if it's configured --- app/models/project.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/models/project.rb b/app/models/project.rb index 106215f4..1ef88569 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -39,7 +39,11 @@ def get_commits Errno::ETIMEDOUT, Faraday::Error::ConnectionFailed => 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 || [] From b9f42d914dcd8d09420f8d57d8457425f4619bf6 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Fri, 30 May 2014 17:12:13 +0200 Subject: [PATCH 199/372] XHR access denied returns error 500 --- app/controllers/application_controller.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 629bc2b5..b32e181c 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -4,7 +4,11 @@ class ApplicationController < ActionController::Base protect_from_forgery with: :exception rescue_from CanCan::AccessDenied do |exception| - redirect_to root_path, :alert => "Access denied" + if request.xhr? + raise exception + else + redirect_to root_path, :alert => "Access denied" + end end protected From d7eb68c0cee0d328db3f47f51c8d15f2e63ece56 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Fri, 30 May 2014 17:11:48 +0200 Subject: [PATCH 200/372] Use poltergeist as test browser when javascript is needed --- Gemfile | 1 + Gemfile.lock | 8 ++++++++ features/support/env.rb | 9 +++++++++ 3 files changed, 18 insertions(+) diff --git a/Gemfile b/Gemfile index 2aeb8554..1d3f88a5 100644 --- a/Gemfile +++ b/Gemfile @@ -82,4 +82,5 @@ group :test do gem 'database_cleaner' gem 'rspec-rails' gem 'factory_girl_rails' + gem 'poltergeist' end diff --git a/Gemfile.lock b/Gemfile.lock index cbe3de23..9d04d9c1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -86,6 +86,7 @@ GEM rack-test (>= 0.5.4) xpath (~> 2.0) chronic (0.10.2) + cliver (0.3.2) coffee-rails (4.0.1) coffee-script (>= 2.2.0) railties (>= 4.0.0, < 5.0) @@ -198,6 +199,11 @@ GEM omniauth (~> 1.0) orm_adapter (0.5.0) pg (0.17.1) + poltergeist (1.5.1) + capybara (~> 2.1) + cliver (~> 0.3.1) + multi_json (~> 1.0) + websocket-driver (>= 0.2.0) polyglot (0.3.4) quiet_assets (1.0.2) railties (>= 3.1, < 5.0) @@ -290,6 +296,7 @@ GEM json (>= 1.8.0) warden (1.2.3) rack (>= 1.0) + websocket-driver (0.3.3) whenever (0.9.0) activesupport (>= 2.3.4) chronic (>= 0.6.3) @@ -325,6 +332,7 @@ DEPENDENCIES omniauth omniauth-github! pg + poltergeist quiet_assets rack-canonical-host rails (~> 4.0.2) diff --git a/features/support/env.rb b/features/support/env.rb index 9f3b86d4..e0f1bf24 100644 --- a/features/support/env.rb +++ b/features/support/env.rb @@ -56,3 +56,12 @@ # 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 From 6e5ad9247441ccf81a156a2acce6bc588053dbe1 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Fri, 30 May 2014 15:01:13 +0200 Subject: [PATCH 201/372] Fundraiser can send a distribution to registered GitHub users --- app/assets/javascripts/distribution.js.coffee | 20 ++++++++ app/controllers/distributions_controller.rb | 41 ++++++++++++++- app/models/ability.rb | 3 +- app/models/distribution.rb | 31 ++++++++++-- app/models/tip.rb | 12 +++++ app/views/distributions/new.html.haml | 12 +++++ .../recipient_suggestions.html.haml | 12 +++++ app/views/distributions/show.html.haml | 21 ++++++++ app/views/projects/show.html.haml | 7 ++- config/locales/en.yml | 2 + config/routes.rb | 4 ++ features/distribution.feature | 31 ++++++++++++ features/step_definitions/common.rb | 6 +++ features/step_definitions/distribution.rb | 50 +++++++++++++++++++ features/step_definitions/tip_for_commit.rb | 3 ++ features/support/bitcoin_daemon_mock.rb | 2 +- features/support/octokit_mock.rb | 8 +++ features/tip_for_commit.feature | 17 +++++-- lib/bitcoin_tipper.rb | 31 +++++------- 19 files changed, 280 insertions(+), 33 deletions(-) create mode 100644 app/assets/javascripts/distribution.js.coffee create mode 100644 app/views/distributions/new.html.haml create mode 100644 app/views/distributions/recipient_suggestions.html.haml create mode 100644 app/views/distributions/show.html.haml create mode 100644 features/distribution.feature create mode 100644 features/step_definitions/distribution.rb create mode 100644 features/support/octokit_mock.rb diff --git a/app/assets/javascripts/distribution.js.coffee b/app/assets/javascripts/distribution.js.coffee new file mode 100644 index 00000000..8049b35b --- /dev/null +++ b/app/assets/javascripts/distribution.js.coffee @@ -0,0 +1,20 @@ +$(document).on "page:change", -> + input = $("#add-recipients-input") + suggestions = $("#recipient-suggestions") + + updateSuggestions = (text) -> + $.ajax + type: 'GET' + url: input.data('suggestion-url') + data: + text: text + success: (data) -> + suggestions.html(data) + suggestions.find(".add-recipient-button").click -> + recipients = $(this).data("recipients") + $("#recipients").append(recipients) + false + + input.on "input", -> + updateSuggestions(input.val()) + diff --git a/app/controllers/distributions_controller.rb b/app/controllers/distributions_controller.rb index 9d6822d1..809fca4c 100644 --- a/app/controllers/distributions_controller.rb +++ b/app/controllers/distributions_controller.rb @@ -1,5 +1,44 @@ class DistributionsController < ApplicationController + load_and_authorize_resource :project + load_and_authorize_resource :distribution, :through => :project + def index - @distributions = Distribution.order(created_at: :desc).page(params[:page]).per(30) + @distributions = @distributions.order(created_at: :desc).page(params[:page]).per(30) end + + def new + end + + def recipient_suggestions + render layout: nil + end + + def create + @distribution.project = @project + @distribution.tips.each do |tip| + tip.project = @project + end + if @distribution.save + redirect_to [@project, @distribution], notice: "Distribution created" + else + render "new" + end + end + + def show + end + + def send_transaction + @distribution.send_transaction! + redirect_to [@project, @distribution], notice: "Transaction sent" + rescue RuntimeError => e + redirect_to [@project, @distribution], error: e.message + end + + private + + def distribution_params + params.require(:distribution).permit(tips_attributes: [:coin_amount, :user_id]) + end + end diff --git a/app/models/ability.rb b/app/models/ability.rb index 65b8a499..20880a1c 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -4,7 +4,8 @@ class Ability def initialize(user) if user and user.nickname.present? can [:update, :decide_tip_amounts], Project, collaborators: {login: user.nickname} - can :create, Project + can [:create, :read], Project + can [:create, :recipient_suggestions, :read, :send_transaction], Distribution, project: {collaborators: {login: user.nickname}} end end end diff --git a/app/models/distribution.rb b/app/models/distribution.rb index 6dcf6486..4df3fdbf 100644 --- a/app/models/distribution.rb +++ b/app/models/distribution.rb @@ -1,24 +1,45 @@ class Distribution < ActiveRecord::Base belongs_to :project, inverse_of: :distributions has_many :tips + accepts_nested_attributes_for :tips + scope :to_send, -> { where(txid: nil) } scope :error, -> { where(is_error: true) } + def sent? + !!txid + end + def total_amount JSON.parse(data).values.map(&:to_d).sum if data end - def send_transaction - return if txid || is_error - return if project.disabled? + def send_transaction! + Distribution.transaction do + lock! + return if sent? + return if is_error? + return if project.disabled? - update_attribute :is_error, true # it's a lock to prevent duplicates + update_attribute :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 total_amount > project.available_amount txid = BitcoinDaemon.instance.send_many(project.address_label, JSON.parse(data)) - update_attribute :is_error, false update_attribute :txid, txid + update_attribute :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 + end + outs.to_json end end diff --git a/app/models/tip.rb b/app/models/tip.rb index e13118e2..95eafd8f 100644 --- a/app/models/tip.rb +++ b/app/models/tip.rb @@ -99,4 +99,16 @@ def check_amount_against_project raise "Not enough funds on project to save #{inspect} (available: #{available_amount})" end end + + def coin_amount + amount.to_f / COIN if amount + end + + def coin_amount=(coin_amount) + if coin_amount + self.amount = (coin_amount.to_f * COIN).round + else + self.amount = nil + end + end end diff --git a/app/views/distributions/new.html.haml b/app/views/distributions/new.html.haml new file mode 100644 index 00000000..af9c91ae --- /dev/null +++ b/app/views/distributions/new.html.haml @@ -0,0 +1,12 @@ += bootstrap_form_for [@project, @distribution] do |f| + %table.table + %thead + %th Recipient + %th Amount + %tbody#recipients + = f.submit "Save" + +.form-group + %label{for: "add-recipients-input"} Add recipient(s) + %input.form-control#add-recipients-input{type: "text", autocomplete: "off", data: {suggestion_url: recipient_suggestions_project_distributions_path(@project)}} + #recipient-suggestions diff --git a/app/views/distributions/recipient_suggestions.html.haml b/app/views/distributions/recipient_suggestions.html.haml new file mode 100644 index 00000000..2388daf4 --- /dev/null +++ b/app/views/distributions/recipient_suggestions.html.haml @@ -0,0 +1,12 @@ += bootstrap_form_for Distribution.new do |f| + %ul.list-unstyled + - User.where('nickname LIKE ?', "%#{params[:text]}%").each do |user| + %li + - recipients = capture_haml do + = f.fields_for :tips, Tip.new(user_id: user.id), child_index: rand(1<<160) do |fields| + %tr + %td + = fields.hidden_field :user_id + = "#{user.nickname} (GitHub user)" + %td= fields.text_field :coin_amount, hide_label: true + %a.btn.btn-default.add-recipient-button{href: "#", data: {recipients: recipients}}= "#{user.nickname} (GitHub user)" diff --git a/app/views/distributions/show.html.haml b/app/views/distributions/show.html.haml new file mode 100644 index 00000000..0a79bc76 --- /dev/null +++ b/app/views/distributions/show.html.haml @@ -0,0 +1,21 @@ +#distribution-show-page + %p + Total amount: #{btc_human @distribution.tips.map(&:amount).sum} + %table.table + %thead + %tr + %th Recipient + %th Address + %th Amount + %th Percentage + %tbody + - @distribution.tips.each do |tip| + %tr + %td.recipient= "#{tip.user.nickname} (GitHub user)" + %td.address= tip.user.bitcoin_address + %td.amount= btc_human tip.amount + %td.percentage 100 + - if @distribution.sent? + Sent + - elsif can? :send_transaction, @distribution + = button_to "Send the transaction", send_transaction_project_distribution_path(@project, @distribution), class: "btn btn-primary", data: {confirm: "#{@distribution.tips.map(&:amount).sum.to_f / COIN} peercoins will be sent. Are you sure?"} diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index 1cc87fe6..b148a5ea 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -15,6 +15,8 @@ = link_to "Edit project", edit_project_path(@project), class: "btn btn-primary" - 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-warning" + - if can? :create, @project.distributions.build + = link_to "New distribution", new_project_distribution_path(@project), class: "btn btn-warning" .row .col-md-4 @@ -96,8 +98,9 @@ = btc_human tip.amount - else will receive a tip - for commit - = link_to tip.commit[0..6], "https://github.com/#{@project.full_name}/commit/#{tip.commit}", target: :blank + - if tip.commit.present? + for commit + = link_to tip.commit[0..6], "https://github.com/#{@project.full_name}/commit/#{tip.commit}", target: :blank - if tip.undecided? when its amount is decided diff --git a/config/locales/en.yml b/config/locales/en.yml index 5514099d..9086dddb 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -29,3 +29,5 @@ en: project: github_url: GitHub URL detailed_description: Detailed description + tip: + coin_amount: Amount diff --git a/config/routes.rb b/config/routes.rb index a99ed6b0..0395ee6c 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -17,6 +17,10 @@ end resources :projects, :only => [:new, :show, :index, :create, :edit, :update] do resources :tips, :only => [:index] + resources :distributions, :only => [:new, :create, :show] do + get :recipient_suggestions, on: :collection + post :send_transaction, on: :member + end member do get :qrcode get :decide_tip_amounts diff --git a/features/distribution.feature b/features/distribution.feature new file mode 100644 index 00000000..bb763083 --- /dev/null +++ b/features/distribution.feature @@ -0,0 +1,31 @@ +Feature: Fundraisers can distribute funds + @javascript + Scenario: + 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 type "bob" in the recipient field + And I select the recipient "bob (GitHub user)" + And I fill the amount to "bob (GitHub user)" with "10" + And I click on "Save" + + Then I should see these distribution lines: + | recipient | address | amount | percentage | + | bob (GitHub user) | mxWfjaZJTNN5QKeZZYQ5HW3vgALFBsnuG1 | 10 | 100 | + 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 these amounts should have been sent from the account of the project: + | address | amount | + | mxWfjaZJTNN5QKeZZYQ5HW3vgALFBsnuG1 | 10.0 | + And the project balance should be "490" + And I should see "Sent" diff --git a/features/step_definitions/common.rb b/features/step_definitions/common.rb index 42724733..015917e6 100644 --- a/features/step_definitions/common.rb +++ b/features/step_definitions/common.rb @@ -123,6 +123,12 @@ def find_new_commit(id) @project.collaborators.create!(login: arg1) end +Given(/^a project managed by "(.*?)"$/) do |arg1| + create(:user, email: "#{arg1}@example.com", nickname: arg1) + @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 diff --git a/features/step_definitions/distribution.rb b/features/step_definitions/distribution.rb new file mode 100644 index 00000000..32399319 --- /dev/null +++ b/features/step_definitions/distribution.rb @@ -0,0 +1,50 @@ + +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(/^I type "(.*?)" in the recipient field$/) do |arg1| + fill_in "add-recipients-input", with: arg1 +end + +Given(/^I select the recipient "(.*?)"$/) do |arg1| + within "#recipient-suggestions" do + click_on arg1 + end +end + +Given(/^I fill the amount to "(.*?)" with "(.*?)"$/) do |arg1, arg2| + within "#recipients tr", text: arg1, exact: true do + fill_in "Amount", with: arg2 + end +end + +Then(/^I should see these distribution lines:$/) do |table| + table.hashes.each do |row| + tr = find("#distribution-show-page tr", text: row["recipient"]) + tr.find(".recipient").should have_content(row["recipient"]) + tr.find(".address").should have_content(row["address"]) + tr.find(".amount").should have_content(row["amount"]) + tr.find(".percentage").should have_content(row["percentage"]) + end +end + +Then(/^these amounts should have been sent from the account of the project:$/) do |table| + 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.should eq(table.hashes) +end + +When(/^the tipper is started$/) do + BitcoinTipper.work +end + +Then(/^no coins should have been sent$/) do + BitcoinDaemon.instance.list_transactions("*").should eq([]) +end + diff --git a/features/step_definitions/tip_for_commit.rb b/features/step_definitions/tip_for_commit.rb index 008ebb22..5fe7209e 100644 --- a/features/step_definitions/tip_for_commit.rb +++ b/features/step_definitions/tip_for_commit.rb @@ -12,6 +12,9 @@ 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( diff --git a/features/support/bitcoin_daemon_mock.rb b/features/support/bitcoin_daemon_mock.rb index 12cd8e66..c1b8dc45 100644 --- a/features/support/bitcoin_daemon_mock.rb +++ b/features/support/bitcoin_daemon_mock.rb @@ -24,7 +24,7 @@ def add_transaction(options) end def list_transactions(account = "", count = 10, from = 0) - @transactions.select { |t| t["account"] == account }[from, count] + @transactions.select { |t| account == "*" ? true : (t["account"] == account) }[from, count] end def send_many(account, recipients, minconf = 1) diff --git a/features/support/octokit_mock.rb b/features/support/octokit_mock.rb new file mode 100644 index 00000000..497d3fa2 --- /dev/null +++ b/features/support/octokit_mock.rb @@ -0,0 +1,8 @@ +class Octokit::Client + def initialize(*args) + end + + def commits(*args) + [] + end +end diff --git a/features/tip_for_commit.feature b/features/tip_for_commit.feature index 8fa257e5..327684af 100644 --- a/features/tip_for_commit.feature +++ b/features/tip_for_commit.feature @@ -4,12 +4,13 @@ Feature: On projects not holding tips, a tip is created for each new commit 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 | - | 123 | - | abc | - | 333 | + | sha | author | + | 123 | bob | + | abc | alice | + | 333 | bob | 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: @@ -18,6 +19,11 @@ Feature: On projects not holding tips, a tip is created for each new commit | abc | 4.95 | | 333 | 4.9005 | + When the tipper is started + Then these amounts should have been sent from the account of the project: + | address | amount | + | mxWfjaZJTNN5QKeZZYQ5HW3vgALFBsnuG1 | 9.9005 | + Scenario: A project holding tips Given a project And the project holds tips @@ -36,3 +42,6 @@ Feature: On projects not holding tips, a tip is created for each new commit | 123 | | | abc | | | 333 | | + + When the tipper is started + Then no coins should have been sent diff --git a/lib/bitcoin_tipper.rb b/lib/bitcoin_tipper.rb index ffbd0a9f..a2e02367 100644 --- a/lib/bitcoin_tipper.rb +++ b/lib/bitcoin_tipper.rb @@ -14,11 +14,18 @@ def self.work end end - self.create_distributions - - Rails.logger.info "Traversing sendmanies..." - Distribution.where(txid: nil).each do |distribution| - distribution.send_transaction + 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 Rails.logger.info "Refunding unclaimed tips..." @@ -34,20 +41,6 @@ def self.work def self.create_distributions Rails.logger.info "Creating distribution" ActiveRecord::Base.transaction do - 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) - outs = Hash.new { 0.to_d } - tips.each do |tip| - tip.update_attribute :distribution_id, distribution.id - outs[tip.user.bitcoin_address] += tip.amount.to_d / COIN - end - distribution.update_attribute :data, outs.to_json - Rails.logger.info " #{distribution.inspect}" - end - end end end From 7f0e8fe0dbc24b9702200560b466aca48bc783fe Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Fri, 30 May 2014 20:00:37 +0200 Subject: [PATCH 202/372] Sending to multiple recipients works --- app/views/distributions/show.html.haml | 7 ++-- features/distribution.feature | 39 +++++++++++++++++++++++ features/step_definitions/distribution.rb | 2 +- 3 files changed, 44 insertions(+), 4 deletions(-) diff --git a/app/views/distributions/show.html.haml b/app/views/distributions/show.html.haml index 0a79bc76..234dbd38 100644 --- a/app/views/distributions/show.html.haml +++ b/app/views/distributions/show.html.haml @@ -1,6 +1,7 @@ #distribution-show-page + - total = @distribution.tips.map(&:amount).sum %p - Total amount: #{btc_human @distribution.tips.map(&:amount).sum} + Total amount: #{btc_human total} %table.table %thead %tr @@ -14,8 +15,8 @@ %td.recipient= "#{tip.user.nickname} (GitHub user)" %td.address= tip.user.bitcoin_address %td.amount= btc_human tip.amount - %td.percentage 100 + %td.percentage= number_to_percentage(tip.amount.to_f * 100 / total, precision: 1) - if @distribution.sent? Sent - elsif can? :send_transaction, @distribution - = button_to "Send the transaction", send_transaction_project_distribution_path(@project, @distribution), class: "btn btn-primary", data: {confirm: "#{@distribution.tips.map(&:amount).sum.to_f / COIN} peercoins will be sent. Are you sure?"} + = button_to "Send the transaction", send_transaction_project_distribution_path(@project, @distribution), class: "btn btn-primary", data: {confirm: "#{total.to_f / COIN} peercoins will be sent. Are you sure?"} diff --git a/features/distribution.feature b/features/distribution.feature index bb763083..c8ae7ef8 100644 --- a/features/distribution.feature +++ b/features/distribution.feature @@ -29,3 +29,42 @@ Feature: Fundraisers can distribute funds | mxWfjaZJTNN5QKeZZYQ5HW3vgALFBsnuG1 | 10.0 | And the project balance should be "490" And I should see "Sent" + + @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 type "bob" in the recipient field + And I select the recipient "bob (GitHub user)" + And I fill the amount to "bob (GitHub user)" with "10" + And I type "carol" in the recipient field + And I select the recipient "carol (GitHub user)" + And I fill the amount to "carol (GitHub user)" with "13.56" + And I click on "Save" + + Then I should see these distribution lines: + | recipient | address | amount | percentage | + | bob (GitHub user) | mxWfjaZJTNN5QKeZZYQ5HW3vgALFBsnuG1 | 10 | 42.4 | + | carol (GitHub user) | 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 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" + And I should see "Sent" + + Scenario: Send to an user without address diff --git a/features/step_definitions/distribution.rb b/features/step_definitions/distribution.rb index 32399319..fd61736c 100644 --- a/features/step_definitions/distribution.rb +++ b/features/step_definitions/distribution.rb @@ -14,7 +14,7 @@ end Given(/^I fill the amount to "(.*?)" with "(.*?)"$/) do |arg1, arg2| - within "#recipients tr", text: arg1, exact: true do + within "#recipients tr", text: /^#{Regexp.escape arg1}/ do fill_in "Amount", with: arg2 end end From ac23eb736bb1a057e380a3e445a7192d304434ff Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sat, 31 May 2014 09:43:47 +0200 Subject: [PATCH 203/372] Handle distribution to user without an address set --- Gemfile | 1 + Gemfile.lock | 2 + app/controllers/distributions_controller.rb | 4 +- app/controllers/users_controller.rb | 2 +- app/models/distribution.rb | 19 +++-- app/models/tip.rb | 1 + app/views/distributions/index.html.haml | 12 ++- app/views/distributions/show.html.haml | 17 +++- app/views/projects/show.html.haml | 81 ++++++++++++------- config/routes.rb | 2 +- ...40531080839_add_sent_at_to_distribution.rb | 5 ++ db/schema.rb | 3 +- features/distribution.feature | 59 ++++++++++++-- features/step_definitions/common.rb | 13 +++ features/step_definitions/distribution.rb | 17 +++- features/step_definitions/web.rb | 13 ++- 16 files changed, 194 insertions(+), 57 deletions(-) create mode 100644 db/migrate/20140531080839_add_sent_at_to_distribution.rb diff --git a/Gemfile b/Gemfile index 1d3f88a5..45f50ee9 100644 --- a/Gemfile +++ b/Gemfile @@ -83,4 +83,5 @@ group :test do gem 'rspec-rails' gem 'factory_girl_rails' gem 'poltergeist' + gem 'timecop' end diff --git a/Gemfile.lock b/Gemfile.lock index 9d04d9c1..27cc69ab 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -284,6 +284,7 @@ GEM thread_safe (0.1.3) atomic tilt (1.4.1) + timecop (0.7.1) tins (0.13.1) treetop (1.4.15) polyglot @@ -345,6 +346,7 @@ DEPENDENCIES sdoc sqlite3 therubyracer + timecop turbolinks twitter-bootstrap-rails! uglifier (>= 1.3.0) diff --git a/app/controllers/distributions_controller.rb b/app/controllers/distributions_controller.rb index 809fca4c..f032c7e3 100644 --- a/app/controllers/distributions_controller.rb +++ b/app/controllers/distributions_controller.rb @@ -30,9 +30,9 @@ def show def send_transaction @distribution.send_transaction! - redirect_to [@project, @distribution], notice: "Transaction sent" + redirect_to [@project, @distribution], flash: {notice: "Transaction sent"} rescue RuntimeError => e - redirect_to [@project, @distribution], error: e.message + redirect_to [@project, @distribution], flash: {error: e.message} end private diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 8ed0650e..079c8349 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -16,7 +16,7 @@ def index def update if @user.update(users_params) - redirect_to @user, notice: 'Your information saved!' + redirect_to @user, notice: 'Your information was saved.' else render :show, alert: 'Error updating peercoin address' end diff --git a/app/models/distribution.rb b/app/models/distribution.rb index 4df3fdbf..6d2f2077 100644 --- a/app/models/distribution.rb +++ b/app/models/distribution.rb @@ -7,21 +7,21 @@ class Distribution < ActiveRecord::Base scope :error, -> { where(is_error: true) } def sent? - !!txid + sent_at or txid end def total_amount - JSON.parse(data).values.map(&:to_d).sum if data + tips.map(&:amount).compact.sum end def send_transaction! Distribution.transaction do lock! - return if sent? - return if is_error? - return if project.disabled? + raise "Already sent" if sent? + raise "Transaction already sent and failed" if is_error? + raise "Project disabled" if project.disabled? - update_attribute :is_error, true # it's a lock to prevent duplicates + update!(sent_at: Time.now, is_error: true) # it's a lock to prevent duplicates end data = generate_data @@ -31,8 +31,7 @@ def send_transaction! txid = BitcoinDaemon.instance.send_many(project.address_label, JSON.parse(data)) - update_attribute :txid, txid - update_attribute :is_error, false + update!(txid: txid, is_error: false) end def generate_data @@ -42,4 +41,8 @@ def generate_data end outs.to_json end + + def all_addresses_known? + tips.all? { |tip| tip.user.bitcoin_address.present? } + end end diff --git a/app/models/tip.rb b/app/models/tip.rb index 95eafd8f..3888b06d 100644 --- a/app/models/tip.rb +++ b/app/models/tip.rb @@ -91,6 +91,7 @@ def notify_user end def notify_user_if_just_decided + return if distribution_id notify_user if amount_was.nil? and amount end diff --git a/app/views/distributions/index.html.haml b/app/views/distributions/index.html.haml index fd564a4c..28a73812 100644 --- a/app/views/distributions/index.html.haml +++ b/app/views/distributions/index.html.haml @@ -1,14 +1,20 @@ -%h1 Last Withdrawals +%h1 Distributions +- if @project + %h2= @project.name %p %table.table %thead %tr %th Created At + %th Sent at %th Transaction %th Result + %th %tbody - @distributions.each do |distribution| %tr %td= l distribution.created_at, format: :short - %td= link_to distribution.txid, transaction_url(distribution.txid), target: '_blank' - %td= distribution.is_error ? "Error" : "Success" + %td= l distribution.sent_at, format: :short if distribution.sent_at + %td= link_to 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/show.html.haml b/app/views/distributions/show.html.haml index 234dbd38..dc9c1808 100644 --- a/app/views/distributions/show.html.haml +++ b/app/views/distributions/show.html.haml @@ -16,7 +16,20 @@ %td.address= tip.user.bitcoin_address %td.amount= btc_human tip.amount %td.percentage= number_to_percentage(tip.amount.to_f * 100 / total, precision: 1) - - if @distribution.sent? - Sent + - 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 log in with their GitHub account. - elsif can? :send_transaction, @distribution = button_to "Send the transaction", send_transaction_project_distribution_path(@project, @distribution), class: "btn btn-primary", data: {confirm: "#{total.to_f / COIN} peercoins will be sent. Are you sure?"} + + - if @distribution.data.present? + %h4 Raw data + %pre= @distribution.data diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index b148a5ea..bfff7c55 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -19,42 +19,11 @@ = link_to "New distribution", new_project_distribution_path(@project), class: "btn btn-warning" .row - .col-md-4 - - if @project.disabled? - .panel.panel-danger - .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} - - - else - .panel.panel-default.project-panel - .panel-heading - %h4.panel-title - Project Sponsors - .panel-body.text-center - %p To give to this project, send peercoins to this address: - %p.bitcoin-address - = @project.bitcoin_address - %p - = image_tag qrcode_project_path(@project, format: :svg), alt: @project.bitcoin_address, class: 'project qrcode' - %p #{100-(CONFIG["our_fee"]*100).round}% of deposited funds will be used to tip for new commits. .col-md-8 - unless @project.description.blank? %h3= @project.description - unless @project.detailed_description.blank? = render_markdown @project.detailed_description - %h4 Balance - = btc_human @project.available_amount - - if @project.auto_tip_commits? - (each new commit receives #{(CONFIG["tip"]*100).round}% of available balance) - - if (unconfirmed_amount = @project.unconfirmed_amount) > 0 - (#{btc_human unconfirmed_amount} unconfirmed) - if @project.tipping_policies_text.try(:text).present? %h4 Tipping policies @@ -138,3 +107,53 @@ %p= link_to image_tag(project_url(@project, format: :svg), alt: 'Peer4Commit'), project_url(@project) %p %input.form-control{type: 'text', value: "[![tip for next commit](#{project_url(@project, format: :svg)})](#{project_url(@project)})"} + + .col-md-4 + - if @project.disabled? + .panel.panel-danger + .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} + + - else + .panel.panel-default.project-panel + .panel-heading + %h4.panel-title + Project informations + .panel-body.text-center + %table.table.text-left + %tr + %td Funds + %td= btc_human @project.available_amount + %tr + %td Distributions + %td + %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.sent? + - label << " sent #{time_ago_in_words(distribution.sent_at)} ago" + - else + - label << " not sent" + = link_to label, [@project, distribution] + = link_to "All distributions", project_distributions_path(@project) + + + .panel.panel-default.project-panel + .panel-heading + %h4.panel-title + Donate + .panel-body.text-center + %p To give to this project, send peercoins to this address: + %p.bitcoin-address + = @project.bitcoin_address + %p + = image_tag qrcode_project_path(@project, format: :svg), alt: @project.bitcoin_address, class: 'project qrcode' + %p #{100-(CONFIG["our_fee"]*100).round}% of deposited funds will be used to tip for new commits. diff --git a/config/routes.rb b/config/routes.rb index 0395ee6c..5f9cb206 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -17,7 +17,7 @@ end resources :projects, :only => [:new, :show, :index, :create, :edit, :update] do resources :tips, :only => [:index] - resources :distributions, :only => [:new, :create, :show] do + resources :distributions, :only => [:new, :create, :show, :index] do get :recipient_suggestions, on: :collection post :send_transaction, on: :member 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/schema.rb b/db/schema.rb index c04232a3..3852f4f2 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20140530132209) do +ActiveRecord::Schema.define(version: 20140531080839) do create_table "cold_storage_transfers", force: true do |t| t.integer "project_id" @@ -58,6 +58,7 @@ 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" diff --git a/features/distribution.feature b/features/distribution.feature index c8ae7ef8..67e946ba 100644 --- a/features/distribution.feature +++ b/features/distribution.feature @@ -1,6 +1,6 @@ Feature: Fundraisers can distribute funds @javascript - Scenario: + 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" @@ -23,12 +23,14 @@ Feature: Fundraisers can distribute funds 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 these amounts should have been sent from the account of the project: + 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" - And I should see "Sent" @javascript Scenario: Send distribution to multiple users @@ -60,11 +62,56 @@ Feature: Fundraisers can distribute funds Then no coins should have been sent When I click on "Send the transaction" - Then these amounts should have been sent from the account of the project: + 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" - And I should see "Sent" - Scenario: Send to an user without address + @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 type "bob" in the recipient field + And I select the recipient "bob (GitHub user)" + And I fill the amount to "bob (GitHub user)" with "10" + And I click on "Save" + + Then I should see these distribution lines: + | recipient | address | amount | percentage | + | bob (GitHub user) | | 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 (GitHub user) | 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" diff --git a/features/step_definitions/common.rb b/features/step_definitions/common.rb index 015917e6..63033515 100644 --- a/features/step_definitions/common.rb +++ b/features/step_definitions/common.rb @@ -6,6 +6,10 @@ ActionMailer::Base.deliveries.size.should eq(arg1.to_i) end +Then(/^no email should have been sent$/) do + ActionMailer::Base.deliveries.should eq([]) +end + When(/^the email counters are reset$/) do ActionMailer::Base.deliveries.clear end @@ -136,3 +140,12 @@ def find_new_commit(id) 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 + diff --git a/features/step_definitions/distribution.rb b/features/step_definitions/distribution.rb index fd61736c..82df70db 100644 --- a/features/step_definitions/distribution.rb +++ b/features/step_definitions/distribution.rb @@ -3,6 +3,10 @@ create(:user, email: "#{arg1}@example.com", nickname: arg1, bitcoin_address: arg2) end +Given(/^a GitHub user "([^"]*?)"$/) do |arg1| + create(:user, email: "#{arg1}@example.com", nickname: arg1, bitcoin_address: nil) +end + Given(/^I type "(.*?)" in the recipient field$/) do |arg1| fill_in "add-recipients-input", with: arg1 end @@ -23,7 +27,7 @@ table.hashes.each do |row| tr = find("#distribution-show-page tr", text: row["recipient"]) tr.find(".recipient").should have_content(row["recipient"]) - tr.find(".address").should have_content(row["address"]) + tr.find(".address").text.should eq(row["address"]) tr.find(".amount").should have_content(row["amount"]) tr.find(".percentage").should have_content(row["percentage"]) end @@ -48,3 +52,14 @@ BitcoinDaemon.instance.list_transactions("*").should eq([]) end +When(/^I set my address to "(.*?)"$/) do |arg1| + visit user_path(@current_user) + fill_in "Peercoin address", with: arg1 + click_on "Update" + page.should have_content "Your information was saved" +end + +When(/^I click on the last distribution$/) do + find("#distribution-list .distribution-link:first-child").click +end + diff --git a/features/step_definitions/web.rb b/features/step_definitions/web.rb index ca696208..5630459e 100644 --- a/features/step_definitions/web.rb +++ b/features/step_definitions/web.rb @@ -10,6 +10,7 @@ visit root_path click_on "Sign in" page.should have_content("Successfully authenticated") + @current_user = User.find_by(nickname: arg1) end Given(/^I'm logged in on GitHub as "(.*?)"$/) do |arg1| @@ -33,6 +34,16 @@ end end +When(/^I log out$/) do + click_on "Sign Out" + page.should 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 @@ -80,7 +91,7 @@ end Then(/^I should see the project balance is "(.*?)"$/) do |arg1| - page.should have_content("Balance #{arg1}") + page.should have_content("Funds #{arg1}") end Then(/^I should see a link "(.*?)" to "(.*?)"$/) do |arg1, arg2| From b8baae48f52e6afa33e625ac5fcc0d520c5c7d51 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sat, 31 May 2014 10:45:31 +0200 Subject: [PATCH 204/372] Fixed crash when sent without date --- app/views/projects/show.html.haml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index bfff7c55..c7fac621 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -139,7 +139,10 @@ %li.distribution-link - label = btc_human(distribution.total_amount) - if distribution.sent? - - label << " sent #{time_ago_in_words(distribution.sent_at)} ago" + - 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] From b445e781efc17b3aea3c758d948b224e1e32bf94 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sat, 31 May 2014 10:48:20 +0200 Subject: [PATCH 205/372] Everyone can see projects and distributions --- app/models/ability.rb | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/models/ability.rb b/app/models/ability.rb index 20880a1c..e4556a21 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -2,10 +2,13 @@ class Ability include CanCan::Ability def initialize(user) + can :read, Project + can :read, Distribution + if user and user.nickname.present? can [:update, :decide_tip_amounts], Project, collaborators: {login: user.nickname} - can [:create, :read], Project - can [:create, :recipient_suggestions, :read, :send_transaction], Distribution, project: {collaborators: {login: user.nickname}} + can [:create], Project + can [:create, :recipient_suggestions, :send_transaction], Distribution, project: {collaborators: {login: user.nickname}} end end end From 5042b9aad3e14232599ad1092dfb1627b9588132 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sat, 31 May 2014 10:50:48 +0200 Subject: [PATCH 206/372] Removed useless parts in project page --- app/views/projects/show.html.haml | 55 +------------------------------ 1 file changed, 1 insertion(+), 54 deletions(-) diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index c7fac621..e112a743 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -38,60 +38,7 @@ - else = "(Last updated on #{date})" - %h4 Tips Paid - = btc_human @project.tips_paid_amount - - - if (failed_distributions = @project.distributions.error).any? and (amount = failed_distributions.map(&:tips).flatten.sum(&:amount)) > 0 - .alert.alert-danger - %strong Some tip transactions failed. - A total of #{btc_human amount} may not have been sent. - - - 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.) - - - 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' - - if tip.decided? - received - = btc_human tip.amount - - else - will receive a tip - - if tip.commit.present? - for commit - = link_to tip.commit[0..6], "https://github.com/#{@project.full_name}/commit/#{tip.commit}", target: :blank - - if tip.undecided? - when its amount is decided - - - if @project.next_tip_amount > 0 - %h4 Next Tip - = btc_human @project.next_tip_amount - - %h4 Contribute and Earn - Donate peercoins 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 peercoins on its balance, you will get a tip! - - - if current_user - - if current_user.bitcoin_address.blank? - Just - = link_to 'tell us', current_user - your peercoin address. - - else - Just check your email or - %a{href: user_omniauth_authorize_path(:github)} Sign In. - - %h4 Promote #{@project.full_name} + %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") From 7825c63bfe09c9dfd2d3ccbac262c3b2f8236326 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sat, 31 May 2014 10:51:58 +0200 Subject: [PATCH 207/372] Display available amount in the shield, instead of the 1% --- app/views/projects/show.svg.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/show.svg.erb b/app/views/projects/show.svg.erb index 38dd757c..8dda8b3e 100644 --- a/app/views/projects/show.svg.erb +++ b/app/views/projects/show.svg.erb @@ -81,7 +81,7 @@ - <%= shield_btc_amount @project.next_tip_amount %> + <%= shield_btc_amount @project.available_amount %> From ad74c676f5867d45f9c669948e93d8e6cd942c6e Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sat, 31 May 2014 11:06:46 +0200 Subject: [PATCH 208/372] Better views --- app/views/distributions/index.html.haml | 4 +++- app/views/distributions/show.html.haml | 11 +++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/app/views/distributions/index.html.haml b/app/views/distributions/index.html.haml index 28a73812..1b947082 100644 --- a/app/views/distributions/index.html.haml +++ b/app/views/distributions/index.html.haml @@ -7,6 +7,7 @@ %tr %th Created At %th Sent at + %th.text-right Amount %th Transaction %th Result %th @@ -15,6 +16,7 @@ %tr %td= l distribution.created_at, format: :short %td= l distribution.sent_at, format: :short if distribution.sent_at - %td= link_to distribution.txid, transaction_url(distribution.txid), target: '_blank' if distribution.txid.present? + %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/show.html.haml b/app/views/distributions/show.html.haml index dc9c1808..92103667 100644 --- a/app/views/distributions/show.html.haml +++ b/app/views/distributions/show.html.haml @@ -1,7 +1,5 @@ #distribution-show-page - total = @distribution.tips.map(&:amount).sum - %p - Total amount: #{btc_human total} %table.table %thead %tr @@ -16,6 +14,11 @@ %td.address= tip.user.bitcoin_address %td.amount= btc_human tip.amount %td.percentage= number_to_percentage(tip.amount.to_f * 100 / total, precision: 1) + + %p + %strong + Total amount: #{btc_human total} + - if @distribution.is_error? %p.alert.alert-danger The transaction failed. @@ -29,7 +32,3 @@ The transaction cannot be sent because some addresses are missing. Ask the recipients to log in with their GitHub account. - elsif can? :send_transaction, @distribution = button_to "Send the transaction", send_transaction_project_distribution_path(@project, @distribution), class: "btn btn-primary", data: {confirm: "#{total.to_f / COIN} peercoins will be sent. Are you sure?"} - - - if @distribution.data.present? - %h4 Raw data - %pre= @distribution.data From c8c6dcc55222bee4e46cb41c65d2b1a35d2d5155 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sat, 31 May 2014 11:19:35 +0200 Subject: [PATCH 209/372] Project actions in information panel --- app/assets/stylesheets/home.css.sass | 6 ++++++ app/views/projects/show.html.haml | 18 +++++++----------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/app/assets/stylesheets/home.css.sass b/app/assets/stylesheets/home.css.sass index 8f94af0d..c60357fd 100644 --- a/app/assets/stylesheets/home.css.sass +++ b/app/assets/stylesheets/home.css.sass @@ -4,3 +4,9 @@ td.money, th.money text-align: right + +.masthead + margin-bottom: 12px + +h1:first-child + margin-top: 0 diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index e112a743..4acb619b 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -5,21 +5,11 @@ = @project.description .row - .col-md-12 + .col-md-8 %h1 = @project.name - if (url = @project.github_url).present? %small= link_to glyph(:github), url, target: '_blank' - .pull-right - - if can? :update, @project - = link_to "Edit project", edit_project_path(@project), class: "btn btn-primary" - - 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-warning" - - if can? :create, @project.distributions.build - = link_to "New distribution", new_project_distribution_path(@project), class: "btn btn-warning" - -.row - .col-md-8 - unless @project.description.blank? %h3= @project.description - unless @project.detailed_description.blank? @@ -95,6 +85,12 @@ = link_to label, [@project, distribution] = link_to "All distributions", project_distributions_path(@project) + - 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" .panel.panel-default.project-panel .panel-heading From ef812d153e5a34b1dad64f56956017af2b0c5c3f Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sat, 31 May 2014 13:36:17 +0200 Subject: [PATCH 210/372] email spec --- features/distribution.feature | 58 +++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/features/distribution.feature b/features/distribution.feature index 67e946ba..182051e4 100644 --- a/features/distribution.feature +++ b/features/distribution.feature @@ -115,3 +115,61 @@ Feature: Fundraisers can distribute funds | address | amount | | mnVba8qrpy5uxYD7dV4NZMQPWjgdt2QC1i | 10.0 | And the project balance should be "490.00" + + @javascript + Scenario: Send to an email address + Given the current time is "2014-03-01 12:35:02 UTC" + + 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 type "bob@example.com" in the recipient field + And I select the recipient "bob@example.com (unknown email address)" + And I fill the amount to "bob@example.com (unknown email address)" with "10" + And I click on "Save" + + Then I should see these distribution lines: + | recipient | address | amount | percentage | + | bob@example.com | Send email request to provide an address | 10 | 100.0 | + 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 + And no email should have been sent + + When I click on "Send email request to provide an address" + Then I should see these distribution lines: + | recipient | address | amount | percentage | + | bob@example.com (address request sent less than a minute ago) | Send request to provide an address | 10 | 100.0 | + + And an email should have been sent to "bob@example.com" + When I visit the link to register from the email + And I fill "Password" with "password" + And I fill "Password confirmation" with "password" + And I click on "Save" + And I fill "Peercoin address" with "mubmzLrtTgDE2WrHkiwSFKuTh2VTSXboYK" + And I click on "Save" + + Then the user with email "bob@example.com" should have "password" as password + And the user with email "bob@example.com" should have "mubmzLrtTgDE2WrHkiwSFKuTh2VTSXboYK" as peercoin address + + 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@example.com | mubmzLrtTgDE2WrHkiwSFKuTh2VTSXboYK | 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 | + | mubmzLrtTgDE2WrHkiwSFKuTh2VTSXboYK | 10.0 | + And the project balance should be "490.00" From 3c669c0b89f8e62f6d91441d52b2ead2d43f8039 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sat, 31 May 2014 14:01:54 +0200 Subject: [PATCH 211/372] Added letters gem --- Gemfile | 4 ++++ Gemfile.lock | 9 +++++++++ 2 files changed, 13 insertions(+) diff --git a/Gemfile b/Gemfile index 45f50ee9..4c9dd9e8 100644 --- a/Gemfile +++ b/Gemfile @@ -85,3 +85,7 @@ group :test do gem 'poltergeist' gem 'timecop' end + +group :development, :test do + gem 'letters' +end diff --git a/Gemfile.lock b/Gemfile.lock index 27cc69ab..6068693c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -66,6 +66,7 @@ GEM multi_json arel (4.0.2) atomic (1.1.14) + awesome_print (1.2.0) bcrypt-ruby (3.1.2) builder (3.1.4) cancancan (1.7.1) @@ -94,6 +95,7 @@ GEM coffee-script-source execjs coffee-script-source (1.6.3) + colorize (0.7.3) commonjs (0.2.7) cucumber (1.3.10) builder (>= 2.1.2) @@ -166,6 +168,11 @@ GEM less-rails (2.4.2) actionpack (>= 3.1) less (~> 2.4.0) + letters (0.4.1) + activesupport + awesome_print + colorize + xml-simple libv8 (3.16.14.3) mail (2.5.4) mime-types (~> 1.16) @@ -301,6 +308,7 @@ GEM whenever (0.9.0) activesupport (>= 2.3.4) chronic (>= 0.6.3) + xml-simple (1.1.3) xpath (2.0.0) nokogiri (~> 1.3) @@ -328,6 +336,7 @@ DEPENDENCIES jquery-rails kaminari less-rails + letters mysql2 octokit omniauth From 0f89a75b54b2f7274efce7c0ba20b13ef9f5ea10 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sat, 31 May 2014 14:02:37 +0200 Subject: [PATCH 212/372] Create user when distribute to an unknown email address --- app/controllers/distributions_controller.rb | 2 +- app/models/tip.rb | 1 + app/models/user.rb | 18 +++++++++++++++ .../recipient_suggestions.html.haml | 12 ++++++++++ app/views/distributions/show.html.haml | 2 +- features/step_definitions/distribution.rb | 23 ++++++++++++++++++- 6 files changed, 55 insertions(+), 3 deletions(-) diff --git a/app/controllers/distributions_controller.rb b/app/controllers/distributions_controller.rb index f032c7e3..a2feb975 100644 --- a/app/controllers/distributions_controller.rb +++ b/app/controllers/distributions_controller.rb @@ -38,7 +38,7 @@ def send_transaction private def distribution_params - params.require(:distribution).permit(tips_attributes: [:coin_amount, :user_id]) + params.require(:distribution).permit(tips_attributes: [:coin_amount, :user_id, {user_attributes: [:email]}]) end end diff --git a/app/models/tip.rb b/app/models/tip.rb index 3888b06d..206d6da7 100644 --- a/app/models/tip.rb +++ b/app/models/tip.rb @@ -1,5 +1,6 @@ class Tip < ActiveRecord::Base belongs_to :user + accepts_nested_attributes_for :user belongs_to :distribution belongs_to :project, inverse_of: :tips diff --git a/app/models/user.rb b/app/models/user.rb index 4a6f3f26..528b9a09 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -35,6 +35,24 @@ def self.update_cache end end + def password_required? + false + end + + def recipient_label + if nickname.present? + "#{nickname} (GitHub user)" + elsif email.present? + if new_record? + "#{email} (unknown email address)" + else + email + end + else + "Unknown user" + end + end + private def generate_login_token! diff --git a/app/views/distributions/recipient_suggestions.html.haml b/app/views/distributions/recipient_suggestions.html.haml index 2388daf4..f9185d73 100644 --- a/app/views/distributions/recipient_suggestions.html.haml +++ b/app/views/distributions/recipient_suggestions.html.haml @@ -10,3 +10,15 @@ = "#{user.nickname} (GitHub user)" %td= fields.text_field :coin_amount, hide_label: true %a.btn.btn-default.add-recipient-button{href: "#", data: {recipients: recipients}}= "#{user.nickname} (GitHub user)" + - if params[:text] =~ Devise::email_regexp + - recipient_name = "#{params[:text]} (unknown email address)" + %li + - recipients = capture_haml do + = f.fields_for :tips, Tip.new(user: User.new(email: params[:text])), child_index: rand(1<<160) do |fields| + %tr + %td + = fields.fields_for :user do |user_fields| + = user_fields.hidden_field :email + = recipient_name + %td= fields.text_field :coin_amount, hide_label: true + %a.btn.btn-default.add-recipient-button{href: "#", data: {recipients: recipients}}= recipient_name diff --git a/app/views/distributions/show.html.haml b/app/views/distributions/show.html.haml index 92103667..4e45f5bd 100644 --- a/app/views/distributions/show.html.haml +++ b/app/views/distributions/show.html.haml @@ -10,7 +10,7 @@ %tbody - @distribution.tips.each do |tip| %tr - %td.recipient= "#{tip.user.nickname} (GitHub user)" + %td.recipient= tip.user.recipient_label %td.address= tip.user.bitcoin_address %td.amount= btc_human tip.amount %td.percentage= number_to_percentage(tip.amount.to_f * 100 / total, precision: 1) diff --git a/features/step_definitions/distribution.rb b/features/step_definitions/distribution.rb index 82df70db..e61d66b1 100644 --- a/features/step_definitions/distribution.rb +++ b/features/step_definitions/distribution.rb @@ -25,7 +25,12 @@ Then(/^I should see these distribution lines:$/) do |table| table.hashes.each do |row| - tr = find("#distribution-show-page tr", text: row["recipient"]) + begin + tr = find("#distribution-show-page tbody tr", text: row["recipient"]) + rescue + puts "Rows: " + all("#distribution-show-page tbody tr").map(&:text).inspect + raise + end tr.find(".recipient").should have_content(row["recipient"]) tr.find(".address").text.should eq(row["address"]) tr.find(".amount").should have_content(row["amount"]) @@ -63,3 +68,19 @@ find("#distribution-list .distribution-link:first-child").click end +Then(/^an email should have been sent to "(.*?)"$/) do |arg1| + ActionMailer::Base.deliveries.map(&:to).should include([arg1]) +end + +When(/^I visit the link to register from the email$/) do + pending # express the regexp above with the code you wish you had +end + +Then(/^the user with email "(.*?)" should have "(.*?)" as password$/) do |arg1, arg2| + pending # express the regexp above with the code you wish you had +end + +Then(/^the user with email "(.*?)" should have "(.*?)" as peercoin address$/) do |arg1, arg2| + pending # express the regexp above with the code you wish you had +end + From f2ba4840f94f9a0fdccacddaec9df880f0337e27 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sat, 31 May 2014 14:08:40 +0200 Subject: [PATCH 213/372] Send email request button --- app/views/distributions/show.html.haml | 7 ++++++- config/routes.rb | 1 + features/distribution.feature | 8 ++++---- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/app/views/distributions/show.html.haml b/app/views/distributions/show.html.haml index 4e45f5bd..0258c5f6 100644 --- a/app/views/distributions/show.html.haml +++ b/app/views/distributions/show.html.haml @@ -11,7 +11,12 @@ - @distribution.tips.each do |tip| %tr %td.recipient= tip.user.recipient_label - %td.address= tip.user.bitcoin_address + %td.address + - if tip.user.bitcoin_address.present? + = tip.user.bitcoin_address + - else + - if tip.user.email.present? + = button_to "Send email request to provide an address", send_email_address_request_user_path(tip.user), class: "btn btn-default" %td.amount= btc_human tip.amount %td.percentage= number_to_percentage(tip.amount.to_f * 100 / total, precision: 1) diff --git a/config/routes.rb b/config/routes.rb index 5f9cb206..65155b9b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -13,6 +13,7 @@ end member do post :send_tips_back + post :send_email_address_request end end resources :projects, :only => [:new, :show, :index, :create, :edit, :update] do diff --git a/features/distribution.feature b/features/distribution.feature index 182051e4..c77b2dcd 100644 --- a/features/distribution.feature +++ b/features/distribution.feature @@ -133,8 +133,8 @@ Feature: Fundraisers can distribute funds And I click on "Save" Then I should see these distribution lines: - | recipient | address | amount | percentage | - | bob@example.com | Send email request to provide an address | 10 | 100.0 | + | recipient | address | amount | percentage | + | bob@example.com | | 10 | 100.0 | And I should see "The transaction cannot be sent because some addresses are missing" And no email should have been sent @@ -145,8 +145,8 @@ Feature: Fundraisers can distribute funds When I click on "Send email request to provide an address" Then I should see these distribution lines: - | recipient | address | amount | percentage | - | bob@example.com (address request sent less than a minute ago) | Send request to provide an address | 10 | 100.0 | + | recipient | address | amount | percentage | + | bob@example.com (address request sent less than a minute ago) | | 10 | 100.0 | And an email should have been sent to "bob@example.com" When I visit the link to register from the email From 8dc2236ab7e84fb0a068020ad1816cc910119b0b Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sat, 31 May 2014 15:58:55 +0200 Subject: [PATCH 214/372] Send to an unknown email address --- app/controllers/users_controller.rb | 22 +++++++++++++++- app/mailers/user_mailer.rb | 7 ++++++ app/models/ability.rb | 2 +- app/views/distributions/show.html.haml | 2 +- .../user_mailer/address_request.html.haml | 19 ++++++++++++++ .../users/set_password_and_address.html.haml | 5 ++++ config/initializers/intercept_emails.rb | 2 +- config/routes.rb | 2 ++ features/distribution.feature | 16 ++++++++---- features/step_definitions/distribution.rb | 25 ++++++++++++++++--- 10 files changed, 89 insertions(+), 13 deletions(-) create mode 100644 app/views/user_mailer/address_request.html.haml create mode 100644 app/views/users/set_password_and_address.html.haml diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 079c8349..21124b74 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -1,6 +1,6 @@ class UsersController < ApplicationController - before_action except: [:login, :index] do + before_action except: [:login, :index, :send_email_address_request, :set_password_and_address] do @user = User.where(id: params[:id]).first unless current_user && current_user == @user redirect_to root_path @@ -42,6 +42,26 @@ def send_tips_back redirect_to @user, notice: 'All your tips have been refunded to their project' end + def send_email_address_request + tip = Tip.find(params[:tip_id]) + authorize! :update, tip.distribution + UserMailer.address_request(tip, current_user).deliver + redirect_to params[:return_url], notice: "Request sent" + end + + def set_password_and_address + @user = User.find(params[:id]) + raise "Invalid token" unless Devise.secure_compare(params[:token], @user.login_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 + redirect_to root_path, notice: "Information saved" + else + flash.now[:alert] = "Please fill all the information" + end + end + end + private def users_params params.require(:user).permit(:bitcoin_address) diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb index f585efb9..6d4bb0da 100644 --- a/app/mailers/user_mailer.rb +++ b/app/mailers/user_mailer.rb @@ -12,4 +12,11 @@ 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 index e4556a21..773623d7 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -8,7 +8,7 @@ def initialize(user) if user and user.nickname.present? can [:update, :decide_tip_amounts], Project, collaborators: {login: user.nickname} can [:create], Project - can [:create, :recipient_suggestions, :send_transaction], Distribution, project: {collaborators: {login: user.nickname}} + can [:create, :update, :recipient_suggestions, :send_transaction], Distribution, project: {collaborators: {login: user.nickname}} end end end diff --git a/app/views/distributions/show.html.haml b/app/views/distributions/show.html.haml index 0258c5f6..796a434c 100644 --- a/app/views/distributions/show.html.haml +++ b/app/views/distributions/show.html.haml @@ -16,7 +16,7 @@ = tip.user.bitcoin_address - else - if tip.user.email.present? - = button_to "Send email request to provide an address", send_email_address_request_user_path(tip.user), class: "btn btn-default" + = button_to "Send email request to provide an address", send_email_address_request_user_path(tip_id: tip.id, return_url: request.url), class: "btn btn-default" %td.amount= btc_human tip.amount %td.percentage= number_to_percentage(tip.amount.to_f * 100 / total, precision: 1) 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..eebd609b --- /dev/null +++ b/app/views/user_mailer/address_request.html.haml @@ -0,0 +1,19 @@ +%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 and lock it with a password. + +%p= link_to 'Set your password and address', set_password_and_address_user_url(@user, token: @user.login_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/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/config/initializers/intercept_emails.rb b/config/initializers/intercept_emails.rb index 5f8ed8bc..2f1e12fd 100644 --- a/config/initializers/intercept_emails.rb +++ b/config/initializers/intercept_emails.rb @@ -1,4 +1,4 @@ -if (SEND_ALL_EMAILS_TO = CONFIG["send_all_emails_to"]).present? +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}" diff --git a/config/routes.rb b/config/routes.rb index 65155b9b..3a6862c2 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -14,6 +14,8 @@ member do post :send_tips_back post :send_email_address_request + get :set_password_and_address + patch :set_password_and_address end end resources :projects, :only => [:new, :show, :index, :create, :edit, :update] do diff --git a/features/distribution.feature b/features/distribution.feature index c77b2dcd..40dc59a8 100644 --- a/features/distribution.feature +++ b/features/distribution.feature @@ -145,18 +145,20 @@ Feature: Fundraisers can distribute funds When I click on "Send email request to provide an address" Then I should see these distribution lines: - | recipient | address | amount | percentage | - | bob@example.com (address request sent less than a minute ago) | | 10 | 100.0 | + | recipient | address | amount | percentage | + | bob@example.com | | 10 | 100.0 | And an email should have been sent to "bob@example.com" - When I visit the link to register from the email + And the email should include "alice" + And the email should include a link to the last distribution + When I visit the link to set my password and address from the email And I fill "Password" with "password" And I fill "Password confirmation" with "password" - And I click on "Save" And I fill "Peercoin address" with "mubmzLrtTgDE2WrHkiwSFKuTh2VTSXboYK" And I click on "Save" - Then the user with email "bob@example.com" should have "password" as password + Then I should see "Information saved" + And the user with email "bob@example.com" should have "password" as password And the user with email "bob@example.com" should have "mubmzLrtTgDE2WrHkiwSFKuTh2VTSXboYK" as peercoin address When I log out @@ -173,3 +175,7 @@ Feature: Fundraisers can distribute funds | address | amount | | mubmzLrtTgDE2WrHkiwSFKuTh2VTSXboYK | 10.0 | And the project balance should be "490.00" + + Scenario: Send to someone who doesn't want to be notified + Scenario: Cannot login from email link if a password has already been set + Scenario: Cannot login from an old email link diff --git a/features/step_definitions/distribution.rb b/features/step_definitions/distribution.rb index e61d66b1..3b8f12b6 100644 --- a/features/step_definitions/distribution.rb +++ b/features/step_definitions/distribution.rb @@ -70,17 +70,34 @@ Then(/^an email should have been sent to "(.*?)"$/) do |arg1| ActionMailer::Base.deliveries.map(&:to).should include([arg1]) + @email = ActionMailer::Base.deliveries.detect { |email| email.to == [arg1] } end -When(/^I visit the link to register from the email$/) do - pending # express the regexp above with the code you wish you had +Then(/^the email should include "(.*?)"$/) do |arg1| + @email.body.should include(arg1) +end + +Then(/^the email should include a link to the last distribution$/) do + distribution = Distribution.last + @email.body.should include(project_distribution_url(distribution.project, distribution)) +end + +When(/^I visit the link to set my password and address from the email$/) do + begin + link = Nokogiri::HTML.parse(@email.body.decoded).css("a").detect { |el| el.text == "Set your password and address" } + link.should_not be_nil + rescue + puts @email.body + raise + end + visit URI.parse(link["href"]).request_uri end Then(/^the user with email "(.*?)" should have "(.*?)" as password$/) do |arg1, arg2| - pending # express the regexp above with the code you wish you had + User.find_by(email: arg1).valid_password?(arg2).should eq(true) end Then(/^the user with email "(.*?)" should have "(.*?)" as peercoin address$/) do |arg1, arg2| - pending # express the regexp above with the code you wish you had + User.find_by(email: arg1).bitcoin_address.should eq(arg2) end From 7159ef55b36929b7a097cf698fe1ec00f1ed9a04 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sat, 31 May 2014 17:05:01 +0200 Subject: [PATCH 215/372] Removed letters gem --- Gemfile | 4 ---- Gemfile.lock | 9 --------- 2 files changed, 13 deletions(-) diff --git a/Gemfile b/Gemfile index 4c9dd9e8..45f50ee9 100644 --- a/Gemfile +++ b/Gemfile @@ -85,7 +85,3 @@ group :test do gem 'poltergeist' gem 'timecop' end - -group :development, :test do - gem 'letters' -end diff --git a/Gemfile.lock b/Gemfile.lock index 6068693c..27cc69ab 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -66,7 +66,6 @@ GEM multi_json arel (4.0.2) atomic (1.1.14) - awesome_print (1.2.0) bcrypt-ruby (3.1.2) builder (3.1.4) cancancan (1.7.1) @@ -95,7 +94,6 @@ GEM coffee-script-source execjs coffee-script-source (1.6.3) - colorize (0.7.3) commonjs (0.2.7) cucumber (1.3.10) builder (>= 2.1.2) @@ -168,11 +166,6 @@ GEM less-rails (2.4.2) actionpack (>= 3.1) less (~> 2.4.0) - letters (0.4.1) - activesupport - awesome_print - colorize - xml-simple libv8 (3.16.14.3) mail (2.5.4) mime-types (~> 1.16) @@ -308,7 +301,6 @@ GEM whenever (0.9.0) activesupport (>= 2.3.4) chronic (>= 0.6.3) - xml-simple (1.1.3) xpath (2.0.0) nokogiri (~> 1.3) @@ -336,7 +328,6 @@ DEPENDENCIES jquery-rails kaminari less-rails - letters mysql2 octokit omniauth From 9b2c07ee9adc4643aad5865a285cdea8c3b8d7f8 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sat, 31 May 2014 17:16:45 +0200 Subject: [PATCH 216/372] Can edit distribution --- app/controllers/distributions_controller.rb | 18 +++++++- app/models/ability.rb | 6 ++- app/models/distribution.rb | 6 ++- app/views/distributions/_form.html.haml | 19 ++++++++ app/views/distributions/edit.html.haml | 1 + app/views/distributions/new.html.haml | 13 +----- app/views/distributions/show.html.haml | 15 ++++--- config/routes.rb | 2 +- features/distribution.feature | 49 +++++++++++++++++++++ 9 files changed, 107 insertions(+), 22 deletions(-) create mode 100644 app/views/distributions/_form.html.haml create mode 100644 app/views/distributions/edit.html.haml diff --git a/app/controllers/distributions_controller.rb b/app/controllers/distributions_controller.rb index a2feb975..8360a2aa 100644 --- a/app/controllers/distributions_controller.rb +++ b/app/controllers/distributions_controller.rb @@ -25,6 +25,21 @@ def create end end + def edit + end + + def update + @distribution.attributes = distribution_params + @distribution.tips.each do |tip| + tip.project = @project + end + if @distribution.save + redirect_to [@project, @distribution], notice: "Distribution updated" + else + render "edit" + end + end + def show end @@ -38,7 +53,6 @@ def send_transaction private def distribution_params - params.require(:distribution).permit(tips_attributes: [:coin_amount, :user_id, {user_attributes: [:email]}]) + params.require(:distribution).permit(tips_attributes: [:id, :coin_amount, :user_id, {user_attributes: [:email]}]) end - end diff --git a/app/models/ability.rb b/app/models/ability.rb index 773623d7..a946097f 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -8,7 +8,11 @@ def initialize(user) if user and user.nickname.present? can [:update, :decide_tip_amounts], Project, collaborators: {login: user.nickname} can [:create], Project - can [:create, :update, :recipient_suggestions, :send_transaction], Distribution, project: {collaborators: {login: user.nickname}} + can [:create], Distribution, project: {collaborators: {login: user.nickname}} + can [:update, :recipient_suggestions], Distribution, project: {collaborators: {login: user.nickname}}, txid: nil, sent_at: nil + can [:send_transaction], Distribution do |distribution| + distribution.can_be_sent? + end end end end diff --git a/app/models/distribution.rb b/app/models/distribution.rb index 6d2f2077..8409069d 100644 --- a/app/models/distribution.rb +++ b/app/models/distribution.rb @@ -43,6 +43,10 @@ def generate_data end def all_addresses_known? - tips.all? { |tip| tip.user.bitcoin_address.present? } + tips.all? { |tip| tip.user.try(:bitcoin_address).present? } + end + + def can_be_sent? + !sent? and all_addresses_known? end end diff --git a/app/views/distributions/_form.html.haml b/app/views/distributions/_form.html.haml new file mode 100644 index 00000000..ee84805f --- /dev/null +++ b/app/views/distributions/_form.html.haml @@ -0,0 +1,19 @@ += bootstrap_form_for [@project, @distribution] do |f| + %table.table + %thead + %th Recipient + %th Amount + %tbody#recipients + = f.fields_for :tips do |fields| + %tr + %td + = fields.hidden_field :id + = fields.hidden_field :user_id + = fields.object.user.recipient_label + %td= fields.text_field :coin_amount, hide_label: true + = f.submit "Save" + +.form-group + %label{for: "add-recipients-input"} Add recipient(s) + %input.form-control#add-recipients-input{type: "text", autocomplete: "off", data: {suggestion_url: recipient_suggestions_project_distributions_path(@project)}} + #recipient-suggestions 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/new.html.haml b/app/views/distributions/new.html.haml index af9c91ae..b1bc3ba0 100644 --- a/app/views/distributions/new.html.haml +++ b/app/views/distributions/new.html.haml @@ -1,12 +1 @@ -= bootstrap_form_for [@project, @distribution] do |f| - %table.table - %thead - %th Recipient - %th Amount - %tbody#recipients - = f.submit "Save" - -.form-group - %label{for: "add-recipients-input"} Add recipient(s) - %input.form-control#add-recipients-input{type: "text", autocomplete: "off", data: {suggestion_url: recipient_suggestions_project_distributions_path(@project)}} - #recipient-suggestions += render "form" diff --git a/app/views/distributions/show.html.haml b/app/views/distributions/show.html.haml index 796a434c..92f5e248 100644 --- a/app/views/distributions/show.html.haml +++ b/app/views/distributions/show.html.haml @@ -10,12 +10,12 @@ %tbody - @distribution.tips.each do |tip| %tr - %td.recipient= tip.user.recipient_label + %td.recipient= tip.user.try(:recipient_label) %td.address - - if tip.user.bitcoin_address.present? + - if tip.user.try(:bitcoin_address).present? = tip.user.bitcoin_address - else - - if tip.user.email.present? + - if tip.user.try(:email).present? = button_to "Send email request to provide an address", send_email_address_request_user_path(tip_id: tip.id, return_url: request.url), class: "btn btn-default" %td.amount= btc_human tip.amount %td.percentage= number_to_percentage(tip.amount.to_f * 100 / total, precision: 1) @@ -35,5 +35,10 @@ - elsif !@distribution.all_addresses_known? %p.alert.alert-warning The transaction cannot be sent because some addresses are missing. Ask the recipients to log in with their GitHub account. - - elsif can? :send_transaction, @distribution - = button_to "Send the transaction", send_transaction_project_distribution_path(@project, @distribution), class: "btn btn-primary", data: {confirm: "#{total.to_f / COIN} peercoins will be sent. Are you sure?"} + .distribution-actions + - if can? :send_transaction, @distribution + .distribution-action + = button_to "Send the transaction", send_transaction_project_distribution_path(@project, @distribution), class: "btn btn-default", data: {confirm: "#{total.to_f / COIN} peercoins will be sent. Are you sure?"} + - if can? :update, @distribution + .distribution-action + = link_to "Edit", edit_project_distribution_path(@project, @distribution), class: "btn btn-default" diff --git a/config/routes.rb b/config/routes.rb index 3a6862c2..8314e047 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -20,7 +20,7 @@ end resources :projects, :only => [:new, :show, :index, :create, :edit, :update] do resources :tips, :only => [:index] - resources :distributions, :only => [:new, :create, :show, :index] do + resources :distributions, :only => [:new, :create, :show, :index, :edit, :update] do get :recipient_suggestions, on: :collection post :send_transaction, on: :member end diff --git a/features/distribution.feature b/features/distribution.feature index 40dc59a8..622d965c 100644 --- a/features/distribution.feature +++ b/features/distribution.feature @@ -177,5 +177,54 @@ Feature: Fundraisers can distribute funds 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 type "bob" in the recipient field + And I select the recipient "bob (GitHub user)" + And I fill the amount to "bob (GitHub user)" with "10" + And I click on "Save" + + Then I should see these distribution lines: + | recipient | address | amount | percentage | + | bob (GitHub user) | 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 (GitHub user)" with "15" + And I type "carol" in the recipient field + And I select the recipient "carol (GitHub user)" + And I fill the amount to "carol (GitHub user)" with "5" + And I click on "Save" + + Then I should see these distribution lines: + | recipient | address | amount | percentage | + | bob (GitHub user) | mxWfjaZJTNN5QKeZZYQ5HW3vgALFBsnuG1 | 15 | 75.0 | + | carol (GitHub user) | 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" + From 2b703f34135fbeae8527231f8806486a26da62a3 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sat, 31 May 2014 17:25:53 +0200 Subject: [PATCH 217/372] Send to known email address --- .../recipient_suggestions.html.haml | 15 +++-- features/distribution.feature | 60 +++++++++++++++++++ features/step_definitions/distribution.rb | 4 ++ 3 files changed, 73 insertions(+), 6 deletions(-) diff --git a/app/views/distributions/recipient_suggestions.html.haml b/app/views/distributions/recipient_suggestions.html.haml index f9185d73..7f62d87a 100644 --- a/app/views/distributions/recipient_suggestions.html.haml +++ b/app/views/distributions/recipient_suggestions.html.haml @@ -11,14 +11,17 @@ %td= fields.text_field :coin_amount, hide_label: true %a.btn.btn-default.add-recipient-button{href: "#", data: {recipients: recipients}}= "#{user.nickname} (GitHub user)" - if params[:text] =~ Devise::email_regexp - - recipient_name = "#{params[:text]} (unknown email address)" + - user = User.where(email: params[:text]).first_or_initialize %li - recipients = capture_haml do - = f.fields_for :tips, Tip.new(user: User.new(email: params[:text])), child_index: rand(1<<160) do |fields| + = f.fields_for :tips, Tip.new(user: user), child_index: rand(1<<160) do |fields| %tr %td - = fields.fields_for :user do |user_fields| - = user_fields.hidden_field :email - = recipient_name + - if user.new_record? + = fields.fields_for :user do |user_fields| + = user_fields.hidden_field :email + - else + = fields.hidden_field :user_id + = user.recipient_label %td= fields.text_field :coin_amount, hide_label: true - %a.btn.btn-default.add-recipient-button{href: "#", data: {recipients: recipients}}= recipient_name + %a.btn.btn-default.add-recipient-button{href: "#", data: {recipients: recipients}}= user.recipient_label diff --git a/features/distribution.feature b/features/distribution.feature index 622d965c..3aa2dc25 100644 --- a/features/distribution.feature +++ b/features/distribution.feature @@ -228,3 +228,63 @@ Feature: Fundraisers can distribute funds | mi9SLroAgc8eUNuLwnZmdyqWdShbNtvr3n | 5.0 | And the project balance should be "480" + @javascript + Scenario: Send to a known email address + Given an user with email "bob@example.com" + + 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 type "bob@example.com" in the recipient field + And I select the recipient "bob@example.com" + And I fill the amount to "bob@example.com" with "10" + And I click on "Save" + + Then I should see these distribution lines: + | recipient | address | amount | percentage | + | bob@example.com | | 10 | 100.0 | + 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 + And no email should have been sent + + When I click on "Send email request to provide an address" + Then I should see these distribution lines: + | recipient | address | amount | percentage | + | bob@example.com | | 10 | 100.0 | + + And an email should have been sent to "bob@example.com" + And the email should include "alice" + And the email should include a link to the last distribution + When I visit the link to set my password and address from the email + And I fill "Password" with "password" + And I fill "Password confirmation" with "password" + And I fill "Peercoin address" with "mubmzLrtTgDE2WrHkiwSFKuTh2VTSXboYK" + And I click on "Save" + + Then I should see "Information saved" + And the user with email "bob@example.com" should have "password" as password + And the user with email "bob@example.com" should have "mubmzLrtTgDE2WrHkiwSFKuTh2VTSXboYK" as peercoin address + + 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@example.com | mubmzLrtTgDE2WrHkiwSFKuTh2VTSXboYK | 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 | + | mubmzLrtTgDE2WrHkiwSFKuTh2VTSXboYK | 10.0 | + And the project balance should be "490.00" + diff --git a/features/step_definitions/distribution.rb b/features/step_definitions/distribution.rb index 3b8f12b6..b263b4ff 100644 --- a/features/step_definitions/distribution.rb +++ b/features/step_definitions/distribution.rb @@ -7,6 +7,10 @@ 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(/^I type "(.*?)" in the recipient field$/) do |arg1| fill_in "add-recipients-input", with: arg1 end From 108add7e6060cf6edcd503abaa1558315348411e Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sat, 31 May 2014 17:28:41 +0200 Subject: [PATCH 218/372] Pending step --- features/step_definitions/common.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/features/step_definitions/common.rb b/features/step_definitions/common.rb index 63033515..32ff767e 100644 --- a/features/step_definitions/common.rb +++ b/features/step_definitions/common.rb @@ -149,3 +149,7 @@ def find_new_commit(id) Timecop.return end +Then(/^pending$/) do + pending +end + From 1015591357863fa11a9489e41a8c2deb4c5b4bf8 Mon Sep 17 00:00:00 2001 From: anilmaurya Date: Mon, 17 Feb 2014 14:50:58 +0530 Subject: [PATCH 219/372] adding (sign in/sign up) page for login using 'twitter_bootstrap_form_for' for sign in/sign up Conflicts: Gemfile Gemfile.lock app/assets/stylesheets/application.css app/views/layouts/application.html.haml app/views/projects/show.html.haml config/routes.rb --- Gemfile | 1 + Gemfile.lock | 9 ++++++ app/assets/stylesheets/application.css | 6 ++++ app/views/devise/confirmations/new.html.haml | 7 +++++ .../confirmation_instructions.html.haml | 4 +++ .../reset_password_instructions.html.haml | 6 ++++ .../mailer/unlock_instructions.html.haml | 5 ++++ app/views/devise/passwords/edit.html.haml | 9 ++++++ app/views/devise/passwords/new.html.haml | 7 +++++ app/views/devise/registrations/edit.html.haml | 28 +++++++++++++++++++ app/views/devise/registrations/new.html.haml | 9 ++++++ app/views/devise/sessions/new.html.haml | 10 +++++++ app/views/devise/shared/_links.haml | 19 +++++++++++++ app/views/devise/unlocks/new.html.haml | 9 ++++++ app/views/layouts/application.html.haml | 2 +- config/routes.rb | 10 +++---- lib/templates/haml/scaffold/_form.html.haml | 10 +++++++ 17 files changed, 145 insertions(+), 6 deletions(-) create mode 100644 app/views/devise/confirmations/new.html.haml create mode 100644 app/views/devise/mailer/confirmation_instructions.html.haml create mode 100644 app/views/devise/mailer/reset_password_instructions.html.haml create mode 100644 app/views/devise/mailer/unlock_instructions.html.haml create mode 100644 app/views/devise/passwords/edit.html.haml create mode 100644 app/views/devise/passwords/new.html.haml create mode 100644 app/views/devise/registrations/edit.html.haml create mode 100644 app/views/devise/registrations/new.html.haml create mode 100644 app/views/devise/sessions/new.html.haml create mode 100644 app/views/devise/shared/_links.haml create mode 100644 app/views/devise/unlocks/new.html.haml create mode 100644 lib/templates/haml/scaffold/_form.html.haml diff --git a/Gemfile b/Gemfile index 45f50ee9..536d3605 100644 --- a/Gemfile +++ b/Gemfile @@ -44,6 +44,7 @@ gem 'devise' gem 'omniauth' gem 'omniauth-github', github: 'alexandrz/omniauth-github', branch: 'provide_emails' gem 'cancancan' +gem 'twitter_bootstrap_form_for', github: 'stouset/twitter_bootstrap_form_for' gem 'octokit' diff --git a/Gemfile.lock b/Gemfile.lock index 27cc69ab..d7e12528 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -33,6 +33,14 @@ GIT specs: bootstrap_form (2.0.1) +GIT + remote: git://github.com/stouset/twitter_bootstrap_form_for.git + revision: 830dbfd439ebb1194e1ae025100fc0e790be37cf + specs: + twitter_bootstrap_form_for (2.0.0.beta) + actionpack (~> 4) + railties (~> 4) + GEM remote: https://rubygems.org/ specs: @@ -349,5 +357,6 @@ DEPENDENCIES timecop turbolinks twitter-bootstrap-rails! + twitter_bootstrap_form_for! uglifier (>= 1.3.0) whenever diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index dfd74b4f..f1b6dba4 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -23,3 +23,9 @@ -moz-box-sizing: content-box; box-sizing: content-box; } +.form-devise { + max-width: 430px; + padding: 15px; + margin: 0 auto; +} + 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..d03cdec1 --- /dev/null +++ b/app/views/devise/registrations/edit.html.haml @@ -0,0 +1,28 @@ += twitter_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.email_field :email, :autofocus => true + - if devise_mapping.confirmable? && resource.pending_reconfirmation? + %div + Currently waiting confirmation for: #{resource.unconfirmed_email} + %div + = f.label :password + %i (leave blank if you don't want to change it) + %br/ + = f.password_field :password, :autocomplete => "off" + %div + = f.label :password_confirmation + %br/ + = f.password_field :password_confirmation + %div + = f.label :current_password + %i (we need your current password to confirm your changes) + %br/ + = f.password_field :current_password + %div= f.submit "Update" + %p + %h3 Cancel my account + %p + Unhappy? #{button_to "Cancel my account", registration_path(resource_name), :data => { :confirm => "Are you sure?" }, :method => :delete} + = 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..18cb70b7 --- /dev/null +++ b/app/views/devise/sessions/new.html.haml @@ -0,0 +1,10 @@ += twitter_bootstrap_form_for(resource, :as => resource_name, :url => session_path(resource_name), html: {class: 'form-devise' }) do |f| + %h2 Sign in + = 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" + %p + = 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..f763fdcf --- /dev/null +++ b/app/views/devise/shared/_links.haml @@ -0,0 +1,19 @@ +- 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/ +- if devise_mapping.omniauthable? + - resource_class.omniauth_providers.each do |provider| + = link_to "Sign in with #{provider.to_s.titleize}", omniauth_authorize_path(resource_name, provider) + %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/layouts/application.html.haml b/app/views/layouts/application.html.haml index 2fee79ed..0426b6c3 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -38,7 +38,7 @@ \/ = link_to 'Sign Out', destroy_user_session_path, method: :delete - else - %a{href: user_omniauth_authorize_path(:github)} Sign in + = link_to "Sign in", new_user_session_path %h3.text-muted.code-pro Peer4Commit = render 'common/menu' = render_flash_message diff --git a/config/routes.rb b/config/routes.rb index 8314e047..b2ce91a8 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -7,6 +7,11 @@ get 'login' => 'home#login' + devise_for :users, + :controllers => { + :omniauth_callbacks => "users/omniauth_callbacks" + } + resources :users, :only => [:show, :update, :index] do collection do get :login @@ -32,9 +37,4 @@ end resources :tips, :only => [:index] resources :distributions, :only => [:index] - - devise_for :users, - :controllers => { - :omniauth_callbacks => "users/omniauth_callbacks" - } 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 From ce761ee15d624717eb85d6c9df5cde4bf2ba32cf Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 1 Jun 2014 09:29:10 +0200 Subject: [PATCH 220/372] Users must confirm their email --- app/controllers/distributions_controller.rb | 17 +++++++++++------ .../users/omniauth_callbacks_controller.rb | 6 +++--- app/models/project.rb | 10 ++++++---- app/models/user.rb | 1 + app/views/devise/shared/_links.haml | 2 +- app/views/layouts/application.html.haml | 2 +- .../20140601072522_add_devise_confirmable.rb | 13 +++++++++++++ db/schema.rb | 6 +++++- features/step_definitions/common.rb | 10 ++++++++-- features/step_definitions/web.rb | 1 + test/factories/users.rb | 1 + 11 files changed, 51 insertions(+), 18 deletions(-) create mode 100644 db/migrate/20140601072522_add_devise_confirmable.rb diff --git a/app/controllers/distributions_controller.rb b/app/controllers/distributions_controller.rb index 8360a2aa..3c6c5ee7 100644 --- a/app/controllers/distributions_controller.rb +++ b/app/controllers/distributions_controller.rb @@ -15,9 +15,7 @@ def recipient_suggestions def create @distribution.project = @project - @distribution.tips.each do |tip| - tip.project = @project - end + finalize_distribution if @distribution.save redirect_to [@project, @distribution], notice: "Distribution created" else @@ -30,9 +28,7 @@ def edit def update @distribution.attributes = distribution_params - @distribution.tips.each do |tip| - tip.project = @project - end + finalize_distribution if @distribution.save redirect_to [@project, @distribution], notice: "Distribution updated" else @@ -55,4 +51,13 @@ def send_transaction def distribution_params params.require(:distribution).permit(tips_attributes: [:id, :coin_amount, :user_id, {user_attributes: [:email]}]) 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/users/omniauth_callbacks_controller.rb b/app/controllers/users/omniauth_callbacks_controller.rb index e634d9f0..3afec97e 100644 --- a/app/controllers/users/omniauth_callbacks_controller.rb +++ b/app/controllers/users/omniauth_callbacks_controller.rb @@ -8,12 +8,12 @@ def github end unless @user if info['primary_email'] - generated_password = Devise.friendly_token.first(8) - @user = User.create!( + @user = User.new( :email => info['primary_email'], - :password => generated_password, :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 diff --git a/app/models/project.rb b/app/models/project.rb index 1ef88569..22a1db2b 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -80,18 +80,20 @@ def tip_for commit # create user unless user generated_password = Devise.friendly_token.first(8) - user = User.create( + user = User.new( email: email, password: generated_password, name: commit.commit.author.name, - nickname: nickname, ) + user.skip_confirmation_notification! end - if nickname - user.update nickname: nickname + if nickname.present? and user.nickname.blank? + user.nickname = nickname end + user.save! + if hold_tips amount = nil else diff --git a/app/models/user.rb b/app/models/user.rb index 528b9a09..24664625 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -5,6 +5,7 @@ class User < ActiveRecord::Base :rememberable, :trackable, :validatable devise :omniauthable, :omniauth_providers => [:github] + devise :confirmable, reconfirmable: true validates :bitcoin_address, bitcoin_address: true diff --git a/app/views/devise/shared/_links.haml b/app/views/devise/shared/_links.haml index f763fdcf..3bef56f7 100644 --- a/app/views/devise/shared/_links.haml +++ b/app/views/devise/shared/_links.haml @@ -15,5 +15,5 @@ %br/ - if devise_mapping.omniauthable? - resource_class.omniauth_providers.each do |provider| - = link_to "Sign in with #{provider.to_s.titleize}", omniauth_authorize_path(resource_name, provider) + = link_to "Sign in with #{provider.to_s.titleize}", omniauth_authorize_path(resource_name, provider, origin: params[:return_url]) %br/ diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 0426b6c3..3c310dad 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -38,7 +38,7 @@ \/ = link_to 'Sign Out', destroy_user_session_path, method: :delete - else - = link_to "Sign in", new_user_session_path + = link_to "Sign in", new_user_session_path(return_url: request.url) %h3.text-muted.code-pro Peer4Commit = render 'common/menu' = render_flash_message 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/schema.rb b/db/schema.rb index 3852f4f2..ff13a86d 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20140531080839) do +ActiveRecord::Schema.define(version: 20140601072522) do create_table "cold_storage_transfers", force: true do |t| t.integer "project_id" @@ -138,6 +138,10 @@ t.datetime "notified_at" t.integer "commits_count", default: 0 t.integer "withdrawn_amount", limit: 8, default: 0 + t.string "confirmation_token" + t.datetime "confirmed_at" + t.datetime "confirmation_sent_at" + t.string "unconfirmed_email" end add_index "users", ["email"], name: "index_users_on_email", unique: true diff --git a/features/step_definitions/common.rb b/features/step_definitions/common.rb index 32ff767e..61f58674 100644 --- a/features/step_definitions/common.rb +++ b/features/step_definitions/common.rb @@ -3,7 +3,12 @@ end Then(/^there should be (\d+) email sent$/) do |arg1| - ActionMailer::Base.deliveries.size.should eq(arg1.to_i) + begin + ActionMailer::Base.deliveries.size.should eq(arg1.to_i) + rescue + p ActionMailer::Base.deliveries + raise + end end Then(/^no email should have been sent$/) do @@ -128,7 +133,8 @@ def find_new_commit(id) end Given(/^a project managed by "(.*?)"$/) do |arg1| - create(:user, email: "#{arg1}@example.com", nickname: arg1) + user = create(:user, email: "#{arg1}@example.com", nickname: arg1) + user.confirm! @project = Project.create!(name: "#{arg1} project", bitcoin_address: 'mq4NtnmQoQoPfNWEPbhSvxvncgtGo6L8WY', address_label: "example_project_account") @project.collaborators.create!(login: arg1) end diff --git a/features/step_definitions/web.rb b/features/step_definitions/web.rb index 5630459e..0c45fc5e 100644 --- a/features/step_definitions/web.rb +++ b/features/step_definitions/web.rb @@ -9,6 +9,7 @@ } visit root_path click_on "Sign in" + click_on "Sign in with Github" page.should have_content("Successfully authenticated") @current_user = User.find_by(nickname: arg1) end diff --git a/test/factories/users.rb b/test/factories/users.rb index 9303c680..6f680e2a 100644 --- a/test/factories/users.rb +++ b/test/factories/users.rb @@ -4,5 +4,6 @@ factory :user do sequence(:email) { |n| "user#{n}@example.com" } password "password" + confirmed_at { Time.now } end end From d287100ce71c7185611a7b5c78608141778fd449 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 1 Jun 2014 11:25:15 +0200 Subject: [PATCH 221/372] Confirmation handled properly when users are notified --- app/controllers/users_controller.rb | 12 ++++++--- app/views/devise/sessions/new.html.haml | 1 + .../user_mailer/address_request.html.haml | 7 ++++-- app/views/user_mailer/new_tip.html.haml | 2 +- features/distribution.feature | 17 +++++++------ features/step_definitions/common.rb | 19 ++++++++++++++ features/step_definitions/distribution.rb | 20 +-------------- features/step_definitions/tip_for_commit.rb | 2 +- features/step_definitions/web.rb | 23 +++++++++++++++++ features/support/bitcoin_daemon_mock.rb | 4 +++ features/tip_for_commit.feature | 25 ++++++++++++++++--- 11 files changed, 95 insertions(+), 37 deletions(-) diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 21124b74..a951f955 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -2,8 +2,12 @@ class UsersController < ApplicationController before_action except: [:login, :index, :send_email_address_request, :set_password_and_address] do @user = User.where(id: params[:id]).first - unless current_user && current_user == @user - redirect_to root_path + 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 @@ -51,10 +55,12 @@ def send_email_address_request def set_password_and_address @user = User.find(params[:id]) - raise "Invalid token" unless Devise.secure_compare(params[:token], @user.login_token) + raise "Blank token" if params[:token].blank? + 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" diff --git a/app/views/devise/sessions/new.html.haml b/app/views/devise/sessions/new.html.haml index 18cb70b7..95348373 100644 --- a/app/views/devise/sessions/new.html.haml +++ b/app/views/devise/sessions/new.html.haml @@ -1,5 +1,6 @@ = twitter_bootstrap_form_for(resource, :as => resource_name, :url => session_path(resource_name), html: {class: 'form-devise' }) do |f| %h2 Sign in + = hidden_field_tag :return_url, params[:return_url] = f.email_field :email, :autofocus => true = f.password_field :password - if devise_mapping.rememberable? diff --git a/app/views/user_mailer/address_request.html.haml b/app/views/user_mailer/address_request.html.haml index eebd609b..bb63776c 100644 --- a/app/views/user_mailer/address_request.html.haml +++ b/app/views/user_mailer/address_request.html.haml @@ -5,9 +5,12 @@ = 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 and lock it with a password. + To get your reward you must provide a Peercoin address. -%p= link_to 'Set your password and address', set_password_and_address_user_url(@user, token: @user.login_token) +- if @user.confirmed? + %p= link_to "Set your Peercoin address", user_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. diff --git a/app/views/user_mailer/new_tip.html.haml b/app/views/user_mailer/new_tip.html.haml index ddbaad6b..b417a42f 100644 --- a/app/views/user_mailer/new_tip.html.haml +++ b/app/views/user_mailer/new_tip.html.haml @@ -4,7 +4,7 @@ %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= 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! diff --git a/features/distribution.feature b/features/distribution.feature index 3aa2dc25..c82ae876 100644 --- a/features/distribution.feature +++ b/features/distribution.feature @@ -117,7 +117,7 @@ Feature: Fundraisers can distribute funds And the project balance should be "490.00" @javascript - Scenario: Send to an email address + Scenario: Send to an unknown email address Given the current time is "2014-03-01 12:35:02 UTC" Given a project managed by "alice" @@ -263,14 +263,17 @@ Feature: Fundraisers can distribute funds And an email should have been sent to "bob@example.com" And the email should include "alice" And the email should include a link to the last distribution - When I visit the link to set my password and address from the email + When I log out + And I click on the "Set your Peercoin address" link in the email + Then I should see "Forgot your password?" + When I fill "Email" with "bob@example.com" And I fill "Password" with "password" - And I fill "Password confirmation" with "password" - And I fill "Peercoin address" with "mubmzLrtTgDE2WrHkiwSFKuTh2VTSXboYK" - And I click on "Save" + And I click on "Sign in" in the sign in form + Then I should see "Peercoin address" + When I fill "Peercoin address" with "mubmzLrtTgDE2WrHkiwSFKuTh2VTSXboYK" + And I click on "Update" + Then I should see "Your information was saved" - Then I should see "Information saved" - And the user with email "bob@example.com" should have "password" as password And the user with email "bob@example.com" should have "mubmzLrtTgDE2WrHkiwSFKuTh2VTSXboYK" as peercoin address When I log out diff --git a/features/step_definitions/common.rb b/features/step_definitions/common.rb index 61f58674..2efffc09 100644 --- a/features/step_definitions/common.rb +++ b/features/step_definitions/common.rb @@ -9,6 +9,11 @@ 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 @@ -159,3 +164,17 @@ def find_new_commit(id) pending end +Then(/^these amounts should have been sent from the account of the project:$/) do |table| + 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.should eq(table.hashes) +end + +When(/^the transaction history is cleared$/) do + BitcoinDaemon.instance.clear_transaction_history +end diff --git a/features/step_definitions/distribution.rb b/features/step_definitions/distribution.rb index b263b4ff..5a301a9f 100644 --- a/features/step_definitions/distribution.rb +++ b/features/step_definitions/distribution.rb @@ -42,17 +42,6 @@ end end -Then(/^these amounts should have been sent from the account of the project:$/) do |table| - 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.should eq(table.hashes) -end - When(/^the tipper is started$/) do BitcoinTipper.work end @@ -87,14 +76,7 @@ end When(/^I visit the link to set my password and address from the email$/) do - begin - link = Nokogiri::HTML.parse(@email.body.decoded).css("a").detect { |el| el.text == "Set your password and address" } - link.should_not be_nil - rescue - puts @email.body - raise - end - visit URI.parse(link["href"]).request_uri + 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| diff --git a/features/step_definitions/tip_for_commit.rb b/features/step_definitions/tip_for_commit.rb index 5fe7209e..67fafc2f 100644 --- a/features/step_definitions/tip_for_commit.rb +++ b/features/step_definitions/tip_for_commit.rb @@ -18,7 +18,7 @@ commit: OpenStruct.new( message: row["message"] || "Some changes", author: OpenStruct.new( - email: "author@example.com", + email: row["email"] || "author@example.com", ), committer: OpenStruct.new( date: Time.now, diff --git a/features/step_definitions/web.rb b/features/step_definitions/web.rb index 0c45fc5e..9548578e 100644 --- a/features/step_definitions/web.rb +++ b/features/step_definitions/web.rb @@ -61,6 +61,12 @@ click_on(arg1) end +Given(/^I click on "(.*?)" in the sign in form$/) do |arg1| + within ".form-devise" do + click_on(arg1) + end +end + Given(/^I check "(.*?)"$/) do |arg1| check(arg1) end @@ -108,3 +114,20 @@ 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 } + link.should_not be_nil + rescue + puts @email.body + raise + end + url = URI.parse(link["href"]).request_uri + visit url +end + +Then(/^the user with email "(.*?)" should have his email confirmed$/) do |arg1| + User.find_by(email: arg1).confirmed?.should be_true +end + diff --git a/features/support/bitcoin_daemon_mock.rb b/features/support/bitcoin_daemon_mock.rb index c1b8dc45..75e3c0c3 100644 --- a/features/support/bitcoin_daemon_mock.rb +++ b/features/support/bitcoin_daemon_mock.rb @@ -27,6 +27,10 @@ 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| diff --git a/features/tip_for_commit.feature b/features/tip_for_commit.feature index 327684af..0b544ef6 100644 --- a/features/tip_for_commit.feature +++ b/features/tip_for_commit.feature @@ -4,10 +4,10 @@ Feature: On projects not holding tips, a tip is created for each new commit 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 | - | 123 | bob | - | abc | alice | - | 333 | bob | + | 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" @@ -24,6 +24,23 @@ Feature: On projects not holding tips, a tip is created for each new commit | address | amount | | mxWfjaZJTNN5QKeZZYQ5HW3vgALFBsnuG1 | 9.9005 | + And an email should have been sent to "alicia@example.com" + When I click on the "Set your password and Peercoin address" link in the email + And I fill "Password" with "password" + And I fill "Password confirmation" with "password" + And I fill "Peercoin address" with "mubmzLrtTgDE2WrHkiwSFKuTh2VTSXboYK" + And I click on "Save" + Then I should see "Information saved" + And the user with email "alicia@example.com" should have "password" as password + And the user with email "alicia@example.com" should have "mubmzLrtTgDE2WrHkiwSFKuTh2VTSXboYK" as peercoin address + And the user with email "alicia@example.com" should have his email confirmed + + When the transaction history is cleared + And the tipper is started + Then these amounts should have been sent from the account of the project: + | address | amount | + | mubmzLrtTgDE2WrHkiwSFKuTh2VTSXboYK | 4.95 | + Scenario: A project holding tips Given a project And the project holds tips From 18d61a567f1d5c20c9a5e181613b0620f20718e1 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 1 Jun 2014 12:22:18 +0200 Subject: [PATCH 222/372] New project redirects to the right login page --- app/controllers/projects_controller.rb | 2 +- app/views/home/login.html.haml | 7 ------- app/views/projects/index.html.haml | 9 +++++++-- config/routes.rb | 2 -- features/create_project.feature | 8 ++++---- 5 files changed, 12 insertions(+), 16 deletions(-) delete mode 100644 app/views/home/login.html.haml diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 3684aa79..92daa7be 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -18,7 +18,7 @@ def show def new unless user_signed_in? - redirect_to login_path(return_url: request.original_url), flash: {info: "You must be logged in to create a new project"} + 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]) diff --git a/app/views/home/login.html.haml b/app/views/home/login.html.haml deleted file mode 100644 index 763f1e05..00000000 --- a/app/views/home/login.html.haml +++ /dev/null @@ -1,7 +0,0 @@ -- content_for :title do - Log in - -.row - .col-md-12 - %p.text-center - %a.btn.btn-default{href: user_omniauth_authorize_path(:github, origin: params[:return_url])} Log in with GitHub diff --git a/app/views/projects/index.html.haml b/app/views/projects/index.html.haml index dd579a14..5f155315 100644 --- a/app/views/projects/index.html.haml +++ b/app/views/projects/index.html.haml @@ -1,4 +1,9 @@ -%h1 Projects +.row + .col-md-10 + %h1 Projects + .col-md-2 + .text-right + = link_to "Create project", new_project_path, class: 'btn btn-default' %p %table.table %thead @@ -16,5 +21,5 @@ %td= project.description %td= project.watchers_count %td= btc_human project.available_amount_cache - %td= link_to 'Details', project, class: 'btn btn-success' + %td= link_to 'Details', project, class: 'btn btn-default' = paginate @projects diff --git a/config/routes.rb b/config/routes.rb index b2ce91a8..6aa0d898 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -5,8 +5,6 @@ get 'audit' => 'home#audit' get 'faq' => 'home#faq' - get 'login' => 'home#login' - devise_for :users, :controllers => { :omniauth_callbacks => "users/omniauth_callbacks" diff --git a/features/create_project.feature b/features/create_project.feature index 7ea1be45..21054fdf 100644 --- a/features/create_project.feature +++ b/features/create_project.feature @@ -4,7 +4,7 @@ Feature: An user can create a project, linked with GitHub or not. When I visit the home page And I click on "Create a project" - And I click on "Log in with GitHub" + And I click on "Sign in with Github" Then I should see "New project" When I fill "Name" with "Project Foo" @@ -27,7 +27,7 @@ Feature: An user can create a project, linked with GitHub or not. When I visit the home page And I click on "Create a project" - And I click on "Log in with GitHub" + And I click on "Sign in with Github" Then I should see "New project" And I click on "Save" @@ -39,7 +39,7 @@ Feature: An user can create a project, linked with GitHub or not. When I visit the home page And I click on "Create a project" - And I click on "Log in with GitHub" + And I click on "Sign in with Github" Then I should see "New project" And I click on "Save" @@ -51,7 +51,7 @@ Feature: An user can create a project, linked with GitHub or not. When I visit the home page And I click on "Create a project" - And I click on "Log in with GitHub" + And I click on "Sign in with Github" Then I should see "New project" When I fill "Name" with "Project Foo" From fa9312db8190f22a26735647954981b2a8bb480e Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 1 Jun 2014 12:36:31 +0200 Subject: [PATCH 223/372] Better sign in form --- app/views/devise/sessions/new.html.haml | 30 ++++++++++++++++--------- app/views/devise/shared/_links.haml | 4 ---- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/app/views/devise/sessions/new.html.haml b/app/views/devise/sessions/new.html.haml index 95348373..c26c42f6 100644 --- a/app/views/devise/sessions/new.html.haml +++ b/app/views/devise/sessions/new.html.haml @@ -1,11 +1,21 @@ -= twitter_bootstrap_form_for(resource, :as => resource_name, :url => session_path(resource_name), html: {class: 'form-devise' }) do |f| - %h2 Sign in - = 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" - %p +.row + .col-md-4 +.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" + .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 index 3bef56f7..1573dbe5 100644 --- a/app/views/devise/shared/_links.haml +++ b/app/views/devise/shared/_links.haml @@ -13,7 +13,3 @@ - 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/ -- if devise_mapping.omniauthable? - - 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]) - %br/ From 7f40af0fcbe238ddabb0fd6067d5cf1aa49313f9 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 1 Jun 2014 12:38:24 +0200 Subject: [PATCH 224/372] Better project index --- app/views/projects/index.html.haml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/views/projects/index.html.haml b/app/views/projects/index.html.haml index 5f155315..e0dbfef2 100644 --- a/app/views/projects/index.html.haml +++ b/app/views/projects/index.html.haml @@ -3,9 +3,9 @@ %h1 Projects .col-md-2 .text-right - = link_to "Create project", new_project_path, class: 'btn btn-default' + = link_to "Create project", new_project_path, class: 'btn btn-default btn-block' %p - %table.table + %table.table.table-hover %thead %tr %th Name @@ -21,5 +21,5 @@ %td= project.description %td= project.watchers_count %td= btc_human project.available_amount_cache - %td= link_to 'Details', project, class: 'btn btn-default' + %td= link_to 'Details', project, class: 'btn btn-default btn-block' = paginate @projects From 230ac097a2926114424b6c49f6242f20fad47afa Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 1 Jun 2014 12:48:19 +0200 Subject: [PATCH 225/372] Can create multiple projects on the same GitHub project --- ...20140601103950_new_default_to_hold_tips.rb | 5 ++++ ..._unique_constraint_to_project_full_name.rb | 9 +++++++ db/schema.rb | 5 ++-- features/create_project.feature | 24 +++++++++++++++++++ 4 files changed, 40 insertions(+), 3 deletions(-) create mode 100644 db/migrate/20140601103950_new_default_to_hold_tips.rb create mode 100644 db/migrate/20140601104108_remove_unique_constraint_to_project_full_name.rb 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/schema.rb b/db/schema.rb index ff13a86d..ae334d4f 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20140601072522) do +ActiveRecord::Schema.define(version: 20140601104108) do create_table "cold_storage_transfers", force: true do |t| t.integer "project_id" @@ -78,7 +78,7 @@ t.integer "available_amount_cache", limit: 8 t.string "github_id" t.string "address_label" - t.boolean "hold_tips", default: false + t.boolean "hold_tips", default: true t.string "cold_storage_withdrawal_address" t.boolean "disabled", default: false t.integer "account_balance", limit: 8 @@ -86,7 +86,6 @@ t.text "detailed_description" end - add_index "projects", ["full_name"], name: "index_projects_on_full_name", unique: true add_index "projects", ["github_id"], name: "index_projects_on_github_id", unique: true create_table "tipping_policies_texts", force: true do |t| diff --git a/features/create_project.feature b/features/create_project.feature index 21054fdf..c30f77a0 100644 --- a/features/create_project.feature +++ b/features/create_project.feature @@ -63,3 +63,27 @@ Feature: An user can create a project, linked with GitHub or not. 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" From f17a986fda79c7fc2ed97a2535b1d3ed1140bc05 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 1 Jun 2014 12:54:44 +0200 Subject: [PATCH 226/372] Better views --- app/views/distributions/_form.html.haml | 40 +++++++++++++------------ app/views/distributions/show.html.haml | 4 +-- 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/app/views/distributions/_form.html.haml b/app/views/distributions/_form.html.haml index ee84805f..ea849a58 100644 --- a/app/views/distributions/_form.html.haml +++ b/app/views/distributions/_form.html.haml @@ -1,19 +1,21 @@ -= bootstrap_form_for [@project, @distribution] do |f| - %table.table - %thead - %th Recipient - %th Amount - %tbody#recipients - = f.fields_for :tips do |fields| - %tr - %td - = fields.hidden_field :id - = fields.hidden_field :user_id - = fields.object.user.recipient_label - %td= fields.text_field :coin_amount, hide_label: true - = f.submit "Save" - -.form-group - %label{for: "add-recipients-input"} Add recipient(s) - %input.form-control#add-recipients-input{type: "text", autocomplete: "off", data: {suggestion_url: recipient_suggestions_project_distributions_path(@project)}} - #recipient-suggestions +.row + .col-md-8 + = bootstrap_form_for [@project, @distribution] do |f| + %table.table + %thead + %th Recipient + %th Amount + %tbody#recipients + = f.fields_for :tips do |fields| + %tr + %td + = fields.hidden_field :id + = fields.hidden_field :user_id + = fields.object.user.recipient_label + %td= fields.text_field :coin_amount, hide_label: true + = f.submit "Save", class: 'btn btn-primary btn-block' + .col-md-4 + .form-group + %label{for: "add-recipients-input"} Add recipient(s) + %input.form-control#add-recipients-input{type: "text", autocomplete: "off", data: {suggestion_url: recipient_suggestions_project_distributions_path(@project)}} + #recipient-suggestions diff --git a/app/views/distributions/show.html.haml b/app/views/distributions/show.html.haml index 92f5e248..ba4483c7 100644 --- a/app/views/distributions/show.html.haml +++ b/app/views/distributions/show.html.haml @@ -34,11 +34,11 @@ 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 log in with their GitHub account. + The transaction cannot be sent because some addresses are missing. Ask the recipients to sign in and provide an address. .distribution-actions - if can? :send_transaction, @distribution .distribution-action = button_to "Send the transaction", send_transaction_project_distribution_path(@project, @distribution), class: "btn btn-default", data: {confirm: "#{total.to_f / COIN} peercoins will be sent. Are you sure?"} - if can? :update, @distribution .distribution-action - = link_to "Edit", edit_project_distribution_path(@project, @distribution), class: "btn btn-default" + = link_to "Edit the distribution", edit_project_distribution_path(@project, @distribution), class: "btn btn-default" From 993c23760766754d709a2b7b7ce3ecf0723f8fc6 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 1 Jun 2014 13:00:52 +0200 Subject: [PATCH 227/372] Better distribution form --- app/views/distributions/_form.html.haml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/app/views/distributions/_form.html.haml b/app/views/distributions/_form.html.haml index ea849a58..e43f50dd 100644 --- a/app/views/distributions/_form.html.haml +++ b/app/views/distributions/_form.html.haml @@ -1,5 +1,5 @@ .row - .col-md-8 + .col-md-12 = bootstrap_form_for [@project, @distribution] do |f| %table.table %thead @@ -13,9 +13,9 @@ = fields.hidden_field :user_id = fields.object.user.recipient_label %td= fields.text_field :coin_amount, hide_label: true - = f.submit "Save", class: 'btn btn-primary btn-block' - .col-md-4 - .form-group - %label{for: "add-recipients-input"} Add recipient(s) - %input.form-control#add-recipients-input{type: "text", autocomplete: "off", data: {suggestion_url: recipient_suggestions_project_distributions_path(@project)}} - #recipient-suggestions + .form-group + %label{for: "add-recipients-input"} Add recipient(s) + %input.form-control#add-recipients-input{type: "text", autocomplete: "off", data: {suggestion_url: recipient_suggestions_project_distributions_path(@project)}} + #recipient-suggestions + .text-center + = f.submit "Save the distribution", class: 'btn btn-primary' From 979479e7f82452080e4905eb5b12df9e830263ad Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 1 Jun 2014 13:08:08 +0200 Subject: [PATCH 228/372] Better loading of suggestions --- app/assets/javascripts/distribution.js.coffee | 41 ++++++++++++++----- 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/app/assets/javascripts/distribution.js.coffee b/app/assets/javascripts/distribution.js.coffee index 8049b35b..dad575c3 100644 --- a/app/assets/javascripts/distribution.js.coffee +++ b/app/assets/javascripts/distribution.js.coffee @@ -1,19 +1,38 @@ $(document).on "page:change", -> input = $("#add-recipients-input") suggestions = $("#recipient-suggestions") + target = $("#recipients") + timer = null + request = null updateSuggestions = (text) -> - $.ajax - type: 'GET' - url: input.data('suggestion-url') - data: - text: text - success: (data) -> - suggestions.html(data) - suggestions.find(".add-recipient-button").click -> - recipients = $(this).data("recipients") - $("#recipients").append(recipients) - false + if timer + clearTimeout(timer) + timer = null + + if request + request.abort() + request = null + + suggestions.html("

Loading...
") + + timer = setTimeout -> + request = $.ajax + type: 'GET' + url: input.data('suggestion-url') + data: + text: text + success: (data) -> + suggestions.hide().html(data).slideDown("fast") + suggestions.find(".add-recipient-button").click -> + recipients = $(this).data("recipients") + target.append(recipients) + suggestions.html("") + input.val("") + false + error: -> + suggestions.html("An error occured.") + , 200 input.on "input", -> updateSuggestions(input.val()) From 2736ea76bc6d76e42171ff0484a1ff7748038d7a Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 1 Jun 2014 13:17:23 +0200 Subject: [PATCH 229/372] Suggestions in columns --- app/assets/stylesheets/distribution.css.sass | 2 + .../recipient_suggestions.html.haml | 55 ++++++++++--------- 2 files changed, 31 insertions(+), 26 deletions(-) create mode 100644 app/assets/stylesheets/distribution.css.sass diff --git a/app/assets/stylesheets/distribution.css.sass b/app/assets/stylesheets/distribution.css.sass new file mode 100644 index 00000000..7ae6e1b4 --- /dev/null +++ b/app/assets/stylesheets/distribution.css.sass @@ -0,0 +1,2 @@ +.add-recipient-button + margin: 6px 0 diff --git a/app/views/distributions/recipient_suggestions.html.haml b/app/views/distributions/recipient_suggestions.html.haml index 7f62d87a..4f30014e 100644 --- a/app/views/distributions/recipient_suggestions.html.haml +++ b/app/views/distributions/recipient_suggestions.html.haml @@ -1,27 +1,30 @@ = bootstrap_form_for Distribution.new do |f| - %ul.list-unstyled - - User.where('nickname LIKE ?', "%#{params[:text]}%").each do |user| - %li - - recipients = capture_haml do - = f.fields_for :tips, Tip.new(user_id: user.id), child_index: rand(1<<160) do |fields| - %tr - %td - = fields.hidden_field :user_id - = "#{user.nickname} (GitHub user)" - %td= fields.text_field :coin_amount, hide_label: true - %a.btn.btn-default.add-recipient-button{href: "#", data: {recipients: recipients}}= "#{user.nickname} (GitHub user)" - - if params[:text] =~ Devise::email_regexp - - user = User.where(email: params[:text]).first_or_initialize - %li - - recipients = capture_haml do - = f.fields_for :tips, Tip.new(user: user), child_index: rand(1<<160) do |fields| - %tr - %td - - if user.new_record? - = fields.fields_for :user do |user_fields| - = user_fields.hidden_field :email - - else - = fields.hidden_field :user_id - = user.recipient_label - %td= fields.text_field :coin_amount, hide_label: true - %a.btn.btn-default.add-recipient-button{href: "#", data: {recipients: recipients}}= user.recipient_label + - suggestions = [] + - if params[:text] =~ Devise::email_regexp + - user = User.where(email: params[:text]).first_or_initialize + - recipients = capture_haml do + = f.fields_for :tips, Tip.new(user: user), child_index: rand(1<<160) do |fields| + %tr + %td + - if user.new_record? + = fields.fields_for :user do |user_fields| + = user_fields.hidden_field :email + - else + = fields.hidden_field :user_id + = user.recipient_label + %td= fields.text_field :coin_amount, hide_label: true + - suggestions << [user.recipient_label, recipients] + - User.where('nickname LIKE ?', "%#{params[:text]}%").each do |user| + - recipients = capture_haml do + = f.fields_for :tips, Tip.new(user_id: user.id), child_index: rand(1<<160) do |fields| + %tr + %td + = fields.hidden_field :user_id + = "#{user.nickname} (GitHub user)" + %td= fields.text_field :coin_amount, hide_label: true + - suggestions << [user.recipient_label, recipients] + - suggestions.each_slice(4) do |slice| + .row + - slice.each do |label, recipients| + .col-md-3 + %a.btn.btn-block.btn-default.add-recipient-button{href: "#", data: {recipients: recipients}}= label From e09de98d2bd4f2b5f52e446e3b0f3af4ff65c4a6 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 1 Jun 2014 13:29:19 +0200 Subject: [PATCH 230/372] Fixed feature --- app/views/devise/sessions/new.html.haml | 41 ++++++++++++------------- features/step_definitions/common.rb | 4 +-- features/step_definitions/web.rb | 2 +- 3 files changed, 23 insertions(+), 24 deletions(-) diff --git a/app/views/devise/sessions/new.html.haml b/app/views/devise/sessions/new.html.haml index c26c42f6..c79ff2da 100644 --- a/app/views/devise/sessions/new.html.haml +++ b/app/views/devise/sessions/new.html.haml @@ -1,21 +1,20 @@ -.row - .col-md-4 -.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" - .col-md-4 - %h4 Other options - = render "devise/shared/links" +#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" + .col-md-4 + %h4 Other options + = render "devise/shared/links" diff --git a/features/step_definitions/common.rb b/features/step_definitions/common.rb index 2efffc09..78c9b599 100644 --- a/features/step_definitions/common.rb +++ b/features/step_definitions/common.rb @@ -33,11 +33,11 @@ end Given(/^a project$/) do - @project = Project.create!(name: "test", full_name: "example/test", github_id: 123, bitcoin_address: 'mq4NtnmQoQoPfNWEPbhSvxvncgtGo6L8WY', address_label: "example_project_account") + @project = Project.create!(name: "test", full_name: "example/test", github_id: 123, 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}", github_id: Digest::SHA1.hexdigest(arg1), bitcoin_address: 'mq4NtnmQoQoPfNWEPbhSvxvncgtGo6L8WY') + @project = Project.create!(name: "test", full_name: "example/#{arg1}", github_id: Digest::SHA1.hexdigest(arg1), bitcoin_address: 'mq4NtnmQoQoPfNWEPbhSvxvncgtGo6L8WY', hold_tips: false) end Given(/^a deposit of "(.*?)"$/) do |arg1| diff --git a/features/step_definitions/web.rb b/features/step_definitions/web.rb index 9548578e..7c6233ec 100644 --- a/features/step_definitions/web.rb +++ b/features/step_definitions/web.rb @@ -62,7 +62,7 @@ end Given(/^I click on "(.*?)" in the sign in form$/) do |arg1| - within ".form-devise" do + within "#sign-in-form" do click_on(arg1) end end From 57d4aaeb2ba149f2cc7f8bca1a725ee2f36d022c Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 1 Jun 2014 13:41:47 +0200 Subject: [PATCH 231/372] Recipient links to user page --- app/controllers/users_controller.rb | 3 +- app/models/user.rb | 4 +- app/views/distributions/_form.html.haml | 3 +- .../recipient_suggestions.html.haml | 5 +- app/views/distributions/show.html.haml | 9 ++- app/views/users/show.html.haml | 51 ++++++++------- features/distribution.feature | 62 +++++++++---------- features/step_definitions/distribution.rb | 2 +- 8 files changed, 77 insertions(+), 62 deletions(-) diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index a951f955..68c36cf1 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -1,6 +1,6 @@ class UsersController < ApplicationController - before_action except: [:login, :index, :send_email_address_request, :set_password_and_address] do + before_action except: [:show, :login, :index, :send_email_address_request, :set_password_and_address] do @user = User.where(id: params[:id]).first if current_user if current_user != @user @@ -12,6 +12,7 @@ class UsersController < ApplicationController end def show + @user = User.find(params[:id]) end def index diff --git a/app/models/user.rb b/app/models/user.rb index 24664625..8293a4b9 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -42,10 +42,10 @@ def password_required? def recipient_label if nickname.present? - "#{nickname} (GitHub user)" + nickname elsif email.present? if new_record? - "#{email} (unknown email address)" + "#{email} (new user)" else email end diff --git a/app/views/distributions/_form.html.haml b/app/views/distributions/_form.html.haml index e43f50dd..b4fdcadd 100644 --- a/app/views/distributions/_form.html.haml +++ b/app/views/distributions/_form.html.haml @@ -7,11 +7,12 @@ %th Amount %tbody#recipients = f.fields_for :tips do |fields| + - user = fields.object.user %tr %td = fields.hidden_field :id = fields.hidden_field :user_id - = fields.object.user.recipient_label + = link_to user.recipient_label, user %td= fields.text_field :coin_amount, hide_label: true .form-group %label{for: "add-recipients-input"} Add recipient(s) diff --git a/app/views/distributions/recipient_suggestions.html.haml b/app/views/distributions/recipient_suggestions.html.haml index 4f30014e..407c87f2 100644 --- a/app/views/distributions/recipient_suggestions.html.haml +++ b/app/views/distributions/recipient_suggestions.html.haml @@ -9,9 +9,10 @@ - if user.new_record? = fields.fields_for :user do |user_fields| = user_fields.hidden_field :email + = user.recipient_label - else = fields.hidden_field :user_id - = user.recipient_label + = link_to user.recipient_label, user %td= fields.text_field :coin_amount, hide_label: true - suggestions << [user.recipient_label, recipients] - User.where('nickname LIKE ?', "%#{params[:text]}%").each do |user| @@ -20,7 +21,7 @@ %tr %td = fields.hidden_field :user_id - = "#{user.nickname} (GitHub user)" + = link_to user.recipient_label, user %td= fields.text_field :coin_amount, hide_label: true - suggestions << [user.recipient_label, recipients] - suggestions.each_slice(4) do |slice| diff --git a/app/views/distributions/show.html.haml b/app/views/distributions/show.html.haml index ba4483c7..3a35d1e6 100644 --- a/app/views/distributions/show.html.haml +++ b/app/views/distributions/show.html.haml @@ -10,7 +10,14 @@ %tbody - @distribution.tips.each do |tip| %tr - %td.recipient= tip.user.try(:recipient_label) + %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.address - if tip.user.try(:bitcoin_address).present? = tip.user.bitcoin_address diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index 05b63c43..01016537 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -1,25 +1,30 @@ %h1= @user.name -%p - %strong Balance -%p - = btc_human @user.balance -%p - %small - You will get the money of your tips when the project they belong hits the threshold of - = btc_human CONFIG["min_payout"].to_d * COIN -%p - %strong E-mail -%p= @user.email -= form_for @user, html: {role: 'form'} do |f| - - if @user.errors.size > 0 - .alert.alert-danger Peercoin address is invalid. - .form-group - = f.label :bitcoin_address - = f.text_field :bitcoin_address, class: 'form-control', placeholder: 'Your peercoin address' - = f.button "Update", class: 'btn btn-default' +- if @user.nickname.present? + %p + %strong GitHub account: + = link_to @user.nickname, "https://github.com/#{@user.nickname}" +- if @user == current_user + %p + %strong Balance + %p + = btc_human @user.balance + %p + %small + You will get the money of your tips when the project they belong hits the threshold of + = btc_human CONFIG["min_payout"].to_d * COIN + %p + %strong E-mail + %p= @user.email + = form_for @user, html: {role: 'form'} do |f| + - if @user.errors.size > 0 + .alert.alert-danger Peercoin address is invalid. + .form-group + = f.label :bitcoin_address + = f.text_field :bitcoin_address, class: 'form-control', placeholder: 'Your peercoin address' + = f.button "Update", class: 'btn btn-default' -- if @user.balance > 0 - .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", confirm: "All the #{to_btc @user.balance} peercoins you received will be sent back to their project. Are you sure?" + - if @user.balance > 0 + .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", confirm: "All the #{to_btc @user.balance} peercoins you received will be sent back to their project. Are you sure?" diff --git a/features/distribution.feature b/features/distribution.feature index c82ae876..2773f67d 100644 --- a/features/distribution.feature +++ b/features/distribution.feature @@ -11,13 +11,13 @@ Feature: Fundraisers can distribute funds And I go to the project page And I click on "New distribution" And I type "bob" in the recipient field - And I select the recipient "bob (GitHub user)" - And I fill the amount to "bob (GitHub user)" with "10" + And I select the recipient "bob" + And I fill the amount to "bob" with "10" And I click on "Save" Then I should see these distribution lines: - | recipient | address | amount | percentage | - | bob (GitHub user) | mxWfjaZJTNN5QKeZZYQ5HW3vgALFBsnuG1 | 10 | 100 | + | recipient | address | amount | percentage | + | bob | mxWfjaZJTNN5QKeZZYQ5HW3vgALFBsnuG1 | 10 | 100 | And I should see "Total amount: 10.00 PPC" When the tipper is started @@ -45,17 +45,17 @@ Feature: Fundraisers can distribute funds And I go to the project page And I click on "New distribution" And I type "bob" in the recipient field - And I select the recipient "bob (GitHub user)" - And I fill the amount to "bob (GitHub user)" with "10" + And I select the recipient "bob" + And I fill the amount to "bob" with "10" And I type "carol" in the recipient field - And I select the recipient "carol (GitHub user)" - And I fill the amount to "carol (GitHub user)" with "13.56" + And I select the recipient "carol" + And I fill the amount to "carol" with "13.56" And I click on "Save" Then I should see these distribution lines: - | recipient | address | amount | percentage | - | bob (GitHub user) | mxWfjaZJTNN5QKeZZYQ5HW3vgALFBsnuG1 | 10 | 42.4 | - | carol (GitHub user) | mi9SLroAgc8eUNuLwnZmdyqWdShbNtvr3n | 13.56 | 57.6 | + | 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 @@ -81,13 +81,13 @@ Feature: Fundraisers can distribute funds And I go to the project page And I click on "New distribution" And I type "bob" in the recipient field - And I select the recipient "bob (GitHub user)" - And I fill the amount to "bob (GitHub user)" with "10" + And I select the recipient "bob" + And I fill the amount to "bob" with "10" And I click on "Save" Then I should see these distribution lines: - | recipient | address | amount | percentage | - | bob (GitHub user) | | 10 | 100.0 | + | 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" @@ -106,8 +106,8 @@ Feature: Fundraisers can distribute funds 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 (GitHub user) | mnVba8qrpy5uxYD7dV4NZMQPWjgdt2QC1i | 10 | 100.0 | + | recipient | address | amount | percentage | + | bob | mnVba8qrpy5uxYD7dV4NZMQPWjgdt2QC1i | 10 | 100.0 | When I click on "Send the transaction" Then I should see "Transaction sent" @@ -128,13 +128,13 @@ Feature: Fundraisers can distribute funds And I go to the project page And I click on "New distribution" And I type "bob@example.com" in the recipient field - And I select the recipient "bob@example.com (unknown email address)" - And I fill the amount to "bob@example.com (unknown email address)" with "10" + And I select the recipient "bob@example.com (new user)" + And I fill the amount to "bob@example.com (new user)" with "10" And I click on "Save" Then I should see these distribution lines: - | recipient | address | amount | percentage | - | bob@example.com | | 10 | 100.0 | + | recipient | address | amount | percentage | + | bob@example.com | | 10 | 100.0 | And I should see "The transaction cannot be sent because some addresses are missing" And no email should have been sent @@ -197,28 +197,28 @@ Feature: Fundraisers can distribute funds And I go to the project page And I click on "New distribution" And I type "bob" in the recipient field - And I select the recipient "bob (GitHub user)" - And I fill the amount to "bob (GitHub user)" with "10" + And I select the recipient "bob" + And I fill the amount to "bob" with "10" And I click on "Save" Then I should see these distribution lines: - | recipient | address | amount | percentage | - | bob (GitHub user) | mxWfjaZJTNN5QKeZZYQ5HW3vgALFBsnuG1 | 10 | 100 | + | 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 (GitHub user)" with "15" + And I fill the amount to "bob" with "15" And I type "carol" in the recipient field - And I select the recipient "carol (GitHub user)" - And I fill the amount to "carol (GitHub user)" with "5" + And I select the recipient "carol" + And I fill the amount to "carol" with "5" And I click on "Save" Then I should see these distribution lines: - | recipient | address | amount | percentage | - | bob (GitHub user) | mxWfjaZJTNN5QKeZZYQ5HW3vgALFBsnuG1 | 15 | 75.0 | - | carol (GitHub user) | mi9SLroAgc8eUNuLwnZmdyqWdShbNtvr3n | 5 | 25.0 | + | 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" diff --git a/features/step_definitions/distribution.rb b/features/step_definitions/distribution.rb index 5a301a9f..4f70479d 100644 --- a/features/step_definitions/distribution.rb +++ b/features/step_definitions/distribution.rb @@ -35,7 +35,7 @@ puts "Rows: " + all("#distribution-show-page tbody tr").map(&:text).inspect raise end - tr.find(".recipient").should have_content(row["recipient"]) + tr.find(".recipient").text.should eq(row["recipient"]) tr.find(".address").text.should eq(row["address"]) tr.find(".amount").should have_content(row["amount"]) tr.find(".percentage").should have_content(row["percentage"]) From fa700dc2b1b104a56325cb81884c31bfa0eb3179 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 1 Jun 2014 14:06:14 +0200 Subject: [PATCH 232/372] Better user form --- app/assets/stylesheets/users.css.sass | 5 +- app/controllers/application_controller.rb | 7 +++ app/views/devise/registrations/edit.html.haml | 53 ++++++++++--------- app/views/layouts/application.html.haml | 4 +- .../user_mailer/address_request.html.haml | 2 +- app/views/users/show.html.haml | 24 --------- features/distribution.feature | 3 +- features/step_definitions/distribution.rb | 5 +- features/step_definitions/web.rb | 3 ++ 9 files changed, 50 insertions(+), 56 deletions(-) diff --git a/app/assets/stylesheets/users.css.sass b/app/assets/stylesheets/users.css.sass index 9d985b83..6d70b9d5 100644 --- a/app/assets/stylesheets/users.css.sass +++ b/app/assets/stylesheets/users.css.sass @@ -3,4 +3,7 @@ // You can use Sass (SCSS) here: http://sass-lang.com/ .send-tips-back-block - margin-top: 50px + +#error_explanation + h2 + font-size: 16px diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index b32e181c..4fed25b5 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -11,9 +11,16 @@ class ApplicationController < ActionController::Base end end + before_filter :configure_permitted_parameters, if: :devise_controller? + 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.for(:account_update) { |u| u.permit(:email, :name, :bitcoin_address, :current_password, :password, :password_confirmation) } + end end diff --git a/app/views/devise/registrations/edit.html.haml b/app/views/devise/registrations/edit.html.haml index d03cdec1..c04f94ee 100644 --- a/app/views/devise/registrations/edit.html.haml +++ b/app/views/devise/registrations/edit.html.haml @@ -1,28 +1,31 @@ -= twitter_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.email_field :email, :autofocus => true - - if devise_mapping.confirmable? && resource.pending_reconfirmation? +.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.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 - Currently waiting confirmation for: #{resource.unconfirmed_email} - %div - = f.label :password - %i (leave blank if you don't want to change it) - %br/ - = f.password_field :password, :autocomplete => "off" - %div - = f.label :password_confirmation - %br/ - = f.password_field :password_confirmation - %div - = f.label :current_password - %i (we need your current password to confirm your changes) - %br/ - = f.password_field :current_password - %div= f.submit "Update" - %p - %h3 Cancel my account + = 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' + %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} + 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/layouts/application.html.haml b/app/views/layouts/application.html.haml index 3c310dad..18f1d539 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -32,9 +32,9 @@ %div.pull-right %small - if current_user - = current_user.full_name + = link_to current_user.full_name, edit_registration_path(current_user), class: "edit-profile-link" \/ - = link_to btc_human(current_user.balance), current_user + = btc_human(current_user.balance) \/ = link_to 'Sign Out', destroy_user_session_path, method: :delete - else diff --git a/app/views/user_mailer/address_request.html.haml b/app/views/user_mailer/address_request.html.haml index bb63776c..87c52cce 100644 --- a/app/views/user_mailer/address_request.html.haml +++ b/app/views/user_mailer/address_request.html.haml @@ -8,7 +8,7 @@ To get your reward you must provide a Peercoin address. - if @user.confirmed? - %p= link_to "Set your Peercoin address", user_url(@user) + %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) diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index 01016537..84ea984e 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -3,28 +3,4 @@ %p %strong GitHub account: = link_to @user.nickname, "https://github.com/#{@user.nickname}" -- if @user == current_user - %p - %strong Balance - %p - = btc_human @user.balance - %p - %small - You will get the money of your tips when the project they belong hits the threshold of - = btc_human CONFIG["min_payout"].to_d * COIN - %p - %strong E-mail - %p= @user.email - = form_for @user, html: {role: 'form'} do |f| - - if @user.errors.size > 0 - .alert.alert-danger Peercoin address is invalid. - .form-group - = f.label :bitcoin_address - = f.text_field :bitcoin_address, class: 'form-control', placeholder: 'Your peercoin address' - = f.button "Update", class: 'btn btn-default' - - if @user.balance > 0 - .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", confirm: "All the #{to_btc @user.balance} peercoins you received will be sent back to their project. Are you sure?" diff --git a/features/distribution.feature b/features/distribution.feature index 2773f67d..5da047fb 100644 --- a/features/distribution.feature +++ b/features/distribution.feature @@ -271,8 +271,9 @@ Feature: Fundraisers can distribute funds And I click on "Sign in" in the sign in form Then I should see "Peercoin address" When I fill "Peercoin address" with "mubmzLrtTgDE2WrHkiwSFKuTh2VTSXboYK" + And I fill "Current password" with "password" And I click on "Update" - Then I should see "Your information was saved" + Then I should see "You updated your account successfully" And the user with email "bob@example.com" should have "mubmzLrtTgDE2WrHkiwSFKuTh2VTSXboYK" as peercoin address diff --git a/features/step_definitions/distribution.rb b/features/step_definitions/distribution.rb index 4f70479d..468bd769 100644 --- a/features/step_definitions/distribution.rb +++ b/features/step_definitions/distribution.rb @@ -51,10 +51,11 @@ end When(/^I set my address to "(.*?)"$/) do |arg1| - visit user_path(@current_user) + step 'I go to edit my profile' fill_in "Peercoin address", with: arg1 + fill_in "Current password", with: "password" click_on "Update" - page.should have_content "Your information was saved" + page.should have_content "You updated your account successfully" end When(/^I click on the last distribution$/) do diff --git a/features/step_definitions/web.rb b/features/step_definitions/web.rb index 7c6233ec..35a93a38 100644 --- a/features/step_definitions/web.rb +++ b/features/step_definitions/web.rb @@ -131,3 +131,6 @@ User.find_by(email: arg1).confirmed?.should be_true end +When(/^I go to edit my profile$/) do + find(".edit-profile-link").click +end From a4f2b1640dee9743b9f332cbb3b1d987ef65d128 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 1 Jun 2014 16:43:48 +0200 Subject: [PATCH 233/372] Can add comment to distribution line --- app/controllers/distributions_controller.rb | 2 +- app/views/distributions/_form.html.haml | 2 ++ .../recipient_suggestions.html.haml | 2 ++ app/views/distributions/show.html.haml | 2 ++ config/locales/en.yml | 1 + .../20140601144116_add_comment_to_tip.rb | 5 +++++ db/schema.rb | 3 ++- features/distribution.feature | 20 +++++++++++++++++++ features/step_definitions/distribution.rb | 7 +++++++ 9 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 db/migrate/20140601144116_add_comment_to_tip.rb diff --git a/app/controllers/distributions_controller.rb b/app/controllers/distributions_controller.rb index 3c6c5ee7..e082eb01 100644 --- a/app/controllers/distributions_controller.rb +++ b/app/controllers/distributions_controller.rb @@ -49,7 +49,7 @@ def send_transaction private def distribution_params - params.require(:distribution).permit(tips_attributes: [:id, :coin_amount, :user_id, {user_attributes: [:email]}]) + params.require(:distribution).permit(tips_attributes: [:id, :coin_amount, :user_id, :comment, {user_attributes: [:email]}]) end def finalize_distribution diff --git a/app/views/distributions/_form.html.haml b/app/views/distributions/_form.html.haml index b4fdcadd..164758e5 100644 --- a/app/views/distributions/_form.html.haml +++ b/app/views/distributions/_form.html.haml @@ -4,6 +4,7 @@ %table.table %thead %th Recipient + %th Comment %th Amount %tbody#recipients = f.fields_for :tips do |fields| @@ -13,6 +14,7 @@ = fields.hidden_field :id = fields.hidden_field :user_id = link_to user.recipient_label, user + %td= fields.text_field :comment, hide_label: true %td= fields.text_field :coin_amount, hide_label: true .form-group %label{for: "add-recipients-input"} Add recipient(s) diff --git a/app/views/distributions/recipient_suggestions.html.haml b/app/views/distributions/recipient_suggestions.html.haml index 407c87f2..f3e04dbf 100644 --- a/app/views/distributions/recipient_suggestions.html.haml +++ b/app/views/distributions/recipient_suggestions.html.haml @@ -13,6 +13,7 @@ - else = fields.hidden_field :user_id = link_to user.recipient_label, user + %td= fields.text_field :comment, hide_label: true %td= fields.text_field :coin_amount, hide_label: true - suggestions << [user.recipient_label, recipients] - User.where('nickname LIKE ?', "%#{params[:text]}%").each do |user| @@ -22,6 +23,7 @@ %td = fields.hidden_field :user_id = link_to user.recipient_label, user + %td= fields.text_field :comment, hide_label: true %td= fields.text_field :coin_amount, hide_label: true - suggestions << [user.recipient_label, recipients] - suggestions.each_slice(4) do |slice| diff --git a/app/views/distributions/show.html.haml b/app/views/distributions/show.html.haml index 3a35d1e6..322f2371 100644 --- a/app/views/distributions/show.html.haml +++ b/app/views/distributions/show.html.haml @@ -4,6 +4,7 @@ %thead %tr %th Recipient + %th Comment %th Address %th Amount %th Percentage @@ -18,6 +19,7 @@ = link_to tip.user.recipient_label, tip.user - else Nobody + %td.comment= tip.comment %td.address - if tip.user.try(:bitcoin_address).present? = tip.user.bitcoin_address diff --git a/config/locales/en.yml b/config/locales/en.yml index 9086dddb..7099db07 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -31,3 +31,4 @@ en: detailed_description: Detailed description tip: coin_amount: Amount + description: Comment 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/schema.rb b/db/schema.rb index ae334d4f..d5018eb2 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20140601104108) do +ActiveRecord::Schema.define(version: 20140601144116) do create_table "cold_storage_transfers", force: true do |t| t.integer "project_id" @@ -109,6 +109,7 @@ t.integer "project_id" t.datetime "refunded_at" t.string "commit_message" + t.string "comment" end add_index "tips", ["distribution_id"], name: "index_tips_on_distribution_id" diff --git a/features/distribution.feature b/features/distribution.feature index 5da047fb..22c81127 100644 --- a/features/distribution.feature +++ b/features/distribution.feature @@ -292,3 +292,23 @@ Feature: Fundraisers can distribute funds | mubmzLrtTgDE2WrHkiwSFKuTh2VTSXboYK | 10.0 | And the project balance should be "490.00" + @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 type "bob" in the recipient field + And I select the recipient "bob" + And I fill the amount to "bob" with "10" + And I fill the comment to "bob" with "Great idea" + And I click on "Save" + + Then I should see these distribution lines: + | recipient | address | comment | amount | percentage | + | bob | mxWfjaZJTNN5QKeZZYQ5HW3vgALFBsnuG1 | Great idea | 10 | 100 | diff --git a/features/step_definitions/distribution.rb b/features/step_definitions/distribution.rb index 468bd769..89aeb546 100644 --- a/features/step_definitions/distribution.rb +++ b/features/step_definitions/distribution.rb @@ -27,6 +27,12 @@ end end +Given(/^I fill the comment to "(.*?)" with "(.*?)"$/) do |arg1, arg2| + within "#recipients tr", text: /^#{Regexp.escape arg1}/ do + fill_in "Comment", with: arg2 + end +end + Then(/^I should see these distribution lines:$/) do |table| table.hashes.each do |row| begin @@ -39,6 +45,7 @@ tr.find(".address").text.should eq(row["address"]) tr.find(".amount").should have_content(row["amount"]) tr.find(".percentage").should have_content(row["percentage"]) + tr.find(".comment").should have_content(row["comment"]) if row["comment"] end end From 425cd182e83d88bb2c4675fee96f48ecc3f24841 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 1 Jun 2014 16:54:37 +0200 Subject: [PATCH 234/372] Added paper trail to projects, distributions and tips --- Gemfile | 1 + Gemfile.lock | 4 ++++ app/models/distribution.rb | 2 ++ app/models/project.rb | 2 ++ app/models/tip.rb | 2 ++ db/migrate/20140601145337_create_versions.rb | 13 +++++++++++++ ...0140601145338_add_object_changes_to_versions.rb | 5 +++++ db/schema.rb | 14 +++++++++++++- 8 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 db/migrate/20140601145337_create_versions.rb create mode 100644 db/migrate/20140601145338_add_object_changes_to_versions.rb diff --git a/Gemfile b/Gemfile index 536d3605..52654d28 100644 --- a/Gemfile +++ b/Gemfile @@ -76,6 +76,7 @@ gem 'html_pipeline_rails' gem 'rails_autolink' gem 'redcarpet' gem 'sanitize' +gem 'paper_trail', '~> 3.0.2' group :test do gem 'cucumber-rails', :require => false diff --git a/Gemfile.lock b/Gemfile.lock index d7e12528..55af592f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -206,6 +206,9 @@ GEM oauth2 (~> 0.8.0) omniauth (~> 1.0) orm_adapter (0.5.0) + paper_trail (3.0.2) + activerecord (>= 3.0, < 5.0) + activesupport (>= 3.0, < 5.0) pg (0.17.1) poltergeist (1.5.1) capybara (~> 2.1) @@ -340,6 +343,7 @@ DEPENDENCIES octokit omniauth omniauth-github! + paper_trail (~> 3.0.2) pg poltergeist quiet_assets diff --git a/app/models/distribution.rb b/app/models/distribution.rb index 8409069d..9bde7263 100644 --- a/app/models/distribution.rb +++ b/app/models/distribution.rb @@ -3,6 +3,8 @@ class Distribution < ActiveRecord::Base has_many :tips accepts_nested_attributes_for :tips + has_paper_trail + scope :to_send, -> { where(txid: nil) } scope :error, -> { where(is_error: true) } diff --git a/app/models/project.rb b/app/models/project.rb index 22a1db2b..b28f3608 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -10,6 +10,8 @@ class Project < ActiveRecord::Base has_one :tipping_policies_text, inverse_of: :project accepts_nested_attributes_for :tipping_policies_text + has_paper_trail + validates :name, presence: true before_validation :strip_full_name diff --git a/app/models/tip.rb b/app/models/tip.rb index 206d6da7..e1413975 100644 --- a/app/models/tip.rb +++ b/app/models/tip.rb @@ -4,6 +4,8 @@ class Tip < ActiveRecord::Base belongs_to :distribution belongs_to :project, inverse_of: :tips + has_paper_trail + validates :amount, numericality: {greater_or_equal_than: 0, allow_nil: true} scope :not_sent, -> { where(distribution_id: nil) } 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/schema.rb b/db/schema.rb index d5018eb2..d056eed4 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20140601144116) do +ActiveRecord::Schema.define(version: 20140601145338) do create_table "cold_storage_transfers", force: true do |t| t.integer "project_id" @@ -147,4 +147,16 @@ 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 + 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 From e103c80f2596c777269fe5ccfd78bfcfb2bbfd9e Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 1 Jun 2014 17:57:15 +0200 Subject: [PATCH 235/372] Refactored tip form --- app/views/distributions/_form.html.haml | 11 ++-------- app/views/distributions/_tip_form.html.haml | 14 +++++++++++++ .../recipient_suggestions.html.haml | 21 ++----------------- 3 files changed, 18 insertions(+), 28 deletions(-) create mode 100644 app/views/distributions/_tip_form.html.haml diff --git a/app/views/distributions/_form.html.haml b/app/views/distributions/_form.html.haml index 164758e5..add8513a 100644 --- a/app/views/distributions/_form.html.haml +++ b/app/views/distributions/_form.html.haml @@ -7,15 +7,8 @@ %th Comment %th Amount %tbody#recipients - = f.fields_for :tips do |fields| - - user = fields.object.user - %tr - %td - = fields.hidden_field :id - = fields.hidden_field :user_id - = link_to user.recipient_label, user - %td= fields.text_field :comment, hide_label: true - %td= fields.text_field :coin_amount, hide_label: true + - f.object.tips.each do |tip| + = render "tip_form", tip: tip, form: f .form-group %label{for: "add-recipients-input"} Add recipient(s) %input.form-control#add-recipients-input{type: "text", autocomplete: "off", data: {suggestion_url: recipient_suggestions_project_distributions_path(@project)}} diff --git a/app/views/distributions/_tip_form.html.haml b/app/views/distributions/_tip_form.html.haml new file mode 100644 index 00000000..45ec0d13 --- /dev/null +++ b/app/views/distributions/_tip_form.html.haml @@ -0,0 +1,14 @@ += form.fields_for :tips, tip, child_index: rand(1<<160) do |fields| + - user = tip.user + %tr + %td + = 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.recipient_label + - else + = fields.hidden_field :user_id + = link_to user.recipient_label, user + %td= fields.text_field :comment, hide_label: true + %td= fields.text_field :coin_amount, hide_label: true diff --git a/app/views/distributions/recipient_suggestions.html.haml b/app/views/distributions/recipient_suggestions.html.haml index f3e04dbf..5bbf6403 100644 --- a/app/views/distributions/recipient_suggestions.html.haml +++ b/app/views/distributions/recipient_suggestions.html.haml @@ -3,28 +3,11 @@ - if params[:text] =~ Devise::email_regexp - user = User.where(email: params[:text]).first_or_initialize - recipients = capture_haml do - = f.fields_for :tips, Tip.new(user: user), child_index: rand(1<<160) do |fields| - %tr - %td - - if user.new_record? - = fields.fields_for :user do |user_fields| - = user_fields.hidden_field :email - = user.recipient_label - - else - = fields.hidden_field :user_id - = link_to user.recipient_label, user - %td= fields.text_field :comment, hide_label: true - %td= fields.text_field :coin_amount, hide_label: true + = render "tip_form", tip: Tip.new(user: user), form: f - suggestions << [user.recipient_label, recipients] - User.where('nickname LIKE ?', "%#{params[:text]}%").each do |user| - recipients = capture_haml do - = f.fields_for :tips, Tip.new(user_id: user.id), child_index: rand(1<<160) do |fields| - %tr - %td - = fields.hidden_field :user_id - = link_to user.recipient_label, user - %td= fields.text_field :comment, hide_label: true - %td= fields.text_field :coin_amount, hide_label: true + = render "tip_form", tip: Tip.new(user_id: user.id), form: f - suggestions << [user.recipient_label, recipients] - suggestions.each_slice(4) do |slice| .row From 6336b52ec05b8018b29409b5ec62353885d352be Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 1 Jun 2014 18:01:00 +0200 Subject: [PATCH 236/372] Can remove distribution line --- app/controllers/distributions_controller.rb | 2 +- app/models/distribution.rb | 2 +- app/views/distributions/_tip_form.html.haml | 1 + config/locales/en.yml | 1 + features/distribution.feature | 29 +++++++++++++++++++++ features/step_definitions/distribution.rb | 7 +++++ 6 files changed, 40 insertions(+), 2 deletions(-) diff --git a/app/controllers/distributions_controller.rb b/app/controllers/distributions_controller.rb index e082eb01..60093eb0 100644 --- a/app/controllers/distributions_controller.rb +++ b/app/controllers/distributions_controller.rb @@ -49,7 +49,7 @@ def send_transaction private def distribution_params - params.require(:distribution).permit(tips_attributes: [:id, :coin_amount, :user_id, :comment, {user_attributes: [:email]}]) + params.require(:distribution).permit(tips_attributes: [:id, :coin_amount, :user_id, :comment, :_destroy, {user_attributes: [:email]}]) end def finalize_distribution diff --git a/app/models/distribution.rb b/app/models/distribution.rb index 9bde7263..5921a469 100644 --- a/app/models/distribution.rb +++ b/app/models/distribution.rb @@ -1,7 +1,7 @@ class Distribution < ActiveRecord::Base belongs_to :project, inverse_of: :distributions has_many :tips - accepts_nested_attributes_for :tips + accepts_nested_attributes_for :tips, allow_destroy: true has_paper_trail diff --git a/app/views/distributions/_tip_form.html.haml b/app/views/distributions/_tip_form.html.haml index 45ec0d13..5900ffd1 100644 --- a/app/views/distributions/_tip_form.html.haml +++ b/app/views/distributions/_tip_form.html.haml @@ -12,3 +12,4 @@ = link_to user.recipient_label, user %td= fields.text_field :comment, hide_label: true %td= fields.text_field :coin_amount, hide_label: true + %td= fields.check_box :_destroy diff --git a/config/locales/en.yml b/config/locales/en.yml index 7099db07..5eb77741 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -32,3 +32,4 @@ en: tip: coin_amount: Amount description: Comment + _destroy: Remove diff --git a/features/distribution.feature b/features/distribution.feature index 22c81127..6775fddc 100644 --- a/features/distribution.feature +++ b/features/distribution.feature @@ -312,3 +312,32 @@ Feature: Fundraisers can distribute funds Then I should see these distribution lines: | recipient | address | comment | amount | percentage | | bob | mxWfjaZJTNN5QKeZZYQ5HW3vgALFBsnuG1 | Great idea | 10 | 100 | + + @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 type "bob" in the recipient field + And I select the recipient "bob" + And I type "carol" in the recipient field + And I select the recipient "carol" + And I remove the recipient "bob" + And I click on "Save" + + Then I should see these distribution lines: + | recipient | address | comment | amount | percentage | + | carol | | | | | + + When I click on "Edit the distribution" + And I remove the recipient "carol" + And I click on "Save" + Then I should see these distribution lines: + | recipient | address | comment | amount | percentage | diff --git a/features/step_definitions/distribution.rb b/features/step_definitions/distribution.rb index 89aeb546..18b01473 100644 --- a/features/step_definitions/distribution.rb +++ b/features/step_definitions/distribution.rb @@ -33,6 +33,12 @@ end end +When(/^I remove the recipient "(.*?)"$/) do |arg1| + within "#recipients tr", text: /^#{Regexp.escape arg1}/ do + check "Remove" + end +end + Then(/^I should see these distribution lines:$/) do |table| table.hashes.each do |row| begin @@ -47,6 +53,7 @@ tr.find(".percentage").should have_content(row["percentage"]) tr.find(".comment").should have_content(row["comment"]) if row["comment"] end + table.hashes.size.should eq(all("#distribution-show-page tbody tr").size) end When(/^the tipper is started$/) do From 100a5ff80e420dd040ceaa0a67fc0fb9ec2eb3bd Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 1 Jun 2014 18:07:38 +0200 Subject: [PATCH 237/372] Can set undecided amount --- app/models/tip.rb | 2 +- app/views/distributions/show.html.haml | 8 ++++++-- features/distribution.feature | 19 +++++++++++++++++++ features/step_definitions/distribution.rb | 8 ++++---- 4 files changed, 30 insertions(+), 7 deletions(-) diff --git a/app/models/tip.rb b/app/models/tip.rb index e1413975..a2b50eee 100644 --- a/app/models/tip.rb +++ b/app/models/tip.rb @@ -109,7 +109,7 @@ def coin_amount end def coin_amount=(coin_amount) - if coin_amount + if coin_amount.present? self.amount = (coin_amount.to_f * COIN).round else self.amount = nil diff --git a/app/views/distributions/show.html.haml b/app/views/distributions/show.html.haml index 322f2371..ef692365 100644 --- a/app/views/distributions/show.html.haml +++ b/app/views/distributions/show.html.haml @@ -26,8 +26,12 @@ - else - if tip.user.try(:email).present? = button_to "Send email request to provide an address", send_email_address_request_user_path(tip_id: tip.id, return_url: request.url), class: "btn btn-default" - %td.amount= btc_human tip.amount - %td.percentage= number_to_percentage(tip.amount.to_f * 100 / total, precision: 1) + %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 tip.amount and total > 0 %p %strong diff --git a/features/distribution.feature b/features/distribution.feature index 6775fddc..54a30da1 100644 --- a/features/distribution.feature +++ b/features/distribution.feature @@ -341,3 +341,22 @@ Feature: Fundraisers can distribute funds And I click on "Save" Then I should see these distribution lines: | recipient | address | comment | amount | percentage | + + @javascript + Scenario: Create distribution line without an amount + Given a GitHub user "bob" + + 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 type "bob" in the recipient field + And I select the recipient "bob" + And I click on "Save" + + Then I should see these distribution lines: + | recipient | amount | percentage | + | bob | Undecided | | diff --git a/features/step_definitions/distribution.rb b/features/step_definitions/distribution.rb index 18b01473..5ce0b686 100644 --- a/features/step_definitions/distribution.rb +++ b/features/step_definitions/distribution.rb @@ -48,10 +48,10 @@ raise end tr.find(".recipient").text.should eq(row["recipient"]) - tr.find(".address").text.should eq(row["address"]) - tr.find(".amount").should have_content(row["amount"]) - tr.find(".percentage").should have_content(row["percentage"]) - tr.find(".comment").should have_content(row["comment"]) if row["comment"] + tr.find(".address").text.should eq(row["address"]) if row["address"] + tr.find(".amount").text.should eq(row["amount"]) if row["amount"] + tr.find(".percentage").text.should eq(row["percentage"]) if row["percentage"] + tr.find(".comment").text.should eq(row["comment"]) if row["comment"] end table.hashes.size.should eq(all("#distribution-show-page tbody tr").size) end From bef14aaa7a47a1fe8612839eb04e99e0b320400c Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 1 Jun 2014 18:59:19 +0200 Subject: [PATCH 238/372] Cannot send undecided tips --- app/models/distribution.rb | 2 +- features/distribution.feature | 9 +++++---- features/step_definitions/distribution.rb | 18 ++++++++++++++++-- features/step_definitions/web.rb | 4 ++++ 4 files changed, 26 insertions(+), 7 deletions(-) diff --git a/app/models/distribution.rb b/app/models/distribution.rb index 5921a469..3fee1131 100644 --- a/app/models/distribution.rb +++ b/app/models/distribution.rb @@ -49,6 +49,6 @@ def all_addresses_known? end def can_be_sent? - !sent? and all_addresses_known? + !sent? and all_addresses_known? and tips.any? and tips.all?(&:decided?) end end diff --git a/features/distribution.feature b/features/distribution.feature index 54a30da1..80ccf959 100644 --- a/features/distribution.feature +++ b/features/distribution.feature @@ -333,18 +333,18 @@ Feature: Fundraisers can distribute funds And I click on "Save" Then I should see these distribution lines: - | recipient | address | comment | amount | percentage | - | carol | | | | | + | recipient | + | carol | When I click on "Edit the distribution" And I remove the recipient "carol" And I click on "Save" Then I should see these distribution lines: - | recipient | address | comment | amount | percentage | + | recipient | @javascript Scenario: Create distribution line without an amount - Given a GitHub user "bob" + Given a GitHub user "bob" who has set his address to "mxWfjaZJTNN5QKeZZYQ5HW3vgALFBsnuG1" Given a project managed by "alice" And our fee is "0" @@ -360,3 +360,4 @@ Feature: Fundraisers can distribute funds Then I should see these distribution lines: | recipient | amount | percentage | | bob | Undecided | | + And I should not see the button "Send the transaction" diff --git a/features/step_definitions/distribution.rb b/features/step_definitions/distribution.rb index 5ce0b686..7f4a0c09 100644 --- a/features/step_definitions/distribution.rb +++ b/features/step_definitions/distribution.rb @@ -49,8 +49,22 @@ end tr.find(".recipient").text.should eq(row["recipient"]) tr.find(".address").text.should eq(row["address"]) if row["address"] - tr.find(".amount").text.should eq(row["amount"]) if row["amount"] - tr.find(".percentage").text.should eq(row["percentage"]) if row["percentage"] + if row["amount"] + text = tr.find(".amount").text + if row["amount"] =~ /\A[0-9.]+\Z/ + text.to_d.should eq(row["amount"].to_d) + else + text.should eq(row["amount"]) + end + end + if row["percentage"] + text = tr.find(".percentage").text + if row["percentage"] =~ /\A[0-9.]+\Z/ + text.to_d.should eq(row["percentage"].to_d) + else + text.should eq(row["percentage"]) + end + end tr.find(".comment").text.should eq(row["comment"]) if row["comment"] end table.hashes.size.should eq(all("#distribution-show-page tbody tr").size) diff --git a/features/step_definitions/web.rb b/features/step_definitions/web.rb index 35a93a38..7d62526b 100644 --- a/features/step_definitions/web.rb +++ b/features/step_definitions/web.rb @@ -83,6 +83,10 @@ page.should have_no_content(arg1) end +Then(/^I should not see the button "(.*?)"$/) do |arg1| + page.should have_no_button(arg1) +end + Given(/^I fill "(.*?)" with:$/) do |arg1, string| fill_in arg1, with: string end From 00ba120781ee521ef555aebf59d4ae01bd6b9452 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Mon, 2 Jun 2014 08:02:20 +0200 Subject: [PATCH 239/372] No block on details button --- app/views/projects/index.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/index.html.haml b/app/views/projects/index.html.haml index e0dbfef2..ace480e6 100644 --- a/app/views/projects/index.html.haml +++ b/app/views/projects/index.html.haml @@ -21,5 +21,5 @@ %td= project.description %td= project.watchers_count %td= btc_human project.available_amount_cache - %td= link_to 'Details', project, class: 'btn btn-default btn-block' + %td= link_to 'Details', project, class: 'btn btn-default' = paginate @projects From 92e8c264a1b7c6bba6007284c7a7c69e98f5a9c6 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Mon, 2 Jun 2014 08:05:11 +0200 Subject: [PATCH 240/372] Project list cleanup --- app/views/projects/index.html.haml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/views/projects/index.html.haml b/app/views/projects/index.html.haml index ace480e6..20f356b9 100644 --- a/app/views/projects/index.html.haml +++ b/app/views/projects/index.html.haml @@ -10,8 +10,7 @@ %tr %th Name %th Description - %th Watchers - %th Balance + %th Funds %th %tbody - @projects.each do |project| @@ -19,7 +18,6 @@ %td %strong= link_to project.name, project %td= project.description - %td= project.watchers_count %td= btc_human project.available_amount_cache %td= link_to 'Details', project, class: 'btn btn-default' = paginate @projects From 67ced859938b19e5894785392cd127e8df429d42 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Mon, 2 Jun 2014 20:22:14 +0200 Subject: [PATCH 241/372] Added button to reward commit authors --- app/controllers/distributions_controller.rb | 2 +- app/helpers/application_helper.rb | 8 ++- app/models/commit.rb | 3 + app/models/project.rb | 21 ++++++- app/models/tip.rb | 1 + app/views/distributions/_form.html.haml | 3 +- app/views/distributions/_origin.html.haml | 5 ++ app/views/distributions/_tip_form.html.haml | 14 +++-- .../recipient_suggestions.html.haml | 23 ++++++++ app/views/distributions/show.html.haml | 4 +- db/migrate/20140602210025_create_commits.rb | 13 +++++ .../20140607100342_add_origin_to_tip.rb | 5 ++ db/schema.rb | 17 +++++- features/distribute_to_commits.feature | 57 +++++++++++++++++++ features/step_definitions/distribution.rb | 27 +++++++++ .../tip_modifier_interface.rb | 4 ++ test/fixtures/commits.yml | 15 +++++ test/models/commit_test.rb | 7 +++ 18 files changed, 219 insertions(+), 10 deletions(-) create mode 100644 app/models/commit.rb create mode 100644 app/views/distributions/_origin.html.haml create mode 100644 db/migrate/20140602210025_create_commits.rb create mode 100644 db/migrate/20140607100342_add_origin_to_tip.rb create mode 100644 features/distribute_to_commits.feature create mode 100644 test/fixtures/commits.yml create mode 100644 test/models/commit_test.rb diff --git a/app/controllers/distributions_controller.rb b/app/controllers/distributions_controller.rb index 60093eb0..a563d42f 100644 --- a/app/controllers/distributions_controller.rb +++ b/app/controllers/distributions_controller.rb @@ -49,7 +49,7 @@ def send_transaction private def distribution_params - params.require(:distribution).permit(tips_attributes: [:id, :coin_amount, :user_id, :comment, :_destroy, {user_attributes: [:email]}]) + params.require(:distribution).permit(tips_attributes: [:id, :coin_amount, :user_id, :comment, :origin_type, :origin_id, :_destroy, {user_attributes: [:email, :nickname]}]) end def finalize_distribution diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 47b1ed98..58925c96 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -42,8 +42,12 @@ def address_url(address, explorer = address_explorers.first) end end + def truncate_commit(sha1) + truncate(sha1, length: 10, omission: "") + end + def commit_tag(sha1) - content_tag(:span, truncate(sha1, length: 10, omission: ""), class: "commit-sha") + content_tag(:span, truncate_commit(sha1), class: "commit-sha") end def render_flash_message @@ -60,6 +64,8 @@ def render_flash_message end def render_markdown(source) + return nil unless source + markdown = Redcarpet::Markdown.new(Redcarpet::Render::HTML.new(safe_links_only: true, filter_html: true)) html = markdown.render(source) clean = Sanitize.clean(html, Sanitize::Config::RELAXED) diff --git a/app/models/commit.rb b/app/models/commit.rb new file mode 100644 index 00000000..4cb67e0e --- /dev/null +++ b/app/models/commit.rb @@ -0,0 +1,3 @@ +class Commit < ActiveRecord::Base + belongs_to :project +end diff --git a/app/models/project.rb b/app/models/project.rb index b28f3608..e8908a2d 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -10,6 +10,8 @@ class Project < ActiveRecord::Base has_one :tipping_policies_text, inverse_of: :project accepts_nested_attributes_for :tipping_policies_text + has_many :commits + has_paper_trail validates :name, presence: true @@ -54,7 +56,20 @@ def get_commits def tip_commits return unless self.deposits.any? - get_commits.each do |commit| + 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 + + commits.each do |commit| # Filter merge request next if commit.commit.message =~ /^(Merge\s|auto\smerge)/ # Filter fake emails @@ -182,6 +197,10 @@ def strip_full_name end end + def github? + full_name.present? + end + def auto_tip_commits !hold_tips end diff --git a/app/models/tip.rb b/app/models/tip.rb index a2b50eee..12cbbb1a 100644 --- a/app/models/tip.rb +++ b/app/models/tip.rb @@ -3,6 +3,7 @@ class Tip < ActiveRecord::Base accepts_nested_attributes_for :user belongs_to :distribution belongs_to :project, inverse_of: :tips + belongs_to :origin, polymorphic: true has_paper_trail diff --git a/app/views/distributions/_form.html.haml b/app/views/distributions/_form.html.haml index add8513a..6eda6ffd 100644 --- a/app/views/distributions/_form.html.haml +++ b/app/views/distributions/_form.html.haml @@ -1,9 +1,10 @@ .row .col-md-12 - = bootstrap_form_for [@project, @distribution] do |f| + = bootstrap_form_for [@project, @distribution], html: {id: "distribution-form"} do |f| %table.table %thead %th Recipient + %th Origin %th Comment %th Amount %tbody#recipients diff --git a/app/views/distributions/_origin.html.haml b/app/views/distributions/_origin.html.haml new file mode 100644 index 00000000..71849090 --- /dev/null +++ b/app/views/distributions/_origin.html.haml @@ -0,0 +1,5 @@ +- case tip.origin +- when Commit + - commit = tip.origin + Commit #{link_to truncate_commit(commit.sha), "https://github.com/#{commit.project.full_name}/commit/#{commit.sha}"}: + %pre= commit.message diff --git a/app/views/distributions/_tip_form.html.haml b/app/views/distributions/_tip_form.html.haml index 5900ffd1..432a1af8 100644 --- a/app/views/distributions/_tip_form.html.haml +++ b/app/views/distributions/_tip_form.html.haml @@ -1,15 +1,21 @@ = form.fields_for :tips, tip, child_index: rand(1<<160) do |fields| - user = tip.user + - raise "An user is required" unless user %tr - %td + %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= fields.text_field :comment, hide_label: true - %td= fields.text_field :coin_amount, hide_label: true - %td= fields.check_box :_destroy + %td.origin + = fields.hidden_field :origin_type + = fields.hidden_field :origin_id + = render "origin", tip: fields.object + %td.comment= fields.text_field :comment, hide_label: true + %td.amount= fields.text_field :coin_amount, hide_label: true + %td.remove= fields.check_box :_destroy diff --git a/app/views/distributions/recipient_suggestions.html.haml b/app/views/distributions/recipient_suggestions.html.haml index 5bbf6403..e35dc197 100644 --- a/app/views/distributions/recipient_suggestions.html.haml +++ b/app/views/distributions/recipient_suggestions.html.haml @@ -9,6 +9,29 @@ - recipients = capture_haml do = render "tip_form", tip: Tip.new(user_id: user.id), form: f - suggestions << [user.recipient_label, recipients] + - if @project.github? + - recipients = capture_haml do + - @project.commits.each do |commit| + - next if Tip.where(origin: commit).any? + - user = User.where(nickname: commit.username).first + - if user + - tip = Tip.new(user_id: user.id) + - else + - tip = Tip.new(user_attributes: {nickname: commit.username, email: commit.email}) + - tip.origin = commit + = render "tip_form", tip: tip, form: f + - suggestions << ["Authors of non rewarded commits", recipients] + - @project.commits.each do |commit| + - if commit.sha =~ /\A#{Regexp.escape params[:text]}/ + - recipients = capture_haml do + - user = User.where(nickname: commit.username).first + - if user + - tip = Tip.new(user_id: user.id) + - else + - tip = Tip.new(user_attributes: {nickname: commit.username, email: commit.email}) + - tip.origin = commit + = render "tip_form", tip: tip, form: f + - suggestions << ["Commit #{truncate_commit commit.sha}", recipients] - suggestions.each_slice(4) do |slice| .row - slice.each do |label, recipients| diff --git a/app/views/distributions/show.html.haml b/app/views/distributions/show.html.haml index ef692365..2dd5a0f2 100644 --- a/app/views/distributions/show.html.haml +++ b/app/views/distributions/show.html.haml @@ -4,6 +4,7 @@ %thead %tr %th Recipient + %th Origin %th Comment %th Address %th Amount @@ -19,7 +20,8 @@ = link_to tip.user.recipient_label, tip.user - else Nobody - %td.comment= tip.comment + %td.origin= render "origin", tip: tip + %td.comment= render_markdown tip.comment %td.address - if tip.user.try(:bitcoin_address).present? = tip.user.bitcoin_address 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/schema.rb b/db/schema.rb index d056eed4..8ea22987 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20140601145338) do +ActiveRecord::Schema.define(version: 20140607100342) do create_table "cold_storage_transfers", force: true do |t| t.integer "project_id" @@ -35,6 +35,18 @@ add_index "collaborators", ["project_id"], name: "index_collaborators_on_project_id" + create_table "commits", force: true do |t| + t.integer "project_id" + t.string "sha" + t.text "message" + t.string "username" + t.string "email" + t.datetime "created_at" + t.datetime "updated_at" + end + + add_index "commits", ["project_id"], name: "index_commits_on_project_id" + create_table "deposits", force: true do |t| t.integer "project_id" t.string "txid" @@ -110,9 +122,12 @@ t.datetime "refunded_at" t.string "commit_message" t.string "comment" + t.integer "origin_id" + t.string "origin_type" end add_index "tips", ["distribution_id"], name: "index_tips_on_distribution_id" + add_index "tips", ["origin_id", "origin_type"], name: "index_tips_on_origin_id_and_origin_type" add_index "tips", ["project_id"], name: "index_tips_on_project_id" add_index "tips", ["user_id"], name: "index_tips_on_user_id" diff --git a/features/distribute_to_commits.feature b/features/distribute_to_commits.feature new file mode 100644 index 00000000..ba600a9f --- /dev/null +++ b/features/distribute_to_commits.feature @@ -0,0 +1,57 @@ +Feature: A project collaborator distribute to commit authors + 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" + + @javascript + Scenario: + 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 type "commits" in the recipient field + And I select the recipient "Authors of non rewarded commits" + Then the distribution form should have these recipients: + | recipient | origin | comment | 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 | origin | address | amount | percentage | + | yugo | Commit BBB: Tiny change | | 0.5 | 100 | + And I should see "Total amount: 0.50 PPC" + When I click on "Send email request to provide an address" + Then I should see "Request sent" + And there should be 1 email sent + And an email should have been sent to "yugo@example.com" + + When the new commits are read + + When I go to the project page + And I click on "New distribution" + And I type "commits" in the recipient field + And I select the recipient "Authors of non rewarded commits" + Then the distribution form should have these recipients: + | recipient | origin | comment | amount | + | gaal | Commit CCC: Some changes | | | diff --git a/features/step_definitions/distribution.rb b/features/step_definitions/distribution.rb index 7f4a0c09..d1a5b78e 100644 --- a/features/step_definitions/distribution.rb +++ b/features/step_definitions/distribution.rb @@ -49,6 +49,7 @@ end tr.find(".recipient").text.should eq(row["recipient"]) tr.find(".address").text.should eq(row["address"]) if row["address"] + tr.find(".origin").text.should eq(row["origin"]) if row["origin"] if row["amount"] text = tr.find(".amount").text if row["amount"] =~ /\A[0-9.]+\Z/ @@ -70,6 +71,32 @@ table.hashes.size.should 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 + puts "Rows: " + all("#distribution-form #recipients tr").map(&:text).inspect + raise + end + tr.find(".recipient").text.should eq(row["recipient"]) + tr.find(".origin").text.should eq(row["origin"]) if row["origin"] + if row["amount"] + text = tr.find_field("Amount").value + if row["amount"] =~ /\A[0-9.]+\Z/ + text.to_d.should eq(row["amount"].to_d) + else + text.should eq(row["amount"]) + end + end + if row["comment"] + text = tr.find_field("Comment").value + text.should eq(row["comment"]) + end + end + all("#distribution-form tbody tr").size.should eq(table.hashes.size) +end + When(/^the tipper is started$/) do BitcoinTipper.work end diff --git a/features/step_definitions/tip_modifier_interface.rb b/features/step_definitions/tip_modifier_interface.rb index 00313066..bf3e6784 100644 --- a/features/step_definitions/tip_modifier_interface.rb +++ b/features/step_definitions/tip_modifier_interface.rb @@ -87,3 +87,7 @@ Then(/^the project should have (\d+) undecided tips$/) do |arg1| @project.tips.undecided.size.should eq(arg1.to_i) end + +Then(/^there should be (\d+) tip$/) do |arg1| + @project.reload.tips.size.should eq(arg1.to_i) +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/models/commit_test.rb b/test/models/commit_test.rb new file mode 100644 index 00000000..2424af32 --- /dev/null +++ b/test/models/commit_test.rb @@ -0,0 +1,7 @@ +require 'test_helper' + +class CommitTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end From 608980f7bd7b9fde5366180142f050d7baaf27a1 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sat, 7 Jun 2014 19:28:58 +0200 Subject: [PATCH 242/372] upgraded bootstrap forms --- Gemfile | 2 +- Gemfile.lock | 13 ++++++------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/Gemfile b/Gemfile index 52654d28..9f28c809 100644 --- a/Gemfile +++ b/Gemfile @@ -71,7 +71,7 @@ gem 'whenever' gem 'rqrcode-rails3' gem 'exception_notification' gem 'rack-canonical-host' -gem 'bootstrap_form', github: 'sigmike/rails-bootstrap-forms', branch: 'removed_for_on_radio_label' +gem 'bootstrap_form', github: 'bootstrap-ruby/rails-bootstrap-forms' gem 'html_pipeline_rails' gem 'rails_autolink' gem 'redcarpet' diff --git a/Gemfile.lock b/Gemfile.lock index 55af592f..ed3045ee 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -7,6 +7,12 @@ GIT omniauth (~> 1.0) omniauth-oauth2 (~> 1.1) +GIT + remote: git://github.com/bootstrap-ruby/rails-bootstrap-forms.git + revision: 5048f09a7a67089f69597418dd3ad51a9d430182 + specs: + bootstrap_form (2.1.1) + GIT remote: git://github.com/capistrano/rvm.git revision: 19e8d15ae3d705499c610370f159d523bbedbd94 @@ -26,13 +32,6 @@ GIT rails (>= 3.1) railties (>= 3.1) -GIT - remote: git://github.com/sigmike/rails-bootstrap-forms.git - revision: a1c8420ab999df56b13d3de8a097528b77a02217 - branch: removed_for_on_radio_label - specs: - bootstrap_form (2.0.1) - GIT remote: git://github.com/stouset/twitter_bootstrap_form_for.git revision: 830dbfd439ebb1194e1ae025100fc0e790be37cf From a5a0fdae5812127691e61ae784cce634ede578be Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sat, 7 Jun 2014 19:31:46 +0200 Subject: [PATCH 243/372] Replaced single recipient input by multiple panels --- app/assets/javascripts/distribution.js.coffee | 57 +++----- app/assets/stylesheets/distribution.css.sass | 2 - app/controllers/distributions_controller.rb | 50 ++++++- app/controllers/registrations_controller.rb | 22 +++ app/models/ability.rb | 2 +- app/models/tip.rb | 17 ++- app/models/user.rb | 21 +++ app/views/devise/registrations/edit.html.haml | 5 +- app/views/distributions/_form.html.haml | 39 +++++- .../new_recipient_form.html.haml | 6 + .../recipient_suggestions.html.haml | 39 ------ app/views/distributions/show.html.haml | 13 +- config/routes.rb | 7 +- features/distribute_to_commits.feature | 79 ++++++++++- features/distribution.feature | 131 +++++++++++++----- features/step_definitions/common.rb | 8 ++ features/step_definitions/distribution.rb | 44 +++++- features/step_definitions/github.rb | 5 + features/support/octokit_mock.rb | 12 ++ 19 files changed, 409 insertions(+), 150 deletions(-) create mode 100644 app/controllers/registrations_controller.rb create mode 100644 app/views/distributions/new_recipient_form.html.haml delete mode 100644 app/views/distributions/recipient_suggestions.html.haml create mode 100644 features/step_definitions/github.rb diff --git a/app/assets/javascripts/distribution.js.coffee b/app/assets/javascripts/distribution.js.coffee index dad575c3..0a2ca00d 100644 --- a/app/assets/javascripts/distribution.js.coffee +++ b/app/assets/javascripts/distribution.js.coffee @@ -1,39 +1,22 @@ $(document).on "page:change", -> - input = $("#add-recipients-input") - suggestions = $("#recipient-suggestions") - target = $("#recipients") - - timer = null - request = null - updateSuggestions = (text) -> - if timer - clearTimeout(timer) - timer = null - - if request - request.abort() - request = null - - suggestions.html("
Loading...
") - - timer = setTimeout -> - request = $.ajax - type: 'GET' - url: input.data('suggestion-url') - data: - text: text - success: (data) -> - suggestions.hide().html(data).slideDown("fast") - suggestions.find(".add-recipient-button").click -> - recipients = $(this).data("recipients") - target.append(recipients) - suggestions.html("") - input.val("") - false - error: -> - suggestions.html("An error occured.") - , 200 - - input.on "input", -> - updateSuggestions(input.val()) + root = $("#add-recipient-panels") + recipients = $("#recipients") + root.find("form").submit -> + form = $(this) + form.find(".alert").remove() + panel = form.closest(".panel") + $.ajax + type: 'GET' + url: form.attr("action") + data: form.serialize() + success: (data) -> + if data.error + form.append($("
").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 diff --git a/app/assets/stylesheets/distribution.css.sass b/app/assets/stylesheets/distribution.css.sass index 7ae6e1b4..e69de29b 100644 --- a/app/assets/stylesheets/distribution.css.sass +++ b/app/assets/stylesheets/distribution.css.sass @@ -1,2 +0,0 @@ -.add-recipient-button - margin: 6px 0 diff --git a/app/controllers/distributions_controller.rb b/app/controllers/distributions_controller.rb index a563d42f..a82a1ed0 100644 --- a/app/controllers/distributions_controller.rb +++ b/app/controllers/distributions_controller.rb @@ -9,10 +9,6 @@ def index def new end - def recipient_suggestions - render layout: nil - end - def create @distribution.project = @project finalize_distribution @@ -46,10 +42,54 @@ def send_transaction redirect_to [@project, @distribution], flash: {error: e.message} end + def new_recipient_form + @tips = [] + if params[:user] and params[:user][:nickname].present? + user = User.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][:email].present? + user = User.where(email: params[:user][:email]).first_or_initialize + if user.new_record? + raise "Invalid email address" unless user.email =~ Devise::email_regexp + user.skip_confirmation_notification! + user.save! + end + @tips << Tip.new(user: user) + elsif params[:not_rewarded_commits] + @project.commits.each do |commit| + next if Tip.where(origin: commit).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 => e + render json: {error: e.message} + end + private def distribution_params - params.require(:distribution).permit(tips_attributes: [:id, :coin_amount, :user_id, :comment, :origin_type, :origin_id, :_destroy, {user_attributes: [:email, :nickname]}]) + if params[:distribution] + params.require(:distribution).permit(tips_attributes: [:id, :coin_amount, :user_id, :comment, :origin_type, :origin_id, :_destroy]) + else + {} + end end def finalize_distribution diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb new file mode 100644 index 00000000..4c564422 --- /dev/null +++ b/app/controllers/registrations_controller.rb @@ -0,0 +1,22 @@ +class RegistrationsController < Devise::RegistrationsController + def update + @user = User.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 + sign_in @user, :bypass => true + redirect_to after_update_path_for(@user) + else + render "edit" + end + end +end diff --git a/app/models/ability.rb b/app/models/ability.rb index a946097f..25a9d69e 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -9,7 +9,7 @@ def initialize(user) can [:update, :decide_tip_amounts], Project, collaborators: {login: user.nickname} can [:create], Project can [:create], Distribution, project: {collaborators: {login: user.nickname}} - can [:update, :recipient_suggestions], Distribution, project: {collaborators: {login: user.nickname}}, txid: nil, sent_at: nil + can [:update, :new_recipient_form], Distribution, project: {collaborators: {login: user.nickname}}, txid: nil, sent_at: nil can [:send_transaction], Distribution do |distribution| distribution.can_be_sent? end diff --git a/app/models/tip.rb b/app/models/tip.rb index 12cbbb1a..a5eb0fe0 100644 --- a/app/models/tip.rb +++ b/app/models/tip.rb @@ -1,6 +1,5 @@ class Tip < ActiveRecord::Base belongs_to :user - accepts_nested_attributes_for :user belongs_to :distribution belongs_to :project, inverse_of: :tips belongs_to :origin, polymorphic: true @@ -116,4 +115,20 @@ def coin_amount=(coin_amount) self.amount = nil end end + + def self.build_from_commit(commit) + if commit.username.present? + user = User.where(nickname: commit.username).first_or_initialize(email: commit.email) + elsif commit.email =~ Devise::email_regexp + user = User.where(email: commit.email).first_or_initialize + else + return nil + end + if user.new_record? + raise "Invalid email address" unless user.email =~ Devise::email_regexp + user.skip_confirmation_notification! + user.save! + end + new(user_id: user.id, origin: commit) + end end diff --git a/app/models/user.rb b/app/models/user.rb index 8293a4b9..5c57dcc4 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -8,6 +8,7 @@ class User < ActiveRecord::Base devise :confirmable, reconfirmable: true validates :bitcoin_address, bitcoin_address: true + validates :password, confirmation: true has_many :tips @@ -36,10 +37,18 @@ def self.update_cache end end + 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 @@ -54,6 +63,18 @@ def recipient_label end end + 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 + private def generate_login_token! diff --git a/app/views/devise/registrations/edit.html.haml b/app/views/devise/registrations/edit.html.haml index c04f94ee..c148112e 100644 --- a/app/views/devise/registrations/edit.html.haml +++ b/app/views/devise/registrations/edit.html.haml @@ -14,8 +14,9 @@ = 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' - %div - = f.password_field :current_password, help: "(we need your current password to confirm your changes)" + - 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 diff --git a/app/views/distributions/_form.html.haml b/app/views/distributions/_form.html.haml index 6eda6ffd..d73b102c 100644 --- a/app/views/distributions/_form.html.haml +++ b/app/views/distributions/_form.html.haml @@ -10,9 +10,40 @@ %tbody#recipients - f.object.tips.each do |tip| = render "tip_form", tip: tip, form: f - .form-group - %label{for: "add-recipients-input"} Add recipient(s) - %input.form-control#add-recipients-input{type: "text", autocomplete: "off", data: {suggestion_url: recipient_suggestions_project_distributions_path(@project)}} - #recipient-suggestions .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 email address + .panel-body + .input-group + = bootstrap_form_for User.new, url: new_recipient_form_project_distributions_path(@project) do |f| + = f.email_field :email, hide_label: true, append: content_tag(:button, "Add", class: "btn btn-default add-recipient-button") + .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") + .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") + .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/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/recipient_suggestions.html.haml b/app/views/distributions/recipient_suggestions.html.haml deleted file mode 100644 index e35dc197..00000000 --- a/app/views/distributions/recipient_suggestions.html.haml +++ /dev/null @@ -1,39 +0,0 @@ -= bootstrap_form_for Distribution.new do |f| - - suggestions = [] - - if params[:text] =~ Devise::email_regexp - - user = User.where(email: params[:text]).first_or_initialize - - recipients = capture_haml do - = render "tip_form", tip: Tip.new(user: user), form: f - - suggestions << [user.recipient_label, recipients] - - User.where('nickname LIKE ?', "%#{params[:text]}%").each do |user| - - recipients = capture_haml do - = render "tip_form", tip: Tip.new(user_id: user.id), form: f - - suggestions << [user.recipient_label, recipients] - - if @project.github? - - recipients = capture_haml do - - @project.commits.each do |commit| - - next if Tip.where(origin: commit).any? - - user = User.where(nickname: commit.username).first - - if user - - tip = Tip.new(user_id: user.id) - - else - - tip = Tip.new(user_attributes: {nickname: commit.username, email: commit.email}) - - tip.origin = commit - = render "tip_form", tip: tip, form: f - - suggestions << ["Authors of non rewarded commits", recipients] - - @project.commits.each do |commit| - - if commit.sha =~ /\A#{Regexp.escape params[:text]}/ - - recipients = capture_haml do - - user = User.where(nickname: commit.username).first - - if user - - tip = Tip.new(user_id: user.id) - - else - - tip = Tip.new(user_attributes: {nickname: commit.username, email: commit.email}) - - tip.origin = commit - = render "tip_form", tip: tip, form: f - - suggestions << ["Commit #{truncate_commit commit.sha}", recipients] - - suggestions.each_slice(4) do |slice| - .row - - slice.each do |label, recipients| - .col-md-3 - %a.btn.btn-block.btn-default.add-recipient-button{href: "#", data: {recipients: recipients}}= label diff --git a/app/views/distributions/show.html.haml b/app/views/distributions/show.html.haml index 2dd5a0f2..ca1d592e 100644 --- a/app/views/distributions/show.html.haml +++ b/app/views/distributions/show.html.haml @@ -1,5 +1,5 @@ #distribution-show-page - - total = @distribution.tips.map(&:amount).sum + - total = @distribution.tips.map(&:amount).sum if @distribution.tips.all?(&:amount) %table.table %thead %tr @@ -33,11 +33,12 @@ = btc_human tip.amount - else %em Undecided - %td.percentage= number_to_percentage(tip.amount.to_f * 100 / total, precision: 1) if tip.amount and total > 0 + %td.percentage= number_to_percentage(tip.amount.to_f * 100 / total, precision: 1) if total and tip.amount and total > 0 - %p - %strong - Total amount: #{btc_human total} + - if total + %p + %strong + Total amount: #{btc_human total} - if @distribution.is_error? %p.alert.alert-danger @@ -51,7 +52,7 @@ %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? :send_transaction, @distribution + - if total and can? :send_transaction, @distribution .distribution-action = button_to "Send the transaction", send_transaction_project_distribution_path(@project, @distribution), class: "btn btn-default", data: {confirm: "#{total.to_f / COIN} peercoins will be sent. Are you sure?"} - if can? :update, @distribution diff --git a/config/routes.rb b/config/routes.rb index 6aa0d898..aab12ce9 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -6,8 +6,9 @@ get 'faq' => 'home#faq' devise_for :users, - :controllers => { - :omniauth_callbacks => "users/omniauth_callbacks" + controllers: { + omniauth_callbacks: "users/omniauth_callbacks", + registrations: "registrations", } resources :users, :only => [:show, :update, :index] do @@ -24,7 +25,7 @@ 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 :recipient_suggestions, on: :collection + get :new_recipient_form, on: :collection post :send_transaction, on: :member end member do diff --git a/features/distribute_to_commits.feature b/features/distribute_to_commits.feature index ba600a9f..f7a7974b 100644 --- a/features/distribute_to_commits.feature +++ b/features/distribute_to_commits.feature @@ -1,5 +1,6 @@ Feature: A project collaborator distribute to commit authors - Background: + @javascript + Scenario: Given a project "a" And the project collaborators are: | seldon | @@ -13,8 +14,6 @@ Feature: A project collaborator distribute to commit authors And the message of commit "BBB" is "Tiny change" And the author of commit "CCC" is "gaal" - @javascript - Scenario: Given I'm logged in as "seldon" And I go to the project page And I click on "Edit project" @@ -26,8 +25,7 @@ Feature: A project collaborator distribute to commit authors When I go to the project page And I click on "New distribution" - And I type "commits" in the recipient field - And I select the recipient "Authors of non rewarded commits" + And I select the commit recipients "Commits not rewarded" Then the distribution form should have these recipients: | recipient | origin | comment | amount | | yugo | Commit BBB: Tiny change | | | @@ -50,8 +48,75 @@ Feature: A project collaborator distribute to commit authors When I go to the project page And I click on "New distribution" - And I type "commits" in the recipient field - And I select the recipient "Authors of non rewarded commits" + And I select the commit recipients "Commits not rewarded" Then the distribution form should have these recipients: | recipient | origin | comment | 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 | origin | comment | amount | + | yugo@example.com | Commit BBB: Tiny change | | | + + And I fill the amount to "yugo@example.com" with "0.5" + And I save the distribution + + Then I should see these distribution lines: + | recipient | origin | address | amount | percentage | + | yugo@example.com | Commit BBB: Tiny change | | 0.5 | 100 | + And I should see "Total amount: 0.50 PPC" + And there should be 0 email sent + When I click on "Send email request to provide an address" + Then I should see "Request sent" + And there should be 1 email sent + And an email should have been sent to "yugo@example.com" + + @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" + + 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 | origin | comment | amount | + | yugo | Commit 170ed604f2: Tiny change | | | + When I add the commit "1329394df" to the recipients + Then the distribution form should have these recipients: + | recipient | origin | comment | 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 | origin | address | amount | percentage | + | yugo | Commit 170ed604f2: Tiny change | | 0.5 | | + | gaal | Commit 1329394df2: Some changes | | Undecided | | diff --git a/features/distribution.feature b/features/distribution.feature index 80ccf959..760035a7 100644 --- a/features/distribution.feature +++ b/features/distribution.feature @@ -10,10 +10,9 @@ Feature: Fundraisers can distribute funds Given I'm logged in as "alice" And I go to the project page And I click on "New distribution" - And I type "bob" in the recipient field - And I select the recipient "bob" + And I add the GitHub user "bob" to the recipients And I fill the amount to "bob" with "10" - And I click on "Save" + And I save the distribution Then I should see these distribution lines: | recipient | address | amount | percentage | @@ -44,13 +43,11 @@ Feature: Fundraisers can distribute funds Given I'm logged in as "alice" And I go to the project page And I click on "New distribution" - And I type "bob" in the recipient field - And I select the recipient "bob" + 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 type "carol" in the recipient field - And I select the recipient "carol" And I fill the amount to "carol" with "13.56" - And I click on "Save" + And I save the distribution Then I should see these distribution lines: | recipient | address | amount | percentage | @@ -69,6 +66,69 @@ Feature: Fundraisers can distribute funds | 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" @@ -80,10 +140,9 @@ Feature: Fundraisers can distribute funds Given I'm logged in as "alice" And I go to the project page And I click on "New distribution" - And I type "bob" in the recipient field - And I select the recipient "bob" + And I add the GitHub user "bob" to the recipients And I fill the amount to "bob" with "10" - And I click on "Save" + And I save the distribution Then I should see these distribution lines: | recipient | address | amount | percentage | @@ -127,10 +186,9 @@ Feature: Fundraisers can distribute funds Given I'm logged in as "alice" And I go to the project page And I click on "New distribution" - And I type "bob@example.com" in the recipient field - And I select the recipient "bob@example.com (new user)" - And I fill the amount to "bob@example.com (new user)" with "10" - And I click on "Save" + And I add the email address "bob@example.com" to the recipients + And I fill the amount to "bob@example.com" with "10" + And I save the distribution Then I should see these distribution lines: | recipient | address | amount | percentage | @@ -196,10 +254,9 @@ Feature: Fundraisers can distribute funds Given I'm logged in as "alice" And I go to the project page And I click on "New distribution" - And I type "bob" in the recipient field - And I select the recipient "bob" + And I add the GitHub user "bob" to the recipients And I fill the amount to "bob" with "10" - And I click on "Save" + And I save the distribution Then I should see these distribution lines: | recipient | address | amount | percentage | @@ -210,10 +267,9 @@ Feature: Fundraisers can distribute funds And I click on "Edit" And I fill the amount to "bob" with "15" - And I type "carol" in the recipient field - And I select the recipient "carol" + And I add the GitHub user "carol" to the recipients And I fill the amount to "carol" with "5" - And I click on "Save" + And I save the distribution Then I should see these distribution lines: | recipient | address | amount | percentage | @@ -239,10 +295,9 @@ Feature: Fundraisers can distribute funds Given I'm logged in as "alice" And I go to the project page And I click on "New distribution" - And I type "bob@example.com" in the recipient field - And I select the recipient "bob@example.com" + And I add the email address "bob@example.com" to the recipients And I fill the amount to "bob@example.com" with "10" - And I click on "Save" + And I save the distribution Then I should see these distribution lines: | recipient | address | amount | percentage | @@ -303,16 +358,19 @@ Feature: Fundraisers can distribute funds When I'm logged in as "alice" And I go to the project page And I click on "New distribution" - And I type "bob" in the recipient field - And I select the recipient "bob" + 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 click on "Save" + And I save the distribution Then I should see these distribution lines: | recipient | address | comment | 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" @@ -325,20 +383,22 @@ Feature: Fundraisers can distribute funds When I'm logged in as "alice" And I go to the project page And I click on "New distribution" - And I type "bob" in the recipient field - And I select the recipient "bob" - And I type "carol" in the recipient field - And I select the recipient "carol" + 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" - And I click on "Save" + 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 click on "Save" + And I save the distribution Then I should see these distribution lines: | recipient | @@ -353,9 +413,8 @@ Feature: Fundraisers can distribute funds When I'm logged in as "alice" And I go to the project page And I click on "New distribution" - And I type "bob" in the recipient field - And I select the recipient "bob" - And I click on "Save" + 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 | diff --git a/features/step_definitions/common.rb b/features/step_definitions/common.rb index 78c9b599..f77fbe23 100644 --- a/features/step_definitions/common.rb +++ b/features/step_definitions/common.rb @@ -40,6 +40,10 @@ @project = Project.create!(name: "test", full_name: "example/#{arg1}", github_id: Digest::SHA1.hexdigest(arg1), bitcoin_address: 'mq4NtnmQoQoPfNWEPbhSvxvncgtGo6L8WY', hold_tips: false) end +Given(/^a project "(.*?)" holding tips$/) do |arg1| + @project = Project.create!(name: "test", full_name: "example/#{arg1}", github_id: Digest::SHA1.hexdigest(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 @@ -148,6 +152,10 @@ def find_new_commit(id) 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 diff --git a/features/step_definitions/distribution.rb b/features/step_definitions/distribution.rb index d1a5b78e..ac231d9c 100644 --- a/features/step_definitions/distribution.rb +++ b/features/step_definitions/distribution.rb @@ -11,19 +11,42 @@ create(:user, email: arg1, nickname: nil, bitcoin_address: nil) end -Given(/^I type "(.*?)" in the recipient field$/) do |arg1| - fill_in "add-recipients-input", with: arg1 +Given(/^I add the GitHub user "(.*?)" to the recipients$/) do |arg1| + within ".panel", text: "GitHub user" do + find("input").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 select the recipient "(.*?)"$/) do |arg1| - within "#recipient-suggestions" do +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").set(arg1) + click_on "Add" + end +end + Given(/^I fill the amount to "(.*?)" with "(.*?)"$/) do |arg1, arg2| - within "#recipients tr", text: /^#{Regexp.escape arg1}/ do - fill_in "Amount", with: arg2 + begin + within "#recipients tr", text: /^#{Regexp.escape 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 @@ -77,6 +100,7 @@ tr = find("#distribution-form #recipients tr", text: row["recipient"]) rescue puts "Rows: " + all("#distribution-form #recipients tr").map(&:text).inspect + p errors: all(".alert.alert-danger").map(&:text) raise end tr.find(".recipient").text.should eq(row["recipient"]) @@ -108,7 +132,9 @@ When(/^I set my address to "(.*?)"$/) do |arg1| step 'I go to edit my profile' fill_in "Peercoin address", with: arg1 - fill_in "Current password", with: "password" + if has_field?("Current password") + fill_in "Current password", with: "password" + end click_on "Update" page.should have_content "You updated your account successfully" end @@ -143,3 +169,7 @@ User.find_by(email: arg1).bitcoin_address.should eq(arg2) end +Given(/^I save the distribution$/) do + click_on "Save" + page.should have_content(/Distribution (created|updated)/) +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/support/octokit_mock.rb b/features/support/octokit_mock.rb index 497d3fa2..718fab0c 100644 --- a/features/support/octokit_mock.rb +++ b/features/support/octokit_mock.rb @@ -5,4 +5,16 @@ def initialize(*args) 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 From 386909dd5c1900af3c1361ec5520b2c9684da4f5 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 8 Jun 2014 11:28:58 +0200 Subject: [PATCH 244/372] Autocomplete commits --- Gemfile | 1 + Gemfile.lock | 5 +++ app/assets/javascripts/application.js.coffee | 1 + app/assets/javascripts/distribution.js.coffee | 24 +++++++++++ app/assets/stylesheets/typeahead.css.sass | 41 +++++++++++++++++++ app/controllers/projects_controller.rb | 22 ++++++++++ app/models/ability.rb | 2 +- app/models/commit.rb | 2 + app/views/distributions/_form.html.haml | 2 +- config/routes.rb | 1 + features/step_definitions/distribution.rb | 2 +- 11 files changed, 100 insertions(+), 3 deletions(-) create mode 100644 app/assets/stylesheets/typeahead.css.sass diff --git a/Gemfile b/Gemfile index 9f28c809..958c093e 100644 --- a/Gemfile +++ b/Gemfile @@ -77,6 +77,7 @@ gem 'rails_autolink' gem 'redcarpet' gem 'sanitize' gem 'paper_trail', '~> 3.0.2' +gem 'twitter-typeahead-rails' group :test do gem 'cucumber-rails', :require => false diff --git a/Gemfile.lock b/Gemfile.lock index ed3045ee..53cf26f1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -301,6 +301,10 @@ GEM polyglot (>= 0.3.1) turbolinks (2.2.0) coffee-rails + twitter-typeahead-rails (0.10.2) + actionpack (>= 3.1) + jquery-rails + railties (>= 3.1) tzinfo (0.3.38) uglifier (2.4.0) execjs (>= 0.3.0) @@ -360,6 +364,7 @@ DEPENDENCIES timecop turbolinks twitter-bootstrap-rails! + twitter-typeahead-rails twitter_bootstrap_form_for! uglifier (>= 1.3.0) whenever diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee index a9743942..9a6f5cf2 100644 --- a/app/assets/javascripts/application.js.coffee +++ b/app/assets/javascripts/application.js.coffee @@ -2,6 +2,7 @@ #= require jquery_ujs #= require twitter/bootstrap #= require turbolinks +#= require twitter/typeahead #= require_tree . $(document).on "ready page:change", -> diff --git a/app/assets/javascripts/distribution.js.coffee b/app/assets/javascripts/distribution.js.coffee index 0a2ca00d..7ead7ceb 100644 --- a/app/assets/javascripts/distribution.js.coffee +++ b/app/assets/javascripts/distribution.js.coffee @@ -20,3 +20,27 @@ $(document).on "page:change", -> 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 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/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 92daa7be..1da2ba35 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -4,6 +4,8 @@ class ProjectsController < ApplicationController before_action :load_project, only: [:qrcode, :edit, :update, :decide_tip_amounts] + load_and_authorize_resource only: [:commit_suggestions] + def index @projects = Project.enabled.order(available_amount_cache: :desc, watchers_count: :desc, full_name: :asc).page(params[:page]).per(30) end @@ -87,6 +89,26 @@ def create 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 + private def project_params params.require(:project).permit(:name, :description, :detailed_description, :full_name, :auto_tip_commits, :hold_tips, tipping_policies_text_attributes: [:text]) diff --git a/app/models/ability.rb b/app/models/ability.rb index 25a9d69e..2204a3de 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -6,7 +6,7 @@ def initialize(user) can :read, Distribution if user and user.nickname.present? - can [:update, :decide_tip_amounts], Project, collaborators: {login: user.nickname} + can [:update, :decide_tip_amounts, :commit_suggestions], Project, collaborators: {login: user.nickname} can [:create], Project can [:create], Distribution, project: {collaborators: {login: user.nickname}} can [:update, :new_recipient_form], Distribution, project: {collaborators: {login: user.nickname}}, txid: nil, sent_at: nil diff --git a/app/models/commit.rb b/app/models/commit.rb index 4cb67e0e..b7e43f9d 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -1,3 +1,5 @@ class Commit < ActiveRecord::Base belongs_to :project + + validates :sha, uniqueness: {scope: :project_id} end diff --git a/app/views/distributions/_form.html.haml b/app/views/distributions/_form.html.haml index d73b102c..fa1b6ecf 100644 --- a/app/views/distributions/_form.html.haml +++ b/app/views/distributions/_form.html.haml @@ -38,7 +38,7 @@ %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") + = 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 diff --git a/config/routes.rb b/config/routes.rb index aab12ce9..942661cc 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -32,6 +32,7 @@ get :qrcode get :decide_tip_amounts patch :decide_tip_amounts + get :commit_suggestions end end resources :tips, :only => [:index] diff --git a/features/step_definitions/distribution.rb b/features/step_definitions/distribution.rb index ac231d9c..a4c9f25c 100644 --- a/features/step_definitions/distribution.rb +++ b/features/step_definitions/distribution.rb @@ -33,7 +33,7 @@ When(/^I add the commit "(.*?)" to the recipients$/) do |arg1| within ".panel", text: "Author of a commit" do - find("input").set(arg1) + find("input:enabled").set(arg1) click_on "Add" end end From f8e93ff0fe683bc7fb8965768a1f260dc0a0dd51 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 8 Jun 2014 12:32:52 +0200 Subject: [PATCH 245/372] Autocomplete GitHub usernames --- app/assets/javascripts/distribution.js.coffee | 24 +++++++++++++++++++ app/controllers/projects_controller.rb | 19 +++++++++++++++ app/models/ability.rb | 2 +- app/views/distributions/_form.html.haml | 2 +- config/routes.rb | 1 + features/step_definitions/distribution.rb | 2 +- 6 files changed, 47 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/distribution.js.coffee b/app/assets/javascripts/distribution.js.coffee index 7ead7ceb..ef50bd49 100644 --- a/app/assets/javascripts/distribution.js.coffee +++ b/app/assets/javascripts/distribution.js.coffee @@ -44,3 +44,27 @@ $(document).on "page:change", -> 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 diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 1da2ba35..665df0af 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -109,6 +109,25 @@ def commit_suggestions end end + def github_user_suggestions + respond_to do |format| + format.json do + query = params[:query] + users = User.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 + private def project_params params.require(:project).permit(:name, :description, :detailed_description, :full_name, :auto_tip_commits, :hold_tips, tipping_policies_text_attributes: [:text]) diff --git a/app/models/ability.rb b/app/models/ability.rb index 2204a3de..415e7882 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -6,7 +6,7 @@ def initialize(user) can :read, Distribution if user and user.nickname.present? - can [:update, :decide_tip_amounts, :commit_suggestions], Project, collaborators: {login: user.nickname} + can [:update, :decide_tip_amounts, :commit_suggestions, :github_user_suggestions], Project, collaborators: {login: user.nickname} can [:create], Project can [:create], Distribution, project: {collaborators: {login: user.nickname}} can [:update, :new_recipient_form], Distribution, project: {collaborators: {login: user.nickname}}, txid: nil, sent_at: nil diff --git a/app/views/distributions/_form.html.haml b/app/views/distributions/_form.html.haml index fa1b6ecf..79288fe3 100644 --- a/app/views/distributions/_form.html.haml +++ b/app/views/distributions/_form.html.haml @@ -31,7 +31,7 @@ %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") + = 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 diff --git a/config/routes.rb b/config/routes.rb index 942661cc..53fa3dc1 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -33,6 +33,7 @@ get :decide_tip_amounts patch :decide_tip_amounts get :commit_suggestions + get :github_user_suggestions end end resources :tips, :only => [:index] diff --git a/features/step_definitions/distribution.rb b/features/step_definitions/distribution.rb index a4c9f25c..a47f8abb 100644 --- a/features/step_definitions/distribution.rb +++ b/features/step_definitions/distribution.rb @@ -13,7 +13,7 @@ Given(/^I add the GitHub user "(.*?)" to the recipients$/) do |arg1| within ".panel", text: "GitHub user" do - find("input").set(arg1) + find("input:enabled").set(arg1) click_on "Add" end end From 7405958084ef646d403765b38d6163f1b16ccfb8 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 8 Jun 2014 13:41:29 +0200 Subject: [PATCH 246/372] Better distribution form --- app/assets/stylesheets/distribution.css.sass | 5 +++++ app/views/distributions/_form.html.haml | 1 + app/views/distributions/_tip_form.html.haml | 2 +- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/distribution.css.sass b/app/assets/stylesheets/distribution.css.sass index e69de29b..af248ff8 100644 --- a/app/assets/stylesheets/distribution.css.sass +++ b/app/assets/stylesheets/distribution.css.sass @@ -0,0 +1,5 @@ +#distribution-form + td.amount + width: 170px + input + text-align: right diff --git a/app/views/distributions/_form.html.haml b/app/views/distributions/_form.html.haml index 79288fe3..1cecad74 100644 --- a/app/views/distributions/_form.html.haml +++ b/app/views/distributions/_form.html.haml @@ -7,6 +7,7 @@ %th Origin %th Comment %th Amount + %th %tbody#recipients - f.object.tips.each do |tip| = render "tip_form", tip: tip, form: f diff --git a/app/views/distributions/_tip_form.html.haml b/app/views/distributions/_tip_form.html.haml index 432a1af8..8bac749f 100644 --- a/app/views/distributions/_tip_form.html.haml +++ b/app/views/distributions/_tip_form.html.haml @@ -17,5 +17,5 @@ = fields.hidden_field :origin_id = render "origin", tip: fields.object %td.comment= fields.text_field :comment, hide_label: true - %td.amount= fields.text_field :coin_amount, hide_label: true + %td.amount= fields.text_field :coin_amount, hide_label: true, append: "PPC" %td.remove= fields.check_box :_destroy From 66eb04fc47de49f4866818f1f208590f5958b5a5 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 8 Jun 2014 14:01:01 +0200 Subject: [PATCH 247/372] Projects are commentable --- Gemfile | 1 + Gemfile.lock | 4 + app/controllers/projects_controller.rb | 1 + app/models/project.rb | 6 + app/models/user.rb | 2 + app/views/projects/show.html.haml | 3 + config/initializers/commontator.rb | 238 ++++++++++++++++++ config/routes.rb | 1 + ...8120038_install_commontator.commontator.rb | 49 ++++ db/schema.rb | 51 +++- 10 files changed, 351 insertions(+), 5 deletions(-) create mode 100644 config/initializers/commontator.rb create mode 100644 db/migrate/20140608120038_install_commontator.commontator.rb diff --git a/Gemfile b/Gemfile index 958c093e..c1e12038 100644 --- a/Gemfile +++ b/Gemfile @@ -78,6 +78,7 @@ gem 'redcarpet' gem 'sanitize' gem 'paper_trail', '~> 3.0.2' gem 'twitter-typeahead-rails' +gem 'commontator', '~> 4.6.0' group :test do gem 'cucumber-rails', :require => false diff --git a/Gemfile.lock b/Gemfile.lock index 53cf26f1..619795ed 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -102,6 +102,9 @@ GEM execjs coffee-script-source (1.6.3) commonjs (0.2.7) + commontator (4.6.1) + jquery-rails + rails (>= 3.1) cucumber (1.3.10) builder (>= 2.1.2) diff-lcs (>= 1.1.3) @@ -330,6 +333,7 @@ DEPENDENCIES capistrano-rails capistrano-rvm! coffee-rails (~> 4.0.0) + commontator (~> 4.6.0) cucumber-rails database_cleaner devise diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 665df0af..ef8ae5fc 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -16,6 +16,7 @@ def show redirect_to root_path, alert: "Project not found" return end + commontator_thread_show(@project) end def new diff --git a/app/models/project.rb b/app/models/project.rb index e8908a2d..443299db 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -14,6 +14,8 @@ class Project < ActiveRecord::Base has_paper_trail + acts_as_commontable + validates :name, presence: true before_validation :strip_full_name @@ -219,4 +221,8 @@ def generate_address! self.bitcoin_address = BitcoinDaemon.instance.get_new_address(address_label) save(validate: false) end + + def to_label + name.presence || id.to_s + end end diff --git a/app/models/user.rb b/app/models/user.rb index 5c57dcc4..01cb4f05 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -14,6 +14,8 @@ class User < ActiveRecord::Base before_create :generate_login_token!, unless: :login_token? + acts_as_commontator + def github_url "https://github.com/#{nickname}" end diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index 4acb619b..111754dd 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -45,6 +45,9 @@ %p %input.form-control{type: 'text', value: "[![tip for next commit](#{project_url(@project, format: :svg)})](#{project_url(@project)})"} + %hr + = commontator_thread(@project) + .col-md-4 - if @project.disabled? .panel.panel-danger diff --git a/config/initializers/commontator.rb b/config/initializers/commontator.rb new file mode 100644 index 00000000..fded3ec2 --- /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| Rails.application.routes.url_helpers.user_path(user) } + + # 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/routes.rb b/config/routes.rb index 53fa3dc1..6ddcdac8 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,4 +1,5 @@ T4c::Application.routes.draw do + mount Commontator::Engine => '/commontator' root 'home#index' 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/schema.rb b/db/schema.rb index 8ea22987..13ae394f 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20140607100342) do +ActiveRecord::Schema.define(version: 20140608120038) do create_table "cold_storage_transfers", force: true do |t| t.integer "project_id" @@ -47,6 +47,48 @@ add_index "commits", ["project_id"], name: "index_commits_on_project_id" + create_table "commontator_comments", force: true 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.datetime "created_at" + t.datetime "updated_at" + end + + add_index "commontator_comments", ["cached_votes_down"], name: "index_commontator_comments_on_cached_votes_down" + add_index "commontator_comments", ["cached_votes_up"], name: "index_commontator_comments_on_cached_votes_up" + 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"], name: "index_commontator_comments_on_thread_id" + + create_table "commontator_subscriptions", force: true do |t| + t.string "subscriber_type", 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 + add_index "commontator_subscriptions", ["thread_id"], name: "index_commontator_subscriptions_on_thread_id" + + create_table "commontator_threads", force: true do |t| + t.string "commontable_type" + t.integer "commontable_id" + t.datetime "closed_at" + t.string "closer_type" + t.integer "closer_id" + t.datetime "created_at" + t.datetime "updated_at" + end + + add_index "commontator_threads", ["commontable_id", "commontable_type"], name: "index_commontator_threads_on_c_id_and_c_type", unique: true + create_table "deposits", force: true do |t| t.integer "project_id" t.string "txid" @@ -112,7 +154,6 @@ add_index "tipping_policies_texts", ["user_id"], name: "index_tipping_policies_texts_on_user_id" create_table "tips", force: true do |t| - t.integer "user_id" t.integer "amount", limit: 8 t.integer "distribution_id" t.datetime "created_at" @@ -124,6 +165,7 @@ t.string "comment" t.integer "origin_id" t.string "origin_type" + t.integer "user_id" end add_index "tips", ["distribution_id"], name: "index_tips_on_distribution_id" @@ -132,7 +174,6 @@ add_index "tips", ["user_id"], name: "index_tips_on_user_id" 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" t.datetime "reset_password_sent_at" @@ -144,7 +185,6 @@ t.string "last_sign_in_ip" t.datetime "created_at" t.datetime "updated_at" - t.string "nickname" t.string "name" t.string "image" t.string "bitcoin_address" @@ -157,9 +197,10 @@ t.datetime "confirmed_at" t.datetime "confirmation_sent_at" t.string "unconfirmed_email" + t.string "email" + t.string "nickname" 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 create_table "versions", force: true do |t| From 5bcfcd3a2011cdb174d5419680e06bf4baae932a Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 8 Jun 2014 14:29:01 +0200 Subject: [PATCH 248/372] Distribution commentable --- app/controllers/distributions_controller.rb | 1 + app/models/distribution.rb | 6 ++++++ app/views/distributions/_tip_form.html.haml | 2 +- app/views/distributions/show.html.haml | 5 ++++- features/step_definitions/distribution.rb | 2 +- 5 files changed, 13 insertions(+), 3 deletions(-) diff --git a/app/controllers/distributions_controller.rb b/app/controllers/distributions_controller.rb index a82a1ed0..ddb1e145 100644 --- a/app/controllers/distributions_controller.rb +++ b/app/controllers/distributions_controller.rb @@ -33,6 +33,7 @@ def update end def show + commontator_thread_show(@distribution) end def send_transaction diff --git a/app/models/distribution.rb b/app/models/distribution.rb index 3fee1131..a1d9ed50 100644 --- a/app/models/distribution.rb +++ b/app/models/distribution.rb @@ -5,6 +5,8 @@ class Distribution < ActiveRecord::Base has_paper_trail + acts_as_commontable + scope :to_send, -> { where(txid: nil) } scope :error, -> { where(is_error: true) } @@ -51,4 +53,8 @@ def all_addresses_known? 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 end diff --git a/app/views/distributions/_tip_form.html.haml b/app/views/distributions/_tip_form.html.haml index 8bac749f..6824198b 100644 --- a/app/views/distributions/_tip_form.html.haml +++ b/app/views/distributions/_tip_form.html.haml @@ -16,6 +16,6 @@ = fields.hidden_field :origin_type = fields.hidden_field :origin_id = render "origin", tip: fields.object - %td.comment= fields.text_field :comment, hide_label: true + %td.tip-comment= fields.text_field :comment, hide_label: true %td.amount= fields.text_field :coin_amount, hide_label: true, append: "PPC" %td.remove= fields.check_box :_destroy diff --git a/app/views/distributions/show.html.haml b/app/views/distributions/show.html.haml index ca1d592e..db93201f 100644 --- a/app/views/distributions/show.html.haml +++ b/app/views/distributions/show.html.haml @@ -21,7 +21,7 @@ - else Nobody %td.origin= render "origin", tip: tip - %td.comment= render_markdown tip.comment + %td.tip-comment= render_markdown tip.comment %td.address - if tip.user.try(:bitcoin_address).present? = tip.user.bitcoin_address @@ -58,3 +58,6 @@ - if can? :update, @distribution .distribution-action = link_to "Edit the distribution", edit_project_distribution_path(@project, @distribution), class: "btn btn-default" + +%hr += commontator_thread(@distribution) diff --git a/features/step_definitions/distribution.rb b/features/step_definitions/distribution.rb index a47f8abb..ae7d12e3 100644 --- a/features/step_definitions/distribution.rb +++ b/features/step_definitions/distribution.rb @@ -89,7 +89,7 @@ text.should eq(row["percentage"]) end end - tr.find(".comment").text.should eq(row["comment"]) if row["comment"] + tr.find(".tip-comment").text.should eq(row["comment"]) if row["comment"] end table.hashes.size.should eq(all("#distribution-show-page tbody tr").size) end From 613b8137e50c39fb3ac97f406cdfe4c180ba3b91 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 8 Jun 2014 14:30:41 +0200 Subject: [PATCH 249/372] User commentable --- app/controllers/users_controller.rb | 1 + app/models/user.rb | 1 + app/views/users/show.html.haml | 2 ++ 3 files changed, 4 insertions(+) diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 68c36cf1..048cba72 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -13,6 +13,7 @@ class UsersController < ApplicationController def show @user = User.find(params[:id]) + commontator_thread_show(@user) end def index diff --git a/app/models/user.rb b/app/models/user.rb index 01cb4f05..c5e9c7a0 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -15,6 +15,7 @@ class User < ActiveRecord::Base before_create :generate_login_token!, unless: :login_token? acts_as_commontator + acts_as_commontable def github_url "https://github.com/#{nickname}" diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index 84ea984e..7f5d6eab 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -4,3 +4,5 @@ %strong GitHub account: = link_to @user.nickname, "https://github.com/#{@user.nickname}" +%hr += commontator_thread(@user) From 2252a0206ee5d5c684e6cf2f5429e54fd30c919e Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 8 Jun 2014 14:42:57 +0200 Subject: [PATCH 250/372] Comment button style --- app/assets/stylesheets/bootstrap_and_overrides.css.less | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/assets/stylesheets/bootstrap_and_overrides.css.less b/app/assets/stylesheets/bootstrap_and_overrides.css.less index b7a52835..4a84e392 100644 --- a/app/assets/stylesheets/bootstrap_and_overrides.css.less +++ b/app/assets/stylesheets/bootstrap_and_overrides.css.less @@ -27,3 +27,10 @@ // // Example: // @linkColor: #ff0000; + +.thread_new_comment { + input { + &:extend(.btn, .btn-default); + } +} + From 2d251cc871841ba40e704edd1db91a78c7260825 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 8 Jun 2014 15:21:45 +0200 Subject: [PATCH 251/372] Renamed origin to reason --- app/controllers/distributions_controller.rb | 4 ++-- app/models/tip.rb | 4 ++-- app/views/distributions/_form.html.haml | 2 +- .../{_origin.html.haml => _reason.html.haml} | 4 ++-- app/views/distributions/_tip_form.html.haml | 8 ++++---- app/views/distributions/show.html.haml | 4 ++-- .../20140608131519_rename_origin_to_reason.rb | 6 ++++++ db/schema.rb | 8 ++++---- features/distribute_to_commits.feature | 16 ++++++++-------- features/step_definitions/distribution.rb | 4 ++-- 10 files changed, 33 insertions(+), 27 deletions(-) rename app/views/distributions/{_origin.html.haml => _reason.html.haml} (78%) create mode 100644 db/migrate/20140608131519_rename_origin_to_reason.rb diff --git a/app/controllers/distributions_controller.rb b/app/controllers/distributions_controller.rb index ddb1e145..87bc978a 100644 --- a/app/controllers/distributions_controller.rb +++ b/app/controllers/distributions_controller.rb @@ -63,7 +63,7 @@ def new_recipient_form @tips << Tip.new(user: user) elsif params[:not_rewarded_commits] @project.commits.each do |commit| - next if Tip.where(origin: commit).any? + next if Tip.where(reason: commit).any? tip = Tip.build_from_commit(commit) @tips << tip if tip end @@ -87,7 +87,7 @@ def new_recipient_form def distribution_params if params[:distribution] - params.require(:distribution).permit(tips_attributes: [:id, :coin_amount, :user_id, :comment, :origin_type, :origin_id, :_destroy]) + params.require(:distribution).permit(tips_attributes: [:id, :coin_amount, :user_id, :comment, :reason_type, :reason_id, :_destroy]) else {} end diff --git a/app/models/tip.rb b/app/models/tip.rb index a5eb0fe0..c9e35e8c 100644 --- a/app/models/tip.rb +++ b/app/models/tip.rb @@ -2,7 +2,7 @@ class Tip < ActiveRecord::Base belongs_to :user belongs_to :distribution belongs_to :project, inverse_of: :tips - belongs_to :origin, polymorphic: true + belongs_to :reason, polymorphic: true has_paper_trail @@ -129,6 +129,6 @@ def self.build_from_commit(commit) user.skip_confirmation_notification! user.save! end - new(user_id: user.id, origin: commit) + new(user_id: user.id, reason: commit) end end diff --git a/app/views/distributions/_form.html.haml b/app/views/distributions/_form.html.haml index 1cecad74..84dd0144 100644 --- a/app/views/distributions/_form.html.haml +++ b/app/views/distributions/_form.html.haml @@ -4,7 +4,7 @@ %table.table %thead %th Recipient - %th Origin + %th Reason %th Comment %th Amount %th diff --git a/app/views/distributions/_origin.html.haml b/app/views/distributions/_reason.html.haml similarity index 78% rename from app/views/distributions/_origin.html.haml rename to app/views/distributions/_reason.html.haml index 71849090..b6646dd8 100644 --- a/app/views/distributions/_origin.html.haml +++ b/app/views/distributions/_reason.html.haml @@ -1,5 +1,5 @@ -- case tip.origin +- case tip.reason - when Commit - - commit = tip.origin + - commit = tip.reason Commit #{link_to truncate_commit(commit.sha), "https://github.com/#{commit.project.full_name}/commit/#{commit.sha}"}: %pre= commit.message diff --git a/app/views/distributions/_tip_form.html.haml b/app/views/distributions/_tip_form.html.haml index 6824198b..de01efe7 100644 --- a/app/views/distributions/_tip_form.html.haml +++ b/app/views/distributions/_tip_form.html.haml @@ -12,10 +12,10 @@ - else = fields.hidden_field :user_id = link_to user.recipient_label, user - %td.origin - = fields.hidden_field :origin_type - = fields.hidden_field :origin_id - = render "origin", tip: fields.object + %td.reason + = fields.hidden_field :reason_type + = fields.hidden_field :reason_id + = render "reason", tip: fields.object %td.tip-comment= fields.text_field :comment, hide_label: true %td.amount= fields.text_field :coin_amount, hide_label: true, append: "PPC" %td.remove= fields.check_box :_destroy diff --git a/app/views/distributions/show.html.haml b/app/views/distributions/show.html.haml index db93201f..e65b99fb 100644 --- a/app/views/distributions/show.html.haml +++ b/app/views/distributions/show.html.haml @@ -4,7 +4,7 @@ %thead %tr %th Recipient - %th Origin + %th Reason %th Comment %th Address %th Amount @@ -20,7 +20,7 @@ = link_to tip.user.recipient_label, tip.user - else Nobody - %td.origin= render "origin", tip: tip + %td.reason= render "reason", tip: tip %td.tip-comment= render_markdown tip.comment %td.address - if tip.user.try(:bitcoin_address).present? 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/schema.rb b/db/schema.rb index 13ae394f..a0846383 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20140608120038) do +ActiveRecord::Schema.define(version: 20140608131519) do create_table "cold_storage_transfers", force: true do |t| t.integer "project_id" @@ -163,14 +163,14 @@ t.datetime "refunded_at" t.string "commit_message" t.string "comment" - t.integer "origin_id" - t.string "origin_type" + t.integer "reason_id" + t.string "reason_type" t.integer "user_id" end add_index "tips", ["distribution_id"], name: "index_tips_on_distribution_id" - add_index "tips", ["origin_id", "origin_type"], name: "index_tips_on_origin_id_and_origin_type" add_index "tips", ["project_id"], name: "index_tips_on_project_id" + add_index "tips", ["reason_id", "reason_type"], name: "index_tips_on_reason_id_and_reason_type" add_index "tips", ["user_id"], name: "index_tips_on_user_id" create_table "users", force: true do |t| diff --git a/features/distribute_to_commits.feature b/features/distribute_to_commits.feature index f7a7974b..2f387a3b 100644 --- a/features/distribute_to_commits.feature +++ b/features/distribute_to_commits.feature @@ -27,7 +27,7 @@ Feature: A project collaborator distribute to commit authors And I click on "New distribution" And I select the commit recipients "Commits not rewarded" Then the distribution form should have these recipients: - | recipient | origin | comment | amount | + | recipient | reason | comment | amount | | yugo | Commit BBB: Tiny change | | | | gaal | Commit CCC: Some changes | | | @@ -36,7 +36,7 @@ Feature: A project collaborator distribute to commit authors And I click on "Save" Then I should see these distribution lines: - | recipient | origin | address | amount | percentage | + | recipient | reason | address | amount | percentage | | yugo | Commit BBB: Tiny change | | 0.5 | 100 | And I should see "Total amount: 0.50 PPC" When I click on "Send email request to provide an address" @@ -50,7 +50,7 @@ Feature: A project collaborator distribute to commit authors And I click on "New distribution" And I select the commit recipients "Commits not rewarded" Then the distribution form should have these recipients: - | recipient | origin | comment | amount | + | recipient | reason | comment | amount | | gaal | Commit CCC: Some changes | | | @javascript @@ -70,14 +70,14 @@ Feature: A project collaborator distribute to commit authors And I click on "New distribution" And I select the commit recipients "Commits not rewarded" Then the distribution form should have these recipients: - | recipient | origin | comment | amount | + | recipient | reason | comment | amount | | yugo@example.com | Commit BBB: Tiny change | | | And I fill the amount to "yugo@example.com" with "0.5" And I save the distribution Then I should see these distribution lines: - | recipient | origin | address | amount | percentage | + | recipient | reason | address | amount | percentage | | yugo@example.com | Commit BBB: Tiny change | | 0.5 | 100 | And I should see "Total amount: 0.50 PPC" And there should be 0 email sent @@ -105,11 +105,11 @@ Feature: A project collaborator distribute to commit authors And I click on "New distribution" And I add the commit "170ed604f287b9fec397389d0b1b3f7d15b82276" to the recipients Then the distribution form should have these recipients: - | recipient | origin | comment | amount | + | recipient | reason | comment | amount | | yugo | Commit 170ed604f2: Tiny change | | | When I add the commit "1329394df" to the recipients Then the distribution form should have these recipients: - | recipient | origin | comment | amount | + | recipient | reason | comment | amount | | yugo | Commit 170ed604f2: Tiny change | | | | gaal | Commit 1329394df2: Some changes | | | @@ -117,6 +117,6 @@ Feature: A project collaborator distribute to commit authors And I click on "Save" Then I should see these distribution lines: - | recipient | origin | address | amount | percentage | + | recipient | reason | address | amount | percentage | | yugo | Commit 170ed604f2: Tiny change | | 0.5 | | | gaal | Commit 1329394df2: Some changes | | Undecided | | diff --git a/features/step_definitions/distribution.rb b/features/step_definitions/distribution.rb index ae7d12e3..bcc2c085 100644 --- a/features/step_definitions/distribution.rb +++ b/features/step_definitions/distribution.rb @@ -72,7 +72,7 @@ end tr.find(".recipient").text.should eq(row["recipient"]) tr.find(".address").text.should eq(row["address"]) if row["address"] - tr.find(".origin").text.should eq(row["origin"]) if row["origin"] + tr.find(".reason").text.should eq(row["reason"]) if row["reason"] if row["amount"] text = tr.find(".amount").text if row["amount"] =~ /\A[0-9.]+\Z/ @@ -104,7 +104,7 @@ raise end tr.find(".recipient").text.should eq(row["recipient"]) - tr.find(".origin").text.should eq(row["origin"]) if row["origin"] + tr.find(".reason").text.should eq(row["reason"]) if row["reason"] if row["amount"] text = tr.find_field("Amount").value if row["amount"] =~ /\A[0-9.]+\Z/ From 68cd627e91a801ceda09c94181b33ee024c36781 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 8 Jun 2014 15:35:12 +0200 Subject: [PATCH 252/372] The comment is now a free reason text --- app/views/distributions/_form.html.haml | 1 - app/views/distributions/_reason.html.haml | 2 ++ app/views/distributions/_tip_form.html.haml | 10 +++++---- app/views/distributions/show.html.haml | 2 -- features/distribute_to_commits.feature | 24 ++++++++++----------- features/distribution.feature | 2 +- 6 files changed, 21 insertions(+), 20 deletions(-) diff --git a/app/views/distributions/_form.html.haml b/app/views/distributions/_form.html.haml index 84dd0144..d7bb06ac 100644 --- a/app/views/distributions/_form.html.haml +++ b/app/views/distributions/_form.html.haml @@ -5,7 +5,6 @@ %thead %th Recipient %th Reason - %th Comment %th Amount %th %tbody#recipients diff --git a/app/views/distributions/_reason.html.haml b/app/views/distributions/_reason.html.haml index b6646dd8..29d7c6e4 100644 --- a/app/views/distributions/_reason.html.haml +++ b/app/views/distributions/_reason.html.haml @@ -3,3 +3,5 @@ - 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 + = render_markdown tip.comment diff --git a/app/views/distributions/_tip_form.html.haml b/app/views/distributions/_tip_form.html.haml index de01efe7..2d3e9af4 100644 --- a/app/views/distributions/_tip_form.html.haml +++ b/app/views/distributions/_tip_form.html.haml @@ -13,9 +13,11 @@ = fields.hidden_field :user_id = link_to user.recipient_label, user %td.reason - = fields.hidden_field :reason_type - = fields.hidden_field :reason_id - = render "reason", tip: fields.object - %td.tip-comment= fields.text_field :comment, hide_label: true + - 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/show.html.haml b/app/views/distributions/show.html.haml index e65b99fb..c2043363 100644 --- a/app/views/distributions/show.html.haml +++ b/app/views/distributions/show.html.haml @@ -5,7 +5,6 @@ %tr %th Recipient %th Reason - %th Comment %th Address %th Amount %th Percentage @@ -21,7 +20,6 @@ - else Nobody %td.reason= render "reason", tip: tip - %td.tip-comment= render_markdown tip.comment %td.address - if tip.user.try(:bitcoin_address).present? = tip.user.bitcoin_address diff --git a/features/distribute_to_commits.feature b/features/distribute_to_commits.feature index 2f387a3b..0d3637e3 100644 --- a/features/distribute_to_commits.feature +++ b/features/distribute_to_commits.feature @@ -27,9 +27,9 @@ Feature: A project collaborator distribute to commit authors 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 | comment | amount | - | yugo | Commit BBB: Tiny change | | | - | gaal | Commit CCC: Some changes | | | + | 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" @@ -50,8 +50,8 @@ Feature: A project collaborator distribute to commit authors 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 | comment | amount | - | gaal | Commit CCC: Some changes | | | + | recipient | reason | amount | + | gaal | Commit CCC: Some changes | | @javascript Scenario: Distribute to commits not linked to a GitHub account @@ -70,8 +70,8 @@ Feature: A project collaborator distribute to commit authors 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 | comment | amount | - | yugo@example.com | Commit BBB: Tiny change | | | + | recipient | reason | amount | + | yugo@example.com | Commit BBB: Tiny change | | And I fill the amount to "yugo@example.com" with "0.5" And I save the distribution @@ -105,13 +105,13 @@ Feature: A project collaborator distribute to commit authors 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 | comment | amount | - | yugo | Commit 170ed604f2: Tiny change | | | + | 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 | comment | amount | - | yugo | Commit 170ed604f2: Tiny change | | | - | gaal | Commit 1329394df2: Some changes | | | + | 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" diff --git a/features/distribution.feature b/features/distribution.feature index 760035a7..ad150e98 100644 --- a/features/distribution.feature +++ b/features/distribution.feature @@ -364,7 +364,7 @@ Feature: Fundraisers can distribute funds And I save the distribution Then I should see these distribution lines: - | recipient | address | comment | amount | percentage | + | recipient | address | reason | amount | percentage | | bob | mxWfjaZJTNN5QKeZZYQ5HW3vgALFBsnuG1 | Great idea | 10 | 100 | From 0b933b82989bf2460224478de087e1bfcc05a113 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 8 Jun 2014 15:43:36 +0200 Subject: [PATCH 253/372] Validate tip reason --- app/models/tip.rb | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/app/models/tip.rb b/app/models/tip.rb index c9e35e8c..082dd8d1 100644 --- a/app/models/tip.rb +++ b/app/models/tip.rb @@ -7,6 +7,7 @@ class Tip < ActiveRecord::Base has_paper_trail validates :amount, numericality: {greater_or_equal_than: 0, allow_nil: true} + validate :validate_reason scope :not_sent, -> { where(distribution_id: nil) } def not_sent? @@ -131,4 +132,15 @@ def self.build_from_commit(commit) end 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 end From 3de09cb6bf7b70b74a9f0de80f6910c00bfcb348 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 8 Jun 2014 15:58:46 +0200 Subject: [PATCH 254/372] Render old tips with commit as reason --- app/controllers/distributions_controller.rb | 2 +- app/views/distributions/_reason.html.haml | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/app/controllers/distributions_controller.rb b/app/controllers/distributions_controller.rb index 87bc978a..5a643b03 100644 --- a/app/controllers/distributions_controller.rb +++ b/app/controllers/distributions_controller.rb @@ -79,7 +79,7 @@ def new_recipient_form end result = render_to_string(layout: false) render json: {result: result} - rescue => e + rescue RuntimeError => e render json: {error: e.message} end diff --git a/app/views/distributions/_reason.html.haml b/app/views/distributions/_reason.html.haml index 29d7c6e4..41b19b80 100644 --- a/app/views/distributions/_reason.html.haml +++ b/app/views/distributions/_reason.html.haml @@ -4,4 +4,11 @@ Commit #{link_to truncate_commit(commit.sha), "https://github.com/#{commit.project.full_name}/commit/#{commit.sha}"}: %pre= commit.message - when nil - = render_markdown tip.comment + - 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} + %pre= tip.commit_message + - else + = render_markdown tip.comment From 14b3df61cfdaf864cc8ef39856fa00f1578263b3 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 8 Jun 2014 16:36:51 +0200 Subject: [PATCH 255/372] Do not add commits rewarded through the old system --- app/controllers/distributions_controller.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/controllers/distributions_controller.rb b/app/controllers/distributions_controller.rb index 5a643b03..f6d7e54b 100644 --- a/app/controllers/distributions_controller.rb +++ b/app/controllers/distributions_controller.rb @@ -64,6 +64,7 @@ def new_recipient_form elsif params[:not_rewarded_commits] @project.commits.each do |commit| next if Tip.where(reason: commit).any? + next if Tip.where(commit: commit.sha).any? tip = Tip.build_from_commit(commit) @tips << tip if tip end From 006f380f2df33575fc9270af2085b8eb03c06292 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 8 Jun 2014 18:12:15 +0200 Subject: [PATCH 256/372] Validate fund availability early --- app/models/distribution.rb | 11 +++++++++++ app/models/project.rb | 6 +++++- app/models/tip.rb | 7 ------- app/views/distributions/_form.html.haml | 3 +++ features/distribution.feature | 17 +++++++++++++++++ 5 files changed, 36 insertions(+), 8 deletions(-) diff --git a/app/models/distribution.rb b/app/models/distribution.rb index a1d9ed50..ffce0ed8 100644 --- a/app/models/distribution.rb +++ b/app/models/distribution.rb @@ -7,6 +7,8 @@ class Distribution < ActiveRecord::Base acts_as_commontable + validate :validate_funds + scope :to_send, -> { where(txid: nil) } scope :error, -> { where(is_error: true) } @@ -57,4 +59,13 @@ def can_be_sent? 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.map(&:amount).compact.sum + errors.add(:base, "Not enough funds") + end + end end diff --git a/app/models/project.rb b/app/models/project.rb index 443299db..288ea7f9 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -133,9 +133,13 @@ def tip_for commit 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 diff --git a/app/models/tip.rb b/app/models/tip.rb index 082dd8d1..19267e6d 100644 --- a/app/models/tip.rb +++ b/app/models/tip.rb @@ -66,7 +66,6 @@ def was_undecided? end - before_save :check_amount_against_project after_save :notify_user_if_just_decided @@ -99,12 +98,6 @@ def notify_user_if_just_decided notify_user if amount_was.nil? and amount end - def check_amount_against_project - if project.available_amount < 0 - raise "Not enough funds on project to save #{inspect} (available: #{available_amount})" - end - end - def coin_amount amount.to_f / COIN if amount end diff --git a/app/views/distributions/_form.html.haml b/app/views/distributions/_form.html.haml index d7bb06ac..c81ea64c 100644 --- a/app/views/distributions/_form.html.haml +++ b/app/views/distributions/_form.html.haml @@ -1,6 +1,9 @@ .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 diff --git a/features/distribution.feature b/features/distribution.feature index ad150e98..57fd991f 100644 --- a/features/distribution.feature +++ b/features/distribution.feature @@ -420,3 +420,20 @@ Feature: Fundraisers can distribute funds | 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" + From 04038e28ae9605bdf2f8b604a595bd01907421dd Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 8 Jun 2014 18:19:29 +0200 Subject: [PATCH 257/372] Only ignore rewarded commits --- app/controllers/distributions_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/distributions_controller.rb b/app/controllers/distributions_controller.rb index f6d7e54b..ed398dc0 100644 --- a/app/controllers/distributions_controller.rb +++ b/app/controllers/distributions_controller.rb @@ -64,7 +64,7 @@ def new_recipient_form elsif params[:not_rewarded_commits] @project.commits.each do |commit| next if Tip.where(reason: commit).any? - next if Tip.where(commit: commit.sha).any? + next if Tip.where(commit: commit.sha).where.not(amount: nil).any? tip = Tip.build_from_commit(commit) @tips << tip if tip end From 96522d58a8446bedcd502a5238c1924715a47dab Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 8 Jun 2014 19:48:02 +0200 Subject: [PATCH 258/372] Added separator --- app/views/projects/show.html.haml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index 111754dd..9f52b4c9 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -28,6 +28,7 @@ - else = "(Last updated on #{date})" + %hr %h4 Promote #{@project.name} %p / AddThis Button BEGIN From cb168873e438e63e2b5e0124b37cc2e4e1f6bb6f Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 8 Jun 2014 20:25:12 +0200 Subject: [PATCH 259/372] Replaced papertrail with custom implementation that stores record states --- Gemfile | 1 - Gemfile.lock | 4 --- app/models/distribution.rb | 2 +- app/models/project.rb | 2 +- app/models/record_change.rb | 3 +++ app/models/tip.rb | 4 +-- config/initializers/record_changes.rb | 10 ++++++++ db/migrate/20140609122234_drop_version.rb | 19 ++++++++++++++ .../20140609122440_create_record_changes.rb | 11 ++++++++ db/schema.rb | 25 +++++++++---------- test/models/record_change_test.rb | 7 ++++++ 11 files changed, 65 insertions(+), 23 deletions(-) create mode 100644 app/models/record_change.rb create mode 100644 config/initializers/record_changes.rb create mode 100644 db/migrate/20140609122234_drop_version.rb create mode 100644 db/migrate/20140609122440_create_record_changes.rb create mode 100644 test/models/record_change_test.rb diff --git a/Gemfile b/Gemfile index c1e12038..c41bcc97 100644 --- a/Gemfile +++ b/Gemfile @@ -76,7 +76,6 @@ gem 'html_pipeline_rails' gem 'rails_autolink' gem 'redcarpet' gem 'sanitize' -gem 'paper_trail', '~> 3.0.2' gem 'twitter-typeahead-rails' gem 'commontator', '~> 4.6.0' diff --git a/Gemfile.lock b/Gemfile.lock index 619795ed..a3e8892d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -208,9 +208,6 @@ GEM oauth2 (~> 0.8.0) omniauth (~> 1.0) orm_adapter (0.5.0) - paper_trail (3.0.2) - activerecord (>= 3.0, < 5.0) - activesupport (>= 3.0, < 5.0) pg (0.17.1) poltergeist (1.5.1) capybara (~> 2.1) @@ -350,7 +347,6 @@ DEPENDENCIES octokit omniauth omniauth-github! - paper_trail (~> 3.0.2) pg poltergeist quiet_assets diff --git a/app/models/distribution.rb b/app/models/distribution.rb index ffce0ed8..aa8dbd8e 100644 --- a/app/models/distribution.rb +++ b/app/models/distribution.rb @@ -3,7 +3,7 @@ class Distribution < ActiveRecord::Base has_many :tips accepts_nested_attributes_for :tips, allow_destroy: true - has_paper_trail + record_changes(include: :tips) acts_as_commontable diff --git a/app/models/project.rb b/app/models/project.rb index 288ea7f9..e3c044c0 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -12,7 +12,7 @@ class Project < ActiveRecord::Base has_many :commits - has_paper_trail + record_changes(except: [:available_amount_cache, :last_commit]) acts_as_commontable 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/tip.rb b/app/models/tip.rb index 19267e6d..d78fead8 100644 --- a/app/models/tip.rb +++ b/app/models/tip.rb @@ -1,11 +1,9 @@ class Tip < ActiveRecord::Base belongs_to :user - belongs_to :distribution + belongs_to :distribution, touch: true belongs_to :project, inverse_of: :tips belongs_to :reason, polymorphic: true - has_paper_trail - validates :amount, numericality: {greater_or_equal_than: 0, allow_nil: true} validate :validate_reason diff --git a/config/initializers/record_changes.rb b/config/initializers/record_changes.rb new file mode 100644 index 00000000..8596453a --- /dev/null +++ b/config/initializers/record_changes.rb @@ -0,0 +1,10 @@ +class ActiveRecord::Base + def self.record_changes(options) + has_many :record_changes, as: :record + + after_save do + state = to_json(options) + RecordChange.create!(record: self, raw_state: state) + end + 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/schema.rb b/db/schema.rb index a0846383..db0422bc 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20140608131519) do +ActiveRecord::Schema.define(version: 20140609122440) do create_table "cold_storage_transfers", force: true do |t| t.integer "project_id" @@ -142,6 +142,17 @@ add_index "projects", ["github_id"], name: "index_projects_on_github_id", unique: true + create_table "record_changes", force: true do |t| + t.integer "record_id" + t.string "record_type" + t.integer "user_id" + t.text "raw_state", limit: 1048576 + 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" + create_table "tipping_policies_texts", force: true do |t| t.integer "project_id" t.integer "user_id" @@ -203,16 +214,4 @@ add_index "users", ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true - 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 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 From fe8bfee7602ae3717d3ff7235bc6e1a5529ed0d5 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Fri, 13 Jun 2014 21:08:44 +0200 Subject: [PATCH 260/372] Integrated new design --- Gemfile | 1 + Gemfile.lock | 10 + app/assets/images/blackboard.jpg | Bin 0 -> 45819 bytes app/assets/images/logo.png | Bin 0 -> 14577 bytes app/assets/images/panel-top.png | Bin 0 -> 3284 bytes app/assets/images/picto-books.png | Bin 0 -> 2439 bytes app/assets/images/picto-peerheart.png | Bin 0 -> 2082 bytes app/assets/images/picto-plane.png | Bin 0 -> 2128 bytes app/assets/images/picto-tasks.png | Bin 0 -> 1612 bytes app/assets/stylesheets/home.css.sass | 177 +- app/assets/stylesheets/justified-nav.css | 72 - .../stylesheets/oldsansblack-webfont.eot | Bin 0 -> 21942 bytes .../stylesheets/oldsansblack-webfont.svg | 1433 +++++++++++++++++ .../stylesheets/oldsansblack-webfont.ttf | Bin 0 -> 43340 bytes .../stylesheets/oldsansblack-webfont.woff | Bin 0 -> 25112 bytes app/assets/stylesheets/oldsansblack.css | 14 + .../stylesheets/same-height-columns.sass | 53 + app/views/common/_menu.html.haml | 19 +- app/views/home/index.html.haml | 94 +- app/views/layouts/application.html.haml | 54 +- app/views/projects/index.html.haml | 45 +- app/views/projects/show.html.haml | 50 +- 22 files changed, 1816 insertions(+), 206 deletions(-) create mode 100644 app/assets/images/blackboard.jpg create mode 100644 app/assets/images/logo.png create mode 100644 app/assets/images/panel-top.png create mode 100644 app/assets/images/picto-books.png create mode 100644 app/assets/images/picto-peerheart.png create mode 100644 app/assets/images/picto-plane.png create mode 100644 app/assets/images/picto-tasks.png create mode 100644 app/assets/stylesheets/oldsansblack-webfont.eot create mode 100644 app/assets/stylesheets/oldsansblack-webfont.svg create mode 100644 app/assets/stylesheets/oldsansblack-webfont.ttf create mode 100644 app/assets/stylesheets/oldsansblack-webfont.woff create mode 100644 app/assets/stylesheets/oldsansblack.css create mode 100644 app/assets/stylesheets/same-height-columns.sass diff --git a/Gemfile b/Gemfile index c41bcc97..366dd143 100644 --- a/Gemfile +++ b/Gemfile @@ -78,6 +78,7 @@ gem 'redcarpet' gem 'sanitize' gem 'twitter-typeahead-rails' gem 'commontator', '~> 4.6.0' +gem 'compass-rails' group :test do gem 'cucumber-rails', :require => false diff --git a/Gemfile.lock b/Gemfile.lock index a3e8892d..945856ca 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -93,6 +93,7 @@ GEM rack-test (>= 0.5.4) xpath (~> 2.0) chronic (0.10.2) + chunky_png (1.3.1) cliver (0.3.2) coffee-rails (4.0.1) coffee-script (>= 2.2.0) @@ -105,6 +106,13 @@ GEM commontator (4.6.1) jquery-rails rails (>= 3.1) + compass (0.12.2) + chunky_png (~> 1.2) + fssm (>= 0.2.7) + sass (~> 3.1) + compass-rails (1.1.7) + compass (>= 0.12.2) + sprockets (<= 2.11.0) cucumber (1.3.10) builder (>= 2.1.2) diff-lcs (>= 1.1.3) @@ -136,6 +144,7 @@ GEM railties (>= 3.0.0) faraday (0.8.9) multipart-post (~> 1.2.0) + fssm (0.2.10) gherkin (2.12.2) multi_json (~> 1.3) github-markdown (0.6.5) @@ -331,6 +340,7 @@ DEPENDENCIES capistrano-rvm! coffee-rails (~> 4.0.0) commontator (~> 4.6.0) + compass-rails cucumber-rails database_cleaner devise diff --git a/app/assets/images/blackboard.jpg b/app/assets/images/blackboard.jpg new file mode 100644 index 0000000000000000000000000000000000000000..db08da9ffd32a44fdf12e73214eec9b9e3ea5cbf GIT binary patch literal 45819 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
qc`V#P(GX<8OR>vgP)w?Cq-)V4ki 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 literal 0 HcmV?d00001 diff --git a/app/assets/images/logo.png b/app/assets/images/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..b35f3e54b26220d81bff7e89b0d10a5df62502f2 GIT binary patch literal 14577 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 literal 0 HcmV?d00001 diff --git a/app/assets/images/panel-top.png b/app/assets/images/panel-top.png new file mode 100644 index 0000000000000000000000000000000000000000..fef2973d1f7ec20b2d706e03f8d3e0c58e991956 GIT binary patch literal 3284 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 literal 0 HcmV?d00001 diff --git a/app/assets/images/picto-peerheart.png b/app/assets/images/picto-peerheart.png new file mode 100644 index 0000000000000000000000000000000000000000..25008bf3d97eb6941ab26cb31606327debb3ff84 GIT binary patch literal 2082 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 literal 0 HcmV?d00001 diff --git a/app/assets/images/picto-tasks.png b/app/assets/images/picto-tasks.png new file mode 100644 index 0000000000000000000000000000000000000000..2c0bcdcd718887c4078e2a8bead14ec99d3f9693 GIT binary patch literal 1612 zcmaJ>drT8|96ug{z|f(HMVvc}W-|M@w$Rc-b!`u9b?sbfqylP>_S#X@0XsuSJ zaZohkFryk+&Sv;L4713xrD+O@AXSttlE4B%mPnbZgj5nqR+c1Pq=dzy2`)^SOK{Xd zPjD?SxT#ZegGRtfCNhjVu~O8M?Idt;tY8^7buNM_^~SlDsdEud$>lT2@PlLh*O(`+ z81DqPlcr@3leR}4jMtrvsn0B~JOBWp?HX8NaNio(e|=Rv!n$ZHz5GKe_4Sf0QPH*~ z?U~)3;L&Q9;{6#4@afET(M!*6tC*qhO4;E+%U4D&%uHlODeX1vd^<}K(ckhVakpYq zM{(K7{r*1GW?C@vaiH6EZfIcWx7sG3zUFv$PsCB+q+$*2n#EB|Pt}Vb#Hm*XRQiFC zLHH@IsuS3~IVQuL6H#6B;@fQJ4?jL3B}X?uvK3W5WZ8k!DIL$^Mx|u@?eLOiIrlP7 z#RRsVTJdw^mg^?#Wv^;5g^Xz~mz(Mfi& zfHTxf6O+QO)+L;ob&Coq_-RLP1hlyMci<@1G2hpGQTfjGs$~1O8Oo&Lt*>hjE_!2c zm;BEq+b^a&YMZ1{pGj*r?u8E;H^)At*a=_px;mTseMxY1^Og_H4_@(@*3ahGZ_DT@ zO>ev*o=wTiFLa-^`U1i$3A?e;1^GiSAAxE`VGF;wUq9XVV9;}CJsD@9YE~814QFst zM!@^i{@6ZWGLU=x?t90dgd>+qH5DCvsQtjTsNF}7;RShva~~Rt_wxLAM5gS#L78#S zrMyMy9hJ!;x-HHEbno+bQ zg>yOWuKcw=_2B%N+t9D}nV!+g`~cB&MB8NR-$#ApAFd2v9@$qAqiw91zAN^oWX9wB zB2wJ*BpdZ_t=e-=w?6VvNWxzc1z{U%Hz=z)e^>dQ#phd1!lhR4p^p4;ep}eC4S6HU Y=K)SJS9(GDp)UA;*JSD7My1L74+nH&X#fBK literal 0 HcmV?d00001 diff --git a/app/assets/stylesheets/home.css.sass b/app/assets/stylesheets/home.css.sass index c60357fd..8e36b713 100644 --- a/app/assets/stylesheets/home.css.sass +++ b/app/assets/stylesheets/home.css.sass @@ -2,11 +2,182 @@ // They will automatically be included in application.css. // You can use Sass (SCSS) here: http://sass-lang.com/ +@import "compass/css3" +@import "compass/utilities/text/replacement" +@import "oldsansblack" +@import "same-height-columns" + +=verdana + font-family: Verdana, Geneva, sans-serif + td.money, th.money text-align: right -.masthead - margin-bottom: 12px - h1:first-child margin-top: 0 + + +=button($top-color, $bottom-color, $border, $text, $text-hover, $hover) + +background(linear-gradient(top, $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) + +#main-logo + +replace-text-with-dimensions("logo.png") + +inline-block(middle) + margin: 10px 0 + text-decoration: none + +#top-bar + +background(linear-gradient(top, #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 + font-family: oldsansblack + margin: 50px 0 + +#main-container + padding-top: 30px + +h1 + font-family: 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: 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 + +.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 + 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/justified-nav.css b/app/assets/stylesheets/justified-nav.css index 0bfb4ac9..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,74 +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 { - margin-bottom: 0; - 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 */ diff --git a/app/assets/stylesheets/oldsansblack-webfont.eot b/app/assets/stylesheets/oldsansblack-webfont.eot new file mode 100644 index 0000000000000000000000000000000000000000..17a103f7b53fd10509de2be4ccd03dd064fcafa2 GIT binary patch literal 21942 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 V2qb#p)X#!*1maEuQUEAkWB@M*h|T~2 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..e25e59525fb56b13e8217b0fcf176769ef81fa62 GIT binary patch literal 43340 zcmd?Se_T}8oi~2Yoge%jW`-ZbI1Dh1!#Ej-aTv#OL<9km5G0fkij)u~Xo!&zW3074 zmUUUz8lTkKq{i5k8fz`<o2F@!P1BTal4i5n)CgRj_vhR} z(3rOS+UJk&>-Ck~xxdam_k4bw&#&|O+%V1pHHQ=d#^v)BSARaVtDGQ;ZSk3PhviTiAPTs%*0Cyx1pD;{}> z#mAnce$-y#88=sor#jg$+51wJ)FJJYPD;O&exv!8X0PU-v|4SBcBZymyG*-Fdq}6( z_3N(b=jvDC?}Yvz_3!IHj!TZqkJ}$_h@Tl>6W<*F^Z2(CS`r>j_BsC>nO`euK zKe;{m(d1ppdy@|*|4YhkDYH|mQg)}jnDRSAqG6_?%J8sZmtnu*XNHebr=`wHZA*PL z^NWqQ`6uI(+H!hR+zQZ#9J>vzgX7!S?D4HEg#DcHO)QN4Tx<~>&l_)N^Ko2)ZNYdiE5%lZZQ*zyE62V9 z*DKNIqHznW8n0%H$3v`oJeSps7qi;&7ugcDsl$8jR8Fz_Q5XBt_%rOT@dK=J{5-pR z{1|J(y)WZEOL47bd^`Kf_!`!V<9l%2hW)+RmZ8mZ?AynWv-<$Y3bbE2zK`9HcXf_e zu~p+?wi@H;0xS<<>sF4jrvT5>X!k7Me*oJdjO}^6^BDU23AW?dUch!@d^h_Cyz52u z_Y(G}@Sc}({WP|pVmpKFXV}i+{?D%iz%;=EfKU{9jo?_xWEzM|-35Mz1{`{&WePq4j!^HX^5Y4mY!{4DTz2zWdMJRTA} z+k+8Y#0X?Sagils-1UI(62^N0V?EDqA3qGdT>z|m(5e?WJB^lG@ZJk}?*;M1Mc`^J zxUU`D*N4`1;Jh6%EPp7t?tVPgi4m>>rU-`}==)Xlc?PY{q2Dd&Q)U|6>BE&?TzP}( zaJC+8_TlVR^g?txC}_VIm_3emt8nKWIB^L5o?zYM0hWSsZ(_yc4%|Hrp7#NVr-8%M zz~5=$?h^2J8ZEbB{F^ZTO~A)#@urmm+8RMog7O^8#QU2tKVQV$d=b1ffVuf1_-Ozw zcVk|@hm#%J(oZ# zToPOwM5_?CIlyoj`v~^)K;ii~F2S|{vZNH-Lcm;s^D5lG6PT-4?3m4`F?&zrZT)!L z_NVa7Ic$W9^VnX&mW~m&0QS=u*=daIC`NV~ zBRh(bJ%f>*#>h@%WG67P6ByNLjOr-x-Hwr*!AK5b6ld_3qj<{+yyYlnr8+AvVm6$I z#Jhsoa0RpA5@x|Az>^59eF$2<2tJvh=XywzE8y%)g3Asum*NDcdBJJhz-dRpX&!Le zes<^BB|+;iDShCCORRhByntXCAUFpI&H;iGfZ!Y;H~|Ru0fKXY;2a=04G2yHf^&f2 z1jf(;2+jin84#QY1SbH&X+Us7%$9ZFN0J>h2UIz9794j19B08*!fiV^>jXIK1his4 zdLVA!gT99VV;|ZaMVngmHzc@j1Nu9LJ0wSTq0jv+Pe_fT@h5vQQ;D>YwvNq)nb}zPNIA4x^JMP^F+0_BwT8Z=f#~XpmM)tsXC$Q4Q9z-AA$`y>S z3*+m8wAcnoaSD>c4@t2N65?_2Kt1$&{&+Lw)&)Q|1Pq=622TNlgTUY^U~mu^+ye}r z0tQb3gU5lvHCbeqfR2+YsJ5gtyW>7z9R-0|SGY%SFIfG0yG; zzUxOmMaxZS*^ZX&Vy2TEB8dR~HS!wn?#A6d-0g#`xD)W!k1|}{#^KXVa2A+b54ms_m|F?ia0Qs#1Nm?kSb7cbsmFWj zfu()G&<0>=12D867&?nta#m39960hS-qe86k?!ecIpFd^;O_v&vk^Fa6=UiIw;uzy z9|X4#fZGS~gcoDlh%t3xOdBzdjTpl!y#D~+zXO!p3(D;UWKPV0W0(O07{f-4VIy?s zVT@xV^y2`=vIAq;ff*}<&lUmOi*dCEPc9*hjctV-8xnl@46{J5?S{kUKV5 zAXk7bqUJePtek}<@dWx<37N66#cb=s#n0N5sYpZurvrP z4YGTgW8@&qAC;IB*3WcEW!K0BRxq-MxksL1Ge&kY&&VLV4LuZLmIp_cvDuhIA?)Xj ztYYDj0X7%sk =PswG)W4UZT?v;$3VGA%LOR<&V{=$)UtQ`9a+^ZaUkuAc^sKPUg zM?PWI&=|La8)`uFJ4W7QO91_yc;}ZCFKZZG%kF{>X&kL*caIua(`Xg@@>maBI%;Mu zN(XBlxytUrzHMX^yBF7%jrv);Vq^D>tz#XduzAL|u$8!e|LA7ciFZ9Px{5tGx{Gxw zhgtW?DfT2_`YyJoK-Z@M&$Afs0gUqy#wO$4QEUU)j$j+a_8hk7f#qWu$4{^w$MyoY z6N;Pt!&o2b$g?GM;v7E;CQ*39j{S4bVT>m+?^VnX&M*0LgQTYTMM>MSh zJ>LPB)q)4QS;JTjYaA0^=#Ex2YV3LAA&UN9(@NiuL8}hm<+y;f}fs)-hCeZ9K-ofaQ-r6&1rD=l!&S&s8g(@ws3Ex=8{1c)Q%PTTKvrIa zth@+Wc?PobB4p(mJb4hZ@*-sAMaatYkd@~lCoe)yk_PX9oFp6c806$7$i_2}jprd7 z&j@?62Tz{?M2iF_V3j~FbwmHs+dBWBzKw8uGo(X+at*M!5%@iTF<$&j@DXcC^xgu@ zEyLJ@=!f*&rWnpgfpb|8a-nlFvLj~4rc=4?j`l2B)X{kPHT9PS>Z zaVT#>j-6pMVDB%2wSNa%)j@yNkD&#ugolLIsTDj!wBLbNMEk>Nbr3V-0>-i%eW~;( zszYnVc!0)!BQ9$2CPD2dg(Cqk_^4H7kcSM=%pTEX_Ix<4XQnb=MUhy=h5yb*i^kFLodmg z^*zu_J(&4>#f;hs3c^Bzv?QBoC36C*wa`V~fUFuLf`(8I32&Gzjyb?a1RMEw#TZc~ zJe5VTTdHtvG2p3&6>vLnRinHPx*vnC+Y5}4#ZR8Zi=b*ByxJ?^*z@pKufYCUE3Co? z!G#Y&qfc5QS1{AAO!Ybd&rOmFTzrG10_4OkYcaYjz_Lr6^7BQlDaOz)eRpPTW%Df1KF8_iGhXSZB`-xj>@VC;QzOvaLzNaG=Xr~anQ8EBsC za|T*~WCvk}TAaFJ9OTJQUacXiO;Qz9o%)O_e<#K+!5eVE%Vw;Ibz_D-$-c{~*;Dwd zVNc`l4)!emYS{t&En$c7SI1=b9J`Y}&t778vs3J6tc{&xudt3-gesBEWQ8}xwz2>t z{vozS(Zz`)I?!qp`6qxz%lGkp2#?iF?Ab-FmBM9u4IYq~_OB3@L;LIhmfV5Ey-epC z)vts`V{zS5IwX?))q4uO3;u{@?!g@4vC(-~Z-oY*#^&jKlk9) zH|a?>J}l^cjVH3#*zeg8H**`G!xw7~^GxpGUhd~}`C@5@w2K?r2VCOw*;ejj{p=G~ zsWUM!^Gung-67Z3H_J6&T`DtYX_l<_HkT|F*ETK9`)8S_6=wNlJ~Qu^ahQ^$nGa;;$z}D;4!LZpPu9+)M;%Vb*PGw4y}HzfcFkk950=`T4q4}ImaD$H zRNPp)6#eODrnI*BWc|!&KK~}(p7+hxR-4SwP29}rbaAQd`la}pW>a3o?~@Z|=53;v z{1pA?$r{(~&OBLLPy>&z`LPv`EzhHaxi*JmsqHaw)}Wrz3lr7R8Er-fUXnC3@1F!X z$usl(a-6rdIWKRqvugROyym=)cC`y?mqKsGtMeYqTl`qna_3`tk2%HboHA=@mdnr* z@X>4KvK4fKXAI&^C6}`t4qM*k$AAVrbUVg&cWi7JADX4kbmm=-z1*4Ce8*ijhs>8Y zKL&tqcRuFKd+hedoXcq>1P=Yuc#K2_Gss{f0O-Jc695>fv&y-A)v~FO(4%QH0mNfl z32ikU&d1{9y!z((wv)J%KJy4G<7MUL{ElNrb{`YJ)c9_UJnylFW@kG_?<}`r$DQTC zd|5;D0AxqSedPl@k7Fn2-6vaDWQ&oa$BdaWt^zOk@e!j+tP(#Ry1OBP;#gsnGynX7 zIIaDIqNrZ?tN8;O35P7Iq02hDJP@bP8J$1C>3Y!QFu5EiN2QdfOy|3mPTk$t{#T{; zRrJfp4{G;FJ9Hnj4CZCB&L1_#4RbluFSE;o85t}?>ywTD7=tut z0@CSPZ!{NOMU6TDmpB1Y5z)zQ&;W?g>;+OdL?m;$E1LXGjh;GxiMc2owz>jkE$yzp zpUzpOTnp6(tS+EehtK^BhI~%8+aNwC~tmsz3yWIXU`tMvvqu!4`@q)mvlBs_|yr)TR`}& z+^Y;(v`xe>O7HlPG^G0k?U^nJj_7?)^Wd;opG%rrf-Ki#OlgR(fJ@Sd>4PC>X!e|9 z9&+aE?cB-HUkID@B==sqQc+%is(;T;={0%-cPp=Hsz!gPx#zQn>fCGJIk`Tr>PLtX2}n zIi7KVSb}be+M2OSX>jBNse7!wdyGGgPjFURjbFZ9MG#Do?97&JU$9qYz0tz|9KbWo_>57h+#2uqJk3v4lbEX|Hee{n9 zScW%ID@%oPdZ8>Cqngam+BlENCcV&H{Y%G-Z#%LUF8jVpC-F)beU&EtGc;*O~ zGSVo_ITy&BYk&`2GYJ`<#aY>`q!D3BHo-k3Z)5tz8{DObH%)qtO+MIc- zhH}|7jv12{)CAQnyvF zAT<~G<-E&LPs*_DF-9ZcyhLzbDfXT`Oz0UAIbteH*60nXS<`IOUAn^A(=8D>(Hu3W zMWDZC>(NLPkqb>_2`n)+&t;zB4^a!7Sx$op4ntRG1YxMp3C(sFxJ@_>7i!!gXNEzO zk#5Pfn4r4TNH3TQHQba*`pzJQTYvT4GcT{%`SjNP)sF;Eh3{$azWq!?-`8tURX3X1#JH(e4V29_01e7JrQ=?6v?^@QnQ9L;8;(LrAwf*&J5EcCY~t zW8UaO&G3K$Q#hKb86KQhvwdOlsxcwb4jz;TU^|7*T^ZoeWbVH+p8C>U9+!#$La+O zI&rhfgPThm1pgI}59vI>sh^d?_wHc>@Qb~2N-$cSFf1F22AqV|N;3TF{ql^<(K2vi znNgmVci9wWreQM8W?@%mj8+3_xhccZFJV7x2Kdewk;_bSFd|o*quOGiG1oL0mt~t9 z5%??CqDfptt~3p@+!?_oxJX8pB{NtwhuAWQ2Z{M$ow#6<;AnzLVS8l&ixvad&5pIS zzy>Rn(q@O}1O>JYQk=V>c*Dlgb#jxZE97b}_AM(bUYf6bVk`0ZyB9V!ta_yTyN%yj zwZN3^wRdgibN!xRtJmnU)V4S)=H#_H9MgTdcDpZV(TDi% zFRZMt-n!7+(^T5JZs#-Y?R<9Iw&qYvL%p~9#D?N>XUPLzV?kylXZAvkv2=#h=`78i zANCs)TVdC;tZ3|CRJ|Lw+HeUUBiF63GGQpDJvRUvm zm^hjyEW|V;aAt87NTJ#0u!GFF`jw%t_<49=fB!z^oWEtxN@=(9_V2sz`SJYY%9sCmRS`C6yL48w53nL@DNPq=vBA)ExpisIG*>(KzJ6iz<_oWL zZ@qFgtb~=T+D6Z-PjRF2(NnK_KKd3JUEd;@i^t#B?tmSbgg&6pC;p200j}r$O0#lJ zY2mw+CVmj-KUD6aXP!`w^4EC;HfRo$HU2@JQ0l@0CHFy_G%PI9Spf?Pc9Y|YwVeao zm17p=RbItQNY-#x$#+OAr9F_)=!5(ZEvA4-FbxY~X@g{p8g!&zEV(#LV5;adW6I`% zQ)Wwr93B#vbQ2TOD7?Ue(xRT%`nmP-?E&ojU+XF2b4wzB(;F!%iS+i)E0NyWuKfP3 z$1h#rtG99Mo8S7@o67GVQ?_j0yhRyjy_+{9Z4Vi5u&L59?qtjXqD!(4Lrw8V4eA_l zn4B4okkdq-=f^z$w;J}|zm*?r(hiIUu3gimmdv2MxmNVijXvDaBLhYfa8?3%hF{LU z9F+iYQc=_+`XdT~`)LA%g-DM>zq$oHSR@o7o#DS$t!ZqPv!TxhBZyAN}KP?Z}?Ved3~~lr#VV;TTZUse4zW_!}He_Khs^>xwWMw(7B>%TKUFs zxGPMw;fT2pX}d7KAX^@zOd%+fMgyIu9Ug?5PlJA&PGg?!m%W#XGIB0SZE+plvjsJbPG=z)@B{^g2t=aFbz_Y=R{Tv}9^Y56#iU@Ij-| zTt|ul5dad04OQH5V9UkY-F)lz)8+qTo=IvRJz6~K>{uLG8nN4po9A~{xjl(I@tLO= z*X-b$p{Mp_6!3U%9zEkPsq|F0g~RvNc;@DY3IzPfRMYMT-E*Ohn_`srg7Vo!`3z9r z;M8S<@&*EDmQcquCj&G$p>U&FRszQf+D9485!qmpO%XZ6EN8)dHo=@G1#6xlH(4n9 zIKUX5lW9rAl+BCNbHUm%gQZZat6kFf$p^dlRQ3$6se7u-R%gHX~!oUCz zxz{%pH|7?vexSB3VwZLtRz{oU?T`Pu`}o#6uYcc?t&&s`NRu=m7#2$N-3AtY4}CP9u-E~0smg=5gYNL-4}AbR6)KHV|^W0km2l!Yl`i^%g$ z18G{AaX^MPRn?nDy^;9>bl>-&#&Kl@Gt4?nQx8&w{w zV^hQ0O`4BaJ-@Zq8>rf{s#21w{Nt>+{G<2zHxF^`Qn{-4rLV5qwyJVxb@K*-vxju8 z<_F-EI8|oQOzk11nod8`oxFauCg$_^kX<0!CdS&T`d@9X8UgA96P5tkAFsmxXcc_Q zO$pq@i?xkoUTJ9L5X=u3Pv=2Co2M%-jHQ^#+9xy{#8{>Y?ZhE}p!NlluZQv|{=%^p zYnw;(Hrr;kwenZm^a8@e+@ePedQH};W6T;({6D~({jKfDVauVXb(R3IQ>HcU242auESP)ftB$|@}LKnC}Xheme z)yRB~={1d=I}jfenoqcf`FcXEo@d3|EMC8RvDucBnp+ac<*6+JXV~JqzvI;4I~V!w z%JbUob)j;L&+oN{LPcgzQ)~O2ZSD2F^W62`#ycN>a20?4+F>Gygtg)!y&W;T`LK8P z#b&lHZdeXLsRg4J&2aP%;IG^toeqcC;EzVg4XG2n3NKhrG>VyBg2NJHvp zZH$_67Rq20^t=-gSCyE|N419O-ol8EqyYybIS2|-;pZpfogBD%Vi7H_^OMgVdc!feRz6V{k}hb+<&0A=TP68Vqd&gpP)BN z{(#rNx9`}YAs<4}gM(Lj{7a|b|4`ZEsf{?@o`|=h+;gwsp;5@;_jGS!_9SABlFLZ~ zy*x=+ofPTP!CXy_S)E7!5I+9=QZ%pfP6JR9nh98+YCgT*bG_asPQLtc~BMpDtVUx7Hx|z!j;HEmIC-CHU3olbn>OLJ^CfUc{ zAziOw+c55rp+C%6ah-wn;sLB!dZTIa!viLo208I?{?oY$63!m4&Is#ef_7%+O+hF~vR=F`@c%Xn_veOcXSI{RNAgRc|@q zZR-7%@>`;ZMD)-_K#yc*Wxg0SQZVu)8o7Z+KFu$u!>`21;Y_foAq5!M3oOHFFo$MK z1=9(^Kx+;c+H@Pg${Pn-n;H%&SC1;my!z0I7T+vK<98mmoNedUqldZm-0Bgx=B4$; zM>ez{-cn2W+XcQjgmJ(XBjerVQa#39+RQOOA62x{u~7{o)O@XkW!%|(8a+e2)PZLb z#4|1pJ?Gd4U+@o-k|uq3)Mb&rb-j6vMihtgB4Ve~$P7r`g6{~@nSw@EKCrNU}O8gd~hHqSl;n1_h8Edh(LUhM*EnD6~K28izI^PsL37AceUHo>;K7`B1MwRC+(#EkZx=+RiC7>2?%h0t~@yv~D zG5~YZot1?<3z?4g<5$M7NYvg;ez35|Qwcq3{%AVkB~=3sV8DZ0@a&urkRYt60=K7H z-uiO)8s&I=y31Z%W;@pXNLTl-v~?r85MG4nvqb5@1LY<*EmH2k!>U zJg;m~0_0zx`_0&0ny6GU)+e=3JgaTMvv7O`pE$N@&W&wSj)OO}uc7Z9otMAJejgF;6vzzy-`Hb(8F z!hv-XV0Jg;XUrFO;_%P=;_2km5ucH4a98a4bbv<=?627K$-uy;dn$PKl9~hiYnCjz zeIJ(&)GU$S=~I4lWz)s;{E#^q=nhMf)ClWaASxhTBq8)p4FzWDcC|M*Vc_~g&l zjlIejU-;XIE!$Js9I!}D>(;;BGteCvTgNx}(yQl1s@(>Gzl}OtL$$Ga?7`TK3?h7) zOroBIkcCE*M~EyIh>QS{5!HGyV?^_T$Qcn#(l`sE8n6jW(M0%9EUHTehbBhk6jL;g z9IBw1%-}pz)I25RQ54Rc2v*)I_5pkYt4L$xhu-s z8`piXp{dSQ+Tbl&UK6qMrtZgm&FeS5d13Xc`QETClIsrV8df*Ac7D^_{Nd4K4=ii( z%?s8BE8He$$ma^MZ*TllBS zOObIAO#{y&5SIv)&@4i(MKqC&77bbNX-4J%*cy_c!X%}|90>Ce&*JvoySZ|_*xpgM zGhED-7bLs#u5xM59-glm7;TWNozrix9?%?wyuONY(t0H-4`2z#iKzm8P0NR532N|K ztl)!9z$+40-3D3<=9+l^1aCzc_L!o4s(a&FvhI@+s9J(48FF+F`b=hDMxQC7&ty5Cwj&U?K5n~1H zuo!6+N%UVbFXI(w&hiRAU-|j*0p-|97z5XmHRne|5Vu{LCuqaC#@`35TQM%!332p1 zL_0cwJGb(^-|>~o3f`+cjd;@@-fQ8%R0=dH(tAoHKQv~=vlN5ejAv0hi$yUK?nwwr z9RNZ1EBzn+0V~Z9jQvo$PqTCEHOUPeh;?+dSM>p@CY(N8+=mht4+8a78CQ{u@l zzx=s)vPO+3Bdq+ncrqn!G$X=oP+Yo?KYIruWsXeJ5~*qGnDGWIqYp}ItenOyQR0W5 zNM^~vkg?TB$zz{z$=0ubuK(KigkD}H4G3FI#D$2i&Q&~Z_m~J*i@K)(;r-3yX@P zX*f?Tiq28_I`QJk_f@{m2VYMIUl;1Y*E3JbGmS@lGYjD|;djzyVuSh93z0-A7=B>7 zI1()Hp}5R68}yLhG@#2t5EoHg3dyi&5#{uxk$E}5($OL!Tg-4|$;4D+G~kk1s#pw} zX2T1JMR+sqTu%{fy^hv+GlVm1@HMQdZ2fMXfhWajdHmVoEpJty^R8$N*9G0HUTNOQ zy_L%z^mx{V{j<5g@}UNwKTun!)95uiXIM&p_g7ty>@L3}R9vQwPhYTT<+nnWp3L}@ zj>@?p18~{}Jy3(WV1|`Y%mz{jr||SuoH{6YF4*8@wCpLyz2BO8MC~(El9h?;>B!Nh8gg7^#s)S|F&O zj*+@)q**av%$^go?g{l2K@sUy6Hx=KT!vLojM*aVkM}&&5xh6}yL3;m%)8R-_j@|L z#f5pPqt2GH4cp&Tu1V%~|Jc`B>gaCib(RKl>l%M}prOv@D{;2BeLcTQIY&740;lmY zoWdhXofxSImFC893i}a!sp2#jYp(-v$%#?2Ot8V_RMq3f8ls3@2$NWiW~hM+WC9R# z6iVK8>xb(9dVV6;DwhJA%TE2(Chj#BHCFgroSvFUo1=D72PL_-4oc` zrzoMd%S(I?CBx+_XB*|0n-1n5)Fq|m z&H!@ZJrcviGMi5RR9brO=P)h?>qZO*|pewQ>@P_Y!x)KCX z^-^!`f}WRqj;;@LpFY!5=5O%(^iukof84t)WNWS2UR>*|X$$P=lB*tX>$~W7R|T^} z!A9^0a=#>s*QX(3&ndvhk`OORhjJ^5WEPCHxx$=%7}b8aP%HE2RgYK3sn8seKDNw|sj?i6g^1ClKGdfBhn_ z-RH}#+c|gb_NE_ftf^iYZdqGh$1Ps_lA4Fy0jGaqrMd3@?0~mrahcECyr6vfme%e_ zT65dCTGv(B4ROhOi@}hlm)y;d-@mLb`88`vQ*gzO)}|eE9LpBpUux;}d2%dzttrvy zGTUA0NvX*;&9dsdeBO%Udnj*$Lyiq=DF@Ah=<5JJZvtu)(`NbQq|4D{Of)Nr1akf5 z48n}#x^My~h^0h5GVr>%*Sor`*pdhv$X&mwx$e6qNNLoh`a&+tqTpDC^nU&0_ZOG> z`Bj?dA}3PlusC>G6P5wAY?MResFs21S_zwA>qPb-p~_>~9*o8zHf%z%nI1S)sGAJW7*0BL|1^s$(nEfJpklguOX< zT`1Xld*6I>OK~y`pR)wGH_)^2 z)aIV`>xZhpK392*dp&o|?+n`QY5vBFz*3qMUTCUep1i+5;9WfWYyNL;uWAY`FF~{k zIu_%N$9U5)-(j~19c!JyaUS8=b=?{$AU@2CeER^&KH`vgxo*gzt2gr4kOqd#h*(Ivu%8$cn?1QTKr7!ulI5)-18d(+D*7D}20 z?YqLtumwJSaE_K2mw)LSV&0NnybOKMru>9#!EF)M2?edd$Ntjr;&hSIUvxF>%$YG2vSi{PjSg>{@GZ8T>3^bEWDAMY} zou?KP%dc)jXho!Z{otBMYuz2IyMuu`Z*hm~bn`#-)@)n3U`6}7(q^m2>h@>4tBQkZ zXKPw*f#$F5+|lNnSKzYS-NBqYy$9Qyy^R~|_uf(MwN6j9+-7rSCfWV-asWE!xFm9P z_Co%`CWJ%pl@o%9WCFD|RsW}_iTn{nxuRsVAWs#MPRJyzEyElSn$*Q1ScS;y#X?Ys z%mQKt>B*-yyGksUK>qCxXYTZ0{6ZOeQd2ed)Yi?CB4L^hO)XDWWBGE({m}gK^l7k> zYwNeIh*bLT)i#ZOAX6sd1I8)w0S=2Dx`>RQofGPvn_e84anA3&)GbYnD`P{|{5oGB_nTFH7^ ztx@}*bpGRcHc#6AI8Wl!l()C`Dj#jzOmCB##^{Yv>CUmgm%fB|z704IsX1j+eg9lu z)%QbQsmMI6{AAlVl_NZ2;YG?nN@Y@}@(E8K`#qlhM7dA%44$>3u2HOMz(%zihh?i# zgk&;-1Im@MV!Q+KNi@IUw?waF!-60`(cRB81Y5)FBK#!t3d!s+=2{C9leX+M7#7c4 z)VbKgS11?z_W^`YT~)KKsVO{30cg3achqRx#`-0ZV<+QXF`eEL%b}0q zy96}?>iQB4!ol0(sE|jjE9o!G(~F`eOxvs?T3w2oD0?^q{CgW=pUfDI$ZRmB!^sk* z6JTF8ZOAlHOM&H2H3Sv!4;UTg&ac+>^*C+5h2=AC&Yr%yuR6;e#(-a{Of}|ul?OWS z^tk)yX7f!|8=TIsRq;*saKGDA*Qs=RbB#zF$LGisO*^Zhh5=%uwEa_bU%K`XOD466Rb(|sAY)!M_w!vPd zgDx3&qP^K4`*QO@@?4HoOCp#mV9M8d@Gr?4!PGR4{$ z*a)-OqOQ;juW}BYO1SNAcw|s{>)JdRhBvsPbM3;8>T+MJ&u?(gHNE6J2PDFNvaOyd&{4%XQeynscthf7>4uK}CE>C+mCv|$mZ!rK|B_uYZ@#p|<57NoHQ81B zP>t}ZCCO#TwY1AsJnf1BEA4-Thb@hLM0Fw@b?ighXTVRT>}$vsBzd10jM}ickWe%b zrZCb>N}wsD3KTNL9TRCrZX7s_(M*7y0dW=}N2>_JY#}bdKsF!Iq~uH|^%pQbXSC%z zXNGaYqBdAHatdMQNJgM&zOZ4_f;k8;QkcdhQlr(Nf`jN!xd-5iV8~I(!@`U}c)>B@ z^Flj1tA7d-n={OnrS0EYwCwacx!qr+MZH{x(Owf-<+rCxA1EIgOj2t8Jb$pfz@+Dn z1tl9^TlM!}ugrW_>he^TSGb*?c;!l;`*jw0#YOB<9Q&ilUxa-`Ic!`Giug_*reU5@q~fJuHqQoH z^0E=lmn>NjafBF;3GE?1Q_RoB#V4kk%mP7$Fsq(pQd~l!XoO^>>=ZQ0Fh7@SG+V6G zRE$}OeddHMVKt>O>{e4V+;JAl&ahBL14RQStcJ+G2elhi zWbk(tYh}-{f%(MJLBtb~J(o)9Huw1Dg_j3ssU=E_XDvjD5szF7_n!C=YJ;6F7k;Ev0ITSuXVf>>byjKf0Bb?Xqh4`dxujvfj% zi#aXPL9V1Do0XP@+yxD8TkFb|Zi6#7!|x7%dD>ym9gX)dj6B#FaQkbDYu)yiTi6`V5u(7g{L%E4^;-glCj`ICcFLoh+ zbOClIe>9PjsYN#BV2Vhdl2g>>=2_q;P#KAo^4y|9cLtjY28a~VYBT~}RKZ4>R+Lsb zV-}2flQlnafuLvz$*4!sm)$MOJ|N$X%%R0f6?S=SvSNconY$pY#*ZM;kiR`O6CxPP zU6Bm@3a{gJ@4wF#k1~Ki&ufPcfBcbr=#c!;$A=H~mv1=O+1nkiGQO8sQx~ZAWv2V8 zf|YkCy=g29t^9Up|N3(2T|9LgM-NNT(-rC__W1ukbm;K4&i}o!qS@cuQCI4#Zu0sX z7P?ESmi73WD>fVy^!QZc6SWdf=)5iP@-bfqt!jNSDh&$NBaeSDH?|%jG$73eJ1{m~ zEN#l!MS})$L1@M4WGZShGHH5vO;IbnTBM>zb(9npEuInzSsOet5x`-vNxy6#CBjE|S}k3ZSEbN09Gr6GI3 zS6_3xyYZsYw#1ohTja3jX+CVZKjiTxr04R;uFaKo9Ui|m->y$dcVw5`V>4Q(>9y@< zHojYTOMxg5mg5itQh#eulaZ_Z2TxJnewxSe9I~R7f0fety~?x7KHjG8 z(FgJ#*&*!?%?8THpdggVQLL%sdL%xsRU#6lJEF~GO@?SQrRqX^1CRR#NtvUPGz{~Hy-V~{t-?0@o*uQQmu_vb1`u{X9 zpW&^_e*R!$pmbeRS8;~Y{{yq$ni)Y!H2j$T+Sd`g-V5oV)=_9k*i))1tWMbZ&`qt< zCu7Ol*Uu2{p=&f7QD3dr-qopj1cR9>TGhIG;YIufb@j1q6;W9~k*%Vdw6`ppYYQ~3 zE$?k@#cpBw+NQwahWTNir=hs`F746sN16hyt-a-sd^r&K@+0NNcX@nsiyJ6Di(Fj9 zXQ^&TgQ!?6!-u#^{Szs*d_Pa*2b31&Q>E1cpE0O>3^NP0$M|X#zJ@H7N>_9rA!h>F zY*Zscvs}RFCi{h|jfr1SDMH!h5=@s^q#u}r{8|82M5Z}eZLr(Pdq8Sa9A=|dx}?+v zI*Pna3q2((dP=vi2$dPm8yl+qwcZ7#8R7P(d5Kc#^X|pY+=_#RNA=wgVSTaZ!xvdvl+9T6g`13LDBei%l61;~L1uziO-K_fO&NZ&)3K-|K@ua?Km^4gg)Es! z%_Ei}Wd!Xc!rFOFeU;Y(CN{1NA$Yh}(%1SXA||fZSiFQ;$J?}LVfUrskJg9C7m7zc zK2#>=7%k0OP&zLDLJrs~*3zyg8kKFzuCZwy{TnK@4J}*m4QNL<>keFN(H(fr+qMg! z0G22CD*n239Pr1!jd9EBG7_Vrx(uyVUnW^emt5i-2;U*;C`-Y?ELCAyVN7IGhL)Tn z>d{G?Bh8G`GCnx{@{eix8f9h%nVr-{tyZjF+fJh9>!v5bx1$z^OL zK#rOFtycX@oi)99b-GfCXfum)ZzdJBDk|{p`rfPCb zA5WHw1*epXEgSfrl=c0C{+@Zd+_C@D->*q@}~%CG|M3O z9I?42R)Xn!BBbT9zJ}-@eNO~uv@{&eMxstk?qwka8q3oAd@b;eUWwWJr44o8X=(O0 zY^bZ-@Fnjr;<9}1Vyj(m@wwf;tT^qCn!mm`+`epYO>f)Wxoy2Qx#eM}*T3ApFzj-M z%ZXPc)(+k5oY2i-XyybS6u<2}Ub)J1xuhtYCl8NKDCLPgzNLlSzM;4<)?kwH9f)jr zbvI%(&ku)IF!WT6B4g62aZqtN#W$VYl4e>r@Qh^b1Ck@${5#F6;otHz(rb@D{;sm< z;wMwke$A=Rz4mhPcV6w6{A0gK!-r1JgVQkv{kyR-gdy?d+x$^U9}LDwFqOn}{j%>e z)dG-!MyLl>^EBP2j!u+f|2auLIo|)E;l3mU_lkfJ+#Nk-+dD#KsWHJ_QkEXRuPKsr z6BwWU=U*Vlu}TB{mH@UQ)U=ngMev!g+m&A^&8xmhnlA>wku;Ac&L#tL78#J0RJDT? znWD0VWWr1rOXpVPMheNUhV~{FQT{T?P6*^Tk~eZ?O@C)s9YlS_1DhffqTcJYLeyLH zWhp4x1tNQ3Y&4a(k|Bqmj`hThLAKmzQ)*$;6Yvnt&U5JFcfI+fiL_l6m&oHA3 zCfm#qShFI|AZ8;B?r6RX3)fb&s`Y5)8s1?nZ(fb7pvGf?ri42ngP_F8xvJ^tIDK$^ zq&8FeUA#5ZJIjS4n1;Hr@^N!fqyv^N47142`&aQ6|6jF<5}1I;?k`=tW>ej^_J*GM zLP`zE>ci+kxaj+x=5Ln zy;S-ssxFdKL>f^cRb8mZB9lmDkD-)HYZ}xiBqkZiRht!f;jxG*qw#Yi1HAH%tP}O)v#i6Jg_ZppexTgJ>!| z2CS9u0FBgo`>n30^D<{0VqH1g{71&Y)kw>zM-Yj2OH#Jd^OM$DfBc&FZ`@25;64WDL?G zzHk>rkN{zxn3a7q!+vr|n_{$}B=U|r_p)lIC;5;z*_4&)y1iDM&)`|tF%w9(+su}1 zOPyzntC|$c8s^RvH9wc%~vyf0^E>xs~L;3hChLw|L%;9#wWoL*P6f&iH1}P zhk_r}Ld~fhVFEi)3Cj)h7ax~X-gx`T?Hg$yE2j!^+lY~X)C^HJ#{``30?uiu*tm`T zlp$evaE59kI{f7Ci5$xtI0G5MgE?y5U5=5`%LYyAnMutWruwlz%{AG?HQC@A8~uW7 zvZrtj(!9kiv1Lzkje0D&#)YH>iiMa=QN5k8BNC#c4l0L^hlrmrYHVYo zDhb)cX5PA=e_wNHY%M>gR7>kdo_&a$E&MCWkHFDuV^v3^n`1@ps1;GtMSU1mioC9_ z{0`0)bQzdVo)9ffp#I1k99*ckB;~5w@1maVR{gjj_iyNjPgC{T)SjH^X6jhLgCeyvHkPnMsE-@ExqDK1+|U!L z57^=Ac>Dq`6fEA@n;L|^p}X%*Mx>{97v?q*oEARH*AU4V0*q=6H}FH&*4G4Dl%G&R zS)BOH;nkfY4@~62N$*^5PnsR=x2n(&QhPn)&7!r$He;<{Mq54VTM;!B@KW>5lVHB7 zK0*M-UV)M9!E@ztelaGBYT^;v=v$e42&7DUh&rKb&6(S)$a&a_!wrwtN?vZ4#gb!jtVLwUGSYm&`M;l zQhD4q{wZ%5lp79GRit4XzktZqtA~!av~owocOI5H#`Qcij0teO;s7s{4PxuQJ?+tZr z{eMR5PN{nf-csuVff}_Q4m2~v(=5POsnqlJYjaa=|e4$%|~M4H7&-?bE#OcaW< z&>}O|kaA7W8Pau`88+Y)>#BdUIF9CIY)v^c4&`xfM0-A08uyOMkiQiLa&>%3&He*5 zb+@XI(_EqHJF=b1J0276O6A)=FLLP|PML{boxM||6B?S{O zB5HJUg(c&1)v_KY&!CXu*=S(lu(ZWB$>;%l=(iqIq9DD>ZnwGAn41ON^ zFPx4OOKM+;kk|jr6kYb`v9#lsQ+IUBjSR1i&0~LT9^Xb)`XW>9#+j^Ilan)9o`Di4 z)o2uxIhsaMid>*emyvFBlV9-X<}(Scn-M|RKF1x1e?fqIZUC9zL)UJ_|0vbBjSNnO z5}8v%=5rPpe^dL6wg!5Ug)Ipjkv4Y%mb?Nl@vrjrFDcu^kMh_{e0?7_W!nwc;g7vI z-mY7v4~Tre+wdJoHF?(q9uwa)n5g~FM79}t3fX3`w6CMdnu)-*#ItA#5rH0)HW!~` z7hE*}&knsvF++5^%V3juJ6{m%nNu;n>x8s&3O2ZvQ@S zJq<$^r&mV~q{UX=$MCf0BF@HVSGVAxAQ>$~|vt^&>tT06~9K^&<(! zG@3{-{S!?`G?pwYv4y$-_xVCdVRPNG81hF#sDD98bwJG^utPZ8Xyg-c2-Nyjj!i>Ug8J zY<1~?k_kRn{TjxEr%8Y7D%iJalB<~#Uohke6ID{{J;9sth=5O4d{RhhO4X;N66(g{ zPofWJYZ33G+ITx6lnC`vK|IyP+o%j1K*wK>W3+KH#IG&d&8745W|!t#!hXkRvlcgbR?Q8X zO~1D1Z`klk={Ht4+k<`0?cZ7EKXC?pMLFsFu})KfxWycHf(>L)He&WfUixgxO9z~o z4qm|NHHxpM<;_C1ritJ#B8bTmo~+i)gD0GSQX8E`)tZGTUd~qZq6J9XwK$aDCQ=0f}inwBR>ZNMHBoijs!m!PT}X-rso7d`$!Ih&Evf@!M`wH|C=lw zbI<>BY*$Vy9`}C*;lj+Hg7OKzegpEOiZ!r*z8T+Cby?((mcYjMk&G-S8~-kW|HZ)n zVqquid29UF68>vW$~DF#chuAZ|Mw{@XabKS1E8Lz9>vZnJk)J4|+_<6EZsFB6HL|L-ivlaE`3lKP^}Oglq8^OPi`c z<7E8He}Pz^-P+smP8Cd)|3&iuTulDY!~Z|(RvJM4{L2?;fcalUvHyk!Ac_gG9}pCq z;R(;1m@{)RW&R(i0a!TV1MO;dAH}Gq7VzIn6+A|!&$HJl@Ec(XO;+;XS{+DZ@O@gY z<6Lu0I6g7F>wAFr2&}mV_R-CFuYd}^o${wbA|5?BvpzeN;-!>4v6i6Dp?RspN8IzU zNIO`iE*{J?M$2eIm0q7v@)FEDidthyoa&cvJ-PmGan~Ld)sg1w-oA9V(6rr6O9RbQ zkcSO4O#=<`Xh38bbqI@YFA~NOV|wXYyF`(lKt1F&Sg9+22>Ux*HJ0$^Nl$s_WLhb?>dOzIxsIzVG*W zsdH7I1R&r*O|=)D3%{c~m$$km)VcT(?_7-pqm&5nAM~t&Fxk5{rvNa`|-U?jxz#QtSIR?0Dg0=-PcAj7tDXi}%; z7tEZPls~Jsk@{CIWSCHwjjW~kaugy%Sj}+QpA1hK2L{mU3L!&`sLNi5h9Qx_5xN0J z=fduWEic_s`OK1i-IXiK-gT~O0eS#z$RCj9c{Q7V<=f*;Yw@)^w!YqUx?uT2PpiwZ z=67vCM`&ESCeP7R;VhRIYHfMmqJ>U}0UWxkrNGzJ((F!YpZ`#WiZHO^=S{l`Dl>9z z`gL8v&~R6)7#d3-SDBMpUnU*r=mReDc>}xOpjF^g)Ugl8AXJF%M#y2g?!z^zEXWI4 zP(7ct09rw^AX+52iw_s5vY^>2ufiH%xmW>bLl)#ESy0_cf3|5LCp#CipgPk)Z5{P4 z{6>0LKQ3}vPxJhJLg})M3FHs4ZrF`?Nz%me%QcPB#5DL{` znky)Fmmw8MC>27Xn}*c5ffm`#plzI(NEkE;3dc8|@dZK9(8Ij+I=)L%VVj-LzAKO} zazo!GT>r2@WeJcj3NhwU6&)p{i;T>CUL_%{i(D1!f_82~4I6shktQ9+y5OG%qTSvQ z@1l->6zrZH^g^?mJ_d$<8N$B!f1-<);x|Lrkh%sxI5}Z*+(@!8-bMH|11MY}emO?* z3((sb-3Xb)HIiSM*<{ET!6tF2{G#ppN%$q!y*EP|%tb@G9SJu>9<6jEd{YE{r$u`# zq8$N_y)wE*s&;!sA=&Lwi>#eOO)@wgx=1@tl3NCkRS0DgNyjTNhvQj;b-ha1o3M8SRv zI=T4Gfu^zwSr9RxvwOE3?fK*O2cE3V>0kVZw9=H|=cQiz;l*c7DF)}}qrKIV!;m+( zveV%-JzD2N?cXt>syxQviKYoDZait6J0i~@5$3EoXdRW zP0hQU%YB{--%38`MQ~RwyGtRDCChRX{%t_RxLROpXq zvlfBu5k{SuPMlZegO@`dH8pb!`14OwWRwqGxhOhTaKTbvhVB=qn`u22%s0) z=yvh6C*p|6J&Qmq>;*kO&& z)!Hy?jsc>Ho3KBQHR&DIa7!oYm**y{p&yOgNv|c3ZR6_v`U%E(wa?B3c9+^BM)z7( z<{9a=yp{-Zfx2wK1mA7wv_}kz#jNfg=tG0o4cof96ELz#(00J6n&_o@rhg2%XVvL- zG-{6o$f%)tqY0ExV>;5nwrWt>t<>|mRm@`~XrX`Z(?Gh9bfJ0JCF!osgDdN2B{Iu` zob%#vijt@d!Ez-B()4xkOhNnf0RB@&!BHAo)|R&}>$MI2rcA0f`V0 z^UX(Oi!~~X?}lMAGfsuS3bY~@b|3>X<;XA(Butr+N8^MQ-xpIYq~#1rr0UD^WRw7Ks|Lglb7-`3S z|DZa;j#q$N@pf9PMcz*mvJq%D(5fJ*7!kr+WwR4$&3Y_Os*8sj8 z7J6|2oPm`QkemW00L~;LCK)NJ2nr*l3`>4JZPfHlBqC?n$HHa_f$eL6K@D>T4nKq4 zT#_M?98=)W)RaE3tL@=1^2~gdbs+$nGzIuG+L|!@jLJUZA7ObxzG%kkv_fEc!4ad% zNREvK{4qPgrqPYFs20;nP5>;g_z^)-tH6|dO=)GV)0v-t?BHa>y0%22t1MEHiDta{bvFn$%k z9BB^|Rdcvaz#8p6-I}P6Avt%l8b)fqX-@743I!M1Re&A~#6Ur0YBB#d%}&B_IF0X@ zrR8H|g+-glj(|Q#Ew)s-%uR!~PzIfzO^70r3402pW=kt!Zu&x|kMhDc-)n<^S*N-6 zT9@=(aJJk#v~}%&y?(NFzclwdnLR$=iWGZVMoQYM*8L4l^5AcRe_GOci^(!Vh1nd} z()l7R%~)?`RZ5b*dr7dhZFPb@c@wF|q3;1#OPhv$MhC0JPGc|Y&!BzA9Cx4&pWbu~ zOC}Z3&L86rlw;{J4Vzi8f&%UNm?`pA378$9tYiUI0%`*ags*omEUFlRfC>~El9i0Jzk)48jy)FkZpZQTbFxD_h4PlI zZ+9H7^)BqJZ@Q<{bDQ(edCTWlHs)CrdEMNq-jkhA)YTvC*wWvP@3Vruj*h$n^;h1k zot9No(Co|u_$bZAo~EXxl!O!&g48*`!}%C(5PBLLDmwSQwsxPdx!JdG-{0d`s+`g2 zP9;oIjnGwaa$N;T|HJsy1mZ)J^azcGt&z4jT5BPs`2gL4>-~9?t*u{!x*z4wTc)u^ zU6yW+jJC>Nb!f$Nb|`(oW%@HTVxe&w1A|+v8nv`)w9~3FY(uD-aDmq|$r?`S8+VOH z`VSN127K#l@Pu(dvB_J}a)tdoL1%<5kjZ`?;M3o{Asu*|+Ba;qr33m&^6Lx#8qUvp zHEX5kG5&Ku>QN*=fdVsJQ-{4+!Z1g8vBV;e+!6OmP2|D~_uV3u1doI6UQIimL3r-) zHOil%8U2iiCrs|%*qqVZy|Vw2;4x*VaDatpU?KOsbo0J6#S=#O(uA)`%~}m%j%nc? z0$7hSr~zsoP_O(tcR)dNia~QS@~$l4=5^@8u^@0N%@!PQUV>FNdZ(ER3I%B%c^~Ot z*}Zkgz{+kAT`e`jF;*ZH)4HNs)2?ae^v9B>kwov+y8{ew)D`zLDcUhm%akc(#UFY< zFz(4SH@!bP^nS9Ig9lIb{TXKXex^|qQMJ^xqko0uLo~2b*(tRIUkDzD;j4(jca$M` zRMX7skCy@G97HvBBK@~3gH3(;cx8Ye(d&wUcJS@&#Wk!SwkuS2+Cz&$&+Vtu2Nb;< z2sF{>04Q0EdNVl5zdUI!i4lfInSRoam-`~WR5HIB@DP;A!;Z=w8o~?_h#?-16+!?Z zA7J?7;2@N*1^L#g`8*kfE!(2nEC)Z3^ukyf&y$%=UaBKJe;q9Rtt#RryckQQ+)lRr zXgb2q9&Ux>;Pe$b_Q3nMeR=h(Yj(|&tA|b;x*9xi$R9iqrCjTjpMHC2yL2IFrJ(5z z+N5(-7W^sIN;T@EklU0?yf+^C7ovy&O+yKwJM*)s@s zWsogH@aU46Gs?#*rI%Pc&bJ`QFk4=tyuvDxe>c(%Jgyi%fc##BO9&kZnF#cpR)l7R zPJ~hfH}c;@fu0cxnw`T}waZweGB`Yh_dSGsIU?SJ@;!v(oT!)B9=y+wynp1^a9;aU z_887TM%Qsq56`FGUx53pbdR!r_zLnF5MMyJMD*f03qkt<#B))GRSGOOkbVrb8F76L zVmo37#mGZ6Q=P4$Iz+zDk>DYn%9lSp2KHP)yUWxJzUqSo6!_R2k{5h5K zXTUG{jA%if6CGc&M3q0xfNPz&|02#W(Jf#-A~2L^F4g;ZLAMk6!3%^vBLm*O31K_J zR)oE%cNXG<-~sWE2b9$X`cDuqMh4D{LDmiWdsFG4j1lyx`yYCqkJdprm&^jw^?VT_@_eEz?RJ;>h0 zmslfkJhn-XODCj{vA2*fx5}IFzgK=m^RBWXDmCiOsLR@P?R;&Q_Mf$V+EdyObxFF7 zx~FuX>X+*8*MB&reM(n!bo4((|0w!U^sl2ojvk8XFr*spHhdggANyeJ)3L{lNyZZ6 zeB%#|dyRd@Q^xnF)=$0HWHhy!&X_(lXPKMLd(FqpXUv~k43>J!GRxhTCoBi!d~pXc z;I>PmVr z=^cAA{;TYVlADsxBoC#erd&)lr5;VIO}W zho>LPG-eiMuFrfw>)x!lX4KE<%TCJn=Vax)n)6leZ*wo^nexi=I`cN=J(|~-cWP$B z%oQ_#@2GM3^XKRPD*wL=mK5|CvO-^>zwnIHg80@sxW6qHjV7vm^ioTZUSc0uUxj2Te63mq9c@54l7RL&3P&bO>D6G7H zERMCvL-;ZDoKtw)8{+shD`KAl39uMaCKpSGU(Q1O&SO7dce0hN3&-?dj9f*)SIvi= z!W}@X$VcuCz#zH>`IfQ;K!xtYZgD+Qx8wS1){48A1Cfi;>nF@L(cS4|?suU~MI%a4 zfRYvgrKt#|nCKb*fD$ZW%TV4flr$Z8&@&oQnw3b+McIfp7igm2GFTuKVTCdalosO* z(dz;}F>#0g4VU0qq8(y=Es9eC+kz`4lA{%I~G z4m0t^oDb>?u_r*KECvlFsEIO^zZ|_jIAX~hEk;{fw*T=qOm)TeF^7=D&n*D|y0ypkp zkE6A`49=Wlzh$qnSJ>~^tLz+V_AEQYUS~f?oB5pWVsAnl^ABiUe_@}ohuA7`Y&Bbh zw$#O*20zzf++B~>)&st7WdF#{vrX)7*2^}tE$oNv82bsk2YyZOWn0+=b`fpf&jRdO z_7-~^p0N^PG7Fn?DFvIoX;L~Cm(zi2k|oW+N_PMre|`ZA_`@(if01ox{~=|w6YM{w z9QKHmt6lzs-n&+~)Tmh3)3vgwsHjxLWg;#Yah-_kMLb)?4QlMF5V6ST68T&rpDUDa zj>y-j#&ty^7T4=VIqKc&`Fc^Fdj9-k6p+p%c8QqhN1EqHTq4f%`;g{zAohs!oF1e@ z&*SASE^>+Ix?IuARxa;Zf9KNMyH}{U6!W4M7nQ1UY2$44nPb>f@Jl<{JiorBEg(sc zE($bi9c>wY)1rBPYby@!U6kV2Iy&00#!H{$*EkydO8T6B&G%#gn;{W}gfc@Uv`9oX zii8dc?L3jtBcaO{i780vL-#}@F~zE0fx}zz4g=1{=w!G#Yut#p#3E^kB#lVMR)nuk zMbbDkoHQXh)gDfoku>22;!zeP&2iym9FmsEldVX`b%w7_L(&?)y2x*J&^RM4_}4{O G8T(&w7Asx= literal 0 HcmV?d00001 diff --git a/app/assets/stylesheets/oldsansblack-webfont.woff b/app/assets/stylesheets/oldsansblack-webfont.woff new file mode 100644 index 0000000000000000000000000000000000000000..1553cfb60d1b68a5d72bb19e11dfb1da2758aaf5 GIT binary patch literal 25112 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 M0003~n_2n*01pKGQUCw| literal 0 HcmV?d00001 diff --git a/app/assets/stylesheets/oldsansblack.css b/app/assets/stylesheets/oldsansblack.css new file mode 100644 index 00000000..3447e5c5 --- /dev/null +++ b/app/assets/stylesheets/oldsansblack.css @@ -0,0 +1,14 @@ +/* Generated by Font Squirrel (http://www.fontsquirrel.com) on June 13, 2014 */ + + + +@font-face { + font-family: 'oldsansblack'; + src: url('oldsansblack-webfont.eot'); + src: url('oldsansblack-webfont.eot?#iefix') format('embedded-opentype'), + url('oldsansblack-webfont.woff') format('woff'), + url('oldsansblack-webfont.ttf') format('truetype'), + url('oldsansblack-webfont.svg#oldsansblackregular') format('svg'); + font-weight: normal; + font-style: normal; +} 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/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/home/index.html.haml b/app/views/home/index.html.haml index 8c2bb9e5..2e5a3442 100644 --- a/app/views/home/index.html.haml +++ b/app/views/home/index.html.haml @@ -1,53 +1,45 @@ -/ Jumbotron -.jumbotron - %h1 Contribute to Open Source - %p.lead Donate peercoins 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 Peercoin address to receive tips!' : 'Change peercoin 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-3 - %h2 How it works? - %p - Fundraisers collect funds and distribute them to make something happen. - %p - %a.btn.btn-primary{href: faq_path} Frequently Asked Questions » - .col-lg-3 - %h2 Donate - %p - Donate to projects to make them happen. - %p - %a.btn.btn-primary{href: projects_path} See projects » - .col-lg-3 - %h2 Contribute - %p - Get paid to contribute to a project. - %p - %a.btn.btn-primary{href: projects_path} See projects » - .col-lg-3 - %h2 Raise funds - %p - Make something great happen by raising funds and distributing them. - %p - %a.btn.btn-primary{href: new_project_path} Create a project » -/ - if current_user -/ %a.btn.btn-primary{href: user_path(current_user)} Change your peercoin address » -/ - else -/ %a.btn.btn-primary{href: user_omniauth_authorize_path(:github)} Sign In » + .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 great 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 make something happen. + .button-container + %a.btn.btn-primary.btn-block{href: faq_path} FAQ + / - if current_user + / %a.btn.btn-primary{href: user_path(current_user)} Change your peercoin address » + / - else + / %a.btn.btn-primary{href: user_omniauth_authorize_path(:github)} Sign In » diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 18f1d539..dd7e6e9f 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -27,33 +27,37 @@ ga('create', 'UA-11108334-6', 'peer4commit.com'); ga('send', 'pageview'); - .container - .masthead - %div.pull-right - %small - - if current_user - = link_to current_user.full_name, edit_registration_path(current_user), class: "edit-profile-link" - \/ - = btc_human(current_user.balance) - \/ - = link_to 'Sign Out', destroy_user_session_path, method: :delete - - else - = link_to "Sign in", new_user_session_path(return_url: request.url) - %h3.text-muted.code-pro Peer4Commit + #top-bar + .container + #session-menu + - if current_user + = link_to current_user.full_name, edit_registration_path(current_user), class: "edit-profile-link" + = link_to 'Sign Out', destroy_user_session_path, method: :delete, class: "btn btn-default" + - else + = link_to "Sign in", new_user_session_path(return_url: request.url), class: "btn btn-default" + %a#main-logo{href: root_path} + %h3 Peer4commit = render 'common/menu' = render_flash_message - = yield - / Site footer - .footer - %p - © - = link_to 'Peer4commit', 'http://peer4commit.com/', target: '_blank' - 2014. Source code is available at #{link_to('github', 'https://github.com/sigmike/peer4commit', target: '_blank')}, - based on #{link_to "Tip4commit", "http://tip4commit.com/"}. - You can support its development with - = link_to('peercoins', 'http://peer4commit.com/projects/1') - or - = link_to('bitcoins', 'http://tip4commit.com/projects/560') + #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. Source code is available at #{link_to('github', 'https://github.com/sigmike/peer4commit', target: '_blank')}, + based on #{link_to "Tip4commit", "http://tip4commit.com/"}. + You can support its development with + = link_to('peercoins', 'http://peer4commit.com/projects/1') + or + = link_to('bitcoins', 'http://tip4commit.com/projects/560') / /container / Bootstrap core JavaScript diff --git a/app/views/projects/index.html.haml b/app/views/projects/index.html.haml index 20f356b9..0182a3bd 100644 --- a/app/views/projects/index.html.haml +++ b/app/views/projects/index.html.haml @@ -1,23 +1,24 @@ -.row - .col-md-10 - %h1 Projects - .col-md-2 - .text-right - = link_to "Create project", new_project_path, class: 'btn btn-default btn-block' -%p - %table.table.table-hover - %thead - %tr - %th Name - %th Description - %th Funds - %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.name, project - %td= project.description - %td= btc_human project.available_amount_cache - %td= link_to 'Details', project, class: 'btn btn-default' - = 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/show.html.haml b/app/views/projects/show.html.haml index 9f52b4c9..cdada413 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -28,30 +28,9 @@ - else = "(Last updated on #{date})" - %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") - %a.addthis_button_preferred_1 - %a.addthis_button_preferred_2 - %a.addthis_button_preferred_3 - %a.addthis_button_preferred_4 - %a.addthis_button_compact - %a.addthis_counter.addthis_bubble_style - / AddThis Button END - - %h4 Embed in README.md - %p= link_to image_tag(project_url(@project, format: :svg), alt: 'Peer4Commit'), project_url(@project) - %p - %input.form-control{type: 'text', value: "[![tip for next commit](#{project_url(@project, format: :svg)})](#{project_url(@project)})"} - - %hr - = commontator_thread(@project) - .col-md-4 - if @project.disabled? - .panel.panel-danger + .panel.panel-danger.note-panel .panel-heading %h4.panel-title Project Disabled @@ -63,7 +42,7 @@ %p Reason: #{reason} - else - .panel.panel-default.project-panel + .panel.panel-default.project-panel.note-panel .panel-heading %h4.panel-title Project informations @@ -96,7 +75,7 @@ - if can? :create, @project.distributions.build = link_to "New distribution", new_project_distribution_path(@project), class: "btn btn-default btn-sm btn-block" - .panel.panel-default.project-panel + .panel.panel-default.project-panel.note-panel .panel-heading %h4.panel-title Donate @@ -107,3 +86,26 @@ %p = image_tag qrcode_project_path(@project, format: :svg), alt: @project.bitcoin_address, class: 'project qrcode' %p #{100-(CONFIG["our_fee"]*100).round}% of deposited funds will be used to tip for new commits. +.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") + %a.addthis_button_preferred_1 + %a.addthis_button_preferred_2 + %a.addthis_button_preferred_3 + %a.addthis_button_preferred_4 + %a.addthis_button_compact + %a.addthis_counter.addthis_bubble_style + / AddThis Button END + + %h4 Embed in README.md + %p= link_to image_tag(project_url(@project, format: :svg), alt: 'Peer4Commit'), project_url(@project) + %p + %input.form-control{type: 'text', value: "[![tip for next commit](#{project_url(@project, format: :svg)})](#{project_url(@project)})"} + + %hr + = commontator_thread(@project) + From facfc1d1e5f64f9e176192eb2f1981740a3f66c9 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sat, 14 Jun 2014 08:05:25 +0200 Subject: [PATCH 261/372] skip tips without user --- app/views/projects/decide_tip_amounts.html.haml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/views/projects/decide_tip_amounts.html.haml b/app/views/projects/decide_tip_amounts.html.haml index 54f59609..e5a9a7cd 100644 --- a/app/views/projects/decide_tip_amounts.html.haml +++ b/app/views/projects/decide_tip_amounts.html.haml @@ -14,6 +14,7 @@ = 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 From 1c8899824d2538fd70e147994a565ff8bf8d4c92 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sat, 14 Jun 2014 08:15:51 +0200 Subject: [PATCH 262/372] fixed font url --- app/assets/stylesheets/oldsansblack.css | 14 -------------- app/assets/stylesheets/oldsansblack.css.sass | 8 ++++++++ 2 files changed, 8 insertions(+), 14 deletions(-) delete mode 100644 app/assets/stylesheets/oldsansblack.css create mode 100644 app/assets/stylesheets/oldsansblack.css.sass diff --git a/app/assets/stylesheets/oldsansblack.css b/app/assets/stylesheets/oldsansblack.css deleted file mode 100644 index 3447e5c5..00000000 --- a/app/assets/stylesheets/oldsansblack.css +++ /dev/null @@ -1,14 +0,0 @@ -/* Generated by Font Squirrel (http://www.fontsquirrel.com) on June 13, 2014 */ - - - -@font-face { - font-family: 'oldsansblack'; - src: url('oldsansblack-webfont.eot'); - src: url('oldsansblack-webfont.eot?#iefix') format('embedded-opentype'), - url('oldsansblack-webfont.woff') format('woff'), - url('oldsansblack-webfont.ttf') format('truetype'), - url('oldsansblack-webfont.svg#oldsansblackregular') format('svg'); - font-weight: normal; - font-style: normal; -} diff --git a/app/assets/stylesheets/oldsansblack.css.sass b/app/assets/stylesheets/oldsansblack.css.sass new file mode 100644 index 00000000..af931fe7 --- /dev/null +++ b/app/assets/stylesheets/oldsansblack.css.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 From 9e7d7071b7cdaf50c55b603cba9359779709de14 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sat, 14 Jun 2014 08:17:44 +0200 Subject: [PATCH 263/372] font fallback --- app/assets/stylesheets/home.css.sass | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/home.css.sass b/app/assets/stylesheets/home.css.sass index 8e36b713..24a418ec 100644 --- a/app/assets/stylesheets/home.css.sass +++ b/app/assets/stylesheets/home.css.sass @@ -10,6 +10,9 @@ =verdana font-family: Verdana, Geneva, sans-serif +=oldsansblack + font-family: oldsansblack, Verdana, Geneva, sans-serif + td.money, th.money text-align: right @@ -85,14 +88,14 @@ body text-transform: uppercase font-size: 48px font-weight: bold - font-family: oldsansblack + +oldsansblack margin: 50px 0 #main-container padding-top: 30px h1 - font-family: oldsansblack + +oldsansblack font-size: 36px color: #8bc139 From 383459d48d4558f2b3b93f2c83033be28453479f Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sat, 14 Jun 2014 09:26:06 +0200 Subject: [PATCH 264/372] better wording --- app/views/home/index.html.haml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/home/index.html.haml b/app/views/home/index.html.haml index 2e5a3442..7fe27227 100644 --- a/app/views/home/index.html.haml +++ b/app/views/home/index.html.haml @@ -29,14 +29,14 @@ .panel-content %h2 Raise funds %p - Make something great happen by raising funds and distributing them. + 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 make something happen. + Fundraisers collect funds and distribute them to contributors. .button-container %a.btn.btn-primary.btn-block{href: faq_path} FAQ / - if current_user From d38f04dbe9555dadd65065dda895bf9618535c65 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sat, 14 Jun 2014 12:26:42 +0200 Subject: [PATCH 265/372] Started new FAQ --- app/views/home/faq.html.md | 104 ++++++++++++++++++++----------------- 1 file changed, 57 insertions(+), 47 deletions(-) diff --git a/app/views/home/faq.html.md b/app/views/home/faq.html.md index 0c666521..142474ae 100644 --- a/app/views/home/faq.html.md +++ b/app/views/home/faq.html.md @@ -1,68 +1,78 @@ Peer4commit FAQ =============== -What is Peer4Commit? +What is Peer4commit? -------------------- -With Peer4Commit you can add projects from GitHub and donate Peercoins to the ones that interest you the most. Anyone that submits code changes and has them accepted will receive Peercoin tips. This helps in providing an incentive for developers to work on important projects that will benefit Peercoin in the future. Peer4commit was adapted by Sigmike from Tip4commit. +Peer4commit is a website where anyone can raise funds for any kind of project. -What is a Commit? ------------------ -Each time someone adds changes to the source code of a supported project, he receives 1% of the project balance. A set of changes is called a "commit". Here is an example commit for Peercoin v0.4: https://github.com/ppcoin/ppcoin/commit/5941effd0085dd26ce9b793ec09dcaffae8e5678 +How is it different than other crowdfunding sites? +-------------------------------------------------- -How do I Receive a Tip for my Commit? -------------------------------------- -We use the email address included in the commit to identify the author and notify him. To receive the tip, the author must follow the link in the email he received and set his Peercoin address. If he doesn't do that within 1 month, the tip goes back to the project balance. +On Peer4commit the fundraiser is not expected to actually do the work. His job is to collect funds and distribute them to the people who make the project happen. He may for example reward commits on an open source project or pay a lobbyist. + +The fundraiser can also do the whole work if he wants to. Whatever he chooses, he will have to explain that in his project description so that potential sponsors know what's going to happen with their funds. The fundraiser can for example reward himself for his raising and distribution job, but that must be clear since the beginning. + +Another difference is that Peer4commit uses Peercoin as the base currency. More currencies will come later, notably Bitcoin. + + +How can I get paid? +------------------- + +Browse the projects and read descriptions to see if there's something you can do. There are so many possibilities it's hard to give a guideline. Actually we hope projects will appear to reward people who makes it easier for potential contributors to find something to do. + + +What can a fundraiser do with the funds? +---------------------------------------- + +He can distribute them to anyone he wants. + +Peer4commit provides tools to help him doing that. He can for example send funds to an email address. Peer4commit will then send an email to ask where the funds should be sent. He can also send funds to a GitHub users, or to the author of a particular commit. More options will be added later: send to a Reddit user, to a peercointalk.org member, etc. + +Peer4commit helps the fundraiser to gather payment informations from users. For example to set the receiving address of a GitHub account, you must log in with your GitHub account. -How do I Donate to a Project I Like? ------------------------------------- -You can see all supported projects here: http://peer4commit.com/projects. To donate to a specific project, open up the page for that project and just send Peercoins to the address that is displayed. For an example, check out Peercoin's main project page: http://peer4commit.com/projects/19. If the project you want to donate to is not supported yet, go to the supported projects page and just copy/paste its GitHub URL (For example: https://github.com/ppcoin/ppcoin) into the input box above the list. Anyone can add a project to Peer4commit, even if you are not the project maintainer. 99% of your donation will be given as tips. 1% will be kept to host the website and pay the transaction fees. -How do I Push my Commits? -------------------------- -Getting write access to the "Master" of a project involves that the project maintainer provides access to you in Github. This type of access would only be given to people trusted by the maintainer. If you already have write access to the project, just push your commits to the default branch of the project as usual. Otherwise, you'll have to fork the project (i.e. Start your own project based on the supported project), make some changes and create a pull request to propose your changes. If your pull request is accepted (Merged), you'll receive one tip per commit. If the changes are simple enough, you can do them in your browser by editing the files on GitHub. Otherwise, you'll have to use Git to clone your fork, make some changes, commit them and push the commits to GitHub. You can find a lot of information about that on GitHub help and on the web. +How is scam prevented? +---------------------- -Make Sure You Read the Project Charter & Tipping Policies Before Starting -------------------------------------------------------------------------- -Project maintainers can refuse your commits for several reasons. It is important to read the "Charter" of the project on its GitHub page, which usually provides guidance on which commits and under what rules they would be accepted. For example, there are very strict rules for contributing to the official ppcoin/ppcoin project. A good way to ensure the maintainer is willing to merge your changes is to first create an issue explaining what you're going to do and ask if they would merge a pull request. Wait for an answer before starting. The project owner can also edit the Tipping Policies section on their Peer4commit project page to include more information on what kind of commits will be tipped. So it's important to read both the project charter on GitHub and the tipping policies that are listed on Peer4commit. +We don't want to restrict the power of the fundraisers. They were trusted by the donors so we let them do anything they want with the funds. The donors must be careful when they give. The fundraiser can take all the funds and disappear, so you must ensure he is trustworthy. You must also make sure he has the ability to acheive what he announces. -Can Project Owners Change the Amount Donated to Each Commit? ------------------------------------------------------------- -Yes, they have a new button "Change project settings" on the project page along the project name. In this screen they can change 2 things (for now): +However we want to give the sponsors the most possible information to help them decide whether a particular fundraiser is trustworthy and able to do the job. -* A text describing their tipping policies that will be displayed on the project page on peer4commit. -* A checkbox that will put all new tips on hold when commits are found. +The system keeps track of all the projects and distributions made by each fundraiser. You can browse them on his profile page. -When the checkbox is active, each new commit generates an "Undecided" tip and the authors are not notified. The project owners can then click on a new button on the project page to decide the tip amounts. They have these choices: +And all projects, distributions and users can be commented. -* Leave undecided (To decide later) -* Free (The commit won't get any tip and the author won't be notified) -* Tiny: 0.1% of the project balance. -* Small: 0.5% of the project balance. -* Normal: 1% -* Big: 2% -* Huge: 5% +A reputation system will certainly be added later. A project should be created on Peer4commit for that. -The authors are notified when the tip amount is decided (Unless they have recently been notified already, or if they said they don't want any more notification, or if they have configured their Peercoin address). The 2 buttons are only available to project collaborators (Those who can push changes to the supported repository). There should be more options in the future. Your ideas are welcome. +But the most important part is that anyone can create a project and multiple people can raise funds for similar projects. So we hope fundraiser will actually compete on trust. Newcomers will have to prove their trustworthiness on small projects and gain reputation. With time their will be well trusted fundraisers who handled many of projects. They may even live by that. -Do You Have an Audit Page Setup for Peer4commit? ------------------------------------------------- -Yes, Peer4commit does have an audit page. It shows different information, such as amount donated, available balance, transaction fees, amount in cold storage and includes addresses for each project. You can view the page here: http://peer4commit.com/audit. -What Measures Have Been Taken to Secure the Funds on Peer4Commit? +How do I donate to a project I like? +------------------------------------ + +Browse the project list and click on "Donate". You will be asked for a personnal 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. + + +How can I raise funds? +---------------------- + +Click on "Create a project". You'll have to fill a description. Make it the most complete possible and provide proofs of your trustworthiness and abilities. + +The project is visible on the project list but will not be particularly highlighted. You will have to communicate about it, that's part of your job. Explain your project on forums, Reddit, etc. + +Do you have an audit page? +-------------------------- +Yes, it shows different information, such as amount donated, available balance, transaction fees, amount in cold storage and includes addresses for each project. You can view the page <%= link_to "here", audit_path %>. + +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 get more tips 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. This is still a manual operation, but will soon be automated. The website runs in an isolated virtual server running only this service. +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. This is still a manual operation, but will soon be automated. The website runs in an isolated virtual server running only this service. -Conclusion ------------ -The commit may not be the best item to identify the value of a contribution, but it's a very convenient way to identify contributors and send them donations. The maintainer of the project doesn't even have to do anything. Supporters can add a project from GitHub and start donating without any extra work on the project side (Except setting their address if they want the tips). A commit can include very important changes that took a very long time to build (Like the v0.4 changes) or a very small change like adding a comma. +In the future the funds will be sent to a multisignature address so that the fundraiser and Peer4commit must agree to send the funds. Contact ------- -If you have any questions, either post them in this thread, message Sigmike on PeercoinTalk.org: http://www.peercointalk.org/index.php?action=profile;u=30141 on Reddit: http://www.reddit.com/user/sigmike or open an issue on GitHub: https://github.com/sigmike/peer4commit/issues. - -References: ------------ -Fork: https://help.github.com/articles/fork-a-repo -Pull Request: https://help.github.com/articles/using-pull-requests -Git: https://help.github.com/articles/set-up-git -Github Help: https://help.github.com/ +If you have any question send a message to <%= mail_to "contact@peer4commit.com" %> or <%= link_to "open an issue on GitHub", "https://github.com/sigmike/peer4commit/issues/new" %>. + From e31c447d4241a9fdcb43fc6a11775e34af087185 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 15 Jun 2014 08:51:28 +0200 Subject: [PATCH 266/372] fixed ie gradients --- app/assets/stylesheets/home.css.sass | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/home.css.sass b/app/assets/stylesheets/home.css.sass index 24a418ec..9169e4fc 100644 --- a/app/assets/stylesheets/home.css.sass +++ b/app/assets/stylesheets/home.css.sass @@ -21,7 +21,7 @@ h1:first-child =button($top-color, $bottom-color, $border, $text, $text-hover, $hover) - +background(linear-gradient(top, $top-color, $bottom-color)) + +background(linear-gradient(180deg, $top-color, $bottom-color)) border-color: $border color: $text @@ -40,7 +40,7 @@ h1:first-child text-decoration: none #top-bar - +background(linear-gradient(top, #2c2c2c, #0a0a0a)) + +background(linear-gradient(180deg, #2c2c2c, #0a0a0a)) $link-color: white $link-color-active: #94d317 From 24bf60dfb34f1c5f2dcaef96d26e1bbf88a642c2 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 15 Jun 2014 08:54:16 +0200 Subject: [PATCH 267/372] Background for browsers not supporting gradients --- app/assets/stylesheets/home.css.sass | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/assets/stylesheets/home.css.sass b/app/assets/stylesheets/home.css.sass index 9169e4fc..9fb78e50 100644 --- a/app/assets/stylesheets/home.css.sass +++ b/app/assets/stylesheets/home.css.sass @@ -21,6 +21,7 @@ h1:first-child =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 @@ -40,6 +41,7 @@ h1:first-child text-decoration: none #top-bar + background: #0a0a0a +background(linear-gradient(180deg, #2c2c2c, #0a0a0a)) $link-color: white $link-color-active: #94d317 From 6964e206f090fc4fccc87765b086b0c0bf2af2d5 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 15 Jun 2014 10:11:12 +0200 Subject: [PATCH 268/372] Updated FAQ --- app/views/home/faq.html.md | 89 +++++++++++++++++++++++++++----------- 1 file changed, 63 insertions(+), 26 deletions(-) diff --git a/app/views/home/faq.html.md b/app/views/home/faq.html.md index 142474ae..6be0c391 100644 --- a/app/views/home/faq.html.md +++ b/app/views/home/faq.html.md @@ -7,60 +7,97 @@ Peer4commit is a website where anyone can raise funds for any kind of project. 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. -On Peer4commit the fundraiser is not expected to actually do the work. His job is to collect funds and distribute them to the people who make the project happen. 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. -The fundraiser can also do the whole work if he wants to. Whatever he chooses, he will have to explain that in his project description so that potential sponsors know what's going to happen with their funds. The fundraiser can for example reward himself for his raising and distribution job, but that must be clear since the beginning. +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 this 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. -Another difference is that Peer4commit uses Peercoin as the base currency. More currencies will come later, notably Bitcoin. +Or imagine you favorite band has given up. You can raise funds to pay them to work on a new album. -How can I get paid? -------------------- +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? -Browse the projects and read descriptions to see if there's something you can do. There are so many possibilities it's hard to give a guideline. Actually we hope projects will appear to reward people who makes it easier for potential contributors to find something to do. +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. -What can a fundraiser do with the funds? ----------------------------------------- +The fundraiser will also have the responsibility of deciding what can be changed and what can not. For example the main goal of the project is not likely to change. Even if you give a small amount and have a small weight in the decisions you can be assured that your funds will still be used for the same goal. Someone who gave 90% of the funds won't be able to change that. -He can distribute them to anyone he wants. -Peer4commit provides tools to help him doing that. He can for example send funds to an email address. Peer4commit will then send an email to ask where the funds should be sent. He can also send funds to a GitHub users, or to the author of a particular commit. More options will be added later: send to a Reddit user, to a peercointalk.org member, etc. +Why use Peer4commit then and not send funds directly to the fundraiser? +----------------------------------------------------------------------- -Peer4commit helps the fundraiser to gather payment informations from users. For example to set the receiving address of a GitHub account, you must log in with your GitHub account. +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. -How is scam prevented? ----------------------- +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 (send to a Reddit user, to a forum member, etc.). -We don't want to restrict the power of the fundraisers. They were trusted by the donors so we let them do anything they want with the funds. The donors must be careful when they give. The fundraiser can take all the funds and disappear, so you must ensure he is trustworthy. You must also make sure he has the ability to acheive what he announces. +We also provide tools to identify donors. When they donate they can provide a address. This address will 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. -However we want to give the sponsors the most possible information to help them decide whether a particular fundraiser is trustworthy and able to do the job. -The system keeps track of all the projects and distributions made by each fundraiser. You can browse them on his profile page. +Can I trust the fundraisers? +---------------------------- -And all projects, distributions and users can be commented. +Not blindly. You should do some researches before you give money. -A reputation system will certainly be added later. A project should be created on Peer4commit for that. +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. -But the most important part is that anyone can create a project and multiple people can raise funds for similar projects. So we hope fundraiser will actually compete on trust. Newcomers will have to prove their trustworthiness on small projects and gain reputation. With time their will be well trusted fundraisers who handled many of projects. They may even live by that. +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 suspicious. 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 is not impersonating the person he claims to be. -How do I donate to a project I like? ------------------------------------- +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. Of course other skills are 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 skills. -Browse the project list and click on "Donate". You will be asked for a personnal 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. +Does it work? +------------- +The project 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 <%= link_to "Peercoin", "http://peercoin.net/" %>. Support for 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 description. Make it the most complete possible and provide proofs of your trustworthiness and abilities. + +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. -Click on "Create a project". You'll have to fill a description. Make it the most complete possible and provide proofs of your trustworthiness and abilities. -The project is visible on the project list but will not be particularly highlighted. You will have to communicate about it, that's part of your job. Explain your project on forums, Reddit, etc. +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? +----------------------------------------- +Browse the projects and read descriptions to see if there's something you can do and contact the fundraiser. There are so many possibilities it's hard to give a guideline. + +We hope projects will appear to make things easier for potential contributors to find something to do. + + +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. + Do you have an audit page? -------------------------- @@ -68,7 +105,7 @@ Yes, it shows different information, such as amount donated, available balance, 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. This is still a manual operation, but will soon be automated. The website runs in an isolated virtual server running only this service. +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. In the future the funds will be sent to a multisignature address so that the fundraiser and Peer4commit must agree to send the funds. From f95ae8051ac4e5b3844a76141d97b224caaf1ac2 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 15 Jun 2014 12:57:55 +0200 Subject: [PATCH 269/372] Some text improvements --- app/views/home/faq.html.md | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/app/views/home/faq.html.md b/app/views/home/faq.html.md index 6be0c391..2cd33ee7 100644 --- a/app/views/home/faq.html.md +++ b/app/views/home/faq.html.md @@ -26,17 +26,17 @@ And there is a big risk. For example if all decisions are made proportional to t 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 be changed and what can not. For example the main goal of the project is not likely to change. Even if you give a small amount and have a small weight in the decisions you can be assured that your funds will still be used for the same goal. Someone who gave 90% of the funds won't be able to change that. +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 then and not send funds directly to the fundraiser? ------------------------------------------------------------------------ +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 (send to a Reddit user, to a forum member, etc.). +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 will 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. @@ -50,14 +50,16 @@ For example the fact a project has a lot of donations is not an indication that 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 suspicious. 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 is not impersonating the person he claims to be. +The fundraiser should also explain in the project description why you can trust him. If he doesn't do that you should be suspicious. 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. Of course other skills are 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 skills. +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. Does it work? ------------- -The project is still young but yes. Some projects were successfully managed. +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. From fde53c6f7026a1ceef2953d2566f4119be84e6f8 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 15 Jun 2014 14:56:50 +0200 Subject: [PATCH 270/372] Fixed page titles --- app/views/layouts/application.html.haml | 2 +- app/views/projects/show.html.haml | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index dd7e6e9f..29790caf 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -7,7 +7,7 @@ %meta{content: "", name: "author"}/ %link{href: image_path("ppcoin.png"), rel: "shortcut icon"}/ - %title= "Peer4Commit — " + (content_for?(:title) ? yield(:title) : "Contribute to Open Source") + %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'} diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index cdada413..9b2e0e62 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -1,5 +1,4 @@ - content_for :title do - Contribute to = @project.name - content_for :description do = @project.description From 65251a0deebd4b6a8ef5569514f295c5def6efae Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Mon, 9 Jun 2014 16:18:56 +0200 Subject: [PATCH 271/372] Donor can provide a return address when they send --- app/assets/stylesheets/projects.css.sass | 4 ++ app/controllers/projects_controller.rb | 11 ++++- app/models/ability.rb | 2 +- app/models/deposit.rb | 3 +- app/models/donation_address.rb | 15 +++++++ app/models/project.rb | 1 + app/views/projects/donate.html.haml | 25 +++++++++++ app/views/projects/donors.html.haml | 23 +++++++++++ app/views/projects/show.html.haml | 5 ++- config/routes.rb | 3 ++ ...0140615122107_create_donation_addresses.rb | 11 +++++ ...5124857_add_donation_address_to_deposit.rb | 5 +++ db/schema.rb | 20 +++++++-- features/donate_to_project.feature | 41 ++++++++++++++++++- .../step_definitions/donate_to_project.rb | 27 ++++++++++++ lib/balance_updater.rb | 6 ++- test/fixtures/donation_addresses.yml | 11 +++++ test/models/donation_address_test.rb | 7 ++++ 18 files changed, 209 insertions(+), 11 deletions(-) create mode 100644 app/models/donation_address.rb create mode 100644 app/views/projects/donate.html.haml create mode 100644 app/views/projects/donors.html.haml create mode 100644 db/migrate/20140615122107_create_donation_addresses.rb create mode 100644 db/migrate/20140615124857_add_donation_address_to_deposit.rb create mode 100644 features/step_definitions/donate_to_project.rb create mode 100644 test/fixtures/donation_addresses.yml create mode 100644 test/models/donation_address_test.rb diff --git a/app/assets/stylesheets/projects.css.sass b/app/assets/stylesheets/projects.css.sass index e06f5961..198e0ad7 100644 --- a/app/assets/stylesheets/projects.css.sass +++ b/app/assets/stylesheets/projects.css.sass @@ -9,3 +9,7 @@ .bitcoin-address word-wrap: break-word + +.donor-list + .amount + text-align: right diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index ef8ae5fc..5e211860 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -4,7 +4,7 @@ class ProjectsController < ApplicationController before_action :load_project, only: [:qrcode, :edit, :update, :decide_tip_amounts] - load_and_authorize_resource only: [:commit_suggestions] + load_and_authorize_resource only: [:commit_suggestions, :donate, :donors] def index @projects = Project.enabled.order(available_amount_cache: :desc, watchers_count: :desc, full_name: :asc).page(params[:page]).per(30) @@ -129,6 +129,15 @@ def github_user_suggestions 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]) diff --git a/app/models/ability.rb b/app/models/ability.rb index 415e7882..0c0573b7 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -2,7 +2,7 @@ class Ability include CanCan::Ability def initialize(user) - can :read, Project + can [:read, :donate, :donors], Project can :read, Distribution if user and user.nickname.present? 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/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 e3c044c0..6c9813a7 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -4,6 +4,7 @@ class Project < ActiveRecord::Base accepts_nested_attributes_for :tips has_many :collaborators has_many :distributions, inverse_of: :project + has_many :donation_addresses, inverse_of: :project has_many :cold_storage_transfers diff --git a/app/views/projects/donate.html.haml b/app/views/projects/donate.html.haml new file mode 100644 index 00000000..803daea5 --- /dev/null +++ b/app/views/projects/donate.html.haml @@ -0,0 +1,25 @@ +.row + .col-md-12 + %h1 Donate to #{@project.name} + - fundraisers = User.where(nickname: @project.collaborators.map(&:login)) + - 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..062a91c6 --- /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.sender-address Sender address + %th.amount Amount + %th.transactions Transactions + %tbody + - groups = @project.deposits.includes(:donation_address).group_by(&:donation_address) + - groups = groups.sort_by { |da, deposits| deposits.map(&:amount).sum }.reverse + - groups.each do |donation_address, deposits| + %tr.donor-row + %td.sender-address= donation_address.try(:sender_address).presence || 'No address provided' + %td.amount= btc_human deposits.map(&:amount).sum + %td.transactions + - deposits.sort_by(&:created_at).each_with_index do |deposit, i| + = link_to "[#{i+1}]", transaction_url(deposit.txid) diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index 9b2e0e62..637d7c04 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -49,7 +49,10 @@ %table.table.text-left %tr %td Funds - %td= btc_human @project.available_amount + %td + = btc_human @project.available_amount + = link_to "Donate", donate_project_path(@project), class: "btn btn-primary" + = link_to "List of donors", donors_project_path(@project) %tr %td Distributions %td diff --git a/config/routes.rb b/config/routes.rb index 6ddcdac8..78793951 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -35,6 +35,9 @@ patch :decide_tip_amounts get :commit_suggestions get :github_user_suggestions + get :donate + post :donate + get :donors end end resources :tips, :only => [:index] 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/schema.rb b/db/schema.rb index db0422bc..6def0ffd 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20140609122440) do +ActiveRecord::Schema.define(version: 20140615124857) do create_table "cold_storage_transfers", force: true do |t| t.integer "project_id" @@ -93,14 +93,16 @@ t.integer "project_id" t.string "txid" t.integer "confirmations" - t.integer "duration", default: 2592000 - t.integer "paid_out", limit: 8 + 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 "amount", limit: 8 + t.integer "donation_address_id" end + add_index "deposits", ["donation_address_id"], name: "index_deposits_on_donation_address_id" add_index "deposits", ["project_id"], name: "index_deposits_on_project_id" create_table "distributions", force: true do |t| @@ -117,6 +119,16 @@ add_index "distributions", ["project_id"], name: "index_distributions_on_project_id" + create_table "donation_addresses", force: true do |t| + t.integer "project_id" + t.string "sender_address" + t.string "donation_address" + t.datetime "created_at" + t.datetime "updated_at" + end + + add_index "donation_addresses", ["project_id"], name: "index_donation_addresses_on_project_id" + create_table "projects", force: true do |t| t.string "url" t.string "bitcoin_address" diff --git a/features/donate_to_project.feature b/features/donate_to_project.feature index ede5fd1c..614c816f 100644 --- a/features/donate_to_project.feature +++ b/features/donate_to_project.feature @@ -1,14 +1,51 @@ Feature: A visitor can donate to a project - Scenario: A visitor sends coins 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" on the project account + 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" diff --git a/features/step_definitions/donate_to_project.rb b/features/step_definitions/donate_to_project.rb new file mode 100644 index 00000000..f7daf46f --- /dev/null +++ b/features/step_definitions/donate_to_project.rb @@ -0,0 +1,27 @@ + +Then(/^I should see the project donation address associated with "(.*?)"$/) do |arg1| + address = @project.donation_addresses.find_by(sender_address: arg1).donation_address + address.should_not be_blank + page.should 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 + address.should_not be_blank + BitcoinDaemon.instance.add_transaction(account: @project.address_label, amount: arg1.to_d, address: address) +end + +Then(/^I should see the donor "(.*?)" sent "(.*?)"$/) do |arg1, arg2| + within ".donor-row", text: arg1 do + find(".amount").text.to_d.should 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/lib/balance_updater.rb b/lib/balance_updater.rb index 1f8277b4..811bfa49 100644 --- a/lib/balance_updater.rb +++ b/lib/balance_updater.rb @@ -83,7 +83,10 @@ def self.work end if category == "receive" - if address != project.bitcoin_address + 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 @@ -95,6 +98,7 @@ def self.work duration: 30.days.to_i, paid_out: 0, paid_out_at: Time.now, + donation_address: donation_address, ) next end 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/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 From 62676169a281d00481a58c0c396baabc54171f49 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 15 Jun 2014 19:52:50 +0200 Subject: [PATCH 272/372] removed donate panel on project show --- app/views/projects/show.html.haml | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index 637d7c04..6e37e9b7 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -77,17 +77,6 @@ - if can? :create, @project.distributions.build = link_to "New distribution", new_project_distribution_path(@project), class: "btn btn-default btn-sm btn-block" - .panel.panel-default.project-panel.note-panel - .panel-heading - %h4.panel-title - Donate - .panel-body.text-center - %p To give to this project, send peercoins to this address: - %p.bitcoin-address - = @project.bitcoin_address - %p - = image_tag qrcode_project_path(@project, format: :svg), alt: @project.bitcoin_address, class: 'project qrcode' - %p #{100-(CONFIG["our_fee"]*100).round}% of deposited funds will be used to tip for new commits. .row .col-md-8 %hr From c30111c9f7b7543367393e8558882e5c54148af5 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 15 Jun 2014 19:56:52 +0200 Subject: [PATCH 273/372] Hide send email button when action is not authorized --- app/views/distributions/show.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/distributions/show.html.haml b/app/views/distributions/show.html.haml index c2043363..9b5ee283 100644 --- a/app/views/distributions/show.html.haml +++ b/app/views/distributions/show.html.haml @@ -24,7 +24,7 @@ - if tip.user.try(:bitcoin_address).present? = tip.user.bitcoin_address - else - - if tip.user.try(:email).present? + - if tip.user.try(:email).present? and can? :update, @distribution = button_to "Send email request to provide an address", send_email_address_request_user_path(tip_id: tip.id, return_url: request.url), class: "btn btn-default" %td.amount - if tip.amount From a513ea65fa5a23019f636b22766ad274a0c47ddf Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 15 Jun 2014 20:00:05 +0200 Subject: [PATCH 274/372] hide pre block when commit message is missing --- app/views/distributions/_reason.html.haml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/views/distributions/_reason.html.haml b/app/views/distributions/_reason.html.haml index 41b19b80..5fa9f93a 100644 --- a/app/views/distributions/_reason.html.haml +++ b/app/views/distributions/_reason.html.haml @@ -9,6 +9,7 @@ Commit #{link_to truncate_commit(tip.commit), "https://github.com/#{tip.project.full_name}/commit/#{tip.commit}"}: - else Commit #{tip.commit} - %pre= tip.commit_message + - if tip.commit_message.present? + %pre= tip.commit_message - else = render_markdown tip.comment From c19c5e49e02ee4709b28eb2cd301f9a99d90fc83 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 15 Jun 2014 20:23:53 +0200 Subject: [PATCH 275/372] Reset confirmation token when request is sent --- app/controllers/users_controller.rb | 1 + app/models/user.rb | 4 ++++ features/distribution.feature | 29 +++++++++++++++++++++++ features/step_definitions/distribution.rb | 5 ++++ 4 files changed, 39 insertions(+) diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 048cba72..62ea74ad 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -51,6 +51,7 @@ def send_tips_back def send_email_address_request tip = Tip.find(params[:tip_id]) authorize! :update, tip.distribution + tip.user.reset_confirmation_token! UserMailer.address_request(tip, current_user).deliver redirect_to params[:return_url], notice: "Request sent" end diff --git a/app/models/user.rb b/app/models/user.rb index c5e9c7a0..4fd97c9b 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -78,6 +78,10 @@ def valid_github_user? end end + def reset_confirmation_token! + generate_confirmation_token! + end + private def generate_login_token! diff --git a/features/distribution.feature b/features/distribution.feature index 57fd991f..f4f2c716 100644 --- a/features/distribution.feature +++ b/features/distribution.feature @@ -234,6 +234,35 @@ Feature: Fundraisers can distribute funds | mubmzLrtTgDE2WrHkiwSFKuTh2VTSXboYK | 10.0 | And the project balance should be "490.00" + @javascript + Scenario: Send to an known email address who has no confirmation token + Given an user with email "bob@example.com" and without password nor confirmation token + And 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 email address "bob@example.com" to the recipients + And I fill the amount to "bob@example.com" with "10" + And I save the distribution + Then I should see these distribution lines: + | recipient | address | amount | percentage | + | bob@example.com | | 10 | 100.0 | + When I click on "Send email request to provide an address" + Then I should see "Request sent" + And an email should have been sent to "bob@example.com" + When I visit the link to set my password and address from the email + And I fill "Password" with "password" + And I fill "Password confirmation" with "password" + And I fill "Peercoin address" with "mubmzLrtTgDE2WrHkiwSFKuTh2VTSXboYK" + And I click on "Save" + + Then I should see "Information saved" + And the user with email "bob@example.com" should have "password" as password + And the user with email "bob@example.com" should have "mubmzLrtTgDE2WrHkiwSFKuTh2VTSXboYK" as peercoin address + Scenario: Send to someone who doesn't want to be notified Then pending diff --git a/features/step_definitions/distribution.rb b/features/step_definitions/distribution.rb index bcc2c085..ed18e439 100644 --- a/features/step_definitions/distribution.rb +++ b/features/step_definitions/distribution.rb @@ -11,6 +11,11 @@ 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").set(arg1) From f9815d96cab5eb41b40783001667af1e5188b07e Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 15 Jun 2014 20:31:44 +0200 Subject: [PATCH 276/372] Fixed invalid check on project balance --- app/models/distribution.rb | 2 +- features/distribution.feature | 22 ++++++++++++++++++++++ features/step_definitions/web.rb | 1 + 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/app/models/distribution.rb b/app/models/distribution.rb index aa8dbd8e..bfe5ec53 100644 --- a/app/models/distribution.rb +++ b/app/models/distribution.rb @@ -33,7 +33,7 @@ def send_transaction! data = generate_data update_attribute(:data, data) - raise "Not enough funds on Distribution##{id}" if total_amount > project.available_amount + raise "Not enough funds on Distribution##{id}" if project.available_amount < 0 txid = BitcoinDaemon.instance.send_many(project.address_label, JSON.parse(data)) diff --git a/features/distribution.feature b/features/distribution.feature index f4f2c716..77053a9c 100644 --- a/features/distribution.feature +++ b/features/distribution.feature @@ -466,3 +466,25 @@ Feature: Fundraisers can distribute funds 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" diff --git a/features/step_definitions/web.rb b/features/step_definitions/web.rb index 7d62526b..799f0e82 100644 --- a/features/step_definitions/web.rb +++ b/features/step_definitions/web.rb @@ -128,6 +128,7 @@ raise end url = URI.parse(link["href"]).request_uri + puts url visit url end From 1465621f12f1a5fba0ef01d2392dbb333767e194 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 15 Jun 2014 20:32:36 +0200 Subject: [PATCH 277/372] Fixed invalid check on project balance --- app/models/distribution.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/distribution.rb b/app/models/distribution.rb index bfe5ec53..f8ce3686 100644 --- a/app/models/distribution.rb +++ b/app/models/distribution.rb @@ -33,7 +33,7 @@ def send_transaction! data = generate_data update_attribute(:data, data) - raise "Not enough funds on Distribution##{id}" if project.available_amount < 0 + 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)) From 08ac07458b8c547dd5df8721548c6b532f659101 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 15 Jun 2014 20:41:00 +0200 Subject: [PATCH 278/372] Better distribution button positions --- app/assets/stylesheets/distribution.css.sass | 4 ++++ app/assets/stylesheets/home.css.sass | 3 +++ app/views/distributions/show.html.haml | 6 +++--- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/app/assets/stylesheets/distribution.css.sass b/app/assets/stylesheets/distribution.css.sass index af248ff8..10155531 100644 --- a/app/assets/stylesheets/distribution.css.sass +++ b/app/assets/stylesheets/distribution.css.sass @@ -1,5 +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/home.css.sass b/app/assets/stylesheets/home.css.sass index 9fb78e50..fdb37d40 100644 --- a/app/assets/stylesheets/home.css.sass +++ b/app/assets/stylesheets/home.css.sass @@ -34,6 +34,9 @@ h1:first-child .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) diff --git a/app/views/distributions/show.html.haml b/app/views/distributions/show.html.haml index 9b5ee283..b0a4f9a0 100644 --- a/app/views/distributions/show.html.haml +++ b/app/views/distributions/show.html.haml @@ -50,12 +50,12 @@ %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 total and can? :send_transaction, @distribution - .distribution-action - = button_to "Send the transaction", send_transaction_project_distribution_path(@project, @distribution), class: "btn btn-default", data: {confirm: "#{total.to_f / COIN} peercoins will be sent. Are you sure?"} - 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) From e28ed306c5e0b112a76473f22e0dc0aa59348e20 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 15 Jun 2014 20:51:52 +0200 Subject: [PATCH 279/372] Display fundraiser --- app/models/project.rb | 4 ++++ app/views/projects/show.html.haml | 7 ++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/app/models/project.rb b/app/models/project.rb index 6c9813a7..60760e94 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -230,4 +230,8 @@ def generate_address! def to_label name.presence || id.to_s end + + def fundraisers + User.where(nickname: collaborators.map(&:login)) + end end diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index 6e37e9b7..486b6101 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -47,11 +47,15 @@ Project informations .panel-body.text-center %table.table.text-left + %tr + %td Fundraiser + %td + - @project.fundraisers.each do |user| + .fundraiser= link_to user.full_name, user %tr %td Funds %td = btc_human @project.available_amount - = link_to "Donate", donate_project_path(@project), class: "btn btn-primary" = link_to "List of donors", donors_project_path(@project) %tr %td Distributions @@ -70,6 +74,7 @@ = link_to label, [@project, distribution] = link_to "All distributions", project_distributions_path(@project) + = 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? From 8b16011b9567ba8d9a653c4ffe8c34b4de864c04 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 15 Jun 2014 20:55:50 +0200 Subject: [PATCH 280/372] Display project list on user page --- app/models/user.rb | 4 ++++ app/views/users/show.html.haml | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/app/models/user.rb b/app/models/user.rb index 4fd97c9b..c1a13eb0 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -82,6 +82,10 @@ def reset_confirmation_token! generate_confirmation_token! end + def projects + Project.joins(:collaborators).where(collaborators: {login: nickname}) + end + private def generate_login_token! diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index 7f5d6eab..0fdce978 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -4,5 +4,11 @@ %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) From 49f0d207002f7cbb38f02039734b414a51306f0f Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Mon, 16 Jun 2014 08:24:39 +0200 Subject: [PATCH 281/372] Cleanup --- features/step_definitions/web.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/features/step_definitions/web.rb b/features/step_definitions/web.rb index 799f0e82..7d62526b 100644 --- a/features/step_definitions/web.rb +++ b/features/step_definitions/web.rb @@ -128,7 +128,6 @@ raise end url = URI.parse(link["href"]).request_uri - puts url visit url end From 334e99aa59b896b983c713cfc39b36cc74dfdf35 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Mon, 16 Jun 2014 07:57:34 +0200 Subject: [PATCH 282/372] Allow email users to create and manage projects --- app/controllers/projects_controller.rb | 2 +- .../users/omniauth_callbacks_controller.rb | 4 +++ app/models/ability.rb | 10 +++--- app/models/collaborator.rb | 1 + app/models/project.rb | 5 +-- app/models/user.rb | 7 ++-- app/views/projects/donate.html.haml | 2 +- app/views/projects/show.html.haml | 2 +- ...20140616055815_add_user_to_collaborator.rb | 5 +++ ...convert_collaborator_nick_names_to_user.rb | 8 +++++ db/schema.rb | 4 ++- features/create_project.feature | 35 +++++++++++++++++++ features/step_definitions/create_project.rb | 6 +++- 13 files changed, 73 insertions(+), 18 deletions(-) create mode 100644 db/migrate/20140616055815_add_user_to_collaborator.rb create mode 100644 db/migrate/20140616060504_convert_collaborator_nick_names_to_user.rb diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 5e211860..a0cdcf0c 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -80,7 +80,7 @@ def qrcode def create @project = Project.new(project_params) @project.hold_tips = true - @project.collaborators.build(login: current_user.nickname) + @project.collaborators.build(user: current_user) authorize! :create, @project if @project.save diff --git a/app/controllers/users/omniauth_callbacks_controller.rb b/app/controllers/users/omniauth_callbacks_controller.rb index 3afec97e..c3a99b31 100644 --- a/app/controllers/users/omniauth_callbacks_controller.rb +++ b/app/controllers/users/omniauth_callbacks_controller.rb @@ -23,6 +23,10 @@ def github @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(@user) redirect_to request.env["omniauth.origin"].presence || after_sign_in_path_for(@user) diff --git a/app/models/ability.rb b/app/models/ability.rb index 0c0573b7..a8fab4aa 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -5,11 +5,11 @@ def initialize(user) can [:read, :donate, :donors], Project can :read, Distribution - if user and user.nickname.present? - can [:update, :decide_tip_amounts, :commit_suggestions, :github_user_suggestions], Project, collaborators: {login: user.nickname} - can [:create], Project - can [:create], Distribution, project: {collaborators: {login: user.nickname}} - can [:update, :new_recipient_form], Distribution, project: {collaborators: {login: user.nickname}}, txid: nil, sent_at: nil + 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 diff --git a/app/models/collaborator.rb b/app/models/collaborator.rb index 3c0e6f15..0bf3805d 100644 --- a/app/models/collaborator.rb +++ b/app/models/collaborator.rb @@ -1,3 +1,4 @@ class Collaborator < ActiveRecord::Base belongs_to :project + belongs_to :user end diff --git a/app/models/project.rb b/app/models/project.rb index 60760e94..9b8eddd7 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -5,6 +5,7 @@ class Project < ActiveRecord::Base has_many :collaborators has_many :distributions, inverse_of: :project has_many :donation_addresses, inverse_of: :project + has_many :users, through: :collaborators has_many :cold_storage_transfers @@ -230,8 +231,4 @@ def generate_address! def to_label name.presence || id.to_s end - - def fundraisers - User.where(nickname: collaborators.map(&:login)) - end end diff --git a/app/models/user.rb b/app/models/user.rb index c1a13eb0..5a4e3195 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -12,6 +12,9 @@ class User < ActiveRecord::Base has_many :tips + has_many :collaborators + has_many :projects, through: :collaborators + before_create :generate_login_token!, unless: :login_token? acts_as_commontator @@ -82,10 +85,6 @@ def reset_confirmation_token! generate_confirmation_token! end - def projects - Project.joins(:collaborators).where(collaborators: {login: nickname}) - end - private def generate_login_token! diff --git a/app/views/projects/donate.html.haml b/app/views/projects/donate.html.haml index 803daea5..c35201b6 100644 --- a/app/views/projects/donate.html.haml +++ b/app/views/projects/donate.html.haml @@ -1,7 +1,7 @@ .row .col-md-12 %h1 Donate to #{@project.name} - - fundraisers = User.where(nickname: @project.collaborators.map(&:login)) + - fundraisers = @project.users - if fundraisers.any? %p - if fundraisers.size > 1 diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index 486b6101..9f040fbe 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -50,7 +50,7 @@ %tr %td Fundraiser %td - - @project.fundraisers.each do |user| + - @project.users.each do |user| .fundraiser= link_to user.full_name, user %tr %td Funds 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..46c014fe --- /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)") + end + + def down + end +end diff --git a/db/schema.rb b/db/schema.rb index 6def0ffd..899564e0 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20140615124857) do +ActiveRecord::Schema.define(version: 20140616060504) do create_table "cold_storage_transfers", force: true do |t| t.integer "project_id" @@ -31,9 +31,11 @@ t.string "login" t.datetime "created_at" t.datetime "updated_at" + t.integer "user_id" end add_index "collaborators", ["project_id"], name: "index_collaborators_on_project_id" + add_index "collaborators", ["user_id"], name: "index_collaborators_on_user_id" create_table "commits", force: true do |t| t.integer "project_id" diff --git a/features/create_project.feature b/features/create_project.feature index c30f77a0..df05546a 100644 --- a/features/create_project.feature +++ b/features/create_project.feature @@ -87,3 +87,38 @@ Feature: An user can create a project, linked with GitHub or not. 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/step_definitions/create_project.rb b/features/step_definitions/create_project.rb index f3ba6a8e..088b5e84 100644 --- a/features/step_definitions/create_project.rb +++ b/features/step_definitions/create_project.rb @@ -25,7 +25,11 @@ end Then(/^the project single collaborators should be "(.*?)"$/) do |arg1| - @project.collaborators.map(&:login).should eq([arg1]) + if arg1 =~ /@/ + @project.collaborators.map(&:user).map(&:email).should eq([arg1]) + else + @project.collaborators.map(&:user).map(&:nickname).should eq([arg1]) + end end Then(/^the project address label should be "(.*?)"$/) do |arg1| From c18cdfcd00f405eb9caf1c09e2cb9e66d0323871 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Mon, 16 Jun 2014 08:30:06 +0200 Subject: [PATCH 283/372] Works with duplicate nickname --- .../20140616060504_convert_collaborator_nick_names_to_user.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db/migrate/20140616060504_convert_collaborator_nick_names_to_user.rb b/db/migrate/20140616060504_convert_collaborator_nick_names_to_user.rb index 46c014fe..66417375 100644 --- a/db/migrate/20140616060504_convert_collaborator_nick_names_to_user.rb +++ b/db/migrate/20140616060504_convert_collaborator_nick_names_to_user.rb @@ -1,6 +1,6 @@ class ConvertCollaboratorNickNamesToUser < ActiveRecord::Migration def up - execute("UPDATE collaborators SET user_id = (SELECT id FROM users WHERE users.nickname = collaborators.login)") + execute("UPDATE collaborators SET user_id = (SELECT id FROM users WHERE users.nickname = collaborators.login LIMIT 1)") end def down From 6899ebfeba0e1a70f21f3e055d310e7251a62cc7 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Mon, 16 Jun 2014 08:37:35 +0200 Subject: [PATCH 284/372] Center home panels --- app/assets/stylesheets/home.css.sass | 1 + 1 file changed, 1 insertion(+) diff --git a/app/assets/stylesheets/home.css.sass b/app/assets/stylesheets/home.css.sass index fdb37d40..02336fbe 100644 --- a/app/assets/stylesheets/home.css.sass +++ b/app/assets/stylesheets/home.css.sass @@ -154,6 +154,7 @@ h4 margin-top: 10px p font-size: 14px + text-align: center .panel.note-panel background: image-url("panel-top.png") left top no-repeat From 7dec2f9fe34bc62c6e477331d6631b6ece3387b5 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Mon, 16 Jun 2014 08:40:00 +0200 Subject: [PATCH 285/372] Do not overwrite name and image --- app/controllers/users/omniauth_callbacks_controller.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/users/omniauth_callbacks_controller.rb b/app/controllers/users/omniauth_callbacks_controller.rb index c3a99b31..2ccadfe4 100644 --- a/app/controllers/users/omniauth_callbacks_controller.rb +++ b/app/controllers/users/omniauth_callbacks_controller.rb @@ -20,8 +20,8 @@ def github 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| From 612674c777e3a93db83b33e0bd541d355b329de9 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Mon, 16 Jun 2014 09:08:13 +0200 Subject: [PATCH 286/372] Better project show with empty items --- app/views/projects/show.html.haml | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index 9f040fbe..6a62279d 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -56,23 +56,28 @@ %td Funds %td = btc_human @project.available_amount - = link_to "List of donors", donors_project_path(@project) + - if @project.deposits.any? + .list-of-donors + = link_to "List of donors", donors_project_path(@project) %tr %td Distributions %td - %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.sent? - - if distribution.sent_at - - label << " sent #{time_ago_in_words(distribution.sent_at)} ago" + - 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.sent? + - if distribution.sent_at + - label << " sent #{time_ago_in_words(distribution.sent_at)} ago" + - else + - label << " sent" - else - - label << " sent" - - else - - label << " not sent" - = link_to label, [@project, distribution] - = link_to "All distributions", project_distributions_path(@project) + - label << " not sent" + = link_to label, [@project, distribution] + = link_to "All distributions", project_distributions_path(@project) = link_to "Donate", donate_project_path(@project), class: "btn btn-primary btn-block" - if can? :update, @project From 47aafa91e4b5da7a926304005d3a2b0b0a51210d Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Mon, 16 Jun 2014 13:35:19 +0200 Subject: [PATCH 287/372] IE8 fixes --- app/views/layouts/application.html.haml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 29790caf..28a586ac 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -16,6 +16,12 @@ = 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 From 6fd437ec329ac60dd2849bd950c3efe3044b54cd Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Mon, 16 Jun 2014 19:48:30 +0200 Subject: [PATCH 288/372] Fixed layout on Firefox --- app/assets/stylesheets/home.css.sass | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/app/assets/stylesheets/home.css.sass b/app/assets/stylesheets/home.css.sass index 02336fbe..375f85a5 100644 --- a/app/assets/stylesheets/home.css.sass +++ b/app/assets/stylesheets/home.css.sass @@ -178,13 +178,16 @@ h4 @media(min-width:992px) #home-page .main-panel - position: relative - padding-bottom: 70px - .button-container - position: absolute - bottom: 0 - left: 15px - right: 15px + .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 From 9f24be3a2d4047684f91914ac5e00ed62457f01e Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Mon, 16 Jun 2014 19:50:34 +0200 Subject: [PATCH 289/372] Added margin to circles --- app/assets/stylesheets/home.css.sass | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/home.css.sass b/app/assets/stylesheets/home.css.sass index 375f85a5..f28b7134 100644 --- a/app/assets/stylesheets/home.css.sass +++ b/app/assets/stylesheets/home.css.sass @@ -133,7 +133,7 @@ h4 width: 100px height: 100px +border-radius(100px) - margin: auto + margin: 10px auto &.how-it-works:before background: #98cc3d image-url("picto-books.png") center center no-repeat From afc70cc01c1276f27951fa97f1fc28c34843c8d9 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Mon, 16 Jun 2014 21:00:57 +0200 Subject: [PATCH 290/372] Updated FAQ --- app/views/home/faq.html.md | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/app/views/home/faq.html.md b/app/views/home/faq.html.md index 2cd33ee7..813ee11e 100644 --- a/app/views/home/faq.html.md +++ b/app/views/home/faq.html.md @@ -3,7 +3,11 @@ Peer4commit FAQ What is Peer4commit? -------------------- -Peer4commit is a website where anyone can raise funds for any kind of project. +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? -------------------------------------------------- @@ -13,7 +17,7 @@ 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 this 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. +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. @@ -38,7 +42,7 @@ The donors can browse the fundraiser history: what projects he managed, how he d 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 will 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. +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? @@ -50,12 +54,17 @@ For example the fact a project has a lot of donations is not an indication that 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 suspicious. 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. +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? ------------- @@ -70,7 +79,7 @@ Examples of successful projects: Currency -------- -For now the only supported currency is <%= link_to "Peercoin", "http://peercoin.net/" %>. Support for other currencies will be added later. +For now the only supported currency is <%= link_to "Peercoin", "http://peercoin.net/" %>. Other currencies will be added later. How can I raise funds? @@ -101,15 +110,11 @@ Browse the project list and click on the "Donate" button. You will be asked for 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. -Do you have an audit page? --------------------------- -Yes, it shows different information, such as amount donated, available balance, transaction fees, amount in cold storage and includes addresses for each project. You can view the page <%= link_to "here", audit_path %>. - 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. +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. -In the future the funds will be sent to a multisignature address so that the fundraiser and Peer4commit must agree to send the funds. +In the future the donations will be sent to a multisignature address so that the fundraiser and Peer4commit must agree to send the funds. The donors will also have key. Contact ------- From 0bdd61320d79f50c61f2ca75cb5b281d8d8d4e01 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Tue, 17 Jun 2014 07:52:08 +0200 Subject: [PATCH 291/372] Added future plans to the FAQ --- app/views/home/faq.html.md | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/app/views/home/faq.html.md b/app/views/home/faq.html.md index 813ee11e..0c093a34 100644 --- a/app/views/home/faq.html.md +++ b/app/views/home/faq.html.md @@ -109,12 +109,47 @@ Browse the project list and click on the "Donate" button. You will be asked for 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. + +### 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. -In the future the donations will be sent to a multisignature address so that the fundraiser and Peer4commit must agree to send the funds. The donors will also have key. +When multisignatures are implemented Peer4commit will not have direct the control over the funds (see above). Contact ------- From 5baae9f8d12fac3f6fc210dedb99ff552236d2ab Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Tue, 17 Jun 2014 08:01:56 +0200 Subject: [PATCH 292/372] Added suggestions made by mhps and Cybnate --- app/views/home/faq.html.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/app/views/home/faq.html.md b/app/views/home/faq.html.md index 0c093a34..a3161ecd 100644 --- a/app/views/home/faq.html.md +++ b/app/views/home/faq.html.md @@ -84,7 +84,13 @@ For now the only supported currency is <%= link_to "Peercoin", "http://peercoin. How can I raise funds? ---------------------- -Click on the <%= link_to '"Create a project"', new_project_path %> button. You'll have to fill a description. Make it the most complete possible and provide proofs of your trustworthiness and abilities. +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. @@ -98,9 +104,7 @@ Fundraisers should explain in the project description whether they intend to pay How can I get paid to do the actual work? ----------------------------------------- -Browse the projects and read descriptions to see if there's something you can do and contact the fundraiser. There are so many possibilities it's hard to give a guideline. - -We hope projects will appear to make things easier for potential contributors to find something to do. +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? From 329e1a351bef8767f7a95d8ff4f4bdb4ee1b3ddb Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Tue, 17 Jun 2014 08:48:30 +0200 Subject: [PATCH 293/372] Note about multisignature and decentralization --- app/views/home/faq.html.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/views/home/faq.html.md b/app/views/home/faq.html.md index a3161ecd..13d5de9b 100644 --- a/app/views/home/faq.html.md +++ b/app/views/home/faq.html.md @@ -139,6 +139,8 @@ Also if a fundraiser is clearly misbehaving the website and the supporter can de ### 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. From 376c214b10b4e7ad1ce62bf8456d5cf7695e4211 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Tue, 17 Jun 2014 08:57:50 +0200 Subject: [PATCH 294/372] Removed useless ERB syntax --- app/views/home/faq.html.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/home/faq.html.md b/app/views/home/faq.html.md index 13d5de9b..ed706427 100644 --- a/app/views/home/faq.html.md +++ b/app/views/home/faq.html.md @@ -79,7 +79,7 @@ Examples of successful projects: Currency -------- -For now the only supported currency is <%= link_to "Peercoin", "http://peercoin.net/" %>. Other currencies will be added later. +For now the only supported currency is [Peercoin](http://peercoin.net/). Other currencies will be added later. How can I raise funds? @@ -159,5 +159,5 @@ When multisignatures are implemented Peer4commit will not have direct the contro Contact ------- -If you have any question send a message to <%= mail_to "contact@peer4commit.com" %> or <%= link_to "open an issue on GitHub", "https://github.com/sigmike/peer4commit/issues/new" %>. +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). From 47bc974780b35a966f3d2366d8d2f779b18a20ed Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Thu, 26 Jun 2014 14:26:00 +0200 Subject: [PATCH 295/372] Only record changes when the state changed --- config/initializers/record_changes.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/config/initializers/record_changes.rb b/config/initializers/record_changes.rb index 8596453a..6d727943 100644 --- a/config/initializers/record_changes.rb +++ b/config/initializers/record_changes.rb @@ -4,7 +4,10 @@ def self.record_changes(options) after_save do state = to_json(options) - RecordChange.create!(record: self, raw_state: state) + 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 From 915d25b7477f569ef968a37f48c9d216332d32d4 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sat, 28 Jun 2014 09:12:27 +0200 Subject: [PATCH 296/372] Display name in audit page --- app/views/home/audit.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/home/audit.html.haml b/app/views/home/audit.html.haml index 029d0bdb..fee93952 100644 --- a/app/views/home/audit.html.haml +++ b/app/views/home/audit.html.haml @@ -35,7 +35,7 @@ - project_slice.each do |project| %tr{class: project.disabled ? "text-muted" : nil} %td - %strong= link_to project.full_name, project + %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) From 7c113329db76100d3e3204c74945569cc8074227 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Fri, 4 Jul 2014 08:22:09 +0200 Subject: [PATCH 297/372] Method to merge 2 users --- app/controllers/distributions_controller.rb | 4 +-- app/controllers/projects_controller.rb | 2 +- app/controllers/registrations_controller.rb | 2 +- .../users/omniauth_callbacks_controller.rb | 4 +-- app/controllers/users_controller.rb | 4 +-- app/models/project.rb | 4 +-- app/models/tip.rb | 4 +-- app/models/user.rb | 31 +++++++++++++++++++ .../20140704060602_add_disabled_to_user.rb | 6 ++++ db/schema.rb | 8 +++-- 10 files changed, 54 insertions(+), 15 deletions(-) create mode 100644 db/migrate/20140704060602_add_disabled_to_user.rb diff --git a/app/controllers/distributions_controller.rb b/app/controllers/distributions_controller.rb index ed398dc0..b538cef7 100644 --- a/app/controllers/distributions_controller.rb +++ b/app/controllers/distributions_controller.rb @@ -46,7 +46,7 @@ def send_transaction def new_recipient_form @tips = [] if params[:user] and params[:user][:nickname].present? - user = User.where(nickname: params[:user][:nickname]).first_or_initialize + 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! @@ -54,7 +54,7 @@ def new_recipient_form end @tips << Tip.new(user: user) elsif params[:user] and params[:user][:email].present? - user = User.where(email: params[:user][:email]).first_or_initialize + user = User.enabled.where(email: params[:user][:email]).first_or_initialize if user.new_record? raise "Invalid email address" unless user.email =~ Devise::email_regexp user.skip_confirmation_notification! diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index a0cdcf0c..5481f80e 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -114,7 +114,7 @@ def github_user_suggestions respond_to do |format| format.json do query = params[:query] - users = User.where.not(nickname: nil).where.not(nickname: '').where('nickname LIKE ? OR name LIKE ?', "%#{query}%", "%#{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, diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb index 4c564422..b21f093d 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/registrations_controller.rb @@ -1,6 +1,6 @@ class RegistrationsController < Devise::RegistrationsController def update - @user = User.find(current_user.id) + @user = User.enabled.find(current_user.id) user_params = devise_parameter_sanitizer.sanitize(:account_update) diff --git a/app/controllers/users/omniauth_callbacks_controller.rb b/app/controllers/users/omniauth_callbacks_controller.rb index 2ccadfe4..4c91afb9 100644 --- a/app/controllers/users/omniauth_callbacks_controller.rb +++ b/app/controllers/users/omniauth_callbacks_controller.rb @@ -2,9 +2,9 @@ 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 :nickname => info["nickname"] + @user = User.enabled.find_by :nickname => info["nickname"] if @user.nil? and info["verified_emails"].any? - @user = User.find_by :email => info["verified_emails"] + @user = User.enabled.find_by :email => info["verified_emails"] end unless @user if info['primary_email'] diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 62ea74ad..cd002dd3 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -1,7 +1,7 @@ class UsersController < ApplicationController before_action except: [:show, :login, :index, :send_email_address_request, :set_password_and_address] do - @user = User.where(id: params[:id]).first + @user = User.enabled.where(id: params[:id]).first if current_user if current_user != @user redirect_to root_path, alert: "Access denied" @@ -57,7 +57,7 @@ def send_email_address_request end def set_password_and_address - @user = User.find(params[:id]) + @user = User.enabled.find(params[:id]) raise "Blank token" if params[:token].blank? raise "Invalid token" unless Devise.secure_compare(params[:token], @user.confirmation_token) if params[:user] diff --git a/app/models/project.rb b/app/models/project.rb index 9b8eddd7..c8e1e6f1 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -91,9 +91,9 @@ def tip_commits def tip_for commit email = commit.commit.author.email if nickname = commit.author.try(:login) - user = User.find_by(nickname: nickname) + user = User.enabled.find_by(nickname: nickname) end - user ||= User.find_by(email: email) + user ||= User.enabled.find_by(email: email) if (next_tip_amount > 0) && Tip.find_by(commit: commit.sha).nil? diff --git a/app/models/tip.rb b/app/models/tip.rb index d78fead8..630691ea 100644 --- a/app/models/tip.rb +++ b/app/models/tip.rb @@ -110,9 +110,9 @@ def coin_amount=(coin_amount) def self.build_from_commit(commit) if commit.username.present? - user = User.where(nickname: commit.username).first_or_initialize(email: commit.email) + user = User.enabled.where(nickname: commit.username).first_or_initialize(email: commit.email) elsif commit.email =~ Devise::email_regexp - user = User.where(email: commit.email).first_or_initialize + user = User.enabled.where(email: commit.email).first_or_initialize else return nil end diff --git a/app/models/user.rb b/app/models/user.rb index 5a4e3195..88397081 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -15,11 +15,17 @@ class User < ActiveRecord::Base 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? acts_as_commontator acts_as_commontable + scope :enabled, -> { where(disabled: false) } + scope :disabled, -> { where(disabled: true) } + def github_url "https://github.com/#{nickname}" end @@ -85,6 +91,31 @@ 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! 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/schema.rb b/db/schema.rb index 899564e0..a295c22e 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20140616060504) do +ActiveRecord::Schema.define(version: 20140704060602) do create_table "cold_storage_transfers", force: true do |t| t.integer "project_id" @@ -199,11 +199,11 @@ add_index "tips", ["user_id"], name: "index_tips_on_user_id" create_table "users", force: true do |t| - t.string "encrypted_password", default: "", null: false + t.string "encrypted_password", default: "", null: false t.string "reset_password_token" 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" @@ -224,8 +224,10 @@ t.string "unconfirmed_email" t.string "email" t.string "nickname" + t.boolean "disabled", default: false end + add_index "users", ["disabled"], name: "index_users_on_disabled" add_index "users", ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true end From 4f49962ecf8a8f315e4d6cdda6a7cc83f8402823 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 6 Jul 2014 10:23:31 +0200 Subject: [PATCH 298/372] Do not record changes on ignored changes --- app/models/project.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/project.rb b/app/models/project.rb index c8e1e6f1..d242fbb7 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -14,7 +14,7 @@ class Project < ActiveRecord::Base has_many :commits - record_changes(except: [:available_amount_cache, :last_commit]) + record_changes(except: [:available_amount_cache, :last_commit, :updated_at]) acts_as_commontable From d3e1eb2a51f90da51e4879782cc132b0b005581b Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 6 Jul 2014 09:59:46 +0200 Subject: [PATCH 299/372] Removed github id from database --- db/migrate/20140706075813_remove_git_hub_id_from_project.rb | 5 +++++ db/schema.rb | 5 +---- features/step_definitions/common.rb | 6 +++--- features/step_definitions/create_project.rb | 4 ---- 4 files changed, 9 insertions(+), 11 deletions(-) create mode 100644 db/migrate/20140706075813_remove_git_hub_id_from_project.rb 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/schema.rb b/db/schema.rb index a295c22e..185b1aa9 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20140704060602) do +ActiveRecord::Schema.define(version: 20140706075813) do create_table "cold_storage_transfers", force: true do |t| t.integer "project_id" @@ -144,7 +144,6 @@ t.string "language" t.string "last_commit" t.integer "available_amount_cache", limit: 8 - t.string "github_id" t.string "address_label" t.boolean "hold_tips", default: true t.string "cold_storage_withdrawal_address" @@ -154,8 +153,6 @@ t.text "detailed_description" end - add_index "projects", ["github_id"], name: "index_projects_on_github_id", unique: true - create_table "record_changes", force: true do |t| t.integer "record_id" t.string "record_type" diff --git a/features/step_definitions/common.rb b/features/step_definitions/common.rb index f77fbe23..a54fdef1 100644 --- a/features/step_definitions/common.rb +++ b/features/step_definitions/common.rb @@ -33,15 +33,15 @@ end Given(/^a project$/) do - @project = Project.create!(name: "test", full_name: "example/test", github_id: 123, bitcoin_address: 'mq4NtnmQoQoPfNWEPbhSvxvncgtGo6L8WY', address_label: "example_project_account", hold_tips: false) + @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}", github_id: Digest::SHA1.hexdigest(arg1), bitcoin_address: 'mq4NtnmQoQoPfNWEPbhSvxvncgtGo6L8WY', hold_tips: false) + @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}", github_id: Digest::SHA1.hexdigest(arg1), bitcoin_address: 'mq4NtnmQoQoPfNWEPbhSvxvncgtGo6L8WY', hold_tips: true) + @project = Project.create!(name: "test", full_name: "example/#{arg1}", bitcoin_address: 'mq4NtnmQoQoPfNWEPbhSvxvncgtGo6L8WY', hold_tips: true) end Given(/^a deposit of "(.*?)"$/) do |arg1| diff --git a/features/step_definitions/create_project.rb b/features/step_definitions/create_project.rb index 088b5e84..a8b65487 100644 --- a/features/step_definitions/create_project.rb +++ b/features/step_definitions/create_project.rb @@ -20,10 +20,6 @@ @project.full_name.should eq(arg1) end -Then(/^the project GitHub ID should be "(.*?)"$/) do |arg1| - @project.github_id.should eq(arg1) -end - Then(/^the project single collaborators should be "(.*?)"$/) do |arg1| if arg1 =~ /@/ @project.collaborators.map(&:user).map(&:email).should eq([arg1]) From 203f59b097a358efda4cd984bde8d4623121130f Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 6 Jul 2014 10:11:29 +0200 Subject: [PATCH 300/372] Changing repository does not break tip for commit --- features/change_github.feature | 37 ++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 features/change_github.feature diff --git a/features/change_github.feature b/features/change_github.feature new file mode 100644 index 00000000..e34d0fae --- /dev/null +++ b/features/change_github.feature @@ -0,0 +1,37 @@ +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" + + 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 | + From d73c62e6b696f1da16841d30b616bd3037a180b5 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 6 Jul 2014 10:56:48 +0200 Subject: [PATCH 301/372] Create commits even when there's no deposit --- app/models/project.rb | 13 ++++++++++--- features/step_definitions/common.rb | 2 ++ features/step_definitions/tip_for_commit.rb | 2 ++ lib/bitcoin_tipper.rb | 7 +++---- 4 files changed, 17 insertions(+), 7 deletions(-) diff --git a/app/models/project.rb b/app/models/project.rb index d242fbb7..f0dfdea9 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -57,9 +57,7 @@ def get_commits commits || [] end - def tip_commits - return unless self.deposits.any? - + def update_commits commits = get_commits commits.each do |commit| @@ -72,8 +70,17 @@ def tip_commits email: commit.commit.author.email, ) end + end + + def tip_commits + 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 diff --git a/features/step_definitions/common.rb b/features/step_definitions/common.rb index a54fdef1..865bd95e 100644 --- a/features/step_definitions/common.rb +++ b/features/step_definitions/common.rb @@ -106,6 +106,8 @@ def find_new_commit(id) When(/^the new commits are read$/) do @project.reload @project.should_receive(:get_commits).and_return(@commits.values.map(&:to_ostruct)) + @project.update_commits + @project.should_receive(:get_commits).and_return(@commits.values.map(&:to_ostruct)) @project.tip_commits end diff --git a/features/step_definitions/tip_for_commit.rb b/features/step_definitions/tip_for_commit.rb index 67fafc2f..03d34858 100644 --- a/features/step_definitions/tip_for_commit.rb +++ b/features/step_definitions/tip_for_commit.rb @@ -8,6 +8,8 @@ end Given(/^the commits on GitHub for project "(.*?)" are$/) do |arg1, table| + @project.reload + @project.full_name.should eq(arg1) commits = [] table.hashes.each do |row| commit = OpenStruct.new( diff --git a/lib/bitcoin_tipper.rb b/lib/bitcoin_tipper.rb index a2e02367..4dd1ca7d 100644 --- a/lib/bitcoin_tipper.rb +++ b/lib/bitcoin_tipper.rb @@ -8,10 +8,9 @@ def self.work_forever def self.work Rails.logger.info "Traversing projects..." Project.enabled.find_each do |project| - if project.available_amount > 0 - Rails.logger.info " Project #{project.id} #{project.full_name}" - project.tip_commits - end + Rails.logger.info " Project #{project.id} #{project.full_name}" + project.update_commits + project.tip_commits end Rails.logger.info "Sending tips to commits..." From 066e5a3c07aa620c268ae7cb4e230925ffe63be6 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 6 Jul 2014 11:12:40 +0200 Subject: [PATCH 302/372] Autolink in detailed description --- app/helpers/application_helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 58925c96..dd38f33e 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -66,7 +66,7 @@ def render_flash_message def render_markdown(source) return nil unless source - markdown = Redcarpet::Markdown.new(Redcarpet::Render::HTML.new(safe_links_only: true, filter_html: true)) + 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 From afe07e130eec1699e10bc7cfd2654b9de375743a Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 6 Jul 2014 12:30:41 +0200 Subject: [PATCH 303/372] Use markdown to display tipping policies --- app/views/projects/show.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index 6a62279d..4c828619 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -16,7 +16,7 @@ - if @project.tipping_policies_text.try(:text).present? %h4 Tipping policies - = auto_link simple_format @project.tipping_policies_text.text + = render_markdown @project.tipping_policies_text.text %small %em - user = @project.tipping_policies_text.user From efc357e707a3817cbb0450877388aeca2c3a7ed7 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Thu, 10 Jul 2014 19:57:53 +0200 Subject: [PATCH 304/372] fixed error on tips page --- app/views/tips/index.html.haml | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/app/views/tips/index.html.haml b/app/views/tips/index.html.haml index 1fb23669..b9807bf6 100644 --- a/app/views/tips/index.html.haml +++ b/app/views/tips/index.html.haml @@ -20,13 +20,16 @@ %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 + - 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? From 9aa7d8fd87ff0714e2dfd695f2098d31706dbc95 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sat, 12 Jul 2014 21:04:52 +0200 Subject: [PATCH 305/372] ignore blocked repositories --- app/models/project.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/project.rb b/app/models/project.rb index f0dfdea9..e05e3195 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -44,7 +44,7 @@ def get_commits client.commits(full_name) end rescue Octokit::BadGateway, Octokit::NotFound, Octokit::InternalServerError, - Errno::ETIMEDOUT, Faraday::Error::ConnectionFailed => e + Errno::ETIMEDOUT, Faraday::Error::ConnectionFailed, Octokit::Forbidden => e Rails.logger.info "Project ##{id}: #{e.class} happened" rescue StandardError => e if CONFIG["airbrake"] From 017f2ed57b63ffec3634a517688574d65779edab Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 13 Jul 2014 07:23:23 +0200 Subject: [PATCH 306/372] Handle already confirmed email --- app/controllers/users_controller.rb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index cd002dd3..79684cc2 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -59,6 +59,12 @@ def send_email_address_request 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) From 2f7fe61e642819ef40916d97131c8da72898b9b6 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Mon, 14 Jul 2014 10:16:23 +0200 Subject: [PATCH 307/372] Screenshot on failing scenario --- Gemfile | 1 + Gemfile.lock | 6 ++++++ features/support/env.rb | 2 ++ 3 files changed, 9 insertions(+) diff --git a/Gemfile b/Gemfile index 366dd143..b7805c38 100644 --- a/Gemfile +++ b/Gemfile @@ -88,4 +88,5 @@ group :test do gem 'factory_girl_rails' gem 'poltergeist' gem 'timecop' + gem 'capybara-screenshot' end diff --git a/Gemfile.lock b/Gemfile.lock index 945856ca..99b12be2 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -92,6 +92,9 @@ GEM rack (>= 1.0.0) rack-test (>= 0.5.4) xpath (~> 2.0) + capybara-screenshot (0.3.20) + capybara (>= 1.0, < 3) + launchy chronic (0.10.2) chunky_png (1.3.1) cliver (0.3.2) @@ -180,6 +183,8 @@ GEM kaminari (0.15.0) actionpack (>= 3.0.0) activesupport (>= 3.0.0) + launchy (2.4.2) + addressable (~> 2.3) less (2.4.0) commonjs (~> 0.2.7) less-rails (2.4.2) @@ -338,6 +343,7 @@ DEPENDENCIES capistrano-bundler (>= 1.1.0) capistrano-rails capistrano-rvm! + capybara-screenshot coffee-rails (~> 4.0.0) commontator (~> 4.6.0) compass-rails diff --git a/features/support/env.rb b/features/support/env.rb index e0f1bf24..501084dd 100644 --- a/features/support/env.rb +++ b/features/support/env.rb @@ -65,3 +65,5 @@ end Capybara.javascript_driver = :poltergeist end + +require 'capybara-screenshot/cucumber' From 122f60f0bfe5afab365fb4186503f354ecea83c2 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Mon, 14 Jul 2014 10:06:53 +0200 Subject: [PATCH 308/372] Assign unique identifier to each user --- app/models/user.rb | 9 +++++++++ app/views/devise/registrations/edit.html.haml | 1 + app/views/users/show.html.haml | 4 ++++ .../20140714074128_add_identifier_to_user.rb | 17 ++++++++++++++++ db/schema.rb | 4 +++- features/step_definitions/user_identifier.rb | 5 +++++ features/step_definitions/web.rb | 4 ++++ features/user_identifier.feature | 20 +++++++++++++++++++ 8 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 db/migrate/20140714074128_add_identifier_to_user.rb create mode 100644 features/step_definitions/user_identifier.rb create mode 100644 features/user_identifier.feature diff --git a/app/models/user.rb b/app/models/user.rb index 88397081..b7c32c07 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -19,6 +19,7 @@ class User < ActiveRecord::Base 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 @@ -125,4 +126,12 @@ def generate_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/devise/registrations/edit.html.haml b/app/views/devise/registrations/edit.html.haml index c148112e..049139c1 100644 --- a/app/views/devise/registrations/edit.html.haml +++ b/app/views/devise/registrations/edit.html.haml @@ -3,6 +3,7 @@ %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? diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index 0fdce978..a3194140 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -1,4 +1,8 @@ %h1= @user.name +%p + %strong Identifier: + = @user.identifier + - if @user.nickname.present? %p %strong GitHub account: 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/schema.rb b/db/schema.rb index 185b1aa9..9e9da2e7 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20140706075813) do +ActiveRecord::Schema.define(version: 20140714074128) do create_table "cold_storage_transfers", force: true do |t| t.integer "project_id" @@ -222,9 +222,11 @@ t.string "email" t.string "nickname" t.boolean "disabled", default: false + t.string "identifier", null: false end add_index "users", ["disabled"], name: "index_users_on_disabled" + add_index "users", ["identifier"], name: "index_users_on_identifier", unique: true add_index "users", ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true end diff --git a/features/step_definitions/user_identifier.rb b/features/step_definitions/user_identifier.rb new file mode 100644 index 00000000..27aee5ae --- /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 + identifier.should be_present + page.should have_content identifier +end diff --git a/features/step_definitions/web.rb b/features/step_definitions/web.rb index 7d62526b..464e7679 100644 --- a/features/step_definitions/web.rb +++ b/features/step_definitions/web.rb @@ -131,6 +131,10 @@ 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| User.find_by(email: arg1).confirmed?.should be_true end 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" From 4e6a6a950101eaf737c70b462f6cd96846796c6d Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Mon, 14 Jul 2014 11:11:53 +0200 Subject: [PATCH 309/372] Distribute to a specific user --- app/assets/javascripts/distribution.js.coffee | 23 +++++++++++++++ app/controllers/distributions_controller.rb | 3 ++ app/controllers/users_controller.rb | 21 +++++++++++++- app/views/distributions/_form.html.haml | 8 ++++++ config/routes.rb | 1 + .../distribute_to_user_identifier.feature | 28 +++++++++++++++++++ features/step_definitions/distribution.rb | 13 +++++++++ 7 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 features/distribute_to_user_identifier.feature diff --git a/app/assets/javascripts/distribution.js.coffee b/app/assets/javascripts/distribution.js.coffee index ef50bd49..e53e1cb6 100644 --- a/app/assets/javascripts/distribution.js.coffee +++ b/app/assets/javascripts/distribution.js.coffee @@ -68,3 +68,26 @@ $(document).on "page:change", -> 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/controllers/distributions_controller.rb b/app/controllers/distributions_controller.rb index b538cef7..ffbc61e8 100644 --- a/app/controllers/distributions_controller.rb +++ b/app/controllers/distributions_controller.rb @@ -61,6 +61,9 @@ def new_recipient_form 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? diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 79684cc2..6f4e8627 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -1,6 +1,6 @@ class UsersController < ApplicationController - before_action except: [:show, :login, :index, :send_email_address_request, :set_password_and_address] do + before_action except: [:show, :login, :index, :send_email_address_request, :set_password_and_address, :suggestions] do @user = User.enabled.where(id: params[:id]).first if current_user if current_user != @user @@ -77,6 +77,25 @@ def set_password_and_address 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/views/distributions/_form.html.haml b/app/views/distributions/_form.html.haml index c81ea64c..759c4348 100644 --- a/app/views/distributions/_form.html.haml +++ b/app/views/distributions/_form.html.haml @@ -20,6 +20,14 @@ .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 diff --git a/config/routes.rb b/config/routes.rb index 78793951..3bc617e1 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -15,6 +15,7 @@ resources :users, :only => [:show, :update, :index] do collection do get :login + get :suggestions end member do post :send_tips_back diff --git a/features/distribute_to_user_identifier.feature b/features/distribute_to_user_identifier.feature new file mode 100644 index 00000000..f78b54da --- /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 "bob@example.com" with "10" + And I save the distribution + + Then I should see these distribution lines: + | recipient | address | amount | percentage | + | bob@example.com | 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/step_definitions/distribution.rb b/features/step_definitions/distribution.rb index ed18e439..95b3a71e 100644 --- a/features/step_definitions/distribution.rb +++ b/features/step_definitions/distribution.rb @@ -3,6 +3,10 @@ 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 @@ -30,6 +34,15 @@ 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").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 From e2fdd6e26d8bdcafe80b5763b4ac92f739df693d Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Fri, 18 Jul 2014 08:31:52 +0200 Subject: [PATCH 310/372] display identifier instead of email --- app/models/user.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/models/user.rb b/app/models/user.rb index b7c32c07..b7c05860 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -65,6 +65,8 @@ def email_required? def recipient_label if nickname.present? nickname + elsif identifier.present? + identifier elsif email.present? if new_record? "#{email} (new user)" From 2ba2e51deec61cebde8ba2aaf3814c58f49cfead Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sat, 30 Aug 2014 18:30:35 +0200 Subject: [PATCH 311/372] added development instructions --- README.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/README.md b/README.md index 8edf630a..8a812610 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,38 @@ Donate peercoins to open source projects or make commits and get tips for it. Official site: http://peer4commit.com/ +Development +=========== + +To run peer4commit in development mode follow these instructions: + +* Install {Ruby}[https://www.ruby-lang.org/en/downloads/] 1.9+ +* Install the {bundler}[http://bundler.io/] gem (you may need root): + gem install bundler +* Install {git}[http://git-scm.com/downloads] +* Clone the repository + git clone git@github.com:sigmike/peer4commit.git + cd peer4commit +* Install the sqlite3 development libraries +* Install the gems (without the production gems): + bundle install --without mysql postgresql +* Create +database.yml+. + cp config/database.yml{.sample,} +* Create +config.yml+ + cp config/config.yml{.example,} +* Edit +config.yml+ +* Initialize the database + bundle exec rake db:migrate +* Make sure +ppcoind+ is running with RPC enabled +* Run the server + bundle exec rails server +* Connect to the server at http://localhost:3000/ + +To update the project balances run this command: + bundle exec rails runner "BalanceUpdater.work" + +To retreive commits and send tips on project that do not hold tips: + bundle exec rails runner "BitcoinTipper.work" License ======= From dd5311a42a921b73d7c35ff8babcd8ec678a7d3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Witrant?= Date: Sat, 30 Aug 2014 18:35:08 +0200 Subject: [PATCH 312/372] converted rdoc syntax to markdown --- README.md | 40 +++++++++++++++++++++++++++++++++------- 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 8a812610..f1e4e642 100644 --- a/README.md +++ b/README.md @@ -13,32 +13,58 @@ Development To run peer4commit in development mode follow these instructions: -* Install {Ruby}[https://www.ruby-lang.org/en/downloads/] 1.9+ -* Install the {bundler}[http://bundler.io/] gem (you may need root): +* Install [Ruby](https://www.ruby-lang.org/en/downloads/) 1.9+ +* Install the [bundler](http://bundler.io/) gem (you may need root): + + gem install bundler -* Install {git}[http://git-scm.com/downloads] + +* Install [git](http://git-scm.com/downloads) * Clone the repository + + git clone git@github.com:sigmike/peer4commit.git cd peer4commit + * Install the sqlite3 development libraries * Install the gems (without the production gems): + + bundle install --without mysql postgresql -* Create +database.yml+. + +* Create `database.yml`. + + cp config/database.yml{.sample,} -* Create +config.yml+ + +* Create `config.yml` + + cp config/config.yml{.example,} -* Edit +config.yml+ + +* Edit `config.yml` + * Initialize the database + + bundle exec rake db:migrate -* Make sure +ppcoind+ is running with RPC enabled + +* Make sure `ppcoind` is running with RPC enabled + * Run the server + + bundle exec rails server + * Connect to the server at http://localhost:3000/ + To update the project balances run this command: + bundle exec rails runner "BalanceUpdater.work" To retreive commits and send tips on project that do not hold tips: + bundle exec rails runner "BitcoinTipper.work" License From 878125718a927ec1afc827882ba4b97da4681f38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Witrant?= Date: Sat, 30 Aug 2014 18:37:31 +0200 Subject: [PATCH 313/372] fixed code blocks --- README.md | 45 +++++++++++++++++++++++++-------------------- 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index f1e4e642..e90f04f5 100644 --- a/README.md +++ b/README.md @@ -14,40 +14,43 @@ Development To run peer4commit in development mode follow these instructions: * Install [Ruby](https://www.ruby-lang.org/en/downloads/) 1.9+ -* Install the [bundler](http://bundler.io/) gem (you may need root): +* Install the [bundler](http://bundler.io/) gem (you may need root): +``` +gem install bundler +``` - gem install bundler - * Install [git](http://git-scm.com/downloads) -* Clone the repository - - git clone git@github.com:sigmike/peer4commit.git - cd peer4commit +* Clone the repository +``` +git clone git@github.com:sigmike/peer4commit.git +cd peer4commit +``` * Install the sqlite3 development libraries -* Install the gems (without the production gems): - - bundle install --without mysql postgresql +* Install the gems (without the production gems): +``` +bundle install --without mysql postgresql +``` * Create `database.yml`. - - - cp config/database.yml{.sample,} +``` +cp config/database.yml{.sample,} +``` * Create `config.yml` - - - cp config/config.yml{.example,} +``` +cp config/config.yml{.example,} +``` * Edit `config.yml` * Initialize the database - - +``` bundle exec rake db:migrate +``` * Make sure `ppcoind` is running with RPC enabled @@ -60,12 +63,14 @@ To run peer4commit in development mode follow these instructions: To update the project balances run this command: - +``` bundle exec rails runner "BalanceUpdater.work" +``` To retreive commits and send tips on project that do not hold tips: - +``` bundle exec rails runner "BitcoinTipper.work" +``` License ======= From b892ed299adb864c1f0b6c5b050980dce294d692 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Wed, 3 Sep 2014 07:54:50 +0200 Subject: [PATCH 314/372] handle new github error --- app/models/project.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/models/project.rb b/app/models/project.rb index e05e3195..ee6a495e 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -44,7 +44,8 @@ def get_commits client.commits(full_name) end rescue Octokit::BadGateway, Octokit::NotFound, Octokit::InternalServerError, - Errno::ETIMEDOUT, Faraday::Error::ConnectionFailed, Octokit::Forbidden => e + Errno::ETIMEDOUT, Faraday::Error::ConnectionFailed, Octokit::Forbidden, + Octokit::Conflict => e Rails.logger.info "Project ##{id}: #{e.class} happened" rescue StandardError => e if CONFIG["airbrake"] From 8734dd0005e8437785b1d88fc49ceb9005b08be2 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Mon, 6 Apr 2015 09:28:47 +0200 Subject: [PATCH 315/372] do not load addthis or google analytics in test --- app/assets/javascripts/application.js.coffee | 15 ++++++++------- app/views/layouts/application.html.haml | 17 +++++++++-------- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee index 9a6f5cf2..1751db04 100644 --- a/app/assets/javascripts/application.js.coffee +++ b/app/assets/javascripts/application.js.coffee @@ -7,10 +7,11 @@ $(document).on "ready page:change", -> - # Remove all global properties set by addthis, otherwise it won't reinitialize - for i of window - delete window[i] if /^addthis/.test(i) or /^_at/.test(i) - window.addthis_share = null - - # Finally, load addthis - $.getScript "http://s7.addthis.com/js/250/addthis_widget.js#pubid=ra-526425ac0ea0780b" + if $("body").data("environment") != "test" + # Remove all global properties set by addthis, otherwise it won't reinitialize + for i of window + delete window[i] if /^addthis/.test(i) or /^_at/.test(i) + window.addthis_share = null + + # Finally, load addthis + $.getScript "http://s7.addthis.com/js/250/addthis_widget.js#pubid=ra-526425ac0ea0780b" diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 28a586ac..e2a5d9f7 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -24,15 +24,16 @@ = 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-11108334-6', 'peer4commit.com'); - ga('send', 'pageview'); + ga('create', 'UA-11108334-6', 'peer4commit.com'); + ga('send', 'pageview'); #top-bar .container #session-menu From 71f1f653c358154c8e8d0ad57aa53c41e4523ef6 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Mon, 6 Apr 2015 09:29:05 +0200 Subject: [PATCH 316/372] Do not send 0 amount --- app/models/distribution.rb | 2 +- features/distribution.feature | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/app/models/distribution.rb b/app/models/distribution.rb index f8ce3686..80f892a5 100644 --- a/app/models/distribution.rb +++ b/app/models/distribution.rb @@ -43,7 +43,7 @@ def send_transaction! def generate_data outs = Hash.new { 0.to_d } tips.each do |tip| - outs[tip.user.bitcoin_address] += tip.amount.to_d / COIN + outs[tip.user.bitcoin_address] += tip.amount.to_d / COIN if tip.amount > 0 end outs.to_json end diff --git a/features/distribution.feature b/features/distribution.feature index 77053a9c..97ea8337 100644 --- a/features/distribution.feature +++ b/features/distribution.feature @@ -488,3 +488,38 @@ Feature: Fundraisers can distribute funds | 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" + From 05e6092851f7253b15b929ab7f1c4f454fdbadec Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 10 May 2015 09:58:09 +0200 Subject: [PATCH 317/372] Updated cucumber --- Gemfile.lock | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 99b12be2..09af0480 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -116,12 +116,15 @@ GEM compass-rails (1.1.7) compass (>= 0.12.2) sprockets (<= 2.11.0) - cucumber (1.3.10) + cucumber (2.0.0) builder (>= 2.1.2) + cucumber-core (~> 1.1.3) diff-lcs (>= 1.1.3) gherkin (~> 2.12) multi_json (>= 1.7.5, < 2.0) - multi_test (>= 0.0.2) + multi_test (>= 0.1.2) + cucumber-core (1.1.3) + gherkin (~> 2.12.0) cucumber-rails (1.4.0) capybara (>= 1.1.2) cucumber (>= 1.2.0) @@ -197,8 +200,8 @@ GEM mime-types (1.25.1) mini_portile (0.5.2) minitest (4.7.5) - multi_json (1.8.4) - multi_test (0.0.3) + multi_json (1.11.0) + multi_test (0.1.2) multi_xml (0.5.5) multipart-post (1.2.0) mysql2 (0.3.14) From 3253abd31a9583bf9ccaa01a59cffc8a7d23c4cf Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 10 May 2015 10:04:59 +0200 Subject: [PATCH 318/372] replaced PPC by XPM --- app/views/distributions/_tip_form.html.haml | 2 +- app/views/projects/decide_tip_amounts.html.haml | 2 +- features/distribute_to_commits.feature | 4 ++-- features/distribution.feature | 14 +++++++------- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/app/views/distributions/_tip_form.html.haml b/app/views/distributions/_tip_form.html.haml index 2d3e9af4..2f2a2100 100644 --- a/app/views/distributions/_tip_form.html.haml +++ b/app/views/distributions/_tip_form.html.haml @@ -19,5 +19,5 @@ = 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.amount= fields.text_field :coin_amount, hide_label: true, append: "XPM" %td.remove= fields.check_box :_destroy diff --git a/app/views/projects/decide_tip_amounts.html.haml b/app/views/projects/decide_tip_amounts.html.haml index e5a9a7cd..78ddf02e 100644 --- a/app/views/projects/decide_tip_amounts.html.haml +++ b/app/views/projects/decide_tip_amounts.html.haml @@ -23,7 +23,7 @@ - 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" + = tip_fields.text_field :decided_free_amount, inline: true, hide_label: true, append: "XPM" .text-center = f.submit 'Send the selected tip amounts' diff --git a/features/distribute_to_commits.feature b/features/distribute_to_commits.feature index 0d3637e3..72b722e1 100644 --- a/features/distribute_to_commits.feature +++ b/features/distribute_to_commits.feature @@ -38,7 +38,7 @@ Feature: A project collaborator distribute to commit authors Then I should see these distribution lines: | recipient | reason | address | amount | percentage | | yugo | Commit BBB: Tiny change | | 0.5 | 100 | - And I should see "Total amount: 0.50 PPC" + And I should see "Total amount: 0.50 XPM" When I click on "Send email request to provide an address" Then I should see "Request sent" And there should be 1 email sent @@ -79,7 +79,7 @@ Feature: A project collaborator distribute to commit authors Then I should see these distribution lines: | recipient | reason | address | amount | percentage | | yugo@example.com | Commit BBB: Tiny change | | 0.5 | 100 | - And I should see "Total amount: 0.50 PPC" + And I should see "Total amount: 0.50 XPM" And there should be 0 email sent When I click on "Send email request to provide an address" Then I should see "Request sent" diff --git a/features/distribution.feature b/features/distribution.feature index 97ea8337..068f8f3e 100644 --- a/features/distribution.feature +++ b/features/distribution.feature @@ -17,7 +17,7 @@ Feature: Fundraisers can distribute funds Then I should see these distribution lines: | recipient | address | amount | percentage | | bob | mxWfjaZJTNN5QKeZZYQ5HW3vgALFBsnuG1 | 10 | 100 | - And I should see "Total amount: 10.00 PPC" + And I should see "Total amount: 10.00 XPM" When the tipper is started Then no coins should have been sent @@ -53,7 +53,7 @@ Feature: Fundraisers can distribute funds | recipient | address | amount | percentage | | bob | mxWfjaZJTNN5QKeZZYQ5HW3vgALFBsnuG1 | 10 | 42.4 | | carol | mi9SLroAgc8eUNuLwnZmdyqWdShbNtvr3n | 13.56 | 57.6 | - And I should see "Total amount: 23.56 PPC" + And I should see "Total amount: 23.56 XPM" When the tipper is started Then no coins should have been sent @@ -84,7 +84,7 @@ Feature: Fundraisers can distribute funds 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 see "Total amount: 10.00 XPM" And I should not see "Send the transaction" And I should see "The transaction cannot be sent because some addresses are missing" @@ -127,7 +127,7 @@ Feature: Fundraisers can distribute funds 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" + And I should see "Total amount: 0.00 XPM" @javascript Scenario: Send to an user without an address @@ -147,7 +147,7 @@ Feature: Fundraisers can distribute funds 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 see "Total amount: 10.00 XPM" And I should not see "Send the transaction" And I should see "The transaction cannot be sent because some addresses are missing" @@ -290,7 +290,7 @@ Feature: Fundraisers can distribute funds Then I should see these distribution lines: | recipient | address | amount | percentage | | bob | mxWfjaZJTNN5QKeZZYQ5HW3vgALFBsnuG1 | 10 | 100 | - And I should see "Total amount: 10.00 PPC" + And I should see "Total amount: 10.00 XPM" Given a GitHub user "carol" who has set his address to "mi9SLroAgc8eUNuLwnZmdyqWdShbNtvr3n" @@ -511,7 +511,7 @@ Feature: Fundraisers can distribute funds | recipient | address | amount | percentage | | bob | mxWfjaZJTNN5QKeZZYQ5HW3vgALFBsnuG1 | 10 | 100.0 | | carol | mi9SLroAgc8eUNuLwnZmdyqWdShbNtvr3n | 0 | 0.0 | - And I should see "Total amount: 10.00 PPC" + And I should see "Total amount: 10.00 XPM" When the tipper is started Then no coins should have been sent From fb5ac2e236cade0f731a88bb50eacfe92f190455 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 10 May 2015 10:21:46 +0200 Subject: [PATCH 319/372] Do not distribute to unknown users --- app/models/tip.rb | 12 +++--------- features/distribute_to_commits.feature | 14 -------------- features/step_definitions/distribution.rb | 2 +- 3 files changed, 4 insertions(+), 24 deletions(-) diff --git a/app/models/tip.rb b/app/models/tip.rb index 630691ea..231a4a9d 100644 --- a/app/models/tip.rb +++ b/app/models/tip.rb @@ -110,17 +110,11 @@ def coin_amount=(coin_amount) def self.build_from_commit(commit) if commit.username.present? - user = User.enabled.where(nickname: commit.username).first_or_initialize(email: commit.email) + user = User.enabled.where(nickname: commit.username).first elsif commit.email =~ Devise::email_regexp - user = User.enabled.where(email: commit.email).first_or_initialize - else - return nil - end - if user.new_record? - raise "Invalid email address" unless user.email =~ Devise::email_regexp - user.skip_confirmation_notification! - user.save! + user = User.enabled.where(email: commit.email).first end + return nil unless user new(user_id: user.id, reason: commit) end diff --git a/features/distribute_to_commits.feature b/features/distribute_to_commits.feature index 0d3637e3..627d6aa0 100644 --- a/features/distribute_to_commits.feature +++ b/features/distribute_to_commits.feature @@ -71,20 +71,6 @@ Feature: A project collaborator distribute to commit authors And I select the commit recipients "Commits not rewarded" Then the distribution form should have these recipients: | recipient | reason | amount | - | yugo@example.com | Commit BBB: Tiny change | | - - And I fill the amount to "yugo@example.com" with "0.5" - And I save the distribution - - Then I should see these distribution lines: - | recipient | reason | address | amount | percentage | - | yugo@example.com | Commit BBB: Tiny change | | 0.5 | 100 | - And I should see "Total amount: 0.50 PPC" - And there should be 0 email sent - When I click on "Send email request to provide an address" - Then I should see "Request sent" - And there should be 1 email sent - And an email should have been sent to "yugo@example.com" @javascript Scenario: Distribute to a single commit diff --git a/features/step_definitions/distribution.rb b/features/step_definitions/distribution.rb index 95b3a71e..1bb673ee 100644 --- a/features/step_definitions/distribution.rb +++ b/features/step_definitions/distribution.rb @@ -117,7 +117,7 @@ begin tr = find("#distribution-form #recipients tr", text: row["recipient"]) rescue - puts "Rows: " + all("#distribution-form #recipients tr").map(&:text).inspect + p rows: all("#distribution-form #recipients tr").map(&:text) p errors: all(".alert.alert-danger").map(&:text) raise end From 30781b2a59f4f2cac496a18d55b323afb92396d7 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 10 May 2015 10:40:26 +0200 Subject: [PATCH 320/372] Removed ability to send to an email address --- app/controllers/distributions_controller.rb | 8 -- app/views/distributions/_form.html.haml | 8 -- features/distribution.feature | 151 -------------------- 3 files changed, 167 deletions(-) diff --git a/app/controllers/distributions_controller.rb b/app/controllers/distributions_controller.rb index ffbc61e8..2113a959 100644 --- a/app/controllers/distributions_controller.rb +++ b/app/controllers/distributions_controller.rb @@ -53,14 +53,6 @@ def new_recipient_form user.save! end @tips << Tip.new(user: user) - elsif params[:user] and params[:user][:email].present? - user = User.enabled.where(email: params[:user][:email]).first_or_initialize - if user.new_record? - raise "Invalid email address" unless user.email =~ Devise::email_regexp - user.skip_confirmation_notification! - 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) diff --git a/app/views/distributions/_form.html.haml b/app/views/distributions/_form.html.haml index 759c4348..16abe353 100644 --- a/app/views/distributions/_form.html.haml +++ b/app/views/distributions/_form.html.haml @@ -28,14 +28,6 @@ .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 email address - .panel-body - .input-group - = bootstrap_form_for User.new, url: new_recipient_form_project_distributions_path(@project) do |f| - = f.email_field :email, hide_label: true, append: content_tag(:button, "Add", class: "btn btn-default add-recipient-button") .col-md-3 .panel.panel-default .panel-heading diff --git a/features/distribution.feature b/features/distribution.feature index 97ea8337..d67f0206 100644 --- a/features/distribution.feature +++ b/features/distribution.feature @@ -175,94 +175,6 @@ Feature: Fundraisers can distribute funds | mnVba8qrpy5uxYD7dV4NZMQPWjgdt2QC1i | 10.0 | And the project balance should be "490.00" - @javascript - Scenario: Send to an unknown email address - Given the current time is "2014-03-01 12:35:02 UTC" - - 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 email address "bob@example.com" to the recipients - And I fill the amount to "bob@example.com" with "10" - And I save the distribution - - Then I should see these distribution lines: - | recipient | address | amount | percentage | - | bob@example.com | | 10 | 100.0 | - 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 - And no email should have been sent - - When I click on "Send email request to provide an address" - Then I should see these distribution lines: - | recipient | address | amount | percentage | - | bob@example.com | | 10 | 100.0 | - - And an email should have been sent to "bob@example.com" - And the email should include "alice" - And the email should include a link to the last distribution - When I visit the link to set my password and address from the email - And I fill "Password" with "password" - And I fill "Password confirmation" with "password" - And I fill "Peercoin address" with "mubmzLrtTgDE2WrHkiwSFKuTh2VTSXboYK" - And I click on "Save" - - Then I should see "Information saved" - And the user with email "bob@example.com" should have "password" as password - And the user with email "bob@example.com" should have "mubmzLrtTgDE2WrHkiwSFKuTh2VTSXboYK" as peercoin address - - 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@example.com | mubmzLrtTgDE2WrHkiwSFKuTh2VTSXboYK | 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 | - | mubmzLrtTgDE2WrHkiwSFKuTh2VTSXboYK | 10.0 | - And the project balance should be "490.00" - - @javascript - Scenario: Send to an known email address who has no confirmation token - Given an user with email "bob@example.com" and without password nor confirmation token - And 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 email address "bob@example.com" to the recipients - And I fill the amount to "bob@example.com" with "10" - And I save the distribution - Then I should see these distribution lines: - | recipient | address | amount | percentage | - | bob@example.com | | 10 | 100.0 | - When I click on "Send email request to provide an address" - Then I should see "Request sent" - And an email should have been sent to "bob@example.com" - When I visit the link to set my password and address from the email - And I fill "Password" with "password" - And I fill "Password confirmation" with "password" - And I fill "Peercoin address" with "mubmzLrtTgDE2WrHkiwSFKuTh2VTSXboYK" - And I click on "Save" - - Then I should see "Information saved" - And the user with email "bob@example.com" should have "password" as password - And the user with email "bob@example.com" should have "mubmzLrtTgDE2WrHkiwSFKuTh2VTSXboYK" as peercoin address - Scenario: Send to someone who doesn't want to be notified Then pending @@ -313,69 +225,6 @@ Feature: Fundraisers can distribute funds | mi9SLroAgc8eUNuLwnZmdyqWdShbNtvr3n | 5.0 | And the project balance should be "480" - @javascript - Scenario: Send to a known email address - Given an user with email "bob@example.com" - - 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 email address "bob@example.com" to the recipients - And I fill the amount to "bob@example.com" with "10" - And I save the distribution - - Then I should see these distribution lines: - | recipient | address | amount | percentage | - | bob@example.com | | 10 | 100.0 | - 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 - And no email should have been sent - - When I click on "Send email request to provide an address" - Then I should see these distribution lines: - | recipient | address | amount | percentage | - | bob@example.com | | 10 | 100.0 | - - And an email should have been sent to "bob@example.com" - And the email should include "alice" - And the email should include a link to the last distribution - When I log out - And I click on the "Set your Peercoin address" link in the email - Then I should see "Forgot your password?" - 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 "Peercoin address" - When I fill "Peercoin address" with "mubmzLrtTgDE2WrHkiwSFKuTh2VTSXboYK" - And I fill "Current password" with "password" - And I click on "Update" - Then I should see "You updated your account successfully" - - And the user with email "bob@example.com" should have "mubmzLrtTgDE2WrHkiwSFKuTh2VTSXboYK" as peercoin address - - 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@example.com | mubmzLrtTgDE2WrHkiwSFKuTh2VTSXboYK | 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 | - | mubmzLrtTgDE2WrHkiwSFKuTh2VTSXboYK | 10.0 | - And the project balance should be "490.00" - @javascript Scenario: Send distribution with a comment Given a GitHub user "bob" who has set his address to "mxWfjaZJTNN5QKeZZYQ5HW3vgALFBsnuG1" From b5175441077f4f1ccec3b1b9ea34bc98d816f831 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 10 May 2015 10:42:24 +0200 Subject: [PATCH 321/372] Fixed tests --- .../distribute_to_user_identifier.feature | 4 +-- features/step_definitions/distribution.rb | 27 +++++++++++++++---- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/features/distribute_to_user_identifier.feature b/features/distribute_to_user_identifier.feature index f78b54da..fba6437c 100644 --- a/features/distribute_to_user_identifier.feature +++ b/features/distribute_to_user_identifier.feature @@ -13,12 +13,12 @@ Feature: Distribute funds to an user identifier 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 "bob@example.com" with "10" + And I fill the amount to "" with "10" And I save the distribution Then I should see these distribution lines: | recipient | address | amount | percentage | - | bob@example.com | mi9SLroAgc8eUNuLwnZmdyqWdShbNtvr3n | 10 | 100.0 | + | | mi9SLroAgc8eUNuLwnZmdyqWdShbNtvr3n | 10 | 100.0 | When I click on "Send the transaction" Then I should see "Transaction sent" diff --git a/features/step_definitions/distribution.rb b/features/step_definitions/distribution.rb index 1bb673ee..c09ca14a 100644 --- a/features/step_definitions/distribution.rb +++ b/features/step_definitions/distribution.rb @@ -56,9 +56,25 @@ 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 "#recipients tr", text: /^#{Regexp.escape arg1}/ do + within_recipient_row(arg1) do fill_in "Amount", with: arg2 end rescue @@ -69,26 +85,27 @@ end Given(/^I fill the comment to "(.*?)" with "(.*?)"$/) do |arg1, arg2| - within "#recipients tr", text: /^#{Regexp.escape arg1}/ do + within_recipient_row(arg1) do fill_in "Comment", with: arg2 end end When(/^I remove the recipient "(.*?)"$/) do |arg1| - within "#recipients tr", text: /^#{Regexp.escape arg1}/ do + 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: row["recipient"]) + 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 - tr.find(".recipient").text.should eq(row["recipient"]) + tr.find(".recipient").text.should eq(recipient) tr.find(".address").text.should eq(row["address"]) if row["address"] tr.find(".reason").text.should eq(row["reason"]) if row["reason"] if row["amount"] From eae5347734645a42c59f4598fb41d95370a21de6 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 10 May 2015 11:20:54 +0200 Subject: [PATCH 322/372] Do not create user acconts automatically --- app/controllers/users_controller.rb | 10 +----- app/models/project.rb | 22 ++---------- app/views/distributions/show.html.haml | 3 -- app/views/projects/_form.html.haml | 2 +- config/routes.rb | 1 - features/change_github.feature | 1 + features/distribute_to_commits.feature | 19 +++++------ features/step_definitions/common.rb | 10 ++++++ features/tip_for_commit.feature | 32 +++++------------- features/tip_modifier_interface.feature | 45 +++++++++++++++++++++---- 10 files changed, 72 insertions(+), 73 deletions(-) diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 6f4e8627..8f17e192 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -1,6 +1,6 @@ class UsersController < ApplicationController - before_action except: [:show, :login, :index, :send_email_address_request, :set_password_and_address, :suggestions] do + 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 @@ -48,14 +48,6 @@ def send_tips_back redirect_to @user, notice: 'All your tips have been refunded to their project' end - def send_email_address_request - tip = Tip.find(params[:tip_id]) - authorize! :update, tip.distribution - tip.user.reset_confirmation_token! - UserMailer.address_request(tip, current_user).deliver - redirect_to params[:return_url], notice: "Request sent" - end - def set_password_and_address @user = User.enabled.find(params[:id]) raise "Blank token" if params[:token].blank? diff --git a/app/models/project.rb b/app/models/project.rb index ee6a495e..84d0fd13 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -103,25 +103,9 @@ def tip_for commit end user ||= User.enabled.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.new( - email: email, - password: generated_password, - name: commit.commit.author.name, - ) - user.skip_confirmation_notification! - end - - if nickname.present? and user.nickname.blank? - user.nickname = nickname - end - - user.save! + if (next_tip_amount > 0) and + Tip.find_by(commit: commit.sha).nil? and + user if hold_tips amount = nil diff --git a/app/views/distributions/show.html.haml b/app/views/distributions/show.html.haml index b0a4f9a0..bf99a0f8 100644 --- a/app/views/distributions/show.html.haml +++ b/app/views/distributions/show.html.haml @@ -23,9 +23,6 @@ %td.address - if tip.user.try(:bitcoin_address).present? = tip.user.bitcoin_address - - else - - if tip.user.try(:email).present? and can? :update, @distribution - = button_to "Send email request to provide an address", send_email_address_request_user_path(tip_id: tip.id, return_url: request.url), class: "btn btn-default" %td.amount - if tip.amount = btc_human tip.amount diff --git a/app/views/projects/_form.html.haml b/app/views/projects/_form.html.haml index f1d65160..8fd3c9d1 100644 --- a/app/views/projects/_form.html.haml +++ b/app/views/projects/_form.html.haml @@ -7,6 +7,6 @@ = 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"} + = 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/config/routes.rb b/config/routes.rb index 3bc617e1..1c64af15 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -19,7 +19,6 @@ end member do post :send_tips_back - post :send_email_address_request get :set_password_and_address patch :set_password_and_address end diff --git a/features/change_github.feature b/features/change_github.feature index e34d0fae..401be459 100644 --- a/features/change_github.feature +++ b/features/change_github.feature @@ -11,6 +11,7 @@ Feature: Fundraiser can change the GitHub repository linked to a project 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: diff --git a/features/distribute_to_commits.feature b/features/distribute_to_commits.feature index 627d6aa0..8a2ff98e 100644 --- a/features/distribute_to_commits.feature +++ b/features/distribute_to_commits.feature @@ -13,6 +13,8 @@ Feature: A project collaborator distribute to commit authors 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 @@ -36,14 +38,9 @@ Feature: A project collaborator distribute to commit authors And I click on "Save" Then I should see these distribution lines: - | recipient | reason | address | amount | percentage | - | yugo | Commit BBB: Tiny change | | 0.5 | 100 | + | recipient | reason | address | amount | percentage | + | yugo | Commit BBB: Tiny change | mxWfjaZJTNN5QKeZZYQ5HW3vgALFBsnuG1 | 0.5 | 100 | And I should see "Total amount: 0.50 PPC" - When I click on "Send email request to provide an address" - Then I should see "Request sent" - And there should be 1 email sent - And an email should have been sent to "yugo@example.com" - When the new commits are read When I go to the project page @@ -84,6 +81,8 @@ Feature: A project collaborator distribute to commit authors 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 @@ -103,6 +102,6 @@ Feature: A project collaborator distribute to commit authors And I click on "Save" Then I should see these distribution lines: - | recipient | reason | address | amount | percentage | - | yugo | Commit 170ed604f2: Tiny change | | 0.5 | | - | gaal | Commit 1329394df2: Some changes | | Undecided | | + | recipient | reason | address | amount | percentage | + | yugo | Commit 170ed604f2: Tiny change | mxWfjaZJTNN5QKeZZYQ5HW3vgALFBsnuG1 | 0.5 | | + | gaal | Commit 1329394df2: Some changes | mi9SLroAgc8eUNuLwnZmdyqWdShbNtvr3n | Undecided | | diff --git a/features/step_definitions/common.rb b/features/step_definitions/common.rb index 865bd95e..f4553ec7 100644 --- a/features/step_definitions/common.rb +++ b/features/step_definitions/common.rb @@ -87,10 +87,20 @@ def find_new_commit(id) 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 diff --git a/features/tip_for_commit.feature b/features/tip_for_commit.feature index 0b544ef6..221494aa 100644 --- a/features/tip_for_commit.feature +++ b/features/tip_for_commit.feature @@ -16,48 +16,32 @@ Feature: On projects not holding tips, a tip is created for each new commit Then the project should have these tips: | commit | amount | | 123 | 5.0 | - | abc | 4.95 | - | 333 | 4.9005 | + | 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.9005 | + | mxWfjaZJTNN5QKeZZYQ5HW3vgALFBsnuG1 | 9.95 | - And an email should have been sent to "alicia@example.com" - When I click on the "Set your password and Peercoin address" link in the email - And I fill "Password" with "password" - And I fill "Password confirmation" with "password" - And I fill "Peercoin address" with "mubmzLrtTgDE2WrHkiwSFKuTh2VTSXboYK" - And I click on "Save" - Then I should see "Information saved" - And the user with email "alicia@example.com" should have "password" as password - And the user with email "alicia@example.com" should have "mubmzLrtTgDE2WrHkiwSFKuTh2VTSXboYK" as peercoin address - And the user with email "alicia@example.com" should have his email confirmed - - When the transaction history is cleared - And the tipper is started - Then these amounts should have been sent from the account of the project: - | address | amount | - | mubmzLrtTgDE2WrHkiwSFKuTh2VTSXboYK | 4.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 | - | 123 | - | abc | - | 333 | + | 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 | | - | abc | | | 333 | | When the tipper is started diff --git a/features/tip_modifier_interface.feature b/features/tip_modifier_interface.feature index 87a42071..51589c0d 100644 --- a/features/tip_modifier_interface.feature +++ b/features/tip_modifier_interface.feature @@ -13,11 +13,18 @@ Feature: A project collaborator can change the tips of commits And the message of commit "BBB" is "Tiny change" And the author of commit "CCC" is "gaal" - Scenario: Without anything modified + 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 2 email sent + 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" @@ -27,6 +34,9 @@ Feature: A project collaborator can change the tips of commits 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 @@ -43,7 +53,7 @@ Feature: A project collaborator can change the tips of commits 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 1 email sent + And there should be 0 email sent When the email counters are reset And I choose the amount "Free: 0%" on commit "CCC" @@ -52,6 +62,21 @@ Feature: A project collaborator can change the tips of commits 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 @@ -92,8 +117,9 @@ Feature: A project collaborator can change the tips of commits | yugo | 1 | Scenario: A collaborator sends large amounts in tips - Given 20 new commits - And a new commit "last" + 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" @@ -102,10 +128,11 @@ Feature: A project collaborator can change the tips of commits 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.088338" for commit "last" + 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" @@ -125,6 +152,8 @@ Feature: A project collaborator can change the tips of commits 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 @@ -136,6 +165,8 @@ Feature: A project collaborator can change the tips of commits 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 @@ -155,6 +186,8 @@ Feature: A project collaborator can change the tips of commits 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 From d1e5ea7624f7a5afad9af23973c18859170c5e1f Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 10 May 2015 14:56:19 +0200 Subject: [PATCH 323/372] Updated some gems --- Gemfile | 2 +- Gemfile.lock | 101 ++++++++++++++++++++++++--------------------------- 2 files changed, 48 insertions(+), 55 deletions(-) diff --git a/Gemfile b/Gemfile index b7805c38..66cf83ba 100644 --- a/Gemfile +++ b/Gemfile @@ -9,7 +9,7 @@ gem 'mysql2', group: :mysql gem 'pg', group: :postgresql # Use SCSS for stylesheets -gem 'sass-rails', '~> 4.0.0' +gem 'sass-rails', '~> 4.0.2' gem 'haml-rails' gem "less-rails" diff --git a/Gemfile.lock b/Gemfile.lock index 09af0480..8fbd236e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -43,26 +43,26 @@ GIT GEM remote: https://rubygems.org/ specs: - actionmailer (4.0.3) - actionpack (= 4.0.3) - mail (~> 2.5.4) - actionpack (4.0.3) - activesupport (= 4.0.3) + actionmailer (4.0.13) + actionpack (= 4.0.13) + mail (~> 2.5, >= 2.5.4) + actionpack (4.0.13) + activesupport (= 4.0.13) builder (~> 3.1.0) erubis (~> 2.7.0) rack (~> 1.5.2) rack-test (~> 0.6.2) - activemodel (4.0.3) - activesupport (= 4.0.3) + activemodel (4.0.13) + activesupport (= 4.0.13) builder (~> 3.1.0) - activerecord (4.0.3) - activemodel (= 4.0.3) + activerecord (4.0.13) + activemodel (= 4.0.13) activerecord-deprecated_finders (~> 1.0.2) - activesupport (= 4.0.3) + activesupport (= 4.0.13) arel (~> 4.0.0) - activerecord-deprecated_finders (1.0.3) - activesupport (4.0.3) - i18n (~> 0.6, >= 0.6.4) + activerecord-deprecated_finders (1.0.4) + activesupport (4.0.13) + i18n (~> 0.6, >= 0.6.9) minitest (~> 4.2) multi_json (~> 1.3) thread_safe (~> 0.1) @@ -72,7 +72,6 @@ GEM builder multi_json arel (4.0.2) - atomic (1.1.14) bcrypt-ruby (3.1.2) builder (3.1.4) cancancan (1.7.1) @@ -96,7 +95,7 @@ GEM capybara (>= 1.0, < 3) launchy chronic (0.10.2) - chunky_png (1.3.1) + chunky_png (1.3.4) cliver (0.3.2) coffee-rails (4.0.1) coffee-script (>= 2.2.0) @@ -109,13 +108,12 @@ GEM commontator (4.6.1) jquery-rails rails (>= 3.1) - compass (0.12.2) + compass (0.12.7) chunky_png (~> 1.2) fssm (>= 0.2.7) - sass (~> 3.1) - compass-rails (1.1.7) + sass (~> 3.2.19) + compass-rails (2.0.0) compass (>= 0.12.2) - sprockets (<= 2.11.0) cucumber (2.0.0) builder (>= 2.1.2) cucumber-core (~> 1.1.3) @@ -173,11 +171,11 @@ GEM json (~> 1.8) multi_xml (>= 0.5.2) httpauth (0.2.1) - i18n (0.6.9) + i18n (0.7.0) jbuilder (1.5.3) activesupport (>= 3.0.0) multi_json (>= 1.2.0) - jquery-rails (3.0.4) + jquery-rails (3.1.2) railties (>= 3.0, < 5.0) thor (>= 0.14, < 2.0) json (1.8.1) @@ -194,10 +192,9 @@ GEM actionpack (>= 3.1) less (~> 2.4.0) libv8 (3.16.14.3) - mail (2.5.4) - mime-types (~> 1.16) - treetop (~> 1.4.8) - mime-types (1.25.1) + mail (2.6.3) + mime-types (>= 1.16, < 3) + mime-types (2.5) mini_portile (0.5.2) minitest (4.7.5) multi_json (1.11.0) @@ -231,31 +228,30 @@ GEM cliver (~> 0.3.1) multi_json (~> 1.0) websocket-driver (>= 0.2.0) - polyglot (0.3.4) quiet_assets (1.0.2) railties (>= 3.1, < 5.0) - rack (1.5.2) + rack (1.5.3) rack-canonical-host (0.0.8) addressable rack (~> 1.0) - rack-test (0.6.2) + rack-test (0.6.3) rack (>= 1.0) - rails (4.0.3) - actionmailer (= 4.0.3) - actionpack (= 4.0.3) - activerecord (= 4.0.3) - activesupport (= 4.0.3) + rails (4.0.13) + actionmailer (= 4.0.13) + actionpack (= 4.0.13) + activerecord (= 4.0.13) + activesupport (= 4.0.13) bundler (>= 1.3.0, < 2.0) - railties (= 4.0.3) - sprockets-rails (~> 2.0.0) + railties (= 4.0.13) + sprockets-rails (~> 2.0) rails_autolink (1.1.5) rails (> 3.1) - railties (4.0.3) - actionpack (= 4.0.3) - activesupport (= 4.0.3) + railties (4.0.13) + actionpack (= 4.0.13) + activesupport (= 4.0.13) rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) - rake (10.1.1) + rake (10.4.2) rdoc (4.1.1) json (~> 1.4) redcarpet (3.1.2) @@ -277,26 +273,27 @@ GEM rspec-mocks (~> 2.14.0) sanitize (2.1.0) nokogiri (>= 1.4.4) - sass (3.2.13) - sass-rails (4.0.1) + sass (3.2.19) + sass-rails (4.0.4) railties (>= 4.0.0, < 5.0) - sass (>= 3.1.10) - sprockets-rails (~> 2.0.0) + sass (~> 3.2.2) + sprockets (~> 2.8, < 2.12) + sprockets-rails (~> 2.0) sawyer (0.5.2) addressable (~> 2.3.5) faraday (~> 0.8, < 0.10) sdoc (0.4.0) json (~> 1.8) rdoc (~> 4.0, < 5.0) - sprockets (2.11.0) + sprockets (2.11.3) hike (~> 1.2) multi_json (~> 1.0) rack (~> 1.0) tilt (~> 1.1, != 1.3.0) - sprockets-rails (2.0.1) + sprockets-rails (2.3.0) actionpack (>= 3.0) activesupport (>= 3.0) - sprockets (~> 2.8) + sprockets (>= 2.8, < 4.0) sqlite3 (1.3.8) sshkit (1.3.0) net-scp (>= 1.1.2) @@ -307,22 +304,18 @@ GEM therubyracer (0.12.0) libv8 (~> 3.16.14.0) ref - thor (0.18.1) - thread_safe (0.1.3) - atomic + thor (0.19.1) + thread_safe (0.3.5) tilt (1.4.1) timecop (0.7.1) tins (0.13.1) - treetop (1.4.15) - polyglot - polyglot (>= 0.3.1) turbolinks (2.2.0) coffee-rails twitter-typeahead-rails (0.10.2) actionpack (>= 3.1) jquery-rails railties (>= 3.1) - tzinfo (0.3.38) + tzinfo (0.3.44) uglifier (2.4.0) execjs (>= 0.3.0) json (>= 1.8.0) @@ -376,7 +369,7 @@ DEPENDENCIES rqrcode-rails3 rspec-rails sanitize - sass-rails (~> 4.0.0) + sass-rails (~> 4.0.2) sdoc sqlite3 therubyracer From 8af2c92a7609ac3d30e2bcc68d92fe3f9058a2a0 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 10 May 2015 14:56:36 +0200 Subject: [PATCH 324/372] Handle missing user in comments --- config/initializers/commontator.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/initializers/commontator.rb b/config/initializers/commontator.rb index fded3ec2..a86eb06b 100644 --- a/config/initializers/commontator.rb +++ b/config/initializers/commontator.rb @@ -46,7 +46,7 @@ # 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| Rails.application.routes.url_helpers.user_path(user) } + config.user_link_proc = lambda { |user, app_routes| user ? Rails.application.routes.url_helpers.user_path(user) : nil } # user_avatar_proc # Type: Proc From 1c4c1e964aa6eafe029421dd320ffa12a8d46463 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 10 May 2015 15:29:43 +0200 Subject: [PATCH 325/372] fixed missing user crash --- app/views/tips/index.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/tips/index.html.haml b/app/views/tips/index.html.haml index b9807bf6..ca10a0ed 100644 --- a/app/views/tips/index.html.haml +++ b/app/views/tips/index.html.haml @@ -38,7 +38,7 @@ - elsif tip.undecided? The amount of the tip has not been decided yet - elsif tip.free? - - elsif tip.user.bitcoin_address.blank? + - 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 From b4d6acd4aff26a9dcac7aed1a035d8bb1eb54aa0 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 10 May 2015 17:02:40 +0200 Subject: [PATCH 326/372] fixed tip model when user has been deleted --- app/models/tip.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/tip.rb b/app/models/tip.rb index 231a4a9d..86758777 100644 --- a/app/models/tip.rb +++ b/app/models/tip.rb @@ -48,7 +48,7 @@ def non_refunded? scope :with_address, -> { joins(:user).where.not('users.bitcoin_address' => [nil, ""]) } def with_address? - user.bitcoin_address.present? + user.present? and user.bitcoin_address.present? end scope :decided, -> { where.not(amount: nil) } @@ -83,7 +83,7 @@ def commit_url attr_accessor :decided_free_amount def notify_user - if amount and amount > 0 and user.bitcoin_address.blank? and !user.unsubscribed + 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 user.touch :notified_at From 7e988078c9ccc3129a5d6b09d78c3f58171b9170 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 20 Mar 2016 10:52:28 +0100 Subject: [PATCH 327/372] Handle 'Repository access blocked' error --- app/models/project.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/project.rb b/app/models/project.rb index 84d0fd13..b90c033a 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -45,7 +45,7 @@ def get_commits end rescue Octokit::BadGateway, Octokit::NotFound, Octokit::InternalServerError, Errno::ETIMEDOUT, Faraday::Error::ConnectionFailed, Octokit::Forbidden, - Octokit::Conflict => e + Octokit::Conflict, Octokit::ClientError => e Rails.logger.info "Project ##{id}: #{e.class} happened" rescue StandardError => e if CONFIG["airbrake"] From 5f1348fd93e26398d2942dd967b7724600772c89 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Thu, 28 Apr 2016 20:31:16 +0200 Subject: [PATCH 328/372] Load addthis with https --- app/assets/javascripts/application.js.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee index 1751db04..0d4810b8 100644 --- a/app/assets/javascripts/application.js.coffee +++ b/app/assets/javascripts/application.js.coffee @@ -14,4 +14,4 @@ $(document).on "ready page:change", -> window.addthis_share = null # Finally, load addthis - $.getScript "http://s7.addthis.com/js/250/addthis_widget.js#pubid=ra-526425ac0ea0780b" + $.getScript "https://s7.addthis.com/js/250/addthis_widget.js#pubid=ra-526425ac0ea0780b" From 463a15cec008c029505ea1bd2a22b941637079e1 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Mon, 29 Aug 2016 00:41:50 +0200 Subject: [PATCH 329/372] Fix non-working amount validation --- app/models/tip.rb | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/models/tip.rb b/app/models/tip.rb index 86758777..c36a2de9 100644 --- a/app/models/tip.rb +++ b/app/models/tip.rb @@ -4,7 +4,7 @@ class Tip < ActiveRecord::Base belongs_to :project, inverse_of: :tips belongs_to :reason, polymorphic: true - validates :amount, numericality: {greater_or_equal_than: 0, allow_nil: true} + validate :validate_amount_is_positive validate :validate_reason scope :not_sent, -> { where(distribution_id: nil) } @@ -128,4 +128,10 @@ def validate_reason 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 From 298761307cb8dfc66a7e35b39d2138cf7aa52913 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Mon, 29 Aug 2016 00:51:43 +0200 Subject: [PATCH 330/372] Mark failed distributions on project page --- app/views/projects/show.html.haml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index 4c828619..09c175cb 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -69,7 +69,9 @@ - @project.distributions.order(created_at: :desc).limit(5).each do |distribution| %li.distribution-link - label = btc_human(distribution.total_amount) - - if distribution.sent? + - if distribution.is_error? + - label << " failed" + - elsif distribution.sent? - if distribution.sent_at - label << " sent #{time_ago_in_words(distribution.sent_at)} ago" - else From a5ff54e5a35259338d798b744cbaf37998e84dc1 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Mon, 29 Aug 2016 00:53:48 +0200 Subject: [PATCH 331/372] Do not expect non processed distributions --- app/models/project.rb | 4 ++++ app/views/home/audit.html.haml | 8 ++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/app/models/project.rb b/app/models/project.rb index b90c033a..ff23b546 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -223,4 +223,8 @@ def generate_address! 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/views/home/audit.html.haml b/app/views/home/audit.html.haml index fee93952..9496b43c 100644 --- a/app/views/home/audit.html.haml +++ b/app/views/home/audit.html.haml @@ -25,6 +25,8 @@ %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 @@ -45,7 +47,8 @@ %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(expected = available + unpaid - cold + fee - txfee) if available and unpaid and cold and fee and txfee + %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) %td.money= btc_human(account - expected, precision: 2) if account and expected %tbody @@ -57,7 +60,8 @@ %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(expected = available + unpaid - cold + fee - txfee) if available and unpaid and cold and fee and txfee + %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) %td.money= btc_human(account - expected, precision: 2) if account and expected From 54a0f7272c7a7d69e4389a6fe7c6d2005b468b71 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sat, 26 Nov 2016 18:21:19 +0100 Subject: [PATCH 332/372] Update sqlite3 and poltergeist gems --- Gemfile.lock | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 8fbd236e..f7a7a055 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -85,7 +85,8 @@ GEM capistrano-rails (1.1.0) capistrano (>= 3.0.0) capistrano-bundler (>= 1.0.0) - capybara (2.2.1) + capybara (2.10.1) + addressable mime-types (>= 1.16) nokogiri (>= 1.3.3) rack (>= 1.0.0) @@ -194,10 +195,10 @@ GEM libv8 (3.16.14.3) mail (2.6.3) mime-types (>= 1.16, < 3) - mime-types (2.5) - mini_portile (0.5.2) + mime-types (2.99.3) + mini_portile2 (2.1.0) minitest (4.7.5) - multi_json (1.11.0) + multi_json (1.12.1) multi_test (0.1.2) multi_xml (0.5.5) multipart-post (1.2.0) @@ -205,8 +206,8 @@ GEM net-scp (1.1.2) net-ssh (>= 2.6.5) net-ssh (2.7.0) - nokogiri (1.6.1) - mini_portile (~> 0.5.0) + nokogiri (1.6.8.1) + mini_portile2 (~> 2.1.0) oauth2 (0.8.1) faraday (~> 0.8) httpauth (~> 0.1) @@ -223,14 +224,13 @@ GEM omniauth (~> 1.0) orm_adapter (0.5.0) pg (0.17.1) - poltergeist (1.5.1) + poltergeist (1.11.0) capybara (~> 2.1) cliver (~> 0.3.1) - multi_json (~> 1.0) websocket-driver (>= 0.2.0) quiet_assets (1.0.2) railties (>= 3.1, < 5.0) - rack (1.5.3) + rack (1.5.5) rack-canonical-host (0.0.8) addressable rack (~> 1.0) @@ -294,7 +294,7 @@ GEM actionpack (>= 3.0) activesupport (>= 3.0) sprockets (>= 2.8, < 4.0) - sqlite3 (1.3.8) + sqlite3 (1.3.12) sshkit (1.3.0) net-scp (>= 1.1.2) net-ssh @@ -321,7 +321,9 @@ GEM json (>= 1.8.0) warden (1.2.3) rack (>= 1.0) - websocket-driver (0.3.3) + websocket-driver (0.6.4) + websocket-extensions (>= 0.1.0) + websocket-extensions (0.1.2) whenever (0.9.0) activesupport (>= 2.3.4) chronic (>= 0.6.3) @@ -380,3 +382,6 @@ DEPENDENCIES twitter_bootstrap_form_for! uglifier (>= 1.3.0) whenever + +BUNDLED WITH + 1.12.5 From b8431234140324df9903a822eb8251bc7cadba3b Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sat, 26 Nov 2016 19:46:09 +0100 Subject: [PATCH 333/372] Update turbolinks --- Gemfile.lock | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index f7a7a055..bd231b05 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -101,10 +101,10 @@ GEM coffee-rails (4.0.1) coffee-script (>= 2.2.0) railties (>= 4.0.0, < 5.0) - coffee-script (2.2.0) + coffee-script (2.4.1) coffee-script-source execjs - coffee-script-source (1.6.3) + coffee-script-source (1.11.1) commonjs (0.2.7) commontator (4.6.1) jquery-rails @@ -141,7 +141,7 @@ GEM exception_notification (4.0.1) actionmailer (>= 3.0.4) activesupport (>= 3.0.4) - execjs (2.0.2) + execjs (2.7.0) factory_girl (4.4.0) activesupport (>= 3.0.0) factory_girl_rails (4.4.1) @@ -193,9 +193,11 @@ GEM actionpack (>= 3.1) less (~> 2.4.0) libv8 (3.16.14.3) - mail (2.6.3) - mime-types (>= 1.16, < 3) - mime-types (2.99.3) + mail (2.6.4) + mime-types (>= 1.16, < 4) + mime-types (3.1) + mime-types-data (~> 3.2015) + mime-types-data (3.2016.0521) mini_portile2 (2.1.0) minitest (4.7.5) multi_json (1.12.1) @@ -251,7 +253,7 @@ GEM activesupport (= 4.0.13) rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) - rake (10.4.2) + rake (11.3.0) rdoc (4.1.1) json (~> 1.4) redcarpet (3.1.2) @@ -290,7 +292,7 @@ GEM multi_json (~> 1.0) rack (~> 1.0) tilt (~> 1.1, != 1.3.0) - sprockets-rails (2.3.0) + sprockets-rails (2.3.3) actionpack (>= 3.0) activesupport (>= 3.0) sprockets (>= 2.8, < 4.0) @@ -309,13 +311,14 @@ GEM tilt (1.4.1) timecop (0.7.1) tins (0.13.1) - turbolinks (2.2.0) - coffee-rails + turbolinks (5.0.1) + turbolinks-source (~> 5) + turbolinks-source (5.0.0) twitter-typeahead-rails (0.10.2) actionpack (>= 3.1) jquery-rails railties (>= 3.1) - tzinfo (0.3.44) + tzinfo (0.3.52) uglifier (2.4.0) execjs (>= 0.3.0) json (>= 1.8.0) From e02336ceb7e58b683a0922a0d0be33c071680511 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 11 Dec 2016 11:57:49 +0100 Subject: [PATCH 334/372] Update javascript to the new turbolinks --- app/assets/javascripts/application.js.coffee | 2 +- app/assets/javascripts/distribution.js.coffee | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee index 0d4810b8..48b89784 100644 --- a/app/assets/javascripts/application.js.coffee +++ b/app/assets/javascripts/application.js.coffee @@ -5,7 +5,7 @@ #= require twitter/typeahead #= require_tree . -$(document).on "ready page:change", -> +$(document).on "turbolinks:load", -> if $("body").data("environment") != "test" # Remove all global properties set by addthis, otherwise it won't reinitialize diff --git a/app/assets/javascripts/distribution.js.coffee b/app/assets/javascripts/distribution.js.coffee index e53e1cb6..6391e192 100644 --- a/app/assets/javascripts/distribution.js.coffee +++ b/app/assets/javascripts/distribution.js.coffee @@ -1,4 +1,4 @@ -$(document).on "page:change", -> +$(document).on "turbolinks:load", -> root = $("#add-recipient-panels") recipients = $("#recipients") From 91499e2e032369a1461aad38024ba0c2a71951d4 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 11 Dec 2016 12:00:04 +0100 Subject: [PATCH 335/372] Ignore test screenshots --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index c74ce787..e34089a1 100644 --- a/.gitignore +++ b/.gitignore @@ -18,4 +18,5 @@ /config/database.yml /config/config.yml -/public/assets \ No newline at end of file +/public/assets +/screenshot_* From 2858b39a5b6105e3ca1e203cbbcf1ba2343dc302 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 11 Dec 2016 12:34:48 +0100 Subject: [PATCH 336/372] Fix phantomjs problem with cached content --- features/step_definitions/web.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/features/step_definitions/web.rb b/features/step_definitions/web.rb index 464e7679..d6cc8c26 100644 --- a/features/step_definitions/web.rb +++ b/features/step_definitions/web.rb @@ -1,3 +1,8 @@ +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] = { From 65c753919b62efcf2c160a9d268af986a1f558f0 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 11 Dec 2016 12:35:08 +0100 Subject: [PATCH 337/372] Clear mock auth after login --- features/step_definitions/web.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/features/step_definitions/web.rb b/features/step_definitions/web.rb index d6cc8c26..0b6a44e4 100644 --- a/features/step_definitions/web.rb +++ b/features/step_definitions/web.rb @@ -16,6 +16,7 @@ click_on "Sign in" click_on "Sign in with Github" page.should have_content("Successfully authenticated") + OmniAuth.config.mock_auth[:github] = nil @current_user = User.find_by(nickname: arg1) end From ca288d521c2930a6d349242ab7eaf88028984168 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 11 Dec 2016 12:45:27 +0100 Subject: [PATCH 338/372] Fix distribution amount check with refunded tips --- app/models/distribution.rb | 2 +- features/distribution.feature | 24 ++++++++++++++++++++++++ features/step_definitions/common.rb | 4 ++++ 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/app/models/distribution.rb b/app/models/distribution.rb index 80f892a5..1f06a5c6 100644 --- a/app/models/distribution.rb +++ b/app/models/distribution.rb @@ -64,7 +64,7 @@ def validate_funds tips = project.tips.to_a tips -= self.tips tips += self.tips - if project.total_deposited < tips.map(&:amount).compact.sum + if project.total_deposited < tips.reject(&:refunded?).map(&:amount).compact.sum errors.add(:base, "Not enough funds") end end diff --git a/features/distribution.feature b/features/distribution.feature index d67f0206..e45b883b 100644 --- a/features/distribution.feature +++ b/features/distribution.feature @@ -338,6 +338,30 @@ Feature: Fundraisers can distribute funds | 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" diff --git a/features/step_definitions/common.rb b/features/step_definitions/common.rb index f4553ec7..17744062 100644 --- a/features/step_definitions/common.rb +++ b/features/step_definitions/common.rb @@ -48,6 +48,10 @@ 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 From ac0c2243f09d72fa940f300f99f23a669308f7b9 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 11 Dec 2016 12:52:11 +0100 Subject: [PATCH 339/372] Downgrade mime-types to support Ruby 1.9 --- Gemfile | 1 + Gemfile.lock | 5 ++--- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index 66cf83ba..c9dfd1a4 100644 --- a/Gemfile +++ b/Gemfile @@ -79,6 +79,7 @@ gem 'sanitize' gem 'twitter-typeahead-rails' gem 'commontator', '~> 4.6.0' gem 'compass-rails' +gem 'mime-types', '~> 2.99' # to support ruby 1.9 group :test do gem 'cucumber-rails', :require => false diff --git a/Gemfile.lock b/Gemfile.lock index bd231b05..45d7e429 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -195,9 +195,7 @@ GEM libv8 (3.16.14.3) mail (2.6.4) mime-types (>= 1.16, < 4) - mime-types (3.1) - mime-types-data (~> 3.2015) - mime-types-data (3.2016.0521) + mime-types (2.99.3) mini_portile2 (2.1.0) minitest (4.7.5) multi_json (1.12.1) @@ -360,6 +358,7 @@ DEPENDENCIES jquery-rails kaminari less-rails + mime-types (~> 2.99) mysql2 octokit omniauth From d3a869e557a5aef7743a75e272db920541816ff9 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 11 Dec 2016 13:44:37 +0100 Subject: [PATCH 340/372] Update all gems --- Gemfile.lock | 292 ++++++++++-------- app/controllers/distributions_controller.rb | 2 +- .../users/omniauth_callbacks_controller.rb | 2 +- features/step_definitions/cold_storage.rb | 17 +- .../commit_from_known_nickname.rb | 4 +- features/step_definitions/common.rb | 25 +- features/step_definitions/create_project.rb | 18 +- features/step_definitions/distribution.rb | 52 ++-- .../step_definitions/donate_to_project.rb | 8 +- features/step_definitions/tip_for_commit.rb | 6 +- .../tip_modifier_interface.rb | 18 +- features/step_definitions/user_identifier.rb | 4 +- features/step_definitions/web.rb | 32 +- 13 files changed, 252 insertions(+), 228 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 45d7e429..46d90761 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -9,24 +9,24 @@ GIT GIT remote: git://github.com/bootstrap-ruby/rails-bootstrap-forms.git - revision: 5048f09a7a67089f69597418dd3ad51a9d430182 + revision: 5512154e247861cd9e07942d04bf1fe64eecbe13 specs: - bootstrap_form (2.1.1) + bootstrap_form (2.5.2) GIT remote: git://github.com/capistrano/rvm.git - revision: 19e8d15ae3d705499c610370f159d523bbedbd94 + revision: 9cfef39cf0022839dca6b5b330dfefeb5fc363e7 specs: - capistrano-rvm (0.1.0) + capistrano-rvm (0.1.2) capistrano (~> 3.0) sshkit (~> 1.2) GIT remote: git://github.com/seyhunak/twitter-bootstrap-rails.git - revision: 4d0bd4271f7f01d79bbb2b72c04f30a5766db0ef + revision: 1d93c5a77049b3d21d17c847ad0531d7714fa229 branch: bootstrap3 specs: - twitter-bootstrap-rails (2.2.7) + twitter-bootstrap-rails (3.1.1) actionpack (>= 3.1) execjs rails (>= 3.1) @@ -67,36 +67,42 @@ GEM multi_json (~> 1.3) thread_safe (~> 0.1) tzinfo (~> 0.3.37) - addressable (2.3.5) - airbrake (3.1.15) - builder - multi_json + addressable (2.5.0) + public_suffix (~> 2.0, >= 2.0.2) + airbrake (5.6.1) + airbrake-ruby (~> 1.6) + airbrake-ruby (1.6.0) + airbrussh (1.1.1) + sshkit (>= 1.6.1, != 1.7.0) arel (4.0.2) - bcrypt-ruby (3.1.2) + bcrypt (3.1.11) builder (3.1.4) - cancancan (1.7.1) - capistrano (3.0.1) + cancancan (1.15.0) + capistrano (3.7.0) + airbrussh (>= 1.0.0) + capistrano-harrow i18n rake (>= 10.0.0) - sshkit (>= 0.0.23) - capistrano-bundler (1.1.1) - capistrano (~> 3.0) - sshkit (>= 1.2.0) - capistrano-rails (1.1.0) - capistrano (>= 3.0.0) - capistrano-bundler (>= 1.0.0) - capybara (2.10.1) + sshkit (>= 1.9.0) + capistrano-bundler (1.2.0) + capistrano (~> 3.1) + sshkit (~> 1.2) + capistrano-harrow (0.5.3) + capistrano-rails (1.2.0) + capistrano (~> 3.1) + capistrano-bundler (~> 1.1) + capybara (2.11.0) addressable mime-types (>= 1.16) nokogiri (>= 1.3.3) rack (>= 1.0.0) rack-test (>= 0.5.4) xpath (~> 2.0) - capybara-screenshot (0.3.20) + capybara-screenshot (1.0.14) capybara (>= 1.0, < 3) launchy chronic (0.10.2) - chunky_png (1.3.4) + chunky_png (1.3.8) cliver (0.3.2) coffee-rails (4.0.1) coffee-script (>= 2.2.0) @@ -115,84 +121,93 @@ GEM sass (~> 3.2.19) compass-rails (2.0.0) compass (>= 0.12.2) - cucumber (2.0.0) + crass (1.0.2) + cucumber (2.4.0) builder (>= 2.1.2) - cucumber-core (~> 1.1.3) + cucumber-core (~> 1.5.0) + cucumber-wire (~> 0.0.1) diff-lcs (>= 1.1.3) - gherkin (~> 2.12) + gherkin (~> 4.0) multi_json (>= 1.7.5, < 2.0) multi_test (>= 0.1.2) - cucumber-core (1.1.3) - gherkin (~> 2.12.0) - cucumber-rails (1.4.0) - capybara (>= 1.1.2) - cucumber (>= 1.2.0) - nokogiri (>= 1.5.0) - rails (>= 3.0.0) - database_cleaner (1.2.0) - devise (3.2.2) - bcrypt-ruby (~> 3.0) + cucumber-core (1.5.0) + gherkin (~> 4.0) + cucumber-rails (1.4.5) + capybara (>= 1.1.2, < 3) + cucumber (>= 1.3.8, < 4) + mime-types (>= 1.16, < 4) + nokogiri (~> 1.5) + railties (>= 3, < 5.1) + cucumber-wire (0.0.1) + database_cleaner (1.5.3) + devise (3.5.10) + bcrypt (~> 3.0) orm_adapter (~> 0.1) railties (>= 3.2.6, < 5) + responders thread_safe (~> 0.1) warden (~> 1.2.3) diff-lcs (1.2.5) erubis (2.7.0) - exception_notification (4.0.1) - actionmailer (>= 3.0.4) - activesupport (>= 3.0.4) + exception_notification (4.2.1) + actionmailer (>= 4.0, < 6) + activesupport (>= 4.0, < 6) execjs (2.7.0) - factory_girl (4.4.0) + factory_girl (4.7.0) activesupport (>= 3.0.0) - factory_girl_rails (4.4.1) - factory_girl (~> 4.4.0) + factory_girl_rails (4.7.0) + factory_girl (~> 4.7.0) railties (>= 3.0.0) - faraday (0.8.9) - multipart-post (~> 1.2.0) + faraday (0.9.2) + multipart-post (>= 1.2, < 3) fssm (0.2.10) - gherkin (2.12.2) - multi_json (~> 1.3) - github-markdown (0.6.5) - haml (4.0.5) + gherkin (4.0.0) + github-markdown (0.6.9) + haml (4.0.7) tilt - haml-rails (0.5.3) + haml-rails (0.9.0) actionpack (>= 4.0.1) activesupport (>= 4.0.1) - haml (>= 3.1, < 5.0) + haml (>= 4.0.6, < 5.0) + html2haml (>= 1.0.1) railties (>= 4.0.1) - hashie (2.0.5) + hashie (3.4.6) hike (1.2.3) - html-pipeline (1.8.0) + html-pipeline (2.4.2) activesupport (>= 2) - nokogiri (~> 1.4) + nokogiri (>= 1.4) + html2haml (2.0.0) + erubis (~> 2.7.0) + haml (~> 4.0.0) + nokogiri (~> 1.6.0) + ruby_parser (~> 3.5) html_pipeline_rails (0.1.0) github-markdown html-pipeline - httparty (0.12.0) - json (~> 1.8) + httparty (0.14.0) multi_xml (>= 0.5.2) - httpauth (0.2.1) i18n (0.7.0) jbuilder (1.5.3) activesupport (>= 3.0.0) multi_json (>= 1.2.0) - jquery-rails (3.1.2) + jquery-rails (3.1.4) railties (>= 3.0, < 5.0) thor (>= 0.14, < 2.0) - json (1.8.1) - jwt (0.1.11) - multi_json (>= 1.5) - kaminari (0.15.0) + json (1.8.3) + jwt (1.5.6) + kaminari (0.17.0) actionpack (>= 3.0.0) activesupport (>= 3.0.0) - launchy (2.4.2) + launchy (2.4.3) addressable (~> 2.3) - less (2.4.0) + less (2.6.0) commonjs (~> 0.2.7) - less-rails (2.4.2) - actionpack (>= 3.1) - less (~> 2.4.0) - libv8 (3.16.14.3) + less-rails (2.8.0) + actionpack (>= 4.0) + less (~> 2.6.0) + sprockets (> 2, < 4) + tilt + libv8 (3.16.14.17) mail (2.6.4) mime-types (>= 1.16, < 4) mime-types (2.99.3) @@ -200,40 +215,43 @@ GEM minitest (4.7.5) multi_json (1.12.1) multi_test (0.1.2) - multi_xml (0.5.5) - multipart-post (1.2.0) - mysql2 (0.3.14) - net-scp (1.1.2) + multi_xml (0.6.0) + multipart-post (2.0.0) + mysql2 (0.4.5) + net-scp (1.2.1) net-ssh (>= 2.6.5) - net-ssh (2.7.0) + net-ssh (3.2.0) nokogiri (1.6.8.1) mini_portile2 (~> 2.1.0) - oauth2 (0.8.1) - faraday (~> 0.8) - httpauth (~> 0.1) - jwt (~> 0.1.4) - multi_json (~> 1.0) - rack (~> 1.2) - octokit (2.7.0) - sawyer (~> 0.5.2) - omniauth (1.1.4) - hashie (>= 1.2, < 3) - rack - omniauth-oauth2 (1.1.1) - oauth2 (~> 0.8.0) - omniauth (~> 1.0) + nokogumbo (1.4.10) + nokogiri + oauth2 (1.2.0) + faraday (>= 0.8, < 0.10) + jwt (~> 1.0) + multi_json (~> 1.3) + multi_xml (~> 0.5) + rack (>= 1.2, < 3) + octokit (4.6.2) + sawyer (~> 0.8.0, >= 0.5.3) + omniauth (1.3.1) + hashie (>= 1.2, < 4) + rack (>= 1.0, < 3) + omniauth-oauth2 (1.4.0) + oauth2 (~> 1.0) + omniauth (~> 1.2) orm_adapter (0.5.0) - pg (0.17.1) - poltergeist (1.11.0) + pg (0.19.0) + poltergeist (1.12.0) capybara (~> 2.1) cliver (~> 0.3.1) websocket-driver (>= 0.2.0) - quiet_assets (1.0.2) + public_suffix (2.0.4) + quiet_assets (1.1.0) railties (>= 3.1, < 5.0) rack (1.5.5) - rack-canonical-host (0.0.8) - addressable - rack (~> 1.0) + rack-canonical-host (0.2.2) + addressable (> 0, < 3) + rack (>= 1.0.0, < 3) rack-test (0.6.3) rack (>= 1.0) rails (4.0.13) @@ -244,48 +262,60 @@ GEM bundler (>= 1.3.0, < 2.0) railties (= 4.0.13) sprockets-rails (~> 2.0) - rails_autolink (1.1.5) + rails_autolink (1.1.6) rails (> 3.1) railties (4.0.13) actionpack (= 4.0.13) activesupport (= 4.0.13) rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) - rake (11.3.0) - rdoc (4.1.1) - json (~> 1.4) - redcarpet (3.1.2) - ref (1.0.5) - rqrcode (0.4.2) + rake (12.0.0) + rdoc (4.3.0) + redcarpet (3.3.4) + ref (2.0.0) + responders (1.1.2) + railties (>= 3.2, < 4.2) + rqrcode (0.10.1) + chunky_png (~> 1.0) rqrcode-rails3 (0.1.7) rqrcode (>= 0.4.2) - rspec-core (2.14.7) - rspec-expectations (2.14.5) - diff-lcs (>= 1.1.3, < 2.0) - rspec-mocks (2.14.6) - rspec-rails (2.14.1) + rspec-core (3.5.4) + rspec-support (~> 3.5.0) + rspec-expectations (3.5.0) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.5.0) + rspec-mocks (3.5.0) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.5.0) + rspec-rails (3.5.2) actionpack (>= 3.0) - activemodel (>= 3.0) activesupport (>= 3.0) railties (>= 3.0) - rspec-core (~> 2.14.0) - rspec-expectations (~> 2.14.0) - rspec-mocks (~> 2.14.0) - sanitize (2.1.0) + rspec-core (~> 3.5.0) + rspec-expectations (~> 3.5.0) + rspec-mocks (~> 3.5.0) + rspec-support (~> 3.5.0) + rspec-support (3.5.0) + ruby_parser (3.8.3) + sexp_processor (~> 4.1) + sanitize (4.4.0) + crass (~> 1.0.2) nokogiri (>= 1.4.4) + nokogumbo (~> 1.4.1) sass (3.2.19) - sass-rails (4.0.4) + sass-rails (4.0.5) railties (>= 4.0.0, < 5.0) sass (~> 3.2.2) - sprockets (~> 2.8, < 2.12) + sprockets (~> 2.8, < 3.0) sprockets-rails (~> 2.0) - sawyer (0.5.2) - addressable (~> 2.3.5) - faraday (~> 0.8, < 0.10) - sdoc (0.4.0) - json (~> 1.8) - rdoc (~> 4.0, < 5.0) - sprockets (2.11.3) + sawyer (0.8.1) + addressable (>= 2.3.5, < 2.6) + faraday (~> 0.8, < 1.0) + sdoc (0.4.2) + json (~> 1.7, >= 1.7.7) + rdoc (~> 4.0) + sexp_processor (4.7.0) + sprockets (2.12.4) hike (~> 1.2) multi_json (~> 1.0) rack (~> 1.0) @@ -295,38 +325,32 @@ GEM activesupport (>= 3.0) sprockets (>= 2.8, < 4.0) sqlite3 (1.3.12) - sshkit (1.3.0) + sshkit (1.11.4) net-scp (>= 1.1.2) - net-ssh - term-ansicolor - term-ansicolor (1.2.2) - tins (~> 0.8) - therubyracer (0.12.0) + net-ssh (>= 2.8.0) + therubyracer (0.12.2) libv8 (~> 3.16.14.0) ref - thor (0.19.1) + thor (0.19.4) thread_safe (0.3.5) tilt (1.4.1) - timecop (0.7.1) - tins (0.13.1) + timecop (0.8.1) turbolinks (5.0.1) turbolinks-source (~> 5) turbolinks-source (5.0.0) - twitter-typeahead-rails (0.10.2) + twitter-typeahead-rails (0.11.1) actionpack (>= 3.1) jquery-rails railties (>= 3.1) tzinfo (0.3.52) - uglifier (2.4.0) - execjs (>= 0.3.0) - json (>= 1.8.0) - warden (1.2.3) + uglifier (3.0.4) + execjs (>= 0.3.0, < 3) + warden (1.2.6) rack (>= 1.0) websocket-driver (0.6.4) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.2) - whenever (0.9.0) - activesupport (>= 2.3.4) + whenever (0.9.7) chronic (>= 0.6.3) xpath (2.0.0) nokogiri (~> 1.3) diff --git a/app/controllers/distributions_controller.rb b/app/controllers/distributions_controller.rb index 2113a959..da9d3829 100644 --- a/app/controllers/distributions_controller.rb +++ b/app/controllers/distributions_controller.rb @@ -49,7 +49,7 @@ def new_recipient_form 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.confirm user.save! end @tips << Tip.new(user: user) diff --git a/app/controllers/users/omniauth_callbacks_controller.rb b/app/controllers/users/omniauth_callbacks_controller.rb index 4c91afb9..7864b71d 100644 --- a/app/controllers/users/omniauth_callbacks_controller.rb +++ b/app/controllers/users/omniauth_callbacks_controller.rb @@ -12,7 +12,7 @@ def github :email => info['primary_email'], :nickname => info['nickname'] ) - @user.confirm! + @user.confirm @user.save! else set_flash_message(:error, :failure, kind: 'GitHub', reason: 'your primary email address should be verified.') diff --git a/features/step_definitions/cold_storage.rb b/features/step_definitions/cold_storage.rb index 841ce22d..82966372 100644 --- a/features/step_definitions/cold_storage.rb +++ b/features/step_definitions/cold_storage.rb @@ -36,15 +36,15 @@ end Then(/^updating the project balance should raise an error$/) do - expect { BalanceUpdater.work }.to raise_error + expect { BalanceUpdater.work }.to raise_error(RuntimeError) end Then(/^the project balance should be "(.*?)"$/) do |arg1| - (@project.reload.available_amount.to_d / COIN).should eq(arg1.to_d) + 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| - (@project.reload.cold_storage_amount / COIN).should eq(arg1.to_d) + 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| @@ -53,9 +53,9 @@ 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) - transactions.map { |t| t["category"] }.should eq(["send"]) - transactions.map { |t| t["address"] }.should eq([arg2]) - transactions.map { |t| -t["amount"].to_d / COIN }.should eq([arg1.to_d]) + 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 @@ -63,10 +63,9 @@ end Then(/^the project should have a cold storage withdrawal address$/) do - @project.reload.cold_storage_withdrawal_address.should_not be_blank + 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 - BitcoinDaemon.instance.get_addresses_by_account(@project.address_label).should include(@project.reload.cold_storage_withdrawal_address) + 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 index e4700b82..28d32a99 100644 --- a/features/step_definitions/commit_from_known_nickname.rb +++ b/features/step_definitions/commit_from_known_nickname.rb @@ -7,10 +7,10 @@ end Then(/^the tip for commit "(.*?)" is for user "(.*?)"$/) do |arg1, arg2| - Tip.find_by_commit!(arg1).user.nickname.should eq(arg2) + expect(Tip.find_by_commit!(arg1).user.nickname).to eq(arg2) end Then(/^there should be no user with email "(.*?)"$/) do |arg1| - User.where(email: arg1).size.should eq(0) + expect(User.where(email: arg1).size).to eq(0) end diff --git a/features/step_definitions/common.rb b/features/step_definitions/common.rb index 17744062..6c01d5e4 100644 --- a/features/step_definitions/common.rb +++ b/features/step_definitions/common.rb @@ -4,7 +4,7 @@ Then(/^there should be (\d+) email sent$/) do |arg1| begin - ActionMailer::Base.deliveries.size.should eq(arg1.to_i) + expect(ActionMailer::Base.deliveries.size).to eq(arg1.to_i) rescue p ActionMailer::Base.deliveries raise @@ -17,7 +17,7 @@ end Then(/^no email should have been sent$/) do - ActionMailer::Base.deliveries.should eq([]) + expect(ActionMailer::Base.deliveries).to eq([]) end When(/^the email counters are reset$/) do @@ -119,28 +119,28 @@ def find_new_commit(id) When(/^the new commits are read$/) do @project.reload - @project.should_receive(:get_commits).and_return(@commits.values.map(&:to_ostruct)) + expect(@project).to receive(:get_commits).and_return(@commits.values.map(&:to_ostruct)) @project.update_commits - @project.should_receive(:get_commits).and_return(@commits.values.map(&:to_ostruct)) + 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| - Tip.where(commit: arg1).to_a.should eq([]) + 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 - amount.should_not be_nil - (amount.to_d / COIN).should eq(arg1.to_d) + 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| - Tip.find_by(commit: arg1).undecided?.should be_true + expect(Tip.find_by(commit: arg1).undecided?).to be true end Then(/^the new last known commit should be "(.*?)"$/) do |arg1| - @project.reload.last_commit.should eq(arg1) + expect(@project.reload.last_commit).to eq(arg1) end Given(/^the project collaborators are:$/) do |table| @@ -159,7 +159,8 @@ def find_new_commit(id) Given(/^a project managed by "(.*?)"$/) do |arg1| user = create(:user, email: "#{arg1}@example.com", nickname: arg1) - user.confirm! + user.confirm + user.save! @project = Project.create!(name: "#{arg1} project", bitcoin_address: 'mq4NtnmQoQoPfNWEPbhSvxvncgtGo6L8WY', address_label: "example_project_account") @project.collaborators.create!(login: arg1) end @@ -189,14 +190,14 @@ def find_new_commit(id) end Then(/^these amounts should have been sent from the account of the project:$/) do |table| - BitcoinDaemon.instance.list_transactions(@project.address_label).map do |tx| + 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.should eq(table.hashes) + end.compact).to eq(table.hashes) end When(/^the transaction history is cleared$/) do diff --git a/features/step_definitions/create_project.rb b/features/step_definitions/create_project.rb index a8b65487..6d32f4cd 100644 --- a/features/step_definitions/create_project.rb +++ b/features/step_definitions/create_project.rb @@ -1,38 +1,38 @@ Then(/^there should be a project "(.*?)"$/) do |arg1| - Project.pluck(:name).should include(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| - @project.description.should eq(string) + expect(@project.description).to eq(string) end Then(/^I should be on the project page$/) do - current_url.should eq(project_url(@project)) + expect(current_url).to eq(project_url(@project)) end Then(/^there should be no project$/) do - Project.all.should be_empty + expect(Project.all).to be_empty end Then(/^the GitHub name of the project should be "(.*?)"$/) do |arg1| - @project.full_name.should eq(arg1) + expect(@project.full_name).to eq(arg1) end Then(/^the project single collaborators should be "(.*?)"$/) do |arg1| if arg1 =~ /@/ - @project.collaborators.map(&:user).map(&:email).should eq([arg1]) + expect(@project.collaborators.map(&:user).map(&:email)).to eq([arg1]) else - @project.collaborators.map(&:user).map(&:nickname).should eq([arg1]) + expect(@project.collaborators.map(&:user).map(&:nickname)).to eq([arg1]) end end Then(/^the project address label should be "(.*?)"$/) do |arg1| - @project.address_label.should eq(arg1) + expect(@project.address_label).to eq(arg1) end Then(/^the project donation address should be the same as account "(.*?)"$/) do |arg1| - @project.bitcoin_address.should eq(BitcoinDaemon.instance.get_addresses_by_account(arg1).first) + 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 index c09ca14a..e9653b7b 100644 --- a/features/step_definitions/distribution.rb +++ b/features/step_definitions/distribution.rb @@ -22,7 +22,7 @@ Given(/^I add the GitHub user "(.*?)" to the recipients$/) do |arg1| within ".panel", text: "GitHub user" do - find("input:enabled").set(arg1) + find("input:enabled[name=\"user[nickname]\"]").set(arg1) click_on "Add" end end @@ -38,7 +38,7 @@ 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").set(user.identifier) + find("input:enabled[name=\"user[identifier]\"]").set(user.identifier) click_on "Add" end end @@ -51,7 +51,7 @@ When(/^I add the commit "(.*?)" to the recipients$/) do |arg1| within ".panel", text: "Author of a commit" do - find("input:enabled").set(arg1) + find("input:enabled[name=\"commit[sha]\"]").set(arg1) click_on "Add" end end @@ -105,28 +105,28 @@ def within_recipient_row(recipient) puts "Rows: " + all("#distribution-show-page tbody tr").map(&:text).inspect raise end - tr.find(".recipient").text.should eq(recipient) - tr.find(".address").text.should eq(row["address"]) if row["address"] - tr.find(".reason").text.should eq(row["reason"]) if row["reason"] + 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/ - text.to_d.should eq(row["amount"].to_d) + expect(text.to_d).to eq(row["amount"].to_d) else - text.should eq(row["amount"]) + expect(text).to eq(row["amount"]) end end if row["percentage"] text = tr.find(".percentage").text if row["percentage"] =~ /\A[0-9.]+\Z/ - text.to_d.should eq(row["percentage"].to_d) + expect(text.to_d).to eq(row["percentage"].to_d) else - text.should eq(row["percentage"]) + expect(text).to eq(row["percentage"]) end end - tr.find(".tip-comment").text.should eq(row["comment"]) if row["comment"] + expect(tr.find(".tip-comment").text).to eq(row["comment"]) if row["comment"] end - table.hashes.size.should eq(all("#distribution-show-page tbody tr").size) + expect(table.hashes.size).to eq(all("#distribution-show-page tbody tr").size) end Then(/^the distribution form should have these recipients:$/) do |table| @@ -138,22 +138,22 @@ def within_recipient_row(recipient) p errors: all(".alert.alert-danger").map(&:text) raise end - tr.find(".recipient").text.should eq(row["recipient"]) - tr.find(".reason").text.should eq(row["reason"]) if row["reason"] + 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/ - text.to_d.should eq(row["amount"].to_d) + expect(text.to_d).to eq(row["amount"].to_d) else - text.should eq(row["amount"]) + expect(text).to eq(row["amount"]) end end if row["comment"] text = tr.find_field("Comment").value - text.should eq(row["comment"]) + expect(text).to eq(row["comment"]) end end - all("#distribution-form tbody tr").size.should eq(table.hashes.size) + expect(all("#distribution-form tbody tr").size).to eq(table.hashes.size) end When(/^the tipper is started$/) do @@ -161,7 +161,7 @@ def within_recipient_row(recipient) end Then(/^no coins should have been sent$/) do - BitcoinDaemon.instance.list_transactions("*").should eq([]) + expect(BitcoinDaemon.instance.list_transactions("*")).to eq([]) end When(/^I set my address to "(.*?)"$/) do |arg1| @@ -171,7 +171,7 @@ def within_recipient_row(recipient) fill_in "Current password", with: "password" end click_on "Update" - page.should have_content "You updated your account successfully" + expect(page).to have_content "You updated your account successfully" end When(/^I click on the last distribution$/) do @@ -179,17 +179,17 @@ def within_recipient_row(recipient) end Then(/^an email should have been sent to "(.*?)"$/) do |arg1| - ActionMailer::Base.deliveries.map(&:to).should include([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| - @email.body.should include(arg1) + expect(@email.body).to include(arg1) end Then(/^the email should include a link to the last distribution$/) do distribution = Distribution.last - @email.body.should include(project_distribution_url(distribution.project, distribution)) + 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 @@ -197,14 +197,14 @@ def within_recipient_row(recipient) end Then(/^the user with email "(.*?)" should have "(.*?)" as password$/) do |arg1, arg2| - User.find_by(email: arg1).valid_password?(arg2).should eq(true) + 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| - User.find_by(email: arg1).bitcoin_address.should eq(arg2) + expect(User.find_by(email: arg1).bitcoin_address).to eq(arg2) end Given(/^I save the distribution$/) do click_on "Save" - page.should have_content(/Distribution (created|updated)/) + 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 index f7daf46f..65e390c2 100644 --- a/features/step_definitions/donate_to_project.rb +++ b/features/step_definitions/donate_to_project.rb @@ -1,19 +1,19 @@ Then(/^I should see the project donation address associated with "(.*?)"$/) do |arg1| address = @project.donation_addresses.find_by(sender_address: arg1).donation_address - address.should_not be_blank - page.should have_content(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 - address.should_not be_blank + expect(address).not_to be_blank BitcoinDaemon.instance.add_transaction(account: @project.address_label, amount: arg1.to_d, address: address) end Then(/^I should see the donor "(.*?)" sent "(.*?)"$/) do |arg1, arg2| within ".donor-row", text: arg1 do - find(".amount").text.to_d.should eq(arg2.to_d) + expect(find(".amount").text.to_d).to eq(arg2.to_d) end end diff --git a/features/step_definitions/tip_for_commit.rb b/features/step_definitions/tip_for_commit.rb index 03d34858..d06f8efe 100644 --- a/features/step_definitions/tip_for_commit.rb +++ b/features/step_definitions/tip_for_commit.rb @@ -9,7 +9,7 @@ Given(/^the commits on GitHub for project "(.*?)" are$/) do |arg1, table| @project.reload - @project.full_name.should eq(arg1) + expect(@project.full_name).to eq(arg1) commits = [] table.hashes.each do |row| commit = OpenStruct.new( @@ -30,7 +30,7 @@ commits << commit end - @project.should_receive(:get_commits).and_return(commits) + expect(@project).to receive(:get_commits).and_return(commits) end When(/^the project tips are built from commits$/) do @@ -44,5 +44,5 @@ amount: tip.amount ? (tip.amount.to_f / COIN).to_s : "", }.with_indifferent_access end - tips.should eq(table.hashes) + 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 index bf3e6784..47415667 100644 --- a/features/step_definitions/tip_modifier_interface.rb +++ b/features/step_definitions/tip_modifier_interface.rb @@ -27,31 +27,31 @@ end Then(/^I should see an access denied$/) do - page.should have_content("Access denied") + expect(page).to have_content("Access denied") end Then(/^the project should not hold tips$/) do - @project.reload.hold_tips.should be_false + expect(@project.reload.hold_tips).to be false end Then(/^the project should hold tips$/) do - @project.reload.hold_tips.should be_true + expect(@project.reload.hold_tips).to be true end Given(/^the project has undedided tips$/) do create(:undecided_tip, project: @project) - @project.reload.should have_undecided_tips + 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) - @project.reload.should have_undecided_tips + 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 - tip.should_not be_nil + expect(tip).not_to be_nil params = { project: { tips_attributes: { @@ -69,7 +69,7 @@ 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 } - tip.should_not be_nil + expect(tip).not_to be_nil params = { project: { tips_attributes: { @@ -85,9 +85,9 @@ end Then(/^the project should have (\d+) undecided tips$/) do |arg1| - @project.tips.undecided.size.should eq(arg1.to_i) + expect(@project.tips.undecided.size).to eq(arg1.to_i) end Then(/^there should be (\d+) tip$/) do |arg1| - @project.reload.tips.size.should eq(arg1.to_i) + 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 index 27aee5ae..cb098b90 100644 --- a/features/step_definitions/user_identifier.rb +++ b/features/step_definitions/user_identifier.rb @@ -1,5 +1,5 @@ Then(/^I should see the identifier of "(.*?)"$/) do |arg1| identifier = User.find_by(email: arg1).identifier - identifier.should be_present - page.should have_content 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 index 0b6a44e4..078823ce 100644 --- a/features/step_definitions/web.rb +++ b/features/step_definitions/web.rb @@ -15,7 +15,7 @@ visit root_path click_on "Sign in" click_on "Sign in with Github" - page.should have_content("Successfully authenticated") + expect(page).to have_content("Successfully authenticated") OmniAuth.config.mock_auth[:github] = nil @current_user = User.find_by(nickname: arg1) end @@ -35,15 +35,15 @@ visit root_path if page.has_content?("Sign Out") click_on "Sign Out" - page.should have_content("Signed out successfully") + expect(page).to have_content("Signed out successfully") else - page.should have_content("Sign in") + expect(page).to have_content("Sign in") end end When(/^I log out$/) do click_on "Sign Out" - page.should have_content "Signed out successfully" + expect(page).to have_content "Signed out successfully" end When(/^I log in as "(.*?)"$/) do |arg1| @@ -82,15 +82,15 @@ end Then(/^I should see "(.*?)"$/) do |arg1| - page.should have_content(arg1) + expect(page).to have_content(arg1) end Then(/^I should not see "(.*?)"$/) do |arg1| - page.should have_no_content(arg1) + expect(page).to have_no_content(arg1) end Then(/^I should not see the button "(.*?)"$/) do |arg1| - page.should have_no_button(arg1) + expect(page).to have_no_button(arg1) end Given(/^I fill "(.*?)" with:$/) do |arg1, string| @@ -103,22 +103,22 @@ Then(/^I should see the project donation address$/) do address = @project.bitcoin_address - address.should_not be_blank - page.should have_content(address) + expect(address).not_to be_blank + expect(page).to have_content(address) end Then(/^I should see the project balance is "(.*?)"$/) do |arg1| - page.should have_content("Funds #{arg1}") + expect(page).to have_content("Funds #{arg1}") end Then(/^I should see a link "(.*?)" to "(.*?)"$/) do |arg1, arg2| - link = find("a", text: arg1, exact: true) - link["href"].should eq(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, exact: true).first - (link.nil? or link["href"] != arg2).should be_true + 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| @@ -128,7 +128,7 @@ 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 } - link.should_not be_nil + expect(link).not_to be_nil rescue puts @email.body raise @@ -142,7 +142,7 @@ end Then(/^the user with email "(.*?)" should have his email confirmed$/) do |arg1| - User.find_by(email: arg1).confirmed?.should be_true + expect(User.find_by(email: arg1).confirmed?).to be true end When(/^I go to edit my profile$/) do From 959171e67ffcb6e5400be2e82fa44ba6713eceb5 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 11 Dec 2016 13:47:56 +0100 Subject: [PATCH 341/372] Fix random tip ordering --- app/views/distributions/_tip_form.html.haml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/views/distributions/_tip_form.html.haml b/app/views/distributions/_tip_form.html.haml index 2d3e9af4..af03720a 100644 --- a/app/views/distributions/_tip_form.html.haml +++ b/app/views/distributions/_tip_form.html.haml @@ -1,4 +1,5 @@ -= form.fields_for :tips, tip, child_index: rand(1<<160) do |fields| +- 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 From 387f6763a72ccd1e53734f0f029e7b337d53168c Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 11 Dec 2016 14:04:27 +0100 Subject: [PATCH 342/372] Downgrade bootstrap form --- Gemfile | 2 +- Gemfile.lock | 9 ++------- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/Gemfile b/Gemfile index c9dfd1a4..2b46708b 100644 --- a/Gemfile +++ b/Gemfile @@ -71,7 +71,7 @@ gem 'whenever' gem 'rqrcode-rails3' gem 'exception_notification' gem 'rack-canonical-host' -gem 'bootstrap_form', github: 'bootstrap-ruby/rails-bootstrap-forms' +gem 'bootstrap_form', '~> 2.3.0' # version 2.4.0 raises a "can't modify frozen string" in gemspec evaluation on old systems gem 'html_pipeline_rails' gem 'rails_autolink' gem 'redcarpet' diff --git a/Gemfile.lock b/Gemfile.lock index 46d90761..eb5a5365 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -7,12 +7,6 @@ GIT omniauth (~> 1.0) omniauth-oauth2 (~> 1.1) -GIT - remote: git://github.com/bootstrap-ruby/rails-bootstrap-forms.git - revision: 5512154e247861cd9e07942d04bf1fe64eecbe13 - specs: - bootstrap_form (2.5.2) - GIT remote: git://github.com/capistrano/rvm.git revision: 9cfef39cf0022839dca6b5b330dfefeb5fc363e7 @@ -76,6 +70,7 @@ GEM sshkit (>= 1.6.1, != 1.7.0) arel (4.0.2) bcrypt (3.1.11) + bootstrap_form (2.3.0) builder (3.1.4) cancancan (1.15.0) capistrano (3.7.0) @@ -360,7 +355,7 @@ PLATFORMS DEPENDENCIES airbrake - bootstrap_form! + bootstrap_form (~> 2.3.0) cancancan capistrano (~> 3.0) capistrano-bundler (>= 1.1.0) From 3c4a331f1ad5e058154fda53fd63e9b8d8bc870d Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 11 Dec 2016 14:06:30 +0100 Subject: [PATCH 343/372] Downgrade gem to support 1.9 --- Gemfile | 1 + Gemfile.lock | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index 2b46708b..6a15f5ed 100644 --- a/Gemfile +++ b/Gemfile @@ -80,6 +80,7 @@ gem 'twitter-typeahead-rails' gem 'commontator', '~> 4.6.0' gem 'compass-rails' gem 'mime-types', '~> 2.99' # to support ruby 1.9 +gem 'public_suffix', '~> 1.4.6' # to support ruby 1.9 group :test do gem 'cucumber-rails', :require => false diff --git a/Gemfile.lock b/Gemfile.lock index eb5a5365..403ee299 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -61,8 +61,7 @@ GEM multi_json (~> 1.3) thread_safe (~> 0.1) tzinfo (~> 0.3.37) - addressable (2.5.0) - public_suffix (~> 2.0, >= 2.0.2) + addressable (2.4.0) airbrake (5.6.1) airbrake-ruby (~> 1.6) airbrake-ruby (1.6.0) @@ -240,7 +239,7 @@ GEM capybara (~> 2.1) cliver (~> 0.3.1) websocket-driver (>= 0.2.0) - public_suffix (2.0.4) + public_suffix (1.4.6) quiet_assets (1.1.0) railties (>= 3.1, < 5.0) rack (1.5.5) @@ -384,6 +383,7 @@ DEPENDENCIES omniauth-github! pg poltergeist + public_suffix (~> 1.4.6) quiet_assets rack-canonical-host rails (~> 4.0.2) From d6b02111be5557477e7921830408fa33cfb02ecc Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 11 Dec 2016 14:18:22 +0100 Subject: [PATCH 344/372] Increase minimum ruby version --- Gemfile | 2 -- Gemfile.lock | 13 +++++++------ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/Gemfile b/Gemfile index 6a15f5ed..99ab6956 100644 --- a/Gemfile +++ b/Gemfile @@ -79,8 +79,6 @@ gem 'sanitize' gem 'twitter-typeahead-rails' gem 'commontator', '~> 4.6.0' gem 'compass-rails' -gem 'mime-types', '~> 2.99' # to support ruby 1.9 -gem 'public_suffix', '~> 1.4.6' # to support ruby 1.9 group :test do gem 'cucumber-rails', :require => false diff --git a/Gemfile.lock b/Gemfile.lock index 403ee299..20dcce49 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -61,7 +61,8 @@ GEM multi_json (~> 1.3) thread_safe (~> 0.1) tzinfo (~> 0.3.37) - addressable (2.4.0) + addressable (2.5.0) + public_suffix (~> 2.0, >= 2.0.2) airbrake (5.6.1) airbrake-ruby (~> 1.6) airbrake-ruby (1.6.0) @@ -204,7 +205,9 @@ GEM libv8 (3.16.14.17) mail (2.6.4) mime-types (>= 1.16, < 4) - mime-types (2.99.3) + mime-types (3.1) + mime-types-data (~> 3.2015) + mime-types-data (3.2016.0521) mini_portile2 (2.1.0) minitest (4.7.5) multi_json (1.12.1) @@ -239,7 +242,7 @@ GEM capybara (~> 2.1) cliver (~> 0.3.1) websocket-driver (>= 0.2.0) - public_suffix (1.4.6) + public_suffix (2.0.4) quiet_assets (1.1.0) railties (>= 3.1, < 5.0) rack (1.5.5) @@ -376,14 +379,12 @@ DEPENDENCIES jquery-rails kaminari less-rails - mime-types (~> 2.99) mysql2 octokit omniauth omniauth-github! pg poltergeist - public_suffix (~> 1.4.6) quiet_assets rack-canonical-host rails (~> 4.0.2) @@ -405,4 +406,4 @@ DEPENDENCIES whenever BUNDLED WITH - 1.12.5 + 1.13.6 From 9a56969b268525281f439cd8ec76e172a03e33c0 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 11 Dec 2016 14:58:26 +0100 Subject: [PATCH 345/372] Add ruby version --- .ruby-version | 1 + 1 file changed, 1 insertion(+) create mode 100644 .ruby-version diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 00000000..0bee604d --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +2.3.3 From 61d4d58edfa30946cad9365d8180140c965bd5e1 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 11 Dec 2016 15:49:12 +0100 Subject: [PATCH 346/372] Update Rails to 4.1 --- Gemfile | 2 +- Gemfile.lock | 71 ++++++++++++++++++++++++++++------------------------ 2 files changed, 39 insertions(+), 34 deletions(-) diff --git a/Gemfile b/Gemfile index 99ab6956..1506b52f 100644 --- a/Gemfile +++ b/Gemfile @@ -1,7 +1,7 @@ source 'https://rubygems.org' # Bundle edge Rails instead: gem 'rails', github: 'rails/rails' -gem 'rails', '~> 4.0.2' +gem 'rails', '~> 4.1.0' # Databases gem 'sqlite3', group: :development diff --git a/Gemfile.lock b/Gemfile.lock index 20dcce49..4b949ca1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -37,30 +37,32 @@ GIT GEM remote: https://rubygems.org/ specs: - actionmailer (4.0.13) - actionpack (= 4.0.13) + actionmailer (4.1.16) + actionpack (= 4.1.16) + actionview (= 4.1.16) mail (~> 2.5, >= 2.5.4) - actionpack (4.0.13) - activesupport (= 4.0.13) - builder (~> 3.1.0) - erubis (~> 2.7.0) + actionpack (4.1.16) + actionview (= 4.1.16) + activesupport (= 4.1.16) rack (~> 1.5.2) rack-test (~> 0.6.2) - activemodel (4.0.13) - activesupport (= 4.0.13) - builder (~> 3.1.0) - activerecord (4.0.13) - activemodel (= 4.0.13) - activerecord-deprecated_finders (~> 1.0.2) - activesupport (= 4.0.13) - arel (~> 4.0.0) - activerecord-deprecated_finders (1.0.4) - activesupport (4.0.13) + actionview (4.1.16) + activesupport (= 4.1.16) + builder (~> 3.1) + erubis (~> 2.7.0) + activemodel (4.1.16) + activesupport (= 4.1.16) + builder (~> 3.1) + activerecord (4.1.16) + activemodel (= 4.1.16) + activesupport (= 4.1.16) + arel (~> 5.0.0) + activesupport (4.1.16) i18n (~> 0.6, >= 0.6.9) - minitest (~> 4.2) - multi_json (~> 1.3) + json (~> 1.7, >= 1.7.7) + minitest (~> 5.1) thread_safe (~> 0.1) - tzinfo (~> 0.3.37) + tzinfo (~> 1.1) addressable (2.5.0) public_suffix (~> 2.0, >= 2.0.2) airbrake (5.6.1) @@ -68,10 +70,10 @@ GEM airbrake-ruby (1.6.0) airbrussh (1.1.1) sshkit (>= 1.6.1, != 1.7.0) - arel (4.0.2) + arel (5.0.1.20140414130214) bcrypt (3.1.11) bootstrap_form (2.3.0) - builder (3.1.4) + builder (3.2.2) cancancan (1.15.0) capistrano (3.7.0) airbrussh (>= 1.0.0) @@ -209,7 +211,7 @@ GEM mime-types-data (~> 3.2015) mime-types-data (3.2016.0521) mini_portile2 (2.1.0) - minitest (4.7.5) + minitest (5.10.1) multi_json (1.12.1) multi_test (0.1.2) multi_xml (0.6.0) @@ -251,19 +253,21 @@ GEM rack (>= 1.0.0, < 3) rack-test (0.6.3) rack (>= 1.0) - rails (4.0.13) - actionmailer (= 4.0.13) - actionpack (= 4.0.13) - activerecord (= 4.0.13) - activesupport (= 4.0.13) + rails (4.1.16) + actionmailer (= 4.1.16) + actionpack (= 4.1.16) + actionview (= 4.1.16) + activemodel (= 4.1.16) + activerecord (= 4.1.16) + activesupport (= 4.1.16) bundler (>= 1.3.0, < 2.0) - railties (= 4.0.13) + railties (= 4.1.16) sprockets-rails (~> 2.0) rails_autolink (1.1.6) rails (> 3.1) - railties (4.0.13) - actionpack (= 4.0.13) - activesupport (= 4.0.13) + railties (4.1.16) + actionpack (= 4.1.16) + activesupport (= 4.1.16) rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) rake (12.0.0) @@ -339,7 +343,8 @@ GEM actionpack (>= 3.1) jquery-rails railties (>= 3.1) - tzinfo (0.3.52) + tzinfo (1.2.2) + thread_safe (~> 0.1) uglifier (3.0.4) execjs (>= 0.3.0, < 3) warden (1.2.6) @@ -387,7 +392,7 @@ DEPENDENCIES poltergeist quiet_assets rack-canonical-host - rails (~> 4.0.2) + rails (~> 4.1.0) rails_autolink redcarpet rqrcode-rails3 From 83b6c583f9f5f0a62038d27b97b9ea351cc91c11 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 11 Dec 2016 16:15:27 +0100 Subject: [PATCH 347/372] Prevent error on invalid repository names --- app/models/project.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/models/project.rb b/app/models/project.rb index ff23b546..62168c70 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -35,6 +35,7 @@ def source_github_url end 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 \ From ce8a0b2fbaaf75d567c652e753787e81287d8624 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 11 Dec 2016 16:25:45 +0100 Subject: [PATCH 348/372] Upgrade Rails to 4.2 and Devise to 4.2 --- Gemfile | 7 +- Gemfile.lock | 104 ++++++++++++-------- app/controllers/application_controller.rb | 2 +- app/controllers/registrations_controller.rb | 2 +- app/models/tip.rb | 2 +- config/application.rb | 2 + config/environments/production.rb | 2 +- config/environments/test.rb | 2 +- config/initializers/cookies_serializer.rb | 1 + 9 files changed, 76 insertions(+), 48 deletions(-) create mode 100644 config/initializers/cookies_serializer.rb diff --git a/Gemfile b/Gemfile index 1506b52f..9a26dc71 100644 --- a/Gemfile +++ b/Gemfile @@ -1,7 +1,7 @@ source 'https://rubygems.org' # Bundle edge Rails instead: gem 'rails', github: 'rails/rails' -gem 'rails', '~> 4.1.0' +gem 'rails', '~> 4.2.0' # Databases gem 'sqlite3', group: :development @@ -9,7 +9,7 @@ gem 'mysql2', group: :mysql gem 'pg', group: :postgresql # Use SCSS for stylesheets -gem 'sass-rails', '~> 4.0.2' +gem 'sass-rails' gem 'haml-rails' gem "less-rails" @@ -21,7 +21,7 @@ gem 'kaminari' gem 'uglifier', '>= 1.3.0' # Use CoffeeScript for .js.coffee assets and views -gem 'coffee-rails', '~> 4.0.0' +gem 'coffee-rails' # See https://github.com/sstephenson/execjs#readme for more supported runtimes gem 'therubyracer', platforms: :ruby @@ -41,6 +41,7 @@ group :doc do end gem 'devise' +gem 'test_after_commit', :group => :test # https://github.com/plataformatec/devise/blob/master/CHANGELOG.md#410 gem 'omniauth' gem 'omniauth-github', github: 'alexandrz/omniauth-github', branch: 'provide_emails' gem 'cancancan' diff --git a/Gemfile.lock b/Gemfile.lock index 4b949ca1..878e800d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -37,31 +37,40 @@ GIT GEM remote: https://rubygems.org/ specs: - actionmailer (4.1.16) - actionpack (= 4.1.16) - actionview (= 4.1.16) + actionmailer (4.2.7.1) + actionpack (= 4.2.7.1) + actionview (= 4.2.7.1) + activejob (= 4.2.7.1) mail (~> 2.5, >= 2.5.4) - actionpack (4.1.16) - actionview (= 4.1.16) - activesupport (= 4.1.16) - rack (~> 1.5.2) + rails-dom-testing (~> 1.0, >= 1.0.5) + actionpack (4.2.7.1) + actionview (= 4.2.7.1) + activesupport (= 4.2.7.1) + rack (~> 1.6) rack-test (~> 0.6.2) - actionview (4.1.16) - activesupport (= 4.1.16) + rails-dom-testing (~> 1.0, >= 1.0.5) + rails-html-sanitizer (~> 1.0, >= 1.0.2) + actionview (4.2.7.1) + activesupport (= 4.2.7.1) builder (~> 3.1) erubis (~> 2.7.0) - activemodel (4.1.16) - activesupport (= 4.1.16) + rails-dom-testing (~> 1.0, >= 1.0.5) + rails-html-sanitizer (~> 1.0, >= 1.0.2) + activejob (4.2.7.1) + activesupport (= 4.2.7.1) + globalid (>= 0.3.0) + activemodel (4.2.7.1) + activesupport (= 4.2.7.1) builder (~> 3.1) - activerecord (4.1.16) - activemodel (= 4.1.16) - activesupport (= 4.1.16) - arel (~> 5.0.0) - activesupport (4.1.16) - i18n (~> 0.6, >= 0.6.9) + activerecord (4.2.7.1) + activemodel (= 4.2.7.1) + activesupport (= 4.2.7.1) + arel (~> 6.0) + activesupport (4.2.7.1) + i18n (~> 0.7) json (~> 1.7, >= 1.7.7) minitest (~> 5.1) - thread_safe (~> 0.1) + thread_safe (~> 0.3, >= 0.3.4) tzinfo (~> 1.1) addressable (2.5.0) public_suffix (~> 2.0, >= 2.0.2) @@ -70,7 +79,7 @@ GEM airbrake-ruby (1.6.0) airbrussh (1.1.1) sshkit (>= 1.6.1, != 1.7.0) - arel (5.0.1.20140414130214) + arel (6.0.3) bcrypt (3.1.11) bootstrap_form (2.3.0) builder (3.2.2) @@ -137,12 +146,11 @@ GEM railties (>= 3, < 5.1) cucumber-wire (0.0.1) database_cleaner (1.5.3) - devise (3.5.10) + devise (4.2.0) bcrypt (~> 3.0) orm_adapter (~> 0.1) - railties (>= 3.2.6, < 5) + railties (>= 4.1.0, < 5.1) responders - thread_safe (~> 0.1) warden (~> 1.2.3) diff-lcs (1.2.5) erubis (2.7.0) @@ -160,6 +168,8 @@ GEM fssm (0.2.10) gherkin (4.0.0) github-markdown (0.6.9) + globalid (0.3.7) + activesupport (>= 4.1.0) haml (4.0.7) tilt haml-rails (0.9.0) @@ -205,6 +215,8 @@ GEM sprockets (> 2, < 4) tilt libv8 (3.16.14.17) + loofah (2.0.3) + nokogiri (>= 1.5.9) mail (2.6.4) mime-types (>= 1.16, < 4) mime-types (3.1) @@ -247,35 +259,44 @@ GEM public_suffix (2.0.4) quiet_assets (1.1.0) railties (>= 3.1, < 5.0) - rack (1.5.5) + rack (1.6.5) rack-canonical-host (0.2.2) addressable (> 0, < 3) rack (>= 1.0.0, < 3) rack-test (0.6.3) rack (>= 1.0) - rails (4.1.16) - actionmailer (= 4.1.16) - actionpack (= 4.1.16) - actionview (= 4.1.16) - activemodel (= 4.1.16) - activerecord (= 4.1.16) - activesupport (= 4.1.16) + rails (4.2.7.1) + actionmailer (= 4.2.7.1) + actionpack (= 4.2.7.1) + actionview (= 4.2.7.1) + activejob (= 4.2.7.1) + activemodel (= 4.2.7.1) + activerecord (= 4.2.7.1) + activesupport (= 4.2.7.1) bundler (>= 1.3.0, < 2.0) - railties (= 4.1.16) - sprockets-rails (~> 2.0) + railties (= 4.2.7.1) + sprockets-rails + rails-deprecated_sanitizer (1.0.3) + activesupport (>= 4.2.0.alpha) + rails-dom-testing (1.0.7) + activesupport (>= 4.2.0.beta, < 5.0) + nokogiri (~> 1.6.0) + rails-deprecated_sanitizer (>= 1.0.1) + rails-html-sanitizer (1.0.3) + loofah (~> 2.0) rails_autolink (1.1.6) rails (> 3.1) - railties (4.1.16) - actionpack (= 4.1.16) - activesupport (= 4.1.16) + railties (4.2.7.1) + actionpack (= 4.2.7.1) + activesupport (= 4.2.7.1) rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) rake (12.0.0) rdoc (4.3.0) redcarpet (3.3.4) ref (2.0.0) - responders (1.1.2) - railties (>= 3.2, < 4.2) + responders (2.3.0) + railties (>= 4.2.0, < 5.1) rqrcode (0.10.1) chunky_png (~> 1.0) rqrcode-rails3 (0.1.7) @@ -329,6 +350,8 @@ GEM sshkit (1.11.4) net-scp (>= 1.1.2) net-ssh (>= 2.8.0) + test_after_commit (1.1.0) + activerecord (>= 3.2) therubyracer (0.12.2) libv8 (~> 3.16.14.0) ref @@ -369,7 +392,7 @@ DEPENDENCIES capistrano-rails capistrano-rvm! capybara-screenshot - coffee-rails (~> 4.0.0) + coffee-rails commontator (~> 4.6.0) compass-rails cucumber-rails @@ -392,15 +415,16 @@ DEPENDENCIES poltergeist quiet_assets rack-canonical-host - rails (~> 4.1.0) + rails (~> 4.2.0) rails_autolink redcarpet rqrcode-rails3 rspec-rails sanitize - sass-rails (~> 4.0.2) + sass-rails sdoc sqlite3 + test_after_commit therubyracer timecop turbolinks diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 4fed25b5..c38a1097 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -21,6 +21,6 @@ def after_sign_in_path_for(user) end def configure_permitted_parameters - devise_parameter_sanitizer.for(:account_update) { |u| u.permit(:email, :name, :bitcoin_address, :current_password, :password, :password_confirmation) } + devise_parameter_sanitizer.permit(:account_update, keys: [:email, :name, :bitcoin_address, :current_password, :password, :password_confirmation]) end end diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb index b21f093d..e0d95db9 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/registrations_controller.rb @@ -13,7 +13,7 @@ def update if successfully_updated set_flash_message :notice, :updated # Sign in the user bypassing validation in case their password changed - sign_in @user, :bypass => true + bypass_sign_in @user redirect_to after_update_path_for(@user) else render "edit" diff --git a/app/models/tip.rb b/app/models/tip.rb index c36a2de9..c1226f37 100644 --- a/app/models/tip.rb +++ b/app/models/tip.rb @@ -85,7 +85,7 @@ def commit_url 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 + UserMailer.new_tip(user, self).deliver_now user.touch :notified_at end end diff --git a/config/application.rb b/config/application.rb index 59da21f9..98b32389 100644 --- a/config/application.rb +++ b/config/application.rb @@ -28,5 +28,7 @@ class Application < Rails::Application config.autoload_paths += %W(#{config.root}/lib) I18n.enforce_available_locales = false + + config.active_record.raise_in_transactional_callbacks = true end end diff --git a/config/environments/production.rb b/config/environments/production.rb index d7e4b501..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 diff --git a/config/environments/test.rb b/config/environments/test.rb index ec5a2086..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. 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 From 3fea907dc5820a2416f37eae36347ae954d9e945 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 11 Dec 2016 16:30:50 +0100 Subject: [PATCH 349/372] Use https to clone repositories --- Gemfile | 8 ++++---- Gemfile.lock | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Gemfile b/Gemfile index 9a26dc71..959b4bdd 100644 --- a/Gemfile +++ b/Gemfile @@ -13,7 +13,7 @@ gem 'sass-rails' gem 'haml-rails' gem "less-rails" -gem 'twitter-bootstrap-rails', github: 'seyhunak/twitter-bootstrap-rails', branch: 'bootstrap3' +gem 'twitter-bootstrap-rails', git: 'https://github.com/seyhunak/twitter-bootstrap-rails.git', branch: 'bootstrap3' gem 'kaminari' @@ -43,9 +43,9 @@ end gem 'devise' gem 'test_after_commit', :group => :test # https://github.com/plataformatec/devise/blob/master/CHANGELOG.md#410 gem 'omniauth' -gem 'omniauth-github', github: 'alexandrz/omniauth-github', branch: 'provide_emails' +gem 'omniauth-github', git: 'https://github.com/alexandrz/omniauth-github.git', branch: 'provide_emails' gem 'cancancan' -gem 'twitter_bootstrap_form_for', github: 'stouset/twitter_bootstrap_form_for' +gem 'twitter_bootstrap_form_for', git: 'https://github.com/stouset/twitter_bootstrap_form_for.git' gem 'octokit' @@ -60,7 +60,7 @@ gem 'octokit' group :development do gem 'capistrano', '~> 3.0' - gem 'capistrano-rvm', github: 'capistrano/rvm' + gem 'capistrano-rvm', git: 'https://github.com/capistrano/rvm.git' gem 'capistrano-bundler', '>= 1.1.0' gem 'capistrano-rails' gem 'quiet_assets' diff --git a/Gemfile.lock b/Gemfile.lock index 878e800d..21003b21 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,5 +1,5 @@ GIT - remote: git://github.com/alexandrz/omniauth-github.git + remote: https://github.com/alexandrz/omniauth-github.git revision: 37a030aa37659831ef80af21b5c7270fe1384b3c branch: provide_emails specs: @@ -8,7 +8,7 @@ GIT omniauth-oauth2 (~> 1.1) GIT - remote: git://github.com/capistrano/rvm.git + remote: https://github.com/capistrano/rvm.git revision: 9cfef39cf0022839dca6b5b330dfefeb5fc363e7 specs: capistrano-rvm (0.1.2) @@ -16,7 +16,7 @@ GIT sshkit (~> 1.2) GIT - remote: git://github.com/seyhunak/twitter-bootstrap-rails.git + remote: https://github.com/seyhunak/twitter-bootstrap-rails.git revision: 1d93c5a77049b3d21d17c847ad0531d7714fa229 branch: bootstrap3 specs: @@ -27,7 +27,7 @@ GIT railties (>= 3.1) GIT - remote: git://github.com/stouset/twitter_bootstrap_form_for.git + remote: https://github.com/stouset/twitter_bootstrap_form_for.git revision: 830dbfd439ebb1194e1ae025100fc0e790be37cf specs: twitter_bootstrap_form_for (2.0.0.beta) From 8c74d13cc2104cfed6d984d355603d107f853093 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 11 Dec 2016 18:25:20 +0100 Subject: [PATCH 350/372] Update ruby version in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e90f04f5..0da0b91f 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Development To run peer4commit in development mode follow these instructions: -* Install [Ruby](https://www.ruby-lang.org/en/downloads/) 1.9+ +* Install [Ruby](https://www.ruby-lang.org/en/downloads/) 2.0+ * Install the [bundler](http://bundler.io/) gem (you may need root): ``` From 445ec32e42ecece62238197e19d9312b7e4defa0 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Fri, 23 Dec 2016 10:35:49 +0100 Subject: [PATCH 351/372] Unshorten dates --- app/views/distributions/index.html.haml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/distributions/index.html.haml b/app/views/distributions/index.html.haml index 1b947082..974d7590 100644 --- a/app/views/distributions/index.html.haml +++ b/app/views/distributions/index.html.haml @@ -14,8 +14,8 @@ %tbody - @distributions.each do |distribution| %tr - %td= l distribution.created_at, format: :short - %td= l distribution.sent_at, format: :short if distribution.sent_at + %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? From 832f641cf8696d753edada6fc9aff85d6b1c2dc4 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Fri, 23 Dec 2016 11:15:28 +0100 Subject: [PATCH 352/372] Update explorers --- app/helpers/application_helper.rb | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index dd38f33e..b71b2a84 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -26,18 +26,17 @@ def to_btc satoshies end def transaction_url(txid) - "http://bkchain.org/ppc/tx/#{txid}" + "https://peercoin.mintr.org/tx/#{txid}" end def address_explorers - [:bkchain, :blockr, :cryptocoin] + [:mintr, :blockr] end def address_url(address, explorer = address_explorers.first) case explorer when :blockr then "http://ppc.blockr.io/address/info/#{address}" - when :bkchain then "http://bkchain.org/ppc/address/#{address}" - when :cryptocoin then "http://ppc.cryptocoinexplorer.com/address/#{address}" + when :mintr then "https://peercoin.mintr.org/address/#{address}" else raise "Unknown provider: #{provider.inspect}" end end From 08cd93354c63c9c02d371884c2802a69117835aa Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Fri, 23 Dec 2016 11:16:55 +0100 Subject: [PATCH 353/372] Ingnore move transactions --- lib/balance_updater.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/balance_updater.rb b/lib/balance_updater.rb index 811bfa49..1c52cff8 100644 --- a/lib/balance_updater.rb +++ b/lib/balance_updater.rb @@ -25,6 +25,10 @@ def self.work 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) From 607823e67f8ff8602f12995c46d71b5cd216ee07 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sat, 4 Mar 2017 08:52:44 +0100 Subject: [PATCH 354/372] Add awesome_print gem --- Gemfile | 1 + Gemfile.lock | 2 ++ 2 files changed, 3 insertions(+) diff --git a/Gemfile b/Gemfile index 959b4bdd..f248ce9f 100644 --- a/Gemfile +++ b/Gemfile @@ -91,3 +91,4 @@ group :test do gem 'timecop' gem 'capybara-screenshot' end +gem 'awesome_print', group: [:development, :test] diff --git a/Gemfile.lock b/Gemfile.lock index 21003b21..b8a8f8ef 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -80,6 +80,7 @@ GEM airbrussh (1.1.1) sshkit (>= 1.6.1, != 1.7.0) arel (6.0.3) + awesome_print (1.7.0) bcrypt (3.1.11) bootstrap_form (2.3.0) builder (3.2.2) @@ -385,6 +386,7 @@ PLATFORMS DEPENDENCIES airbrake + awesome_print bootstrap_form (~> 2.3.0) cancancan capistrano (~> 3.0) From 61440592b90cc04b10fb3783a7c59d27da289378 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sat, 4 Mar 2017 09:00:28 +0100 Subject: [PATCH 355/372] Fix donation to multiple projects in a single transaction --- features/donate_to_project.feature | 19 ++++++++++++++++ features/step_definitions/cold_storage.rb | 2 +- features/step_definitions/common.rb | 2 +- .../step_definitions/donate_to_project.rb | 22 +++++++++++++++++++ features/step_definitions/web.rb | 12 +++++++++- lib/balance_updater.rb | 2 +- 6 files changed, 55 insertions(+), 4 deletions(-) diff --git a/features/donate_to_project.feature b/features/donate_to_project.feature index 614c816f..53213036 100644 --- a/features/donate_to_project.feature +++ b/features/donate_to_project.feature @@ -49,3 +49,22 @@ Feature: A visitor can donate to a project 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/step_definitions/cold_storage.rb b/features/step_definitions/cold_storage.rb index 82966372..ced4006c 100644 --- a/features/step_definitions/cold_storage.rb +++ b/features/step_definitions/cold_storage.rb @@ -31,7 +31,7 @@ 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 updated$/) do +When(/^the project (?:balance is|balances are) updated$/) do BalanceUpdater.work end diff --git a/features/step_definitions/common.rb b/features/step_definitions/common.rb index 6c01d5e4..5fb11554 100644 --- a/features/step_definitions/common.rb +++ b/features/step_definitions/common.rb @@ -36,7 +36,7 @@ @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| +Given(/^a project "([^"]*?)"$/) do |arg1| @project = Project.create!(name: "test", full_name: "example/#{arg1}", bitcoin_address: 'mq4NtnmQoQoPfNWEPbhSvxvncgtGo6L8WY', hold_tips: false) end diff --git a/features/step_definitions/donate_to_project.rb b/features/step_definitions/donate_to_project.rb index 65e390c2..fdf810e5 100644 --- a/features/step_definitions/donate_to_project.rb +++ b/features/step_definitions/donate_to_project.rb @@ -1,3 +1,14 @@ +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 @@ -11,6 +22,17 @@ 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) diff --git a/features/step_definitions/web.rb b/features/step_definitions/web.rb index 078823ce..b2aef649 100644 --- a/features/step_definitions/web.rb +++ b/features/step_definitions/web.rb @@ -101,6 +101,11 @@ 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 @@ -108,7 +113,12 @@ end Then(/^I should see the project balance is "(.*?)"$/) do |arg1| - expect(page).to have_content("Funds #{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| diff --git a/lib/balance_updater.rb b/lib/balance_updater.rb index 1c52cff8..31d8ecaf 100644 --- a/lib/balance_updater.rb +++ b/lib/balance_updater.rb @@ -35,7 +35,7 @@ def self.work next end - if deposit = Deposit.find_by_txid(txid) + if deposit = project.deposits.find_by_txid(txid) deposit.update_attribute(:confirmations, confirmations) next end From ed959305d16d6ee7249a80e7682eccf2a270f878 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 9 Jul 2017 12:13:54 +0200 Subject: [PATCH 356/372] Display date of each deposit --- app/assets/stylesheets/projects.css.sass | 5 +++++ app/views/projects/donors.html.haml | 20 ++++++++++---------- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/app/assets/stylesheets/projects.css.sass b/app/assets/stylesheets/projects.css.sass index 198e0ad7..708a8cca 100644 --- a/app/assets/stylesheets/projects.css.sass +++ b/app/assets/stylesheets/projects.css.sass @@ -13,3 +13,8 @@ .donor-list .amount text-align: right + + .txid abbr + font-variant: none + text-decoration: none + border-bottom: none diff --git a/app/views/projects/donors.html.haml b/app/views/projects/donors.html.haml index 062a91c6..7c8c4298 100644 --- a/app/views/projects/donors.html.haml +++ b/app/views/projects/donors.html.haml @@ -8,16 +8,16 @@ %table.table.table-hover.donor-list %thead %tr - %th.sender-address Sender address + %th.date Date %th.amount Amount - %th.transactions Transactions + %th.sender-address Sender address + %th.transactions Transaction %tbody - - groups = @project.deposits.includes(:donation_address).group_by(&:donation_address) - - groups = groups.sort_by { |da, deposits| deposits.map(&:amount).sum }.reverse - - groups.each do |donation_address, deposits| + - @project.deposits.includes(:donation_address).order(created_at: :desc).each do |deposit| %tr.donor-row - %td.sender-address= donation_address.try(:sender_address).presence || 'No address provided' - %td.amount= btc_human deposits.map(&:amount).sum - %td.transactions - - deposits.sort_by(&:created_at).each_with_index do |deposit, i| - = link_to "[#{i+1}]", transaction_url(deposit.txid) + %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) From 182e4bb8dde7133d04cd66c9ad64facc48263d20 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 9 Jul 2017 12:20:31 +0200 Subject: [PATCH 357/372] Fix test failing randomly because of order --- features/step_definitions/common.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/step_definitions/common.rb b/features/step_definitions/common.rb index 5fb11554..e857a5b5 100644 --- a/features/step_definitions/common.rb +++ b/features/step_definitions/common.rb @@ -197,7 +197,7 @@ def find_new_commit(id) "amount" => (-tx["amount"]).to_s, } end - end.compact).to eq(table.hashes) + end.compact.sort_by { |x| x["address"] }).to eq(table.hashes.sort_by { |x| x["address"] }) end When(/^the transaction history is cleared$/) do From 9219949b4bdd9837c14f1e7decfccd051742ee14 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 9 Jul 2017 12:20:46 +0200 Subject: [PATCH 358/372] Update gems --- Gemfile | 1 + Gemfile.lock | 335 ++++++++++++++++++++++++++++----------------------- 2 files changed, 182 insertions(+), 154 deletions(-) diff --git a/Gemfile b/Gemfile index f248ce9f..9c13300b 100644 --- a/Gemfile +++ b/Gemfile @@ -92,3 +92,4 @@ group :test do gem 'capybara-screenshot' end gem 'awesome_print', group: [:development, :test] +gem 'commonmarker' diff --git a/Gemfile.lock b/Gemfile.lock index b8a8f8ef..33910731 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -37,68 +37,65 @@ GIT GEM remote: https://rubygems.org/ specs: - actionmailer (4.2.7.1) - actionpack (= 4.2.7.1) - actionview (= 4.2.7.1) - activejob (= 4.2.7.1) + actionmailer (4.2.9) + actionpack (= 4.2.9) + actionview (= 4.2.9) + activejob (= 4.2.9) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 1.0, >= 1.0.5) - actionpack (4.2.7.1) - actionview (= 4.2.7.1) - activesupport (= 4.2.7.1) + actionpack (4.2.9) + actionview (= 4.2.9) + activesupport (= 4.2.9) rack (~> 1.6) rack-test (~> 0.6.2) rails-dom-testing (~> 1.0, >= 1.0.5) rails-html-sanitizer (~> 1.0, >= 1.0.2) - actionview (4.2.7.1) - activesupport (= 4.2.7.1) + actionview (4.2.9) + activesupport (= 4.2.9) builder (~> 3.1) erubis (~> 2.7.0) rails-dom-testing (~> 1.0, >= 1.0.5) - rails-html-sanitizer (~> 1.0, >= 1.0.2) - activejob (4.2.7.1) - activesupport (= 4.2.7.1) + rails-html-sanitizer (~> 1.0, >= 1.0.3) + activejob (4.2.9) + activesupport (= 4.2.9) globalid (>= 0.3.0) - activemodel (4.2.7.1) - activesupport (= 4.2.7.1) + activemodel (4.2.9) + activesupport (= 4.2.9) builder (~> 3.1) - activerecord (4.2.7.1) - activemodel (= 4.2.7.1) - activesupport (= 4.2.7.1) + activerecord (4.2.9) + activemodel (= 4.2.9) + activesupport (= 4.2.9) arel (~> 6.0) - activesupport (4.2.7.1) + activesupport (4.2.9) i18n (~> 0.7) - json (~> 1.7, >= 1.7.7) minitest (~> 5.1) thread_safe (~> 0.3, >= 0.3.4) tzinfo (~> 1.1) - addressable (2.5.0) + addressable (2.5.1) public_suffix (~> 2.0, >= 2.0.2) - airbrake (5.6.1) - airbrake-ruby (~> 1.6) - airbrake-ruby (1.6.0) - airbrussh (1.1.1) + airbrake (6.2.0) + airbrake-ruby (~> 2.3) + airbrake-ruby (2.3.0) + airbrussh (1.3.0) sshkit (>= 1.6.1, != 1.7.0) - arel (6.0.3) - awesome_print (1.7.0) + arel (6.0.4) + awesome_print (1.8.0) bcrypt (3.1.11) bootstrap_form (2.3.0) - builder (3.2.2) - cancancan (1.15.0) - capistrano (3.7.0) + builder (3.2.3) + cancancan (2.0.0) + capistrano (3.8.2) airbrussh (>= 1.0.0) - capistrano-harrow i18n rake (>= 10.0.0) sshkit (>= 1.9.0) capistrano-bundler (1.2.0) capistrano (~> 3.1) sshkit (~> 1.2) - capistrano-harrow (0.5.3) - capistrano-rails (1.2.0) + capistrano-rails (1.3.0) capistrano (~> 3.1) capistrano-bundler (~> 1.1) - capybara (2.11.0) + capybara (2.14.4) addressable mime-types (>= 1.16) nokogiri (>= 1.3.3) @@ -111,23 +108,36 @@ GEM chronic (0.10.2) chunky_png (1.3.8) cliver (0.3.2) - coffee-rails (4.0.1) + coffee-rails (4.2.2) coffee-script (>= 2.2.0) - railties (>= 4.0.0, < 5.0) + railties (>= 4.0.0) coffee-script (2.4.1) coffee-script-source execjs - coffee-script-source (1.11.1) + coffee-script-source (1.12.2) commonjs (0.2.7) + commonmarker (0.16.5) + ruby-enum (~> 0.5) commontator (4.6.1) jquery-rails rails (>= 3.1) - compass (0.12.7) + compass (1.0.3) chunky_png (~> 1.2) - fssm (>= 0.2.7) - sass (~> 3.2.19) - compass-rails (2.0.0) - compass (>= 0.12.2) + compass-core (~> 1.0.2) + compass-import-once (~> 1.0.5) + rb-fsevent (>= 0.9.3) + rb-inotify (>= 0.9) + sass (>= 3.3.13, < 3.5) + compass-core (1.0.3) + multi_json (~> 1.0) + sass (>= 3.3.0, < 3.5) + compass-import-once (1.0.5) + sass (>= 3.2, < 3.5) + compass-rails (3.0.2) + compass (~> 1.0.0) + sass-rails (< 5.1) + sprockets (< 4.0) + concurrent-ruby (1.0.5) crass (1.0.2) cucumber (2.4.0) builder (>= 2.1.2) @@ -139,73 +149,83 @@ GEM multi_test (>= 0.1.2) cucumber-core (1.5.0) gherkin (~> 4.0) - cucumber-rails (1.4.5) + cucumber-rails (1.5.0) capybara (>= 1.1.2, < 3) cucumber (>= 1.3.8, < 4) - mime-types (>= 1.16, < 4) + mime-types (>= 1.17, < 4) nokogiri (~> 1.5) - railties (>= 3, < 5.1) + railties (>= 4, < 5.2) cucumber-wire (0.0.1) - database_cleaner (1.5.3) - devise (4.2.0) + database_cleaner (1.6.1) + devise (4.3.0) bcrypt (~> 3.0) orm_adapter (~> 0.1) - railties (>= 4.1.0, < 5.1) + railties (>= 4.1.0, < 5.2) responders warden (~> 1.2.3) - diff-lcs (1.2.5) + diff-lcs (1.3) erubis (2.7.0) exception_notification (4.2.1) actionmailer (>= 4.0, < 6) activesupport (>= 4.0, < 6) execjs (2.7.0) - factory_girl (4.7.0) + factory_girl (4.8.0) activesupport (>= 3.0.0) - factory_girl_rails (4.7.0) - factory_girl (~> 4.7.0) + factory_girl_rails (4.8.0) + factory_girl (~> 4.8.0) railties (>= 3.0.0) - faraday (0.9.2) + faraday (0.12.1) multipart-post (>= 1.2, < 3) - fssm (0.2.10) - gherkin (4.0.0) + ffi (1.9.18) + gherkin (4.1.3) github-markdown (0.6.9) - globalid (0.3.7) - activesupport (>= 4.1.0) - haml (4.0.7) + globalid (0.4.0) + activesupport (>= 4.2.0) + haml (5.0.1) + temple (>= 0.8.0) tilt - haml-rails (0.9.0) + haml-rails (1.0.0) actionpack (>= 4.0.1) activesupport (>= 4.0.1) - haml (>= 4.0.6, < 5.0) + haml (>= 4.0.6, < 6.0) html2haml (>= 1.0.1) railties (>= 4.0.1) - hashie (3.4.6) - hike (1.2.3) - html-pipeline (2.4.2) + hashie (3.5.5) + html-pipeline (2.6.0) activesupport (>= 2) nokogiri (>= 1.4) - html2haml (2.0.0) + html2haml (2.2.0) erubis (~> 2.7.0) - haml (~> 4.0.0) - nokogiri (~> 1.6.0) + haml (>= 4.0, < 6) + nokogiri (>= 1.6.0) ruby_parser (~> 3.5) html_pipeline_rails (0.1.0) github-markdown html-pipeline - httparty (0.14.0) + httparty (0.15.5) multi_xml (>= 0.5.2) - i18n (0.7.0) + i18n (0.8.5) jbuilder (1.5.3) activesupport (>= 3.0.0) multi_json (>= 1.2.0) - jquery-rails (3.1.4) - railties (>= 3.0, < 5.0) + jquery-rails (4.3.1) + rails-dom-testing (>= 1, < 3) + railties (>= 4.2.0) thor (>= 0.14, < 2.0) - json (1.8.3) + json (1.8.6) jwt (1.5.6) - kaminari (0.17.0) - actionpack (>= 3.0.0) - activesupport (>= 3.0.0) + kaminari (1.0.1) + activesupport (>= 4.1.0) + kaminari-actionview (= 1.0.1) + kaminari-activerecord (= 1.0.1) + kaminari-core (= 1.0.1) + kaminari-actionview (1.0.1) + actionview + kaminari-core (= 1.0.1) + kaminari-activerecord (1.0.1) + activerecord + kaminari-core (= 1.0.1) + kaminari-core (1.0.1) launchy (2.4.3) addressable (~> 2.3) less (2.6.0) @@ -215,170 +235,176 @@ GEM less (~> 2.6.0) sprockets (> 2, < 4) tilt - libv8 (3.16.14.17) + libv8 (3.16.14.19) loofah (2.0.3) nokogiri (>= 1.5.9) - mail (2.6.4) + mail (2.6.6) mime-types (>= 1.16, < 4) mime-types (3.1) mime-types-data (~> 3.2015) mime-types-data (3.2016.0521) - mini_portile2 (2.1.0) - minitest (5.10.1) + mini_portile2 (2.2.0) + minitest (5.10.2) multi_json (1.12.1) multi_test (0.1.2) multi_xml (0.6.0) multipart-post (2.0.0) - mysql2 (0.4.5) + mysql2 (0.4.7) net-scp (1.2.1) net-ssh (>= 2.6.5) - net-ssh (3.2.0) - nokogiri (1.6.8.1) - mini_portile2 (~> 2.1.0) - nokogumbo (1.4.10) + net-ssh (4.1.0) + nokogiri (1.8.0) + mini_portile2 (~> 2.2.0) + nokogumbo (1.4.13) nokogiri - oauth2 (1.2.0) - faraday (>= 0.8, < 0.10) + oauth2 (1.4.0) + faraday (>= 0.8, < 0.13) jwt (~> 1.0) multi_json (~> 1.3) multi_xml (~> 0.5) rack (>= 1.2, < 3) - octokit (4.6.2) + octokit (4.7.0) sawyer (~> 0.8.0, >= 0.5.3) - omniauth (1.3.1) - hashie (>= 1.2, < 4) - rack (>= 1.0, < 3) + omniauth (1.6.1) + hashie (>= 3.4.6, < 3.6.0) + rack (>= 1.6.2, < 3) omniauth-oauth2 (1.4.0) oauth2 (~> 1.0) omniauth (~> 1.2) orm_adapter (0.5.0) - pg (0.19.0) - poltergeist (1.12.0) + pg (0.21.0) + poltergeist (1.15.0) capybara (~> 2.1) cliver (~> 0.3.1) websocket-driver (>= 0.2.0) - public_suffix (2.0.4) + public_suffix (2.0.5) quiet_assets (1.1.0) railties (>= 3.1, < 5.0) - rack (1.6.5) - rack-canonical-host (0.2.2) + rack (1.6.8) + rack-canonical-host (0.2.3) addressable (> 0, < 3) rack (>= 1.0.0, < 3) rack-test (0.6.3) rack (>= 1.0) - rails (4.2.7.1) - actionmailer (= 4.2.7.1) - actionpack (= 4.2.7.1) - actionview (= 4.2.7.1) - activejob (= 4.2.7.1) - activemodel (= 4.2.7.1) - activerecord (= 4.2.7.1) - activesupport (= 4.2.7.1) + rails (4.2.9) + actionmailer (= 4.2.9) + actionpack (= 4.2.9) + actionview (= 4.2.9) + activejob (= 4.2.9) + activemodel (= 4.2.9) + activerecord (= 4.2.9) + activesupport (= 4.2.9) bundler (>= 1.3.0, < 2.0) - railties (= 4.2.7.1) + railties (= 4.2.9) sprockets-rails rails-deprecated_sanitizer (1.0.3) activesupport (>= 4.2.0.alpha) - rails-dom-testing (1.0.7) + rails-dom-testing (1.0.8) activesupport (>= 4.2.0.beta, < 5.0) - nokogiri (~> 1.6.0) + nokogiri (~> 1.6) rails-deprecated_sanitizer (>= 1.0.1) rails-html-sanitizer (1.0.3) loofah (~> 2.0) rails_autolink (1.1.6) rails (> 3.1) - railties (4.2.7.1) - actionpack (= 4.2.7.1) - activesupport (= 4.2.7.1) + railties (4.2.9) + actionpack (= 4.2.9) + activesupport (= 4.2.9) rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) rake (12.0.0) + rb-fsevent (0.10.2) + rb-inotify (0.9.10) + ffi (>= 0.5.0, < 2) rdoc (4.3.0) - redcarpet (3.3.4) + redcarpet (3.4.0) ref (2.0.0) - responders (2.3.0) - railties (>= 4.2.0, < 5.1) + responders (2.4.0) + actionpack (>= 4.2.0, < 5.3) + railties (>= 4.2.0, < 5.3) rqrcode (0.10.1) chunky_png (~> 1.0) rqrcode-rails3 (0.1.7) rqrcode (>= 0.4.2) - rspec-core (3.5.4) - rspec-support (~> 3.5.0) - rspec-expectations (3.5.0) + rspec-core (3.6.0) + rspec-support (~> 3.6.0) + rspec-expectations (3.6.0) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.5.0) - rspec-mocks (3.5.0) + rspec-support (~> 3.6.0) + rspec-mocks (3.6.0) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.5.0) - rspec-rails (3.5.2) + rspec-support (~> 3.6.0) + rspec-rails (3.6.0) actionpack (>= 3.0) activesupport (>= 3.0) railties (>= 3.0) - rspec-core (~> 3.5.0) - rspec-expectations (~> 3.5.0) - rspec-mocks (~> 3.5.0) - rspec-support (~> 3.5.0) - rspec-support (3.5.0) - ruby_parser (3.8.3) + rspec-core (~> 3.6.0) + rspec-expectations (~> 3.6.0) + rspec-mocks (~> 3.6.0) + rspec-support (~> 3.6.0) + rspec-support (3.6.0) + ruby-enum (0.7.1) + i18n + ruby_parser (3.9.0) sexp_processor (~> 4.1) - sanitize (4.4.0) + sanitize (4.5.0) crass (~> 1.0.2) nokogiri (>= 1.4.4) nokogumbo (~> 1.4.1) - sass (3.2.19) - sass-rails (4.0.5) - railties (>= 4.0.0, < 5.0) - sass (~> 3.2.2) - sprockets (~> 2.8, < 3.0) - sprockets-rails (~> 2.0) + sass (3.4.25) + sass-rails (5.0.6) + railties (>= 4.0.0, < 6) + sass (~> 3.1) + sprockets (>= 2.8, < 4.0) + sprockets-rails (>= 2.0, < 4.0) + tilt (>= 1.1, < 3) sawyer (0.8.1) addressable (>= 2.3.5, < 2.6) faraday (~> 0.8, < 1.0) sdoc (0.4.2) json (~> 1.7, >= 1.7.7) rdoc (~> 4.0) - sexp_processor (4.7.0) - sprockets (2.12.4) - hike (~> 1.2) - multi_json (~> 1.0) - rack (~> 1.0) - tilt (~> 1.1, != 1.3.0) - sprockets-rails (2.3.3) - actionpack (>= 3.0) - activesupport (>= 3.0) - sprockets (>= 2.8, < 4.0) - sqlite3 (1.3.12) - sshkit (1.11.4) + sexp_processor (4.9.0) + sprockets (3.7.1) + concurrent-ruby (~> 1.0) + rack (> 1, < 3) + sprockets-rails (3.2.0) + actionpack (>= 4.0) + activesupport (>= 4.0) + sprockets (>= 3.0.0) + sqlite3 (1.3.13) + sshkit (1.14.0) net-scp (>= 1.1.2) net-ssh (>= 2.8.0) + temple (0.8.0) test_after_commit (1.1.0) activerecord (>= 3.2) - therubyracer (0.12.2) - libv8 (~> 3.16.14.0) + therubyracer (0.12.3) + libv8 (~> 3.16.14.15) ref thor (0.19.4) - thread_safe (0.3.5) - tilt (1.4.1) - timecop (0.8.1) + thread_safe (0.3.6) + tilt (2.0.7) + timecop (0.9.1) turbolinks (5.0.1) turbolinks-source (~> 5) - turbolinks-source (5.0.0) + turbolinks-source (5.0.3) twitter-typeahead-rails (0.11.1) actionpack (>= 3.1) jquery-rails railties (>= 3.1) - tzinfo (1.2.2) + tzinfo (1.2.3) thread_safe (~> 0.1) - uglifier (3.0.4) + uglifier (3.2.0) execjs (>= 0.3.0, < 3) - warden (1.2.6) + warden (1.2.7) rack (>= 1.0) - websocket-driver (0.6.4) + websocket-driver (0.6.5) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.2) whenever (0.9.7) chronic (>= 0.6.3) - xpath (2.0.0) + xpath (2.1.0) nokogiri (~> 1.3) PLATFORMS @@ -395,6 +421,7 @@ DEPENDENCIES capistrano-rvm! capybara-screenshot coffee-rails + commonmarker commontator (~> 4.6.0) compass-rails cucumber-rails @@ -437,4 +464,4 @@ DEPENDENCIES whenever BUNDLED WITH - 1.13.6 + 1.15.1 From f589764597c1ce19e4c83e0a7341fc762e858720 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 9 Jul 2017 12:23:55 +0200 Subject: [PATCH 359/372] Fix warnings --- app/assets/stylesheets/home.css.sass | 2 +- .../stylesheets/{oldsansblack.css.sass => oldsansblack.sass} | 0 app/assets/stylesheets/users.css.sass | 2 -- 3 files changed, 1 insertion(+), 3 deletions(-) rename app/assets/stylesheets/{oldsansblack.css.sass => oldsansblack.sass} (100%) diff --git a/app/assets/stylesheets/home.css.sass b/app/assets/stylesheets/home.css.sass index f28b7134..6c68e889 100644 --- a/app/assets/stylesheets/home.css.sass +++ b/app/assets/stylesheets/home.css.sass @@ -3,7 +3,7 @@ // You can use Sass (SCSS) here: http://sass-lang.com/ @import "compass/css3" -@import "compass/utilities/text/replacement" +@import "compass/typography/text/replacement" @import "oldsansblack" @import "same-height-columns" diff --git a/app/assets/stylesheets/oldsansblack.css.sass b/app/assets/stylesheets/oldsansblack.sass similarity index 100% rename from app/assets/stylesheets/oldsansblack.css.sass rename to app/assets/stylesheets/oldsansblack.sass diff --git a/app/assets/stylesheets/users.css.sass b/app/assets/stylesheets/users.css.sass index 6d70b9d5..1d6284eb 100644 --- a/app/assets/stylesheets/users.css.sass +++ b/app/assets/stylesheets/users.css.sass @@ -2,8 +2,6 @@ // They will automatically be included in application.css. // You can use Sass (SCSS) here: http://sass-lang.com/ -.send-tips-back-block - #error_explanation h2 font-size: 16px From 063ccc0923d17d76a51187b26b2dcffbcb65285b Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 9 Jul 2017 15:34:09 +0200 Subject: [PATCH 360/372] Remove deprecation warning --- Gemfile | 4 +++- Gemfile.lock | 20 ++++++++++++++------ 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/Gemfile b/Gemfile index 9c13300b..e7ee3f0d 100644 --- a/Gemfile +++ b/Gemfile @@ -11,7 +11,9 @@ gem 'pg', group: :postgresql # Use SCSS for stylesheets gem 'sass-rails' gem 'haml-rails' -gem "less-rails" + +# use fork to remove warning, see https://github.com/metaskills/less-rails/issues/122 and https://github.com/metaskills/less-rails/pull/137 +gem "less-rails", git: 'https://github.com/brendon/less-rails.git', branch: 'fix-sprockets-loading' gem 'twitter-bootstrap-rails', git: 'https://github.com/seyhunak/twitter-bootstrap-rails.git', branch: 'bootstrap3' diff --git a/Gemfile.lock b/Gemfile.lock index 33910731..45e335af 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -7,6 +7,18 @@ GIT omniauth (~> 1.0) omniauth-oauth2 (~> 1.1) +GIT + remote: https://github.com/brendon/less-rails.git + revision: 704326e74174fd11615e2488d3f2286dc0499603 + branch: fix-sprockets-loading + specs: + less-rails (2.8.0) + actionpack (>= 4.0) + grease + less (~> 2.6.0) + sprockets (> 2, < 4) + tilt + GIT remote: https://github.com/capistrano/rvm.git revision: 9cfef39cf0022839dca6b5b330dfefeb5fc363e7 @@ -181,6 +193,7 @@ GEM github-markdown (0.6.9) globalid (0.4.0) activesupport (>= 4.2.0) + grease (0.3.1) haml (5.0.1) temple (>= 0.8.0) tilt @@ -230,11 +243,6 @@ GEM addressable (~> 2.3) less (2.6.0) commonjs (~> 0.2.7) - less-rails (2.8.0) - actionpack (>= 4.0) - less (~> 2.6.0) - sprockets (> 2, < 4) - tilt libv8 (3.16.14.19) loofah (2.0.3) nokogiri (>= 1.5.9) @@ -435,7 +443,7 @@ DEPENDENCIES jbuilder (~> 1.2) jquery-rails kaminari - less-rails + less-rails! mysql2 octokit omniauth From 40b3df20a5885aabe40abba1d84800462d1d9130 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 9 Jul 2017 16:39:30 +0200 Subject: [PATCH 361/372] Remove pg deprecation warning --- Gemfile | 2 +- Gemfile.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index e7ee3f0d..e188a19a 100644 --- a/Gemfile +++ b/Gemfile @@ -6,7 +6,7 @@ gem 'rails', '~> 4.2.0' # Databases gem 'sqlite3', group: :development gem 'mysql2', group: :mysql -gem 'pg', group: :postgresql +gem 'pg', '~> 0.20.0', group: :postgresql # fixed version to avoid warning, see https://stackoverflow.com/questions/44607324/installing-newest-version-of-rails-4-with-postgres-the-pgconn-pgresult-and-p # Use SCSS for stylesheets gem 'sass-rails' diff --git a/Gemfile.lock b/Gemfile.lock index 45e335af..18669e54 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -280,7 +280,7 @@ GEM oauth2 (~> 1.0) omniauth (~> 1.2) orm_adapter (0.5.0) - pg (0.21.0) + pg (0.20.0) poltergeist (1.15.0) capybara (~> 2.1) cliver (~> 0.3.1) @@ -448,7 +448,7 @@ DEPENDENCIES octokit omniauth omniauth-github! - pg + pg (~> 0.20.0) poltergeist quiet_assets rack-canonical-host From f293af91cc287b1a02d3057c7a55abc043c93294 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 21 Jan 2018 20:19:45 +0100 Subject: [PATCH 362/372] Fix invalid transactions with peercoin 0.6 --- app/views/home/audit.html.haml | 4 +- db/schema.rb | 203 +++++++++++++++++---------------- lib/balance_updater.rb | 21 +++- 3 files changed, 123 insertions(+), 105 deletions(-) diff --git a/app/views/home/audit.html.haml b/app/views/home/audit.html.haml index 9496b43c..8a6254ac 100644 --- a/app/views/home/audit.html.haml +++ b/app/views/home/audit.html.haml @@ -49,7 +49,7 @@ %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) + %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 @@ -62,7 +62,7 @@ %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) + %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 diff --git a/db/schema.rb b/db/schema.rb index 9e9da2e7..f47ff206 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,91 +11,94 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20140714074128) do +ActiveRecord::Schema.define(version: 20180121184454) do - create_table "cold_storage_transfers", 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.integer "amount", limit: 8 - t.string "address" - t.string "txid" + t.string "address", limit: 255 + t.string "txid", limit: 255 t.integer "confirmations" t.datetime "created_at" t.datetime "updated_at" t.integer "fee" end - add_index "cold_storage_transfers", ["project_id"], name: "index_cold_storage_transfers_on_project_id" + add_index "cold_storage_transfers", ["project_id"], name: "index_cold_storage_transfers_on_project_id", using: :btree - create_table "collaborators", force: true do |t| + create_table "collaborators", force: :cascade do |t| t.integer "project_id" - t.string "login" + 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" - add_index "collaborators", ["user_id"], name: "index_collaborators_on_user_id" + 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: true do |t| + create_table "commits", force: :cascade do |t| t.integer "project_id" - t.string "sha" + t.string "sha", limit: 255 t.text "message" - t.string "username" - t.string "email" + 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" + add_index "commits", ["project_id"], name: "index_commits_on_project_id", using: :btree - create_table "commontator_comments", force: true do |t| - t.string "creator_type" + create_table "commontator_comments", force: :cascade do |t| + t.string "creator_type", limit: 255 t.integer "creator_id" - t.string "editor_type" + t.string "editor_type", limit: 255 t.integer "editor_id" - t.integer "thread_id", null: false - t.text "body", null: false + 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.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" - add_index "commontator_comments", ["cached_votes_up"], name: "index_commontator_comments_on_cached_votes_up" - 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"], name: "index_commontator_comments_on_thread_id" + 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: true do |t| - t.string "subscriber_type", null: false - t.integer "subscriber_id", null: false - t.integer "thread_id", null: false + 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 - add_index "commontator_subscriptions", ["thread_id"], name: "index_commontator_subscriptions_on_thread_id" + 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: true do |t| - t.string "commontable_type" + 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" + t.string "closer_type", limit: 255 t.integer "closer_id" t.datetime "created_at" t.datetime "updated_at" end - add_index "commontator_threads", ["commontable_id", "commontable_type"], name: "index_commontator_threads_on_c_id_and_c_type", unique: true + 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: true do |t| + create_table "deposits", force: :cascade do |t| t.integer "project_id" - t.string "txid" + t.string "txid", limit: 255 t.integer "confirmations" - t.integer "duration", default: 2592000 + t.integer "duration", default: 2592000 t.integer "paid_out", limit: 8 t.datetime "paid_out_at" t.datetime "created_at" @@ -104,13 +107,13 @@ t.integer "donation_address_id" end - add_index "deposits", ["donation_address_id"], name: "index_deposits_on_donation_address_id" - add_index "deposits", ["project_id"], name: "index_deposits_on_project_id" + 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: true do |t| - t.string "txid" + 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" @@ -119,52 +122,53 @@ t.datetime "sent_at" end - add_index "distributions", ["project_id"], name: "index_distributions_on_project_id" + add_index "distributions", ["project_id"], name: "index_distributions_on_project_id", using: :btree - create_table "donation_addresses", force: true do |t| + create_table "donation_addresses", force: :cascade do |t| t.integer "project_id" - t.string "sender_address" - t.string "donation_address" + t.string "sender_address", limit: 255 + t.string "donation_address", limit: 255 t.datetime "created_at" t.datetime "updated_at" end - add_index "donation_addresses", ["project_id"], name: "index_donation_addresses_on_project_id" + add_index "donation_addresses", ["project_id"], name: "index_donation_addresses_on_project_id", using: :btree - create_table "projects", force: true do |t| - t.string "url" - t.string "bitcoin_address" + 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" - t.string "full_name" - t.string "source_full_name" + 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" - t.string "last_commit" + t.string "language", limit: 255 + t.string "last_commit", limit: 255 t.integer "available_amount_cache", limit: 8 - t.string "address_label" - t.boolean "hold_tips", default: true - t.string "cold_storage_withdrawal_address" - t.boolean "disabled", default: false + 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" + t.string "disabled_reason", limit: 255 t.text "detailed_description" + t.integer "stake_mint_amount", limit: 8 end - create_table "record_changes", force: true do |t| + create_table "record_changes", force: :cascade do |t| t.integer "record_id" - t.string "record_type" + t.string "record_type", limit: 255 t.integer "user_id" - t.text "raw_state", limit: 1048576 + 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" + 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: true do |t| + create_table "tipping_policies_texts", force: :cascade do |t| t.integer "project_id" t.integer "user_id" t.text "text" @@ -172,61 +176,62 @@ t.datetime "updated_at" end - add_index "tipping_policies_texts", ["project_id"], name: "index_tipping_policies_texts_on_project_id" - add_index "tipping_policies_texts", ["user_id"], name: "index_tipping_policies_texts_on_user_id" + 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: true do |t| + create_table "tips", force: :cascade do |t| + t.integer "user_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" - t.string "comment" + t.string "commit_message", limit: 255 + t.string "comment", limit: 255 t.integer "reason_id" - t.string "reason_type" - t.integer "user_id" + t.string "reason_type", limit: 255 end - add_index "tips", ["distribution_id"], name: "index_tips_on_distribution_id" - add_index "tips", ["project_id"], name: "index_tips_on_project_id" - add_index "tips", ["reason_id", "reason_type"], name: "index_tips_on_reason_id_and_reason_type" - 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 "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 "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.string "confirmation_token" + 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" - t.string "email" - t.string "nickname" - t.boolean "disabled", default: false - t.string "identifier", null: false + t.string "unconfirmed_email", limit: 255 + t.boolean "disabled", default: false + t.string "identifier", limit: 255, null: false end - add_index "users", ["disabled"], name: "index_users_on_disabled" - add_index "users", ["identifier"], name: "index_users_on_identifier", 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/lib/balance_updater.rb b/lib/balance_updater.rb index 31d8ecaf..c51c1d99 100644 --- a/lib/balance_updater.rb +++ b/lib/balance_updater.rb @@ -1,6 +1,6 @@ module BalanceUpdater - def self.work - Project.all.each do |project| + def self.work(projects = Project.all) + projects.each do |project| start = 0 count = 10 @@ -8,13 +8,13 @@ def self.work project.update(account_balance: (BitcoinDaemon.instance.get_balance(project.address_label) * COIN).to_i) - next if project.disabled? - 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? @@ -57,6 +57,17 @@ def self.work 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}" @@ -110,6 +121,8 @@ def self.work raise "Unexpected transaction: #{transaction.inspect}" end + project.update(stake_mint_amount: stake_mint) + break if transactions.size < count start += count end From e210955dce90d9ae0e6a649e01651fd505e2dab5 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 21 Jan 2018 20:51:03 +0100 Subject: [PATCH 363/372] Add missing migration --- db/migrate/20180121184454_add_stake_mint_to_project.rb | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 db/migrate/20180121184454_add_stake_mint_to_project.rb 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 From a3fef3d6fddbf0532d19522b1c1a9eb93284e75b Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Thu, 22 Mar 2018 07:50:11 +0100 Subject: [PATCH 364/372] Update vulnerable gems --- Gemfile.lock | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 18669e54..ec04f688 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -150,7 +150,7 @@ GEM sass-rails (< 5.1) sprockets (< 4.0) concurrent-ruby (1.0.5) - crass (1.0.2) + crass (1.0.3) cucumber (2.4.0) builder (>= 2.1.2) cucumber-core (~> 1.5.0) @@ -244,14 +244,15 @@ GEM less (2.6.0) commonjs (~> 0.2.7) libv8 (3.16.14.19) - loofah (2.0.3) + loofah (2.2.1) + crass (~> 1.0.2) nokogiri (>= 1.5.9) mail (2.6.6) mime-types (>= 1.16, < 4) mime-types (3.1) mime-types-data (~> 3.2015) mime-types-data (3.2016.0521) - mini_portile2 (2.2.0) + mini_portile2 (2.3.0) minitest (5.10.2) multi_json (1.12.1) multi_test (0.1.2) @@ -261,9 +262,9 @@ GEM net-scp (1.2.1) net-ssh (>= 2.6.5) net-ssh (4.1.0) - nokogiri (1.8.0) - mini_portile2 (~> 2.2.0) - nokogumbo (1.4.13) + nokogiri (1.8.2) + mini_portile2 (~> 2.3.0) + nokogumbo (1.5.0) nokogiri oauth2 (1.4.0) faraday (>= 0.8, < 0.13) @@ -355,10 +356,10 @@ GEM i18n ruby_parser (3.9.0) sexp_processor (~> 4.1) - sanitize (4.5.0) + sanitize (4.6.4) crass (~> 1.0.2) nokogiri (>= 1.4.4) - nokogumbo (~> 1.4.1) + nokogumbo (~> 1.4) sass (3.4.25) sass-rails (5.0.6) railties (>= 4.0.0, < 6) From 9415265029e96d19d46e1d5a24bb700d85409dc6 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Fri, 16 Nov 2018 08:41:53 +0100 Subject: [PATCH 365/372] Update rack --- Gemfile | 1 + Gemfile.lock | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index e188a19a..2a68e09a 100644 --- a/Gemfile +++ b/Gemfile @@ -95,3 +95,4 @@ group :test do end gem 'awesome_print', group: [:development, :test] gem 'commonmarker' +gem 'rack', '~> 1.6.11' diff --git a/Gemfile.lock b/Gemfile.lock index ec04f688..b2e010ae 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -289,7 +289,7 @@ GEM public_suffix (2.0.5) quiet_assets (1.1.0) railties (>= 3.1, < 5.0) - rack (1.6.8) + rack (1.6.11) rack-canonical-host (0.2.3) addressable (> 0, < 3) rack (>= 1.0.0, < 3) @@ -452,6 +452,7 @@ DEPENDENCIES pg (~> 0.20.0) poltergeist quiet_assets + rack (~> 1.6.11) rack-canonical-host rails (~> 4.2.0) rails_autolink From 7e29cb75cc417b030c2e07ffff0dbf12933d1ae5 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Fri, 16 Nov 2018 08:46:46 +0100 Subject: [PATCH 366/372] Update some gems --- Gemfile | 4 ++++ Gemfile.lock | 20 ++++++++++++-------- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/Gemfile b/Gemfile index 2a68e09a..89100270 100644 --- a/Gemfile +++ b/Gemfile @@ -96,3 +96,7 @@ end gem 'awesome_print', group: [:development, :test] gem 'commonmarker' gem 'rack', '~> 1.6.11' +gem "sprockets", ">= 3.7.2" +gem "ffi", ">= 1.9.24" +gem "loofah", ">= 2.2.3" +gem "rails-html-sanitizer", ">= 1.0.4" diff --git a/Gemfile.lock b/Gemfile.lock index b2e010ae..45062f63 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -149,8 +149,8 @@ GEM compass (~> 1.0.0) sass-rails (< 5.1) sprockets (< 4.0) - concurrent-ruby (1.0.5) - crass (1.0.3) + concurrent-ruby (1.1.3) + crass (1.0.4) cucumber (2.4.0) builder (>= 2.1.2) cucumber-core (~> 1.5.0) @@ -188,7 +188,7 @@ GEM railties (>= 3.0.0) faraday (0.12.1) multipart-post (>= 1.2, < 3) - ffi (1.9.18) + ffi (1.9.25) gherkin (4.1.3) github-markdown (0.6.9) globalid (0.4.0) @@ -244,7 +244,7 @@ GEM less (2.6.0) commonjs (~> 0.2.7) libv8 (3.16.14.19) - loofah (2.2.1) + loofah (2.2.3) crass (~> 1.0.2) nokogiri (>= 1.5.9) mail (2.6.6) @@ -262,7 +262,7 @@ GEM net-scp (1.2.1) net-ssh (>= 2.6.5) net-ssh (4.1.0) - nokogiri (1.8.2) + nokogiri (1.8.5) mini_portile2 (~> 2.3.0) nokogumbo (1.5.0) nokogiri @@ -312,8 +312,8 @@ GEM activesupport (>= 4.2.0.beta, < 5.0) nokogiri (~> 1.6) rails-deprecated_sanitizer (>= 1.0.1) - rails-html-sanitizer (1.0.3) - loofah (~> 2.0) + rails-html-sanitizer (1.0.4) + loofah (~> 2.2, >= 2.2.2) rails_autolink (1.1.6) rails (> 3.1) railties (4.2.9) @@ -374,7 +374,7 @@ GEM json (~> 1.7, >= 1.7.7) rdoc (~> 4.0) sexp_processor (4.9.0) - sprockets (3.7.1) + sprockets (3.7.2) concurrent-ruby (~> 1.0) rack (> 1, < 3) sprockets-rails (3.2.0) @@ -438,6 +438,7 @@ DEPENDENCIES devise exception_notification factory_girl_rails + ffi (>= 1.9.24) haml-rails html_pipeline_rails httparty @@ -445,6 +446,7 @@ DEPENDENCIES jquery-rails kaminari less-rails! + loofah (>= 2.2.3) mysql2 octokit omniauth @@ -455,6 +457,7 @@ DEPENDENCIES rack (~> 1.6.11) rack-canonical-host rails (~> 4.2.0) + rails-html-sanitizer (>= 1.0.4) rails_autolink redcarpet rqrcode-rails3 @@ -462,6 +465,7 @@ DEPENDENCIES sanitize sass-rails sdoc + sprockets (>= 3.7.2) sqlite3 test_after_commit therubyracer From 6ba61dd673d3890da9bcf0531f7693884d0386b1 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Thu, 6 Dec 2018 09:46:34 +0100 Subject: [PATCH 367/372] Update rails --- Gemfile.lock | 86 +++++++++++++++++++++++++++------------------------- 1 file changed, 44 insertions(+), 42 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 45062f63..ba62d26b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -49,36 +49,36 @@ GIT GEM remote: https://rubygems.org/ specs: - actionmailer (4.2.9) - actionpack (= 4.2.9) - actionview (= 4.2.9) - activejob (= 4.2.9) + actionmailer (4.2.11) + actionpack (= 4.2.11) + actionview (= 4.2.11) + activejob (= 4.2.11) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 1.0, >= 1.0.5) - actionpack (4.2.9) - actionview (= 4.2.9) - activesupport (= 4.2.9) + actionpack (4.2.11) + actionview (= 4.2.11) + activesupport (= 4.2.11) rack (~> 1.6) rack-test (~> 0.6.2) rails-dom-testing (~> 1.0, >= 1.0.5) rails-html-sanitizer (~> 1.0, >= 1.0.2) - actionview (4.2.9) - activesupport (= 4.2.9) + actionview (4.2.11) + activesupport (= 4.2.11) builder (~> 3.1) erubis (~> 2.7.0) rails-dom-testing (~> 1.0, >= 1.0.5) rails-html-sanitizer (~> 1.0, >= 1.0.3) - activejob (4.2.9) - activesupport (= 4.2.9) + activejob (4.2.11) + activesupport (= 4.2.11) globalid (>= 0.3.0) - activemodel (4.2.9) - activesupport (= 4.2.9) + activemodel (4.2.11) + activesupport (= 4.2.11) builder (~> 3.1) - activerecord (4.2.9) - activemodel (= 4.2.9) - activesupport (= 4.2.9) + activerecord (4.2.11) + activemodel (= 4.2.11) + activesupport (= 4.2.11) arel (~> 6.0) - activesupport (4.2.9) + activesupport (4.2.11) i18n (~> 0.7) minitest (~> 5.1) thread_safe (~> 0.3, >= 0.3.4) @@ -191,7 +191,7 @@ GEM ffi (1.9.25) gherkin (4.1.3) github-markdown (0.6.9) - globalid (0.4.0) + globalid (0.4.1) activesupport (>= 4.2.0) grease (0.3.1) haml (5.0.1) @@ -217,7 +217,8 @@ GEM html-pipeline httparty (0.15.5) multi_xml (>= 0.5.2) - i18n (0.8.5) + i18n (0.9.5) + concurrent-ruby (~> 1.0) jbuilder (1.5.3) activesupport (>= 3.0.0) multi_json (>= 1.2.0) @@ -247,13 +248,14 @@ GEM loofah (2.2.3) crass (~> 1.0.2) nokogiri (>= 1.5.9) - mail (2.6.6) - mime-types (>= 1.16, < 4) - mime-types (3.1) + mail (2.7.1) + mini_mime (>= 0.1.1) + mime-types (3.2.2) mime-types-data (~> 3.2015) - mime-types-data (3.2016.0521) + mime-types-data (3.2018.0812) + mini_mime (1.0.1) mini_portile2 (2.3.0) - minitest (5.10.2) + minitest (5.11.3) multi_json (1.12.1) multi_test (0.1.2) multi_xml (0.6.0) @@ -295,33 +297,33 @@ GEM rack (>= 1.0.0, < 3) rack-test (0.6.3) rack (>= 1.0) - rails (4.2.9) - actionmailer (= 4.2.9) - actionpack (= 4.2.9) - actionview (= 4.2.9) - activejob (= 4.2.9) - activemodel (= 4.2.9) - activerecord (= 4.2.9) - activesupport (= 4.2.9) + rails (4.2.11) + actionmailer (= 4.2.11) + actionpack (= 4.2.11) + actionview (= 4.2.11) + activejob (= 4.2.11) + activemodel (= 4.2.11) + activerecord (= 4.2.11) + activesupport (= 4.2.11) bundler (>= 1.3.0, < 2.0) - railties (= 4.2.9) + railties (= 4.2.11) sprockets-rails rails-deprecated_sanitizer (1.0.3) activesupport (>= 4.2.0.alpha) - rails-dom-testing (1.0.8) - activesupport (>= 4.2.0.beta, < 5.0) + rails-dom-testing (1.0.9) + activesupport (>= 4.2.0, < 5.0) nokogiri (~> 1.6) rails-deprecated_sanitizer (>= 1.0.1) rails-html-sanitizer (1.0.4) loofah (~> 2.2, >= 2.2.2) rails_autolink (1.1.6) rails (> 3.1) - railties (4.2.9) - actionpack (= 4.2.9) - activesupport (= 4.2.9) + railties (4.2.11) + actionpack (= 4.2.11) + activesupport (= 4.2.11) rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) - rake (12.0.0) + rake (12.3.1) rb-fsevent (0.10.2) rb-inotify (0.9.10) ffi (>= 0.5.0, < 2) @@ -377,7 +379,7 @@ GEM sprockets (3.7.2) concurrent-ruby (~> 1.0) rack (> 1, < 3) - sprockets-rails (3.2.0) + sprockets-rails (3.2.1) actionpack (>= 4.0) activesupport (>= 4.0) sprockets (>= 3.0.0) @@ -391,7 +393,7 @@ GEM therubyracer (0.12.3) libv8 (~> 3.16.14.15) ref - thor (0.19.4) + thor (0.20.3) thread_safe (0.3.6) tilt (2.0.7) timecop (0.9.1) @@ -402,7 +404,7 @@ GEM actionpack (>= 3.1) jquery-rails railties (>= 3.1) - tzinfo (1.2.3) + tzinfo (1.2.5) thread_safe (~> 0.1) uglifier (3.2.0) execjs (>= 0.3.0, < 3) From 2481b6954c34a6f6dee986e66899f336ebf11778 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Wed, 20 Mar 2019 08:01:55 +0100 Subject: [PATCH 368/372] Update gems --- Gemfile.lock | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index ba62d26b..fc139968 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -92,7 +92,7 @@ GEM sshkit (>= 1.6.1, != 1.7.0) arel (6.0.4) awesome_print (1.8.0) - bcrypt (3.1.11) + bcrypt (3.1.12) bootstrap_form (2.3.0) builder (3.2.3) cancancan (2.0.0) @@ -149,7 +149,7 @@ GEM compass (~> 1.0.0) sass-rails (< 5.1) sprockets (< 4.0) - concurrent-ruby (1.1.3) + concurrent-ruby (1.1.5) crass (1.0.4) cucumber (2.4.0) builder (>= 2.1.2) @@ -169,10 +169,10 @@ GEM railties (>= 4, < 5.2) cucumber-wire (0.0.1) database_cleaner (1.6.1) - devise (4.3.0) + devise (4.6.1) bcrypt (~> 3.0) orm_adapter (~> 0.1) - railties (>= 4.1.0, < 5.2) + railties (>= 4.1.0, < 6.0) responders warden (~> 1.2.3) diff-lcs (1.3) @@ -254,7 +254,7 @@ GEM mime-types-data (~> 3.2015) mime-types-data (3.2018.0812) mini_mime (1.0.1) - mini_portile2 (2.3.0) + mini_portile2 (2.4.0) minitest (5.11.3) multi_json (1.12.1) multi_test (0.1.2) @@ -264,8 +264,8 @@ GEM net-scp (1.2.1) net-ssh (>= 2.6.5) net-ssh (4.1.0) - nokogiri (1.8.5) - mini_portile2 (~> 2.3.0) + nokogiri (1.10.1) + mini_portile2 (~> 2.4.0) nokogumbo (1.5.0) nokogiri oauth2 (1.4.0) @@ -323,16 +323,16 @@ GEM activesupport (= 4.2.11) rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) - rake (12.3.1) + rake (12.3.2) rb-fsevent (0.10.2) rb-inotify (0.9.10) ffi (>= 0.5.0, < 2) rdoc (4.3.0) redcarpet (3.4.0) ref (2.0.0) - responders (2.4.0) - actionpack (>= 4.2.0, < 5.3) - railties (>= 4.2.0, < 5.3) + responders (2.4.1) + actionpack (>= 4.2.0, < 6.0) + railties (>= 4.2.0, < 6.0) rqrcode (0.10.1) chunky_png (~> 1.0) rqrcode-rails3 (0.1.7) From 2e21e775532efa8a4d203adafa7794eb2abe3028 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sat, 24 Aug 2019 12:01:08 +0200 Subject: [PATCH 369/372] Fix CVE-2015-9284 https://github.com/omniauth/omniauth/wiki/Resolving-CVE-2015-9284 --- Gemfile | 1 + Gemfile.lock | 4 ++++ app/views/devise/sessions/new.html.haml | 2 +- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index 89100270..9a0ea0d7 100644 --- a/Gemfile +++ b/Gemfile @@ -45,6 +45,7 @@ end gem 'devise' gem 'test_after_commit', :group => :test # https://github.com/plataformatec/devise/blob/master/CHANGELOG.md#410 gem 'omniauth' +gem 'omniauth-rails_csrf_protection', '~> 0.1' gem 'omniauth-github', git: 'https://github.com/alexandrz/omniauth-github.git', branch: 'provide_emails' gem 'cancancan' gem 'twitter_bootstrap_form_for', git: 'https://github.com/stouset/twitter_bootstrap_form_for.git' diff --git a/Gemfile.lock b/Gemfile.lock index fc139968..c9d9692a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -282,6 +282,9 @@ GEM omniauth-oauth2 (1.4.0) oauth2 (~> 1.0) omniauth (~> 1.2) + omniauth-rails_csrf_protection (0.1.2) + actionpack (>= 4.2) + omniauth (>= 1.3.1) orm_adapter (0.5.0) pg (0.20.0) poltergeist (1.15.0) @@ -453,6 +456,7 @@ DEPENDENCIES octokit omniauth omniauth-github! + omniauth-rails_csrf_protection (~> 0.1) pg (~> 0.20.0) poltergeist quiet_assets diff --git a/app/views/devise/sessions/new.html.haml b/app/views/devise/sessions/new.html.haml index c79ff2da..a38ec0bd 100644 --- a/app/views/devise/sessions/new.html.haml +++ b/app/views/devise/sessions/new.html.haml @@ -14,7 +14,7 @@ - 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" + = 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" From ac83342f9b549271ac93ff8715a9c4b4c497da94 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sat, 24 Aug 2019 12:06:36 +0200 Subject: [PATCH 370/372] Remove unused code --- app/views/home/index.html.haml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/app/views/home/index.html.haml b/app/views/home/index.html.haml index 7fe27227..1455191f 100644 --- a/app/views/home/index.html.haml +++ b/app/views/home/index.html.haml @@ -4,7 +4,7 @@ .jumbotron .container %h1 Make anything happen - %p.lead + %p.lead %a.btn.btn-lg.btn-success{href: projects_path} See projects @@ -39,7 +39,3 @@ Fundraisers collect funds and distribute them to contributors. .button-container %a.btn.btn-primary.btn-block{href: faq_path} FAQ - / - if current_user - / %a.btn.btn-primary{href: user_path(current_user)} Change your peercoin address » - / - else - / %a.btn.btn-primary{href: user_omniauth_authorize_path(:github)} Sign In » From 272b3caf8e7278e47cbdb3e66e52ea39304ea476 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Fri, 13 Sep 2019 08:47:56 +0200 Subject: [PATCH 371/372] Fix CVE-2019-16109 --- Gemfile | 2 +- Gemfile.lock | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Gemfile b/Gemfile index 9a0ea0d7..01f743e7 100644 --- a/Gemfile +++ b/Gemfile @@ -42,7 +42,7 @@ group :doc do gem 'sdoc', require: false end -gem 'devise' +gem 'devise', "~> 4.7.1" gem 'test_after_commit', :group => :test # https://github.com/plataformatec/devise/blob/master/CHANGELOG.md#410 gem 'omniauth' gem 'omniauth-rails_csrf_protection', '~> 0.1' diff --git a/Gemfile.lock b/Gemfile.lock index c9d9692a..d31a520d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -92,7 +92,7 @@ GEM sshkit (>= 1.6.1, != 1.7.0) arel (6.0.4) awesome_print (1.8.0) - bcrypt (3.1.12) + bcrypt (3.1.13) bootstrap_form (2.3.0) builder (3.2.3) cancancan (2.0.0) @@ -169,10 +169,10 @@ GEM railties (>= 4, < 5.2) cucumber-wire (0.0.1) database_cleaner (1.6.1) - devise (4.6.1) + devise (4.7.1) bcrypt (~> 3.0) orm_adapter (~> 0.1) - railties (>= 4.1.0, < 6.0) + railties (>= 4.1.0) responders warden (~> 1.2.3) diff-lcs (1.3) @@ -264,7 +264,7 @@ GEM net-scp (1.2.1) net-ssh (>= 2.6.5) net-ssh (4.1.0) - nokogiri (1.10.1) + nokogiri (1.10.4) mini_portile2 (~> 2.4.0) nokogumbo (1.5.0) nokogiri @@ -317,7 +317,7 @@ GEM activesupport (>= 4.2.0, < 5.0) nokogiri (~> 1.6) rails-deprecated_sanitizer (>= 1.0.1) - rails-html-sanitizer (1.0.4) + rails-html-sanitizer (1.2.0) loofah (~> 2.2, >= 2.2.2) rails_autolink (1.1.6) rails (> 3.1) @@ -326,7 +326,7 @@ GEM activesupport (= 4.2.11) rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) - rake (12.3.2) + rake (12.3.3) rb-fsevent (0.10.2) rb-inotify (0.9.10) ffi (>= 0.5.0, < 2) @@ -440,7 +440,7 @@ DEPENDENCIES compass-rails cucumber-rails database_cleaner - devise + devise (~> 4.7.1) exception_notification factory_girl_rails ffi (>= 1.9.24) From c63313c7281d28186849adb23fae93873c52da33 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Fri, 20 Sep 2019 12:43:42 +0200 Subject: [PATCH 372/372] Close peer4commit --- app/controllers/application_controller.rb | 8 ++++++++ app/views/layouts/application.html.haml | 17 +++-------------- app/views/layouts/closed.html.haml | 5 +++++ 3 files changed, 16 insertions(+), 14 deletions(-) create mode 100644 app/views/layouts/closed.html.haml diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index c38a1097..1cfaa08e 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -13,6 +13,8 @@ class ApplicationController < ActionController::Base before_filter :configure_permitted_parameters, if: :devise_controller? + before_filter :closed + protected def after_sign_in_path_for(user) params[:return_url].presence || @@ -23,4 +25,10 @@ def after_sign_in_path_for(user) 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/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index e2a5d9f7..2cad8c64 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -6,7 +6,7 @@ %meta{content: "", name: "description"}/ %meta{content: "", name: "author"}/ %link{href: image_path("ppcoin.png"), rel: "shortcut icon"}/ - + %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.")} @@ -22,7 +22,7 @@ %script{:src => "https://oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js"} - + = csrf_meta_tags %body{data: {environment: Rails.env}} - if Rails.env.production? @@ -36,15 +36,8 @@ ga('send', 'pageview'); #top-bar .container - #session-menu - - if current_user - = link_to current_user.full_name, edit_registration_path(current_user), class: "edit-profile-link" - = link_to 'Sign Out', destroy_user_session_path, method: :delete, class: "btn btn-default" - - else - = link_to "Sign in", new_user_session_path(return_url: request.url), class: "btn btn-default" %a#main-logo{href: root_path} %h3 Peer4commit - = render 'common/menu' = render_flash_message #main-content - if content_for?(:main_content) @@ -59,12 +52,8 @@ %p © = link_to 'Peer4commit', 'http://peer4commit.com/', target: '_blank' - 2014. Source code is available at #{link_to('github', 'https://github.com/sigmike/peer4commit', 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/"}. - You can support its development with - = link_to('peercoins', 'http://peer4commit.com/projects/1') - or - = link_to('bitcoins', 'http://tip4commit.com/projects/560') / /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.

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 IW0WNQKQ9~aHUIzs literal 1150 zcmcgnu?@mN47>;=N>V`y6@mdWL6%_)*oKB7=x7)sBc!B2Jm-WH)+_l03E{{&+vhtc zK*lG}!S@>bDX;*rWpczxJ0}3-{uRsHGq~K1z3Vhy-_%1MDXq9Z5AnkA4)q?pJUnl; zBA%;{Jv0}7qnE~`^g_>B?eE8&^@`1@2*tT Date: Sun, 4 May 2014 17:24:18 +0200 Subject: [PATCH 171/372] Fixed form display in project list --- app/views/projects/index.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/index.html.haml b/app/views/projects/index.html.haml index 0ec8db44..e07e8f3a 100644 --- a/app/views/projects/index.html.haml +++ b/app/views/projects/index.html.haml @@ -1,6 +1,6 @@ %h1 Supported Projects %p - = form_tag projects_path, role: 'form', class: 'form-inline', method: :post do |f| + = form_tag projects_path, role: 'form', method: :post do |f| .form-group .row .col-lg-10 From 67270de40f737e47d5cd8500678c7fd0a4cc1bc4 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 4 May 2014 19:35:10 +0200 Subject: [PATCH 172/372] Fixed address form layout --- app/views/users/show.html.haml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index e126f892..05b63c43 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -10,13 +10,13 @@ %p %strong E-mail %p= @user.email -= form_for @user, html: {role: 'form', class: 'form-inline'} do |f| += form_for @user, html: {role: 'form'} do |f| - if @user.errors.size > 0 .alert.alert-danger Peercoin address is invalid. .form-group = f.label :bitcoin_address = f.text_field :bitcoin_address, class: 'form-control', placeholder: 'Your peercoin address' - = f.button :update, class: 'btn btn-default' + = f.button "Update", class: 'btn btn-default' - if @user.balance > 0 .send-tips-back-block From 05ca120fbda57c85983c635e2e019a2a74bbee02 Mon Sep 17 00:00:00 2001 From: goodev Date: Thu, 8 May 2014 11:29:43 +0800 Subject: [PATCH 173/372] fix favicon link change favicon.png to favicon.ico --- app/views/layouts/application.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 516d1c25..88d17e2d 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -5,7 +5,7 @@ %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"}/ + %link{href: "/favicon.ico", rel: "shortcut icon"}/ %title= "Peer4Commit — " + (content_for?(:title) ? yield(:title) : "Contribute to Open Source") From 07bbc1ee97fccc4c6dbb58e2b6f45b28f9e19835 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Thu, 8 May 2014 09:20:30 +0200 Subject: [PATCH 174/372] Fixed unreachable buttons at a very specific screen width --- app/views/projects/show.html.haml | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index b8caa95a..5422573b 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -4,14 +4,16 @@ - content_for :description do = @project.description -%h1 - = @project.full_name - %small= link_to glyph(:github), @project.github_url, target: '_blank' - .pull-right - - if can? :update, @project - = link_to "Change project settings", edit_project_path(@project), class: "btn btn-primary" - - 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-warning" +.row + .col-md-12 + %h1 + = @project.full_name + %small= link_to glyph(:github), @project.github_url, target: '_blank' + .pull-right + - if can? :update, @project + = link_to "Change project settings", edit_project_path(@project), class: "btn btn-primary" + - 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-warning" .row .col-md-4 From 3c21b59b7b5ecdf0b5e48b11ba35d03e4f6ce2a2 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Thu, 8 May 2014 09:24:04 +0200 Subject: [PATCH 175/372] Use PNG asset as favicon --- app/assets/images/ppcoin.png | Bin 0 -> 79169 bytes app/views/layouts/application.html.haml | 2 +- public/favicon.ico | Bin 1150 -> 0 bytes 3 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 app/assets/images/ppcoin.png delete mode 100644 public/favicon.ico diff --git a/app/assets/images/ppcoin.png b/app/assets/images/ppcoin.png new file mode 100644 index 0000000000000000000000000000000000000000..62a291c19b1363223e9af84e35fae17e4f855767 GIT binary patch literal 79169 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 D`a(#k literal 0 HcmV?d00001 diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 88d17e2d..2fee79ed 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -5,7 +5,7 @@ %meta{content: "width=device-width, initial-scale=1.0", name: "viewport"}/ %meta{content: "", name: "description"}/ %meta{content: "", name: "author"}/ - %link{href: "/favicon.ico", rel: "shortcut icon"}/ + %link{href: image_path("ppcoin.png"), rel: "shortcut icon"}/ %title= "Peer4Commit — " + (content_for?(:title) ? yield(:title) : "Contribute to Open Source") diff --git a/public/favicon.ico b/public/favicon.ico deleted file mode 100644 index 2994337f63a9ecdafe1e69d6cf0c0a06b896e9e1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1150 zcmcgnu?@mN47>;=N>V`y6@mdWL6%_)*oKB7=x7)sBc!B2Jm-WH)+_l03E{{&+vhtc zK*lG}!S@>bDX;*rWpczxJ0}3-{uRsHGq~K1z3Vhy-_%1MDXq9Z5AnkA4)q?pJUnl; zBA%;{Jv0}7qnE~`^g_>B?eE8&^@`1@2*tT Date: Thu, 8 May 2014 09:29:03 +0200 Subject: [PATCH 176/372] Primecoin favicon --- app/assets/images/ppcoin.png | Bin 79169 -> 0 bytes app/assets/images/primecoin.png | Bin 0 -> 59111 bytes app/views/layouts/application.html.haml | 2 +- 3 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 app/assets/images/ppcoin.png create mode 100644 app/assets/images/primecoin.png diff --git a/app/assets/images/ppcoin.png b/app/assets/images/ppcoin.png deleted file mode 100644 index 62a291c19b1363223e9af84e35fae17e4f855767..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 79169 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 D`a(#k diff --git a/app/assets/images/primecoin.png b/app/assets/images/primecoin.png new file mode 100644 index 0000000000000000000000000000000000000000..63e9d4a121b5b01fe041da5fcc2c2d93b0b8448a GIT binary patch literal 59111 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