decidim-comparative_stats 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (91) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE-AGPLv3.txt +661 -0
  3. data/README.md +187 -0
  4. data/Rakefile +40 -0
  5. data/app/assets/config/admin/comparative_stats_manifest.css +4 -0
  6. data/app/assets/config/admin/comparative_stats_manifest.js +2 -0
  7. data/app/assets/config/comparative_stats_manifest.css +4 -0
  8. data/app/assets/config/comparative_stats_manifest.js +2 -0
  9. data/app/assets/images/bcn-logo.png +0 -0
  10. data/app/assets/images/decidim/comparative_stats/icon.svg +1 -0
  11. data/app/assets/images/platoniq-logo.png +0 -0
  12. data/app/assets/javascripts/decidim/comparative_stats/geocoded_events.js.es6 +94 -0
  13. data/app/assets/javascripts/decidim/comparative_stats/graphs.js.es6 +13 -0
  14. data/app/assets/stylesheets/decidim/comparative_stats/geocoded_events.scss +23 -0
  15. data/app/assets/stylesheets/decidim/comparative_stats/graphs.scss +6 -0
  16. data/app/assets/stylesheets/decidim/comparative_stats/widget.scss +8 -0
  17. data/app/cells/decidim/comparative_stats/metric_piecharts/show.erb +14 -0
  18. data/app/cells/decidim/comparative_stats/metric_piecharts_cell.rb +39 -0
  19. data/app/cells/decidim/comparative_stats/metric_timelines/show.erb +14 -0
  20. data/app/cells/decidim/comparative_stats/metric_timelines_cell.rb +23 -0
  21. data/app/cells/decidim/comparative_stats/participatory_processes_timeline/show.erb +55 -0
  22. data/app/cells/decidim/comparative_stats/participatory_processes_timeline_cell.rb +51 -0
  23. data/app/cells/decidim/comparative_stats/participatory_spaces_geocoded_events/show.erb +55 -0
  24. data/app/cells/decidim/comparative_stats/participatory_spaces_geocoded_events_cell.rb +108 -0
  25. data/app/commands/decidim/comparative_stats/admin/create_endpoint.rb +46 -0
  26. data/app/commands/decidim/comparative_stats/admin/destroy_endpoint.rb +43 -0
  27. data/app/commands/decidim/comparative_stats/admin/update_endpoint.rb +47 -0
  28. data/app/controllers/decidim/comparative_stats/admin/application_controller.rb +16 -0
  29. data/app/controllers/decidim/comparative_stats/admin/endpoints_controller.rb +83 -0
  30. data/app/controllers/decidim/comparative_stats/admin/graphs_controller.rb +16 -0
  31. data/app/controllers/decidim/comparative_stats/widgets_controller.rb +31 -0
  32. data/app/forms/decidim/comparative_stats/admin/endpoint_form.rb +26 -0
  33. data/app/helpers/decidim/comparative_stats/application_helper.rb +27 -0
  34. data/app/models/decidim/comparative_stats/application_record.rb +10 -0
  35. data/app/models/decidim/comparative_stats/endpoint.rb +22 -0
  36. data/app/permissions/decidim/comparative_stats/admin/permissions.rb +24 -0
  37. data/app/presenters/decidim/comparative_stats/admin_log/endpoint_presenter.rb +46 -0
  38. data/app/views/decidim/comparative_stats/admin/endpoints/_form.html.erb +7 -0
  39. data/app/views/decidim/comparative_stats/admin/endpoints/edit.html.erb +16 -0
  40. data/app/views/decidim/comparative_stats/admin/endpoints/index.html.erb +66 -0
  41. data/app/views/decidim/comparative_stats/admin/endpoints/new.html.erb +16 -0
  42. data/app/views/decidim/comparative_stats/admin/graphs/show.html.erb +15 -0
  43. data/app/views/decidim/comparative_stats/widgets/_all.html.erb +1 -0
  44. data/app/views/decidim/comparative_stats/widgets/_embed.html.erb +7 -0
  45. data/app/views/decidim/comparative_stats/widgets/_embed_modal.html.erb +27 -0
  46. data/app/views/decidim/comparative_stats/widgets/_global_stats.html.erb +4 -0
  47. data/app/views/decidim/comparative_stats/widgets/_global_stats_timeline.html.erb +4 -0
  48. data/app/views/decidim/comparative_stats/widgets/_processes_timeline.html.erb +4 -0
  49. data/app/views/decidim/comparative_stats/widgets/_spaces_geocoded_events.html.erb +5 -0
  50. data/app/views/decidim/comparative_stats/widgets/_tabs.html.erb +19 -0
  51. data/app/views/decidim/comparative_stats/widgets/show.html.erb +1 -0
  52. data/app/views/layouts/decidim/admin/comparative_stats.html.erb +28 -0
  53. data/app/views/layouts/decidim/comparative_stats/widget.html.erb +22 -0
  54. data/config/i18n-tasks.yml +10 -0
  55. data/config/locales/ca.yml +57 -0
  56. data/config/locales/cs.yml +57 -0
  57. data/config/locales/en.yml +64 -0
  58. data/config/locales/es.yml +57 -0
  59. data/db/migrate/20191219104548_create_decidim_comparative_stats_endpoints.rb +13 -0
  60. data/db/migrate/20200122072955_add_name_version_to_comparative_stats_endpoints.rb +8 -0
  61. data/db/migrate/20200130203914_rename_version_field_in_comparative_stats_endpoints.rb +7 -0
  62. data/db/seeds.rb +42 -0
  63. data/lib/decidim/comparative_stats.rb +22 -0
  64. data/lib/decidim/comparative_stats/admin.rb +10 -0
  65. data/lib/decidim/comparative_stats/admin_engine.rb +50 -0
  66. data/lib/decidim/comparative_stats/api_fetcher.rb +93 -0
  67. data/lib/decidim/comparative_stats/cached_http_adapter.rb +23 -0
  68. data/lib/decidim/comparative_stats/engine.rb +34 -0
  69. data/lib/decidim/comparative_stats/queries/global_events.graphql +84 -0
  70. data/lib/decidim/comparative_stats/queries/global_history_metrics.graphql +10 -0
  71. data/lib/decidim/comparative_stats/queries/global_metrics.graphql +6 -0
  72. data/lib/decidim/comparative_stats/queries/name_and_version.graphql +6 -0
  73. data/lib/decidim/comparative_stats/queries/participatory_processes.graphql +12 -0
  74. data/lib/decidim/comparative_stats/queries/v022/global_events.graphql +76 -0
  75. data/lib/decidim/comparative_stats/test/factories.rb +13 -0
  76. data/lib/decidim/comparative_stats/version.rb +10 -0
  77. data/vendor/assets/images/draw/layers-2x.png +0 -0
  78. data/vendor/assets/images/draw/layers.png +0 -0
  79. data/vendor/assets/images/draw/marker-icon-2x.png +0 -0
  80. data/vendor/assets/images/draw/marker-icon.png +0 -0
  81. data/vendor/assets/images/draw/marker-shadow.png +0 -0
  82. data/vendor/assets/images/draw/spritesheet-2x.png +0 -0
  83. data/vendor/assets/images/draw/spritesheet.png +0 -0
  84. data/vendor/assets/images/draw/spritesheet.svg +156 -0
  85. data/vendor/assets/images/images/layers-2x.png +0 -0
  86. data/vendor/assets/images/images/layers.png +0 -0
  87. data/vendor/assets/images/images/marker-icon-2x.png +0 -0
  88. data/vendor/assets/images/images/marker-icon.png +0 -0
  89. data/vendor/assets/images/images/marker-shadow.png +0 -0
  90. data/vendor/assets/javascripts/leaflet.js +5 -0
  91. metadata +234 -0
@@ -0,0 +1,14 @@
1
+ <div class="row">
2
+ <% metrics.each do |name, metric| %>
3
+ <div class="column" style="padding: 1em">
4
+ <div class="card">
5
+ <div class="card-divider">
6
+ <%= title name %>
7
+ </div>
8
+ <div class="card-section">
9
+ <%= line_chart metric, id: "line_chart_#{name}" %>
10
+ </div>
11
+ </div>
12
+ </div>
13
+ <% end %>
14
+ </div>
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module ComparativeStats
5
+ class MetricTimelinesCell < MetricPiechartsCell
6
+ def metrics
7
+ history = {}
8
+ endpoints.each do |endpoint|
9
+ endpoint.api.fetch_global_history_metrics.data.metrics.each do |item|
10
+ history[item.name] ||= []
11
+ history[item.name] << {
12
+ name: endpoint.name,
13
+ data: item.history.map do |i|
14
+ [i.key, i.value]
15
+ end.to_h
16
+ }
17
+ end
18
+ end
19
+ history
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,55 @@
1
+ <div class="row column full-height">
2
+ <%= timeline [], id: "participatoryProcessesChart", height: "100%" %>
3
+ </div>
4
+ <script>
5
+ (function() {
6
+ var rows = [];
7
+ <% timeline_graph.each do |row| %>
8
+ rows.push([
9
+ "<%= row[:name] %>",
10
+ "<%= row[:title] %>",
11
+ new Date("<%= row[:start_date] %>"),
12
+ new Date("<%= row[:end_date] %>")
13
+ ]);
14
+ <% end %>
15
+
16
+ var drawChart = function() {
17
+ var dataTable = new google.visualization.DataTable();
18
+ dataTable.addColumn({ type: 'string', id: 'Platform' });
19
+ dataTable.addColumn({ type: 'string', id: 'Process' });
20
+ dataTable.addColumn({ type: 'date', id: 'Start' });
21
+ dataTable.addColumn({ type: 'date', id: 'End' });
22
+ dataTable.addRows(rows);
23
+ var chart = Chartkick.charts["participatoryProcessesChart"];
24
+ chart.getChartObject().draw(dataTable);
25
+
26
+ // hack the real height
27
+ var h = parseInt($("#participatoryProcessesChart div:first-child div:first-child div:first-child div svg").attr("height")) + 70;
28
+ chart.getChartObject().draw(dataTable, {
29
+ height: h,
30
+ // hAxis: {
31
+ // minValue: new Date(2019, 0, 0),
32
+ // maxValue: new Date(2020, 0, 0)
33
+ // },
34
+ width: "100%",
35
+ timeline: {
36
+ // showRowLabels: false,
37
+ rowLabelStyle: {
38
+ fontSize: 16
39
+ },
40
+ // showBarLabels: false
41
+ }
42
+ });
43
+ // extra svg tweaks
44
+ $('#participatoryProcessesChart div div div svg g:first text').attr({'x': 5, "text-anchor": "start", "font-weight": "bold"})
45
+ };
46
+
47
+ google.charts.setOnLoadCallback(drawChart);
48
+
49
+ // Rewdraw on tab appearance
50
+ $("[data-tabs]").on("change.zf.tabs", drawChart);
51
+ // redraw graph when window resize is completed
52
+ $(window).on('resizeEnd', drawChart);
53
+
54
+ }());
55
+ </script>
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "chartkick"
4
+ require "chartkick/helper"
5
+
6
+ module Decidim
7
+ module ComparativeStats
8
+ # This cell renders an graph with participatory processes
9
+ # the `model` is expected to be a collection of API endpoints
10
+ #
11
+ class ParticipatoryProcessesTimelineCell < Decidim::ViewModel
12
+ include Chartkick::Helper
13
+
14
+ def show
15
+ return unless model
16
+
17
+ render :show
18
+ end
19
+
20
+ def endpoints
21
+ model
22
+ end
23
+
24
+ def timeline_graph
25
+ rows = []
26
+ endpoints.each do |endpoint|
27
+ endpoint.api.fetch_participatory_processes.data.participatory_processes.each do |item|
28
+ next unless item.start_date
29
+
30
+ start_date = Date.parse(item.start_date)
31
+ end_date = Date.parse(item.end_date.presence || Date.current.end_of_year.to_s)
32
+ # let's not trust people writing proper ordered dates
33
+ start_date, end_date = end_date, start_date if start_date > end_date
34
+ rows << {
35
+ name: endpoint.name,
36
+ title: first_text(item.title.translations),
37
+ start_date: start_date,
38
+ end_date: end_date
39
+ }
40
+ end
41
+ end
42
+ rows
43
+ end
44
+
45
+ def first_text(translations)
46
+ item = translations.find { |i| i.text.present? }
47
+ item&.text || ""
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,55 @@
1
+ <template id="marker-popup-meeting">
2
+ <div class="map-info__content">
3
+ <h3>${title}</h3>
4
+ <div id="bodyContent">
5
+ <p>{{html description}}</p>
6
+ <div class="map__date-adress">
7
+ <div class="card__datetime">
8
+ <div class="card__datetime__date">
9
+ ${startTimeDay} <span class="card__datetime__month">${startTimeMonth} ${startTimeYear}</span>
10
+ </div>
11
+ <div class="card__datetime__time">${starTime}</div>
12
+ </div>
13
+ <div class="address card__extra">
14
+ <div class="address__icon">{{html icon}}</div>
15
+ <div class="address__details">
16
+ <strong>{{html location}}</strong><br>
17
+ <span>${address}</span><br>
18
+ <span>{{html locationHints}}</span>
19
+ </div>
20
+ </div>
21
+ </div>
22
+ <div class="map-info__button">
23
+ <a href="${link}" class="button button--sc">
24
+ <%= t("decidim.meetings.meetings_map.view_meeting") %>
25
+ </a>
26
+ </div>
27
+ </div>
28
+ </div>
29
+ </template>
30
+
31
+ <template id="marker-popup-proposal">
32
+ <div class="map-info__content">
33
+ <h3>${title}</h3>
34
+ <div id="bodyContent">
35
+ <p>{{html body}}</p>
36
+ <div class="map__date-adress">
37
+ <div class="address card__extra">
38
+ <div class="address__icon">{{html icon}}</div>
39
+ <div class="address__details">
40
+ <span>${address}</span><br>
41
+ </div>
42
+ </div>
43
+ </div>
44
+ <div class="map-info__button">
45
+ <a href="${link}" class="button button--sc">
46
+ <%= t("decidim.proposals.proposals.index.view_proposal") %>
47
+ </a>
48
+ </div>
49
+ </div>
50
+ </div>
51
+ </template>
52
+
53
+ <div class="row column full-height">
54
+ <%= content_tag(:div, "", id: "geocoded_events", class: "map", data: { geocoded_events: geocoded_events }) %>
55
+ </div>
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module ComparativeStats
5
+ # This cell renders a map with participatory spaces
6
+ # the `model` is spected to be a collection of API endpoints
7
+ class ParticipatorySpacesGeocodedEventsCell < Decidim::ViewModel
8
+ include Decidim::MapHelper
9
+ include Decidim::LayoutHelper
10
+
11
+ def show
12
+ return unless model
13
+
14
+ render :show
15
+ end
16
+
17
+ def endpoints
18
+ model
19
+ end
20
+
21
+ def geocoded_events
22
+ @events = {}
23
+
24
+ endpoints.each do |endpoint|
25
+ # skip endpoints under version 0.21
26
+ next unless endpoint.api.valid? "0.21"
27
+
28
+ @events[endpoint.id] = {
29
+ name: endpoint.name,
30
+ meetings: {},
31
+ proposals: {}
32
+ }
33
+ results = endpoint.api.fetch_global_events
34
+ next unless results.respond_to? :data
35
+
36
+ results.data.assemblies.each do |assembly|
37
+ assembly.components.each do |component|
38
+ if component.respond_to? :meetings
39
+ component.meetings.edges.each do |edge|
40
+ add_meeting(edge.node.to_h, endpoint, assembly, component, :assemblies)
41
+ end
42
+ elsif componet.respond_to? :proposals
43
+ component.proposals.edges.each do |edge|
44
+ add_proposal(edge.node.to_h, endpoint, assembly, component, :assemblies)
45
+ end
46
+ end
47
+ end
48
+ end
49
+ results.data.participatory_processes.each do |participatory_process|
50
+ participatory_process.components.each do |component|
51
+ if component.respond_to? :meetings
52
+ component.meetings.edges.each do |edge|
53
+ add_meeting(edge.node.to_h, endpoint, participatory_process, component, :processes)
54
+ end
55
+ elsif component.respond_to? :proposals
56
+ component.proposals.edges.each do |edge|
57
+ add_proposal(edge.node.to_h, endpoint, participatory_process, component, :processes)
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+ @events.to_json
64
+ end
65
+
66
+ def first_translation(text)
67
+ return text unless text.is_a? Hash
68
+
69
+ translations = text["translations"]
70
+ if translations
71
+ item = translations.find { |i| i["text"].present? }
72
+ return item["text"] || "" if item
73
+ end
74
+ ""
75
+ end
76
+
77
+ def add_proposal(proposal, endpoint, participatory_space, component, type)
78
+ @events[endpoint.id][:proposals]["#{type}_proposal_#{proposal["id"]}"] = {
79
+ latitude: proposal["coordinates"]["latitude"],
80
+ longitude: proposal["coordinates"]["longitude"],
81
+ address: proposal["address"],
82
+ title: first_translation(proposal["title"]),
83
+ body: truncate(first_translation(proposal["body"]), length: 100),
84
+ icon: icon("proposals", width: 40, height: 70, remove_icon_class: true),
85
+ link: endpoint.endpoint.remove("api") << "#{type}/#{participatory_space.slug}/f/#{component.id}/proposals/#{proposal["id"]}"
86
+ }
87
+ end
88
+
89
+ def add_meeting(meeting, endpoint, participatory_space, component, type)
90
+ @events[endpoint.id][:meetings]["#{type}_meeting_#{meeting["id"]}"] = {
91
+ latitude: meeting["coordinates"]["latitude"],
92
+ longitude: meeting["coordinates"]["longitude"],
93
+ address: meeting["address"],
94
+ title: first_translation(meeting["title"]),
95
+ # description: first_translation(meeting["description"]),
96
+ startTimeDay: l(meeting["startTime"].to_date, format: "%d"),
97
+ startTimeMonth: l(meeting["startTime"].to_date, format: "%B"),
98
+ startTimeYear: l(meeting["startTime"].to_date, format: "%Y"),
99
+ startTime: "#{meeting["startTime"].to_date.strftime("%H:%M")} - #{meeting["endTime"].to_date.strftime("%H:%M")}",
100
+ icon: icon("meetings", width: 40, height: 70, remove_icon_class: true),
101
+ location: first_translation(meeting["location"]),
102
+ locationHints: first_translation(meeting["location_hints"]),
103
+ link: endpoint.endpoint.remove("api") << "#{type}/#{participatory_space.slug}/f/#{component.id}/meetings/#{meeting["id"]}"
104
+ }
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module ComparativeStats
5
+ module Admin
6
+ # A command with all the business logic when creating an endpoint
7
+ class CreateEndpoint < Rectify::Command
8
+ # Public: Initializes the command.
9
+ #
10
+ # form - A form object with the params.
11
+ def initialize(form)
12
+ @form = form
13
+ end
14
+
15
+ # Executes the command. Broadcasts these events:
16
+ #
17
+ # - :ok when everything is valid.
18
+ # - :invalid if the form wasn't valid and we couldn't proceed.
19
+ #
20
+ # Returns nothing.
21
+ def call
22
+ return broadcast(:invalid) if form.invalid?
23
+
24
+ create_endpoint
25
+ broadcast(:ok)
26
+ end
27
+
28
+ private
29
+
30
+ attr_reader :form
31
+
32
+ def create_endpoint
33
+ Decidim.traceability.create!(
34
+ Decidim::ComparativeStats::Endpoint,
35
+ form.current_user,
36
+ endpoint: form.endpoint,
37
+ name: form.context.api.name_and_version.application_name,
38
+ api_version: form.context.api.name_and_version.version,
39
+ organization: form.current_organization,
40
+ active: form.active
41
+ )
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module ComparativeStats
5
+ module Admin
6
+ # A command with all the business logic when destroying an endpoint
7
+ class DestroyEndpoint < Rectify::Command
8
+ # Public: Initializes the command.
9
+ #
10
+ # form - A form object with the params.
11
+ def initialize(endpoint, current_user)
12
+ @endpoint = endpoint
13
+ @current_user = current_user
14
+ end
15
+
16
+ # Executes the command. Broadcasts these events:
17
+ #
18
+ # - :ok when everything is valid.
19
+ # - :invalid if the form wasn't valid and we couldn't proceed.
20
+ #
21
+ # Returns nothing.
22
+ def call
23
+ destroy_endpoint!
24
+ broadcast(:ok)
25
+ end
26
+
27
+ private
28
+
29
+ attr_reader :endpoint, :current_user
30
+
31
+ def destroy_endpoint!
32
+ Decidim.traceability.perform_action!(
33
+ :delete,
34
+ endpoint,
35
+ current_user
36
+ ) do
37
+ endpoint.destroy!
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module ComparativeStats
5
+ module Admin
6
+ # A command with all the business logic when updating an endpoint
7
+ class UpdateEndpoint < Rectify::Command
8
+ # Public: Initializes the command.
9
+ #
10
+ # form - A form object with the params.
11
+ def initialize(endpoint, form, user)
12
+ @endpoint = endpoint
13
+ @form = form
14
+ @user = user
15
+ end
16
+
17
+ # Executes the command. Broadcasts these events:
18
+ #
19
+ # - :ok when everything is valid.
20
+ # - :invalid if the form wasn't valid and we couldn't proceed.
21
+ #
22
+ # Returns nothing.
23
+ def call
24
+ return broadcast(:invalid) if form.invalid?
25
+
26
+ update_endpoint!
27
+ broadcast(:ok)
28
+ end
29
+
30
+ private
31
+
32
+ attr_reader :form
33
+
34
+ def update_endpoint!
35
+ Decidim.traceability.update!(
36
+ @endpoint,
37
+ @user,
38
+ endpoint: form.endpoint,
39
+ name: form.name,
40
+ api_version: form.context.api.name_and_version.version,
41
+ active: form.active
42
+ )
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end