diff --git a/app/controllers/admin/api/stats_controller.rb b/app/controllers/admin/api/stats_controller.rb index af8ffb3aac4..37be264bffe 100644 --- a/app/controllers/admin/api/stats_controller.rb +++ b/app/controllers/admin/api/stats_controller.rb @@ -1,20 +1,18 @@ class Admin::Api::StatsController < Admin::Api::BaseController def show - unless params[:events].present? || + unless params[:event].present? || params[:visits].present? || params[:spending_proposals].present? || - params[:budget_investments].present? + params[:budget_investments].present? || + params[:user_supported_budgets].present? return render json: {}, status: :bad_request end ds = Ahoy::DataSource.new - if params[:events].present? - event_types = params[:events].split "," - event_types.each do |event| - ds.add event.titleize, Ahoy::Event.where(name: event).group_by_day(:time).count - end + if params[:event].present? + ds.add params[:event].titleize, Ahoy::Event.where(name: params[:event]).group_by_day(:time).count end if params[:visits].present? @@ -29,6 +27,9 @@ def show ds.add "Budget Investments", Budget::Investment.group_by_day(:created_at).count end + if params[:user_supported_budgets].present? + ds.add "User supported budgets", Vote.where(votable_type: "Budget::Investment").group_by_day(:updated_at).count + end render json: ds.build end end diff --git a/app/controllers/admin/stats_controller.rb b/app/controllers/admin/stats_controller.rb index 352232d55d4..b6a926792aa 100644 --- a/app/controllers/admin/stats_controller.rb +++ b/app/controllers/admin/stats_controller.rb @@ -1,7 +1,7 @@ class Admin::StatsController < Admin::BaseController def show - @event_types = Ahoy::Event.group(:name).count + @event_types = Ahoy::Event.pluck(:name).uniq.sort @visits = Visit.count @debates = Debate.with_hidden.count @@ -31,6 +31,17 @@ def show @investments = Budget::Investment.where(budget_id: budgets_ids).count end + def graph + @name = params[:id] + @event = params[:event] + + if params[:event] + @count = Ahoy::Event.where(name: params[:event]).count + else + @count = params[:count] + end + end + def proposal_notifications @proposal_notifications = ProposalNotification.all @proposals_with_notifications = @proposal_notifications.select(:proposal_id).distinct.count @@ -41,9 +52,50 @@ def direct_messages @users_who_have_sent_message = DirectMessage.select(:sender_id).distinct.count end + + def budgets + @budgets = Budget.all + end + + def budget_supporting + @budget = Budget.find(params[:budget_id]) + heading_ids = @budget.heading_ids + + votes = Vote.where(votable_type: "Budget::Investment"). + includes(:budget_investment). + where(budget_investments: { heading_id: heading_ids }) + + @vote_count = votes.count + @user_count = votes.select(:voter_id).distinct.count + + @voters_in_heading = {} + @budget.headings.each do |heading| + @voters_in_heading[heading] = voters_in_heading(heading) + end + end + + def budget_balloting + @budget = Budget.find(params[:budget_id]) + @user_count = @budget.ballots.select {|ballot| ballot.lines.any? }.count + + @vote_count = @budget.lines.count + + @vote_count_by_heading = @budget.lines.group(:heading_id).count.collect {|k,v| [Budget::Heading.find(k).name, v]}.sort + + @user_count_by_district = User.where.not(balloted_heading_id: nil).group(:balloted_heading_id).count.collect {|k,v| [Budget::Heading.find(k).name, v]}.sort + end + def polls @polls = ::Poll.current @participants = ::Poll::Voter.where(poll: @polls) end + private + + def voters_in_heading(heading) + Vote.where(votable_type: "Budget::Investment"). + includes(:budget_investment). + where(budget_investments: { heading_id: heading.id }). + select("votes.voter_id").distinct.count + end end diff --git a/app/helpers/budgets_helper.rb b/app/helpers/budgets_helper.rb index 15fdc91a878..d8425c2170f 100644 --- a/app/helpers/budgets_helper.rb +++ b/app/helpers/budgets_helper.rb @@ -44,10 +44,6 @@ def namespaced_budget_investment_vote_path(investment, options = {}) end end - def display_budget_countdown?(budget) - budget.balloting? - end - def css_for_ballot_heading(heading) return "" if current_ballot.blank? || @current_filter == "unfeasible" current_ballot.has_lines_in_heading?(heading) ? "is-active" : "" diff --git a/app/helpers/stats_helper.rb b/app/helpers/stats_helper.rb index b36977b3e28..57db7166a0d 100644 --- a/app/helpers/stats_helper.rb +++ b/app/helpers/stats_helper.rb @@ -1,24 +1,27 @@ module StatsHelper - def events_chart_tag(events, opt = {}) - events = events.join(",") if events.is_a? Array + def chart_tag(opt = {}) opt[:data] ||= {} - opt[:data][:graph] = admin_api_stats_path(events: events) + opt[:data][:graph] = admin_api_stats_path(chart_data(opt)) content_tag :div, "", opt end - def visits_chart_tag(opt = {}) - events = events.join(",") if events.is_a? Array - opt[:data] ||= {} - opt[:data][:graph] = admin_api_stats_path(visits: true) - content_tag :div, "", opt + def chart_data(opt = {}) + data = nil + if opt[:id].present? + data = { opt[:id] => true } + elsif opt[:event].present? + data = { event: opt[:event] } + end + data end - def spending_proposals_chart_tag(opt = {}) - events = events.join(",") if events.is_a? Array - opt[:data] ||= {} - opt[:data][:graph] = admin_api_stats_path(spending_proposals: true) - content_tag :div, "", opt + def graph_link_text(event) + text = t("admin.stats.graph.#{event}") + if text.to_s.match(/translation missing/) + text = event + end + text end def budget_investments_chart_tag(opt = {}) diff --git a/app/models/budget.rb b/app/models/budget.rb index 8c34e030db4..8cc5c9b8f6c 100644 --- a/app/models/budget.rb +++ b/app/models/budget.rb @@ -31,6 +31,7 @@ def name_uniqueness_by_budget has_many :ballots, dependent: :destroy has_many :groups, dependent: :destroy has_many :headings, through: :groups + has_many :lines, through: :ballots, class_name: "Budget::Ballot::Line" has_many :phases, class_name: Budget::Phase has_one :poll diff --git a/app/views/admin/stats/_graph.html.erb b/app/views/admin/stats/_graph.html.erb new file mode 100644 index 00000000000..1a89752899d --- /dev/null +++ b/app/views/admin/stats/_graph.html.erb @@ -0,0 +1,6 @@ +<%= back_link_to %> + +
+

<%= t "admin.stats.graph.#{name || event}" %> (<%= count %>)

+ <%= chart_tag id: name, event: event %> +
diff --git a/app/views/admin/stats/budget_balloting.html.erb b/app/views/admin/stats/budget_balloting.html.erb new file mode 100644 index 00000000000..72d1d6a5471 --- /dev/null +++ b/app/views/admin/stats/budget_balloting.html.erb @@ -0,0 +1,57 @@ +<%= back_link_to budgets_admin_stats_path %> + +

<%= @budget.name %> - <%= t("admin.stats.budget_balloting.title") %>

+ +
+
+
+ +
+ +
+

+ <%= t("admin.stats.budget_balloting.participant_count") %> +
+ + <%= @user_count %> + +

+
+
+
+ + + + + <% @vote_count_by_heading.each do |heading_name, count| %> + + + + + <% end %> +
<%= t("admin.stats.budget_balloting.votes_per_heading") %>
+ <%= heading_name %> + + <%= number_with_delimiter count %> +
+ + + + + <% @user_count_by_district.each do |heading_name, count| %> + + + + + <% end %> +
<%= t("admin.stats.budget_balloting.participants_per_district") %>
+ <%= heading_name %> + + <%= number_with_delimiter count %> +
diff --git a/app/views/admin/stats/budget_supporting.html.erb b/app/views/admin/stats/budget_supporting.html.erb new file mode 100644 index 00000000000..6eb995a1531 --- /dev/null +++ b/app/views/admin/stats/budget_supporting.html.erb @@ -0,0 +1,49 @@ +<% content_for :head do %> + <%= javascript_include_tag "stat_graphs", "data-turbolinks-track" => true %> +<% end %> + +<%= back_link_to budgets_admin_stats_path %> + +

<%= @budget.name %> - <%= t("admin.stats.budget_supporting.title") %>

+ +
+
+
+ +
+ +
+

+ <%= t("admin.stats.budget_supporting.participant_count") %> +
+ + <%= @user_count %> + +

+
+
+
+ +<%= render "graph", name: "user_supported_budgets", event: "", count: @user_count %> + + + + + + <% @voters_in_heading.each do |heading, count| %> + + + + + <% end %> +
<%= t("admin.stats.budget_supporting.headings") %><%= t("admin.stats.budget_supporting.users") %>
+ <%= heading.name %> + + <%= number_with_delimiter count %> +
diff --git a/app/views/admin/stats/budgets.html.erb b/app/views/admin/stats/budgets.html.erb new file mode 100644 index 00000000000..2c12b6b3768 --- /dev/null +++ b/app/views/admin/stats/budgets.html.erb @@ -0,0 +1,17 @@ +<%= back_link_to admin_stats_path %> + +

<%= t("admin.stats.budgets.title") %>

+ +<% @budgets.each do |budget| %> + + + + + +
+ <%= budget.name %> + + <%= link_to t("admin.stats.budgets.supporting_phase"), budget_supporting_admin_stats_path(budget_id: budget.id), class: "button hollow" %> + <%= link_to t("admin.stats.budgets.balloting_phase"), budget_balloting_admin_stats_path(budget_id: budget.id), class: "button hollow" %> +
+<% end %> diff --git a/app/views/admin/stats/graph.html.erb b/app/views/admin/stats/graph.html.erb new file mode 100644 index 00000000000..af6d1111a07 --- /dev/null +++ b/app/views/admin/stats/graph.html.erb @@ -0,0 +1,5 @@ +<% content_for :head do %> + <%= javascript_include_tag "stat_graphs", 'data-turbolinks-track' => true %> +<% end %> + +<%= render 'graph', name: @name, event: @event, count: @count %> diff --git a/app/views/admin/stats/show.html.erb b/app/views/admin/stats/show.html.erb index 86855a15b45..650f23fe50e 100644 --- a/app/views/admin/stats/show.html.erb +++ b/app/views/admin/stats/show.html.erb @@ -1,7 +1,4 @@ -<% content_for :head do %> - <%= javascript_include_tag "stat_graphs", "data-turbolinks-track" => true %> -<% end %> -
+

<%= t "admin.stats.show.stats_title" %>

@@ -9,6 +6,8 @@
<%= link_to t("admin.stats.show.polls"), polls_admin_stats_path, class: "button hollow" %> + <%= link_to t("admin.stats.show.participatory_budgets"), + budgets_admin_stats_path, class: "button hollow" %> <%= link_to t("admin.stats.show.direct_messages"), direct_messages_admin_stats_path, class: "button hollow" %> <%= link_to t("admin.stats.show.proposal_notifications"), @@ -22,7 +21,8 @@
@@ -119,24 +119,14 @@
-

<%= t "admin.stats.show.visits_title" %>

- <%= visits_chart_tag id: "visits" %> -
- -
- <% @event_types.each do |event, count| %> -

<%= event.titleize %> (<%= count %>)

- <%= events_chart_tag event %> + <% @event_types.each do |event| %> +

+ <%= link_to graph_link_text(event), + graph_admin_stats_path(event: event) %> +

<% end %>
- <% if feature?(:spending_proposals) %> -
-

<%= t "admin.stats.show.spending_proposals_title" %>

- <%= spending_proposals_chart_tag id: "spending_proposals" %> -
- <% end %> - <% if feature?(:budgets) %>

<%= t "admin.stats.show.budgets_title" %>

diff --git a/app/views/layouts/admin.html.erb b/app/views/layouts/admin.html.erb index 6b231ce49e4..0f9a82856d3 100644 --- a/app/views/layouts/admin.html.erb +++ b/app/views/layouts/admin.html.erb @@ -33,7 +33,7 @@
<%= render "layouts/flash" %> - <%= render "layouts/officing_booth" %> + <%= render "layouts/officing_booth" if controller.class.parent == Officing && session[:booth_id].present? %> <%= yield %>
diff --git a/config/initializers/ahoy.rb b/config/initializers/ahoy.rb index c7c93b89c44..a6bf607699b 100644 --- a/config/initializers/ahoy.rb +++ b/config/initializers/ahoy.rb @@ -8,4 +8,8 @@ def track_event(name, properties, options) event.ip = request.ip end end + + def exclude? + false + end end diff --git a/config/initializers/vote_extensions.rb b/config/initializers/vote_extensions.rb index 548d75350aa..374013e99ba 100644 --- a/config/initializers/vote_extensions.rb +++ b/config/initializers/vote_extensions.rb @@ -2,6 +2,7 @@ include Graphqlable belongs_to :signature + belongs_to :budget_investment, foreign_key: "votable_id", class_name: "Budget::Investment" scope :public_for_api, -> do where(%{(votes.votable_type = 'Debate' and votes.votable_id in (?)) or diff --git a/config/locales/en/admin.yml b/config/locales/en/admin.yml index ad66ad884a9..f47b0eb19e3 100644 --- a/config/locales/en/admin.yml +++ b/config/locales/en/admin.yml @@ -1451,13 +1451,34 @@ en: verified_users_who_didnt_vote_proposals: Verified users who didn't votes proposals visits: Visits votes: Total votes - spending_proposals_title: Spending Proposals budgets_title: Participatory budgeting - visits_title: Visits + participatory_budgets: Participatory Budgets direct_messages: Direct messages proposal_notifications: Proposal notifications incomplete_verifications: Incomplete verifications polls: Polls + graph: + debate_created: Debates + spending_proposals: Investment projects + visit: Visits + level_2_user: Level 2 users + proposal_created: Citizen proposals + budgets: + title: "Participatory Budgets - Participation stats" + supporting_phase: Supporting phase + balloting_phase: Final voting + budget_balloting: + title: "Final voting stats" + vote_count: Votes + participant_count: Participants + votes_per_heading: Votes per heading + participants_per_district: Participants per district + budget_supporting: + title: "Supporting phase stats" + headings: Headings + users: Users + vote_count: Votes + participant_count: Participants direct_messages: title: Direct messages total: Total diff --git a/config/locales/es/admin.yml b/config/locales/es/admin.yml index b55e1dae832..c789029150e 100644 --- a/config/locales/es/admin.yml +++ b/config/locales/es/admin.yml @@ -1450,13 +1450,34 @@ es: verified_users_who_didnt_vote_proposals: Usuarios verificados que no han votado propuestas visits: Visitas votes: Votos - spending_proposals_title: Propuestas de inversión budgets_title: Presupuestos participativos - visits_title: Visitas + participatory_budgets: Presupuestos Participativos direct_messages: Mensajes directos proposal_notifications: Notificaciones de propuestas incomplete_verifications: Verificaciones incompletas polls: Votaciones + graph: + debate_created: Debates + spending_proposals: Propuestas de inversión + visit: Visitas + level_2_user: Usuarios nivel 2 + proposal_created: Propuestas Ciudadanas + budgets: + title: "Presupuestos participativos - Estadisticas de participación" + supporting_phase: Fase de apoyos + balloting_phase: Votación final + budget_balloting: + title: "Estadísticas votación final" + vote_count: Votos + participant_count: Participantes + votes_per_heading: Votos por partida + participants_per_district: Participantes por distrito + budget_supporting: + title: "Estadísticas fase de apoyos" + headings: Partidas + users: Usuarios + vote_count: Votos + participant_count: Participantes direct_messages: title: Mensajes directos total: Total diff --git a/config/routes/admin.rb b/config/routes/admin.rb index c5b35052893..917f1447c0e 100644 --- a/config/routes/admin.rb +++ b/config/routes/admin.rb @@ -194,6 +194,10 @@ end resource :stats, only: :show do + get :graph, on: :member + get :budgets, on: :collection + get :budget_supporting, on: :member + get :budget_balloting, on: :member get :proposal_notifications, on: :collection get :direct_messages, on: :collection get :polls, on: :collection diff --git a/spec/controllers/admin/api/stats_controller_spec.rb b/spec/controllers/admin/api/stats_controller_spec.rb index d9a2cdeb4eb..291c100d81e 100644 --- a/spec/controllers/admin/api/stats_controller_spec.rb +++ b/spec/controllers/admin/api/stats_controller_spec.rb @@ -31,23 +31,13 @@ it "returns single events formated for working with c3.js" do sign_in user - get :show, params: { events: "foo" } + get :show, params: { event: "foo" } expect(response).to be_ok data = JSON.parse(response.body) expect(data).to eq "x" => ["2015-01-01", "2015-01-02"], "Foo" => [2, 1] end - - it "returns combined comma separated events formated for working with c3.js" do - sign_in user - get :show, params: { events: "foo,bar" } - - expect(response).to be_ok - - data = JSON.parse(response.body) - expect(data).to eq "x" => ["2015-01-01", "2015-01-02", "2015-01-03"], "Foo" => [2, 1, 0], "Bar" => [1, 0, 2] - end end context "visits present" do @@ -69,29 +59,6 @@ end end - context "visits and events present" do - it "returns combined events and visits formated for working with c3.js" do - time_1 = Time.zone.local(2015, 01, 01) - time_2 = Time.zone.local(2015, 01, 02) - - create :ahoy_event, name: "foo", time: time_1 - create :ahoy_event, name: "foo", time: time_2 - create :ahoy_event, name: "foo", time: time_2 - - create :visit, started_at: time_1 - create :visit, started_at: time_1 - create :visit, started_at: time_2 - - sign_in user - get :show, params: { events: "foo", visits: true } - - expect(response).to be_ok - - data = JSON.parse(response.body) - expect(data).to eq "x" => ["2015-01-01", "2015-01-02"], "Foo" => [1, 2], "Visits" => [2, 1] - end - end - context "budget investments present" do it "returns budget investments formated for working with c3.js" do time_1 = Time.zone.local(2017, 04, 01) diff --git a/spec/features/admin/stats_spec.rb b/spec/features/admin/stats_spec.rb index 1e3cb508746..bc5bb3a2cf3 100644 --- a/spec/features/admin/stats_spec.rb +++ b/spec/features/admin/stats_spec.rb @@ -97,11 +97,171 @@ visit admin_stats_path - expect(page).to have_content "Level 2 User (1)" + expect(page).to have_content "Level two users 1" end end + describe "Budget investments" do + context "Supporting phase" do + background do + @budget = create(:budget) + @group_all_city = create(:budget_group, budget: @budget) + @heading_all_city = create(:budget_heading, group: @group_all_city) + end + + scenario "Number of supports in investment projects" do + group_2 = create(:budget_group, budget: @budget) + investment1 = create(:budget_investment, heading: create(:budget_heading, group: group_2)) + investment2 = create(:budget_investment, heading: @heading_all_city) + + 1.times { create(:vote, votable: investment1) } + 2.times { create(:vote, votable: investment2) } + + visit admin_stats_path + click_link "Participatory Budgets" + within("#budget_#{@budget.id}") do + click_link "Supporting phase" + end + + expect(page).to have_content "Votes 3" + end + + scenario "Number of users that have supported an investment project" do + user1 = create(:user, :level_two) + user2 = create(:user, :level_two) + user3 = create(:user, :level_two) + + group_2 = create(:budget_group, budget: @budget) + investment1 = create(:budget_investment, heading: create(:budget_heading, group: group_2)) + investment2 = create(:budget_investment, heading: @heading_all_city) + + create(:vote, votable: investment1, voter: user1) + create(:vote, votable: investment1, voter: user2) + create(:vote, votable: investment2, voter: user1) + + visit admin_stats_path + click_link "Participatory Budgets" + within("#budget_#{@budget.id}") do + click_link "Supporting phase" + end + + expect(page).to have_content "Participants 2" + end + + scenario "Number of users that have supported investments projects per geozone" do + budget = create(:budget) + + group_all_city = create(:budget_group, budget: budget) + group_districts = create(:budget_group, budget: budget) + + all_city = create(:budget_heading, group: group_all_city) + carabanchel = create(:budget_heading, group: group_districts) + barajas = create(:budget_heading, group: group_districts) + + all_city_investment = create(:budget_investment, heading: all_city) + carabanchel_investment = create(:budget_investment, heading: carabanchel) + carabanchel_investment = create(:budget_investment, heading: carabanchel) + + Budget::Investment.all.each do |investment| + create(:vote, votable: investment) + end + + visit admin_stats_path + click_link "Participatory Budgets" + within("#budget_#{budget.id}") do + click_link "Supporting phase" + end + + within("#budget_heading_#{all_city.id}") do + expect(page).to have_content all_city.name + expect(page).to have_content 1 + end + + within("#budget_heading_#{carabanchel.id}") do + expect(page).to have_content carabanchel.name + expect(page).to have_content 2 + end + + within("#budget_heading_#{barajas.id}") do + expect(page).to have_content barajas.name + expect(page).to have_content 0 + end + end + end + + context "Balloting phase" do + background do + @budget = create(:budget, :balloting) + @group = create(:budget_group, budget: @budget) + @heading = create(:budget_heading, group: @group) + @investment = create(:budget_investment, :feasible, :selected, heading: @heading) + end + + scenario "Number of votes in investment projects" do + ballot_1 = create(:budget_ballot, budget: @budget) + ballot_2 = create(:budget_ballot, budget: @budget) + + group_2 = create(:budget_group, budget: @budget) + heading_2 = create(:budget_heading, group: group_2) + investment_2 = create(:budget_investment, :feasible, :selected, heading: heading_2) + + create(:budget_ballot_line, ballot: ballot_1, investment: @investment) + create(:budget_ballot_line, ballot: ballot_1, investment: investment_2) + create(:budget_ballot_line, ballot: ballot_2, investment: investment_2) + + visit admin_stats_path + click_link "Participatory Budgets" + within("#budget_#{@budget.id}") do + click_link "Final voting" + end + + expect(page).to have_content "Votes 3" + end + + scenario "Number of users that have voted a investment project" do + user_1 = create(:user, :level_two) + user_2 = create(:user, :level_two) + user_3 = create(:user, :level_two) + + ballot_1 = create(:budget_ballot, budget: @budget, user: user_1) + ballot_2 = create(:budget_ballot, budget: @budget, user: user_2) + ballot_3 = create(:budget_ballot, budget: @budget, user: user_3) + + create(:budget_ballot_line, ballot: ballot_1, investment: @investment) + create(:budget_ballot_line, ballot: ballot_2, investment: @investment) + + visit admin_stats_path + click_link "Participatory Budgets" + within("#budget_#{@budget.id}") do + click_link "Final voting" + end + + expect(page).to have_content "Participants 2" + end + end + + end + + context "graphs" do + scenario "event graphs", :js do + campaign = create(:campaign) + + visit root_path(track_id: campaign.track_id) + visit admin_stats_path + + within("#stats") do + click_link campaign.name + end + + expect(page).to have_content "#{campaign.name} (1)" + within("#graph") do + event_created_at = Ahoy::Event.where(name: campaign.name).first.time + expect(page).to have_content event_created_at.strftime("%Y-%m-%d") + end + end + end + context "Proposal notifications" do scenario "Summary stats" do diff --git a/spec/features/campaigns_spec.rb b/spec/features/campaigns_spec.rb index 681cd33c1e6..517c95481a3 100644 --- a/spec/features/campaigns_spec.rb +++ b/spec/features/campaigns_spec.rb @@ -15,8 +15,13 @@ 5.times { visit root_url(track_id: @campaign2.track_id) } visit admin_stats_path + click_link @campaign1.name expect(page).to have_content "#{@campaign1.name} (3)" + + click_link "Go back" + click_link @campaign2.name + expect(page).to have_content "#{@campaign2.name} (5)" end @@ -25,9 +30,13 @@ visit root_url(track_id: "999") visit admin_stats_path + click_link @campaign1.name expect(page).to have_content "#{@campaign1.name} (1)" + + click_link "Go back" + expect(page).not_to have_content @campaign2.name.to_s end -end \ No newline at end of file +end