merges master and fixes conflicts

This commit is contained in:
kikito
2015-08-18 09:35:13 +02:00
80 changed files with 18565 additions and 128 deletions

1
.rspec
View File

@@ -1 +1,2 @@
--color

View File

@@ -1,4 +1,6 @@
language: ruby
addons:
postgresql: "9.4"
rvm:
- "2.2.2"
cache: bundler

View File

@@ -36,6 +36,9 @@ gem 'initialjs-rails'
gem 'unicorn'
gem 'paranoia'
gem 'ahoy_matey', '~> 1.2.1'
gem 'groupdate' # group temporary data
group :development, :test do
# Call 'byebug' anywhere in the code to stop execution and get a debugger console
gem 'byebug'
@@ -46,6 +49,7 @@ group :development, :test do
gem 'rspec-rails', '~> 3.0'
gem 'capybara'
gem 'factory_girl_rails'
gem 'fuubar'
gem 'launchy'
gem 'quiet_assets'
gem 'letter_opener_web', '~> 1.2.0'

View File

@@ -44,12 +44,23 @@ GEM
awesome_nested_set (>= 3.0)
acts_as_votable (0.10.0)
addressable (2.3.8)
ahoy_matey (1.2.1)
addressable
browser (>= 0.4.0)
errbase
geocoder
rails
referer-parser (>= 0.3.0)
request_store
user_agent_parser
uuidtools
arel (6.0.3)
awesome_nested_set (3.0.2)
activerecord (>= 4.0.0, < 5)
bcrypt (3.1.10)
binding_of_caller (0.7.2)
debug_inspector (>= 0.0.1)
browser (0.9.1)
builder (3.2.2)
byebug (5.0.0)
columnize (= 0.9.0)
@@ -116,6 +127,7 @@ GEM
email_spec (1.6.0)
launchy (~> 2.1)
mail (~> 2.2)
errbase (0.0.3)
erubis (2.7.0)
execjs (2.5.2)
factory_girl (4.5.0)
@@ -132,8 +144,14 @@ GEM
activesupport (~> 4.1, >= 4.1.1)
railties (~> 4.1, >= 4.1.1)
tzinfo (~> 1.2, >= 1.2.2)
fuubar (2.0.0)
rspec (~> 3.0)
ruby-progressbar (~> 1.4)
geocoder (1.2.9)
globalid (0.3.6)
activesupport (>= 4.1.0)
groupdate (2.4.0)
activesupport (>= 3)
highline (1.7.3)
http-cookie (1.0.2)
domain_name (~> 0.5)
@@ -218,12 +236,18 @@ GEM
thor (>= 0.18.1, < 2.0)
raindrops (0.15.0)
rake (10.4.2)
referer-parser (0.3.0)
request_store (1.2.0)
responders (2.1.0)
railties (>= 4.2.0, < 5)
rest-client (1.8.0)
http-cookie (>= 1.0.2, < 2.0)
mime-types (>= 1.16, < 3.0)
netrc (~> 0.7)
rspec (3.3.0)
rspec-core (~> 3.3.0)
rspec-expectations (~> 3.3.0)
rspec-mocks (~> 3.3.0)
rspec-core (3.3.2)
rspec-support (~> 3.3.0)
rspec-expectations (3.3.1)
@@ -241,6 +265,7 @@ GEM
rspec-mocks (~> 3.3.0)
rspec-support (~> 3.3.0)
rspec-support (3.3.0)
ruby-progressbar (1.7.5)
sass (3.4.16)
sass-rails (5.0.3)
railties (>= 4.0.0, < 5.0)
@@ -291,6 +316,8 @@ GEM
kgio (~> 2.6)
rack
raindrops (~> 0.7)
user_agent_parser (2.2.0)
uuidtools (2.1.5)
warden (1.2.3)
rack (>= 1.0)
web-console (2.2.1)
@@ -311,6 +338,7 @@ DEPENDENCIES
acts-as-taggable-on
acts_as_commentable_with_threading
acts_as_votable
ahoy_matey (~> 1.2.1)
byebug
cancancan
capistrano (= 3.4.0)
@@ -327,6 +355,8 @@ DEPENDENCIES
factory_girl_rails
foundation-rails
foundation_rails_helper
fuubar
groupdate
i18n-tasks
initialjs-rails
jquery-rails

View File

@@ -20,7 +20,7 @@ Las herramientas utilizadas para el frontend no están cerradas aún. Los estilo
## Configuración para desarrollo y tests
Prerequisitos: tener instalado git, ImageMagick, Ruby 2.2.2, la gema `bundler`, y una librería moderna de PostgreSQL.
Prerequisitos: tener instalado git, ImageMagick, Ruby 2.2.2, la gema `bundler`, y PostgreSQL (9.4 o superior).
```
cd participacion

View File

@@ -21,7 +21,7 @@ Frontend tools used include [SCSS](http://sass-lang.com/) over [Foundation](http
## Configuration for development and test environments
Prerequisites: install git, ImageMagick, Ruby 2.2.2, bundler gem and PostgreSQL.
Prerequisites: install git, ImageMagick, Ruby 2.2.2, bundler gem and PostgreSQL (>=9.4).
```
cd participacion

Binary file not shown.

View File

@@ -21,4 +21,5 @@
<glyph unicode="&#108;" d="M477 350c0-7-2-14-8-19l-206-207-39-39c-6-5-12-8-20-8-7 0-14 3-19 8l-142 142c-6 6-8 12-8 20 0 7 2 14 8 19l38 39c6 5 12 8 20 8 7 0 14-3 19-8l84-84 188 188c5 5 12 8 19 8 8 0 14-3 20-8l38-39c6-6 8-12 8-20z"/>
<glyph unicode="&#109;" d="M140 73l26 26-67 67-26-26 0-30 37 0 0-37z m150 265c0 4-2 7-7 7-1 0-3-1-4-2l-155-155c-2-2-2-3-2-5 0-4 2-6 6-6 2 0 4 0 5 2l155 154c1 2 2 3 2 5z m-16 55l119-119-238-237-118 0 0 118z m195-27c0-10-3-19-10-26l-48-47-118 118 47 48c7 7 15 10 26 10 10 0 18-3 26-10l67-67c7-8 10-16 10-26z"/>
<glyph unicode="&#110;" d="M494 327c0-4-3-9-8-14l-103-101 24-143c0-1 0-3 0-5 0-4-1-8-3-10-2-3-4-5-8-5-4 0-8 2-12 4l-128 67-128-67c-4-2-8-4-12-4-4 0-7 2-9 5-2 2-3 6-3 10 0 1 0 3 1 5l24 143-104 101c-4 6-7 10-7 14 0 7 6 12 16 13l144 21 64 130c4 8 8 12 14 12 6 0 10-4 14-12l64-130 144-21c10-1 16-6 16-13z"/>
<glyph unicode="&#112;" d="M256 448c-96 0-208-64-256-192 48-96 144-160 256-160 112 0 208 64 256 160-48 128-160 192-256 192z m0-320c-96 0-176 64-192 128 16 64 96 128 192 128 96 0 176-64 192-128-16-64-96-128-192-128z m0 224c-10 0-19-2-28-5 17-7 28-24 28-43 0-27-21-48-48-48-19 0-36 11-43 28-3-9-5-18-5-28 0-53 43-96 96-96 53 0 96 43 96 96 0 53-43 96-96 96z"/>
</font></defs></svg>

Before

Width:  |  Height:  |  Size: 8.0 KiB

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

Binary file not shown.

View File

@@ -17,6 +17,10 @@
//= require ckeditor/init
//= require social-share-button
//= require initial
//= require ahoy
//= require d3
//= require c3
//= require c3ext
//= require app
//= require_tree .
@@ -25,6 +29,7 @@ var initialize_modules = function() {
App.Users.initialize();
App.Votes.initialize();
App.Tags.initialize();
App.Stats.initialize();
};
$(function(){

View File

@@ -0,0 +1,11 @@
# Helper for generate C3.js graphs
#----------------------------------------------------------------------
buildGraph = (el) ->
url = $(el).data 'graph'
conf = bindto: el, data: {x: 'x', url: url, mimeType: 'json'}, axis: { x: {type: 'timeseries',tick: { format: '%Y-%m-%d' } }}
graph = c3.generate conf
App.Stats =
initialize: ->
buildGraph(g) for g in $("[data-graph]")

View File

@@ -0,0 +1,153 @@
// Table of Contents
//
// 01. Global styles
// 02. Sidebar
// 03. List elements
//
// 01. Global styles
// - - - - - - - - - - - - - - - - - - - - - - - - -
body.admin {
background: white;
h2 {
font-size: rem-calc(24);
font-weight: bold;
}
form {
.button {
margin-top: 0;
}
}
.button.secondary {
margin-right: rem-calc(12);
}
.button.create {
background: #EFD90C;
color: $text;
&:hover {
background: #BDAB09;
}
}
.admin-content {
margin-top: rem-calc(24);
}
.is-featured {
margin-top: rem-calc(36);
}
}
// 02. Sidebar
// - - - - - - - - - - - - - - - - - - - - - - - - -
.admin-sidebar {
margin-left: rem-calc(-15);
margin-top: rem-calc(-48);
ul {
list-style-type: none;
margin-left: 0;
padding: 0;
[class^="icon-"] {
display: inline-block;
font-size: rem-calc(24);
padding-right: rem-calc(24);
padding-top: rem-calc(4);
}
li {
background: #2E343F;
border-bottom: 1px solid #292f39;
border-top: 1px solid #353c49;
margin: 0;
outline: 0;
&:first-child {
background: #2B3139;
color: rgba(255,255,255,0.3);
padding: rem-calc(24) rem-calc(12);
text-transform: uppercase;
}
&.active{
background: #373D47;
a:not(.button) {
color: white;
}
}
}
li a:not(.button) {
color: rgba(255,255,255,0.3);
line-height: rem-calc(48);
padding-left: rem-calc(12);
vertical-align: top;
&:hover {
color: white;
}
}
}
}
// 03. List elements
// - - - - - - - - - - - - - - - - - - - - - - - - -
.admin-list {
list-style-type: none;
margin: 0;
form {
clear: both;
.checkbox {
font-size: rem-calc(12);
}
}
li {
border-bottom: 1px solid #E7E9EC;
font-size: rem-calc(14);
min-height: rem-calc(72);
padding: rem-calc(12);
&:first-child {
border-top: 1px solid #E7E9EC;
}
&:nth-child(odd) {
background: #F0F2F6;
}
}
.tag {
float: left;
font-size: rem-calc(18);
padding: 0;
}
.button {
margin: 0;
}
}
.delete {
border-bottom: 1px dotted #CF2A0E;
color: #F04124;
font-size: rem-calc(11);
margin-right: rem-calc(12);
&:hover, &:active, &:focus {
border: 0;
color: #cf2a0e;
}
}

View File

@@ -6,5 +6,7 @@
@import "fonts";
@import "icons";
@import "variables";
@import "admin";
@import "participacion";
@import "debates";
@import "c3";

View File

@@ -138,10 +138,6 @@
// 02. Index
// - - - - - - - - - - - - - - - - - - - - - - - - -
.featured-debates {
margin-top: rem-calc(23);
}
.debate-featured {
.panel {
@@ -244,7 +240,6 @@
.debates-list {
margin-bottom: rem-calc(48);
margin-top: rem-calc(24);
}
.debate {

View File

@@ -79,3 +79,6 @@
.icon-star:before {
content: "n";
}
.icon-eye:before {
content: "p";
}

View File

@@ -10,6 +10,7 @@
// 08. Forms
// 09. Alerts
// 10. User account
// 11. Filters
//
// 01. Variables
@@ -111,7 +112,6 @@ h1, h2, h3, h4, h5, h6 {
}
.sidebar {
margin-top: rem-calc(24);
margin-bottom: rem-calc(48);
}
@@ -585,3 +585,36 @@ form {
}
}
}
// 11. Filters
// - - - - - - - - - - - - - - - - - - - - - - - - -
.filters {
h2 {
display: inline-block;
font-size: rem-calc(24);
margin: rem-calc(24) 0;
}
select {
height: auto;
margin: rem-calc(24) rem-calc(6);
min-width: rem-calc(180);
outline: 0;
padding: rem-calc(12);
width: auto;
optgroup {
font-size: rem-calc(14);
}
option {
&:after {
content: "a";
font-family: "icons";
}
}
}
}

View File

@@ -0,0 +1,32 @@
class Admin::OfficialsController < Admin::BaseController
def index
@officials = User.officials.page(params[:page])
end
def search
@users = User.with_email(params[:email]).page(params[:page])
end
def edit
@user = User.find(params[:id])
end
def update
@user = User.find(params[:id])
@user.update(user_params)
redirect_to admin_officials_path, notice: t("admin.officials.flash.official_updated")
end
def destroy
@official = User.officials.find(params[:id])
@official.remove_official_position!
redirect_to admin_officials_path, notice: t("admin.officials.flash.official_destroyed")
end
private
def user_params
params.require(:user).permit(:official_position, :official_level)
end
end

View File

@@ -0,0 +1,17 @@
class Admin::SettingsController < Admin::BaseController
def index
@settings = Setting.all
end
def update
@setting = Setting.find(params[:id])
@setting.update(settings_params)
redirect_to admin_settings_path, notice: t("admin.settings.flash.updated")
end
private
def settings_params
params.require(:setting).permit(:value)
end
end

View File

@@ -0,0 +1,13 @@
class Api::ApiController < ApplicationController
before_action :authenticate_user!
protect_from_forgery with: :null_session
skip_authorization_check
before_action :verify_administrator
private
def verify_administrator
raise CanCan::AccessDenied unless current_user.try(:administrator?)
end
end

View File

@@ -0,0 +1,22 @@
class Api::StatsController < Api::ApiController
def show
unless params[:events].present? || params[:visits].present?
return render json: {}, status: :bad_request
end
ds = Ahoy::DataSource.new
if params[:events].present?
event_types = params[:events].split ','
event_types.each do |event|
ds.add event.titleize, Ahoy::Event.where(name: event).group_by_day(:time).count
end
end
if params[:visits].present?
ds.add "Visits", Visit.group_by_day(:started_at).count
end
render json: ds.build
end
end

View File

@@ -26,7 +26,9 @@ class DebatesController < ApplicationController
def create
@debate = Debate.new(debate_params)
@debate.author = current_user
if @debate.save_with_captcha
ahoy.track :debate_created, debate_id: @debate.id
redirect_to @debate, notice: t('flash.actions.create.notice', resource_name: 'Debate')
else
load_featured_tags

View File

@@ -0,0 +1,14 @@
class StatsController < ApplicationController
skip_authorization_check
before_action :verify_administrator
def show
@event_types = Ahoy::Event.select(:name).uniq.pluck(:name)
end
private
def verify_administrator
raise CanCan::AccessDenied unless current_user.try(:administrator?)
end
end

View File

@@ -4,6 +4,14 @@ module AdminHelper
render "/#{namespace}/menu"
end
def official_level_options
options = []
(0..5).each do |i|
options << [[t("admin.officials.level_#{i}"), Setting.value_for("official_level_#{i}_name")].compact.join(': '), i]
end
options
end
private
def namespace

View File

@@ -0,0 +1,16 @@
module StatsHelper
def events_chart_tag(events, opt={})
events = events.join(',') if events.is_a? Array
opt[:data] ||= {}
opt[:data][:graph] = api_stats_path(events: events)
content_tag :div, "", opt
end
def visits_chart_tag(opt={})
events = events.join(',') if events.is_a? Array
opt[:data] ||= {}
opt[:data][:graph] = api_stats_path(visits: true)
content_tag :div, "", opt
end
end

View File

@@ -0,0 +1,49 @@
# This class combines multiple collections with shared keys into a
# hash of collections compatible with C3.js charts
#----------------------------------------------------------------------
module Ahoy
class DataSource
# 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{ |k,v| add_key k }
end
def build
data = { x: [] }
keys.each do |k|
# Add the key with a valid date format
data[:x].push k.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
data[col[:name]].push count
end
end
return data
end
private
def collections
@collections ||= []
end
def keys
@keys ||= []
end
def add_key(key)
keys.push(key) unless keys.include? key
end
end
end

8
app/models/ahoy/event.rb Normal file
View File

@@ -0,0 +1,8 @@
module Ahoy
class Event < ActiveRecord::Base
self.table_name = "ahoy_events"
belongs_to :visit
belongs_to :user
end
end

View File

@@ -22,6 +22,9 @@ class Debate < ActiveRecord::Base
before_validation :sanitize_description
before_validation :sanitize_tag_list
# Ahoy setup
visitable # Ahoy will automatically assign visit_id on create
def self.search(params)
if params[:tag]
tagged_with(params[:tag])

7
app/models/setting.rb Normal file
View File

@@ -0,0 +1,7 @@
class Setting < ActiveRecord::Base
default_scope { order(key: :desc) }
def self.value_for(key)
where(key: key).pluck(:value).first
end
end

View File

@@ -12,6 +12,7 @@ class User < ActiveRecord::Base
validates :first_name, presence: true, if: :use_first_name?
validates :last_name, presence: true, if: :use_last_name?
validates :nickname, presence: true, if: :use_nickname?
validates :official_level, inclusion: {in: 0..5}
validates_associated :organization, message: false
@@ -20,9 +21,7 @@ class User < ActiveRecord::Base
scope :administrators, -> { joins(:administrators) }
scope :moderators, -> { joins(:moderator) }
scope :organizations, -> { joins(:organization) }
attr_accessor :organization_name
attr_accessor :is_organization
scope :officials, -> { where("official_level > 0") }
def name
return nickname if use_nickname?
@@ -47,6 +46,23 @@ class User < ActiveRecord::Base
organization.present?
end
def official?
official_level && official_level > 0
end
def add_official_position!(position, level)
return if position.blank? || level.blank?
update official_position: position, official_level: level.to_i
end
def remove_official_position!
update official_position: nil, official_level: 0
end
def self.with_email(e)
e.present? ? where(email: e) : none
end
private
def use_first_name?
!organization? && !use_nickname?

4
app/models/visit.rb Normal file
View File

@@ -0,0 +1,4 @@
class Visit < ActiveRecord::Base
has_many :ahoy_events, class_name: "Ahoy::Event"
belongs_to :user
end

View File

@@ -1,6 +1,56 @@
<ul id="admin_menu">
<li><%= link_to t('admin.menu.debate_topics'), admin_tags_path %></li>
<li><%= link_to t('admin.menu.hidden_debates'), admin_debates_path %></li>
<li><%= link_to t('admin.menu.hidden_comments'), admin_comments_path %></li>
<li><%= link_to t('admin.menu.organizations'), admin_organizations_path %></li>
</ul>
<nav class="admin-sidebar">
<ul id="admin_menu">
<li>
<%= t("admin.dashboard.index.title") %>
</li>
<li <%= 'class=active' if controller_name == 'tags' %>>
<%= link_to admin_tags_path do %>
<i class="icon-comment-quotes"></i>
<%= t('admin.menu.debate_topics') %>
<% end %>
</li>
<li <%= 'class=active' if controller_name == 'debates' %>>
<%= link_to admin_debates_path do %>
<i class="icon-eye"></i>
<%= t('admin.menu.hidden_debates') %>
<% end %>
</li>
<li <%= 'class=active' if controller_name == 'comments' %>>
<%= link_to admin_comments_path do %>
<i class="icon-chat-bubble-two"></i>
<%= t('admin.menu.hidden_comments') %>
<% end %>
</li>
<li <%= 'class=active' if controller_name == 'organizations' %>>
<%= link_to admin_organizations_path do %>
<i class="icon-comment-quotes"></i>
<%= t('admin.menu.organizations') %>
<% end %>
</li>
<li <%= 'class=active' if controller_name == 'officials' %>>
<%= link_to admin_officials_path do %>
<i class="icon-chat-bubble-two"></i>
<%= t('admin.menu.officials') %>
<% end %>
</li>
<li <%= 'class=active' if controller_name == 'settings' %>>
<%= link_to admin_settings_path do %>
<i class="icon-chat-bubble-two"></i>
<%= t('admin.menu.settings') %>
<% end %>
</li>
<li <%= 'class=active' if controller_name == 'stats' %>>
<%= link_to stats_path do %>
<i class="icon-chat-bubble-two"></i>
<%= t('admin.menu.stats') %>
<% end %>
</li>
</ul>
</nav>

View File

@@ -1,14 +1,18 @@
<div class="left">
<h1><%= t("admin.comments.index.title") %></h1>
<h2><%= t("admin.comments.index.title") %></h2>
<ul>
<% @comments.each do |comment| %>
<li id="<%= dom_id(comment) %>">
<%= comment.body %>
<%= link_to t("admin.actions.restore"), restore_admin_comment_path(comment),
method: :put, data: { confirm: t("admin.actions.confirm") } %>
</li>
<% end %>
</ul>
</div>
<ul class="admin-list">
<% @comments.each do |comment| %>
<li id="<%= dom_id(comment) %>">
<div class="row">
<div class="small-12 medium-10 column">
<%= comment.body %>
</div>
<div class="small-12 medium-2 column">
<%= link_to t("admin.actions.restore"), restore_admin_comment_path(comment),
method: :put, data: { confirm: t("admin.actions.confirm") },
class: "button radius tiny success right" %>
</div>
</div>
</li>
<% end %>
</ul>

View File

@@ -1 +1,5 @@
<h1><%= t("admin.dashboard.index.title") %></h1>
<h2><%= t("admin.dashboard.index.title") %></h2>
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.</p>
<p>Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem.</p>

View File

@@ -1,13 +1,13 @@
<div class="left">
<h1><%= t("admin.debates.index.title") %></h1>
<h2><%= t("admin.debates.index.title") %></h2>
<ul>
<% @debates.each do |debate| %>
<%= link_to admin_debate_path(debate) do %>
<li id="<%= dom_id(debate) %>">
<%= link_to debate.title, admin_debate_path(debate) %>
</li>
<% end %>
<% end %>
</ul>
</div>
<ul class="admin-list">
<% @debates.each do |debate| %>
<li id="<%= dom_id(debate) %>">
<%= link_to debate.title, admin_debate_path(debate) %>
<%= link_to t("admin.actions.restore"), restore_admin_debate_path(debate),
method: :put, data: { confirm: t("admin.actions.confirm") },
class: "button radius tiny success right" %>
</li>
<% end %>
</ul>

View File

@@ -1,13 +1,12 @@
<div class="left">
<h1><%= t("admin.debates.index.title") %></h1>
<h2><%= t("admin.debates.index.title") %></h2>
<div>
<div><%= @debate.title %></div>
<div><%= @debate.description %></div>
<h3><%= @debate.title %></h3>
<div>
<%= link_to t('admin.actions.restore'), restore_admin_debate_path(@debate),
method: :put, data: { confirm: t('admin.actions.confirm') } %>
</div>
</div>
</div>
<div><%= @debate.description %></div>
<%= link_to t("admin.debates.show.back"), admin_debates_path,
class: "button radius small secondary" %>
<%= link_to t("admin.actions.restore"), restore_admin_debate_path(@debate),
method: :put, data: { confirm: t("admin.actions.confirm") },
class: "button radius small success" %>

View File

@@ -0,0 +1,14 @@
<h1><%= t("admin.officials.edit.title") %></h1>
<%= @user.name %> (<%= @user.email %>)
<%= form_for @user, url: admin_official_path(@user) do |f| %>
<%= f.text_field :official_position %>
<%= f.select :official_level, official_level_options %>
<%= f.submit %>
<% if @user.official? %>
<%= link_to t("admin.officials.edit.destroy"), admin_official_path(@user), method: :delete, class: 'button tiny alert' %>
<% else %>
<%= link_to t("admin.officials.edit.cancel"), admin_officials_path, class: 'button tiny alert' %>
<% end %>
<% end %>

View File

@@ -0,0 +1,25 @@
<h1><%= t("admin.officials.index.title") %></h1>
<div>
<%= form_for(User.new, url: search_admin_officials_path, as: :user, method: :get) do |f| %>
<%= text_field_tag :email, "", label: false, placeholder: t("admin.officials.index.search_email_placeholder") %>
<%= f.submit t("admin.officials.index.search") %>
<% end %>
</div>
<div>
<%= page_entries_info @officials %>
</div>
<div>
<% @officials.each do |official| %>
<%= link_to official.name, edit_admin_official_path(official) %>
<%= official.official_position %>
<%= t("admin.officials.level_#{official.official_level}") %>
<br/><br/>
<% end %>
</div>
<div>
<%= paginate @officials %>
</div>

View File

@@ -0,0 +1,21 @@
<h1><%= t("admin.officials.search.title") %></h1>
<div>
<%= form_for(User.new, url: search_admin_officials_path, as: :user, method: :get) do |f| %>
<%= text_field_tag :email, "", label: false, placeholder: t("admin.officials.index.search_email_placeholder") %>
<%= f.submit t("admin.officials.search.search") %>
<% end %>
</div>
<div>
<%= page_entries_info @users %>
</div>
<div>
<% @users.each do |user| %>
<%= link_to user.name, edit_admin_official_path(user) %>
<%= user.official_position %>
<%= t("admin.officials.level_#{user.official_level}") %>
<%= link_to user.official? ? t("admin.officials.search.edit_official") : t("admin.officials.search.make_official"), edit_admin_official_path(user) %>
<% end %>
</div>

View File

@@ -0,0 +1,15 @@
<h1><%= t("admin.settings.index.title") %></h1>
<ul>
<% @settings.each do |setting| %>
<li>
<strong><%= setting.key.classify %></strong>
<%= form_for(setting, url: admin_setting_path(setting), html: { id: "edit_#{dom_id(setting)}"}) do |f| %>
<%= f.text_field :value, label: false, id: dom_id(setting) %>
<%= f.submit(class: "button radius tiny") %>
<% end %>
</li>
<% end %>
</ul>

View File

@@ -1,29 +1,46 @@
<div class="left">
<h1><%= t("admin.tags.index.add_tag") %></h1>
<h2><%= t("admin.tags.index.add_tag") %></h2>
<%= form_for(@tag, url: admin_tags_path, as: :tag) do |f| %>
<%= f.text_field :name, placeholder: t("admin.tags.name.placeholder") %>
<%= f.check_box :featured, label: false %>
<%= t("admin.tags.mark_as_featured") %>
<%= f.submit(class: "button radius small") %>
<% end %>
<%= form_for(@tag, url: admin_tags_path, as: :tag) do |f| %>
<div class="row">
<div class="small-12 medium-6 column">
<%= f.label :name, t("admin.tags.name.placeholder") %>
<%= f.text_field :name, placeholder: t("admin.tags.name.placeholder"), label: false %>
</div>
<h1><%= t("admin.tags.index.title") %></h1>
<div class="is-featured small-12 medium-6 column">
<%= f.label :featured do %>
<%= f.check_box :featured, label: false %>
<span class="checkbox"><%= t("admin.tags.mark_as_featured") %></span>
<% end %>
</div>
</div>
<ul>
<% @tags.each do |tag| %>
<li>
<strong><%= tag.name %></strong>
<%= f.submit(class: "button radius create") %>
<%= form_for(tag, url: admin_tag_path(tag), as: :tag, html: { id: "edit_tag_#{tag.id}"}) do |f| %>
<%= f.check_box :featured, label: false, id: "tag_featured_#{tag.id}" %>
<%= t("admin.tags.mark_as_featured") %>
<%= f.submit(class: "button radius tiny") %>
<%= link_to t("admin.tags.destroy"), admin_tag_path(tag), method: :delete, class: 'button tiny alert' %>
<% end %>
<h3><%= t("admin.tags.index.title") %></h3>
<ul class="admin-list">
<% @tags.each do |tag| %>
<li>
<span class="tag"><%= tag.name %></span>
<%= form_for(tag,
url: admin_tag_path(tag),
as: :tag,
html: { id: "edit_tag_#{tag.id}", class: "text-right"}) do |f| %>
<%= f.label "featured_#{tag.id}" do %>
<%= f.check_box :featured, label: false, id: "tag_featured_#{tag.id}", class: "left" %>
<span class="checkbox left"><%= t("admin.tags.mark_as_featured") %></span>
<% end %>
</li>
<% end %>
</ul>
</div>
<%= f.submit(class: "button radius tiny success") %>
<br>
<%= link_to t("admin.tags.destroy"), admin_tag_path(tag), method: :delete, class: "delete" %>
<% end %>
</li>
<% end %>
</ul>

View File

@@ -1,6 +1,79 @@
<section role="main">
<!-- Filters -->
<div class="filters row">
<div class="small-12 column">
<h2><%= t("debates.index.showing") %></h2>
<select class="inline-block">
<option value="filter_debates">
<%= t("debates.index.filter_debates") %>
</option>
<option value="filter_initiatives">
<%= t("debates.index.filter_initiatives") %>
</option>
<option value="filter_debates_and_initiatives">
<%= t("debates.index.filter_debates_and_initiatives") %>
</option>
</select>
<select class="inline-block">
<option value="filter_news">
<%= t("debates.index.filter_news") %>
</option>
<option value="filter_votes">
<%= t("debates.index.filter_votes") %>
</option>
<option value="filter_rated">
<%= t("debates.index.filter_rated") %>
</option>
</select>
</div>
</div>
<!-- /. Filters -->
<!-- Filter topic results -->
<div class="filters row">
<div class="small-12 column">
<h2><%= t("debates.index.showing") %></h2>
<select class="inline-block">
<option value="filter_debates">
<%= t("debates.index.filter_debates") %>
</option>
<option value="filter_initiatives">
<%= t("debates.index.filter_initiatives") %>
</option>
<option value="filter_debates_and_initiatives">
<%= t("debates.index.filter_debates_and_initiatives") %>
</option>
</select>
<select class="inline-block">
<option value="filter_news">
<%= t("debates.index.filter_news") %>
</option>
<option value="filter_votes">
<%= t("debates.index.filter_votes") %>
</option>
<option value="filter_rated">
<%= t("debates.index.filter_rated") %>
</option>
</select>
<h2><%= t("debates.index.tag") %></h2>
<select class="inline-block">
<option value="">Lista de temas</option>
</select>
<h2>(43)</h2>
</div>
</div>
<!-- /. Filter topic results -->
<div class="row">
<div id="debates" class="small-12 medium-9 column debates-list">
<div id="debates" class="debates-list small-12 medium-9 column">
<%= render @debates %>
</div>
<div class="small-12 medium-3 column">
@@ -10,4 +83,4 @@
</aside>
</div>
</div>
</section>
</section>

View File

@@ -10,12 +10,6 @@
<% end %>
]
</div>
<div class="external-links">
<%= link_to t("layouts.header.participation"), root_path, class: "selected" %>&nbsp;|
<%= link_to t("layouts.header.external_link_transparency"), "#" %>&nbsp;|
<%= link_to t("layouts.header.external_link_opendata"), "#" %>&nbsp;|
<%= link_to t("layouts.header.external_link_blog"), "#" %>
</div>
</div>
</section>
@@ -25,7 +19,7 @@
<li class="name">
<%= link_to root_path do %>
<%= image_tag('header_logo_madrid.png', class: 'left', size: '96x96') %>
<%= t("layouts.header.open_gov", open: "<strong>#{t('layouts.header.open')}</strong>").html_safe %> | <span><%= t("layouts.header.participation") %></span>
<%= t("layouts.header.open_gov", open: "<strong>#{t('layouts.header.open')}</strong>").html_safe %> | <span><%= t("admin.dashboard.index.title") %></span>
<% end %>
</li>
<li class="toggle-topbar menu-icon"><a href="#"><span><%= t("layouts.header.menu") %></span></a></li>

View File

@@ -12,22 +12,26 @@
<%= csrf_meta_tags %>
</head>
<body>
<body class="admin">
<%= render 'layouts/admin_header' %>
<% if notice %>
<p class="alert-box success"><%= notice %></p>
<% end %>
<div class="row">
<% if alert %>
<p class="alert-box"><%= alert %></p>
<% end %>
<div class="small-12 medium-3 column">
<%= side_menu %>
</div>
<div class="left" style="padding:50px;">
<%= side_menu %>
<div class="admin-content small-12 medium-9 column">
<% if notice %>
<div class="alert-box radius success"><%= notice %></div>
<% end %>
<% if alert %>
<div class="alert-box radius alert"><%= alert %></div>
<% end %>
<%= yield %>
</div>
</div>
<%= yield %>
</body>
</html>
</html>

View File

@@ -26,4 +26,4 @@
<%= render 'layouts/footer' %>
</body>
</html>
</html>

View File

@@ -0,0 +1,13 @@
<h1>Stats</h1>
<h3>Visits</h3>
<%= visits_chart_tag id: "visits" %>
<h3>Combined</h3>
<%= events_chart_tag @event_types, id: 'combined' %>
<% @event_types.each do |event_type| %>
<h3><%= event_type.titleize %></h3>
<%= events_chart_tag event_type %>
<% end %>

View File

@@ -1,5 +1,10 @@
<section role="main">
<div id="featured-debates" class="featured-debates row">
<div class="filters row">
<div class="small-12 column">
<h2><%= t("welcome.featured_debates") %></h2>
</div>
</div>
<div id="featured-debates" class="row">
<%= render partial: "featured_debate", collection: @featured_debates %>
</div>
</section>
</section>

View File

@@ -95,13 +95,13 @@ ignore_missing:
ignore_unused:
- 'activerecord.*'
- 'admin.organizations.index.filter.*'
- 'unauthorized.*'
- 'simple_captcha.*'
- 'admin.officials.level_*'
# - '{devise,kaminari,will_paginate}.*'
# - 'simple_form.{yes,no}'
# - 'simple_form.{placeholders,hints,labels}.*'
# - 'simple_form.{error_notification,required}.:'
ignore_unused:
- 'unauthorized.*'
- 'simple_captcha.*'
## Exclude these keys from the `i18n-tasks eq-base' report:
# ignore_eq_base:

View File

@@ -0,0 +1,9 @@
class Ahoy::Store < Ahoy::Stores::ActiveRecordStore
# Track user IP
def track_event(name, properties, options)
super do |event|
event.ip = request.ip
end
end
end

View File

@@ -0,0 +1,10 @@
Kaminari.configure do |config|
# config.default_per_page = 25
# config.max_per_page = nil
# config.window = 4
# config.outer_window = 0
# config.left = 0
# config.right = 0
# config.page_method_name = :page
# config.param_name = :page
end

View File

@@ -23,5 +23,7 @@ en:
nickname: Nickname
password: Password
phone_number: Phone number
official_position: Official position
official_level: Official level
organization:
name: Organization name

View File

@@ -23,5 +23,7 @@ es:
nickname: Pseudónimo
password: Contraseña
phone_number: Teléfono
official_position: Cargo público
official_level: Nivel del cargo
organization:
name: Nombre de organización

View File

@@ -1,13 +1,21 @@
en:
admin:
settings:
index:
title: Global settings
flash:
updated: 'Setting updated!'
dashboard:
index:
title: Administration
menu:
settings: Global settings
debate_topics: Debate topics
hidden_debates: Hidden debates
hidden_comments: Hidden comments
organizations: Organizations
officials: Officials
stats: Statistics
organizations:
index:
title: Organizations
@@ -41,5 +49,30 @@ en:
debates:
index:
title: Hidden debates
show:
back: Back
restore:
success: The debate has been restored
officials:
level_0: Level 0
level_1: Level 1
level_2: Level 2
level_3: Level 3
level_4: Level 4
level_5: Level 5
index:
title: Officials
search_email_placeholder: 'Search user by email'
search: Search
search:
title: 'Officials: Search users'
edit_official: Edit official
make_official: Make this user an official
search: Search
edit:
title: 'Officials: edit user'
destroy: "Remove 'Official' condition"
cancel: "Cancel"
flash:
official_updated: 'Official position saved!'
official_destroyed: 'User is not an official anymore'

View File

@@ -1,13 +1,21 @@
es:
admin:
settings:
index:
title: Configuración global
flash:
updated: 'Valor actualizado'
dashboard:
index:
title: Administración
menu:
settings: Configuración global
debate_topics: Temas de debate
hidden_debates: Debates ocultos
hidden_comments: Comentarios ocultos
organizations: Organizaciones
officials: Cargos públicos
stats: Estadísticas
organizations:
index:
title: Organizaciones
@@ -41,5 +49,30 @@ es:
debates:
index:
title: Debates ocultos
show:
back: Volver
restore:
success: El debate ha sido permitido
officials:
level_0: Nivel 0
level_1: Nivel 1
level_2: Nivel 2
level_3: Nivel 3
level_4: Nivel 4
level_5: Nivel 5
index:
title: Cargos Públicos
search_email_placeholder: 'Buscar usuario por email'
search: Buscar
search:
title: 'Cargos Públicos: Búsqueda de usuarios'
edit_official: Editar cargo público
make_official: Convertir en cargo público
search: Buscar
edit:
title: 'Cargos Públicos: Editar usuario'
destroy: "Eliminar condición de 'Cargo Público'"
cancel: "Cancelar"
flash:
official_updated: 'Datos del cargo público guardados'
official_destroyed: 'Datos guardados: el usuario ya no es cargo público'

View File

@@ -31,6 +31,14 @@ en:
debates:
index:
create_debate: Create a debate
showing: You are seeing
tag: with the topic
filter_debates: debates
filter_initiatives: initiatives
filter_debates_and_initiatives: debates and initiatives
filter_news: the newest
filter_votes: the most voted
filter_rated: the best rated
debate:
debate: Debate
comments:
@@ -131,3 +139,5 @@ en:
default: "You are not authorized to access this page."
manage:
all: "You are not authorized to %{action} %{subject}."
welcome:
featured_debates: Features debates

View File

@@ -31,6 +31,14 @@ es:
debates:
index:
create_debate: Crea un debate
showing: "Estás viendo"
tag: "con el tema"
filter_debates: debates
filter_initiatives: iniciativas
filter_debates_and_initiatives: debates e iniciativas
filter_news: "más nuevos"
filter_votes: "más votados"
filter_rated: mejor valorados
debate:
debate: Debate
comments:
@@ -129,17 +137,7 @@ es:
subject: Alguien ha respondido a tu comentario
unauthorized:
default: "No tienes permiso para acceder a esta página."
index:
all: "No tienes permiso para listar %{subject}"
show:
all: "No tienes permiso para ver %{subject}"
edit:
all: "No tienes permiso para editar %{subject}"
update:
all: "No tienes permiso para modificar %{subject}"
create:
all: "No tienes permiso para crear %{subject}"
delete:
all: "No tienes permiso para borrar %{subject}"
manage:
all: "No tienes permiso para realizar la acción '%{action}' sobre %{subject}."
welcome:
featured_debates: Debates destacados

View File

@@ -0,0 +1,17 @@
en:
views:
pagination:
first: "&laquo; First"
last: "Last &raquo;"
previous: "&lsaquo; Prev"
next: "Next &rsaquo;"
truncate: "&hellip;"
helpers:
page_entries_info:
one_page:
display_entries:
zero: "No %{entry_name} found"
one: "Displaying <b>1</b> %{entry_name}"
other: "Displaying <b>all %{count}</b> %{entry_name}"
more_pages:
display_entries: "Displaying %{entry_name} <b>%{first}&nbsp;-&nbsp;%{last}</b> of <b>%{total}</b> in total"

View File

@@ -0,0 +1,17 @@
es:
views:
pagination:
first: "&laquo; Primera"
last: "Última &raquo;"
previous: "&lsaquo; Anterior"
next: "Siguiente &rsaquo;"
truncate: "&hellip;"
helpers:
page_entries_info:
one_page:
display_entries:
zero: "No se han encontrado %{entry_name}"
one: "Encontrado <b>1</b> %{entry_name}"
other: "Encontrados <b> %{count}</b> %{entry_name}"
more_pages:
display_entries: "Se muestran <b> del %{first}&nbsp;al&nbsp;%{last}</b> de un total de <b>%{total}</b> %{entry_name}"

View File

@@ -21,6 +21,11 @@ Rails.application.routes.draw do
end
resource :account, controller: "account", only: [:show, :update]
resource :stats, only: [:show]
namespace :api do
resource :stats, only: [:show]
end
namespace :admin do
root to: "dashboard#index"
@@ -40,6 +45,11 @@ Rails.application.routes.draw do
end
resources :tags, only: [:index, :create, :update, :destroy]
resources :officials, only: [:index, :edit, :update, :destroy] do
collection { get :search}
end
resources :settings, only: [:index, :update]
end
namespace :moderation do

View File

@@ -0,0 +1,56 @@
class CreateVisits < ActiveRecord::Migration
def change
create_table :visits, id: false do |t|
t.uuid :id, default: nil, primary_key: true
t.uuid :visitor_id, default: nil
# the rest are recommended but optional
# simply remove the columns you don't want
# standard
t.string :ip
t.text :user_agent
t.text :referrer
t.text :landing_page
# user
t.integer :user_id
# add t.string :user_type if polymorphic
# traffic source
t.string :referring_domain
t.string :search_keyword
# technology
t.string :browser
t.string :os
t.string :device_type
t.integer :screen_height
t.integer :screen_width
# location
t.string :country
t.string :region
t.string :city
t.string :postal_code
t.decimal :latitude
t.decimal :longitude
# utm parameters
t.string :utm_source
t.string :utm_medium
t.string :utm_term
t.string :utm_content
t.string :utm_campaign
# native apps
# t.string :platform
# t.string :app_version
# t.string :os_version
t.timestamp :started_at
end
add_index :visits, [:user_id]
end
end

View File

@@ -0,0 +1,20 @@
class CreateAhoyEvents < ActiveRecord::Migration
def change
create_table :ahoy_events, id: false do |t|
t.uuid :id, default: nil, primary_key: true
t.uuid :visit_id, default: nil
# user
t.integer :user_id
# add t.string :user_type if polymorphic
t.string :name
t.jsonb :properties
t.timestamp :time
end
add_index :ahoy_events, [:visit_id]
add_index :ahoy_events, [:user_id]
add_index :ahoy_events, [:time]
end
end

View File

@@ -0,0 +1,5 @@
class AddIpToAhoyEvent < ActiveRecord::Migration
def change
add_column :ahoy_events, :ip, :string
end
end

View File

@@ -0,0 +1,5 @@
class AddVisitIdToDebate < ActiveRecord::Migration
def change
add_column :debates, :visit_id, :string
end
end

View File

@@ -0,0 +1,6 @@
class AddOfficialPositionToUser < ActiveRecord::Migration
def change
add_column :users, :official_position, :string
add_column :users, :official_level, :integer, default: 0
end
end

View File

@@ -0,0 +1,8 @@
class AddSettings < ActiveRecord::Migration
def change
create_table :settings do |t|
t.string :key
t.string :value
end
end
end

View File

@@ -11,7 +11,8 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20150815154430) do
ActiveRecord::Schema.define(version: 20150817150457) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -21,6 +22,19 @@ ActiveRecord::Schema.define(version: 20150815154430) do
add_index "administrators", ["user_id"], name: "index_administrators_on_user_id", using: :btree
create_table "ahoy_events", id: :uuid, default: nil, force: :cascade do |t|
t.uuid "visit_id"
t.integer "user_id"
t.string "name"
t.jsonb "properties"
t.datetime "time"
t.string "ip"
end
add_index "ahoy_events", ["time"], name: "index_ahoy_events_on_time", using: :btree
add_index "ahoy_events", ["user_id"], name: "index_ahoy_events_on_user_id", using: :btree
add_index "ahoy_events", ["visit_id"], name: "index_ahoy_events_on_visit_id", using: :btree
create_table "comments", force: :cascade do |t|
t.integer "commentable_id"
t.string "commentable_type"
@@ -48,6 +62,7 @@ ActiveRecord::Schema.define(version: 20150815154430) do
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.datetime "hidden_at"
t.string "visit_id"
end
add_index "debates", ["hidden_at"], name: "index_debates_on_hidden_at", using: :btree
@@ -67,6 +82,11 @@ ActiveRecord::Schema.define(version: 20150815154430) do
add_index "organizations", ["user_id"], name: "index_organizations_on_user_id", using: :btree
create_table "settings", force: :cascade do |t|
t.string "key"
t.string "value"
end
create_table "simple_captcha_data", force: :cascade do |t|
t.string "key", limit: 40
t.string "value", limit: 6
@@ -117,16 +137,48 @@ ActiveRecord::Schema.define(version: 20150815154430) do
t.datetime "confirmation_sent_at"
t.string "unconfirmed_email"
t.string "nickname"
t.boolean "use_nickname", default: false, null: false
t.boolean "email_on_debate_comment", default: false
t.boolean "email_on_comment_reply", default: false
t.string "phone_number", limit: 30
t.boolean "use_nickname", default: false, null: false
t.boolean "email_on_debate_comment", default: false
t.boolean "email_on_comment_reply", default: false
t.string "official_position"
t.integer "official_level", default: 0
end
add_index "users", ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true, using: :btree
add_index "users", ["email"], name: "index_users_on_email", unique: true, using: :btree
add_index "users", ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true, using: :btree
create_table "visits", id: :uuid, default: nil, force: :cascade do |t|
t.uuid "visitor_id"
t.string "ip"
t.text "user_agent"
t.text "referrer"
t.text "landing_page"
t.integer "user_id"
t.string "referring_domain"
t.string "search_keyword"
t.string "browser"
t.string "os"
t.string "device_type"
t.integer "screen_height"
t.integer "screen_width"
t.string "country"
t.string "region"
t.string "city"
t.string "postal_code"
t.decimal "latitude"
t.decimal "longitude"
t.string "utm_source"
t.string "utm_medium"
t.string "utm_term"
t.string "utm_content"
t.string "utm_campaign"
t.datetime "started_at"
end
add_index "visits", ["user_id"], name: "index_visits_on_user_id", using: :btree
create_table "votes", force: :cascade do |t|
t.integer "votable_id"
t.string "votable_type"

View File

@@ -1,7 +1,8 @@
# This file should contain all the record creation needed to seed the database with its default values.
# The data can then be loaded with the rake db:seed (or created alongside the db with db:setup).
#
# Examples:
#
# cities = City.create([{ name: 'Chicago' }, { name: 'Copenhagen' }])
# Mayor.create(name: 'Emanuel', city: cities.first)
# Names for the moderation console, as a hint for moderators
# to know better how to assign users with official positions
Setting.create(key: 'official_level_0_name', value: 'No cargo público')
Setting.create(key: 'official_level_1_name', value: 'Organización Municipal')
Setting.create(key: 'official_level_2_name', value: 'Funcionariado')
Setting.create(key: 'official_level_3_name', value: 'Directores generales')
Setting.create(key: 'official_level_4_name', value: 'Concejales')
Setting.create(key: 'official_level_5_name', value: 'Alcaldes')

View File

@@ -0,0 +1,95 @@
require 'rails_helper'
describe Api::StatsController do
describe 'GET index' do
let(:user) { create(:administrator).user }
context 'events or visits not present' do
it 'should respond with bad_request' do
sign_in user
get :show
expect(response).to_not be_ok
expect(response.status).to eq 400
end
end
context 'events present' do
before :each do
time_1 = DateTime.parse("2015-01-01")
time_2 = DateTime.parse("2015-01-02")
time_3 = DateTime.parse("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 'should return single events formated for working with c3.js' do
sign_in user
get :show, events: 'foo'
expect(response).to be_ok
data = JSON.parse(response.body)
expect(data).to eq "x"=>["2015-01-01", "2015-01-02"], "Foo"=>[2, 1]
end
it 'should return combined comma separated events formated for working with c3.js' do
sign_in user
get :show, events: 'foo,bar'
expect(response).to be_ok
data = JSON.parse(response.body)
expect(data).to eq "x"=>["2015-01-01", "2015-01-02", "2015-01-03"], "Foo"=>[2, 1, 0], "Bar"=>[1, 0, 2]
end
end
context 'visits present' do
it 'should return visits formated for working with c3.js' do
time_1 = DateTime.parse("2015-01-01")
time_2 = DateTime.parse("2015-01-02")
create :visit, started_at: time_1
create :visit, started_at: time_1
create :visit, started_at: time_2
sign_in user
get :show, visits: true
expect(response).to be_ok
data = JSON.parse(response.body)
expect(data).to eq "x"=>["2015-01-01", "2015-01-02"], "Visits"=>[2, 1]
end
end
context 'visits and events present' do
it 'should return combined events and visits formated for working with c3.js' do
time_1 = DateTime.parse("2015-01-01")
time_2 = DateTime.parse("2015-01-02")
create :ahoy_event, name: 'foo', time: time_1
create :ahoy_event, name: 'foo', time: time_2
create :ahoy_event, name: 'foo', time: time_2
create :visit, started_at: time_1
create :visit, started_at: time_1
create :visit, started_at: time_2
sign_in user
get :show, events: 'foo', visits: true
expect(response).to be_ok
data = JSON.parse(response.body)
expect(data).to eq "x"=>["2015-01-01", "2015-01-02"], "Foo"=>[1, 2], "Visits"=>[2, 1]
end
end
end
end

View File

@@ -0,0 +1,25 @@
require 'rails_helper'
describe DebatesController do
before(:all) do
@original_captcha_pass_value = SimpleCaptcha.always_pass
SimpleCaptcha.always_pass = true
end
after(:all) do
SimpleCaptcha.always_pass = @original_captcha_pass_value
end
describe 'POST create' do
it 'should create an ahoy event' do
sign_in create(:user)
post :create, debate: { title: 'foo', description: 'foo bar', terms_of_service: 1 }
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
end

View File

@@ -68,4 +68,19 @@ FactoryGirl.define do
end
end
factory :setting do
sequence(:key) { |n| "setting key number #{n}" }
sequence(:value) { |n| "setting number #{n} value" }
end
factory :ahoy_event, :class => Ahoy::Event do
id { SecureRandom.uuid }
time DateTime.now
sequence(:name) {|n| "Event #{n} type"}
end
factory :visit do
id { SecureRandom.uuid }
started_at DateTime.now
end
end

View File

@@ -0,0 +1,78 @@
require 'rails_helper'
feature 'Admin officials' do
background do
@citizen = create(:user, first_name: "Citizen", last_name: "Kane")
@official = create(:user, official_position: "Mayor", official_level: 5)
@admin = create(:administrator)
login_as(@admin.user)
end
scenario 'Index' do
visit admin_officials_path
expect(page).to have_content @official.name
expect(page).to_not have_content @citizen.name
expect(page).to have_content @official.official_position
expect(page).to have_content @official.official_level
end
scenario 'Edit an official' do
visit admin_officials_path
click_link @official.name
expect(current_path).to eq(edit_admin_official_path(@official))
expect(page).to_not have_content @citizen.name
expect(page).to have_content @official.name
expect(page).to have_content @official.email
fill_in 'user_official_position', with: 'School Teacher'
select '3', from: 'user_official_level'
click_button 'Update User'
expect(page).to have_content 'Official position saved!'
visit admin_officials_path
expect(page).to have_content @official.name
expect(page).to have_content 'School Teacher'
expect(page).to have_content '3'
end
scenario 'Create an official' do
visit admin_officials_path
fill_in 'email', with: @citizen.email
click_button 'Search'
expect(current_path).to eq(search_admin_officials_path)
expect(page).to_not have_content @official.name
click_link @citizen.name
fill_in 'user_official_position', with: 'Hospital manager'
select '4', from: 'user_official_level'
click_button 'Update User'
expect(page).to have_content 'Official position saved!'
visit admin_officials_path
expect(page).to have_content @official.name
expect(page).to have_content @citizen.name
expect(page).to have_content 'Hospital manager'
expect(page).to have_content '4'
end
scenario 'Destroy' do
visit edit_admin_official_path(@official)
click_link "Remove 'Official' condition"
expect(page).to have_content 'User is not an official anymore'
expect(current_path).to eq(admin_officials_path)
expect(page).to_not have_content @citizen.name
expect(page).to_not have_content @official.name
end
end

View File

@@ -0,0 +1,30 @@
require 'rails_helper'
feature 'Admin settings' do
background do
@setting1 = create(:setting)
@setting2 = create(:setting)
@setting3 = create(:setting)
login_as(create(:administrator).user)
end
scenario 'Index' do
visit admin_settings_path
expect(page).to have_content @setting1.key.classify
expect(page).to have_content @setting2.key.classify
expect(page).to have_content @setting3.key.classify
end
scenario 'Update' do
visit admin_settings_path
within("#edit_setting_#{@setting2.id}") do
fill_in "setting_#{@setting2.id}", with: 'Super Users of level 2'
click_button 'Update Setting'
end
expect(page).to have_content 'Setting updated!'
end
end

View File

@@ -15,6 +15,8 @@ feature 'Moderate debates' do
click_link 'Hide'
end
expect(page).to have_css("#debate_#{debate.id}.faded")
login_as(citizen)
visit debates_path

View File

@@ -0,0 +1,36 @@
require 'rails_helper'
describe Ahoy::DataSource do
describe '#build' do
before :each do
time_1 = DateTime.parse("2015-01-01")
time_2 = DateTime.parse("2015-01-02")
time_3 = DateTime.parse("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 'should work without data sources' do
ds = Ahoy::DataSource.new
expect(ds.build).to eq x: []
end
it 'should work with single data sources' do
ds = Ahoy::DataSource.new
ds.add 'foo', Ahoy::Event.where(name: 'foo').group_by_day(:time).count
expect(ds.build).to eq :x=>["2015-01-01", "2015-01-02"], "foo"=>[2, 1]
end
it 'should combine 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
expect(ds.build).to eq :x=>["2015-01-01", "2015-01-02", "2015-01-03"], "foo"=>[2, 1, 0], "bar"=>[1, 0, 2]
end
end
end

View File

@@ -143,4 +143,80 @@ describe User do
end
end
describe "official?" do
it "is false when the user is not an official" do
expect(subject.official_level).to eq(0)
expect(subject.official?).to be false
end
it "is true when the user is an official" do
subject.official_level = 3
subject.save
expect(subject.official?).to be true
end
end
describe "add_official_position!" do
it "is false when level not valid" do
expect(subject.add_official_position!("Boss", 89)).to be false
end
it "updates official position fields" do
expect(subject).not_to be_official
subject.add_official_position!("Veterinarian", 2)
expect(subject).to be_official
expect(subject.official_position).to eq("Veterinarian")
expect(subject.official_level).to eq(2)
subject.add_official_position!("Brain surgeon", 3)
expect(subject.official_position).to eq("Brain surgeon")
expect(subject.official_level).to eq(3)
end
end
describe "remove_official_position!" do
it "updates official position fields" do
subject.add_official_position!("Brain surgeon", 3)
expect(subject).to be_official
subject.remove_official_position!
expect(subject).not_to be_official
expect(subject.official_position).to be_nil
expect(subject.official_level).to eq(0)
end
end
describe "officials scope" do
it "returns only users with official positions" do
create(:user, official_position: "Mayor", official_level: 1)
create(:user, official_position: "Director", official_level: 3)
create(:user, official_position: "Math Teacher", official_level: 4)
create(:user, official_position: "Manager", official_level: 5)
2.times { create(:user) }
officials = User.officials
expect(officials.size).to eq(4)
officials.each do |user|
expect(user.official_level).to be > 0
expect(user.official_position).to be_present
end
end
end
describe "self.with_email" do
it "find users by email" do
user1 = create(:user, email: "larry@madrid.es")
create(:user, email: "bird@madrid.es")
search = User.with_email("larry@madrid.es")
expect(search.size).to eq(1)
expect(search.first).to eq(user1)
end
it "returns no results if no email provided" do
expect(User.with_email(" ").size).to eq(0)
end
end
end

View File

@@ -1,6 +1,7 @@
require 'factory_girl_rails'
require 'database_cleaner'
require "email_spec"
require 'email_spec'
require 'devise'
Dir["./spec/support/**/*.rb"].sort.each { |f| require f }
RSpec.configure do |config|
@@ -8,6 +9,7 @@ RSpec.configure do |config|
config.filter_run :focus
config.run_all_when_everything_filtered = true
config.include Devise::TestHelpers, :type => :controller
config.include FactoryGirl::Syntax::Methods
config.include(EmailSpec::Helpers)
config.include(EmailSpec::Matchers)

7049
vendor/assets/javascripts/c3.js vendored Normal file

File diff suppressed because it is too large Load Diff

380
vendor/assets/javascripts/c3ext.js vendored Normal file
View File

@@ -0,0 +1,380 @@
var c3ext = {};
c3ext.generate = function (options) {
if (options.zoom2 != null) {
zoom2_reducers = options.zoom2.reducers || {};
zoom2_enabled = options.zoom2.enabled;
_zoom2_factor = options.zoom2.factor || 1;
_zoom2_maxItems = options.zoom2.maxItems;
}
if (!zoom2_enabled) {
return c3.generate(options);
}
var originalData = Q.copy(options.data);
var zoom2_reducers;
var zoom2_enabled;
var _zoom2_maxItems;
if (_zoom2_maxItems == null) {
var el = d3.select(options.bindto)[0][0];
if (el != null) {
var availWidth = el.clientWidth;
var pointSize = 20;
_zoom2_maxItems = Math.ceil(availWidth / pointSize);
}
if (_zoom2_maxItems == null || _zoom2_maxItems < 10) {
_zoom2_maxItems = 10;
}
}
function onZoomChanged(e) {
refresh();
}
var zoom2 = c3ext.ZoomBehavior({ changed: onZoomChanged, bindto: options.bindto });
zoom2.enhance = function () {
_zoom2_maxItems *= 2;
var totalItems = zoom2.getZoom().totalItems;
if (_zoom2_maxItems > totalItems)
_zoom2_maxItems = totalItems;
refresh();
}
zoom2.dehance = function () {
_zoom2_maxItems = Math.ceil(_zoom2_maxItems / 2) + 1;
refresh();
}
zoom2.maxItems = function () { return _zoom2_maxItems; };
function zoomAndReduceData(list, zoomRange, func, maxItems) {
//var maxItems = 10;//Math.ceil(10 * zoomFactor);
var list2 = list.slice(zoomRange[0], zoomRange[1]);
var chunkSize = 1;
var list3 = list2;
if (list3.length > maxItems) {
var chunkSize = Math.ceil(list2.length / maxItems);
list3 = list3.splitIntoChunksOf(chunkSize).map(func);
}
//console.log("x" + getCurrentZoomLevel() + ", maxItems=" + maxItems + " chunkSize=" + chunkSize + " totalBefore=" + list2.length + ", totalAfter=" + list3.length);
return list3;
}
function first(t) { return t[0]; }
var getDataForZoom = function (data) {
if (data.columns == null || data.columns.length == 0)
return;
var zoomInfo = zoom2.getZoom();
if (zoomInfo.totalItems != data.columns[0].length - 1) {
zoom2.setOptions({ totalItems: data.columns[0].length - 1 });
zoomInfo = zoom2.getZoom();
}
data.columns = originalData.columns.map(function (column) {
var name = column[0];
var reducer = zoom2_reducers[name] || first; //by default take the first
var values = column.slice(1);
var newValues = zoomAndReduceData(values, zoomInfo.currentZoom, reducer, _zoom2_maxItems);
return [name].concat(newValues);
});
return data;
};
getDataForZoom(options.data);
var chart = c3.generate(options);
var _chart_load_org = chart.load.bind(chart);
chart.zoom2 = zoom2;
chart.load = function (data) {
if (data.unload) {
unload(data.unload);
delete data.unload;
}
Q.copy(data, originalData);
refresh();
}
chart.unload = function (names) {
unload(names);
refresh();
}
function unload(names) {
originalData.columns.removeAll(function (t) { names.contains(t); });
}
function refresh() {
var data = Q.copy(originalData)
getDataForZoom(data);
_chart_load_org(data);
};
return chart;
}
c3ext.ZoomBehavior = function (options) {
var zoom = { __type: "ZoomBehavior" };
var _zoom2_factor;
var _left;
var totalItems;
var currentZoom;
var bindto = options.bindto;
var _zoomChanged = options.changed || function () { };
var element;
var mousewheelTimer;
var deltaY = 0;
var leftRatio = 0;
zoom.setOptions = function (options) {
if (options == null)
options = {};
_zoom2_factor = options.factor || 1;
_left = 0;
totalItems = options.totalItems || 0;
currentZoom = [0, totalItems];
_zoomChanged = options.changed || _zoomChanged;
}
zoom.setOptions(options);
function verifyZoom(newZoom) {
//newZoom.sort();
if (newZoom[1] > totalItems) {
var diff = newZoom[1] - totalItems;
newZoom[0] -= diff;
newZoom[1] -= diff;
}
if (newZoom[0] < 0) {
var diff = newZoom[0] * -1;
newZoom[0] += diff;
newZoom[1] += diff;
}
if (newZoom[1] > totalItems)
newZoom[1] = totalItems;
if (newZoom[0] < 0)
newZoom[0] = 0;
}
function zoomAndPan(zoomFactor, left) {
var itemsToShow = Math.ceil(totalItems / zoomFactor);
var newZoom = [left, left + itemsToShow];
verifyZoom(newZoom);
currentZoom = newZoom;
onZoomChanged();
}
function onZoomChanged() {
if (_zoomChanged != null)
_zoomChanged(zoom.getZoom());
}
function applyZoomAndPan() {
zoomAndPan(_zoom2_factor, _left);
}
function getItemsToShow() {
var itemsToShow = Math.ceil(totalItems / _zoom2_factor);
return itemsToShow;
}
zoom.getZoom = function () {
return { totalItems: totalItems, currentZoom: currentZoom.slice() };
}
zoom.factor = function (factor, skipDraw) {
if (arguments.length == 0)
return _zoom2_factor;
_zoom2_factor = factor;
if (_zoom2_factor < 1)
_zoom2_factor = 1;
if (skipDraw)
return;
applyZoomAndPan();
}
zoom.left = function (left, skipDraw) {
if (arguments.length == 0)
return _left;
_left = left;
if (_left < 0)
_left = 0;
var pageSize = getItemsToShow();
//_left += pageSize;
if (_left + pageSize > totalItems)
_left = totalItems - pageSize;
console.log({ left: _left, pageSize: pageSize });
if (skipDraw)
return;
applyZoomAndPan();
}
zoom.zoomAndPanByRatio = function (zoomRatio, panRatio) {
var pageSize = getItemsToShow();
var leftOffset = Math.round(pageSize * panRatio);
var mouseLeft = _left + leftOffset;
zoom.factor(zoom.factor() * zoomRatio, true);
var finalLeft = mouseLeft;
if (zoomRatio != 1) {
var pageSize2 = getItemsToShow();
var leftOffset2 = Math.round(pageSize2 * panRatio);
finalLeft = mouseLeft - leftOffset2;
}
zoom.left(finalLeft, true);
applyZoomAndPan();
}
zoom.zoomIn = function () {
zoom.zoomAndPanByRatio(2, 0);
}
zoom.zoomOut = function () {
zoom.zoomAndPanByRatio(0.5, 0);
}
zoom.panLeft = function () {
zoom.zoomAndPanByRatio(1, -1);
}
zoom.panRight = function () {
zoom.zoomAndPanByRatio(1, 1);
}
zoom.reset = function () {
_left = 0;
_zoom2_factor = 1;
applyZoomAndPan();
}
function doZoom() {
if (deltaY != 0) {
var maxDelta = 10;
var multiply = (maxDelta + deltaY) / maxDelta;
//var factor = chart.zoom2.factor()*multiply;
//factor= Math.ceil(factor*100) / 100;
console.log({ deltaY: deltaY, multiply: multiply });
zoom.zoomAndPanByRatio(multiply, leftRatio);//0.5);//leftRatio);
deltaY = 0;
}
}
function element_mousewheel(e) {
deltaY += e.deltaY;
leftRatio = (e.offsetX - 70) / (e.currentTarget.offsetWidth - 70);
//console.log({ "e.offsetX": e.offsetX, "e.currentTarget.offsetWidth": e.currentTarget.offsetWidth, leftRatio: leftRatio });
mousewheelTimer.set(150);
e.preventDefault();
}
if (bindto != null) {
element = $(options.bindto);
if (element.mousewheel) {
mousewheelTimer = new Timer(doZoom);
element.mousewheel(element_mousewheel);
}
}
return zoom;
}
if (typeof (Q) == "undefined") {
var Q = function () {
};
Q.copy = function (src, target, options, depth) {
///<summary>Copies an object into a target object, recursively cloning any object or array in the way, overwrite=true will overwrite a primitive field value even if exists</summary>
///<param name="src" />
///<param name="target" />
///<param name="options" type="Object">{ overwrite:false }</param>
///<returns type="Object">The copied object</returns>
if (depth == null)
depth = 0;
if (depth == 100) {
console.warn("Q.copy is in depth of 100 - possible circular reference")
}
options = options || { overwrite: false };
if (src == target || src == null)
return target;
if (typeof (src) != "object") {
if (options.overwrite || target == null)
return src;
return target;
}
if (typeof (src.clone) == "function") {
if (options.overwrite || target == null)
return src.clone();
return target;
}
if (target == null) {
if (src instanceof Array)
target = [];
else
target = {};
}
if (src instanceof Array) {
for (var i = 0; i < src.length; i++) {
var item = src[i];
var item2 = target[i];
item2 = Q.copy(item, item2, options, depth + 1);
target[i] = item2;
}
target.splice(src.length, target.length - src.length);
return target;
}
for (var p in src) {
var value = src[p];
var value2 = target[p];
value2 = Q.copy(value, value2, options, depth + 1);
target[p] = value2;
}
return target;
}
}
if (typeof (Timer) == "undefined") {
var Timer = function (action, ms) {
this.action = action;
if (ms != null)
this.set(ms);
}
Timer.prototype.set = function (ms) {
if (ms == null)
ms = this._ms;
else
this._ms = ms;
this.clear();
if (ms == null)
return;
this.timeout = window.setTimeout(this.onTick.bind(this), ms);
}
Timer.prototype.onTick = function () {
this.clear();
this.action();
}
Timer.prototype.clear = function (ms) {
if (this.timeout == null)
return;
window.clearTimeout(this.timeout);
this.timeout = null;
}
}
if (typeof(Array.prototype.splitIntoChunksOf)=="undefined") {
Array.prototype.splitIntoChunksOf = function (countInEachChunk) {
var chunks = Math.ceil(this.length / countInEachChunk);
var list = [];
for (var i = 0; i < this.length; i += countInEachChunk) {
list.push(this.slice(i, i + countInEachChunk));
}
return list;
}
}

9503
vendor/assets/javascripts/d3.js vendored Normal file

File diff suppressed because it is too large Load Diff

163
vendor/assets/stylesheets/c3.css vendored Normal file
View File

@@ -0,0 +1,163 @@
/*-- Chart --*/
.c3 svg {
font: 10px sans-serif;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0); }
.c3 path, .c3 line {
fill: none;
stroke: #000; }
.c3 text {
-webkit-user-select: none;
-moz-user-select: none;
user-select: none; }
.c3-legend-item-tile, .c3-xgrid-focus, .c3-ygrid, .c3-event-rect, .c3-bars path {
shape-rendering: crispEdges; }
.c3-chart-arc path {
stroke: #fff; }
.c3-chart-arc text {
fill: #fff;
font-size: 13px; }
/*-- Axis --*/
/*-- Grid --*/
.c3-grid line {
stroke: #aaa; }
.c3-grid text {
fill: #aaa; }
.c3-xgrid, .c3-ygrid {
stroke-dasharray: 3 3; }
/*-- Text on Chart --*/
.c3-text.c3-empty {
fill: #808080;
font-size: 2em; }
/*-- Line --*/
.c3-line {
stroke-width: 1px; }
/*-- Point --*/
.c3-circle._expanded_ {
stroke-width: 1px;
stroke: white; }
.c3-selected-circle {
fill: white;
stroke-width: 2px; }
/*-- Bar --*/
.c3-bar {
stroke-width: 0; }
.c3-bar._expanded_ {
fill-opacity: 0.75; }
/*-- Focus --*/
.c3-target.c3-focused {
opacity: 1; }
.c3-target.c3-focused path.c3-line, .c3-target.c3-focused path.c3-step {
stroke-width: 2px; }
.c3-target.c3-defocused {
opacity: 0.3 !important; }
/*-- Region --*/
.c3-region {
fill: steelblue;
fill-opacity: 0.1; }
/*-- Brush --*/
.c3-brush .extent {
fill-opacity: 0.1; }
/*-- Select - Drag --*/
/*-- Legend --*/
.c3-legend-item {
font-size: 12px; }
.c3-legend-item-hidden {
opacity: 0.15; }
.c3-legend-background {
opacity: 0.75;
fill: white;
stroke: lightgray;
stroke-width: 1; }
/*-- Title --*/
.c3-title {
font: 14px sans-serif; }
/*-- Tooltip --*/
.c3-tooltip-container {
z-index: 10; }
.c3-tooltip {
border-collapse: collapse;
border-spacing: 0;
background-color: #fff;
empty-cells: show;
-webkit-box-shadow: 7px 7px 12px -9px #777777;
-moz-box-shadow: 7px 7px 12px -9px #777777;
box-shadow: 7px 7px 12px -9px #777777;
opacity: 0.9; }
.c3-tooltip tr {
border: 1px solid #CCC; }
.c3-tooltip th {
background-color: #aaa;
font-size: 14px;
padding: 2px 5px;
text-align: left;
color: #FFF; }
.c3-tooltip td {
font-size: 13px;
padding: 3px 6px;
background-color: #fff;
border-left: 1px dotted #999; }
.c3-tooltip td > span {
display: inline-block;
width: 10px;
height: 10px;
margin-right: 6px; }
.c3-tooltip td.value {
text-align: right; }
/*-- Area --*/
.c3-area {
stroke-width: 0;
opacity: 0.2; }
/*-- Arc --*/
.c3-chart-arcs-title {
dominant-baseline: middle;
font-size: 1.3em; }
.c3-chart-arcs .c3-chart-arcs-background {
fill: #e0e0e0;
stroke: none; }
.c3-chart-arcs .c3-chart-arcs-gauge-unit {
fill: #000;
font-size: 16px; }
.c3-chart-arcs .c3-chart-arcs-gauge-max {
fill: #777; }
.c3-chart-arcs .c3-chart-arcs-gauge-min {
fill: #777; }
.c3-chart-arc .c3-gauge-value {
fill: #000;
/* font-size: 28px !important;*/ }