2
Gemfile
2
Gemfile
@@ -18,6 +18,7 @@ gem 'delayed_job_active_record', '~> 4.1.0'
|
||||
gem 'devise', '~> 3.5.7'
|
||||
gem 'devise-async', '~> 0.10.2'
|
||||
gem 'devise_security_extension', '~> 0.10.0'
|
||||
gem 'faker', '~> 1.8.7'
|
||||
gem 'foundation-rails', '~> 6.4.3.0'
|
||||
gem 'foundation_rails_helper', '~> 2.0.0'
|
||||
gem 'graphiql-rails', '~> 1.4.1'
|
||||
@@ -64,7 +65,6 @@ group :development, :test do
|
||||
gem 'bullet', '~> 5.7.0'
|
||||
gem 'byebug', '~> 10.0.0'
|
||||
gem 'factory_bot_rails', '~> 4.8.2'
|
||||
gem 'faker', '~> 1.8.7'
|
||||
gem 'i18n-tasks', '~> 0.9.20'
|
||||
gem 'knapsack_pro', '~> 0.53.0'
|
||||
gem 'launchy', '~> 2.4.3'
|
||||
|
||||
@@ -9,6 +9,8 @@ CKEDITOR.editorConfig = function( config )
|
||||
// config.language = 'fr';
|
||||
// config.uiColor = '#AADC6E';
|
||||
|
||||
config.forcePasteAsPlainText = true;
|
||||
|
||||
/* Filebrowser routes */
|
||||
// The location of an external file browser, that should be launched when "Browse Server" button is pressed.
|
||||
config.filebrowserBrowseUrl = "/ckeditor/attachment_files";
|
||||
|
||||
@@ -7,20 +7,27 @@
|
||||
|
||||
var ProposalGraph = function(url) {
|
||||
this.url = url;
|
||||
this.successfulProposalDataUrl = null;
|
||||
this.proposalAchievementsUrl = null;
|
||||
this.targetId = null;
|
||||
this.groupBy = null;
|
||||
this.proposalSuccess = null;
|
||||
this.progressLabel = 'Progress';
|
||||
this.supportsLabel = 'Supports';
|
||||
this.successLabel = 'Success';
|
||||
this.chart = null;
|
||||
this.goals = null;
|
||||
this.achievements = null;
|
||||
this.xColumnValues = null;
|
||||
this.succesfulColumnValues = null;
|
||||
this.progressColumnValues = null;
|
||||
};
|
||||
|
||||
ProposalGraph.prototype.refresh = function() {
|
||||
this.refreshGoals()
|
||||
.then(this.refreshData.bind(this))
|
||||
.then(this.refreshSuccessfulData.bind(this))
|
||||
.then(this.refreshAchievements.bind(this))
|
||||
.done(this.draw.bind(this));
|
||||
};
|
||||
|
||||
@@ -62,7 +69,7 @@
|
||||
ProposalGraph.prototype.parseData = function(data) {
|
||||
var key;
|
||||
|
||||
this.xColumnValues = [ 'x' ];
|
||||
this.xColumnValues = [ ];
|
||||
this.progressColumnValues = [ this.progressLabel ];
|
||||
|
||||
for (key in data) {
|
||||
@@ -73,14 +80,78 @@
|
||||
}
|
||||
};
|
||||
|
||||
ProposalGraph.prototype.refreshSuccessfulData = function() {
|
||||
return $.ajax({
|
||||
url: this.successfulProposalDataUrl,
|
||||
cache: false,
|
||||
success: function (data) {
|
||||
this.parseSuccessfulProposalData(data);
|
||||
}.bind(this),
|
||||
data: {
|
||||
group_by: this.groupBy
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
ProposalGraph.prototype.parseSuccessfulProposalData = function(data) {
|
||||
var key;
|
||||
|
||||
this.successfulColumnValues = [ this.successLabel ];
|
||||
|
||||
for (key in data) {
|
||||
if (data.hasOwnProperty(key)) {
|
||||
this.addXColumnValue(key);
|
||||
this.successfulColumnValues.push(data[key]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
ProposalGraph.prototype.refreshAchievements = function() {
|
||||
return $.ajax({
|
||||
url: this.proposalAchievementsUrl,
|
||||
cache: false,
|
||||
success: function (data) {
|
||||
this.parseAchievements(data);
|
||||
}.bind(this),
|
||||
data: {
|
||||
group_by: this.groupBy
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
ProposalGraph.prototype.parseAchievements = function(data) {
|
||||
var group;
|
||||
|
||||
this.achievements = [];
|
||||
for (group in data) {
|
||||
if (data.hasOwnProperty(group)) {
|
||||
this.addXColumnValue(group);
|
||||
this.achievements.push({
|
||||
value: group,
|
||||
text: data[group][data[group].length - 1].title
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
ProposalGraph.prototype.addXColumnValue = function (value) {
|
||||
if (this.xColumnValues.indexOf(value) === -1) {
|
||||
this.xColumnValues.push(value);
|
||||
}
|
||||
}
|
||||
|
||||
ProposalGraph.prototype.draw = function(data) {
|
||||
this.xColumnValues = this.xColumnValues.sort();
|
||||
this.xColumnValues.unshift('x');
|
||||
|
||||
this.chart = c3.generate({
|
||||
bindto: '#' + this.targetId,
|
||||
data: {
|
||||
x: 'x',
|
||||
columns: [
|
||||
this.xColumnValues,
|
||||
this.progressColumnValues
|
||||
this.progressColumnValues,
|
||||
this.successfulColumnValues
|
||||
]
|
||||
},
|
||||
axis: {
|
||||
@@ -93,13 +164,25 @@
|
||||
}
|
||||
},
|
||||
x: {
|
||||
type: 'category'
|
||||
type: 'category',
|
||||
tick: {
|
||||
fit: true,
|
||||
culling: {
|
||||
max: 15
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
y: {
|
||||
lines: this.goals
|
||||
},
|
||||
x: {
|
||||
lines: this.achievements
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
position: 'right'
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -107,10 +190,13 @@
|
||||
$(document).ready(function () {
|
||||
$('[data-proposal-graph-url]').each(function () {
|
||||
var graph = new ProposalGraph($(this).data('proposal-graph-url'));
|
||||
graph.successfulProposalDataUrl = $(this).data('successful-proposal-graph-url');
|
||||
graph.proposalAchievementsUrl = $(this).data('proposal-achievements-url');
|
||||
graph.targetId = $(this).attr('id');
|
||||
graph.groupBy = $(this).data('proposal-graph-group-by');
|
||||
graph.progressLabel = $(this).data('proposal-graph-progress-label');
|
||||
graph.supportsLabel = $(this).data('proposal-graph-supports-label');
|
||||
graph.successLabel = $(this).data('proposal-graph-success-label');
|
||||
graph.proposalSuccess = parseInt($(this).data('proposal-success'), 10);
|
||||
|
||||
graph.refresh();
|
||||
|
||||
@@ -52,6 +52,16 @@ class ProposalsDashboardController < Dashboard::BaseController
|
||||
render json: ProposalSupportsQuery.for(params)
|
||||
end
|
||||
|
||||
def successful_supports
|
||||
authorize! :dashboard, proposal
|
||||
render json: SuccessfulProposalSupportsQuery.for(params)
|
||||
end
|
||||
|
||||
def achievements
|
||||
authorize! :dashboard, proposal
|
||||
render json: ProposalAchievementsQuery.for(params)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def proposal_dashboard_action
|
||||
|
||||
59
app/services/proposal_achievements_query.rb
Normal file
59
app/services/proposal_achievements_query.rb
Normal file
@@ -0,0 +1,59 @@
|
||||
class ProposalAchievementsQuery
|
||||
attr_reader :params
|
||||
|
||||
def self.for(params)
|
||||
query = ProposalAchievementsQuery.new params
|
||||
query.results
|
||||
end
|
||||
|
||||
def initialize(params)
|
||||
@params = params
|
||||
end
|
||||
|
||||
def results
|
||||
grouped_results = groups
|
||||
grouped_results.each do |key, achievements|
|
||||
grouped_results[key] = []
|
||||
|
||||
achievements.each do |achievement|
|
||||
grouped_results[key] << {
|
||||
executed_at: achievements.last.executed_at,
|
||||
title: achievements.last.proposal_dashboard_action.title
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
grouped_results
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def groups
|
||||
return achievements.group_by { |v| v.executed_at.to_date.year } if params[:group_by] == 'year'
|
||||
return achievements.group_by { |v| "#{v.executed_at.to_date.cweek}/#{v.executed_at.to_date.year}" } if params[:group_by] == 'week'
|
||||
return achievements.group_by { |v| "#{v.executed_at.to_date.year}-#{v.executed_at.to_date.month}" } if params[:group_by] == 'month'
|
||||
achievements.group_by { |a| a.executed_at.to_date }
|
||||
end
|
||||
|
||||
def achievements
|
||||
ProposalExecutedDashboardAction
|
||||
.includes(:proposal_dashboard_action)
|
||||
.where(proposal: proposal, executed_at: start_date.beginning_of_day..end_date.end_of_day)
|
||||
.order(executed_at: :asc)
|
||||
end
|
||||
|
||||
def proposal
|
||||
@proposal ||= Proposal.find(params[:proposal_id])
|
||||
end
|
||||
|
||||
def start_date
|
||||
return Date.parse(params[:start_date]) unless params[:start_date].blank?
|
||||
proposal.created_at.to_date
|
||||
end
|
||||
|
||||
def end_date
|
||||
return Date.parse(params[:end_date]) unless params[:end_date].blank?
|
||||
Date.today
|
||||
end
|
||||
end
|
||||
|
||||
72
app/services/successful_proposal_supports_query.rb
Normal file
72
app/services/successful_proposal_supports_query.rb
Normal file
@@ -0,0 +1,72 @@
|
||||
class SuccessfulProposalSupportsQuery
|
||||
attr_reader :params
|
||||
|
||||
def self.for(params)
|
||||
query = SuccessfulProposalSupportsQuery.new params
|
||||
query.results
|
||||
end
|
||||
|
||||
def initialize(params)
|
||||
@params = params
|
||||
end
|
||||
|
||||
def results
|
||||
grouped_votes = groups
|
||||
grouped_votes.each do |group, votes|
|
||||
grouped_votes[group] = votes.inject(0) { |sum, vote| sum + vote.vote_weight }
|
||||
end
|
||||
|
||||
accumulated = 0
|
||||
grouped_votes.each do |k, v|
|
||||
accumulated += v
|
||||
grouped_votes[k] = accumulated
|
||||
end
|
||||
|
||||
grouped_votes
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def groups
|
||||
return votes.group_by { |v| v.voted_at.to_date.year } if params[:group_by] == 'year'
|
||||
return votes.group_by { |v| "#{v.voted_at.to_date.cweek}/#{v.voted_at.to_date.year}" } if params[:group_by] == 'week'
|
||||
return votes.group_by { |v| "#{v.voted_at.to_date.year}-#{v.voted_at.to_date.month}" } if params[:group_by] == 'month'
|
||||
votes.group_by { |v| v.voted_at.to_date }
|
||||
end
|
||||
|
||||
def votes
|
||||
return [] if successful_proposal.nil?
|
||||
|
||||
Vote
|
||||
.select("created_at + interval '#{days_diff} day' voted_at, *")
|
||||
.where(votable: successful_proposal)
|
||||
.where("created_at + interval '#{days_diff} day' between ? and ?", start_date.beginning_of_day, end_date.end_of_day)
|
||||
.order(created_at: :asc)
|
||||
end
|
||||
|
||||
def proposal
|
||||
@proposal ||= Proposal.find(params[:proposal_id])
|
||||
end
|
||||
|
||||
def successful_proposal
|
||||
@successful_proposal ||= Proposal.find_by(id: Setting['proposals.successful_proposal_id'])
|
||||
end
|
||||
|
||||
def days_diff
|
||||
return 0 if successful_proposal.nil?
|
||||
return 0 if proposal.published_at.nil?
|
||||
|
||||
(proposal.published_at.to_date - successful_proposal.published_at.to_date).to_i
|
||||
end
|
||||
|
||||
def start_date
|
||||
return Date.parse(params[:start_date]) unless params[:start_date].blank?
|
||||
proposal.created_at.to_date
|
||||
end
|
||||
|
||||
def end_date
|
||||
return Date.parse(params[:end_date]) unless params[:end_date].blank?
|
||||
Date.today
|
||||
end
|
||||
end
|
||||
|
||||
@@ -13,9 +13,12 @@
|
||||
<div class="small-12 column">
|
||||
<div id="proposal-graph"
|
||||
data-proposal-graph-url="<%= supports_proposal_dashboard_index_path(proposal, format: :json) %>"
|
||||
data-successful-proposal-graph-url="<%= successful_supports_proposal_dashboard_index_path(proposal, format: :json) %>"
|
||||
data-proposal-achievements-url="<%= achievements_proposal_dashboard_index_path(proposal, format: :json) %>"
|
||||
data-proposal-graph-group-by="<%= params[:group_by] %>"
|
||||
data-proposal-graph-progress-label="<%= t '.progress' %>"
|
||||
data-proposal-graph-supports-label="<%= t '.supports' %>"
|
||||
data-proposal-graph-progress-label="<%= t('.progress') %>"
|
||||
data-proposal-graph-supports-label="<%= t('.supports') %>"
|
||||
data-proposal-graph-success-label="<%= t('.success') %>"
|
||||
data-proposal-success="<%= Setting["votes_for_proposal_success"] %>"
|
||||
class="c3"
|
||||
style="max-height: 320px; position: relative;"></div>
|
||||
|
||||
@@ -520,6 +520,7 @@ en:
|
||||
group_by_date: Daily
|
||||
progress: Acumulated progress
|
||||
supports: Supports
|
||||
success: Ideal progress
|
||||
index:
|
||||
title: Edition
|
||||
edit_proposal_link: Edit
|
||||
|
||||
@@ -520,6 +520,7 @@ es:
|
||||
group_by_date: Diario
|
||||
progress: Progreso acumulado
|
||||
supports: Apoyos
|
||||
success: Progreso ideal
|
||||
index:
|
||||
title: Edición
|
||||
edit_proposal_link: Editar propuesta
|
||||
|
||||
@@ -7,7 +7,9 @@ resources :proposals do
|
||||
collection do
|
||||
patch :publish
|
||||
get :supports
|
||||
get :successful_supports
|
||||
get :progress
|
||||
get :achievements
|
||||
end
|
||||
|
||||
member do
|
||||
|
||||
@@ -17,4 +17,422 @@ namespace :proposal_actions do
|
||||
task initialize_successful_proposal_id: :environment do
|
||||
Setting['proposals.successful_proposal_id'] = nil
|
||||
end
|
||||
|
||||
desc 'Simulate successful proposal'
|
||||
task create_successful_proposal: :environment do
|
||||
expected_supports = [
|
||||
1049,
|
||||
596,
|
||||
273,
|
||||
208,
|
||||
97,
|
||||
74,
|
||||
148,
|
||||
116,
|
||||
83,
|
||||
62,
|
||||
42,
|
||||
20,
|
||||
36,
|
||||
40,
|
||||
44,
|
||||
38,
|
||||
45,
|
||||
17,
|
||||
15,
|
||||
15,
|
||||
10,
|
||||
28,
|
||||
22,
|
||||
32,
|
||||
26,
|
||||
15,
|
||||
16,
|
||||
21,
|
||||
26,
|
||||
25,
|
||||
14,
|
||||
12,
|
||||
11,
|
||||
18,
|
||||
27,
|
||||
27,
|
||||
22,
|
||||
119,
|
||||
103,
|
||||
65,
|
||||
79,
|
||||
140,
|
||||
96,
|
||||
102,
|
||||
96,
|
||||
65,
|
||||
42,
|
||||
39,
|
||||
108,
|
||||
380,
|
||||
424,
|
||||
302,
|
||||
233,
|
||||
98,
|
||||
88,
|
||||
78,
|
||||
149,
|
||||
202,
|
||||
137,
|
||||
135,
|
||||
48,
|
||||
52,
|
||||
90,
|
||||
47,
|
||||
120,
|
||||
83,
|
||||
55,
|
||||
29,
|
||||
38,
|
||||
51,
|
||||
64,
|
||||
105,
|
||||
27,
|
||||
17,
|
||||
8,
|
||||
13,
|
||||
16,
|
||||
118,
|
||||
105,
|
||||
69,
|
||||
136,
|
||||
85,
|
||||
50,
|
||||
32,
|
||||
32,
|
||||
34,
|
||||
38,
|
||||
24,
|
||||
23,
|
||||
34,
|
||||
16,
|
||||
41,
|
||||
22,
|
||||
13,
|
||||
17,
|
||||
44,
|
||||
98,
|
||||
52,
|
||||
42,
|
||||
38,
|
||||
12,
|
||||
7,
|
||||
14,
|
||||
14,
|
||||
25,
|
||||
20,
|
||||
21,
|
||||
10,
|
||||
10,
|
||||
11,
|
||||
22,
|
||||
44,
|
||||
28,
|
||||
9,
|
||||
35,
|
||||
30,
|
||||
24,
|
||||
22,
|
||||
91,
|
||||
41,
|
||||
34,
|
||||
42,
|
||||
23,
|
||||
21,
|
||||
18,
|
||||
18,
|
||||
19,
|
||||
21,
|
||||
58,
|
||||
31,
|
||||
30,
|
||||
24,
|
||||
38,
|
||||
32,
|
||||
20,
|
||||
372,
|
||||
520,
|
||||
178,
|
||||
85,
|
||||
150,
|
||||
562,
|
||||
212,
|
||||
110,
|
||||
50,
|
||||
49,
|
||||
53,
|
||||
69,
|
||||
134,
|
||||
78,
|
||||
42,
|
||||
62,
|
||||
76,
|
||||
141,
|
||||
101,
|
||||
196,
|
||||
209,
|
||||
196,
|
||||
211,
|
||||
165,
|
||||
181,
|
||||
361,
|
||||
736,
|
||||
325,
|
||||
194,
|
||||
194,
|
||||
126,
|
||||
122,
|
||||
143,
|
||||
186,
|
||||
339,
|
||||
169,
|
||||
97,
|
||||
125,
|
||||
120,
|
||||
152,
|
||||
88,
|
||||
27,
|
||||
45,
|
||||
23,
|
||||
35,
|
||||
39,
|
||||
53,
|
||||
40,
|
||||
23,
|
||||
26,
|
||||
22,
|
||||
20,
|
||||
30,
|
||||
18,
|
||||
22,
|
||||
15,
|
||||
50,
|
||||
42,
|
||||
23,
|
||||
11,
|
||||
94,
|
||||
113,
|
||||
115,
|
||||
122,
|
||||
159,
|
||||
184,
|
||||
173,
|
||||
211,
|
||||
161,
|
||||
144,
|
||||
115,
|
||||
99,
|
||||
80,
|
||||
77,
|
||||
123,
|
||||
355,
|
||||
338,
|
||||
226,
|
||||
201,
|
||||
70,
|
||||
47,
|
||||
117,
|
||||
116,
|
||||
61,
|
||||
79,
|
||||
284,
|
||||
607,
|
||||
565,
|
||||
541,
|
||||
347,
|
||||
265,
|
||||
204,
|
||||
158,
|
||||
127,
|
||||
110,
|
||||
173,
|
||||
137,
|
||||
92,
|
||||
135,
|
||||
95,
|
||||
104,
|
||||
131,
|
||||
106,
|
||||
103,
|
||||
85,
|
||||
81,
|
||||
46,
|
||||
58,
|
||||
88,
|
||||
108,
|
||||
85,
|
||||
78,
|
||||
52,
|
||||
39,
|
||||
21,
|
||||
33,
|
||||
50,
|
||||
57,
|
||||
53,
|
||||
32,
|
||||
263,
|
||||
162,
|
||||
89,
|
||||
142,
|
||||
70,
|
||||
48,
|
||||
39,
|
||||
26,
|
||||
19,
|
||||
25,
|
||||
24,
|
||||
36,
|
||||
48,
|
||||
48,
|
||||
26,
|
||||
19,
|
||||
40,
|
||||
1916,
|
||||
535,
|
||||
214,
|
||||
106,
|
||||
73,
|
||||
50,
|
||||
42,
|
||||
62,
|
||||
54,
|
||||
54,
|
||||
82,
|
||||
124,
|
||||
112,
|
||||
104,
|
||||
328,
|
||||
256,
|
||||
309,
|
||||
547,
|
||||
68,
|
||||
27,
|
||||
41,
|
||||
55,
|
||||
55,
|
||||
37,
|
||||
32,
|
||||
29,
|
||||
14,
|
||||
18,
|
||||
23,
|
||||
21,
|
||||
18,
|
||||
11,
|
||||
10,
|
||||
16,
|
||||
12,
|
||||
49,
|
||||
74,
|
||||
230,
|
||||
110,
|
||||
63,
|
||||
17,
|
||||
14,
|
||||
26,
|
||||
300,
|
||||
137,
|
||||
45,
|
||||
25,
|
||||
7,
|
||||
6,
|
||||
19,
|
||||
12,
|
||||
7,
|
||||
53,
|
||||
53,
|
||||
14,
|
||||
14,
|
||||
17,
|
||||
10,
|
||||
8,
|
||||
6,
|
||||
5,
|
||||
7,
|
||||
5,
|
||||
3,
|
||||
5,
|
||||
5,
|
||||
4,
|
||||
4,
|
||||
3,
|
||||
1,
|
||||
4,
|
||||
7,
|
||||
7,
|
||||
5,
|
||||
6,
|
||||
3,
|
||||
3,
|
||||
8,
|
||||
6,
|
||||
6,
|
||||
4,
|
||||
7,
|
||||
4,
|
||||
5,
|
||||
9,
|
||||
5,
|
||||
1,
|
||||
3,
|
||||
4,
|
||||
1,
|
||||
2,
|
||||
5,
|
||||
4,
|
||||
3,
|
||||
5
|
||||
]
|
||||
|
||||
tags = Faker::Lorem.words(25)
|
||||
author = User.all.sample
|
||||
description = "<p>#{Faker::Lorem.paragraphs.join('</p><p>')}</p>"
|
||||
proposal = Proposal.create!(author: author,
|
||||
title: Faker::Lorem.sentence(3).truncate(60),
|
||||
question: Faker::Lorem.sentence(3) + "?",
|
||||
summary: Faker::Lorem.sentence(3),
|
||||
responsible_name: Faker::Name.name,
|
||||
external_url: Faker::Internet.url,
|
||||
description: description,
|
||||
created_at: Time.now - expected_supports.length.days,
|
||||
tag_list: tags.sample(3).join(','),
|
||||
geozone: Geozone.all.sample,
|
||||
skip_map: "1",
|
||||
terms_of_service: "1",
|
||||
published_at: Time.now - expected_supports.length.days)
|
||||
|
||||
|
||||
expected_supports.each_with_index do |supports, day_offset|
|
||||
supports.times do |i|
|
||||
user = User.create!(
|
||||
username: "user_#{proposal.id}_#{day_offset}_#{i}",
|
||||
email: "user_#{proposal.id}_#{day_offset}_#{i}@consul.dev",
|
||||
password: '12345678',
|
||||
password_confirmation: '12345678',
|
||||
confirmed_at: Time.current - expected_supports.length.days,
|
||||
terms_of_service: '1',
|
||||
gender: ['Male', 'Female'].sample,
|
||||
date_of_birth: rand((Time.current - 80.years)..(Time.current - 16.years)),
|
||||
public_activity: (rand(1..100) > 30)
|
||||
)
|
||||
|
||||
Vote.create!(
|
||||
votable: proposal,
|
||||
voter: user,
|
||||
vote_flag: false,
|
||||
vote_weight: 1,
|
||||
created_at: proposal.published_at + day_offset.days,
|
||||
updated_at: proposal.published_at + day_offset.days
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
Setting['proposals.successful_proposal_id'] = proposal.id
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user