├── .gitignore ├── .rspec ├── .rvmrc ├── .travis.yml ├── Capfile ├── Gemfile ├── Gemfile.lock ├── Guardfile ├── LICENSE ├── README.markdown ├── Rakefile ├── app ├── controllers │ ├── application_controller.rb │ ├── comments_controller.rb │ ├── episodes_controller.rb │ ├── feedback_messages_controller.rb │ ├── info_controller.rb │ ├── users_controller.rb │ └── versions_controller.rb ├── helpers │ ├── application_helper.rb │ ├── comments_helper.rb │ ├── episodes_helper.rb │ ├── error_messages_helper.rb │ ├── feedback_messages_helper.rb │ ├── info_helper.rb │ ├── layout_helper.rb │ ├── users_helper.rb │ └── versions_helper.rb ├── mailers │ └── mailer.rb ├── models │ ├── ability.rb │ ├── comment.rb │ ├── episode.rb │ ├── feedback_message.rb │ ├── tag.rb │ ├── tagging.rb │ └── user.rb └── views │ ├── comments │ ├── _comment.html.erb │ ├── _comment_headline.html.erb │ ├── _form.html.erb │ ├── create.js.erb │ ├── destroy.js.erb │ ├── edit.html.erb │ ├── edit.js.erb │ ├── index.html.erb │ ├── new.html.erb │ ├── new.js.erb │ └── update.js.erb │ ├── episodes │ ├── _comments.html.erb │ ├── _episode.html.erb │ ├── _form.html.erb │ ├── _show_notes.html.erb │ ├── _similar.html.erb │ ├── edit.html.erb │ ├── index.html.erb │ ├── index.rss.builder │ ├── new.html.erb │ ├── show.html.erb │ └── show.js.erb │ ├── feedback_messages │ └── new.html.erb │ ├── info │ ├── about.html.erb │ ├── give_back.html.erb │ └── moderators.html.erb │ ├── layouts │ └── application.html.erb │ ├── mailer │ ├── comment_response.text.erb │ └── feedback.text.erb │ └── users │ ├── ban.js.erb │ ├── edit.html.erb │ └── show.html.erb ├── config.ru ├── config ├── application.rb ├── boot.rb ├── deploy.rb ├── environment.rb ├── environments │ ├── development.rb │ ├── production.rb │ └── test.rb ├── examples │ ├── app_config.yml │ ├── database.yml │ └── production.sphinx.conf ├── initializers │ ├── backtrace_silencers.rb │ ├── inflections.rb │ ├── mime_types.rb │ ├── omniauth.rb │ ├── secret_token.rb │ └── session_store.rb ├── locales │ └── en.yml ├── routes.rb └── schedule.rb ├── db ├── migrate │ ├── 20080509050853_create_episodes.rb │ ├── 20080620015230_create_tags.rb │ ├── 20080620015432_create_taggings.rb │ ├── 20080620022227_create_comments.rb │ ├── 20080702045900_add_position_to_episodes.rb │ ├── 20080721011326_create_downloads.rb │ ├── 20080722025256_create_sponsors.rb │ ├── 20080727044942_add_foreign_key_indexes.rb │ ├── 20080730035149_add_comments_count_to_episodes.rb │ ├── 20080820014608_add_position_to_comments.rb │ ├── 20090128014441_add_force_top_to_sponsors.rb │ ├── 20090228183216_add_seconds_to_episodes.rb │ ├── 20090228184049_remove_seconds_from_downloads.rb │ ├── 20090311165121_create_spam_reports.rb │ ├── 20090311181239_add_hit_count_to_spam_reports.rb │ ├── 20090318164300_remove_user_ip_from_spam_reports.rb │ ├── 20090403023001_add_asciicasts_to_episodes.rb │ ├── 20091121172820_create_spam_checks.rb │ ├── 20091121181751_create_spam_questions.rb │ ├── 20101117191759_create_users.rb │ ├── 20101209224952_add_github_uid_to_users.rb │ ├── 20101209230056_rename_avatar_url_to_gravatar_token.rb │ ├── 20101210220007_add_user_id_to_comments.rb │ ├── 20110416201115_add_ancestry_to_comments.rb │ ├── 20110416214833_add_legacy_to_episodes.rb │ ├── 20110416232852_create_feedback_messages.rb │ ├── 20110421060544_cleanup.rb │ ├── 20110423184218_add_legacy_to_comments.rb │ ├── 20110503025228_add_file_sizes_to_episodes.rb │ ├── 20110504180955_add_hidden_to_comments.rb │ ├── 20110630221611_create_versions.rb │ ├── 20110630234413_add_moderator_to_users.rb │ ├── 20110701001805_add_banned_at_to_users.rb │ ├── 20110701025738_remove_hidden_from_comments.rb │ └── 20110725215614_add_email_on_reply_to_users.rb ├── schema.rb └── seeds.rb ├── lib ├── code_formatter.rb └── tasks │ ├── .gitkeep │ └── application.rake ├── log └── .gitignore ├── public ├── 404.html ├── 422.html ├── 500.html ├── favicon.ico ├── flash │ └── clippy.swf ├── images │ ├── give_back.jpg │ ├── give_back_banner.png │ ├── guest.png │ ├── icons │ │ ├── asciicasts.png │ │ ├── browse_code.png │ │ ├── comments.png │ │ ├── facebook.png │ │ ├── itunes.png │ │ ├── new_comment.png │ │ ├── rss.png │ │ ├── show_notes.png │ │ └── twitter.png │ ├── ipod_railscasts_cover.jpg │ ├── logo.png │ ├── progress_large.gif │ ├── quicktime.gif │ ├── railscasts_cover.jpg │ ├── railscasts_logo.png │ ├── railsrumble.png │ ├── ryan_bates.jpg │ ├── sublimevideo.png │ └── views │ │ ├── full.png │ │ ├── grid.png │ │ └── list.png ├── javascripts │ ├── application.js │ ├── jquery.js │ ├── jquery.min.js │ └── rails.js ├── robots.txt └── stylesheets │ ├── .gitkeep │ ├── application.css │ ├── coderay.css │ └── feeds.css ├── script ├── rails └── setup ├── spec ├── factories.rb ├── helpers │ └── comments_helper_spec.rb ├── lib │ └── code_formatter_spec.rb ├── mailers │ └── mailer_spec.rb ├── models │ ├── ability_spec.rb │ ├── comment_spec.rb │ ├── episode_spec.rb │ ├── feedback_message_spec.rb │ ├── tag_spec.rb │ ├── tagging_spec.rb │ └── user_spec.rb ├── requests │ ├── comments_request_spec.rb │ ├── episodes_request_spec.rb │ ├── feedback_messages_request_spec.rb │ ├── info_request_spec.rb │ └── users_request_spec.rb ├── spec_helper.rb └── support │ ├── auth_macros.rb │ └── mailer_macros.rb ├── tmp └── .gitignore └── vendor ├── .gitignore └── plugins └── .gitkeep /.gitignore: -------------------------------------------------------------------------------- 1 | .bundle 2 | db/*.sqlite3 3 | log/*.log 4 | log/*.pid 5 | tmp/**/* 6 | tmp/* 7 | coverage/* 8 | config/database.yml 9 | config/app_config.yml 10 | config/*.sphinx.conf 11 | config/initializers/development_mail.rb 12 | db/sphinx 13 | public/assets 14 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --colour 2 | -------------------------------------------------------------------------------- /.rvmrc: -------------------------------------------------------------------------------- 1 | rvm use 1.9.2@railscasts --create 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | before_script: 2 | - "script/setup" 3 | 4 | rvm: 5 | - 1.9.2 6 | -------------------------------------------------------------------------------- /Capfile: -------------------------------------------------------------------------------- 1 | load 'deploy' if respond_to?(:namespace) # cap2 differentiator 2 | Dir['vendor/plugins/*/recipes/*.rb'].each { |plugin| load(plugin) } 3 | load 'config/deploy' -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'http://rubygems.org' 2 | 3 | gem "rails", "3.0.10" 4 | gem "mysql2" 5 | gem "redcarpet" 6 | gem "coderay" 7 | gem "thinking-sphinx", ">= 2.0.1", :require => "thinking_sphinx" 8 | gem "whenever", :require => false 9 | gem "will_paginate", ">= 3.0.pre2" 10 | gem "jquery-rails" 11 | gem "omniauth", ">= 0.2.2" 12 | gem "exception_notification", :git => "git://github.com/rails/exception_notification.git", :require => "exception_notifier" 13 | gem "ancestry" 14 | gem "cancan", :git => "git://github.com/ryanb/cancan.git", :branch => "2.0" 15 | gem "paper_trail" 16 | 17 | group :development, :test do 18 | gem "rspec-rails" 19 | gem "launchy" 20 | end 21 | 22 | group :test do 23 | gem "factory_girl_rails" 24 | gem "capybara" 25 | gem "database_cleaner" 26 | gem "guard" 27 | gem "guard-rspec" 28 | gem "fakeweb" 29 | gem "simplecov", :require => false 30 | end 31 | 32 | group :development do 33 | gem "thin" 34 | gem "nifty-generators" 35 | gem "capistrano" 36 | end 37 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GIT 2 | remote: git://github.com/rails/exception_notification.git 3 | revision: 192a49a02d63d28b23ed41cebadfedd490929cf1 4 | specs: 5 | exception_notification (1.0.0) 6 | 7 | GIT 8 | remote: git://github.com/ryanb/cancan.git 9 | revision: 63865cc7d8df9ea080e7fb1adf6ca8eeb1719ee9 10 | branch: 2.0 11 | specs: 12 | cancan (1.6.3) 13 | 14 | GEM 15 | remote: http://rubygems.org/ 16 | specs: 17 | aaronh-chronic (0.3.9) 18 | abstract (1.0.0) 19 | actionmailer (3.0.10) 20 | actionpack (= 3.0.10) 21 | mail (~> 2.2.19) 22 | actionpack (3.0.10) 23 | activemodel (= 3.0.10) 24 | activesupport (= 3.0.10) 25 | builder (~> 2.1.2) 26 | erubis (~> 2.6.6) 27 | i18n (~> 0.5.0) 28 | rack (~> 1.2.1) 29 | rack-mount (~> 0.6.14) 30 | rack-test (~> 0.5.7) 31 | tzinfo (~> 0.3.23) 32 | activemodel (3.0.10) 33 | activesupport (= 3.0.10) 34 | builder (~> 2.1.2) 35 | i18n (~> 0.5.0) 36 | activerecord (3.0.10) 37 | activemodel (= 3.0.10) 38 | activesupport (= 3.0.10) 39 | arel (~> 2.0.10) 40 | tzinfo (~> 0.3.23) 41 | activeresource (3.0.10) 42 | activemodel (= 3.0.10) 43 | activesupport (= 3.0.10) 44 | activesupport (3.0.10) 45 | addressable (2.2.5) 46 | ancestry (1.2.3) 47 | activerecord (>= 2.2.2) 48 | arel (2.0.10) 49 | builder (2.1.2) 50 | capistrano (2.6.0) 51 | highline 52 | net-scp (>= 1.0.0) 53 | net-sftp (>= 2.0.0) 54 | net-ssh (>= 2.0.14) 55 | net-ssh-gateway (>= 1.1.0) 56 | capybara (1.0.0) 57 | mime-types (>= 1.16) 58 | nokogiri (>= 1.3.3) 59 | rack (>= 1.0.0) 60 | rack-test (>= 0.5.4) 61 | selenium-webdriver (~> 0.2.0) 62 | xpath (~> 0.1.4) 63 | childprocess (0.2.0) 64 | ffi (~> 1.0.6) 65 | coderay (0.9.5) 66 | configuration (1.2.0) 67 | daemons (1.1.2) 68 | database_cleaner (0.6.7) 69 | diff-lcs (1.1.2) 70 | erubis (2.6.6) 71 | abstract (>= 1.0.0) 72 | eventmachine (0.12.10) 73 | factory_girl (1.3.2) 74 | factory_girl_rails (1.0) 75 | factory_girl (~> 1.3) 76 | rails (>= 3.0.0.beta4) 77 | fakeweb (1.3.0) 78 | faraday (0.6.1) 79 | addressable (~> 2.2.4) 80 | multipart-post (~> 1.1.0) 81 | rack (>= 1.1.0, < 2) 82 | ffi (1.0.9) 83 | guard (0.3.4) 84 | thor (~> 0.14.6) 85 | guard-rspec (0.3.1) 86 | guard (>= 0.2.2) 87 | highline (1.6.1) 88 | i18n (0.5.0) 89 | jquery-rails (0.2.5) 90 | rails (~> 3.0) 91 | thor (~> 0.14.4) 92 | json_pure (1.5.3) 93 | launchy (0.4.0) 94 | configuration (>= 0.0.5) 95 | rake (>= 0.8.1) 96 | mail (2.2.19) 97 | activesupport (>= 2.3.6) 98 | i18n (>= 0.4.0) 99 | mime-types (~> 1.16) 100 | treetop (~> 1.4.8) 101 | mime-types (1.16) 102 | multi_json (0.0.5) 103 | multipart-post (1.1.0) 104 | mysql2 (0.2.6) 105 | net-ldap (0.1.1) 106 | net-scp (1.0.4) 107 | net-ssh (>= 1.99.1) 108 | net-sftp (2.0.5) 109 | net-ssh (>= 2.0.9) 110 | net-ssh (2.1.4) 111 | net-ssh-gateway (1.1.0) 112 | net-ssh (>= 1.99.1) 113 | nifty-generators (0.4.2) 114 | nokogiri (1.4.7) 115 | oa-basic (0.2.2) 116 | multi_json (~> 0.0.2) 117 | nokogiri (~> 1.4.2) 118 | oa-core (= 0.2.2) 119 | rest-client (~> 1.6.0) 120 | oa-core (0.2.2) 121 | rack (~> 1.1) 122 | oa-enterprise (0.2.2) 123 | net-ldap (~> 0.1.1) 124 | nokogiri (~> 1.4.2) 125 | oa-core (= 0.2.2) 126 | pyu-ruby-sasl (~> 0.0.3.1) 127 | rubyntlm (~> 0.1.1) 128 | oa-more (0.2.2) 129 | multi_json (~> 0.0.2) 130 | oa-core (= 0.2.2) 131 | rest-client (~> 1.6.0) 132 | oa-oauth (0.2.2) 133 | faraday (~> 0.6.1) 134 | multi_json (~> 0.0.2) 135 | nokogiri (~> 1.4.2) 136 | oa-core (= 0.2.2) 137 | oauth (~> 0.4.0) 138 | oauth2 (~> 0.3.0) 139 | oa-openid (0.2.2) 140 | oa-core (= 0.2.2) 141 | rack-openid (~> 1.2.0) 142 | ruby-openid-apps-discovery 143 | oauth (0.4.4) 144 | oauth2 (0.3.0) 145 | faraday (~> 0.6.0) 146 | multi_json (~> 0.0.4) 147 | omniauth (0.2.2) 148 | oa-basic (= 0.2.2) 149 | oa-core (= 0.2.2) 150 | oa-enterprise (= 0.2.2) 151 | oa-more (= 0.2.2) 152 | oa-oauth (= 0.2.2) 153 | oa-openid (= 0.2.2) 154 | paper_trail (2.2.5) 155 | rails (~> 3) 156 | polyglot (0.3.2) 157 | pyu-ruby-sasl (0.0.3.2) 158 | rack (1.2.3) 159 | rack-mount (0.6.14) 160 | rack (>= 1.0.0) 161 | rack-openid (1.2.0) 162 | rack (>= 1.1.0) 163 | ruby-openid (>= 2.1.8) 164 | rack-test (0.5.7) 165 | rack (>= 1.0) 166 | rails (3.0.10) 167 | actionmailer (= 3.0.10) 168 | actionpack (= 3.0.10) 169 | activerecord (= 3.0.10) 170 | activeresource (= 3.0.10) 171 | activesupport (= 3.0.10) 172 | bundler (~> 1.0) 173 | railties (= 3.0.10) 174 | railties (3.0.10) 175 | actionpack (= 3.0.10) 176 | activesupport (= 3.0.10) 177 | rake (>= 0.8.7) 178 | rdoc (~> 3.4) 179 | thor (~> 0.14.4) 180 | rake (0.9.2) 181 | rdoc (3.9.4) 182 | redcarpet (1.17.2) 183 | rest-client (1.6.1) 184 | mime-types (>= 1.16) 185 | riddle (1.2.1) 186 | rspec (2.6.0) 187 | rspec-core (~> 2.6.0) 188 | rspec-expectations (~> 2.6.0) 189 | rspec-mocks (~> 2.6.0) 190 | rspec-core (2.6.4) 191 | rspec-expectations (2.6.0) 192 | diff-lcs (~> 1.1.2) 193 | rspec-mocks (2.6.0) 194 | rspec-rails (2.6.1) 195 | actionpack (~> 3.0) 196 | activesupport (~> 3.0) 197 | railties (~> 3.0) 198 | rspec (~> 2.6.0) 199 | ruby-openid (2.1.8) 200 | ruby-openid-apps-discovery (1.2.0) 201 | ruby-openid (>= 2.1.7) 202 | rubyntlm (0.1.1) 203 | rubyzip (0.9.4) 204 | selenium-webdriver (0.2.2) 205 | childprocess (>= 0.1.9) 206 | ffi (>= 1.0.7) 207 | json_pure 208 | rubyzip 209 | simplecov (0.4.2) 210 | simplecov-html (~> 0.4.4) 211 | simplecov-html (0.4.4) 212 | thin (1.2.11) 213 | daemons (>= 1.0.9) 214 | eventmachine (>= 0.12.6) 215 | rack (>= 1.0.0) 216 | thinking-sphinx (2.0.1) 217 | activerecord (>= 3.0.3) 218 | riddle (>= 1.2.1) 219 | thor (0.14.6) 220 | treetop (1.4.10) 221 | polyglot 222 | polyglot (>= 0.3.1) 223 | tzinfo (0.3.29) 224 | whenever (0.6.2) 225 | aaronh-chronic (>= 0.3.9) 226 | activesupport (>= 2.3.4) 227 | will_paginate (3.0.pre2) 228 | xpath (0.1.4) 229 | nokogiri (~> 1.3) 230 | 231 | PLATFORMS 232 | ruby 233 | 234 | DEPENDENCIES 235 | ancestry 236 | cancan! 237 | capistrano 238 | capybara 239 | coderay 240 | database_cleaner 241 | exception_notification! 242 | factory_girl_rails 243 | fakeweb 244 | guard 245 | guard-rspec 246 | jquery-rails 247 | launchy 248 | mysql2 249 | nifty-generators 250 | omniauth (>= 0.2.2) 251 | paper_trail 252 | rails (= 3.0.10) 253 | redcarpet 254 | rspec-rails 255 | simplecov 256 | thin 257 | thinking-sphinx (>= 2.0.1) 258 | whenever 259 | will_paginate (>= 3.0.pre2) 260 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | # A sample Guardfile 2 | # More info at http://github.com/guard/guard#readme 3 | 4 | 5 | guard 'rspec', :version => 2, :notify => false do 6 | watch(%r{^spec/.+_spec\.rb}) 7 | watch(%r{^lib/(.+)\.rb}) { |m| "spec/lib/#{m[1]}_spec.rb" } 8 | watch('spec/spec_helper.rb') { "spec" } 9 | 10 | # Rails example 11 | watch('spec/spec_helper.rb') { "spec" } 12 | # watch('config/routes.rb') { "spec/routing" } 13 | watch('app/controllers/application_controller.rb') { "spec/controllers" } 14 | watch(%r{^spec/.+_spec\.rb}) 15 | watch(%r{^app/(.+)\.rb}) { |m| "spec/#{m[1]}_spec.rb" } 16 | watch(%r{^lib/(.+)\.rb}) { |m| "spec/lib/#{m[1]}_spec.rb" } 17 | watch(%r{^app/controllers/(.+)_(controller)\.rb}) { |m| ["spec/routing/#{m[1]}_routing_spec.rb", "spec/#{m[2]}s/#{m[1]}_#{m[2]}_spec.rb", "spec/requests/#{m[1]}_request_spec.rb"] } 18 | watch(%r{^app/views/(.+)/.+}) { |m| ["spec/requests/#{m[1]}_request_spec.rb"] } 19 | watch('spec/factories.rb') { "spec/models" } 20 | end 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Ryan Bates, RailsCasts 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | # RailsCasts 2 | 3 | This is the source code for the [RailsCasts site](http://railscasts.com/). If you want the source code for the episodes, see the [railscasts-episodes](http://github.com/ryanb/railscasts-episodes) project. 4 | 5 | Please [let me know](http://railscasts.com/feedback) if you plan to use this app for your site. 6 | 7 | **IMPORTANT:** This source code is out of date with the latest changes at railscasts.com to ensure the security of the payment processing. I hope to open-source the latest additions at some point. 8 | 9 | 10 | ## Setup 11 | 12 | This is designed to run on Ruby 1.9.2 or higher. If you're using [RVM](http://rvm.beginrescueend.com/) it should automatically switch to 1.9.2 when entering the directory. 13 | 14 | Run `script/setup`. This will generate the config files, install gems, and migrate the database. 15 | 16 | You can then start the server with `rails s` and run the specs with `rake`. 17 | 18 | You may want to install [Sphinx](http://sphinxsearch.com/), run the index and start rake commands, and set `thinking_sphinx: true` in `app_config.yml`. This isn't required since it will default to a primitive search. 19 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require File.expand_path('../config/application', __FILE__) 5 | require 'rake' 6 | 7 | desc "Run tests with coverage" 8 | task :coverage do 9 | ENV['COVERAGE'] = "true" 10 | Rake::Task["spec"].execute 11 | Launchy.open("file://" + File.expand_path("../coverage/index.html", __FILE__)) 12 | end 13 | 14 | Railscasts::Application.load_tasks 15 | -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | protect_from_forgery 3 | enable_authorization do |exception| 4 | redirect_to root_url, :alert => exception.message 5 | end 6 | 7 | protected 8 | 9 | # overrides ActionController::RequestForgeryProtection#handle_unverified_request 10 | def handle_unverified_request 11 | super 12 | cookies.delete(:token) 13 | end 14 | 15 | private 16 | 17 | def user_for_paper_trail 18 | current_user && current_user.id 19 | end 20 | 21 | def current_user 22 | @current_user ||= User.find_by_token(cookies[:token]) if cookies[:token] 23 | end 24 | helper_method :current_user 25 | 26 | def redirect_to_target_or_default(default, *options) 27 | redirect_to(session[:return_to] || default, *options) 28 | session[:return_to] = nil 29 | end 30 | 31 | def store_target_location 32 | session[:return_to] = request.url 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /app/controllers/comments_controller.rb: -------------------------------------------------------------------------------- 1 | class CommentsController < ApplicationController 2 | load_and_authorize_resource 3 | 4 | def index 5 | @comments = @comments.search(params[:comment_search]).recent.paginate(:page => params[:page], :per_page => 50) 6 | end 7 | 8 | def new 9 | @comment = Comment.new(:parent_id => params[:parent_id], :episode_id => params[:episode_id], :user => current_user) 10 | end 11 | 12 | def create 13 | @comment = current_user.comments.build(params[:comment]) 14 | @comment.request = request 15 | @comment.save 16 | respond_to do |format| 17 | format.html do 18 | if @comment.errors.present? 19 | render :new 20 | else 21 | @comment.notify_other_commenters 22 | redirect_to(episode_path(@comment.episode, :view => "comments")) 23 | end 24 | end 25 | format.js 26 | end 27 | end 28 | 29 | def edit 30 | end 31 | 32 | def update 33 | @comment.update_attributes(params[:comment]) 34 | respond_to do |format| 35 | format.html do 36 | if @comment.errors.present? 37 | render :edit 38 | else 39 | redirect_to(episode_path(@comment.episode, :view => "comments")) 40 | end 41 | end 42 | format.js 43 | end 44 | end 45 | 46 | def destroy 47 | @comment.destroy 48 | flash[:notice] = "Deleted comment. #{undo_link}" 49 | respond_to do |format| 50 | format.html { redirect_to episode_path(@comment.episode, :view => "comments") } 51 | format.js 52 | end 53 | end 54 | 55 | private 56 | 57 | def undo_link 58 | if can? :revert, :versions 59 | version = @comment.versions.scoped.last 60 | view_context.link_to("undo", revert_version_path(version), :method => :post) if can? :revert, version 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /app/controllers/episodes_controller.rb: -------------------------------------------------------------------------------- 1 | class EpisodesController < ApplicationController 2 | load_and_authorize_resource :find_by => :param 3 | 4 | def index 5 | @tag = Tag.find(params[:tag_id]) if params[:tag_id] 6 | if params[:search].blank? 7 | @episodes = (@tag ? @tag.episodes : Episode).accessible_by(current_ability).recent 8 | else 9 | @episodes = Episode.search_published(params[:search], params[:tag_id]) 10 | end 11 | respond_to do |format| 12 | format.html { @episodes = @episodes.paginate(:page => params[:page], :per_page => episodes_per_page) } 13 | format.rss 14 | end 15 | end 16 | 17 | def show 18 | if params[:id] != @episode.to_param 19 | headers["Status"] = "301 Moved Permanently" 20 | redirect_to episode_url(@episode) 21 | else 22 | @comment = Comment.new(:episode => @episode, :user => current_user) 23 | end 24 | end 25 | 26 | def new 27 | @episode.position = Episode.maximum(:position).to_i + 1 28 | end 29 | 30 | def create 31 | @episode.load_file_sizes 32 | if @episode.save 33 | redirect_to @episode, :notice => "Successfully created episode." 34 | else 35 | render :new 36 | end 37 | end 38 | 39 | def edit 40 | end 41 | 42 | def update 43 | @episode.load_file_sizes 44 | if @episode.update_attributes(params[:episode]) 45 | redirect_to @episode, :notice => "Successfully updated episode." 46 | else 47 | render :edit 48 | end 49 | end 50 | 51 | private 52 | 53 | def episodes_per_page 54 | case params[:view] 55 | when "list" then 40 56 | when "grid" then 24 57 | else 10 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /app/controllers/feedback_messages_controller.rb: -------------------------------------------------------------------------------- 1 | class FeedbackMessagesController < ApplicationController 2 | def new 3 | @feedback_message = FeedbackMessage.new 4 | if current_user 5 | @feedback_message.name = current_user.name 6 | @feedback_message.email = current_user.email 7 | end 8 | end 9 | 10 | def create 11 | if params[:email].present? 12 | redirect_to root_url, :notice => "Your feedback message was caught by the spam filter because you filled in the invisible email field. Please try again without filling in the false email field and let me know that this happened." 13 | else 14 | @feedback_message = FeedbackMessage.new(params[:feedback_message]) 15 | if @feedback_message.save 16 | Mailer.feedback(@feedback_message).deliver 17 | redirect_to root_url, :notice => "Thank you for the feedback." 18 | else 19 | render :new 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /app/controllers/info_controller.rb: -------------------------------------------------------------------------------- 1 | class InfoController < ApplicationController 2 | def about 3 | end 4 | 5 | def give_back 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/controllers/users_controller.rb: -------------------------------------------------------------------------------- 1 | class UsersController < ApplicationController 2 | before_filter :load_current_user, :only => [:edit, :update] 3 | load_and_authorize_resource 4 | 5 | def show 6 | end 7 | 8 | def create 9 | omniauth = request.env["omniauth.auth"] 10 | logger.info omniauth.inspect 11 | @user = User.find_by_github_uid(omniauth["uid"]) || User.create_from_omniauth(omniauth) 12 | cookies.permanent[:token] = @user.token 13 | redirect_to_target_or_default root_url, :notice => "Signed in successfully" 14 | end 15 | 16 | def edit 17 | end 18 | 19 | def update 20 | @user.attributes = params[:user] 21 | @user.save! 22 | redirect_to @user, :notice => "Successfully updated profile." 23 | end 24 | 25 | def login 26 | session[:return_to] = params[:return_to] if params[:return_to] 27 | if Rails.env.development? 28 | cookies.permanent[:token] = User.first.token 29 | redirect_to_target_or_default root_url, :notice => "Signed in successfully" 30 | else 31 | redirect_to "/auth/github" 32 | end 33 | end 34 | 35 | def logout 36 | cookies.delete(:token) 37 | redirect_to root_url, :notice => "You have been logged out." 38 | end 39 | 40 | def ban 41 | @user = User.find(params[:id]) 42 | @user.update_attribute(:banned_at, Time.now) 43 | @comments = @user.comments 44 | @comments.each(&:destroy) 45 | respond_to do |format| 46 | format.html { redirect_to :back, :notice => "User #{@user.name} has been banned." } 47 | format.js 48 | end 49 | end 50 | 51 | def unsubscribe 52 | @user = User.find_by_unsubscribe_token!(params[:token]) 53 | @user.update_attributes!(:email_on_reply => false) 54 | redirect_to root_url, :notice => "You have been unsubscribed from further email notifications." 55 | end 56 | 57 | private 58 | 59 | def load_current_user 60 | @user = current_user 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /app/controllers/versions_controller.rb: -------------------------------------------------------------------------------- 1 | class VersionsController < ApplicationController 2 | def revert 3 | @version = Version.find(params[:id]) 4 | if @version.reify 5 | @version.reify.save! 6 | else 7 | @version.item.destroy 8 | end 9 | redirect_to :back, :notice => "Undid #{@version.event}." 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | require "builder" 2 | 3 | module ApplicationHelper 4 | def textilize(text) 5 | CodeFormatter.new(text).to_html.html_safe unless text.blank? 6 | end 7 | 8 | def tab_link(name, url) 9 | selected = url.all? { |key, value| params[key] == value } 10 | link_to(name, url, :class => (selected ? "selected tab" : "tab")) 11 | end 12 | 13 | def avatar_url(comment_or_user, size = 64) 14 | default_url = "#{root_url}images/guest.png" 15 | token = gravatar_token(comment_or_user) 16 | if token.present? 17 | "http://gravatar.com/avatar/#{gravatar_token(comment_or_user)}.png?s=#{size}&d=#{CGI.escape(default_url)}" 18 | else 19 | default_url 20 | end 21 | end 22 | 23 | # TODO refactor me into comment/user class 24 | def gravatar_token(comment_or_user) 25 | case comment_or_user 26 | when Comment 27 | token = gravatar_token(comment_or_user.user) 28 | if token.present? 29 | token 30 | elsif comment_or_user.email.present? 31 | Digest::MD5.hexdigest(comment_or_user.email.downcase) 32 | end 33 | when User 34 | if comment_or_user.gravatar_token.present? 35 | comment_or_user.gravatar_token 36 | elsif comment_or_user.email.present? 37 | Digest::MD5.hexdigest(comment_or_user.email.downcase) 38 | end 39 | else nil 40 | end 41 | end 42 | 43 | def video_tag(path, options = {}) 44 | xml = Builder::XmlMarkup.new 45 | xml.video :width => options[:width], :height => options[:height], :poster => options[:poster], :controls => "controls", :preload => "none" do 46 | xml.source :src => "#{path}.mp4", :type => "video/mp4" 47 | xml.source :src => "#{path}.m4v", :type => "video/mp4" 48 | xml.source :src => "#{path}.webm", :type => "video/webm" 49 | xml.source :src => "#{path}.ogv", :type => "video/ogg" 50 | end.html_safe 51 | end 52 | 53 | def field(f, attribute, options = {}) 54 | return if f.object.new_record? && cannot?(:create, f.object, attribute) 55 | return if !f.object.new_record? && cannot?(:update, f.object, attribute) 56 | label_name = options.delete(:label) 57 | type = options.delete(:type) || :text_field 58 | content_tag(:div, (f.label(attribute, label_name) + f.send(type, attribute, options)), :class => "field") 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /app/helpers/comments_helper.rb: -------------------------------------------------------------------------------- 1 | module CommentsHelper 2 | def format_comment(comment) 3 | if comment.legacy? 4 | simple_format(keep_spaces_at_beginning(h(comment.content))) 5 | else 6 | CodeFormatter.new(comment.content).to_html.html_safe 7 | end 8 | end 9 | 10 | def keep_spaces_at_beginning(content) 11 | content.split("\n").map do |line| 12 | line.sub(/^ +/) do |spaces| 13 | ' ' * spaces.length 14 | end 15 | end.join("\n") 16 | end 17 | 18 | def fix_url(url) 19 | if url =~ /^https?\:\/\// 20 | url 21 | else 22 | "http://#{url}" 23 | end 24 | end 25 | 26 | def nested_comments(comments) 27 | comments.map do |comment, sub_comments| 28 | render(comment) + content_tag(:div, nested_comments(sub_comments), :class => "nested_comments") 29 | end.join.html_safe 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /app/helpers/episodes_helper.rb: -------------------------------------------------------------------------------- 1 | module EpisodesHelper 2 | def episode_video_tag(episode) 3 | video_tag episode.asset_url("videos"), :poster => "/assets/episodes/posters/loading#{800 if episode.legacy?}.png", :width => (episode.legacy? ? 800 : 960), :height => 600 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/helpers/error_messages_helper.rb: -------------------------------------------------------------------------------- 1 | module ErrorMessagesHelper 2 | # Render error messages for the given objects. The :message and :header_message options are allowed. 3 | def error_messages_for(*objects) 4 | options = objects.extract_options! 5 | options[:header_message] ||= "Invalid Fields" 6 | options[:message] ||= "Correct the following errors and try again." 7 | messages = objects.compact.map { |o| o.errors.full_messages }.flatten 8 | unless messages.empty? 9 | content_tag(:div, :class => "error_messages") do 10 | list_items = messages.map { |msg| content_tag(:li, msg) } 11 | content_tag(:h2, options[:header_message]) + content_tag(:p, options[:message]) + content_tag(:ul, list_items.join.html_safe) 12 | end 13 | end 14 | end 15 | 16 | module FormBuilderAdditions 17 | def error_messages(options = {}) 18 | @template.error_messages_for(@object, options) 19 | end 20 | end 21 | end 22 | 23 | ActionView::Helpers::FormBuilder.send(:include, ErrorMessagesHelper::FormBuilderAdditions) 24 | -------------------------------------------------------------------------------- /app/helpers/feedback_messages_helper.rb: -------------------------------------------------------------------------------- 1 | module FeedbackMessagesHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/info_helper.rb: -------------------------------------------------------------------------------- 1 | module InfoHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/layout_helper.rb: -------------------------------------------------------------------------------- 1 | module LayoutHelper 2 | def title(page_title, show_title = true) 3 | content_for(:title) { h(page_title.to_s) } 4 | @show_title = show_title 5 | end 6 | 7 | def show_title? 8 | @show_title 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /app/helpers/users_helper.rb: -------------------------------------------------------------------------------- 1 | module UsersHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/versions_helper.rb: -------------------------------------------------------------------------------- 1 | module VersionsHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/mailers/mailer.rb: -------------------------------------------------------------------------------- 1 | class Mailer < ActionMailer::Base 2 | def feedback(message) 3 | @message = message 4 | mail :to => "feedback@railscasts.com", :from => @message.email, :subject => "RailsCasts Feedback from #{@message.name}" 5 | end 6 | 7 | def comment_response(comment, user) 8 | @comment = comment 9 | @user = user 10 | mail :to => @user.email, :from => "noreply@railscasts.com", :subject => "Comment Response on RailsCasts" 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /app/models/ability.rb: -------------------------------------------------------------------------------- 1 | class Ability 2 | include CanCan::Ability 3 | 4 | def initialize(user) 5 | can :read, :episodes, ["published_at <= ?", Time.zone.now] do |episode| 6 | episode.published_at <= Time.now.utc 7 | end 8 | can :access, :info 9 | can :create, :feedback_messages 10 | can [:read, :create, :login, :unsubscribe], :users 11 | 12 | if user 13 | can :logout, :users 14 | can :update, :users, :id => user.id 15 | unless user.banned? 16 | can :create, :comments 17 | can [:update, :destroy], :comments do |comment| 18 | comment.created_at >= 15.minutes.ago && comment.user_id == user.id 19 | end 20 | end 21 | 22 | if user.moderator? 23 | can :read, :episodes 24 | can :update, :episodes, :notes 25 | can [:update, :destroy, :index], :comments 26 | can :ban, :users 27 | can :revert, :versions 28 | end 29 | 30 | if user.admin? 31 | can :access, :all 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /app/models/comment.rb: -------------------------------------------------------------------------------- 1 | class Comment < ActiveRecord::Base 2 | belongs_to :episode, :counter_cache => true 3 | belongs_to :user 4 | 5 | validates_presence_of :content, :episode_id 6 | 7 | scope :recent, order("created_at DESC") 8 | 9 | has_paper_trail 10 | has_ancestry 11 | 12 | def self.search(query) 13 | if query.blank? 14 | scoped 15 | else 16 | conditions = %w[content name email site_url].map { |c| "comments.#{c} like :query" } 17 | where(conditions.join(" or "), :query => "%#{query}%") 18 | end 19 | end 20 | 21 | def request=(request) 22 | self.user_ip = request.remote_ip 23 | self.user_agent = request.env['HTTP_USER_AGENT'] 24 | self.referrer = request.env['HTTP_REFERER'] 25 | end 26 | 27 | def notify_other_commenters 28 | users_to_notify.each do |user| 29 | Mailer.comment_response(self, user).deliver 30 | end 31 | end 32 | 33 | def users_to_notify 34 | ancestors.map(&:user).compact.select { |u| u.email.present? && u.email_on_reply? && u != user } 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /app/models/episode.rb: -------------------------------------------------------------------------------- 1 | class Episode < ActiveRecord::Base 2 | has_many :comments, :dependent => :destroy 3 | has_many :taggings, :dependent => :destroy 4 | has_many :tags, :through => :taggings 5 | 6 | has_paper_trail 7 | 8 | scope :published, lambda { where('published_at <= ?', Time.now.utc) } 9 | scope :unpublished, lambda { where('published_at > ?', Time.now.utc) } 10 | scope :tagged, lambda { |tag_id| tag_id ? joins(:taggings).where(:taggings => {:tag_id => tag_id}) : scoped } 11 | scope :recent, order('position DESC') 12 | 13 | validates_presence_of :published_at, :name 14 | serialize :file_sizes 15 | 16 | before_create :set_permalink 17 | 18 | # sometimes ThinkingSphinx isn't loaded for rake tasks 19 | if respond_to? :define_index 20 | define_index do 21 | indexes :name 22 | indexes position, :sortable => true 23 | indexes description 24 | indexes notes 25 | indexes comments.content, :as => :comment_content 26 | indexes tags(:name), :as => :tag_names 27 | 28 | has published_at 29 | has taggings.tag_id, :as => :tag_ids 30 | end 31 | end 32 | 33 | def self.search_published(query, tag_id = nil) 34 | if APP_CONFIG['thinking_sphinx'] 35 | with = tag_id ? {:tag_ids => tag_id.to_i} : {} 36 | search(query, :conditions => { :published_at => 0..Time.now.utc.to_i }, :with => with, 37 | :field_weights => { :name => 20, :description => 15, :notes => 5, :tag_names => 10 }) 38 | else 39 | published.primitive_search(query) 40 | end 41 | rescue ThinkingSphinx::ConnectionError => e 42 | APP_CONFIG['thinking_sphinx'] = false 43 | raise e 44 | end 45 | 46 | def self.primitive_search(query, join = "AND") 47 | where(primitive_search_conditions(query, join)) 48 | end 49 | 50 | def similar_episodes 51 | if APP_CONFIG['thinking_sphinx'] 52 | self.class.search("#{name} #{tag_names}", :without_ids => [id], 53 | :conditions => { :published_at => 0..Time.now.utc.to_i }, 54 | :match_mode => :any, :page => 1, :per_page => 5, 55 | :field_weights => { :name => 20, :description => 15, :notes => 5, :tag_names => 10 }) 56 | else 57 | self.class.published.limit(5).primitive_search(name, "OR") 58 | end 59 | rescue ThinkingSphinx::ConnectionError => e 60 | APP_CONFIG['thinking_sphinx'] = false 61 | raise e 62 | end 63 | 64 | def full_name 65 | "\##{position} #{name}" 66 | end 67 | 68 | def tag_names=(names) 69 | self.tags = Tag.with_names(names.split(/\s+/)) 70 | end 71 | 72 | def tag_names 73 | tags.map(&:name).join(' ') 74 | end 75 | 76 | def to_param 77 | [position, permalink].join('-') 78 | end 79 | 80 | def asset_name 81 | [padded_position, permalink].join('-') 82 | end 83 | 84 | def asset_url(path, ext = nil) 85 | "http://media.railscasts.com/assets/episodes/#{path}/#{asset_name}" + (ext ? ".#{ext}" : "") 86 | end 87 | 88 | def padded_position 89 | position.to_s.rjust(3, "0") 90 | end 91 | 92 | def last_published? 93 | self == self.class.published.last 94 | end 95 | 96 | def published? 97 | published_at <= Time.zone.now 98 | end 99 | 100 | def duration 101 | if seconds 102 | min, sec = *seconds.divmod(60) 103 | [min, sec.to_s.rjust(2, '0')].join(':') 104 | end 105 | end 106 | 107 | def duration=(duration) 108 | if duration.present? 109 | min, sec = *duration.split(':').map(&:to_i) 110 | self.seconds = min*60 + sec 111 | end 112 | end 113 | 114 | def self.find_by_param!(param) 115 | find_by_position!(param.to_i) 116 | end 117 | 118 | def files 119 | [ 120 | {:name => "source code", :info => "Project Files in Zip", :url => asset_url("sources", "zip"), :size => file_size("zip")}, 121 | {:name => "mp4", :info => "Full Size H.264 Video", :url => asset_url("videos", "mp4"), :size => file_size("mp4")}, 122 | {:name => "m4v", :info => "Smaller H.264 Video", :url => asset_url("videos", "m4v"), :size => file_size("m4v")}, 123 | {:name => "webm", :info => "Full Size VP8 Video", :url => asset_url("videos", "webm"), :size => file_size("webm")}, 124 | {:name => "ogv", :info => "Full Size Theora Video", :url => asset_url("videos", "ogv"), :size => file_size("ogv")}, 125 | ] 126 | end 127 | 128 | def file_size(ext) 129 | (file_sizes && file_sizes[ext]).to_i 130 | end 131 | 132 | # TODO test me 133 | def available_files 134 | files.select { |f| f[:size].to_i > 0 } 135 | end 136 | 137 | def load_file_sizes 138 | self.file_sizes = {} 139 | files.each do |file| 140 | ext = file[:url][/\w+$/] 141 | self.file_sizes[ext] = fetch_file_size(file[:url]) 142 | end 143 | end 144 | 145 | def fetch_file_size(path) 146 | url = URI.parse(path) 147 | response = Net::HTTP.start(url.host, url.port) do |http| 148 | http.request_head(url.path) 149 | end 150 | if response.code == "200" 151 | response["content-length"] 152 | end 153 | end 154 | 155 | def previous 156 | self.class.where("position < ?", position).order("position desc").first 157 | end 158 | 159 | def next 160 | self.class.where("position > ?", position).order("position").first 161 | end 162 | 163 | private 164 | 165 | def self.primitive_search_conditions(query, join) 166 | query.split(/\s+/).map do |word| 167 | '(' + %w[name description notes].map { |col| "#{col} LIKE #{sanitize('%' + word.to_s + '%')}" }.join(' OR ') + ')' 168 | end.join(" #{join} ") 169 | end 170 | 171 | def set_permalink 172 | self.permalink = name.downcase.gsub(/[^0-9a-z]+/, ' ').strip.gsub(' ', '-') if name 173 | end 174 | end 175 | -------------------------------------------------------------------------------- /app/models/feedback_message.rb: -------------------------------------------------------------------------------- 1 | class FeedbackMessage < ActiveRecord::Base 2 | attr_accessible :name, :email, :content 3 | validates_presence_of :name, :email, :content 4 | end 5 | -------------------------------------------------------------------------------- /app/models/tag.rb: -------------------------------------------------------------------------------- 1 | class Tag < ActiveRecord::Base 2 | has_many :taggings, :dependent => :destroy 3 | has_many :episodes, :through => :taggings 4 | 5 | def self.with_names(names) 6 | names.map do |name| 7 | Tag.find_or_create_by_name(name) 8 | end 9 | end 10 | 11 | def display_name 12 | name.titleize.gsub("E ", "e") 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /app/models/tagging.rb: -------------------------------------------------------------------------------- 1 | class Tagging < ActiveRecord::Base 2 | belongs_to :episode 3 | belongs_to :tag 4 | end 5 | -------------------------------------------------------------------------------- /app/models/user.rb: -------------------------------------------------------------------------------- 1 | class User < ActiveRecord::Base 2 | attr_accessible :name, :email, :site_url, :email_on_reply 3 | before_create { generate_token(:token) } 4 | has_many :comments 5 | has_paper_trail 6 | 7 | def self.create_from_omniauth(omniauth) 8 | User.new.tap do |user| 9 | user.github_uid = omniauth["uid"] 10 | user.github_username = omniauth["user_info"]["nickname"] 11 | user.email = omniauth["user_info"]["email"] 12 | user.name = omniauth["user_info"]["name"] 13 | user.site_url = omniauth["user_info"]["urls"]["Blog"] if omniauth["user_info"]["urls"] 14 | user.gravatar_token = omniauth["extra"]["user_hash"]["gravatar_id"] if omniauth["extra"] && omniauth["extra"]["user_hash"] 15 | user.email_on_reply = true 16 | user.save! 17 | end 18 | end 19 | 20 | def generated_unsubscribe_token 21 | if unsubscribe_token.blank? 22 | generate_token(:unsubscribe_token) 23 | save! 24 | end 25 | unsubscribe_token 26 | end 27 | 28 | def generate_token(column) 29 | begin 30 | self[column] = SecureRandom.urlsafe_base64 31 | end while User.exists?(column => self[column]) 32 | end 33 | 34 | def display_name 35 | name.present? ? name : github_username 36 | end 37 | 38 | def banned? 39 | banned_at 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /app/views/comments/_comment.html.erb: -------------------------------------------------------------------------------- 1 | <%= div_for comment do %> 2 |
<%= image_tag avatar_url(comment), :size => "64x64", :alt => "Avatar" %>
3 |
4 | <%= link_to "Ban User", ban_user_path(comment.user), :confirm => "Are you certain you want to ban this user? It will delete all of his comments and not allow him to comment again.", :remote => true, :method => :put if comment.user && can?(:ban, comment.user) %> 5 |
6 |
7 | <%= render "comments/comment_headline", :comment => comment %> 8 |
9 | <%= format_comment(comment) %> 10 |
11 | <%= link_to "Reply", new_comment_path(:parent_id => comment, :episode_id => comment.episode_id), :remote => true if can?(:create, :comments) %> 12 | <%= link_to "Edit", edit_comment_path(comment), :remote => true if can?(:update, comment) %> 13 | <%= link_to "Delete", comment, :method => :delete, :remote => true if can?(:destroy, comment) %> 14 |
15 |
16 |
17 |
18 | <% end %> 19 | -------------------------------------------------------------------------------- /app/views/comments/_comment_headline.html.erb: -------------------------------------------------------------------------------- 1 |
2 | <% if comment.new_record? %> 3 | New comment as 4 | <% end %> 5 | 6 | <% if comment.user %> 7 | <%= link_to comment.user.display_name, comment.user %> 8 | <% else %> 9 | <%= link_to_unless comment.site_url.blank?, comment.name, fix_url(comment.site_url), :rel => "nofollow" %> 10 | <% end %> 11 | <% if params[:controller] != "episodes" %> 12 | on <%= link_to comment.episode.full_name, episode_path(comment.episode, :view => "comments", :anchor => "comment_#{comment.id}") %> 13 | <% end %> 14 | 15 | <% unless comment.created_at.nil? %> 16 | <%= link_to episode_path(comment.episode, :view => "comments", :anchor => "comment_#{comment.id}"), :class => "created_at" do %> 17 | <% if params[:controller] == "episodes" %> 18 | <%= time_ago_in_words comment.created_at %> ago 19 | <% else %> 20 | <%= comment.created_at.strftime("%Y-%m-%d %H:%M:%I") %> 21 | <% end %> 22 | <% end %> 23 | <% end %> 24 |
25 | -------------------------------------------------------------------------------- /app/views/comments/_form.html.erb: -------------------------------------------------------------------------------- 1 | <% if current_user.nil? %> 2 |

First <%= link_to "sign in through GitHub", login_path(:return_to => request.url) %> to post a comment.

3 | <% elsif cannot? :create, :comments %> 4 |

You have been banned from creating comments. Please <%= link_to "contact me", feedback_path %> if you are a legitimate user.

5 | <% else %> 6 | <%= div_for @comment do %> 7 |
<%= image_tag avatar_url(@comment), :size => "64x64", :alt => "Avatar" %>
8 |
9 | <%= render "comments/comment_headline", :comment => @comment %> 10 |
11 | <%= form_for @comment, :remote => true do |f| %> 12 | <%= f.error_messages %> 13 |
14 | <%= f.hidden_field :episode_id %> 15 | <%= f.hidden_field :parent_id %> 16 |
17 | <% if @comment.user_id && @comment.user_id != current_user.id %> 18 |

Please follow the <%= link_to "moderator guidelines", moderators_path %>.

19 | <% end %> 20 |
21 | Use Markdown for formatting. <%= link_to "See examples.", "javascript:void(0)", :class => "markdown_link" %> 22 | 63 |
64 |
65 | <%= f.text_area :content, :rows => '12', :cols => 65 %> 66 |
67 | <% if @comment.legacy? %> 68 |
69 | <%= f.check_box :legacy %> 70 | <%= f.label :legacy, "Legacy comment (markdown disabled)", :class => "check_box" %> 71 |
72 | <% end %> 73 |
74 | <%= f.submit(@comment.new_record? ? "Post Comment" : "Update Comment") %> 75 |
76 | <% end %> 77 |
78 |
79 |
80 | <% end %> 81 | <% end %> 82 | -------------------------------------------------------------------------------- /app/views/comments/create.js.erb: -------------------------------------------------------------------------------- 1 | <% if @comment.parent_id.present? %> 2 | var container = $("#<%= dom_id(@comment.parent) %>").next(".nested_comments"); 3 | <% else %> 4 | var container = $("#comments"); 5 | <% end %> 6 | <% if @comment.errors.present? %> 7 | container.find("form").prepend("<%= escape_javascript error_messages_for(@comment) %>"); 8 | <% else %> 9 | container.children("#new_comment").replaceWith("<%= escape_javascript render(@comment) %>"); 10 | <% end %> 11 | -------------------------------------------------------------------------------- /app/views/comments/destroy.js.erb: -------------------------------------------------------------------------------- 1 | $("#<%= dom_id(@comment) %>").replaceWith("<%= escape_javascript("

#{flash.delete(:notice)}

".html_safe) %>"); 2 | -------------------------------------------------------------------------------- /app/views/comments/edit.html.erb: -------------------------------------------------------------------------------- 1 | <% title "Edit Comment" %> 2 | 3 |
4 | <%= render "form" %> 5 |
6 | -------------------------------------------------------------------------------- /app/views/comments/edit.js.erb: -------------------------------------------------------------------------------- 1 | $("#<%= dom_id(@comment) %>").replaceWith("<%= escape_javascript render("form") %>"); 2 | $("#<%= dom_id(@comment) %>").find("#comment_content")[0].focus(); 3 | -------------------------------------------------------------------------------- /app/views/comments/index.html.erb: -------------------------------------------------------------------------------- 1 | <% title "Recent Comments" %> 2 | 3 |
4 | <%= form_tag comments_path, :method => :get do %> 5 |

6 | <%= text_field_tag :comment_search, params[:comment_search] %> 7 | <%= submit_tag "Search Comments", :name => nil %> 8 |

9 | <% end %> 10 |
11 | <%= will_paginate :previous_label => h("< Previous Page"), :next_label => h("Next Page >") %> 12 | <%= render @comments %> 13 | <%= will_paginate :previous_label => h("< Previous Page"), :next_label => h("Next Page >") %> 14 |
15 |
16 | -------------------------------------------------------------------------------- /app/views/comments/new.html.erb: -------------------------------------------------------------------------------- 1 | <% title "New Comment" %> 2 | 3 |
4 | <%= render "form" %> 5 |
6 | -------------------------------------------------------------------------------- /app/views/comments/new.js.erb: -------------------------------------------------------------------------------- 1 | if ($("#comment_<%= params[:parent_id] %>").next(".nested_comments").children("#new_comment").length == 0) { 2 | $("#comment_<%= params[:parent_id] %>").next(".nested_comments").prepend("<%= escape_javascript render("form") %>"); 3 | $("#comment_<%= params[:parent_id] %>").next(".nested_comments").find("#comment_content")[0].focus(); 4 | } 5 | -------------------------------------------------------------------------------- /app/views/comments/update.js.erb: -------------------------------------------------------------------------------- 1 | <% if @comment.errors.present? %> 2 | $("#<%= dom_id(@comment) %>").find("form").prepend("<%= escape_javascript error_messages_for(@comment) %>"); 3 | <% else %> 4 | $("#<%= dom_id(@comment) %>").replaceWith("<%= escape_javascript render(@comment) %>"); 5 | <% end %> 6 | -------------------------------------------------------------------------------- /app/views/episodes/_comments.html.erb: -------------------------------------------------------------------------------- 1 |
2 | <%= nested_comments @episode.comments.includes(:user).arrange(:order => "created_at asc") %> 3 | <%= render "comments/form" %> 4 |
5 | -------------------------------------------------------------------------------- /app/views/episodes/_episode.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
<%= link_to image_tag("/assets/episodes/stills/#{episode.asset_name}.png", :size => "200x125", :alt => episode.name), episode %>
3 |
4 |
5 | Episode #<%= episode.position %> – 6 | <%= episode.published_at.strftime('%b %d, %Y') %> 7 | <% unless episode.published? %> 8 | NOT YET RELEASED 9 | <% end %> 10 |
11 |

<%= link_to episode.name, episode %>

12 |
<%= episode.description %>
13 |
14 | <%= link_to "Watch Episode", episode_path(episode, :autoplay => true), :class => "watch_button" %> 15 | (<%= pluralize (episode.seconds/60).round, "minute" %>, <%= link_to pluralize(episode.comments.size, "comment"), episode_path(episode, :view => "comments") %>) 16 |
17 |
18 |
19 |
20 | -------------------------------------------------------------------------------- /app/views/episodes/_form.html.erb: -------------------------------------------------------------------------------- 1 |
2 |

Please follow the <%= link_to "moderator guidelines", moderators_path %>.

3 | <%= form_for @episode do |f| %> 4 | <%= f.error_messages %> 5 | <%= field f, :position %> 6 | <%= field f, :name %> 7 | <%= field f, :tag_names, :label => "Tags" %> 8 | <%= field f, :description, :type => :text_area, :rows => 6 %> 9 | <%= field f, :notes, :type => :text_area, :rows => 20, :cols => 80 %> 10 | <%= field f, :published_at, :type => :date_select, :label => "Publish Date" %> 11 | <%= field f, :duration %> 12 | <%= field f, :asciicasts, :type => :check_box, :label => "Available on ASCIIcasts" %> 13 |
<%= f.submit %>
14 | <% end %> 15 |
16 | -------------------------------------------------------------------------------- /app/views/episodes/_show_notes.html.erb: -------------------------------------------------------------------------------- 1 | <% if can? :edit, @episode %> 2 |
<%= link_to "Edit", edit_episode_path(@episode) %>
3 | <% end %> 4 | 5 |
6 | <% if @episode.asciicasts? %> 7 | 8 | <%= image_tag "icons/asciicasts.png", :size => "12x15", :class => "icon" %> 9 | <%= link_to "Read on ASCIIcasts", "http://asciicasts.com/episodes/#{@episode.to_param}" %> 10 | 11 | <% end %> 12 | <% if @episode.file_sizes && @episode.file_sizes["zip"] %> 13 | 14 | <%= image_tag "icons/browse_code.png", :size => "22x21", :class => "icon" %> 15 | <%= link_to "Browse Source Code", "http://github.com/railscasts/episode-#{@episode.position}" %> 16 | 17 | <% end %> 18 |
19 | 20 |
21 | <%= textilize @episode.notes %> 22 |
23 | -------------------------------------------------------------------------------- /app/views/episodes/_similar.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 | <%= render @episode.similar_episodes %> 4 |
5 |
6 | -------------------------------------------------------------------------------- /app/views/episodes/edit.html.erb: -------------------------------------------------------------------------------- 1 | <% title "Edit Episode" %> 2 | 3 | <%= render :partial => 'form' %> 4 | -------------------------------------------------------------------------------- /app/views/episodes/index.html.erb: -------------------------------------------------------------------------------- 1 | <% title "Ruby on Rails Screencasts", false %> 2 | 3 |
4 |
5 |
6 | View: 7 | <%= link_to_unless params[:view].blank?, image_tag("views/full.png", :size => "15x10"), params.merge(:view => nil, :page => nil), :title => "Full View" %> 8 | <%= link_to_unless params[:view] == "list", image_tag("views/list.png", :size => "14x10"), params.merge(:view => "list", :page => nil), :title => "List View" %> 9 | <%= link_to_unless params[:view] == "grid", image_tag("views/grid.png", :size => "11x10"), params.merge(:view => "grid", :page => nil), :title => "Grid View" %> 10 |
11 |
12 |

Categories

13 |
    14 | <% for tag in Tag.order("name") %> 15 |
  • <%= link_to_unless(params[:tag_id].to_i == tag.id, tag.display_name, params.merge(:tag_id => tag.id, :page => nil)) %>
  • 16 | <% end %> 17 |
18 |
19 | 22 | 38 |
39 | 40 | 41 |
42 | <% if @tag || params[:search].present? %> 43 |
44 | Filtering: 45 | <% if params[:search].present? %> 46 | <%= params[:search] %> <%= link_to "x", params.merge(:search => nil, :page => nil) %> 47 | <% end %> 48 | <% if @tag %> 49 | <%= @tag.display_name %> <%= link_to "x", params.merge(:tag_id => nil, :page => nil) %> 50 | <% end %> 51 |
52 | <% end %> 53 | <% if @episodes.empty? %> 54 |

No episodes found. <%= link_to "See all episodes.", episodes_path %>

55 | <% else %> 56 | <% if params[:view].nil? %> 57 |
58 | <%= render @episodes %> 59 |
60 | <% elsif params[:view] == "list" %> 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | <% for episode in @episodes %> 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | <% end %> 78 |
#NameCommentsDurationReleased
<%= episode.position %><%= link_to episode.name, episode %><%= link_to pluralize(episode.comments.size, "comment"), episode_path(episode, :view => "comments"), :class => "comment_count" %><%= pluralize (episode.seconds/60).round, "minute" %><%= episode.published_at.strftime('%b %d, %Y') %>
79 | <% elsif params[:view] == "grid" %> 80 |
81 | <% for episode in @episodes %> 82 |
83 |
<%= link_to image_tag("/assets/episodes/stills/#{episode.asset_name}.png", :size => "200x125"), episode %>
84 |
85 | #<%= episode.position %> 86 | <%= link_to episode.name, episode %> 87 |
88 |
89 | <% end %> 90 |
91 |
92 | <% end %> 93 | 94 | <%= will_paginate :previous_label => h("< Previous Page"), :next_label => h("Next Page >") %> 95 | <% end %> 96 |
97 | <% if can? :create, :episodes %> 98 |
<%= link_to "New Episode", new_episode_path %>
99 | <% end %> 100 |
101 | -------------------------------------------------------------------------------- /app/views/episodes/index.rss.builder: -------------------------------------------------------------------------------- 1 | title = "RailsCasts" 2 | author = "Ryan Bates" 3 | description = "Every week you will be treated to a new RailsCasts episode featuring tips and tricks with Ruby on Rails, the popular web development framework. These screencasts are short and focus on one technique so you can quickly move on to applying it to your own project. The topics are geared toward the intermediate Rails developer, but beginners and experts will get something out of it as well." 4 | keywords = "rails, ruby on rails, screencasts, podcasts, tips, tricks, tutorials, training, programming, railscast" 5 | 6 | if params[:ipod] 7 | title += " (Mobile)" 8 | description += " This version is for mobile devices which cannot support the full resolution version." 9 | keywords += ', mobile' 10 | image = "http://railscasts.com/images/ipod_railscasts_cover.jpg" 11 | ext = 'm4v' 12 | else 13 | description += " This is the full resolution version, a lower reoslution for mobile devices is also available." 14 | image = "http://railscasts.com/images/railscasts_cover.jpg" 15 | ext = 'mp4' 16 | end 17 | 18 | xml.rss "xmlns:itunes" => "http://www.itunes.com/dtds/podcast-1.0.dtd", "xmlns:media" => "http://search.yahoo.com/mrss/", :version => "2.0" do 19 | xml.channel do 20 | xml.title title 21 | xml.link 'http://railscasts.com' 22 | xml.description description 23 | xml.language 'en' 24 | xml.pubDate @episodes.first.published_at.to_s(:rfc822) 25 | xml.lastBuildDate @episodes.first.published_at.to_s(:rfc822) 26 | xml.itunes :author, author 27 | xml.itunes :keywords, keywords 28 | xml.itunes :explicit, 'clean' 29 | xml.itunes :image, :href => image 30 | xml.itunes :owner do 31 | xml.itunes :name, author 32 | xml.itunes :email, 'ryan@railscasts.com' 33 | end 34 | xml.itunes :block, 'no' 35 | xml.itunes :category, :text => 'Technology' do 36 | xml.itunes :category, :text => 'Software How-To' 37 | end 38 | xml.itunes :category, :text => 'Education' do 39 | xml.itunes :category, :text => 'Training' 40 | end 41 | 42 | @episodes.each do |episode| 43 | xml.item do 44 | xml.title episode.full_name 45 | xml.description episode.description 46 | xml.pubDate episode.published_at.to_s(:rfc822) 47 | xml.enclosure :url => episode.asset_url("videos", ext), :length => episode.file_size(ext), :type => 'video/mp4' 48 | xml.link episode_url(episode) 49 | xml.guid({:isPermaLink => "false"}, episode.permalink) 50 | xml.itunes :author, author 51 | xml.itunes :subtitle, truncate(episode.description, :length => 150) 52 | xml.itunes :summary, episode.description 53 | xml.itunes :explicit, 'no' 54 | xml.itunes :duration, episode.duration 55 | end 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /app/views/episodes/new.html.erb: -------------------------------------------------------------------------------- 1 | <% title "New Episode" %> 2 | 3 | <%= render :partial => 'form' %> 4 | -------------------------------------------------------------------------------- /app/views/episodes/show.html.erb: -------------------------------------------------------------------------------- 1 | <% title @episode.full_name, false %> 2 | 3 |
"> 4 | <% if params[:autoplay] %> 5 |
<%= episode_video_tag @episode %>
6 | <% else %> 7 | 10 | <% end %> 11 |
"> 12 |
<%= link_to image_tag("/assets/episodes/stills/#{@episode.asset_name}.png", :size => "200x125", :alt => @episode.name), {:autoplay => true}, :class => "play_video" %>
13 |

14 | #<%= @episode.position %> 15 | <%= @episode.name %> 16 |

17 |
18 | <%= @episode.published_at.strftime('%b %d, %Y') %> | 19 | <%= pluralize (@episode.seconds/60).round, "minute" %> | 20 | <%= raw @episode.tags.map { |tag| link_to tag.display_name, root_path(:tag_id => tag) }.join(", ") %> 21 | <% unless @episode.published? %> 22 | NOT YET RELEASED 23 | <% end %> 24 |
25 |
<%= @episode.description %>
26 |
27 | <%= link_to({:autoplay => true}, {:class => "play_video watch_button"}) do %> 28 | Click to Play Video ▶ 29 | <% end %> 30 |
31 | 34 | 43 |
44 |
45 | 52 | 55 |
56 | -------------------------------------------------------------------------------- /app/views/episodes/show.js.erb: -------------------------------------------------------------------------------- 1 | $("#episode .nav_section").html("<%= escape_javascript(%w[comments similar].include?(params[:view]) ? render(params[:view]) : render("show_notes")) %>"); 2 | -------------------------------------------------------------------------------- /app/views/feedback_messages/new.html.erb: -------------------------------------------------------------------------------- 1 | <% title "Feedback", false %> 2 | 3 |
4 |
5 |

Feedback

6 | <%= form_for @feedback_message do |f| %> 7 | <%= f.error_messages %> 8 |
9 |

You can also contact @railscasts on Twitter, or send an email to .

12 |
13 |
14 | <%= label_tag :email %> 15 | <%= text_field_tag :email, "", :autocomplete => "off" %> 16 | (do not fill) 17 |
18 |
19 | <%= f.label :name, "Your Name" %> 20 | <%= f.text_field :name %> 21 |
22 |
23 | <%= f.label :email, "Your Email Address" %> 24 | <%= f.text_field :email %> 25 |
26 |
27 | <%= f.label :content, "Message" %> 28 | <%= f.text_area :content, :rows => 14, :cols => 50 %> 29 |
30 |
<%= f.submit "Send Feedback" %>
31 | <% end %> 32 |
33 |
34 |

Please send feedback about:

35 | 40 |

Unfortunately I am unable to answer generic Rails questions which are not specific to RailsCasts. For this I recommend asking on one of the following forums.

41 | 46 |
47 |
48 | -------------------------------------------------------------------------------- /app/views/info/about.html.erb: -------------------------------------------------------------------------------- 1 | <% title "About RailsCasts", false %> 2 | 3 |
4 |
5 | <%= image_tag "ryan_bates.jpg", :size => "192x227" %> 6 | Ryan Bates 7 |
8 |

About RailsCasts

9 |

RailsCasts is produced by Ryan Bates (<%= link_to "rbates", "http://twitter.com/rbates" %> on Twitter and <%= link_to "ryanb", "https://github.com/ryanb" %> on GitHub). A new episode will be released each Monday featuring tips and tricks with Ruby on Rails. The screencasts are short and focus on one technique so you can quickly move on to applying it to your own project. The topics target the intermediate Rails developer, but beginners and experts will get something out of it as well.

10 | 11 |

ASCIIcasts

12 |

If you prefer text over video, please visit <%= link_to "ASCIIcasts", "http://asciicasts.com/" %> where Eifion Bedford has done a wonderful job of translating the majority of the episodes into textual form.

13 | 14 |

TextMate Theme

15 |

Do you like the TextMate theme used in these screencasts? It is custom made and based off of the theme by idlefingers. I am using the Bitstream Vera Sans Mono font.

16 |

Download Textmate Theme

17 | 18 |

Contact

19 |

If you have any comments, suggestions, questions, etc. I'd love to hear them! Please use the <%= link_to "Feedback Page", feedback_path %> or send an email to .

22 | 23 |

Moderators

24 |

Thank you to all moderators who help keep the site clean and up to date:

25 | 30 |

If you are interested in becoming a moderator, see the <%= link_to "moderator guidelines", moderators_path %>.

31 | 32 |

Special Thanks

33 |

Special thanks to <%= link_to "Linode", "http://www.linode.com/?utm_source=railscasts.com&utm_medium=Badge&utm_campaign=Railscasts" %> for providing hosting for this site and <%= link_to "SublimeVideo", "http://sublimevideo.net/" %> for providing the episode player.

34 | 35 |

This Site

36 |

This site is available in <%= link_to "open source", "https://github.com/ryanb/railscasts" %>. It is intended to be used as an example Rails app, so feel free to copy bits of it for your own project. If you would like to use it as a starting point for your own site please contact me.

37 | 38 |

Software Used

39 | 46 | 47 |

License

48 |

All RailsCasts episodes are under the Creative Commons license. You are free to distribute unedited versions of the episodes for non-commercial purposes. You are also free to translate them into any language. If you would like to edit the video please contact me.

49 |

Creative Commons License

50 |
51 | -------------------------------------------------------------------------------- /app/views/info/give_back.html.erb: -------------------------------------------------------------------------------- 1 | <% title "Give Back to Open Source", false %> 2 | 3 |
4 | <%= image_tag "give_back.jpg", :size => "158x60", :id => "give_back" %> 5 |

A lot of effort is put into open source software which is given away for free. In <%= link_to "Episode 200", episode_path(200) %> I am challenging everyone to give back to the open source community.

6 |
7 |

Here's what I want you to do:

8 |
    9 |
  1. Look in your biggest Rails project
  2. 10 |
  3. Go through every gem and plugin used
  4. 11 |
  5. 12 | Contribute back to each by: 13 |
      14 |
    • Making a donation
    • 15 |
    • Fixing a bug
    • 16 |
    • Adding documentation
    • 17 |
    • Or simply thanking them
    • 18 |
    19 |
  6. 20 |
21 |
22 |

Make a difference and give back to open source!

23 |
24 | -------------------------------------------------------------------------------- /app/views/info/moderators.html.erb: -------------------------------------------------------------------------------- 1 | <% title "Moderator Guidelines" %> 2 | 3 |
4 |

Thank you to all who are interested in becoming moderators. I currently have enough moderators but may need more in the future. You can still request to become a moderator, and I will consider it when a position is available.

5 | 6 |

Moderators primarily help deal with the spam issues, but also help fix show notes and review unreleased episodes. If you are interested in becoming a moderator, <%= link_to "let me know", feedback_path %> and provide reasons as to why you should become one along with your GitHub username.

7 | 8 |

Editing User Comments

9 |

Moderators are able to edit user comments. This is intended to fix incorrect formatting. Do not alter what the user is saying. If you must add a note, ensure that it is clearly marked as edited by you.

10 | 11 |

Deleting User Comments

12 |

Comments can be removed if they provide no value to the conversation, are offensive, or are spam. Do not delete comments which have replies, instead edit them to remove the content and include a note saying it has been removed by a moderator. If a comment is a simple "thank you" and is over a month old, feel free to delete it.

13 | 14 |

Banning Users

15 |

Users can be banned. When this happens all of their comments will be deleted and they will be unable to add further comments. Users who are spamming the site should be banned. Banning users is not available on older comments because a user account system was not available then. You will need to delete those individually.

16 |

If you are ever uncertain whether it is spam or not, click on their profile and read their other comments. If you are still unsure, leave it alone. We do not want to ban legitimate users.

17 |

Both banning users and deleting comments have a confirmation dialog, so don't worry too much about accidentally hitting the button. If you do accidentally delete a comment or ban a user, let me know immediately and provide as much detail as you can.

18 |

If at any time I find a moderator not following these guidelines I will remove the privilege and revert all actions he has done. A paper trail of all changes are kept.

19 | 20 |

Reviewing Unreleased Episodes

21 |

On the rare occasion I finish recording an episode early, it will show up on the front page for moderators before it is released. Feel free to watch this and provide feedback if you find problems. Please send feedback through email or the <%= link_to "feedback page", feedback_path %>, not the comments section until the episode is released.

22 | 23 |

Editing Episode Show Notes

24 |

Moderators can edit episode show notes. Feel free to add notes based on what users mention in the comments. Also please include links to related episodes, mention problems with specific Rails versions, fix minor problems with the source code, etc. If you are doing significant changes to the show notes, please ask me first.

25 | 26 |

Special Thanks

27 |

Thank you to all who are interested in becoming moderators. I am very grateful to all who can help clean up the site and make it better. Your name will appear in a moderators list in the <%= link_to "About page", about_path %> for credit. If you are interested in becoming a moderator, <%= link_to "let me know", feedback_path %> and provide reasons as to why you should become one along with your GitHub username.

28 |

If you have ideas on how I can improve the moderator experience, please add an issue to the <%= link_to "GitHub project", "https://github.com/ryanb/railscasts" %>.

29 |
30 | -------------------------------------------------------------------------------- /app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | <%= content_for?(:title) ? yield(:title) : "Ruby on Rails Screencasts" %> - RailsCasts 6 | 7 | 8 | 9 | <%= stylesheet_link_tag "application", "coderay" %> 10 | <%= javascript_include_tag "jquery.min", "rails", "http://cdn.sublimevideo.net/js/3s7oes9q.js", "application" %> 11 | <%= csrf_meta_tag %> 12 | <%= yield(:head) %> 13 | 14 | 15 |
16 | 17 | 35 |
36 | 37 | 51 | 52 | <% flash.each do |name, msg| %> 53 | <%= content_tag(:div, raw(msg), :id => "flash_#{name}") %> 54 | <% end %> 55 | 56 |
57 | <% if show_title? %> 58 |

<%= yield(:title) %>

59 | <% end %> 60 | 61 | <%= yield %> 62 |
63 | 68 | 69 | 70 | 74 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /app/views/mailer/comment_response.text.erb: -------------------------------------------------------------------------------- 1 | Someone has responded to your comment on RailsCasts. Here is the response. 2 | 3 | --- 4 | From: <%= raw @comment.user.name %> 5 | 6 | <%= raw @comment.content %> 7 | --- 8 | 9 | To view the full comment: <%= episode_url(@comment.episode, :view => "comments") %> 10 | 11 | To unsubscribe from future emails from RailsCasts, just click this link: 12 | <%= unsubscribe_url(@user.generated_unsubscribe_token) %> 13 | -------------------------------------------------------------------------------- /app/views/mailer/feedback.text.erb: -------------------------------------------------------------------------------- 1 | Name: <%= raw @message.name %> 2 | Email: <%= raw @message.email %> 3 | 4 | <%= raw @message.content %> 5 | -------------------------------------------------------------------------------- /app/views/users/ban.js.erb: -------------------------------------------------------------------------------- 1 | <% @comments.each do |comment| %> 2 | $("#<%= dom_id(comment) %>").remove(); 3 | <% end %> 4 | -------------------------------------------------------------------------------- /app/views/users/edit.html.erb: -------------------------------------------------------------------------------- 1 | <% title "Edit Profile" %> 2 | 3 |
4 | <%= form_for @user do |f| %> 5 | <%= f.error_messages %> 6 |

7 | <%= f.label :name %> 8 | <%= f.text_field :name %> 9 |

10 |

11 | <%= f.label :email %> 12 | <%= f.text_field :email %> 13 |

14 |

15 | <%= f.label :site_url, "Site URL" %> 16 | <%= f.text_field :site_url %> 17 |

18 |

19 | <%= f.check_box :email_on_reply %> 20 | <%= f.label :email_on_reply, "Receive email when a user replies to your comment", :class => "check_box" %> 21 |

22 |

<%= f.submit "Update Profile" %>

23 | <% end %> 24 |
25 | -------------------------------------------------------------------------------- /app/views/users/show.html.erb: -------------------------------------------------------------------------------- 1 | <% title "#{@user.display_name}'s Profile" %> 2 | 3 |
4 | <% if @user == current_user %> 5 |

<%= link_to "Edit Profile", edit_user_path(@user) %> | <%= link_to "Log Out", logout_path %>

6 |

This is your profile.

7 | <% end %> 8 | <% if @user.moderator? %> 9 |

This user is a moderator.

10 | <% end %> 11 | <% if @user.banned? %> 12 |

This user is banned from comments.

13 | <% end %> 14 |

GitHub User: <%= link_to @user.github_username, "https://github.com/#{@user.github_username}" %>

15 | <% unless @user.site_url.blank? %> 16 |

Site: <%= link_to @user.site_url, fix_url(@user.site_url) %>

17 | <% end %> 18 |
19 |

<%= link_to "Ban this User", ban_user_path(@user), :confirm => "Are you certain you want to ban this user? It will delete all of his comments and not allow him to comment again.", :remote => true, :method => :put if can?(:ban, @user) && !@user.banned? %>

20 |
21 |

Comments by <%= @user.name %>

22 |
23 | <% for comment in @user.comments.recent %> 24 | <%= render comment %> 25 | <% end %> 26 |
27 |
28 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require ::File.expand_path('../config/environment', __FILE__) 4 | run Railscasts::Application 5 | -------------------------------------------------------------------------------- /config/application.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../boot', __FILE__) 2 | 3 | require 'net/http' 4 | require 'yaml' 5 | APP_CONFIG = YAML.load(File.read(File.expand_path('../app_config.yml', __FILE__))) 6 | 7 | require 'rails/all' 8 | 9 | # If you have a Gemfile, require the gems listed there, including any gems 10 | # you've limited to :test, :development, or :production. 11 | Bundler.require(:default, Rails.env) if defined?(Bundler) 12 | 13 | module Railscasts 14 | class Application < Rails::Application 15 | # Settings in config/environments/* take precedence over those specified here. 16 | # Application configuration should go into files in config/initializers 17 | # -- all .rb files in that directory are automatically loaded. 18 | 19 | # Custom directories with classes and modules you want to be autoloadable. 20 | # config.autoload_paths += %W(#{config.root}/extras) 21 | 22 | # Only load the plugins named here, in the order given (default is alphabetical). 23 | # :all can be used as a placeholder for all plugins not explicitly named. 24 | # config.plugins = [ :exception_notification, :ssl_requirement, :all ] 25 | 26 | # Activate observers that should always be running. 27 | # config.active_record.observers = :cacher, :garbage_collector, :forum_observer 28 | 29 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. 30 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. 31 | # config.time_zone = 'Central Time (US & Canada)' 32 | 33 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. 34 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] 35 | # config.i18n.default_locale = :de 36 | 37 | # JavaScript files you want as :defaults (application.js is always included). 38 | # config.action_view.javascript_expansions[:defaults] = %w(jquery rails) 39 | 40 | # Configure the default encoding used in templates for Ruby 1.9. 41 | config.encoding = "utf-8" 42 | 43 | # Configure sensitive parameters which will be filtered from the log file. 44 | config.filter_parameters += [:password] 45 | 46 | config.time_zone = 'Pacific Time (US & Canada)' 47 | 48 | config.autoload_paths += %W(#{Rails.root}/lib) 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /config/boot.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | 3 | # Set up gems listed in the Gemfile. 4 | gemfile = File.expand_path('../../Gemfile', __FILE__) 5 | begin 6 | ENV['BUNDLE_GEMFILE'] = gemfile 7 | require 'bundler' 8 | Bundler.setup 9 | rescue Bundler::GemNotFound => e 10 | STDERR.puts e.message 11 | STDERR.puts "Try running `bundle install`." 12 | exit! 13 | end if File.exist?(gemfile) 14 | -------------------------------------------------------------------------------- /config/deploy.rb: -------------------------------------------------------------------------------- 1 | set :whenever_command, "bundle exec whenever" 2 | require "bundler/capistrano" 3 | require "whenever/capistrano" 4 | require "thinking_sphinx/deploy/capistrano" 5 | 6 | set :application, "railscasts.com" 7 | role :app, application 8 | role :web, application 9 | role :db, application, :primary => true 10 | 11 | set :user, "rbates" 12 | set :deploy_to, "/var/apps/railscasts" 13 | set :deploy_via, :remote_cache 14 | set :use_sudo, false 15 | 16 | set :scm, "git" 17 | set :repository, "git://github.com/ryanb/railscasts.git" 18 | set :branch, "master" 19 | 20 | namespace :deploy do 21 | desc "Tell Passenger to restart." 22 | task :restart, :roles => :web do 23 | run "touch #{deploy_to}/current/tmp/restart.txt" 24 | end 25 | 26 | desc "Do nothing on startup so we don't get a script/spin error." 27 | task :start do 28 | puts "You may need to restart Apache." 29 | end 30 | 31 | desc "Symlink extra configs and folders." 32 | task :symlink_extras do 33 | run "ln -nfs #{shared_path}/config/database.yml #{release_path}/config/database.yml" 34 | run "ln -nfs #{shared_path}/config/app_config.yml #{release_path}/config/app_config.yml" 35 | run "ln -nfs #{shared_path}/config/production.sphinx.conf #{release_path}/config/production.sphinx.conf" 36 | run "ln -nfs #{shared_path}/assets #{release_path}/public/assets" 37 | run "ln -nfs #{shared_path}/db/sphinx #{release_path}/db/sphinx" 38 | end 39 | 40 | desc "Setup shared directory." 41 | task :setup_shared do 42 | run "mkdir #{shared_path}/assets" 43 | run "mkdir #{shared_path}/config" 44 | run "mkdir #{shared_path}/db" 45 | run "mkdir #{shared_path}/db/sphinx" 46 | put File.read("config/examples/database.yml"), "#{shared_path}/config/database.yml" 47 | put File.read("config/examples/app_config.yml"), "#{shared_path}/config/app_config.yml" 48 | put File.read("config/examples/production.sphinx.conf"), "#{shared_path}/config/production.sphinx.conf" 49 | puts "Now edit the config files and fill assets folder in #{shared_path}." 50 | end 51 | 52 | desc "Sync the public/assets directory." 53 | task :assets do 54 | system "rsync -vr --exclude='.DS_Store' public/assets #{user}@#{application}:/var/apps/railscasts/shared/" 55 | end 56 | 57 | desc "Make sure there is something to deploy" 58 | task :check_revision, :roles => :web do 59 | unless `git rev-parse HEAD` == `git rev-parse origin/master` 60 | puts "WARNING: HEAD is not the same as origin/master" 61 | puts "Run `git push` to sync changes." 62 | exit 63 | end 64 | end 65 | end 66 | 67 | before "deploy", "deploy:check_revision" 68 | after "deploy", "deploy:cleanup" # keeps only last 5 releases 69 | after "deploy:setup", "deploy:setup_shared" 70 | after "deploy:update_code", "deploy:symlink_extras" 71 | before "deploy:update_code", "thinking_sphinx:stop" 72 | after "deploy:update_code", "thinking_sphinx:start" 73 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the rails application 2 | require File.expand_path('../application', __FILE__) 3 | 4 | # Initialize the rails application 5 | Railscasts::Application.initialize! 6 | -------------------------------------------------------------------------------- /config/environments/development.rb: -------------------------------------------------------------------------------- 1 | Railscasts::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb 3 | 4 | # In the development environment your application's code is reloaded on 5 | # every request. This slows down response time but is perfect for development 6 | # since you don't have to restart the webserver when you make code changes. 7 | config.cache_classes = false 8 | 9 | # Log error messages when you accidentally call methods on nil. 10 | config.whiny_nils = true 11 | 12 | # Show full error reports and disable caching 13 | config.consider_all_requests_local = true 14 | config.action_view.debug_rjs = true 15 | config.action_controller.perform_caching = false 16 | 17 | # Don't care if the mailer can't send 18 | config.action_mailer.raise_delivery_errors = false 19 | 20 | # Print deprecation notices to the Rails logger 21 | config.active_support.deprecation = :log 22 | 23 | # Only use best-standards-support built into browsers 24 | config.action_dispatch.best_standards_support = :builtin 25 | 26 | config.action_mailer.default_url_options = { :host => "railscasts.dev" } 27 | end 28 | 29 | -------------------------------------------------------------------------------- /config/environments/production.rb: -------------------------------------------------------------------------------- 1 | Railscasts::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb 3 | 4 | # The production environment is meant for finished, "live" apps. 5 | # Code is not reloaded between requests 6 | config.cache_classes = true 7 | 8 | # Full error reports are disabled and caching is turned on 9 | config.consider_all_requests_local = false 10 | config.action_controller.perform_caching = true 11 | 12 | # Specifies the header that your server uses for sending files 13 | config.action_dispatch.x_sendfile_header = "X-Sendfile" 14 | 15 | # For nginx: 16 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' 17 | 18 | # If you have no front-end server that supports something like X-Sendfile, 19 | # just comment this out and Rails will serve the files 20 | 21 | # See everything in the log (default is :info) 22 | config.log_level = :warn 23 | 24 | # Use a different logger for distributed setups 25 | # config.logger = SyslogLogger.new 26 | 27 | # Use a different cache store in production 28 | # config.cache_store = :mem_cache_store 29 | 30 | # Disable Rails's static asset server 31 | # In production, Apache or nginx will already do this 32 | config.serve_static_assets = false 33 | 34 | # Enable serving of images, stylesheets, and javascripts from an asset server 35 | # config.action_controller.asset_host = "http://assets.example.com" 36 | 37 | # Disable delivery errors, bad email addresses will be ignored 38 | # config.action_mailer.raise_delivery_errors = false 39 | 40 | # Enable threaded mode 41 | # config.threadsafe! 42 | 43 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 44 | # the I18n.default_locale when a translation can not be found) 45 | config.i18n.fallbacks = true 46 | 47 | # Send deprecation notices to registered listeners 48 | config.active_support.deprecation = :notify 49 | 50 | config.action_mailer.delivery_method = :sendmail 51 | ignore_exceptions = ExceptionNotifier.default_ignore_exceptions + [ActionView::MissingTemplate] 52 | config.middleware.use ExceptionNotifier, :email_prefix => "[ERROR] ", :sender_address => 'noreply@railscasts.com', :exception_recipients => "ryan@railscasts.com", :ignore_exceptions => ignore_exceptions 53 | 54 | config.action_mailer.default_url_options = { :host => "railscasts.com" } 55 | end 56 | -------------------------------------------------------------------------------- /config/environments/test.rb: -------------------------------------------------------------------------------- 1 | Railscasts::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb 3 | 4 | # The test environment is used exclusively to run your application's 5 | # test suite. You never need to work with it otherwise. Remember that 6 | # your test database is "scratch space" for the test suite and is wiped 7 | # and recreated between test runs. Don't rely on the data there! 8 | config.cache_classes = true 9 | 10 | # Log error messages when you accidentally call methods on nil. 11 | config.whiny_nils = true 12 | 13 | # Show full error reports and disable caching 14 | config.consider_all_requests_local = true 15 | config.action_controller.perform_caching = false 16 | 17 | # Raise exceptions instead of rendering exception templates 18 | config.action_dispatch.show_exceptions = false 19 | 20 | # Disable request forgery protection in test environment 21 | config.action_controller.allow_forgery_protection = false 22 | 23 | # Tell Action Mailer not to deliver emails to the real world. 24 | # The :test delivery method accumulates sent emails in the 25 | # ActionMailer::Base.deliveries array. 26 | config.action_mailer.delivery_method = :test 27 | 28 | # Use SQL instead of Active Record's schema dumper when creating the test database. 29 | # This is necessary if your schema can't be completely dumped by the schema dumper, 30 | # like if you have constraints or database-specific column types 31 | # config.active_record.schema_format = :sql 32 | 33 | # Print deprecation notices to the stderr 34 | config.active_support.deprecation = :stderr 35 | 36 | config.action_mailer.default_url_options = { :host => "www.example.com" } 37 | end 38 | -------------------------------------------------------------------------------- /config/examples/app_config.yml: -------------------------------------------------------------------------------- 1 | session_key: "_railscasts_session" 2 | session_secret: 40bc73b6a7be494f7de7d9d3b27a3cd81b1d7857f1fbc48445222d728677cb2378f93a96b780542d9fbf92bc18725556d3320ef4d21f4394b7c6b9f94705f78b 3 | thinking_sphinx: false 4 | github_id: oauth_id 5 | github_secret: oauth_secret 6 | -------------------------------------------------------------------------------- /config/examples/database.yml: -------------------------------------------------------------------------------- 1 | # MySQL. Versions 4.1 and 5.0 are recommended. 2 | # 3 | # Install the MySQL driver: 4 | # gem install mysql2 5 | # 6 | # And be sure to use new-style password hashing: 7 | # http://dev.mysql.com/doc/refman/5.0/en/old-client.html 8 | development: 9 | adapter: mysql2 10 | encoding: utf8 11 | reconnect: false 12 | database: railscasts_development 13 | pool: 5 14 | username: root 15 | password: 16 | host: localhost 17 | 18 | # Warning: The database defined as "test" will be erased and 19 | # re-generated from your development database when you run "rake". 20 | # Do not set this db to the same as development or production. 21 | test: 22 | adapter: mysql2 23 | encoding: utf8 24 | reconnect: false 25 | database: railscasts_test 26 | pool: 5 27 | username: root 28 | password: 29 | host: localhost 30 | 31 | production: 32 | adapter: mysql2 33 | encoding: utf8 34 | reconnect: false 35 | database: railscasts_production 36 | pool: 5 37 | username: root 38 | password: 39 | host: localhost 40 | -------------------------------------------------------------------------------- /config/examples/production.sphinx.conf: -------------------------------------------------------------------------------- 1 | indexer 2 | { 3 | mem_limit = 64M 4 | } 5 | 6 | searchd 7 | { 8 | address = 127.0.0.1 9 | port = 3312 10 | log = /var/apps/railscasts/shared/log/searchd.log 11 | query_log = /var/apps/railscasts/shared/log/searchd.query.log 12 | read_timeout = 5 13 | max_children = 30 14 | pid_file = /var/apps/railscasts/shared/log/searchd.development.pid 15 | max_matches = 1000 16 | } 17 | -------------------------------------------------------------------------------- /config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 7 | # Rails.backtrace_cleaner.remove_silencers! 8 | -------------------------------------------------------------------------------- /config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format 4 | # (all these examples are active by default): 5 | # ActiveSupport::Inflector.inflections do |inflect| 6 | # inflect.plural /^(ox)$/i, '\1en' 7 | # inflect.singular /^(ox)en/i, '\1' 8 | # inflect.irregular 'person', 'people' 9 | # inflect.uncountable %w( fish sheep ) 10 | # end 11 | -------------------------------------------------------------------------------- /config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | # Mime::Type.register_alias "text/html", :iphone 6 | -------------------------------------------------------------------------------- /config/initializers/omniauth.rb: -------------------------------------------------------------------------------- 1 | Rails.application.config.middleware.use OmniAuth::Builder do 2 | # Sign up at https://github.com/account/applications 3 | provider :github, APP_CONFIG["github_id"], APP_CONFIG["github_secret"] 4 | end 5 | -------------------------------------------------------------------------------- /config/initializers/secret_token.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | # Make sure the secret is at least 30 characters and all random, 6 | # no regular words or you'll be exposed to dictionary attacks. 7 | Railscasts::Application.config.secret_token = APP_CONFIG['session_secret'] 8 | -------------------------------------------------------------------------------- /config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Railscasts::Application.config.session_store :cookie_store, :key => APP_CONFIG['session_key'] 4 | 5 | # Use the database for sessions instead of the cookie-based default, 6 | # which shouldn't be used to store highly confidential information 7 | # (create the session table with "rails generate session_migration") 8 | # Railscasts::Application.config.session_store :active_record_store 9 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Sample localization file for English. Add more files in this directory for other locales. 2 | # See http://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points. 3 | 4 | en: 5 | hello: "Hello world" 6 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | Railscasts::Application.routes.draw do 2 | root :to => "episodes#index" 3 | 4 | match "auth/:provider/callback" => "users#create" 5 | match "about" => "info#about", :as => "about" 6 | match "give_back" => "info#give_back", :as => "give_back" 7 | match "moderators" => "info#moderators", :as => "moderators" 8 | match "login" => "users#login", :as => "login" 9 | match "logout" => "users#logout", :as => "logout" 10 | match "feedback" => "feedback_messages#new", :as => "feedback" 11 | match "episodes/archive" => redirect("/?view=list") 12 | match 'unsubscribe/:token' => 'users#unsubscribe', :as => "unsubscribe" 13 | post "versions/:id/revert" => "versions#revert", :as => "revert_version" 14 | 15 | resources :users do 16 | member { put :ban } 17 | end 18 | resources :comments 19 | resources :episodes 20 | resources :feedback_messages 21 | 22 | match "tags/:id" => redirect("/?tag_id=%{id}") 23 | end 24 | -------------------------------------------------------------------------------- /config/schedule.rb: -------------------------------------------------------------------------------- 1 | every 1.day, :at => "2:34 am" do 2 | rake "thinking_sphinx:index" 3 | end 4 | 5 | every 1.day, :at => "1:15 am" do 6 | rake "asciicasts" 7 | end 8 | 9 | every :reboot do 10 | rake "thinking_sphinx:start" 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20080509050853_create_episodes.rb: -------------------------------------------------------------------------------- 1 | class CreateEpisodes < ActiveRecord::Migration 2 | def self.up 3 | create_table :episodes do |t| 4 | t.string :name 5 | t.string :permalink 6 | t.text :description 7 | t.text :notes 8 | t.datetime :published_at 9 | t.timestamps 10 | end 11 | end 12 | 13 | def self.down 14 | drop_table :episodes 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /db/migrate/20080620015230_create_tags.rb: -------------------------------------------------------------------------------- 1 | class CreateTags < ActiveRecord::Migration 2 | def self.up 3 | create_table :tags do |t| 4 | t.string :name 5 | t.timestamps 6 | end 7 | end 8 | 9 | def self.down 10 | drop_table :tags 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /db/migrate/20080620015432_create_taggings.rb: -------------------------------------------------------------------------------- 1 | class CreateTaggings < ActiveRecord::Migration 2 | def self.up 3 | create_table :taggings do |t| 4 | t.belongs_to :episode 5 | t.belongs_to :tag 6 | t.timestamps 7 | end 8 | end 9 | 10 | def self.down 11 | drop_table :taggings 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /db/migrate/20080620022227_create_comments.rb: -------------------------------------------------------------------------------- 1 | class CreateComments < ActiveRecord::Migration 2 | def self.up 3 | create_table :comments do |t| 4 | t.belongs_to :episode 5 | t.text :content 6 | t.string :name 7 | t.string :email 8 | t.string :site_url 9 | t.string :user_ip 10 | t.string :user_agent 11 | t.string :referrer 12 | t.timestamps 13 | end 14 | end 15 | 16 | def self.down 17 | drop_table :comments 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /db/migrate/20080702045900_add_position_to_episodes.rb: -------------------------------------------------------------------------------- 1 | class AddPositionToEpisodes < ActiveRecord::Migration 2 | def self.up 3 | add_column :episodes, :position, :integer, :default => 0 4 | Episode.all.each_with_index { |e, i| e.update_attribute(:position, i+1) } 5 | end 6 | 7 | def self.down 8 | remove_column :episodes, :position 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /db/migrate/20080721011326_create_downloads.rb: -------------------------------------------------------------------------------- 1 | class CreateDownloads < ActiveRecord::Migration 2 | def self.up 3 | create_table :downloads do |t| 4 | t.integer :episode_id 5 | t.string :url 6 | t.string :format 7 | t.integer :bytes 8 | t.integer :seconds 9 | t.timestamps 10 | end 11 | end 12 | 13 | def self.down 14 | drop_table :downloads 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /db/migrate/20080722025256_create_sponsors.rb: -------------------------------------------------------------------------------- 1 | class CreateSponsors < ActiveRecord::Migration 2 | def self.up 3 | create_table :sponsors do |t| 4 | t.string :name 5 | t.boolean :active, :default => false, :null => false 6 | t.string :site_url 7 | t.string :image_url 8 | t.timestamps 9 | end 10 | end 11 | 12 | def self.down 13 | drop_table :sponsors 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /db/migrate/20080727044942_add_foreign_key_indexes.rb: -------------------------------------------------------------------------------- 1 | class AddForeignKeyIndexes < ActiveRecord::Migration 2 | def self.up 3 | add_index :comments, :episode_id 4 | add_index :downloads, :episode_id 5 | add_index :taggings, :episode_id 6 | add_index :taggings, :tag_id 7 | end 8 | 9 | def self.down 10 | remove_index :comments, :episode_id 11 | remove_index :downloads, :episode_id 12 | remove_index :taggings, :episode_id 13 | remove_index :taggings, :tag_id 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /db/migrate/20080730035149_add_comments_count_to_episodes.rb: -------------------------------------------------------------------------------- 1 | class AddCommentsCountToEpisodes < ActiveRecord::Migration 2 | def self.up 3 | add_column :episodes, :comments_count, :integer, :default => 0, :null => false 4 | execute "UPDATE episodes SET comments_count=(SELECT COUNT(*) FROM comments WHERE episode_id=episodes.id)" 5 | end 6 | 7 | def self.down 8 | remove_column :episodes, :comments_count 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /db/migrate/20080820014608_add_position_to_comments.rb: -------------------------------------------------------------------------------- 1 | class AddPositionToComments < ActiveRecord::Migration 2 | def self.up 3 | add_column :comments, :position, :integer 4 | 5 | # terribly inefficient, but it gets the job done. 6 | Episode.all.each do |episode| 7 | episode.comments.each_with_index do |comment, index| 8 | comment.update_attribute :position, index+1 9 | end 10 | end 11 | end 12 | 13 | def self.down 14 | remove_column :comments, :position 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /db/migrate/20090128014441_add_force_top_to_sponsors.rb: -------------------------------------------------------------------------------- 1 | class AddForceTopToSponsors < ActiveRecord::Migration 2 | def self.up 3 | add_column :sponsors, :force_top, :boolean, :default => false, :null => false 4 | end 5 | 6 | def self.down 7 | remove_column :sponsors, :force_top 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20090228183216_add_seconds_to_episodes.rb: -------------------------------------------------------------------------------- 1 | class AddSecondsToEpisodes < ActiveRecord::Migration 2 | def self.up 3 | add_column :episodes, :seconds, :integer 4 | Episode.reset_column_information 5 | Episode.all(:include => :downloads).each do |episode| 6 | # there are more efficient ways to do this, but it is just a one time deal so it's okay. 7 | episode.update_attribute(:seconds, episode.downloads.first.seconds) 8 | end 9 | end 10 | 11 | def self.down 12 | remove_column :episodes, :seconds 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /db/migrate/20090228184049_remove_seconds_from_downloads.rb: -------------------------------------------------------------------------------- 1 | class RemoveSecondsFromDownloads < ActiveRecord::Migration 2 | def self.up 3 | remove_column :downloads, :seconds 4 | end 5 | 6 | def self.down 7 | add_column :downloads, :seconds, :integer 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20090311165121_create_spam_reports.rb: -------------------------------------------------------------------------------- 1 | class CreateSpamReports < ActiveRecord::Migration 2 | def self.up 3 | create_table :spam_reports do |t| 4 | t.integer :comment_id 5 | t.string :comment_ip 6 | t.string :comment_site_url 7 | t.string :comment_name 8 | t.string :user_ip 9 | t.datetime :confirmed_at 10 | t.timestamps 11 | end 12 | end 13 | 14 | def self.down 15 | drop_table :spam_reports 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /db/migrate/20090311181239_add_hit_count_to_spam_reports.rb: -------------------------------------------------------------------------------- 1 | class AddHitCountToSpamReports < ActiveRecord::Migration 2 | def self.up 3 | add_column :spam_reports, :hit_count, :integer 4 | end 5 | 6 | def self.down 7 | remove_column :spam_reports, :hit_count 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20090318164300_remove_user_ip_from_spam_reports.rb: -------------------------------------------------------------------------------- 1 | class RemoveUserIpFromSpamReports < ActiveRecord::Migration 2 | def self.up 3 | remove_column :spam_reports, :user_ip 4 | end 5 | 6 | def self.down 7 | add_column :spam_reports, :user_ip, :string 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20090403023001_add_asciicasts_to_episodes.rb: -------------------------------------------------------------------------------- 1 | class AddAsciicastsToEpisodes < ActiveRecord::Migration 2 | def self.up 3 | add_column :episodes, :asciicasts, :boolean, :default => false, :null => false 4 | end 5 | 6 | def self.down 7 | remove_column :episodes, :asciicasts 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20091121172820_create_spam_checks.rb: -------------------------------------------------------------------------------- 1 | class CreateSpamChecks < ActiveRecord::Migration 2 | def self.up 3 | create_table :spam_checks do |t| 4 | t.string :regexp 5 | t.integer :weight 6 | t.timestamps 7 | end 8 | end 9 | 10 | def self.down 11 | drop_table :spam_checks 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /db/migrate/20091121181751_create_spam_questions.rb: -------------------------------------------------------------------------------- 1 | class CreateSpamQuestions < ActiveRecord::Migration 2 | def self.up 3 | create_table :spam_questions do |t| 4 | t.string :question 5 | t.string :answer 6 | t.timestamps 7 | end 8 | end 9 | 10 | def self.down 11 | drop_table :spam_questions 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /db/migrate/20101117191759_create_users.rb: -------------------------------------------------------------------------------- 1 | class CreateUsers < ActiveRecord::Migration 2 | def self.up 3 | create_table :users do |t| 4 | t.string :token 5 | t.string :name 6 | t.string :github_username 7 | t.string :email 8 | t.string :site_url 9 | t.string :avatar_url 10 | t.boolean :admin 11 | t.timestamps 12 | end 13 | end 14 | 15 | def self.down 16 | drop_table :users 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /db/migrate/20101209224952_add_github_uid_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddGithubUidToUsers < ActiveRecord::Migration 2 | def self.up 3 | add_column :users, :github_uid, :string 4 | end 5 | 6 | def self.down 7 | remove_column :users, :github_uid 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20101209230056_rename_avatar_url_to_gravatar_token.rb: -------------------------------------------------------------------------------- 1 | class RenameAvatarUrlToGravatarToken < ActiveRecord::Migration 2 | def self.up 3 | rename_column :users, :avatar_url, :gravatar_token 4 | end 5 | 6 | def self.down 7 | rename_column :users, :gravatar_token, :avatar_url 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20101210220007_add_user_id_to_comments.rb: -------------------------------------------------------------------------------- 1 | class AddUserIdToComments < ActiveRecord::Migration 2 | def self.up 3 | add_column :comments, :user_id, :integer 4 | end 5 | 6 | def self.down 7 | remove_column :comments, :user_id 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20110416201115_add_ancestry_to_comments.rb: -------------------------------------------------------------------------------- 1 | class AddAncestryToComments < ActiveRecord::Migration 2 | def self.up 3 | add_column :comments, :ancestry, :string 4 | add_index :comments, :ancestry 5 | end 6 | 7 | def self.down 8 | remove_column :comments, :ancestry 9 | remove_index :comments, :ancestry 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20110416214833_add_legacy_to_episodes.rb: -------------------------------------------------------------------------------- 1 | class AddLegacyToEpisodes < ActiveRecord::Migration 2 | def self.up 3 | add_column :episodes, :legacy, :boolean, :default => false, :null => false 4 | end 5 | 6 | def self.down 7 | remove_column :episodes, :legacy 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20110416232852_create_feedback_messages.rb: -------------------------------------------------------------------------------- 1 | class CreateFeedbackMessages < ActiveRecord::Migration 2 | def self.up 3 | create_table :feedback_messages do |t| 4 | t.string :name 5 | t.string :email 6 | t.string :concerning 7 | t.text :content 8 | t.timestamps 9 | end 10 | end 11 | 12 | def self.down 13 | drop_table :feedback_messages 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /db/migrate/20110421060544_cleanup.rb: -------------------------------------------------------------------------------- 1 | class Cleanup < ActiveRecord::Migration 2 | def self.up 3 | drop_table :downloads 4 | drop_table :spam_questions 5 | drop_table :spam_checks 6 | drop_table :spam_reports 7 | drop_table :sponsors 8 | remove_column :feedback_messages, :concerning 9 | end 10 | 11 | def self.down 12 | create_table "downloads" do |t| 13 | t.integer "episode_id" 14 | t.string "url" 15 | t.string "format" 16 | t.integer "bytes" 17 | t.datetime "created_at" 18 | t.datetime "updated_at" 19 | end 20 | add_index "downloads", ["episode_id"], :name => "index_downloads_on_episode_id" 21 | 22 | add_column :feedback_messages, :concerning, :string 23 | 24 | create_table "spam_checks" do |t| 25 | t.string "regexp" 26 | t.integer "weight" 27 | t.datetime "created_at" 28 | t.datetime "updated_at" 29 | end 30 | 31 | create_table "spam_questions" do |t| 32 | t.string "question" 33 | t.string "answer" 34 | t.datetime "created_at" 35 | t.datetime "updated_at" 36 | end 37 | 38 | create_table "spam_reports" do |t| 39 | t.integer "comment_id" 40 | t.string "comment_ip" 41 | t.string "comment_site_url" 42 | t.string "comment_name" 43 | t.datetime "confirmed_at" 44 | t.datetime "created_at" 45 | t.datetime "updated_at" 46 | t.integer "hit_count" 47 | end 48 | 49 | create_table "sponsors" do |t| 50 | t.string "name" 51 | t.boolean "active", :default => false, :null => false 52 | t.string "site_url" 53 | t.string "image_url" 54 | t.datetime "created_at" 55 | t.datetime "updated_at" 56 | t.boolean "force_top", :default => false, :null => false 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /db/migrate/20110423184218_add_legacy_to_comments.rb: -------------------------------------------------------------------------------- 1 | class AddLegacyToComments < ActiveRecord::Migration 2 | def self.up 3 | add_column :comments, :legacy, :boolean, :default => false, :null => false 4 | end 5 | 6 | def self.down 7 | remove_column :comments, :legacy 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20110503025228_add_file_sizes_to_episodes.rb: -------------------------------------------------------------------------------- 1 | class AddFileSizesToEpisodes < ActiveRecord::Migration 2 | def self.up 3 | add_column :episodes, :file_sizes, :text 4 | end 5 | 6 | def self.down 7 | remove_column :episodes, :file_sizes 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20110504180955_add_hidden_to_comments.rb: -------------------------------------------------------------------------------- 1 | class AddHiddenToComments < ActiveRecord::Migration 2 | def self.up 3 | add_column :comments, :hidden, :boolean, :default => false, :null => false 4 | end 5 | 6 | def self.down 7 | remove_column :comments, :hidden 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20110630221611_create_versions.rb: -------------------------------------------------------------------------------- 1 | class CreateVersions < ActiveRecord::Migration 2 | def self.up 3 | create_table :versions do |t| 4 | t.string :item_type, :null => false 5 | t.integer :item_id, :null => false 6 | t.string :event, :null => false 7 | t.string :whodunnit 8 | t.text :object 9 | t.datetime :created_at 10 | end 11 | add_index :versions, [:item_type, :item_id] 12 | end 13 | 14 | def self.down 15 | remove_index :versions, [:item_type, :item_id] 16 | drop_table :versions 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /db/migrate/20110630234413_add_moderator_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddModeratorToUsers < ActiveRecord::Migration 2 | def self.up 3 | add_column :users, :moderator, :boolean, :default => false, :null => false 4 | end 5 | 6 | def self.down 7 | remove_column :users, :moderator 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20110701001805_add_banned_at_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddBannedAtToUsers < ActiveRecord::Migration 2 | def self.up 3 | add_column :users, :banned_at, :datetime 4 | end 5 | 6 | def self.down 7 | remove_column :users, :banned_at 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20110701025738_remove_hidden_from_comments.rb: -------------------------------------------------------------------------------- 1 | class RemoveHiddenFromComments < ActiveRecord::Migration 2 | def self.up 3 | remove_column :comments, :hidden 4 | end 5 | 6 | def self.down 7 | add_column :comments, :hidden, :boolean, :default => false, :null => false 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20110725215614_add_email_on_reply_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddEmailOnReplyToUsers < ActiveRecord::Migration 2 | def self.up 3 | add_column :users, :email_on_reply, :boolean, :default => false, :null => false 4 | add_column :users, :unsubscribe_token, :string 5 | end 6 | 7 | def self.down 8 | remove_column :users, :unsubscribe_token 9 | remove_column :users, :email_on_reply 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/schema.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # This file is auto-generated from the current state of the database. Instead 3 | # of editing this file, please use the migrations feature of Active Record to 4 | # incrementally modify your database, and then regenerate this schema definition. 5 | # 6 | # Note that this schema.rb definition is the authoritative source for your 7 | # database schema. If you need to create the application database on another 8 | # system, you should be using db:schema:load, not running all the migrations 9 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations 10 | # you'll amass, the slower it'll run and the greater likelihood for issues). 11 | # 12 | # It's strongly recommended to check this file into your version control system. 13 | 14 | ActiveRecord::Schema.define(:version => 20110725215614) do 15 | 16 | create_table "comments", :force => true do |t| 17 | t.integer "episode_id" 18 | t.text "content" 19 | t.string "name" 20 | t.string "email" 21 | t.string "site_url" 22 | t.string "user_ip" 23 | t.string "user_agent" 24 | t.string "referrer" 25 | t.datetime "created_at" 26 | t.datetime "updated_at" 27 | t.integer "position" 28 | t.integer "user_id" 29 | t.string "ancestry" 30 | t.boolean "legacy", :default => false, :null => false 31 | end 32 | 33 | add_index "comments", ["ancestry"], :name => "index_comments_on_ancestry" 34 | add_index "comments", ["episode_id"], :name => "index_comments_on_episode_id" 35 | 36 | create_table "episodes", :force => true do |t| 37 | t.string "name" 38 | t.string "permalink" 39 | t.text "description" 40 | t.text "notes" 41 | t.datetime "published_at" 42 | t.datetime "created_at" 43 | t.datetime "updated_at" 44 | t.integer "position", :default => 0 45 | t.integer "comments_count", :default => 0, :null => false 46 | t.integer "seconds" 47 | t.boolean "asciicasts", :default => false, :null => false 48 | t.boolean "legacy", :default => false, :null => false 49 | t.text "file_sizes" 50 | end 51 | 52 | create_table "feedback_messages", :force => true do |t| 53 | t.string "name" 54 | t.string "email" 55 | t.text "content" 56 | t.datetime "created_at" 57 | t.datetime "updated_at" 58 | end 59 | 60 | create_table "taggings", :force => true do |t| 61 | t.integer "episode_id" 62 | t.integer "tag_id" 63 | t.datetime "created_at" 64 | t.datetime "updated_at" 65 | end 66 | 67 | add_index "taggings", ["episode_id"], :name => "index_taggings_on_episode_id" 68 | add_index "taggings", ["tag_id"], :name => "index_taggings_on_tag_id" 69 | 70 | create_table "tags", :force => true do |t| 71 | t.string "name" 72 | t.datetime "created_at" 73 | t.datetime "updated_at" 74 | end 75 | 76 | create_table "users", :force => true do |t| 77 | t.string "token" 78 | t.string "name" 79 | t.string "github_username" 80 | t.string "email" 81 | t.string "site_url" 82 | t.string "gravatar_token" 83 | t.boolean "admin" 84 | t.datetime "created_at" 85 | t.datetime "updated_at" 86 | t.string "github_uid" 87 | t.boolean "moderator", :default => false, :null => false 88 | t.datetime "banned_at" 89 | t.boolean "email_on_reply", :default => false, :null => false 90 | t.string "unsubscribe_token" 91 | end 92 | 93 | create_table "versions", :force => true do |t| 94 | t.string "item_type", :null => false 95 | t.integer "item_id", :null => false 96 | t.string "event", :null => false 97 | t.string "whodunnit" 98 | t.text "object" 99 | t.datetime "created_at" 100 | end 101 | 102 | add_index "versions", ["item_type", "item_id"], :name => "index_versions_on_item_type_and_item_id" 103 | 104 | end 105 | -------------------------------------------------------------------------------- /db/seeds.rb: -------------------------------------------------------------------------------- 1 | # This file should contain all the record creation needed to seed the database with its default values. 2 | # The data can then be loaded with the rake db:seed (or created alongside the db with db:setup). 3 | # 4 | # Examples: 5 | # 6 | # cities = City.create([{ :name => 'Chicago' }, { :name => 'Copenhagen' }]) 7 | # Mayor.create(:name => 'Daley', :city => cities.first) 8 | -------------------------------------------------------------------------------- /lib/code_formatter.rb: -------------------------------------------------------------------------------- 1 | class CodeFormatter 2 | def initialize(text) 3 | @text = text 4 | end 5 | 6 | def to_html 7 | text = @text.clone 8 | codes = [] 9 | text.gsub!(/^``` ?(.*?)\r?\n(.+?)\r?\n```\r?$/m) do |match| 10 | code = { :id => "CODE#{codes.size}ENDCODE", :name => ($1.empty? ? nil : $1), :content => $2 } 11 | codes << code 12 | "\n\n#{code[:id]}\n\n" 13 | end 14 | html = Redcarpet.new(text, :filter_html, :hard_wrap, :autolink, :no_intraemphasis).to_html 15 | codes.each do |code| 16 | html.sub!("

#{code[:id]}

") do 17 | <<-EOS 18 |
19 |
20 | #{CGI.escapeHTML(code[:name].to_s)} 21 | #{clippy(code[:content])} 22 |
23 | #{CodeRay.scan(code[:content], language(code[:name])).div(:css => :class)} 24 |
25 | EOS 26 | end 27 | end 28 | html 29 | end 30 | 31 | def language(path) 32 | case path.to_s.strip 33 | when /\.yml$/ then "yaml" 34 | when /\.js$/ then "java_script" 35 | when /\.scss$/ then "css" 36 | when /\.erb$/, /\.html$/ then "rhtml" 37 | when /\.rb$/, /\.rake$/, /\.gemspec/, /file$/, /console$/, "rails" then "ruby" 38 | when /([a-z0-9]+)$/i then $1 39 | else "text" 40 | end 41 | end 42 | 43 | def clippy(text) 44 | id = "clippy_#{rand(10000000)}" 45 | <<-EOS 46 |
47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 |
58 | EOS 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /lib/tasks/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/railscasts/010f1927c9ccb9604d0af1c6326419bd5371b56c/lib/tasks/.gitkeep -------------------------------------------------------------------------------- /lib/tasks/application.rake: -------------------------------------------------------------------------------- 1 | desc "Check if ASCIIcasts are available for episodes." 2 | task :asciicasts => :environment do 3 | require 'thinking_sphinx' # for some reason this isn't being loaded in the environment 4 | Episode.all(:conditions => { :asciicasts => false }).each do |episode| 5 | response = Net::HTTP.get_response(URI.parse("http://asciicasts.com/episodes/#{episode.to_param}")) 6 | episode.update_attribute(:asciicasts, true) if response.code == "200" 7 | sleep 3 # so we don't hammer the server 8 | end 9 | end 10 | 11 | desc "Reset position attribute for all comments, sometimes it gets out of sync" 12 | task :reset_comment_positions => :environment do 13 | Episode.find_each do |episode| 14 | episode.comments.all(:order => "created_at").each_with_index do |comment, index| 15 | comment.update_attribute(:position, index+1) 16 | end 17 | episode.update_attribute(:comments_count, episode.comments.count) 18 | end 19 | end 20 | 21 | desc "Fix the code blocks in all episodes" 22 | task :fix_episodes => :environment do 23 | Episode.find_each do |episode| 24 | notes = episode.notes.dup 25 | notes.gsub!("\r\n", "\n") 26 | notes.gsub!(/^\/\* (.+) \*\/$|^\<\!\-\- (.+) \-\-\>$|^\# (.+)$|^\/\/ (.+)$/) do |match| 27 | path = $1 || $2 || $3 || $4 28 | if path =~ /\.\w+$/ || path =~ /file$/ || path =~ /console$/ 29 | "@@@\n\n@@@ #{path}" 30 | else 31 | match 32 | end 33 | end 34 | notes.gsub!("\n\n@@@\n", "\n@@@\n") 35 | notes.gsub!(/@@@ .+\n@@@\n\n/, "") 36 | notes.gsub!("@@@", "```") 37 | notes.gsub!(/\*(\w+?)\*/, '**\1**') 38 | notes.gsub!("**\n* ", "**\n\n* ") 39 | notes.gsub!(/"(.+?)"\:(\S+)/, '[\1](\2)') 40 | notes.gsub!("\n", "\r\n") 41 | episode.notes = notes 42 | episode.legacy = true 43 | episode.save! 44 | end 45 | end 46 | 47 | desc "Mark legacy comments" 48 | task :fix_comments => :environment do 49 | Comment.update_all(:legacy => true) 50 | end 51 | 52 | desc "Fill the episode file size values" 53 | task :episode_file_sizes => :environment do 54 | Episode.order("position desc").each do |episode| 55 | episode.load_file_sizes 56 | sleep 1 57 | puts "File sizes for episode #{episode.position}: #{episode.file_sizes.inspect}" 58 | episode.save! 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /log/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/railscasts/010f1927c9ccb9604d0af1c6326419bd5371b56c/log/.gitignore -------------------------------------------------------------------------------- /public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Not Found - RailsCasts 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 39 |
40 | 41 | 51 | 52 |
53 |
54 |

Not Found (404)

55 |

The page you requested does not exist. You may have mistyped the address or the page may have moved.

56 |
57 |
58 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Change Rejected - RailsCasts 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 39 |
40 | 41 | 51 | 52 |
53 |
54 |

Change Rejected (422)

55 |

The change you wanted was rejected. Maybe you tried to change something you didn't have access to.

56 |
57 |
58 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Error Occurred - RailsCasts 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 39 |
40 | 41 | 51 | 52 |
53 |
54 |

Error Occurred (500)

55 |

We're sorry, but something went wrong. Please try again later.

56 |
57 |
58 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/railscasts/010f1927c9ccb9604d0af1c6326419bd5371b56c/public/favicon.ico -------------------------------------------------------------------------------- /public/flash/clippy.swf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/railscasts/010f1927c9ccb9604d0af1c6326419bd5371b56c/public/flash/clippy.swf -------------------------------------------------------------------------------- /public/images/give_back.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/railscasts/010f1927c9ccb9604d0af1c6326419bd5371b56c/public/images/give_back.jpg -------------------------------------------------------------------------------- /public/images/give_back_banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/railscasts/010f1927c9ccb9604d0af1c6326419bd5371b56c/public/images/give_back_banner.png -------------------------------------------------------------------------------- /public/images/guest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/railscasts/010f1927c9ccb9604d0af1c6326419bd5371b56c/public/images/guest.png -------------------------------------------------------------------------------- /public/images/icons/asciicasts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/railscasts/010f1927c9ccb9604d0af1c6326419bd5371b56c/public/images/icons/asciicasts.png -------------------------------------------------------------------------------- /public/images/icons/browse_code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/railscasts/010f1927c9ccb9604d0af1c6326419bd5371b56c/public/images/icons/browse_code.png -------------------------------------------------------------------------------- /public/images/icons/comments.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/railscasts/010f1927c9ccb9604d0af1c6326419bd5371b56c/public/images/icons/comments.png -------------------------------------------------------------------------------- /public/images/icons/facebook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/railscasts/010f1927c9ccb9604d0af1c6326419bd5371b56c/public/images/icons/facebook.png -------------------------------------------------------------------------------- /public/images/icons/itunes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/railscasts/010f1927c9ccb9604d0af1c6326419bd5371b56c/public/images/icons/itunes.png -------------------------------------------------------------------------------- /public/images/icons/new_comment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/railscasts/010f1927c9ccb9604d0af1c6326419bd5371b56c/public/images/icons/new_comment.png -------------------------------------------------------------------------------- /public/images/icons/rss.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/railscasts/010f1927c9ccb9604d0af1c6326419bd5371b56c/public/images/icons/rss.png -------------------------------------------------------------------------------- /public/images/icons/show_notes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/railscasts/010f1927c9ccb9604d0af1c6326419bd5371b56c/public/images/icons/show_notes.png -------------------------------------------------------------------------------- /public/images/icons/twitter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/railscasts/010f1927c9ccb9604d0af1c6326419bd5371b56c/public/images/icons/twitter.png -------------------------------------------------------------------------------- /public/images/ipod_railscasts_cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/railscasts/010f1927c9ccb9604d0af1c6326419bd5371b56c/public/images/ipod_railscasts_cover.jpg -------------------------------------------------------------------------------- /public/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/railscasts/010f1927c9ccb9604d0af1c6326419bd5371b56c/public/images/logo.png -------------------------------------------------------------------------------- /public/images/progress_large.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/railscasts/010f1927c9ccb9604d0af1c6326419bd5371b56c/public/images/progress_large.gif -------------------------------------------------------------------------------- /public/images/quicktime.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/railscasts/010f1927c9ccb9604d0af1c6326419bd5371b56c/public/images/quicktime.gif -------------------------------------------------------------------------------- /public/images/railscasts_cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/railscasts/010f1927c9ccb9604d0af1c6326419bd5371b56c/public/images/railscasts_cover.jpg -------------------------------------------------------------------------------- /public/images/railscasts_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/railscasts/010f1927c9ccb9604d0af1c6326419bd5371b56c/public/images/railscasts_logo.png -------------------------------------------------------------------------------- /public/images/railsrumble.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/railscasts/010f1927c9ccb9604d0af1c6326419bd5371b56c/public/images/railsrumble.png -------------------------------------------------------------------------------- /public/images/ryan_bates.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/railscasts/010f1927c9ccb9604d0af1c6326419bd5371b56c/public/images/ryan_bates.jpg -------------------------------------------------------------------------------- /public/images/sublimevideo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/railscasts/010f1927c9ccb9604d0af1c6326419bd5371b56c/public/images/sublimevideo.png -------------------------------------------------------------------------------- /public/images/views/full.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/railscasts/010f1927c9ccb9604d0af1c6326419bd5371b56c/public/images/views/full.png -------------------------------------------------------------------------------- /public/images/views/grid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/railscasts/010f1927c9ccb9604d0af1c6326419bd5371b56c/public/images/views/grid.png -------------------------------------------------------------------------------- /public/images/views/list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/railscasts/010f1927c9ccb9604d0af1c6326419bd5371b56c/public/images/views/list.png -------------------------------------------------------------------------------- /public/javascripts/application.js: -------------------------------------------------------------------------------- 1 | $(function() { 2 | if ($("#episode").length > 0) { 3 | sublimevideo.ready(function() { 4 | if ($("#episode video").length > 0) { 5 | sublimevideo.prepareAndPlay($("#episode video")[0]); 6 | $("#episode video.sublimed").attr("poster", $("#episode video.sublimed").data("poster")); 7 | } else { 8 | $(".play_video").click(function(e) { 9 | var video = $('
').hide().html($("#video_template").html()); 10 | $("#video_template").before(video); 11 | sublimevideo.prepareAndPlay($("#episode video")[0]); 12 | $("#episode video.sublimed").attr("poster", $("#episode video.sublimed").data("poster")); 13 | setTimeout(function() { 14 | $("#episode > .info").addClass("video_info"); 15 | $("#video_wrapper").show(); 16 | }, 200); 17 | e.preventDefault(); 18 | }); 19 | } 20 | $(document).keypress(function(e) { 21 | if (e.which === 32 && !$(e.target).is('input, textarea') && $("#episode video").length > 0) { 22 | var video = $("#episode video")[0]; 23 | if (video.paused) { 24 | sublimevideo.play(); 25 | } else { 26 | sublimevideo.pause(); 27 | } 28 | e.preventDefault(); 29 | } 30 | }); 31 | }); 32 | sublimevideo.load(); 33 | 34 | $("#episode .nav a.tab").click(function(e) { 35 | $("#episode .nav li a").removeClass("selected"); 36 | $(this).addClass("selected"); 37 | $("#episode .nav_section").append('
'); 38 | $.getScript(this.href); 39 | if (history && history.replaceState) { 40 | history.replaceState(null, document.title, this.href); 41 | } 42 | e.preventDefault(); 43 | }); 44 | 45 | $(".markdown_link").live("click", function(e) { 46 | $(this).next(".markdown_examples").slideToggle(); 47 | }); 48 | 49 | $(".clippy").live({ 50 | 'clippycopy': function(e, data) { 51 | data.text = $(this).children(".clippy_code").text(); 52 | }, 53 | 'clippyover': function() { 54 | $(this).children(".clippy_label").text("copy to clipboard"); 55 | }, 56 | 'clippyout': function() { 57 | $(this).children(".clippy_label").text(""); 58 | }, 59 | 'clippycopied': function() { 60 | $(this).children(".clippy_label").text("copied"); 61 | } 62 | }); 63 | } 64 | }); 65 | -------------------------------------------------------------------------------- /public/javascripts/rails.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jquery-ujs 3 | * 4 | * http://github.com/rails/jquery-ujs/blob/master/src/rails.js 5 | * 6 | * This rails.js file supports jQuery 1.4.3 and 1.4.4 . 7 | * 8 | */ 9 | 10 | jQuery(function ($) { 11 | var csrf_token = $('meta[name=csrf-token]').attr('content'), 12 | csrf_param = $('meta[name=csrf-param]').attr('content'); 13 | 14 | $.fn.extend({ 15 | /** 16 | * Triggers a custom event on an element and returns the event result 17 | * this is used to get around not being able to ensure callbacks are placed 18 | * at the end of the chain. 19 | * 20 | * TODO: deprecate with jQuery 1.4.2 release, in favor of subscribing to our 21 | * own events and placing ourselves at the end of the chain. 22 | */ 23 | triggerAndReturn: function (name, data) { 24 | var event = new $.Event(name); 25 | this.trigger(event, data); 26 | 27 | return event.result !== false; 28 | }, 29 | 30 | /** 31 | * Handles execution of remote calls. Provides following callbacks: 32 | * 33 | * - ajax:before - is execute before the whole thing begings 34 | * - ajax:loading - is executed before firing ajax call 35 | * - ajax:success - is executed when status is success 36 | * - ajax:complete - is execute when status is complete 37 | * - ajax:failure - is execute in case of error 38 | * - ajax:after - is execute every single time at the end of ajax call 39 | */ 40 | callRemote: function () { 41 | var el = this, 42 | method = el.attr('method') || el.attr('data-method') || 'GET', 43 | url = el.attr('action') || el.attr('href'), 44 | dataType = el.attr('data-type') || ($.ajaxSettings && $.ajaxSettings.dataType) || 'script'; 45 | 46 | if (url === undefined) { 47 | throw "No URL specified for remote call (action or href must be present)."; 48 | } else { 49 | if (el.triggerAndReturn('ajax:before')) { 50 | var data = el.is('form') ? el.serializeArray() : []; 51 | $.ajax({ 52 | url: url, 53 | data: data, 54 | dataType: dataType, 55 | type: method.toUpperCase(), 56 | beforeSend: function (xhr) { 57 | el.trigger('ajax:loading', xhr); 58 | }, 59 | success: function (data, status, xhr) { 60 | el.trigger('ajax:success', [data, status, xhr]); 61 | }, 62 | complete: function (xhr) { 63 | el.trigger('ajax:complete', xhr); 64 | }, 65 | error: function (xhr, status, error) { 66 | el.trigger('ajax:failure', [xhr, status, error]); 67 | } 68 | }); 69 | } 70 | 71 | el.trigger('ajax:after'); 72 | } 73 | } 74 | }); 75 | 76 | /** 77 | * confirmation handler 78 | */ 79 | 80 | $('body').delegate('a[data-confirm], button[data-confirm], input[data-confirm]', 'click.rails', function () { 81 | var el = $(this); 82 | if (el.triggerAndReturn('confirm')) { 83 | if (!confirm(el.attr('data-confirm'))) { 84 | return false; 85 | } 86 | } 87 | }); 88 | 89 | 90 | 91 | /** 92 | * remote handlers 93 | */ 94 | $('form[data-remote]').live('submit.rails', function (e) { 95 | $(this).callRemote(); 96 | e.preventDefault(); 97 | }); 98 | 99 | $('a[data-remote],input[data-remote]').live('click.rails', function (e) { 100 | $(this).callRemote(); 101 | e.preventDefault(); 102 | }); 103 | 104 | $('a[data-method]:not([data-remote])').live('click.rails', function (e){ 105 | var link = $(this), 106 | href = link.attr('href'), 107 | method = link.attr('data-method'), 108 | form = $('
'), 109 | metadata_input = ''; 110 | 111 | if (csrf_param !== undefined && csrf_token !== undefined) { 112 | metadata_input += ''; 113 | } 114 | 115 | form.hide() 116 | .append(metadata_input) 117 | .appendTo('body'); 118 | 119 | e.preventDefault(); 120 | form.submit(); 121 | }); 122 | 123 | /** 124 | * disable-with handlers 125 | */ 126 | var disable_with_input_selector = 'input[data-disable-with]', 127 | disable_with_form_remote_selector = 'form[data-remote]:has(' + disable_with_input_selector + ')', 128 | disable_with_form_not_remote_selector = 'form:not([data-remote]):has(' + disable_with_input_selector + ')'; 129 | 130 | var disable_with_input_function = function () { 131 | $(this).find(disable_with_input_selector).each(function () { 132 | var input = $(this); 133 | input.data('enable-with', input.val()) 134 | .attr('value', input.attr('data-disable-with')) 135 | .attr('disabled', 'disabled'); 136 | }); 137 | }; 138 | 139 | $(disable_with_form_remote_selector).live('ajax:before.rails', disable_with_input_function); 140 | $(disable_with_form_not_remote_selector).live('submit.rails', disable_with_input_function); 141 | 142 | $(disable_with_form_remote_selector).live('ajax:complete.rails', function () { 143 | $(this).find(disable_with_input_selector).each(function () { 144 | var input = $(this); 145 | input.removeAttr('disabled') 146 | .val(input.data('enable-with')); 147 | }); 148 | }); 149 | 150 | var jqueryVersion = $().jquery; 151 | 152 | if ( (jqueryVersion === '1.4') || (jqueryVersion === '1.4.1') || (jqueryVersion === '1.4.2') ){ 153 | alert('This rails.js does not support the jQuery version you are using. Please read documentation.'); 154 | } 155 | 156 | }); 157 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/wc/norobots.html for documentation on how to use the robots.txt file 2 | # 3 | # To ban all spiders from the entire site uncomment the next two lines: 4 | # User-Agent: * 5 | # Disallow: / 6 | -------------------------------------------------------------------------------- /public/stylesheets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/railscasts/010f1927c9ccb9604d0af1c6326419bd5371b56c/public/stylesheets/.gitkeep -------------------------------------------------------------------------------- /public/stylesheets/application.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #FFF; 3 | font-family: Verdana, Helvetica, Arial; 4 | font-size: 14px; 5 | margin: 0; 6 | } 7 | 8 | a { 9 | color: #1B97F2; 10 | text-decoration: none; 11 | } 12 | 13 | a:hover { 14 | text-decoration: underline; 15 | } 16 | 17 | a img { 18 | border: none; 19 | } 20 | 21 | h1 { 22 | font-size: 24px; 23 | } 24 | 25 | h2 { 26 | font-size: 18px; 27 | } 28 | 29 | h3 { 30 | font-size: 14px; 31 | } 32 | 33 | h4 { 34 | font-size: 12px; 35 | } 36 | 37 | .clear { 38 | clear: both; 39 | } 40 | 41 | ul { 42 | margin: 4px 0; 43 | padding-left: 17px; 44 | } 45 | 46 | ul.horizontal { 47 | list-style: none; 48 | margin: 0; 49 | padding: 0; 50 | } 51 | 52 | ul.horizontal li { 53 | margin: 0; 54 | padding: 0; 55 | float: left; 56 | } 57 | 58 | #flash_notice, #flash_alert { 59 | padding: 10px 0; 60 | text-align: center; 61 | color: #FFF; 62 | } 63 | 64 | #flash_notice { 65 | background-color: #267923; 66 | } 67 | 68 | #flash_alert { 69 | background-color: #871F22; 70 | } 71 | 72 | #flash_notice a, #flash_alert a { 73 | color: #FFF; 74 | } 75 | 76 | .field { 77 | margin: 10px 0; 78 | } 79 | 80 | .special_email_field { 81 | display: none; 82 | } 83 | 84 | label { 85 | display: block; 86 | } 87 | 88 | label.check_box { 89 | display: inline; 90 | } 91 | 92 | 93 | /*** ERRORS ***/ 94 | 95 | .field_with_errors { 96 | display: inline; 97 | } 98 | 99 | .error_messages { 100 | width: 400px; 101 | border: 2px solid #CF0000; 102 | padding: 0px; 103 | padding-bottom: 12px; 104 | margin: 20px 0; 105 | background-color: #f0f0f0; 106 | font-size: 12px; 107 | } 108 | 109 | .error_messages h2 { 110 | text-align: left; 111 | font-weight: bold; 112 | padding: 5px 10px; 113 | font-size: 12px; 114 | margin: 0; 115 | background-color: #c00; 116 | color: #fff; 117 | } 118 | 119 | .error_messages p { 120 | margin: 8px 10px; 121 | } 122 | 123 | .error_messages ul { 124 | margin: 0; 125 | margin-left: 10px; 126 | } 127 | 128 | 129 | /*** SECTIONS ***/ 130 | 131 | #top { 132 | position: relative; 133 | } 134 | 135 | #top .logo { 136 | margin-top: 50px; 137 | margin-left: 100px; 138 | padding-bottom: 10px; 139 | } 140 | 141 | #top .subscribe { 142 | position: absolute; 143 | bottom: 10px; 144 | right: 100px; 145 | } 146 | 147 | #top .subscribe li { 148 | position: relative; 149 | margin-left: 8px; 150 | font-size: 12px; 151 | } 152 | 153 | #top .subscribe li .name { 154 | display: none; 155 | } 156 | 157 | #top .subscribe li:hover .name { 158 | display: block; 159 | position: absolute; 160 | top: -22px; left: -84px; width: 200px; 161 | color: #000; 162 | text-align: center; 163 | } 164 | 165 | #nav_bar { 166 | position: relative; 167 | padding: 8px 100px; 168 | background-color: #333; 169 | border-top: solid 1px #FFF; 170 | background: -webkit-gradient(linear, left top, left bottom, from(#5C5C5C), to(#111)); 171 | background: -moz-linear-gradient(top, #5C5C5C, #111); 172 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#5C5C5C', endColorstr='#111'); 173 | border-bottom: solid 3px #DE9F00; 174 | margin-bottom: 15px; 175 | } 176 | 177 | #nav_bar .nav { 178 | float: right; 179 | padding-top: 2px; 180 | } 181 | 182 | #nav_bar .nav li { 183 | padding-left: 25px; 184 | font-size: 14px; 185 | color: #BBB; 186 | } 187 | 188 | #nav_bar .nav li a { 189 | color: #FFF; 190 | } 191 | 192 | #main { 193 | padding-top: 15px; 194 | max-width: 1500px; 195 | } 196 | 197 | #footer { 198 | clear: both; 199 | font-size: 11px; 200 | text-align: center; 201 | padding: 25px 0; 202 | color: #555; 203 | } 204 | 205 | .content { 206 | margin: 0 100px; 207 | } 208 | 209 | .content { 210 | position: relative; 211 | } 212 | 213 | .actions { 214 | margin: 8px 0; 215 | } 216 | 217 | .actions a { 218 | padding: 3px 8px 2px 8px; 219 | font-size: 12px; 220 | color: #000; 221 | border: 1px solid #999; 222 | border-radius: 5px; 223 | -webkit-border-radius: 5px; 224 | -moz-border-radius: 5px; 225 | background: #DDD; 226 | background: -webkit-gradient(linear, left top, left bottom, from(#F5F5F5), to(#DDD)); 227 | background: -moz-linear-gradient(top, #F5F5F5, #DDD); 228 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#F5F5F5', endColorstr='#DDD'); 229 | text-decoration: none; 230 | } 231 | 232 | 233 | /*** EPISODES ***/ 234 | 235 | .episodes { 236 | margin-right: 200px; 237 | } 238 | 239 | .unreleased { 240 | color: #D00; 241 | font-weight: bold; 242 | padding-left: 5px; 243 | } 244 | 245 | .episodes h2 .remove { 246 | font-size: 14px; 247 | font-weight: normal; 248 | } 249 | 250 | .full .episode { 251 | padding-bottom: 18px; 252 | border-bottom: solid 1px #E5E5E5; 253 | margin-bottom: 20px; 254 | } 255 | 256 | .full .screenshot { 257 | float: left; 258 | } 259 | 260 | .screenshot img { 261 | border: solid 1px #999; 262 | display: block; 263 | } 264 | 265 | .full .main { 266 | margin-left: 215px; 267 | } 268 | 269 | .full .info { 270 | color: #999; 271 | font-size: 12px; 272 | } 273 | 274 | .full .number { 275 | text-transform: uppercase; 276 | font-weight: bold; 277 | } 278 | 279 | .full h2, .episode h2 a { 280 | color: #E98C08; 281 | } 282 | 283 | .full h2 { 284 | margin: 3px 0; 285 | } 286 | 287 | .full .description { 288 | margin-bottom: 17px; 289 | } 290 | 291 | .full .stats { 292 | font-size: 11px; 293 | color: #777; 294 | font-weight: normal; 295 | padding-left: 5px; 296 | } 297 | 298 | .full .stats a { 299 | color: #777; 300 | } 301 | 302 | .full .watch_button { 303 | font-size: 13px; 304 | padding: 5px 10px 6px 10px; 305 | font-weight: bold; 306 | border: 1px solid #466A98; 307 | border-radius: 4px; 308 | -webkit-border-radius: 4px; 309 | -moz-border-radius: 4px; 310 | color: #FFF; 311 | background: #386FB2; 312 | background: -webkit-gradient(linear, left top, left bottom, from(#67B0EF), to(#2B5EA4)); 313 | background: -moz-linear-gradient(top, #579CEA, #2B5EA4); 314 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#579CEA', endColorstr='#2B5EA4'); 315 | text-decoration: none; 316 | text-shadow: 0 -1px 0 #036; 317 | } 318 | 319 | .full .notes { 320 | font-size: 11px; 321 | margin-top: 10px; 322 | } 323 | 324 | .list { 325 | border-collapse: collapse; 326 | width: 100%; 327 | } 328 | 329 | .list td, .list th { 330 | border-bottom: solid 1px #CCC; 331 | } 332 | 333 | .list td { 334 | padding: 5px; 335 | } 336 | 337 | .list th { 338 | padding: 5px; 339 | text-align: left; 340 | color: #777; 341 | } 342 | 343 | .list .comment_count { 344 | color: #000; 345 | } 346 | 347 | 348 | .episodes .grid { 349 | margin-right: -30px; 350 | } 351 | 352 | .grid .episode { 353 | float: left; 354 | margin-right: 20px; 355 | margin-bottom: 50px; 356 | position: relative; 357 | } 358 | 359 | .grid .name { 360 | font-size: 13px; 361 | position: absolute; 362 | top: 135px; 363 | left: 0; 364 | } 365 | 366 | 367 | /*** FILTERS ***/ 368 | 369 | .episodes .filters { 370 | margin-bottom: 15px; 371 | } 372 | 373 | .episodes .filter { 374 | border: solid 1px #999; 375 | border-radius: 10px; 376 | -webkit-border-radius: 10px; 377 | -moz-border-radius: 10px; 378 | background-color: #DDD; 379 | padding: 2px 8px; 380 | margin-bottom: 10px; 381 | font-size: 12px; 382 | } 383 | 384 | .episodes .filter a { 385 | padding-left: 5px; 386 | } 387 | 388 | 389 | /*** PAGINATION ***/ 390 | 391 | .pagination { 392 | margin: 8px 0; 393 | font-size: 12px; 394 | } 395 | 396 | .pagination .disabled { 397 | color: #999; 398 | } 399 | 400 | .pagination em { 401 | font-style: normal; 402 | } 403 | 404 | .pagination a, .pagination em, .pagination .previous_page, .pagination .next_page { 405 | padding: 4px; 406 | } 407 | 408 | 409 | /*** SIDE ***/ 410 | 411 | .side { 412 | position: absolute; 413 | right: 0; 414 | top: 0; 415 | } 416 | 417 | .side .banner { 418 | margin-top: 15px; 419 | } 420 | 421 | .episode_views { 422 | padding: 0 20px; 423 | margin: 0; 424 | font-weight: normal; 425 | color: #444; 426 | font-size: 12px; 427 | text-align: center; 428 | border-radius: 6px; 429 | -webkit-border-radius: 6px; 430 | -moz-border-radius: 6px; 431 | border: solid 1px #BBB; 432 | background-color: #DDD; 433 | background: -webkit-gradient(linear, left top, left bottom, from(#EEE), to(#CCC)); 434 | background: -moz-linear-gradient(top, #EEE, #CCC); 435 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#EEE', endColorstr='#CCC'); 436 | margin-bottom: 14px; 437 | } 438 | 439 | .episode_views .view { 440 | padding-left: 3px; 441 | } 442 | 443 | .episode_views .view img { 444 | background-color: #FFF; 445 | background: -webkit-gradient(linear, left top, left bottom, from(#EEE), to(#FFF)); 446 | background: -moz-linear-gradient(top, #EEE, #FFF); 447 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#EEE', endColorstr='#FFF'); 448 | padding: 7px; 449 | vertical-align: middle; 450 | border: solid 1px #BBB; 451 | border-top: none; 452 | border-bottom: none; 453 | } 454 | 455 | .episode_views .view a img { 456 | background: none; 457 | padding: 2px; 458 | border: none; 459 | } 460 | 461 | 462 | /*** CATEGORIES ***/ 463 | 464 | .categories ul { 465 | list-style: none; 466 | margin: 0; 467 | padding: 0; 468 | border: solid 1px #BBB; 469 | border-top: none; 470 | padding: 5px 20px; 471 | border-bottom-left-radius: 5px; 472 | border-bottom-right-radius: 5px; 473 | -webkit-border-bottom-left-radius: 5px; 474 | -webkit-border-bottom-right-radius: 5px; 475 | -moz-border-radius-bottomleft: 5px; 476 | -moz-border-radius-bottomright: 5px; 477 | } 478 | 479 | .categories li { 480 | margin: 5px 0; 481 | font-size: 14px; 482 | } 483 | 484 | .categories h2 { 485 | padding: 4px 20px; 486 | margin: 0; 487 | font-weight: normal; 488 | color: #444; 489 | font-size: 14px; 490 | text-align: center; 491 | border-top-left-radius: 6px; 492 | border-top-right-radius: 6px; 493 | -webkit-border-top-left-radius: 6px; 494 | -webkit-border-top-right-radius: 6px; 495 | -moz-border-radius-topleft: 6px; 496 | -moz-border-radius-topright: 6px; 497 | border: solid 1px #BBB; 498 | border-bottom-color: #AAA; 499 | background-color: #DDD; 500 | background: -webkit-gradient(linear, left top, left bottom, from(#EEE), to(#CCC)); 501 | background: -moz-linear-gradient(top, #EEE, #CCC); 502 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#EEE', endColorstr='#CCC'); 503 | } 504 | 505 | 506 | /*** EPISODE ***/ 507 | 508 | #episode { 509 | margin: 0 auto; 510 | width: 962px; 511 | } 512 | 513 | #episode.legacy { 514 | width: 802px; 515 | } 516 | 517 | #episode #video_wrapper { 518 | border: solid 1px #777; 519 | margin-bottom: 14px; 520 | } 521 | 522 | #episode video { 523 | display: block; 524 | } 525 | 526 | #episode > .info { 527 | position: relative; 528 | margin-bottom: 15px; 529 | background-color: #FFF; 530 | } 531 | 532 | #episode .info .screenshot { 533 | float: left; 534 | padding-right: 14px; 535 | } 536 | 537 | #episode .info h1 { 538 | margin: 0; 539 | padding: 0; 540 | font-size: 20px; 541 | margin-bottom: 3px; 542 | } 543 | 544 | #episode .info h1 .position { 545 | color: #999; 546 | } 547 | 548 | #episode .info .details { 549 | color: #777; 550 | font-size: 12px; 551 | } 552 | 553 | #episode .info .watch { 554 | padding-bottom: 8px; 555 | } 556 | 557 | #episode .info .watch_button { 558 | font-size: 13px; 559 | padding: 5px 10px 6px 10px; 560 | font-weight: bold; 561 | border: 1px solid #466A98; 562 | border-radius: 4px; 563 | -webkit-border-radius: 4px; 564 | -moz-border-radius: 4px; 565 | color: #FFF; 566 | background: #386FB2; 567 | background: -webkit-gradient(linear, left top, left bottom, from(#67B0EF), to(#2B5EA4)); 568 | background: -moz-linear-gradient(top, #579CEA, #2B5EA4); 569 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#579CEA', endColorstr='#2B5EA4'); 570 | text-decoration: none; 571 | text-shadow: 0 -1px 0 #036; 572 | } 573 | 574 | #episode .info .details a { 575 | color: #777; 576 | } 577 | 578 | #episode .info .description { 579 | margin-top: 6px; 580 | margin-bottom: 18px; 581 | margin-right: 140px; 582 | } 583 | 584 | #episode.legacy .info .description { 585 | margin-right: 0; 586 | } 587 | 588 | #episode .info .social { 589 | position: absolute; 590 | right: -14px; 591 | bottom: 18px; 592 | } 593 | 594 | #episode .info .downloads { 595 | position: absolute; 596 | bottom: 0; 597 | right: 0; 598 | font-size: 13px; 599 | } 600 | 601 | #episode .info .downloads li { 602 | margin-left: 8px; 603 | } 604 | 605 | #episode .info .downloads li .overlay { 606 | font-size: 12px; 607 | display: none; 608 | color: #999; 609 | position: absolute; 610 | top: -22px; 611 | right: 110px; 612 | width: 220px; 613 | text-align: right; 614 | } 615 | 616 | #episode .info .downloads li:hover .overlay { 617 | display: block; 618 | } 619 | 620 | #episode .video_info .screenshot, #episode .video_info .description, #episode .video_info .watch { 621 | display: none; 622 | } 623 | 624 | #episode .nav { 625 | padding: 0 8px; 626 | margin: 0; 627 | height: 35px; 628 | font-weight: normal; 629 | font-size: 14px; 630 | text-align: center; 631 | border-top-left-radius: 6px; 632 | border-top-right-radius: 6px; 633 | -webkit-border-top-left-radius: 6px; 634 | -webkit-border-top-right-radius: 6px; 635 | -moz-border-radius-topleft: 6px; 636 | -moz-border-radius-topright: 6px; 637 | border: solid 1px #BBB; 638 | border-bottom-color: #AAA; 639 | background-color: #DDD; 640 | background: -webkit-gradient(linear, left top, left bottom, from(#EEE), to(#CCC)); 641 | background: -moz-linear-gradient(top, #EEE, #CCC); 642 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#EEE', endColorstr='#CCC'); 643 | } 644 | 645 | #episode .nav li { 646 | margin: 0 6px; 647 | margin-top: 9px; 648 | } 649 | 650 | #episode .nav li a { 651 | color: #393939; 652 | padding: 5px 8px; 653 | padding-bottom: 10px; 654 | } 655 | 656 | #episode .nav li a.selected { 657 | border: solid 1px #CCC; 658 | border-bottom: none; 659 | background-color: #F5F5F5; 660 | color: #555; 661 | border-top-left-radius: 5px; 662 | border-top-right-radius: 5px; 663 | -webkit-border-top-left-radius: 5px; 664 | -webkit-border-top-right-radius: 5px; 665 | -moz-border-radius-topleft: 5px; 666 | -moz-border-radius-topright: 5px; 667 | cursor: default; 668 | } 669 | 670 | #episode .nav li a.selected:hover { 671 | text-decoration: none; 672 | } 673 | 674 | #episode .nav .previous, #episode .nav .next { 675 | float: right; 676 | } 677 | 678 | #episode .nav_section { 679 | background-color: #F5F5F5; 680 | border: solid 1px #BBB; 681 | border-top: none; 682 | padding: 10px 20px; 683 | position: relative; 684 | } 685 | 686 | #episode .nav_section .progress { 687 | z-index: 100; 688 | position: absolute; 689 | top: 40px; 690 | left: 400px; 691 | background-color: #333; 692 | background-color: rgba(0, 0, 0, 0.70); 693 | border-radius: 15px; 694 | -webkit-border-radius: 15px; 695 | -moz-border-radius: 15px; 696 | padding: 50px; 697 | } 698 | 699 | #episode.legacy .nav_section .progress { 700 | left: 320px; 701 | } 702 | 703 | #episode .asciicasts { 704 | padding-right: 20px; 705 | } 706 | 707 | #episode .browse_code img { 708 | vertical-align: bottom; 709 | } 710 | 711 | #episode .show_notes ul { 712 | margin-top: -10px; /* quick hack to move resources list under headline */ 713 | padding-top: 0; 714 | margin-bottom: 20px; 715 | } 716 | 717 | #episode .episode_actions { 718 | float: right; 719 | } 720 | 721 | #episode .similar { 722 | margin-top: 15px; 723 | } 724 | 725 | #episode .similar .episode:last-child { 726 | border-bottom: none; 727 | } 728 | 729 | 730 | /*** COMMENTS ***/ 731 | 732 | #comments { 733 | margin-top: 10px; 734 | } 735 | 736 | .comment { 737 | margin: 20px 0; 738 | position: relative; 739 | } 740 | 741 | .comment .avatar { 742 | float: left; 743 | border: solid 1px #CCC; 744 | color: #FFF; 745 | padding: 4px; 746 | } 747 | 748 | .comment .avatar img { 749 | display: block; 750 | } 751 | 752 | .comment .user_actions { 753 | position: absolute; 754 | top: 72px; 755 | } 756 | 757 | .comment .main { 758 | margin-left: 83px; 759 | } 760 | 761 | .nested_comments { 762 | margin-left: 83px; 763 | } 764 | 765 | .nested_comments .nested_comments .nested_comments .nested_comments { 766 | margin-left: 0; 767 | } 768 | 769 | .comment .headline { 770 | padding: 5px 14px; 771 | font-size: 12px; 772 | background-color: #DDD; 773 | border: solid 1px #BBB; 774 | border-bottom: none; 775 | border-top-left-radius: 5px; 776 | border-top-right-radius: 5px; 777 | -webkit-border-top-left-radius: 5px; 778 | -webkit-border-top-right-radius: 5px; 779 | -moz-border-radius-topleft: 5px; 780 | -moz-border-radius-topright: 5px; 781 | background: -webkit-gradient(linear, left top, left bottom, from(#EEE), to(#CCC)); 782 | background: -moz-linear-gradient(top, #EEE, #CCC); 783 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#EEE', endColorstr='#CCC'); 784 | } 785 | 786 | .comment .position, .comment .name { 787 | font-weight: bold; 788 | } 789 | 790 | .comment .created_at { 791 | color: #666; 792 | padding-left: 8px; 793 | } 794 | 795 | .comment_content { 796 | padding: 1px 15px; 797 | background-color: #FFF; 798 | border: solid 1px #CCC; 799 | border-top: none; 800 | border-bottom-left-radius: 5px; 801 | border-bottom-right-radius: 5px; 802 | -webkit-border-bottom-left-radius: 5px; 803 | -webkit-border-bottom-right-radius: 5px; 804 | -moz-border-radius-bottomleft: 5px; 805 | -moz-border-radius-bottomright: 5px; 806 | } 807 | 808 | .comment .actions { 809 | margin-bottom: 12px; 810 | } 811 | 812 | .comment blockquote { 813 | margin: 0; 814 | padding-left: 10px; 815 | border-left: solid 5px #CCC; 816 | color: #777; 817 | } 818 | 819 | 820 | #add_comment { 821 | margin-top: 30px; 822 | } 823 | 824 | .formatting { 825 | font-size: 12px; 826 | margin: 8px 0; 827 | } 828 | 829 | .markdown_examples { 830 | margin-top: 5px; 831 | } 832 | 833 | .markdown_examples table { 834 | border-collapse: collapse; 835 | } 836 | 837 | .markdown_examples table td { 838 | border: solid 1px #CCC; 839 | padding: 2px 10px; 840 | } 841 | 842 | .markdown_examples .code_block { 843 | width: 200px; 844 | } 845 | 846 | 847 | /*** CODE BLOCKS ***/ 848 | 849 | .code_block { 850 | margin: 12px 0; 851 | } 852 | 853 | .code_header { 854 | position: relative; 855 | background-color: #E0E0E0; 856 | font-size: 12px; 857 | padding: 4px 7px; 858 | border: solid 1px #B6B6B6; 859 | border-bottom: none; 860 | } 861 | 862 | .code_header .clippy { 863 | position: absolute; 864 | top: 4px; 865 | right: 7px; 866 | } 867 | 868 | .clippy_label { 869 | position: absolute; 870 | right: 20px; 871 | top: 1px; 872 | text-align: right; 873 | width: 200px; 874 | font-size: 10px; 875 | color: #555; 876 | } 877 | 878 | .CodeRay { 879 | overflow: auto; 880 | border: 1px solid #777; 881 | border-top: none; 882 | padding: 5px 7px; 883 | font-size: 13px; 884 | margin: 0; 885 | line-height: 17px; 886 | } 887 | 888 | code { 889 | border: solid 1px #CCC; 890 | background-color: #EEE; 891 | font-family: 'Menlo', 'Courier New', 'Terminal', monospace; 892 | padding: 0 3px; 893 | } 894 | 895 | pre code { 896 | display: block; 897 | background-color: #EEE; 898 | padding: 5px 7px; 899 | } 900 | 901 | 902 | /*** ABOUT ***/ 903 | 904 | #about { 905 | margin-right: 300px; 906 | } 907 | 908 | #about .ryan { 909 | float: right; 910 | padding: 7px; 911 | margin-left: 50px; 912 | border: solid 1px #CCC; 913 | text-align: center; 914 | font-weight: bold; 915 | } 916 | 917 | #about .ryan img { 918 | display: block; 919 | padding-bottom: 5px; 920 | } 921 | 922 | #about h2 { 923 | margin-top: 25px; 924 | margin-bottom: 8px; 925 | } 926 | 927 | #about p { 928 | margin: 8px 0; 929 | } 930 | 931 | 932 | /*** FEEDBACK ***/ 933 | 934 | #feedback { 935 | width: 680px; 936 | float: left; 937 | } 938 | 939 | #feedback_instructions { 940 | margin-left: 700px; 941 | min-width: 225px; 942 | } 943 | 944 | #feedback_instructions li { 945 | margin: 5px 0; 946 | } 947 | 948 | #feedback h2.bar { 949 | padding: 4px 20px; 950 | margin: 0; 951 | font-weight: normal; 952 | color: #333; 953 | font-size: 14px; 954 | text-align: center; 955 | border-top-left-radius: 6px; 956 | border-top-right-radius: 6px; 957 | -webkit-border-top-left-radius: 6px; 958 | -webkit-border-top-right-radius: 6px; 959 | -moz-border-radius-topleft: 6px; 960 | -moz-border-radius-topright: 6px; 961 | border: solid 1px #BBB; 962 | border-bottom-color: #AAA; 963 | background-color: #DDD; 964 | background: -webkit-gradient(linear, left top, left bottom, from(#F3F3F3), to(#BBB)); 965 | background: -moz-linear-gradient(top, #F3F3F3, #BBB); 966 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#F3F3F3', endColorstr='#BBB'); 967 | } 968 | 969 | #feedback form { 970 | background-color: #EEE; 971 | padding: 10px 20px; 972 | border: solid 1px #BBB; 973 | border-top: none; 974 | border-bottom-left-radius: 5px; 975 | border-bottom-right-radius: 5px; 976 | -webkit-border-bottom-left-radius: 5px; 977 | -webkit-border-bottom-right-radius: 5px; 978 | -moz-border-radius-bottomleft: 5px; 979 | -moz-border-radius-bottomright: 5px; 980 | } 981 | 982 | #feedback .info { 983 | font-size: 13px; 984 | margin-bottom: 30px; 985 | } 986 | 987 | #feedback label { 988 | float: left; 989 | width: 160px; 990 | text-align: right; 991 | padding-right: 10px; 992 | padding-top: 2px; 993 | } 994 | 995 | #feedback .actions { 996 | padding-left: 170px; 997 | } 998 | 999 | #feedback input, #feedback textarea { 1000 | font-size: 14px; 1001 | } 1002 | 1003 | 1004 | /*** GIVE BACK ***/ 1005 | 1006 | #give_back { 1007 | float: left; 1008 | margin-right: 8px; 1009 | margin-bottom: 5px; 1010 | } 1011 | 1012 | #give_back_instructions { 1013 | font-size: 16px; 1014 | margin-top: 20px; 1015 | } 1016 | 1017 | #give_back_instructions ol { 1018 | margin: 0; 1019 | padding: 0; 1020 | } 1021 | 1022 | #give_back_instructions li { 1023 | padding: 0; 1024 | margin: 30px 28px; 1025 | margin-right: 0; 1026 | } 1027 | 1028 | #give_back_instructions li li { 1029 | padding: 0; 1030 | margin: 15px 28px; 1031 | margin-right: 0; 1032 | } 1033 | -------------------------------------------------------------------------------- /public/stylesheets/coderay.css: -------------------------------------------------------------------------------- 1 | .CodeRay, .CodeRay pre { 2 | font-family: 'Menlo', 'Courier New', 'Terminal', monospace; 3 | background-color: #232323; 4 | color: #E6E0DB; 5 | } 6 | .CodeRay pre { 7 | margin: 0px; 8 | padding: 0px; 9 | } 10 | 11 | .CodeRay .an { color:#E7BE69 } /* html attribute */ 12 | .CodeRay .c { color:#BC9358; font-style: italic; } /* comment */ 13 | .CodeRay .ch { color:#509E4F } /* escaped character */ 14 | .CodeRay .cl { color:#FFF } /* class */ 15 | .CodeRay .co { color:#FFF } /* constant */ 16 | .CodeRay .fl { color:#A4C260 } /* float */ 17 | .CodeRay .fu { color:#FFC56D } /* function */ 18 | .CodeRay .gv { color:#D0CFFE } /* global variable */ 19 | .CodeRay .i { color:#A4C260 } /* integer */ 20 | .CodeRay .il { background:#151515 } /* inline code */ 21 | .CodeRay .iv { color:#D0CFFE } /* instance variable */ 22 | .CodeRay .pp { color:#E7BE69 } /* doctype */ 23 | .CodeRay .r { color:#CB7832 } /* keyword */ 24 | .CodeRay .kw { color:#CB7832 } /* keyword */ 25 | .CodeRay .rx { color:#A4C260 } /* regex */ 26 | .CodeRay .s { color:#A4C260 } /* string */ 27 | .CodeRay .sy { color:#6C9CBD } /* symbol */ 28 | .CodeRay .ta { color:#E7BE69 } /* html tag */ 29 | .CodeRay .pc { color:#6C9CBD } /* boolean */ 30 | -------------------------------------------------------------------------------- /public/stylesheets/feeds.css: -------------------------------------------------------------------------------- 1 | #feeds_list { 2 | border-collapse: collapse; 3 | } 4 | 5 | #feeds_list td { 6 | border: 1px solid #999; 7 | padding: 4px 8px; 8 | } -------------------------------------------------------------------------------- /script/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application. 3 | 4 | APP_PATH = File.expand_path('../../config/application', __FILE__) 5 | require File.expand_path('../../config/boot', __FILE__) 6 | require 'rails/commands' 7 | -------------------------------------------------------------------------------- /script/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'fileutils' 3 | 4 | # copy config files 5 | Dir['config/examples/*'].each do |source| 6 | destination = "config/#{File.basename(source)}" 7 | unless File.exist? destination 8 | FileUtils.cp(source, destination) 9 | puts "Generated #{destination}" 10 | end 11 | end 12 | 13 | # run rake and setup tasks 14 | system "bundle" 15 | system "bundle exec rake db:create:all" 16 | system "bundle exec rake db:migrate --trace" 17 | -------------------------------------------------------------------------------- /spec/factories.rb: -------------------------------------------------------------------------------- 1 | Factory.define :episode do |f| 2 | f.name 'Foo Bar' 3 | f.description 'Lorem' 4 | f.notes 'Ipsum' 5 | f.seconds 600 6 | f.published_at Time.now 7 | end 8 | 9 | Factory.define :tag do |f| 10 | f.name "Bar" 11 | end 12 | 13 | Factory.define :comment do |f| 14 | f.content 'Hello world.' 15 | f.episode { |c| c.association(:episode) } 16 | f.user { |c| c.association(:user) } 17 | end 18 | 19 | Factory.define :user do |f| 20 | f.name "Foo Bar" 21 | f.sequence(:github_username) { |n| "foo#{n}" } 22 | f.sequence(:github_uid) { |n| n } 23 | f.sequence(:email) { |n| "foo#{n}@example.com" } 24 | f.email_on_reply true 25 | end 26 | 27 | Factory.define :feedback_message do |f| 28 | f.name "Foo Bar" 29 | f.content "Hello World" 30 | f.sequence(:email) { |n| "foo#{n}@example.com" } 31 | end 32 | -------------------------------------------------------------------------------- /spec/helpers/comments_helper_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../spec_helper' 2 | 3 | describe CommentsHelper do 4 | it "handles line breaks properly" do 5 | comment = Factory.build(:comment, :content => "foo\nbar\n\nbaz", :legacy => true) 6 | helper.format_comment(comment).should eq("

foo\n
bar

\n\n

baz

") 7 | end 8 | 9 | it "escapes html" do 10 | comment = Factory.build(:comment, :content => "", :legacy => true) 11 | helper.format_comment(comment).should eq("

<foo>

") 12 | end 13 | 14 | it "uses   for spaces at beginning of lines" do 15 | comment = Factory.build(:comment, :content => " foo bar", :legacy => true) 16 | helper.format_comment(comment).should eq("

  foo bar

") 17 | end 18 | 19 | it "uses markdown for non-legacy comments" do 20 | comment = Factory.build(:comment, :content => "**foo**", :legacy => false) 21 | helper.format_comment(comment).strip.should eq("

foo

") 22 | end 23 | 24 | it "adds http protocol to url if it doesn't exist" do 25 | helper.fix_url("foo.com").should eq("http://foo.com") 26 | helper.fix_url("http://foo.com").should eq("http://foo.com") 27 | helper.fix_url("https://foo.com").should eq("https://foo.com") 28 | helper.fix_url("javascript:foo").should eq("http://javascript:foo") 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /spec/lib/code_formatter_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../spec_helper' 2 | 3 | describe CodeFormatter do 4 | def format(text) 5 | CodeFormatter.new(text).to_html 6 | end 7 | 8 | it "determines language based on file path" do 9 | formatter = CodeFormatter.new("") 10 | formatter.language("unknown").should eq("unknown") 11 | formatter.language("hello.rb").should eq("ruby") 12 | formatter.language("hello.js").should eq("java_script") 13 | formatter.language("hello.css").should eq("css") 14 | formatter.language("hello.html.erb").should eq("rhtml") 15 | formatter.language("hello.yml").should eq("yaml") 16 | formatter.language("Gemfile").should eq("ruby") 17 | formatter.language("app.rake").should eq("ruby") 18 | formatter.language("foo.gemspec").should eq("ruby") 19 | formatter.language("rails console").should eq("ruby") 20 | formatter.language("hello.js.rjs").should eq("rjs") 21 | formatter.language("hello.scss").should eq("css") 22 | formatter.language("rails").should eq("ruby") 23 | formatter.language("foo.bar ").should eq("bar") 24 | formatter.language("foo ").should eq("foo") 25 | formatter.language("").should eq("text") 26 | formatter.language(nil).should eq("text") 27 | formatter.language("0```").should eq("text") 28 | end 29 | 30 | it "converts to markdown" do 31 | format("hello **world**").strip.should eq("

hello world

") 32 | end 33 | 34 | it "hard wraps return statements" do 35 | format("hello\nworld").strip.should eq("

hello
\nworld

") 36 | end 37 | 38 | it "autolinks a url" do 39 | format("http://www.example.com/").strip.should eq('

http://www.example.com/

') 40 | end 41 | 42 | it "formats code block" do 43 | # This could use some more extensive tests 44 | format("```\nfoo\n```").strip.should include("
") 45 | end 46 | 47 | it "handle back-slashes in code block" do 48 | # This could use some more extensive tests 49 | format("```\nf\\'oo\n```").strip.should include("f\\'oo") 50 | end 51 | 52 | it "does not allow html" do 53 | format("").strip.should eq("") 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /spec/mailers/mailer_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Mailer do 4 | describe "feedback" do 5 | let(:message) { Factory(:feedback_message) } 6 | let(:mail) { Mailer.feedback(message) } 7 | 8 | it "includes message with name and email" do 9 | mail.subject.should eq("RailsCasts Feedback from #{message.name}") 10 | mail.to.should eq(["feedback@railscasts.com"]) 11 | mail.from.should eq([message.email]) 12 | mail.body.encoded.should match(message.content) 13 | end 14 | end 15 | 16 | describe "comment_response" do 17 | let(:user) { Factory(:user) } 18 | let(:comment) { Factory(:comment) } 19 | let(:mail) { Mailer.comment_response(comment, user) } 20 | 21 | it "includes comment content and link to comment page" do 22 | mail.subject.should eq("Comment Response on RailsCasts") 23 | mail.to.should eq([user.email]) 24 | mail.from.should eq(["noreply@railscasts.com"]) 25 | mail.body.encoded.should include(comment.content) 26 | mail.body.encoded.should include(episode_url(comment.episode, :view => "comments")) 27 | mail.body.encoded.should include(unsubscribe_url(user.generated_unsubscribe_token)) 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /spec/models/ability_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | require "cancan/matchers" 3 | 4 | describe "Ability" do 5 | describe "as guest" do 6 | before(:each) do 7 | @ability = Ability.new(nil) 8 | end 9 | 10 | it "can only view and create users" do 11 | @ability.should be_able_to(:login, :users) 12 | @ability.should be_able_to(:show, :users) 13 | @ability.should be_able_to(:create, :users) 14 | @ability.should be_able_to(:unsubscribe, :users) 15 | @ability.should_not be_able_to(:update, :users) 16 | end 17 | 18 | it "can only view episodes which are published" do 19 | @ability.should be_able_to(:index, :episodes) 20 | @ability.should be_able_to(:show, Factory.build(:episode, :published_at => 2.days.ago)) 21 | @ability.should_not be_able_to(:show, Factory.build(:episode, :published_at => 2.days.from_now)) 22 | @ability.should_not be_able_to(:create, :episodes) 23 | @ability.should_not be_able_to(:update, :episodes) 24 | @ability.should_not be_able_to(:destroy, :episodes) 25 | end 26 | 27 | it "can access any info pages" do 28 | @ability.should be_able_to(:access, :info) 29 | end 30 | 31 | it "can create feedback messages" do 32 | @ability.should be_able_to(:create, :feedback_messages) 33 | end 34 | 35 | it "cannot even create comments" do 36 | @ability.should_not be_able_to(:create, :comments) 37 | @ability.should_not be_able_to(:update, :comments) 38 | @ability.should_not be_able_to(:destroy, :comments) 39 | @ability.should_not be_able_to(:index, :comments) 40 | end 41 | end 42 | 43 | describe "as normal user" do 44 | before(:each) do 45 | @user = Factory(:user) 46 | @ability = Ability.new(@user) 47 | end 48 | 49 | it "can update himself, but not other users" do 50 | @ability.should be_able_to(:show, User.new) 51 | @ability.should be_able_to(:login, :users) 52 | @ability.should be_able_to(:logout, :users) 53 | @ability.should be_able_to(:create, :users) 54 | @ability.should be_able_to(:update, @user) 55 | @ability.should_not be_able_to(:update, User.new) 56 | @ability.should_not be_able_to(:ban, :users) 57 | end 58 | 59 | it "can create comments and update/destroy within 15 minutes if he owns them" do 60 | @ability.should be_able_to(:create, :comments) 61 | @ability.should be_able_to(:update, Factory(:comment, :user => @user, :created_at => 10.minutes.ago)) 62 | @ability.should_not be_able_to(:update, Factory(:comment, :user => @user, :created_at => 20.minutes.ago)) 63 | @ability.should be_able_to(:destroy, Factory(:comment, :user => @user, :created_at => 10.minutes.ago)) 64 | @ability.should_not be_able_to(:destroy, Factory(:comment, :user => @user, :created_at => 20.minutes.ago)) 65 | @ability.should_not be_able_to(:destroy, Factory(:comment, :user => User.new, :created_at => 10.minutes.ago)) 66 | end 67 | 68 | it "can create feedback messages" do 69 | @ability.should be_able_to(:create, :feedback_messages) 70 | end 71 | 72 | it "can only view episodes which are published" do 73 | @ability.should be_able_to(:index, :episodes) 74 | @ability.should be_able_to(:show, Factory.build(:episode, :published_at => 2.days.ago)) 75 | @ability.should_not be_able_to(:show, Factory.build(:episode, :published_at => 2.days.from_now)) 76 | @ability.should_not be_able_to(:create, :episodes) 77 | @ability.should_not be_able_to(:update, :episodes) 78 | @ability.should_not be_able_to(:destroy, :episodes) 79 | end 80 | 81 | it "can access any info pages" do 82 | @ability.should be_able_to(:access, :info) 83 | end 84 | end 85 | 86 | describe "as banned user" do 87 | before(:each) do 88 | @user = Factory(:user, :banned_at => Time.now) 89 | @ability = Ability.new(@user) 90 | end 91 | 92 | it "cannot create or update comments" do 93 | @ability.should_not be_able_to(:create, :comments) 94 | @ability.should_not be_able_to(:update, :comments) 95 | end 96 | end 97 | 98 | describe "as moderator" do 99 | before(:each) do 100 | @user = Factory(:user, :moderator => true) 101 | @ability = Ability.new(@user) 102 | end 103 | 104 | it "can ban users" do 105 | @ability.should be_able_to(:ban, :users) 106 | end 107 | 108 | it "can revert versions" do 109 | @ability.should be_able_to(:revert, :versions) 110 | end 111 | 112 | it "can list, update and destroy any comments" do 113 | @ability.should be_able_to(:update, Factory(:comment, :user => User.new, :created_at => 20.minutes.ago)) 114 | @ability.should be_able_to(:destroy, Factory(:comment, :user => User.new, :created_at => 20.minutes.ago)) 115 | @ability.should be_able_to(:index, :comments) 116 | end 117 | 118 | it "can view episodes which are not yet published" do 119 | @ability.should be_able_to(:index, Factory.build(:episode, :published_at => 2.days.from_now)) 120 | @ability.should be_able_to(:show, Factory.build(:episode, :published_at => 2.days.from_now)) 121 | end 122 | 123 | it "can update episode show notes, nothing else" do 124 | @ability.should be_able_to(:update, :episodes, :notes) 125 | @ability.should_not be_able_to(:update, :episodes, :name) 126 | @ability.should_not be_able_to(:update, :episodes, :name) 127 | @ability.should_not be_able_to(:destroy, :episodes) 128 | end 129 | end 130 | 131 | describe "as admin" do 132 | it "can access all" do 133 | user = Factory(:user, :admin => true) 134 | ability = Ability.new(user) 135 | ability.should be_able_to(:access, :all) 136 | end 137 | end 138 | end 139 | -------------------------------------------------------------------------------- /spec/models/comment_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | require "ostruct" 3 | 4 | describe Comment do 5 | it "validates the presence of episode_id and content" do 6 | comment = Comment.new 7 | %w[episode_id content].each do |attr| 8 | comment.should have(1).error_on(attr) 9 | end 10 | end 11 | 12 | it "sets request based attributes" do 13 | comment = Factory.build(:comment, :site_url => 'example.com') 14 | comment.request = OpenStruct.new(:remote_ip => 'ip', :env => { 'HTTP_USER_AGENT' => 'agent', 'HTTP_REFERER' => 'referrer' }) 15 | comment.user_ip.should eq('ip') 16 | comment.user_agent.should eq('agent') 17 | comment.referrer.should eq('referrer') 18 | end 19 | 20 | it "sorts recent comments in descending order by created_at time" do 21 | Comment.delete_all 22 | c1 = Factory(:comment, :created_at => 2.weeks.ago) 23 | c2 = Factory(:comment, :created_at => Time.now) 24 | Comment.recent.should eq([c2, c1]) 25 | end 26 | 27 | it "notifies owners of all previous commenters except self" do 28 | c1 = Factory(:comment) 29 | c2a = Factory(:comment, :parent => c1) 30 | c2b = Factory(:comment, :parent => c1) 31 | c3 = Factory(:comment, :parent => c2a, :user => c2a.user) 32 | c3.notify_other_commenters 33 | email_count.should eq(1) 34 | last_email.to.should include(c1.user.email) 35 | end 36 | 37 | it "does not notify user when user does not want email" do 38 | c1 = Factory(:comment, :user => nil) 39 | c2 = Factory(:comment, :parent => c2, :user => Factory(:user, :email_on_reply => false)) 40 | c3 = Factory(:comment, :parent => c2, :user => Factory(:user, :email => "")) 41 | c4 = Factory(:comment, :parent => c3) 42 | c4.users_to_notify.should eq([]) 43 | end 44 | 45 | it "searches by comment site url" do 46 | c1 = Factory(:comment, :site_url => "http://example.com") 47 | c2 = Factory(:comment, :site_url => "http://example2.com") 48 | Comment.search("example.com").should eq([c1]) 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /spec/models/episode_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Episode do 4 | it "finds published" do 5 | a = Factory(:episode, :published_at => 2.weeks.ago) 6 | b = Factory(:episode, :published_at => 2.weeks.from_now) 7 | Episode.published.should include(a) 8 | Episode.published.should_not include(b) 9 | end 10 | 11 | it "finds unpublished" do 12 | a = Factory(:episode, :published_at => 2.weeks.ago) 13 | b = Factory(:episode, :published_at => 2.weeks.from_now) 14 | Episode.unpublished.should include(b) 15 | Episode.unpublished.should_not include(a) 16 | end 17 | 18 | it "sorts recent episodes in descending order" do 19 | Episode.delete_all 20 | e1 = Factory(:episode, :position => 1) 21 | e2 = Factory(:episode, :position => 2) 22 | Episode.recent.should eq([e2, e1]) 23 | end 24 | 25 | it "assigns tags to episodes" do 26 | episode = Factory(:episode, :tag_names => 'foo bar') 27 | episode.tags.map(&:name).should eq(%w[foo bar]) 28 | episode.tag_names.should eq('foo bar') 29 | end 30 | 31 | it "requires publication date and name" do 32 | episode = Episode.new 33 | episode.should have(1).error_on(:published_at) 34 | episode.should have(1).error_on(:name) 35 | end 36 | 37 | it "automatically generates permalink when creating episode" do 38 | episode = Factory(:episode, :name => ' Hello_ *World* 2.1. ') 39 | episode.permalink.should eq('hello-world-2-1') 40 | end 41 | 42 | it "includes position and permalink in to_param" do 43 | episode = Factory(:episode, :name => 'Foo Bar') 44 | episode.to_param.should eq("#{episode.position}-foo-bar") 45 | end 46 | 47 | it "translates single digit seconds into duration with minutes" do 48 | Episode.new(:seconds => 60*8+3).duration.should eq('8:03') 49 | end 50 | 51 | it "translates double digit seconds into duration with minutes" do 52 | Episode.new(:seconds => 60*8+12).duration.should eq('8:12') 53 | end 54 | 55 | it "returns nil for duration if seconds aren't set" do 56 | Episode.new(:seconds => nil).duration.should be_nil 57 | end 58 | 59 | it "parses duration into seconds" do 60 | Episode.new(:duration => '10:03').seconds.should eq(603) 61 | Episode.new(:duration => '').seconds.should be_nil 62 | end 63 | 64 | it "knows if it's the last published episode" do 65 | a = Factory(:episode, :published_at => 2.weeks.ago) 66 | b = Factory(:episode, :published_at => 1.week.ago) 67 | c = Factory(:episode, :published_at => 2.weeks.from_now) 68 | a.should_not be_last_published 69 | b.should be_last_published 70 | c.should_not be_last_published 71 | end 72 | 73 | it "has media.railscasts.com asset url" do 74 | episode = Factory(:episode, :name => "Hello world") 75 | episode.position = 23 76 | episode.asset_url("videos").should eq("http://media.railscasts.com/assets/episodes/videos/023-hello-world") 77 | episode.asset_url("videos", "mp4").should eq("http://media.railscasts.com/assets/episodes/videos/023-hello-world.mp4") 78 | end 79 | 80 | it "has files with file sizes" do 81 | episode = Factory(:episode, :name => "Hello world", :file_sizes => {"zip" => "12345"}) 82 | episode.files[0][:name].should eq("source code") 83 | episode.files.map { |f| f[:name] }.should eq(["source code", "mp4", "m4v", "webm", "ogv"]) 84 | episode.files.map { |f| f[:info] }.should eq(["Project Files in Zip", "Full Size H.264 Video", "Smaller H.264 Video", "Full Size VP8 Video", "Full Size Theora Video"]) 85 | episode.files[0][:url].should include("http://media.railscasts.com/assets/episodes/sources/") 86 | episode.files[0][:size].should eq(12345) 87 | end 88 | 89 | it "sets file size to zero when unknown" do 90 | episode = Factory(:episode, :name => "Hello world", :file_sizes => nil) 91 | episode.files[0][:size].should eq(0) 92 | end 93 | 94 | it "loads the file sizes for each file" do 95 | episode = Factory(:episode, :name => "Hello world") 96 | episode.position = 42 97 | %w[mp4 m4v webm ogv].each_with_index do |ext, index| 98 | FakeWeb.register_uri(:head, "http://media.railscasts.com/assets/episodes/videos/042-hello-world.#{ext}", :content_length => index) 99 | end 100 | FakeWeb.register_uri(:head, "http://media.railscasts.com/assets/episodes/sources/042-hello-world.zip", :content_length => 4) 101 | episode.load_file_sizes 102 | episode.file_sizes.should eq( 103 | "mp4" => "0", 104 | "m4v" => "1", 105 | "webm" => "2", 106 | "ogv" => "3", 107 | "zip" => "4" 108 | ) 109 | end 110 | 111 | it "returns nil as file size when response is not 200" do 112 | FakeWeb.register_uri(:head, "http://example.com/foo", :content_length => "123", :status => ["404", "Not Found"]) 113 | episode = Factory.build(:episode) 114 | episode.fetch_file_size("http://example.com/foo").should eq(nil) 115 | end 116 | 117 | it "has a full name which includes position" do 118 | Episode.delete_all 119 | Factory(:episode, :position => 123, :name => "Foo Bar").full_name.should eq('#123 Foo Bar') 120 | end 121 | 122 | it "knows the next and previous episode based on position" do 123 | Episode.delete_all 124 | e1 = Factory(:episode, :position => 1) 125 | e2 = Factory(:episode, :position => 6) 126 | e1.previous.should be_nil 127 | e1.next.should eq(e2) 128 | e2.next.should be_nil 129 | e2.previous.should eq(e1) 130 | end 131 | 132 | describe "primitive search" do 133 | before(:each) do 134 | Episode.delete_all 135 | APP_CONFIG['thinking_sphinx'] = false 136 | end 137 | 138 | it "looks in name, description, and notes" do 139 | e1 = Factory(:episode, :name => 'foo', :description => 'bar', :notes => 'baz', :published_at => 2.weeks.ago) 140 | e2 = Factory(:episode, :name => 'foo test bar', :description => 'baz', :published_at => 2.weeks.ago) 141 | e3 = Factory(:episode, :name => 'foo', :published_at => 2.weeks.ago) 142 | Episode.search_published('foo bar baz').should eq([e1, e2]) 143 | end 144 | 145 | it "does not find unpublished" do 146 | e1 = Factory(:episode, :name => 'foo', :published_at => 2.weeks.ago) 147 | e2 = Factory(:episode, :name => 'foo', :published_at => 2.weeks.from_now) 148 | Episode.search_published('foo').should eq([e1]) 149 | end 150 | end 151 | end 152 | -------------------------------------------------------------------------------- /spec/models/feedback_message_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe FeedbackMessage do 4 | it "validates the presence of name, email and content" do 5 | feedback_message = FeedbackMessage.new 6 | %w[name email content].each do |attr| 7 | feedback_message.should have(1).error_on(attr) 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /spec/models/tag_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Tag do 4 | it "finds or creates tags with names" do 5 | Tag.delete_all 6 | Tag.create!(:name => 'foo') 7 | tags = Tag.with_names(['foo', 'bar']) 8 | tags.should have(2).records 9 | Tag.find(:all).should eq(tags) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/models/tagging_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Tagging do 4 | before(:each) do 5 | @tagging = Tagging.new 6 | end 7 | 8 | it "is valid" do 9 | @tagging.should be_valid 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/models/user_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe User do 4 | it "creates unique token when saving" do 5 | User.create!.token.should_not == User.create!.token 6 | end 7 | 8 | it "creates from omniauth hash" do 9 | omniauth = {"provider" => "github", "uid" => "123", "user_info" => {}, "extra" => {"user_hash" => {}}} 10 | omniauth["user_info"]["email"] = "foo@example.com" 11 | omniauth["user_info"]["name"] = "Bar" 12 | omniauth["user_info"]["nickname"] = "foo" 13 | omniauth["user_info"]["urls"] = {"GitHub" => "githubsite", "Blog" => "customsite"} 14 | omniauth["extra"]["user_hash"]["gravatar_id"] = "avatar" 15 | user = User.create_from_omniauth(omniauth) 16 | user.email.should eq("foo@example.com") 17 | user.github_uid.should eq("123") 18 | user.github_username.should eq("foo") 19 | user.name.should eq("Bar") 20 | user.gravatar_token.should eq("avatar") 21 | user.site_url.should eq("customsite") 22 | user.email_on_reply.should be_true 23 | user 24 | end 25 | 26 | it "generates persistant unsubscribe_token" do 27 | user = Factory(:user) 28 | user.unsubscribe_token.should be_nil 29 | token = user.generated_unsubscribe_token 30 | user.reload.unsubscribe_token.should eq(token) 31 | user.generated_unsubscribe_token.should eq(token) 32 | end 33 | 34 | it "uses github name as display name when original is blank" do 35 | Factory(:user, :name => "", :github_username => "hank").display_name.should eq("hank") 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /spec/requests/comments_request_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "Comments request" do 4 | it "creates and replies when not signed in" do 5 | episode = Factory(:episode, :name => "Blast from the Past") 6 | visit episode_path(episode) 7 | click_on "0 Comments" 8 | click_on "sign in through GitHub" 9 | page.should have_content("New comment") 10 | click_on "Post Comment" 11 | page.should have_content("Invalid Fields") 12 | fill_in "comment_content", :with => "Hello world!" 13 | click_on "Post Comment" 14 | page.should have_content("Blast from the Past") 15 | page.should have_content("Hello world!") 16 | click_on "Reply" 17 | fill_in "comment_content", :with => "Hello back." 18 | click_on "Post Comment" 19 | page.should have_content("Hello back.") 20 | end 21 | 22 | it "send email to original authors when replying to comment" do 23 | comment = Factory(:comment) 24 | login 25 | visit episode_path(comment.episode, :view => "comments") 26 | click_on "Reply" 27 | fill_in "comment_content", :with => "Hello back." 28 | click_on "Post Comment" 29 | page.should have_content("Hello back.") 30 | last_email.to.should include(comment.user.email) 31 | end 32 | 33 | it "creates when banned" do 34 | login Factory(:user, :banned_at => Time.now) 35 | visit episode_path(Factory(:episode), :view => "comments") 36 | page.should have_content("banned") 37 | end 38 | 39 | it "updates a comment" do 40 | user = Factory(:user, :admin => true) 41 | login user 42 | episode = Factory(:episode, :name => "Blast from the Past") 43 | comment = Factory(:comment, :content => "Hello world!", :episode_id => episode.id) 44 | visit episode_path(episode, :view => "comments") 45 | click_on "Edit" 46 | fill_in "comment_content", :with => "" 47 | click_on "Update Comment" 48 | page.should have_content("Invalid Fields") 49 | fill_in "comment_content", :with => "Hello back." 50 | click_on "Update Comment" 51 | page.should have_content("Hello back.") 52 | comment.versions(true).size.should eq(2) 53 | comment.versions.last.whodunnit.to_i.should eq(user.id) 54 | end 55 | 56 | it "destroys a comment" do 57 | login Factory(:user, :admin => true) 58 | episode = Factory(:episode, :name => "Blast from the Past") 59 | Factory(:comment, :content => "Hello world!", :episode_id => episode.id) 60 | visit episode_path(episode, :view => "comments") 61 | click_on "Delete" 62 | page.should_not have_content("Hello world!") 63 | click_on "undo" 64 | page.should have_content("Hello world!") 65 | end 66 | 67 | it "lists and search recent comments" do 68 | login Factory(:user, :admin => true) 69 | Factory(:comment, :content => "Hello world!") 70 | Factory(:comment, :content => "Back to the Future", :site_url => "http://example.com") 71 | visit comments_path 72 | page.should have_content("Hello world!") 73 | page.should have_content("Back to the Future") 74 | fill_in "comment_search", :with => "Future" 75 | click_on "Search Comments" 76 | page.should_not have_content("Hello world!") 77 | page.should have_content("Back to the Future") 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /spec/requests/episodes_request_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "Episodes request" do 4 | it "lists published" do 5 | Factory(:episode, :name => "Blast from the Past", :published_at => 2.days.ago) 6 | Factory(:episode, :name => "Back to the Future", :published_at => 2.days.from_now) 7 | visit episodes_path 8 | page.should have_content("Blast from the Past") 9 | page.should_not have_content("Back to the Future") 10 | end 11 | 12 | it "provides RSS feed of episodes" do 13 | Factory(:episode, :name => "Blast from the Past", :published_at => 2.days.ago) 14 | visit episodes_path(:format => :rss) 15 | page.should have_content("Blast from the Past") 16 | end 17 | 18 | it "has different views" do 19 | Factory(:episode, :name => "Blast from the Past", :description => "I am from 1960", :published_at => 2.days.ago) 20 | visit episodes_path 21 | page.should have_content("I am from 1960") 22 | click_on "List View" 23 | page.should_not have_content("I am from 1960") 24 | page.should have_content("Duration") 25 | click_on "Grid View" 26 | page.should_not have_content("I am from 1960") 27 | page.should_not have_content("Duration") 28 | end 29 | 30 | it "searches by name" do 31 | Factory(:episode, :name => "Blast from the Past") 32 | Factory(:episode, :name => "Back to the Future") 33 | visit episodes_path 34 | fill_in "search", :with => "Blast" 35 | click_on "Search Episodes" 36 | page.should have_content("Blast from the Past") 37 | page.should_not have_content("Back to the Future") 38 | click_on "x" 39 | page.should have_content("Blast from the Past") 40 | page.should have_content("Back to the Future") 41 | end 42 | 43 | it "filters by tag" do 44 | Factory(:episode, :name => "Blast from the Past", :tags => [Factory.build(:tag, :name => "Oldtimes")]) 45 | Factory(:episode, :name => "Back to the Future") 46 | visit episodes_path 47 | click_on "Oldtimes" 48 | page.should have_content("Blast from the Past") 49 | page.should_not have_content("Back to the Future") 50 | click_on "x" 51 | page.should have_content("Blast from the Past") 52 | page.should have_content("Back to the Future") 53 | end 54 | 55 | it "contains show notes, comments, and similar episodes" do 56 | episode = Factory(:episode, :name => "Blast from the Past", :notes => "Show notes!", :position => 1) 57 | Factory(:comment, :content => "Hello world", :episode => episode) 58 | Factory(:episode, :name => "Star Wars", :position => 2) 59 | Factory(:episode, :name => "Past and Present", :position => 3) 60 | visit episodes_path 61 | click_on "Blast from the Past" 62 | page.should have_content("Blast from the Past") 63 | page.should have_content("Show notes!") 64 | page.should_not have_content("Hello world") 65 | click_on "1 Comment" 66 | page.should_not have_content("Show notes!") 67 | page.should have_content("Hello world") 68 | click_on "Similar Episodes" 69 | page.should_not have_content("Show notes!") 70 | page.should_not have_content("Hello world") 71 | page.should have_content("Past and Present") 72 | page.should_not have_content("Star Wars") 73 | click_on "Next Episode" 74 | page.should have_content("Star Wars") 75 | click_on "Previous Episode" 76 | page.should have_content("Blast from the Past") 77 | end 78 | 79 | it "redirects to episode when full permalink isn't used" do 80 | episode = Factory(:episode, :name => "Blast from the Past") 81 | visit episode_path("#{episode.position}-anything") 82 | page.current_path.should eq(episode_path("#{episode.position}-#{episode.permalink}")) 83 | end 84 | 85 | it "reports unauthorized access when attempting to create an episode as a normal user" do 86 | Factory(:episode, :name => "Blast from the Past", :published_at => 2.days.ago) 87 | visit new_episode_path 88 | page.should have_content("not authorized") 89 | end 90 | 91 | it "creates a new episode with default position" do 92 | FakeWeb.register_uri(:head, /.*/, :content_length => 123) 93 | login Factory(:user, :admin => true) 94 | visit episodes_path 95 | click_on "New Episode" 96 | click_on "Create" 97 | page.should have_content("Invalid Fields") 98 | fill_in "Name", :with => "Blast from the Past" 99 | fill_in "Duration", :with => "15:23" 100 | click_on "Create" 101 | page.current_path.should eq(episode_path(Episode.last)) 102 | page.should have_content("Blast from the Past") 103 | page.should have_content("15 minutes") 104 | Episode.last.file_size("mp4").should == 123 105 | end 106 | 107 | it "edits an episode as admin" do 108 | FakeWeb.register_uri(:head, /.*/, :content_length => 123) 109 | login Factory(:user, :admin => true) 110 | episode = Factory(:episode, :name => "Blast from the Past") 111 | visit episode_path(episode) 112 | click_on "Edit" 113 | fill_in "Name", :with => "" 114 | click_on "Update" 115 | page.should have_content("Invalid Fields") 116 | fill_in "Name", :with => "Back to the Future" 117 | click_on "Update" 118 | page.current_path.should eq(episode_path(episode)) 119 | page.should have_content("Back to the Future") 120 | end 121 | 122 | it "edits an episode show notes as moderator" do 123 | FakeWeb.register_uri(:head, /.*/, :content_length => 123) 124 | login Factory(:user, :moderator => true) 125 | episode = Factory(:episode) 126 | visit episode_path(episode) 127 | click_on "Edit" 128 | page.should_not have_content("Name") 129 | fill_in "Notes", :with => "Updating notes!" 130 | click_on "Update" 131 | page.should have_content("Updating notes!") 132 | episode.reload.file_size("mp4").should == 123 133 | end 134 | 135 | it "redirects /episodes/archive to episodes list" do 136 | visit "/episodes/archive" 137 | page.current_url.should eq(root_url(:view => "list")) 138 | end 139 | end 140 | -------------------------------------------------------------------------------- /spec/requests/feedback_messages_request_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "FeedbackMessages request" do 4 | before(:each) do 5 | ActionMailer::Base.deliveries = [] 6 | end 7 | 8 | it "sends an email to feedback@railscasts.com when feedback is submitted" do 9 | visit feedback_path 10 | click_button "Send" 11 | page.should have_content("Invalid Fields") 12 | fill_in "Name", :with => "Foo" 13 | fill_in "Email Address", :with => "foo@example.com" 14 | fill_in "Message", :with => "Hello" 15 | click_button "Send" 16 | page.should have_content("Thank you for the feedback.") 17 | ActionMailer::Base.deliveries.count.should eq(1) 18 | end 19 | 20 | it "includes logged in user email and name" do 21 | login Factory(:user, :name => "Foo Test", :email => "footest@example.com") 22 | visit feedback_path 23 | find_by_id("feedback_message_name").value.should eq("Foo Test") 24 | find_by_id("feedback_message_email").value.should eq("footest@example.com") 25 | end 26 | 27 | it "does not send an email when filling out fake email field (honeypot)" do 28 | ActionMailer::Base.deliveries = [] 29 | visit feedback_path 30 | fill_in "email", :with => "foo@example.com" 31 | fill_in "Name", :with => "Foo" 32 | fill_in "Email Address", :with => "foo@example.com" 33 | fill_in "Message", :with => "Hello" 34 | click_button "Send" 35 | page.should have_content("caught") 36 | ActionMailer::Base.deliveries.count.should eq(0) 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /spec/requests/info_request_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "Info request" do 4 | it "has about page" do 5 | visit about_path 6 | page.should have_content("About RailsCasts") 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /spec/requests/users_request_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "Users request" do 4 | it "shows profile" do 5 | user = Factory(:user, :name => "Jack Sparrow", :github_username => "jsparrow") 6 | visit user_path(user) 7 | page.should have_content(user.name) 8 | page.should have_content(user.github_username) 9 | page.should_not have_content("Edit Profile") 10 | end 11 | 12 | it "edits current user" do 13 | user = Factory(:user) 14 | login user 15 | visit user_path(user) 16 | page.should have_content("This is your profile.") 17 | click_on "Edit Profile" 18 | fill_in "Name", :with => "Leonardo" 19 | uncheck "user_email_on_reply" 20 | click_on "Update Profile" 21 | page.should have_content("Successfully updated profile") 22 | page.should have_content("Leonardo") 23 | user.reload.email_on_reply.should be_false 24 | end 25 | 26 | it "logs out current user" do 27 | user = Factory(:user) 28 | login user 29 | visit logout_path 30 | visit user_path(user) 31 | page.should_not have_content("This is your profile.") 32 | end 33 | 34 | it "logs in existing user and redirects to return_to parameter" do 35 | user = Factory(:user) 36 | OmniAuth.config.add_mock(:github, "uid" => user.github_uid) 37 | visit login_path(:return_to => user_path(user)) 38 | page.should have_content("This is your profile.") 39 | end 40 | 41 | it "logs in unknown profile and creates user" do 42 | User.count.should be_zero 43 | OmniAuth.config.add_mock(:github, "uid" => "54321") 44 | visit login_path 45 | page.current_path.should eq("/") 46 | User.count.should eq(1) 47 | User.last.github_uid.should eq("54321") 48 | end 49 | 50 | it "bans user as moderator" do 51 | user = Factory(:user, :moderator => true) 52 | login user 53 | bad_user = Factory(:user) 54 | comment = Factory(:comment, :user => bad_user) 55 | visit episode_path(comment.episode, :view => "comments") 56 | click_on "Ban User" 57 | bad_user.reload.should be_banned 58 | bad_user.comments.size.should eq(0) 59 | end 60 | 61 | it "unsubscribe a user from comment replies" do 62 | user = Factory(:user) 63 | visit unsubscribe_path(user.generated_unsubscribe_token) 64 | page.should have_content("unsubscribed") 65 | user.reload.email_on_reply.should be_false 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | if ENV['COVERAGE'] 2 | require "simplecov" 3 | SimpleCov.start "rails" 4 | end 5 | 6 | # This file is copied to spec/ when you run 'rails generate rspec:install' 7 | ENV["RAILS_ENV"] ||= 'test' 8 | require File.expand_path("../../config/environment", __FILE__) 9 | require "rspec/rails" 10 | require "capybara/rspec" 11 | 12 | # Capybara.javascript_driver = :webkit 13 | 14 | FakeWeb.allow_net_connect = false 15 | 16 | # Requires supporting ruby files with custom matchers and macros, etc, 17 | # in spec/support/ and its subdirectories. 18 | Dir[Rails.root.join("spec/support/**/*.rb")].each {|f| require f} 19 | 20 | OmniAuth.config.test_mode = true 21 | OmniAuth.config.mock_auth[:github] = { 22 | 'uid' => '12345', 23 | "user_info" => { 24 | "email" => "foo@example.com", 25 | "nickname" => "foobar", 26 | "name" => "Foo Bar" 27 | } 28 | } 29 | 30 | RSpec.configure do |config| 31 | # == Mock Framework 32 | # 33 | # If you prefer to use mocha, flexmock or RR, uncomment the appropriate line: 34 | # 35 | # config.mock_with :mocha 36 | # config.mock_with :flexmock 37 | # config.mock_with :rr 38 | 39 | # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures 40 | # config.fixture_path = "#{::Rails.root}/spec/fixtures" 41 | 42 | # If you're not using ActiveRecord, or you'd prefer not to run each of your 43 | # examples within a transaction, remove the following line or assign false 44 | # instead of true. 45 | config.use_transactional_fixtures = false 46 | 47 | config.before(:suite) do 48 | DatabaseCleaner.strategy = :transaction 49 | DatabaseCleaner.clean_with(:truncation) 50 | end 51 | 52 | config.before(:each) do 53 | FakeWeb.clean_registry 54 | reset_email 55 | if example.metadata[:js] 56 | DatabaseCleaner.strategy = :truncation 57 | else 58 | DatabaseCleaner.start 59 | end 60 | end 61 | 62 | config.after(:each) do 63 | DatabaseCleaner.clean 64 | if example.metadata[:js] 65 | DatabaseCleaner.strategy = :transaction 66 | end 67 | end 68 | 69 | config.include AuthMacros 70 | config.include MailerMacros 71 | end 72 | 73 | 74 | # quick hack to get Rack to be quiet about "warning: regexp match /.../n against to UTF-8 string" until we upgrade to Rails 3.1 with Rack 1.3 75 | module Rack 76 | module Utils 77 | def escape(s) 78 | CGI.escape(s.to_s) 79 | end 80 | def unescape(s) 81 | CGI.unescape(s) 82 | end 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /spec/support/auth_macros.rb: -------------------------------------------------------------------------------- 1 | module AuthMacros 2 | def login(user = nil) 3 | user ||= Factory(:user) 4 | OmniAuth.config.add_mock(:github, "uid" => user.github_uid) 5 | visit login_path 6 | @_current_user = user 7 | end 8 | 9 | def current_user 10 | @_current_user 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /spec/support/mailer_macros.rb: -------------------------------------------------------------------------------- 1 | module MailerMacros 2 | def last_email 3 | ActionMailer::Base.deliveries.last 4 | end 5 | 6 | def reset_email 7 | ActionMailer::Base.deliveries = [] 8 | end 9 | 10 | def email_count 11 | ActionMailer::Base.deliveries.size 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /tmp/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/railscasts/010f1927c9ccb9604d0af1c6326419bd5371b56c/tmp/.gitignore -------------------------------------------------------------------------------- /vendor/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/railscasts/010f1927c9ccb9604d0af1c6326419bd5371b56c/vendor/.gitignore -------------------------------------------------------------------------------- /vendor/plugins/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/railscasts/010f1927c9ccb9604d0af1c6326419bd5371b56c/vendor/plugins/.gitkeep --------------------------------------------------------------------------------