merges master and fixes conflicts
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
language: ruby
|
||||
addons:
|
||||
postgresql: "9.4"
|
||||
rvm:
|
||||
- "2.2.2"
|
||||
cache: bundler
|
||||
|
||||
4
Gemfile
4
Gemfile
@@ -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'
|
||||
|
||||
30
Gemfile.lock
30
Gemfile.lock
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
@@ -21,4 +21,5 @@
|
||||
<glyph unicode="l" 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="m" 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="n" 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="p" 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.
@@ -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(){
|
||||
|
||||
11
app/assets/javascripts/stats.js.coffee
Normal file
11
app/assets/javascripts/stats.js.coffee
Normal 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]")
|
||||
153
app/assets/stylesheets/admin.scss
Normal file
153
app/assets/stylesheets/admin.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -6,5 +6,7 @@
|
||||
@import "fonts";
|
||||
@import "icons";
|
||||
@import "variables";
|
||||
@import "admin";
|
||||
@import "participacion";
|
||||
@import "debates";
|
||||
@import "c3";
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -79,3 +79,6 @@
|
||||
.icon-star:before {
|
||||
content: "n";
|
||||
}
|
||||
.icon-eye:before {
|
||||
content: "p";
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
32
app/controllers/admin/officials_controller.rb
Normal file
32
app/controllers/admin/officials_controller.rb
Normal 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
|
||||
17
app/controllers/admin/settings_controller.rb
Normal file
17
app/controllers/admin/settings_controller.rb
Normal 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
|
||||
13
app/controllers/api/api_controller.rb
Normal file
13
app/controllers/api/api_controller.rb
Normal 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
|
||||
22
app/controllers/api/stats_controller.rb
Normal file
22
app/controllers/api/stats_controller.rb
Normal 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
|
||||
@@ -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
|
||||
|
||||
14
app/controllers/stats_controller.rb
Normal file
14
app/controllers/stats_controller.rb
Normal 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
|
||||
@@ -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
|
||||
|
||||
16
app/helpers/stats_helper.rb
Normal file
16
app/helpers/stats_helper.rb
Normal 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
|
||||
49
app/models/ahoy/data_source.rb
Normal file
49
app/models/ahoy/data_source.rb
Normal 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
8
app/models/ahoy/event.rb
Normal file
@@ -0,0 +1,8 @@
|
||||
module Ahoy
|
||||
class Event < ActiveRecord::Base
|
||||
self.table_name = "ahoy_events"
|
||||
|
||||
belongs_to :visit
|
||||
belongs_to :user
|
||||
end
|
||||
end
|
||||
@@ -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
7
app/models/setting.rb
Normal 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
|
||||
@@ -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
4
app/models/visit.rb
Normal file
@@ -0,0 +1,4 @@
|
||||
class Visit < ActiveRecord::Base
|
||||
has_many :ahoy_events, class_name: "Ahoy::Event"
|
||||
belongs_to :user
|
||||
end
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" %>
|
||||
|
||||
14
app/views/admin/officials/edit.html.erb
Normal file
14
app/views/admin/officials/edit.html.erb
Normal 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 %>
|
||||
25
app/views/admin/officials/index.html.erb
Normal file
25
app/views/admin/officials/index.html.erb
Normal 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>
|
||||
21
app/views/admin/officials/search.html.erb
Normal file
21
app/views/admin/officials/search.html.erb
Normal 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>
|
||||
15
app/views/admin/settings/index.html.erb
Normal file
15
app/views/admin/settings/index.html.erb
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -10,12 +10,6 @@
|
||||
<% end %>
|
||||
]
|
||||
</div>
|
||||
<div class="external-links">
|
||||
<%= link_to t("layouts.header.participation"), root_path, class: "selected" %> |
|
||||
<%= link_to t("layouts.header.external_link_transparency"), "#" %> |
|
||||
<%= link_to t("layouts.header.external_link_opendata"), "#" %> |
|
||||
<%= 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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -26,4 +26,4 @@
|
||||
|
||||
<%= render 'layouts/footer' %>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
13
app/views/stats/show.html.erb
Normal file
13
app/views/stats/show.html.erb
Normal 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 %>
|
||||
@@ -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>
|
||||
|
||||
@@ -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:
|
||||
|
||||
9
config/initializers/ahoy.rb
Normal file
9
config/initializers/ahoy.rb
Normal 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
|
||||
10
config/initializers/kaminari_config.rb
Normal file
10
config/initializers/kaminari_config.rb
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
17
config/locales/kaminari.en.yml
Normal file
17
config/locales/kaminari.en.yml
Normal file
@@ -0,0 +1,17 @@
|
||||
en:
|
||||
views:
|
||||
pagination:
|
||||
first: "« First"
|
||||
last: "Last »"
|
||||
previous: "‹ Prev"
|
||||
next: "Next ›"
|
||||
truncate: "…"
|
||||
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} - %{last}</b> of <b>%{total}</b> in total"
|
||||
17
config/locales/kaminari.es.yml
Normal file
17
config/locales/kaminari.es.yml
Normal file
@@ -0,0 +1,17 @@
|
||||
es:
|
||||
views:
|
||||
pagination:
|
||||
first: "« Primera"
|
||||
last: "Última »"
|
||||
previous: "‹ Anterior"
|
||||
next: "Siguiente ›"
|
||||
truncate: "…"
|
||||
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} al %{last}</b> de un total de <b>%{total}</b> %{entry_name}"
|
||||
@@ -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
|
||||
|
||||
56
db/migrate/20150804144230_create_visits.rb
Normal file
56
db/migrate/20150804144230_create_visits.rb
Normal 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
|
||||
20
db/migrate/20150804144231_create_ahoy_events.rb
Normal file
20
db/migrate/20150804144231_create_ahoy_events.rb
Normal 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
|
||||
5
db/migrate/20150808100936_add_ip_to_ahoy_event.rb
Normal file
5
db/migrate/20150808100936_add_ip_to_ahoy_event.rb
Normal file
@@ -0,0 +1,5 @@
|
||||
class AddIpToAhoyEvent < ActiveRecord::Migration
|
||||
def change
|
||||
add_column :ahoy_events, :ip, :string
|
||||
end
|
||||
end
|
||||
5
db/migrate/20150808102442_add_visit_id_to_debate.rb
Normal file
5
db/migrate/20150808102442_add_visit_id_to_debate.rb
Normal file
@@ -0,0 +1,5 @@
|
||||
class AddVisitIdToDebate < ActiveRecord::Migration
|
||||
def change
|
||||
add_column :debates, :visit_id, :string
|
||||
end
|
||||
end
|
||||
@@ -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
|
||||
8
db/migrate/20150817150457_add_settings.rb
Normal file
8
db/migrate/20150817150457_add_settings.rb
Normal 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
|
||||
60
db/schema.rb
60
db/schema.rb
@@ -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"
|
||||
|
||||
15
db/seeds.rb
15
db/seeds.rb
@@ -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')
|
||||
95
spec/controllers/api/stats_controller_spec.rb
Normal file
95
spec/controllers/api/stats_controller_spec.rb
Normal 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
|
||||
25
spec/controllers/debates_controller_spec.rb
Normal file
25
spec/controllers/debates_controller_spec.rb
Normal 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
|
||||
@@ -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
|
||||
|
||||
78
spec/features/admin/officials_spec.rb
Normal file
78
spec/features/admin/officials_spec.rb
Normal 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
|
||||
30
spec/features/admin/settings_spec.rb
Normal file
30
spec/features/admin/settings_spec.rb
Normal 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
|
||||
@@ -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
|
||||
|
||||
|
||||
36
spec/models/ahoy/data_source_spec.rb
Normal file
36
spec/models/ahoy/data_source_spec.rb
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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
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
380
vendor/assets/javascripts/c3ext.js
vendored
Normal 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
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
163
vendor/assets/stylesheets/c3.css
vendored
Normal 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;*/ }
|
||||
Reference in New Issue
Block a user