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('
' + title + - '
'+content+'
') ; + 'id="portlet_' + windowId + + '">
' + title + + '
'+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 $("
Drop to re-position
"); - }, - connectWith: ".column", - update: updatePortletPositions(current_user, channel_id) - }); + opacity: 0.6, + helper: function( event ) { + return $("
Drop to re-position
"); + }, + 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) %> -
+
diff --git a/app/views/channels/_import.html.erb b/app/views/channels/_import.html.erb index 4becb7e..e90567f 100644 --- a/app/views/channels/_import.html.erb +++ b/app/views/channels/_import.html.erb @@ -1,46 +1,48 @@
+

<%= t(:import) %>

<%= t(:upload_select) %> -

-<% flash.each do |name, msg| %> -
-

<%= msg %>

-
-
-<% end %> +
+ <%= form_for :upload, :url => upload_channel_path(@channel), :html => { :multipart => true } do |f| %> <%= f.file_field :csv %> -

+

<%= t(:time_zone) %> <%= time_zone_select 'feed', 'time_zone', nil, :default => 'UTC' %> -

+

<%= f.submit t(:upload), :disable_with => t(:uploading) %> <% end %> +

+ +

<%= t(:export) %>

+ <%= t(:download_feeds) %> +

+ <%= button_to t(:download), "#{@ssl_api_domain}stream/channels/#{@channel.id}/feeds?api_key=#{@channel.write_api_key}" %>
@@ -56,7 +56,7 @@ function commentflag(m_id, user_id) type: 'POST', data: {user_id : user_id}, success: function(data) { - $('#commentflag' + m_id).parent().parent().append("

<%= t(:comment_reported) %>
"); + $('#commentflag' + m_id).parent().parent().append("

<%= t(:comment_reported) %>
"); $('#imgflag' + m_id).attr('src', '/images/flag_red.gif'); $('#flaglink' + m_id).removeAttr('onclick'); $('#flaglink' + m_id).unbind('mouseenter mouseleave'); diff --git a/app/views/channels/index.html.erb b/app/views/channels/index.html.erb index d0011ba..de2b50d 100644 --- a/app/views/channels/index.html.erb +++ b/app/views/channels/index.html.erb @@ -32,7 +32,7 @@ -
+
<% end %> @@ -41,18 +41,18 @@ <%= d.submit t(:channel_create), :class => 'btn btn-primary' %> <% end %> - <% if is_admin? %> + <% if current_admin_user.present? %> -


+


<% @channels.each do |c| %> <% if c.social %> <%= t(:social_channel) %>: <%= link_to c.name, (Rails.env == 'production') ? "http://www.socialsensornetwork.com/#{c.slug}" : "/s/#{c.slug}" %> -
+
<% end %> <% end %> -
+
<%= link_to t(:social_channel_create), :controller => 'channels', :action => 'social_new' %> <% end %> @@ -66,7 +66,7 @@
<%= t(:help_channel) %> - (<%= t(:help_options) %>) + (<%= t(:help_options) %>) diff --git a/app/views/channels/social_show.html.erb b/app/views/channels/social_show.html.erb index e3b095b..b86c054 100644 --- a/app/views/channels/social_show.html.erb +++ b/app/views/channels/social_show.html.erb @@ -93,7 +93,7 @@ <% if !@channel.description.blank? %>
<%= t(:channel_directions) %>: <%= @channel.description %>
-
+
<% end %> <%= form_tag "#{@post_url}" do %> @@ -122,19 +122,19 @@ <% end %> -

+

<% @fields.each do |f| %> -

+

<% end %> diff --git a/app/views/channels/watched.html.erb b/app/views/channels/watched.html.erb index 4cd8108..f26e8a1 100644 --- a/app/views/channels/watched.html.erb +++ b/app/views/channels/watched.html.erb @@ -1,17 +1,15 @@
-
+
-
- <% if @channels.empty? %> - <%= t(:watched_empty) %> -

- <%= link_to t(:watched_find), public_channels_path %> - <% else %> - <%= render :partial => 'list' %> - <% end %> -
+ <% if @channels.empty? %> + <%= t(:watched_empty) %> +

+ <%= link_to t(:watched_find), public_channels_path %> + <% else %> + <%= render :partial => 'list' %> + <% end %>
diff --git a/app/views/charts/_display.html.erb b/app/views/charts/_display.html.erb index 49fb42b..e968e8c 100644 --- a/app/views/charts/_display.html.erb +++ b/app/views/charts/_display.html.erb @@ -2,10 +2,10 @@ -

+

<%= t(:chart_embed_code) %>: -
+
-


+


diff --git a/app/views/charts/index.html.erb b/app/views/charts/index.html.erb index c5c7e40..ed7a75b 100644 --- a/app/views/charts/index.html.erb +++ b/app/views/charts/index.html.erb @@ -23,7 +23,7 @@ <% @channel.attribute_names.each do |attr| %> <% if attr.index('field') and @channel[attr] and !@channel[attr].empty? %> - + <%= render :partial => 'config', :locals => { :displayconfig => true, @@ -35,7 +35,7 @@ :height => @height } %> - + <% end %> <% end %>
@@ -43,12 +43,12 @@ diff --git a/app/views/devise/confirmations/new.html.erb b/app/views/devise/confirmations/new.html.erb new file mode 100644 index 0000000..85ab304 --- /dev/null +++ b/app/views/devise/confirmations/new.html.erb @@ -0,0 +1,13 @@ +

Resend confirmation instructions

+ +<%= form_for(resource, :as => resource_name, :url => confirmation_path(resource_name), :html => { :method => :post }) do |f| %> + <%= devise_error_messages! %> + +
<%= f.label :login %>
+ <%= f.text_field :login, :autofocus => true %>
+ +
<%= f.submit "Resend confirmation instructions" %>
+<% end %> + +<%= render "devise/shared/links" %> + diff --git a/app/views/devise/mailer/confirmation_instructions.html.erb b/app/views/devise/mailer/confirmation_instructions.html.erb new file mode 100644 index 0000000..36670f9 --- /dev/null +++ b/app/views/devise/mailer/confirmation_instructions.html.erb @@ -0,0 +1,5 @@ +

Welcome <%= @email %>!

+ +

You can confirm your account email through the link below:

+ +

<%= link_to 'Confirm my account', confirmation_url(@resource, :confirmation_token => @token) %>

diff --git a/app/views/devise/mailer/reset_password_instructions.html.erb b/app/views/devise/mailer/reset_password_instructions.html.erb new file mode 100644 index 0000000..93de6d0 --- /dev/null +++ b/app/views/devise/mailer/reset_password_instructions.html.erb @@ -0,0 +1,8 @@ +

Hello <%= @resource.email %>!

+ +

Someone has requested a link to change your password. You can do this through the link below.

+ +

<%= link_to 'Change my password', edit_password_url(@resource, :reset_password_token => @token) %>

+ +

If you didn't request this, please ignore this email.

+

Your password won't change until you access the link above and create a new one.

diff --git a/app/views/devise/mailer/unlock_instructions.html.erb b/app/views/devise/mailer/unlock_instructions.html.erb new file mode 100644 index 0000000..f59615f --- /dev/null +++ b/app/views/devise/mailer/unlock_instructions.html.erb @@ -0,0 +1,7 @@ +

Hello <%= @resource.email %>!

+ +

Your account has been locked due to an excessive number of unsuccessful sign in attempts.

+ +

Click the link below to unlock your account:

+ +

<%= link_to 'Unlock my account', unlock_url(@resource, :unlock_token => @token) %>

diff --git a/app/views/devise/passwords/edit.html.erb b/app/views/devise/passwords/edit.html.erb new file mode 100644 index 0000000..61f1ca1 --- /dev/null +++ b/app/views/devise/passwords/edit.html.erb @@ -0,0 +1,28 @@ +
+
+ + + + <%= form_for(resource, :as => resource_name, :url => password_path(resource_name), :html => { :method => :put, :class => 'form-horizontal' }) do |f| %> + <%= devise_error_messages! %> + <%= f.hidden_field :reset_password_token %> + +
+ +
<%= f.password_field :password, :autofocus => true, :class => 'form-control' %>
+
+ +
+ +
<%= f.password_field :password_confirmation, :class => 'form-control' %>
+
+ +
+ +

<%= f.submit t(:password_change), :class => 'btn btn-primary' %>

+
+ + <% end %> +
+
+ diff --git a/app/views/devise/passwords/new.html.erb b/app/views/devise/passwords/new.html.erb new file mode 100644 index 0000000..d05e5c5 --- /dev/null +++ b/app/views/devise/passwords/new.html.erb @@ -0,0 +1,34 @@ +
+ + + +
+ + <%= t(:password_forgot_message) %> + +

+ + <%= form_for(resource, :as => resource_name, :url => password_path(resource_name), :html => { :method => :post, :class => 'form-horizontal' }) do |f| %> + <%= devise_error_messages! %> + +
+ +
<%= f.text_field :email, :class => 'form-control' %>
+
+ + +
+

<%= f.submit t(:submit), :class => 'btn btn-primary' %>

+
+ + <% end %> + +
+ +
+ + + + diff --git a/app/views/devise/registrations/edit.html.erb b/app/views/devise/registrations/edit.html.erb new file mode 100644 index 0000000..7dd348d --- /dev/null +++ b/app/views/devise/registrations/edit.html.erb @@ -0,0 +1,30 @@ +

Edit <%= resource_name.to_s.humanize %>

+ +<%= form_for(resource, :as => resource_name, :url => registration_path(resource_name), :html => { :method => :put }) do |f| %> + <%= devise_error_messages! %> + +
<%= f.label :login %>
+ <%= f.text_field :login, :autofocus => true %>
+ + <% if devise_mapping.confirmable? && resource.pending_reconfirmation? %> +
Currently waiting confirmation for: <%= resource.unconfirmed_email %>
+ <% end %> + +
<%= f.label :password %> (leave blank if you don't want to change it)
+ <%= f.password_field :password, :autocomplete => "off" %>
+ +
<%= f.label :password_confirmation %>
+ <%= f.password_field :password_confirmation %>
+ +
<%= f.label :current_password %> (we need your current password to confirm your changes)
+ <%= f.password_field :current_password %>
+ +
<%= f.submit "Update" %>
+<% end %> + +

Cancel my account

+ +

Unhappy? <%= button_to "Cancel my account", registration_path(resource_name), :data => { :confirm => "Are you sure?" }, :method => :delete %>

+ +<%= link_to "Back", :back %> + diff --git a/app/views/devise/registrations/new.html.erb b/app/views/devise/registrations/new.html.erb new file mode 100644 index 0000000..b13773e --- /dev/null +++ b/app/views/devise/registrations/new.html.erb @@ -0,0 +1,51 @@ +
+
+ + + + <%= form_for(resource, :as => resource_name, :url => registration_path(resource_name), :html => {:class => 'form-horizontal'}) do |f| %> + <%= devise_error_messages! %> + + +
+ +
<%= f.text_field :login, :class => 'form-control' %>
+
+ +
+ +
<%= f.text_field :email, :class => 'form-control' %>
+
+ +
+ +
+

+ <%= time_zone_select 'user', 'time_zone', nil, {:default => 'Eastern Time (US & Canada)'}, {:class => 'form-control'} %> +

+
+
+ +
+ +
<%= f.password_field :password, :class => 'form-control' %>
+
+ +
+ +
<%= f.password_field :password_confirmation, :class => 'form-control' %>
+
+ +
+ +

<%= f.submit t(:create_account), :id => "user_submit", :class => 'btn btn-primary' %>

+
+ + <% end %> +
+
+ + + diff --git a/app/views/devise/sessions/new.html.erb b/app/views/devise/sessions/new.html.erb new file mode 100644 index 0000000..df35fa1 --- /dev/null +++ b/app/views/devise/sessions/new.html.erb @@ -0,0 +1,57 @@ +
+
+ + + + <% if @failed %><%= t(:signin_try_again) %>

<% end %> + <% if @mail_message.present? %><%= @mail_message %>

<% end %> + + <%= form_for(resource, :as => resource_name, :url => session_path(resource_name), :html => { :id => 'loginform', :class => 'form-horizontal' }) do |f| %> + + +
+ +
<%= f.text_field :login, :value => cookies['user_id'], :class => 'form-control' %>
+
+ +
+ +
+ <%= f.password_field :password, :class => 'form-control' %> + <%= link_to t(:forgot), new_user_password_path, :id => 'forgot_password' %> +
+
+ +
+
+
+ +
+
+
+ +
+

<%= f.submit t(:signin), :class => 'btn btn-primary' %>

+
+ <% end %> + +
+
+ + + diff --git a/app/views/devise/shared/_links.erb b/app/views/devise/shared/_links.erb new file mode 100644 index 0000000..8045cf6 --- /dev/null +++ b/app/views/devise/shared/_links.erb @@ -0,0 +1,12 @@ +<%- if controller_name != 'sessions' %> + <%= link_to t(:signin), new_session_path(resource_name) %>
+<% end -%> + +<%- if devise_mapping.registerable? && controller_name != 'registrations' %> + <%= link_to t(:signup), new_registration_path(resource_name) %>
+<% end -%> + +<%- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations' %> + <%= link_to t(:password_forgot), new_password_path(resource_name) %>
+<% end -%> + diff --git a/app/views/devise/unlocks/new.html.erb b/app/views/devise/unlocks/new.html.erb new file mode 100644 index 0000000..8f6cfeb --- /dev/null +++ b/app/views/devise/unlocks/new.html.erb @@ -0,0 +1,12 @@ +

Resend unlock instructions

+ +<%= form_for(resource, :as => resource_name, :url => unlock_path(resource_name), :html => { :method => :post }) do |f| %> + <%= devise_error_messages! %> + +
<%= f.label :email %>
+ <%= f.email_field :email, :autofocus => true %>
+ +
<%= f.submit "Resend unlock instructions" %>
+<% end %> + +<%= render "devise/shared/links" %> diff --git a/app/views/docs/_sidebar.html.erb b/app/views/docs/_sidebar.html.erb new file mode 100644 index 0000000..2145d40 --- /dev/null +++ b/app/views/docs/_sidebar.html.erb @@ -0,0 +1,43 @@ + + diff --git a/app/views/docs/channels.html.erb b/app/views/docs/channels.html.erb new file mode 100644 index 0000000..977b2ab --- /dev/null +++ b/app/views/docs/channels.html.erb @@ -0,0 +1,136 @@ +
+ +
+ <%= render 'docs/sidebar' %> +
+ +
+ +

Channels

+ Channels are where your application stores and retrieves any type of data. Each channel has a Private View and a Public View. The Private View is only accessible by signing into your ThingSpeak.com user account. The Public View is what other viewers will see when they visit your ThingSpeak Channel. You can have different info on each view, customize the view with Plugins, and even disable the Public View.

+

+ <%= image_tag 'channel_public_view.png', :size => '600x533' %> +


+ Channel Views have the following features: +

+
    +
  • Channel Watch
  • +
  • Share via Social Networks
  • +
  • Developer Info
  • +
  • Ability to embed ThingSpeak Plugins
  • +
  • Drag-and-drop Organization
  • +
  • Tags
  • +
  • Comments
  • +
+

+

Channels API

+ To read and write to a ThingSpeak Channel, your application must make requests to the ThingSpeak API using HTTP requests. Each ThingSpeak Channel allows for 8 fields of data (both numeric and alphanumeric formats), location information, and a status update. Each entry is stored with a date and time stamp and is assigned a unique Entry ID (entry_id). After the data is stored, you can retrieve the data by time selection or by Entry ID. In addition to storing and retrieving numeric and alphanumeric data, the ThingSpeak API allows for numeric data processing such as timescaling, averaging, median, summing, and rounding. The channel feeds supports JSON, XML, and CSV formats for integration into applications. +

+ +
+

Keywords

+ Here are some keywords that are used in the API. An understanding of the terms will make the API documentation easier to understand. +

+
    +
  • Channel - The name for where data can be inserted or retrieved within the ThingSpeak API, identified by a numerical Channel ID
  • +
  • Channel ID - Every channel has a unique Channel ID. The Channel ID number is used to identify the channel when your application reads data from the channel
  • +
  • Field - One of eight specific locations for data inside of a channel, identified by a number between 1 to 8 – A field can store numeric data from sensors or alphanumeric strings from serial devices or RFID readers
  • +
  • Status - A short status message to augment the data stored in a channel
  • +
  • Location - The latitude, longitude, and elevation of where data is being sent from
  • +
  • Feed - The collective name for the data stored inside a channel, which may be any combination of field data, status updates, and location info
  • +
  • Write API Key – A 16 digit code that allows an application to write data to a channel
  • +
  • Read API Key – A 16 digit code that allows an application to read the data stored in a channel
  • +
+

+ +
+

Base URL Addresses and Locations

+ Regular URL: +

+
http://api.thingspeak.com
+
+ Secure URL: +

+
https://api.thingspeak.com
+
+ IP Address: +

+
http://184.106.153.149
+
+ Cross-domain XML: +

+
http://api.thingspeak.com/crossdomain.xml
+

+ +
+

API Keys

+

+

Private / Public Channels

+ By default, your channel is private and requires a Read API Key to access its feed. You can make a channel public which gives other users the ability to use your feed without a Read API Key. +


+

Write API Key

+ In order to update a channel, you need to know your Write API Key. If your Write API Key gets compromised you can generate a new key. +

+ Follow these steps to get your Write API Key: +

+
    +
  • Select Channels
  • +
  • Select the Channel to update
  • +
  • Select Manage API Keys
  • +
+

+

Read API Key

+ The Read API Key allows your application to read data from the API. You can generate multiple Read API Keys for different applications. +

+ Follow these steps to get a Read API Key: +

+
    +
  • Select Channels
  • +
  • Select the Channel to update
  • +
  • Select Manage API Keys
  • +
  • Select Generate New Read API Key
  • +
+


+ +
+

Rate Limits

+ The open service via ThingSpeak.com has a rate limit of an update per channel every 15 seconds. This limit is so that the service can remain free and give everyone a high-level of service. The API source will also be made available on GitHub so that you can run this locally or via a shared web host provider. At that point you will be able to to tweak settings for your application requirements. +

+ +

Caching

+ Caching is implemented on JSON and XML formats on feeds. Feeds that return more than 100 entries are cached for 5 minutes. This will allow great performance for popular applications. The Last API call and feeds that specify "results=100" or less are not cached, so that you can produce real-time applications. +













+ +
+ <%= render 'docs/channels/update' %> + +
+ <%= render 'docs/channels/feed' %> + +
+ <%= render 'docs/channels/field' %> + +
+ <%= render 'docs/channels/status' %> + +
+ <%= render 'docs/channels/public_index' %> + +
+ <%= render 'docs/channels/create' %> + +
+ <%= render 'docs/channels/clear' %> + +
+ <%= render 'docs/channels/destroy' %> + +

+
+ <%= render 'docs/channels/importer' %> + +











+ +
+
+ diff --git a/app/views/docs/channels/_clear.html.erb b/app/views/docs/channels/_clear.html.erb new file mode 100644 index 0000000..61f9cd3 --- /dev/null +++ b/app/views/docs/channels/_clear.html.erb @@ -0,0 +1,47 @@ +
+ <%= render 'response' %> +

Clear a Channel

+
+ +
+To clear all feed data from a Channel, send an HTTP DELETE to <%= @ssl_api_domain %>channels/CHANNEL_ID/feeds.json.xml . + +

+Valid parameters: +
    +
  • api_key (string) - User's API Key; please note that this is different than a Channel API key, and can be found in your account details. (required).
  • +
+ +
+Example DELETE: + +
+DELETE <%= @ssl_api_domain %>channels/4/feeds.json.xml
+       api_key=<%= @user_api_key %>
+
+ +
+ +
+ The response will be a webpage with your Channel. +
+ +
+ The response will be an empty JSON array, for example: + +
+[]
+
+
+ + +
+ The response will be an empty XML array, for example: + +
+<?xml version="1.0" encoding="UTF-8"?>
+<nil-classes type="array" />
+
+ +
+ diff --git a/app/views/docs/channels/_create.html.erb b/app/views/docs/channels/_create.html.erb new file mode 100644 index 0000000..932e302 --- /dev/null +++ b/app/views/docs/channels/_create.html.erb @@ -0,0 +1,101 @@ +
+ <%= render 'response' %> +

Create a Channel

+
+ +
+To create a new Channel, send an HTTP POST to <%= @ssl_api_domain %>channels.json.xml . + +

+Valid parameters: +
    +
  • api_key (string) - User's API Key; please note that this is different than a Channel API key, and can be found in your account details. (required).
  • +
  • description (string) - Description of the Channel (optional)
  • +
  • elevation (integer) - Elevation in meters (optional)
  • +
  • field1 (string) - Field1 name (optional)
  • +
  • field2 (string) - Field2 name (optional)
  • +
  • field3 (string) - Field3 name (optional)
  • +
  • field4 (string) - Field4 name (optional)
  • +
  • field5 (string) - Field5 name (optional)
  • +
  • field6 (string) - Field6 name (optional)
  • +
  • field7 (string) - Field7 name (optional)
  • +
  • field8 (string) - Field8 name (optional)
  • +
  • latitude (decimal) - Latitude in degrees (optional)
  • +
  • longitude (decimal) - Longitude in degrees (optional)
  • +
  • name (string) - Name of the Channel (optional)
  • +
  • public_flag (true/false) - Whether the Channel should be public, default false (optional)
  • +
  • tags (string) - Comma-separated list of tags (optional)
  • +
  • url (string) - Webpage URL for the Channel (optional)
  • +
+ +
+Example POST: + +
+POST <%= @ssl_api_domain %>channels.json.xml
+     api_key=<%= @user_api_key %>
+     name=My New Channel
+
+ +
+ +
+ The response will be a webpage with your newly created Channel. +
+ +
+ The response will be a JSON object of the new channel, for example: + +
+{
+  "id": 4,
+  "name": "My New Channel",
+  "description": null,
+  "latitude": null,
+  "longitude": null,
+  "created_at": "2014-03-25T13:12:50-04:00",
+  "elevation": null,
+  "last_entry_id": null,
+  "ranking": 15,
+  "username": "hans",
+  "tags": [],
+  "api_keys":
+  [
+    {
+      "api_key": "XXXXXXXXXXXXXXXX",
+      "write_flag": true
+    }
+  ]
+}
+
+ +
+ +
+ The response will be an XML object of the new channel, for example: + +
+<?xml version="1.0" encoding="UTF-8"?>
+<channel>
+    <id type="integer">4</id>
+    <name>My New Channel</name>
+    <description nil="true" />
+    <latitude type="decimal" nil="true" />
+    <longitude type="decimal" nil="true" />
+    <created-at type="dateTime">2014-03-25T20:17:44-04:00</created-at>
+    <elevation nil="true" />
+    <last-entry-id type="integer" nil="true" />
+    <ranking type="integer">15</ranking>
+    <username>hans</username>
+    <tags type="array" />
+    <api-keys type="array">
+        <api-key>
+            <api-key>XXXXXXXXXXXXXXXX</api-key>
+            <write-flag type="boolean">true</write-flag>
+        </api-key>
+    </api-keys>
+</channel>
+
+ +
+ diff --git a/app/views/docs/channels/_destroy.html.erb b/app/views/docs/channels/_destroy.html.erb new file mode 100644 index 0000000..1d90d8a --- /dev/null +++ b/app/views/docs/channels/_destroy.html.erb @@ -0,0 +1,72 @@ +
+ <%= render 'response' %> +

Delete a Channel

+
+ +
+To create a new Channel, send an HTTP DELETE to <%= @ssl_api_domain %>channels/CHANNEL_ID.json.xml , +replacing CHANNEL_ID with the ID of your Channel. + +

+Valid parameters: +
    +
  • api_key (string) - User's API Key; please note that this is different than a Channel API key, and can be found in your account details. (required).
  • +
+ +
+Example DELETE: + +
+DELETE <%= @ssl_api_domain %>channels/4.json.xml
+       api_key=<%= @user_api_key %>
+
+ +
+ +
+ The response will be a webpage with a list of Channels. +
+ +
+ The response will be a JSON object of the Channel before it was deleted, for example: + +
+{
+  "id": 4,
+  "name": "My New Channel",
+  "description": null,
+  "latitude": null,
+  "longitude": null,
+  "created_at": "2014-03-25T13:12:50-04:00",
+  "elevation": null,
+  "last_entry_id": null,
+  "ranking": 15,
+  "username": "hans",
+  "tags": []
+}
+
+ +
+ +
+ The response will be an XML object of the Channel before it was deleted, for example: + +
+<?xml version="1.0" encoding="UTF-8"?>
+<channel>
+    <id type="integer">4</id>
+    <name>My New Channel</name>
+    <description nil="true" />
+    <latitude type="decimal" nil="true" />
+    <longitude type="decimal" nil="true" />
+    <created-at type="dateTime">2014-03-25T20:17:44-04:00</created-at>
+    <elevation nil="true" />
+    <last-entry-id type="integer" nil="true" />
+    <ranking type="integer">15</ranking>
+    <username>hans</username>
+    <tags type="array" />
+</channel>
+
+ +
+ diff --git a/app/views/docs/channels/_feed.html.erb b/app/views/docs/channels/_feed.html.erb new file mode 100644 index 0000000..39c1103 --- /dev/null +++ b/app/views/docs/channels/_feed.html.erb @@ -0,0 +1,305 @@ +
+ <%= render 'response' %> +

Get a Channel Feed

+
+ +
+To view a Channel feed, send an HTTP GET to <%= @ssl_api_domain %>channels/CHANNEL_ID/feeds.json.xml , +replacing CHANNEL_ID with the ID of your Channel. + +

+Valid parameters: +
    +
  • key (string) Read API Key for this specific Channel (optional--no key required for public channels)
  • +
  • results (integer) Number of entries to retrieve, 8000 max, default of 100 (optional)
  • +
  • days (integer) Days from now to include in feed (optional)
  • +
  • start (datetime) Start date in format YYYY-MM-DD%20HH:NN:SS (optional)
  • +
  • end (datetime) End date in format YYYY-MM-DD%20HH:NN:SS (optional)
  • +
  • offset (integer) Offset of your timezone without daylight savings time (optional)
  • +
  • status (true/false) Include status updates in feed by setting "status=true" (optional)
  • +
  • location (true/false) Include latitude, longitude, and elevation in feed by setting "location=true" (optional)
  • +
  • min (decimal) Minimum value to include in response (optional)
  • +
  • max (decimal) Maximum value to include in response (optional)
  • +
  • round (integer) Round to this many decimal places (optional)
  • +
  • timescale (integer or string) Get first value in this many minutes, valid values: 10, 15, 20, 30, 60, 240, 720, 1440, "daily" (optional)
  • +
  • sum (integer or string) Get sum of this many minutes, valid values: 10, 15, 20, 30, 60, 240, 720, 1440, "daily" (optional)
  • +
  • average (integer or string) Get average of this many minutes, valid values: 10, 15, 20, 30, 60, 240, 720, 1440, "daily" (optional)
  • +
  • median (integer or string) Get median of this many minutes, valid values: 10, 15, 20, 30, 60, 240, 720, 1440, "daily" (optional)
  • +
  • callback (string) Function name to be used for JSONP cross-domain requests (optional)
  • +
+ +
+ Please note that the results parameter is not compatible with timescale, sum, average, or median. +
+ +
+Example GET: + +
GET <%= @ssl_api_domain %>channels/9/feeds.json.xml?results=2
+ +
+ +
+ The response will be an HTML page with the JSON Channel feed, for example: + +
+{
+  "channel":
+  {
+    "id": 9,
+    "name": "my_house",
+    "description": "Netduino Plus connected to sensors around the house",
+    "latitude": "40.44",
+    "longitude": "-79.996",
+    "field1": "Light",
+    "field2": "Outside Temperature",
+    "created_at": "2010-12-13T20:20:06-05:00",
+    "updated_at": "2014-02-26T12:43:04-05:00",
+    "last_entry_id": 6060625
+  },
+  "feeds":
+  [
+    {
+      "created_at": "2014-02-26T12:42:49-05:00",
+      "entry_id": 6060624,
+      "field1": "188",
+      "field2": "25.902335456475583"
+    },
+    {
+      "created_at": "2014-02-26T12:43:04-05:00",
+      "entry_id": 6060625,
+      "field1": "164",
+      "field2": "25.222929936305732"
+    }
+  ]
+}
+
+
+ +
+ The response will be a JSON object of the Channel feed, for example: + +
+{
+  "channel":
+  {
+    "id": 9,
+    "name": "my_house",
+    "description": "Netduino Plus connected to sensors around the house",
+    "latitude": "40.44",
+    "longitude": "-79.996",
+    "field1": "Light",
+    "field2": "Outside Temperature",
+    "created_at": "2010-12-13T20:20:06-05:00",
+    "updated_at": "2014-02-26T12:43:04-05:00",
+    "last_entry_id": 6060625
+  },
+  "feeds":
+  [
+    {
+      "created_at": "2014-02-26T12:42:49-05:00",
+      "entry_id": 6060624,
+      "field1": "188",
+      "field2": "25.902335456475583"
+    },
+    {
+      "created_at": "2014-02-26T12:43:04-05:00",
+      "entry_id": 6060625,
+      "field1": "164",
+      "field2": "25.222929936305732"
+    }
+  ]
+}
+
+ +
+ +
+ The response will be an XML object of the Channel feed, for example: + +
+<?xml version="1.0" encoding="UTF-8"?>
+<channel>
+  <id type="integer">9</id>
+  <name>my_house</name>
+  <description>Netduino Plus connected to sensors around the house</description>
+  <latitude type="decimal">40.44</latitude>
+  <longitude type="decimal">-79.996</longitude>
+  <field1>Light</field1>
+  <field2>Outside Temperature</field2>
+  <created-at type="dateTime">2010-12-13T20:20:06-05:00</created-at>
+  <updated-at type="dateTime">2014-02-26T12:49:19-05:00</updated-at>
+  <last-entry-id type="integer">6060650</last-entry-id>
+  <feeds type="array">
+    <feed>
+      <created-at type="dateTime">2014-02-26T12:49:04-05:00</created-at>
+      <entry-id type="integer">6060649</entry-id>
+      <field1>160</field1>
+      <field2>25.307855626326962</field2>
+      <id type="integer" nil="true"/>
+    </feed>
+    <feed>
+      <created-at type="dateTime">2014-02-26T12:49:19-05:00</created-at>
+      <entry-id type="integer">6060650</entry-id>
+      <field1>171</field1>
+      <field2>22.929936305732483</field2>
+      <id type="integer" nil="true"/>
+    </feed>
+  </feeds>
+</channel>
+
+ +
+ +
+ +Live examples: + + + +

+ +
+ <%= render 'response' %> +

Get Last Entry in a Channel Feed

+
+ +
+To get the last entry in a Channel feed, send an HTTP GET to <%= @ssl_api_domain %>channels/CHANNEL_ID/feeds/last.json.xml , +replacing CHANNEL_ID with the ID of your Channel. + +

+Valid parameters: +
    +
  • key (string) Read API Key for this specific Channel (optional--no key required for public channels)
  • +
  • offset (integer) Offset of your timezone without daylight savings time (optional)
  • +
  • status (true/false) Include status updates in feed by setting "status=true" (optional)
  • +
  • location (true/false) Include latitude, longitude, and elevation in feed by setting "location=true" (optional)
  • +
  • callback (string) Function name to be used for JSONP cross-domain requests (optional)
  • +
+ +
+Example GET: + +
GET <%= @ssl_api_domain %>channels/9/feeds/last.json.xml
+ +
+ +
+ The response will be a JSON object of the most recent feed, for example: + +
+{
+  "created_at": "2014-02-26T21:27:21Z",
+  "entry_id": 6061519,
+  "field1": "176",
+  "field2": "28.195329087048833"
+}
+
+
+ +
+ The response will be a JSON object of the most recent feed, for example: + +
+{
+  "created_at": "2014-02-26T21:27:21Z",
+  "entry_id": 6061519,
+  "field1": "176",
+  "field2": "28.195329087048833"
+}
+
+ +
+ +
+ The response will be an XML object of the most recent feed, for example: + +
+<?xml version="1.0" encoding="UTF-8"?>
+<feed>
+  <created-at type="dateTime">2014-02-26T21:28:51Z</created-at>
+  <entry-id type="integer">6061525</entry-id>
+  <field1>200</field1>
+  <field2>28.365180467091296</field2>
+  <id type="integer" nil="true"/>
+</feed>
+
+ +
+ + +

+ +
+ <%= render 'response' %> +

Get Specific Entry in a Channel

+
+ +
+To get a specific entry in a Channel's feed, send an HTTP GET to <%= @ssl_api_domain %>channels/CHANNEL_ID/feeds/ENTRY_ID.json.xml , +replacing CHANNEL_ID with the ID of your Channel and ENTRY_ID with the ID of your entry. + +

+Valid parameters: +
    +
  • key (string) Read API Key for this specific Channel (optional--no key required for public channels)
  • +
  • offset (integer) Offset of your timezone without daylight savings time (optional)
  • +
  • status (true/false) Include status updates in feed by setting "status=true" (optional)
  • +
  • location (true/false) Include latitude, longitude, and elevation in feed by setting "location=true" (optional)
  • +
  • callback (string) Function name to be used for JSONP cross-domain requests (optional)
  • +
+ +
+Example GET: + +
GET <%= @ssl_api_domain %>channels/9/feeds/6061519.json.xml
+ +
+ +
+ The response will be a JSON object of the feed entry, for example: + +
+{
+  "created_at": "2014-02-26T21:27:21Z",
+  "entry_id": 6061519,
+  "field1": "176",
+  "field2": "28.195329087048833"
+}
+
+ +
+ The response will be a JSON object of the feed entry, for example: + +
+{
+  "created_at": "2014-02-26T21:27:21Z",
+  "entry_id": 6061519,
+  "field1": "176",
+  "field2": "28.195329087048833"
+}
+
+ +
+ +
+ The response will be an XML object of the most recent feed, for example: + +
+<?xml version="1.0" encoding="UTF-8"?>
+<feed>
+  <created-at type="dateTime">2014-02-26T21:27:21Z</created-at>
+  <entry-id type="integer">6061519</entry-id>
+  <field1>176</field1>
+  <field2>28.195329087048833</field2>
+  <id type="integer" nil="true"/>
+</feed>
+
+ +
+ diff --git a/app/views/docs/channels/_field.html.erb b/app/views/docs/channels/_field.html.erb new file mode 100644 index 0000000..4b3c832 --- /dev/null +++ b/app/views/docs/channels/_field.html.erb @@ -0,0 +1,213 @@ +
+ <%= render 'response' %> +

Get a Channel Field Feed

+
+ +
+To view a Channel's field feed, send an HTTP GET to <%= @ssl_api_domain %>channels/CHANNEL_ID/fields/FIELD_ID.json.xml , +replacing CHANNEL_ID with the ID of your Channel and FIELD_ID with the ID of your field. + +

+Valid parameters: +
    +
  • key (string) Read API Key for this specific Channel (optional--no key required for public channels)
  • +
  • results (integer) Number of entries to retrieve, 8000 max, default of 100 (optional)
  • +
  • days (integer) Days from now to include in feed (optional)
  • +
  • start (datetime) Start date in format YYYY-MM-DD%20HH:NN:SS (optional)
  • +
  • end (datetime) End date in format YYYY-MM-DD%20HH:NN:SS (optional)
  • +
  • offset (integer) Offset of your timezone without daylight savings time (optional)
  • +
  • status (true/false) Include status updates in feed by setting "status=true" (optional)
  • +
  • location (true/false) Include latitude, longitude, and elevation in feed by setting "location=true" (optional)
  • +
  • min (decimal) Minimum value to include in response (optional)
  • +
  • max (decimal) Maximum value to include in response (optional)
  • +
  • round (integer) Round to this many decimal places (optional)
  • +
  • timescale (integer or string) Get first value in this many minutes, valid values: 10, 15, 20, 30, 60, 240, 720, 1440, "daily" (optional)
  • +
  • sum (integer or string) Get sum of this many minutes, valid values: 10, 15, 20, 30, 60, 240, 720, 1440, "daily" (optional)
  • +
  • average (integer or string) Get average of this many minutes, valid values: 10, 15, 20, 30, 60, 240, 720, 1440, "daily" (optional)
  • +
  • median (integer or string) Get median of this many minutes, valid values: 10, 15, 20, 30, 60, 240, 720, 1440, "daily" (optional)
  • +
  • callback (string) Function name to be used for JSONP cross-domain requests (optional)
  • +
+ +
+ Please note that the results parameter is not compatible with timescale, sum, average, or median. +
+ +
+Example GET: + +
GET <%= @ssl_api_domain %>channels/9/fields/1.json.xml?results=2
+ +
+ +
+ The response will be an HTML page with the JSON Channel's field feed, for example: + +
+{
+  "channel":
+  {
+    "id": 9,
+    "name": "my_house",
+    "description": "Netduino Plus connected to sensors around the house",
+    "latitude": "40.44",
+    "longitude": "-79.996",
+    "field1": "Light",
+    "field2": "Outside Temperature",
+    "created_at": "2010-12-13T20:20:06-05:00",
+    "updated_at": "2014-02-26T12:43:04-05:00",
+    "last_entry_id": 6060625
+  },
+  "feeds":
+  [
+    {
+      "created_at": "2014-02-26T12:42:49-05:00",
+      "entry_id": 6060624,
+      "field1": "188"
+    },
+    {
+      "created_at": "2014-02-26T12:43:04-05:00",
+      "entry_id": 6060625,
+      "field1": "164"
+    }
+  ]
+}
+
+
+ +
+ The response will be a JSON object of the Channel's field feed, for example: + +
+{
+  "channel":
+  {
+    "id": 9,
+    "name": "my_house",
+    "description": "Netduino Plus connected to sensors around the house",
+    "latitude": "40.44",
+    "longitude": "-79.996",
+    "field1": "Light",
+    "field2": "Outside Temperature",
+    "created_at": "2010-12-13T20:20:06-05:00",
+    "updated_at": "2014-02-26T12:43:04-05:00",
+    "last_entry_id": 6060625
+  },
+  "feeds":
+  [
+    {
+      "created_at": "2014-02-26T12:42:49-05:00",
+      "entry_id": 6060624,
+      "field1": "188"
+    },
+    {
+      "created_at": "2014-02-26T12:43:04-05:00",
+      "entry_id": 6060625,
+      "field1": "164"
+    }
+  ]
+}
+
+ +
+ +
+ The response will be an XML object of the Channel's field feed, for example: + +
+<?xml version="1.0" encoding="UTF-8"?>
+<channel>
+  <id type="integer">9</id>
+  <name>my_house</name>
+  <description>Netduino Plus connected to sensors around the house</description>
+  <latitude type="decimal">40.44</latitude>
+  <longitude type="decimal">-79.996</longitude>
+  <field1>Light</field1>
+  <field2>Outside Temperature</field2>
+  <created-at type="dateTime">2010-12-13T20:20:06-05:00</created-at>
+  <updated-at type="dateTime">2014-02-26T12:49:19-05:00</updated-at>
+  <last-entry-id type="integer">6060650</last-entry-id>
+  <feeds type="array">
+    <feed>
+      <created-at type="dateTime">2014-02-26T12:49:04-05:00</created-at>
+      <entry-id type="integer">6060649</entry-id>
+      <field1>160</field1>
+      <id type="integer" nil="true"/>
+    </feed>
+    <feed>
+      <created-at type="dateTime">2014-02-26T12:49:19-05:00</created-at>
+      <entry-id type="integer">6060650</entry-id>
+      <field1>171</field1>
+      <id type="integer" nil="true"/>
+    </feed>
+  </feeds>
+</channel>
+
+ +
+ +

+ +
+ <%= render 'response' %> +

Get Last Entry in a Field Feed

+
+ +
+To get the last entry in a Channel's field feed, send an HTTP GET to <%= @ssl_api_domain %>channels/CHANNEL_ID/fields/FIELD_ID/last.json.xml , +replacing CHANNEL_ID with the ID of your Channel and FIELD_ID with the ID of your field. + +

+Valid parameters: +
    +
  • key (string) Read API Key for this specific Channel (optional--no key required for public channels)
  • +
  • offset (integer) Offset of your timezone without daylight savings time (optional)
  • +
  • status (true/false) Include status updates in feed by setting "status=true" (optional)
  • +
  • location (true/false) Include latitude, longitude, and elevation in feed by setting "location=true" (optional)
  • +
  • callback (string) Function name to be used for JSONP cross-domain requests (optional)
  • +
  • prepend (string) Text to add before the API response (optional)
  • +
  • append (string) Text to add after the API response (optional)
  • +
+ +
+Example GET: + +
GET <%= @ssl_api_domain %>channels/9/fields/1/last.json.xml
+ +
+ +
+ The response will be the Channel field's most recent value, for example: + +
+176
+
+
+ +
+ The response will be a JSON object of the most recent feed, for example: + +
+{
+  "created_at": "2014-02-26T21:27:21Z",
+  "entry_id": 6061519,
+  "field1": "176"
+}
+
+ +
+ +
+ The response will be an XML object of the most recent feed, for example: + +
+<?xml version="1.0" encoding="UTF-8"?>
+<feed>
+  <created-at type="dateTime">2014-02-26T21:28:51Z</created-at>
+  <entry-id type="integer">6061525</entry-id>
+  <field1>200</field1>
+  <id type="integer" nil="true"/>
+</feed>
+
+ +
+ diff --git a/app/views/docs/channels/_importer.html.erb b/app/views/docs/channels/_importer.html.erb new file mode 100644 index 0000000..c6dd4e8 --- /dev/null +++ b/app/views/docs/channels/_importer.html.erb @@ -0,0 +1,8 @@ +

Importer

+ +

Using the ThingSpeak Importer, you are able to import data from a CSV file directly into a ThingSpeak Channel. The access the Importer, select a Channel, and click Import Data.

+

The format for the CSV should be the following:

+
datetime,field1,field2,field3,field4,field5,field6,field7,field8,latitude,longitude,elevation,status
+

Here is an example CSV file: Sample CSV File

+You only have to send a datetime stamp and at least one field. The datetime stamp can be in many formats such as epoch, ISO 8601, or MySQL time. If the datetime includes a GMT/UTC offset, we will use that to properly import the data. If your datetime stamps do not have a GMT / UTC offset, you can specify a time zone that the data was logged in. + diff --git a/app/views/docs/channels/_public_index.html.erb b/app/views/docs/channels/_public_index.html.erb new file mode 100644 index 0000000..c604e4a --- /dev/null +++ b/app/views/docs/channels/_public_index.html.erb @@ -0,0 +1,149 @@ +
+ <%= render 'response' %> +

List Public Channels

+
+ +
+To view a list of public Channels, send an HTTP GET to +
+<%= @ssl_api_domain %>channels/public.json.xml . + +

+Valid parameters: +
    +
  • page (integer) Page number to retrieve (optional)
  • +
  • tag (string) Name of tag to search for (optional)
  • +
  • username (string) Person's username that you want to search Channels for (optional)
  • +
+ +
+Example GET: + +
GET <%= @ssl_api_domain %>channels/public.json.xml
+ +
+ +
+ The response will be a webpage with a list of public Channels. +
+ +
+ The response will be a JSON object of public Channels, for example: + +
+{
+  "pagination":
+  {
+    "current_page": 1,
+    "per_page": 15,
+    "total_entries": 653
+  },
+  "channels":
+  [
+    {
+      "id": 9,
+      "name": "my_house",
+      "description": "Netduino Plus connected to sensors around the house",
+      "latitude": "40.44",
+      "longitude": "-79.996",
+      "created_at": "2010-12-13T20:20:06-05:00",
+      "elevation": "",
+      "last_entry_id": 6062691,
+      "ranking" :100,
+      "username":"hans",
+      "tags":
+      [
+        {
+          "id": 9,
+          "name": "temp"
+        },{
+          "id": 25,
+          "name": "light"
+        }
+      ]
+    },
+    {
+      "id": 5683,
+      "name": "Residential Data Points",
+      "description": "Arduino Uno + Ethernet Shield",
+      "latitude": "35.664548",
+      "longitude": "-78.654972",
+      "created_at": "2013-05-15T12:33:57-04:00",
+      "elevation": "100",
+      "last_entry_id": 731713,
+      "ranking": 100,
+      "username": "samlro",
+      "tags":
+      [
+        {
+          "id": 950,
+          "name": "Analog Inputs"
+        }
+      ]
+    }
+  ]
+}
+
+ +
+ +
+ The response will be an XML object of public Channels, for example: + +
+<?xml version="1.0" encoding="UTF-8"?>
+<response>
+  <pagination>
+    <current-page type="WillPaginate::PageNumber">1</current-page>
+    <per-page type="integer">15</per-page>
+    <total-entries type="integer">654</total-entries>
+  </pagination>
+  <channels type="array">
+    <channel>
+      <id type="integer">9</id>
+      <name>my_house</name>
+      <description>
+      Netduino Plus connected to sensors around the house
+      </description>
+      <latitude type="decimal">40.44</latitude>
+      <longitude type="decimal">-79.996</longitude>
+      <created-at type="dateTime">2010-12-13T20:20:06-05:00</created-at>
+      <elevation/>
+      <last-entry-id type="integer">6062720</last-entry-id>
+      <ranking type="integer">100</ranking>
+      <username>hans</username>
+      <tags type="array">
+        <tag>
+          <id type="integer">9</id>
+          <name>temp</name>
+        </tag>
+        <tag>
+          <id type="integer">25</id>
+          <name>light</name>
+        </tag>
+      </tags>
+    </channel>
+    <channel>
+      <id type="integer">5683</id>
+      <name>Residential Data Points</name>
+      <description>Arduino Uno + Ethernet Shield</description>
+      <latitude type="decimal">35.664548</latitude>
+      <longitude type="decimal">-78.654972</longitude>
+      <created-at type="dateTime">2013-05-15T12:33:57-04:00</created-at>
+      <elevation>100</elevation>
+      <last-entry-id type="integer">731720</last-entry-id>
+      <ranking type="integer">100</ranking>
+      <username>samlro</username>
+      <tags type="array">
+        <tag>
+          <id type="integer">950</id>
+          <name>Analog Inputs</name>
+        </tag>
+      </tags>
+    </channel>
+  </channels>
+</response>
+
+ +
+ diff --git a/app/views/docs/channels/_status.html.erb b/app/views/docs/channels/_status.html.erb new file mode 100644 index 0000000..499a103 --- /dev/null +++ b/app/views/docs/channels/_status.html.erb @@ -0,0 +1,109 @@ +
+ <%= render 'response' %> +

Get Status Updates

+
+ +
+To view a Channel's status updates, send an HTTP GET to <%= @ssl_api_domain %>channels/CHANNEL_ID/status.json.xml , +replacing CHANNEL_ID with the ID of your Channel. + +

+Valid parameters: +
    +
  • key (string) Read API Key for this specific Channel (optional--no key required for public channels)
  • +
  • offset (integer) Offset of your timezone without daylight savings time (optional)
  • +
  • callback (string) Function name to be used for JSONP cross-domain requests (optional)
  • +
+ +
+Example GET: + +
GET <%= @ssl_api_domain %>channels/1417/status.json.xml
+ +
+ +
+ The response will be a JSON object of Channel statuses, for example: + +
+{
+  "channel":
+  {
+    "name": "CheerLights",
+    "latitude": "40.5",
+    "longitude": "-80.22"
+  },
+  "feeds":
+  [
+    {
+      "created_at": "2014-02-26T02:28:01Z",
+      "entry_id": 11888,
+      "status": "@cheerlights green"
+    },
+    {
+      "created_at": "2014-02-26T22:05:31Z",
+      "entry_id" :11889,
+      "status": "@cheerlights blue"
+    }
+  ]
+}
+
+
+ +
+ The response will be a JSON object of Channel statuses, for example: + +
+{
+  "channel":
+  {
+    "name": "CheerLights",
+    "latitude": "40.5",
+    "longitude": "-80.22"
+  },
+  "feeds":
+  [
+    {
+      "created_at": "2014-02-26T02:28:01Z",
+      "entry_id": 11888,
+      "status": "@cheerlights green"
+    },
+    {
+      "created_at": "2014-02-26T22:05:31Z",
+      "entry_id" :11889,
+      "status": "@cheerlights blue"
+    }
+  ]
+}
+
+ +
+ +
+ The response will be an XML object of Channel statuses, for example: + +
+<?xml version="1.0" encoding="UTF-8"?>
+<channel>
+  <name>CheerLights</name>
+  <latitude type="decimal">40.5</latitude>
+  <longitude type="decimal">-80.22</longitude>
+  <feeds type="array">
+    <feed>
+      <created-at type="dateTime">2014-02-26T02:28:01Z</created-at>
+      <entry-id type="integer">11888</entry-id>
+      <status>@cheerlights green</status>
+      <id type="integer" nil="true"/>
+    </feed>
+    <feed>
+      <created-at type="dateTime">2014-02-26T22:05:31Z</created-at>
+      <entry-id type="integer">11889</entry-id>
+      <status>@cheerlights blue</status>
+      <id type="integer" nil="true"/>
+    </feed>
+  </feeds>
+</channel>
+
+ +
+ diff --git a/app/views/docs/channels/_update.html.erb b/app/views/docs/channels/_update.html.erb new file mode 100644 index 0000000..205edb0 --- /dev/null +++ b/app/views/docs/channels/_update.html.erb @@ -0,0 +1,100 @@ +
+ <%= render 'response' %> +

Update Channel Feed

+
+ +
+To update a Channel feed, send an HTTP GET or POST to
+<%= @ssl_api_domain %>update.json.xml . + +

+Valid parameters: +
    +
  • api_key (string) - Write API Key for this specific Channel (required). The Write API Key can optionally be sent via an X-THINGSPEAKAPIKEY HTTP header.
  • +
  • field1 (string) - Field 1 data (optional)
  • +
  • field2 (string) - Field 2 data (optional)
  • +
  • field3 (string) - Field 3 data (optional)
  • +
  • field4 (string) - Field 4 data (optional)
  • +
  • field5 (string) - Field 5 data (optional)
  • +
  • field6 (string) - Field 6 data (optional)
  • +
  • field7 (string) - Field 7 data (optional)
  • +
  • field8 (string) - Field 8 data (optional)
  • +
  • lat (decimal) - Latitude in degrees (optional)
  • +
  • long (decimal) - Longitude in degrees (optional)
  • +
  • elevation (integer) - Elevation in meters (optional)
  • +
  • status (string) - Status update message (optional)
  • +
  • twitter (string) - Twitter username linked to ThingTweet (optional)
  • +
  • tweet (string) - Twitter status update; see updating ThingTweet for more info (optional)
  • +
  • created_at (datetime) - Date when this feed entry was created, in the format
    YYYY-MM-DD%20HH:NN:SS (optional)
  • +
+ +
+Example POST: + +
+POST <%= @ssl_api_domain %>update.json.xml
+     api_key=<%= @channel_api_key %>
+     field1=73
+
+ +
+ +
+ The response will be the entry ID of the update, for example: 18 +

+ If the response is 0 then the update failed. +
+ +
+ The response will be a JSON object of the new feed, for example: + +
+{
+  "channel_id": 3,
+  "field1": "73",
+  "field2": null,
+  "field3": null,
+  "field4": null,
+  "field5": null,
+  "field6": null,
+  "field7": null,
+  "field8": null,
+  "created_at": "2014-02-25T14:13:01-05:00",
+  "entry_id": 320,
+  "status": null,
+  "latitude": null,
+  "longitude": null,
+  "elevation": null,
+  "location":null
+}
+
+ +
+ +
+ The response will be an XML object of the new feed, for example: + +
+<?xml version="1.0" encoding="UTF-8"?>
+<feed>
+  <channel-id type="integer">3</channel-id>
+  <field1>73</field1>
+  <field2 nil="true"/>
+  <field3 nil="true"/>
+  <field4 nil="true"/>
+  <field5 nil="true"/>
+  <field6 nil="true"/>
+  <field7 nil="true"/>
+  <field8 nil="true"/>
+  <created-at type="dateTime">2014-02-25T14:15:42-05:00</created-at>
+  <entry-id type="integer">321</entry-id>
+  <status nil="true"/>
+  <latitude type="decimal" nil="true"/>
+  <longitude type="decimal" nil="true"/>
+  <elevation nil="true"/>
+  <location nil="true"/>
+</feed>
+
+ +
+ diff --git a/app/views/docs/charts.html.erb b/app/views/docs/charts.html.erb new file mode 100644 index 0000000..31c2025 --- /dev/null +++ b/app/views/docs/charts.html.erb @@ -0,0 +1,89 @@ +
+ +
+ <%= render 'docs/sidebar' %> +
+ +
+ +

Charts

+ The Charts API allows you to create an instant visualization of your data. The chart displays properly in all modern browsers and mobile devices. The chart can also show dynamic data by loading new data automatically. +

+ Use the Charts API to present numerical data stored in ThingSpeak Channels on charts. Supported chart types are line, bar, column, and step. Options include size, color, and labels. +

+ +
+

Creating a Chart

+ + To create a chart, open the following URL, replacing CHANNEL_ID and FIELD_ID with values from one of your Channels. +

+
http://api.thingspeak.com/channels/CHANNEL_ID/charts/FIELD_ID
+ +
+ Valid chart parameters: +
    +
  • title (string) Chart title, default: Channel name (optional)
  • +
  • xaxis (string) Chart's x-axis label, default: "Date" (optional)
  • +
  • yaxis (string) Chart's y-axis label, default: field name (optional)
  • +
  • color (string) Line color, default: red (optional)
  • +
  • bgcolor (string) Background color, default: white (optional)
  • +
  • type (line/bar/column) Type of chart, default: line (optional)
  • +
  • width (integer) Chart width in pixels, iframe width will be 20px larger, default chart width: 400 (optional)
  • +
  • height (integer) Chart height in pixels, iframe height will be 20px larger, default chart height: 200 (optional)
  • +
  • dynamic (true/false) Make chart update automatically every 15 seconds, default: false (optional)
  • +
  • step (true/false) Draw chart as a step chart, default: false (optional)
  • +
  • export (true/false) Show export buttons, so that chart can be saved as an image, default: false (optional)
  • +
+ +
+ Valid feed parameters: +
    +
  • key (string) Read API Key for this specific Channel (optional--no key required for public channels)
  • +
  • results (integer) Number of entries to retrieve, 8000 max, default of 100 (optional)
  • +
  • days (integer) Days from now to include in feed (optional)
  • +
  • start (datetime) Start date in format YYYY-MM-DD%20HH:NN:SS (optional)
  • +
  • end (datetime) End date in format YYYY-MM-DD%20HH:NN:SS (optional)
  • +
  • offset (integer) Offset of your timezone without daylight savings time (optional)
  • +
  • status (true/false) Include status updates in feed by setting "status=true" (optional)
  • +
  • location (true/false) Include latitude, longitude, and elevation in feed by setting "location=true" (optional)
  • +
  • min (decimal) Minimum value to include in response (optional)
  • +
  • max (decimal) Maximum value to include in response (optional)
  • +
  • round (integer) Round to this many decimal places (optional)
  • +
  • timescale (integer or string) Get first value in this many minutes, valid values: 10, 15, 20, 30, 60, 240, 720, 1440, "daily" (optional)
  • +
  • sum (integer or string) Get sum of this many minutes, valid values: 10, 15, 20, 30, 60, 240, 720, 1440, "daily" (optional)
  • +
  • average (integer or string) Get average of this many minutes, valid values: 10, 15, 20, 30, 60, 240, 720, 1440, "daily" (optional)
  • +
  • median (integer or string) Get median of this many minutes, valid values: 10, 15, 20, 30, 60, 240, 720, 1440, "daily" (optional)
  • +
+ +
+ Please note that the results parameter is not compatible with timescale, sum, average, or median. +
+ +

+ +
+

Embedding a Chart

+ To place a ThingSpeak Chart on your webpage, use the Chart API as the source of an iframe. +

+ Chart Embed Code: +

+
<iframe width="450" height="250" style="border: 1px solid #cccccc;" src="http://thingspeak.com/channels/CHANNEL_ID/charts/FIELD_ID"></iframe>
+

+ Example Chart: + + +
+

Embedding a Dynamic Chart

+ To place a dynamic ThingSpeak Chart on your webpage, use the Chart API as the source of an iframe and add the chart parameter ”dynamic=true”. +

+ Chart Embed Code: +

+
<iframe width="450" height="250" style="border: 1px solid #cccccc;" src="http://thingspeak.com/channels/CHANNEL_ID/charts/FIELD_ID?dynamic=true"></iframe>
+

+ Example Chart: + +











+ +
+
+ diff --git a/app/views/docs/errors.html.erb b/app/views/docs/errors.html.erb new file mode 100644 index 0000000..78c2489 --- /dev/null +++ b/app/views/docs/errors.html.erb @@ -0,0 +1,75 @@ +
+ +
+ <%= render 'docs/sidebar' %> +
+ +
+ +
+ <%= render 'response' %> +

Error Codes

+
+ +

+ + + + + + + + + + <% t(:error_codes).each do |key, values| %> + + + + + + + <% end %> + +
<%= t(:error_code) %><%= t(:error_http_status) %><%= t(:error_message) %><%= t(:error_details) %>
<%= key %><%= values[:http_status] %><%= values[:message] %><%= values[:details] %>
+ +
+ All errors will be sent with their corresponding HTTP status code. +

+
+ +
+ Example error response: error_auth_required +
+ +
+ The response will be a JSON error object, for example: + +
+{
+  "status": "401",
+  "error":
+  {
+    "error_code": "error_auth_required",
+    "message": "Authorization Required",
+    "details": "Please make sure that your API key is correct."
+  }
+}
+    
+
+ +
+ The response will be an XML error object, for example: + +
+<?xml version="1.0" encoding="UTF-8"?>
+<error>
+  <error-code>error_auth_required</error-code>
+  <message>Authorization Required</message>
+  <details>Please make sure that your API key is correct.</details>
+</error>
+    
+ +
+
+
+ diff --git a/app/views/docs/index.html.erb b/app/views/docs/index.html.erb index c88aa0d..4e06071 100644 --- a/app/views/docs/index.html.erb +++ b/app/views/docs/index.html.erb @@ -1,4 +1,39 @@
- welcome + +
+ <%= render 'docs/sidebar' %> +
+ +
+ +

Getting Started

+ + + +
+

Support

+

Please post your questions, comments, and feature requests in the ThingSpeak Forum.

+ +

+

Open Source

+ +

The ThingSpeak API is available on GitHub and includes the complete ThingSpeak API for processing HTTP requests, storing numeric and alphanumeric data, numeric data processing, location tracking, and status updates.  The open source version follows the same documentation as the ThingSpeak hosted service.

+ + + +

+

HTTP Headers

+ If you would like to reduce the number of HTTP headers sent by our application, please add the parameter "headers=false" to any HTTP request. + +











+ +
diff --git a/app/views/docs/plugins.html.erb b/app/views/docs/plugins.html.erb new file mode 100644 index 0000000..44c876b --- /dev/null +++ b/app/views/docs/plugins.html.erb @@ -0,0 +1,26 @@ +
+ +
+ <%= render 'docs/sidebar' %> +
+ +
+ +

Plugins

+ + Plugins are way to create your applications natively on the ThingSpeak platform. You can create applications using HTML, CSS, and JavaScript to create mashups of ThingSpeak services with other web services. Plugins can either be private or public. Private plugins can only be viewed from a private ThingSpeak Channel view. Public Plugins can be embedded on your website or added to a ThingSpeak Channel view. +

+ +
+

Example Plugin

+

Display a Google Gauge Visualization using ThingSpeak Plugins [Source Code]

+ +

+ +

+ +











+ +
+
+ diff --git a/app/views/docs/users.html.erb b/app/views/docs/users.html.erb new file mode 100644 index 0000000..114af8e --- /dev/null +++ b/app/views/docs/users.html.erb @@ -0,0 +1,22 @@ +
+ +
+ <%= render 'docs/sidebar' %> +
+ +
+ +

Users

+

+ +
+ <%= render 'docs/users/show' %> + +
+ <%= render 'docs/users/user_channels' %> + +











+ +
+
+ diff --git a/app/views/docs/users/_show.html.erb b/app/views/docs/users/_show.html.erb new file mode 100644 index 0000000..f687ae7 --- /dev/null +++ b/app/views/docs/users/_show.html.erb @@ -0,0 +1,59 @@ +
+ <%= render 'response' %> +

Get User Information

+
+ +
+To get a user's information, send an HTTP GET to <%= @ssl_api_domain %>users/USERNAME.json.xml , +replacing USERNAME with the person's username. + +

+Valid parameters: +
    +
  • api_key (string) - User's API Key which, if provided, will show private information such as email addresses. (optional)
  • +
+ +
+Example GET: + +
+GET <%= @ssl_api_domain %>users/hans.json.xml
+
+ +
+ +
+ The response will be a webpage with information about the user. +
+ +
+ The response will be a JSON object of the user, for example: + +
+{
+  "id": 4,
+  "login": "hans",
+  "created_at": "2010-12-03T09:17:52-05:00",
+  "bio": "Web Developer @iobridge, @thingspeak",
+  "website": "http://www.iamshadowlord.com"
+}
+
+ +
+ +
+ The response will be an XML object of the user, for example: + +
+<?xml version="1.0" encoding="UTF-8"?>
+<user>
+  <id type="integer">4</id>
+  <login>hans</login>
+  <created-at type="dateTime">2010-12-03T09:17:52-05:00</created-at>
+  <bio>Web Developer @iobridge, @thingspeak</bio>
+  <website>http://www.iamshadowlord.com</website>
+</user>
+
+ +
+ diff --git a/app/views/docs/users/_user_channels.html.erb b/app/views/docs/users/_user_channels.html.erb new file mode 100644 index 0000000..14821cc --- /dev/null +++ b/app/views/docs/users/_user_channels.html.erb @@ -0,0 +1,136 @@ +
+ <%= render 'response' %> +

List User's Channels

+
+ +
+To get a list of a user's Channels, send an HTTP GET to <%= @ssl_api_domain %>users/USERNAME/channels.json.xml , +replacing USERNAME with the person's username. + +

+Valid parameters: +
    +
  • api_key (string) - User's API Key which, if provided, will also show private channels. (optional)
  • +
+ +
+Example GET: + +
+GET <%= @ssl_api_domain %>users/hans/channels.json.xml
+
+ +
+ +
+ The response will be a webpage with a list of the user's Channels. +
+ +
+ The response will be a JSON object of the user's Channels, for example: + +
+{
+  "channels":
+  [
+    {
+      "id": 3,
+      "name": "ioBridge Server",
+      "description": "ioBridge IO-204 connected to web server",
+      "latitude": null,
+      "longitude": null,
+      "created_at": "2010-12-03T09:26:23-05:00",
+      "elevation": "",
+      "last_entry_id": 163690,
+      "ranking": 85,
+      "username": "hans",
+      "tags":
+      [
+        {
+          "id": 24,
+          "name": "temperature"
+        }
+      ]
+    },
+    {
+      "id": 9,
+      "name": "my_house",
+      "description": "Netduino Plus connected to sensors around the house",
+      "latitude": "40.44",
+      "longitude": "-79.996",
+      "created_at": "2010-12-13T20:20:06-05:00",
+      "elevation": "",
+      "last_entry_id": 6062844,
+      "ranking": 100,
+      "username": "hans",
+      "tags":
+      [
+        {
+          "id": 9,
+          "name": "temp"
+        },
+        {
+          "id": 25,
+          "name": "light"
+        }
+      ]
+    }
+  ]
+}
+
+ +
+ +
+ The response will be an XML object of the user's Channels, for example: + +
+<?xml version="1.0" encoding="UTF-8"?>
+<response>
+  <channels type="array">
+    <channel>
+      <id type="integer">3</id>
+      <name>ioBridge Server</name>
+      <description>ioBridge IO-204 connected to web server</description>
+      <latitude nil="true"/>
+      <longitude nil="true"/>
+      <created-at type="dateTime">2010-12-03T09:26:23-05:00</created-at>
+      <elevation/>
+      <last-entry-id type="integer">163690</last-entry-id>
+      <ranking type="integer">85</ranking>
+      <username>hans</username>
+      <tags type="array">
+        <tag>
+          <id type="integer">24</id>
+          <name>temperature</name>
+        </tag>
+      </tags>
+    </channel>
+    <channel>
+      <id type="integer">9</id>
+      <name>my_house</name>
+      <description>Netduino Plus connected to sensors around the house</description>
+      <latitude type="decimal">40.44</latitude>
+      <longitude type="decimal">-79.996</longitude>
+      <created-at type="dateTime">2010-12-13T20:20:06-05:00</created-at>
+      <elevation/>
+      <last-entry-id type="integer">6062860</last-entry-id>
+      <ranking type="integer">100</ranking>
+      <username>hans</username>
+      <tags type="array">
+        <tag>
+          <id type="integer">9</id>
+          <name>temp</name>
+        </tag>
+        <tag>
+          <id type="integer">25</id>
+          <name>light</name>
+        </tag>
+      </tags>
+    </channel>
+  </channels>
+</response>
+
+ +
+ diff --git a/app/views/layouts/_footer.html.erb b/app/views/layouts/_footer.html.erb index e04d0c8..2c7ccfb 100644 --- a/app/views/layouts/_footer.html.erb +++ b/app/views/layouts/_footer.html.erb @@ -6,7 +6,7 @@ ThingSpeak.com | Blog | Forum | - Documentation | + Documentation | Tutorials | RSS Feed diff --git a/app/views/layouts/_header.html.erb b/app/views/layouts/_header.html.erb index 86e1ebd..4746c89 100644 --- a/app/views/layouts/_header.html.erb +++ b/app/views/layouts/_header.html.erb @@ -34,6 +34,9 @@
  • <%= link_to t(:profile_edit), edit_profile_path %>
  • + <% if current_admin_user.present? %> +
  • <%= link_to t(:admin), "/admin" %>
  • + <% end %> <% else %>
  • ><%= link_to t(:channels), public_channels_path %>
  • @@ -43,10 +46,10 @@ diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 9929d8c..48221ec 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -23,6 +23,12 @@
    <%= render 'layouts/header' %>
    + <% if notice.present? %> +

    <%= notice %>X

    + <% end %> + <% if alert.present? %> +

    <%= alert %>X

    + <% end %> <%= yield %>
    diff --git a/app/views/mailer/password_reset.html.erb b/app/views/mailer/password_reset.html.erb index e22845e..563cbba 100644 --- a/app/views/mailer/password_reset.html.erb +++ b/app/views/mailer/password_reset.html.erb @@ -6,11 +6,11 @@

    <%= t(:password_reset_message1) %> -
    +
    <%= t(:password_reset_message2) %> -
    +
    <%= t(:password_reset_message3) %> -

    +

    <%= @webpage %>

    diff --git a/app/views/pages/export.html.erb b/app/views/pages/export.html.erb index 70c5af6..8519637 100644 --- a/app/views/pages/export.html.erb +++ b/app/views/pages/export.html.erb @@ -111,6 +111,6 @@ -
    +
    diff --git a/app/views/pages/headers.html.erb b/app/views/pages/headers.html.erb index dba94f3..2a8210c 100644 --- a/app/views/pages/headers.html.erb +++ b/app/views/pages/headers.html.erb @@ -1,7 +1,8 @@ <% for header in request.env.select {|k,v| k.match("^HTTP.*")} %> - + <% end %>
    <%=header[0].split('_',2)[1]%><%=header[1]%><%=header[0].split('_',2)[1]%><%=header[1]%>
    + diff --git a/app/views/pages/home.html.erb b/app/views/pages/home.html.erb index 5b327a6..d6cd475 100644 --- a/app/views/pages/home.html.erb +++ b/app/views/pages/home.html.erb @@ -203,7 +203,7 @@
  • Twitter
  • GitHub
  • ThingSpeak Community
  • -
  • Documentation and Tutorials
  • +
  • Documentation and Tutorials
  • Questions
  • diff --git a/app/views/pages/social_home.html.erb b/app/views/pages/social_home.html.erb index 7786c8f..3e6cc30 100644 --- a/app/views/pages/social_home.html.erb +++ b/app/views/pages/social_home.html.erb @@ -87,10 +87,10 @@ <% end %>

    the social sensor network is forming.

    -

    We have one thing in common. At almost all times we are connected to the web.
    +

    We have one thing in common. At almost all times we are connected to the web.
    The Social Sensor Network allows everyone to report and share data in an open and meaningful way.

    -
    +
    @@ -128,12 +128,12 @@ } -
    -
    +
    +
    demo channels -
    +
    IRS Refunds Frozen Online Poker Money diff --git a/app/views/pipes/index.html.erb b/app/views/pipes/index.html.erb index bda2e6c..ecedff9 100644 --- a/app/views/pipes/index.html.erb +++ b/app/views/pipes/index.html.erb @@ -1,7 +1,7 @@

    Pipes

    <%= link_to 'New Pipe', new_pipe_path %> -

    +

    @@ -15,5 +15,5 @@ <% end %>
    IDNameSlugURLDate
    -
    +
    <%= will_paginate @pipes %> diff --git a/app/views/plugins/edit.html.erb b/app/views/plugins/edit.html.erb index a08db2b..0bea223 100644 --- a/app/views/plugins/edit.html.erb +++ b/app/views/plugins/edit.html.erb @@ -48,7 +48,7 @@ <% end %> -

    +

    <%= t(:plugin_delete_message) %>

    <%= button_to t(:plugin_delete), plugin_path(@plugin.id), :method => 'delete', :data => { :confirm => t(:confirm_plugin_delete) }, :class => 'btn btn-danger' %> diff --git a/app/views/plugins/index.html.erb b/app/views/plugins/index.html.erb index ae8a6cc..a0d90e0 100644 --- a/app/views/plugins/index.html.erb +++ b/app/views/plugins/index.html.erb @@ -23,7 +23,7 @@ -

    +

    <% end %> <%= form_for :plugin do |p| %> diff --git a/app/views/status/_recent.html.erb b/app/views/status/_recent.html.erb index a5b1adc..765d942 100644 --- a/app/views/status/_recent.html.erb +++ b/app/views/status/_recent.html.erb @@ -8,8 +8,8 @@
    <% @statuses.each do |r| %> -
    <%= r.status %>
    <%= r.created_at %>
    -
    +
    <%= r.status %>
    <%= r.created_at %>
    +
    <% end %>