├── lib
├── .gitkeep
├── file_uploader.rb
├── avatar_uploader.rb
├── format_helpers.rb
├── weibo_auth.rb
└── fragment.rb
├── locale
├── en.yml
└── zh_cn.yml
├── public
├── wb_8ebec4284bb2f32a.txt
├── google5ee017e87ea0953c.html
├── favicon.ico
├── images
│ ├── quote.png
│ ├── spinner.gif
│ ├── website.png
│ ├── btn_delete.png
│ ├── btn_sticky.png
│ ├── default_logo.jpg
│ └── robbin_foods_shop.jpg
├── stylesheets
│ ├── default
│ │ ├── x.png
│ │ ├── y.png
│ │ ├── 401.png
│ │ ├── 403.png
│ │ ├── 404.png
│ │ ├── 500.png
│ │ ├── bg.gif
│ │ ├── hd_bg.gif
│ │ ├── search.png
│ │ ├── header_tabs.png
│ │ ├── piont_69c.png
│ │ ├── piont_ddd.png
│ │ ├── document.css
│ │ ├── github.css
│ │ └── content.css
│ ├── attachment.css
│ └── hightlight
│ │ ├── github.min.css
│ │ └── googlecode.min.css
├── BingSiteAuth.xml
├── robots.txt
└── javascripts
│ ├── application.js
│ ├── jquery-ujs.js
│ └── highlight.min.js
├── TODO
├── app
├── views
│ ├── error
│ │ ├── 401.erb
│ │ ├── 403.erb
│ │ ├── 404.erb
│ │ └── 500.erb
│ ├── admin
│ │ ├── index.erb
│ │ ├── new_blog.erb
│ │ ├── _menu.erb
│ │ ├── edit_blog.erb
│ │ ├── attachment_fail.erb
│ │ ├── attachment.erb
│ │ ├── blogs.erb
│ │ ├── new_attachment.erb
│ │ ├── _attachment.erb
│ │ ├── comments.erb
│ │ ├── edit_profile.erb
│ │ ├── accounts.erb
│ │ ├── _form_js.erb
│ │ └── _form.erb
│ ├── blog
│ │ ├── _articles_count.erb
│ │ ├── index.erb
│ │ ├── tag_cloud.erb
│ │ ├── tag.erb
│ │ ├── _comment.erb
│ │ ├── _weibo_share.erb
│ │ ├── create_comment.js.erb
│ │ ├── _right.erb
│ │ ├── _blog.erb
│ │ └── show.erb
│ ├── home
│ │ ├── weibo_callback.erb
│ │ ├── search.erb
│ │ ├── index.erb
│ │ ├── weibo.erb
│ │ ├── login.erb
│ │ └── rss.xml.erb
│ └── layouts
│ │ └── application.erb
├── app.rb
├── controllers
│ ├── blog.rb
│ ├── home.rb
│ └── admin.rb
└── helpers.rb
├── models
├── blog_content.rb
├── attachment.rb
├── blog_comment.rb
├── account.rb
└── blog.rb
├── .components
├── config
├── apps.rb
├── database.example.yml
├── app_config.example.yml
├── database.rb
├── boot.rb
├── rainbows.rb
├── nginx.conf
└── puma.rb
├── .gitignore
├── test
├── models
│ ├── blog_test.rb
│ ├── attachment_test.rb
│ ├── blog_content_test.rb
│ ├── blog_comments_test.rb
│ └── account_test.rb
├── factories.rb
├── app
│ └── controllers
│ │ ├── home_controller_test.rb
│ │ ├── test_controller_test.rb
│ │ ├── admin_controller_test.rb
│ │ └── blog_controller_test.rb
├── test.rake
└── test_config.rb
├── db
├── migrate
│ ├── 011_remove_updated_at_from_blogs.rb
│ ├── 010_add_cached_tag_list_to_blogs.rb
│ ├── 017_remove_category_from_blogs.rb
│ ├── 008_add_modifed_at_to_blogs.rb
│ ├── 013_add_index_to_blogs.rb
│ ├── 015_add_profile_to_accounts.rb
│ ├── 002_create_blog_contents.rb
│ ├── 007_add_counter_to_blogs.rb
│ ├── 001_create_accounts.rb
│ ├── 005_add_account_id_to_blogs.rb
│ ├── 012_create_attachments.rb
│ ├── 006_create_blog_comments.rb
│ ├── 003_create_blogs.rb
│ ├── 009_add_columns_to_blogs.rb
│ ├── 016_add_logo_to_accounts.rb
│ ├── 014_add_auth_to_accounts.rb
│ └── 004_acts_as_taggable_on_migration.rb
├── seeds.rb
└── schema.rb
├── config.ru
├── Rakefile
├── zbatery.sh
├── elastic_index.rb
├── rainbows.sh
├── Gemfile
├── README.md
├── stat_codes.sh
├── Gemfile.lock
└── webcache.md
/lib/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/locale/en.yml:
--------------------------------------------------------------------------------
1 | en:
2 | hello: "Robbin Fan"
--------------------------------------------------------------------------------
/public/wb_8ebec4284bb2f32a.txt:
--------------------------------------------------------------------------------
1 | open.weibo.com
--------------------------------------------------------------------------------
/TODO:
--------------------------------------------------------------------------------
1 | - Next
2 | * update ruby to 2.1 and padrino to 0.12
3 |
--------------------------------------------------------------------------------
/public/google5ee017e87ea0953c.html:
--------------------------------------------------------------------------------
1 | google-site-verification: google5ee017e87ea0953c.html
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/robbin/robbin_site/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/public/images/quote.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/robbin/robbin_site/HEAD/public/images/quote.png
--------------------------------------------------------------------------------
/public/images/spinner.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/robbin/robbin_site/HEAD/public/images/spinner.gif
--------------------------------------------------------------------------------
/public/images/website.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/robbin/robbin_site/HEAD/public/images/website.png
--------------------------------------------------------------------------------
/public/images/btn_delete.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/robbin/robbin_site/HEAD/public/images/btn_delete.png
--------------------------------------------------------------------------------
/public/images/btn_sticky.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/robbin/robbin_site/HEAD/public/images/btn_sticky.png
--------------------------------------------------------------------------------
/app/views/error/401.erb:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/views/error/403.erb:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/images/default_logo.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/robbin/robbin_site/HEAD/public/images/default_logo.jpg
--------------------------------------------------------------------------------
/public/stylesheets/default/x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/robbin/robbin_site/HEAD/public/stylesheets/default/x.png
--------------------------------------------------------------------------------
/public/stylesheets/default/y.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/robbin/robbin_site/HEAD/public/stylesheets/default/y.png
--------------------------------------------------------------------------------
/app/views/error/404.erb:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/BingSiteAuth.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 953E8E8307196A93C605FACB44BAA059
4 |
--------------------------------------------------------------------------------
/public/stylesheets/default/401.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/robbin/robbin_site/HEAD/public/stylesheets/default/401.png
--------------------------------------------------------------------------------
/public/stylesheets/default/403.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/robbin/robbin_site/HEAD/public/stylesheets/default/403.png
--------------------------------------------------------------------------------
/public/stylesheets/default/404.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/robbin/robbin_site/HEAD/public/stylesheets/default/404.png
--------------------------------------------------------------------------------
/public/stylesheets/default/500.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/robbin/robbin_site/HEAD/public/stylesheets/default/500.png
--------------------------------------------------------------------------------
/public/stylesheets/default/bg.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/robbin/robbin_site/HEAD/public/stylesheets/default/bg.gif
--------------------------------------------------------------------------------
/app/views/error/500.erb:
--------------------------------------------------------------------------------
1 |
2 |
Internal Server Error
3 |
--------------------------------------------------------------------------------
/public/images/robbin_foods_shop.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/robbin/robbin_site/HEAD/public/images/robbin_foods_shop.jpg
--------------------------------------------------------------------------------
/public/stylesheets/default/hd_bg.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/robbin/robbin_site/HEAD/public/stylesheets/default/hd_bg.gif
--------------------------------------------------------------------------------
/public/stylesheets/default/search.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/robbin/robbin_site/HEAD/public/stylesheets/default/search.png
--------------------------------------------------------------------------------
/public/stylesheets/default/header_tabs.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/robbin/robbin_site/HEAD/public/stylesheets/default/header_tabs.png
--------------------------------------------------------------------------------
/public/stylesheets/default/piont_69c.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/robbin/robbin_site/HEAD/public/stylesheets/default/piont_69c.png
--------------------------------------------------------------------------------
/public/stylesheets/default/piont_ddd.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/robbin/robbin_site/HEAD/public/stylesheets/default/piont_ddd.png
--------------------------------------------------------------------------------
/models/blog_content.rb:
--------------------------------------------------------------------------------
1 | class BlogContent < ActiveRecord::Base
2 | acts_as_cached
3 | validates :content, :presence => true
4 | end
5 |
--------------------------------------------------------------------------------
/.components:
--------------------------------------------------------------------------------
1 | ---
2 | :orm: activerecord
3 | :test: minitest
4 | :mock: none
5 | :script: jquery
6 | :renderer: erb
7 | :stylesheet: none
8 | :admin_renderer: erb
9 |
--------------------------------------------------------------------------------
/app/views/admin/index.erb:
--------------------------------------------------------------------------------
1 | <%= partial 'admin/menu' %>
2 |
9 |
--------------------------------------------------------------------------------
/lib/file_uploader.rb:
--------------------------------------------------------------------------------
1 | class FileUploader < CarrierWave::Uploader::Base
2 | storage :file
3 |
4 | def store_dir
5 | "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/config/apps.rb:
--------------------------------------------------------------------------------
1 | Padrino.configure_apps do
2 | # enable :sessions
3 | set :session_secret, APP_CONFIG['session_secret']
4 | end
5 |
6 | # Mounts the core application for this project
7 | Padrino.mount('RobbinSite').to('/')
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.DS_Store
2 | log
3 | log/**/*
4 | tmp/**/*
5 | bin/*
6 | .bundle/
7 | config/database.yml
8 | config/app_config.yml
9 | public/uploads
10 | vendor/gems/*
11 | !vendor/gems/cache/
12 | .sass-cache/*
13 | db/*.db
14 |
--------------------------------------------------------------------------------
/models/attachment.rb:
--------------------------------------------------------------------------------
1 | class Attachment < ActiveRecord::Base
2 | mount_uploader :file, FileUploader
3 | validates_presence_of :file
4 |
5 | belongs_to :account
6 | belongs_to :blog
7 | scope :orphan, where(:blog_id => nil)
8 | end
9 |
--------------------------------------------------------------------------------
/test/models/blog_test.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path(File.dirname(__FILE__) + '/../test_config.rb')
2 |
3 | describe "Blog Model" do
4 | it 'can construct a new instance' do
5 | @blog = Blog.new
6 | refute_nil @blog
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/app/views/admin/new_blog.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 | <% form_for @blog, url(:admin, :blog), :id => 'editor' do |f| %>
4 | <%= partial 'admin/form', :object => f %>
5 | <% end %>
6 |
7 |
8 |
9 | <%= partial 'admin/form_js' %>
--------------------------------------------------------------------------------
/test/models/attachment_test.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path(File.dirname(__FILE__) + '/../test_config.rb')
2 |
3 | describe "Attachment Model" do
4 | it 'can construct a new instance' do
5 | @attachment = Attachment.new
6 | refute_nil @attachment
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/db/migrate/011_remove_updated_at_from_blogs.rb:
--------------------------------------------------------------------------------
1 | class RemoveUpdatedAtFromBlogs < ActiveRecord::Migration
2 | def self.up
3 | remove_column :blogs, :updated_at
4 | end
5 |
6 | def self.down
7 | add_column :blogs, :updated_at, :datetime
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/test/models/blog_content_test.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path(File.dirname(__FILE__) + '/../test_config.rb')
2 |
3 | describe "BlogContent Model" do
4 | it 'can construct a new instance' do
5 | @blog_content = BlogContent.new
6 | refute_nil @blog_content
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/db/migrate/010_add_cached_tag_list_to_blogs.rb:
--------------------------------------------------------------------------------
1 | class AddCachedTagListToBlogs < ActiveRecord::Migration
2 | def self.up
3 | add_column :blogs, :cached_tag_list, :string
4 | end
5 |
6 | def self.down
7 | remove_column :blogs, :cached_tag_list
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/test/models/blog_comments_test.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path(File.dirname(__FILE__) + '/../test_config.rb')
2 |
3 | describe "BlogComments Model" do
4 | it 'can construct a new instance' do
5 | @blog_comments = BlogComments.new
6 | refute_nil @blog_comments
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/app/views/blog/_articles_count.erb:
--------------------------------------------------------------------------------
1 | <% if blogs.total_pages > 1 %>
2 | <% content_for :javascripts do %>
3 |
8 | <% end %>
9 | <% end %>
--------------------------------------------------------------------------------
/app/views/admin/_menu.erb:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/db/migrate/017_remove_category_from_blogs.rb:
--------------------------------------------------------------------------------
1 | class RemoveCategoryFromBlogs < ActiveRecord::Migration
2 | def self.up
3 | remove_column :blogs, :category
4 | end
5 |
6 | def self.down
7 | add_column :blogs, :category, :string, :limit => 20, :default => "blog"
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/app/views/admin/edit_blog.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 | <% form_for @blog, url(:admin, :blog, :id => @blog.id), :method => :put, :id => 'editor' do |f| %>
4 | <%= partial 'admin/form', :object => f %>
5 | <% end %>
6 |
7 |
8 |
9 | <%= partial 'admin/form_js' %>
--------------------------------------------------------------------------------
/test/factories.rb:
--------------------------------------------------------------------------------
1 | FactoryGirl.define do
2 | sequence :email do |n|
3 | "email#{n}@gmail.com"
4 | end
5 |
6 | factory :account do
7 | sequence(:email) {|n| "fankai#{n}@gmail.com"}
8 | password 'javaeye'
9 | password_confirmation 'javaeye'
10 | role 'admin'
11 | end
12 | end
--------------------------------------------------------------------------------
/test/app/controllers/home_controller_test.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path(File.dirname(__FILE__) + '/../../test_config.rb')
2 |
3 | describe "HomeController" do
4 | before do
5 | get '/'
6 | end
7 |
8 | it "should return hello world text" do
9 | assert_equal "Hello World", last_response.body
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/test/app/controllers/test_controller_test.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path(File.dirname(__FILE__) + '/../../test_config.rb')
2 |
3 | describe "TestController" do
4 | before do
5 | get '/'
6 | end
7 |
8 | it "should return hello world text" do
9 | assert_equal "Hello World", last_response.body
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/test/app/controllers/admin_controller_test.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path(File.dirname(__FILE__) + '/../../test_config.rb')
2 |
3 | describe "AdminController" do
4 | before do
5 | get '/'
6 | end
7 |
8 | it "should return hello world text" do
9 | assert_equal "Hello World", last_response.body
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/db/migrate/008_add_modifed_at_to_blogs.rb:
--------------------------------------------------------------------------------
1 | class AddModifedAtToBlogs < ActiveRecord::Migration
2 | def self.up
3 | add_column :blogs, :modified_at, :datetime
4 | Blog.all.each {|blog| blog.modified_at = Time.now; blog.save!}
5 | end
6 |
7 | def self.down
8 | remove_column :blogs, :modified_at
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # See http://www.robotstxt.org/wc/norobots.html for documentation on how to use the robots.txt file
2 | User-agent: *
3 | Disallow: /login
4 | Disallow: /search
5 | Disallow: /rss
6 | Disallow: /admin/
7 | Disallow: /uploads/
8 | Disallow: /stylesheets/
9 | Disallow: /javascripts/
10 | Disallow: /images/
11 | Disallow: /*.md$
--------------------------------------------------------------------------------
/db/migrate/013_add_index_to_blogs.rb:
--------------------------------------------------------------------------------
1 | class AddIndexToBlogs < ActiveRecord::Migration
2 | def self.up
3 | add_index :blogs, :content_updated_at
4 | remove_index :blogs, :blog_content_id
5 | end
6 |
7 | def self.down
8 | remove_index :blogs, :content_updated_at
9 | add_index :blogs, :blog_content_id
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/public/stylesheets/attachment.css:
--------------------------------------------------------------------------------
1 | body {
2 | background-color:transparent;
3 | color:black;
4 | font-family: Helvetica, Tahoma, Arial, sans-serif;
5 | font-size:12px;
6 | line-height:1em;
7 | padding: 0;
8 | margin: 0;
9 | }
10 |
11 | a {
12 | color:#006699;
13 | text-decoration:none;
14 | }
15 |
16 | img {
17 | border: 0px;
18 | }
--------------------------------------------------------------------------------
/test/app/controllers/blog_controller_test.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path(File.dirname(__FILE__) + '/../../test_config.rb')
2 |
3 | describe "BlogController" do
4 | before do
5 | get '/'
6 | end
7 |
8 | it "should return hello world text" do
9 | refute_nil last_response.body
10 | # assert_equal "Hello World", last_response.body
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/app/views/admin/attachment_fail.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | <%= stylesheet_link_tag 'attachment' %>
6 | <%= javascript_include_tag 'jquery', 'jquery-ujs', 'application' %>
7 |
8 |
9 | 上传失败
10 |
11 |
12 |
--------------------------------------------------------------------------------
/db/migrate/015_add_profile_to_accounts.rb:
--------------------------------------------------------------------------------
1 | class AddProfileToAccounts < ActiveRecord::Migration
2 | def self.up
3 | add_column :accounts, :profile_url, :string
4 | add_column :accounts, :profile_image_url, :string
5 | end
6 |
7 | def self.down
8 | remove_column :accounts, :profile_url
9 | remove_column :accounts, :profile_image_url
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/db/migrate/002_create_blog_contents.rb:
--------------------------------------------------------------------------------
1 | class CreateBlogContents < ActiveRecord::Migration
2 | def self.up
3 | create_table :blog_contents do |t|
4 | # set limit 64k+1 to force column type longtext
5 | t.text :content, :null => false, :limit => 64.kilobytes + 1
6 | end
7 | end
8 |
9 | def self.down
10 | drop_table :blog_contents
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/app/views/admin/attachment.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | <%= stylesheet_link_tag 'attachment' %>
6 | <%= javascript_include_tag 'jquery', 'jquery-ujs', 'application' %>
7 |
8 |
9 | <%= partial 'admin/attachment', :object => @attachment %>
10 |
11 |
12 |
--------------------------------------------------------------------------------
/test/test.rake:
--------------------------------------------------------------------------------
1 | require 'rake/testtask'
2 |
3 | test_tasks = Dir['test/*/'].map { |d| File.basename(d) }
4 |
5 | test_tasks.each do |folder|
6 | Rake::TestTask.new("test:#{folder}") do |test|
7 | test.pattern = "test/#{folder}/**/*_test.rb"
8 | test.verbose = true
9 | end
10 | end
11 |
12 | desc "Run application test suite"
13 | task 'test' => test_tasks.map { |f| "test:#{f}" }
14 |
--------------------------------------------------------------------------------
/app/views/home/weibo_callback.erb:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/db/migrate/007_add_counter_to_blogs.rb:
--------------------------------------------------------------------------------
1 | class AddCounterToBlogs < ActiveRecord::Migration
2 | def self.up
3 | add_column :blogs, :comments_count, :integer, :default => 0, :null => false
4 | add_column :accounts, :blogs_count, :integer, :default => 0, :null => false
5 | end
6 |
7 | def self.down
8 | remove_column :blogs, :comments_count
9 | remove_column :accounts, :blogs_count
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/app/views/home/search.erb:
--------------------------------------------------------------------------------
1 | <% @nav = 'blog' %>
2 | <% @title = "博客文章搜索结果" %>
3 | <% @description = '博客文章搜索结果' %>
4 |
5 |
6 |
博客文章
7 | <%= partial 'blog/blog', :collection => @blogs %>
8 |
9 | 搜索结果:<%= @blogs.size %>篇文章
10 |
11 |
12 |
13 |
14 | <%= partial 'blog/right' %>
15 |
--------------------------------------------------------------------------------
/db/migrate/001_create_accounts.rb:
--------------------------------------------------------------------------------
1 | class CreateAccounts < ActiveRecord::Migration
2 | def self.up
3 | create_table :accounts do |t|
4 | t.string :name
5 | t.string :email
6 | t.string :crypted_password
7 | t.string :role
8 | t.datetime :created_at
9 | end
10 | add_index :accounts, :email, :unique => true
11 | end
12 |
13 | def self.down
14 | drop_table :accounts
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/db/migrate/005_add_account_id_to_blogs.rb:
--------------------------------------------------------------------------------
1 | class AddAccountIdToBlogs < ActiveRecord::Migration
2 | def self.up
3 | add_column :blogs, :account_id, :integer
4 | add_index :blogs, :account_id
5 | Blog.all.each do |blog|
6 | if blog.account.blank?
7 | blog.account_id = 1; blog.save!
8 | end
9 | end
10 | end
11 |
12 | def self.down
13 | remove_column :blogs, :account_id
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/db/migrate/012_create_attachments.rb:
--------------------------------------------------------------------------------
1 | class CreateAttachments < ActiveRecord::Migration
2 | def self.up
3 | create_table :attachments do |t|
4 | t.string :file
5 | t.integer :account_id
6 | t.integer :blog_id
7 | t.datetime :created_at
8 | end
9 | add_index :attachments, :account_id
10 | add_index :attachments, :blog_id
11 | end
12 |
13 | def self.down
14 | drop_table :attachments
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/db/migrate/006_create_blog_comments.rb:
--------------------------------------------------------------------------------
1 | class CreateBlogComments < ActiveRecord::Migration
2 | def self.up
3 | create_table :blog_comments do |t|
4 | t.references :account
5 | t.references :blog
6 | t.text :content
7 | t.datetime :created_at
8 | end
9 | add_index :blog_comments, :account_id
10 | add_index :blog_comments, :blog_id
11 | end
12 |
13 | def self.down
14 | drop_table :blog_comments
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/db/migrate/003_create_blogs.rb:
--------------------------------------------------------------------------------
1 | class CreateBlogs < ActiveRecord::Migration
2 | def self.up
3 | create_table :blogs do |t|
4 | t.string :title, :limit => 255, :null => false
5 | t.string :slug_url, :limit => 255
6 | t.integer :view_count, :default => 0, :null => false
7 | t.references :blog_content, :null => false
8 | t.timestamps
9 | end
10 | add_index :blogs, :blog_content_id
11 | end
12 |
13 | def self.down
14 | drop_table :blogs
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/test/models/account_test.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path(File.dirname(__FILE__) + '/../test_config.rb')
2 |
3 | describe "Account Model" do
4 |
5 | before do
6 | @admin = create(:account)
7 | end
8 |
9 | it 'can construct a new instance' do
10 | account = build(:account)
11 | refute_nil account
12 | end
13 |
14 | it 'can pass attribute' do
15 | user = create(:account, :email => "test@test.com")
16 | assert_equal("test@test.com", user.email)
17 | refute @admin.new_record?
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/app/views/home/index.erb:
--------------------------------------------------------------------------------
1 | <% @nav = 'home' %>
2 | <% @title = "#{APP_CONFIG['site_title']} | 第#{params[:page]}页" if params[:page] && params[:page].to_i > 1 %>
3 |
4 |
5 | <%= partial 'blog/blog', :collection => @blogs %>
6 |
7 | <%= (will_paginate @blogs, :previous_label => '前一页', :next_label => '后一页').to_s.html_safe %>
8 |
9 |
10 |
11 |
12 | <%= partial 'blog/right' %>
13 |
14 |
15 | <%= partial 'blog/articles_count', :locals => {:blogs => @blogs} %>
--------------------------------------------------------------------------------
/app/views/home/weibo.erb:
--------------------------------------------------------------------------------
1 | <% @nav = 'weibo' %>
2 | <% @title = '新浪微博最近更新内容' %>
3 | <% @description = '我的新浪微博最近更新的内容' %>
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/lib/avatar_uploader.rb:
--------------------------------------------------------------------------------
1 | class AvatarUploader < CarrierWave::Uploader::Base
2 | include CarrierWave::MiniMagick
3 | storage :file
4 |
5 | process :resize_to_fit => [80, 80]
6 | process :convert => 'png'
7 |
8 | def filename
9 | "logo.#{model.logo.file.extension}" if original_filename
10 | end
11 |
12 | def store_dir
13 | "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
14 | end
15 |
16 | def default_url
17 | "/images/default_logo.jpg"
18 | end
19 |
20 | def extension_white_list
21 | %w(jpg jpeg gif png)
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/config.ru:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env rackup
2 | # encoding: utf-8
3 |
4 | # This file can be used to start Padrino,
5 | # just execute it from the command line.
6 |
7 | require File.expand_path("../config/boot.rb", __FILE__)
8 |
9 | # use ruby standard logger because padrino logger has odd error in my production environment.
10 | require 'logger'
11 | class ::Logger; alias_method :write, :<<; end
12 |
13 | if ENV['RACK_ENV'] == 'production'
14 | logger = ::Logger.new("log/production.log")
15 | logger.level = ::Logger::WARN
16 | use Rack::CommonLogger, logger
17 | end
18 |
19 | run Padrino.application
20 |
--------------------------------------------------------------------------------
/db/migrate/009_add_columns_to_blogs.rb:
--------------------------------------------------------------------------------
1 | class AddColumnsToBlogs < ActiveRecord::Migration
2 | def self.up
3 | rename_column :blogs, :modified_at, :content_updated_at
4 | add_column :blogs, :commentable, :boolean, :default => true, :null => false
5 | add_column :blogs, :original, :boolean, :default => true, :null => false
6 | add_column :blogs, :original_url, :string, :limit => 255
7 | end
8 |
9 | def self.down
10 | rename_column :blogs, :content_updated_at, :modified_at
11 | remove_column :blogs, :commentable
12 | remove_column :blogs, :original
13 | remove_column :blogs, :original_url
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/test/test_config.rb:
--------------------------------------------------------------------------------
1 | PADRINO_ENV = 'test' unless defined?(PADRINO_ENV)
2 | require File.expand_path('../../config/boot', __FILE__)
3 |
4 | FactoryGirl.find_definitions
5 | DatabaseCleaner.strategy = :transaction
6 |
7 | class MiniTest::Unit::TestCase
8 | include Rack::Test::Methods
9 | include FactoryGirl::Syntax::Methods
10 |
11 | def setup
12 | DatabaseCleaner.start
13 | end
14 |
15 | def teardown
16 | DatabaseCleaner.clean
17 | end
18 |
19 | def app
20 | ##
21 | # You can handle all padrino applications using instead:
22 | # Padrino.application
23 | RobbinSite.tap { |app| }
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/app/views/blog/index.erb:
--------------------------------------------------------------------------------
1 | <% @nav = 'blog' %>
2 | <% @title = "最近博客文章" %>
3 | <% @title << " | 第#{params[:page]}页" if params[:page] && params[:page].to_i > 1 %>
4 |
5 | <% @description = '最近发表的博客文章列表' %>
6 |
7 |
8 |
博客文章
9 | <%= partial 'blog/blog', :collection => @blogs %>
10 |
11 | <%= (will_paginate @blogs, :previous_label => '前一页', :next_label => '后一页').to_s.html_safe %>
12 |
13 |
14 |
15 |
16 | <%= partial 'blog/right' %>
17 |
18 |
19 | <%= partial 'blog/articles_count', :locals => {:blogs => @blogs} %>
--------------------------------------------------------------------------------
/db/migrate/016_add_logo_to_accounts.rb:
--------------------------------------------------------------------------------
1 | class AddLogoToAccounts < ActiveRecord::Migration
2 | def self.up
3 | add_column :accounts, :logo, :string
4 | add_column :blogs, :category, :string, :limit => 20
5 | Blog.transaction do
6 | Blog.all.each do |blog|
7 | blog.category = 'blog' if blog.tag_list.include?('blog')
8 | blog.category = 'note' if blog.tag_list.include?('note')
9 | blog.tag_list = blog.tag_list - ['note', 'blog']
10 | blog.save!
11 | end
12 | end
13 | end
14 |
15 | def self.down
16 | remove_column :accounts, :logo
17 | remove_column :blogs, :category
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require 'bundler/setup'
2 | require 'padrino-core/cli/rake'
3 |
4 | PadrinoTasks.use(:database)
5 | PadrinoTasks.use(:activerecord)
6 | PadrinoTasks.init
7 |
8 | namespace :metric do
9 | desc "project statistics"
10 | task 'stat' do
11 | puts "\nRuby:"
12 | stat_files Dir.glob('**/*.rb') - Dir.glob('test/**/*.rb') - Dir.glob('db/**/*.rb') - Dir.glob('config/**/*.rb')
13 | end
14 | end
15 |
16 | private
17 | def stat_files fs
18 | c = 0
19 | fc = 0
20 | fs.each do |f|
21 | fc += 1
22 | data = File.binread f
23 | c += data.count "\n"
24 | end
25 | puts "files: #{fc}"
26 | puts "lines: #{c}"
27 | end
28 |
29 |
30 |
--------------------------------------------------------------------------------
/app/views/home/login.erb:
--------------------------------------------------------------------------------
1 |
2 | <% form_for @account, url(:login) do |f| %>
3 | <%= f.error_messages %>
4 |
5 | <%= f.label :email, :caption => "Email地址" %>
6 | <%= f.text_field :email %>
7 |
8 |
9 | <%= f.label :password, :caption => "输入密码" %>
10 | <%= f.password_field :password %>
11 |
12 |
13 | <%= check_box_tag :remember_me, :checked => true %>
14 | <%= label_tag :remember_me, :caption => "两周内自动登录" %>
15 |
16 |
17 | <%= f.submit '', :class => 'login' %>
18 | <%= link_to '', url(:weibo_login), :class => 'sina_btn' %>
19 |
20 | <% end %>
21 |
22 |
23 |
--------------------------------------------------------------------------------
/zbatery.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | # set ruby GC parameters
4 | RUBY_HEAP_MIN_SLOTS=600000
5 | RUBY_FREE_MIN=200000
6 | RUBY_GC_MALLOC_LIMIT=60000000
7 | export RUBY_HEAP_MIN_SLOTS RUBY_FREE_MIN RUBY_GC_MALLOC_LIMIT
8 |
9 | pid="log/rainbows.pid"
10 |
11 | case "$1" in
12 | start)
13 | bundle exec zbatery -c config/rainbows.rb -E production -D
14 | ;;
15 | stop)
16 | kill `cat $pid`
17 | ;;
18 | force-stop)
19 | kill -9 `cat $pid`
20 | ;;
21 | restart)
22 | $0 stop
23 | sleep 2
24 | $0 start
25 | ;;
26 | reload)
27 | kill -USR2 `cat $pid`
28 | ;;
29 | *)
30 | echo $"Usage: $0 {start|stop|force-stop|restart|reload}"
31 | ;;
32 | esac
--------------------------------------------------------------------------------
/app/views/blog/tag_cloud.erb:
--------------------------------------------------------------------------------
1 | <% @nav = 'tag' %>
2 | <% @title = '文章分类' %>
3 | <% @description = '全站文章所有文章分类的列表' %>
4 |
5 |
6 |
文章分类
7 |
8 |
9 |
10 | <% Blog.cached_tag_cloud.each do |tag| %>
11 | -
12 | <%= link_to "#{tag.name}".html_safe, url(:blog, :tag, :name => tag.name), :class => 'tag' %>
13 | X <%= tag.count %>
14 |
15 | <% end %>
16 |
17 |
18 |
19 |
20 |
21 |
22 | <%= partial 'blog/right' %>
23 |
24 |
--------------------------------------------------------------------------------
/db/migrate/014_add_auth_to_accounts.rb:
--------------------------------------------------------------------------------
1 | class AddAuthToAccounts < ActiveRecord::Migration
2 | def self.up
3 | remove_index :accounts, :email
4 | add_column :accounts, :uid, :string
5 | add_column :accounts, :provider, :string, :limit => 20
6 | add_column :accounts, :comments_count, :integer, :default => 0, :null => false
7 | remove_column :blogs, :original
8 | remove_column :blogs, :original_url
9 | end
10 |
11 | def self.down
12 | remove_column :accounts, :uid
13 | remove_column :accounts, :provider
14 | remove_column :accounts, :comments_count
15 | add_column :blogs, :original, :boolean
16 | add_column :blogs, :original_url, :string
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/config/database.example.yml:
--------------------------------------------------------------------------------
1 | # MySQL. Versions 4.1 and 5.0 are recommended.
2 | #
3 | # Install the MYSQL driver
4 | # gem install mysql2
5 | #
6 | # Ensure the MySQL gem is defined in your Gemfile
7 | # gem 'mysql2'
8 | #
9 | # And be sure to use new-style password hashing:
10 | # http://dev.mysql.com/doc/refman/5.0/en/old-client.html
11 | development: &default
12 | adapter: mysql2
13 | encoding: utf8
14 | reconnect: false
15 | database: robbin_site
16 | pool: 5
17 | username: root
18 | password: fankai
19 | host: localhost
20 | socket: /tmp/mysql.sock
21 |
22 | test:
23 | <<: *default
24 | database: robbin_site_test
25 |
26 | production:
27 | <<: *default
28 | reconnect: true
29 | pool: 16
--------------------------------------------------------------------------------
/app/views/blog/tag.erb:
--------------------------------------------------------------------------------
1 | <% @nav = 'tag' %>
2 | <% @title = params[:name].force_encoding("UTF-8") + "分类文章" %>
3 | <% @title << " | 第#{params[:page]}页" if params[:page] && params[:page].to_i > 1 %>
4 | <% @description = params[:name].force_encoding("UTF-8") + "分类下所有文章列表" %>
5 |
6 |
7 |
Tag: <%= params[:name].force_encoding("UTF-8") %>
8 | <%= partial 'blog/blog', :collection => @blogs %>
9 |
10 | <%= (will_paginate @blogs, :previous_label => '前一页', :next_label => '后一页').to_s.html_safe %>
11 |
12 |
13 |
14 |
15 | <%= partial 'blog/right' %>
16 |
17 |
18 | <%= partial 'blog/articles_count', :locals => {:blogs => @blogs} %>
--------------------------------------------------------------------------------
/app/app.rb:
--------------------------------------------------------------------------------
1 | class RobbinSite < Padrino::Application
2 | use ActiveRecord::ConnectionAdapters::ConnectionManagement
3 | register Padrino::Rendering
4 | register Padrino::Helpers
5 | register WillPaginate::Sinatra
6 |
7 | enable :sessions
8 | mime_type :md, 'text/plain'
9 |
10 | # layout :my_layout # Layout can be in views/layouts/foo.ext or views/foo.ext (default :application)
11 |
12 | error ActiveRecord::RecordNotFound do
13 | halt 404
14 | end
15 |
16 | error 401 do
17 | render 'error/401'
18 | end
19 |
20 | error 403 do
21 | render 'error/403'
22 | end
23 |
24 | error 404 do
25 | render 'error/404', :layout => 'application'
26 | end
27 |
28 | error 500 do
29 | render 'error/500'
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/config/app_config.example.yml:
--------------------------------------------------------------------------------
1 | development: &default
2 | session_secret: '1234567890'
3 | site_url: 'http://localhost:3000'
4 | site_title: 'robbin的自言自语'
5 | site_motto: 'Small is beautiful, constraint is liberty.'
6 | site_description: '范凯个人网站,发表个人原创的博客文章和学习笔记,包括互联网产品,研发和运营类的文章'
7 | show_me_url: 'http://robbinfan.com/blog/28/about-robbin'
8 | blog_search_ping: false
9 | google_cse: '007540752216561218107:cgbwg20yn_o'
10 | weibo_api_key: '1234567890'
11 | weibo_api_secret: '1234567890'
12 | weibo_redirect_uri: 'http://robbinfan.com/weibo_callback'
13 | weibo_uid: '1654762921'
14 | weibo_uid_verifier: '6bd0aff2'
15 |
16 | test:
17 | <<: *default
18 |
19 | production:
20 | <<: *default
21 | site_url: 'http://robbinfan.com'
22 | blog_search_ping: true
23 |
--------------------------------------------------------------------------------
/models/blog_comment.rb:
--------------------------------------------------------------------------------
1 | class BlogComment < ActiveRecord::Base
2 | acts_as_cached
3 |
4 | validates_presence_of :content
5 |
6 | belongs_to :blog, :counter_cache => :comments_count
7 | belongs_to :account, :counter_cache => :comments_count
8 |
9 | after_save :clean_cache
10 | before_destroy :clean_cache
11 |
12 | def clean_cache
13 | APP_CACHE.delete("#{CACHE_PREFIX}/layout/right") # clean layout right column cache in _right.erb
14 | end
15 |
16 | def content_cache_key
17 | "#{CACHE_PREFIX}/comment_content/#{self.id}"
18 | end
19 |
20 | def brief_content
21 | Sanitize.clean(content).truncate(100)
22 | end
23 |
24 | def md_content
25 | APP_CACHE.fetch(content_cache_key) do
26 | Sanitize.clean(GitHub::Markdown.to_html(content, :gfm), Sanitize::Config::RELAXED)
27 | end
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/app/views/blog/_comment.erb:
--------------------------------------------------------------------------------
1 |
17 |
--------------------------------------------------------------------------------
/app/views/blog/_weibo_share.erb:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/elastic_index.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path("../config/boot.rb", __FILE__)
2 |
3 | # use ruby standard logger because padrino logger has odd error in my production environment.
4 | require 'logger'
5 | class ::Logger; alias_method :write, :<<; end
6 |
7 | if ENV['RACK_ENV'] == 'production'
8 | logger = ::Logger.new("log/production.log")
9 | logger.level = ::Logger::WARN
10 | use Rack::CommonLogger, logger
11 | end
12 |
13 | client = Elasticsearch::Client.new log: true
14 | # client.index index: 'robbin_site', type: 'test', id: 1, body: {title: '范凯的博客文章', content: '博客内容更新'}
15 |
16 | Blog.all.each do |blog|
17 | client.index index: 'robbin_site', type: 'article', id: blog.id, body: {title: blog.title, content: blog.content}
18 | end
19 | #
20 | # client.search index: 'robbin_site', body: { query: { match: { _all: '减肥' } } }
21 |
22 | # curl -XGET 'http://localhost:9200/robbin_site/article/_search?q=_all:减肥'
23 |
24 |
--------------------------------------------------------------------------------
/db/migrate/004_acts_as_taggable_on_migration.rb:
--------------------------------------------------------------------------------
1 | class ActsAsTaggableOnMigration < ActiveRecord::Migration
2 | def self.up
3 | create_table :tags do |t|
4 | t.string :name
5 | end
6 |
7 | create_table :taggings do |t|
8 | t.references :tag
9 |
10 | # You should make sure that the column created is
11 | # long enough to store the required class names.
12 | t.references :taggable, :polymorphic => true
13 | t.references :tagger, :polymorphic => true
14 |
15 | # Limit is created to prevent MySQL error on index
16 | # length for MyISAM table type: http://bit.ly/vgW2Ql
17 | t.string :context, :limit => 128
18 |
19 | t.datetime :created_at
20 | end
21 |
22 | add_index :taggings, :tag_id
23 | add_index :taggings, [:taggable_id, :taggable_type, :context]
24 | end
25 |
26 | def self.down
27 | drop_table :taggings
28 | drop_table :tags
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/app/views/admin/blogs.erb:
--------------------------------------------------------------------------------
1 | <%= partial 'admin/menu' %>
2 |
3 |
4 |
5 | 文章列表: 共<%= @blogs.total_entries %>篇
6 |
7 | |
8 | 文章标题 |
9 | 发表时间 |
10 | 浏览数 |
11 | 评论数 |
12 | Tag |
13 | 操作 |
14 |
15 | <% @blogs.each_with_index do |blog, index| %>
16 |
17 | | <%= index + 1 %> |
18 | <%= link_to blog.title, blog_url(blog), :target => '_blank' %> |
19 | <%= time_ago_in_words(blog.created_at) %> |
20 | <%= blog.view_count %> |
21 | <%= blog.comments_count %> |
22 | <%= blog.cached_tag_list %> |
23 | 操作 |
24 |
25 | <% end %>
26 |
27 | <%= (will_paginate @blogs, :previous_label => '前一页', :next_label => '后一页').to_s.html_safe %>
28 |
29 |
--------------------------------------------------------------------------------
/rainbows.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | # set ruby GC parameters
4 | RUBY_HEAP_MIN_SLOTS=600000
5 | RUBY_FREE_MIN=200000
6 | RUBY_GC_MALLOC_LIMIT=60000000
7 | export RUBY_HEAP_MIN_SLOTS RUBY_FREE_MIN RUBY_GC_MALLOC_LIMIT
8 |
9 | pid="log/rainbows.pid"
10 |
11 | case "$1" in
12 | start)
13 | bundle exec rainbows -c config/rainbows.rb -E production -D
14 | ;;
15 | stop)
16 | kill -QUIT `cat $pid`
17 | ;;
18 | restart)
19 | $0 stop
20 | sleep 1
21 | $0 start
22 | ;;
23 | reload)
24 | kill -USR2 `cat $pid`
25 | ;;
26 | force-stop)
27 | kill -INT `cat $pid`
28 | ;;
29 | shutdown-workers)
30 | kill -WINCH `cat $pid`
31 | ;;
32 | increment-worker)
33 | kill -TTIN `cat $pid`
34 | ;;
35 | decrement-worker)
36 | kill -TTOU `cat $pid`
37 | ;;
38 | logrotate)
39 | kill -USR1 `cat $pid`
40 | ;;
41 | *)
42 | echo $"Usage: $0 {start|stop|restart|reload|force-stop|shutdown-workers|increment-worker|decrement-worker|logrotate|}"
43 | ;;
44 | esac
--------------------------------------------------------------------------------
/app/views/blog/create_comment.js.erb:
--------------------------------------------------------------------------------
1 | $(document).scrollTop($('div#comments').offset().top);
2 | $('div#comments>ul').prepend('<%= js_escape_html(partial("blog/comment", :object => @comment)).html_safe %>');
3 | $('div#comments>ul>li#<%= @comment.id %>').hide().fadeIn('slow');
4 | $('textarea#blog_comment_content').attr("value", "");
5 |
6 | $('a[data-remote=true]').on('click', function(e) {
7 | var element = $(this);
8 | if (e.stopped) return;
9 | e.preventDefault(); e.stopped = true;
10 | JSAdapter.sendRequest(element, {
11 | verb: element.data('method') || 'get',
12 | url: element.attr('href')
13 | });
14 | });
15 |
16 | $('li#<%= @comment.id %> div.cot_con a.quote_comment').click(function(){
17 | var commentBlock = $(this).closest('li');
18 | $.get('<%= url(:blog, :quote_comment) %>', {id: commentBlock.attr('id')}, function(data){
19 | var body = $('textarea#blog_comment_content').val() + data;
20 | $('textarea#blog_comment_content').val(body);
21 | $('textarea#blog_comment_content').focus();
22 | });
23 | return false;
24 | });
--------------------------------------------------------------------------------
/app/views/blog/_right.erb:
--------------------------------------------------------------------------------
1 | <% cache("#{CACHE_PREFIX}/layout/right", :expires_in => 1.day) do %>
2 |
3 | 肉饼铺子微信号
4 |
5 | <%= image_tag("robbin_foods_shop.jpg", :alt => "肉饼铺子微信订阅号", :title => "肉饼铺子微信订阅号") %>
6 |
7 |
8 | 文章分类
9 |
10 | <% Blog.cached_tag_cloud.select {|t| t.count > 2}.each do |tag| %>
11 | <%= link_to "#{tag.name}#{tag.count}".html_safe, url(:blog, :tag, :name => tag.name) %>
12 | <% end %>
13 |
14 |
15 | 热门文章
16 |
17 | <% Blog.hot_blogs(5).each do |blog| %>
18 | <%= link_to blog.title, blog_url(blog) %>
19 | <% end %>
20 |
21 |
22 | 最新评论
23 |
31 |
32 | <% end %>
33 |
34 |
37 |
--------------------------------------------------------------------------------
/app/views/admin/new_attachment.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | <%= stylesheet_link_tag 'attachment' %>
6 | <%= javascript_include_tag 'jquery', 'jquery-ujs', 'application' %>
7 |
8 |
9 | <% form_for :attachment, url(:admin, :create_attachment), :id => "attachment_form", :multipart => true do |f| -%>
10 | <%= f.file_field :file, :onchange => "upload(this.value);" %>
11 | 上传中 <%= image_tag "spinner.gif" %>
12 |
13 | <% end %>
14 |
15 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/app/views/home/rss.xml.erb:
--------------------------------------------------------------------------------
1 | <% cache("#{CACHE_PREFIX}/rss/all") do -%>
2 |
3 |
4 |
5 | <%= APP_CONFIG['site_title'] %>
6 | <%= APP_CONFIG['site_description'] %>
7 | <%= APP_CONFIG['site_url'] %>
8 | zh-CN
9 | Copyright 2012-2013, robbinfan.com
10 | http://blogs.law.harvard.edu/tech/rss
11 |
12 | <% @blogs.each do |blog| %>
13 | -
14 | <%= blog.title %>
15 |
16 | ]]>
17 |
18 | <%= blog.created_at.rfc822 %>
19 | <%= APP_CONFIG['site_url'] + blog_url(blog) %>
20 | <%= APP_CONFIG['site_url'] + blog_url(blog) %>
21 |
22 | <% end %>
23 |
24 |
25 | <% end -%>
--------------------------------------------------------------------------------
/app/views/admin/_attachment.erb:
--------------------------------------------------------------------------------
1 |
2 | <%= link_to attachment.file.file.identifier, attachment.file.url, :target => '_blank' %>
3 | <%= link_to image_tag('btn_delete.png', :title => '删除附件'), url(:admin, :attachment, :id => attachment.id), :method => :delete, :remote => true, :confirm => '是否删除附件' %>
4 |
<%= image_tag 'btn_sticky.png', :title => '将图片插入编辑器' %>
5 |
6 |
21 |
--------------------------------------------------------------------------------
/app/views/admin/comments.erb:
--------------------------------------------------------------------------------
1 | <%= partial 'admin/menu' %>
2 |
3 |
4 |
5 | 评论列表: 共<%= @comments.total_entries %>条
6 |
7 | |
8 | 评论者 |
9 | 被评文章 |
10 | 评论时间 |
11 | 评论内容 |
12 | 操作 |
13 |
14 | <% @comments.each_with_index do |comment, index| %>
15 |
23 | <% end %>
24 |
25 | <%= (will_paginate @comments, :previous_label => '前一页', :next_label => '后一页').to_s.html_safe %>
26 |
27 |
--------------------------------------------------------------------------------
/db/seeds.rb:
--------------------------------------------------------------------------------
1 | # Seed add you the ability to populate your db.
2 | # We provide you a basic shell for interaction with the end user.
3 | # So try some code like below:
4 | #
5 | # name = shell.ask("What's your name?")
6 | # shell.say name
7 | #
8 | email = shell.ask "Which email do you want use for logging into admin?"
9 | name = shell.ask "What is your name?"
10 | password = shell.ask "Tell me the password to use:"
11 |
12 | shell.say ""
13 |
14 | account = Account.create(:email => email, :name => name, :password => password, :password_confirmation => password, :role => "admin")
15 |
16 | if account.valid?
17 | shell.say "================================================================="
18 | shell.say "Account has been successfully created, now you can login with:"
19 | shell.say "================================================================="
20 | shell.say " email: #{email}"
21 | shell.say " name: #{name}"
22 | shell.say " password: #{password}"
23 | shell.say "================================================================="
24 | else
25 | shell.say "Sorry but some thing went wrong!"
26 | shell.say ""
27 | account.errors.full_messages.each { |m| shell.say " - #{m}" }
28 | end
29 |
30 | shell.say ""
31 |
--------------------------------------------------------------------------------
/public/stylesheets/hightlight/github.min.css:
--------------------------------------------------------------------------------
1 | pre code{display:block;padding:.5em;color:#333;background:#f8f8ff}pre .comment,pre .template_comment,pre .diff .header,pre .javadoc{color:#998;font-style:italic}pre .keyword,pre .css .rule .keyword,pre .winutils,pre .javascript .title,pre .nginx .title,pre .subst,pre .request,pre .status{color:#333;font-weight:bold}pre .number,pre .hexcolor,pre .ruby .constant{color:#099}pre .string,pre .tag .value,pre .phpdoc,pre .tex .formula{color:#d14}pre .title,pre .id{color:#900;font-weight:bold}pre .javascript .title,pre .lisp .title,pre .clojure .title,pre .subst{font-weight:normal}pre .class .title,pre .haskell .type,pre .vhdl .literal,pre .tex .command{color:#458;font-weight:bold}pre .tag,pre .tag .title,pre .rules .property,pre .django .tag .keyword{color:#000080;font-weight:normal}pre .attribute,pre .variable,pre .lisp .body{color:#008080}pre .regexp{color:#009926}pre .class{color:#458;font-weight:bold}pre .symbol,pre .ruby .symbol .string,pre .lisp .keyword,pre .tex .special,pre .prompt{color:#990073}pre .built_in,pre .lisp .title,pre .clojure .built_in{color:#0086b3}pre .preprocessor,pre .pi,pre .doctype,pre .shebang,pre .cdata{color:#999;font-weight:bold}pre .deletion{background:#fdd}pre .addition{background:#dfd}pre .diff .change{background:#0086b3}pre .chunk{color:#aaa}
--------------------------------------------------------------------------------
/config/database.rb:
--------------------------------------------------------------------------------
1 | # Setup our logger
2 | ActiveRecord::Base.logger = logger
3 |
4 | # Raise exception on mass assignment protection for Active Record models.
5 | ActiveRecord::Base.mass_assignment_sanitizer = :strict
6 |
7 | # Log the query plan for queries taking more than this (works
8 | # with SQLite, MySQL, and PostgreSQL).
9 | ActiveRecord::Base.auto_explain_threshold_in_seconds = 0.5
10 |
11 | # Include Active Record class name as root for JSON serialized output.
12 | ActiveRecord::Base.include_root_in_json = false
13 |
14 | # Store the full class name (including module namespace) in STI type column.
15 | ActiveRecord::Base.store_full_sti_class = true
16 |
17 | # Use ISO 8601 format for JSON serialized times and dates.
18 | ActiveSupport.use_standard_json_time_format = true
19 |
20 | # Don't escape HTML entities in JSON, leave that for the #json_escape helper
21 | # if you're including raw JSON in an HTML page.
22 | ActiveSupport.escape_html_entities_in_json = false
23 |
24 | # Set timezone to local
25 | ActiveRecord::Base.default_timezone = :local
26 | ActiveRecord::Base.time_zone_aware_attributes = false
27 |
28 | # Now we can establish connection with our db.
29 | ActiveRecord::Base.establish_connection YAML::load(File.open(File.expand_path("#{PADRINO_ROOT}/config", __FILE__) + '/database.yml'))[PADRINO_ENV]
--------------------------------------------------------------------------------
/lib/format_helpers.rb:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 | module Padrino
3 | module Helpers
4 | module FormatHelpers
5 | # override default helper for readable time.
6 | def distance_of_time_in_words(from_time, to_time = 0, include_seconds = false)
7 | from_time = from_time.to_time if from_time.respond_to?(:to_time)
8 | to_time = to_time.to_time if to_time.respond_to?(:to_time)
9 | distance_in_minutes = (((to_time - from_time).abs)/60).round
10 | distance_in_seconds = ((to_time - from_time).abs).round
11 |
12 | case distance_in_minutes
13 | when 0..1
14 | return (distance_in_minutes==0) ? '刚刚' : '1 分钟前' unless include_seconds
15 | case distance_in_seconds
16 | when 0..5 then '5 秒钟前'
17 | when 6..10 then '10 秒钟前'
18 | when 11..20 then '20 秒钟前'
19 | when 21..40 then '半分钟前'
20 | when 41..59 then '1 分钟前'
21 | else '1 分钟'
22 | end
23 |
24 | when 2..44 then "#{distance_in_minutes} 分钟前"
25 | when 45..1439 then "#{(distance_in_minutes.to_f / 60).round} 小时前"
26 | when 1440..2879 then "昨天"
27 | when 2880..4319 then "前天"
28 | else from_time.strftime(include_seconds ? "%Y-%m-%d %H:%M" : "%Y-%m-%d")
29 | end
30 | end
31 | end
32 | end
33 | end
--------------------------------------------------------------------------------
/app/views/admin/edit_profile.erb:
--------------------------------------------------------------------------------
1 | <%= partial 'admin/menu' %>
2 |
3 |
4 |
5 | <% form_for @account, url(:admin, :profile, :id => @account.id), :method => :put, :multipart => true do |form| %>
6 |
7 | 修改用户信息
8 |
9 | | 用户名字 |
10 | <%= form.text_field :name, :class => 'title' %> |
11 |
12 |
13 |
14 | | Email地址 |
15 | <%= form.text_field :email, :class => 'title' %> |
16 |
17 |
18 |
19 | | 修改密码 |
20 | <%= form.password_field :password, :class => 'title' %> |
21 |
22 |
23 |
24 | | 确认密码 |
25 | <%= form.password_field :password_confirmation, :class => 'title' %> |
26 |
27 |
28 |
29 | | 头像 |
30 |
31 | <%= image_tag(@account.logo.url) %>
32 | <%= form.file_field :logo %>
33 | |
34 |
35 |
36 |
37 | | |
38 |
39 | <%= form.submit '', :class => 'submit' %>
40 | |
41 |
42 |
43 | <% end %>
44 |
45 |
46 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | # Project requirements
4 | gem 'rake'
5 | gem 'tilt', '~> 1.3.7'
6 | gem 'padrino-core', '~> 0.11'
7 | gem 'padrino-helpers', '~> 0.11'
8 |
9 | # Component requirements
10 | gem 'bcrypt-ruby', :require => 'bcrypt'
11 | gem 'erubis', '~> 2.7.0'
12 | gem 'activerecord', '~> 3.2', :require => 'active_record'
13 | gem 'mysql2'
14 | gem 'dalli', :require => 'active_support/cache/dalli_store'
15 | gem 'kgio'
16 | gem "second_level_cache", :git => "git://github.com/csdn-dev/second_level_cache.git"
17 | gem 'acts-as-taggable-on', :git => "git://github.com/robbin/acts-as-taggable-on.git"
18 | gem 'github-markdown', :require => 'github/markdown'
19 | gem 'will_paginate', :require => ['will_paginate/active_record', 'will_paginate/view_helpers/sinatra']
20 | gem 'sanitize'
21 | gem 'carrierwave', :require => ['carrierwave', 'carrierwave/orm/activerecord']
22 | gem 'mini_magick'
23 | gem 'rest-client'
24 | gem 'elasticsearch'
25 |
26 | # Production requirements
27 | group :production do
28 | gem 'zbatery'
29 | # gem 'rainbows'
30 | end
31 |
32 | # Development requirements
33 | group :development do
34 | gem 'pry-padrino'
35 | gem 'padrino-gen', '~> 0.11'
36 | gem 'thin'
37 | end
38 |
39 | # Test requirements
40 | group :test do
41 | gem 'minitest', "~>2.6.0", :require => "minitest/autorun"
42 | gem 'rack-test', :require => "rack/test"
43 | gem 'factory_girl'
44 | gem 'database_cleaner'
45 | end
--------------------------------------------------------------------------------
/app/views/blog/_blog.erb:
--------------------------------------------------------------------------------
1 | <%
2 | # read view_count from model cache if model has been cached.
3 | view_count = blog.view_count
4 | if b = Blog.read_second_level_cache(blog.id)
5 | view_count = b.view_count
6 | end
7 | %>
8 |
9 |
10 |
11 | <%= link_to blog.title, blog_url(blog) %>
12 | <% if account_admin? %>
13 | <%= link_to '', url(:admin, :blog, :id => blog.id), :method => :delete, :confirm => '你要删除这篇文章吗?', :class => 'del', :title => '删除' %>
14 | <%= link_to '', url(:admin, :edit_blog, :id => blog.id), :class => 'edit', :title => '编辑' %>
15 | <% end %>
16 |
17 |
18 |
19 | <% blog.cached_tags.each do |tag| %>
20 | <%= link_to "#{tag}".html_safe, url(:blog, :tag, :name => tag), :class => 'tag', :rel => 'tag' %>
21 | <% end %>
22 |
23 | <%= blog.content.truncate(300) %>
24 |
25 |
26 |
27 | <%= blog.account.name %>
28 | <%= time_ago_in_words(blog.created_at) %>发表
29 | <%= time_ago_in_words(blog.content_updated_at) %>更新
30 | <%= view_count %>次浏览
31 | <%= link_to blog.comments_count.to_s, blog_url(blog), :anchor => 'comments', :class => 'comment', :title => '评论' %>
32 |
33 |
34 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Robbin's website
2 |
3 |
4 |
5 | This is my personal website project.
6 |
7 | ## System requirements
8 |
9 | * ruby 1.9, recommend 1.9 p327 version
10 | * MySQL 5.x, you should set utf-8 default encoding utf-8 at `my.cnf`, like this:
11 |
12 | [client] # on 5.0 or 5.1
13 | default-character-set=utf8
14 | [mysqld]
15 | default-character-set=utf8
16 |
17 | [mysqld] # on 5.5
18 | collation-server = utf8_unicode_ci
19 | init-connect='SET NAMES utf8'
20 | character-set-server = utf8
21 |
22 | * memcached
23 | * nginx as web server, `config/nginx.conf` is my nginx configuration snippet.
24 |
25 | ## Install
26 | 1. run `bundle install`
27 | 2. copy `config/app_config.example.yml` to `config/app_config.yml` and copy `config/database.example.yml` to `config/database.yml`
28 | 3. modify database config for your need.
29 | 4. create database match your database.yml and start your database.
30 | 5. run `bundle exec rake secret` to generate session secret key and fill it in app_config.
31 | 6. run `bundle exec rake ar:migrate` to setup database schema.
32 | 7. run `bundle exec rake db:seed` to generate admin user.
33 | 8. start memcached with `memcached -d`.
34 | 9. run `bundle exec thin start` for development environment and run `./zbatery.sh start` for production environment.
35 |
36 | ## Run on Windows
37 |
38 | remove such lines in `Gemfile` and run with thin.
39 |
40 | gem 'kgio'
41 | gem 'zbatery'
42 |
43 | ## MIT License
--------------------------------------------------------------------------------
/public/stylesheets/hightlight/googlecode.min.css:
--------------------------------------------------------------------------------
1 | pre code{display:block;padding:.5em;background:white;color:black}pre .comment,pre .template_comment,pre .javadoc,pre .comment *{color:#800}pre .keyword,pre .ruby .function .keyword,pre .function .keyword,pre .sub .keyword,pre .method,pre .list .title,pre .tag .title,pre .setting .value,pre .winutils,pre .tex .command{color:#008}pre .envvar,pre .tex .special{color:#660}pre .string,pre .tag .value,pre .cdata,pre .filter .argument,pre .attr_selector,pre .apache .cbracket,pre .date,pre .regexp{color:#080}pre .sub .identifier,pre .pi,pre .tag,pre .tag .keyword,pre .decorator,pre .ini .title,pre .shebang,pre .input_number,pre .hexcolor,pre .rules .value,pre .css .value .number,pre .literal,pre .symbol,pre .ruby .symbol .string,pre .ruby .symbol .keyword,pre .ruby .symbol .keymethods,pre .number,pre .css .function{color:#066}pre .class .title,pre .haskell .label,pre .smalltalk .class,pre .javadoctag,pre .yardoctag,pre .phpdoc,pre .typename,pre .tag .attribute,pre .doctype,pre .class .id,pre .built_in,pre .setting,pre .params,pre .variable{color:#606}pre .css .tag,pre .rules .property,pre .pseudo,pre .subst{color:#000}pre .css .class,pre .css .id{color:#9b703f}pre .value .important{color:#f70;font-weight:bold}pre .rules .keyword{color:#c5af75}pre .annotation,pre .apache .sqbracket,pre .nginx .built_in{color:#9b859d}pre .preprocessor,pre .preprocessor *{color:#444}pre .tex .formula{background-color:#EEE;font-style:italic}pre .diff .header,pre .chunk{color:#808080;font-weight:bold}pre .diff .change{background-color:#bccff9}pre .addition{background-color:#baeeba}pre .deletion{background-color:#ffc8bd}pre .comment .yardoctag{font-weight:bold}
--------------------------------------------------------------------------------
/lib/weibo_auth.rb:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 | require 'timeout'
3 |
4 | OpenSSL::SSL::VERIFY_PEER = OpenSSL::SSL::VERIFY_NONE
5 |
6 | class WeiboAuth
7 |
8 | def authorize_url
9 | "https://api.weibo.com/oauth2/authorize?response_type=code&client_id=#{APP_CONFIG['weibo_api_key']}&redirect_uri=#{URI.escape APP_CONFIG['weibo_redirect_uri']}"
10 | end
11 |
12 | def callback(code)
13 | @uid = Timeout::timeout(20) do
14 | @access_token = JSON.parse(RestClient.post('https://api.weibo.com/oauth2/access_token',
15 | :client_id => APP_CONFIG['weibo_api_key'],
16 | :client_secret => APP_CONFIG['weibo_api_secret'],
17 | :grant_type => 'authorization_code',
18 | :code => code,
19 | :redirect_uri => APP_CONFIG['weibo_redirect_uri'])
20 | )['access_token']
21 | JSON.parse(RestClient.get("https://api.weibo.com/2/account/get_uid.json?access_token=#{@access_token}"))['uid']
22 | end
23 | raise Error, "验证失败" unless @uid
24 | rescue Timeout::Error
25 | raise Error, "访问超时,请稍后重试"
26 | end
27 |
28 | def get_user_info
29 | user_info = Timeout::timeout(20) do
30 | JSON.parse(RestClient.get("https://api.weibo.com/2/users/show.json?uid=#{@uid}&access_token=#{@access_token}"))
31 | end
32 | unless user_info["name"]
33 | STDERR.puts "Weibo获取用户信息错误:" + user_info.inspect
34 | raise Error, "获取用户信息时发生错误,请稍后重试"
35 | end
36 | user_info
37 | rescue Timeout::Error
38 | raise Error, "访问超时,请稍后重试"
39 | end
40 | end
--------------------------------------------------------------------------------
/app/views/admin/accounts.erb:
--------------------------------------------------------------------------------
1 | <%= partial 'admin/menu' %>
2 |
3 |
4 |
5 | 管理员列表: 共<%= @admin_accounts.size %>人
6 |
7 | |
8 | 名字 |
9 | 注册时间 |
10 | 文章数 |
11 | 评论数 |
12 | 操作 |
13 |
14 | <% @admin_accounts.each_with_index do |account, index| %>
15 |
16 | | <%= index + 1 %> |
17 | <%= account.name %> |
18 | <%= time_ago_in_words(account.created_at) %> |
19 | <%= account.blogs_count %> |
20 | <%= account.comments_count %> |
21 | <%= link_to '编辑', url(:admin, :edit_profile, :id => account.id) %> |
22 |
23 | <% end %>
24 |
25 |
26 | 评论者列表: 共<%= @commenters.total_entries %>人
27 |
28 | |
29 | 名字 |
30 | 注册时间 |
31 | 评论数 |
32 | 操作 |
33 |
34 | <% @commenters.each_with_index do |account, index| %>
35 |
36 | | <%= index+1 %> |
37 | <%= commenter_link account %> |
38 | <%= time_ago_in_words(account.created_at) %> |
39 | <%= account.comments_count %> |
40 | <%= link_to '删除', url(:admin, :account, :id => account.id), :method => :delete, :remote => true, :confirm => '要删除用户吗?' %> |
41 |
42 | <% end %>
43 |
44 | <%= (will_paginate @commenters, :previous_label => '前一页', :next_label => '后一页').to_s.html_safe %>
45 |
46 |
--------------------------------------------------------------------------------
/stat_codes.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | r1=`find app/ -name *.rb|grep -v admin|grep -v helper|grep -v locale|grep -v views|xargs cat|grep -v "^\s*#"|grep -v "^\s*$" |wc -l`
4 | printf "Controllers: \t\t%s\n" $r1
5 | r7=`find app/ -name *helpers.rb|grep -v admin|xargs cat|grep -v "^\s*#"|grep -v "^\s*$" |wc -l`
6 | printf "Helpers: \t\t%s\n" $r7
7 | r2=`find app/views -name *.erb|grep -v admin|xargs cat|grep -v "^\s*#"|grep -v "^\s*$" |wc -l`
8 | printf "Views: \t\t\t%s\n" $r2
9 | r3=`find models/ -name *.rb|grep -v admin|xargs cat|grep -v "^\s*#"|grep -v "^\s*$" |wc -l`
10 | printf "Models: \t\t%s\n" $r3
11 | r8=`find db/ -name *.rb|xargs cat|grep -v "^\s*#"|grep -v "^\s*$" |wc -l`
12 | printf "DB & Migration: \t%s\n" $r8
13 | r9=`find config -name *.rb|xargs cat|grep -v "^\s*#"|grep -v "^\s*$" |wc -l`
14 | printf "Configuration: \t\t%s\n" $r9
15 |
16 | r4=`find lib/ -name *.rb|grep -v admin|xargs cat|grep -v "^\s*#"|grep -v "^\s*$" |wc -l`
17 | printf "Libraries: \t\t%s\n" $r4
18 | r5=`find test/models -name *.rb|grep -v admin|xargs cat|grep -v "^\s*#"|grep -v "^\s*$" |wc -l`
19 | printf "Unit test: \t\t%s\n" $r5
20 | r6=`find test/app -name *.rb|grep -v admin|xargs cat|grep -v "^\s*#"|grep -v "^\s*$" |wc -l`
21 | printf "Function test: \t\t%s\n" $r6
22 | total=`expr $r1 + $r2 + $r3 + $r4 + $r5 + $r6 + $r7 + $r8 + $r9`
23 |
24 | printf "Ruby code Lines: \t%s\n" $total
25 | printf "All .rb files Lines: \t%s\n" `find . -name *.rb|grep -v admin|xargs cat|grep -v "^\s*#"|grep -v "^\s*$" |wc -l`
26 | echo "-----------------------------------"
27 | s1=`find public -name *.css|grep -v admin|xargs cat|grep -v "^\s*#"|grep -v "^\s*$" |wc -l`
28 | s2=`find public -name *.js|grep -v admin|xargs cat|grep -v "^\s*#"|grep -v "^\s*$" |wc -l`
29 | printf "StyleSheets: \t\t%s\n" $s1
30 | printf "Javascripts: \t\t%s\n" $s2
31 | echo "-----------------------------------"
32 | printf "Total: \t\t\t%s\n" `expr $r1 + $r2 + $r3 + $r4 + $r5 + $r6 + $r7 + $r8 + $r9 + $s2`
--------------------------------------------------------------------------------
/lib/fragment.rb:
--------------------------------------------------------------------------------
1 | module Padrino
2 | module Cache
3 | module Helpers
4 | module Fragment
5 | include Padrino::Helpers::OutputHelpers
6 |
7 | ##
8 | # This helper is used anywhere in your application you would like to associate a fragment
9 | # to be cached. It can be used in within a route:
10 | #
11 | # @param [String] key
12 | # cache key
13 | # @param [Hash] opts
14 | # cache options, e.g :expires_in
15 | # @param [Proc]
16 | # Execution result to store in the cache
17 | #
18 | # @example
19 | # # Caching a fragment
20 | # class MyTweets < Padrino::Application
21 | # enable :caching # turns on caching mechanism
22 | #
23 | # controller '/tweets' do
24 | # get :feed, :map => '/:username' do
25 | # username = params[:username]
26 | #
27 | # @feed = cache( "feed_for_#{username}", :expires_in => 3 ) do
28 | # @tweets = Tweet.all( :username => username )
29 | # render 'partials/feedcontent'
30 | # end
31 | #
32 | # # Below outputs @feed somewhere in its markup
33 | # render 'feeds/show'
34 | # end
35 | # end
36 | # end
37 | #
38 | # @api public
39 | def cache(key, opts = nil, &block)
40 | began_at = Time.now
41 | if value = APP_CACHE.read(key.to_s)
42 | logger.debug "GET Fragment", began_at, key.to_s if defined?(logger)
43 | concat_content(value)
44 | else
45 | value = capture_html(&block)
46 | APP_CACHE.write(key.to_s, value, opts)
47 | logger.debug "SET Fragment", began_at, key.to_s if defined?(logger)
48 | concat_content(value)
49 | end
50 | end
51 | end # Fragment
52 | end # Helpers
53 | end # Cache
54 | end # Padrino
55 |
--------------------------------------------------------------------------------
/public/stylesheets/default/document.css:
--------------------------------------------------------------------------------
1 | @charset "utf-8";
2 | /* CSS Document */
3 |
4 | /*
5 | YUI 3.8.0 (build 5744)
6 | Copyright 2012 Yahoo! Inc. All rights reserved.
7 | Licensed under the BSD License.
8 | http://yuilibrary.com/license/
9 | */
10 | html{color:#444;background:#FFF}
11 | body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,code,form,fieldset,legend,input,textarea,p,blockquote,th,td{margin:0;padding:0}
12 | table{border-collapse:collapse;border-spacing:0}
13 | fieldset,img{border:0}
14 | address,caption,cite,code,dfn,em,strong,th,var{font-style:normal;font-weight:normal}
15 | ol,ul{list-style:none}
16 | caption,th{text-align:left}
17 | h1,h2,h3,h4,h5,h6{font-size:100%;font-weight:normal}
18 | q:before,q:after{content:''}
19 | abbr,acronym{border:0;font-variant:normal}
20 | sup{vertical-align:text-top}
21 | sub{vertical-align:text-bottom}
22 | input,textarea,select{font-family:inherit;font-size:inherit;font-weight:inherit}
23 | input,textarea,select{font-size:100%}
24 | legend{color:#000}
25 | #yui3-css-stamp.cssreset{display:none}
26 | /* Copyright (c) 2012, Yahoo! Inc. All rights reserved. */
27 | html{ font-size:100%;}
28 | body, select, button { font-size:12px; font-family: Helvetica, Tahoma, Arial, sans-serif; line-height:24px;}
29 | input, textarea { font-size:12px; font-family: Monaco, Helvetica, Tahoma, Arial, sans-serif; line-height:24px;}
30 | button { cursor: pointer; }
31 | input{outline: none;}
32 | i, em, cite { font-style: normal; }
33 | a, a:link { color: #666; text-decoration: none; }
34 | a:visited {}
35 | a:active, a:hover { color:#ff6600; text-decoration:underline;}
36 | a:focus { outline: none; }
37 | .clearfix {_zoom: 1;}
38 | .clearfix:after {content: ".";display: block;visibility: hidden;clear: both;height: 0px;}
39 | .more { float: right; }
40 | .more a { font-weight: normal; font-size: 12px; }
41 | .fl, .fr { display: inline; float: left; }
42 | .fr { float: right; }
43 |
44 | input{ border:1px #ccc solid; height:28px; border-radius: 5px;-webkit-border-radius: 5px;-moz-border-radius: 5px;-khtml-border-radius: 5px;}
45 |
46 | .relative{ position:relative;}
47 | .relative .box{ position:relative;}
--------------------------------------------------------------------------------
/config/boot.rb:
--------------------------------------------------------------------------------
1 | # Defines our constants
2 | PADRINO_ENV = ENV['PADRINO_ENV'] ||= ENV['RACK_ENV'] ||= 'development' unless defined?(PADRINO_ENV)
3 | PADRINO_ROOT = File.expand_path('../..', __FILE__) unless defined?(PADRINO_ROOT)
4 |
5 | # Load our dependencies
6 | require 'rubygems' unless defined?(Gem)
7 | require 'bundler/setup'
8 | Bundler.require(:default, PADRINO_ENV)
9 |
10 | ##
11 | # ## Enable devel logging
12 | #
13 | # Padrino::Logger::Config[:development][:log_level] = :devel
14 | # Padrino::Logger::Config[:development][:log_static] = true
15 | #
16 | # ## Configure your I18n
17 | #
18 | I18n.default_locale = 'zh_cn'
19 |
20 | Dir.glob(File.expand_path("#{PADRINO_ROOT}/locale", __FILE__) + '/**/*.yml').each do |file|
21 | I18n.load_path << file
22 | end
23 |
24 | # ## Configure your HTML5 data helpers
25 | #
26 | # Padrino::Helpers::TagHelpers::DATA_ATTRIBUTES.push(:dialog)
27 | # text_field :foo, :dialog => true
28 | # Generates:
29 | #
30 | # ## Add helpers to mailer
31 | #
32 | # Mail::Message.class_eval do
33 | # include Padrino::Helpers::NumberHelpers
34 | # include Padrino::Helpers::TranslationHelpers
35 | # end
36 |
37 | ##
38 | # Add your before (RE)load hooks here
39 | #
40 | Padrino.before_load do
41 | end
42 |
43 | ##
44 | # Add your after (RE)load hooks here
45 | #
46 | Padrino.after_load do
47 | end
48 |
49 | # load project config
50 | APP_CONFIG = YAML.load_file(File.expand_path("#{PADRINO_ROOT}/config", __FILE__) + '/app_config.yml')[PADRINO_ENV]
51 |
52 | # initialize memcache and ActiveRecord Cache
53 | Dalli.logger = logger
54 | APP_CACHE = ActiveSupport::Cache::DalliStore.new("127.0.0.1")
55 | CACHE_PREFIX = "robbin"
56 | SecondLevelCache.configure do |config|
57 | config.cache_store = APP_CACHE
58 | config.logger = logger
59 | config.cache_key_prefix = CACHE_PREFIX
60 | end
61 |
62 | # Set acts_as_taggable
63 | ActsAsTaggableOn.remove_unused_tags = true
64 | ActsAsTaggableOn.strict_case_match = true
65 |
66 | # Set will_paginate default page number
67 | WillPaginate.per_page = 15
68 |
69 | # Set carrierwave sanitize
70 | CarrierWave::SanitizedFile.sanitize_regexp = /[^[:word:]\.\-\+]/
71 |
72 | Padrino.load!
--------------------------------------------------------------------------------
/public/javascripts/application.js:
--------------------------------------------------------------------------------
1 | // Put your application scripts here
2 | $(function(){
3 | // response search query on main nav both mouse click and keyboard return key
4 | $('button#search-button').click(function() {
5 | var keyword = $.trim($('input#search-box').val());
6 | if (keyword != null && keyword != '') {
7 | $('form#cse-search-box').submit();
8 | }
9 | });
10 |
11 | $('input#search-box').keyup(function(event) {
12 | if (event.keyCode == 13) {
13 | var keyword = $.trim($('input#search-box').val());
14 | if (keyword != null && keyword != '') {
15 | $('form#cse-search-box').submit();
16 | }
17 | }
18 | });
19 | });
20 |
21 |
22 | // add function insertAtCaret which can insert something at cursor in textarea input box.
23 | (function($){
24 | $.fn.extend({
25 | insertAtCaret: function(myValue) {
26 | var $t=$(this)[0];
27 | if (document.selection) {
28 | this.focus();
29 | sel = document.selection.createRange();
30 | sel.text = myValue;
31 | this.focus();
32 | } else if ($t.selectionStart || $t.selectionStart == '0') {
33 | var startPos = $t.selectionStart;
34 | var endPos = $t.selectionEnd;
35 | var scrollTop = $t.scrollTop;
36 | $t.value = $t.value.substring(0, startPos) + myValue + $t.value.substring(endPos, $t.value.length);
37 | this.focus();
38 | $t.selectionStart = startPos + myValue.length;
39 | $t.selectionEnd = startPos + myValue.length;
40 | $t.scrollTop = scrollTop;
41 | } else {
42 | this.value += myValue;
43 | this.focus();
44 | }
45 | }
46 | })
47 | })(jQuery);
48 |
49 | // filter illegal tag, only number, underscore, alphabet and chinese words, add #, + like C#, C++
50 | filterTags = function(tags) {
51 | var newArray = new Array();
52 | if (tags != null && tags.length >0) {
53 | var re_tag = new RegExp("^(?!_)(?!.*?_$)[\+#a-zA-Z0-9_ \u4e00-\u9fa5]+$");
54 | var tag_list = tags.split(/\s*,\s*/);
55 | for(var i = 0; i< tag_list.length; i++) {
56 | if (re_tag.test(tag_list[i])){
57 | newArray.push(tag_list[i]);
58 | }
59 | }
60 | }
61 | return newArray;
62 | };
--------------------------------------------------------------------------------
/app/views/admin/_form_js.erb:
--------------------------------------------------------------------------------
1 | <% content_for :javascripts do %>
2 | <%= javascript_include_tag 'highlight.min' %>
3 |
62 | <% end %>
--------------------------------------------------------------------------------
/config/rainbows.rb:
--------------------------------------------------------------------------------
1 | # rainbows config
2 | Rainbows! do
3 | use :ThreadPool
4 | worker_connections 16
5 | end
6 |
7 | # paths and things
8 | wd = File.expand_path('../../', __FILE__)
9 | tmp_path = File.join(wd, 'log')
10 | Dir.mkdir(tmp_path) unless File.exist?(tmp_path)
11 | socket_path = File.join(tmp_path, 'rainbows.sock')
12 | pid_path = File.join(tmp_path, 'rainbows.pid')
13 | err_path = File.join(tmp_path, 'rainbows.error.log')
14 | out_path = File.join(tmp_path, 'rainbows.out.log')
15 |
16 | # Use at least one worker per core if you're on a dedicated server,
17 | # more will usually help for _short_ waits on databases/caches.
18 | worker_processes 2
19 |
20 | # If running the master process as root and the workers as an unprivileged
21 | # user, do this to switch euid/egid in the workers (also chowns logs):
22 | # user "unprivileged_user", "unprivileged_group"
23 |
24 | # tell it where to be
25 | working_directory wd
26 |
27 | # listen on both a Unix domain socket and a TCP port,
28 | # we use a shorter backlog for quicker failover when busy
29 | listen 8080, :tcp_nopush => true
30 |
31 | # nuke workers after 30 seconds instead of 60 seconds (the default)
32 | timeout 30
33 |
34 | # feel free to point this anywhere accessible on the filesystem
35 | pid pid_path
36 |
37 | # By default, the Unicorn logger will write to stderr.
38 | # Additionally, ome applications/frameworks log to stderr or stdout,
39 | # so prevent them from going to /dev/null when daemonized here:
40 | stderr_path err_path
41 | stdout_path out_path
42 |
43 | preload_app true
44 |
45 | before_fork do |server, worker|
46 | # # This allows a new master process to incrementally
47 | # # phase out the old master process with SIGTTOU to avoid a
48 | # # thundering herd (especially in the "preload_app false" case)
49 | # # when doing a transparent upgrade. The last worker spawned
50 | # # will then kill off the old master process with a SIGQUIT.
51 | old_pid = "#{server.config[:pid]}.oldbin"
52 |
53 | if old_pid != server.pid
54 | begin
55 | sig = (worker.nr + 1) >= server.worker_processes ? :QUIT : :TTOU
56 | Process.kill(sig, File.read(old_pid).to_i)
57 | rescue Errno::ENOENT, Errno::ESRCH
58 | end
59 | end
60 | #
61 | # Throttle the master from forking too quickly by sleeping. Due
62 | # to the implementation of standard Unix signal handlers, this
63 | # helps (but does not completely) prevent identical, repeated signals
64 | # from being lost when the receiving process is busy.
65 | # sleep 1
66 | end
67 |
68 | after_fork do |server, worker|
69 | end
--------------------------------------------------------------------------------
/app/controllers/blog.rb:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | RobbinSite.controllers :blog do
4 |
5 | get :index do
6 | @blogs = Blog.order('id DESC').page(params[:page])
7 | render 'blog/index'
8 | end
9 |
10 | get :tag_cloud, :map => '/tag' do
11 | render 'blog/tag_cloud'
12 | end
13 |
14 | get :tag, :map => '/tag/:name' do
15 | @blogs = Blog.tagged_with(params[:name]).order('content_updated_at DESC').page(params[:page])
16 | if @blogs.blank?
17 | halt 404
18 | else
19 | render 'blog/tag'
20 | end
21 | end
22 |
23 | get :show_url, :map => '/blog/:id/:url', :provides => [:html, :md] do
24 | @blog = Blog.find params[:id].to_i
25 | case content_type
26 | when :md then
27 | @blog.content
28 | when :html then
29 | @blog.increment_view_count
30 | render 'blog/show'
31 | end
32 | end
33 |
34 | get :show, :map => '/blog/:id', :provides => [:html, :md] do
35 | @blog = Blog.find params[:id].to_i
36 | redirect blog_url(@blog, mime_type = content_type), 301 unless @blog.slug_url.blank?
37 | case content_type
38 | when :md then
39 | @blog.content
40 | when :html then
41 | @blog.increment_view_count
42 | render 'blog/show'
43 | end
44 | end
45 |
46 | get :quote_comment, :map => '/comment/quote' do
47 | return false unless account_login?
48 | return false unless params[:id]
49 | comment = BlogComment.find params[:id].to_i
50 | body = "\n> #{comment.account.name} 评论:\n"
51 | comment.content.gsub(/\n{3,}/, "\n\n").split("\n").each {|line| body << "> #{line}\n"}
52 | body
53 | end
54 |
55 | post :comment_preview, :map => '/comment/preview' do
56 | return false unless account_login?
57 | Sanitize.clean(GitHub::Markdown.to_html(params[:term], :gfm), Sanitize::Config::RELAXED) if params[:term]
58 | end
59 |
60 | post :create_comment, :map => '/blog/:id/comments' do
61 | content_type :js
62 | halt 401 unless account_login?
63 | blog = Blog.find params[:id]
64 | halt 403 unless blog.commentable?
65 | @comment = blog.comments.create(:account => current_account, :content => params[:blog_comment][:content])
66 | render 'blog/create_comment'
67 | end
68 |
69 | delete :comment, :map => '/comment/:id' do
70 | content_type :js
71 | comment = BlogComment.find params[:id]
72 | if account_admin? || (account_commenter? && comment.account == current_account)
73 | comment.destroy
74 | "$('div#comments>ul>li##{comment.id}').fadeOut('slow', function(){$(this).remove();});"
75 | else
76 | halt 403
77 | end
78 | end
79 |
80 | end
--------------------------------------------------------------------------------
/app/views/admin/_form.erb:
--------------------------------------------------------------------------------
1 | <% content_for :stylesheets do %>
2 | <%= stylesheet_link_tag 'default/github' %>
3 | <%= stylesheet_link_tag 'hightlight/github.min' %>
4 | <% end %>
5 |
6 |
7 |
8 | | 文章标题 |
9 | <%= form.text_field :title, :class => 'title' %> |
10 |
11 |
12 |
13 | | 文章内容 |
14 |
15 |
19 | <%= form.text_area :content %>
20 |
21 |
25 | |
26 |
27 |
28 |
29 | | Tag |
30 |
31 | <%= form.text_field :user_tags, :placeholder => '用逗号分开,不超过3个,Tag必须是字母数字空格下划线和中文' %>
32 |
33 | <% Blog.cached_tag_cloud.each do |tag| %>
34 | <%= tag %>
35 | <% end %>
36 |
37 | |
38 |
39 |
40 |
41 | | 附件 |
42 |
43 | <% unless @attachments.blank? %>
44 | 已上传的文件:
45 | <%= partial 'admin/attachment', :collection => @attachments %>
46 | <% end %>
47 | 上传新的文件:
48 |
49 |
50 |
51 |
52 | 提示信息:
53 | - 上传文件请压缩后再上传,允许zip, rar, gz, tar, bz2, 7z, jar, war格式的压缩文件
54 | - 上传图片推荐使用png, jpg, gif等类型
55 | - 文件大小不能超过5MB
56 |
57 | |
58 |
59 |
60 |
61 | | URL后缀 |
62 |
63 | <%= form.text_field :slug_url, :placeholder => '填写URL英文关键词,用-连接' %>
64 | |
65 |
66 |
67 |
68 | | 允许评论 |
69 |
70 | <%= form.check_box :commentable, :style => 'display:inline;' %>
71 |
72 | |
73 |
74 |
75 |
76 | | |
77 |
78 | <%= form.submit '', :class => 'submit' %>
79 | |
80 |
81 |
--------------------------------------------------------------------------------
/config/nginx.conf:
--------------------------------------------------------------------------------
1 | http {
2 | include mime.types;
3 | default_type application/octet-stream;
4 | access_log off;
5 | server_tokens off;
6 | charset utf-8;
7 | client_max_body_size 5M;
8 | keepalive_timeout 60 20;
9 | send_timeout 10;
10 | sendfile on;
11 | tcp_nopush on;
12 | tcp_nodelay off;
13 |
14 | gzip on;
15 | gzip_min_length 1k;
16 | gzip_disable "MSIE [1-6]\.";
17 | gzip_http_version 1.1;
18 | gzip_types text/plain text/css application/x-javascript application/xml application/json application/atom+xml application/rss+xml;
19 | gzip_vary on;
20 |
21 | server {
22 | listen 80;
23 | server_name localhost;
24 | location / {
25 | root html;
26 | index index.html index.htm;
27 | }
28 | error_page 500 502 503 504 /50x.html;
29 | location = /50x.html {
30 | root html;
31 | }
32 | }
33 |
34 | server {
35 | listen 80;
36 | server_name www.robbinfan.com;
37 | root /webroot/robbin_site/public;
38 | rewrite ^/(.*)$ http://robbinfan.com/$1 permanent;
39 | }
40 |
41 | server {
42 | listen 80;
43 | server_name robbinfan.com;
44 | root /webroot/robbin_site/public;
45 | rewrite /leanstartup/lean_startup_note.html /blog/27/lean-startup permanent;
46 | add_header X-UA-Compatible IE=Edge,chrome=1;
47 |
48 | location @rainbows {
49 | proxy_set_header X-Real-IP $remote_addr;
50 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
51 | proxy_set_header Host $http_host;
52 | proxy_pass http://127.0.0.1:8080;
53 | }
54 |
55 | location / {
56 | try_files $uri @rainbows;
57 | }
58 |
59 | location ~ ^/default/(.*).(png|gif)$ {
60 | access_log off;
61 | error_log /dev/null crit;
62 | expires 3d;
63 | add_header Cache-Control public;
64 | add_header ETag "";
65 | break;
66 | }
67 |
68 | location ~ ^/(images|javascripts|stylesheets|uploads)/ {
69 | access_log off;
70 | error_log /dev/null crit;
71 | expires max;
72 | add_header Cache-Control public;
73 | add_header ETag "";
74 | break;
75 | }
76 |
77 | error_page 404 406 /404.html;
78 | error_page 500 502 503 504 /500.html;
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/models/account.rb:
--------------------------------------------------------------------------------
1 | require 'openssl'
2 | require 'base64'
3 |
4 | class Account < ActiveRecord::Base
5 | attr_accessor :password, :password_confirmation
6 | acts_as_cached
7 | mount_uploader :logo, AvatarUploader
8 | has_many :blogs
9 | has_many :blog_comments, :dependent => :destroy
10 | has_many :attachments
11 |
12 | # Validations
13 | validates_presence_of :email, :if => :native_login_required
14 | validates_presence_of :password, :if => :native_login_required
15 | validates_presence_of :password_confirmation, :if => :native_login_required
16 | validates_length_of :password, :within => 4..40, :if => :native_login_required
17 | validates_confirmation_of :password, :if => :native_login_required
18 | validates_length_of :email, :within => 3..100, :if => :native_login_required
19 | validates_uniqueness_of :email, :case_sensitive => false, :if => :native_login_required
20 | validates_format_of :email, :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i, :if => :native_login_required
21 | validates_format_of :role, :with => /[A-Za-z]/
22 |
23 | # Callbacks
24 | before_save :encrypt_password, :if => :password_required
25 |
26 | ##
27 | # This method is for authentication purpose
28 | #
29 | def self.authenticate(email, password)
30 | account = first(:conditions => { :email => email }) if email.present?
31 | account && account.has_password?(password) ? account : nil
32 | end
33 |
34 | def has_password?(password)
35 | ::BCrypt::Password.new(crypted_password) == password
36 | end
37 |
38 | def admin?
39 | self.role == 'admin'
40 | end
41 |
42 | def commenter?
43 | self.role == 'commenter'
44 | end
45 |
46 | def encrypt_cookie_value
47 | cipher = OpenSSL::Cipher::AES.new(256, :CBC)
48 | cipher.encrypt
49 | cipher.key = APP_CONFIG['session_secret']
50 | Base64.encode64(cipher.update("#{id} #{crypted_password}") + cipher.final)
51 | end
52 |
53 | def self.decrypt_cookie_value(encrypted_value)
54 | decipher = OpenSSL::Cipher::AES.new(256, :CBC)
55 | decipher.decrypt
56 | decipher.key = APP_CONFIG['session_secret']
57 | plain = decipher.update(Base64.decode64(encrypted_value)) + decipher.final
58 | id, crypted_password = plain.split
59 | return id.to_i, crypted_password
60 | rescue
61 | return 0, ""
62 | end
63 |
64 | def self.validate_cookie(encrypted_value)
65 | user_id, crypted_password = decrypt_cookie_value(encrypted_value)
66 | if (account = Account.find_by_id(user_id)) && (account.crypted_password = crypted_password)
67 | return account
68 | end
69 | end
70 |
71 | private
72 | def encrypt_password
73 | self.crypted_password = ::BCrypt::Password.create(password) unless password.blank?
74 | end
75 |
76 | def password_required
77 | crypted_password.blank? || password.present?
78 | end
79 |
80 | def native_login_required
81 | provider.blank? && password_required
82 | end
83 | end
84 |
--------------------------------------------------------------------------------
/app/helpers.rb:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 | require 'xmlrpc/client'
3 |
4 | RobbinSite.helpers do
5 | include Padrino::Cache::Helpers::Fragment
6 |
7 | # authentication helpers
8 | def current_account
9 | return @current_account if @current_account
10 | return @current_account = Account.find_by_id(session[:account_id]) if session[:account_id]
11 | if request.cookies['user'] && (@current_account = Account.validate_cookie(request.cookies['user']))
12 | session[:account_id] = @current_account.id
13 | return @current_account
14 | end
15 | end
16 |
17 | def account_login?
18 | current_account ? true : false
19 | end
20 |
21 | def account_admin?
22 | current_account && current_account.admin? ? true : false
23 | end
24 |
25 | def account_commenter?
26 | current_account && current_account.commenter? ? true : false
27 | end
28 |
29 | # blog article url generator for SEO purpose
30 | def blog_url(blog, mime_type = :html)
31 | if blog.slug_url.blank?
32 | slug_url = url(:blog, :show, :id => blog.id)
33 | else
34 | slug_url = url(:blog, :show_url, :id => blog.id, :url => blog.slug_url)
35 | end
36 | slug_url << "." << mime_type.to_s if mime_type != :html
37 | slug_url
38 | end
39 |
40 | # generate commenter logo and link
41 | def commenter_logo(commenter)
42 | if commenter.provider && commenter.provider == 'weibo'
43 | link_to image_tag(commenter.profile_image_url, :alt => commenter.name), "http://weibo.com/#{commenter.profile_url}", :target => '_blank', :rel => 'nofollow'
44 | else
45 | link_to image_tag(commenter.logo.url, :alt => commenter.name), APP_CONFIG['site_url']
46 | end
47 | end
48 |
49 | def commenter_link(commenter)
50 | if commenter.provider && commenter.provider == 'weibo'
51 | link_to commenter.name, "http://weibo.com/#{commenter.profile_url}", :target => '_blank', :rel => 'nofollow'
52 | else
53 | link_to commenter.name, APP_CONFIG['site_url']
54 | end
55 | end
56 |
57 | # blog search ping for SEO purpose
58 | def ping_search_engine(blog)
59 | # http://www.google.cn/intl/zh-CN/help/blogsearch/pinging_API.html
60 | # http://www.baidu.com/search/blogsearch_help.html
61 | baidu = XMLRPC::Client.new2("http://ping.baidu.com/ping/RPC2")
62 | baidu.timeout = 5 # set timeout 5 seconds
63 | baidu.call("weblogUpdates.extendedPing",
64 | APP_CONFIG['site_title'],
65 | APP_CONFIG['site_url'],
66 | APP_CONFIG['site_url'] + '/' + blog_url(blog),
67 | APP_CONFIG['site_url'] + '/rss')
68 |
69 | google = XMLRPC::Client.new2("http://blogsearch.google.com/ping/RPC2")
70 | google.timeout = 5 # set timeout 5 seconds
71 | google.call("weblogUpdates.extendedPing",
72 | APP_CONFIG['site_title'],
73 | APP_CONFIG['site_url'],
74 | APP_CONFIG['site_url'] + '/' + blog_url(blog),
75 | APP_CONFIG['site_url'] + '/rss',
76 | blog.cached_tag_list.gsub(/,/, '|'))
77 | rescue Exception => e
78 | logger.error e
79 | end
80 | end
--------------------------------------------------------------------------------
/models/blog.rb:
--------------------------------------------------------------------------------
1 | class Blog < ActiveRecord::Base
2 | acts_as_cached(:version => 1, :expires_in => 1.week)
3 | acts_as_taggable
4 |
5 | attr_protected :account_id, :blog_content_id
6 |
7 | after_save :clean_cache
8 | before_destroy :clean_cache
9 |
10 | belongs_to :blog_content, :dependent => :destroy
11 | belongs_to :account, :counter_cache => true
12 | has_many :comments, :class_name => 'BlogComment', :dependent => :destroy
13 | has_many :attachments, :dependent => :destroy
14 |
15 | validates :title, :presence => true
16 | validates :title, :length => {:in => 3..50}
17 |
18 | delegate :content, :to => :blog_content, :allow_nil => true
19 |
20 | # virtual property for setting tag_list
21 | def user_tags
22 | self.tag_list.join(" , ")
23 | end
24 |
25 | def user_tags=(tags)
26 | unless tags.blank?
27 | # filter illegal characters
28 | self.tag_list = tags.split(/\s*,\s*/).uniq.collect {|t| t.downcase}.select {|t| t =~ /^(?!_)(?!.*?_$)[\+#a-zA-Z0-9_\s\u4e00-\u9fa5]+$/}.join(",")
29 | end
30 | end
31 |
32 | # virtual property for blog_content's content body
33 | def content=(value) # must prepend self otherwise do not update blog_content
34 | self.blog_content ||= BlogContent.new
35 | self.blog_content.content = value
36 | self.content_updated_at = Time.now
37 | end
38 |
39 | def update_blog(param_hash)
40 | self.transaction do
41 | update_attributes!(param_hash)
42 | blog_content.save!
43 | save!
44 | end
45 | rescue
46 | return false
47 | end
48 |
49 | def attach!(owner)
50 | self.transaction do
51 | owner.attachments.orphan.each {|attachment| attachment.update_attribute(:blog_id, self.id) }
52 | end
53 | end
54 |
55 | # blog viewer hit counter
56 | def increment_view_count
57 | increment(:view_count) # add view_count += 1
58 | write_second_level_cache # update cache per hit, but do not touch db
59 | # update db per 10 hits
60 | self.class.update_all({:view_count => view_count}, :id => id) if view_count % 10 == 0
61 | end
62 |
63 | def cached_tags
64 | cached_tag_list ? cached_tag_list.split(/\s*,\s*/) : []
65 | end
66 |
67 | def clean_cache
68 | APP_CACHE.delete("#{CACHE_PREFIX}/blog_tags/tag_cloud") # clean tag_cloud
69 | APP_CACHE.delete("#{CACHE_PREFIX}/rss/all") # clean rss cache
70 | APP_CACHE.delete("#{CACHE_PREFIX}/layout/right") # clean layout right column cache in _right.erb
71 | end
72 |
73 | def content_cache_key
74 | "#{CACHE_PREFIX}/blog_content/#{self.id}/#{content_updated_at.to_i}"
75 | end
76 |
77 | def md_content # cached markdown format blog content
78 | APP_CACHE.fetch(content_cache_key) { GitHub::Markdown.to_html(content, :gfm) }
79 | end
80 |
81 | def self.cached_tag_cloud
82 | APP_CACHE.fetch("#{CACHE_PREFIX}/blog_tags/tag_cloud") do
83 | self.tag_counts.sort_by(&:count).reverse
84 | end
85 | end
86 |
87 | def self.hot_blogs(count)
88 | self.order('comments_count DESC, view_count DESC').limit(count)
89 | end
90 | end
91 |
--------------------------------------------------------------------------------
/public/javascripts/jquery-ujs.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Padrino Javascript Jquery Adapter
3 | * Created for use with Padrino Ruby Web Framework (http://www.padrinorb.com)
4 | **/
5 |
6 | /* Remote Form Support
7 | * form_for @user, '/user', :remote => true
8 | **/
9 |
10 | $('form[data-remote=true]').on('submit', function(e) {
11 | e.preventDefault(); e.stopped = true;
12 | var element = $(this), message = element.data('confirm');
13 | if (message && !confirm(message)) { return false; }
14 | JSAdapter.sendRequest(element, {
15 | verb: element.data('method') || element.attr('method') || 'post',
16 | url: element.attr('action'),
17 | dataType: element.data('type') || ($.ajaxSettings && $.ajaxSettings.dataType) || 'script',
18 | params: element.serializeArray()
19 | });
20 | });
21 |
22 | /* Confirmation Support
23 | * link_to 'sign out', '/logout', :confirm => 'Log out?'
24 | **/
25 |
26 | $('a[data-confirm]').on('click', function(e) {
27 | var message = $(this).data('confirm');
28 | if (!confirm(message)) { e.preventDefault(); e.stopped = true; }
29 | });
30 |
31 | /*
32 | * Link Remote Support
33 | * link_to 'add item', '/create', :remote => true
34 | **/
35 |
36 | $('a[data-remote=true]').on('click', function(e) {
37 | var element = $(this);
38 | if (e.stopped) return;
39 | e.preventDefault(); e.stopped = true;
40 | JSAdapter.sendRequest(element, {
41 | verb: element.data('method') || 'get',
42 | url: element.attr('href')
43 | });
44 | });
45 |
46 | /*
47 | * Link Method Support
48 | * link_to 'delete item', '/destroy', :method => :delete
49 | **/
50 |
51 | $('a[data-method]:not([data-remote])').on('click', function(e) {
52 | if (e.stopped) return;
53 | JSAdapter.sendMethod($(this));
54 | e.preventDefault(); e.stopped = true;
55 | });
56 |
57 | /* JSAdapter */
58 | var JSAdapter = {
59 | // Sends an xhr request to the specified url with given verb and params
60 | // JSAdapter.sendRequest(element, { verb: 'put', url : '...', params: {} });
61 | sendRequest: function(element, options) {
62 | var verb = options.verb, url = options.url, params = options.params, dataType = options.dataType;
63 | var event = element.trigger('ajax:before');
64 | if (event.stopped) return false;
65 | $.ajax({
66 | url: url,
67 | type: verb.toUpperCase() || 'POST',
68 | data: params || [],
69 | dataType: dataType,
70 |
71 | beforeSend: function(request) { element.trigger('ajax:loading', [ request ]); },
72 | complete: function(request) { element.trigger('ajax:complete', [ request ]); },
73 | success: function(request) { element.trigger('ajax:success', [ request ]); },
74 | error: function(request) { element.trigger('ajax:failure', [ request ]); }
75 | });
76 | element.trigger('ajax:after');
77 | },
78 | // Triggers a particular method verb to be triggered in a form posting to the url
79 | // JSAdapter.sendMethod(element);
80 | sendMethod: function(element) {
81 | var verb = element.data('method');
82 | var url = element.attr('href');
83 | var form = $('');
84 | form.hide().appendTo('body');
85 | if (verb !== 'post') {
86 | var field = '';
87 | form.append(field);
88 | }
89 | form.submit();
90 | }
91 | };
92 |
--------------------------------------------------------------------------------
/db/schema.rb:
--------------------------------------------------------------------------------
1 | # encoding: UTF-8
2 | # This file is auto-generated from the current state of the database. Instead
3 | # of editing this file, please use the migrations feature of Active Record to
4 | # incrementally modify your database, and then regenerate this schema definition.
5 | #
6 | # Note that this schema.rb definition is the authoritative source for your
7 | # database schema. If you need to create the application database on another
8 | # system, you should be using db:schema:load, not running all the migrations
9 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations
10 | # you'll amass, the slower it'll run and the greater likelihood for issues).
11 | #
12 | # It's strongly recommended to check this file into your version control system.
13 |
14 | ActiveRecord::Schema.define(:version => 17) do
15 |
16 | create_table "accounts", :force => true do |t|
17 | t.string "name"
18 | t.string "email"
19 | t.string "crypted_password"
20 | t.string "role"
21 | t.datetime "created_at"
22 | t.integer "blogs_count", :default => 0, :null => false
23 | t.string "uid"
24 | t.string "provider", :limit => 20
25 | t.integer "comments_count", :default => 0, :null => false
26 | t.string "profile_url"
27 | t.string "profile_image_url"
28 | t.string "logo"
29 | end
30 |
31 | create_table "attachments", :force => true do |t|
32 | t.string "file"
33 | t.integer "account_id"
34 | t.integer "blog_id"
35 | t.datetime "created_at"
36 | end
37 |
38 | add_index "attachments", ["account_id"], :name => "index_attachments_on_account_id"
39 | add_index "attachments", ["blog_id"], :name => "index_attachments_on_blog_id"
40 |
41 | create_table "blog_comments", :force => true do |t|
42 | t.integer "account_id"
43 | t.integer "blog_id"
44 | t.text "content"
45 | t.datetime "created_at"
46 | end
47 |
48 | add_index "blog_comments", ["account_id"], :name => "index_blog_comments_on_account_id"
49 | add_index "blog_comments", ["blog_id"], :name => "index_blog_comments_on_blog_id"
50 |
51 | create_table "blog_contents", :force => true do |t|
52 | t.text "content", :limit => 16777215, :null => false
53 | end
54 |
55 | create_table "blogs", :force => true do |t|
56 | t.string "title", :null => false
57 | t.string "slug_url"
58 | t.integer "view_count", :default => 0, :null => false
59 | t.integer "blog_content_id", :null => false
60 | t.datetime "created_at", :null => false
61 | t.integer "account_id"
62 | t.integer "comments_count", :default => 0, :null => false
63 | t.datetime "content_updated_at"
64 | t.boolean "commentable", :default => true, :null => false
65 | t.string "cached_tag_list"
66 | end
67 |
68 | add_index "blogs", ["account_id"], :name => "index_blogs_on_account_id"
69 | add_index "blogs", ["content_updated_at"], :name => "index_blogs_on_content_updated_at"
70 |
71 | create_table "taggings", :force => true do |t|
72 | t.integer "tag_id"
73 | t.integer "taggable_id"
74 | t.string "taggable_type"
75 | t.integer "tagger_id"
76 | t.string "tagger_type"
77 | t.string "context", :limit => 128
78 | t.datetime "created_at"
79 | end
80 |
81 | add_index "taggings", ["tag_id"], :name => "index_taggings_on_tag_id"
82 | add_index "taggings", ["taggable_id", "taggable_type", "context"], :name => "index_taggings_on_taggable_id_and_taggable_type_and_context"
83 |
84 | create_table "tags", :force => true do |t|
85 | t.string "name"
86 | end
87 |
88 | end
89 |
--------------------------------------------------------------------------------
/app/controllers/home.rb:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | RobbinSite.controllers do
4 |
5 | before :login, :weibo_login do
6 | redirect url(:index) if account_login?
7 | end
8 |
9 | get :index do
10 | @blogs = Blog.order('id DESC').page(params[:page])
11 | render 'home/index'
12 | end
13 |
14 | get :search do
15 | client = Elasticsearch::Client.new log: true
16 | raw_result = client.search index: 'robbin_site', body: { query: { match: { _all: params[:q] } } }
17 | blog_ids = []
18 | raw_result['hits']['hits'].each do |search_id|
19 | blog_ids << search_id['_id'].to_i
20 | end
21 | @blogs = Blog.find(blog_ids)
22 | render 'home/search'
23 | end
24 |
25 | get :weibo do
26 | render 'home/weibo'
27 | end
28 |
29 | get :rss do
30 | content_type :rss
31 | @blogs = Blog.order('id DESC').limit(20)
32 | render 'home/rss'
33 | end
34 |
35 | # native authentication
36 | get :login, :map => '/login' do
37 | @account = Account.new
38 | render 'home/login'
39 | end
40 |
41 | post :login, :map => '/login' do
42 | login_tries = APP_CACHE.read("#{CACHE_PREFIX}/login_counter/#{request.ip}")
43 | halt 403 if login_tries && login_tries.to_i > 5 # reject ip if login tries is over 5 times
44 | @account = Account.new(params[:account])
45 | if login_account = Account.authenticate(@account.email, @account.password)
46 | session[:account_id] = login_account.id
47 | response.set_cookie('user', {:value => login_account.encrypt_cookie_value, :path => "/", :expires => 2.weeks.since, :httponly => true}) if params[:remember_me]
48 | flash[:notice] = '成功登录'
49 | redirect url(:index)
50 | else
51 | # retry 5 times per one hour
52 | APP_CACHE.increment("#{CACHE_PREFIX}/login_counter/#{request.ip}", 1, :expires_in => 1.hour)
53 | render 'home/login'
54 | end
55 | end
56 |
57 | delete :logout, :map => '/logout' do
58 | if account_login?
59 | session[:account_id] = nil
60 | response.delete_cookie("user")
61 | flash[:notice] = "成功退出"
62 | end
63 | redirect url(:index)
64 | end
65 |
66 | # weibo authentication
67 | get :weibo_login do
68 | session[:quick_login] = true if params[:quick_login]
69 | redirect WeiboAuth.new.authorize_url
70 | end
71 |
72 | get :weibo_callback do
73 | halt 401, "没有微博验证码" unless params[:code]
74 | auth = WeiboAuth.new
75 | begin
76 | auth.callback(params[:code])
77 | user_info = auth.get_user_info
78 | @account = Account.where(:provider => 'weibo', :uid => user_info['id'].to_i).first
79 | # create commenter account when first weibo login
80 | unless @account
81 | @account = Account.create(:provider => 'weibo', :uid => user_info['id'], :name => user_info['screen_name'], :role => 'commenter', :profile_url => user_info['profile_url'], :profile_image_url => user_info['profile_image_url'])
82 | end
83 | # update weibo profile if profile is empty
84 | if @account.profile_url.blank? || @account.profile_image_url.blank?
85 | @account.update_attributes(:profile_url => user_info['profile_url'], :profile_image_url => user_info['profile_image_url'])
86 | end
87 | session[:account_id] = @account.id
88 | if session[:quick_login]
89 | session[:quick_login] = nil
90 | render 'home/weibo_callback', :layout => false
91 | else
92 | flash[:notice] = '成功登录'
93 | redirect_to url(:index)
94 | end
95 | rescue => e
96 | STDERR.puts e
97 | STDERR.puts e.backtrace.join("\n")
98 | halt 401, "授权失败,请重试几次"
99 | end
100 | end
101 | end
--------------------------------------------------------------------------------
/app/views/layouts/application.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | <%= @title ? "#{@title} - #{APP_CONFIG['site_title']}": APP_CONFIG['site_title'] %>
7 |
8 |
9 |
10 | <%= stylesheet_link_tag 'default/document', 'default/content' %>
11 | <%= yield_content :stylesheets %>
12 |
13 |
14 |
15 |
34 |
35 |
52 |
53 |
54 | <%= yield %>
55 |
56 |
57 |
66 | <%= javascript_include_tag 'jquery', 'jquery-ujs', 'application' %>
67 | <%= yield_content :javascripts %>
68 |
77 |
78 |
79 |
--------------------------------------------------------------------------------
/config/puma.rb:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env puma
2 |
3 | # The directory to operate out of.
4 | #
5 | # The default is the current directory.
6 | #
7 | # directory '/u/apps/lolcat'
8 |
9 | # Use a object or block as the rack application. This allows the
10 | # config file to be the application itself.
11 | #
12 | # app do |env|
13 | # puts env
14 | #
15 | # body = 'Hello, World!'
16 | #
17 | # [200, { 'Content-Type' => 'text/plain', 'Content-Length' => body.length.to_s }, [body]]
18 | # end
19 |
20 | # Load “path” as a rackup file.
21 | #
22 | # The default is “config.ru”.
23 | #
24 | # rackup '/u/apps/lolcat/config.ru'
25 |
26 | # Set the environment in which the rack's app will run. The value must be a string.
27 | #
28 | # The default is “development”.
29 | #
30 | environment 'production'
31 |
32 | # Daemonize the server into the background. Highly suggest that
33 | # this be combined with “pidfile” and “stdout_redirect”.
34 | #
35 | # The default is “false”.
36 | #
37 | # daemonize
38 | daemonize true
39 |
40 | wd = File.expand_path('../../', __FILE__)
41 | tmp_path = File.join(wd, 'log')
42 | Dir.mkdir(tmp_path) unless File.exist?(tmp_path)
43 |
44 | pidfile File.join(tmp_path, 'puma.pid')
45 | state_path File.join(tmp_path, 'puma.state')
46 | stdout_redirect File.join(tmp_path, 'puma.out.log'), File.join(tmp_path, 'puma.err.log'), true
47 |
48 | # Store the pid of the server in the file at “path”.
49 | #
50 | # pidfile '/u/apps/lolcat/tmp/pids/puma.pid'
51 |
52 | # Use “path” as the file to store the server info state. This is
53 | # used by “pumactl” to query and control the server.
54 | #
55 | # state_path '/u/apps/lolcat/tmp/pids/puma.state'
56 |
57 | # Redirect STDOUT and STDERR to files specified. The 3rd parameter
58 | # (“append”) specifies whether the output is appended, the default is
59 | # “false”.
60 | #
61 | # stdout_redirect '/u/apps/lolcat/log/stdout', '/u/apps/lolcat/log/stderr'
62 | # stdout_redirect '/u/apps/lolcat/log/stdout', '/u/apps/lolcat/log/stderr', true
63 |
64 | # Disable request logging.
65 | #
66 | # The default is “false”.
67 | #
68 | # quiet
69 |
70 | # Configure “min” to be the minimum number of threads to use to answer
71 | # requests and “max” the maximum.
72 | #
73 | # The default is “0, 16”.
74 | #
75 | threads 0, 16
76 |
77 | # Bind the server to “url”. “tcp://”, “unix://” and “ssl://” are the only
78 | # accepted protocols.
79 | #
80 | # The default is “tcp://0.0.0.0:9292”.
81 | #
82 | bind 'tcp://0.0.0.0:8080'
83 | # bind 'unix:///var/run/puma.sock'
84 | # bind 'unix:///var/run/puma.sock?umask=0777'
85 | # bind 'ssl://127.0.0.1:9292?key=path_to_key&cert=path_to_cert'
86 |
87 | # Instead of “bind 'ssl://127.0.0.1:9292?key=path_to_key&cert=path_to_cert'” you
88 | # can also use the “ssl_bind” option.
89 | #
90 | # ssl_bind '127.0.0.1', '9292', { key: path_to_key, cert: path_to_cert }
91 |
92 | # Code to run before doing a restart. This code should
93 | # close log files, database connections, etc.
94 | #
95 | # This can be called multiple times to add code each time.
96 | #
97 | # on_restart do
98 | # puts 'On restart...'
99 | # end
100 |
101 | # Command to use to restart puma. This should be just how to
102 | # load puma itself (ie. 'ruby -Ilib bin/puma'), not the arguments
103 | # to puma, as those are the same as the original process.
104 | #
105 | # restart_command '/u/app/lolcat/bin/restart_puma'
106 |
107 | # === Cluster mode ===
108 |
109 | # How many worker processes to run.
110 | #
111 | # The default is “0”.
112 | #
113 | workers 0
114 |
115 | # Code to run when a worker boots to setup the process before booting
116 | # the app.
117 | #
118 | # This can be called multiple times to add hooks.
119 | #
120 | # on_worker_boot do
121 | # puts 'On worker boot...'
122 | # end
123 |
124 | # === Puma control rack application ===
125 |
126 | # Start the puma control rack application on “url”. This application can
127 | # be communicated with to control the main server. Additionally, you can
128 | # provide an authentication token, so all requests to the control server
129 | # will need to include that token as a query parameter. This allows for
130 | # simple authentication.
131 | #
132 | # Check out https://github.com/puma/puma/blob/master/lib/puma/app/status.rb
133 | # to see what the app has available.
134 | #
135 | # activate_control_app 'unix:///var/run/pumactl.sock'
136 | # activate_control_app 'unix:///var/run/pumactl.sock', { auth_token: '12345' }
137 | # activate_control_app 'unix:///var/run/pumactl.sock', { no_token: true }
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | GIT
2 | remote: git://github.com/csdn-dev/second_level_cache.git
3 | revision: f6185488ebf1bf5d1e7e18e1448c61bb367f51d2
4 | specs:
5 | second_level_cache (1.6.2)
6 | activesupport (~> 3.2.0)
7 |
8 | GIT
9 | remote: git://github.com/robbin/acts-as-taggable-on.git
10 | revision: f52d2b9347a30ce2bdbf4c2d8ad1a7ced7e50d86
11 | specs:
12 | acts-as-taggable-on (2.3.3)
13 | activerecord (~> 3.0)
14 |
15 | GEM
16 | remote: https://rubygems.org/
17 | specs:
18 | activemodel (3.2.21)
19 | activesupport (= 3.2.21)
20 | builder (~> 3.0.0)
21 | activerecord (3.2.21)
22 | activemodel (= 3.2.21)
23 | activesupport (= 3.2.21)
24 | arel (~> 3.0.2)
25 | tzinfo (~> 0.3.29)
26 | activesupport (3.2.21)
27 | i18n (~> 0.6, >= 0.6.4)
28 | multi_json (~> 1.0)
29 | arel (3.0.3)
30 | bcrypt (3.1.9)
31 | bcrypt-ruby (3.1.5)
32 | bcrypt (>= 3.1.3)
33 | builder (3.0.4)
34 | carrierwave (0.10.0)
35 | activemodel (>= 3.2.0)
36 | activesupport (>= 3.2.0)
37 | json (>= 1.7)
38 | mime-types (>= 1.16)
39 | coderay (1.1.0)
40 | crass (0.2.1)
41 | daemons (1.1.9)
42 | dalli (2.7.2)
43 | database_cleaner (1.3.0)
44 | elasticsearch (1.0.13)
45 | elasticsearch-api (= 1.0.13)
46 | elasticsearch-transport (= 1.0.13)
47 | elasticsearch-api (1.0.13)
48 | multi_json
49 | elasticsearch-transport (1.0.13)
50 | faraday
51 | multi_json
52 | erubis (2.7.0)
53 | eventmachine (1.0.8)
54 | factory_girl (4.5.0)
55 | activesupport (>= 3.0.0)
56 | faraday (0.9.2)
57 | multipart-post (>= 1.2, < 3)
58 | github-markdown (0.6.7)
59 | http_router (0.11.1)
60 | rack (>= 1.0.0)
61 | url_mount (~> 0.2.1)
62 | i18n (0.6.11)
63 | json (1.8.1)
64 | kgio (2.9.2)
65 | method_source (0.8.2)
66 | mime-types (2.4.3)
67 | mini_magick (4.0.1)
68 | mini_portile (0.6.1)
69 | minitest (2.6.2)
70 | multi_json (1.10.1)
71 | multipart-post (2.0.0)
72 | mysql2 (0.3.17)
73 | netrc (0.8.0)
74 | nokogiri (1.6.5)
75 | mini_portile (~> 0.6.0)
76 | nokogumbo (1.1.12)
77 | nokogiri
78 | padrino-core (0.11.3)
79 | activesupport (>= 3.1, < 4.0)
80 | http_router (~> 0.11.0)
81 | rack-protection (>= 1.5.0)
82 | sinatra (~> 1.4.2)
83 | thor (~> 0.17.0)
84 | tilt (~> 1.3.7)
85 | padrino-gen (0.11.3)
86 | bundler (~> 1.0)
87 | padrino-core (= 0.11.3)
88 | padrino-helpers (0.11.3)
89 | i18n (~> 0.6)
90 | padrino-core (= 0.11.3)
91 | pry (0.10.1)
92 | coderay (~> 1.1.0)
93 | method_source (~> 0.8.1)
94 | slop (~> 3.4)
95 | pry-padrino (0.1.2)
96 | pry (>= 0.8)
97 | rack (1.5.2)
98 | rack-protection (1.5.3)
99 | rack
100 | rack-test (0.6.2)
101 | rack (>= 1.0)
102 | rainbows (4.6.2)
103 | kgio (~> 2.5)
104 | rack (~> 1.1)
105 | unicorn (~> 4.8)
106 | raindrops (0.13.0)
107 | rake (10.4.0)
108 | rest-client (1.7.2)
109 | mime-types (>= 1.16, < 3.0)
110 | netrc (~> 0.7)
111 | sanitize (3.0.3)
112 | crass (~> 0.2.0)
113 | nokogiri (>= 1.4.4)
114 | nokogumbo (= 1.1.12)
115 | sinatra (1.4.5)
116 | rack (~> 1.4)
117 | rack-protection (~> 1.4)
118 | tilt (~> 1.3, >= 1.3.4)
119 | slop (3.6.0)
120 | thin (1.6.3)
121 | daemons (~> 1.0, >= 1.0.9)
122 | eventmachine (~> 1.0)
123 | rack (~> 1.0)
124 | thor (0.17.0)
125 | tilt (1.3.7)
126 | tzinfo (0.3.42)
127 | unicorn (4.8.3)
128 | kgio (~> 2.6)
129 | rack
130 | raindrops (~> 0.7)
131 | url_mount (0.2.1)
132 | rack
133 | will_paginate (3.0.7)
134 | zbatery (4.2.0)
135 | rainbows (~> 4.6)
136 |
137 | PLATFORMS
138 | ruby
139 |
140 | DEPENDENCIES
141 | activerecord (~> 3.2)
142 | acts-as-taggable-on!
143 | bcrypt-ruby
144 | carrierwave
145 | dalli
146 | database_cleaner
147 | elasticsearch
148 | erubis (~> 2.7.0)
149 | factory_girl
150 | github-markdown
151 | kgio
152 | mini_magick
153 | minitest (~> 2.6.0)
154 | mysql2
155 | padrino-core (~> 0.11)
156 | padrino-gen (~> 0.11)
157 | padrino-helpers (~> 0.11)
158 | pry-padrino
159 | rack-test
160 | rake
161 | rest-client
162 | sanitize
163 | second_level_cache!
164 | thin
165 | tilt (~> 1.3.7)
166 | will_paginate
167 | zbatery
168 |
169 | BUNDLED WITH
170 | 1.10.6
171 |
--------------------------------------------------------------------------------
/app/controllers/admin.rb:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | RobbinSite.controllers :admin do
4 |
5 | before do
6 | halt 403 unless account_admin?
7 | end
8 |
9 | get :index do
10 | render 'admin/index'
11 | end
12 |
13 | # blog related routes: publish, update, delete blog and blog content editor preview...
14 | get :new_blog, :map => '/admin/blog/new' do
15 | @blog = Blog.new
16 | @attachments = current_account.attachments.orphan
17 | render 'admin/new_blog'
18 | end
19 |
20 | post :blog, :map => '/admin/blog' do
21 | @blog = Blog.new(params[:blog])
22 | @blog.account = current_account
23 | if @blog.save
24 | @blog.attach!(current_account)
25 | ping_search_engine(@blog) if APP_CONFIG['blog_search_ping'] # only ping search engine in production environment
26 | flash[:notice] = '文章成功发布'
27 | redirect url(:blog, :show, :id => @blog.id)
28 | else
29 | render 'admin/new_blog'
30 | end
31 | end
32 |
33 | post :blog_preview, :map => '/admin/blog/preview' do
34 | GitHub::Markdown.to_html(params[:term], :gfm) if params[:term]
35 | end
36 |
37 | get :edit_blog, :map => '/admin/blog/:id/edit' do
38 | @blog = Blog.find params[:id].to_i
39 | @attachments = current_account.attachments.where(:blog_id => [nil, @blog.id]).order('id ASC')
40 | render 'admin/edit_blog'
41 | end
42 |
43 | put :blog, :map => '/admin/blog/:id' do
44 | @blog = Blog.find params[:id].to_i
45 | if @blog.update_blog(params[:blog])
46 | @blog.attach!(current_account)
47 | flash[:notice] = '文章修改完成'
48 | redirect url(:blog, :show, :id => @blog.id)
49 | else
50 | render 'admin/edit_blog'
51 | end
52 | end
53 |
54 | delete :blog, :map => '/admin/blog/:id' do
55 | @blog = Blog.find params[:id].to_i
56 | @blog.destroy
57 | flash[:notice] = '文章已经删除'
58 | redirect url(:index)
59 | end
60 |
61 | delete :comment, :map => '/admin/comment/:id' do
62 | content_type :js
63 | comment = BlogComment.find params[:id]
64 | comment.destroy
65 | "$('tr#comment_#{comment.id}').fadeOut('slow', function(){ $(this).remove();});"
66 | end
67 |
68 | # attachment related routes: upload, show, delete attachment...
69 | get :new_attachment, :map => '/admin/attachment/new' do
70 | @attachment = Attachment.new
71 | render 'admin/new_attachment', :layout => false
72 | end
73 |
74 | get :attachment, :map => '/admin/attachment/:id' do
75 | @attachment = Attachment.find params[:id]
76 | render 'admin/attachment', :layout => false
77 | end
78 |
79 | post :create_attachment, :map => '/admin/attachment' do
80 | attachment = Attachment.new(params[:attachment])
81 | attachment.account = current_account
82 | if attachment.save
83 | redirect_to url(:admin, :attachment, :id => attachment.id)
84 | else
85 | render 'admin/attachment_fail', :layout => false
86 | end
87 | end
88 |
89 | delete :attachment, :map => '/admin/attachment/:id' do
90 | content_type :js
91 | @attachment = Attachment.find params[:id]
92 | @attachment.destroy
93 | "$('#attachment_#{@attachment.id}').html(' #{@attachment.file} 附件已被删除')"
94 | end
95 |
96 | # admin console related: profile, accounts, blogs, comments...
97 | get :edit_profile, :map => '/admin/profile/:id/edit' do
98 | @account = Account.find params[:id]
99 | if @account.admin?
100 | render 'admin/edit_profile'
101 | else
102 | redirect_to url(:admin, :accounts)
103 | end
104 | end
105 |
106 | put :profile, :map => '/admin/profile/:id' do
107 | @account = Account.find params[:id]
108 | if @account.update_attributes(params[:account])
109 | flash[:notice] = '修改个人设置成功'
110 | redirect_to url(:admin, :edit_profile, :id => @account.id)
111 | else
112 | render 'admin/edit_profile'
113 | end
114 | end
115 |
116 | get :accounts, :map => '/admin/accounts' do
117 | @admin_accounts = Account.where(:role => 'admin').order('id ASC')
118 | @commenters = Account.where(:role => 'commenter').order('id DESC').page(params[:page]).per_page(100)
119 | render 'admin/accounts'
120 | end
121 |
122 | delete :account, :map => '/admin/account/:id' do
123 | content_type :js
124 | account = Account.find params[:id]
125 | if account.commenter?
126 | account.destroy
127 | "$('tr#account_#{account.id}').fadeOut('slow', function(){ $(this).remove();});"
128 | end
129 | end
130 |
131 | get :blogs, :map => '/admin/blogs' do
132 | @blogs = Blog.order('id DESC').page(params[:page]).per_page(100)
133 | render 'admin/blogs'
134 | end
135 |
136 | get :comments, :map => '/admin/comments' do
137 | @comments = BlogComment.order('id DESC').page(params[:page]).per_page(100)
138 | render 'admin/comments'
139 | end
140 | end
--------------------------------------------------------------------------------
/locale/zh_cn.yml:
--------------------------------------------------------------------------------
1 | zh_cn:
2 | date:
3 | abbr_day_names:
4 | - 日
5 | - 一
6 | - 二
7 | - 三
8 | - 四
9 | - 五
10 | - 六
11 | abbr_month_names:
12 | -
13 | - 1月
14 | - 2月
15 | - 3月
16 | - 4月
17 | - 5月
18 | - 6月
19 | - 7月
20 | - 8月
21 | - 9月
22 | - 10月
23 | - 11月
24 | - 12月
25 | day_names:
26 | - 星期日
27 | - 星期一
28 | - 星期二
29 | - 星期三
30 | - 星期四
31 | - 星期五
32 | - 星期六
33 | formats:
34 | default: ! '%Y-%m-%d'
35 | long: ! '%Y年%b%d日'
36 | short: ! '%b%d日'
37 | month_names:
38 | -
39 | - 一月
40 | - 二月
41 | - 三月
42 | - 四月
43 | - 五月
44 | - 六月
45 | - 七月
46 | - 八月
47 | - 九月
48 | - 十月
49 | - 十一月
50 | - 十二月
51 | order:
52 | - :year
53 | - :month
54 | - :day
55 | datetime:
56 | distance_in_words:
57 | about_x_hours:
58 | one: 大约一小时
59 | other: 大约 %{count} 小时
60 | about_x_months:
61 | one: 大约一个月
62 | other: 大约 %{count} 个月
63 | about_x_years:
64 | one: 大约一年
65 | other: 大约 %{count} 年
66 | almost_x_years:
67 | one: 接近一年
68 | other: 接近 %{count} 年
69 | half_a_minute: 半分钟
70 | less_than_x_minutes:
71 | one: 不到一分钟
72 | other: 不到 %{count} 分钟
73 | less_than_x_seconds:
74 | one: 不到一秒
75 | other: 不到 %{count} 秒
76 | over_x_years:
77 | one: 一年多
78 | other: ! '%{count} 年多'
79 | x_days:
80 | one: 一天
81 | other: ! '%{count} 天'
82 | x_minutes:
83 | one: 一分钟
84 | other: ! '%{count} 分钟'
85 | x_months:
86 | one: 一个月
87 | other: ! '%{count} 个月'
88 | x_seconds:
89 | one: 一秒
90 | other: ! '%{count} 秒'
91 | prompts:
92 | day: 日
93 | hour: 时
94 | minute: 分
95 | month: 月
96 | second: 秒
97 | year: 年
98 | errors: &errors
99 | format: ! '%{attribute} %{message}'
100 | messages:
101 | accepted: 必须是可被接受的
102 | blank: 不能为空字符
103 | confirmation: 与确认值不匹配
104 | empty: 不能留空
105 | equal_to: 必须等于 %{count}
106 | even: 必须为双数
107 | exclusion: 是保留关键字
108 | greater_than: 必须大于 %{count}
109 | greater_than_or_equal_to: 必须大于或等于 %{count}
110 | inclusion: 不包含于列表中
111 | invalid: 是无效的
112 | less_than: 必须小于 %{count}
113 | less_than_or_equal_to: 必须小于或等于 %{count}
114 | not_a_number: 不是数字
115 | not_an_integer: 必须是整数
116 | odd: 必须为单数
117 | record_invalid: ! '验证失败: %{errors}'
118 | taken: 已经被使用
119 | too_long: 过长(最长为 %{count} 个字符)
120 | too_short: 过短(最短为 %{count} 个字符)
121 | wrong_length: 长度非法(必须为 %{count} 个字符)
122 | template:
123 | body: 如下字段出现错误:
124 | header:
125 | one: 有 1 个错误发生导致「%{model}」无法被保存。
126 | other: 有 %{count} 个错误发生导致「%{model}」无法被保存。
127 | helpers:
128 | select:
129 | prompt: 请选择
130 | submit:
131 | create: 新增%{model}
132 | submit: 储存%{model}
133 | update: 更新%{model}
134 | number:
135 | currency:
136 | format:
137 | delimiter: ! ','
138 | format: ! '%u %n'
139 | precision: 2
140 | separator: .
141 | significant: false
142 | strip_insignificant_zeros: false
143 | unit: CN¥
144 | format:
145 | delimiter: ! ','
146 | precision: 3
147 | separator: .
148 | significant: false
149 | strip_insignificant_zeros: false
150 | human:
151 | decimal_units:
152 | format: ! '%n %u'
153 | units:
154 | billion: 十亿
155 | million: 百万
156 | quadrillion: 千兆
157 | thousand: 千
158 | trillion: 兆
159 | unit: ''
160 | format:
161 | delimiter: ''
162 | precision: 1
163 | significant: false
164 | strip_insignificant_zeros: false
165 | storage_units:
166 | format: ! '%n %u'
167 | units:
168 | byte:
169 | one: Byte
170 | other: Bytes
171 | gb: GB
172 | kb: KB
173 | mb: MB
174 | tb: TB
175 | percentage:
176 | format:
177 | delimiter: ''
178 | precision:
179 | format:
180 | delimiter: ''
181 | support:
182 | array:
183 | last_word_connector: ! ', 和 '
184 | two_words_connector: ! ' 和 '
185 | words_connector: ! ', '
186 | time:
187 | am: 上午
188 | formats:
189 | default: ! '%Y年%b%d日 %A %H:%M:%S %Z'
190 | long: ! '%Y年%b%d日 %H:%M'
191 | short: ! '%b%d日 %H:%M'
192 | pm: 下午
193 | # remove these aliases after 'activemodel' and 'activerecord' namespaces are removed from Rails repository
194 | activemodel:
195 | errors:
196 | <<: *errors
197 | activerecord:
198 | errors:
199 | <<: *errors
200 |
--------------------------------------------------------------------------------
/app/views/blog/show.erb:
--------------------------------------------------------------------------------
1 | <% @title = @blog.title %>
2 | <% @description = @blog.cached_tag_list.to_s + ' ' + @blog.content.truncate(200).gsub(/(\r|\n)/, ' ') %>
3 |
4 | <% content_for :stylesheets do %>
5 | <%= stylesheet_link_tag 'default/github', 'hightlight/github.min' %>
6 | <% end %>
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
<%= link_to @blog.title, blog_url(@blog) %>
15 | <%= link_to '', blog_url(@blog, mime_type = :md), :class => 'markdown', :title => 'Markdown格式原文' %>
16 | <% if account_admin? %>
17 | <%= link_to '', url(:admin, :blog, :id => @blog.id), :method => :delete, :confirm => '要删除文章吗?', :class => 'del', :title => '删除' %>
18 | <%= link_to '', url(:admin, :edit_blog, :id => @blog.id), :class => 'edit', :title => '编辑' %>
19 | <% end %>
20 |
21 | <% unless @blog.cached_tags.blank? %>
22 |
23 | <% @blog.cached_tags.each do |tag| %>
24 | <%= link_to "#{tag}".html_safe, url(:blog, :tag, :name => tag), :class => 'tag', :rel => 'tag' %>
25 | <% end %>
26 |
27 | <% end %>
28 |
29 | <%= @blog.md_content.html_safe %>
30 |
31 |
32 |
33 | <%= @blog.account.name %>
34 | <%= time_ago_in_words(@blog.created_at) %>发表
35 | <%= time_ago_in_words(@blog.content_updated_at) %>更新
36 | <%= @blog.view_count %>次浏览
37 |
38 |
39 |
40 |
41 |
42 | <%= partial 'blog/weibo_share', :locals => {:content => (@blog.title + ":" + @blog.content).truncate(120).gsub(/\r|\n/, '')} %>
43 |
44 |
45 |
79 |
80 |
81 |
82 |
83 |
84 |
85 | <%= partial 'blog/right' %>
86 |
87 |
88 | <% content_for :javascripts do %>
89 | <%= javascript_include_tag 'highlight.min' %>
90 |
146 | <% end %>
--------------------------------------------------------------------------------
/public/stylesheets/default/github.css:
--------------------------------------------------------------------------------
1 | .con .text {
2 | font-family: Helvetica, Tahoma, arial, sans-serif;
3 | font-size: 14px;
4 | line-height: 1.6;
5 | padding-top: 10px;
6 | padding-bottom: 10px;
7 | background-color: white;
8 | padding: 30px 40px; }
9 |
10 | .con .text ol,
11 | .con .text ol li,
12 | .con .text ul li ol li{ list-style-type:decimal;}
13 | .con .text ul,
14 | .con .text ul li{ list-style-type:disc;}
15 |
16 | body > *:first-child {
17 | margin-top: 0 !important; }
18 | body > *:last-child {
19 | margin-bottom: 0 !important; }
20 |
21 | .con .text a {color: #f60; }
22 | .con .text a.absent {color: #cc0000; }
23 | .con .text a.anchor {
24 | display: block;
25 | padding-left: 30px;
26 | margin-left: -30px;
27 | cursor: pointer;
28 | position: absolute;
29 | top: 0;
30 | left: 0;
31 | bottom: 0; }
32 |
33 | .con .text h1, .con .text h2, .con .text h3, .con .text h4, .con .text h5, .con .text h6 {
34 | margin: 20px 0 10px;
35 | padding: 0;
36 | font-weight: bold;
37 | -webkit-font-smoothing: antialiased;
38 | cursor: text;
39 | position: relative; }
40 |
41 | .con .text h1:hover a.anchor, .con .text h2:hover a.anchor, .con .text h3:hover a.anchor, .con .text h4:hover a.anchor, .con .text h5:hover a.anchor, .con .text h6:hover a.anchor {
42 | background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA09pVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoMTMuMCAyMDEyMDMwNS5tLjQxNSAyMDEyLzAzLzA1OjIxOjAwOjAwKSAgKE1hY2ludG9zaCkiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6OUM2NjlDQjI4ODBGMTFFMTg1ODlEODNERDJBRjUwQTQiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6OUM2NjlDQjM4ODBGMTFFMTg1ODlEODNERDJBRjUwQTQiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo5QzY2OUNCMDg4MEYxMUUxODU4OUQ4M0REMkFGNTBBNCIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo5QzY2OUNCMTg4MEYxMUUxODU4OUQ4M0REMkFGNTBBNCIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PsQhXeAAAABfSURBVHjaYvz//z8DJYCRUgMYQAbAMBQIAvEqkBQWXI6sHqwHiwG70TTBxGaiWwjCTGgOUgJiF1J8wMRAIUA34B4Q76HUBelAfJYSA0CuMIEaRP8wGIkGMA54bgQIMACAmkXJi0hKJQAAAABJRU5ErkJggg==) no-repeat 10px center;
43 | text-decoration: none; }
44 |
45 | .con .text h1 tt, .con .text h1 code {
46 | font-size: inherit; }
47 |
48 | .con .text h2 tt, .con .text h2 code {
49 | font-size: inherit; }
50 |
51 | .con .text h3 tt, .con .text h3 code {
52 | font-size: inherit; }
53 |
54 | .con .text h4 tt, .con .text h4 code {
55 | font-size: inherit; }
56 |
57 | .con .text h5 tt, .con .text h5 code {
58 | font-size: inherit; }
59 |
60 | .con .text h6 tt, .con .text h6 code {
61 | font-size: inherit; }
62 |
63 | .con .text h1 {
64 | font-size: 28px;
65 | color: #333; }
66 |
67 | .con .text h2 {
68 | font-size: 24px;
69 | border-bottom: 1px solid #cccccc;
70 | color: #333; }
71 |
72 | .con .text h3 {
73 | font-size: 18px; color: #333;
74 | text-shadow:none;}
75 |
76 | .con .text h4 {
77 | font-size: 16px; }
78 |
79 | .con .text h5 {
80 | font-size: 14px; }
81 |
82 | .con .text h6 {
83 | color: #777777;
84 | font-size: 14px; }
85 |
86 | .con .text p, .con .text blockquote, .con .text ul, .con .text ol, .con .text dl, .con .text li, .con .text table, .con .text pre {
87 | margin: 15px 0; }
88 |
89 | .con .text hr {
90 | background: transparent url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAYAAAAECAYAAACtBE5DAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYwIDYxLjEzNDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNSBNYWNpbnRvc2giIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6OENDRjNBN0E2NTZBMTFFMEI3QjRBODM4NzJDMjlGNDgiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6OENDRjNBN0I2NTZBMTFFMEI3QjRBODM4NzJDMjlGNDgiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo4Q0NGM0E3ODY1NkExMUUwQjdCNEE4Mzg3MkMyOUY0OCIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo4Q0NGM0E3OTY1NkExMUUwQjdCNEE4Mzg3MkMyOUY0OCIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PqqezsUAAAAfSURBVHjaYmRABcYwBiM2QSA4y4hNEKYDQxAEAAIMAHNGAzhkPOlYAAAAAElFTkSuQmCC) repeat-x 0 0;
91 | border: 0 none;
92 | color: #cccccc;
93 | height: 4px;
94 | padding: 0;
95 | }
96 |
97 | body > h2:first-child {
98 | margin-top: 0;
99 | padding-top: 0; }
100 | body > h1:first-child {
101 | margin-top: 0;
102 | padding-top: 0; }
103 | body > h1:first-child + h2 {
104 | margin-top: 0;
105 | padding-top: 0; }
106 | body > h3:first-child, body > h4:first-child, body > h5:first-child, body > h6:first-child {
107 | margin-top: 0;
108 | padding-top: 0; }
109 |
110 | a:first-child h1, a:first-child h2, a:first-child h3, a:first-child h4, a:first-child h5, a:first-child h6 {
111 | margin-top: 0;
112 | padding-top: 0; }
113 |
114 | .con .text h1 p,.con .text h2 p, .con .text h3 p, .con .text h4 p, .con .text h5 p, .con .text h6 p {
115 | margin-top: 0; }
116 |
117 | .con .text li p.first {
118 | display: inline-block; }
119 | .con .text li {
120 | margin: 0; }
121 | .con .text ul, .con .text ol {
122 | padding-left: 30px; }
123 |
124 | .con .text ul :first-child, .con .text ol :first-child {
125 | margin-top: 0; }
126 |
127 | .con .text dl {
128 | padding: 0; }
129 | .con .text dl dt {
130 | font-size: 14px;
131 | font-weight: bold;
132 | font-style: italic;
133 | padding: 0;
134 | margin: 15px 0 5px; }
135 | .con .text dl dt:first-child {
136 | padding: 0; }
137 | dl dt > :first-child {
138 | margin-top: 0; }
139 | dl dt > :last-child {
140 | margin-bottom: 0; }
141 | .con .text dl dd {
142 | margin: 0 0 15px;
143 | padding: 0 15px; }
144 | dl dd > :first-child {
145 | margin-top: 0; }
146 | dl dd > :last-child {
147 | margin-bottom: 0; }
148 |
149 | .con .text blockquote {
150 | border-left: 4px solid #dddddd;
151 | padding: 0 15px;
152 | color: #777777; }
153 | blockquote > :first-child {
154 | margin-top: 0; }
155 | blockquote > :last-child {
156 | margin-bottom: 0; }
157 |
158 | .con .text table {
159 | padding: 0;border-collapse: collapse; }
160 | .con .text table tr {
161 | border-top: 1px solid #cccccc;
162 | background-color: white;
163 | margin: 0;
164 | padding: 0; }
165 | .con .text table tr:nth-child(2n) {
166 | background-color: #f8f8f8; }
167 | .con .text table tr th {
168 | font-weight: bold;
169 | border: 1px solid #cccccc;
170 | text-align: left;
171 | margin: 0;
172 | padding: 6px 13px; }
173 | .con .text table tr td {
174 | border: 1px solid #cccccc;
175 | text-align: left;
176 | margin: 0;
177 | padding: 6px 13px; }
178 | .con .text table tr th :first-child, .con .text table tr td :first-child {
179 | margin-top: 0; }
180 | .con .text table tr th :last-child, .con .text table tr td :last-child {
181 | margin-bottom: 0; }
182 |
183 | img {
184 | max-width: 100%; }
185 |
186 | .con .text span.frame {
187 | display: block;
188 | overflow: hidden; }
189 | span.frame > span {
190 | border: 1px solid #dddddd;
191 | display: block;
192 | float: left;
193 | overflow: hidden;
194 | margin: 13px 0 0;
195 | padding: 7px;
196 | width: auto; }
197 | .con .text span.frame span img {
198 | display: block;
199 | float: left; }
200 | .con .text span.frame span span {
201 | clear: both;
202 | color: #333333;
203 | display: block;
204 | padding: 5px 0 0; }
205 | .con .text span.align-center {
206 | display: block;
207 | overflow: hidden;
208 | clear: both; }
209 | span.align-center > span {
210 | display: block;
211 | overflow: hidden;
212 | margin: 13px auto 0;
213 | text-align: center; }
214 | .con .text span.align-center span img {
215 | margin: 0 auto;
216 | text-align: center; }
217 | .con .text span.align-right {
218 | display: block;
219 | overflow: hidden;
220 | clear: both; }
221 | span.align-right > span {
222 | display: block;
223 | overflow: hidden;
224 | margin: 13px 0 0;
225 | text-align: right; }
226 | .con .text span.align-right span img {
227 | margin: 0;
228 | text-align: right; }
229 | .con .text span.float-left {
230 | display: block;
231 | margin-right: 13px;
232 | overflow: hidden;
233 | float: left; }
234 | .con .text span.float-left span {
235 | margin: 13px 0 0; }
236 | .con .text span.float-right {
237 | display: block;
238 | margin-left: 13px;
239 | overflow: hidden;
240 | float: right; }
241 | span.float-right > span {
242 | display: block;
243 | overflow: hidden;
244 | margin: 13px auto 0;
245 | text-align: right; }
246 |
247 | .con .text code, tt {
248 | margin: 0 2px;
249 | padding: 0 5px;
250 | white-space: nowrap;
251 | border: 1px solid #eaeaea;
252 | background-color: #f8f8f8;
253 | border-radius: 3px; }
254 |
255 | .con .text pre code {
256 | margin: 0;
257 | padding: 0;
258 | white-space: pre;
259 | border: none;
260 | /* word-wrap: break-word;*/
261 | background: transparent; }
262 |
263 | .con .text .highlight pre {
264 | background-color: #f8f8f8;
265 | border: 1px solid #cccccc;
266 | font-size: 13px;
267 | line-height: 19px;
268 | overflow: auto;
269 | padding: 6px 10px;
270 | border-radius: 3px; }
271 |
272 | .con .text pre {
273 | background-color: #f8f8f8;
274 | border: 1px solid #cccccc;
275 | font-size: 13px;
276 | line-height: 19px;
277 | overflow: auto;
278 | padding: 6px 10px;
279 | border-radius: 3px; }
280 | .con .text pre code, .con .text pre tt {
281 | background-color: transparent;
282 | border: none; }
283 |
284 | @media print {
285 | .con .text table, pre {
286 | page-break-inside: avoid;
287 | }
288 | }
289 |
290 | /* blog comments markdown fix */
291 | .comment .cot_con ul {
292 | padding: 0 0 0 20px;
293 | }
294 |
295 | .comment .cot_con ul li {
296 | padding: 0;
297 | list-style-type: disc;
298 | border-bottom:0;
299 | }
300 |
301 | .cot_con h3 {
302 | font-size: 15px; color: #666;
303 | text-shadow:none;
304 | }
--------------------------------------------------------------------------------
/webcache.md:
--------------------------------------------------------------------------------
1 | # Web应用的缓存设计模式
2 |
3 | ## ORM缓存引言
4 |
5 | 从10年前的2003年开始,在Web应用领域,ORM(对象-关系映射)框架就开始逐渐普及,并且流行开来,其中最广为人知的就是Java的开源ORM框架Hibernate,后来Hibernate也成为了EJB3的实现框架;2005年以后,ORM开始普及到其他编程语言领域,其中最有名气的是Ruby on rails框架的ORM - ActiveRecord。如今各种开源框架的ORM,乃至ODM(对象-文档关系映射,用在访问NoSQLDB)层出不穷,功能都十分强大,也很普及。
6 |
7 | 然而围绕ORM的性能问题,也一直有很多批评的声音。其实ORM的架构对插入缓存技术是非常容易的,我做的很多项目和产品,但凡使用ORM,缓存都是标配,性能都非常好。而且我发现业界使用ORM的案例都忽视了缓存的运用,或者说没有意识到ORM缓存可以带来巨大的性能提升。
8 |
9 | ## ORM缓存应用案例
10 |
11 | 我们去年有一个老产品重写的项目,这个产品有超过10年历史了,数据库的数据量很大,多个表都是上千万条记录,最大的表记录达到了9000万条,Web访问的请求数每天有300万左右。
12 |
13 | 老产品采用了传统的解决性能问题的方案:Web层采用了动态页面静态化技术,超过一定时间的文章生成静态HTML文件;对数据库进行分库分表,按年拆表。动态页面静态化和分库分表是应对大访问量和大数据量的常规手段,本身也有效。但它的缺点也很多,比方说增加了代码复杂度和维护难度,跨库运算的困难等等,这个产品的代码维护历来非常困难,导致bug很多。
14 |
15 | 进行产品重写的时候,我们放弃了动态页面静态化,采用了纯动态网页;放弃了分库分表,直接操作千万级,乃至近亿条记录的大表进行SQL查询;也没有采取读写分离技术,全部查询都是在单台主数据库上进行;数据库访问全部使用ActiveRecord,进行了大量的ORM缓存。上线以后的效果非常好:单台MySQL数据库服务器CPU的IO Wait低于5%;用单台1U服务器2颗4核至强CPU已经可以轻松支持每天350万动态请求量;最重要的是,插入缓存并不需要代码增加多少复杂度,可维护性非常好。
16 |
17 | 总之,采用ORM缓存是Web应用提升性能一种有效的思路,这种思路和传统的提升性能的解决方案有很大的不同,但它在很多应用场景(包括高度动态化的SNS类型应用)非常有效,而且不会显著增加代码复杂度,所以这也是我自己一直偏爱的方式。因此我一直很想写篇文章,结合示例代码介绍ORM缓存的编程技巧。
18 |
19 | 今年春节前后,我开发自己的个人网站项目,有意识的大量使用了ORM缓存技巧。对一个没多少访问量的个人站点来说,有些过度设计了,但我也想借这个机会把常用的ORM缓存设计模式写成示例代码,提供给大家参考。我的个人网站源代码是开源的,托管在github上:[robbin_site](https://github.com/robbin/robbin_site)
20 |
21 | ## ORM缓存的基本理念
22 |
23 | 我在2007年的时候写过一篇文章,分析ORM缓存的理念:[ORM对象缓存探讨](http://robbinfan.com/blog/3/orm-cache) ,所以这篇文章不展开详谈了,总结来说,ORM缓存的基本理念是:
24 |
25 | * 以减少数据库服务器磁盘IO为最终目的,而不是减少发送到数据库的SQL条数。实际上使用ORM,会显著增加SQL条数,有时候会成倍增加SQL。
26 | * 数据库schema设计的取向是尽量设计 _细颗粒度_ 的表,表和表之间用外键关联,颗粒度越细,缓存对象的单位越小,缓存的应用场景越广泛
27 | * 尽量避免多表关联查询,尽量拆成多个表单独的主键查询,尽量多制造 `n + 1` 条查询,不要害怕“臭名昭著”的 `n + 1` 问题,实际上 `n + 1` 才能有效利用ORM缓存
28 |
29 | ## 利用表关联实现透明的对象缓存
30 |
31 | 在设计数据库的schema的时候,设计多个细颗粒度的表,用外键关联起来。当通过ORM访问关联对象的时候,ORM框架会将关联对象的访问转化成用主键查询关联表,发送 `n + 1`条SQL。而基于主键的查询可以直接利用对象缓存。
32 |
33 | 我们自己开发了一个基于ActiveRecord封装的对象缓存框架:[second_level_cache](https://github.com/csdn-dev/second_level_cache) ,从这个ruby插件的名称就可以看出,实现借鉴了Hibernate的二级缓存实现。这个对象缓存的配置和使用,可以看我写的[ActiveRecord对象缓存配置](http://robbinfan.com/blog/33/activerecord-object-cache) 。
34 |
35 | 下面用一个实际例子来演示一下对象缓存起到的作用:访问我个人站点的首页。 这个页面的数据需要读取三张表:blogs表获取文章信息,blog_contents表获取文章内容,accounts表获取作者信息。三张表的model定义片段如下,完整代码请看[models](https://github.com/robbin/robbin_site/tree/master/models) :
36 |
37 | class Account < ActiveRecord::Base
38 | acts_as_cached
39 | has_many :blogs
40 | end
41 |
42 | class Blog < ActiveRecord::Base
43 | acts_as_cached
44 | belongs_to :blog_content, :dependent => :destroy
45 | belongs_to :account, :counter_cache => true
46 | end
47 |
48 | class BlogContent < ActiveRecord::Base
49 | acts_as_cached
50 | end
51 |
52 | 传统的做法是发送一条三表关联的查询语句,类似这样的:
53 |
54 | SELECT blogs.*, blog_contents.content, account.name
55 | FROM blogs
56 | LEFT JOIN blog_contents ON blogs.blog_content_id = blog_contents.id
57 | LEFT JOIN accounts ON blogs.account_id = account.id
58 |
59 | 往往单条SQL语句就搞定了,但是复杂SQL的带来的表扫描范围可能比较大,造成的数据库服务器磁盘IO会高很多,数据库实际IO负载往往无法得到有效缓解。
60 |
61 | 我的做法如下,完整代码请看[home.rb](https://github.com/robbin/robbin_site/blob/master/app/controllers/home.rb) :
62 |
63 | @blogs = Blog.order('id DESC').page(params[:page])
64 |
65 | 这是一条分页查询,实际发送的SQL如下:
66 |
67 | SELECT * FROM blogs ORDER BY id DESC LIMIT 20
68 |
69 | 转成了单表查询,磁盘IO会小很多。至于文章内容,则是通过`blog.content`的对象访问获得的,由于首页抓取20篇文章,所以实际上会多出来20条主键查询SQL访问blog_contents表。就像下面这样:
70 |
71 | DEBUG - BlogContent Load (0.3ms) SELECT `blog_contents`.* FROM `blog_contents` WHERE `blog_contents`.`id` = 29 LIMIT 1
72 | DEBUG - BlogContent Load (0.2ms) SELECT `blog_contents`.* FROM `blog_contents` WHERE `blog_contents`.`id` = 28 LIMIT 1
73 | DEBUG - BlogContent Load (1.3ms) SELECT `blog_contents`.* FROM `blog_contents` WHERE `blog_contents`.`id` = 27 LIMIT 1
74 | ......
75 | DEBUG - BlogContent Load (0.9ms) SELECT `blog_contents`.* FROM `blog_contents` WHERE `blog_contents`.`id` = 10 LIMIT 1
76 |
77 | 但是主键查询SQL不会造成表的扫描,而且往往已经被数据库buffer缓存,所以基本不会发生数据库服务器的磁盘IO,因而总体的数据库IO负载会远远小于前者的多表联合查询。特别是当使用对象缓存之后,会缓存所有主键查询语句,这20条SQL语句往往并不会全部发生,特别是热点数据,缓存命中率很高:
78 |
79 | DEBUG - Cache read: robbin/blog/29/1
80 | DEBUG - Cache read: robbin/account/1/0
81 | DEBUG - Cache read: robbin/blogcontent/29/0
82 | DEBUG - Cache read: robbin/account/1/0
83 | DEBUG - Cache read: robbin/blog/28/1
84 | ......
85 | DEBUG - Cache read: robbin/blogcontent/11/0
86 | DEBUG - Cache read: robbin/account/1/0
87 | DEBUG - Cache read: robbin/blog/10/1
88 | DEBUG - Cache read: robbin/blogcontent/10/0
89 | DEBUG - Cache read: robbin/account/1/0
90 |
91 | 拆分n+1条查询的方式,看起来似乎非常违反大家的直觉,但实际上这是真理,我实践经验证明:数据库服务器的瓶颈往往是磁盘IO,而不是SQL并发数量。因此 _拆分n+1条查询本质上是以增加n条SQL语句为代价,简化复杂SQL,换取数据库服务器磁盘IO的降低_ 当然这样做以后,对于ORM来说,有额外的好处,就是可以高效的使用缓存了。
92 |
93 | ## 按照column拆表实现细粒度对象缓存
94 |
95 | 数据库的瓶颈往往在磁盘IO上,所以应该尽量避免对大表的扫描。传统的拆表是按照row去拆分,保持表的体积不会过大,但是缺点是造成应用代码复杂度很高;使用ORM缓存的办法,则是按照column进行拆表,原则一般是:
96 |
97 | * 将大字段拆分出来,放在一个单独的表里面,表只有主键和大字段,外键放在主表当中
98 | * 将不参与where条件和统计查询的字段拆分出来,放在独立的表中,外键放在主表当中
99 |
100 | _按照column拆表本质上是一个去关系化的过程。主表只保留参与关系运算的字段,将非关系型的字段剥离到关联表当中,关联表仅允许主键查询,以Key-Value DB的方式来访问。因此这种缓存设计模式本质上是一种SQLDB和NoSQLDB的混合架构设计_
101 |
102 | 下面看一个实际的例子:文章的内容content字段是一个大字段,该字段不能放在blogs表中,否则会造成blogs表过大,表扫描造成较多的磁盘IO。我实际做法是创建blog_contents表,保存content字段,schema简化定义如下:
103 |
104 | CREATE TABLE `blogs` (
105 | `id` int(11) NOT NULL AUTO_INCREMENT,
106 | `title` varchar(255) NOT NULL,
107 | `blog_content_id` int(11) NOT NULL,
108 | `content_updated_at` datetime DEFAULT NULL,
109 | PRIMARY KEY (`id`),
110 | );
111 |
112 | CREATE TABLE `blog_contents` (
113 | `id` int(11) NOT NULL AUTO_INCREMENT,
114 | `content` mediumtext NOT NULL,
115 | PRIMARY KEY (`id`)
116 | );
117 |
118 | blog_contents表只有content大字段,其外键保存到主表blogs的blog_content_id字段里面。
119 |
120 | model定义和相关的封装如下:
121 |
122 | class Blog < ActiveRecord::Base
123 | acts_as_cached
124 | delegate :content, :to => :blog_content, :allow_nil => true
125 |
126 | def content=(value)
127 | self.blog_content ||= BlogContent.new
128 | self.blog_content.content = value
129 | self.content_updated_at = Time.now
130 | end
131 | end
132 |
133 | class BlogContent < ActiveRecord::Base
134 | acts_as_cached
135 | validates :content, :presence => true
136 | end
137 |
138 | 在Blog类上定义了虚拟属性content,当访问`blog.content`的时候,实际上会发生一条主键查询的SQL语句,获取`blog_content.content`内容。由于BlogContent上面定义了对象缓存`acts_as_cached`,只要被访问过一次,content内容就会被缓存到memcached里面。
139 |
140 | 这种缓存技术实际会非常有效,因为: _只要缓存足够大,所有文章内容可以全部被加载到缓存当中,无论文章内容表有多么大,你都不需要再访问数据库了_ 更进一步的是: _这张大表你永远都只需要通过主键进行访问,绝无可能出现表扫描的状况_ 为何当数据量大到9000万条记录以后,我们的系统仍然能够保持良好的性能,秘密就在于此。
141 |
142 | 还有一点非常重要: _使用以上两种对象缓存的设计模式,你除了需要添加一条缓存声明语句acts_as_cached以外,不需要显式编写一行代码_ 有效利用缓存的代价如此之低,何乐而不为呢?
143 |
144 | 以上两种缓存设计模式都不需要显式编写缓存代码,以下的缓存设计模式则需要编写少量的缓存代码,不过代码的增加量非常少。
145 |
146 | ## 写一致性缓存
147 |
148 | 写一致性缓存,叫做write-through cache,是一个CPU Cache借鉴过来的概念,意思是说,当数据库记录被修改以后,同时更新缓存,不必进行额外的缓存过期处理操作。但在应用系统中,我们需要一点技巧来实现写一致性缓存。来看一个例子:
149 |
150 | 我的网站文章原文是markdown格式的,当页面显示的时候,需要转换成html的页面,这个转换过程本身是非常消耗CPU的,我使用的是Github的markdown的库。Github为了提高性能,用C写了转换库,但如果是非常大的文章,仍然是一个耗时的过程,Ruby应用服务器的负载就会比较高。
151 |
152 | 我的解决办法是缓存markdown原文转换好的html页面的内容,这样当再次访问该页面的时候,就不必再次转换了,直接从缓存当中取出已经缓存好的页面内容即可,极大提升了系统性能。我的网站文章最终页的代码执行时间开销往往小于10ms,就是这个原因。代码如下:
153 |
154 | def md_content # cached markdown format blog content
155 | APP_CACHE.fetch(content_cache_key) { GitHub::Markdown.to_html(content, :gfm) }
156 | end
157 |
158 | 这里存在一个如何进行缓存过期的问题,当文章内容被修改以后,应该更新缓存内容,让老的缓存过期,否则就会出现数据不一致的现象。进行缓存过期处理是比较麻烦的,我们可以利用一个技巧来实现自动缓存过期:
159 |
160 | def content_cache_key
161 | "#{CACHE_PREFIX}/blog_content/#{self.id}/#{content_updated_at.to_i}"
162 | end
163 |
164 | 当构造缓存对象的key的时候,我用文章内容被更新的时间来构造key值,这个文章内容更新时间用的是blogs表的content_updated_at字段,当文章被更新的时候,blogs表会进行update,更新该字段。因此每当文章内容被更新,缓存的页面内容的key就会改变,应用程序下次访问文章页面的时候,缓存就会失效,于是重新调用`GitHub::Markdown.to_html(content, :gfm)`生成新的页面内容。 而老的页面缓存内容再也不会被应用程序存取,根据memcached的LRU算法,当缓存填满之后,将被优先剔除。
165 |
166 | 除了文章内容缓存之外,文章的评论内容转换成html以后也使用了这种缓存设计模式。具体可以看相应的源代码:[blog_comment.rb](https://github.com/robbin/robbin_site/blob/master/models/blog_comment.rb)
167 |
168 | ## 片段缓存和过期处理
169 |
170 | Web应用当中有大量的并非实时更新的数据,这些数据都可以使用缓存,避免每次存取的时候都进行数据库查询和运算。这种片段缓存的应用场景很多,例如:
171 |
172 | * 展示网站的Tag分类统计(只要没有更新文章分类,或者发布新文章,缓存一直有效)
173 | * 输出网站RSS(只要没有发新文章,缓存一直有效)
174 | * 网站右侧栏(如果没有新的评论或者发布新文章,则在一段时间例如一天内基本不需要更新)
175 |
176 | 以上应用场景都可以使用缓存,代码示例:
177 |
178 | def self.cached_tag_cloud
179 | APP_CACHE.fetch("#{CACHE_PREFIX}/blog_tags/tag_cloud") do
180 | self.tag_counts.sort_by(&:count).reverse
181 | end
182 | end
183 |
184 | 对全站文章的Tag云进行查询,对查询结果进行缓存
185 |
186 | <% cache("#{CACHE_PREFIX}/layout/right", :expires_in => 1.day) do %>
187 |
188 |
189 | <% Blog.cached_tag_cloud.select {|t| t.count > 2}.each do |tag| %>
190 | <%= link_to "#{tag.name}#{tag.count}".html_safe, url(:blog, :tag, :name => tag.name) %>
191 | <% end %>
192 |
193 | ......
194 | <% end %>
195 |
196 | 对全站右侧栏页面进行缓存,过期时间是1天。
197 |
198 | 缓存的过期处理往往是比较麻烦的事情,但在ORM框架当中,我们可以利用model对象的回调,很容易实现缓存过期处理。我们的缓存都是和文章,以及评论相关的,所以可以直接注册Blog类和BlogComment类的回调接口,声明当对象被保存或者删除的时候调用删除方法:
199 |
200 | class Blog < ActiveRecord::Base
201 | acts_as_cached
202 | after_save :clean_cache
203 | before_destroy :clean_cache
204 | def clean_cache
205 | APP_CACHE.delete("#{CACHE_PREFIX}/blog_tags/tag_cloud") # clean tag_cloud
206 | APP_CACHE.delete("#{CACHE_PREFIX}/rss/all") # clean rss cache
207 | APP_CACHE.delete("#{CACHE_PREFIX}/layout/right") # clean layout right column cache in _right.erb
208 | end
209 | end
210 |
211 | class BlogComment < ActiveRecord::Base
212 | acts_as_cached
213 | after_save :clean_cache
214 | before_destroy :clean_cache
215 | def clean_cache
216 | APP_CACHE.delete("#{CACHE_PREFIX}/layout/right") # clean layout right column cache in _right.erb
217 | end
218 | end
219 |
220 | 在Blog对象的`after_save`和`before_destroy`上注册`clean_cache`方法,当文章被修改或者删除的时候,删除以上缓存内容。总之,可以利用ORM对象的回调接口进行缓存过期处理,而不需要到处写缓存清理代码。
221 |
222 | ## 对象写入缓存
223 |
224 | 我们通常说到缓存,总是认为缓存是提升应用读取性能的,其实缓存也可以有效的提升应用的写入性能。我们看一个常见的应用场景:记录文章点击次数这个功能。
225 |
226 | 文章点击次数需要每次访问文章页面的时候,都要更新文章的点击次数字段view_count,然后文章必须实时显示文章的点击次数,因此常见的读缓存模式完全无效了。每次访问都必须更新数据库,当访问量很大以后数据库是吃不消的,因此我们必须同时做到两点:
227 |
228 | * 每次文章页面被访问,都要实时更新文章的点击次数,并且显示出来
229 | * 不能每次文章页面被访问,都更新数据库,否则数据库吃不消
230 |
231 | 对付这种应用场景,我们可以利用对象缓存的不一致,来实现对象写入缓存。原理就是每次页面展示的时候,只更新缓存中的对象,页面显示的时候优先读取缓存,但是不更新数据库,让缓存保持不一致,积累到n次,直接更新一次数据库,但绕过缓存过期操作。具体的做法可以参考[blog.rb](https://github.com/robbin/robbin_site/blob/master/models/blog.rb) :
232 |
233 | # blog viewer hit counter
234 | def increment_view_count
235 | increment(:view_count) # add view_count += 1
236 | write_second_level_cache # update cache per hit, but do not touch db
237 | # update db per 10 hits
238 | self.class.update_all({:view_count => view_count}, :id => id) if view_count % 10 == 0
239 | end
240 |
241 | `increment(:view_count)`增加view_count计数,关键代码是第2行`write_second_level_cache`,更新view_count之后直接写入缓存,但不更新数据库。累计10次点击,再更新一次数据库相应的字段。另外还要注意,如果blog对象不是通过主键查询,而是通过查询语句构造的,要优先读取一次缓存,保证页面点击次数的显示一致性,因此 [_blog.erb](https://github.com/robbin/robbin_site/blob/master/app/views/blog/_blog.erb) 这个页面模版文件开头有这样一段代码:
242 |
243 | <%
244 | # read view_count from model cache if model has been cached.
245 | view_count = blog.view_count
246 | if b = Blog.read_second_level_cache(blog.id)
247 | view_count = b.view_count
248 | end
249 | %>
250 |
251 | 采用对象写入缓存的设计模式,就可以非常容易的实现写入操作的缓存,在这个例子当中,我们仅仅增加了一行缓存写入代码,而这个时间开销大约是1ms,就可以实现文章实时点击计数功能,是不是非常简单和巧妙?实际上我们也可以使用这种设计模式实现很多数据库写入的缓存功能。
252 |
253 | 常用的ORM缓存设计模式就是以上的几种,本质上都是非常简单的编程技巧,代码的增加量和复杂度也非常低,只需要很少的代码就可以实现,但是在实际应用当中,特别是当数据量很庞大,访问量很高的时候,可以发挥惊人的效果。我们实际的系统当中,缓存命中次数:SQL查询语句,一般都是5:1左右,即每次向数据库查询一条SQL,都会在缓存当中命中5次,数据主要都是从缓存当中得到,而非来自于数据库了。
254 |
255 | ## 其他缓存的使用技巧
256 |
257 | 还有一些并非ORM特有的缓存设计模式,但是在Web应用当中也比较常见,简单提及一下:
258 |
259 | ### 用数据库来实现的缓存
260 |
261 | 在我这个网站当中,每篇文章都标记了若干tag,而tag关联关系都是保存到数据库里面的,如果每次显示文章,都需要额外查询关联表获取tag,显然会非常消耗数据库。在我使用的`acts-as-taggable-on`插件中,它在blogs表当中添加了一个`cached_tag_list`字段,保存了该文章标记的tag。当文章被修改的时候,会自动相应更新该字段,避免了每次显示文章的时候都需要去查询关联表的开销。
262 |
263 | ### HTTP客户端缓存
264 |
265 | 基于资源协议实现的HTTP客户端缓存也是一种非常有效的缓存设计模式,我在2009年写过一篇文章详细的讲解了:[基于资源的HTTP Cache的实现介绍](http://robbinfan.com/blog/13/http-cache-implement) ,所以这里就不再复述了。
266 |
267 | ### 用缓存实现计数器功能
268 |
269 | 这种设计模式有点类似于对象写入缓存,利用缓存写入的低开销来实现高性能计数器。举一个例子:用户登录为了避免遭遇密码暴力破解,我限定了每小时每IP只能尝试登录5次,如果超过5次,拒绝该IP再次尝试登录。代码实现很简单,如下:
270 |
271 | post :login, :map => '/login' do
272 | login_tries = APP_CACHE.read("#{CACHE_PREFIX}/login_counter/#{request.ip}")
273 | halt 403 if login_tries && login_tries.to_i > 5 # reject ip if login tries is over 5 times
274 | @account = Account.new(params[:account])
275 | if login_account = Account.authenticate(@account.email, @account.password)
276 | session[:account_id] = login_account.id
277 | redirect url(:index)
278 | else
279 | # retry 5 times per one hour
280 | APP_CACHE.increment("#{CACHE_PREFIX}/login_counter/#{request.ip}", 1, :expires_in => 1.hour)
281 | render 'home/login'
282 | end
283 | end
284 |
285 | 等用户POST提交登录信息之后,先从缓存当中取该IP尝试登录次数,如果大于5次,直接拒绝掉;如果不足5次,而且登录失败,计数加1,显示再次尝试登录页面。
286 |
--------------------------------------------------------------------------------
/public/stylesheets/default/content.css:
--------------------------------------------------------------------------------
1 | @charset "utf-8";
2 |
3 | /*heard*/
4 | body{ background:#bebebe url(bg.gif);}
5 | .wrap{ max-width:1000px;margin:0 auto; }
6 | .header{ height:170px; background:#4c7298 url(hd_bg.gif);}
7 | .header .wrap{}
8 | .header .user_img{ float:left; margin:60px 20px 0 0; }
9 | .header .user_img img{border-left:none; border-radius:50px;-webkit-border-radius:50px;-moz-border-radius:50px;-khtml-border-radius:50px;}
10 | .header .blog_title{color:#fff; font-weight:bold; font-size:35px; line-height:35px; padding-top:60px;}
11 | .header .blog_motto{ color:#fff; font-size:18px; font-weight:bold; }
12 | .header .write{ float:right; color:#fff; text-align:right;}
13 | .header .write a{color:#fff; margin-left:8px; margin-right:8px; display:inline-block; height:20px; line-height:20px; vertical-align:middle; background:url(y.png) 0px -240px no-repeat; padding-left:16px;}
14 | .header .write a:hover{background:url(y.png) 0px -270px no-repeat; color:#f87b00;}
15 | .header .write input{ background:none; border:none; color:#fff;background: url(y.png) 0px -716px no-repeat; padding-left:20px;}
16 | .header .write input:hover{ color:#f60;background: url(y.png) 0px -746px no-repeat; text-decoration:underline; cursor:pointer;}
17 | .header .write form{ display:inline;}
18 | .header .write .admin{background:url(y.png) -3px -980px no-repeat;}
19 | .header .write .admin:hover{background:url(y.png) -3px -1010px no-repeat;}
20 | /*nav*/
21 | .nav{ font-size:14px; background:url(x.png) 0 0 ; height:53px;}
22 | .nav li{ float:left; }
23 | .nav a{display:block; font-size:16px; height:46px; width:110px; margin-right:10px; text-align:center; line-height:53px;color:#666;}
24 | .nav a:hover{ border-bottom:3px solid #69c ; color:#333; text-decoration:none;}
25 | .nav .current{ border-bottom:3px solid #69c ; color:#333; font-weight:bold;}
26 | .nav .search{ height:45px; /*background:url(x.png) 0 -60px;*/ padding-top:8px;}
27 | .nav input{ background:#fff; border:1px #ccc solid; border-right:none; height:30px; border-radius: 5px 0 0 5px;-webkit-border-radius: 5px 0 0 5px;-moz-border-radius: 5px 0 0 5px;-khtml-border-radius: 5px 0 0 5px; padding:0 10px; height:30px; width:189px; float:left;}
28 | .nav button{ background:#fff url(y.png) 3px 5px no-repeat; border:1px #ccc solid; border-left:none; border-radius:0 5px 5px 0;-webkit-border-radius:0 5px 5px 0;-moz-border-radius:0 5px 5px 0 ;-khtml-border-radius:0 5px 5px 0; padding:0; margin:0; width:30px; height:32px;}
29 | .nav button:hover{ background:#fff url(y.png) 3px -35px no-repeat;}
30 |
31 | /*left*/
32 | .left{ width:720px; margin-right:20px; float:left; padding-top:20px;}
33 | .left h3{font-size:26px;margin-bottom: 10px;font-weight: bold;color: #fff; text-shadow:#999 1px 1px 0;}
34 | .left h3 span{ color:#666; text-shadow:none;}
35 | .left h3 a.tag span{color: #B77B1B;}
36 | .left h3 a.tag:hover span{color: #fff;}
37 | .publish a.tag,
38 | .left a.tag {display: inline-block;width: auto;height: 19px;overflow: hidden;padding: 0 0 0 20px;background: url(x.png) no-repeat 0 -130px;color: #B77B1B;line-height: 19px;font-weight: normal;font-size: 12px;text-decoration: none;cursor: pointer;vertical-align: middle; margin-right:8px;}
39 | .publish a.tag span,
40 | .left a.tag span {display: inline-block;width: auto;height: 19px;overflow: hidden;padding: 0 10px 0 0;background: url(x.png) no-repeat right -160px;}
41 | .publish a.tag:hover,
42 | .left a.tag:hover{background-position:0 -190px;color:#fff;text-decoration:none}
43 | .publish a.tag:hover span,
44 | .left a.tag:hover span{background-position:right -220px}
45 |
46 | .con{border-radius:5px; -webkit-border-radius:5px;-moz-border-radius:5px;-khtml-border-radius:5px; background:#fff; margin-bottom:40px;box-shadow: 0px 1px 2px #999;}
47 | .con .tit{ border-bottom:1px solid #eee; padding:20px 40px 15px 40px; font-size:22px; font-weight:bold;}
48 | .con em{padding:0 3px; margin:0 3px; background:#369; color:#fff;}
49 | .con .lost_search{padding:140px 40px 140px 160px; font-size:16px; min-height:100px; background:url(search.png) no-repeat 80px 115px ;}
50 | .tit .note_icon,
51 | .tit .blog_icon{ display:block; float:left; width:24px; height:24px;background:url(x.png) no-repeat; margin-right:10px;}
52 | .tit .note_icon{ background-position:0 -353px;}
53 | .tit .blog_icon{ background-position:0 -393px;}
54 | .tit .edit,
55 | .tit .markdown,
56 | .tit .del{ display:block; float:right; width:24px; height:24px; margin-left:20px; background:url(y.png) no-repeat;}
57 | .tit .edit{ background-position:2px -78px;}
58 | .tit .del{background-position:2px -158px;}
59 | .tit .markdown{ background-position:0 -856px;}
60 | .tit .edit:hover{ background-position:2px -118px;}
61 | .tit .del:hover{background-position:2px -198px;}
62 | .tit .markdown:hover{ background-position:0 -886px;}
63 |
64 | .text{padding:20px 40px; font-size:14px; min-height:120px;}
65 | .con .text .infor_tag{margin-top: -10px;margin-bottom: 10px;}
66 | .text .img{ margin-bottom:10px; text-align:center;}
67 | .text img{ max-width:600px;}
68 | .note_edit{ background:url(y.png) no-repeat 0 -388px;; color:#999; padding-left:18px; margin:20px 0 -20px 40px; color:#f60;}
69 |
70 | .con .info{ background:#f7f7f7; border-top:1px solid #eee; padding:10px 40px 10px 40px;border-radius: 0 0 5px 5px;-webkit-border-radius: 0 0 5px 5px;-moz-border-radius: 0 0 5px 5px;-khtml-border-radius: 0 0 5px 5px;}
71 | .con .info .author,
72 | .con .info .time,
73 | .con .info .edit,
74 | .con .info .views,
75 | .con .info .comment{ background:url(y.png) no-repeat; color:#999; margin-left:5px; padding-left:18px;}
76 | .con .info .author{ background-position:0 -923px;}
77 | .con .info a.author:hover{ background-position:0 -953px;}
78 | .con .info .time{ background-position:0 -303px;}
79 | .con .info .edit{ background-position:0 -363px;}
80 | .con .info .views{ background-position:0 -483px;}
81 | .con .info .comment{ background-position:0 -423px; padding:0 0 0 18px;}
82 | .con .info .comment:hover{ background-position:0 -453px; color:#f60;}
83 |
84 | /*right*/
85 | .right{ width:200px; float:right; background:#fff url(x.png) repeat-x 0 -995px; margin-top:-5px; padding:20px; margin-bottom:20px;border-radius: 0 0 5px 5px;-webkit-border-radius: 0 0 5px 5px;-moz-border-radius: 0 0 5px 5px;-khtml-border-radius: 0 0 5px 5px; box-shadow: 0px 1px 2px #999;}
86 | .right h3{ font-size:18px; margin-bottom:10px; padding-bottom:2px; font-weight:bold; color:#666;border-bottom: 2px #ddd solid;}
87 | .right ul{ margin-bottom:20px;}
88 | .right .tag { font-size:14px; line-height:16px; margin-bottom:30px;}
89 | .right .tag a{color: #999;display: block;border: 1px solid #DDD;padding: 6px 8px;margin-bottom: 5px;font-weight: 400;overflow: hidden;background-color: #F9F9F9;border-radius: 5px;-moz-border-radius: 5px;-webkit-border-radius: 5px;box-shadow: inset 0 1px white,0 0 1px rgba(34, 25, 25, .1);-moz-box-shadow: inset 0 1px #fff,0 0 1px rgba(34,25,25,.1);-webkit-box-shadow: inset 0 1px white,0 0 1px rgba(34, 25, 25, .1);}
90 | .right .tag a:hover{color:#666;text-decoration:none;background-color:#fff}
91 | .right .tag a span {border-left: 1px solid #DDD;display: block;float: right;margin: -5px 0 -5px 5px;padding: 6px 5px;text-align: center;width: 20px;}
92 | .right .tag a span:hover{color:#666;text-decoration:none;background-color:#fff}
93 | .right .rss a{ display:block; background:url(x.png) 0 -260px no-repeat; height:30px; line-height:30px; border:none; margin-right:5px; font-size:18px; padding-left:40px; font-weight:bold;}
94 | .right .rss a:hover{ background-position: 0px -310px; }
95 | .right .rss input{ padding-left:5px;}
96 | .right .hot_blog,
97 | .right .hot_note{ line-height:18px; margin-bottom:30px;}
98 | .right .hot_blog a,
99 | .right .hot_note a{display:block; margin-bottom:18px;}
100 | .right .comment li{border-bottom:1px dotted #ddd; margin-bottom:15px; padding-bottom:5px; }
101 | .right .comment a{color:#369;}
102 | .right .comment{word-break: break-all;}
103 | .right .user_name{ color:#666;}
104 |
105 | /*footer*/
106 | .footer{ text-align:center; background:#fff; height: 70px; padding-top:30px; box-shadow: 0px -1px 1px #aaa;}
107 | .footer a{ display:inline-block; height:20px; font-size:14px; color:#999; font-weight:bold;}
108 | .footer .weibo,
109 | .footer .github,
110 | .footer .mail{ background:url(y.png) no-repeat; padding-left:25px; margin-right:20px;}
111 | .footer .weibo{ background-position:0 -540px;}
112 | .footer .weibo:hover{background-position:0 -570px; color:#f60;}
113 | .footer .github{ background-position:0 -785px;}
114 | .footer .github:hover{ background-position:0 -825px; color:#f60}
115 | .footer .mail{ background-position:0 -598px;}
116 | .footer .mail:hover{ background-position:0 -628px; color:#f60;}
117 | .footer .iteye{background:url(x.png) no-repeat 0 -310px; padding-left:45px; }
118 | .footer .iteye:hover{ background-position:0 -340px; color:#f60;}
119 | .footer .certification a{ font-weight:100; font-size:12px; color:#666;}
120 |
121 | /*page_nav*/
122 | .page_nav a{ width:50px; height:34px; font-size:18px; font-weight:bold; line-height:34px; padding:0 10px; background:#ddd url(x.png) no-repeat; color:#fff; box-shadow: 0px 1px 1px #aaa; border-radius:3px;-webkit-border-radius: 3px;-moz-border-radius: 3px;-khtml-border-radius: 3px; margin:-20px 0 30px 0;}
123 | .page_nav a:hover{ background:#f60 url(x.png) no-repeat; text-decoration:none; display:block;}
124 | .page_nav .next{ float:right; background-position:65px -393px; padding-right:40px; text-align:right;}
125 | .page_nav .next:hover{ background-position:65px -393px;}
126 | .page_nav .prev{ float:left;background-position:13px -363px; padding-left:40px;}
127 | .page_nav .prev:hover{ background-position:13px -363px;}
128 | /*content*/
129 | .con .content h1 {font-weight:bold;}
130 | .con .content p{ margin-bottom:20px;}
131 | .con .content .tit{ border:none; padding-top:30px; padding-bottom:0;}
132 | .con .content .tran{ display:inline-block; background:url(x.png) no-repeat 0 -642px; width:30px; height:30px; vertical-align:middle; margin-right:5px;}
133 | .con .content .text{ padding-top:0px; padding-bottom:10px;}
134 | .cont_tag{ margin-bottom:10px;}
135 | .con .content .text p a{ color:#f60;}
136 | .con .content .info{ background:none; border:none; margin-top:-10px;}
137 | .share{ padding:0 40px;}
138 | .share span{ float:right; margin-left:10px;}
139 | #blog_content_preview{ display:none; width:620px;box-shadow: 0px 0px 0px #999; border:1px #ccc solid; margin-bottom:10px;padding:35px;}
140 | #blog_content_preview td{ width:auto;}
141 | .blog_content_editor_tab{ background:url(piont_ddd.png) repeat-x bottom; margin-bottom:10px; width:690px; }
142 | .blog_content_editor_tab a{ display: block; float:left; border:1px #ddd solid; border-top:none; height:30px; width:80px; text-align:center; line-height:32px;margin-right:10px;background:url(piont_ddd.png) repeat-x top;}
143 | .blog_content_editor_tab a:hover{ text-decoration:none; color:#666; background:url(piont_69c.png) repeat-x top; }
144 | .blog_content_editor_tab a.current{ border-bottom:1px #fff solid; background:url(piont_69c.png) repeat-x top;}
145 | /*publish*/
146 | .publish{ margin-top:20px;}
147 | .publish .con{ padding:40px;}
148 | .publish .content{}
149 | .publish .content th{ width:100px; text-align:right; padding:5px 20px 5px 0; font-size:16px; font-weight:bold;}
150 | .publish .content td{ width:800px; padding:5px 0;}
151 | .publish input{ width: 680px; border:1px #ccc solid; height:28px; border-radius: 5px;-webkit-border-radius: 5px;-moz-border-radius: 5px;-khtml-border-radius: 5px; padding:0 5px;}
152 | .publish input.submit{ width:110px; height:40px; border:none;border-radius:0;-webkit-border-radius:0;-moz-border-radius: 0px;-khtml-border-radius: 0px;}
153 | .publish textarea{ height:500px; width: 670px; padding:10px; border:1px #ccc solid; border-radius: 5px;-webkit-border-radius: 5px;-moz-border-radius: 5px;-khtml-border-radius: 5px; font-size:14px; line-height:1.6; outline: none;}
154 | .publish label{ vertical-align:middle; margin-right:20px;}
155 | .publish label input{ vertical-align:middle; width:auto;}
156 | .publish input.title{ font-size:14px;}
157 | .publish div.tag{ margin:5px 0;}
158 | .submit{ background:url(x.png) no-repeat 0 -440px; border:none; width:110px; height:40px; margin-left:-100px;}
159 | .submit:hover{background:url(x.png) no-repeat 0 -490px; cursor:pointer;}
160 | /*comment*/
161 | .con .comment{ padding:20px 40px; }
162 | .con .comment h1{ font-size:20px;font-weight:bold;}
163 | .con .comment h2{ font-size:18px;font-weight:bold; border-bottom:2px #ddd solid; padding-bottom:3px;margin-bottom:10px; }
164 | .con .comment h2 span{ font-size:22px; margin-right:5px;}
165 | .con .comment h3 {font-size: 16px; font-weight:bold;color: #666;text-shadow:none;}
166 | .con .comment h4{font-size:14px; font-weight:bold; margin-bottom:10px;}
167 | .con .comment .user_img{ float:left; margin-right:20px; width:40px; height:40px;}
168 | .con .comment .user_img img{ width:40px; height:40px; border-radius:50px;-webkit-border-radius:50px;-moz-border-radius:50px;-khtml-border-radius:50px;}
169 | .con .comment .cot_con{ float:left; width:580px;}
170 | .con .comment .cot_con .info{ padding:0; border:none; background:none;}
171 | .con .comment .cot_con .info .user_name{ font-size:14px; font-weight:bold;}
172 | .con .comment ul li{ border-bottom:1px #eee solid; padding:15px 0;}
173 | .con .comment ul li:last-child { border-bottom: 0; margin-bottom:20px;}
174 | .con .comment textarea{ height:140px; width: 615px; border:1px #ccc solid; border-radius: 5px;-webkit-border-radius: 5px;-moz-border-radius: 5px;-khtml-border-radius: 5px; padding:5px 10px; font-size:14px;outline: none;}
175 | .con .comment blockquote {border-left: 4px solid #dddddd;padding: 0 15px;color: #777777; }
176 | .con .comment blockquote > :first-child { margin-top: 0; }
177 | .con .comment blockquote > :last-child { margin-bottom: 0; }
178 | .con .comment pre code{word-wrap: break-word;word-break: break-all;}
179 | .con .comment .editor_switcher {height:25px;margin-bottom:6px;background:url(piont_ddd.png) repeat-x bottom;}
180 | .con .comment .editor_switcher ul {margin:0;padding:0;list-style:none;width:200px;font-weight:bold;}
181 | .con .comment .editor_switcher ul li {margin:0 5px;padding:0;list-style:none;float:left;border:1px #ddd solid; border-top:none; width:80px; text-align:center; margin-right:10px;background:url(piont_ddd.png) repeat-x top;}
182 | .con .comment .editor_switcher li:hover{ color:#666; background:url(piont_69c.png) repeat-x top; }
183 | .con .comment .editor_switcher li.current{ border-bottom:1px #fff solid; background:url(piont_69c.png) repeat-x top;}
184 | .con .comment .editor_switcher li a {text-decoration:none;}
185 | .con .comment #comment_preview {display:none;margin-bottom:10px;}
186 | .relative #comment_preview{ border:1px #ddd solid;box-shadow:none; padding:5px 10px; min-height:140px;}
187 |
188 | .comment_btn{background:url(x.png) no-repeat 0 -540px; border:none; width:110px; height:40px; float:right; border-radius: 0px;-webkit-border-radius: 0px;-moz-border-radius: 0px;-khtml-border-radius: 0px; margin-top:10px;}
189 | .comment_btn:hover{background:url(x.png) no-repeat 0 -590px; cursor:pointer;}
190 | .quote_comment{ display:inline-block; width:16px; height:16px; background:url(y.png) no-repeat 0 -1039px; float:right;}
191 | .quote_comment:hover{ background-position:0 -1069px;}
192 | .delete{ display:inline-block; width:16px; height:16px; background:url(y.png) no-repeat 0 -660px; float:right; margin-left:5px; }
193 | .delete:hover{ background-position:0 -690px;}
194 |
195 |
196 | /*tag_list*/
197 | .tag_list{ padding:30px 40px; min-height:300px;}
198 | .tag_list li{ float:left; width:200px; margin-right:10px; margin-bottom:10px;}
199 |
200 | .tab_list{height:34px; padding-left:10px;}
201 | .tab_list {list-style:none;width:720px;height:34px;margin:0 auto;}
202 | .tab_list li{background:url(header_tabs.png) repeat-x;background-position:214px 0;display:block;float:left;width:215px;height:34px;cursor:pointer;font-size:18px; font-weight:bold;color:#aaa;line-height:34px;text-align:center;}
203 | .tab_list li.current {color:#666;}
204 | .tab_list li.current {background-position: 0 0;}
205 | .tab_list li:hover{background-position: 0 0; color:#666;}
206 |
207 | .box{border-radius:5px; -webkit-border-radius:5px;-moz-border-radius:5px;-khtml-border-radius:5px; background:#fefefe; width:360px; padding:30px;color:#333; text-align:center; font-size:14px; top:-130px; right:-100px;box-shadow: 0px 0px 8px #999; border:3px #ccc solid \9; }
208 | .close{ width:30px; height:30px; background:url(y.png) 5px -655px no-repeat; position:absolute; right:0; top:0; border:none;}
209 | .close:hover{ background-position:5px -685px;}
210 | .box p{ margin-bottom:20px;}
211 | .box button.google_btn,
212 | .box button.sina_btn{ background: url(x.png); border:none; width:110px; height:40px;}
213 | .box button.google_btn{ background-position:0 -690px; margin-right:10px}
214 | .box button.sina_btn{ background-position:0 -740px;}
215 | .box button.sina_btn:hover{ background-position:0 -940px;}
216 |
217 | /*page*/
218 | .pagination{ margin-bottom:30px; margin-top:-15px; text-align:center;}
219 | .pagination .previous_page,
220 | .pagination a,
221 | .pagination em,
222 | .pagination .next_page{ background:#fff; display: inline-block; padding:0px 8px; margin:0 2px;border-radius: 2px;-webkit-border-radius: 2px;-moz-border-radius: 2px;-khtml-border-radius: 2px;box-shadow: 0px 1px 2px #999;}
223 | .pagination .previous_page:hover,
224 | .pagination a:hover,
225 | .pagination .next_page:hover{ background:#eee;}
226 | .pagination em{ color:#f60;}
227 | .pagination .disabled{ background:#ddd;}
228 | .pagination .disabled:hover{ background:#ddd;}
229 |
230 | /*login*/
231 | .login{ margin-top:20px; height:200px; mborder-radius: 5px;-webkit-border-radius: 5px;-moz-border-radius: 5px;-khtml-border-radius: 5px;background: #fff;margin-bottom: 40px;box-shadow: 0px 1px 2px #999; padding:100px 300px; text-align:center;}
232 | .login label{ display:inline-block; width:90px; text-align:right; padding-right:10px;}
233 | .login p{ margin:10px;}
234 | .login input{ width:220px; padding:0 5px; vertical-align:middle;}
235 | .login input.login{ background:#ccc; height:40px; width:120px; padding:0; margin:0; background:url(x.png) no-repeat 0 -790px; mborder-radius: 0px;-webkit-border-radius: 0px;-moz-border-radius: 0px;-khtml-border-radius: 0px;box-shadow: none; border:0; margin-left:110px; vertical-align:middle;}
236 | .login input.login:hover{ background-position:0 -840px;cursor:pointer;}
237 | .login a.sina_btn{ display:inline-block;background: url(x.png) 0 -740px no-repeat;border: none;width: 110px;height: 40px; margin-left:10px; vertical-align:middle;}
238 | .login a.sina_btn:hover{ background-position:0 -940px;}
239 | .remember_me{}
240 | .remember_me label{ width:auto; padding-right:30px;}
241 | .remember_me input{ width:20px;}
242 | .error_info{ margin-top:20px; height:200px; line-height:200px; mborder-radius: 5px;-webkit-border-radius: 5px;-moz-border-radius: 5px;-khtml-border-radius: 5px;background: #fff;margin-bottom: 40px;box-shadow: 0px 1px 2px #999; padding:100px 200px; font-size:40px; font-weight:bold; color:#69c;}
243 | .error_test{ padding-left:300px;}
244 | .error_401{ background:#fff url(401.png) no-repeat 100px center;}
245 | .error_403{ background:#fff url(403.png) no-repeat 100px center;}
246 | .error_404{ background:#fff url(404.png) no-repeat 230px center;}
247 | .error_500{ background:#fff url(500.png) no-repeat 100px center;}
248 |
249 | /*flash notice*/
250 | div#flash-notice{ width:100px;height:24px;text-align:center;position:fixed;background-color:red;color:#fff;top:0px;right:0px;display:none;}
251 | div#comment-error-info{ width:120px;height:24px;text-align:center;float:left;position:relative;background-color:red;color:#fff;display:none;}
252 | div#form-error-info{ width:100%;height:20px;text-align:left;display:block;color:red;}
253 | div#form-error-info span{padding:0 5px;}
254 |
255 | /* admin */
256 | table.admin caption{ text-align:center; font-size:16px;font:bold;margin-bottom:10px;}
257 | table.admin td {padding:5px 10px;}
258 | div.admin_menu {text-align:center;background-color:white;padding:10px;margin-top:10px;font-size:14px;}
259 | div.admin_menu a{margin:0 30px;}
260 | table.admin {padding: 0;border-collapse: collapse; margin:10px auto 25px auto; }
261 | table.admin tr {border-top: 1px solid #cccccc;background-color: white;margin: 0;padding: 0; }
262 | table.admin tr:nth-child(2n) {background-color: #f8f8f8; }
263 | table.admin tr th {font-weight: bold;border: 1px solid #cccccc;text-align: left;margin: 0;padding: 6px 13px; }
264 | table.admin tr td {border: 1px solid #cccccc;text-align: left;margin: 0;padding: 6px 13px; }
265 | table.admin tr:hover{ background:#eee;}
266 | table.admin tr th :first-child,
267 | table.admin tr td :first-child {margin-top: 0; }
268 | table.admin tr th :last-child,
269 | table.admin tr td :last-child {margin-bottom: 0; }
270 | table.edit_pro{padding: 0;border-collapse: collapse; margin:10px auto; }
271 | table.edit_pro caption{ text-align:center; font-size:16px;font:bold;margin-bottom:10px;}
272 | #account_logo{ border:none;}
273 |
274 | /************************************************************************************
275 | MEDIA QUERIES
276 | *************************************************************************************/
277 |
278 | /* for 980px or less */
279 | @media screen and (max-width: 980px) {
280 | .header{ height:100px;}
281 | .header .user_img{float: left;margin: 10px 20px 0 10px;}
282 | .header .blog_motto{ display:none;}
283 | .header .blog_title{padding-top: 40px;}
284 | .header .write{}
285 | .left {width:98%; margin:0 1%;}
286 | .right{ display:none;}
287 | .search{ display:none;}
288 | .con .comment textarea{ width:94%; padding:3%}
289 | .wrap{ width:98%;}
290 | .login{ padding:10% 10%; margin:10px auto; height:auto;}
291 |
292 | }
293 |
294 | @media screen and (max-width:750px) {
295 | .text img{max-width:100%;}
296 | .nav{ padding:0 3%}
297 | .nav li{ width:15%;}
298 | .nav a{ width:auto;}
299 | .left {width:98%; margin:0 1%;}
300 | .right{ display:none;}
301 | .search{ display:none;}
302 | .con .comment{ padding:20px;}
303 | .con .comment .user_img{width: 18%;margin-right: 5%;}
304 | .con .comment .cot_con{ width:77%;}
305 | .relative .box{ width:80%; padding:5%; right:-5%;}
306 | .text{word-break: break-all;word-wrap: break-word;}
307 | .left h3.category_title{ display:none;}
308 | }
309 |
310 | @media screen and (max-width: 480px) {
311 | .nav{ padding:0 20px;background: url(x.png) 0 0;height: 36px;}
312 | .nav a{ line-height:36px; height:33px;}
313 | .nav li{ width:20%;}
314 | .nav li:nth-child(4n){ display:none;}
315 | .header{ height:60px;}
316 | .header .user_img{ display:none;}
317 | .header .blog_title{ font-size:26px; margin-left:20px;padding-top: 15px;}
318 | .header .write{ float:none; margin-left:10px; text-align:left;}
319 | .header .write{ display:none;}
320 | .nav a{ font-size:14px; padding:0 3px;}
321 | .left h3{ margin-left:15px;}
322 | .con .tit{padding:15px 20px 10px 20px; font-size:18px;}
323 | .con .text .infor_tag{ margin-bottom:5px;}
324 | .text{padding:15px 20px;}
325 | .con{ margin-bottom:20px;}
326 | .con .info{ padding:15px 20px;}
327 | .con .info .author,
328 | .con .info .edit{ display:none;}
329 | .tit .markdown{ display:none;}
330 | .con .content .text{padding:0 20px 10px 20px;}
331 | .con .content .info{ margin-top:0;}
332 | .pagination{margin-top:0;}
333 | .login input.login{ margin-left:auto;}
334 | .con .info .views{ display:none}
335 | .con .text video{ width:100%; height:100%;}
336 | .con .comment #comment_preview{margin-top: 24px;}
337 | }
--------------------------------------------------------------------------------
/public/javascripts/highlight.min.js:
--------------------------------------------------------------------------------
1 | var hljs=new function(){function m(p){return p.replace(/&/gm,"&").replace(/"}while(y.length||w.length){var v=u().splice(0,1)[0];z+=m(x.substr(q,v.offset-q));q=v.offset;if(v.event=="start"){z+=t(v.node);s.push(v.node)}else{if(v.event=="stop"){var p,r=s.length;do{r--;p=s[r];z+=(""+p.nodeName.toLowerCase()+">")}while(p!=v.node);s.splice(r,1);while(r'+M[0]+""}else{r+=M[0]}O=P.lR.lastIndex;M=P.lR.exec(L)}return r+L.substr(O,L.length-O)}function J(L,M){if(M.sL&&e[M.sL]){var r=d(M.sL,L);x+=r.keyword_count;return r.value}else{return F(L,M)}}function I(M,r){var L=M.cN?'':"";if(M.rB){y+=L;M.buffer=""}else{if(M.eB){y+=m(r)+L;M.buffer=""}else{y+=L;M.buffer=r}}D.push(M);A+=M.r}function G(N,M,Q){var R=D[D.length-1];if(Q){y+=J(R.buffer+N,R);return false}var P=q(M,R);if(P){y+=J(R.buffer+N,R);I(P,M);return P.rB}var L=v(D.length-1,M);if(L){var O=R.cN?"":"";if(R.rE){y+=J(R.buffer+N,R)+O}else{if(R.eE){y+=J(R.buffer+N,R)+O+m(M)}else{y+=J(R.buffer+N+M,R)+O}}while(L>1){O=D[D.length-2].cN?"":"";y+=O;L--;D.length--}var r=D[D.length-1];D.length--;D[D.length-1].buffer="";if(r.starts){I(r.starts,"")}return R.rE}if(w(M,R)){throw"Illegal"}}var E=e[B];var D=[E.dM];var A=0;var x=0;var y="";try{var s,u=0;E.dM.buffer="";do{s=p(C,u);var t=G(s[0],s[1],s[2]);u+=s[0].length;if(!t){u+=s[1].length}}while(!s[2]);if(D.length>1){throw"Illegal"}return{r:A,keyword_count:x,value:y}}catch(H){if(H=="Illegal"){return{r:0,keyword_count:0,value:m(C)}}else{throw H}}}function g(t){var p={keyword_count:0,r:0,value:m(t)};var r=p;for(var q in e){if(!e.hasOwnProperty(q)){continue}var s=d(q,t);s.language=q;if(s.keyword_count+s.r>r.keyword_count+r.r){r=s}if(s.keyword_count+s.r>p.keyword_count+p.r){r=p;p=s}}if(r.language){p.second_best=r}return p}function i(r,q,p){if(q){r=r.replace(/^((<[^>]+>|\t)+)/gm,function(t,w,v,u){return w.replace(/\t/g,q)})}if(p){r=r.replace(/\n/g,"
")}return r}function n(t,w,r){var x=h(t,r);var v=a(t);var y,s;if(v=="no-highlight"){return}if(v){y=d(v,x)}else{y=g(x);v=y.language}var q=c(t);if(q.length){s=document.createElement("pre");s.innerHTML=y.value;y.value=k(q,c(s),x)}y.value=i(y.value,w,r);var u=t.className;if(!u.match("(\\s|^)(language-)?"+v+"(\\s|$)")){u=u?(u+" "+v):v}if(/MSIE [678]/.test(navigator.userAgent)&&t.tagName=="CODE"&&t.parentNode.tagName=="PRE"){s=t.parentNode;var p=document.createElement("div");p.innerHTML=""+y.value+"
";t=p.firstChild.firstChild;p.firstChild.cN=s.cN;s.parentNode.replaceChild(p.firstChild,s)}else{t.innerHTML=y.value}t.className=u;t.result={language:v,kw:y.keyword_count,re:y.r};if(y.second_best){t.second_best={language:y.second_best.language,kw:y.second_best.keyword_count,re:y.second_best.r}}}function o(){if(o.called){return}o.called=true;var r=document.getElementsByTagName("pre");for(var p=0;p|>=|>>|>>=|>>>|>>>=|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~";this.BE={b:"\\\\.",r:0};this.ASM={cN:"string",b:"'",e:"'",i:"\\n",c:[this.BE],r:0};this.QSM={cN:"string",b:'"',e:'"',i:"\\n",c:[this.BE],r:0};this.CLCM={cN:"comment",b:"//",e:"$"};this.CBLCLM={cN:"comment",b:"/\\*",e:"\\*/"};this.HCM={cN:"comment",b:"#",e:"$"};this.NM={cN:"number",b:this.NR,r:0};this.CNM={cN:"number",b:this.CNR,r:0};this.BNM={cN:"number",b:this.BNR,r:0};this.inherit=function(r,s){var p={};for(var q in r){p[q]=r[q]}if(s){for(var q in s){p[q]=s[q]}}return p}}();hljs.LANGUAGES.bash=function(){var e={"true":1,"false":1};var b={cN:"variable",b:"\\$([a-zA-Z0-9_]+)\\b"};var a={cN:"variable",b:"\\$\\{(([^}])|(\\\\}))+\\}",c:[hljs.CNM]};var f={cN:"string",b:'"',e:'"',i:"\\n",c:[hljs.BE,b,a],r:0};var c={cN:"string",b:"'",e:"'",r:0};var d={cN:"test_condition",b:"",e:"",c:[f,c,b,a,hljs.CNM],k:{literal:e},r:0};return{dM:{k:{keyword:{"if":1,then:1,"else":1,fi:1,"for":1,"break":1,"continue":1,"while":1,"in":1,"do":1,done:1,echo:1,exit:1,"return":1,set:1,declare:1},literal:e},c:[{cN:"shebang",b:"(#!\\/bin\\/bash)|(#!\\/bin\\/sh)",r:10},b,a,hljs.HCM,hljs.CNM,f,c,hljs.inherit(d,{b:"\\[ ",e:" \\]",r:0}),hljs.inherit(d,{b:"\\[\\[ ",e:" \\]\\]"})]}}}();hljs.LANGUAGES.ini={cI:true,dM:{i:"[^\\s]",c:[{cN:"comment",b:";",e:"$"},{cN:"title",b:"^\\[",e:"\\]"},{cN:"setting",b:"^[a-z0-9_\\[\\]]+[ \\t]*=[ \\t]*",e:"$",c:[{cN:"value",eW:true,k:{on:1,off:1,"true":1,"false":1,yes:1,no:1},c:[hljs.QSM,hljs.NM]}]}]}};hljs.LANGUAGES.python=function(){var a=[{cN:"string",b:"(u|b)?r?'''",e:"'''",r:10},{cN:"string",b:'(u|b)?r?"""',e:'"""',r:10},{cN:"string",b:"(u|r|ur)'",e:"'",c:[hljs.BE],r:10},{cN:"string",b:'(u|r|ur)"',e:'"',c:[hljs.BE],r:10},{cN:"string",b:"(b|br)'",e:"'",c:[hljs.BE]},{cN:"string",b:'(b|br)"',e:'"',c:[hljs.BE]}].concat([hljs.ASM,hljs.QSM]);var c={cN:"title",b:hljs.UIR};var b={cN:"params",b:"\\(",e:"\\)",c:a.concat([hljs.CNM])};return{dM:{k:{keyword:{and:1,elif:1,is:1,global:1,as:1,"in":1,"if":1,from:1,raise:1,"for":1,except:1,"finally":1,print:1,"import":1,pass:1,"return":1,exec:1,"else":1,"break":1,not:1,"with":1,"class":1,assert:1,yield:1,"try":1,"while":1,"continue":1,del:1,or:1,def:1,lambda:1,nonlocal:10},built_in:{None:1,True:1,False:1,Ellipsis:1,NotImplemented:1}},i:"(|->|\\?)",c:a.concat([hljs.HCM,{cN:"function",b:"\\bdef ",e:":",i:"$",k:{def:1},c:[c,b],r:10},{cN:"class",b:"\\bclass ",e:":",i:"[${]",k:{"class":1},c:[c,b],r:10},hljs.CNM,{cN:"decorator",b:"@",e:"$"}])}}}();hljs.LANGUAGES.java={dM:{k:{"false":1,"synchronized":1,"int":1,"abstract":1,"float":1,"private":1,"char":1,"interface":1,"boolean":1,"static":1,"null":1,"if":1,"const":1,"for":1,"true":1,"while":1,"long":1,"throw":1,strictfp:1,"finally":1,"protected":1,"extends":1,"import":1,"native":1,"final":1,"implements":1,"return":1,"void":1,"enum":1,"else":1,"break":1,"transient":1,"new":1,"catch":1,"instanceof":1,"byte":1,"super":1,"class":1,"volatile":1,"case":1,assert:1,"short":1,"package":1,"default":1,"double":1,"public":1,"try":1,"this":1,"switch":1,"continue":1,"throws":1},c:[{cN:"javadoc",b:"/\\*\\*",e:"\\*/",c:[{cN:"javadoctag",b:"@[A-Za-z]+"}],r:10},hljs.CLCM,hljs.CBLCLM,hljs.ASM,hljs.QSM,{cN:"class",b:"(class |interface )",e:"{",k:{"class":1,"interface":1},i:":",c:[{b:"(implements|extends)",k:{"extends":1,"implements":1},r:10},{cN:"title",b:hljs.UIR}]},hljs.CNM,{cN:"annotation",b:"@[A-Za-z]+"}]}};hljs.LANGUAGES.cs={dM:{k:{"abstract":1,as:1,base:1,bool:1,"break":1,"byte":1,"case":1,"catch":1,"char":1,checked:1,"class":1,"const":1,"continue":1,decimal:1,"default":1,delegate:1,"do":1,"double":1,"else":1,"enum":1,event:1,explicit:1,extern:1,"false":1,"finally":1,fixed:1,"float":1,"for":1,foreach:1,"goto":1,"if":1,implicit:1,"in":1,"int":1,"interface":1,internal:1,is:1,lock:1,"long":1,namespace:1,"new":1,"null":1,object:1,operator:1,out:1,override:1,params:1,"private":1,"protected":1,"public":1,readonly:1,ref:1,"return":1,sbyte:1,sealed:1,"short":1,sizeof:1,stackalloc:1,"static":1,string:1,struct:1,"switch":1,"this":1,"throw":1,"true":1,"try":1,"typeof":1,uint:1,ulong:1,unchecked:1,unsafe:1,ushort:1,using:1,virtual:1,"volatile":1,"void":1,"while":1,ascending:1,descending:1,from:1,get:1,group:1,into:1,join:1,let:1,orderby:1,partial:1,select:1,set:1,value:1,"var":1,where:1,yield:1},c:[{cN:"comment",b:"///",e:"$",rB:true,c:[{cN:"xmlDocTag",b:"///|"},{cN:"xmlDocTag",b:"?",e:">"}]},hljs.CLCM,hljs.CBLCLM,{cN:"preprocessor",b:"#",e:"$",k:{"if":1,"else":1,elif:1,endif:1,define:1,undef:1,warning:1,error:1,line:1,region:1,endregion:1,pragma:1,checksum:1}},{cN:"string",b:'@"',e:'"',c:[{b:'""'}]},hljs.ASM,hljs.QSM,hljs.CNM]}};hljs.LANGUAGES.xml=function(){var b="[A-Za-z0-9\\._:-]+";var a={eW:true,c:[{cN:"attribute",b:b,r:0},{b:'="',rB:true,e:'"',c:[{cN:"value",b:'"',eW:true}]},{b:"='",rB:true,e:"'",c:[{cN:"value",b:"'",eW:true}]},{b:"=",c:[{cN:"value",b:"[^\\s/>]+"}]}]};return{cI:true,dM:{c:[{cN:"pi",b:"<\\?",e:"\\?>",r:10},{cN:"doctype",b:"",r:10,c:[{b:"\\[",e:"\\]"}]},{cN:"comment",b:"",r:10},{cN:"cdata",b:"<\\!\\[CDATA\\[",e:"\\]\\]>",r:10},{cN:"tag",b:"",rE:true,sL:"css"}},{cN:"tag",b:"
<%= @blog.comments_count %>条评论
48 |49 | <% if @blog.comments_count > 0 %> 50 | <%= partial 'blog/comment', :collection => @blog.comments.reverse %> 51 | <% end %> 52 |
53 |