diff --git a/Gemfile b/Gemfile index 6ccf17e66..e19fdc4df 100644 --- a/Gemfile +++ b/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' diff --git a/app/assets/javascripts/ckeditor/config.js b/app/assets/javascripts/ckeditor/config.js index 8d6edd348..ede6210b3 100644 --- a/app/assets/javascripts/ckeditor/config.js +++ b/app/assets/javascripts/ckeditor/config.js @@ -8,6 +8,8 @@ CKEDITOR.editorConfig = function( config ) // Define changes to default configuration here. For example: // 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. diff --git a/app/assets/javascripts/dashboard_graphs.js b/app/assets/javascripts/dashboard_graphs.js index 4077aff01..26f0aebb5 100644 --- a/app/assets/javascripts/dashboard_graphs.js +++ b/app/assets/javascripts/dashboard_graphs.js @@ -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(); diff --git a/app/controllers/proposals_dashboard_controller.rb b/app/controllers/proposals_dashboard_controller.rb index 497311010..2fb86587d 100644 --- a/app/controllers/proposals_dashboard_controller.rb +++ b/app/controllers/proposals_dashboard_controller.rb @@ -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 diff --git a/app/services/proposal_achievements_query.rb b/app/services/proposal_achievements_query.rb new file mode 100644 index 000000000..084fd9f71 --- /dev/null +++ b/app/services/proposal_achievements_query.rb @@ -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 + diff --git a/app/services/successful_proposal_supports_query.rb b/app/services/successful_proposal_supports_query.rb new file mode 100644 index 000000000..50cb6103f --- /dev/null +++ b/app/services/successful_proposal_supports_query.rb @@ -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 + diff --git a/app/views/proposals_dashboard/progress.html.erb b/app/views/proposals_dashboard/progress.html.erb index f65da52a5..430b5ad13 100644 --- a/app/views/proposals_dashboard/progress.html.erb +++ b/app/views/proposals_dashboard/progress.html.erb @@ -13,9 +13,12 @@
#{Faker::Lorem.paragraphs.join('
')}
" + 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