├── lib ├── tasks │ ├── .gitkeep │ ├── rubocop.rake │ ├── cache_cooker.rake │ ├── mailchimp.rake │ ├── import.rake │ ├── travis.rake │ ├── stripe.rake │ └── setup.rake ├── null_object.rb ├── md_mentions.rb ├── rake_exception_notification.rb ├── mail_chimp │ └── web_hooks.rb └── cache_cooker.rb ├── vendor ├── plugins │ └── .gitkeep └── assets │ ├── images │ ├── .gitkeep │ └── facebox │ │ ├── loading.gif │ │ └── closelabel.png │ ├── javascripts │ └── .gitkeep │ └── stylesheets │ ├── .gitkeep │ └── facebox.css ├── app ├── views │ ├── shared │ │ ├── _branded_footer.html.haml │ │ ├── _ios_icon.html.haml │ │ ├── _flash.html.haml │ │ ├── _sad_pinkie.html.haml │ │ ├── _broadcasts.html.haml │ │ ├── _navigation.html.haml │ │ └── _credit_card.html.haml │ ├── comments │ │ ├── update.text.erb │ │ ├── destroy.js.erb │ │ └── _show.html.haml │ ├── announcements │ │ ├── dismiss.js.erb │ │ ├── show.html.haml │ │ ├── index.html.haml │ │ └── _announcement.html.haml │ ├── user_email │ │ ├── dismiss_warning.js.coffee │ │ ├── update.js.coffee │ │ ├── _warning.html.haml │ │ └── change.html.haml │ ├── articles │ │ ├── _comments.html.haml │ │ ├── _explore_article.html.haml │ │ ├── _subway.html.haml │ │ ├── index.html.haml │ │ ├── _footer.html.haml │ │ ├── _header.html.haml │ │ ├── _archives.html.haml │ │ ├── shared.html.haml │ │ └── show.haml │ ├── broadcast_mailer │ │ └── broadcast.text.erb │ ├── admin │ │ ├── articles │ │ │ ├── new.html.haml │ │ │ ├── edit.html.haml │ │ │ ├── index.html.haml │ │ │ └── _form.html.haml │ │ ├── announcements │ │ │ ├── new.html.haml │ │ │ ├── edit.html.haml │ │ │ ├── _form.html.haml │ │ │ └── index.html.haml │ │ ├── reports │ │ │ └── index.html.haml │ │ └── broadcasts │ │ │ └── new.html.haml │ ├── users │ │ ├── mailchimp_yearly_billing.js.coffee │ │ ├── current_credit_card.html.haml │ │ ├── _sidebar.html.haml │ │ ├── destroy.html.haml │ │ ├── show.html.haml │ │ ├── edit.html.haml │ │ ├── _settings_page.html.haml │ │ ├── _form.html.haml │ │ ├── profile.html.haml │ │ ├── notifications.html.haml │ │ └── billing.html.haml │ ├── account_mailer │ │ ├── canceled.text.erb │ │ ├── mailchimp_yearly_billing.text.erb │ │ ├── payment_created.text.erb │ │ ├── card_expiring.text.erb │ │ ├── failed_payment.text.erb │ │ └── unsubscribed.text.erb │ ├── conversation_mailer │ │ ├── mentioned.text.erb │ │ ├── comment_made.text.erb │ │ └── started.text.erb │ ├── registration_mailer │ │ └── email_confirmation.text.erb │ ├── home │ │ ├── public_archives.html.haml │ │ ├── chat.html.haml │ │ ├── contact.html.haml │ │ ├── about.html.haml │ │ └── open_source.html.haml │ ├── layouts │ │ ├── landing.html.haml │ │ ├── application.html.haml │ │ └── maintenance.html.erb │ ├── sessions │ │ ├── expired_link.html.haml │ │ ├── failure.html.haml │ │ └── problems.html.haml │ └── subscriptions │ │ ├── _thanks.html.haml │ │ ├── new.html.haml │ │ └── redirect.html.haml ├── models │ ├── payment.rb │ ├── payment_log.rb │ ├── collection.rb │ ├── article_visit.rb │ ├── volume.rb │ ├── announcement.rb │ ├── payment_gateway.rb │ ├── secret_generator.rb │ ├── authorization.rb │ ├── credit_card.rb │ ├── payment_gateway │ │ └── mail_chimp.rb │ ├── shared_article.rb │ ├── subscription.rb │ ├── card_expirer.rb │ ├── broadcaster.rb │ ├── article_link.rb │ ├── comment.rb │ ├── authorization_link.rb │ ├── reports.rb │ ├── user_manager.rb │ ├── article.rb │ └── conversation_notifier.rb ├── assets │ ├── images │ │ ├── avatar.png │ │ ├── contact.jpg │ │ ├── divider.png │ │ ├── preview.png │ │ ├── santa.jpg │ │ ├── ecc_logo.png │ │ ├── tinyrobo.png │ │ ├── announcement.png │ │ ├── beta_badge.png │ │ ├── icons │ │ │ ├── chess.png │ │ │ ├── heart.png │ │ │ ├── ruby.png │ │ │ ├── tower.png │ │ │ ├── question.png │ │ │ ├── experiments.png │ │ │ └── nuts_bolts.png │ │ ├── logo │ │ │ ├── large.png │ │ │ └── ios-icon.png │ │ ├── payment │ │ │ ├── cvc.gif │ │ │ └── dolla_billz.jpg │ │ ├── ruby-divider.png │ │ ├── sad_pinkie.png │ │ ├── controls │ │ │ ├── close.png │ │ │ ├── next.png │ │ │ └── previous.png │ │ ├── ecc_logo_black.png │ │ ├── pr-subscribers.pdf │ │ ├── cross_scratches.png │ │ └── backgrounds │ │ │ └── clean_textile.png │ ├── javascripts │ │ ├── pjax.coffee │ │ ├── pr.toggle.js.coffee │ │ ├── github_redirect_warning.js.coffee │ │ ├── application.js │ │ └── save.js │ └── stylesheets │ │ ├── partials │ │ ├── _contact.sass │ │ ├── _chat.sass │ │ ├── _mobile.sass │ │ ├── _open_source.sass │ │ ├── _email_confirmation_warning.sass │ │ ├── _broadcasts.sass │ │ ├── _flash.sass │ │ ├── _announcements.sass │ │ ├── _sharebox.sass │ │ ├── _admin.sass │ │ ├── _payments.sass │ │ ├── _navigation.sass │ │ ├── _fonts.sass │ │ ├── _landing.sass │ │ ├── _archives.sass │ │ ├── _library.sass │ │ ├── _form.sass │ │ ├── _subscribe.sass │ │ ├── _layout.sass │ │ └── _articles.sass │ │ └── application.css.sass ├── controllers │ ├── admin │ │ ├── reports_controller.rb │ │ ├── magic_controller.rb │ │ ├── broadcasts_controller.rb │ │ ├── articles_controller.rb │ │ └── announcements_controller.rb │ ├── hooks_controller.rb │ ├── announcements_controller.rb │ ├── home_controller.rb │ ├── user_email_controller.rb │ ├── sessions_controller.rb │ ├── subscriptions_controller.rb │ └── comments_controller.rb ├── mailers │ ├── registration_mailer.rb │ ├── broadcast_mailer.rb │ ├── conversation_mailer.rb │ └── account_mailer.rb ├── helpers │ ├── article_helper.rb │ └── application_helper.rb └── decorators │ ├── collection_decorator.rb │ ├── volume_decorator.rb │ ├── comment_decorator.rb │ ├── article_decorator.rb │ ├── user_decorator.rb │ └── subscription_decorator.rb ├── .rvmrc.example ├── doc └── header.png ├── Procfile ├── db ├── dump.sql.bz2 ├── migrate │ ├── 20130816174433_add_slug_to_articles.rb │ ├── 20130809174926_add_summary_to_articles.rb │ ├── 20120803001039_add_url_to_announcements.rb │ ├── 20120831192230_add_access_token_to_user.rb │ ├── 20120801233245_add_position_to_collections.rb │ ├── 20150821164859_add_discourse_url_to_articles.rb │ ├── 20121011002134_add_coupon_code_to_subscriptions.rb │ ├── 20130820143254_add_share_token_to_user.rb │ ├── 20120319214410_add_beta_tester_field_to_users.rb │ ├── 20120328151105_add_volume_id_to_articles.rb │ ├── 20120406161709_add_account_disabled_field_to_user.rb │ ├── 20120831190102_add_status_to_users.rb │ ├── 20120914175304_add_notify_updates_to_users.rb │ ├── 20120407205455_add_collection_id_to_articles.rb │ ├── 20130920195554_add_recommended_to_articles.rb │ ├── 20111108180149_add_notify_comment_made_to_users.rb │ ├── 20120516152433_add_notifications_enabled_to_users.rb │ ├── 20120626145819_add_queue_to_delayed_jobs.rb │ ├── 20110912175336_add_issue_number_to_articles.rb │ ├── 20131114225709_add_interval_to_subscriptions.rb │ ├── 20111206143647_add_broadcast_columns_to_announcements.rb │ ├── 20120328150809_create_volumes.rb │ ├── 20120112195907_create_article_visits.rb │ ├── 20110830041617_add_admin_field_to_user.rb │ ├── 20110829215505_create_authorizations.rb │ ├── 20110829230541_add_github_nickname_to_user.rb │ ├── 20120407204814_create_collections.rb │ ├── 20110820183757_create_users.rb │ ├── 20110909193908_create_announcements.rb │ ├── 20131204201817_add_email_confirmed_to_users.rb │ ├── 20121019183135_create_credit_cards.rb │ ├── 20110830162757_create_shared_articles.rb │ ├── 20110901130928_create_comments.rb │ ├── 20110829215511_create_authorization_links.rb │ ├── 20110821020718_create_articles.rb │ ├── 20130215174537_create_payments.rb │ ├── 20110912173127_add_email_preferences_to_users.rb │ ├── 20110916140124_change_mail_columns_to_text.rb │ ├── 20110916131749_create_emails.rb │ ├── 20110916132222_create_delayed_jobs.rb │ └── 20120927154744_add_payment_models.rb └── seeds.rb ├── config ├── initializers │ ├── chat.rb │ ├── mailhopper.rb │ ├── secret_token.rb │ ├── domain_settings.rb │ ├── mailchimp_settings.rb │ ├── stripe.rb │ ├── cache_cooker_settings.rb │ ├── mime_types.rb │ ├── omniauth.rb │ ├── inflections.rb │ ├── backtrace_silencers.rb │ ├── session_store.rb │ ├── rails_security_workarounds.rb │ ├── exception_notifier.rb │ ├── mail_settings.rb │ ├── markdown.rb │ └── stripe_webhooks.rb ├── environment.rb ├── deploy │ ├── production.rb │ ├── old_production.rb │ └── vagrant.rb ├── boot.rb ├── locales │ └── en.yml ├── schedule.rb ├── database.yml.example ├── environments │ ├── development.rb │ ├── test.rb │ └── production.rb └── delayed_job.god ├── public ├── favicon.ico ├── fonts │ ├── folks-light.eot │ ├── folks-light.ttf │ ├── folks-light.woff │ ├── folks-normal.eot │ ├── folks-normal.ttf │ ├── inconsolata.eot │ ├── inconsolata.ttf │ ├── inconsolata.woff │ └── folks-normal.woff ├── robots.txt ├── 422.html ├── 404.html └── 500.html ├── config.ru ├── test ├── factories │ ├── authorization_factory.rb │ ├── credit_card.rb │ ├── comment_factory.rb │ ├── announcement_factory.rb │ ├── stripe.rb │ ├── article_factory.rb │ └── user_factory.rb ├── integration │ ├── authorization_failure_test.rb │ ├── account_cancelation_test.rb │ ├── profile_test.rb │ ├── edit_article_test.rb │ ├── disabled_accounts_test.rb │ ├── article_routing_test.rb │ ├── article_footnote_test.rb │ ├── confirm_user_email_test.rb │ ├── credit_card_expiration_test.rb │ ├── change_billing_interval_test.rb │ ├── broadcast_test.rb │ └── shared_article_test.rb ├── support │ ├── stripe │ │ └── invoice.rb │ ├── outbox.rb │ └── mini_contest.rb └── unit │ ├── mailers │ ├── account_mailer_test.rb │ └── broadcast_mailer_test.rb │ ├── user_manager_test.rb │ ├── decorators │ └── comment_decorator_test.rb │ ├── user_test.rb │ └── card_expirer_test.rb ├── script ├── delayed_job └── rails ├── .gitignore ├── .travis.yml ├── .rubocop.yml ├── Capfile ├── Rakefile ├── .env.example └── Gemfile /lib/tasks/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vendor/plugins/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vendor/assets/images/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vendor/assets/javascripts/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vendor/assets/stylesheets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/views/shared/_branded_footer.html.haml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.rvmrc.example: -------------------------------------------------------------------------------- 1 | rvm 1.9.2@practicing-ruby-web 2 | -------------------------------------------------------------------------------- /app/views/comments/update.text.erb: -------------------------------------------------------------------------------- 1 | <%= @comment.content %> -------------------------------------------------------------------------------- /app/views/announcements/dismiss.js.erb: -------------------------------------------------------------------------------- 1 | $('#broadcasts').slideUp(); -------------------------------------------------------------------------------- /app/views/comments/destroy.js.erb: -------------------------------------------------------------------------------- 1 | $('.comment[data-id=<%= @comment.id %>]').remove(); -------------------------------------------------------------------------------- /app/models/payment.rb: -------------------------------------------------------------------------------- 1 | class Payment < ActiveRecord::Base 2 | belongs_to :user 3 | end 4 | -------------------------------------------------------------------------------- /app/models/payment_log.rb: -------------------------------------------------------------------------------- 1 | class PaymentLog < ActiveRecord::Base 2 | belongs_to :user 3 | end -------------------------------------------------------------------------------- /app/views/user_email/dismiss_warning.js.coffee: -------------------------------------------------------------------------------- 1 | $("#email-confirmation-warning").fadeOut() 2 | -------------------------------------------------------------------------------- /app/models/collection.rb: -------------------------------------------------------------------------------- 1 | class Collection < ActiveRecord::Base 2 | has_many :articles 3 | end 4 | -------------------------------------------------------------------------------- /doc/header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elm-city-craftworks/practicing-ruby-web/HEAD/doc/header.png -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: script/rails s 2 | worker: bundle exec rake jobs:work 3 | mail: bundle exec mailcatcher -f 4 | -------------------------------------------------------------------------------- /db/dump.sql.bz2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elm-city-craftworks/practicing-ruby-web/HEAD/db/dump.sql.bz2 -------------------------------------------------------------------------------- /config/initializers/chat.rb: -------------------------------------------------------------------------------- 1 | CHAT_GUEST_URL = ENV["CHAT_GUEST_URL"] 2 | CHAT_LOGIN_URL = ENV["CHAT_LOGIN_URL"] 3 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elm-city-craftworks/practicing-ruby-web/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /app/views/shared/_ios_icon.html.haml: -------------------------------------------------------------------------------- 1 | %link{ :rel => 'apple-touch-icon', :href => image_path('logo/ios-icon.png') } 2 | -------------------------------------------------------------------------------- /config/initializers/mailhopper.rb: -------------------------------------------------------------------------------- 1 | Mailhopper::Base.setup do |config| 2 | config.default_delivery_method = :smtp 3 | end -------------------------------------------------------------------------------- /app/assets/images/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elm-city-craftworks/practicing-ruby-web/HEAD/app/assets/images/avatar.png -------------------------------------------------------------------------------- /app/assets/images/contact.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elm-city-craftworks/practicing-ruby-web/HEAD/app/assets/images/contact.jpg -------------------------------------------------------------------------------- /app/assets/images/divider.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elm-city-craftworks/practicing-ruby-web/HEAD/app/assets/images/divider.png -------------------------------------------------------------------------------- /app/assets/images/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elm-city-craftworks/practicing-ruby-web/HEAD/app/assets/images/preview.png -------------------------------------------------------------------------------- /app/assets/images/santa.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elm-city-craftworks/practicing-ruby-web/HEAD/app/assets/images/santa.jpg -------------------------------------------------------------------------------- /lib/null_object.rb: -------------------------------------------------------------------------------- 1 | class << (NullObject = Object.new) 2 | def method_missing(id, *a, &b) 3 | NullObject 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /public/fonts/folks-light.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elm-city-craftworks/practicing-ruby-web/HEAD/public/fonts/folks-light.eot -------------------------------------------------------------------------------- /public/fonts/folks-light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elm-city-craftworks/practicing-ruby-web/HEAD/public/fonts/folks-light.ttf -------------------------------------------------------------------------------- /public/fonts/folks-light.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elm-city-craftworks/practicing-ruby-web/HEAD/public/fonts/folks-light.woff -------------------------------------------------------------------------------- /public/fonts/folks-normal.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elm-city-craftworks/practicing-ruby-web/HEAD/public/fonts/folks-normal.eot -------------------------------------------------------------------------------- /public/fonts/folks-normal.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elm-city-craftworks/practicing-ruby-web/HEAD/public/fonts/folks-normal.ttf -------------------------------------------------------------------------------- /public/fonts/inconsolata.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elm-city-craftworks/practicing-ruby-web/HEAD/public/fonts/inconsolata.eot -------------------------------------------------------------------------------- /public/fonts/inconsolata.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elm-city-craftworks/practicing-ruby-web/HEAD/public/fonts/inconsolata.ttf -------------------------------------------------------------------------------- /public/fonts/inconsolata.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elm-city-craftworks/practicing-ruby-web/HEAD/public/fonts/inconsolata.woff -------------------------------------------------------------------------------- /app/assets/images/ecc_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elm-city-craftworks/practicing-ruby-web/HEAD/app/assets/images/ecc_logo.png -------------------------------------------------------------------------------- /app/assets/images/tinyrobo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elm-city-craftworks/practicing-ruby-web/HEAD/app/assets/images/tinyrobo.png -------------------------------------------------------------------------------- /public/fonts/folks-normal.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elm-city-craftworks/practicing-ruby-web/HEAD/public/fonts/folks-normal.woff -------------------------------------------------------------------------------- /app/assets/images/announcement.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elm-city-craftworks/practicing-ruby-web/HEAD/app/assets/images/announcement.png -------------------------------------------------------------------------------- /app/assets/images/beta_badge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elm-city-craftworks/practicing-ruby-web/HEAD/app/assets/images/beta_badge.png -------------------------------------------------------------------------------- /app/assets/images/icons/chess.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elm-city-craftworks/practicing-ruby-web/HEAD/app/assets/images/icons/chess.png -------------------------------------------------------------------------------- /app/assets/images/icons/heart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elm-city-craftworks/practicing-ruby-web/HEAD/app/assets/images/icons/heart.png -------------------------------------------------------------------------------- /app/assets/images/icons/ruby.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elm-city-craftworks/practicing-ruby-web/HEAD/app/assets/images/icons/ruby.png -------------------------------------------------------------------------------- /app/assets/images/icons/tower.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elm-city-craftworks/practicing-ruby-web/HEAD/app/assets/images/icons/tower.png -------------------------------------------------------------------------------- /app/assets/images/logo/large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elm-city-craftworks/practicing-ruby-web/HEAD/app/assets/images/logo/large.png -------------------------------------------------------------------------------- /app/assets/images/payment/cvc.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elm-city-craftworks/practicing-ruby-web/HEAD/app/assets/images/payment/cvc.gif -------------------------------------------------------------------------------- /app/assets/images/ruby-divider.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elm-city-craftworks/practicing-ruby-web/HEAD/app/assets/images/ruby-divider.png -------------------------------------------------------------------------------- /app/assets/images/sad_pinkie.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elm-city-craftworks/practicing-ruby-web/HEAD/app/assets/images/sad_pinkie.png -------------------------------------------------------------------------------- /app/assets/images/controls/close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elm-city-craftworks/practicing-ruby-web/HEAD/app/assets/images/controls/close.png -------------------------------------------------------------------------------- /app/assets/images/controls/next.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elm-city-craftworks/practicing-ruby-web/HEAD/app/assets/images/controls/next.png -------------------------------------------------------------------------------- /app/assets/images/ecc_logo_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elm-city-craftworks/practicing-ruby-web/HEAD/app/assets/images/ecc_logo_black.png -------------------------------------------------------------------------------- /app/assets/images/icons/question.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elm-city-craftworks/practicing-ruby-web/HEAD/app/assets/images/icons/question.png -------------------------------------------------------------------------------- /app/assets/images/logo/ios-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elm-city-craftworks/practicing-ruby-web/HEAD/app/assets/images/logo/ios-icon.png -------------------------------------------------------------------------------- /app/assets/images/pr-subscribers.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elm-city-craftworks/practicing-ruby-web/HEAD/app/assets/images/pr-subscribers.pdf -------------------------------------------------------------------------------- /app/assets/images/controls/previous.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elm-city-craftworks/practicing-ruby-web/HEAD/app/assets/images/controls/previous.png -------------------------------------------------------------------------------- /app/assets/images/cross_scratches.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elm-city-craftworks/practicing-ruby-web/HEAD/app/assets/images/cross_scratches.png -------------------------------------------------------------------------------- /app/assets/images/icons/experiments.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elm-city-craftworks/practicing-ruby-web/HEAD/app/assets/images/icons/experiments.png -------------------------------------------------------------------------------- /app/assets/images/icons/nuts_bolts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elm-city-craftworks/practicing-ruby-web/HEAD/app/assets/images/icons/nuts_bolts.png -------------------------------------------------------------------------------- /app/models/article_visit.rb: -------------------------------------------------------------------------------- 1 | # DEPRECATED 2 | # 3 | class ArticleVisit < ActiveRecord::Base 4 | belongs_to :user 5 | belongs_to :article 6 | end 7 | -------------------------------------------------------------------------------- /app/models/volume.rb: -------------------------------------------------------------------------------- 1 | class Volume < ActiveRecord::Base 2 | has_many :articles 3 | 4 | def name 5 | "Volume #{number}" 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/assets/images/payment/dolla_billz.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elm-city-craftworks/practicing-ruby-web/HEAD/app/assets/images/payment/dolla_billz.jpg -------------------------------------------------------------------------------- /vendor/assets/images/facebox/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elm-city-craftworks/practicing-ruby-web/HEAD/vendor/assets/images/facebox/loading.gif -------------------------------------------------------------------------------- /vendor/assets/images/facebox/closelabel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elm-city-craftworks/practicing-ruby-web/HEAD/vendor/assets/images/facebox/closelabel.png -------------------------------------------------------------------------------- /app/controllers/admin/reports_controller.rb: -------------------------------------------------------------------------------- 1 | module Admin 2 | class ReportsController < ApplicationController 3 | before_filter :admin_only 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/assets/images/backgrounds/clean_textile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elm-city-craftworks/practicing-ruby-web/HEAD/app/assets/images/backgrounds/clean_textile.png -------------------------------------------------------------------------------- /app/assets/javascripts/pjax.coffee: -------------------------------------------------------------------------------- 1 | jQuery -> 2 | $('.paginated-list .controls a').pjax('[data-pjax-container]') 3 | $('#settings-sidebar a').pjax('[data-pjax-container]') -------------------------------------------------------------------------------- /app/views/articles/_comments.html.haml: -------------------------------------------------------------------------------- 1 | - if @article.discourse_url.present? 2 | #comments 3 | = link_to "Discuss this article on discourse", @article.discourse_url 4 | -------------------------------------------------------------------------------- /config/initializers/secret_token.rb: -------------------------------------------------------------------------------- 1 | token = Rails.env.production? ? ENV["SECRET_TOKEN"] : ('x' * 30) 2 | 3 | PracticingRubyWeb::Application.config.secret_token = token 4 | -------------------------------------------------------------------------------- /app/models/announcement.rb: -------------------------------------------------------------------------------- 1 | class Announcement < ActiveRecord::Base 2 | belongs_to :author, :class_name => "User" 3 | 4 | scope :broadcasts, where(:broadcast => true) 5 | end 6 | -------------------------------------------------------------------------------- /app/models/payment_gateway.rb: -------------------------------------------------------------------------------- 1 | module PaymentGateway 2 | 3 | # TODO Enable MailChimp 4 | def self.for_user(user) 5 | PaymentGateway::Stripe.new(user) 6 | end 7 | 8 | end -------------------------------------------------------------------------------- /app/models/secret_generator.rb: -------------------------------------------------------------------------------- 1 | module SecretGenerator 2 | extend self 3 | 4 | def generate(length=30) 5 | (0...length).map{ ('a'..'z').to_a[rand(26)] }.join 6 | end 7 | end -------------------------------------------------------------------------------- /db/migrate/20130816174433_add_slug_to_articles.rb: -------------------------------------------------------------------------------- 1 | class AddSlugToArticles < ActiveRecord::Migration 2 | def change 3 | add_column :articles, :slug, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/assets/stylesheets/partials/_contact.sass: -------------------------------------------------------------------------------- 1 | #contact 2 | font-family: sans-serif 3 | img 4 | box-sizing: border-box 5 | +image-box 6 | margin: 0 auto 7 | width: 100% 8 | -------------------------------------------------------------------------------- /app/models/authorization.rb: -------------------------------------------------------------------------------- 1 | class Authorization < ActiveRecord::Base 2 | has_one :authorization_link 3 | belongs_to :user 4 | 5 | def confirmed? 6 | !!user 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /app/views/broadcast_mailer/broadcast.text.erb: -------------------------------------------------------------------------------- 1 | <%= @body.html_safe %> 2 | 3 | ---- 4 | 5 | You can customize your notification settings via the link below: 6 | 7 | <%= profile_settings_url %> -------------------------------------------------------------------------------- /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 PracticingRubyWeb::Application 5 | -------------------------------------------------------------------------------- /db/migrate/20130809174926_add_summary_to_articles.rb: -------------------------------------------------------------------------------- 1 | class AddSummaryToArticles < ActiveRecord::Migration 2 | def change 3 | add_column :articles, :summary, :text 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20120803001039_add_url_to_announcements.rb: -------------------------------------------------------------------------------- 1 | class AddUrlToAnnouncements < ActiveRecord::Migration 2 | def change 3 | add_column :announcements, :url, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20120831192230_add_access_token_to_user.rb: -------------------------------------------------------------------------------- 1 | class AddAccessTokenToUser < ActiveRecord::Migration 2 | def change 3 | add_column :users, :access_token, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /test/factories/authorization_factory.rb: -------------------------------------------------------------------------------- 1 | FactoryGirl.define do 2 | factory :authorization do |a| 3 | a.github_uid(12345) 4 | a.association(:user, :factory => :user) 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the rails application 2 | require File.expand_path('../application', __FILE__) 3 | 4 | # Initialize the rails application 5 | PracticingRubyWeb::Application.initialize! 6 | -------------------------------------------------------------------------------- /app/models/credit_card.rb: -------------------------------------------------------------------------------- 1 | class CreditCard < ActiveRecord::Base 2 | belongs_to :user 3 | 4 | def description 5 | "XXXX-XXXX-XXXX-#{last_four} #{expiration_month}/#{expiration_year}" 6 | end 7 | end -------------------------------------------------------------------------------- /db/migrate/20120801233245_add_position_to_collections.rb: -------------------------------------------------------------------------------- 1 | class AddPositionToCollections < ActiveRecord::Migration 2 | def change 3 | add_column :collections, :position, :integer 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/views/admin/articles/new.html.haml: -------------------------------------------------------------------------------- 1 | = form_for @article, :url => admin_articles_path, 2 | :html => { :'data-track-changes' => true } do |f| 3 | = render :partial => "form", :locals => {:f => f} 4 | 5 | -------------------------------------------------------------------------------- /app/views/user_email/update.js.coffee: -------------------------------------------------------------------------------- 1 | unless <%= @user.errors.any? %> 2 | $.facebox "

Email updated and confirmation sent. Thanks!

" 3 | else 4 | alert "<%= @user.errors.full_messages.join(', ') %>" 5 | -------------------------------------------------------------------------------- /config/deploy/production.rb: -------------------------------------------------------------------------------- 1 | set :deploy_to, "/home/deploy" 2 | set :user, "deploy" 3 | 4 | server "practicingruby.com", :app, :web, :db, :primary => true 5 | 6 | after 'deploy:restart', 'unicorn:restart' 7 | -------------------------------------------------------------------------------- /config/initializers/domain_settings.rb: -------------------------------------------------------------------------------- 1 | url_options = { :host => ENV["HOST"] } 2 | 3 | ActionMailer::Base.default_url_options = url_options 4 | Rails.application.routes.default_url_options = url_options 5 | -------------------------------------------------------------------------------- /db/migrate/20150821164859_add_discourse_url_to_articles.rb: -------------------------------------------------------------------------------- 1 | class AddDiscourseUrlToArticles < ActiveRecord::Migration 2 | def change 3 | add_column :articles, :discourse_url, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /script/delayed_job: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require File.expand_path(File.join(File.dirname(__FILE__), '..', 'config', 'environment')) 4 | require 'delayed/command' 5 | Delayed::Command.new(ARGV).daemonize 6 | -------------------------------------------------------------------------------- /app/controllers/hooks_controller.rb: -------------------------------------------------------------------------------- 1 | class HooksController < ApplicationController 2 | def receive 3 | webhooks = MailChimp::WebHooks.new(params) 4 | 5 | render :text => webhooks.process 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20121011002134_add_coupon_code_to_subscriptions.rb: -------------------------------------------------------------------------------- 1 | class AddCouponCodeToSubscriptions < ActiveRecord::Migration 2 | def change 3 | add_column :subscriptions, :coupon_code, :text 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/views/users/mailchimp_yearly_billing.js.coffee: -------------------------------------------------------------------------------- 1 | $.facebox "Thanks for switching to yearly billing! We've recieved your request 2 | and will be in touch shortly to finish up the process.", 'confirm-interval-change' 3 | -------------------------------------------------------------------------------- /db/migrate/20130820143254_add_share_token_to_user.rb: -------------------------------------------------------------------------------- 1 | class AddShareTokenToUser < ActiveRecord::Migration 2 | def change 3 | change_table :users do |t| 4 | t.string :share_token 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .bundle 2 | db/*.sqlite3 3 | log/*.log 4 | tmp/ 5 | .rvmrc 6 | config/database.yml 7 | lib/development_mail_interceptor.rb 8 | .sass-cache/ 9 | public/assets 10 | coverage 11 | .kick 12 | .pow* 13 | .env 14 | -------------------------------------------------------------------------------- /app/assets/javascripts/pr.toggle.js.coffee: -------------------------------------------------------------------------------- 1 | $(document).on 'click', 'a[data-toggle]', (e) -> 2 | target = $(this).data('toggle') 3 | $(target).toggleClass('visible') 4 | $.post '/toggle_nav' 5 | e.preventDefault() 6 | -------------------------------------------------------------------------------- /config/initializers/mailchimp_settings.rb: -------------------------------------------------------------------------------- 1 | MailChimpSettings = { 2 | :list_id => ENV["MAILCHIMP_LIST_ID"], 3 | :api_key => ENV["MAILCHIMP_API_KEY"], 4 | :webhook_key => ENV["MAILCHIMP_WEBHOOK_KEY"] 5 | } 6 | -------------------------------------------------------------------------------- /config/initializers/stripe.rb: -------------------------------------------------------------------------------- 1 | STRIPE_SECRET_KEY = ENV["STRIPE_SECRET_KEY"] 2 | STRIPE_PUBLISHABLE_KEY = ENV["STRIPE_PUBLISHABLE_KEY"] 3 | STRIPE_WEBHOOK_PATH = ENV["TRAVIS"] ? "a/b/c/d" : ENV["STRIPE_WEBHOOK_PATH"] 4 | -------------------------------------------------------------------------------- /db/migrate/20120319214410_add_beta_tester_field_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddBetaTesterFieldToUsers < ActiveRecord::Migration 2 | def change 3 | add_column :users, :beta_tester, :boolean, :default => false 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20120328151105_add_volume_id_to_articles.rb: -------------------------------------------------------------------------------- 1 | class AddVolumeIdToArticles < ActiveRecord::Migration 2 | def change 3 | change_table :articles do |t| 4 | t.belongs_to :volume 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /config/boot.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | 3 | # Set up gems listed in the Gemfile. 4 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 5 | 6 | require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE']) 7 | -------------------------------------------------------------------------------- /app/assets/stylesheets/partials/_chat.sass: -------------------------------------------------------------------------------- 1 | #chat 2 | text-align: center 3 | font-family: sans-serif 4 | 5 | a.btn 6 | margin: 10px 0 7 | width: 300px 8 | p 9 | font-size: 14px 10 | text-align: center 11 | -------------------------------------------------------------------------------- /db/migrate/20120406161709_add_account_disabled_field_to_user.rb: -------------------------------------------------------------------------------- 1 | class AddAccountDisabledFieldToUser < ActiveRecord::Migration 2 | def change 3 | add_column :users, :account_disabled, :boolean, :default => false 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20120831190102_add_status_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddStatusToUsers < ActiveRecord::Migration 2 | def change 3 | add_column :users, :status, :string 4 | add_column :users, :contact_email, :string 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20120914175304_add_notify_updates_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddNotifyUpdatesToUsers < ActiveRecord::Migration 2 | def change 3 | add_column :users, :notify_updates, :boolean, :default => true, :null => false 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/views/shared/_flash.html.haml: -------------------------------------------------------------------------------- 1 | #flash 2 | - flash.each do |flashtype, message| 3 | %div{:class => "flash #{flashtype}", :id => "flash-#{flashtype}"}= message 4 | :javascript 5 | setTimeout("$('#flash').slideUp()", 4000); 6 | -------------------------------------------------------------------------------- /config/initializers/cache_cooker_settings.rb: -------------------------------------------------------------------------------- 1 | CacheCooker.base_uri ENV["CACHE_COOKER_URI"] 2 | CacheCooker.digest_auth ENV["CACHE_COOKER_USERNAME"], ENV["CACHE_COOKER_PASSWORD"] 3 | CacheCooker.realm ENV["CACHE_COOKER_REALM"] 4 | -------------------------------------------------------------------------------- /db/migrate/20120407205455_add_collection_id_to_articles.rb: -------------------------------------------------------------------------------- 1 | class AddCollectionIdToArticles < ActiveRecord::Migration 2 | def change 3 | change_table :articles do |t| 4 | t.belongs_to :collection 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20130920195554_add_recommended_to_articles.rb: -------------------------------------------------------------------------------- 1 | class AddRecommendedToArticles < ActiveRecord::Migration 2 | def change 3 | add_column :articles, :recommended, :boolean, :default => false, :null => false 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /test/factories/credit_card.rb: -------------------------------------------------------------------------------- 1 | FactoryGirl.define do 2 | factory :credit_card do |c| 3 | c.last_four("1234") 4 | c.expiration_month { Date.today.month } 5 | c.expiration_year { Date.today.year } 6 | c.user 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /db/migrate/20111108180149_add_notify_comment_made_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddNotifyCommentMadeToUsers < ActiveRecord::Migration 2 | def change 3 | add_column :users, :notify_comment_made, :boolean, :default => false, :null => false 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/views/articles/_explore_article.html.haml: -------------------------------------------------------------------------------- 1 | %div{class: "row_#{index}"} 2 | = link_to article_path(article) do 3 | %strong= article.list_title 4 | .description 5 | #{article.issue_number}, #{article.published_time.strftime("%B %Y")} 6 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /test/integration/authorization_failure_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class AuthorizationFailureTest < ActionDispatch::IntegrationTest 4 | test "auth failure page should load successfully" do 5 | visit auth_failure_path 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20120516152433_add_notifications_enabled_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddNotificationsEnabledToUsers < ActiveRecord::Migration 2 | def change 3 | add_column :users, :notifications_enabled, :boolean, :null => false, :default => false 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/assets/javascripts/github_redirect_warning.js.coffee: -------------------------------------------------------------------------------- 1 | $(document).on 'click', 2 | "a[href='/subscriptions/new'][data-redirect-warning='true']", 3 | (e) -> 4 | e.preventDefault() 5 | $.facebox {ajax: '/subscriptions/redirect'}, 'redirect-warning' 6 | -------------------------------------------------------------------------------- /app/mailers/registration_mailer.rb: -------------------------------------------------------------------------------- 1 | class RegistrationMailer < ActionMailer::Base 2 | def email_confirmation(user) 3 | @user = user 4 | mail(:to => @user.contact_email, 5 | :subject => "Confirm your Practicing Ruby subscription") 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/models/payment_gateway/mail_chimp.rb: -------------------------------------------------------------------------------- 1 | module PaymentGateway 2 | class MailChimp 3 | 4 | def initialize(user) 5 | @user = user 6 | end 7 | 8 | def subscribe(params = {}) 9 | raise NotImplementedError 10 | end 11 | end 12 | end -------------------------------------------------------------------------------- /app/views/admin/announcements/new.html.haml: -------------------------------------------------------------------------------- 1 | - content_for(:header) { "New Announcement" } 2 | 3 | = form_for @announcement, :url => admin_announcements_path, 4 | :html => { :'data-track-changes' => true } do |f| 5 | = render :partial => "form", :locals => {:f => f} -------------------------------------------------------------------------------- /app/models/shared_article.rb: -------------------------------------------------------------------------------- 1 | class SharedArticle < ActiveRecord::Base 2 | before_create do 3 | write_attribute(:secret, SecretGenerator.generate(12)) 4 | write_attribute(:views, 0) 5 | end 6 | 7 | belongs_to :user 8 | belongs_to :article 9 | end 10 | -------------------------------------------------------------------------------- /app/views/account_mailer/canceled.text.erb: -------------------------------------------------------------------------------- 1 | Name: <%= @user.name || 'Unknown' %> 2 | Github: <%= @user.github_nickname %> 3 | Email: <%= @user.contact_email %> 4 | Payment Provider: <%= @user.payment_provider || 'None' %> 5 | Provider Id: <%= @user.payment_provider_id || "N/A" %> 6 | -------------------------------------------------------------------------------- /app/views/shared/_sad_pinkie.html.haml: -------------------------------------------------------------------------------- 1 | = image_tag "sad_pinkie.png", :class => "sad_pinkie", 2 | :title => "Sad Pinkie: http://regolithx.deviantart.com/art/Sad-Pinkie-Pie-Vector-285845297" 3 | // Image Credit: http://regolithx.deviantart.com/art/Sad-Pinkie-Pie-Vector-285845297 4 | -------------------------------------------------------------------------------- /app/views/admin/announcements/edit.html.haml: -------------------------------------------------------------------------------- 1 | - content_for(:header) { "Edit Announcement" } 2 | 3 | = form_for @announcement, :url => admin_announcement_path(@announcement), 4 | :html => { :'data-track-changes' => true } do |f| 5 | = render :partial => "form", :locals => {:f => f} -------------------------------------------------------------------------------- /app/views/announcements/show.html.haml: -------------------------------------------------------------------------------- 1 | - content_for(:title) { @announcement.title } 2 | 3 | #announcements 4 | = render :partial => "announcement", 5 | :locals => {:announcement => @announcement} 6 | 7 | %p= link_to "← back to announcements".html_safe, announcements_path -------------------------------------------------------------------------------- /db/migrate/20120626145819_add_queue_to_delayed_jobs.rb: -------------------------------------------------------------------------------- 1 | class AddQueueToDelayedJobs < ActiveRecord::Migration 2 | def self.up 3 | add_column :delayed_jobs, :queue, :string 4 | end 5 | 6 | def self.down 7 | remove_column :delayed_jobs, :queue 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.0.0 4 | before_script: 5 | - bundle exec rake travis:setup 6 | - export DISPLAY=:99.0 7 | - sh -e /etc/init.d/xvfb start 8 | notifications: 9 | email: 10 | - jordan.byron@gmail.com 11 | - gregory.t.brown@gmail.com 12 | -------------------------------------------------------------------------------- /app/assets/stylesheets/partials/_mobile.sass: -------------------------------------------------------------------------------- 1 | @media only screen and (max-device-width: 480px) 2 | // TODO Re-evaluate font size in mobile safari 3 | // body 4 | // font-size: 25px 5 | pre 6 | overflow: scroll 7 | -webkit-overflow-scrolling: touch 8 | font-size: 0.75em -------------------------------------------------------------------------------- /app/views/user_email/_warning.html.haml: -------------------------------------------------------------------------------- 1 | - if show_email_warning? 2 | #email-confirmation-warning 3 | Your email address isn't verified yet. Please check your inbox, or 4 | = link_to "update your contact info", change_email_path, 5 | :rel => "facebox" 6 | to try again. 7 | -------------------------------------------------------------------------------- /db/migrate/20110912175336_add_issue_number_to_articles.rb: -------------------------------------------------------------------------------- 1 | class AddIssueNumberToArticles < ActiveRecord::Migration 2 | def self.up 3 | add_column :articles, :issue_number, :text 4 | end 5 | 6 | def self.down 7 | remove_column :articles, :issue_number 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /app/views/account_mailer/mailchimp_yearly_billing.text.erb: -------------------------------------------------------------------------------- 1 | Name: <%= @user.name || 'Unknown' %> 2 | Github: <%= @user.github_nickname %> 3 | Email: <%= @user.contact_email %> 4 | Payment Provider: <%= @user.payment_provider || 'None' %> 5 | Provider Id: <%= @user.payment_provider_id || "N/A" %> 6 | -------------------------------------------------------------------------------- /db/migrate/20131114225709_add_interval_to_subscriptions.rb: -------------------------------------------------------------------------------- 1 | class AddIntervalToSubscriptions < ActiveRecord::Migration 2 | def change 3 | add_column :subscriptions, :interval, :string, default: 'month' 4 | rename_column :subscriptions, :monthly_rate_cents, :rate_cents 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /test/factories/comment_factory.rb: -------------------------------------------------------------------------------- 1 | FactoryGirl.define do 2 | sequence(:comment_body) { |n| "Comment #{n}" } 3 | 4 | factory :comment do |c| 5 | c.body { |_| FactoryGirl.generate(:comment_body) } 6 | c.commentable { FactoryGirl.create(:article) } 7 | c.association :user 8 | end 9 | end -------------------------------------------------------------------------------- /app/assets/stylesheets/partials/_open_source.sass: -------------------------------------------------------------------------------- 1 | body.home-open_source 2 | font-family: sans-serif 3 | header h1 4 | font-size: 27px 5 | font-weight: bold 6 | h3 7 | font-size: 1.5em 8 | li 9 | list-style-type: circle 10 | margin-bottom: 10px 11 | text-align: justify 12 | 13 | -------------------------------------------------------------------------------- /app/views/articles/_subway.html.haml: -------------------------------------------------------------------------------- 1 | #subway 2 | - @collections.each do |collection| 3 | = link_to collection.icon, collection.path, :rel => "tooltip", 4 | :title => collection.name 5 | - @volumes.each do |volume| 6 | = link_to volume.icon, volume.path, :rel => "tooltip", 7 | :title => volume.name -------------------------------------------------------------------------------- /app/views/conversation_mailer/mentioned.text.erb: -------------------------------------------------------------------------------- 1 | You can see what was said and share your own thoughts via the link below: 2 | 3 | <%= article_url(@article, :anchor => "comments") %> 4 | 5 | ---- 6 | 7 | You can customize your notification settings via the link below: 8 | 9 | <%= profile_settings_url %> 10 | -------------------------------------------------------------------------------- /app/views/users/current_credit_card.html.haml: -------------------------------------------------------------------------------- 1 | %h1 Current credit card 2 | %hr 3 | 4 | %p 5 | %strong Card #: 6 | ************#{@card.last4} 7 | 8 | %p 9 | %strong Expiration: 10 | #{@card.exp_month}/#{@card.exp_year} 11 | 12 | %hr 13 | = link_to "Change your credit card", '#', :class => "btn update-cc" -------------------------------------------------------------------------------- /config/deploy/old_production.rb: -------------------------------------------------------------------------------- 1 | set :user, "git" 2 | set :deploy_to, "/var/rapp/#{application}" 3 | 4 | server "173.246.46.66", :app, :web, :db, :primary => true 5 | 6 | namespace :deploy do 7 | task :restart, :roles => :app do 8 | run "touch #{current_path}/tmp/restart.txt" 9 | end 10 | end 11 | 12 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_from: rubocop-todo.yml 2 | 3 | HashSyntax: 4 | Enabled: false 5 | IfUnlessModifier: 6 | Enabled: false 7 | SignalException: 8 | Enabled: false 9 | StringLiterals: 10 | Enabled: false 11 | ClassLength: 12 | Enabled: false 13 | 14 | AllCops: 15 | Excludes: 16 | - vendor/** 17 | -------------------------------------------------------------------------------- /db/migrate/20111206143647_add_broadcast_columns_to_announcements.rb: -------------------------------------------------------------------------------- 1 | class AddBroadcastColumnsToAnnouncements < ActiveRecord::Migration 2 | def change 3 | add_column :announcements, :broadcast, :boolean, :default => false, :null => false 4 | add_column :announcements, :broadcast_message, :text 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /test/integration/account_cancelation_test.rb: -------------------------------------------------------------------------------- 1 | require_relative '../test_helper' 2 | 3 | class AccountCancelationTest < ActionDispatch::IntegrationTest 4 | test "user account is disabled" do 5 | simulated_user 6 | .register(Support::SimulatedUser.default) 7 | .cancel_account 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /app/views/admin/articles/edit.html.haml: -------------------------------------------------------------------------------- 1 | - if @article.published? 2 | = javascript_tag "alert('Be careful buddy, this is a published article!!!')" 3 | 4 | = form_for @article, :url => admin_article_path(@article), 5 | :html => { :'data-track-changes' => true } do |f| 6 | = render :partial => "form", :locals => {:f => f} 7 | -------------------------------------------------------------------------------- /app/views/conversation_mailer/comment_made.text.erb: -------------------------------------------------------------------------------- 1 | You can see what has been said and share your own thoughts via the link below: 2 | 3 | <%= article_url(@article, :anchor => "comments") %> 4 | 5 | ---- 6 | 7 | You can customize your notification settings via the link below: 8 | 9 | <%= profile_settings_url %> 10 | -------------------------------------------------------------------------------- /app/views/conversation_mailer/started.text.erb: -------------------------------------------------------------------------------- 1 | You can see what people are saying and share your own thoughts via the link below: 2 | 3 | <%= article_url(@article, :anchor => "comments") %> 4 | 5 | ---- 6 | 7 | You can customize your notification settings via the link below: 8 | 9 | <%= profile_settings_url %> 10 | -------------------------------------------------------------------------------- /app/views/registration_mailer/email_confirmation.text.erb: -------------------------------------------------------------------------------- 1 | Hello, 2 | 3 | To confirm your Practicing Ruby email, click the link below. 4 | 5 | <%= confirm_email_url(:secret => @user.access_token) %> 6 | 7 | If you run into problems, don't hesitate to contact gregory@practicingruby.com 8 | 9 | Thanks! 10 | -greg 11 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/views/announcements/index.html.haml: -------------------------------------------------------------------------------- 1 | - content_for(:title) { "Announcements" } 2 | 3 | - if current_user.admin? 4 | #admin 5 | = button_to "New announcement", new_admin_announcement_path, :method => :get 6 | .clear 7 | 8 | #announcements 9 | 10 | = render :partial => "announcement", :collection => @announcements -------------------------------------------------------------------------------- /config/initializers/omniauth.rb: -------------------------------------------------------------------------------- 1 | Rails.application.config.middleware.use OmniAuth::Builder do 2 | case ENV["AUTH_MODE"] 3 | when "developer" 4 | provider :developer, :fields => [:nickname], :uid_field => :nickname 5 | when "github" 6 | provider :github, ENV["GITHUB_CLIENT_KEY"], ENV["GITHUB_SECRET"] 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /config/schedule.rb: -------------------------------------------------------------------------------- 1 | set :output, "/home/deploy/current/log/cron_log.log" 2 | 3 | # Make sure +bundle exec+ is used when executing rake tasks 4 | # 5 | job_type :rake, "cd :path && RAILS_ENV=:environment bundle exec rake :task --silent :output" 6 | 7 | every 1.month do 8 | rake "stripe:card_exipration_notice" 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20120328150809_create_volumes.rb: -------------------------------------------------------------------------------- 1 | class CreateVolumes < ActiveRecord::Migration 2 | def change 3 | create_table :volumes do |t| 4 | t.integer :number 5 | t.text :description 6 | t.date :start_date 7 | t.date :finish_date 8 | t.timestamps 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/views/announcements/_announcement.html.haml: -------------------------------------------------------------------------------- 1 | .announcement 2 | .title 3 | = link_to announcement.title, announcement_path(announcement) 4 | .on= announcement.created_at.to_date 5 | - if current_user.admin? 6 | .right 7 | = link_to "Edit", edit_admin_announcement_path(announcement) 8 | 9 | = md(announcement.body) -------------------------------------------------------------------------------- /db/migrate/20120112195907_create_article_visits.rb: -------------------------------------------------------------------------------- 1 | class CreateArticleVisits < ActiveRecord::Migration 2 | def change 3 | create_table :article_visits do |t| 4 | t.belongs_to :user 5 | t.belongs_to :article 6 | t.integer :views, :default => 1 7 | 8 | t.timestamps 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /Capfile: -------------------------------------------------------------------------------- 1 | load 'deploy' if respond_to?(:namespace) # cap2 differentiator 2 | 3 | # Uncomment if you are using Rails' asset pipeline 4 | # load 'deploy/assets' 5 | 6 | Dir['vendor/gems/*/recipes/*.rb','vendor/plugins/*/recipes/*.rb'].each { |plugin| load(plugin) } 7 | 8 | load 'config/deploy' # remove this line to skip loading any of the default tasks -------------------------------------------------------------------------------- /app/views/home/public_archives.html.haml: -------------------------------------------------------------------------------- 1 | - content_for(:title) { "Delightful lessons for dedicated programmers - Practicing Ruby" } 2 | - content_for :header do 3 | %h1 Practicing Ruby 4 | %h2 Delightful lessons for dedicated programmers 5 | 6 | = image_tag "ruby-divider.png" 7 | 8 | #archives 9 | = render 'articles/archives' 10 | -------------------------------------------------------------------------------- /db/migrate/20110830041617_add_admin_field_to_user.rb: -------------------------------------------------------------------------------- 1 | class AddAdminFieldToUser < ActiveRecord::Migration 2 | def self.up 3 | change_table :users do |t| 4 | t.boolean :admin, :default => false 5 | end 6 | end 7 | 8 | def self.down 9 | change_table :users do |t| 10 | t.remove :admin 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /db/migrate/20110829215505_create_authorizations.rb: -------------------------------------------------------------------------------- 1 | class CreateAuthorizations < ActiveRecord::Migration 2 | def self.up 3 | create_table :authorizations do |t| 4 | t.text :github_uid 5 | t.belongs_to :user 6 | t.timestamps 7 | end 8 | end 9 | 10 | def self.down 11 | drop_table :authorizations 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /db/migrate/20110829230541_add_github_nickname_to_user.rb: -------------------------------------------------------------------------------- 1 | class AddGithubNicknameToUser < ActiveRecord::Migration 2 | def self.up 3 | change_table :users do |t| 4 | t.text :github_nickname 5 | end 6 | end 7 | 8 | def self.down 9 | change_table :users do |t| 10 | t.remove :github_nickname 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/tasks/rubocop.rake: -------------------------------------------------------------------------------- 1 | unless Rails.env.production? 2 | require "rubocop/rake_task" 3 | 4 | namespace :test do 5 | desc "Run RuboCop style and lint checks" 6 | Rubocop::RakeTask.new(:rubocop) do |t| 7 | t.options = ["--rails"] 8 | end 9 | end 10 | 11 | Rake::Task[:test].enhance { Rake::Task["test:rubocop"].invoke } 12 | end 13 | -------------------------------------------------------------------------------- /app/models/subscription.rb: -------------------------------------------------------------------------------- 1 | class Subscription < ActiveRecord::Base 2 | belongs_to :user 3 | 4 | def self.active 5 | where(:finish_date => nil).first 6 | end 7 | 8 | def self.cancel_account 9 | active.update_attributes(:finish_date => Date.today) if active 10 | end 11 | 12 | def active? 13 | finish_date.blank? 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /app/views/users/_sidebar.html.haml: -------------------------------------------------------------------------------- 1 | #settings-sidebar 2 | = link_to "Profile", profile_settings_path, 3 | class: ('active' if current == 'profile') 4 | = link_to "Notifications", notification_settings_path, 5 | class: ('active' if current == 'notifications') 6 | = link_to "Billing", billing_settings_path, 7 | class: ('active' if current == 'billing') 8 | -------------------------------------------------------------------------------- /db/migrate/20120407204814_create_collections.rb: -------------------------------------------------------------------------------- 1 | class CreateCollections < ActiveRecord::Migration 2 | def change 3 | create_table :collections do |t| 4 | t.integer :id 5 | t.string :name 6 | t.text :description 7 | t.string :image_file_name 8 | t.string :slug 9 | t.timestamps 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /app/models/card_expirer.rb: -------------------------------------------------------------------------------- 1 | CardExpirer = ->(date) { 2 | cards = CreditCard.includes(:user) 3 | .where(:expiration_year => date.year, 4 | :expiration_month => date.month, 5 | "users.status" => "active") 6 | cards.each do |card| 7 | AccountMailer.card_expiring(card) 8 | end 9 | } 10 | -------------------------------------------------------------------------------- /app/views/users/destroy.html.haml: -------------------------------------------------------------------------------- 1 | - content_for(:title) { "Sorry to see you go"} 2 | - content_for(:header) { "Sorry to see you go" } 3 | 4 | %h3 5 | Please feel free to email #{mail_to "gregory@practicingruby.com"} with 6 | feedback. 7 | %p 8 | And remember it may take up to 24 hours before your account is fully cancelled. 9 | 10 | = render "shared/sad_pinkie" -------------------------------------------------------------------------------- /test/factories/announcement_factory.rb: -------------------------------------------------------------------------------- 1 | FactoryGirl.define do 2 | sequence(:announcement_title) { |n| "Announcement #{n}" } 3 | 4 | factory :announcement do |a| 5 | a.title { |_| FactoryGirl.generate(:announcement_title) } 6 | a.body "Announcement Body" 7 | a.broadcast_message { |announcement| announcement.title } 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /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 | require File.expand_path('../lib/rake_exception_notification', __FILE__) 7 | 8 | PracticingRubyWeb::Application.load_tasks 9 | -------------------------------------------------------------------------------- /app/views/shared/_broadcasts.html.haml: -------------------------------------------------------------------------------- 1 | - if active_broadcasts.any? && current_user.try(:status) == "active" 2 | #top-bar 3 | #broadcasts 4 | = link_to "Dismiss", dismiss_broadcasts_path, :class => "dismiss", 5 | :remote => true 6 | - active_broadcasts.each do |broadcast| 7 | .broadcast 8 | = link_to broadcast.broadcast_message, broadcast.url 9 | -------------------------------------------------------------------------------- /db/migrate/20110820183757_create_users.rb: -------------------------------------------------------------------------------- 1 | class CreateUsers < ActiveRecord::Migration 2 | def self.up 3 | create_table :users do |t| 4 | t.text :first_name 5 | t.text :last_name 6 | t.text :email 7 | t.text :mailchimp_web_id 8 | 9 | t.timestamps 10 | end 11 | end 12 | 13 | def self.down 14 | drop_table :users 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /app/views/users/show.html.haml: -------------------------------------------------------------------------------- 1 | #profile 2 | .header 3 | .avatar= @user.icon(100) 4 | 5 | %h1= @user.github_nickname 6 | 7 | .info 8 | = "Member since #{@user.member_since}" 9 | 10 | - if @user.comments.any? 11 | %br 12 | = "#{@user.comments.count} comments made" 13 | 14 | - #TODO Add website link 15 | - #TODO Display comments made by user -------------------------------------------------------------------------------- /db/migrate/20110909193908_create_announcements.rb: -------------------------------------------------------------------------------- 1 | class CreateAnnouncements < ActiveRecord::Migration 2 | def self.up 3 | create_table :announcements do |t| 4 | t.text :title 5 | t.text :body 6 | t.belongs_to :author 7 | 8 | t.timestamps 9 | end 10 | end 11 | 12 | def self.down 13 | drop_table :announcements 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /test/integration/profile_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "../test_helper" 2 | 3 | class ProfileTest < ActionDispatch::IntegrationTest 4 | test "contact email is validated" do 5 | simulated_user 6 | .register(Support::SimulatedUser.default) 7 | .edit_profile(:email => "jordan byron at gmail dot com") 8 | 9 | assert_content "Contact email is invalid" 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/controllers/announcements_controller.rb: -------------------------------------------------------------------------------- 1 | class AnnouncementsController < ApplicationController 2 | def index 3 | redirect_to "http://elmcitycraftworks.org/" 4 | end 5 | 6 | def show 7 | redirect_to "http://elmcitycraftworks.org/" 8 | end 9 | 10 | def dismiss 11 | ids = active_broadcasts.map(&:id) 12 | session[:dismissed_broadcasts] += ids 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /app/views/layouts/landing.html.haml: -------------------------------------------------------------------------------- 1 | !!! 5 2 | %html{:lang => 'en'} 3 | %head 4 | %title Practicing Ruby | A unique journal curated by Gregory Brown 5 | 6 | = stylesheet_link_tag 'application' 7 | = javascript_include_tag 'application' 8 | = csrf_meta_tag 9 | 10 | = render :partial => "shared/ios_icon" 11 | 12 | = yield(:header_bottom) 13 | 14 | %body.landing= yield 15 | -------------------------------------------------------------------------------- /app/views/account_mailer/payment_created.text.erb: -------------------------------------------------------------------------------- 1 | Hi there! 2 | 3 | On <%= @payment.invoice_date %>, we charged your card ending in <%= @payment.credit_card_last_four %> a total of <%= number_to_currency @payment.amount %> 4 | for your Practicing Ruby subscription. Please hang on to this email as 5 | proof of payment, and let us know if you have any questions! 6 | 7 | Thanks, 8 | Gregory Brown (practicingruby.com) 9 | -------------------------------------------------------------------------------- /app/views/articles/index.html.haml: -------------------------------------------------------------------------------- 1 | - content_for(:title) { "Articles" } 2 | - if params[:new_subscription] 3 | :coffeescript 4 | $.facebox '#{escape_javascript render('subscriptions/thanks')}', 'thanks-box' 5 | - content_for(:header) do 6 | %h1 Practicing Ruby 7 | %h2 Delightful lessons for dedicated programmers 8 | = image_tag "ruby-divider.png" 9 | 10 | #archives 11 | = render 'archives' 12 | -------------------------------------------------------------------------------- /app/assets/stylesheets/partials/_email_confirmation_warning.sass: -------------------------------------------------------------------------------- 1 | #email-confirmation-warning 2 | width: 750px 3 | font-family: Helvetica, sans-serif 4 | @extend #share-alert 5 | box-sizing: border-box 6 | a.dismiss 7 | float: right 8 | color: #fff 9 | font-size: 0.75em 10 | line-height: 3em 11 | strong 12 | display: block 13 | font-weight: bold 14 | margin-bottom: 0.5em 15 | -------------------------------------------------------------------------------- /db/migrate/20131204201817_add_email_confirmed_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddEmailConfirmedToUsers < ActiveRecord::Migration 2 | def change 3 | add_column :users, :email_confirmed, :boolean, :default => false, 4 | :null => false 5 | execute %{update users set email_confirmed = true where status in 6 | ('active', 'confirmed', 'payment_pending')} 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /db/migrate/20121019183135_create_credit_cards.rb: -------------------------------------------------------------------------------- 1 | class CreateCreditCards < ActiveRecord::Migration 2 | def up 3 | create_table :credit_cards do |t| 4 | t.belongs_to :user 5 | t.string :last_four 6 | t.integer :expiration_month 7 | t.integer :expiration_year 8 | t.timestamps 9 | end 10 | end 11 | 12 | def down 13 | drop_table :credit_cards 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /db/migrate/20110830162757_create_shared_articles.rb: -------------------------------------------------------------------------------- 1 | class CreateSharedArticles < ActiveRecord::Migration 2 | def self.up 3 | create_table :shared_articles do |t| 4 | t.belongs_to :user 5 | t.belongs_to :article 6 | t.text :secret 7 | t.integer :views 8 | 9 | t.timestamps 10 | end 11 | end 12 | 13 | def self.down 14 | drop_table :shared_articles 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /db/migrate/20110901130928_create_comments.rb: -------------------------------------------------------------------------------- 1 | class CreateComments < ActiveRecord::Migration 2 | def self.up 3 | create_table :comments do |t| 4 | t.integer :commentable_id 5 | t.string :commentable_type 6 | 7 | t.belongs_to :user 8 | 9 | t.text :body 10 | 11 | t.timestamps 12 | end 13 | end 14 | 15 | def self.down 16 | drop_table :comments 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test/support/stripe/invoice.rb: -------------------------------------------------------------------------------- 1 | module Support 2 | module Stripe 3 | class Invoice 4 | attr_accessor :id, :customer, :period_start, :date, :total, :paid, 5 | :invoice_items, :closed 6 | 7 | Lines = Struct.new(:invoiceitems) 8 | 9 | def lines 10 | Lines.new(invoice_items) 11 | end 12 | 13 | def save 14 | # noop 15 | end 16 | end 17 | end 18 | end -------------------------------------------------------------------------------- /app/views/articles/_footer.html.haml: -------------------------------------------------------------------------------- 1 | #article-footer 2 | %h3 If you enjoyed this issue of Practicing Ruby, please share it! 3 | 4 | %p 5 | Copy-and-paste the link below to give your friends free access to 6 | this article: 7 | 8 | = text_field_tag :share_link, article_url(@article), :size => 80 9 | 10 | %p 11 | We believe that sharing is caring, so feel free to post this link anywhere 12 | you'd like! 13 | -------------------------------------------------------------------------------- /app/views/sessions/expired_link.html.haml: -------------------------------------------------------------------------------- 1 | - content_for(:header) { "Drat!" } 2 | 3 | %h3 The confirmation link you've clicked on is either expired or invalid. 4 | 5 | %p 6 | If you have already confirmed your email address, you can try 7 | #{link_to 'logging in', login_path}, but if that doesn't work 8 | just send an email to #{mail_to "support@elmcitycraftworks.org"} 9 | and I will help you resolve this issue. 10 | 11 | -------------------------------------------------------------------------------- /db/migrate/20110829215511_create_authorization_links.rb: -------------------------------------------------------------------------------- 1 | class CreateAuthorizationLinks < ActiveRecord::Migration 2 | def self.up 3 | create_table :authorization_links do |t| 4 | t.text :mailchimp_email 5 | t.text :github_nickname 6 | t.text :secret 7 | t.belongs_to :authorization 8 | t.timestamps 9 | end 10 | end 11 | 12 | def self.down 13 | drop_table :authorization_links 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /db/migrate/20110821020718_create_articles.rb: -------------------------------------------------------------------------------- 1 | class CreateArticles < ActiveRecord::Migration 2 | def self.up 3 | create_table :articles do |t| 4 | t.text :subject 5 | t.text :body 6 | t.text :status, :default => "draft" 7 | t.text :mailchimp_campaign_id 8 | t.datetime :published_time 9 | 10 | t.timestamps 11 | end 12 | end 13 | 14 | def self.down 15 | drop_table :articles 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /app/helpers/article_helper.rb: -------------------------------------------------------------------------------- 1 | module ArticleHelper 2 | def article_url(article, params={}) 3 | return super unless current_user && current_user.active? 4 | 5 | ArticleLink.new(article, params).url(current_user.share_token) 6 | end 7 | 8 | def article_path(article, params={}) 9 | return super unless current_user && current_user.active? 10 | 11 | ArticleLink.new(article, params).path(current_user.share_token) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /db/migrate/20130215174537_create_payments.rb: -------------------------------------------------------------------------------- 1 | class CreatePayments < ActiveRecord::Migration 2 | def change 3 | create_table :payments do |t| 4 | t.belongs_to :user 5 | t.date :invoice_date 6 | t.decimal :amount 7 | t.string :stripe_invoice_id 8 | t.string :credit_card_last_four 9 | t.boolean :email_sent, :default => false, :null => false 10 | 11 | t.timestamps 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/tasks/cache_cooker.rake: -------------------------------------------------------------------------------- 1 | # Rake tasks to bake caches 2 | # 3 | namespace :bake do 4 | desc 'Refresh the articles cache' 5 | task :articles => :environment do 6 | puts "Fire up the oven. It's time to start cookin!" 7 | 8 | Article.order("published_time DESC").each do |article| 9 | CacheCooker.delay.bake("/articles/#{article.id}") 10 | end 11 | 12 | puts "#{Article.count} articles baked and ready to serve :)" 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /db/migrate/20110912173127_add_email_preferences_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddEmailPreferencesToUsers < ActiveRecord::Migration 2 | def self.up 3 | add_column :users, :notify_conversations, :boolean, :default => true, :null => false 4 | add_column :users, :notify_mentions, :boolean, :default => true, :null => false 5 | end 6 | 7 | def self.down 8 | remove_column :users, :notify_conversations 9 | remove_column :users, :notify_mentions 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20110916140124_change_mail_columns_to_text.rb: -------------------------------------------------------------------------------- 1 | class ChangeMailColumnsToText < ActiveRecord::Migration 2 | COLUMNS = [ :to_address, :cc_address, :bcc_address, :reply_to_address, 3 | :subject ] 4 | 5 | def up 6 | COLUMNS.each do |column| 7 | change_column :emails, column, :text 8 | end 9 | end 10 | 11 | def down 12 | COLUMNS.each do |column| 13 | change_column :emails, column, :string 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /app/models/broadcaster.rb: -------------------------------------------------------------------------------- 1 | class Broadcaster 2 | def self.notify_subscribers(params) 3 | BroadcastMailer.recipients.each do |subscriber| 4 | BroadcastMailer.broadcast(params, subscriber).deliver 5 | end 6 | end 7 | 8 | def self.notify_testers(params) 9 | subscriber = Struct.new(:contact_email, :share_token) 10 | .new(params[:to], "testtoken") 11 | 12 | BroadcastMailer.broadcast(params, subscriber).deliver 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /app/views/articles/_header.html.haml: -------------------------------------------------------------------------------- 1 | - content_for(:title) do 2 | #{@article.subject} - #{@article.issue_number} 3 | - content_for(:description) do 4 | #{@article.summary} 5 | - content_for(:header) do 6 | %div{:style => "text-align: center; margin: 20 px"} 7 | = link_to root_path do 8 | = image_tag "//i.imgur.com/hYoGfNJ.png", :width => "75%" 9 | .bigtext{style: 'text-align: center;'} 10 | %div{:style => "padding-top: 40px;"} 11 | = @article.subject 12 | -------------------------------------------------------------------------------- /lib/tasks/mailchimp.rake: -------------------------------------------------------------------------------- 1 | include ActionView::Helpers::TextHelper 2 | include RakeExceptionNotification 3 | 4 | namespace :mailchimp do 5 | desc 'Disable accounts which have been unsubscribed in mailchimp' 6 | task :disable_unsubscribed => :environment do 7 | 8 | puts "# Running mailchip:disable_unsubscribed at #{Time.now}" 9 | 10 | exception_notify do 11 | user_manager = UserManager.new 12 | user_manager.disable_unsubscribed_users 13 | end 14 | end 15 | end -------------------------------------------------------------------------------- /app/assets/stylesheets/partials/_broadcasts.sass: -------------------------------------------------------------------------------- 1 | #top-bar 2 | background-color: darken(#f0f1f4, 10%) 3 | 4 | #broadcasts 5 | width: $page-width 6 | margin: 0 auto 7 | text-align: center 8 | font-family: sans-serif 9 | a.dismiss 10 | float: right 11 | color: #1b1b1b 12 | font-size: 0.75em 13 | line-height: 3em 14 | text-decoration: none 15 | .broadcast 16 | display: block 17 | text-align: center 18 | padding: 7px 0 19 | a 20 | color: $red 21 | -------------------------------------------------------------------------------- /config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | PracticingRubyWeb::Application.config.session_store :cookie_store, :key => '_practicing-ruby-web_session' 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 | # PracticingRubyWeb::Application.config.session_store :active_record_store 9 | -------------------------------------------------------------------------------- /app/assets/stylesheets/partials/_flash.sass: -------------------------------------------------------------------------------- 1 | #flash 2 | width: $page-width 3 | margin: 0 auto 4 | text-align: center 5 | font-family: 'Helvetica', sans-serif 6 | .flash 7 | text-align: center 8 | padding: 0.5em 1em 9 | margin: 0.5em 10 | display: inline-block 11 | +border-radius(2px) 12 | background-color: #FAFAE2 13 | 14 | &.notice 15 | background-color: #8c8 16 | color: #252 17 | 18 | &.error 19 | background-color: #c88 20 | color: #522 21 | -------------------------------------------------------------------------------- /config/initializers/rails_security_workarounds.rb: -------------------------------------------------------------------------------- 1 | # https://groups.google.com/forum/#!topic/rubyonrails-security/bahr2JLnxvk 2 | 3 | ActiveSupport::XmlMini.backend = 'Nokogiri' 4 | 5 | # https://groups.google.com/forum/#!topic/rubyonrails-security/7VlB_pck3hU 6 | # 7 | module ActiveSupport 8 | module JSON 9 | module Encoding 10 | 11 | private 12 | 13 | class EscapedString 14 | def to_s 15 | self 16 | end 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /app/views/home/chat.html.haml: -------------------------------------------------------------------------------- 1 | #chat 2 | %p 3 | Get in touch with your fellow Practicing Rubyists! 4 | 5 | = link_to "Chat with a guest account", CHAT_GUEST_URL, 6 | :class => "btn" 7 | = link_to "Chat with your Campfire account", CHAT_LOGIN_URL, 8 | :class => "btn" 9 | %p 10 | You can log in as a guest without any extra setup. 11 | %br 12 | For transcripts, you need a (free) Campfire account. 13 | 14 | %p 15 | Email #{mail_to "gregory@practicingruby.com"} for an invite. 16 | 17 | -------------------------------------------------------------------------------- /app/views/comments/_show.html.haml: -------------------------------------------------------------------------------- 1 | .comment{:data => { :id => comment.id, 2 | :editable => (comment.editable_by?(current_user)).inspect }} 3 | .header 4 | = comment.user.icon(32) 5 | = link_to comment.user.github_nickname, 6 | user_path(comment.user.github_nickname), :class => "user-profile" 7 | .controls= comment.controls 8 | .on 9 | = comment.time_ago 10 | ago 11 | .clear 12 | .content 13 | - cache("comment_body_#{comment.id}") do 14 | = comment.content 15 | -------------------------------------------------------------------------------- /app/views/subscriptions/_thanks.html.haml: -------------------------------------------------------------------------------- 1 | %h1 You're awesome. Thanks for subscribing! 2 | 3 | %p.image= image_tag "payment/dolla_billz.jpg" 4 | 5 | %p 6 | Your account is now set up, which means that you can enjoy everything that 7 | Practicing Ruby has to offer. You can start by 8 | #{link_to 'browsing through our library', articles_path} or reading a 9 | #{link_to 'randomly selected article', random_article_path}. 10 | If you have any questions, please send an email to 11 | #{mail_to 'gregory@practicingruby.com'}. 12 | -------------------------------------------------------------------------------- /app/views/user_email/change.html.haml: -------------------------------------------------------------------------------- 1 | #change-email 2 | = form_for @user, :remote => true, :url => update_email_path, 3 | :html => {'parsley-validate' => ''} do |f| 4 | = error_messages_for(@user) 5 | .field 6 | = f.label :contact_email, "Email Address" 7 | = f.email_field :contact_email, :required => true, 8 | 'parsley-remote' => email_unique_users_path 9 | .field 10 | = f.submit "Send confirmation email", :class => 'btn-small' 11 | :coffeescript 12 | $('#edit_user_#{@user.id}').parsley() 13 | -------------------------------------------------------------------------------- /app/assets/stylesheets/partials/_announcements.sass: -------------------------------------------------------------------------------- 1 | #announcements 2 | .announcement 3 | margin: 1em 0 4 | font-family: 'Helvetica', sans-serif 5 | +markdown 6 | .title 7 | font-size: 1.1em 8 | font-weight: bold 9 | padding: 0.5em 10 | background-color: #eee 11 | .right 12 | float: right 13 | margin-right: 0.5em 14 | .on 15 | display: inline-block 16 | float: right 17 | color: #555 18 | font-weight: normal 19 | a 20 | color: #000 -------------------------------------------------------------------------------- /lib/md_mentions.rb: -------------------------------------------------------------------------------- 1 | module MdMentions 2 | class Render < MdEmoji::Render 3 | def paragraph(text) 4 | mentioned_text = text.gsub(/@([a-z\d-]+)(\.[a-z]{3})?/i) do |mention| 5 | github = $1 6 | user = User.where("LOWER(github_nickname) = ?", github.downcase).exists? 7 | 8 | if user && $2.blank? 9 | %{@#{github}} 10 | else 11 | mention 12 | end 13 | end 14 | 15 | super mentioned_text 16 | end 17 | end 18 | end -------------------------------------------------------------------------------- /app/models/article_link.rb: -------------------------------------------------------------------------------- 1 | class ArticleLink 2 | include Rails.application.routes.url_helpers 3 | 4 | def initialize(article, params={}) 5 | self.article = article 6 | self.params = params 7 | end 8 | 9 | def path(token) 10 | article_path(article, params_with_token(token)) 11 | end 12 | 13 | def url(token) 14 | article_url(article, params_with_token(token)) 15 | end 16 | 17 | private 18 | 19 | attr_accessor :params, :article 20 | 21 | def params_with_token(token) 22 | {:u => token}.merge(params) 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /config/initializers/exception_notifier.rb: -------------------------------------------------------------------------------- 1 | if Rails.env.production? 2 | PracticingRubyWeb::Application.config.middleware.use ExceptionNotification::Rack, 3 | :email => { 4 | :email_prefix => "[Practicing Ruby] ", 5 | :sender_address => %{"Exception Notifier" }, 6 | :exception_recipients => %w{gregory.t.brown@gmail.com jordan.byron@gmail.com} 7 | }, 8 | :ignore_crawlers => %w{EasouSpider YandexBot}, 9 | :ignore_exceptions => [ActionView::MissingTemplate] + ExceptionNotifier.ignored_exceptions 10 | end 11 | -------------------------------------------------------------------------------- /config/initializers/mail_settings.rb: -------------------------------------------------------------------------------- 1 | if ENV['SMTP_ADDRESS'] 2 | ActionMailer::Base.smtp_settings = { 3 | :address => ENV["SMTP_ADDRESS"], 4 | :port => ENV["SMTP_PORT"] || 587, 5 | :domain => ENV["SMTP_DOMAIN"], 6 | :user_name => ENV["SMTP_USERNAME"], 7 | :password => ENV["SMTP_PASSWORD"], 8 | :authentication => ENV["SMTP_AUTH"], 9 | :enable_starttls_auto => ENV["SMTP_TLS"] == 'true' 10 | } 11 | else 12 | warn "ActionMailer::Base.smtp_settings was not set!" 13 | end 14 | -------------------------------------------------------------------------------- /test/factories/stripe.rb: -------------------------------------------------------------------------------- 1 | require_relative '../support/stripe/invoice' 2 | 3 | FactoryGirl.define do 4 | sequence (:stripe_customer_id) { |n| "cus_mock#{n}" } 5 | sequence (:stripe_invoice_id) { |n| "in_mock#{n}" } 6 | 7 | factory 'support/stripe/invoice' do 8 | id { FactoryGirl.generate :stripe_invoice_id } 9 | customer { FactoryGirl.generate :stripe_customer_id } 10 | period_start Time.now.to_i 11 | date Time.now.to_i 12 | total 100_00 13 | paid true 14 | closed false 15 | end 16 | end -------------------------------------------------------------------------------- /app/views/sessions/failure.html.haml: -------------------------------------------------------------------------------- 1 | - content_for(:title) { "GitHub is broken"} 2 | - content_for(:header) { "GitHub is broken#{': ' + @message if @message}" } 3 | 4 | %h3 5 | Sorry but you can't sign in right now because of problems with GitHub 6 | %p 7 | Check out 8 | #{link_to "GitHub's status page", "http://status.github.com"} for the most 9 | up-to-date information and 10 | = mail_to "support@elmcitycraftworks.org", "contact us", 11 | :subject => "GitHub is down and I can't sign in :(" 12 | if the problem persists. 13 | 14 | = render "shared/sad_pinkie" -------------------------------------------------------------------------------- /lib/tasks/import.rake: -------------------------------------------------------------------------------- 1 | namespace :import do 2 | task :articles do 3 | sh %{ bzcat db/dump.sql.bz2 | #{psql}} 4 | end 5 | 6 | def psql(env=Rails.env.to_s) 7 | config = YAML.load_file("config/database.yml")[env] 8 | 9 | command = 'psql' 10 | 11 | if config['password'] 12 | command = "PGPASSWORD=#{config['password']} #{command}" 13 | end 14 | 15 | if config['username'] 16 | command = "#{command} -U#{config['username']}" 17 | end 18 | 19 | "#{command} -h#{config['host'] || 'localhost'} #{config['database']}" 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /app/mailers/broadcast_mailer.rb: -------------------------------------------------------------------------------- 1 | class BroadcastMailer < ActionMailer::Base 2 | default :from => "Gregory at Practicing Ruby " 3 | 4 | def self.recipients 5 | User.where(:notify_updates => true).to_notify 6 | end 7 | 8 | def broadcast(message, subscriber) 9 | article_finder = ->(e) { ArticleLink.new(Article[e]).url(subscriber.share_token) } 10 | 11 | @body = Mustache.render(message[:body], :article => article_finder) 12 | mail(:to => subscriber.contact_email, 13 | :subject => message[:subject]) 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /app/views/users/edit.html.haml: -------------------------------------------------------------------------------- 1 | - content_for(:title) { "Settings" } 2 | - content_for(:header) { "Settings" } 3 | 4 | = render :partial => 'sidebar', :locals => {:current => 'account'} 5 | 6 | = form_for @user do |f| 7 | = render :partial => "form", :locals => {:f => f} 8 | %hr 9 | %p 10 | = f.submit "Update Settings" 11 | = link_to "Unsubscribe from Practicing Ruby", user_path(@user), :method => :delete, 12 | :data => {:confirm => "Are you sure you want to unsubscribe?"}, 13 | :id => "cancel" 14 | %span.cancel-notice Cancellation may take up to 24 hours 15 | -------------------------------------------------------------------------------- /app/assets/stylesheets/partials/_sharebox.sass: -------------------------------------------------------------------------------- 1 | #share-alert 2 | width: 750px 3 | box-sizing: border-box 4 | font-family: sans-serif 5 | background-color: #FAFAE2 6 | border: 1px dashed #AE9331 7 | font-size: 0.9em 8 | color: #333 9 | text-align: center 10 | margin: 10px auto 11 | padding: 1em 0.5em 12 | +border-radius(4px) 13 | a 14 | color: #000 15 | 16 | #share-banner 17 | background-color: #FAFAE2 18 | +single-box-shadow(#333, 0, 0, 2px) 19 | font-size: 0.9em 20 | color: #333 21 | text-align: center 22 | padding: 1em 0.5em 23 | a 24 | color: #000 25 | -------------------------------------------------------------------------------- /app/decorators/collection_decorator.rb: -------------------------------------------------------------------------------- 1 | class CollectionDecorator < Draper::Decorator 2 | delegate_all 3 | 4 | def self.icon(name) 5 | h.content_tag(:span, 6 | h.image_tag("icons/#{name}"), 7 | :class => 'icon collection' 8 | ) 9 | end 10 | 11 | def header 12 | h.content_tag(:div, :class => 'collection') do 13 | [ icon, collection.name ].join("\n").html_safe 14 | end.html_safe 15 | end 16 | 17 | def icon 18 | self.class.icon(collection.image_file_name) 19 | end 20 | 21 | def path 22 | h.collection_path(collection.slug) 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /app/models/comment.rb: -------------------------------------------------------------------------------- 1 | class Comment < ActiveRecord::Base 2 | after_create { |comment| ConversationNotifier.broadcast(comment) } 3 | 4 | belongs_to :commentable, :polymorphic => true 5 | belongs_to :user 6 | 7 | validates_presence_of :body 8 | 9 | def mentioned_users 10 | mentions = body.scan(/@([a-z\d-]+)/i).flatten.map {|m| m.downcase } 11 | 12 | User.where("LOWER(github_nickname) IN (?)", mentions) 13 | end 14 | 15 | def editable_by?(user) 16 | self.user == user || user.admin? 17 | end 18 | 19 | def first_comment? 20 | commentable.comments.count == 1 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /app/views/subscriptions/new.html.haml: -------------------------------------------------------------------------------- 1 | - content_for(:title) { "Subscribe" } 2 | - content_for :header_bottom do 3 | = javascript_include_tag "https://js.stripe.com/v1/" 4 | :coffeescript 5 | $ -> new PR.PaymentProcessor('#{STRIPE_PUBLISHABLE_KEY}') 6 | 7 | %h1 8 | Direct subscriptions to Practicing Ruby are no longer available 9 | 10 | %p 11 | If you'd like to support Practicing Ruby and get early access to 12 | its articles, join 13 | = link_to "The Practicing Developer's Workshop.", "http://practicingdeveloper.com/workshop" 14 | 15 | %p 16 | Any questions? Email gregory at practicingruby.com -------------------------------------------------------------------------------- /app/views/account_mailer/card_expiring.text.erb: -------------------------------------------------------------------------------- 1 | We just wanted to let you know that your credit card ending in 2 | <%= @card.last_four %> will expire on 3 | <%= "#{@card.expiration_month}/#{@card.expiration_year}" %>. Please update your 4 | billing information by visiting the following URL and 5 | clicking 'Update your credit card': 6 | 7 | <%= billing_settings_url %> 8 | 9 | If you don't update your card before it expires, the billing gremlins may 10 | disable your account and that makes us sad. As always if you have any questions 11 | or problems don't hesitate to contact us. 12 | 13 | Thanks for subscribing to Practicing Ruby! <3 <3 <3 -------------------------------------------------------------------------------- /db/migrate/20110916131749_create_emails.rb: -------------------------------------------------------------------------------- 1 | # Everything listed in this migration will be added to a migration file 2 | # inside of your main app. 3 | class CreateEmails < ActiveRecord::Migration 4 | def self.up 5 | create_table :emails do |t| 6 | t.string :from_address, :null => false 7 | t.string :to_address, 8 | :cc_address, 9 | :bcc_address, 10 | :reply_to_address, 11 | :subject 12 | t.text :content 13 | t.datetime :sent_at 14 | t.timestamps 15 | end 16 | end 17 | 18 | def self.down 19 | drop_table :emails 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /app/views/account_mailer/failed_payment.text.erb: -------------------------------------------------------------------------------- 1 | Shucks! Your last payment to Practicing Ruby didn't work! 2 | 3 | We tried charging your card ending in <%= @charge.card.last4 %> which expires on 4 | <%= "#{@charge.card.exp_month}/#{@charge.card.exp_year}" %>. If this card is no 5 | longer valid, please update it by visiting the following URL and clicking 6 | 'Update your credit card': 7 | 8 | <%= billing_settings_url %> 9 | 10 | We will attempt to charge your card again in 7 days. If you aren't sure 11 | why your payment failed, just reply to this email and we will look into it. 12 | 13 | Thanks for subscribing to Practicing Ruby! <3 <3 <3 -------------------------------------------------------------------------------- /app/views/admin/articles/index.html.haml: -------------------------------------------------------------------------------- 1 | %h1 Articles 2 | 3 | %p= link_to "Create Article", new_admin_article_path 4 | 5 | %table.admin 6 | %thead 7 | %tr 8 | %th Title 9 | %th{:colspan => 2} 10 | %tbody 11 | - @articles.each do |article| 12 | %tr{:class => cycle('even','odd')} 13 | %td= link_to article.subject, edit_admin_article_path(article) 14 | %td= link_to "Show", article_path(article) 15 | %td= link_to "Destroy", 16 | admin_article_path(article), 17 | :data => { :confirm => "Are you sure you wish to delete this article?" }, 18 | :method => 'DELETE', 19 | :class => "delete" -------------------------------------------------------------------------------- /app/models/authorization_link.rb: -------------------------------------------------------------------------------- 1 | class AuthorizationLink < ActiveRecord::Base 2 | belongs_to :authorization 3 | 4 | before_save do 5 | if mailchimp_email && secret.blank? 6 | write_attribute(:secret, SecretGenerator.generate) 7 | end 8 | end 9 | 10 | def self.activate(key) 11 | if link = find_by_secret(key) 12 | auth = link.authorization 13 | user = User.find_by_email(link.mailchimp_email) 14 | 15 | user.github_nickname = link.github_nickname 16 | user.save! 17 | 18 | auth.user_id = user.id 19 | auth.save! 20 | 21 | link.destroy 22 | 23 | true 24 | else 25 | false 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /app/views/articles/_archives.html.haml: -------------------------------------------------------------------------------- 1 | %p{:style => "font-size: 1.05em; color: #666; text-align: center"} 2 | Welcome to the world's largest open-access collection of lessons 3 | for experienced Ruby developers! Click on any article 4 | below to begin your journey, or 5 | = link_to "learn more about this project.", about_path 6 | 7 | %hr 8 | 9 | %table.archives 10 | %tbody 11 | - @articles.each do |article| 12 | %tr 13 | %td.article 14 | = link_to article.list_title, article_path(article) 15 | %td.issue-number= article.issue_number 16 | %tr 17 | %td.description= article.summary 18 | %td.month= article.published_time.strftime("%B %Y") 19 | -------------------------------------------------------------------------------- /lib/tasks/travis.rake: -------------------------------------------------------------------------------- 1 | require 'fileutils' 2 | 3 | namespace :travis do 4 | desc 'Create database.yml for testing' 5 | task :setup do 6 | Rake::Task["setup:environment"].invoke 7 | 8 | # Setup our database.yml file 9 | # 10 | File.open(Rails.root.join("config", "database.yml"), 'w') do |f| 11 | f << <<-CONFIG 12 | test: 13 | adapter: postgresql 14 | database: pr_test 15 | username: postgres 16 | min_messages: error 17 | encoding: utf8 18 | CONFIG 19 | end 20 | 21 | # Create the database 22 | # 23 | `psql -c 'create database pr_test;' -U postgres` 24 | 25 | # Load the schema 26 | # 27 | Rake::Task["db:test:load"].invoke 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /config/database.yml.example: -------------------------------------------------------------------------------- 1 | development: 2 | adapter: postgresql 3 | database: practicing-ruby-devel 4 | username: 5 | password: 6 | host: localhost 7 | min_messages: error 8 | 9 | # Warning: The database defined as "test" will be erased and 10 | # re-generated from your development database when you run "rake". 11 | # Do not set this db to the same as development or production. 12 | test: 13 | adapter: postgresql 14 | database: practicing-ruby-test 15 | username: 16 | password: 17 | host: localhost 18 | min_messages: error 19 | 20 | production: 21 | adapter: postgresql 22 | database: practicing-ruby-production 23 | username: 24 | password: 25 | host: localhost 26 | 27 | -------------------------------------------------------------------------------- /app/views/admin/announcements/_form.html.haml: -------------------------------------------------------------------------------- 1 | 2 | = error_messages_for(@announcement) 3 | 4 | / %p 5 | / = f.label :title 6 | / = f.text_field :title 7 | / 8 | / %p 9 | / = f.label :author_id 10 | / = f.select :author_id, 11 | / User.where(:admin => true).order("first_name").map {|c| [c.first_name, c.id]} 12 | 13 | %p 14 | = f.check_box :broadcast 15 | = f.label :broadcast 16 | 17 | %p 18 | = f.label :broadcast_message 19 | = f.text_field :broadcast_message 20 | 21 | %p 22 | = f.label :url, "Broadcast URL" 23 | = f.text_field :url 24 | 25 | 26 | / #announcements= f.text_area :body, 'data-preview' => true 27 | 28 | %p 29 | = f.submit 30 | = link_to "Cancel", admin_announcements_path -------------------------------------------------------------------------------- /app/assets/stylesheets/partials/_admin.sass: -------------------------------------------------------------------------------- 1 | table#reports 2 | margin: 1em 0 3 | border-color: #600 4 | border-width: 0 0 1px 1px 5 | border-style: solid 6 | td, th 7 | padding: 4px 8 | border-color: #600 9 | border-width: 1px 1px 0 0 10 | border-style: solid 11 | margin: 0 12 | background-color: #FFC 13 | &.num 14 | text-align: right 15 | th 16 | color: #000 17 | 18 | #control-bar 19 | position: fixed 20 | bottom: 0 21 | left: 0 22 | right: 0 23 | width: 100% 24 | padding: 5px 25 | background-color: #eee 26 | +single-box-shadow(#777, 0, 0, 2px) 27 | 28 | .content 29 | width: 750px 30 | margin: 0 auto 31 | .control-bar-padding 32 | height: 50px 33 | -------------------------------------------------------------------------------- /app/views/admin/reports/index.html.haml: -------------------------------------------------------------------------------- 1 | - content_for(:header) { "Reports" } 2 | - content_for(:title) { "Reports" } 3 | 4 | %h3 Active subscribers by payment method 5 | 6 | %table#reports 7 | %tr 8 | %th Payment method 9 | %th Count 10 | - Reports.payment_provider_counts.each do |e| 11 | %tr 12 | %td= e.payment_provider.humanize 13 | %td.num= e.user_count 14 | %tr 15 | %th Total paid accounts 16 | %th.num= Reports.paid_subscriber_count 17 | 18 | %h3 Recent signups (last 7 days) 19 | 20 | %ul 21 | - Reports.recent_signups.each do |e| 22 | %li= e 23 | 24 | 25 | %h3 Recent cancellations (last 7 days) 26 | 27 | %ul 28 | - Reports.recent_cancellations.each do |e| 29 | %li= e 30 | -------------------------------------------------------------------------------- /test/factories/article_factory.rb: -------------------------------------------------------------------------------- 1 | FactoryGirl.define do 2 | sequence(:article_subject) { |n| "Article #{n}" } 3 | 4 | factory :article do |a| 5 | a.subject { |_| FactoryGirl.generate(:article_subject) } 6 | a.body "Article Body" 7 | a.issue_number "1" 8 | a.status "published" 9 | a.volume 10 | a.published_time { Time.now } 11 | a.discourse_url "http://discourse.practicingruby.com/" 12 | 13 | factory :public_article do 14 | status "public" 15 | end 16 | end 17 | 18 | sequence(:volume_number) { |n| n } 19 | 20 | factory :volume do |v| 21 | v.number { |_| FactoryGirl.generate(:volume_number) } 22 | v.description "The Best Volume Evar" 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /app/views/admin/announcements/index.html.haml: -------------------------------------------------------------------------------- 1 | %h1 Announcements 2 | 3 | %p= link_to "Create Announcement", new_admin_announcement_path 4 | 5 | %table.admin 6 | %thead 7 | %tr 8 | %th Title 9 | %th Author 10 | %th{:colspan => 2} 11 | %tbody 12 | - @announcements.each do |announcement| 13 | %tr{:class => cycle('even','odd')} 14 | %td= link_to announcement.title, edit_admin_announcement_path(announcement) 15 | %td= announcement.author.try(:name) 16 | %td.action= link_to "Destroy", 17 | admin_announcement_path(announcement), 18 | :data => { :confirm => "Are you sure you wish to delete this announcement?"} , 19 | :method => 'DELETE', 20 | :class => "delete" -------------------------------------------------------------------------------- /app/views/subscriptions/redirect.html.haml: -------------------------------------------------------------------------------- 1 | 2 | %div{:style => "text-align: center"} 3 | %p{:style => "text-align: center; font-size: 1.2em;"} 4 | %strong 5 | For only $8/month, you can help us educate Practicing Developers worldwide. 6 | 7 | = button_to "Become a Practicing Ruby subscriber", login_path, :method => :get, 8 | :id => "subscribe" 9 | 10 | %p{:style => "text-align: center"} 11 | We use Github for authentication and Stripe.js for payment processing. 12 | To keep your sensitive information safe, we do not store any passwords 13 | or payment information on our servers. 14 | 15 | %p{:style => "text-align: center"} 16 | = link_to "Why should I subscribe to Practicing Ruby?", about_path 17 | -------------------------------------------------------------------------------- /config/deploy/vagrant.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Capistrano config to deploy the Practicing Ruby app to a Vagrant VM that was 3 | # provisioned with the Practicing Ruby Chef cookbook 4 | # 5 | # See https://github.com/elm-city-craftworks/practicing-ruby-cookbook#deploying 6 | # 7 | 8 | set :deploy_to, "/home/deploy" 9 | set :user, "deploy" 10 | 11 | # Only production is supported at the moment 12 | set :rails_env, "production" 13 | 14 | # Set custom hostname to connect to Vagrant VM 15 | server "practicingruby.local", :app, :web, :db, :primary => true 16 | 17 | desc "Import articles, volumes, and collections from the server" 18 | task :seed do 19 | run_rake "db:seed" 20 | run_rake "import:articles" 21 | end 22 | 23 | after "deploy:restart", "unicorn:restart" 24 | -------------------------------------------------------------------------------- /app/views/articles/shared.html.haml: -------------------------------------------------------------------------------- 1 | - content_for(:nudge) do 2 | #share-alert 3 | Practicing Ruby publishes delightful lessons for Ruby progreammers. 4 | %br 5 | You can help us continue to produce amazing content by 6 | = link_to "becoming a paid supporter.", root_path 7 | 8 | = render "header" 9 | 10 | #article 11 | - cache("article_body_#{@article.id}") do 12 | = md(@article.body) 13 | 14 | #share-alert 15 | The discussion thread for this article is available to subscribers 16 | only. If you want to share some feedback or ask me a question, please 17 | = link_to "subscribe", root_path 18 | , 19 | = link_to "log in", login_path 20 | , or send an email to 21 | = mail_to "support@elmcitycraftworks.org" 22 | -------------------------------------------------------------------------------- /app/views/users/_settings_page.html.haml: -------------------------------------------------------------------------------- 1 | - content_for :header_bottom do 2 | = javascript_include_tag "https://js.stripe.com/v1/" 3 | :coffeescript 4 | $ -> 5 | new PR.PaymentProcessor('#{STRIPE_PUBLISHABLE_KEY}') 6 | $('img.user-icon[title]').tooltip() 7 | 8 | $(document).on 'click', '#facebox .confirm-interval-change a', (e) -> 9 | $(this).replaceWith('
') 10 | 11 | spinnerOpts = { 12 | lines: 9, 13 | length: 4, 14 | width: 3, 15 | radius: 5, 16 | corners: 0.8, 17 | hwaccel: true, 18 | speed: 1.6 19 | } 20 | 21 | new Spinner(spinnerOpts).spin $('div.loading')[0] 22 | 23 | %div{'data-pjax-container' => ''} 24 | = yield(:settings_panel) 25 | -------------------------------------------------------------------------------- /test/unit/mailers/account_mailer_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class AccountMailerTest < ActionMailer::TestCase 4 | context "mailchimp account is deleted" do 5 | 6 | test "user is notified they have been unsubscribed" do 7 | assert ActionMailer::Base.deliveries.empty? 8 | 9 | user = FactoryGirl.create(:user) 10 | user_manager = UserManager.new 11 | 12 | user_manager.stubs(:client).returns(mock(:list_unsubscribe)) 13 | 14 | user_manager.delete_user(user.email) 15 | 16 | refute ActionMailer::Base.deliveries.empty? 17 | 18 | message = ActionMailer::Base.deliveries.first 19 | 20 | assert message.to.include?(user.email) 21 | assert message.subject == "Sorry to see you go" 22 | end 23 | 24 | end 25 | end -------------------------------------------------------------------------------- /app/controllers/admin/magic_controller.rb: -------------------------------------------------------------------------------- 1 | class Admin::MagicController < ApplicationController 2 | before_filter :admin_only 3 | 4 | def freebie 5 | user = User.where(:github_nickname => params["nickname"]).first 6 | 7 | raise if user.subscriptions.active 8 | 9 | user.status = "active" 10 | user.save 11 | 12 | user.subscriptions.create(:payment_provider => "free", 13 | :rate_cents => 0, 14 | :start_date => Date.today) 15 | 16 | render :text => "ok" 17 | end 18 | 19 | def hashed_id 20 | user = User.where(:github_nickname => params["nickname"]).first 21 | 22 | raise unless user.github_nickname 23 | 24 | render :text => user.hashed_id 25 | end 26 | 27 | end 28 | -------------------------------------------------------------------------------- /app/decorators/volume_decorator.rb: -------------------------------------------------------------------------------- 1 | class VolumeDecorator < Draper::Decorator 2 | delegate_all 3 | 4 | def header 5 | h.content_tag(:div, :class => 'volume') do 6 | [ icon, volume.name ].join("\n").html_safe 7 | end.html_safe 8 | end 9 | 10 | def icon 11 | h.content_tag(:span, volume.number, :class => 'icon volume') 12 | end 13 | 14 | def path 15 | h.volume_path(volume.number) 16 | end 17 | 18 | def publish_range 19 | if volume.articles.published.empty? || volume.start_date.nil? 20 | "Coming Soon ..." 21 | else 22 | date_format = "%B %Y" 23 | 24 | [ 25 | volume.start_date.strftime(date_format), 26 | volume.finish_date.strftime(date_format) 27 | ].join(" - ") 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /app/decorators/comment_decorator.rb: -------------------------------------------------------------------------------- 1 | class CommentDecorator < Draper::Decorator 2 | delegate_all 3 | decorates_association :user 4 | 5 | def time_ago 6 | h.time_ago_in_words comment.created_at 7 | end 8 | 9 | def controls 10 | if comment.editable_by? h.current_user 11 | [ h.link_to('Edit', '#', :class => "edit", :title => "Edit"), 12 | h.link_to('Delete', h.comment_path(comment), 13 | :class => "remove", :title => "Delete", 14 | :method => :delete, :remote => true, 15 | :data => { :confirm => "Do you really want to delete this comment?" }) 16 | ].join("\n").html_safe 17 | end 18 | end 19 | 20 | def content 21 | MdPreview::CustomParser.parse(comment.body, MdMentions::Render) 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /app/views/users/_form.html.haml: -------------------------------------------------------------------------------- 1 | = error_messages_for(@user) 2 | 3 | .field 4 | = f.label :contact_email, "Email:" 5 | = f.text_field :contact_email 6 | %p 7 | = f.check_box :notify_updates 8 | = f.label :notify_updates, "Notify me about content and website updates" 9 | %p 10 | = f.check_box :notify_conversations 11 | = f.label :notify_conversations, "Notify me when conversations start" 12 | %p 13 | = f.check_box :notify_mentions 14 | = f.label :notify_mentions, "Notify me when I am mentioned in a conversation" 15 | %p 16 | = f.check_box :notify_comment_made 17 | = f.label :notify_comment_made, "Notify me every time a comment is made" 18 | %p 19 | = f.check_box :beta_tester 20 | = f.label :beta_tester, "Give me early access to experimental website features" 21 | -------------------------------------------------------------------------------- /app/views/home/contact.html.haml: -------------------------------------------------------------------------------- 1 | - content_for(:title) { "Contact" } 2 | 3 | #contact 4 | %p 5 | As Practicing Ruby's editor, its my job to make sure that you have a 6 | great experience here. I am sure that you will be as happy with 7 | Practicing Ruby as my son was when he met Santa Claus: 8 | 9 | = image_tag "santa.jpg" 10 | 11 | %p 12 | But I also believe we can do even better than that. 13 | If you have a 14 | question about Practicing Ruby, problems with your account, or anything in 15 | between don't hesitate to get in touch. 16 | I can be reached via email at 17 | #{ mail_to "gregory@practicingruby.com", nil, :encode => "javascript"} 18 | and on Twitter at 19 | #{link_to "@practicingruby", "http://twitter.com/practicingruby"}. 20 | .clear 21 | -------------------------------------------------------------------------------- /app/decorators/article_decorator.rb: -------------------------------------------------------------------------------- 1 | class ArticleDecorator < Draper::Decorator 2 | delegate_all 3 | decorates_association :volume 4 | decorates_association :collection 5 | 6 | def list_title 7 | title = article.subject 8 | if article.status == "draft" 9 | title = "[DRAFT] #{subject}" 10 | end 11 | title 12 | end 13 | 14 | def list_link(options={:text => list_title}) 15 | link_text = options.delete(:text) 16 | path = if h.current_user 17 | h.article_path(article) 18 | else 19 | h.root_path 20 | end 21 | 22 | h.link_to(link_text, path, options) 23 | end 24 | 25 | def issue_number 26 | "Issue #{article.issue_number}" 27 | end 28 | 29 | def published_date 30 | article.published_time.strftime("%B %e, %Y") 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /app/views/shared/_navigation.html.haml: -------------------------------------------------------------------------------- 1 | %nav 2 | %ul 3 | - unless current_user.try(:active?) 4 | %li= link_to "Home", root_path 5 | %li= link_to "About Us", about_path 6 | - else 7 | %li= link_to "Home", articles_path 8 | %li= link_to "Chat", "https://practicingruby.slack.com" 9 | %li= link_to "Open Source", open_source_path 10 | %li= link_to "Blog", "http://practicingdeveloper.com" 11 | %li= link_to "Twitter", "http://twitter.com/practicingdev" 12 | - if current_user.try(:active?) 13 | %li.right= link_to "Log out", logout_path 14 | %li.right= link_to "Settings", user_settings_path 15 | - else 16 | - if current_user 17 | %li.right= link_to "Log out", logout_path 18 | - else 19 | %li.right= link_to "Log in", login_path 20 | -------------------------------------------------------------------------------- /config/initializers/markdown.rb: -------------------------------------------------------------------------------- 1 | module MdPreview 2 | module CustomParser 3 | extend self 4 | 5 | def parse(content, renderer = MdEmoji::Render) 6 | markdown = Redcarpet::Markdown.new(renderer, 7 | :autolink => true, 8 | :space_after_headers => true, 9 | :no_intra_emphasis => true, 10 | :fenced_code_blocks => true, 11 | :footnotes => true) 12 | 13 | syntax_highlighter(markdown.render(content)).html_safe 14 | end 15 | 16 | def syntax_highlighter(html) 17 | doc = Nokogiri::HTML(html) 18 | 19 | doc.search("//code[@class]").each do |code| 20 | code.parent.replace Albino.colorize(code.text.rstrip, code[:class]) 21 | end 22 | 23 | doc.xpath("//body").children.to_s 24 | end 25 | end 26 | end -------------------------------------------------------------------------------- /app/views/admin/broadcasts/new.html.haml: -------------------------------------------------------------------------------- 1 | - content_for(:header) { "Broadcast Mailer" } 2 | - content_for(:title) { "Broadcast Mailer" } 3 | 4 | = form_tag admin_broadcasts_path, :class => "broadcast-mailer" do 5 | %p 6 | = label_tag :to, "To (for testing only)" 7 | = text_field_tag :to, params[:to] || "gregory@practicingruby.com" 8 | 9 | %p 10 | = label_tag :subject 11 | = text_field_tag :subject, params[:subject] 12 | %p 13 | = label_tag :body 14 | %br 15 | = text_area_tag :body, params[:body], :cols => 72 16 | 17 | %p 18 | = submit_tag "Send", 19 | :data => {:confirm => "Are you sure you want to send this to all users?" } 20 | or 21 | = submit_tag "Test" 22 | 23 | - content_for :header_bottom do 24 | :javascript 25 | $(function(){ $('textarea').elastic(); }); 26 | -------------------------------------------------------------------------------- /config/initializers/stripe_webhooks.rb: -------------------------------------------------------------------------------- 1 | Stripe.api_key = STRIPE_SECRET_KEY 2 | 3 | StripeEvent.setup do 4 | subscribe 'charge.failed' do |event| 5 | charge = event.data.object 6 | 7 | gateway = PaymentGateway::Stripe.for_customer(charge.customer) 8 | 9 | gateway.charge_failed(charge) if gateway 10 | end 11 | 12 | subscribe 'customer.subscription.deleted' do |event| 13 | subscription = event.data.object 14 | 15 | gateway = PaymentGateway::Stripe.for_customer(subscription.customer) 16 | 17 | gateway.subscription_ended(subscription) if gateway 18 | end 19 | 20 | subscribe 'invoice.payment_succeeded' do |event| 21 | invoice = event.data.object 22 | 23 | gateway = PaymentGateway::Stripe.for_customer(invoice.customer) 24 | 25 | gateway.payment_created(invoice) if gateway 26 | end 27 | end -------------------------------------------------------------------------------- /app/controllers/admin/broadcasts_controller.rb: -------------------------------------------------------------------------------- 1 | module Admin 2 | class BroadcastsController < ApplicationController 3 | before_filter :admin_only 4 | 5 | def new 6 | 7 | end 8 | 9 | def create 10 | if params[:subject].blank? || params[:body].blank? 11 | flash[:error] = "Subject and body are required" 12 | render(:new) && return 13 | end 14 | 15 | message = { :subject => params[:subject], 16 | :body => params[:body] } 17 | 18 | if params[:commit] == "Test" 19 | message[:to] = params[:to] 20 | 21 | Broadcaster.notify_testers(message) 22 | else 23 | Broadcaster.delay.notify_subscribers(message) 24 | end 25 | 26 | flash[:notice] = "Message sent" 27 | redirect_to :action => :new 28 | 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /test/integration/edit_article_test.rb: -------------------------------------------------------------------------------- 1 | require_relative '../test_helper' 2 | 3 | class EditArticleTest < ActionDispatch::IntegrationTest 4 | setup do 5 | simulated_user.register(Support::SimulatedUser.default) 6 | 7 | @article = FactoryGirl.create(:article, :slug => "awesome-article") 8 | user = User.first 9 | user.admin = true 10 | user.save 11 | end 12 | 13 | test "can edit articles with slugs" do 14 | visit edit_admin_article_path(@article.slug) 15 | 16 | click_button "Update Article" 17 | 18 | assert_current_path article_path(@article) 19 | end 20 | 21 | test "can edit articles without slugs" do 22 | @article.update_attributes(:slug => nil) 23 | 24 | visit edit_admin_article_path(@article.id) 25 | 26 | click_button "Update Article" 27 | 28 | assert_current_path article_path(@article) 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /app/controllers/home_controller.rb: -------------------------------------------------------------------------------- 1 | class HomeController < ApplicationController 2 | before_filter :attempt_user_login, :only => 'chat' 3 | def public_archives 4 | redirect_to(articles_path) && return if current_user.try(:active?) 5 | 6 | @branded_footer = true 7 | @articles = Article.order("published_time DESC").public 8 | 9 | @articles = @articles.decorate 10 | end 11 | 12 | def about 13 | @articles = Article.order("published_time DESC").public 14 | 15 | 16 | @counts = { :published => Article.published.count, 17 | :members => Article.subscriber_only.count, 18 | :public => @articles.count } 19 | end 20 | 21 | def toggle_nav 22 | session[:nav_hidden] = !session[:nav_hidden] 23 | 24 | render :text => "OK" 25 | end 26 | 27 | def chat 28 | render :layout => false 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/tasks/stripe.rake: -------------------------------------------------------------------------------- 1 | include RakeExceptionNotification 2 | 3 | namespace :stripe do 4 | desc 'Adds and updates customers active credit cards' 5 | task :sync_credit_cards => :environment do 6 | User.where(:payment_provider => 'stripe').find_each do |user| 7 | payment_gateway = user.payment_gateway 8 | 9 | stripe_card = payment_gateway.current_credit_card 10 | 11 | card = CreditCard.find_or_create_by_user_id(user.id) 12 | 13 | card.last_four = stripe_card.last4 14 | card.expiration_month = stripe_card.exp_month 15 | card.expiration_year = stripe_card.exp_year 16 | 17 | card.save 18 | end 19 | end 20 | 21 | desc 'Notifies customers that their credit card is about to expire' 22 | task :card_exipration_notice => :environment do 23 | exception_notify do 24 | CardExpirer.call(Date.today.next_month) 25 | end 26 | end 27 | end -------------------------------------------------------------------------------- /app/assets/stylesheets/application.css.sass: -------------------------------------------------------------------------------- 1 | //= require md_preview 2 | //= require_self 3 | //= require_tree '../../../vendor/assets/stylesheets' 4 | 5 | @import compass/reset 6 | @import compass/css3 7 | @import sassy-buttons 8 | 9 | @import partials/mixins 10 | 11 | @import partials/mobile 12 | @import partials/layout 13 | @import partials/fonts 14 | @import partials/landing 15 | @import partials/navigation 16 | @import partials/library 17 | @import partials/articles 18 | @import partials/form 19 | @import partials/comments 20 | @import partials/flash 21 | @import partials/sharebox 22 | @import partials/subscribe 23 | @import partials/contact 24 | @import partials/open_source 25 | @import partials/broadcasts 26 | @import partials/settings 27 | @import partials/admin 28 | @import partials/payments 29 | @import partials/archives 30 | @import partials/email_confirmation_warning 31 | @import partials/chat 32 | -------------------------------------------------------------------------------- /test/factories/user_factory.rb: -------------------------------------------------------------------------------- 1 | FactoryGirl.define do 2 | sequence(:email) { |n| "person#{n}@example.com" } 3 | sequence(:payment_provider_id) { |n| "person_#{n}" } 4 | 5 | factory :user do |u| 6 | u.first_name 'Frank' 7 | u.last_name 'Pepelio' 8 | u.github_nickname 'frankpepelio' 9 | u.notifications_enabled true 10 | u.email_confirmed true 11 | u.email { FactoryGirl.generate(:email) } 12 | u.contact_email { FactoryGirl.generate(:email) } 13 | u.payment_provider 'mailchimp' 14 | u.payment_provider_id { FactoryGirl.generate(:payment_provider_id) } 15 | u.status 'active' 16 | before(:create) { 17 | User.skip_callback(:save, :before, :send_confirmation_email) } 18 | after(:create) { 19 | User.set_callback(:save, :before, :send_confirmation_email) } 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /app/views/shared/_credit_card.html.haml: -------------------------------------------------------------------------------- 1 | .payment-errors= @errors 2 | 3 | .cc-num 4 | %label Card Number 5 | %input.card-number{type: "text", size: 20, autocomplete: "off", 6 | placeholder: "4242-4242-4242-4242"} 7 | .cc-cvc 8 | %label 9 | CVC 10 | %a#show-cvc-help{:href => "#cvc", :tabindex => -1} ? 11 | %input.card-cvc{type:"text", size: 4, autocomplete: "off", placeholder: "123"} 12 | .cc-exp 13 | %label Card Expiration 14 | = select_month nil, { add_month_numbers: true }, 15 | { name: nil, id: nil, class: "card-expiry-month" } 16 | = select_year nil, 17 | { start_year: Date.today.year, end_year: Date.today.year+15}, 18 | { name: nil, id: nil, class: "card-expiry-year"} 19 | 20 | #cvc-help 21 | %p 22 | For added security, we verify your CVC, CVV, or CID code. This code isn't 23 | stored and is checked before processing your payment. 24 | %p= image_tag "payment/cvc.gif" 25 | -------------------------------------------------------------------------------- /public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Practicing Ruby - The change you wanted was rejected (422) 5 | 22 | 23 | 24 | 25 | 26 |
27 |

The change you wanted was rejected.

28 |

Maybe you tried to change something you didn't have access to.

29 |

← practicingruby.com

30 |
31 | 32 | 33 | -------------------------------------------------------------------------------- /app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | def show_email_warning? 3 | current_user && 4 | current_user.active? && 5 | current_user.email_confirmed == false && 6 | session[:dismiss_email_warning] != true 7 | end 8 | 9 | def md(content) 10 | MdPreview::Parser.parse(content) 11 | end 12 | 13 | def beta_testers 14 | yield if current_user.try(:beta_tester) 15 | end 16 | 17 | def error_messages_for(object) 18 | if object.errors.any? 19 | content_tag(:div, :id => "errorExplanation") do 20 | content_tag(:h2) { "Whoops, looks like something went wrong." } + 21 | content_tag(:p) { "Please review the form below and make the appropriate changes." } + 22 | content_tag(:ul) do 23 | object.errors.full_messages.map do |msg| 24 | content_tag(:li) { msg } 25 | end.join("\n").html_safe 26 | end 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Practicing Ruby - The page you were looking for doesn't exist (404) 5 | 22 | 23 | 24 | 25 | 26 |
27 |

The page you were looking for doesn't exist.

28 |

You may have mistyped the address or the page may have moved.

29 |

← practicingruby.com

30 |
31 | 32 | 33 | -------------------------------------------------------------------------------- /app/assets/stylesheets/partials/_payments.sass: -------------------------------------------------------------------------------- 1 | p.note 2 | background-color: #FAFAE2 3 | border: 1px dashed #AE9331 4 | color: #333 5 | text-align: left 6 | padding: 0.75em 7 | margin: 1em -0.75em 8 | +border-radius(4px) 9 | a 10 | color: #333 11 | 12 | #payment-form 13 | border-top: 1px solid #ddd 14 | padding-top: 1em 15 | 16 | .payment-errors 17 | display: none 18 | margin-bottom: 1em 19 | color: $red 20 | font-weight: bold 21 | 22 | .cc-num, .cc-cvc, .cc-exp 23 | display: inline-block 24 | margin-right: 1.75em 25 | .cc-exp 26 | margin-right: 0 27 | 28 | .card-number 29 | width: 200px 30 | 31 | span#processing-spinner 32 | display: inline-block 33 | margin-left: 30px 34 | height: 10px 35 | 36 | p.image img 37 | +image-box 38 | 39 | #cvc-help 40 | display: none 41 | #facebox .cvc-help 42 | padding-top: 20px!important 43 | img 44 | display: block 45 | margin: 0 auto 46 | -------------------------------------------------------------------------------- /test/integration/disabled_accounts_test.rb: -------------------------------------------------------------------------------- 1 | require_relative '../test_helper' 2 | 3 | class DisabledAccountsTest < ActionDispatch::IntegrationTest 4 | setup do 5 | @authorization = FactoryGirl.create(:authorization) 6 | @user = @authorization.user 7 | @article = FactoryGirl.create(:article) 8 | end 9 | 10 | test "can log in normally when account is not disabled" do 11 | sign_user_in 12 | visit articles_path 13 | 14 | assert_equal articles_path, current_path 15 | end 16 | 17 | test "gets rerouted to session problem page when logging into a disabled account" do 18 | @user.disable 19 | sign_user_in 20 | 21 | assert_equal problems_sessions_path, current_path 22 | end 23 | 24 | test "gets rerouted to problem page when disabled after sign-in" do 25 | sign_user_in 26 | @user.disable 27 | 28 | visit user_settings_path 29 | 30 | assert_equal problems_sessions_path, current_path 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /test/integration/article_routing_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "../test_helper" 2 | 3 | class ArticleRoutingTest < ActionDispatch::IntegrationTest 4 | setup do 5 | @article = FactoryGirl.create(:article, :slug => "awesome-article") 6 | simulated_user.register(Support::SimulatedUser.default) 7 | end 8 | 9 | test "by slug" do 10 | visit "/articles/awesome-article" 11 | 12 | assert_content @article.subject 13 | 14 | assert_current_path "/articles/awesome-article" 15 | end 16 | 17 | test "by id" do 18 | id = @article.id 19 | visit "/articles/#{id}" 20 | 21 | assert_content @article.subject 22 | 23 | assert_current_path "/articles/awesome-article" 24 | end 25 | 26 | test "with invalid slug" do 27 | visit "/articles/i-do-no-exist" 28 | 29 | assert_equal 404, page.status_code 30 | end 31 | 32 | test "with invalid id" do 33 | visit "/articles/99999" 34 | 35 | assert_equal 404, page.status_code 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /app/models/reports.rb: -------------------------------------------------------------------------------- 1 | class << (Reports = Object.new) 2 | def payment_provider_counts 3 | Subscription.select("payment_provider, count(*) as user_count") 4 | .where("finish_date is null") 5 | .group("payment_provider") 6 | end 7 | 8 | def paid_subscriber_count 9 | Subscription.where("finish_date is null and payment_provider <> 'free'").count 10 | end 11 | 12 | def recent_signups 13 | Subscription.includes("user") 14 | .where("finish_date is null and payment_provider <> 'free' and start_date > ?", Date.today-7) 15 | .map { |e| "#{e.user.contact_email} (#{e.user.github_nickname})" } 16 | end 17 | 18 | def recent_cancellations 19 | Subscription.includes("user") 20 | .where("finish_date > ? and payment_provider <> 'free'", Date.today-7) 21 | .map { |e| "#{e.user.contact_email} (#{e.user.github_nickname})" } 22 | end 23 | 24 | 25 | end 26 | -------------------------------------------------------------------------------- /app/assets/stylesheets/partials/_navigation.sass: -------------------------------------------------------------------------------- 1 | #home-run 2 | position: fixed 3 | top: 0 4 | right: 40px 5 | background-color: #1b1b1b 6 | padding: 5px 15px 7 | +single-box-shadow(#333, 0, 0, 2px) 8 | +border-bottom-radius(2px) 9 | +single-transition(all, .10s, ease-out, 0) 10 | color: #fff 11 | font-size: 2em 12 | text-decoration: none 13 | z-index: 2 14 | &:hover 15 | padding-top: 15px 16 | 17 | nav 18 | font-family: sans-serif 19 | letter-spacing: normal 20 | width: 100% 21 | padding: 10px 0 22 | background-color: #f0f1f4 23 | border-bottom: 2px solid darken(#f0f1f4, 10%) 24 | ul 25 | width: 750px 26 | margin: 0 auto 27 | li 28 | display: inline 29 | margin-right: 1.5em 30 | a 31 | color: $dark-blue 32 | text-decoration: none 33 | &.subscribe 34 | color: $red 35 | font-weight: bold 36 | &.right 37 | float: right 38 | margin-right: 0 39 | margin-left: 1.5em 40 | -------------------------------------------------------------------------------- /app/assets/stylesheets/partials/_fonts.sass: -------------------------------------------------------------------------------- 1 | @font-face 2 | font-family: 'Folks' 3 | src: url('/fonts/folks-normal.eot') 4 | src: url('/fonts/folks-normal.eot?#iefix') format('embedded-opentype'), url('/fonts/folks-normal.ttf') format('truetype'), url('/fonts/folks-normal.svg#SketchyMedium') format('svg') 5 | font-weight: normal 6 | font-style: normal 7 | @font-face 8 | font-family: 'Folks-Light' 9 | src: url('/fonts/folks-light.eot') 10 | src: url('/fonts/folks-light.eot?#iefix') format('embedded-opentype'), url('/fonts/folks-light.ttf') format('truetype'), url('/fonts/folks-light.svg#SketchyMedium') format('svg') 11 | font-weight: normal 12 | font-style: normal 13 | @font-face 14 | font-family: 'Inconsolata' 15 | src: url('/fonts/inconsolata.eot') 16 | src: url('/fonts/inconsolata.eot?#iefix') format('embedded-opentype'), url('/fonts/inconsolata.ttf') format('truetype'), url('/fonts/inconsolata.svg#SketchyMedium') format('svg') 17 | font-weight: normal 18 | font-style: normal -------------------------------------------------------------------------------- /public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | We're sorry, but something went wrong (500) 5 | 22 | 23 | 24 | 25 | 26 |
27 |

We're sorry, but something went wrong.

28 |

Please let us know about it: support@elmcitycraftworks.org

29 |

← practicingruby.com

30 |
31 | 32 | 33 | -------------------------------------------------------------------------------- /app/views/users/profile.html.haml: -------------------------------------------------------------------------------- 1 | - content_for :settings_panel do 2 | = render :partial => 'sidebar', :locals => {:current => 'profile'} 3 | 4 | = form_for @user, :html => {:class => "setting-panel edit_user"} do |f| 5 | = hidden_field_tag :current_page, :profile 6 | = error_messages_for(@user) 7 | .field 8 | = f.label :contact_email, "Email Address" 9 | = f.text_field :contact_email 10 | = @user.decorate.icon(30, :title => "Change your avatar at Gravatar.com") 11 | %p 12 | = f.label :beta_tester do 13 | = f.check_box :beta_tester 14 | Give me early access to experimental website features 15 | %hr 16 | %p 17 | = f.submit "Update Settings" 18 | = link_to "Unsubscribe from Practicing Ruby", user_path(@user), :method => :delete, 19 | :data => {:confirm => "Are you sure you want to unsubscribe?"}, 20 | :id => "cancel" 21 | %span.cancel-notice.info Cancellation may take up to 24 hours 22 | 23 | = render 'settings_page' 24 | -------------------------------------------------------------------------------- /app/controllers/user_email_controller.rb: -------------------------------------------------------------------------------- 1 | class UserEmailController < ApplicationController 2 | def confirm 3 | user = User.find_by_access_token(params[:secret]) 4 | 5 | if user 6 | user.clear_access_token 7 | user.update_attributes(:email_confirmed => true) 8 | flash[:notice] = "Email address confirmed" 9 | else 10 | flash[:error] = "Sorry that confirmation link is out of date." 11 | end 12 | 13 | redirect_to user_settings_path 14 | end 15 | 16 | def change 17 | @user = current_user 18 | 19 | render :layout => false 20 | end 21 | 22 | def update 23 | @user = current_user 24 | @user.contact_email = params[:user][:contact_email] 25 | changed = @user.changed.include?("contact_email") 26 | @user.save 27 | 28 | # Send the confirmation email even if it wasn't changed 29 | RegistrationMailer.email_confirmation(@user).deliver if !changed 30 | end 31 | 32 | def dismiss_warning 33 | session[:dismiss_email_warning] = true 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /app/models/user_manager.rb: -------------------------------------------------------------------------------- 1 | class UserManager 2 | attr_reader :client, :list_id 3 | 4 | def initialize 5 | @client = Mailchimp::API.new(MailChimpSettings[:api_key]) 6 | @client.throws_exceptions = true 7 | @list_id = MailChimpSettings[:list_id] 8 | end 9 | 10 | def delete_user(email) 11 | client.list_unsubscribe(:id => list_id, 12 | :email_address => email, 13 | :delete_member => true) 14 | 15 | AccountMailer.unsubscribed(email) 16 | end 17 | 18 | def unsubscribed_users 19 | client.list_members(:id => list_id, :status => "unsubscribed")["data"].map {|u| u["email"] } 20 | end 21 | 22 | def disable_unsubscribed_users 23 | unsubscribed_users.each do |email| 24 | user_record = User.where(:email => email).first 25 | 26 | if user_record 27 | user_record.disable 28 | else 29 | puts "No record for #{email}" 30 | end 31 | 32 | delete_user(email) 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/rake_exception_notification.rb: -------------------------------------------------------------------------------- 1 | module RakeExceptionNotification 2 | # Exception notification (rails3) only works as a rack middleware, 3 | # but what if you need notifications inside a rake task or a script? 4 | # This is a quick hack around that. 5 | # 6 | # Source: https://gist.github.com/551136 7 | # 8 | # Wrap your code inside an exception_notify block and you will be notified of exceptions 9 | # 10 | # exception_notify { # Dangerous Code Here } 11 | def exception_notify 12 | yield 13 | rescue Exception => exception 14 | if Rails.env.production? 15 | env = {} 16 | env['exception_notifier.options'] = { 17 | # TODO: DRY this configuration up 18 | :email_prefix => '[Practicing Ruby Rake] ', 19 | :exception_recipients => %w{gregory.t.brown@gmail.com jordan.byron@gmail.com}, 20 | :sections => ['backtrace'] 21 | } 22 | ExceptionNotifier::Notifier.exception_notification(env, exception).deliver 23 | end 24 | raise exception 25 | end 26 | end -------------------------------------------------------------------------------- /app/assets/stylesheets/partials/_landing.sass: -------------------------------------------------------------------------------- 1 | body.home-public_archives, body.articles-index 2 | header 3 | text-align: center 4 | padding: 20px 0 5 | h1, h2 6 | margin: 30px 0 7 | h1 8 | margin-top: 0 9 | color: $red 10 | font-size: 98px 11 | line-height: 98px 12 | h2 13 | color: rgba(69, 76, 86, 0.77) 14 | font-size: 32px 15 | font-family: 'Folks-Light', 'Folks', sans-serif 16 | p#description 17 | color: #888 18 | font-size: 20px 19 | p, h2, li 20 | font-family: sans-serif 21 | letter-spacing: normal 22 | 23 | #landing 24 | padding: 0 20px 25 | background: #fff 26 | border: 2px solid #ddd 27 | border-radius: 2px 28 | 29 | h2.archives-header 30 | margin: 20px 0 31 | padding-top: 10px 32 | color: #454c56 33 | font-size: 20px 34 | border-bottom: 1px solid #ddd 35 | padding-bottom: 5px 36 | a 37 | font-size: 0.6em 38 | float: right 39 | display: block 40 | margin-top: 7px 41 | -------------------------------------------------------------------------------- /app/decorators/user_decorator.rb: -------------------------------------------------------------------------------- 1 | class UserDecorator < Draper::Decorator 2 | delegate_all 3 | 4 | def member_since 5 | h.l(user.created_at.to_date, :format => :long) 6 | end 7 | 8 | def icon(size=32, options={}) 9 | image_path = h.image_path("avatar.png") 10 | 11 | unless user.contact_email.blank? 12 | hash = Digest::MD5.hexdigest(user.contact_email.downcase) 13 | default = CGI.escape("http://#{h.request.host_with_port}#{image_path}") 14 | image_path = "https://www.gravatar.com/avatar/#{hash}?s=#{size}&d=#{default}" 15 | end 16 | 17 | # Manually set height / width so layouts don't collapse while gravatars are 18 | # loading 19 | # 20 | h.image_tag(image_path, options.merge(:alt => user.name, 21 | :style => "width: #{size}px; height: #{size}px;", :class => "user-icon")) 22 | end 23 | 24 | def link_to_github 25 | h.link_to user.github_nickname, github_url 26 | end 27 | 28 | def github_url 29 | "https://github.com/#{user.github_nickname}" 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /app/assets/javascripts/application.js: -------------------------------------------------------------------------------- 1 | //= require jquery 2 | //= require jquery_ujs 3 | //= require md_preview 4 | //= require jquery.elastic 5 | //= require parsley 6 | //= require_tree '../../../vendor/assets/javascripts' 7 | //= require_self 8 | //= require_tree . 9 | 10 | // Setup the PR [Practicing Ruby] Namespace 11 | var PR = PR ? PR : new Object(); 12 | 13 | PR.setupNamespace = function(namespace){ 14 | if(PR[namespace] == undefined) 15 | PR[namespace] = {} 16 | } 17 | 18 | // Facebox Assets 19 | $.facebox.settings.closeImage = '/assets/facebox/closelabel.png'; 20 | $.facebox.settings.loadingImage = '/assets/facebox/loading.gif'; 21 | 22 | $(document).on('click', 'a[rel=facebox]', function(e) { 23 | e.preventDefault(); 24 | $.facebox({ajax: $(this).attr('href')}); 25 | }); 26 | 27 | PR.immediate = function(){ 28 | $('a[rel=tooltip]').tooltip(); 29 | $('.bigtext').each(function() { 30 | var box = $(this) 31 | var fontsize = parseInt(box.css('font-size')); 32 | box.bigtext({maxfontsize: fontsize}); 33 | }); 34 | }; 35 | -------------------------------------------------------------------------------- /app/views/admin/articles/_form.html.haml: -------------------------------------------------------------------------------- 1 | = error_messages_for(@article) 2 | 3 | %p 4 | = f.label :subject 5 | %br 6 | = f.text_field :subject 7 | %p 8 | = f.label :slug 9 | %br 10 | = f.text_field :slug 11 | %p 12 | = f.label :discourse_url 13 | %br 14 | = f.text_field :discourse_url 15 | %p 16 | = f.label :published_time 17 | = f.date_select :published_time 18 | %p 19 | = f.label :volume_id 20 | = f.collection_select :volume_id, Volume.order("number desc"), :id, :number 21 | %p 22 | = f.label :collection_id 23 | = f.collection_select :collection_id, Collection.all, :id, :name 24 | %p 25 | = f.label :issue_number 26 | %br 27 | = f.text_field :issue_number 28 | %p 29 | = f.label :status 30 | %br 31 | = f.text_field :status 32 | %p 33 | = f.check_box :recommended 34 | = f.label :recommended 35 | %p 36 | = f.label :summary 37 | %br 38 | = f.text_area :summary, :rows => 10, :cols => 100 39 | %p 40 | = f.text_area :body, :rows => 60, :cols => 80, :'data-preview' => true 41 | 42 | .control-bar-padding 43 | #control-bar 44 | .content= f.submit 45 | -------------------------------------------------------------------------------- /test/support/outbox.rb: -------------------------------------------------------------------------------- 1 | module Support 2 | class Outbox 3 | def initialize(test, messages) 4 | @test = test 5 | @messages = messages 6 | end 7 | 8 | attr_reader :messages 9 | 10 | def has_message_with(params) 11 | subject_matches = false 12 | bcc_matches = false 13 | 14 | matched_messages = @messages.select do |e| 15 | subject_matches = e.subject.match(params.fetch(:subject, //)) 16 | 17 | bcc_matches = if params[:bcc] 18 | Set[*e.bcc] == Set[*params[:bcc]] 19 | else 20 | true 21 | end 22 | 23 | subject_matches && bcc_matches 24 | end 25 | 26 | unless matched_messages.count == params.fetch(:count, 1) 27 | @test.assert subject_matches, "Expected subject to match but didn't" 28 | @test.assert bcc_matches, "Expected bcc to match but didn't" 29 | 30 | @test.flunk "Number of delivered messages was not as expected" 31 | end 32 | end 33 | 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /app/views/users/notifications.html.haml: -------------------------------------------------------------------------------- 1 | - content_for(:title) { "Notification Settings" } 2 | 3 | - content_for :settings_panel do 4 | = render :partial => 'sidebar', :locals => {:current => 'notifications'} 5 | 6 | = form_for @user, :html => {:class => "setting-panel edit_user"} do |f| 7 | = hidden_field_tag :current_page, :notifications 8 | .info 9 | All notifications will be sent to #{@user.contact_email} 10 | %hr 11 | 12 | = f.label :notify_updates do 13 | = f.check_box :notify_updates 14 | Notify me about content and website updates 15 | 16 | = f.label :notify_conversations do 17 | = f.check_box :notify_conversations 18 | Notify me when conversations start 19 | 20 | = f.label :notify_mentions do 21 | = f.check_box :notify_mentions 22 | Notify me when I am mentioned in a conversation 23 | 24 | = f.label :notify_comment_made do 25 | = f.check_box :notify_comment_made 26 | Notify me every time a comment is made 27 | 28 | %hr 29 | %p 30 | = f.submit "Update Settings" 31 | 32 | = render 'settings_page' 33 | -------------------------------------------------------------------------------- /test/integration/article_footnote_test.rb: -------------------------------------------------------------------------------- 1 | require_relative '../test_helper' 2 | 3 | class ArticleFootnoteTest < ActionDispatch::IntegrationTest 4 | setup do 5 | @authorization = FactoryGirl.create(:authorization) 6 | @user = @authorization.user 7 | @article = FactoryGirl.create(:article) 8 | end 9 | 10 | test "article with footnotes has them rendered" do 11 | @article.update_attributes(:body => " 12 | # Test Article 13 | 14 | This paragraph should have a footnote after it.[^1] 15 | 16 | This paragraph should not. 17 | 18 | [^1]: This should appear as a footnote. 19 | ") 20 | 21 | sign_user_in 22 | 23 | visit article_path(@article.id) 24 | 25 | assert_css "a[rel=footnote]" 26 | assert_css ".footnotes li#fn1" 27 | end 28 | 29 | test "article with no footnotes does not render any" do 30 | @article.update_attributes(:body => " 31 | # Test Article 32 | 33 | This article has no footnotes. 34 | ") 35 | 36 | sign_user_in 37 | 38 | visit article_path(@article.id) 39 | 40 | assert_no_css "a[rel=footnote]" 41 | assert_no_css ".footnotes" 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /test/unit/user_manager_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'mocha/setup' 3 | 4 | class UserManagerTest < ActiveSupport::TestCase 5 | context "disable_unsubscribed_users" do 6 | setup do 7 | @user_manager = UserManager.new 8 | @user_manager.stubs(:delete_user) # Don't try to delete users via the API 9 | end 10 | 11 | test "disables users which exist in the database" do 12 | to_be_disabled_user = FactoryGirl.create(:user) 13 | 14 | @user_manager.expects(:unsubscribed_users).returns([to_be_disabled_user.email]) 15 | 16 | @user_manager.disable_unsubscribed_users 17 | 18 | to_be_disabled_user.reload # Load latest changes from the DB 19 | 20 | assert to_be_disabled_user.disabled?, "Account not disabled" 21 | end 22 | 23 | test "deletes users in mail chimp" do 24 | to_be_disabled_user = FactoryGirl.create(:user) 25 | 26 | @user_manager.expects(:unsubscribed_users).returns([to_be_disabled_user.email]) 27 | @user_manager.expects(:delete_user).with(to_be_disabled_user.email) 28 | 29 | @user_manager.disable_unsubscribed_users 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /app/assets/javascripts/save.js: -------------------------------------------------------------------------------- 1 | /* 2 | * save.js 3 | * Track changes in a form and display a warning if they haven't been submitted 4 | * 5 | * Usage: 6 | * Add 'data-track-changes=true' to any form which you want to track changes 7 | * 8 | *
...
9 | */ 10 | 11 | Save = { 12 | init: function(){ 13 | 14 | // Track keydown so changes are tracked immediately in 15 | // textarea and input[type=text] elements, and change for select 16 | jQuery('form[data-track-changes=true] :input'). 17 | keydown(Save.change).change(Save.change); 18 | 19 | jQuery('form[data-track-changes=true]').submit(function(e){ 20 | jQuery('*[data-changed=true]').attr('data-changed', false); 21 | }); 22 | 23 | window.onbeforeunload = Save.beforeUnload; 24 | }, 25 | beforeUnload: function(){ 26 | if(Save.changesMade()) return "You have unsaved changes."; 27 | }, 28 | changesMade: function(){ 29 | return (jQuery('*[data-changed=true]').length > 0) 30 | }, 31 | change: function(){ 32 | jQuery(this).attr('data-changed', true); 33 | } 34 | 35 | }; 36 | 37 | jQuery(function() { Save.init(); }); -------------------------------------------------------------------------------- /app/views/layouts/application.html.haml: -------------------------------------------------------------------------------- 1 | !!! 5 2 | %html{:lang => 'en'} 3 | %head 4 | %title 5 | = yield(:title).blank? ? controller_name.humanize : yield(:title) 6 | - if yield(:description) 7 | %meta{:name => "description", :content => yield(:description)} 8 | = stylesheet_link_tag 'application' 9 | = stylesheet_link_tag "//netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.css" 10 | = javascript_include_tag 'application' 11 | = csrf_meta_tag 12 | 13 | = render :partial => "shared/ios_icon" 14 | 15 | = yield(:header_bottom) 16 | 17 | %body{class: "#{controller.controller_name}-#{controller.action_name}"} 18 | = render :partial => "shared/navigation" unless @skip_navbar 19 | = render :partial => "shared/broadcasts" 20 | = render :partial => "user_email/warning" 21 | = render :partial => "shared/flash", :locals => { :flash => flash } 22 | 23 | = yield(:nudge) 24 | 25 | %header= yield(:header) 26 | #content 27 | = yield 28 | %footer 29 | = render(:partial => "shared/branded_footer") if @branded_footer 30 | :javascript 31 | PR.immediate(); 32 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | HOST=localhost:3000 2 | 3 | AUTH_MODE=developer 4 | 5 | ### MailCatcher SMTP ### 6 | 7 | SMTP_ADDRESS = localhost 8 | SMTP_PORT = 1025 9 | 10 | ### Github Auth Mode ### 11 | 12 | # AUTH_MODE=github 13 | # GITHUB_CLIENT_KEY=key 14 | # GITHUB_SECRET=seekrit 15 | 16 | ### SendGrid SMTP ### 17 | 18 | # SMTP_ADDRESS = smtp.sendgrid.net 19 | # SMTP_DOMAIN = practicingruby.com 20 | # SMTP_USERNAME = pr-user 21 | # SMTP_PASSWORD = pr-pass 22 | # SMTP_AUTH = plain 23 | # SMTP_TLS = true 24 | 25 | ### Stripe API 26 | 27 | STRIPE_WEBHOOK_PATH=/oh/yeah/stripe/webhooks 28 | # STRIPE_SECRET_KEY=stripe-secret 29 | # STRIPE_PUBLISHABLE_KEY=stripe-publishable 30 | 31 | ### Mailchimp API 32 | 33 | # MAILCHIMP_LIST_ID=listid 34 | # MAILCHIMP_API_KEY=key 35 | # MAILCHIMP_WEBHOOK_KEY=key 36 | 37 | ### Cookie session secret, needed in production mode. Generate using rake secret 38 | # SECRET_TOKEN=token 39 | 40 | ## Cache Cooker ### 41 | 42 | # CACHE_COOKER_URI=practicingruby.com 43 | # CACHE_COOKER_USERNAME=pr-user 44 | # CACHE_COOKER_PASSWORD=pr-pass 45 | # CACHE_COOKER_REALM=Practicing Ruby 46 | 47 | ## Chat URLs 48 | 49 | CHAT_GUEST_URL=http://campfire.com/guest 50 | CHAT_LOGIN_URL=http://campfire.com/login 51 | -------------------------------------------------------------------------------- /test/integration/confirm_user_email_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "../test_helper" 2 | 3 | class ConfirmUserEmailTest < ActionDispatch::IntegrationTest 4 | test "warning is displayed when email hasn't been confirmed" do 5 | simulated_user.register(Support::SimulatedUser.default) 6 | 7 | assert_content "Your email address isn't verified yet" 8 | end 9 | 10 | test "warning is hidden when email is confirmed" do 11 | simulated_user 12 | .register(Support::SimulatedUser.default) 13 | .confirm_email 14 | 15 | assert_no_content "Your email address isn't verified yet" 16 | end 17 | 18 | test "confirmation email is re-sent when address kept the same" do 19 | Capybara.current_driver = Capybara.javascript_driver 20 | 21 | simulated_user 22 | .register(Support::SimulatedUser.default) 23 | .update_email_address 24 | .confirm_email 25 | end 26 | 27 | test "confirmation email is re-sent when address changed" do 28 | Capybara.current_driver = Capybara.javascript_driver 29 | 30 | simulated_user 31 | .register(Support::SimulatedUser.default) 32 | .update_email_address("new-address@practicingruby.com") 33 | .confirm_email 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /config/environments/development.rb: -------------------------------------------------------------------------------- 1 | PracticingRubyWeb::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_controller.perform_caching = true 15 | 16 | # Don't care if the mailer can't send 17 | # config.action_mailer.raise_delivery_errors = false 18 | 19 | # Print deprecation notices to the Rails logger 20 | config.active_support.deprecation = :log 21 | 22 | # Only use best-standards-support built into browsers 23 | config.action_dispatch.best_standards_support = :builtin 24 | 25 | # Do not compress assets 26 | config.assets.compress = false 27 | 28 | # Expands the lines which load the assets 29 | config.assets.debug = true 30 | end 31 | 32 | -------------------------------------------------------------------------------- /app/models/article.rb: -------------------------------------------------------------------------------- 1 | class Article < ActiveRecord::Base 2 | has_many :comments, :as => :commentable 3 | belongs_to :volume 4 | belongs_to :collection 5 | 6 | validates_presence_of :issue_number 7 | validates_uniqueness_of :slug, :allow_blank => true 8 | 9 | def self.in_volume(number) 10 | includes(:volume) 11 | .where("volumes.number = ?", number) 12 | end 13 | 14 | def self.subscriber_only 15 | where(:status => "published") 16 | end 17 | 18 | def self.public 19 | where(:status => "public") 20 | end 21 | 22 | def self.published 23 | where(:status => ["published", "public"]) 24 | end 25 | 26 | def self.drafts 27 | where(:status => "draft") 28 | end 29 | 30 | def self.[](key) 31 | find_by_slug(key) || find_by_id(key) 32 | end 33 | 34 | def to_param 35 | if slug.present? 36 | slug 37 | else 38 | id.to_s 39 | end 40 | end 41 | 42 | def collaboration? 43 | !!(summary =~ /w\./) 44 | end 45 | 46 | def full_subject 47 | "Issue #{issue_number}: #{subject}" 48 | end 49 | 50 | def published? 51 | status == "published" 52 | end 53 | 54 | def published_date 55 | (published_time || created_at).strftime('%Y.%m.%d') 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /app/views/sessions/problems.html.haml: -------------------------------------------------------------------------------- 1 | - content_for(:title) { "Paid subscriptions to Practicing Ruby have ended" } 2 | - content_for(:header) { "Paid subscriptions to Practicing Ruby have ended" } 3 | 4 | %p 5 | All paid subscriptions to Practicing Ruby have been cancelled, 6 | as of December 14, 2015. 7 | 8 | %p 9 | Don't worry, we aren't closing up shop! Practicing Ruby will continue 10 | on throughout 2016, with a total of 12 collaborative works scheduled 11 | to be published, and I will set aside at least 60 working days 12 | in 2016 to assist with these collaborations. 13 | 14 | %p 15 | I will be continuing the work on Practicing Ruby through a 16 | new project called "The Practicing Developer's Workshop". 17 | 18 | %p 19 | As a former Practicing Ruby subscriber, you will be invited to 20 | join this new educational project. Just like Practicing Ruby, 21 | payment is optional, but there is no difference in benefits 22 | between free and paid memberships. 23 | 24 | %p 25 | You should hear from me by the end of the year about all of 26 | this, but if you want to discuss it sooner, feel free to 27 | email me at: gregory@practicingruby.com. 28 | 29 | %p 30 | Thank you for your support! Here's to great things in 2016. -greg -------------------------------------------------------------------------------- /test/integration/credit_card_expiration_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'rake' 3 | 4 | class CreditCardExpirationTest < ActionMailer::TestCase 5 | 6 | test "emails are sent to customers when their cards are about to expire" do 7 | date = Date.today.next_month 8 | 9 | card = FactoryGirl.create(:credit_card, :expiration_year => date.year, 10 | :expiration_month => date.month) 11 | 12 | CardExpirer.call(date) 13 | 14 | message = ActionMailer::Base.deliveries.first 15 | 16 | assert message.to.include?(card.user.contact_email) 17 | assert message.body.to_s.include?(card.last_four) 18 | end 19 | 20 | test "emails are only sent for cards that expire next month" do 21 | this_month = Date.today 22 | FactoryGirl.create(:credit_card, :expiration_year => this_month.year, 23 | :expiration_month => this_month.month ) 24 | 25 | future_month = Date.today + 2.months 26 | FactoryGirl.create(:credit_card, :expiration_year => future_month.year, 27 | :expiration_month => future_month.month ) 28 | 29 | CardExpirer.call(Date.today.next_month) 30 | 31 | assert ActionMailer::Base.deliveries.empty? 32 | end 33 | end -------------------------------------------------------------------------------- /app/models/conversation_notifier.rb: -------------------------------------------------------------------------------- 1 | class ConversationNotifier 2 | def self.broadcast(comment) 3 | new(comment).broadcast 4 | end 5 | 6 | def initialize(comment) 7 | self.comment = comment 8 | self.author = comment.user 9 | self.article = comment.commentable 10 | end 11 | 12 | def broadcast 13 | return unless article.published? 14 | 15 | if comment.first_comment? 16 | ConversationMailer.started(article, conversation_watchers) 17 | else 18 | ConversationMailer.comment_made(comment, comment_watchers) 19 | end 20 | 21 | ConversationMailer.mentioned(comment, mentioned_watchers) 22 | end 23 | 24 | private 25 | 26 | attr_accessor :comment, :author, :article 27 | 28 | def comment_watchers 29 | users.where(:notify_comment_made => true) 30 | end 31 | 32 | def conversation_watchers 33 | users.where(:notify_conversations => true) 34 | end 35 | 36 | def mentioned_watchers 37 | comment.mentioned_users.where(:notify_mentions => true) 38 | end 39 | 40 | def users 41 | relation = User.where("id != ?", author.id) 42 | 43 | if mentioned_watchers.any? 44 | relation.where("users.id NOT IN(?)", mentioned_watchers) 45 | else 46 | relation 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /db/migrate/20110916132222_create_delayed_jobs.rb: -------------------------------------------------------------------------------- 1 | class CreateDelayedJobs < ActiveRecord::Migration 2 | def self.up 3 | create_table :delayed_jobs, :force => true do |table| 4 | table.integer :priority, :default => 0 # Allows some jobs to jump to the front of the queue 5 | table.integer :attempts, :default => 0 # Provides for retries, but still fail eventually. 6 | table.text :handler # YAML-encoded string of the object that will do work 7 | table.text :last_error # reason for last failure (See Note below) 8 | table.datetime :run_at # When to run. Could be Time.zone.now for immediately, or sometime in the future. 9 | table.datetime :locked_at # Set when a client is working on this object 10 | table.datetime :failed_at # Set when all retries have failed (actually, by default, the record is deleted instead) 11 | table.string :locked_by # Who is working on this object (if locked) 12 | table.timestamps 13 | end 14 | 15 | add_index :delayed_jobs, [:priority, :run_at], :name => 'delayed_jobs_priority' 16 | end 17 | 18 | def self.down 19 | drop_table :delayed_jobs 20 | end 21 | end -------------------------------------------------------------------------------- /app/mailers/conversation_mailer.rb: -------------------------------------------------------------------------------- 1 | class ConversationMailer < ActionMailer::Base 2 | def started(article, users) 3 | @article = article 4 | 5 | batch(users) do |addresses| 6 | mail( 7 | :to => "gregory@practicingruby.com", 8 | :bcc => addresses, 9 | :subject => "Conversation has started on '#{@article.subject}'" 10 | ).deliver 11 | end 12 | end 13 | 14 | def mentioned(comment, users) 15 | @article = comment.commentable 16 | 17 | batch(users) do |addresses| 18 | mail( 19 | :to => "gregory@practicingruby.com", 20 | :bcc => addresses, 21 | :subject => "Someone mentioned you in '#{@article.subject}'" 22 | ).deliver 23 | end 24 | end 25 | 26 | def comment_made(comment, users) 27 | @article = comment.commentable 28 | 29 | batch(users) do |addresses| 30 | mail( 31 | :to => "gregory@practicingruby.com", 32 | :bcc => addresses, 33 | :subject => "A comment was added to '#{@article.subject}'" 34 | ).deliver 35 | end 36 | end 37 | 38 | private 39 | 40 | def batch(users) 41 | users.to_notify.find_in_batches(:batch_size => 25) do |group| 42 | yield group.map(&:contact_email) 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /test/unit/decorators/comment_decorator_test.rb: -------------------------------------------------------------------------------- 1 | require_relative '../../test_helper' 2 | 3 | class CommentDecoratorTest < ActiveSupport::TestCase 4 | test "parses at mentions and adds a link to the mentioned users profile" do 5 | frank = FactoryGirl.create(:user, :github_nickname => "frank-pepelio") 6 | comment = FactoryGirl.create(:comment, :body => "@frank-pepelio: Hey dude!") 7 | comment = comment.decorate 8 | 9 | assert comment.content[/ "@unknown: Who are you?") 15 | comment = comment.decorate 16 | 17 | refute comment.content[/ "jordanbyron") 25 | comment = FactoryGirl.create(:comment, 26 | :body => "Dude email me me@jordanbyron.com") 27 | comment = comment.decorate 28 | 29 | refute comment.content[/ [:edit, :update, :destroy] 5 | 6 | def index 7 | @articles = Article.order("created_at DESC") 8 | end 9 | 10 | def new 11 | @article = Article.new 12 | end 13 | 14 | def create 15 | @article = Article.new(params[:article]) 16 | 17 | if @article.save 18 | flash[:notice] = "Article successfully created." 19 | redirect_to article_path(@article) 20 | else 21 | render :action => :new 22 | end 23 | end 24 | 25 | def edit 26 | 27 | end 28 | 29 | def update 30 | if @article.update_attributes(params[:article]) 31 | expire_fragment("article_body_#{@article.id}") 32 | 33 | flash[:notice] = "Article successfully updated." 34 | redirect_to article_path(@article) 35 | else 36 | render :action => :edit 37 | end 38 | end 39 | 40 | def destroy 41 | @article.destroy 42 | 43 | flash[:notice] = "Article successfully destroyed." 44 | redirect_to admin_articles_path 45 | end 46 | 47 | private 48 | 49 | def find_article 50 | @article = Article[params[:id]] 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /app/controllers/admin/announcements_controller.rb: -------------------------------------------------------------------------------- 1 | class Admin::AnnouncementsController < ApplicationController 2 | before_filter :admin_only 3 | before_filter :find_announcement, :only => [:edit, :update, :destroy] 4 | 5 | def index 6 | @announcements = Announcement.order("created_at DESC") 7 | end 8 | 9 | def new 10 | @announcement = Announcement.new(:author_id => current_user.id) 11 | end 12 | 13 | def create 14 | @announcement = Announcement.new(params[:announcement]) 15 | 16 | if @announcement.save 17 | flash[:notice] = "Announcement successfully created." 18 | redirect_to root_path #announcement_path(@announcement) 19 | else 20 | render :action => :new 21 | end 22 | end 23 | 24 | def edit 25 | 26 | end 27 | 28 | def update 29 | if @announcement.update_attributes(params[:announcement]) 30 | flash[:notice] = "Announcement successfully updated." 31 | redirect_to root_path #announcement_path(@announcement) 32 | else 33 | render :action => :edit 34 | end 35 | end 36 | 37 | def destroy 38 | @announcement.destroy 39 | 40 | flash[:notice] = "Announcement successfully destroyed." 41 | redirect_to admin_announcements_path 42 | end 43 | 44 | private 45 | 46 | def find_announcement 47 | @announcement = Announcement.find(params[:id]) 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /app/mailers/account_mailer.rb: -------------------------------------------------------------------------------- 1 | class AccountMailer < ActionMailer::Base 2 | def unsubscribed(address) 3 | @address = address 4 | 5 | mail(:to => address, 6 | :subject => "Sorry to see you go").deliver 7 | end 8 | 9 | def canceled(user) 10 | @user = user 11 | 12 | mail(:to => "support@elmcitycraftworks.org", 13 | :subject => "[Practicing Ruby] Account cancellation").deliver 14 | end 15 | 16 | def failed_payment(user, charge) 17 | @user = user 18 | @charge = charge 19 | 20 | mail(:to => user.contact_email, 21 | :subject => "There was a problem with your payment for Practicing Ruby :(").deliver 22 | end 23 | 24 | def payment_created(user, payment) 25 | @payment = payment 26 | 27 | mail(:to => user.contact_email, 28 | :subject => "Receipt for your payment to practicingruby.com").deliver 29 | 30 | @payment.update_attributes(:email_sent => true) 31 | end 32 | 33 | def card_expiring(card) 34 | @card = card 35 | user = card.user 36 | 37 | mail(:to => user.contact_email, 38 | :subject => "Oh No! Your credit card is expiring next month.").deliver 39 | end 40 | 41 | def mailchimp_yearly_billing(user) 42 | @user = user 43 | 44 | mail(:to => "support@elmcitycraftworks.org", 45 | :subject => "[Practicing Ruby] Mailchimp yearly billing request").deliver 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /db/migrate/20120927154744_add_payment_models.rb: -------------------------------------------------------------------------------- 1 | class AddPaymentModels < ActiveRecord::Migration 2 | def up 3 | create_table :subscriptions do |t| 4 | t.belongs_to :user 5 | t.date :start_date, :null => false 6 | t.date :finish_date 7 | t.text :payment_provider 8 | t.integer :monthly_rate_cents 9 | end 10 | 11 | create_table :payment_logs do |t| 12 | t.belongs_to :user 13 | t.text :raw_data 14 | end 15 | 16 | add_column :users, :payment_provider, :text 17 | add_column :users, :payment_provider_id, :text 18 | 19 | migrate_sql = %{UPDATE users 20 | SET payment_provider = 'mailchimp', 21 | payment_provider_id = users.mailchimp_web_id 22 | WHERE mailchimp_web_id IS NOT NULL } 23 | 24 | User.connection.execute(migrate_sql) 25 | 26 | # TODO: Drop mailchimp_web_id column 27 | #remove_column :users, :mailchimp_web_id 28 | end 29 | 30 | def down 31 | drop_table :subscriptions 32 | drop_table :payment_logs 33 | 34 | #add_column :users, :mailchimp_web_id, :text 35 | 36 | migrate_sql = %{UPDATE users 37 | SET mailchimp_web_id = users.payment_provider_id 38 | WHERE payment_provider = 'mailchimp' } 39 | 40 | User.connection.execute(migrate_sql) 41 | 42 | remove_column :users, :payment_provider 43 | remove_column :users, :payment_provider_id 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /test/integration/change_billing_interval_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "../test_helper" 2 | 3 | class StubCardError < Stripe::CardError 4 | def initialize(message) 5 | super(message, nil, nil, nil, nil, nil) 6 | end 7 | end 8 | 9 | class ChangeBillingIntervalTest < ActionDispatch::IntegrationTest 10 | setup do 11 | Capybara.current_driver = Capybara.javascript_driver 12 | end 13 | 14 | test "can change to yearly billing" do 15 | simulated_user. 16 | register(Support::SimulatedUser.default). 17 | change_billing_interval 18 | 19 | assert_equal "year", User.first.subscriptions.active.interval 20 | end 21 | 22 | test "can change to monthly billing" do 23 | simulated_user. 24 | register(Support::SimulatedUser.default.merge(:billing_interval => 'year')). 25 | change_billing_interval 26 | 27 | assert_equal "month", User.first.subscriptions.active.interval 28 | end 29 | 30 | test "gracefully reports card errors when changing billing methods" do 31 | skip_unless_stripe_configured 32 | 33 | PaymentGateway::Stripe.any_instance.stubs(:change_interval).raises( 34 | StubCardError, "Your card was declined.") 35 | 36 | simulated_user. 37 | register(Support::SimulatedUser.default). 38 | change_billing_interval(stripe: true) 39 | 40 | assert_equal "month", User.first.subscriptions.active.interval 41 | 42 | assert_content "declined" 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /app/controllers/sessions_controller.rb: -------------------------------------------------------------------------------- 1 | class SessionsController < ApplicationController 2 | def new 3 | case ENV["AUTH_MODE"] 4 | when "developer" 5 | redirect_to "/auth/developer" 6 | when "github" 7 | redirect_to "/auth/github" 8 | else 9 | raise "Programmer Error: Invalid auth mode!" 10 | end 11 | end 12 | 13 | def create 14 | auth = request.env['omniauth.auth'] 15 | github_uid = auth["uid"].to_s 16 | 17 | authorization = Authorization.find_or_create_by_github_uid(github_uid) 18 | 19 | session["authorization_id"] = authorization.id 20 | 21 | if authorization.user.blank? 22 | user = User.new(:contact_email => auth["info"]["email"], 23 | :github_nickname => auth["info"]["nickname"]) 24 | user.status = "authorized" 25 | user.save 26 | 27 | authorization.update_attributes(:user_id => user.id) 28 | 29 | redirect_to new_subscription_path 30 | elsif authorization.user.status == "active" 31 | redirect_back_or_default(articles_path) 32 | elsif authorization.user.status == "disabled" 33 | redirect_to problems_sessions_path 34 | else 35 | redirect_to new_subscription_path 36 | end 37 | end 38 | 39 | def destroy 40 | session.delete("authorization_id") 41 | clear_location 42 | redirect_to "/" 43 | end 44 | 45 | def failure 46 | @message = params[:message].humanize if params[:message] 47 | end 48 | 49 | end 50 | -------------------------------------------------------------------------------- /app/views/layouts/maintenance.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Maintenance - Practicing Ruby 5 | 37 | 38 | 39 |
40 |

Practicing Ruby: Down for Maintenance

41 |

42 | Hi Folks! We are down for <%= reason ? reason : "maintenance" %> 43 | as of <%= Time.now.utc.strftime("%H:%M %Z") %>. 44 |

45 |

46 | We will be back <%= deadline ? deadline : "shortly" %>. 47 | Thanks for visiting! 48 |

49 |
50 | 51 | 52 | -------------------------------------------------------------------------------- /app/controllers/subscriptions_controller.rb: -------------------------------------------------------------------------------- 1 | class SubscriptionsController < ApplicationController 2 | before_filter :authenticate, :except => [:redirect, :index] 3 | before_filter :hide_nav 4 | before_filter :ye_shall_not_pass, :only => [:new, :create, :index] 5 | 6 | def index 7 | if %w[authorized pending_confirmation confirmed 8 | payment_pending].include? current_user.try(:status) 9 | redirect_to(:action => :new) && return 10 | end 11 | 12 | @article_count = [Article.published.count / 10, "0+"].join 13 | end 14 | 15 | def create 16 | payment_gateway = current_user.payment_gateway 17 | begin 18 | payment_gateway.subscribe(params) 19 | redirect_to articles_path(:new_subscription => true) 20 | rescue Stripe::CardError => e 21 | @errors = e.message 22 | render :action => :new 23 | end 24 | end 25 | 26 | def redirect 27 | render :layout => false 28 | end 29 | 30 | def coupon_valid 31 | payment_gateway = current_user.payment_gateway 32 | 33 | valid = payment_gateway.coupon_valid?(params[:coupon]) 34 | 35 | render :json => { :coupon_valid => valid }.to_json 36 | end 37 | 38 | private 39 | 40 | def ye_shall_not_pass 41 | if current_user && current_user.status == "active" 42 | redirect_to root_path, :notice => "Your account is already setup." 43 | end 44 | end 45 | 46 | private 47 | 48 | def hide_nav 49 | @hide_nav = true 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /test/integration/broadcast_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class BroadcastTest < ActionDispatch::IntegrationTest 4 | 5 | setup do 6 | @authorization = FactoryGirl.create(:authorization) 7 | @user = @authorization.user 8 | @article = FactoryGirl.create(:article) 9 | end 10 | 11 | test "broadcasts are visible to logged in users" do 12 | announcement = FactoryGirl.create(:announcement, :broadcast => true) 13 | 14 | sign_user_in 15 | 16 | assert_content announcement.broadcast_message 17 | 18 | visit article_path(@article) 19 | 20 | assert_content announcement.broadcast_message 21 | 22 | end 23 | 24 | test "broadcasts are not visible when set to false" do 25 | announcement = FactoryGirl.create(:announcement, :broadcast => false) 26 | 27 | sign_user_in 28 | 29 | assert_no_content announcement.broadcast_message 30 | 31 | visit article_path(@article) 32 | 33 | assert_no_content announcement.broadcast_message 34 | end 35 | 36 | test "broadcasts are only visible when signed in" do 37 | announcement = FactoryGirl.create(:announcement, :broadcast => true) 38 | 39 | share = SharedArticle.find_or_create_by_article_id_and_user_id( 40 | @article.id, @user.id) 41 | 42 | visit shared_article_path(share.secret) 43 | 44 | assert_no_content announcement.broadcast_message 45 | 46 | sign_user_in 47 | 48 | assert_content announcement.broadcast_message 49 | end 50 | 51 | end 52 | -------------------------------------------------------------------------------- /app/decorators/subscription_decorator.rb: -------------------------------------------------------------------------------- 1 | class SubscriptionDecorator < Draper::Decorator 2 | delegate_all 3 | 4 | def status 5 | if subscription.active? 6 | "Active" 7 | else 8 | "Canceled" 9 | end 10 | end 11 | 12 | def amount 13 | rate = subscription.rate_cents || 0.0 14 | currency = h.number_to_currency(rate / 100.0) 15 | 16 | "#{currency}/#{interval}" 17 | end 18 | 19 | def change_billing_interval_message 20 | h.content_tag(:p) do 21 | %{You are about to change to a 22 | #{alternate_billing_interval}ly billing cycle. 23 | Since you have already paid for this #{subscription.interval} 24 | we will prorate your invoice to adjust for the time you've 25 | already paid for.} 26 | end + 27 | h.content_tag(:p) do 28 | action = (subscription.interval == "month" ? "charged" : "refunded") 29 | 30 | %{You will be #{action} after you click the button below and 31 | won't be charged again until #{Date.today + 1.year}.} 32 | end + 33 | h.content_tag(:p) do 34 | h.link_to "Change to #{alternate_billing_interval}ly billing", 35 | h.change_billing_interval_user_path(h.current_user, 36 | :interval => alternate_billing_interval), 37 | :class => 'btn', :method => 'post' 38 | end 39 | end 40 | 41 | def alternate_billing_interval 42 | case subscription.interval 43 | when 'month' 44 | 'year' 45 | when 'year' 46 | 'month' 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /vendor/assets/stylesheets/facebox.css: -------------------------------------------------------------------------------- 1 | #facebox { 2 | position: absolute; 3 | top: 0; 4 | left: 0; 5 | z-index: 100; 6 | text-align: left; 7 | } 8 | 9 | 10 | #facebox .popup{ 11 | position:relative; 12 | border:3px solid rgba(0,0,0,0); 13 | -webkit-border-radius:5px; 14 | -moz-border-radius:5px; 15 | border-radius:5px; 16 | -webkit-box-shadow:0 0 18px rgba(0,0,0,0.4); 17 | -moz-box-shadow:0 0 18px rgba(0,0,0,0.4); 18 | box-shadow:0 0 18px rgba(0,0,0,0.4); 19 | } 20 | 21 | #facebox .content { 22 | display: block; 23 | width: 370px; 24 | padding: 10px; 25 | background: #fff; 26 | -webkit-border-radius:4px; 27 | -moz-border-radius:4px; 28 | border-radius:4px; 29 | } 30 | 31 | #facebox .content > p:first-child{ 32 | margin-top:0; 33 | } 34 | #facebox .content > p:last-child{ 35 | margin-bottom:0; 36 | } 37 | 38 | #facebox .close{ 39 | position:absolute; 40 | top:-3px; 41 | right:5px; 42 | padding:2px; 43 | /* background:#fff;*/ 44 | } 45 | #facebox .close img{ 46 | opacity:0.3; 47 | } 48 | #facebox .close:hover img{ 49 | opacity:1.0; 50 | } 51 | 52 | #facebox .loading { 53 | text-align: center; 54 | } 55 | 56 | #facebox .image { 57 | text-align: center; 58 | } 59 | 60 | #facebox img { 61 | border: 0; 62 | margin: 0; 63 | } 64 | 65 | #facebox_overlay { 66 | position: fixed; 67 | top: 0px; 68 | left: 0px; 69 | height:100%; 70 | width:100%; 71 | } 72 | 73 | .facebox_hide { 74 | z-index:-100; 75 | } 76 | 77 | .facebox_overlayBG { 78 | background-color: #000; 79 | z-index: 99; 80 | } -------------------------------------------------------------------------------- /test/unit/mailers/broadcast_mailer_test.rb: -------------------------------------------------------------------------------- 1 | require_relative '../../test_helper' 2 | 3 | class BroadcastMailerTest < ActionMailer::TestCase 4 | test "users without confirmed emails are not notified" do 5 | do_not_mail = %w{authorized pending_confirmation}.map do |status| 6 | FactoryGirl.create(:user, :status => status) 7 | end 8 | 9 | 5.times { FactoryGirl.create(:user) } 10 | 11 | Broadcaster.notify_subscribers(:body => "Only you all can see this", 12 | :subject => "TEST") 13 | 14 | messages = ActionMailer::Base.deliveries 15 | 16 | assert_equal 5, messages.count 17 | 18 | do_not_mail.each do |user| 19 | refute messages.any? { |m| m.to.include?(user.contact_email) }, 20 | "User with status '#{user.status}' was sent a broadcast" 21 | end 22 | end 23 | 24 | test "article links can be generated from template" do 25 | user = FactoryGirl.create(:user) 26 | slug = "a-fancy-article" 27 | 28 | article = FactoryGirl.create(:article, :slug => slug) 29 | 30 | message_body = "Here's an amazing article\n{{#article}}#{slug}{{/article}}" 31 | 32 | Broadcaster.notify_subscribers(:body => message_body, :subject => "Hi there!") 33 | 34 | message = ActionMailer::Base.deliveries.first 35 | 36 | url = ArticleLink.new(Article[slug]).url(user.share_token) 37 | 38 | expected_body = "Here's an amazing article\n#{url}" 39 | 40 | assert message.body.to_s[expected_body], "Expected link to be expanded, instead got: #{message.body.to_s}" 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/mail_chimp/web_hooks.rb: -------------------------------------------------------------------------------- 1 | module MailChimp 2 | class WebHooks 3 | 4 | attr_reader :params 5 | 6 | def initialize(params) 7 | @params = params 8 | @user_manager = UserManager.new 9 | end 10 | 11 | def process 12 | case request_type 13 | when "subscribe" 14 | subscribe 15 | when "unsubscribe" 16 | unsubscribe 17 | when "profile" 18 | profile 19 | else 20 | params[:type] = "unsupported" 21 | end 22 | 23 | "ok (#{request_type})" 24 | end 25 | 26 | def request_type 27 | params[:type] 28 | end 29 | 30 | def subscribe 31 | if user = find_user 32 | user.enable(params[:data][:web_id]) 33 | else 34 | # Don't create a user based on MailChimp's request. 35 | end 36 | end 37 | 38 | def unsubscribe 39 | find_user.try(:disable) 40 | 41 | unless params[:data][:action] == "delete" 42 | @user_manager.delete_user(params[:data][:email]) 43 | end 44 | end 45 | 46 | def profile 47 | user = find_user 48 | 49 | user.update_attributes(:first_name => params[:data][:merges][:FNAME], 50 | :last_name => params[:data][:merges][:LNAME], 51 | :email => params[:data][:email]) 52 | end 53 | 54 | def find_user 55 | User.where(%{ 56 | (payment_provider = ? AND payment_provider_id = ?) OR LOWER(email) = ?}, 57 | 'mailchimp', params[:data][:web_id], params[:data][:email].downcase 58 | ).first 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /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 | 9 | def create_user(github_nickname) 10 | u = User.new(:github_nickname => github_nickname, 11 | :contact_email => "#{github_nickname}@example.com") 12 | u.status = "active" 13 | 14 | yield u if block_given? 15 | 16 | u.save 17 | Authorization.create(:user_id => u.id, :github_uid => github_nickname) 18 | end 19 | 20 | # ------------------------------------------------------------------------------ 21 | # Create an admin user 22 | # ------------------------------------------------------------------------------ 23 | 24 | create_user("admin") { |u| u.admin = true } 25 | 26 | # ------------------------------------------------------------------------------ 27 | # Create several active accounts 28 | # ------------------------------------------------------------------------------ 29 | 30 | %w[user1 user2 user3].each do |e| 31 | create_user(e) 32 | end 33 | 34 | # ------------------------------------------------------------------------------ 35 | # Create an account for each other account status 36 | # ------------------------------------------------------------------------------ 37 | 38 | %w[authorized pending_confirmation confirmed disabled].each do |e| 39 | create_user(e) { |u| u.status = e } 40 | end 41 | -------------------------------------------------------------------------------- /app/controllers/comments_controller.rb: -------------------------------------------------------------------------------- 1 | class CommentsController < ApplicationController 2 | before_filter :authenticate 3 | before_filter :authenticate_user 4 | before_filter :find_comment, :only => [:show, :update, :destroy] 5 | before_filter :commentator_only, :only => [:update, :destroy] 6 | 7 | def create 8 | @comment = Comment.new(params[:comment]) 9 | @comment.user = current_user 10 | 11 | if @comment.save 12 | flash[:notice] = "Comment posted!" 13 | redirect_to article_path(@comment.commentable, :anchor => "comments") 14 | else 15 | flash[:error] = "Please enter some text to create a comment!" 16 | redirect_to article_path(@comment.commentable) 17 | end 18 | end 19 | 20 | def show 21 | render :text => @comment.body 22 | end 23 | 24 | def update 25 | @comment.update_attributes(:body => params[:value]) 26 | expire_fragment("comment_body_#{@comment.id}") 27 | 28 | decorate 29 | 30 | respond_to do |format| 31 | format.text 32 | end 33 | end 34 | 35 | def destroy 36 | @comment.destroy 37 | 38 | respond_to do |format| 39 | format.js 40 | end 41 | end 42 | 43 | def parse 44 | @comment = Comment.new(:body => params[:text]) 45 | 46 | decorate 47 | 48 | render :text => @comment.content 49 | end 50 | 51 | private 52 | 53 | def find_comment 54 | @comment = Comment.find(params[:id]) 55 | end 56 | 57 | def decorate 58 | @comment = @comment.decorate 59 | end 60 | 61 | def commentator_only 62 | raise "Access Denied" unless @comment.editable_by? current_user 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /test/unit/user_test.rb: -------------------------------------------------------------------------------- 1 | require_relative '../test_helper' 2 | 3 | class UserTest < ActiveSupport::TestCase 4 | setup do 5 | @user = FactoryGirl.create(:user) 6 | end 7 | 8 | context "email" do 9 | test "surpresses whitespace" do 10 | address = "gregory@practicingruby.com" 11 | 12 | assert @user.update_attributes(:contact_email => "#{address} ") 13 | 14 | @user.reload 15 | 16 | assert_equal address, @user.contact_email 17 | end 18 | end 19 | 20 | context "status" do 21 | test "can not be updated through update_attributes" do 22 | @user.update_attributes(:status => "disabled") 23 | 24 | refute @user.status == "disabled", "User#status was updated" 25 | end 26 | 27 | test "can only be set to a valid status" do 28 | @user.status = "FAKE-STATUS" 29 | refute @user.save, "User#status set to an invalid status" 30 | assert @user.errors["status"].any? 31 | end 32 | end 33 | 34 | context "confirmation email" do 35 | setup do 36 | ActionMailer::Base.deliveries.clear 37 | end 38 | 39 | test "isn't sent unless the user is active" do 40 | @user.status = "authorized" 41 | @user.contact_email = "new-email@test.com" 42 | 43 | @user.save 44 | 45 | assert ActionMailer::Base.deliveries.empty?, "Confirmation email sent" 46 | end 47 | 48 | test "is sent when the email has been updated" do 49 | @user.contact_email = "new-email@test.com" 50 | 51 | @user.save 52 | 53 | mail = ActionMailer::Base.deliveries.pop 54 | 55 | assert_match "Confirm", mail.subject 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /app/assets/stylesheets/partials/_archives.sass: -------------------------------------------------------------------------------- 1 | body.articles-index #archives, body.home-public_archives #archives 2 | margin-top: 20px 3 | 4 | p, h2, li 5 | font-family: sans-serif 6 | letter-spacing: normal 7 | h1 8 | font-size: 2.5em 9 | margin: 0.5em 0 10 | h2 11 | //background: #ecf0f1 12 | color: #7B7D83 13 | padding: 10px 0 14 | border-bottom: 1px solid #ddd 15 | ul.articles 16 | padding: 0 20px 17 | margin: 10px 0 18 | li 19 | padding: 20px 0 20 | border-bottom: 1px solid #dddddd 21 | a 22 | color: #333 23 | text-decoration: none 24 | &:hover 25 | color: $light-blue 26 | .title 27 | display: inline 28 | .description 29 | margin-top: 5px 30 | font-size: 0.75em 31 | .details 32 | color: #aaa 33 | font-weight: lighter 34 | float: right 35 | &:last-child 36 | border-bottom: none 37 | table.archives 38 | width: 100% 39 | font-family: sans-serif 40 | letter-spacing: normal 41 | table 42 | width: 100% 43 | margin-bottom: 20px 44 | td 45 | &.month, &.article, &.issue-number, &.description 46 | padding: 5px 0 47 | &.month 48 | text-align: right 49 | &.issue-number 50 | padding: 5px 0 51 | color: #aaa 52 | width: 100px 53 | text-align: right 54 | a 55 | color: #000 56 | text-decoration: none 57 | font-weight: normal 58 | &.description, &.month 59 | padding-top: 0 60 | padding-bottom: 20px 61 | font-size: 0.75em 62 | color: #666666 63 | -------------------------------------------------------------------------------- /app/views/home/about.html.haml: -------------------------------------------------------------------------------- 1 | - content_for :header do 2 | %h1 3 | About Practicing Ruby 4 | 5 | #landing 6 | %p{:style => "font-size: 1.1em"} 7 | We provide concentrated doses of programming experience across a wide range 8 | of topics. 9 | 10 | %p 11 | Our lessons are developed by #{link_to "Gregory Brown", "https://twitter.com/practicingruby"}, 12 | with the help of many contributors. Even though this project is called Practicing Ruby, 13 | most of our articles tend to focus on exploring the field of software development 14 | in general rather than fixating entirely on language-specific details. 15 | 16 | %p 17 | Practicing Ruby's 18 | = link_to "complete library", root_path 19 | is freely available to everyone in the world. See our 20 | = link_to "Open Source", open_source_path 21 | page for all our code and manuscripts, including the 22 | Rails application that runs this website. 23 | 24 | 25 | %div{:style => "border: 1px solid #ccc; padding: 10px; background: #fff"} 26 | %p{:style => "font-size: 1.2em"} 27 | We're proud to be a reader-focused, reader-funded publication. 28 | 29 | %p 30 | Instead of showing advertisements, seeking corporate sponsorship, 31 | or using this website as "content marketing", 32 | we rely on subscription revenue to fund Practicing Ruby's development. 33 | 34 | %p 35 | If you'd like to help support this project, consider joining 36 | = link_to "The Practicing Developer's Workshop", "http://practicingdeveloper.com/workshop/" 37 | 38 | 39 | %p{:style => "text-align: center"}= link_to "Return to Practicing Ruby's archives", root_path 40 | -------------------------------------------------------------------------------- /lib/tasks/setup.rake: -------------------------------------------------------------------------------- 1 | require 'rails_setup' 2 | require 'rainbow/ext/string' 3 | 4 | namespace :setup do 5 | desc 'Create .env file from .env.example' 6 | setup_task :environment do 7 | find_or_create_file("#{Rails.root}/.env", ".env") 8 | done(".env") 9 | end 10 | end 11 | 12 | desc 'Setup Practicing Ruby for development' 13 | setup_task :setup do 14 | 15 | puts # Empty Line 16 | puts "#{heart} You are awesome #{heart}" 17 | 18 | section "Configuration Files" do 19 | database = Rails.root.join('config', 'database.yml').to_s 20 | 21 | find_or_create_file(database, "Database config", true) 22 | done "database.yml" 23 | 24 | Rake::Task["setup:environment"].invoke 25 | end 26 | 27 | section "Database" do 28 | begin 29 | # Check if there are pending migrations 30 | silence { Rake::Task["db:abort_if_pending_migrations"].invoke } 31 | done "Skip: Database already setup" 32 | rescue Exception 33 | silence do 34 | Rake::Task["db:create"].invoke 35 | Rake::Task["db:migrate"].invoke 36 | Rake::Task["db:seed"].invoke 37 | Rake::Task["import:articles"].invoke 38 | end 39 | done "Database setup" 40 | end 41 | end 42 | 43 | section "Dependencies" do 44 | `easy_install Pygments` 45 | 46 | done "Pygments installed" 47 | end 48 | 49 | puts # Empty Line 50 | puts %{#{'===='.color(:green)} Setup Complete #{'===='.color(:green)}} 51 | puts # Empty Line 52 | 53 | if console.agree("Would you like to run the test suite? (y/n)") 54 | silence { Rake::Task["db:test:load"].invoke } 55 | ENV["TRAVIS"] = 'TRUE' # Skip tests that won't run on travis 56 | Rake::Task["test"].invoke 57 | end 58 | 59 | end 60 | -------------------------------------------------------------------------------- /app/views/account_mailer/unsubscribed.text.erb: -------------------------------------------------------------------------------- 1 | Hello there! 2 | 3 | You are now unsubscribed from Practicing Ruby. If you actually 4 | meant to cancel your subscription, you won't be billed 5 | further, and you won't receive further notifications about 6 | Practicing Ruby content. You are of course welcome to 7 | respond to this email with some feedback, but that's 8 | totally optional. It was awesome to have you as a subscriber, 9 | even if you're ready to move on now. 10 | 11 | But there's also a chance that you're receiving this email 12 | and DIDN'T intentionally cancel your account. If that's the 13 | case, I have some bad news and good news for you. 14 | 15 | The bad news is that MailChimp's integration with Amazon Payments 16 | isn't great, and so it never updates people's expiration 17 | dates or card numbers unless they cancel their subscription and 18 | sign up again. There isn't a way for me to detect this, because 19 | Mailchimp handles your payment, not me. 20 | 21 | The good news is that we have switched to Stripe for payments, 22 | which gives us the ability to make sure this problem will 23 | never happen for you again. When you resubscribe, you'll 24 | be able to update your billing information whenever you want, 25 | and you'll also get a warning about any failed payments BEFORE 26 | your account is disabled, not afterwards. 27 | 28 | To resubscribe, please log in to http://practicingruby.com 29 | and you will be guided through the reactivation process. 30 | 31 | Once you've updated your billing information, your account should be 32 | reactivated immediately. But if you run into *any problems at all* 33 | during this process, or if you have questions, simply reply to this 34 | email and I'll respond as soon as I can. 35 | 36 | Thanks! 37 | -greg 38 | -------------------------------------------------------------------------------- /config/environments/test.rb: -------------------------------------------------------------------------------- 1 | PracticingRubyWeb::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 | config.action_mailer.raise_delivery_errors = true 28 | 29 | # Use SQL instead of Active Record's schema dumper when creating the test database. 30 | # This is necessary if your schema can't be completely dumped by the schema dumper, 31 | # like if you have constraints or database-specific column types 32 | # config.active_record.schema_format = :sql 33 | 34 | # Print deprecation notices to the stderr 35 | config.active_support.deprecation = :stderr 36 | 37 | # Configure static asset server for tests with Cache-Control for performance 38 | config.serve_static_assets = true 39 | config.static_cache_control = "public, max-age=3600" 40 | end 41 | -------------------------------------------------------------------------------- /app/assets/stylesheets/partials/_library.sass: -------------------------------------------------------------------------------- 1 | body.articles-index 2 | $blueprint-grid-margin: 20px 3 | $blueprint-grid-width: 18px 4 | $blueprint-grid-columns: 20 5 | 6 | @import "blueprint/grid" 7 | 8 | p, #explore-articles 9 | font-family: sans-serif 10 | letter-spacing: normal 11 | p.center 12 | text-align: center 13 | #explore-articles 14 | +container 15 | margin: 30px auto 16 | h2 17 | color: $dark-blue 18 | font-size: 20px 19 | padding-bottom: 10px 20 | text-align: center 21 | #recommended, #recent 22 | +column(10) 23 | div 24 | // Color nubbins 25 | $i: 0 26 | @each $color in #9b59b6, $green, $red, $green, #9b59b6 27 | &.row_#{$i} 28 | a 29 | border-left: 4px solid 30 | border-left-color: $color 31 | a 32 | border-right: 4px solid 33 | border-right-color: $color 34 | $i: $i + 1 35 | 36 | vertical-align: top 37 | .description 38 | font-size: 12px 39 | color: #666 40 | text-align: justify 41 | margin-top: 5px 42 | a 43 | color: #333 44 | font-size: 13px 45 | text-decoration: none 46 | background-color: #fff 47 | display: block 48 | border: 1px solid #aaa 49 | border-bottom: none 50 | padding: 7px 10px 51 | strong 52 | font-weight: bold 53 | color: #333 54 | &.bottom 55 | border-bottom: 1px solid #aaa 56 | &:hover 57 | background-color: #FAFAE2 58 | &:last-child a 59 | border-bottom: 1px solid #aaa 60 | #recommended 61 | +last 62 | div a 63 | border-left: 1px solid #aaa !important 64 | #recent 65 | div a 66 | border-right: 1px solid #aaa !important 67 | -------------------------------------------------------------------------------- /app/views/home/open_source.html.haml: -------------------------------------------------------------------------------- 1 | - content_for(:header) do 2 | %h1 3 | All of Practicing Ruby is free and open source. 4 | 5 | %p 6 | The spirit of community contribution for the public good is 7 | baked into everything we do here. 8 | 9 | %h3 Our programming lessons are free documentation. 10 | %p 11 | The 12 | = link_to "Markdown-formatted manuscripts", 13 | "http://github.com/elm-city-craftworks/practicing-ruby-manuscripts" 14 | for all of our articles are freely available under the Creative Commons 15 | Attribution-Sharealike license. 16 | 17 | %h3 Our web application is free software. 18 | 19 | %p 20 | The 21 | = link_to "Rails application", 22 | "http://github.com/elm-city-craftworks/practicing-ruby-web" 23 | that powers practicingruby.com is freely available under the GNU Affero GPL 24 | + maintained by Gregory Brown and Jordan Byron. 25 | 26 | %p 27 | Mathias Lafeldt has also developed a 28 | = link_to "Chef cookbook", 29 | "http://github.com/elm-city-craftworks/practicing-ruby-cookbook" 30 | that sets up a production-like testbed environment for our web application. 31 | It is freely available under the Apache License 2.0. 32 | 33 | 34 | %h3 We also work on other F/OSS projects. 35 | 36 | %p 37 | We don't just produce educational materials, we also work on projects that 38 | directly help people, both programmers and non-programmers alike: 39 | 40 | %ul{:style => "padding-left: 20px"} 41 | %li 42 | Gregory Brown is the original author of the 43 | = link_to "Prawn PDF generation toolkit for Ruby.", "http://prawn.majesticseacreature.com" 44 | %li 45 | Jordan Byron developed a Rails-based 46 | = link_to "clinic management system", "https://github.com/mission-of-mercy/mission-of-mercy#readme" 47 | to support the Mission of Mercy charitable healthcare organization. 48 | 49 | %p{:style => "text-align: center"}= link_to "Return to Practicing Ruby's archives", root_path 50 | -------------------------------------------------------------------------------- /app/views/articles/show.haml: -------------------------------------------------------------------------------- 1 | - @skip_navbar = true 2 | - if current_user.try(:active?) 3 | - content_for(:header_bottom) do 4 | :coffeescript 5 | $ -> 6 | PR.Comments.init('#{comments_path}/') 7 | = render "header" 8 | 9 | #article 10 | %hr 11 | %p{:style => "text-align: center; font-size: 0.8em"} 12 | #{link_to "Practicing Ruby", "http://practicingruby.com"} #{@article.issue_number} :: 13 | 14 | - if @article.collaboration? 15 | Published on #{@article.published_date} in collaboration with a special guest. 16 | -else 17 | Published by #{link_to "Gregory Brown", "https://twitter.com/practicingdev"} on #{@article.published_date} 18 | 19 | %hr 20 | - cache("article_body_#{@article.id}") do 21 | = md(@article.body) 22 | %hr 23 | %div{:style => "text-align: center; margin: 10px"} 24 | ==
25 |   26 | 27 | - if @comments 28 | %hr 29 | = render :partial => "articles/comments" 30 | - else 31 | %hr 32 | %div{:style => "text-align: center; margin: 50px"} 33 | = link_to root_path do 34 | = image_tag "//i.imgur.com/hYoGfNJ.png", :width => "75%" -------------------------------------------------------------------------------- /lib/cache_cooker.rb: -------------------------------------------------------------------------------- 1 | # Keep your caches piping hot with CacheCooker! 2 | # 3 | # CacheCooker hooks into your Rails app and helps you keep your caches up-to-date 4 | # after deployments or changes 5 | # 6 | # TODO: Setup 7 | # TODO: Usage 8 | # 9 | class CacheCooker 10 | include HTTParty 11 | 12 | # HTTP Digest Realm 13 | # 14 | def self.realm(realm = nil) 15 | if realm 16 | default_options[:realm] = realm 17 | else 18 | default_options[:realm] 19 | end 20 | end 21 | 22 | # Set these values in `/config/initializers/cache_cooker_settings.rb` 23 | # 24 | # digest_auth 'cachecooker', 'secret' 25 | # base_uri 'http://practicingruby.dev' 26 | # realm 'Practicing Ruby' 27 | 28 | # 'Bake' a given url, creating cache files if necessary 29 | # 30 | def self.bake(url) 31 | CacheCooker.get(url, :headers => {'cache-cooker' => 'enabled'}) 32 | end 33 | 34 | # Include +Oven+ in your ApplicationController and call 35 | # +authenticate_cache_cooker+ in a before filter to check for cache cooker 36 | # requests. 37 | # 38 | # class ApplicationController < ActionController::Base 39 | # include CacheCooker::Oven 40 | # before_filter :authenticate 41 | # 42 | # private 43 | # 44 | # def authenticate 45 | # if authenticate_cache_cooker 46 | # current_user = SomeUser 47 | # end 48 | # end 49 | # end 50 | # 51 | module Oven 52 | private 53 | 54 | def cache_cooker? 55 | !!request.env["HTTP_CACHE_COOKER"] 56 | end 57 | 58 | def authenticate_cache_cooker 59 | return false unless cache_cooker? 60 | 61 | authenticate_or_request_with_http_digest(CacheCooker.realm) do |user| 62 | unless user == CacheCooker.default_options[:digest_auth][:username] 63 | return false 64 | end 65 | 66 | password = CacheCooker.default_options[:digest_auth][:password] 67 | 68 | Digest::MD5.hexdigest([user, CacheCooker.realm, password].join(":")) 69 | end 70 | end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /config/delayed_job.god: -------------------------------------------------------------------------------- 1 | rails_root = File.dirname(File.dirname(__FILE__)) 2 | 3 | God.watch do |w| 4 | script = "cd #{rails_root} && /usr/local/bin/ruby #{rails_root}/script/delayed_job" 5 | w.name = "practicing_ruby_delayed_job" 6 | w.group = "practicing_ruby" 7 | w.interval = 60.seconds 8 | w.start = "#{script} start" 9 | w.restart = "#{script} restart" 10 | w.stop = "#{script} stop" 11 | w.start_grace = 20.seconds 12 | w.restart_grace = 20.seconds 13 | w.pid_file = "#{rails_root}/tmp/pids/delayed_job.pid" 14 | w.log = "#{rails_root}/log/delayed_job.god.log" 15 | w.err_log = "#{rails_root}/log/delayed_job.god.errors.log" 16 | 17 | w.env = { 'RAILS_ENV' => "production" } 18 | 19 | w.behavior(:clean_pid_file) 20 | 21 | # retart if memory gets too high 22 | w.transition(:up, :restart) do |on| 23 | on.condition(:memory_usage) do |c| 24 | c.above = 300.megabytes 25 | c.times = 2 26 | c.notify = 'jordan' 27 | end 28 | end 29 | 30 | # determine the state on startup 31 | w.transition(:init, { true => :up, false => :start }) do |on| 32 | on.condition(:process_running) do |c| 33 | c.running = true 34 | end 35 | end 36 | 37 | # determine when process has finished starting 38 | w.transition([:start, :restart], :up) do |on| 39 | on.condition(:process_running) do |c| 40 | c.running = true 41 | c.interval = 5.seconds 42 | end 43 | 44 | # failsafe 45 | on.condition(:tries) do |c| 46 | c.times = 5 47 | c.transition = :start 48 | c.interval = 5.seconds 49 | c.notify = 'jordan' 50 | end 51 | end 52 | 53 | # start if process is not running 54 | w.transition(:up, :start) do |on| 55 | on.condition(:process_running) do |c| 56 | c.running = false 57 | end 58 | end 59 | 60 | # notify if process exists 61 | w.transition(:up, :start) do |on| 62 | on.condition(:process_exits) { |c| c.notify = 'jordan' } 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /test/integration/shared_article_test.rb: -------------------------------------------------------------------------------- 1 | require_relative '../test_helper' 2 | 3 | class SharedArticleTest < ActionDispatch::IntegrationTest 4 | setup do 5 | @authorization = FactoryGirl.create(:authorization) 6 | @user = @authorization.user 7 | @article = FactoryGirl.create(:article) 8 | end 9 | 10 | test "shared article visible without logging in" do 11 | assert_article_visible(:guest) 12 | end 13 | 14 | test "shared article visit without logging in (draft)" do 15 | @article.update_attributes(:status => "draft") 16 | 17 | assert_article_visible(:guest) 18 | end 19 | 20 | test "shared article visible to logged in users" do 21 | sign_user_in 22 | 23 | assert_article_visible(:subscriber) 24 | end 25 | 26 | test "shared article visible to logged in users (draft)" do 27 | @article.update_attributes(:status => "draft") 28 | 29 | sign_user_in 30 | 31 | assert_article_visible(:subscriber) 32 | end 33 | 34 | (User::STATUSES - User::ACTIVE_STATUSES).each do |e| 35 | test "Users in status #{e} should be treated as guests" do 36 | unauthorized_user = FactoryGirl.create(:user, :status => e) 37 | @authorization.user = unauthorized_user 38 | @authorization.save 39 | 40 | sign_user_in 41 | 42 | assert_article_visible(:guest) 43 | end 44 | end 45 | 46 | 47 | test "requesting invalid share key causes a 404 response" do 48 | visit shared_article_path("notarealkey") 49 | 50 | assert_equal 404, page.status_code 51 | end 52 | 53 | def assert_article_visible(state) 54 | visit article_path(@article, :u => @user.share_token) 55 | 56 | assert_equal 200, page.status_code 57 | 58 | assert_current_path Rails.application.routes.url_helpers.article_path(@article) 59 | assert_url_has_param "u", @user.share_token 60 | 61 | case state 62 | when :guest 63 | assert_content("subscribe") 64 | when :subscriber 65 | assert_content("discourse") 66 | else 67 | raise ArgumentError, "Invalid state: #{state}" 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /test/unit/card_expirer_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class CardExpirerTest < ActiveSupport::TestCase 4 | setup do 5 | #FIXME: Less duplication, push into factories w. better names? 6 | 7 | ActionMailer::Base.deliveries.clear 8 | 9 | 10 | @users = {} 11 | @users[:active_expiring] = FactoryGirl.create(:user, :status => "active") 12 | @users[:inactive_expiring] = FactoryGirl.create(:user, :status => "disabled") 13 | @users[:non_expiring] = FactoryGirl.create(:user, :status => "active") 14 | 15 | 16 | @cards = {} 17 | 18 | @cards[:active_expiring] = FactoryGirl.create(:credit_card, 19 | :expiration_month => Date.today.month, 20 | :expiration_year => Date.today.year, 21 | :user => @users[:active_expiring]) 22 | 23 | @cards[:inactive_expiring] = FactoryGirl.create(:credit_card, 24 | :expiration_month => Date.today.month, 25 | :expiration_year => Date.today.year, 26 | :user => @users[:inactive_expiring]) 27 | 28 | @cards[:non_expiring] = FactoryGirl.create(:credit_card, 29 | :expiration_month => Date.today.month, 30 | :expiration_year => Date.today.year + 1, 31 | :user => @users[:active_expiring]) 32 | 33 | # Just a quick validation to alert us of a bad testing rig 34 | # 35 | assert(@users.values.map { |e| e.contact_email }.uniq.size == @users.size, 36 | "Programmer error, check your factories") 37 | end 38 | 39 | test "only sends notifications to active subscribers" do 40 | CardExpirer.call(Date.today) 41 | 42 | assert ActionMailer::Base.deliveries.count == 1, "Should only send one expiration notice" 43 | 44 | assert_equal [@users[:active_expiring].contact_email], 45 | ActionMailer::Base.deliveries.first.to 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'rails', '3.2.22.2' 4 | gem 'rake', '~> 0.9.0' 5 | gem 'json', '~> 1.7.7' 6 | gem 'multi_xml', '>= 0.5.2' 7 | 8 | gem 'pg', '0.16.0' 9 | gem 'mailchimp' 10 | gem 'omniauth-oauth2', '~> 1.1.1' 11 | gem 'omniauth-github', '~> 1.0.1' 12 | 13 | gem 'redcarpet', "~> 3.1.0" 14 | gem 'albino' 15 | gem 'nokogiri', '~> 1.5.11' 16 | gem 'md_preview' 17 | gem 'md_emoji' 18 | 19 | gem 'stripe' 20 | gem 'stripe_event' 21 | 22 | gem 'will_paginate', '>= 3.0.5' 23 | gem 'haml' 24 | gem 'coffee-filter', '~> 0.1.1' 25 | gem 'jquery-rails', '~> 1.0.19' 26 | gem 'draper' 27 | gem 'rack-pjax', '~> 0.6.0' 28 | 29 | gem 'httparty', '>= 0.10.0' 30 | 31 | gem 'mailhopper', '~> 0.0.4' 32 | gem 'delayed_mailhopper' 33 | gem 'delayed_job', '~> 3.0.3' 34 | gem 'delayed_job_active_record' 35 | gem 'daemons', :require => false 36 | 37 | gem 'whenever' 38 | gem 'rails_setup' 39 | 40 | gem 'mustache' 41 | 42 | gem 'dotenv-rails' 43 | 44 | gem 'pry-rails', groups: [:development, :test] 45 | 46 | group :development do 47 | gem 'dotenv-deployment' 48 | gem 'capistrano' 49 | gem 'capistrano_confirm_branch' 50 | gem 'capistrano-unicorn', :require => false 51 | gem 'capistrano-maintenance' 52 | gem 'capfire' 53 | gem 'foreman' 54 | gem 'mailcatcher' 55 | end 56 | 57 | group :assets do 58 | gem 'sass-rails', '~> 3.2.0' 59 | gem 'coffee-rails', '~> 3.2.0' 60 | gem 'uglifier' 61 | gem 'compass-rails' 62 | gem 'sassy-buttons' 63 | gem 'turbo-sprockets-rails3' 64 | gem 'parsley-rails' 65 | end 66 | 67 | group :test do 68 | gem 'minitest' 69 | gem 'capybara', '=2.0.3' 70 | gem 'capybara-screenshot' 71 | gem 'launchy' 72 | gem 'factory_girl_rails' 73 | gem 'mocha', :require => false 74 | gem 'database_cleaner' 75 | gem 'test_notifier' 76 | gem 'turn', '~> 0.9.5' 77 | gem 'simplecov', :require => false 78 | gem 'poltergeist' 79 | gem 'rubocop', '~> 0.18.1' 80 | end 81 | 82 | group :production do 83 | gem 'god', :require => false 84 | gem 'exception_notification', "~> 4.0.1" 85 | gem 'rack-google_analytics' 86 | gem 'unicorn' 87 | end 88 | -------------------------------------------------------------------------------- /app/assets/stylesheets/partials/_form.sass: -------------------------------------------------------------------------------- 1 | form 2 | input[type=submit] 3 | +classy-button 4 | 5 | label, input[type=text], input[type=email], select 6 | font-size: 14px 7 | font-weight: normal 8 | line-height: 20px 9 | label 10 | display: block 11 | margin-bottom: 5px 12 | color: #333 13 | font-size: 0.75em 14 | input[type=text], input[type=email], select 15 | display: inline-block 16 | height: 20px 17 | padding: 4px 6px 18 | margin: 0 19 | font-size: 14px 20 | line-height: 20px 21 | color: #555 22 | +border-radius(3px) 23 | input[type=text], input[type=email] 24 | background-color: #fff 25 | border: 1px solid #CCC 26 | +single-box-shadow(rgba(0,0,0,0.075), 0, 1px, 1px, false, true) 27 | &:focus 28 | outline: 0 29 | outline: thin dotted 9 30 | border-color: $dark-blue 31 | select 32 | height: 30px 33 | line-height: 30px 34 | background-color: #fff 35 | border: 1px solid #CCC 36 | 37 | #errorExplanation 38 | margin: 1em 0 39 | padding: 1em 40 | background-color: rgba(204, 136, 136, 0.45) 41 | +border-radius(3px) 42 | h2 43 | color: #333 44 | font-size: 1.25em 45 | p 46 | color: #9E4040 47 | ul 48 | list-style-type: disc 49 | margin-left: 2em 50 | li 51 | color: #522 52 | 53 | ul.parsley-error-list li 54 | color: #9E4040 55 | font-size: 0.75em 56 | margin: 5px 0 57 | 58 | input.parsley-error 59 | border: 1px solid #9E4040 !important 60 | 61 | div.field_with_errors 62 | display: inline-block 63 | color: #522 64 | input 65 | border: 1px solid #522 66 | 67 | #admin 68 | font-size: 0.4em 69 | float: right 70 | 71 | form.edit_authorization_link 72 | input[type=email] 73 | font-size: 1.25em 74 | padding: 3px 5px 75 | font-family: 'Folks', sans-serif 76 | vertical-align: middle 77 | width: 350px 78 | margin-right: 0.5em 79 | 80 | form.broadcast-mailer 81 | input[type=text] 82 | width: 400px 83 | font-size: 1.1em 84 | textarea 85 | font-family: "Inconsolata", monospace 86 | font-size: 0.9em 87 | min-height: 100px 88 | -------------------------------------------------------------------------------- /app/assets/stylesheets/partials/_subscribe.sass: -------------------------------------------------------------------------------- 1 | input#subscribe 2 | +money-button 3 | font-size: 1.0em 4 | text-decoration: none 5 | display: block 6 | width: 350px 7 | text-align: center 8 | margin: 20px auto 9 | font-weight: normal 10 | 11 | body.home-public_archives 12 | p 13 | font-family: sans-serif 14 | &.instructions 15 | @extend #share-alert 16 | box-sizing: border-box 17 | text-align: justify 18 | padding: 1em 19 | p.financial-help 20 | color: #777 21 | font-size: 0.8em 22 | text-align: center 23 | font-style: italic 24 | body.subscriptions-new, body.subscriptions-create 25 | font-family: sans-serif 26 | letter-spacing: initial 27 | 28 | #content 29 | margin-top: 3em 30 | width: 550px 31 | background-color: #fff 32 | padding: 1.5em 33 | +single-box-shadow(rgba(0,0,0,0.5), 0, 0, 1px) 34 | +border-radius(2px) 35 | 36 | h1 37 | font-size: 2em 38 | font-family: 'Folks' 39 | .branded 40 | color: $red 41 | #payment-form 42 | .submit 43 | text-align: center 44 | border-top: 1px solid #ddd 45 | margin-top: 1.5em 46 | padding-top: 1.5em 47 | input.submit-button 48 | +money-button 49 | padding: 0.3em 1em 50 | margin: 0 51 | .billing-cycle, .email 52 | display: inline-block 53 | margin-bottom: 1em 54 | .billing-cycle 55 | label.billing-option 56 | display: inline-block 57 | margin-top: 0.25em 58 | margin-right: 1.5em 59 | margin-bottom: 0 60 | 61 | .email 62 | margin-right: 28px 63 | input 64 | width: 200px 65 | footer 66 | margin: 0 67 | 68 | #facebox .content.thanks-box 69 | width: 600px 70 | font-family: sans-serif 71 | padding: 15px 72 | 73 | h1 74 | text-align: center 75 | font-size: 1.5em 76 | img 77 | width: 100% 78 | box-sizing: border-box 79 | p 80 | text-align: center 81 | 82 | #facebox .redirect-warning 83 | font-family: sans-serif 84 | letter-spacing: normal 85 | h1 86 | font-weight: bold 87 | p 88 | font-size: 12px 89 | color: #888 90 | text-align: left 91 | -------------------------------------------------------------------------------- /app/assets/stylesheets/partials/_layout.sass: -------------------------------------------------------------------------------- 1 | $page-width: 750px 2 | 3 | html 4 | font-size: 100% 5 | -webkit-text-size-adjust: 100% 6 | -ms-text-size-adjust: 100% 7 | overflow-y: scroll 8 | margin: 0 9 | padding: 0 10 | 11 | ::-moz-selection, ::selection 12 | background: #A0BDC8 13 | text-shadow: none 14 | 15 | article, aside, details, figure, footer, header, nav, section, summary 16 | display: block 17 | 18 | body 19 | margin: 0 20 | padding: 0 21 | font-size: 1em 22 | line-height: 1.4 23 | background: #f9f9f9 24 | font-family: 'Folks', sans-serif 25 | color: $dark-blue 26 | letter-spacing: 0.04em 27 | 28 | header, #content 29 | width: $page-width 30 | margin: 0 auto 31 | header 32 | margin: 0.5em auto 33 | font-size: 3em 34 | line-height: 48px 35 | i 36 | color: #ccc 37 | span.logo 38 | color: #555 39 | a 40 | text-decoration: none 41 | color: #555 42 | .volume, .collection 43 | span.icon 44 | +group-icon(48px) 45 | vertical-align: top 46 | margin-right: 0.25em 47 | .collection 48 | color: $red 49 | span.icon 50 | background-color: $red 51 | .volume 52 | span.icon 53 | font-size: 0.75em 54 | background-color: $dark-blue 55 | color: #fff 56 | footer 57 | clear: both 58 | text-align: center 59 | padding: 20px 60 | a#ecc-logo 61 | position: relative 62 | width: 246px 63 | height: 165px 64 | display: block 65 | margin: 0 auto 66 | img 67 | position: absolute 68 | left: 0 69 | top: 0 70 | +single-transition(opacity, 0.5s, ease-in-out) 71 | &.black 72 | opacity: 0 73 | &:hover img.black 74 | opacity: 1 75 | p 76 | margin: 1em 0 77 | text-align: justify 78 | 79 | a 80 | color: #1b1b1b 81 | font-weight: bold 82 | 83 | h3 84 | font-size: 1.5em 85 | 86 | strong 87 | color: #000 88 | 89 | hr 90 | background-color: #ddd 91 | color: #ddd 92 | border: none 93 | height: 1px 94 | 95 | a.btn 96 | +classy-button 97 | text-decoration: none 98 | -------------------------------------------------------------------------------- /app/assets/stylesheets/partials/_articles.sass: -------------------------------------------------------------------------------- 1 | ul#articles 2 | background: #fff 3 | +border-radius(4px) 4 | border: 2px solid #ddd 5 | margin: 1em 0 6 | li 7 | padding: 0.5em 1em 8 | font-size: 1.1em 9 | border-bottom: 1px solid #ddd 10 | +single-transition(border-color, 0.25s) 11 | &:first-child 12 | +border-top-radius(4px) 13 | &:last-child 14 | border: none 15 | +border-bottom-radius(4px) 16 | &:hover 17 | +background-image(linear-gradient(#f9f9f9, #e9e9e9)) 18 | span.right 19 | float: right 20 | color: #777 21 | width: 85px 22 | display: inline-block 23 | span.issue-number 24 | width: 150px 25 | display: inline-block 26 | float: right 27 | color: #777 28 | a 29 | text-decoration: none 30 | color: $dark-blue 31 | display: block 32 | 33 | .paginated-list 34 | position: relative 35 | .controls a 36 | position: absolute 37 | top: 50% 38 | margin-top: -21px 39 | &.next 40 | right: -50px 41 | &.previous 42 | left: -50px 43 | &.disabled 44 | +opacity(0.2) 45 | cursor: default 46 | 47 | #article 48 | font-size: 1.1em 49 | margin: 1.5em 0 50 | +markdown 51 | 52 | #article-header 53 | margin-bottom: 1.5em 54 | padding: 0.5em 0 55 | border-bottom: 1px solid #ddd 56 | border-top: 1px solid #ddd 57 | line-height: 24px 58 | font-size: 1.1em 59 | height: 24px 60 | color: #888 61 | .right 62 | float: right 63 | display: inline-block 64 | a[rel=tooltip] 65 | display: inline-block 66 | margin-left: 0.5em 67 | color: #555 68 | text-decoration: none 69 | span.icon 70 | +group-icon(24px) 71 | margin-right: 0 72 | &.volume 73 | font-size: 17px 74 | 75 | #article-footer 76 | padding: 1em 77 | background-color: #ecf0f1 78 | border: 1px solid #ddd 79 | input[type=text] 80 | box-sizing: border-box 81 | width: 100% 82 | font-size: 1em 83 | color: #333 84 | padding: 2px 4px 85 | p 86 | font-family: sans-serif 87 | letter-spacing: normal 88 | text-align: justify 89 | color: #252526 90 | &:last-of-type 91 | margin-bottom: 0 92 | -------------------------------------------------------------------------------- /config/environments/production.rb: -------------------------------------------------------------------------------- 1 | PracticingRubyWeb::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 = :debug 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 | # Compress JavaScripts and CSS 51 | config.assets.compress = true 52 | 53 | # Don't fallback to assets pipeline if a precompiled asset is missed 54 | config.assets.compile = false 55 | 56 | # Generate digests for assets URLs 57 | config.assets.digest = true 58 | end 59 | -------------------------------------------------------------------------------- /app/views/users/billing.html.haml: -------------------------------------------------------------------------------- 1 | - content_for(:title) { "Billing" } 2 | 3 | - content_for :settings_panel do 4 | = render :partial => 'sidebar', :locals => {:current => 'billing'} 5 | 6 | #billing-settings.setting-panel 7 | - if @active_subscription && @active_subscription.payment_provider == "stripe" 8 | = link_to "Switch to #{@active_subscription.alternate_billing_interval}ly billing", '#confirm-interval-change', 9 | :class => 'pull-right btn btn-small', :id => 'change-billing-interval' 10 | - elsif @active_subscription && @active_subscription.payment_provider == "mailchimp" 11 | = link_to "Switch to yearly billing", 12 | mailchimp_yearly_billing_user_path(current_user), 13 | :method => 'post', :remote => true, :class => 'pull-right btn btn-small' 14 | 15 | %h2 Subscription History 16 | 17 | - if @subscriptions.empty? 18 | %p 19 | Your subscription history is not available. If you have any questions 20 | about your account, please email 21 | #{mail_to "support@elmcitycraftworks.org"}. 22 | - else 23 | %table.table-bordered.table.table-hover 24 | %thead 25 | %tr 26 | %th Start 27 | %th End 28 | %th Status 29 | %th Amount 30 | %tbody 31 | - @subscriptions.each do |subscription| 32 | %tr 33 | %td= subscription.start_date 34 | %td= subscription.finish_date 35 | %td= subscription.status 36 | %td= subscription.amount 37 | 38 | - if @active_subscription && @active_subscription.payment_provider == "stripe" 39 | %p 40 | = link_to "Update credit card", '#', 41 | :class => "btn btn-small update-cc" 42 | - if @credit_card 43 | %span#current-credit-card Current Card: #{@credit_card.description} 44 | - if @active_subscription 45 | #confirm-interval-change 46 | %h1 Change your billing cycle 47 | 48 | = @active_subscription.change_billing_interval_message 49 | 50 | #update-credit-card 51 | %h1 Update your credit card 52 | 53 | = form_tag update_credit_card_path, :id => "payment-form" do 54 | = render 'shared/credit_card' 55 | %hr 56 | %p 57 | = submit_tag "Update", :class => "submit-button" 58 | %span#processing-spinner 59 | 60 | = render 'settings_page' 61 | --------------------------------------------------------------------------------