decidim-comparative_stats 1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE-AGPLv3.txt +661 -0
- data/README.md +187 -0
- data/Rakefile +40 -0
- data/app/assets/config/admin/comparative_stats_manifest.css +4 -0
- data/app/assets/config/admin/comparative_stats_manifest.js +2 -0
- data/app/assets/config/comparative_stats_manifest.css +4 -0
- data/app/assets/config/comparative_stats_manifest.js +2 -0
- data/app/assets/images/bcn-logo.png +0 -0
- data/app/assets/images/decidim/comparative_stats/icon.svg +1 -0
- data/app/assets/images/platoniq-logo.png +0 -0
- data/app/assets/javascripts/decidim/comparative_stats/geocoded_events.js.es6 +94 -0
- data/app/assets/javascripts/decidim/comparative_stats/graphs.js.es6 +13 -0
- data/app/assets/stylesheets/decidim/comparative_stats/geocoded_events.scss +23 -0
- data/app/assets/stylesheets/decidim/comparative_stats/graphs.scss +6 -0
- data/app/assets/stylesheets/decidim/comparative_stats/widget.scss +8 -0
- data/app/cells/decidim/comparative_stats/metric_piecharts/show.erb +14 -0
- data/app/cells/decidim/comparative_stats/metric_piecharts_cell.rb +39 -0
- data/app/cells/decidim/comparative_stats/metric_timelines/show.erb +14 -0
- data/app/cells/decidim/comparative_stats/metric_timelines_cell.rb +23 -0
- data/app/cells/decidim/comparative_stats/participatory_processes_timeline/show.erb +55 -0
- data/app/cells/decidim/comparative_stats/participatory_processes_timeline_cell.rb +51 -0
- data/app/cells/decidim/comparative_stats/participatory_spaces_geocoded_events/show.erb +55 -0
- data/app/cells/decidim/comparative_stats/participatory_spaces_geocoded_events_cell.rb +108 -0
- data/app/commands/decidim/comparative_stats/admin/create_endpoint.rb +46 -0
- data/app/commands/decidim/comparative_stats/admin/destroy_endpoint.rb +43 -0
- data/app/commands/decidim/comparative_stats/admin/update_endpoint.rb +47 -0
- data/app/controllers/decidim/comparative_stats/admin/application_controller.rb +16 -0
- data/app/controllers/decidim/comparative_stats/admin/endpoints_controller.rb +83 -0
- data/app/controllers/decidim/comparative_stats/admin/graphs_controller.rb +16 -0
- data/app/controllers/decidim/comparative_stats/widgets_controller.rb +31 -0
- data/app/forms/decidim/comparative_stats/admin/endpoint_form.rb +26 -0
- data/app/helpers/decidim/comparative_stats/application_helper.rb +27 -0
- data/app/models/decidim/comparative_stats/application_record.rb +10 -0
- data/app/models/decidim/comparative_stats/endpoint.rb +22 -0
- data/app/permissions/decidim/comparative_stats/admin/permissions.rb +24 -0
- data/app/presenters/decidim/comparative_stats/admin_log/endpoint_presenter.rb +46 -0
- data/app/views/decidim/comparative_stats/admin/endpoints/_form.html.erb +7 -0
- data/app/views/decidim/comparative_stats/admin/endpoints/edit.html.erb +16 -0
- data/app/views/decidim/comparative_stats/admin/endpoints/index.html.erb +66 -0
- data/app/views/decidim/comparative_stats/admin/endpoints/new.html.erb +16 -0
- data/app/views/decidim/comparative_stats/admin/graphs/show.html.erb +15 -0
- data/app/views/decidim/comparative_stats/widgets/_all.html.erb +1 -0
- data/app/views/decidim/comparative_stats/widgets/_embed.html.erb +7 -0
- data/app/views/decidim/comparative_stats/widgets/_embed_modal.html.erb +27 -0
- data/app/views/decidim/comparative_stats/widgets/_global_stats.html.erb +4 -0
- data/app/views/decidim/comparative_stats/widgets/_global_stats_timeline.html.erb +4 -0
- data/app/views/decidim/comparative_stats/widgets/_processes_timeline.html.erb +4 -0
- data/app/views/decidim/comparative_stats/widgets/_spaces_geocoded_events.html.erb +5 -0
- data/app/views/decidim/comparative_stats/widgets/_tabs.html.erb +19 -0
- data/app/views/decidim/comparative_stats/widgets/show.html.erb +1 -0
- data/app/views/layouts/decidim/admin/comparative_stats.html.erb +28 -0
- data/app/views/layouts/decidim/comparative_stats/widget.html.erb +22 -0
- data/config/i18n-tasks.yml +10 -0
- data/config/locales/ca.yml +57 -0
- data/config/locales/cs.yml +57 -0
- data/config/locales/en.yml +64 -0
- data/config/locales/es.yml +57 -0
- data/db/migrate/20191219104548_create_decidim_comparative_stats_endpoints.rb +13 -0
- data/db/migrate/20200122072955_add_name_version_to_comparative_stats_endpoints.rb +8 -0
- data/db/migrate/20200130203914_rename_version_field_in_comparative_stats_endpoints.rb +7 -0
- data/db/seeds.rb +42 -0
- data/lib/decidim/comparative_stats.rb +22 -0
- data/lib/decidim/comparative_stats/admin.rb +10 -0
- data/lib/decidim/comparative_stats/admin_engine.rb +50 -0
- data/lib/decidim/comparative_stats/api_fetcher.rb +93 -0
- data/lib/decidim/comparative_stats/cached_http_adapter.rb +23 -0
- data/lib/decidim/comparative_stats/engine.rb +34 -0
- data/lib/decidim/comparative_stats/queries/global_events.graphql +84 -0
- data/lib/decidim/comparative_stats/queries/global_history_metrics.graphql +10 -0
- data/lib/decidim/comparative_stats/queries/global_metrics.graphql +6 -0
- data/lib/decidim/comparative_stats/queries/name_and_version.graphql +6 -0
- data/lib/decidim/comparative_stats/queries/participatory_processes.graphql +12 -0
- data/lib/decidim/comparative_stats/queries/v022/global_events.graphql +76 -0
- data/lib/decidim/comparative_stats/test/factories.rb +13 -0
- data/lib/decidim/comparative_stats/version.rb +10 -0
- data/vendor/assets/images/draw/layers-2x.png +0 -0
- data/vendor/assets/images/draw/layers.png +0 -0
- data/vendor/assets/images/draw/marker-icon-2x.png +0 -0
- data/vendor/assets/images/draw/marker-icon.png +0 -0
- data/vendor/assets/images/draw/marker-shadow.png +0 -0
- data/vendor/assets/images/draw/spritesheet-2x.png +0 -0
- data/vendor/assets/images/draw/spritesheet.png +0 -0
- data/vendor/assets/images/draw/spritesheet.svg +156 -0
- data/vendor/assets/images/images/layers-2x.png +0 -0
- data/vendor/assets/images/images/layers.png +0 -0
- data/vendor/assets/images/images/marker-icon-2x.png +0 -0
- data/vendor/assets/images/images/marker-icon.png +0 -0
- data/vendor/assets/images/images/marker-shadow.png +0 -0
- data/vendor/assets/javascripts/leaflet.js +5 -0
- metadata +234 -0
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "decidim/comparative_stats/admin"
|
4
|
+
require "decidim/comparative_stats/engine"
|
5
|
+
require "decidim/comparative_stats/admin_engine"
|
6
|
+
require "chartkick"
|
7
|
+
|
8
|
+
module Decidim
|
9
|
+
# This namespace holds the logic of the `ComparativeStats` component. This component
|
10
|
+
# allows users to create comparative_stats in a participatory space.
|
11
|
+
module ComparativeStats
|
12
|
+
include ActiveSupport::Configurable
|
13
|
+
|
14
|
+
autoload :ApiFetcher, "decidim/comparative_stats/api_fetcher"
|
15
|
+
autoload :CachedHTTPAdapter, "decidim/comparative_stats/cached_http_adapter"
|
16
|
+
|
17
|
+
# Sets the expiration time for the statistic data.
|
18
|
+
config_accessor :stats_cache_expiration_time do
|
19
|
+
24.hours
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Decidim
|
4
|
+
module ComparativeStats
|
5
|
+
# This is the engine that runs on the public interface of `ComparativeStats`.
|
6
|
+
class AdminEngine < ::Rails::Engine
|
7
|
+
isolate_namespace Decidim::ComparativeStats::Admin
|
8
|
+
|
9
|
+
paths["db/migrate"] = nil
|
10
|
+
paths["lib/tasks"] = nil
|
11
|
+
|
12
|
+
routes do
|
13
|
+
# Add admin engine routes here
|
14
|
+
resources :endpoints
|
15
|
+
get "graphs/:graph", to: "graphs#show", as: :graphs, defaults: { graph: "global_stats" }
|
16
|
+
root to: "endpoints#index"
|
17
|
+
end
|
18
|
+
|
19
|
+
initializer "decidim_comparative_stats.admin_mount_routes" do
|
20
|
+
Decidim::Core::Engine.routes do
|
21
|
+
mount Decidim::ComparativeStats::AdminEngine, at: "/admin/comparative_stats", as: "decidim_admin_comparative_stats"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
initializer "decidim_comparative_stats.add_cells_view_paths" do
|
26
|
+
Cell::ViewModel.view_paths << File.expand_path("#{Decidim::ComparativeStats::Engine.root}/app/cells")
|
27
|
+
Cell::ViewModel.view_paths << File.expand_path("#{Decidim::ComparativeStats::Engine.root}/app/views") # for partials
|
28
|
+
end
|
29
|
+
|
30
|
+
initializer "decidim_comparative_stats.admin_assets" do |app|
|
31
|
+
app.config.assets.precompile += %w(admin/comparative_stats_manifest.js admin/comparative_stats_manifest.css)
|
32
|
+
end
|
33
|
+
|
34
|
+
def load_seed
|
35
|
+
nil
|
36
|
+
end
|
37
|
+
|
38
|
+
initializer "decidim_comparative_stats.admin_menu" do
|
39
|
+
Decidim.menu :admin_menu do |menu|
|
40
|
+
menu.item I18n.t("menu.comparative_stats", scope: "decidim.admin"),
|
41
|
+
decidim_admin_comparative_stats.endpoints_path,
|
42
|
+
icon_name: "graph",
|
43
|
+
position: 3.5,
|
44
|
+
active: is_active_link?(decidim_admin_comparative_stats.endpoints_path, :inclusive) ||
|
45
|
+
is_active_link?(decidim_admin_comparative_stats.graphs_path, :inclusive)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "graphlient"
|
4
|
+
require "decidim/comparative_stats/version"
|
5
|
+
|
6
|
+
module Decidim
|
7
|
+
module ComparativeStats
|
8
|
+
# Class used to fetch and validate Decidim API calls
|
9
|
+
class ApiFetcher
|
10
|
+
def initialize(endpoint)
|
11
|
+
@errors = []
|
12
|
+
@queries = {}
|
13
|
+
@endpoint = endpoint
|
14
|
+
end
|
15
|
+
|
16
|
+
attr_reader :errors, :endpoint
|
17
|
+
|
18
|
+
attr_writer :client
|
19
|
+
|
20
|
+
def client
|
21
|
+
@client ||= Graphlient::Client.new(endpoint,
|
22
|
+
http: CachedHTTPAdapter,
|
23
|
+
http_options: {
|
24
|
+
read_timeout: 20,
|
25
|
+
write_timeout: 30
|
26
|
+
})
|
27
|
+
end
|
28
|
+
|
29
|
+
# When creating name and version are fetched from the api
|
30
|
+
# Update action should allow the user to change the name but not the version
|
31
|
+
def name_and_version
|
32
|
+
@name_and_version ||= fetch_name_and_version.data.decidim
|
33
|
+
end
|
34
|
+
|
35
|
+
# Queries the GraphQL api using one of files in lib/decidim/comparative_stats/queries
|
36
|
+
def query(tag)
|
37
|
+
@queries[tag] ||= client.query versioned_query(tag)
|
38
|
+
rescue Faraday::Error
|
39
|
+
@errors << "Not a valid Decidim API URL"
|
40
|
+
rescue Graphlient::Errors::Error => e
|
41
|
+
@errors << e.message
|
42
|
+
end
|
43
|
+
|
44
|
+
# Checks if is a valid Decidim API URL
|
45
|
+
def valid?(min_version = MIN_API_VERSION)
|
46
|
+
response = fetch_name_and_version
|
47
|
+
return false unless response
|
48
|
+
|
49
|
+
if Gem::Version.new(min_version) > Gem::Version.new(response.data.decidim.version)
|
50
|
+
@errors << "Decidim version detect (#{response.data.decidim.version}) should be at least #{min_version}"
|
51
|
+
return false
|
52
|
+
end
|
53
|
+
true
|
54
|
+
end
|
55
|
+
|
56
|
+
# returns the last error
|
57
|
+
def error
|
58
|
+
@errors.last
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def versioned_query(tag)
|
64
|
+
file = File.join(__dir__, "queries", "#{tag}.graphql")
|
65
|
+
raise NameError unless File.exist?(file)
|
66
|
+
|
67
|
+
unless tag == "name_and_version"
|
68
|
+
if Gem::Version.new(name_and_version.version) < Gem::Version.new("0.23")
|
69
|
+
alt_file = File.join(__dir__, "queries", "v022", "#{tag}.graphql")
|
70
|
+
file = alt_file if File.exist?(alt_file)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
File.open(file).read
|
74
|
+
end
|
75
|
+
|
76
|
+
# Syntactic sugar to query graphql queries:
|
77
|
+
# i.e:
|
78
|
+
# query "name_and_version" => fetch_name_and_version
|
79
|
+
def method_missing(name)
|
80
|
+
_, key = name.to_s.split("fetch_")
|
81
|
+
if key
|
82
|
+
query key
|
83
|
+
else
|
84
|
+
super
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def respond_to_missing?(method_name, include_private = false)
|
89
|
+
method_name.to_s.start_with?("fetch_") || super
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "graphlient/adapters/http/http_adapter"
|
4
|
+
|
5
|
+
module Decidim
|
6
|
+
module ComparativeStats
|
7
|
+
class CachedHTTPAdapter < Graphlient::Adapters::HTTP::FaradayAdapter
|
8
|
+
def execute(document:, operation_name: nil, variables: {}, context: {})
|
9
|
+
body = {}
|
10
|
+
# Strip uniq identifier generated by GraphQL class
|
11
|
+
body["query"] = document.to_query_string.gsub(/GraphQL__Client__OperationDefinition_[0-9 ]+/, "")
|
12
|
+
body["variables"] = variables if variables.any?
|
13
|
+
|
14
|
+
Rails.cache.fetch(
|
15
|
+
"graphlient/api_queries/#{url.hash}/#{body.to_json.hash}",
|
16
|
+
expires_in: Decidim::ComparativeStats.stats_cache_expiration_time
|
17
|
+
) do
|
18
|
+
super
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rails"
|
4
|
+
require "decidim/core"
|
5
|
+
|
6
|
+
module Decidim
|
7
|
+
module ComparativeStats
|
8
|
+
# This is the engine that runs on the public interface of comparative_stats.
|
9
|
+
class Engine < ::Rails::Engine
|
10
|
+
isolate_namespace Decidim::ComparativeStats
|
11
|
+
|
12
|
+
routes do
|
13
|
+
# Add engine routes here
|
14
|
+
get "widgets/:graph", to: "widgets#show", as: :widget, defaults: { graph: "global_stats" }
|
15
|
+
root to: "widgets#show"
|
16
|
+
end
|
17
|
+
|
18
|
+
initializer "decidim_comparative_stats.mount_routes" do
|
19
|
+
Decidim::Core::Engine.routes do
|
20
|
+
mount Decidim::ComparativeStats::Engine, at: "/comparative_stats", as: "decidim_comparative_stats"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
initializer "decidim_comparative_stats.add_cells_view_paths" do
|
25
|
+
Cell::ViewModel.view_paths << File.expand_path("#{Decidim::ComparativeStats::Engine.root}/app/cells")
|
26
|
+
Cell::ViewModel.view_paths << File.expand_path("#{Decidim::ComparativeStats::Engine.root}/app/views") # for partials
|
27
|
+
end
|
28
|
+
|
29
|
+
initializer "decidim_comparative_stats.assets" do |app|
|
30
|
+
app.config.assets.precompile += %w(comparative_stats_manifest.js comparative_stats_manifest.css)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
query {
|
2
|
+
assemblies {
|
3
|
+
id
|
4
|
+
slug
|
5
|
+
components (filter: {withGeolocationEnabled: true }) {
|
6
|
+
id
|
7
|
+
__typename
|
8
|
+
...geolocatedMeetings
|
9
|
+
...geolocatedProposals
|
10
|
+
}
|
11
|
+
}
|
12
|
+
participatoryProcesses {
|
13
|
+
id
|
14
|
+
slug
|
15
|
+
components (filter: {withGeolocationEnabled: true }) {
|
16
|
+
id
|
17
|
+
__typename
|
18
|
+
...geolocatedMeetings
|
19
|
+
...geolocatedProposals
|
20
|
+
}
|
21
|
+
}
|
22
|
+
}
|
23
|
+
|
24
|
+
fragment geolocatedMeetings on Meetings {
|
25
|
+
meetings {
|
26
|
+
edges {
|
27
|
+
node {
|
28
|
+
id
|
29
|
+
address
|
30
|
+
title {
|
31
|
+
translations {
|
32
|
+
text
|
33
|
+
}
|
34
|
+
}
|
35
|
+
description {
|
36
|
+
translations {
|
37
|
+
text
|
38
|
+
}
|
39
|
+
}
|
40
|
+
startTime
|
41
|
+
endTime
|
42
|
+
location {
|
43
|
+
translations {
|
44
|
+
text
|
45
|
+
}
|
46
|
+
}
|
47
|
+
locationHints {
|
48
|
+
translations {
|
49
|
+
text
|
50
|
+
}
|
51
|
+
}
|
52
|
+
coordinates {
|
53
|
+
latitude
|
54
|
+
longitude
|
55
|
+
}
|
56
|
+
}
|
57
|
+
}
|
58
|
+
}
|
59
|
+
}
|
60
|
+
|
61
|
+
fragment geolocatedProposals on Proposals {
|
62
|
+
proposals {
|
63
|
+
edges {
|
64
|
+
node {
|
65
|
+
id
|
66
|
+
address
|
67
|
+
title {
|
68
|
+
translations {
|
69
|
+
text
|
70
|
+
}
|
71
|
+
}
|
72
|
+
body {
|
73
|
+
translations {
|
74
|
+
text
|
75
|
+
}
|
76
|
+
}
|
77
|
+
coordinates {
|
78
|
+
latitude
|
79
|
+
longitude
|
80
|
+
}
|
81
|
+
}
|
82
|
+
}
|
83
|
+
}
|
84
|
+
}
|
@@ -0,0 +1,76 @@
|
|
1
|
+
query {
|
2
|
+
assemblies {
|
3
|
+
id
|
4
|
+
slug
|
5
|
+
components (filter: {withGeolocationEnabled: true }) {
|
6
|
+
id
|
7
|
+
__typename
|
8
|
+
...geolocatedMeetings
|
9
|
+
...geolocatedProposals
|
10
|
+
}
|
11
|
+
}
|
12
|
+
participatoryProcesses {
|
13
|
+
id
|
14
|
+
slug
|
15
|
+
components (filter: {withGeolocationEnabled: true }) {
|
16
|
+
id
|
17
|
+
__typename
|
18
|
+
...geolocatedMeetings
|
19
|
+
...geolocatedProposals
|
20
|
+
}
|
21
|
+
}
|
22
|
+
}
|
23
|
+
|
24
|
+
fragment geolocatedMeetings on Meetings {
|
25
|
+
meetings {
|
26
|
+
edges {
|
27
|
+
node {
|
28
|
+
id
|
29
|
+
address
|
30
|
+
title {
|
31
|
+
translations {
|
32
|
+
text
|
33
|
+
}
|
34
|
+
}
|
35
|
+
description {
|
36
|
+
translations {
|
37
|
+
text
|
38
|
+
}
|
39
|
+
}
|
40
|
+
startTime
|
41
|
+
endTime
|
42
|
+
location {
|
43
|
+
translations {
|
44
|
+
text
|
45
|
+
}
|
46
|
+
}
|
47
|
+
locationHints {
|
48
|
+
translations {
|
49
|
+
text
|
50
|
+
}
|
51
|
+
}
|
52
|
+
coordinates {
|
53
|
+
latitude
|
54
|
+
longitude
|
55
|
+
}
|
56
|
+
}
|
57
|
+
}
|
58
|
+
}
|
59
|
+
}
|
60
|
+
|
61
|
+
fragment geolocatedProposals on Proposals {
|
62
|
+
proposals {
|
63
|
+
edges {
|
64
|
+
node {
|
65
|
+
id
|
66
|
+
address
|
67
|
+
title
|
68
|
+
body
|
69
|
+
coordinates {
|
70
|
+
latitude
|
71
|
+
longitude
|
72
|
+
}
|
73
|
+
}
|
74
|
+
}
|
75
|
+
}
|
76
|
+
}
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "decidim/core/test/factories"
|
4
|
+
|
5
|
+
FactoryBot.define do
|
6
|
+
factory :endpoint, class: "Decidim::ComparativeStats::Endpoint" do
|
7
|
+
endpoint { Faker::Internet.url }
|
8
|
+
name { Faker::Name.name }
|
9
|
+
api_version { Decidim::ComparativeStats::MIN_API_VERSION }
|
10
|
+
organization { create :organization }
|
11
|
+
active { true }
|
12
|
+
end
|
13
|
+
end
|