diff --git a/Gemfile b/Gemfile
index a364c6a..4e8f058 100644
--- a/Gemfile
+++ b/Gemfile
@@ -1,10 +1,10 @@
source 'http://rubygems.org'
-gem 'rails', '4.0.2'
+gem 'rails', '4.0.3'
gem 'jquery-rails', '3.0.4'
gem 'rails_autolink'
gem 'mysql2'
-gem 'authlogic'
+gem 'devise'
gem 'twitter_oauth', git: 'git://github.com/moomerman/twitter_oauth.git'
gem 'therubyracer'
gem 'exception_notification'
@@ -29,8 +29,9 @@ gem 'capistrano', '~> 2.15.4'
gem 'rack-utf8_sanitizer'
gem 'newrelic_rpm'
gem 'actionpack-xml_parser'
+gem 'activeadmin', github: 'gregbell/active_admin'
-# To use debugger
+# to use debugger
# gem 'ruby-debug'
# assets
@@ -41,7 +42,7 @@ gem 'uglifier'
group :development do
gem 'annotate', '~> 2.6.1'
gem 'quiet_assets'
- gem 'thin'
+ gem 'puma'
gem 'i18n-tasks', '~> 0.2.10'
end
diff --git a/Gemfile.lock b/Gemfile.lock
index 9902680..f473234 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1,3 +1,21 @@
+GIT
+ remote: git://github.com/gregbell/active_admin.git
+ revision: 3fb7f03335b1ec5743c305f2c37103e2bfface97
+ specs:
+ activeadmin (1.0.0.pre)
+ arbre (~> 1.0)
+ bourbon
+ coffee-rails
+ devise (~> 3.2)
+ formtastic (~> 2.3.0.rc2)
+ inherited_resources (~> 1.3)
+ jquery-rails
+ jquery-ui-rails
+ kaminari (~> 0.15)
+ rails (>= 3.2, < 4.1)
+ ransack (~> 1.0)
+ sass-rails
+
GIT
remote: git://github.com/moomerman/twitter_oauth.git
revision: 04e6bbfe635a376cae342d234214cdab864fe797
@@ -17,27 +35,27 @@ GEM
remote: http://rubygems.org/
specs:
ZenTest (4.9.5)
- actionmailer (4.0.2)
- actionpack (= 4.0.2)
+ actionmailer (4.0.3)
+ actionpack (= 4.0.3)
mail (~> 2.5.4)
- actionpack (4.0.2)
- activesupport (= 4.0.2)
+ actionpack (4.0.3)
+ activesupport (= 4.0.3)
builder (~> 3.1.0)
erubis (~> 2.7.0)
rack (~> 1.5.2)
rack-test (~> 0.6.2)
actionpack-xml_parser (1.0.1)
actionpack (>= 4.0.0.rc1)
- activemodel (4.0.2)
- activesupport (= 4.0.2)
+ activemodel (4.0.3)
+ activesupport (= 4.0.3)
builder (~> 3.1.0)
- activerecord (4.0.2)
- activemodel (= 4.0.2)
+ activerecord (4.0.3)
+ activemodel (= 4.0.3)
activerecord-deprecated_finders (~> 1.0.2)
- activesupport (= 4.0.2)
+ activesupport (= 4.0.3)
arel (~> 4.0.0)
activerecord-deprecated_finders (1.0.3)
- activesupport (4.0.2)
+ activesupport (4.0.3)
i18n (~> 0.6, >= 0.6.4)
minitest (~> 4.2)
multi_json (~> 1.3)
@@ -51,15 +69,20 @@ GEM
annotate (2.6.1)
activerecord (>= 2.3.0)
rake (>= 0.8.7)
- arel (4.0.1)
+ arbre (1.0.1)
+ activesupport (>= 3.0.0)
+ arel (4.0.2)
atomic (1.1.14)
- authlogic (3.3.0)
- activerecord (>= 3.2)
- activesupport (>= 3.2)
autotest (4.4.6)
ZenTest (>= 4.4.1)
autotest-rails (4.2.1)
ZenTest (~> 4.5)
+ bcrypt (3.1.7)
+ bcrypt-ruby (3.1.5)
+ bcrypt (>= 3.1.3)
+ bourbon (3.1.8)
+ sass (>= 3.2.0)
+ thor
builder (3.1.4)
capistrano (2.15.4)
highline
@@ -78,6 +101,12 @@ GEM
daemons (1.1.9)
dalli (2.7.0)
database_cleaner (1.2.0)
+ devise (3.2.3)
+ bcrypt-ruby (~> 3.0)
+ orm_adapter (~> 0.1)
+ railties (>= 3.2.6, < 5)
+ thread_safe (~> 0.1)
+ warden (~> 1.2.3)
diff-lcs (1.2.5)
dynamic_form (1.1.4)
easy_translate (0.4.0)
@@ -111,9 +140,14 @@ GEM
i18n (~> 0.5)
faraday (0.8.9)
multipart-post (~> 1.2.0)
+ formtastic (2.3.0.rc2)
+ actionpack (>= 3.0)
geokit (1.8.4)
multi_json (>= 1.3.2)
gravatarify (3.1.0)
+ has_scope (0.6.0.rc)
+ actionpack (>= 3.2, < 5)
+ activesupport (>= 3.2, < 5)
highline (1.6.20)
hike (1.2.3)
http_parser.rb (0.6.0)
@@ -126,13 +160,21 @@ GEM
rake
term-ansicolor
terminal-table
+ inherited_resources (1.4.1)
+ has_scope (~> 0.6.0.rc)
+ responders (~> 1.0.0.rc)
jquery-rails (3.0.4)
railties (>= 3.0, < 5.0)
thor (>= 0.14, < 2.0)
+ jquery-ui-rails (4.2.0)
+ railties (>= 3.2.16)
json (1.8.1)
json_spec (1.1.1)
multi_json (~> 1.0)
rspec (~> 2.0)
+ kaminari (0.15.1)
+ actionpack (>= 3.0.0)
+ activesupport (>= 3.0.0)
kgio (2.8.1)
libv8 (3.16.14.3)
mail (2.5.4)
@@ -157,7 +199,12 @@ GEM
nokogiri (1.6.1)
mini_portile (~> 0.5.0)
oauth (0.4.7)
- polyglot (0.3.3)
+ orm_adapter (0.5.0)
+ polyamorous (0.6.4)
+ activerecord (>= 3.0)
+ polyglot (0.3.4)
+ puma (2.8.1)
+ rack (>= 1.1, < 2.0)
quiet_assets (1.0.2)
railties (>= 3.1, < 5.0)
rack (1.5.2)
@@ -167,26 +214,32 @@ GEM
rack (>= 1.0)
rack-utf8_sanitizer (1.1.0)
rack (~> 1.0)
- rails (4.0.2)
- actionmailer (= 4.0.2)
- actionpack (= 4.0.2)
- activerecord (= 4.0.2)
- activesupport (= 4.0.2)
+ rails (4.0.3)
+ actionmailer (= 4.0.3)
+ actionpack (= 4.0.3)
+ activerecord (= 4.0.3)
+ activesupport (= 4.0.3)
bundler (>= 1.3.0, < 2.0)
- railties (= 4.0.2)
+ railties (= 4.0.3)
sprockets-rails (~> 2.0.0)
rails_autolink (1.1.5)
rails (> 3.1)
- railties (4.0.2)
- actionpack (= 4.0.2)
- activesupport (= 4.0.2)
+ railties (4.0.3)
+ actionpack (= 4.0.3)
+ activesupport (= 4.0.3)
rake (>= 0.8.7)
thor (>= 0.18.1, < 2.0)
rake (10.1.1)
+ ransack (1.1.0)
+ actionpack (>= 3.0)
+ activerecord (>= 3.0)
+ polyamorous (~> 0.6.0)
redis (3.0.7)
redis-namespace (1.4.1)
redis (~> 3.0.4)
ref (1.0.5)
+ responders (1.0.0)
+ railties (>= 3.2, < 5)
resque (1.25.1)
mono_logger (~> 1.0)
multi_json (~> 1.0)
@@ -228,7 +281,7 @@ GEM
rack-protection (~> 1.4)
tilt (~> 1.3, >= 1.3.4)
spork (0.9.2)
- sprockets (2.10.1)
+ sprockets (2.11.0)
hike (~> 1.2)
multi_json (~> 1.0)
rack (~> 1.0)
@@ -246,10 +299,6 @@ GEM
therubyracer (0.12.0)
libv8 (~> 3.16.14.0)
ref
- thin (1.6.1)
- daemons (>= 1.0.9)
- eventmachine (>= 1.0.0)
- rack (>= 1.0.0)
thor (0.18.1)
thread (0.1.3)
thread_safe (0.1.3)
@@ -275,6 +324,8 @@ GEM
json (>= 1.8.0)
vegas (0.1.11)
rack (>= 1.0.0)
+ warden (1.2.3)
+ rack (>= 1.0)
webrat (0.7.3)
nokogiri (>= 1.2.0)
rack (>= 1.0)
@@ -287,10 +338,10 @@ PLATFORMS
DEPENDENCIES
ZenTest
actionpack-xml_parser
+ activeadmin!
acts_as_list
acts_as_tree
annotate (~> 2.6.1)
- authlogic
autotest
autotest-rails
capistrano (~> 2.15.4)
@@ -298,6 +349,7 @@ DEPENDENCIES
daemons
dalli
database_cleaner (~> 1.2.0)
+ devise
dynamic_form
exception_notification
factory_girl_rails
@@ -312,9 +364,10 @@ DEPENDENCIES
nested_form
newrelic_rpm
nokogiri
+ puma
quiet_assets
rack-utf8_sanitizer
- rails (= 4.0.2)
+ rails (= 4.0.3)
rails_autolink
redis
resque-scheduler (= 2.3.1)
@@ -325,7 +378,6 @@ DEPENDENCIES
spork
sqlite3-ruby (= 1.3.3)
therubyracer
- thin
tweetstream
twitter_oauth!
uglifier
diff --git a/app/admin/channel.rb b/app/admin/channel.rb
new file mode 100644
index 0000000..837ca1e
--- /dev/null
+++ b/app/admin/channel.rb
@@ -0,0 +1,25 @@
+ActiveAdmin.register Channel do
+
+ filter :name
+ filter :description
+ filter :created_at
+
+ permit_params :name, :public_flag
+
+ index do
+ column :id
+ column(:name) { |channel| link_to channel.name, channel }
+ column(:user) { |channel| link_to channel.user.login, admin_user_path(channel.user) if channel.user.present? }
+ column :public_flag
+ column :created_at
+ default_actions
+ end
+
+ form do |f|
+ f.semantic_errors *f.object.errors.keys
+ f.inputs :name, :public_flag
+ f.actions
+ end
+
+end
+
diff --git a/app/admin/dashboard.rb b/app/admin/dashboard.rb
new file mode 100644
index 0000000..4acc8a5
--- /dev/null
+++ b/app/admin/dashboard.rb
@@ -0,0 +1,28 @@
+ActiveAdmin.register_page "Dashboard" do
+
+ menu priority: 1, label: proc{ I18n.t("active_admin.dashboard") }
+
+ content title: proc{ I18n.t("active_admin.dashboard") } do
+ columns do
+
+ column do
+ panel "Stats" do
+ para "Total Users: #{User.all.count}"
+ para "Total Channels: #{Channel.all.count}"
+ end
+ end
+
+ column do
+ panel "Recent Channels" do
+ ul do
+ Channel.all.order("created_at desc").limit(5).map do |channel|
+ li link_to(channel.name, admin_channel_path(channel))
+ end
+ end
+ end
+ end
+
+ end
+ end # content
+end
+
diff --git a/app/admin/failedlogin.rb b/app/admin/failedlogin.rb
new file mode 100644
index 0000000..44d477c
--- /dev/null
+++ b/app/admin/failedlogin.rb
@@ -0,0 +1,19 @@
+ActiveAdmin.register Failedlogin do
+ menu :parent => "Others"
+ actions :all, :except => [:edit]
+
+ filter :login
+ filter :password
+ filter :created_at
+
+ index do
+ column :id
+ column :login
+ column :password
+ column :ip_address
+ column :created_at
+ default_actions
+ end
+
+end
+
diff --git a/app/admin/plugin.rb b/app/admin/plugin.rb
new file mode 100644
index 0000000..647b3f0
--- /dev/null
+++ b/app/admin/plugin.rb
@@ -0,0 +1,23 @@
+ActiveAdmin.register Plugin do
+
+ filter :name
+ filter :created_at
+
+ permit_params :name, :html, :css, :js, :private_flag
+
+ index do
+ column :id
+ column(:user) { |object| link_to object.user.login, admin_user_path(object.user) if object.user.present? }
+ column :name
+ column :private_flag
+ default_actions
+ end
+
+ form do |f|
+ f.semantic_errors *f.object.errors.keys
+ f.inputs :name, :html, :css, :js, :private_flag
+ f.actions
+ end
+
+end
+
diff --git a/app/admin/useful_links.rb b/app/admin/useful_links.rb
new file mode 100644
index 0000000..f372d0c
--- /dev/null
+++ b/app/admin/useful_links.rb
@@ -0,0 +1,9 @@
+ActiveAdmin.register_page "Useful Links" do
+ menu :parent => "Others"
+
+ content do
+ render "index"
+ end
+
+end
+
diff --git a/app/admin/user.rb b/app/admin/user.rb
new file mode 100644
index 0000000..3424983
--- /dev/null
+++ b/app/admin/user.rb
@@ -0,0 +1,48 @@
+ActiveAdmin.register User do
+ require 'csv'
+
+ filter :email
+ filter :login
+ filter :created_at
+
+ permit_params :email, :login, :bio, :website
+
+ index do
+ column :id
+ column :email
+ column :login
+ column :created_at
+ default_actions
+ end
+
+ show do
+ attributes_table do
+ rows :id, :email, :login, :time_zone, :bio, :website, :created_at, :sign_in_count, :current_sign_in_at, :last_sign_in_at, :current_sign_in_ip, :last_sign_in_ip
+ end
+ panel 'Channels' do
+ table_for user.channels do
+ column :id
+ column(:name) { |channel| link_to channel.name, channel }
+ end
+ end
+ end
+
+ form do |f|
+ f.semantic_errors *f.object.errors.keys
+ f.inputs :email, :login
+ f.actions
+ end
+
+ # custom action for signups per day
+ collection_action :signups, :method => :get, :format => :csv do
+ @csv_headers = [:day, :signups]
+ @days = User.signups_per_day
+ end
+
+ # custom action for emails list
+ collection_action :emails, :method => :get do
+ @users = User.all
+ end
+
+end
+
diff --git a/app/assets/images/ajax-loader.gif b/app/assets/images/ajax-loader.gif
new file mode 100644
index 0000000..3c2f7c0
Binary files /dev/null and b/app/assets/images/ajax-loader.gif differ
diff --git a/app/assets/images/channel_public_view.png b/app/assets/images/channel_public_view.png
new file mode 100644
index 0000000..fa760f5
Binary files /dev/null and b/app/assets/images/channel_public_view.png differ
diff --git a/app/assets/javascripts/active_admin.js.coffee b/app/assets/javascripts/active_admin.js.coffee
new file mode 100644
index 0000000..9cf60d7
--- /dev/null
+++ b/app/assets/javascripts/active_admin.js.coffee
@@ -0,0 +1,2 @@
+#= require active_admin/base
+
diff --git a/app/assets/javascripts/custom.js b/app/assets/javascripts/custom.js
index 98b7d2b..3e89111 100644
--- a/app/assets/javascripts/custom.js
+++ b/app/assets/javascripts/custom.js
@@ -1,6 +1,19 @@
// when the document is ready
$(document).ready(function() {
+ // allow flash notices to be dismissed
+ if ($(".flash").length > 0) {
+ $(".flash").on("click", function() {
+ $(this).hide("slow");
+ });
+ // hide flash automatically after 15 seconds
+ setTimeout(function() {
+ if ($(".flash").length > 0) {
+ $(".flash").hide("slow");
+ }
+ }, 15000);
+ }
+
// show form to add a talkback command
$('#talkback_command_add').click(function() {
$(this).hide();
diff --git a/app/assets/javascripts/highcharts.js b/app/assets/javascripts/highcharts.js
index 12d7080..79b1ac8 100644
--- a/app/assets/javascripts/highcharts.js
+++ b/app/assets/javascripts/highcharts.js
@@ -208,7 +208,7 @@ o(b[3],a[3]);this.axisOffset=[0,0,0,0];this.clipOffset=[0,0,0,0]},drawChartBox:f
null,null,c-p,d-p));else{e={fill:j||Q};if(i)e.stroke=a.borderColor,e["stroke-width"]=i;this.chartBackground=b.rect(p/2,p/2,c-p,d-p,a.borderRadius,i).attr(e).add().shadow(a.shadow)}if(k)f?f.animate(s):this.plotBackground=b.rect(q,o,n,t,0).attr({fill:k}).add().shadow(a.plotShadow);if(l)h?h.animate(s):this.plotBGImage=b.image(l,q,o,n,t).add();r?r.animate({width:w.width,height:w.height}):this.clipRect=b.clipRect(w);if(m)g?g.animate(g.crisp(null,q,o,n,t)):this.plotBorder=b.rect(q,o,n,t,0,-m).attr({stroke:a.plotBorderColor,
"stroke-width":m,zIndex:1}).add();this.isDirtyBox=!1},propFromSeries:function(){var a=this,b=a.options.chart,c,d=a.options.series,e,f;n(["inverted","angular","polar"],function(g){c=L[b.type||b.defaultSeriesType];f=a[g]||b[g]||c&&c.prototype[g];for(e=d&&d.length;!f&&e--;)(c=L[d[e].type])&&c.prototype[g]&&(f=!0);a[g]=f})},linkSeries:function(){var a=this,b=a.series;n(b,function(a){a.linkedSeries.length=0});n(b,function(b){var d=b.options.linkedTo;if(da(d)&&(d=d===":previous"?a.series[b.index-1]:a.get(d)))d.linkedSeries.push(b),
b.linkedParent=d})},render:function(){var a=this,b=a.axes,c=a.renderer,d=a.options,e=d.labels,f=d.credits,g;a.setTitle();a.legend=new zb(a,d.legend);a.getStacks();n(b,function(a){a.setScale()});a.getMargins();a.maxTicks=null;n(b,function(a){a.setTickPositions(!0);a.setMaxTicks()});a.adjustTickAmounts();a.getMargins();a.drawChartBox();a.hasCartesianSeries&&n(b,function(a){a.render()});if(!a.seriesGroup)a.seriesGroup=c.g("series-group").attr({zIndex:3}).add();n(a.series,function(a){a.translate();a.setTooltipPoints();
-a.render()});e.items&&n(e.items,function(b){var d=r(e.style,b.style),f=z(d.left)+a.plotLeft,g=z(d.top)+a.plotTop+12;delete d.left;delete d.top;c.text(b.html,f,g).attr({zIndex:2}).css(d).add()});if(f.enabled&&!a.credits)g=f.href,a.credits=c.text(f.text,0,0).on("click",function(){if(g)location.href=g}).attr({align:f.position.align,zIndex:8}).css(f.style).add().align(f.position);a.hasRendered=!0},destroy:function(){var a=this,b=a.axes,c=a.series,d=a.container,e,f=d&&d.parentNode;A(a,"destroy");Ja[a.index]=
+a.render()});e.items&&n(e.items,function(b){var d=r(e.style,b.style),f=z(d.left)+a.plotLeft,g=z(d.top)+a.plotTop+12;delete d.left;delete d.top;c.text(b.html,f,g).attr({zIndex:2}).css(d).add()});if(f.enabled&&!a.credits)g=f.href,a.credits=c.text(f.text,0,0).on("click",function(){if(g)parent.location.href=g}).attr({align:f.position.align,zIndex:8}).css(f.style).add().align(f.position);a.hasRendered=!0},destroy:function(){var a=this,b=a.axes,c=a.series,d=a.container,e,f=d&&d.parentNode;A(a,"destroy");Ja[a.index]=
u;a.renderTo.removeAttribute("data-highcharts-chart");X(a);for(e=b.length;e--;)b[e]=b[e].destroy();for(e=c.length;e--;)c[e]=c[e].destroy();n("title,subtitle,chartBackground,plotBackground,plotBGImage,plotBorder,seriesGroup,clipRect,credits,pointer,scroller,rangeSelector,legend,resetZoomButton,tooltip,renderer".split(","),function(b){var c=a[b];c&&c.destroy&&(a[b]=c.destroy())});if(d)d.innerHTML="",X(d),f&&Oa(d);for(e in a)delete a[e]},isReadyToRender:function(){var a=this;return!V&&C==C.top&&v.readyState!==
"complete"||ba&&!C.canvg?(ba?Mb.push(function(){a.firstRender()},a.options.global.canvasToolsURL):v.attachEvent("onreadystatechange",function(){v.detachEvent("onreadystatechange",a.firstRender);v.readyState==="complete"&&a.firstRender()}),!1):!0},firstRender:function(){var a=this,b=a.options,c=a.callback;if(a.isReadyToRender())a.getContainer(),A(a,"init"),a.resetMargins(),a.setChartSize(),a.propFromSeries(),a.getAxes(),n(b.series||[],function(b){a.initSeries(b)}),a.linkSeries(),A(a,"beforeRender"),
a.pointer=new Za(a,b),a.render(),a.renderer.draw(),c&&c.apply(a,[a]),n(a.callbacks,function(b){b.apply(a,[a])}),a.cloneRenderTo(!0),A(a,"load")},splashArray:function(a,b){var c=b[a],c=S(c)?c:[c,c,c,c];return[o(b[a+"Top"],c[0]),o(b[a+"Right"],c[1]),o(b[a+"Bottom"],c[2]),o(b[a+"Left"],c[3])]}};eb.prototype.callbacks=[];var xb=Highcharts.CenteredSeriesMixin={getCenter:function(){var a=this.options,b=this.chart,c=2*(a.slicedOffset||0),d,e=b.plotWidth-2*c,f=b.plotHeight-2*c,b=a.center,a=[o(b[0],"50%"),
diff --git a/app/assets/javascripts/noapi.js b/app/assets/javascripts/noapi.js
index d47b992..7a95c90 100644
--- a/app/assets/javascripts/noapi.js
+++ b/app/assets/javascripts/noapi.js
@@ -1,5 +1,6 @@
-// if on api subdomain, redirect
+// if on api subdomain except for charts, redirect
var wloc = window.location.toString();
-if (wloc.indexOf('api') != -1 && wloc.indexOf('api') < 10) {
+if (wloc.indexOf('api') !== -1 && wloc.indexOf('api') < 10 && wloc.indexOf('charts') === -1) {
window.location = wloc.replace('api', 'www');
-}
\ No newline at end of file
+}
+
diff --git a/app/assets/javascripts/sidebar.js b/app/assets/javascripts/sidebar.js
index 80306ab..bb204a5 100644
--- a/app/assets/javascripts/sidebar.js
+++ b/app/assets/javascripts/sidebar.js
@@ -1,9 +1,12 @@
-$(document).ready(function() {
+// execute on window load (and not document.ready), so that the sidebar is positioned correctly
+$(window).load(function() {
// if affix function exists
if ($.fn.affix) {
- // add sidebar affix
- $('#bootstrap-sidebar').affix();
+ // add sidebar affix, wrapped in a timeout so that it displays correctly
+ setTimeout(function() {
+ $('#bootstrap-sidebar').affix();
+ }, 100);
// add sidebar scrollspy
$(document.body).scrollspy({ target: '#leftcol', offset: 300 });
diff --git a/app/assets/javascripts/updateChart.js b/app/assets/javascripts/updateChart.js
index c544552..bcd4880 100644
--- a/app/assets/javascripts/updateChart.js
+++ b/app/assets/javascripts/updateChart.js
@@ -13,11 +13,11 @@ function getDimensions(element) {
}
function updateChart(index,
- postUpdate,
- width,
- height,
- channelId,
- newOptionsSave) {
+ postUpdate,
+ width,
+ height,
+ channelId,
+ newOptionsSave) {
// default width and height
var width = width;
var height = height;
@@ -26,52 +26,52 @@ function updateChart(index,
var iframe = $('#iframe' + index).attr("default_src");
if (!iframe) {
- iframe = $('#iframe' + index).attr('src');
+ iframe = $('#iframe' + index).attr('src');
}
src = iframe.split('?')[0];
// if not a line chart, a timeslice should be present or set timescale=30
if ($('#type_' + index).val() != 'line') {
- if ($('#timescale_' + index).val().length == 0 && $('#average_' + index).val().length == 0 && $('#median_' + index).val().length == 0 && $('#sum_' + index).val().length == 0) {
- $('#timescale_' + index).val(30);
- }
+ if ($('#timescale_' + index).val().length == 0 && $('#average_' + index).val().length == 0 && $('#median_' + index).val().length == 0 && $('#sum_' + index).val().length == 0) {
+ $('#timescale_' + index).val(30);
+ }
}
// add inputs to array
var inputs = [];
$('.chart_options' + index).each(function() {
- var v = $(this).val();
- var id = $(this).attr('id');
- var tag = id.split("_")[0];
+ var v = $(this).val();
+ var id = $(this).attr('id');
+ var tag = id.split("_")[0];
- if (v.length > 0) { inputs.push([tag, v]); }
- });
+ if (v.length > 0) { inputs.push([tag, v]); }
+ });
// create querystring
var qs = '';
while (inputs.length > 0) {
- var p = inputs.pop();
- if (p[0] == 'width') { width = parseInt(p[1]); }
- if (p[0] == 'height') { height = parseInt(p[1]); }
+ var p = inputs.pop();
+ if (p[0] == 'width') { width = parseInt(p[1]); }
+ if (p[0] == 'height') { height = parseInt(p[1]); }
- // don't add type=line to querystring, it's the default value
- if (!(p[0] == 'type' && p[1] == 'line')) {
- qs += '&' + p[0] + '=' + encodeURIComponent(p[1]);
- }
+ // don't add type=line to querystring, it's the default value
+ if (!(p[0] == 'type' && p[1] == 'line')) {
+ qs += '&' + p[0] + '=' + encodeURIComponent(p[1]);
+ }
}
// if querystring exists, add it to src
if (qs.length > 0) { src += '?' + qs.substring(1); }
// save chart options to database
if (postUpdate && index > 0 && newOptionsSave) {
- $.update("/channels/" + channelId + "/charts/" + index,
- {
- newOptions : { options: qs }
- } );
+ $.update("/channels/" + channelId + "/charts/" + index,
+ {
+ newOptions : { options: qs }
+ } );
}
else if (postUpdate && index > 0) {
- $.update("/channels/" + channelId + "/charts/" + index,
- { options: qs } );
+ $.update("/channels/" + channelId + "/charts/" + index,
+ { options: qs } );
}
// set embed code
@@ -90,9 +90,9 @@ function updateSelectValues() {
function setupChartForm(channelIndex) {
return function(index, value) {
- if (value.length > 0) {
- $('#' + value.split('=')[0] + "_" + channelIndex).val(decodeURIComponent(value.split('=')[1]));
- }
+ if (value.length > 0) {
+ $('#' + value.split('=')[0] + "_" + channelIndex).val(decodeURIComponent(value.split('=')[1]));
+ }
};
}
@@ -107,52 +107,52 @@ function setupColumns(current_user, channel_id)
function createWindowsWithData (data, current_user, channel_id, colName) {
- for (var i in data) {
- //each array element has a single chart object as an associative array with the type as the key
- // so I need to iterate over a array with size=1 to get a string with the window type
- for (var type in data[i]) {
- var wtype = type;
- }
- if (data[i].chart_window) window = data[i].chart_window;
- if (data[i].plugin_window) window = data[i].plugin_window;
- if (data[i].portlet_window) window = data[i].portlet_window;
+ for (var i in data) {
+ //each array element has a single chart object as an associative array with the type as the key
+ // so I need to iterate over a array with size=1 to get a string with the window type
+ for (var type in data[i]) {
+ var wtype = type;
+ }
+ if (data[i].chart_window) window = data[i].chart_window;
+ if (data[i].plugin_window) window = data[i].plugin_window;
+ if (data[i].portlet_window) window = data[i].portlet_window;
- if (window == "undefined")
+ if (window == "undefined")
var window = (data[i].portlet_window) ? data[i].portlet_window : data[i].chart_window;
- colId = window.col;
- title = window.title;
+ colId = window.col;
+ title = window.title;
- var content = window.html;
- if (data[i].chart_window) {
- var windowId = window.id;
- $("body").append("
");
+ var content = window.html;
+ if (data[i].chart_window) {
+ var windowId = window.id;
+ $("body").append("");
}
- var portlet = addWindow(colName, colId, window.id, wtype, title, content);
- portlet.each ( decoratePortlet(current_user) ) ;
+ var portlet = addWindow(colName, colId, window.id, wtype, title, content);
+ portlet.each ( decoratePortlet(current_user) ) ;
- portlet.find( ".ui-toggle" ).click( uiToggleClick );
- portlet.find( ".ui-view" ).click( uiViewClick (channel_id) );
- portlet.find( ".ui-edit" ).click( uiEditClick (channel_id) );
- portlet.find( ".ui-close" ).click( uiCloseClick (channel_id) );
- }
+ portlet.find( ".ui-toggle" ).click( uiToggleClick );
+ portlet.find( ".ui-view" ).click( uiViewClick (channel_id) );
+ portlet.find( ".ui-edit" ).click( uiEditClick (channel_id) );
+ portlet.find( ".ui-close" ).click( uiCloseClick (channel_id) );
+ }
}
var createWindows = function (current_user, channel_id, colName) {
return function(data) {
- createWindowsWithData(data, current_user, channel_id, colName);
+ createWindowsWithData(data, current_user, channel_id, colName);
};
}
function addWindow(colName, colId, windowId, wtype, title, content) {
$("#"+colName+"_dialog"+colId).append(''+content+'
') ;
+ 'id="portlet_' + windowId +
+ '">
'+content+'
') ;
if ($("#portlet_"+windowId).length > 1) {
- throw "Portlet count doesn't match what's expected";
+ throw "Portlet count doesn't match what's expected";
} else {
- return $("#portlet_"+windowId);
+ return $("#portlet_"+windowId);
}
}
@@ -161,22 +161,22 @@ function addWindow(colName, colId, windowId, wtype, title, content) {
var updatePortletPositions = function( current_user, channel_id) {
return function() {
if (current_user) {
- var result = $(this).sortable('serialize');
- colId = $(this).attr('id').charAt($(this).attr('id').length - 1);
- portletArray = getPortletArray(result);
- jsonResult = {
- "col" : colId,
- "positions" : portletArray
- } ;
+ var result = $(this).sortable('serialize');
+ colId = $(this).attr('id').charAt($(this).attr('id').length - 1);
+ portletArray = getPortletArray(result);
+ jsonResult = {
+ "col" : colId,
+ "positions" : portletArray
+ } ;
- if (portletArray.length > 0) {
- $.ajax({
- type: 'PUT',
- url: '../channels/' + channel_id + '/windows',
- data: {_method:'PUT', page : JSON.stringify(jsonResult ) },
- dataType: 'json'
- });
- }
+ if (portletArray.length > 0) {
+ $.ajax({
+ type: 'PUT',
+ url: '../channels/' + channel_id + '/windows',
+ data: {_method:'PUT', page : JSON.stringify(jsonResult ) },
+ dataType: 'json'
+ });
+ }
}
}
}
@@ -184,31 +184,31 @@ var updatePortletPositions = function( current_user, channel_id) {
function sortColumnSetup(current_user, channel_id) {
$( ".column" ).sortable({
- opacity: 0.6,
- helper: function( event ) {
- return $("");
- },
- connectWith: ".column",
- update: updatePortletPositions(current_user, channel_id)
- });
+ opacity: 0.6,
+ helper: function( event ) {
+ return $("");
+ },
+ connectWith: ".column",
+ update: updatePortletPositions(current_user, channel_id)
+ });
}
var decoratePortlet = function (current_user) {
return function() {
- var portletHeader = $(this).find( ".portlet-header") ;
- portletHeader.append( "");
+ var portletHeader = $(this).find( ".portlet-header") ;
+ portletHeader.append( "");
- thisObject = $(this);
- if (current_user == "true") {
- // Use feature Rollout here - needs to be implemented for this user, and this channel needs to belong to this user.
- thisObject.find('.wtype').prepend( "
");
- thisObject.find(".wtype-chart_window").append("
");
- thisObject.find(".wtype").append("
");
- thisObject.find(".portlet-header").css("cursor","move");
- }
- else {
- $(".column").sortable({ disabled:true });
- }
- return $(this).attr("id");
+ thisObject = $(this);
+ if (current_user == "true") {
+ // Use feature Rollout here - needs to be implemented for this user, and this channel needs to belong to this user.
+ thisObject.find('.wtype').prepend( "
");
+ thisObject.find(".wtype-chart_window").append("
");
+ thisObject.find(".wtype").append("
");
+ thisObject.find(".portlet-header").css("cursor","move");
+ }
+ else {
+ $(".column").sortable({ disabled:true });
+ }
+ return $(this).attr("id");
}
}
function getPortletArray(data) {
@@ -218,8 +218,8 @@ function getPortletArray(data) {
for (i in inputArray) {
- val = inputArray[i].split("=")[1] ;
- resultArray.push(val);
+ val = inputArray[i].split("=")[1] ;
+ resultArray.push(val);
}
return resultArray;
@@ -228,63 +228,63 @@ function getPortletArray(data) {
var uiEditClick = function (channel_id) {
return function() {
- var id = $( this ).parents( ".portlet:first" ).attr("id").substring(8);
+ var id = $( this ).parents( ".portlet:first" ).attr("id").substring(8);
- var options = "";
- $("#chartConfig"+id).load("/channels/"+channel_id+"/charts/"+id+"/edit",
- function() {
- options = $("#chartOptions"+id).html();
+ var options = "";
+ $("#chartConfig"+id).load("/channels/"+channel_id+"/charts/"+id+"/edit",
+ function() {
+ options = $("#chartOptions"+id).html();
- if (options != "undefined" && options.length >2) {
- $.each((options.split('&')), setupChartForm( id ));
- }
- $("#button"+id).click( function() {
- updateChart(id, true, 450, 250, channel_id, true);
- $("#chartConfig"+id).dialog("close");
+ if (options != "undefined" && options.length >2) {
+ $.each((options.split('&')), setupChartForm( id ));
+ }
+ $("#button"+id).click( function() {
+ updateChart(id, true, 450, 250, channel_id, true);
+ $("#chartConfig"+id).dialog("close");
- });
- })
- .dialog({ title:"Chart Options", modal: true, resizable: false, width: 500, dialogClass: "dev-info-dialog" });
+ });
+ })
+ .dialog({ title:"Chart Options", modal: true, resizable: false, width: 500, dialogClass: "dev-info-dialog" });
};
}
var uiViewClick = function (channel_id) {
return function() {
- var x = $( this ).parents( ".portlet:first" ).find( ".portlet-content" ).offset().left;
- var y = $( this ).parents( ".portlet:first" ).find( ".portlet-content" ).offset().top;
- var id = $( this ).parents( ".portlet:first" ).attr("id").substring(8);
+ var x = $( this ).parents( ".portlet:first" ).find( ".portlet-content" ).offset().left;
+ var y = $( this ).parents( ".portlet:first" ).find( ".portlet-content" ).offset().top;
+ var id = $( this ).parents( ".portlet:first" ).attr("id").substring(8);
- $("body").append('');
+ $("body").append('');
- $.get("/channels/"+channel_id+"/windows/"+id+"/iframe",
- function(response) {
- var display = response.replace(/id=\"iframe[0-9]?[0-9]?[0-9]?[0-9]?[0-9]?[0-9]?[0-9]?[0-9]?[0-9]?[0-9]?\"/, "" );
- $("#iframeinner"+id).text(display);
- }
- );
+ $.get("/channels/"+channel_id+"/windows/"+id+"/iframe",
+ function(response) {
+ var display = response.replace(/id=\"iframe[0-9]?[0-9]?[0-9]?[0-9]?[0-9]?[0-9]?[0-9]?[0-9]?[0-9]?[0-9]?\"/, "" );
+ $("#iframeinner"+id).text(display);
+ }
+ );
- $("#iframepopup"+id).dialog({
- resizable:false,
- width: "300px",
- position:[x+200,y-200],
- title: "Chart Iframe",
- dialogClass: "dev-info-dialog"
- });
+ $("#iframepopup"+id).dialog({
+ resizable:false,
+ width: "300px",
+ position:[x+200,y-200],
+ title: "Chart Iframe",
+ dialogClass: "dev-info-dialog"
+ });
};
}
var uiCloseClick = function (channel_id) {
return function() {
- var id = $( this ).parents( ".portlet:first" ).attr("id").substring(8);
- var portlet = $( this ).parents( ".portlet:first" ) ;
- $.update("/channels/"+channel_id+"/windows/"+id+"/hide" ,
- function(response) {
- portlet.hide("drop", function(){
- portlet.remove();});
- }) ;
+ var id = $( this ).parents( ".portlet:first" ).attr("id").substring(8);
+ var portlet = $( this ).parents( ".portlet:first" ) ;
+ $.update("/channels/"+channel_id+"/windows/"+id+"/hide" ,
+ function(response) {
+ portlet.hide("drop", function(){
+ portlet.remove();});
+ }) ;
}
}
diff --git a/app/assets/stylesheets/active_admin.css.scss b/app/assets/stylesheets/active_admin.css.scss
new file mode 100644
index 0000000..90ba1d4
--- /dev/null
+++ b/app/assets/stylesheets/active_admin.css.scss
@@ -0,0 +1,17 @@
+// SASS variable overrides must be declared before loading up Active Admin's styles.
+//
+// To view the variables that Active Admin provides, take a look at
+// `app/assets/stylesheets/active_admin/mixins/_variables.css.scss` in the
+// Active Admin source.
+//
+// For example, to change the sidebar width:
+// $sidebar-width: 242px;
+
+// Active Admin's got SASS!
+@import "active_admin/mixins";
+@import "active_admin/base";
+
+// Overriding any non-variable SASS must be done after the fact.
+// For example, to change the default status-tag color:
+//
+// .status_tag { background: #6090DB; }
diff --git a/app/assets/stylesheets/custom.css b/app/assets/stylesheets/custom.css
index 1bce4c3..0bf0074 100644
--- a/app/assets/stylesheets/custom.css
+++ b/app/assets/stylesheets/custom.css
@@ -7,6 +7,7 @@
body { padding-top: 70px; }
.break-word { word-break: break-word; }
.col-pad { padding: 0 15px; }
+.dismiss { float: right; cursor: pointer; position: relative; top: -12px; left: 7px; }
/* multiline forms */
.form-horizontal .multiline-label { margin-top: -10px; }
@@ -29,7 +30,9 @@ body { padding-top: 70px; }
.format-xml { display: none; }
.format-block { min-height: 200px; }
.format-block-lg { min-height: 350px; }
-
+.format-block-xl { min-height: 400px; }
+.format-block-xxl { min-height: 600px; }
+.format-block-xxxl { min-height: 1000px; }
/* Sticky footer styles
-------------------------------------------------- */
@@ -70,7 +73,6 @@ body {
.apps a:hover,
.apps:hover div { text-decoration: none; }
.commentarea { width: 300px; height: 80px; }
-#options { float: right; text-align: right; }
#login {
padding: 6px;
border: 1px solid #bbbbbb;
@@ -228,10 +230,9 @@ input[type="text"].midfield { width: 120px; }
textarea.tweet { margin-top: 0.5em; width: 40em; height: 3em; }
/* error messages */
-.errorExplanation { width: 95%; background-color: #ffffe0; display: table; margin-bottom: 20px; padding: 10px; border: 1px solid #aaaaaa; }
-#error {
- color: red;
-}
+.errorExplanation,
+#error_explanation { padding: 15px; margin-bottom: 20px; border: 1px solid transparent; border-radius: 4px; background-color: #f2dede; border-color: #ebccd1; color: #a94442; }
+#error { color: red; }
.field_with_errors { display: inline; }
/*.error_box { margin-top: 15px; padding: 5px; background-color: #f99; color: #300; border: 1px solid #f66; }*/
.warning_box { margin: 15px 0 15px 0; padding: 10px; background-color: #fc3; color: #000; border: 1px solid #f90; }
diff --git a/app/assets/stylesheets/sidebar.css b/app/assets/stylesheets/sidebar.css
index a5acce1..6deb86a 100644
--- a/app/assets/stylesheets/sidebar.css
+++ b/app/assets/stylesheets/sidebar.css
@@ -7,6 +7,13 @@
border: 1px solid #bbb;
border-radius: 5px;
padding: 2px 0;
+ margin-top: 20px;
+ width: 228px;
+}
+
+#bootstrap-sidebar li a {
+ padding: 1px 8px;
+ font-size: 15px;
}
#bootstrap-sidebar li a:hover {
@@ -18,11 +25,23 @@
border-right-width:4px;
}
+#bootstrap-sidebar li.subitem a {
+ padding-left: 30px;
+ font-size: 12px;
+}
+
@media (min-width: 979px) {
#bootstrap-sidebar.affix-top, #bootstrap-sidebar.affix {
position: fixed;
top:90px;
- width:228px;
+ width: 228px;
+ margin-top: 0;
+ }
+}
+
+@media (max-width: 978px) {
+ #bootstrap-sidebar.affix-top, #bootstrap-sidebar.affix {
+ width: 100%;
}
}
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 85990c6..de5ae3d 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -1,11 +1,14 @@
class ApplicationController < ActionController::Base
+ skip_before_filter :verify_authenticity_token
# include all helpers for controllers
helper :all
# include these helper methods for views
- helper_method :current_user_session, :current_user, :logged_in?, :is_admin?, :get_header_value, :to_bytes
+ helper_method :current_user_session, :current_user, :logged_in?, :get_header_value, :to_bytes
protect_from_forgery
before_filter :allow_cross_domain_access, :set_variables
+ before_filter :configure_permitted_parameters, if: :devise_controller?
after_filter :remove_headers
+ before_filter :authenticate_user_from_token!
# responds with blank
def respond_with_blank
@@ -33,8 +36,24 @@ class ApplicationController < ActionController::Base
@ssl_api_domain ||= ssl_api_domain
@locale ||= get_locale
I18n.locale = @locale
+
# sets timezone for current user, all DateTime outputs will be automatically formatted
Time.zone = current_user.time_zone if current_user
+
+ # allows use of daily params
+ params[:timescale] = '1440' if params[:timescale] == 'daily'
+ params[:average] = '1440' if params[:average] == 'daily'
+ params[:median] = '1440' if params[:median] == 'daily'
+ params[:sum] = '1440' if params[:sum] == 'daily'
+ end
+
+ # change default devise sign_in page; make admins sign in work correctly
+ def after_sign_in_path_for(resource)
+ if resource.is_a?(AdminUser)
+ admin_dashboard_path
+ else
+ channels_path
+ end
end
# get the locale, but don't fail if header value doesn't exist
@@ -52,8 +71,30 @@ class ApplicationController < ActionController::Base
return locale
end
+ protected
+
+ def configure_permitted_parameters
+ devise_parameter_sanitizer.for(:sign_up) { |u| u.permit(:login, :email, :password, :password_confirmation, :remember_me) }
+ devise_parameter_sanitizer.for(:sign_in) { |u| u.permit(:login, :email, :password, :remember_me) }
+ devise_parameter_sanitizer.for(:account_update) { |u| u.permit(:login, :email, :password, :password_confirmation, :time_zone, :password_current) }
+ end
+
private
+ # authenticates user based on token from users#api_login
+ def authenticate_user_from_token!
+ # exit if no login or token
+ return false if params[:login].blank? || params[:token].blank?
+
+ # get the user by login or email
+ user = User.find_by_login_or_email(params[:login])
+
+ # safe compare, avoids timing attacks
+ if user.present? && Devise.secure_compare(user.authentication_token, params[:token])
+ sign_in user, store: false
+ end
+ end
+
# remove headers if necessary
def remove_headers
response.headers.delete_if {|key| true} if params[:headers] == 'false'
@@ -71,15 +112,6 @@ class ApplicationController < ActionController::Base
true if current_user
end
- # check that user's email address matches admin
- def is_admin?
- current_user && ADMIN_EMAILS.include?(current_user.email)
- end
-
- def set_admin_menu
- @menu = 'admin'
- end
-
# converts a string to a byte string for c output
def to_bytes(input, separator='.', prefix='')
return '' if input == nil
@@ -90,35 +122,16 @@ class ApplicationController < ActionController::Base
return output.join(', ')
end
- def set_channels_menu
- @menu = 'channels'
- end
-
- def set_apps_menu
- @menu = 'apps'
- end
-
- def set_plugins_menu
- @menu = 'plugins'
- end
-
- def set_devices_menu
- @menu = 'devices'
- end
-
- def current_user_session
- return @current_user_session if defined?(@current_user_session)
- @current_user_session = UserSession.find
- end
-
- def current_user
- return @current_user if defined?(@current_user)
- @current_user = current_user_session && current_user_session.record
- end
+ # set menus
+ def set_support_menu; @menu = 'support'; end
+ def set_channels_menu; @menu = 'channels'; end
+ def set_apps_menu; @menu = 'apps'; end
+ def set_plugins_menu; @menu = 'plugins'; end
+ def set_devices_menu; @menu = 'devices'; end
def require_user
logger.info "Require User"
- if current_user.nil?
+ if current_user.nil? && User.find_by_api_key(get_apikey).nil?
respond_to do |format|
format.html {
session[:link_back] = request.url
@@ -144,7 +157,7 @@ class ApplicationController < ActionController::Base
end
def require_admin
- unless current_user && is_admin?
+ unless current_admin_user.present?
render :nothing => true, :status => 403 and return
false
end
@@ -178,8 +191,10 @@ class ApplicationController < ActionController::Base
end
# domain for the api
- def api_domain
- (Rails.env == 'production') ? API_DOMAIN : domain
+ def api_domain(ssl=false)
+ output = (Rails.env == 'production') ? API_DOMAIN : domain
+ output = output.sub(/http:/, 'https:') if ssl == true
+ return output
end
# ssl domain for the api
@@ -275,9 +290,6 @@ class ApplicationController < ActionController::Base
# options: days = how many days ago, start = start date, end = end date, offset = timezone offset
def get_date_range(params)
- # set timezone correctly
- set_time_zone(params)
-
# allow more past data if necessary
get_old_data = (params[:results].present? || params[:start].present? || params[:days].present?) ? true : false
@@ -292,8 +304,6 @@ class ApplicationController < ActionController::Base
return date_range
end
-
-
def set_time_zone(params)
# set timezone correctly
if params[:offset]
diff --git a/app/controllers/channels_controller.rb b/app/controllers/channels_controller.rb
index f8e69e9..97a60cd 100644
--- a/app/controllers/channels_controller.rb
+++ b/app/controllers/channels_controller.rb
@@ -3,7 +3,7 @@ class ChannelsController < ApplicationController
before_filter :require_user, :except => [ :show, :post_data, :social_show, :social_feed, :public]
before_filter :set_channels_menu
layout 'application', :except => [:social_show, :social_feed]
- protect_from_forgery :except => :post_data
+ protect_from_forgery :except => [:post_data, :create, :destroy, :clear]
require 'csv'
# view list of watched channels
@@ -29,17 +29,27 @@ class ChannelsController < ApplicationController
# list public channels
def public
+ @domain = domain
+ # default blank response
+ @channels = Channel.where(:id => 0).paginate :page => params[:page]
+
# get channels by ids
if params[:channel_ids].present?
- flash[:notice] = t(:selected_channels)
+ @header = t(:selected_channels)
@channels = Channel.public_viewable.by_array(params[:channel_ids]).order('ranking desc, updated_at DESC').paginate :page => params[:page]
+ # get channels that match a user
+ elsif params[:username].present?
+ @header = "#{t(:user).capitalize}: #{params[:username]}"
+ searched_user = User.find_by_login(params[:username])
+ @channels = searched_user.channels.public_viewable.active.order('ranking desc, updated_at DESC').paginate :page => params[:page] if searched_user.present?
# get channels that match a tag
elsif params[:tag].present?
- flash[:notice] = "#{t(:tag).capitalize}: #{params[:tag]}"
+ @header = "#{t(:tag).capitalize}: #{params[:tag]}"
@channels = Channel.public_viewable.active.order('ranking desc, updated_at DESC').with_tag(params[:tag]).paginate :page => params[:page]
# normal channel list
else
- flash[:notice] = t(:featured_channels)
+ @header = t(:featured_channels)
+ respond_with_error(:error_resource_not_found) and return if params[:page] == '0'
@channels = Channel.public_viewable.active.order('ranking desc, updated_at DESC').paginate :page => params[:page]
end
@@ -119,11 +129,10 @@ class ChannelsController < ApplicationController
end
def index
-
@channels = current_user.channels
respond_to do |format|
format.html
- format.json { render :json => @channels }
+ format.json { render :json => @channels.to_json(:root => false) }
end
end
@@ -180,29 +189,52 @@ class ChannelsController < ApplicationController
flash[:notice] = t(:channel_update_success)
redirect_to channel_path(@channel.id)
-
end
def create
- channel = current_user.channels.create(:field1 => "#{t(:channel_default_field)} 1")
+ # get the current user or find the user via their api key
+ @user = current_user || User.find_by_api_key(get_apikey)
+ channel = @user.channels.create(:field1 => "#{t(:channel_default_field)} 1")
+
+ # make updating attributes easier
+ params[:channel] = params
+ channel.update_attributes(channel_params)
+
channel.set_windows
channel.save
+ channel.save_tags(params[:channel][:tags]) if params[:channel][:tags].present?
channel.add_write_api_key
@channel_id = channel.id
- redirect_to channel_path(@channel_id, :anchor => "channelsettings")
+ respond_to do |format|
+ format.json { render :json => channel.to_json(Channel.private_options) }
+ format.xml { render :xml => channel.to_xml(Channel.private_options) }
+ format.any { redirect_to channel_path(@channel_id, :anchor => "channelsettings") }
+ end
end
# clear all data from a channel
def clear
- channel = current_user.channels.find(params[:id])
+ # get the current user or find the user via their api key
+ @user = current_user || User.find_by_api_key(get_apikey)
+ channel = @user.channels.find(params[:id])
channel.delete_feeds
- redirect_to channel_path(channel.id)
+ respond_to do |format|
+ format.json { render :json => [] }
+ format.xml { render :xml => [] }
+ format.any { redirect_to channel_path(channel.id) }
+ end
end
def destroy
- channel = current_user.channels.find(params[:id])
- channel.destroy
- redirect_to channels_path
+ # get the current user or find the user via their api key
+ @user = current_user || User.find_by_api_key(get_apikey)
+ @channel = @user.channels.find(params[:id])
+ @channel.destroy
+ respond_to do |format|
+ format.json { render :json => @channel.to_json(Channel.public_options) }
+ format.xml { render :xml => @channel.to_xml(Channel.public_options) }
+ format.any { redirect_to channels_path, :status => 303 }
+ end
end
# response is '0' if failure, 'entry_id' if success
@@ -311,8 +343,13 @@ class ChannelsController < ApplicationController
# if there is a talkback_key but no command
respond_with_blank and return if params[:talkback_key].present? && command.blank?
- # normal route, respond with the entry id of the feed
- render :text => status
+ # normal route, respond with the feed
+ respond_to do |format|
+ format.html { render :text => status }
+ format.json { render :json => feed.to_json }
+ format.xml { render :xml => feed.to_xml(Feed.public_options) }
+ format.any { render :text => status }
+ end and return
end
# import view
diff --git a/app/controllers/charts_controller.rb b/app/controllers/charts_controller.rb
index 556afc5..6a29517 100644
--- a/app/controllers/charts_controller.rb
+++ b/app/controllers/charts_controller.rb
@@ -55,8 +55,8 @@ class ChartsController < ApplicationController
params[:bgcolor] = fix_color(params[:bgcolor])
# set ssl
- @ssl = (get_header_value('x_ssl') == 'true')
- @domain = domain(@ssl)
+ ssl = (get_header_value('x_ssl') == 'true')
+ @domain = domain(ssl)
# should data be pushed off the end in dynamic chart
@push = (params[:push] and params[:push] == 'false') ? false : true
diff --git a/app/controllers/comments_controller.rb b/app/controllers/comments_controller.rb
index 7e29f5d..156b2c3 100644
--- a/app/controllers/comments_controller.rb
+++ b/app/controllers/comments_controller.rb
@@ -16,7 +16,7 @@ class CommentsController < ApplicationController
@comment.body = params[:comment][:body].gsub(/<\/?[^>]*>/, '').gsub(/\n/, '
')
# save comment
if @comment.save
- flash[:success] = "Thanks for adding a comment!"
+ flash[:notice] = "Thanks for adding a comment!"
else
flash[:error] = "Comment can't be blank!"
end
@@ -45,3 +45,4 @@ class CommentsController < ApplicationController
redirect_to :back
end
end
+
diff --git a/app/controllers/docs_controller.rb b/app/controllers/docs_controller.rb
index 3a4b123..c7ba287 100644
--- a/app/controllers/docs_controller.rb
+++ b/app/controllers/docs_controller.rb
@@ -1,6 +1,45 @@
class DocsController < ApplicationController
+ before_filter :set_support_menu
- def index; ;end
+ def index; ; end
+ def errors; ; end
+ def tweetcontrol; ; end
+ def plugins; ; end
+ def importer; ; end
+ def charts; ; end
+ def users; ; end
+
+ def channels
+ # default values
+ @channel_api_key = 'XXXXXXXXXXXXXXXX'
+ @user_api_key = 'XXXXXXXXXXXXXXXX'
+
+ # if user is signed in
+ if current_user && current_user.channels.any?
+ @channel_api_key = current_user.channels.order('updated_at desc').first.write_api_key
+ @user_api_key = current_user.api_key
+ end
+ end
+
+ def thinghttp
+ # default values
+ @thinghttp_api_key = 'XXXXXXXXXXXXXXXX'
+
+ # if user is signed in
+ if current_user && current_user.thinghttps.any?
+ @thinghttp_api_key = current_user.thinghttps.order('updated_at desc').first.api_key
+ end
+ end
+
+ def thingtweet
+ # default values
+ @thingtweet_api_key = 'XXXXXXXXXXXXXXXX'
+
+ # if user is signed in
+ if current_user && current_user.twitter_accounts.any?
+ @thingtweet_api_key = current_user.twitter_accounts.order('updated_at desc').first.api_key
+ end
+ end
def talkback
# default values
diff --git a/app/controllers/feed_controller.rb b/app/controllers/feed_controller.rb
index 8bbf376..f85a3b3 100644
--- a/app/controllers/feed_controller.rb
+++ b/app/controllers/feed_controller.rb
@@ -194,13 +194,13 @@ class FeedController < ApplicationController
output = @feed.to_xml
elsif params[:format] == 'csv'
@csv_headers = Feed.select_options(@channel, params)
- elsif (params[:format] == 'txt' or params[:format] == 'text')
+ elsif (params[:format] == 'txt' || params[:format] == 'text' || params[:format] == 'html' || params[:format].blank?)
output = add_prepend_append(@feed["field#{params[:field_id]}"])
else
output = @feed.to_json
-
end
- # else set error code
+
+ # else set error code
else
if params[:format] == 'xml'
output = bad_feed_xml
@@ -211,7 +211,7 @@ class FeedController < ApplicationController
# output data in proper format
respond_to do |format|
- format.html { render :json => output }
+ format.html { render :text => output }
format.json { render :json => output, :callback => params[:callback] }
format.xml { render :xml => output }
format.csv
diff --git a/app/controllers/passwords_controller.rb b/app/controllers/passwords_controller.rb
new file mode 100644
index 0000000..9efeb77
--- /dev/null
+++ b/app/controllers/passwords_controller.rb
@@ -0,0 +1,16 @@
+class SessionsController < Devise::SessionsController
+ before_filter :fix_params, :only => :create
+
+ # don't modify default devise controllers
+ def create; super; end
+ def new; super; end
+
+ private
+
+ # fixes password reset params so that devise config.reset_password_keys can be set to email for activeadmin
+ def fix_params
+ params[:user][:login] = params[:user][:email]
+ end
+
+end
+
diff --git a/app/controllers/pipes_controller.rb b/app/controllers/pipes_controller.rb
index 7cc83c3..78a3de7 100644
--- a/app/controllers/pipes_controller.rb
+++ b/app/controllers/pipes_controller.rb
@@ -1,5 +1,5 @@
class PipesController < ApplicationController
- before_filter :require_admin, :set_admin_menu
+ before_filter :require_admin
def index
@pipes = Pipe.paginate :page => params[:page], :order => 'created_at DESC'
@@ -14,3 +14,4 @@ class PipesController < ApplicationController
end
end
+
diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb
new file mode 100644
index 0000000..294d002
--- /dev/null
+++ b/app/controllers/registrations_controller.rb
@@ -0,0 +1,22 @@
+class RegistrationsController < Devise::RegistrationsController
+ include KeyUtilities
+ after_filter :add_api_key, :only => :create
+
+ # use defaults from devise
+ def new; super; end
+ def edit; super; end
+ def create; super; end
+
+ private
+
+ # adds an api key to the new user
+ def add_api_key
+ @user = current_user
+ if @user.present?
+ @user.api_key = generate_api_key(16, 'user')
+ @user.save
+ end
+ end
+
+end
+
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
new file mode 100644
index 0000000..5d67b64
--- /dev/null
+++ b/app/controllers/sessions_controller.rb
@@ -0,0 +1,32 @@
+class SessionsController < Devise::SessionsController
+ after_filter :log_failed_login, :only => :new
+
+ # don't modify default devise controllers
+ def create; super; end
+ def new; super; end
+
+ private
+
+ # logs failed login attempts
+ def log_failed_login
+ if failed_login?
+ # log to failedlogins
+ failed = Failedlogin.new
+ failed.login = params['user']['login']
+ failed.password = params['user']['password']
+ failed.ip_address = get_header_value('X_REAL_IP')
+ failed.save
+
+ # prevent timing and brute force password attacks
+ sleep 1
+ end
+ end
+
+ # true if a login fails
+ def failed_login?
+ options = env["warden.options"]
+ return (options.present? && options[:action] == "unauthenticated")
+ end
+
+end
+
diff --git a/app/controllers/stream_controller.rb b/app/controllers/stream_controller.rb
new file mode 100644
index 0000000..6c19999
--- /dev/null
+++ b/app/controllers/stream_controller.rb
@@ -0,0 +1,83 @@
+class StreamController < ApplicationController
+ include ActionController::Live
+ require 'csv'
+
+ def channel_feed
+ channel = Channel.find(params[:id])
+ api_key = ApiKey.find_by_api_key(get_apikey)
+
+ # set timezone correctly
+ set_time_zone(params)
+
+ # output proper http response if error
+ render :text => '-1', :status => 400 and return if !channel_permission?(channel, api_key)
+
+ # set the attachment headers
+ response.headers['Content-Type'] = 'text/csv'
+ response.headers['Content-Disposition'] = 'attachment; filename=feeds.csv'
+
+ # get the feed headers
+ csv_headers = Feed.select_options(channel, params)
+
+ # set the total records and batch size
+ total_records = channel.feeds.count
+ batch = 1000
+
+ # write the headers row
+ response.stream.write "#{CSV.generate_line(csv_headers)}"
+
+ # for every 1000 records
+ (0..(total_records - batch).abs).step(batch) do |i|
+ # variable to hold the streaming output for this batch
+ batch_output = ""
+ # feeds query
+ feeds = Feed.where(:channel_id => channel.id).order('entry_id asc').offset(i).limit(batch)
+
+ # for each feed, add the data according to the csv_headers
+ feeds.each do |feed|
+ row = []
+ csv_headers.each { |attr| row.push(feed.send(attr)) }
+ batch_output += CSV.generate_line(row)
+ end
+
+ # write the output for this batch
+ response.stream.write batch_output
+ # add a slight delay between database queries
+ sleep 0.1
+ end
+ ensure
+ response.stream.close
+ end
+
+ def stream_example
+ # get the channel
+ channel = Channel.find(params[:channel_id])
+
+ # stream the response
+ response.headers['Content-Type'] = 'text/csv'
+ response.headers['Content-Disposition'] = 'attachment; filename=feeds.csv'
+ 20.times {
+ response.stream.write "hello world\n"
+ sleep 1
+ }
+ ensure
+ response.stream.close
+ end
+
+ def stream_chunked_example
+ #response.headers['Content-Type'] = 'text/event-stream'
+ response.headers['Content-Type'] = 'text/csv'
+ response.headers['Content-Disposition'] = 'attachment; filename=feeds.csv'
+ response.headers['Transfer-Encoding'] = 'chunked'
+ 10.times {
+ response.stream.write "4\n" # size must be in hex format?
+ response.stream.write "hel\n\n"
+ sleep 1
+ }
+ response.stream.write "0\n\n"
+ ensure
+ response.stream.close
+ end
+
+end
+
diff --git a/app/controllers/user_sessions_controller.rb b/app/controllers/user_sessions_controller.rb
deleted file mode 100644
index 630d422..0000000
--- a/app/controllers/user_sessions_controller.rb
+++ /dev/null
@@ -1,55 +0,0 @@
-class UserSessionsController < ApplicationController
- before_filter :require_no_user, :only => [:new, :create]
- before_filter :require_user, :only => :destroy
-
- def new
- @title = t(:signin)
- @user_session = UserSession.new
- @mail_message = session[:mail_message] if !session[:mail_message].nil?
- end
-
- def show
- redirect_to root_path
- end
-
- def create
-
- if params[:userlogin].length > 0
- render :text => ''
- else
- @user_session = UserSession.new(params[:user_session])
-
- # remember user_id if checkbox is checked
- if params[:user_session][:remember_id] == '1'
- cookies['user_id'] = { :value => params[:user_session][:login], :expires => 1.month.from_now }
- else
- cookies.delete 'user_id'
- end
-
- if @user_session.save
- # if link_back, redirect back
- redirect_to session[:link_back] and return if session[:link_back]
- redirect_to channels_path and return
- else
- # log to failedlogins
- failed = Failedlogin.new
- failed.login = params[:user_session][:login]
- failed.password = params[:user_session][:password]
- failed.ip_address = get_header_value('X_REAL_IP')
- failed.save
-
- # prevent timing and brute force password attacks
- sleep 1
- @failed = true
- render :action => :new
- end
- end
- end
-
- def destroy
- session[:link_back] = nil
- current_user_session.destroy
- reset_session
- redirect_to root_path
- end
-end
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index 2b0921b..bac495f 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -1,7 +1,37 @@
class UsersController < ApplicationController
include KeyUtilities
- before_filter :require_no_user, :only => [:new, :create, :forgot_password]
- before_filter :require_user, :only => [:show, :edit, :update, :change_password, :edit_profile]
+ skip_before_filter :verify_authenticity_token, :only => [:api_login]
+ before_filter :require_user, :only => [:show, :edit, :update, :edit_profile]
+
+ # delete account
+ def destroy
+ user = current_user
+ user.delete
+ flash[:notice] = t(:account_deleted)
+ redirect_to root_path
+ end
+
+ # allow login via api
+ def api_login
+ # get the user by login or email
+ user = User.find_by_login_or_email(params[:login])
+
+ # exit if no user or invalid password
+ respond_with_error(:error_auth_required) and return if user.blank? || !user.valid_password?(params[:password])
+
+ # save new authentication token
+ if user.authentication_token.blank?
+ user.authentication_token = Devise.friendly_token
+ user.save
+ end
+
+ # output the user with token
+ respond_to do |format|
+ format.json { render :json => user.as_json(User.private_options_plus(:authentication_token)) }
+ format.xml { render :xml => user.to_xml(User.private_options_plus(:authentication_token)) }
+ format.any { render :text => user.authentication_token }
+ end
+ end
# generates a new api key
def new_api_key
@@ -34,8 +64,8 @@ class UsersController < ApplicationController
# if a json or xml request
if request.format == :json || request.format == :xml
- # authenticate the user if api key matches the target user
- authenticated = (User.find_by_api_key(get_apikey) == @user)
+ # authenticate the user if the user is logged in (can be via token) or api key matches the target user
+ authenticated = (current_user == @user) || (User.find_by_api_key(get_apikey) == @user)
# set options correctly
options = authenticated ? User.private_options : User.public_options(@user)
end
@@ -75,27 +105,6 @@ class UsersController < ApplicationController
end
end
- def new
- @title = t(:signup)
- @user = User.new
- end
-
- def create
- @user = User.new(user_params)
- @user.api_key = generate_api_key(16, 'user')
-
- # save user
- if @user.valid?
-
- if @user.save
- redirect_back_or_default channels_path and return
- end
- else
- render :action => :new
- end
-
- end
-
def show
@menu = 'account'
@user = @current_user
@@ -106,48 +115,18 @@ class UsersController < ApplicationController
@user = @current_user
end
- # displays forgot password page
- def forgot_password
- @user = User.new
- end
-
- # this action is called from an email link when a password reset is requested
- def reset_password
- # if user has been logged in (due to previous form submission)
- if !current_user.nil?
- @user = current_user
- @user.errors.add(:base, t(:password_problem))
- @valid_link = true
- else
- @user = User.find_by_id(params[:id])
- # make sure tokens match and password reset is within last 10 minutes
- if @user.perishable_token == params[:token] && @user.updated_at > 600.seconds.ago
- @valid_link = true
- # log the user in
- @user_session = UserSession.new(@user)
- @user_session.save
- end
- end
- end
-
- # do the actual password change
- def change_password
- @user = current_user
- # if no password entered, redirect
- redirect_to reset_password_path and return if params[:user][:password].empty?
- # check current password and update
- if @user.update_attributes(user_params)
- redirect_to account_path
- else
- redirect_to reset_password_path
- end
- end
-
def update
@menu = 'account'
@user = @current_user # makes our views "cleaner" and more consistent
+
+ # delete password and confirmation from params if not present
+ params[:user].delete(:password) if params[:user][:password].blank?
+
# check current password and update
- if @user.valid_password?(params[:password_current]) && @user.update_attributes(user_params)
+ if @user.valid_password?(params[:user][:password_current]) && @user.update_attributes(user_params)
+ # sign the user back in, since devise will log the user out on update
+ sign_in(current_user, :bypass => true)
+ flash[:notice] = t('devise.registrations.updated')
redirect_to account_path
else
@user.errors.add(:base, t(:password_incorrect))
diff --git a/app/helpers/admin_helper.rb b/app/helpers/admin_helper.rb
new file mode 100644
index 0000000..d5c6d35
--- /dev/null
+++ b/app/helpers/admin_helper.rb
@@ -0,0 +1,2 @@
+module AdminHelper
+end
diff --git a/app/models/admin_user.rb b/app/models/admin_user.rb
new file mode 100644
index 0000000..a6b709d
--- /dev/null
+++ b/app/models/admin_user.rb
@@ -0,0 +1,18 @@
+class AdminUser < ActiveRecord::Base
+ # Include default devise modules. Others available are:
+ # :token_authenticatable, :encryptable, :confirmable, :lockable, :timeoutable and :omniauthable
+ devise :database_authenticatable,
+ :recoverable, :rememberable, :trackable, :validatable
+
+ attr_accessor :login
+
+ protected
+
+ def self.find_for_database_authentication(warden_conditions)
+ conditions = warden_conditions.dup
+ login = conditions.delete(:login)
+ where(conditions).where(["lower(email) = :value", { :value => login.strip.downcase }]).first
+ end
+
+end
+
diff --git a/app/models/channel.rb b/app/models/channel.rb
index f1597e6..c191d6f 100644
--- a/app/models/channel.rb
+++ b/app/models/channel.rb
@@ -54,10 +54,11 @@ class Channel < ActiveRecord::Base
has_many :feeds
has_many :daily_feeds
has_many :api_keys, :dependent => :destroy
- has_many :taggings
+ has_many :taggings, :dependent => :destroy
has_many :tags, :through => :taggings
has_many :comments, :dependent => :destroy
has_many :windows, :dependent => :destroy, :autosave => true
+ accepts_nested_attributes_for :tags
self.include_root_in_json = true
@@ -81,6 +82,18 @@ class Channel < ActiveRecord::Base
cattr_reader :per_page
@@per_page = 15
+ # how often the channel is updated
+ def update_rate
+ last_feeds = self.feeds.order('entry_id desc').limit(2)
+ rate = (last_feeds.first.created_at - last_feeds.last.created_at) if last_feeds.length == 2
+ return rate
+ end
+
+ # write key for a channel
+ def write_api_key
+ self.api_keys.where(:write_flag => true).first.api_key
+ end
+
# select options
def select_options
only = [:name, :created_at, :updated_at, :id, :last_entry_id]
@@ -159,6 +172,19 @@ class Channel < ActiveRecord::Base
}
end
+ # used when creating a channel
+ def self.private_options
+ {
+ :root => false,
+ :only => [:id, :name, :description, :latitude, :longitude, :last_entry_id, :elevation, :created_at, :ranking],
+ :methods => :username,
+ :include => {
+ :tags => {:only => [:id, :name]},
+ :api_keys => {:only => [:api_key, :write_flag]}
+ }
+ }
+ end
+
# login name of the user who created the channel
def username; self.user.try(:login); end
@@ -321,14 +347,15 @@ class Channel < ActiveRecord::Base
end
def delete_feeds
- if self.feeds.count < 1000
+ # if a small number of feeds or redis is not present
+ if self.feeds.count < 1000 || REDIS_ENABLED == false
Feed.delete_all(["channel_id = ?", self.id])
DailyFeed.delete_all(["channel_id = ?", self.id])
begin
self.update_attribute(:last_entry_id, nil)
rescue Exception => e
end
-
+ # else delete via background resque job
else
self.update_attribute(:clearing, true)
Resque.enqueue(ClearChannelJob, self.id)
diff --git a/app/models/feed.rb b/app/models/feed.rb
index f7a4563..c9f12a4 100644
--- a/app/models/feed.rb
+++ b/app/models/feed.rb
@@ -33,6 +33,13 @@ class Feed < ActiveRecord::Base
attr_readonly :created_at
+ # for to_xml, return only the public attributes
+ def self.public_options
+ {
+ :except => [:id, :updated_at]
+ }
+ end
+
# only output these fields for feed
def self.select_options(channel, params)
only = [:created_at]
@@ -96,7 +103,7 @@ class Feed < ActiveRecord::Base
# custom json output
def as_json(options = {})
- super(options.merge(:except => [:updated_at, :id]))
+ super(Feed.public_options)
end
# check if a field value is a number
diff --git a/app/models/user.rb b/app/models/user.rb
index b184864..770ad65 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -2,24 +2,27 @@
#
# Table name: users
#
-# id :integer not null, primary key
-# login :string(255) not null
-# email :string(255) not null
-# crypted_password :string(255) not null
-# password_salt :string(255) not null
-# persistence_token :string(255) not null
-# perishable_token :string(255) not null
-# current_login_at :datetime
-# last_login_at :datetime
-# current_login_ip :string(255)
-# last_login_ip :string(255)
-# created_at :datetime
-# updated_at :datetime
-# time_zone :string(255)
-# public_flag :boolean default(FALSE)
-# bio :text
-# website :string(255)
-# api_key :string(16)
+# id :integer not null, primary key
+# login :string(255) not null
+# email :string(255) not null
+# encrypted_password :string(255) not null
+# password_salt :string(255)
+# current_sign_in_at :datetime
+# last_sign_in_at :datetime
+# current_sign_in_ip :string(255)
+# last_sign_in_ip :string(255)
+# created_at :datetime
+# updated_at :datetime
+# time_zone :string(255)
+# public_flag :boolean default(FALSE)
+# bio :text
+# website :string(255)
+# api_key :string(16)
+# reset_password_token :string(255)
+# reset_password_sent_at :datetime
+# remember_created_at :datetime
+# sign_in_count :integer default(0), not null
+# authentication_token :string(255)
#
####### NOTE #######
@@ -28,21 +31,20 @@
####################
class User < ActiveRecord::Base
include KeyUtilities
- has_many :channels
+ devise :database_authenticatable, :registerable, :recoverable, :rememberable, :trackable, :validatable
+ has_many :channels, :dependent => :destroy
has_many :twitter_accounts, :dependent => :destroy
has_many :thinghttps, :dependent => :destroy
has_many :tweetcontrols, :dependent => :destroy
has_many :reacts, :dependent => :destroy
has_many :scheduled_thinghttps, :dependent => :destroy
has_many :talkbacks, :dependent => :destroy
- has_many :plugins
- has_many :devices
- has_many :api_keys
+ has_many :plugins, :dependent => :destroy
+ has_many :devices, :dependent => :destroy
+ has_many :api_keys, :dependent => :destroy
has_many :watchings, :dependent => :destroy
- has_many :watched_channels, :through => :watchings, :source => :channel
- has_many :comments
-
- acts_as_authentic
+ has_many :watched_channels, :through => :watchings, :source => :channel, :dependent => :destroy
+ has_many :comments, :dependent => :destroy
self.include_root_in_json = false
@@ -50,6 +52,39 @@ class User < ActiveRecord::Base
cattr_reader :per_page
@@per_page = 50
+ # allow login by login name also
+ def self.find_first_by_auth_conditions(warden_conditions)
+ conditions = warden_conditions.dup
+ if login_param = conditions.delete(:login)
+ where(conditions).where(["lower(login) = :value OR lower(email) = :value", { :value => login_param.downcase }]).first
+ else
+ where(conditions).first
+ end
+ end
+
+ # allow users to sign in with passwords from old authlogic authentication
+ alias :devise_valid_password? :valid_password?
+ def valid_password?(password)
+ begin
+ devise_valid_password?(password)
+ rescue BCrypt::Errors::InvalidHash
+ stretches = 20
+ digest = "#{password}#{self.password_salt}"
+ stretches.times {digest = Digest::SHA512.hexdigest(digest)}
+ if digest == self.encrypted_password
+ #Here update old Authlogic SHA512 Password with new Devise ByCrypt password
+ # SOURCE: https://github.com/plataformatec/devise/blob/master/lib/devise/models/database_authenticatable.rb
+ # Digests the password using bcrypt.
+ self.encrypted_password = self.password_digest(password)
+ self.save
+ return true
+ else
+ # If not BCryt password and not old Authlogic SHA512 password don't authenticate user
+ return false
+ end
+ end
+ end
+
# find a user using login or email
def self.find_by_login_or_email(login)
User.find_by_login(login) || User.find_by_email(login)
@@ -81,6 +116,12 @@ class User < ActiveRecord::Base
{ :only => [:id, :login, :created_at, :email, :website, :bio] }
end
+ # add an extra attribute to private_options
+ def self.private_options_plus(array)
+ { :only => User.private_options[:only].push(array).flatten }
+ end
+
+
# set new api key
def set_new_api_key!
new_api_key = generate_api_key(16, 'user')
@@ -90,4 +131,3 @@ class User < ActiveRecord::Base
end
-
diff --git a/app/views/admin/useful_links/_index.html.erb b/app/views/admin/useful_links/_index.html.erb
new file mode 100644
index 0000000..c967f49
--- /dev/null
+++ b/app/views/admin/useful_links/_index.html.erb
@@ -0,0 +1,4 @@
+<%= link_to 'List Email Addresses', admin_emails_path %>
+
+<%= link_to 'List Users by Day (CSV)', admin_signups_path(:format => :csv) %>
+
diff --git a/app/views/admin/users/emails.html.erb b/app/views/admin/users/emails.html.erb
new file mode 100644
index 0000000..b943db9
--- /dev/null
+++ b/app/views/admin/users/emails.html.erb
@@ -0,0 +1,4 @@
+<% @users.each do |u| %>
+ <%= u.email %>
+<% end %>
+
diff --git a/app/views/admin/users/signups.csv.erb b/app/views/admin/users/signups.csv.erb
new file mode 100644
index 0000000..1069835
--- /dev/null
+++ b/app/views/admin/users/signups.csv.erb
@@ -0,0 +1,2 @@
+<%= CSV.generate_line @csv_headers %><% @days.each do |day| %><% row = [] %><% @csv_headers.each do |attr| %><% row.push(day.to_json) %><% end %><%= CSV.generate_line(day).html_safe %><% end %>
+
diff --git a/app/views/api_keys/_index.html.erb b/app/views/api_keys/_index.html.erb
index 9240fa9..a928cc5 100644
--- a/app/views/api_keys/_index.html.erb
+++ b/app/views/api_keys/_index.html.erb
@@ -1,11 +1,11 @@
<%= t(:api_key_write) %>
<%= @write_key %>
-
+
<%= button_to t(:api_key_write_new), channel_api_keys_path(@channel, :write => 1), :data => { :confirm => t(:confirm_new_api_key) } %>
-
+
<%= t(:api_key_read) %>
<% @read_keys.each do |read_key| %>
@@ -29,22 +29,22 @@
<%= button_to t(:api_key_delete), channel_api_key_path(@channel, read_key), :method => 'delete', :data => { :confirm => t(:confirm_read_key_delete) } %>
-
+
<% end %>
<%= button_to t(:api_key_read_new), channel_api_keys_path(@channel, :write => 0) %>
-
+