Merge pull request #5503 from consuldemocracy/admin_stats_without_ahoy

Improve stats graphs in admin section
This commit is contained in:
Javi Martín
2024-05-09 14:54:51 +02:00
committed by GitHub
59 changed files with 432 additions and 568 deletions

View File

@@ -5,14 +5,12 @@
var buildGraph;
buildGraph = function(el) {
var conf, url;
url = $(el).data("graph");
var conf;
conf = {
bindto: el,
data: {
x: "x",
url: url,
mimeType: "json"
json: $(el).data("graph")
},
axis: {
x: {

View File

@@ -0,0 +1,11 @@
.stats-event-links {
@include header-font-size(h3);
font-weight: bold;
list-style-type: none;
margin-#{$global-left}: 0;
margin-top: $line-height;
* + * {
margin-top: $line-height;
}
}

View File

@@ -6,7 +6,6 @@ class Admin::Settings::FeaturesTabComponent < ApplicationComponent
feature.google_login
feature.twitter_login
feature.wordpress_login
feature.public_stats
feature.signature_sheets
feature.user.recommendations
feature.user.recommendations_on_debates

View File

@@ -30,7 +30,7 @@
</div>
</div>
<%= render "admin/stats/graph", name: "user_supported_budgets", event: "", count: user_count %>
<%= render Admin::Stats::ChartComponent.new(chart) %>
<table class="investment-projects-summary user-count-by-heading">
<thead>

View File

@@ -28,4 +28,8 @@ class Admin::Stats::BudgetSupportingComponent < ApplicationComponent
[heading, headings_stats[heading.id][:total_participants_support_phase]]
end
end
def chart
@chart ||= Ahoy::Chart.new("budget_investment_supported")
end
end

View File

@@ -0,0 +1,4 @@
<div id="graph" class="small-12 column">
<h2><%= title %></h2>
<%= chart_tag %>
</div>

View File

@@ -0,0 +1,25 @@
class Admin::Stats::ChartComponent < ApplicationComponent
attr_reader :chart
def initialize(chart)
@chart = chart
end
private
def count
chart.count
end
def event
chart.event_name
end
def chart_tag
tag.div("data-graph": chart.data_points.to_json)
end
def title
"#{chart.title} (#{count})"
end
end

View File

@@ -0,0 +1 @@
<%= link_list(*links, class: "stats-event-links") %>

View File

@@ -0,0 +1,20 @@
class Admin::Stats::EventLinksComponent < ApplicationComponent
attr_reader :event_names
use_helpers :link_list
def initialize(event_names)
@event_names = event_names
end
private
def link_text(event)
Ahoy::Chart.new(event).title
end
def links
event_names.map do |event|
[link_text(event), graph_admin_stats_path(event: event)]
end
end
end

View File

@@ -0,0 +1,5 @@
<div class="stats-global-chart">
<h2><%= title %></h2>
<%= chart_tag %>
<%= render Admin::Stats::EventLinksComponent.new(event_names) %>
</div>

View File

@@ -0,0 +1,19 @@
class Admin::Stats::GlobalChartComponent < ApplicationComponent
private
def event_names
Ahoy::Chart.active_event_names
end
def chart_tag
tag.div("data-graph": data_points.to_json)
end
def data_points
Ahoy::Chart.active_events_data_points
end
def title
t("admin.stats.graph.title")
end
end

View File

@@ -1,4 +1,4 @@
<%= back_link_to %>
<%= back_link_to admin_stats_path %>
<%= header %>

View File

@@ -1,30 +0,0 @@
class Admin::Api::StatsController < Admin::Api::BaseController
def show
if params[:event].blank? &&
params[:visits].blank? &&
params[:budget_investments].blank? &&
params[:user_supported_budgets].blank?
return render json: {}, status: :bad_request
end
ds = Ahoy::DataSource.new
if params[:event].present?
ds.add params[:event].titleize, Ahoy::Event.where(name: params[:event]).group_by_day(:time).count
end
if params[:visits].present?
ds.add "Visits", Visit.group_by_day(:started_at).count
end
if params[:budget_investments].present?
ds.add "Budget Investments", Budget::Investment.group_by_day(:created_at).count
end
if params[:user_supported_budgets].present?
ds.add "User supported budgets",
Vote.where(votable_type: "Budget::Investment").group_by_day(:updated_at).count
end
render json: ds.build
end
end

View File

@@ -1,7 +1,5 @@
class Admin::StatsController < Admin::BaseController
def show
@event_types = Ahoy::Event.distinct.order(:name).pluck(:name)
@visits = Visit.count
@debates = Debate.with_hidden.count
@proposals = Proposal.with_hidden.count
@@ -30,14 +28,7 @@ class Admin::StatsController < Admin::BaseController
end
def graph
@name = params[:id]
@event = params[:event]
if params[:event]
@count = Ahoy::Event.where(name: params[:event]).count
else
@count = params[:count]
end
@chart = Ahoy::Chart.new(params[:event])
end
def proposal_notifications

View File

@@ -11,7 +11,6 @@ class ApplicationController < ActionController::Base
before_action :ensure_signup_complete
around_action :switch_locale
before_action :track_email_campaign
before_action :set_return_url
check_authorization unless: :devise_controller?
@@ -91,13 +90,6 @@ class ApplicationController < ActionController::Base
end
end
def track_email_campaign
if params[:track_id]
campaign = Campaign.find_by(track_id: params[:track_id])
ahoy.track campaign.name if campaign.present?
end
end
def set_return_url
if request.get? && !devise_controller? && is_navigational_format?
store_location_for(:user, request.fullpath)

View File

@@ -44,21 +44,6 @@ module CommentableActions
@resources = @search_terms.present? ? resource_relation.search(@search_terms) : nil
end
def create
@resource = resource_model.new(strong_params)
@resource.author = current_user
if @resource.save
track_event
redirect_path = url_for(controller: controller_name, action: :show, id: @resource.id)
redirect_to redirect_path, notice: t("flash.actions.create.#{resource_name.underscore}")
else
load_geozones
set_resource_instance
render :new
end
end
def edit
end
@@ -74,10 +59,6 @@ module CommentableActions
private
def track_event
ahoy.track :"#{resource_name}_created", "#{resource_name}_id": resource.id
end
def tag_cloud
TagCloud.new(resource_model, params[:search])
end

View File

@@ -19,6 +19,17 @@ class DebatesController < ApplicationController
helper_method :resource_model, :resource_name
respond_to :html, :js
def create
@debate = Debate.new(debate_params)
@debate.author = current_user
if @debate.save
redirect_to debate_path(@debate), notice: t("flash.actions.create.debate")
else
render :new
end
end
def index_customization
@featured_debates = @debates.featured
end

View File

@@ -51,7 +51,6 @@ class Legislation::AnnotationsController < Legislation::BaseController
@annotation = @draft_version.annotations.new(annotation_params)
@annotation.author = current_user
if @annotation.save
track_event
render json: @annotation.to_json
else
render json: @annotation.errors.full_messages, status: :unprocessable_entity
@@ -100,12 +99,6 @@ class Legislation::AnnotationsController < Legislation::BaseController
[:quote, :text, ranges: [:start, :startOffset, :end, :endOffset]]
end
def track_event
ahoy.track :legislation_annotation_created,
legislation_annotation_id: @annotation.id,
legislation_draft_version_id: @draft_version.id
end
def convert_ranges_parameters
annotation = params[:legislation_annotation]
if annotation && annotation[:ranges] && annotation[:ranges].is_a?(String)

View File

@@ -12,7 +12,6 @@ class Legislation::AnswersController < Legislation::BaseController
if @process.debate_phase.open?
@answer.user = current_user
@answer.save!
track_event
respond_to do |format|
format.js
format.html { redirect_to legislation_process_question_path(@process, @question) }
@@ -35,11 +34,4 @@ class Legislation::AnswersController < Legislation::BaseController
def allowed_params
[:legislation_question_option_id]
end
def track_event
ahoy.track :legislation_answer_created,
legislation_answer_id: @answer.id,
legislation_question_option_id: @answer.legislation_question_option_id,
legislation_question_id: @answer.legislation_question_id
end
end

View File

@@ -17,7 +17,6 @@ class Management::ProposalsController < Management::BaseController
published_at: Time.current))
if @resource.save
track_event
redirect_path = url_for(controller: controller_name, action: :show, id: @resource.id)
redirect_to redirect_path, notice: t("flash.actions.create.#{resource_name.underscore}")
else

View File

@@ -1,29 +0,0 @@
class StatsController < ApplicationController
include FeatureFlags
feature_flag :public_stats
skip_authorization_check
def index
@visits = daily_cache("visits") { Visit.count }
@debates = daily_cache("debates") { Debate.with_hidden.count }
@proposals = daily_cache("proposals") { Proposal.with_hidden.count }
@comments = daily_cache("comments") { Comment.not_valuations.with_hidden.count }
@debate_votes = daily_cache("debate_votes") { Vote.count_for("Debate") }
@proposal_votes = daily_cache("proposal_votes") { Vote.count_for("Proposal") }
@comment_votes = daily_cache("comment_votes") { Vote.count_for("Comment") }
@investment_votes = daily_cache("budget_investment_votes") { Vote.count_for("Budget::Investment") }
@votes = daily_cache("votes") { Vote.count }
@verified_users = daily_cache("verified_users") { User.with_hidden.level_two_or_three_verified.count }
@unverified_users = daily_cache("unverified_users") { User.with_hidden.unverified.count }
end
private
def daily_cache(key, &)
Rails.cache.fetch("public_stats/#{Time.current.strftime("%Y-%m-%d")}/#{key}", &)
end
end

View File

@@ -28,7 +28,6 @@ class Verification::SmsController < ApplicationController
@sms = Verification::Sms.new(sms_params.merge(user: current_user))
if @sms.verified?
current_user.update!(confirmed_phone: current_user.unconfirmed_phone)
ahoy.track(:level_2_user, user_id: current_user.id) rescue nil
if VerifiedUser.phone?(current_user)
current_user.update(verified_at: Time.current)

View File

@@ -1,34 +1,4 @@
module StatsHelper
def chart_tag(opt = {})
opt[:data] ||= {}
opt[:data][:graph] = admin_api_stats_path(chart_data(opt))
tag.div(**opt)
end
def chart_data(opt = {})
data = nil
if opt[:id].present?
data = { opt[:id] => true }
elsif opt[:event].present?
data = { event: opt[:event] }
end
data
end
def graph_link_text(event)
text = t("admin.stats.graph.#{event}")
if text.to_s.match(/translation missing/)
text = event
end
text
end
def budget_investments_chart_tag(opt = {})
opt[:data] ||= {}
opt[:data][:graph] = admin_api_stats_path(budget_investments: true)
tag.div(**opt)
end
def number_to_stats_percentage(number, options = {})
number_to_percentage(number, { strip_insignificant_zeros: true, precision: 2 }.merge(options))
end

77
app/models/ahoy/chart.rb Normal file
View File

@@ -0,0 +1,77 @@
module Ahoy
class Chart
attr_reader :event_name
delegate :count, to: :records
delegate :t, to: "ApplicationController.helpers"
def initialize(event_name)
@event_name = event_name
end
def self.active_event_names
event_names_with_collections.select { |name, collection| collection.any? }.keys.sort_by do |event_name|
new(event_name).title
end
end
def self.event_names_with_collections
{
budget_investment_created: Budget::Investment.with_hidden,
debate_created: Debate.with_hidden,
legislation_annotation_created: Legislation::Annotation.with_hidden,
legislation_answer_created: Legislation::Answer.with_hidden,
level_3_user: User.with_hidden.level_three_verified,
proposal_created: Proposal.with_hidden
}
end
def self.active_events_data_points
Ahoy::DataSource.build do |data_source|
active_event_names.map { |event_name| new(event_name) }.each do |chart|
data_source.add chart.title, chart.data
end
end
end
def data_points
Ahoy::DataSource.build do |data_source|
data_source.add title, data
end
end
def title
t("admin.stats.graph.#{event_name}")
end
def data
records_by_day.count
end
private
def records
case event_name.to_sym
when :budget_investment_supported
Vote.where(votable_type: "Budget::Investment")
when :visits
Visit.all
else
self.class.event_names_with_collections[event_name.to_sym]
end
end
def records_by_day
raise "Unknown event #{event_name}" unless records.respond_to?(:group_by_day)
records.group_by_day(date_field)
end
def date_field
if event_name.to_sym == :level_3_user
:verified_at
else
:created_at
end
end
end
end

View File

@@ -4,24 +4,28 @@
module Ahoy
class DataSource
def self.build(&block)
new.tap { |data_source| block.call(data_source) }.build
end
# Adds a collection with the datasource
# Name is the name of the collection and will be showed in the
# chart
def add(name, collection)
collections.push data: collection, name: name
collection.each_key { |key| add_key key }
dates.merge(collection.keys)
end
def build
data = { x: [] }
shared_keys.each do |k|
dates.sort.each do |date|
# Add the key with a valid date format
data[:x].push k.strftime("%Y-%m-%d")
data[:x].push date.strftime("%Y-%m-%d")
# Add the value for each column, or 0 if not present
collections.each do |col|
data[col[:name]] ||= []
count = col[:data][k] || 0
count = col[:data][date] || 0
data[col[:name]].push count
end
end
@@ -35,12 +39,8 @@ module Ahoy
@collections ||= []
end
def shared_keys
@shared_keys ||= []
end
def add_key(key)
shared_keys.push(key) unless shared_keys.include? key
def dates
@dates ||= Set.new
end
end
end

View File

@@ -1,2 +0,0 @@
class Campaign < ApplicationRecord
end

View File

@@ -70,7 +70,6 @@ class Setting < ApplicationRecord
"feature.google_login": true,
"feature.twitter_login": true,
"feature.wordpress_login": false,
"feature.public_stats": true,
"feature.signature_sheets": true,
"feature.user.recommendations": true,
"feature.user.recommendations_on_debates": true,

View File

@@ -1,4 +1,5 @@
class Visit < ApplicationRecord
alias_attribute :created_at, :started_at
has_many :ahoy_events, class_name: "Ahoy::Event"
belongs_to :user
end

View File

@@ -1,4 +0,0 @@
<div id="graph" class="small-12 column">
<h2><%= t "admin.stats.graph.#{name || event}" %> (<%= count %>)</h2>
<%= chart_tag id: name, event: event %>
</div>

View File

@@ -1,4 +1,4 @@
<%= back_link_to %>
<%= back_link_to admin_stats_path %>
<h2><%= t("admin.stats.direct_messages.title") %></h2>

View File

@@ -4,4 +4,4 @@
<%= back_link_to admin_stats_path %>
<%= render "graph", name: @name, event: @event, count: @count %>
<%= render Admin::Stats::ChartComponent.new(@chart) %>

View File

@@ -1,4 +1,4 @@
<%= back_link_to %>
<%= back_link_to admin_stats_path %>
<h2 id="top"><%= t("admin.stats.polls.title") %></h2>

View File

@@ -1,4 +1,4 @@
<%= back_link_to %>
<%= back_link_to admin_stats_path %>
<h2><%= t("admin.stats.proposal_notifications.title") %></h2>

View File

@@ -1,3 +1,7 @@
<% content_for :head do %>
<%= javascript_include_tag "stat_graphs", "data-turbolinks-track" => "reload" %>
<% end %>
<div id="stats" class="stats">
<div class="row">
<div class="small-12 column">
@@ -24,7 +28,7 @@
<div class="small-12 medium-3 column">
<p class="featured">
<%= link_to t("admin.stats.show.summary.visits"),
graph_admin_stats_path(id: "visits", count: @visits) %> <br>
graph_admin_stats_path(event: "visits") %> <br>
<span class="number"><%= number_with_delimiter(@visits) %></span>
</p>
<p>
@@ -110,21 +114,7 @@
</div>
<div class="small-12 column">
<% @event_types.each do |event| %>
<h3>
<%= link_to graph_link_text(event),
graph_admin_stats_path(event: event) %>
</h3>
<% end %>
</div>
<% if feature?(:budgets) %>
<div class="small-12 column">
<h2><%= t "admin.stats.show.budgets_title" %></h2>
<%= budget_investments_chart_tag id: "budget_investments" %>
</div>
<% end %>
<%= render Admin::Stats::GlobalChartComponent.new %>
</div>
</div>
</div>

View File

@@ -1,63 +0,0 @@
<div class="stats row-full">
<div class="row">
<div class="small-12 column">
<h1><%= t "admin.stats.show.stats_title" %></h1>
<div class="row stats-numbers">
<div class="small-12 medium-4 column">
<p class="featured">
<%= t "stats.index.visits" %><br>
<span class="number"><%= number_with_delimiter(@visits) %></span>
</p>
<p>
<%= t "stats.index.debates" %><br>
<span class="number"><%= number_with_delimiter(@debates) %></span>
</p>
<p>
<%= t "stats.index.proposals" %><br>
<span class="number"><%= number_with_delimiter(@proposals) %></span>
</p>
<p>
<%= t "stats.index.comments" %><br>
<span class="number"><%= number_with_delimiter(@comments) %></span>
</p>
</div>
<div class="small-12 medium-4 column">
<p class="featured">
<%= t "stats.index.proposal_votes" %><br>
<span class="number"><%= number_with_delimiter(@proposal_votes) %><br></span>
</p>
<p>
<%= t "stats.index.debate_votes" %><br>
<span class="number"><%= number_with_delimiter(@debate_votes) %></span>
</p>
<p>
<%= t "stats.index.comment_votes" %><br>
<span class="number"><%= number_with_delimiter(@comment_votes) %></span>
</p>
<p>
<%= t "stats.index.votes" %><br>
<span class="number"><%= number_with_delimiter(@votes) %></span>
</p>
</div>
<div class="small-12 medium-4 column">
<p class="featured">
<%= t "stats.index.verified_users" %><br>
<span class="number"><%= number_with_delimiter(@verified_users) %></span>
</p>
<p>
<%= t "stats.index.unverified_users" %><br>
<span class="number"><%= number_with_delimiter(@unverified_users) %></span>
</p>
</div>
</div>
</div>
</div>

View File

@@ -1,12 +0,0 @@
{
"<%= t("stats.index.visits") %>" : "<%= number_with_delimiter(@visits) %>",
"<%= t("stats.index.debates") %>" : "<%= number_with_delimiter(@debates) %>",
"<%= t("stats.index.proposals") %>" : "<%= number_with_delimiter(@proposals) %>",
"<%= t("stats.index.comments") %>" : "<%= number_with_delimiter(@comments) %>",
"<%= t("stats.index.proposal_votes") %>" : "<%= number_with_delimiter(@proposal_votes) %>",
"<%= t("stats.index.debate_votes") %>" : "<%= number_with_delimiter(@debate_votes) %>",
"<%= t("stats.index.comment_votes") %>" : "<%= number_with_delimiter(@comment_votes) %>",
"<%= t("stats.index.votes") %>" : "<%= number_with_delimiter(@votes) %>",
"<%= t("stats.index.verified_users") %>" : "<%= number_with_delimiter(@verified_users) %>",
"<%= t("stats.index.unverified_users") %>" : "<%= number_with_delimiter(@unverified_users) %>"
}

View File

@@ -1480,7 +1480,6 @@ en:
verified_users_who_didnt_vote_proposals: Verified users who didn't votes proposals
visits: Visits
votes: Total votes
budgets_title: Participatory budgeting
participatory_budgets: Participatory Budgets
direct_messages: Direct messages
proposal_notifications: Proposal notifications
@@ -1488,10 +1487,15 @@ en:
polls: Polls
sdg: SDG
graph:
debate_created: Debates
visit: Visits
level_2_user: Level 2 users
proposal_created: Citizen proposals
budget_investment_created: Budget investments created
budget_investment_supported: Budget investments supported
debate_created: Debates created
legislation_annotation_created: Legislation annotations created
legislation_answer_created: Legislation answers created
level_3_user: Level 3 users verified
proposal_created: Citizen proposals created
title: Graphs
visits: Visits
budgets:
no_data_before_balloting_phase: "There isn't any data to show before the balloting phase."
title: "Participatory Budgets - Participation stats"

View File

@@ -761,18 +761,6 @@ en:
youtube: "%{org} YouTube"
telegram: "%{org} Telegram"
instagram: "%{org} Instagram"
stats:
index:
visits: Visits
debates: Debates
proposals: Proposals
comments: Comments
proposal_votes: Votes on proposals
debate_votes: Votes on debates
comment_votes: Votes on comments
votes: Total votes
verified_users: Verified users
unverified_users: Unverified users
unauthorized:
default: You do not have permission to access this page.
manage:

View File

@@ -116,8 +116,6 @@ en:
allow_attached_documents_description: "Allows users to upload documents when creating proposals and investment projects from Participatory Budgets"
guides: "Guides to create proposals or investment projects"
guides_description: "Displays a guide to differences between proposals and investment projects if there is an active participatory budget"
public_stats: "Public stats"
public_stats_description: "Display public stats in the Administration panel"
help_page: "Help page"
help_page_description: 'Displays a Help menu that contains a page with an info section about each enabled feature. Also custom pages and menus can be created in the "Custom pages" and "Custom content blocks" sections'
remote_translations: "Remote translation"

View File

@@ -1480,7 +1480,6 @@ es:
verified_users_who_didnt_vote_proposals: Usuarios verificados que no han votado propuestas
visits: Visitas
votes: Votos
budgets_title: Presupuestos participativos
participatory_budgets: Presupuestos Participativos
direct_messages: Mensajes directos
proposal_notifications: Notificaciones de propuestas
@@ -1488,10 +1487,15 @@ es:
polls: Votaciones
sdg: ODS
graph:
debate_created: Debates
budget_investment_created: Proyectos de gasto creados
budget_investment_supported: Proyectos de gasto apoyados
debate_created: Debates creados
legislation_annotation_created: Anotaciones creadas
legislation_answer_created: Repuestas de legislación colaborativa creadas
level_3_user: Usuarios de nivel 3 verificados
proposal_created: Propuestas Ciudadanas creadas
title: Gráficos
visit: Visitas
level_2_user: Usuarios nivel 2
proposal_created: Propuestas Ciudadanas
budgets:
no_data_before_balloting_phase: "No hay datos que mostrar hasta que la fase de votación no esté abierta."
title: "Presupuestos participativos - Estadisticas de participación"

View File

@@ -761,18 +761,6 @@ es:
youtube: "YouTube de %{org}"
telegram: "Telegram de %{org}"
instagram: "Instagram de %{org}"
stats:
index:
visits: Visitas
debates: Debates
proposals: Propuestas ciudadanas
comments: Comentarios
proposal_votes: Votos en propuestas
debate_votes: Votos en debates
comment_votes: Votos en comentarios
votes: Votos
verified_users: Usuarios verificados
unverified_users: Usuarios sin verificar
unauthorized:
default: No tienes permiso para acceder a esta página.
manage:

View File

@@ -116,8 +116,6 @@ es:
allow_attached_documents_description: "Permite que los usuarios suban documentos al crear propuestas y proyectos de gasto de los Presupuestos participativos"
guides: "Guías para crear propuestas o proyectos de inversión"
guides_description: "Muestra una guía de diferencias entre las propuestas y los proyectos de gasto si hay un presupuesto participativo activo"
public_stats: "Estadísticas públicas"
public_stats_description: "Muestra las estadísticas públicas en el panel de Administración"
help_page: "Página de ayuda"
help_page_description: 'Muestra un menú Ayuda que contiene una página con una sección de información sobre cada funcionalidad habilitada. También se pueden crear páginas y menús personalizados en las secciones "Personalizar páginas" y "Personalizar bloques"'
remote_translations: "Traducciones remotas"

View File

@@ -32,7 +32,6 @@ Rails.application.routes.draw do
get "/consul.json", to: "installation#details"
get "robots.txt", to: "robots#index"
resources :stats, only: [:index]
resources :images, only: [:destroy]
resources :documents, only: [:destroy]
resources :follows, only: [:create, :destroy]

View File

@@ -241,10 +241,6 @@ namespace :admin do
end
end
namespace :api do
resource :stats, only: :show
end
resources :geozones, only: [:index, :new, :create, :edit, :update, :destroy]
namespace :site_customization do

View File

@@ -0,0 +1,13 @@
require "rails_helper"
describe Admin::Stats::ChartComponent do
it "renders a tag with JSON data" do
create(:user, :level_three, verified_at: "2009-09-09")
chart = Ahoy::Chart.new(:level_3_user)
render_inline Admin::Stats::ChartComponent.new(chart)
expect(page).to have_css "h2", exact_text: "Level 3 users verified (1)"
expect(page).to have_css "[data-graph='{\"x\":[\"2009-09-09\"],\"Level 3 users verified\":[1]}']"
end
end

View File

@@ -0,0 +1,16 @@
require "rails_helper"
describe Admin::Stats::EventLinksComponent do
it "renders a list of links" do
render_inline Admin::Stats::EventLinksComponent.new(
%w[legislation_annotation_created legislation_answer_created]
)
expect(page).to have_link count: 2
page.find("ul") do |list|
expect(list).to have_link "Legislation annotations created"
expect(list).to have_link "Legislation answers created"
end
end
end

View File

@@ -0,0 +1,18 @@
require "rails_helper"
describe Admin::Stats::GlobalChartComponent do
before do
allow(Ahoy::Chart).to receive(:active_event_names).and_return(
%i[budget_investment_created level_3_user]
)
end
it "renders an <h2> tag with a graph and links" do
render_inline Admin::Stats::GlobalChartComponent.new
expect(page).to have_css "h2", exact_text: "Graphs"
expect(page).to have_css "[data-graph]"
expect(page).to have_link "Budget investments created"
expect(page).to have_link "Level 3 users verified"
end
end

View File

@@ -0,0 +1,11 @@
require "rails_helper"
describe Admin::Stats::SDGComponent do
include Rails.application.routes.url_helpers
it "renders a links to go back to the admin stats index" do
render_inline Admin::Stats::SDGComponent.new(SDG::Goal.all)
expect(page).to have_link "Go back", href: admin_stats_path
end
end

View File

@@ -1,68 +0,0 @@
require "rails_helper"
describe Admin::Api::StatsController, :admin do
describe "GET index" do
context "events or visits not present" do
it "responds with bad_request" do
get :show
expect(response).not_to be_ok
expect(response).to have_http_status 400
end
end
context "events present" do
before do
time_1 = Time.zone.local(2015, 01, 01)
time_2 = Time.zone.local(2015, 01, 02)
time_3 = Time.zone.local(2015, 01, 03)
create(:ahoy_event, name: "foo", time: time_1)
create(:ahoy_event, name: "foo", time: time_1)
create(:ahoy_event, name: "foo", time: time_2)
create(:ahoy_event, name: "bar", time: time_1)
create(:ahoy_event, name: "bar", time: time_3)
create(:ahoy_event, name: "bar", time: time_3)
end
it "returns single events formated for working with c3.js" do
get :show, params: { event: "foo" }
expect(response).to be_ok
expect(response.parsed_body).to eq "x" => ["2015-01-01", "2015-01-02"], "Foo" => [2, 1]
end
end
context "visits present" do
it "returns visits formated for working with c3.js" do
time_1 = Time.zone.local(2015, 01, 01)
time_2 = Time.zone.local(2015, 01, 02)
create(:visit, started_at: time_1)
create(:visit, started_at: time_1)
create(:visit, started_at: time_2)
get :show, params: { visits: true }
expect(response).to be_ok
expect(response.parsed_body).to eq "x" => ["2015-01-01", "2015-01-02"], "Visits" => [2, 1]
end
end
context "budget investments present" do
it "returns budget investments formated for working with c3.js" do
time_1 = Time.zone.local(2017, 04, 01)
time_2 = Time.zone.local(2017, 04, 02)
create(:budget_investment, created_at: time_1)
create(:budget_investment, created_at: time_2)
create(:budget_investment, created_at: time_2)
get :show, params: { budget_investments: true }
expect(response).to be_ok
expect(response.parsed_body).to eq "x" => ["2017-04-01", "2017-04-02"], "Budget Investments" => [1, 2]
end
end
end
end

View File

@@ -9,34 +9,6 @@ describe DebatesController do
end
end
describe "POST create" do
before do
InvisibleCaptcha.timestamp_enabled = false
end
after do
InvisibleCaptcha.timestamp_enabled = true
end
it "creates an ahoy event" do
debate_attributes = {
terms_of_service: "1",
translations_attributes: {
"0" => {
title: "A sample debate",
description: "this is a sample debate",
locale: "en"
}
}
}
sign_in create(:user)
post :create, params: { debate: debate_attributes }
expect(Ahoy::Event.where(name: :debate_created).count).to eq 1
expect(Ahoy::Event.last.properties["debate_id"]).to eq Debate.last.id
end
end
describe "PUT mark_featured" do
it "ignores query parameters" do
debate = create(:debate)

View File

@@ -36,27 +36,6 @@ describe Legislation::AnnotationsController do
end
let(:user) { create(:user, :level_two) }
it "creates an ahoy event" do
sign_in user
post :create, params: {
process_id: legal_process.id,
draft_version_id: draft_version.id,
legislation_annotation: {
"quote" => "ipsum",
"ranges" => [{
"start" => "/p[1]",
"startOffset" => 6,
"end" => "/p[1]",
"endOffset" => 11
}],
"text" => "una anotacion"
}
}
expect(Ahoy::Event.where(name: :legislation_annotation_created).count).to eq 1
expect(Ahoy::Event.last.properties["legislation_annotation_id"]).to eq Legislation::Annotation.last.id
end
it "does not create an annotation if the draft version is a final version" do
sign_in user

View File

@@ -10,20 +10,6 @@ describe Legislation::AnswersController do
let(:question_option) { create(:legislation_question_option, question: question, value: "Yes") }
let(:user) { create(:user, :level_two) }
it "creates an ahoy event" do
sign_in user
post :create, params: {
process_id: legal_process.id,
question_id: question.id,
legislation_answer: {
legislation_question_option_id: question_option.id
}
}
expect(Ahoy::Event.where(name: :legislation_answer_created).count).to eq 1
expect(Ahoy::Event.last.properties["legislation_answer_id"]).to eq Legislation::Answer.last.id
end
it "creates an answer if the process debate phase is open" do
sign_in user

View File

@@ -1,17 +1,6 @@
FactoryBot.define do
factory :ahoy_event, class: "Ahoy::Event" do
id { SecureRandom.uuid }
time { DateTime.current }
sequence(:name) { |n| "Event #{n} type" }
end
factory :visit do
id { SecureRandom.uuid }
started_at { DateTime.current }
end
factory :campaign do
sequence(:name) { |n| "Campaign #{n}" }
sequence(:track_id, &:to_s)
end
end

View File

@@ -0,0 +1,106 @@
require "rails_helper"
describe Ahoy::Chart do
describe ".active_events_data_points" do
it "groups several events together" do
create(:budget_investment, created_at: "2010-01-01")
create(:budget_investment, created_at: "2010-01-02")
create(:legislation_annotation, created_at: "2010-01-01")
create(:legislation_annotation, created_at: "2010-01-03")
expect(Ahoy::Chart.active_events_data_points).to eq({
x: ["2010-01-01", "2010-01-02", "2010-01-03"],
"Budget investments created" => [1, 1, 0],
"Legislation annotations created" => [1, 0, 1]
})
end
it "sorts events alphabetically" do
create(:budget_investment, created_at: "2010-01-01")
create(:legislation_annotation, created_at: "2010-01-01")
I18n.with_locale(:es) do
expect(Ahoy::Chart.active_events_data_points.keys).to eq([
:x,
"Anotaciones creadas",
"Proyectos de gasto creados"
])
end
end
end
describe "#data_points" do
it "raises an exception for unknown events" do
chart = Ahoy::Chart.new(:mystery)
expect { chart.data_points }.to raise_exception "Unknown event mystery"
end
it "returns data associated with the event" do
time_1 = Time.zone.local(2015, 01, 01)
time_2 = Time.zone.local(2015, 01, 02)
time_3 = Time.zone.local(2015, 01, 03)
create(:proposal, created_at: time_1)
create(:proposal, created_at: time_1)
create(:proposal, created_at: time_2)
create(:debate, created_at: time_1)
create(:debate, created_at: time_3)
chart = Ahoy::Chart.new(:proposal_created)
expect(chart.data_points).to eq x: ["2015-01-01", "2015-01-02"],
"Citizen proposals created" => [2, 1]
end
it "accepts strings as the event name" do
create(:proposal, created_at: Time.zone.local(2015, 01, 01))
create(:debate, created_at: Time.zone.local(2015, 01, 02))
chart = Ahoy::Chart.new("proposal_created")
expect(chart.data_points).to eq x: ["2015-01-01"], "Citizen proposals created" => [1]
end
it "returns visits data for the visits event" do
time_1 = Time.zone.local(2015, 01, 01)
time_2 = Time.zone.local(2015, 01, 02)
create(:visit, started_at: time_1)
create(:visit, started_at: time_1)
create(:visit, started_at: time_2)
chart = Ahoy::Chart.new(:visits)
expect(chart.data_points).to eq x: ["2015-01-01", "2015-01-02"], "Visits" => [2, 1]
end
it "returns user supports for the budget_investment_supported event" do
time_1 = Time.zone.local(2017, 04, 01)
time_2 = Time.zone.local(2017, 04, 02)
create(:vote, votable: create(:budget_investment), created_at: time_1)
create(:vote, votable: create(:budget_investment), created_at: time_2)
create(:vote, votable: create(:budget_investment), created_at: time_2)
create(:vote, votable: create(:proposal), created_at: time_2)
chart = Ahoy::Chart.new(:budget_investment_supported)
expect(chart.data_points).to eq x: ["2017-04-01", "2017-04-02"],
"Budget investments supported" => [1, 2]
end
it "returns level three verified dates for the level_3_user event" do
time_1 = Time.zone.local(2001, 01, 01)
time_2 = Time.zone.local(2001, 01, 02)
create(:user, :level_two, level_two_verified_at: time_1)
create(:user, :level_three, verified_at: time_2)
create(:user, :level_three, verified_at: time_2, level_two_verified_at: time_1)
chart = Ahoy::Chart.new(:level_3_user)
expect(chart.data_points).to eq x: ["2001-01-02"], "Level 3 users verified" => [2]
end
end
end

View File

@@ -2,18 +2,9 @@ require "rails_helper"
describe Ahoy::DataSource do
describe "#build" do
before do
time_1 = Time.zone.local(2015, 01, 01)
time_2 = Time.zone.local(2015, 01, 02)
time_3 = Time.zone.local(2015, 01, 03)
create(:ahoy_event, name: "foo", time: time_1)
create(:ahoy_event, name: "foo", time: time_1)
create(:ahoy_event, name: "foo", time: time_2)
create(:ahoy_event, name: "bar", time: time_1)
create(:ahoy_event, name: "bar", time: time_3)
create(:ahoy_event, name: "bar", time: time_3)
end
let(:january_first) { Time.zone.local(2015, 01, 01) }
let(:january_second) { Time.zone.local(2015, 01, 02) }
let(:january_third) { Time.zone.local(2015, 01, 03) }
it "works without data sources" do
ds = Ahoy::DataSource.new
@@ -22,17 +13,27 @@ describe Ahoy::DataSource do
it "works with single data sources" do
ds = Ahoy::DataSource.new
ds.add "foo", Ahoy::Event.where(name: "foo").group_by_day(:time).count
ds.add "foo", { january_first => 2, january_second => 1 }
expect(ds.build).to eq :x => ["2015-01-01", "2015-01-02"], "foo" => [2, 1]
end
it "combines data sources" do
ds = Ahoy::DataSource.new
ds.add "foo", Ahoy::Event.where(name: "foo").group_by_day(:time).count
ds.add "bar", Ahoy::Event.where(name: "bar").group_by_day(:time).count
ds.add "foo", { january_first => 2, january_second => 1 }
ds.add "bar", { january_first => 1, january_third => 2 }
expect(ds.build).to eq :x => ["2015-01-01", "2015-01-02", "2015-01-03"],
"foo" => [2, 1, 0],
"bar" => [1, 0, 2]
end
it "returns data ordered by dates" do
ds = Ahoy::DataSource.new
ds.add "foo", { january_third => 2, january_second => 1 }
ds.add "bar", { january_first => 2, january_second => 1 }
expect(ds.build).to eq :x => ["2015-01-01", "2015-01-02", "2015-01-03"],
"foo" => [0, 1, 2],
"bar" => [2, 1, 0]
end
end
end

View File

@@ -72,18 +72,6 @@ describe "Stats", :admin do
expect(page).to have_content "UNVERIFIED USERS\n1"
expect(page).to have_content "TOTAL USERS\n1"
end
scenario "Level 2 user Graph" do
create(:geozone)
visit account_path
click_link "Verify my account"
verify_residence
confirm_phone
visit admin_stats_path
expect(page).to have_content "LEVEL TWO USERS\n1"
end
end
describe "Budget investments" do
@@ -150,21 +138,30 @@ describe "Stats", :admin do
end
end
context "graphs" do
scenario "event graphs", :with_frozen_time do
campaign = create(:campaign)
visit root_path(track_id: campaign.track_id)
expect(page).to have_content "Sign out"
describe "graphs", :with_frozen_time do
scenario "event graphs" do
create(:debate)
visit admin_stats_path
within("#stats") do
click_link campaign.name
click_link "Debates created"
end
expect(page).to have_content "#{campaign.name} (1)"
expect(page).to have_content "Debates created (1)"
within("#graph") do
expect(page).to have_content Date.current.strftime("%Y-%m-%d")
end
end
scenario "Level 3 user Graph" do
create(:user, :level_three)
visit admin_stats_path
click_link "Level 3 users verified"
expect(page).to have_content "Level 3 users verified (1)"
within("#graph") do
expect(page).to have_content Date.current.strftime("%Y-%m-%d")
@@ -183,6 +180,8 @@ describe "Stats", :admin do
visit admin_stats_path
click_link "Proposal notifications"
expect(page).to have_link "Go back", href: admin_stats_path
within("#proposal_notifications_count") do
expect(page).to have_content "3"
end
@@ -232,6 +231,8 @@ describe "Stats", :admin do
visit admin_stats_path
click_link "Direct messages"
expect(page).to have_link "Go back", href: admin_stats_path
within("#direct_messages_count") do
expect(page).to have_content "3"
end
@@ -253,6 +254,8 @@ describe "Stats", :admin do
click_link "Polls"
end
expect(page).to have_link "Go back", href: admin_stats_path
within("#web_participants") do
expect(page).to have_content "3"
end

View File

@@ -1,37 +0,0 @@
require "rails_helper"
describe "Email campaigns", :admin do
let!(:campaign1) { create(:campaign) }
let!(:campaign2) { create(:campaign) }
scenario "Track email templates" do
3.times { visit root_path(track_id: campaign1.track_id) }
5.times { visit root_path(track_id: campaign2.track_id) }
visit admin_stats_path
click_link campaign1.name
expect(page).to have_content "#{campaign1.name} (3)"
click_link "Go back"
click_link campaign2.name
expect(page).to have_content "#{campaign2.name} (5)"
end
scenario "Do not track erroneous track_ids" do
invalid_id = Campaign.last.id + 1
visit root_path(track_id: campaign1.track_id)
visit root_path(track_id: invalid_id)
visit admin_stats_path
expect(page).to have_content campaign1.name
expect(page).not_to have_content campaign2.name
click_link campaign1.name
expect(page).to have_content "#{campaign1.name} (1)"
end
end

View File

@@ -164,23 +164,25 @@ describe "Notifications" do
end
scenario "With internal link" do
admin_notification.update!(link: "/stats")
admin_notification.update!(link: "/debates")
visit notifications_path
expect(page).to have_content("Notification title")
expect(page).to have_content("Notification body")
first("#notification_#{notification.id} a").click
expect(page).to have_current_path("/stats")
expect(page).to have_current_path "/debates"
end
scenario "Without a link" do
admin_notification.update!(link: "/stats")
admin_notification.update!(link: nil)
visit notifications_path
expect(page).to have_content("Notification title")
expect(page).to have_content("Notification body")
expect(page).not_to have_link(notification_path(notification), visible: :all)
expect(page).to have_content "Notification title"
expect(page).to have_content "Notification body"
expect(page).not_to have_link href: notification_path(notification), visible: :all
end
end

View File

@@ -1,43 +0,0 @@
require "rails_helper"
describe "Stats" do
context "Summary" do
scenario "General" do
create(:debate)
2.times { create(:proposal) }
3.times { create(:comment, commentable: Debate.first) }
4.times { create(:visit) }
visit stats_path
expect(page).to have_content "DEBATES\n1"
expect(page).to have_content "PROPOSALS\n2"
expect(page).to have_content "COMMENTS\n3"
expect(page).to have_content "VISITS\n4"
end
scenario "Votes" do
create(:debate, voters: Array.new(1) { create(:user) })
create(:proposal, voters: Array.new(2) { create(:user) })
create(:comment, voters: Array.new(3) { create(:user) })
visit stats_path
expect(page).to have_content "VOTES ON DEBATES\n1"
expect(page).to have_content "VOTES ON PROPOSALS\n2"
expect(page).to have_content "VOTES ON COMMENTS\n3"
expect(page).to have_content "TOTAL VOTES\n6"
end
scenario "Users" do
1.times { create(:user, :level_three) }
2.times { create(:user, :level_two) }
2.times { create(:user) }
visit stats_path
expect(page).to have_content "VERIFIED USERS\n3"
expect(page).to have_content "UNVERIFIED USERS\n2"
end
end
end