From d418f650519a482e1bdba8878eaebf4130e4f9fa Mon Sep 17 00:00:00 2001 From: Fernando Blat Date: Fri, 10 Jun 2022 12:19:42 +0200 Subject: [PATCH 1/8] Basic activity browser --- .../decidim/activity_browser_controller.rb | 83 ++++ .../app/packs/src/decidim/activity_browser.js | 445 ++++++++++++++++++ decidim-core/app/packs/src/decidim/index.js | 2 + .../decidim/activity_browser/data.html.erb | 1 + .../decidim/activity_browser/index.html.erb | 96 ++++ decidim-core/config/routes.rb | 3 + 6 files changed, 630 insertions(+) create mode 100644 decidim-core/app/controllers/decidim/activity_browser_controller.rb create mode 100644 decidim-core/app/packs/src/decidim/activity_browser.js create mode 100644 decidim-core/app/views/decidim/activity_browser/data.html.erb create mode 100644 decidim-core/app/views/decidim/activity_browser/index.html.erb diff --git a/decidim-core/app/controllers/decidim/activity_browser_controller.rb b/decidim-core/app/controllers/decidim/activity_browser_controller.rb new file mode 100644 index 0000000000000..c18022f939651 --- /dev/null +++ b/decidim-core/app/controllers/decidim/activity_browser_controller.rb @@ -0,0 +1,83 @@ +# frozen_string_literal: true + +require "csv" + +module Decidim + # The controller to display Decidim activity + class ActivityBrowserController < Decidim::ApplicationController + + def index + @activity_data = activity_data + end + + def data + send_data activity_data, type: Mime[:csv], filename: "activity.csv" + end + + private + + def activity_data + return File.read('/Users/fernando/Desktop/decidim_activities_report.csv') + + headers = %w(timestamp item_type item_id target_type target_id user_id participatory_space_id participatory_space_type) + + CSV.generate do |csv| + # Header + csv << headers + # Data + 1000.times do + item_type_target_type.each do |item_type, target_types| + if target_types.empty? + csv << [generate_timestamp, item_type, generate_id, nil, nil, generate_id, generate_participatory_space_type] + else + target_types.each do |target_type| + csv << [generate_timestamp, item_type, generate_id, target_type, generate_id, generate_id, generate_participatory_space_type] + end + end + end + end + end + end + + def generate_timestamp + year = [2020, 2021, 2022].sample + month = rand(12) + 1 + day = rand(30) + 1 + hour = rand(23) + 1 + minute = rand(59) + 1 + + # Time.parse("#{year}-#{month}-#{day} #{hour}:#{minute}:00") + Date.new(year, month, day) rescue nil + end + + def item_type_target_type + { + comment: [:proposal, :debate, :meeting, :initiative], + comment_vote: [:comment], + proposal: [], + endorsement: [:proposal, :debate, :meeting, :initiative], + following: [:user], + meeting: [], + meeting_registration: [:meeting], + debate: [], + initiative: [], + initiative_vote: [:initiative] + } + end + + # 1..30 random + def generate_id + rand(30)+1 + end + + # 1..4 random + def generate_participatory_space_id + rand(3)+1 + end + + def generate_participatory_space_type + [:participatory_process, :assembly].sample + end + + end +end diff --git a/decidim-core/app/packs/src/decidim/activity_browser.js b/decidim-core/app/packs/src/decidim/activity_browser.js new file mode 100644 index 0000000000000..065a429946ddd --- /dev/null +++ b/decidim-core/app/packs/src/decidim/activity_browser.js @@ -0,0 +1,445 @@ +import { max, histogram, extent } from "d3-array"; +import { scaleTime, scaleLinear } from "d3-scale"; +import { timeMonth } from "d3-time"; +import { select } from "d3-selection"; +import { axisBottom, axisLeft } from "d3-axis"; + +export default class ActivityBrowser { + constructor(data) { + this.arr = this.csvToArray(data); + + // State variables + this.cachedUsers = null; + this.cachedTotalUsers = null; + this.cachedContributions = null; + this.cachedTotalContributions = null; + this.nodeSelected = null; + this.dates = this.getDates(this.arr); + this.timerActive = false; + this.datesInterval = null; + this.currentDateIndex = 0; + } + + run() { + this.refreshData(this.arr, true); + + // Enable tooltip + tippy('.users span'); + tippy('.contributions span'); + + // Click on date ranges + $('[data-time-range]').on('click', (e) => { + $('[data-time-range]').removeClass('bold'); + + const $element = $(e.currentTarget); + $element.addClass('bold'); + + const range = $element.data('time-range'); + let filteredData = this.arr; + const today = new Date(); + let fromRange; + switch(range) { + case 'month': + fromRange = new Date(new Date().setDate(today.getDate() - 30)) + filteredData = this.filterDataByDateRange(fromRange, this.arr); + this.refreshData(filteredData, false); + break; + case 'year': + fromRange = new Date(new Date().setDate(today.getDate() - 365)) + filteredData = this.filterDataByDateRange(fromRange, this.arr); + this.refreshData(filteredData, false); + break; + case 'all': + this.refreshData(this.arr, true); + break; + } + }); + + // Click on timer + $('[data-timer]').on('click', () => { + if (this.timerActive) { + this.timerActive = false; + clearInterval(this.datesInterval); + + this.resetActiveElements(); + $('div.date').html(''); + $(this).html('Start timer'); + } else { + this.timerActive = true; + $(this).html('Stop timer'); + this.datesInterval = setInterval(setDateInterval, 2000); + $('div.date').html(this.dates[this.currentDateIndex].toLocaleDateString()); + const filteredData = this.filterDataByDate(this.dates[this.currentDateIndex], this.arr); + this.refreshData(filteredData, false); + + function setDateInterval() { + this.currentDateIndex++; + $('div.date').html(this.dates[this.currentDateIndex].toLocaleDateString()); + const filteredData = this.filterDataByDate(this.dates[this.currentDateIndex], this.arr); + this.refreshData(filteredData, false); + } + } + }); + + // Click on contributons + $('ul.contributions span').click((e) => { + if(this.nodeSelected === null) { + const $element = $(e.currentTarget); + this.nodeSelected = {type: $element.data('type'), id: $element.data('id') } + } else { + this.nodeSelected = null; + } + }); + + // Click on users + $('ul.users span').click((e) => { + if(this.nodeSelected === null) { + const $element = $(e.currentTarget); + this.nodeSelected = {type: $element.data('type'), id: $element.data('id') } + } else { + this.nodeSelected = null; + } + }); + + // Mouse over on contributions + $('ul.contributions span').hover((e) => { + if(this.timerActive || this.nodeSelected) { return false; } + + let $element = $(e.currentTarget); + let id = $element.data('id').toString(); + let type = $element.data('type'); + + const filteredData = this.arr.filter(i => ((i.item_type === type && i.item_id === id) || (i.target_type === type && i.target_id === id))); + this.refreshData(filteredData, false); + }, (e) => { + if(this.timerActive || this.nodeSelected) { return false; } + this.refreshData(this.arr, true); + }); + + // Mouse over on users + $('ul.users span').hover((e) => { + if(this.timerActive || this.nodeSelected) { return false; } + + let $element = $(e.currentTarget); + let id = $element.data('id').toString(); + + const filteredData = this.arr.filter(i => (i.user_id === id || (i.target_type === 'user' && i.target_id === i.user_id))); + this.refreshData(filteredData, false); + }, (e) => { + if(this.timerActive || this.nodeSelected) { return false; } + this.refreshData(this.arr, true); + }); + } + + + /////////////////////// + // Private functions + /////////////////////// + + + csvToArray(str, delimiter = ",") { + // slice from start of text to the first \n index + // use split to create an array from string by delimiter + const headers = str.slice(0, str.indexOf("\n")).split(delimiter); + + // slice from \n index + 1 to the end of the text + // use split to create an array of each csv value row + const rows = str.slice(str.indexOf("\n") + 1).split("\n"); + + // Map the rows + // split values from each row into an array + // use headers.reduce to create an object + // object properties derived from headers:values + // the object passed as an element of the array + const arr = rows.map((row) => { + const values = row.split(delimiter); + const el = headers.reduce((object, header, index) => { + object[header] = values[index]; + return object; + }, {}); + return el; + }); + + // return the array + return arr; + } + + getDates(arr) { + return [...new Set(arr.map(i => i.timestamp))].map(i => new Date(i)).sort((a,b) => { + return a - b; + }); + } + + getUsers(arr) { + const usersWithCount = arr.reduce((sums,i) => { + if(i.user_id !== undefined && i.user_id !== null && i.user_id.toString().length > 0) { + const key = i.user_id + if(!(key in sums)) { sums[key] = {count: 0, timestamp: i.timestamp, item_url: i.item_url} } + sums[key].count++; + } + return sums; + }, {}); + + return this.mapToSortedArrayWithClass(usersWithCount, 10); + } + + getContributions(arr) { + const contributionsWithCount = arr.reduce((sums, i) => { + if(i.item_type === "proposal" || i.item_type === "debate" || i.item_type === "meeting" || i.item_type === "initiative") { + const key = `${i.item_type}_${i.item_id}`; + if(!(key in sums)) { sums[key] = {count: 0, timestamp: i.timestamp, item_url: i.item_url} } + sums[key].count++; + } + if(i.target_type === "proposal" || i.target_type === "debate" || i.target_type === "meeting" || i.target_type === "initiative") { + const key = `${i.target_type}_${i.target_id}`; + if(!(key in sums)) { sums[key] = {count: 0, timestamp: i.timestamp, item_url: i.item_url} } + sums[key].count++; + } + return sums; + }, {}); + + return this.mapToSortedArrayWithClass(contributionsWithCount, 10); + } + + mapToSortedArrayWithClass(itemsWithCount, totalClasses) { + const totalItems = Object.keys(itemsWithCount).length; + + if(totalItems === 1) { + const id = Object.keys(itemsWithCount)[0]; + return [{id: id, count: 1, group: 1, timestamp: itemsWithCount[id].timestamp, item_url: itemsWithCount[id].item_url}] + } + + let result = []; + let groups = {} + for(let i=1;i<=totalClasses;i++){ + groups[i] = []; + } + + const minValue = 0; + const maxValue = max(Object.values(itemsWithCount).map(v => Math.log(v.count))); + let bin = maxValue/(totalClasses - 1); + // When maxValue is 0 + if(bin === 0) { bin = 1; } + + Object.keys(itemsWithCount).forEach(id => { + const count = itemsWithCount[id].count; + if(count !== undefined) { + let group = Math.floor(Math.log(count)/bin) + 1; + groups[group].push(id); + } + }); + + Object.entries(groups).forEach(([group, ids]) => { + ids.forEach((id) => { + if(!this.contains(result, id)) { + result.push({ id: id, count: itemsWithCount[id].count, group: group, timestamp: itemsWithCount[id].timestamp, item_url: itemsWithCount[id].item_url }); + } + }) + }); + + return result.sort((a,b) => { + return new Date(a.timestamp) - new Date(b.timestamp) + }); + } + + contains(arr, id) { + let i = arr.length; + if(i === 0) { return false; } + + while (i--) { + if (arr[i].id === id) { + return true; + } + } + return false; + } + + updateComments(arr) { + const data = arr.filter(i => i.item_type === 'comment'); + $('#interactions .comments').html(`${data.length} comments`); + } + + updateVotes(arr) { + const data = arr.filter(i => i.item_type !== undefined && i.item_type.indexOf('vote') > -1); + $('#interactions .votes').html(`${data.length} votes`); + } + + updateEndorsements(arr) { + const data = arr.filter(i => i.item_type === 'endorsement'); + $('#interactions .supports').html(`${data.length} supports`); + } + + updateFollowings(arr) { + const data = arr.filter(i => i.item_type === 'following'); + $('#interactions .followings').html(`${data.length} followings`); + } + + filterDataByDateRange(dateFrom, arr) { + return arr.filter(i => new Date(i.timestamp) >= dateFrom); + } + + filterDataByDate(date, arr) { + let dateString = date.toISOString().split('T')[0]; + return arr.filter(i => i.timestamp === dateString); + } + + renderHistogram(selector, rawData) { + // set the dimensions and margins of the graph + const margin = {top: 10, right: 30, bottom: 30, left: 40}, + width = 400 - margin.left - margin.right, + height = 200 - margin.top - margin.bottom; + + const data = Object.values(rawData.reduce((sums,i) => { + const key = i.timestamp + if(!(key in sums)) { sums[key] = {value: 0, timestamp: new Date(i.timestamp)} } + sums[key].value++; + + return sums; + }, {})); + + // set the ranges + const x = scaleTime() + .domain(extent(data.map(d => d.timestamp))) + .rangeRound([0, width]); + const y = scaleLinear() + .range([height, 0]); + + // set the parameters for the histogram + const histogramInstance = histogram() + .value(function(d) { return d.timestamp; }) + .domain(x.domain()) + .thresholds(x.ticks(timeMonth)); + + + // append the svg object to the body of the page + // append a 'group' element to 'svg' + // moves the 'group' element to the top left margin + if($(`${selector} svg`).length) { $(`${selector} svg`).remove() } + let svg = select(selector).append("svg") + .attr("width", width + margin.left + margin.right) + .attr("height", height + margin.top + margin.bottom) + .append("g") + .attr("transform", + "translate(" + margin.left + "," + margin.top + ")"); + + // group the data for the bars + const bins = histogramInstance(data); + + // Scale the range of the data in the y domain + y.domain([0, max(bins, function(d) { return d.length; })]); + + debugger + + // append the bar rectangles to the svg element + svg.selectAll("rect") + .data(bins) + .enter().append("rect") + .attr("class", "bar") + .attr("x", 1) + .attr("transform", function(d) { + return "translate(" + x(d.x0) + "," + y(d.length) + ")"; }) + .attr("width", function(d) { return x(d.x1) - x(d.x0) -1 ; }) + .attr("height", function(d) { return height - y(d.length); }); + + // add the x Axis + svg.append("g") + .attr("transform", "translate(0," + height + ")") + .call(axisBottom(x)); + + // add the y Axis + svg.append("g") + .call(axisLeft(y)); + } + + refreshData(filteredData, useTotals = false) { + this.resetActiveElements(); + + // Render contributions + let contributions = null; + let totalContributions = null; + if(useTotals) { + if(this.cachedContributions === null) { + this.cachedContributions = this.getContributions(filteredData); + } + if(this.cachedTotalContributions === null) { + this.cachedTotalContributions = Object.keys(this.cachedContributions).length; + } + contributions = this.cachedContributions; + totalContributions = this.cachedTotalContributions; + } else { + contributions = this.getContributions(filteredData); + totalContributions = Object.keys(contributions).length; + } + + if(useTotals) { + $('#contributions h2 span.total').html(totalContributions); + $('#contributions h2 span.partial').html(''); + } else { + $('#contributions h2 span.partial').html(`${totalContributions} /`); + } + + if($('ul.contributions').html().length === 0) { + // Display contributions by timestamp + contributions.forEach((contribution) => { + const [itemType,itemId] = contribution.id.split('_') + $('ul.contributions').append(`
  • `); + }); + } + + // Render users + let users = null; + let totalUsers = null; + if(useTotals) { + if(this.cachedUsers === null) { + this.cachedUsers = this.getUsers(filteredData); + } + if(this.cachedTotalUsers === null) { + this.cachedTotalUsers = Object.keys(this.cachedUsers).length; + } + users = this.cachedUsers; + totalUsers = this.cachedTotalUsers; + } else { + users = this.getUsers(filteredData); + totalUsers = Object.keys(users).length; + } + + // Add count users + if(useTotals) { + $('#users h2 span.total').html(totalUsers); + $('#users h2 span.partial').html(''); + } else { + $('#users h2 span.partial').html(`${totalUsers} /`); + } + + if($('ul.users').html().length === 0) { + // Display users by timestamp + users.forEach(user => { + $('ul.users').append(`
  • `); + }) + } + + this.updateComments(filteredData); + this.updateVotes(filteredData); + this.updateFollowings(filteredData); + this.updateEndorsements(filteredData); + + users.forEach(user => { + $(`.users span[data-id="${user.id}"]`).addClass(`active${user.group}`); + }) + + contributions.forEach(contribution => { + const [itemType,itemId] = contribution.id.split('_') + $(`.contributions span[data-id="${itemId}"][data-type="${itemType}"]`).addClass(`active${contribution.group}`); + }) + + this.renderHistogram("#users-histogram", users); + this.renderHistogram("#contributions-histogram", contributions); + } + + resetActiveElements() { + for(let i=1;i<=10;i++) { + $(`.users span.active${i}`).removeClass(`active${i}`); + $(`.contributions span.active${i}`).removeClass(`active${i}`); + } + } +} diff --git a/decidim-core/app/packs/src/decidim/index.js b/decidim-core/app/packs/src/decidim/index.js index e558fa269893e..b8801772fea7d 100644 --- a/decidim-core/app/packs/src/decidim/index.js +++ b/decidim-core/app/packs/src/decidim/index.js @@ -16,6 +16,7 @@ import addInputEmoji from "src/decidim/input_emoji" import dialogMode from "src/decidim/dialog_mode" import FocusGuard from "src/decidim/focus_guard" import backToListLink from "src/decidim/back_to_list" +import ActivityBrowser from "src/decidim/activity_browser" window.Decidim = window.Decidim || {}; window.Decidim.config = new Configuration() @@ -25,6 +26,7 @@ window.Decidim.FormValidator = FormValidator; window.Decidim.DataPicker = DataPicker; window.Decidim.CommentsComponent = CommentsComponent; window.Decidim.addInputEmoji = addInputEmoji; +window.Decidim.ActivityBrowser = ActivityBrowser; $(() => { window.theDataPicker = new DataPicker($(".data-picker")); diff --git a/decidim-core/app/views/decidim/activity_browser/data.html.erb b/decidim-core/app/views/decidim/activity_browser/data.html.erb new file mode 100644 index 0000000000000..257cc5642cb1a --- /dev/null +++ b/decidim-core/app/views/decidim/activity_browser/data.html.erb @@ -0,0 +1 @@ +foo diff --git a/decidim-core/app/views/decidim/activity_browser/index.html.erb b/decidim-core/app/views/decidim/activity_browser/index.html.erb new file mode 100644 index 0000000000000..e27d864163f3e --- /dev/null +++ b/decidim-core/app/views/decidim/activity_browser/index.html.erb @@ -0,0 +1,96 @@ +
    +

    Date

    +

    + Start timer - + Last month - + Last year - + All +

    +
    + +
    +

    People

    +
    +
      +
      +
      +
      + +
      +

      Interactions

      +
      +
      +
      +
      +
      +
      +
      + +
      +

      Contributions

      +
      +
        +
        +
        +
        +
        + + + + + + + + diff --git a/decidim-core/config/routes.rb b/decidim-core/config/routes.rb index b04a36a081ef7..32d73b157e5d6 100644 --- a/decidim-core/config/routes.rb +++ b/decidim-core/config/routes.rb @@ -31,6 +31,9 @@ post "omniauth_registrations" => "devise/omniauth_registrations#create" end + get "/activity-browser", to: "activity_browser#index" + get "/activity-browser-data", to: "activity_browser#data" + resource :manifest, only: [:show] resource :locale, only: [:create] From 28e5813ecb746a300facd46d096e753144a548ac Mon Sep 17 00:00:00 2001 From: Eduardo Martinez Echevarria Date: Fri, 10 Jun 2022 12:26:52 +0200 Subject: [PATCH 2/8] Add migration to store activities --- ...0220610083030_create_decidim_activities.rb | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 decidim-core/db/migrate/20220610083030_create_decidim_activities.rb diff --git a/decidim-core/db/migrate/20220610083030_create_decidim_activities.rb b/decidim-core/db/migrate/20220610083030_create_decidim_activities.rb new file mode 100644 index 0000000000000..b20e3e86ca6fc --- /dev/null +++ b/decidim-core/db/migrate/20220610083030_create_decidim_activities.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +class CreateDecidimActivities < ActiveRecord::Migration[6.1] + def change + create_table :decidim_activities do |t| + t.datetime :timestamp + t.string :item_type + t.integer :item_id + t.string :target_type + t.integer :target_id + t.integer :decidim_user_id + t.string :participatory_space_type + t.integer :participatory_space_id + t.integer :decidim_organization_id + t.string :item_url + t.string :target_url + t.string :participatory_space_url + end + end +end From ba169efae011c6a04fa80d61b55cc14b81cba89d Mon Sep 17 00:00:00 2001 From: Eduardo Martinez Echevarria Date: Fri, 10 Jun 2022 12:27:15 +0200 Subject: [PATCH 3/8] Define Decidim::Activity model --- decidim-core/app/models/decidim/activity.rb | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 decidim-core/app/models/decidim/activity.rb diff --git a/decidim-core/app/models/decidim/activity.rb b/decidim-core/app/models/decidim/activity.rb new file mode 100644 index 0000000000000..a1c39a08b9076 --- /dev/null +++ b/decidim-core/app/models/decidim/activity.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Decidim + # This class stores data of activities related with + # creation of users, follows and resources. + class Activity < ApplicationRecord + self.table_name = "decidim_activities" + + belongs_to :organization, foreign_key: "decidim_organization_id", class_name: "Decidim::Organization" + end +end From 309e8c2fa531e6578cf8180519eed67661fcfe37 Mon Sep 17 00:00:00 2001 From: Eduardo Martinez Echevarria Date: Fri, 10 Jun 2022 12:30:32 +0200 Subject: [PATCH 4/8] Define tasks to generate decidim activities --- .../lib/tasks/decidim_activities.rake | 570 ++++++++++++++++++ 1 file changed, 570 insertions(+) create mode 100644 decidim-core/lib/tasks/decidim_activities.rake diff --git a/decidim-core/lib/tasks/decidim_activities.rake b/decidim-core/lib/tasks/decidim_activities.rake new file mode 100644 index 0000000000000..93a903d3b7d77 --- /dev/null +++ b/decidim-core/lib/tasks/decidim_activities.rake @@ -0,0 +1,570 @@ +# frozen_string_literal: true + +namespace :decidim do + namespace :activities do + desc "Displays a set of stats of the organizations" + task report: [:environment] do + heading "Stats for #{Decidim::Organization.count} organization(s):" + hr + heading "Users" + report "Users", Decidim::User.count + report "Users with activity", users_with_activity + + heading "Comments" + report "Comments", Decidim::Comments::Comment.count + report "Comment votes", Decidim::Comments::CommentVote.count + + heading "Proposals" + report "Proposals", Decidim::Proposals::Proposal.count + report "Proposal votes", Decidim::Proposals::ProposalVote.count + report "Proposal endorsements", Decidim::Endorsement.where(resource_type: "Decidim::Proposals::Proposal").count + + heading "Meetings" + report "Meetings", Decidim::Meetings::Meeting.count + report "Meeting registrations", Decidim::Meetings::Registration.count + + heading "Debates" + report "Debates", Decidim::Debates::Debate.count + report "Debates endorsements", Decidim::Endorsement.where(resource_type: "Decidim::Debates::Debate").count + + heading "Participatory Processes" + report "Participatory Processes", Decidim::ParticipatoryProcess.count + + if model_present?("Decidim::Assembly") + heading "Assemblies" + report "Assemblies", Decidim::Assembly.count + report "Assembly Members", Decidim::AssemblyMember.count + end + + if model_present?("Decidim::Initiative") + heading "Initiatives" + report "Initiatives", Decidim::Initiative.count + report "Initiatives votes", Decidim::InitiativesVote.count + end + hr + end + + desc "Extracts decidim activities" + task activities_csv: [:environment] do + path = "/tmp/decidim_activities_report.csv" + File.binwrite(path, Decidim::Exporters::CSV.new(activities_collection).export(",").read) + end + + desc "Loads decidim activities on a previous interval if defined" + task :load_activities, [:minutes_interval] => :environment do |_t, args| + interval = args[:minutes_interval].present? ? args[:minutes_interval].to_i.minutes.ago : nil + activities_collection(interval).each do |activity| + next if Decidim::Activity.exists?(item_type: activity[:item_type], item_id: activity[:item_id]) + + Decidim::Activity.create(activity.except(:item_title, :target_title, :participatory_space_title)) + end + end + + TYPES_CONVERSIONS = { + "Decidim::Comments::Comment" => "comment", + "Decidim::Comments::CommentVote" => "comment_vote", + "Decidim::Debates::Debate" => "debate", + "Decidim::Endorsement" => "endorsement", + "Decidim::Follow" => "following", + "Decidim::Meetings::Meeting" => "meeting", + "Decidim::Meetings::Registration" => "meeting_registration", + "Decidim::ParticipatoryProcess" => "participatory_process", + "Decidim::Proposals::Proposal" => "proposal", + "Decidim::Proposals::ProposalVote" => "proposal_vote", + "Decidim::Initiative" => "initiative", + "Decidim::InitiativesVote" => "initiative_vote", + "Decidim::Pages::Page" => "page", + "Decidim::Budgets::Project" => "budget_project", + "Decidim::UserBaseEntity" => "user", + "Decidim::User" => "user", + "Decidim::Assembly" => "assembly", + "Decidim::AssemblyMember" => "assembly_member", + "Decidim::Proposals::CollaborativeDraft" => "proposal_collaborative_draft", + "Decidim::Accountability::Result" => "accountability_result", + "Decidim::Blogs::Post" => "blog_post", + "Decidim::Consultations::Question" => "consultation_question", + "Decidim::Votings::Voting" => "voting", + "Decidim::Conference" => "conference" + }.freeze + + RESOURCES_WITHOUT_TITLE = [ + "Decidim::AssemblyMember", + "Decidim::Comments::Comment", + "Decidim::Endorsement", + "Decidim::Follow", + "Decidim::Meetings::Registration", + "Decidim::Proposals::ProposalVote", + "Decidim::Comments::CommentVote", + "Decidim::InitiativesVote" + ] + + + RESOURCES_WITHOUT_URL = [ + "Decidim::AssemblyMember", + "Decidim::Endorsement", + "Decidim::Follow", + "Decidim::Meetings::Registration", + "Decidim::Proposals::ProposalVote", + "Decidim::Comments::CommentVote", + "Decidim::InitiativesVote" + ] + + def activities_collection(time_interval = nil) + [ + ["Decidim::User", :present_user], + ["Decidim::Comments::Comment", :present_comment], + ["Decidim::Comments::CommentVote", :present_comment_vote], + ["Decidim::Proposals::Proposal", :present_proposal], + ["Decidim::Proposals::CollaborativeDraft", :present_proposal], + ["Decidim::Proposals::ProposalVote", :present_proposal_vote], + ["Decidim::Endorsement", :present_endorsement], + ["Decidim::Meetings::Meeting", :present_meeting], + ["Decidim::Meetings::Registration", :present_meeting_registration], + ["Decidim::Debates::Debate", :present_debate], + ["Decidim::ParticipatoryProcess", :present_participatory_process], + ["Decidim::Assembly", :present_assembly], + ["Decidim::AssemblyMember", :present_assembly_member], + ["Decidim::Initiative", :present_initiative], + ["Decidim::InitiativesVote", :present_initiatives_vote], + ["Decidim::Follow", :present_follow], + ["Decidim::Blogs::Post", :present_post] + ].map do |(model, presenter)| + next [] unless model_present?(model) + + items = time_interval.present? ? model.constantize.where("created_at >= ?", time_interval) : model.constantize.all + + items.map do |item| + transform_data(send(presenter, item)) + end + end.compact.flatten + end + + def transform_data(data) + data[:timestamp] = data[:timestamp].strftime("%Y-%m-%d") + data[:item_type] = type_map(data[:item_type]) + data[:target_type] = type_map(data[:target_type]) + data[:participatory_space_type] = type_map(data[:participatory_space_type]) + data + end + + def type_map(type) + return if type.blank? + + TYPES_CONVERSIONS[type].presence || "unknown: #{type}" + end + + def present_user(item) + { + timestamp: item.created_at, + item_type: item.class.name, + item_id: item.id, + target_type: nil, + target_id: nil, + decidim_user_id: nil, + participatory_space_type: nil, + participatory_space_id: nil, + decidim_organization_id: item.decidim_organization_id, + item_title: locator_title(item), + item_url: user_url(item), + target_title: nil, + target_url: nil, + participatory_space_title: nil, + participatory_space_url: nil + } + end + + def present_comment(item) + { + timestamp: item.created_at, + item_type: item.class.name, + item_id: item.id, + target_type: item.decidim_commentable_type, + target_id: item.decidim_commentable_id, + decidim_user_id: item.decidim_author_id, + participatory_space_type: item.component&.participatory_space_type, + participatory_space_id: item.component&.participatory_space_id, + decidim_organization_id: item.commentable&.organization&.id, + item_title: locator_title(item), + item_url: comment_url(item), + target_title: locator_title(item.commentable), + target_url: locator_url(item.commentable), + participatory_space_title: locator_title(item.component&.participatory_space), + participatory_space_url: locator_url(item.component&.participatory_space) + } + end + + def present_comment_vote(item) + { + timestamp: item.created_at, + item_type: item.class.name, + item_id: item.id, + target_type: "Decidim::Comments::Comment", + target_id: item.decidim_comment_id, + decidim_user_id: nil, + participatory_space_type: item.comment&.component&.participatory_space_type, + participatory_space_id: item.comment&.component&.participatory_space_id, + decidim_organization_id: item.comment&.organization&.id, + item_title: locator_title(item.comment), + item_url: locator_url(item.comment), + target_title: locator_title(item.comment.commentable), + target_url: locator_url(item.comment.commentable), + participatory_space_title: locator_title(item.comment.component&.participatory_space), + participatory_space_url: locator_url(item.comment.component&.participatory_space) + } + end + + def present_proposal(item) + space_title = locator_title(item.component&.participatory_space) + space_url = locator_url(item.component&.participatory_space) + { + timestamp: item.created_at, + item_type: item.class.name, + item_id: item.id, + target_type: item.component&.participatory_space_type, + target_id: item.component&.participatory_space_id, + decidim_user_id: item.coauthorships.first&.decidim_author_id, + participatory_space_type: item.component&.participatory_space_type, + participatory_space_id: item.component&.participatory_space_id, + decidim_organization_id: item.organization&.id, + item_title: locator_title(item), + item_url: locator_url(item), + target_title: space_title, + target_url: space_url, + participatory_space_title: space_title, + participatory_space_url: space_url + } + end + + def present_proposal_vote(item) + { + timestamp: item.created_at, + item_type: item.class.name, + item_id: item.id, + target_type: "Decidim::Proposals::Proposal", + target_id: item.decidim_proposal_id, + decidim_user_id: nil, + participatory_space_type: item.proposal&.component&.participatory_space_type, + participatory_space_id: item.proposal&.component&.participatory_space_id, + decidim_organization_id: item.proposal&.organization&.id, + item_title: locator_title(item), + item_url: locator_url(item), + target_title: locator_title(item.proposal), + target_url: locator_url(item.proposal), + participatory_space_title: locator_title(item.proposal&.component&.participatory_space), + participatory_space_url: locator_url(item.proposal&.component&.participatory_space) + } + end + + def present_endorsement(item) + { + timestamp: item.created_at, + item_type: item.class.name, + item_id: item.id, + target_type: item.resource_type, + target_id: item.resource_id, + decidim_user_id: item.decidim_author_id, + participatory_space_type: item.resource&.try(:component)&.participatory_space_type, + participatory_space_id: item.resource&.try(:component)&.participatory_space_id, + decidim_organization_id: item.resource&.organization&.id, + item_title: locator_title(item), + item_url: locator_url(item), + target_title: locator_title(item.resource), + target_url: locator_url(item.resource), + participatory_space_title: locator_title(item.resource&.try(:component)&.participatory_space), + participatory_space_url: locator_url(item.resource&.try(:component)&.participatory_space) + } + end + + def present_meeting(item) + space_title = locator_title(item.component&.participatory_space) + space_url = locator_url(item.component&.participatory_space) + { + timestamp: item.created_at, + item_type: item.class.name, + item_id: item.id, + target_type: item.component&.participatory_space_type, + target_id: item.component&.participatory_space_id, + decidim_user_id: item.decidim_author_id, + participatory_space_type: item.component&.participatory_space_type, + participatory_space_id: item.component&.participatory_space_id, + decidim_organization_id: item.organization&.id, + item_title: locator_title(item), + item_url: locator_url(item), + target_title: space_title, + target_url: space_url, + participatory_space_title: space_title, + participatory_space_url: space_url + } + end + + def present_meeting_registration(item) + { + timestamp: item.created_at, + item_type: item.class.name, + item_id: item.id, + target_type: "Decidim::Meetings::Meeting", + target_id: item.decidim_meeting_id, + decidim_user_id: item.decidim_user_id, + participatory_space_type: item.meeting&.component&.participatory_space_type, + participatory_space_id: item.meeting&.component&.participatory_space_id, + decidim_organization_id: item.meeting&.organization&.id, + item_title: locator_title(item), + item_url: locator_url(item), + target_title: locator_title(item.meeting), + target_url: locator_url(item.meeting), + participatory_space_title: locator_title(item.meeting&.component&.participatory_space), + participatory_space_url: locator_url(item.meeting&.component&.participatory_space) + } + end + + def present_debate(item) + space_title = locator_title(item.component&.participatory_space) + space_url = locator_url(item.component&.participatory_space) + { + timestamp: item.created_at, + item_type: item.class.name, + item_id: item.id, + target_type: item.component&.participatory_space_type, + target_id: item.component&.participatory_space_id, + decidim_user_id: item.decidim_author_id, + participatory_space_type: item.component&.participatory_space_type, + participatory_space_id: item.component&.participatory_space_id, + decidim_organization_id: item.organization&.id, + item_title: locator_title(item), + item_url: locator_url(item), + target_title: space_title, + target_url: space_url, + participatory_space_title: space_title, + participatory_space_url: space_url + } + end + + def present_participatory_process(item) + { + timestamp: item.created_at, + item_type: item.class.name, + item_id: item.id, + target_type: nil, + target_id: nil, + decidim_user_id: nil, + participatory_space_type: "Decidim::ParticipatoryProcess", + participatory_space_id: item.id, + decidim_organization_id: item.decidim_organization_id, + item_title: locator_title(item), + item_url: locator_url(item), + target_title: nil, + target_url: nil, + participatory_space_title: locator_title(item), + participatory_space_url: locator_url(item) + } + end + + def present_assembly(item) + { + timestamp: item.created_at, + item_type: item.class.name, + item_id: item.id, + target_type: nil, + target_id: nil, + decidim_user_id: nil, + participatory_space_type: "Decidim::Assembly", + participatory_space_id: item.id, + decidim_organization_id: item.decidim_organization_id, + item_title: locator_title(item), + item_url: locator_url(item), + target_title: nil, + target_url: nil, + participatory_space_title: locator_title(item), + participatory_space_url: locator_url(item) + } + end + + def present_assembly_member(item) + space_title = locator_title(item.assembly) + space_url = locator_url(item.assembly) + { + timestamp: item.created_at, + item_type: item.class.name, + item_id: item.id, + target_type: "Decidim::Assembly", + target_id: item.decidim_assembly_id, + decidim_user_id: item.decidim_user_id, + participatory_space_type: "Decidim::Assembly", + participatory_space_id: item.decidim_assembly_id, + decidim_organization_id: item.organization&.id, + item_title: locator_title(item), + item_url: locator_url(item), + target_title: space_title, + target_url: space_url, + participatory_space_title: space_title, + participatory_space_url: space_url + } + end + + def present_initiative(item) + space_title = locator_title(item) + space_url = locator_url(item) + { + timestamp: item.created_at, + item_type: item.class.name, + item_id: item.id, + target_type: nil, + target_id: nil, + decidim_user_id: item.decidim_author_id, + participatory_space_type: "Decidim::Initiative", + participatory_space_id: item.id, + decidim_organization_id: item.decidim_organization_id, + item_title: space_title, + item_url: space_url, + target_title: nil, + target_url: nil, + participatory_space_title: space_title, + participatory_space_url: space_url + } + end + + def present_initiatives_vote(item) + space_title = locator_title(item.initiative) + space_url = locator_url(item.initiative) + { + timestamp: item.created_at, + item_type: item.class.name, + item_id: item.id, + target_type: "Decidim::Initiative", + target_id: item.decidim_initiative_id, + decidim_user_id: nil, + participatory_space_type: "Decidim::Initiative", + participatory_space_id: item.decidim_initiative_id, + decidim_organization_id: item.initiative&.decidim_organization_id, + item_title: locator_title(item), + item_url: locator_url(item), + target_title: space_title, + target_url: space_url, + participatory_space_title: space_title, + participatory_space_url: space_url + } + end + + def present_follow(item) + { + timestamp: item.created_at, + item_type: item.class.name, + item_id: item.id, + target_type: item.decidim_followable_type, + target_id: item.decidim_followable_id, + decidim_user_id: item.decidim_user_id, + participatory_space_type: item.followable&.try(:component)&.participatory_space_type, + participatory_space_id: item.followable&.try(:component)&.participatory_space_id, + decidim_organization_id: item.user&.organization&.id, + item_title: locator_title(item), + item_url: locator_url(item), + target_title: locator_title(item.followable), + target_url: locator_url(item.followable), + participatory_space_title: locator_title(item.followable&.try(:component)&.participatory_space), + participatory_space_url: locator_url(item.followable&.try(:component)&.participatory_space) + } + end + + def present_post(item) + space_title = locator_title(item.component&.participatory_space) + space_url = locator_url(item.component&.participatory_space) + { + timestamp: item.created_at, + item_type: item.class.name, + item_id: item.id, + target_type: item.component&.participatory_space_type, + target_id: item.component&.participatory_space_id, + decidim_user_id: item.decidim_author_id, + participatory_space_type: item.component&.participatory_space_type, + participatory_space_id: item.component&.participatory_space_id, + decidim_organization_id: item.organization&.id, + item_title: locator_title(item), + item_url: locator_url(item), + target_title: space_title, + target_url: space_url, + participatory_space_title: space_title, + participatory_space_url: space_url + } + end + + def locator_url(item) + return if item.blank? + return if RESOURCES_WITHOUT_URL.any? { |class_name| item.is_a?(class_name.constantize) rescue false } + + if item.is_a?(Decidim::Comments::Comment) + comment_url(item) + elsif item.is_a?(Decidim::Budgets::Project) + item.polymorphic_resource_url({}) + elsif [Decidim::User, Decidim::UserGroup, Decidim::UserBaseEntity].any? { |klass| item.is_a?(klass) } + user_url(item) + else + ::Decidim::ResourceLocatorPresenter.new(item).url + end + rescue => e + "FAIL" + end + + def locator_title(item) + return if item.blank? + return if RESOURCES_WITHOUT_TITLE.any? { |class_name| item.is_a?(class_name.constantize) rescue false } + return user_title(item) if [Decidim::User, Decidim::UserGroup, Decidim::UserBaseEntity].any? { |klass| item.is_a?(klass) } + + item.try(:title) || item.try(:name) || item.try(:subject) || "#{resource.model_name.human} ##{resource.id}" + rescue + "0000_FAIL" + end + + def user_title(item) + item.nickname + end + + def comment_url(item) + item.reported_content_url + rescue + "FAIL" + end + + def user_url(item) + Decidim::UserPresenter.new(item).profile_url + end + + def hr(extra = "\n") + puts "===========================================#{extra}" + end + + def report(title, value) + puts " * #{title}: #{value}" + end + + def heading(title) + puts "\n== #{title}" + end + + def users_with_activity + [ + ["Decidim::Comments::Comment", :decidim_author_id], + ["Decidim::Comments::CommentVote", :decidim_author_id], + ["Decidim::Coauthorship", :decidim_author_id], + ["Decidim::Proposals::ProposalVote", :decidim_author_id], + ["Decidim::Endorsement", :decidim_author_id], + ["Decidim::Meetings::Meeting", :decidim_author_id], + ["Decidim::Meetings::Registration", :decidim_user_id], + ["Decidim::Debates::Debate", :decidim_author_id], + ["Decidim::Initiative", :decidim_author_id], + ["Decidim::InitiativesVote", :decidim_author_id], + ["Decidim::Follow", :decidim_user_id] + ].map do |(model, attribute)| + next [] unless model_present?(model) + + model.constantize.select(attribute).distinct.pluck(attribute) + end.compact.flatten.uniq.count + end + + def model_present?(model_name) + model_name.constantize.is_a? Class + rescue NameError + heading "#{model_name} not installed" + false + end + end +end From daf71f75d0dc0d26a6fc2c985fae027545c20ad0 Mon Sep 17 00:00:00 2001 From: Eduardo Martinez Echevarria Date: Fri, 10 Jun 2022 12:30:57 +0200 Subject: [PATCH 5/8] Replace FAIL messages with nil --- decidim-core/lib/tasks/decidim_activities.rake | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/decidim-core/lib/tasks/decidim_activities.rake b/decidim-core/lib/tasks/decidim_activities.rake index 93a903d3b7d77..35a5c11e96514 100644 --- a/decidim-core/lib/tasks/decidim_activities.rake +++ b/decidim-core/lib/tasks/decidim_activities.rake @@ -501,7 +501,7 @@ namespace :decidim do ::Decidim::ResourceLocatorPresenter.new(item).url end rescue => e - "FAIL" + nil end def locator_title(item) @@ -511,7 +511,7 @@ namespace :decidim do item.try(:title) || item.try(:name) || item.try(:subject) || "#{resource.model_name.human} ##{resource.id}" rescue - "0000_FAIL" + nil end def user_title(item) @@ -521,7 +521,7 @@ namespace :decidim do def comment_url(item) item.reported_content_url rescue - "FAIL" + nil end def user_url(item) From 11d0deef5a64c09996d42c9cff6dae79b430bc20 Mon Sep 17 00:00:00 2001 From: Eduardo Martinez Echevarria Date: Fri, 10 Jun 2022 13:11:45 +0200 Subject: [PATCH 6/8] Take activities data from Decidim::Activity --- .../app/controllers/decidim/activity_browser_controller.rb | 5 +++-- decidim-core/app/packs/src/decidim/activity_browser.js | 6 +++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/decidim-core/app/controllers/decidim/activity_browser_controller.rb b/decidim-core/app/controllers/decidim/activity_browser_controller.rb index c18022f939651..bf265a388eaa2 100644 --- a/decidim-core/app/controllers/decidim/activity_browser_controller.rb +++ b/decidim-core/app/controllers/decidim/activity_browser_controller.rb @@ -17,9 +17,10 @@ def data private def activity_data - return File.read('/Users/fernando/Desktop/decidim_activities_report.csv') + # return File.read('/Users/fernando/Desktop/decidim_activities_report.csv') + return Decidim::Exporters::CSV.new(Decidim::Activity.where(organization: current_organization).map(&:attributes)).export(",").read - headers = %w(timestamp item_type item_id target_type target_id user_id participatory_space_id participatory_space_type) + headers = %w(timestamp item_type item_id target_type target_id decidim_user_id participatory_space_id participatory_space_type) CSV.generate do |csv| # Header diff --git a/decidim-core/app/packs/src/decidim/activity_browser.js b/decidim-core/app/packs/src/decidim/activity_browser.js index 065a429946ddd..d92e5243d57c8 100644 --- a/decidim-core/app/packs/src/decidim/activity_browser.js +++ b/decidim-core/app/packs/src/decidim/activity_browser.js @@ -123,7 +123,7 @@ export default class ActivityBrowser { let $element = $(e.currentTarget); let id = $element.data('id').toString(); - const filteredData = this.arr.filter(i => (i.user_id === id || (i.target_type === 'user' && i.target_id === i.user_id))); + const filteredData = this.arr.filter(i => (i.decidim_user_id === id || (i.target_type === 'user' && i.target_id === i.decidim_user_id))); this.refreshData(filteredData, false); }, (e) => { if(this.timerActive || this.nodeSelected) { return false; } @@ -172,8 +172,8 @@ export default class ActivityBrowser { getUsers(arr) { const usersWithCount = arr.reduce((sums,i) => { - if(i.user_id !== undefined && i.user_id !== null && i.user_id.toString().length > 0) { - const key = i.user_id + if(i.decidim_user_id !== undefined && i.decidim_user_id !== null && i.decidim_user_id.toString().length > 0) { + const key = i.decidim_user_id if(!(key in sums)) { sums[key] = {count: 0, timestamp: i.timestamp, item_url: i.item_url} } sums[key].count++; } From 47394fac7d360d3428523ffafc083e18f05841c2 Mon Sep 17 00:00:00 2001 From: Fernando Blat Date: Fri, 10 Jun 2022 13:28:20 +0200 Subject: [PATCH 7/8] Adjust for bigger size --- .../app/packs/src/decidim/activity_browser.js | 96 ++++++++++++++----- .../decidim/activity_browser/index.html.erb | 6 +- 2 files changed, 76 insertions(+), 26 deletions(-) diff --git a/decidim-core/app/packs/src/decidim/activity_browser.js b/decidim-core/app/packs/src/decidim/activity_browser.js index d92e5243d57c8..a0028229bf4c1 100644 --- a/decidim-core/app/packs/src/decidim/activity_browser.js +++ b/decidim-core/app/packs/src/decidim/activity_browser.js @@ -18,6 +18,7 @@ export default class ActivityBrowser { this.timerActive = false; this.datesInterval = null; this.currentDateIndex = 0; + this.selectedRange = 'all'; } run() { @@ -28,17 +29,17 @@ export default class ActivityBrowser { tippy('.contributions span'); // Click on date ranges - $('[data-time-range]').on('click', (e) => { + $(document).on("click", '[data-time-range]', (e) => { $('[data-time-range]').removeClass('bold'); const $element = $(e.currentTarget); $element.addClass('bold'); - const range = $element.data('time-range'); + this.selectedRange = $element.data('time-range'); let filteredData = this.arr; const today = new Date(); let fromRange; - switch(range) { + switch(this.selectedRange) { case 'month': fromRange = new Date(new Date().setDate(today.getDate() - 30)) filteredData = this.filterDataByDateRange(fromRange, this.arr); @@ -56,7 +57,7 @@ export default class ActivityBrowser { }); // Click on timer - $('[data-timer]').on('click', () => { + $(document).on("click", '[data-timer]', (e) => { if (this.timerActive) { this.timerActive = false; clearInterval(this.datesInterval); @@ -82,7 +83,7 @@ export default class ActivityBrowser { }); // Click on contributons - $('ul.contributions span').click((e) => { + $(document).on("click", 'ul.contributons span', (e) => { if(this.nodeSelected === null) { const $element = $(e.currentTarget); this.nodeSelected = {type: $element.data('type'), id: $element.data('id') } @@ -92,7 +93,7 @@ export default class ActivityBrowser { }); // Click on users - $('ul.users span').click((e) => { + $(document).on("click", 'ul.users span', (e) => { if(this.nodeSelected === null) { const $element = $(e.currentTarget); this.nodeSelected = {type: $element.data('type'), id: $element.data('id') } @@ -102,7 +103,7 @@ export default class ActivityBrowser { }); // Mouse over on contributions - $('ul.contributions span').hover((e) => { + $(document).on("hover", 'ul.contributions span', (e) => { if(this.timerActive || this.nodeSelected) { return false; } let $element = $(e.currentTarget); @@ -117,7 +118,7 @@ export default class ActivityBrowser { }); // Mouse over on users - $('ul.users span').hover((e) => { + $(document).on("mouseenter", 'ul.users span', (e) => { if(this.timerActive || this.nodeSelected) { return false; } let $element = $(e.currentTarget); @@ -125,7 +126,8 @@ export default class ActivityBrowser { const filteredData = this.arr.filter(i => (i.decidim_user_id === id || (i.target_type === 'user' && i.target_id === i.decidim_user_id))); this.refreshData(filteredData, false); - }, (e) => { + }) + $(document).on("mouseleave", "ul.users span", (e) => { if(this.timerActive || this.nodeSelected) { return false; } this.refreshData(this.arr, true); }); @@ -171,14 +173,62 @@ export default class ActivityBrowser { } getUsers(arr) { - const usersWithCount = arr.reduce((sums,i) => { - if(i.decidim_user_id !== undefined && i.decidim_user_id !== null && i.decidim_user_id.toString().length > 0) { - const key = i.decidim_user_id - if(!(key in sums)) { sums[key] = {count: 0, timestamp: i.timestamp, item_url: i.item_url} } - sums[key].count++; + let usersTotal = new Set(); + let usersWithActivity = new Set(); + let usersWithCount = {}; + arr.forEach(i => { + if(i.item_type === "user"){ + usersTotal.add(i.item_id) + } else if (i.user_id !== undefined && i.user_id !== null && i.user_id.toString().length > 0) { + usersTotal.add(i.user_id) + usersWithActivity.add(i.user_id) } - return sums; - }, {}); + }); + + console.log(usersTotal.size, usersWithActivity.size, this.selectedRange); + + // Truncate to 3000 nodes + if(usersTotal.size > 3000) { + if(this.selectedRange === 'all') { + usersWithCount = arr.reduce((sums,i) => { + if(i.user_id !== undefined && i.user_id !== null && i.user_id.toString().length > 0) { + const key = i.user_id + if(!(key in sums)) { sums[key] = {count: 0, timestamp: i.timestamp, item_url: i.item_url } } + sums[key].count++; + } + return sums; + }, {}); + } else { + const pendingSlots = 3000 - usersWithActivity.size; + let pending = 0; + usersWithCount = arr.reduce((sums,i) => { + const key = i.item_id + if(i.item_type === "user") { + if(pending <= pendingSlots) { + if(!(key in sums)) { sums[key] = {count: 0, timestamp: i.timestamp, item_url: i.item_url } } + sums[key].count++; + pending++; + } + } else if(i.user_id !== undefined && i.user_id !== null && i.user_id.toString().length > 0) { + if(!(key in sums)) { sums[key] = {count: 0, timestamp: i.timestamp, item_url: i.item_url } } + sums[key].count++; + } + return sums; + }, {}); + } + } else { + usersWithCount = arr.reduce((sums,i) => { + const key = i.item_id + if(i.item_type === "user") { + if(!(key in sums)) { sums[key] = {count: 0, timestamp: i.timestamp, item_url: i.item_url } } + sums[key].count++; + } else if(i.user_id !== undefined && i.user_id !== null && i.user_id.toString().length > 0) { + if(!(key in sums)) { sums[key] = {count: 0, timestamp: i.timestamp, item_url: i.item_url } } + sums[key].count++; + } + return sums; + }, {}); + } return this.mapToSortedArrayWithClass(usersWithCount, 10); } @@ -328,8 +378,6 @@ export default class ActivityBrowser { // Scale the range of the data in the y domain y.domain([0, max(bins, function(d) { return d.length; })]); - debugger - // append the bar rectangles to the svg element svg.selectAll("rect") .data(bins) @@ -378,13 +426,14 @@ export default class ActivityBrowser { $('#contributions h2 span.partial').html(`${totalContributions} /`); } - if($('ul.contributions').html().length === 0) { + $('ul.contributions').html('') + //if($('ul.contributions').html().length === 0) { // Display contributions by timestamp contributions.forEach((contribution) => { const [itemType,itemId] = contribution.id.split('_') $('ul.contributions').append(`
      • `); }); - } + //} // Render users let users = null; @@ -411,12 +460,13 @@ export default class ActivityBrowser { $('#users h2 span.partial').html(`${totalUsers} /`); } - if($('ul.users').html().length === 0) { + $('ul.users').html('') + //if($('ul.users').html().length === 0) { // Display users by timestamp users.forEach(user => { - $('ul.users').append(`
      • `); + $('ul.users').append(`
      • `); }) - } + //} this.updateComments(filteredData); this.updateVotes(filteredData); diff --git a/decidim-core/app/views/decidim/activity_browser/index.html.erb b/decidim-core/app/views/decidim/activity_browser/index.html.erb index e27d864163f3e..e05acdee1270f 100644 --- a/decidim-core/app/views/decidim/activity_browser/index.html.erb +++ b/decidim-core/app/views/decidim/activity_browser/index.html.erb @@ -8,7 +8,7 @@

        -
        +

        People

          @@ -16,7 +16,7 @@
          -
          +

          Interactions

          @@ -26,7 +26,7 @@
          -
          +

          Contributions

            From b5427a69943d3d4a8e53cc84966b14200061a3b7 Mon Sep 17 00:00:00 2001 From: Fernando Blat Date: Fri, 10 Jun 2022 13:48:13 +0200 Subject: [PATCH 8/8] Latest updated --- .../decidim/activity_browser_controller.rb | 2 +- .../app/packs/src/decidim/activity_browser.js | 108 ++++++++++-------- 2 files changed, 60 insertions(+), 50 deletions(-) diff --git a/decidim-core/app/controllers/decidim/activity_browser_controller.rb b/decidim-core/app/controllers/decidim/activity_browser_controller.rb index bf265a388eaa2..a75e66b772554 100644 --- a/decidim-core/app/controllers/decidim/activity_browser_controller.rb +++ b/decidim-core/app/controllers/decidim/activity_browser_controller.rb @@ -17,7 +17,7 @@ def data private def activity_data - # return File.read('/Users/fernando/Desktop/decidim_activities_report.csv') + return File.read('/Users/fernando/Desktop/decidim_activities_report.csv') return Decidim::Exporters::CSV.new(Decidim::Activity.where(organization: current_organization).map(&:attributes)).export(",").read headers = %w(timestamp item_type item_id target_type target_id decidim_user_id participatory_space_id participatory_space_type) diff --git a/decidim-core/app/packs/src/decidim/activity_browser.js b/decidim-core/app/packs/src/decidim/activity_browser.js index a0028229bf4c1..5596ec56aaf4e 100644 --- a/decidim-core/app/packs/src/decidim/activity_browser.js +++ b/decidim-core/app/packs/src/decidim/activity_browser.js @@ -103,7 +103,7 @@ export default class ActivityBrowser { }); // Mouse over on contributions - $(document).on("hover", 'ul.contributions span', (e) => { + $(document).on("mouseenter", 'ul.contributions span', (e) => { if(this.timerActive || this.nodeSelected) { return false; } let $element = $(e.currentTarget); @@ -112,7 +112,9 @@ export default class ActivityBrowser { const filteredData = this.arr.filter(i => ((i.item_type === type && i.item_id === id) || (i.target_type === type && i.target_id === id))); this.refreshData(filteredData, false); - }, (e) => { + }) + + $(document).on("mouseleave", "ul.contributions span", (e) => { if(this.timerActive || this.nodeSelected) { return false; } this.refreshData(this.arr, true); }); @@ -127,6 +129,7 @@ export default class ActivityBrowser { const filteredData = this.arr.filter(i => (i.decidim_user_id === id || (i.target_type === 'user' && i.target_id === i.decidim_user_id))); this.refreshData(filteredData, false); }) + $(document).on("mouseleave", "ul.users span", (e) => { if(this.timerActive || this.nodeSelected) { return false; } this.refreshData(this.arr, true); @@ -187,48 +190,57 @@ export default class ActivityBrowser { console.log(usersTotal.size, usersWithActivity.size, this.selectedRange); - // Truncate to 3000 nodes - if(usersTotal.size > 3000) { - if(this.selectedRange === 'all') { - usersWithCount = arr.reduce((sums,i) => { - if(i.user_id !== undefined && i.user_id !== null && i.user_id.toString().length > 0) { - const key = i.user_id - if(!(key in sums)) { sums[key] = {count: 0, timestamp: i.timestamp, item_url: i.item_url } } - sums[key].count++; - } - return sums; - }, {}); - } else { - const pendingSlots = 3000 - usersWithActivity.size; - let pending = 0; - usersWithCount = arr.reduce((sums,i) => { - const key = i.item_id - if(i.item_type === "user") { - if(pending <= pendingSlots) { - if(!(key in sums)) { sums[key] = {count: 0, timestamp: i.timestamp, item_url: i.item_url } } - sums[key].count++; - pending++; - } - } else if(i.user_id !== undefined && i.user_id !== null && i.user_id.toString().length > 0) { - if(!(key in sums)) { sums[key] = {count: 0, timestamp: i.timestamp, item_url: i.item_url } } - sums[key].count++; - } - return sums; - }, {}); + // // Truncate to 3000 nodes + // if(usersTotal.size > 3000) { + // if(this.selectedRange === 'all') { + // usersWithCount = arr.reduce((sums,i) => { + // if(i.user_id !== undefined && i.user_id !== null && i.user_id.toString().length > 0) { + // const key = i.user_id + // if(!(key in sums)) { sums[key] = {count: 0, timestamp: i.timestamp, item_url: i.item_url } } + // sums[key].count++; + // } + // return sums; + // }, {}); + // } else { + // const pendingSlots = 3000 - usersWithActivity.size; + // let pending = 0; + // usersWithCount = arr.reduce((sums,i) => { + // const key = i.item_id + // if(i.item_type === "user") { + // if(pending <= pendingSlots) { + // if(!(key in sums)) { sums[key] = {count: 0, timestamp: i.timestamp, item_url: i.item_url } } + // sums[key].count++; + // pending++; + // } + // } else if(i.user_id !== undefined && i.user_id !== null && i.user_id.toString().length > 0) { + // if(!(key in sums)) { sums[key] = {count: 0, timestamp: i.timestamp, item_url: i.item_url } } + // sums[key].count++; + // } + // return sums; + // }, {}); + // } + // } else { + // usersWithCount = arr.reduce((sums,i) => { + // const key = i.item_id + // if(i.item_type === "user") { + // if(!(key in sums)) { sums[key] = {count: 0, timestamp: i.timestamp, item_url: i.item_url } } + // sums[key].count++; + // } else if(i.user_id !== undefined && i.user_id !== null && i.user_id.toString().length > 0) { + // if(!(key in sums)) { sums[key] = {count: 0, timestamp: i.timestamp, item_url: i.item_url } } + // sums[key].count++; + // } + // return sums; + // }, {}); + // } + + usersWithCount = arr.reduce((sums,i) => { + const key = i.user_id + if(i.user_id !== undefined && i.user_id !== null && i.user_id.toString().length > 0) { + if(!(key in sums)) { sums[key] = {count: 0, timestamp: i.timestamp, item_url: i.item_url } } + sums[key].count++; } - } else { - usersWithCount = arr.reduce((sums,i) => { - const key = i.item_id - if(i.item_type === "user") { - if(!(key in sums)) { sums[key] = {count: 0, timestamp: i.timestamp, item_url: i.item_url } } - sums[key].count++; - } else if(i.user_id !== undefined && i.user_id !== null && i.user_id.toString().length > 0) { - if(!(key in sums)) { sums[key] = {count: 0, timestamp: i.timestamp, item_url: i.item_url } } - sums[key].count++; - } - return sums; - }, {}); - } + return sums; + }, {}); return this.mapToSortedArrayWithClass(usersWithCount, 10); } @@ -426,14 +438,13 @@ export default class ActivityBrowser { $('#contributions h2 span.partial').html(`${totalContributions} /`); } - $('ul.contributions').html('') - //if($('ul.contributions').html().length === 0) { + if($('ul.contributions').html().length === 0) { // Display contributions by timestamp contributions.forEach((contribution) => { const [itemType,itemId] = contribution.id.split('_') $('ul.contributions').append(`
          • `); }); - //} + } // Render users let users = null; @@ -460,13 +471,12 @@ export default class ActivityBrowser { $('#users h2 span.partial').html(`${totalUsers} /`); } - $('ul.users').html('') - //if($('ul.users').html().length === 0) { + if($('ul.users').html().length === 0) { // Display users by timestamp users.forEach(user => { $('ul.users').append(`
          • `); }) - //} + } this.updateComments(filteredData); this.updateVotes(filteredData);