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 @@
+
+
+
+
+
+
+
+
+
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
-
+
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);