decidim-comparative_stats 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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