├── 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 |
2 |
Unauthorized
3 |
-------------------------------------------------------------------------------- /app/views/error/403.erb: -------------------------------------------------------------------------------- 1 |
2 |
Forbidden
3 |
-------------------------------------------------------------------------------- /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 |
2 |
File not found
3 |
-------------------------------------------------------------------------------- /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 |
3 |
4 |

5 | 管理控制台 6 |

7 |
8 |
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 |
2 | <%= link_to '查看评论列表', url(:admin, :comments) %> 3 | <%= link_to '查看用户列表', url(:admin, :accounts) %> 4 | <%= link_to '查看文章列表', url(:admin, :blogs) %> 5 | <%= link_to '修改用户信息', url(:admin, :edit_profile, :id => current_account) %> 6 |
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 |
  • 2 |
    <%= commenter_logo(comment.account) %>
    3 |
    4 |
    5 | <%= commenter_link(comment.account) %> 6 | <%= time_ago_in_words(comment.created_at) %> 7 | <% if account_admin? || (account_commenter? && comment.account == current_account) %> 8 | <%= link_to '', url(:blog, :comment, :id => comment.id), :method => :delete, :remote => true, :confirm => '要删除评论吗?', :class => 'delete', :title => '删除评论' %> 9 | <% end %> 10 | <% if account_login? %> 11 | 12 | <% end %> 13 |
    14 |
    <%= comment.md_content.html_safe %>
    15 |
    16 |
  • 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 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | <% @blogs.each_with_index do |blog, index| %> 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | <% end %> 26 |
    文章列表: 共<%= @blogs.total_entries %>篇
    文章标题发表时间浏览数评论数Tag操作
    <%= index + 1 %><%= link_to blog.title, blog_url(blog), :target => '_blank' %><%= time_ago_in_words(blog.created_at) %><%= blog.view_count %><%= blog.comments_count %><%= blog.cached_tag_list %>操作
    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 |
    35 | RSS订阅 36 |
    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 | 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 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | <% @comments.each_with_index do |comment, index| %> 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | <% end %> 24 |
    评论列表: 共<%= @comments.total_entries %>条
    评论者被评文章评论时间评论内容操作
    <%= index + 1 %><%= commenter_link comment.account %><%= link_to comment.blog.title, blog_url(comment.blog), :anchor => 'comments', :target => '_blank' %><%= time_ago_in_words(comment.created_at) %><%= comment.brief_content %><%= link_to "删除", url(:admin, :comment, :id => comment.id), :method => :delete, :remote => true, :confirm => '要删除评论吗?' %>
    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 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 34 | 35 | 36 | 37 | 38 | 41 | 42 |
    修改用户信息
    用户名字<%= form.text_field :name, :class => 'title' %>
    Email地址<%= form.text_field :email, :class => 'title' %>
    修改密码<%= form.password_field :password, :class => 'title' %>
    确认密码<%= form.password_field :password_confirmation, :class => 'title' %>
    头像 31 |

    <%= image_tag(@account.logo.url) %>

    32 | <%= form.file_field :logo %> 33 |
      39 | <%= form.submit '', :class => 'submit' %> 40 |
    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 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | <% @admin_accounts.each_with_index do |account, index| %> 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | <% end %> 24 |
    管理员列表: 共<%= @admin_accounts.size %>人
    名字注册时间文章数评论数操作
    <%= index + 1 %><%= account.name %><%= time_ago_in_words(account.created_at) %><%= account.blogs_count %><%= account.comments_count %><%= link_to '编辑', url(:admin, :edit_profile, :id => account.id) %>
    25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | <% @commenters.each_with_index do |account, index| %> 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | <% end %> 43 |
    评论者列表: 共<%= @commenters.total_entries %>人
    名字注册时间评论数操作
    <%= index+1 %><%= commenter_link account %><%= time_ago_in_words(account.created_at) %><%= account.comments_count %><%= link_to '删除', url(:admin, :account, :id => account.id), :method => :delete, :remote => true, :confirm => '要删除用户吗?' %>
    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 | 10 | 11 | 12 | 13 | 14 | 26 | 27 | 28 | 29 | 30 | 38 | 39 | 40 | 41 | 42 | 58 | 59 | 60 | 61 | 62 | 65 | 66 | 67 | 68 | 69 | 73 | 74 | 75 | 76 | 77 | 80 | 81 |
    文章标题<%= form.text_field :title, :class => 'title' %>
    文章内容 15 |
    16 | 编辑器 17 | 预览文章 18 |
    19 | <%= form.text_area :content %> 20 |
    21 | 25 |
    Tag 31 | <%= form.text_field :user_tags, :placeholder => '用逗号分开,不超过3个,Tag必须是字母数字空格下划线和中文' %> 32 |
    33 | <% Blog.cached_tag_cloud.each do |tag| %> 34 | 35 | <% end %> 36 |
    37 |
    附件 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 |
    URL后缀 63 | <%= form.text_field :slug_url, :placeholder => '填写URL英文关键词,用-连接' %> 64 |
    允许评论 70 | <%= form.check_box :commentable, :style => 'display:inline;' %> 71 |
    72 |
      78 | <%= form.submit '', :class => 'submit' %> 79 |
    -------------------------------------------------------------------------------- /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 |
    16 |
    17 |
    website icon
    18 |
    <%= APP_CONFIG['site_title'] %>
    19 |
    20 | <% unless account_login? -%> 21 | <%= link_to '登录', url(:login) -%> 22 | <% else -%> 23 | <%= current_account.name -%> 24 | <% if account_admin? -%> 25 | <%= link_to '写文章', url(:admin, :new_blog) -%> 26 | <%= link_to '管理', url(:admin, :index), :class => 'admin' -%> 27 | <% end -%> 28 | <%= button_to '退出', url(:logout), :method => :delete -%> 29 | <% end -%> 30 |
    31 |
    <%= APP_CONFIG['site_motto'] %>
    32 |
    33 |
    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 | 44 | 45 |
    46 |
    47 |

    <%= @blog.comments_count %>条评论

    48 |
      49 | <% if @blog.comments_count > 0 %> 50 | <%= partial 'blog/comment', :collection => @blog.comments.reverse %> 51 | <% end %> 52 |
    53 |
    54 | <% if @blog.commentable? %> 55 | 56 |
    57 | 64 | <% form_for BlogComment.new, url(:blog, :create_comment, :id => @blog.id), :remote => true do |f| %> 65 | <%= f.text_area :content, :class => 'comment' %> 66 |
    67 |
    68 | <%= f.submit '', :class => 'comment_btn' %> 69 | <% end %> 70 |
    "> 71 | 72 |

    我想知道与谁交流思想,请用微博登录留言。

    73 | 74 |
    75 |
    76 | <% end %> 77 | 78 |
    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+=("")}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:""}]},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:"|$)",e:">",k:{title:{style:1}},c:[a],starts:{cN:"css",e:"",rE:true,sL:"css"}},{cN:"tag",b:"|$)",e:">",k:{title:{script:1}},c:[a],starts:{cN:"javascript",e:"<\/script>",rE:true,sL:"javascript"}},{cN:"vbscript",b:"<%",e:"%>",sL:"vbscript"},{cN:"tag",b:"",c:[{cN:"title",b:"[^ />]+"},a]}]}}}();hljs.LANGUAGES.css=function(){var a={cN:"function",b:hljs.IR+"\\(",e:"\\)",c:[{eW:true,eE:true,c:[hljs.NM,hljs.ASM,hljs.QSM]}]};return{cI:true,dM:{i:"[=/|']",c:[hljs.CBLCLM,{cN:"id",b:"\\#[A-Za-z0-9_-]+"},{cN:"class",b:"\\.[A-Za-z0-9_-]+",r:0},{cN:"attr_selector",b:"\\[",e:"\\]",i:"$"},{cN:"pseudo",b:":(:)?[a-zA-Z0-9\\_\\-\\+\\(\\)\\\"\\']+"},{cN:"at_rule",b:"@(font-face|page)",l:"[a-z-]+",k:{"font-face":1,page:1}},{cN:"at_rule",b:"@",e:"[{;]",eE:true,k:{"import":1,page:1,media:1,charset:1},c:[a,hljs.ASM,hljs.QSM,hljs.NM]},{cN:"tag",b:hljs.IR,r:0},{cN:"rules",b:"{",e:"}",i:"[^\\s]",r:0,c:[hljs.CBLCLM,{cN:"rule",b:"[^\\s]",rB:true,e:";",eW:true,c:[{cN:"attribute",b:"[A-Z\\_\\.\\-]+",e:":",eE:true,i:"[^\\s]",starts:{cN:"value",eW:true,eE:true,c:[a,hljs.NM,hljs.QSM,hljs.ASM,hljs.CBLCLM,{cN:"hexcolor",b:"\\#[0-9A-F]+"},{cN:"important",b:"!important"}]}}]}]}]}}}();hljs.LANGUAGES.javascript={dM:{k:{keyword:{"in":1,"if":1,"for":1,"while":1,"finally":1,"var":1,"new":1,"function":1,"do":1,"return":1,"void":1,"else":1,"break":1,"catch":1,"instanceof":1,"with":1,"throw":1,"case":1,"default":1,"try":1,"this":1,"switch":1,"continue":1,"typeof":1,"delete":1},literal:{"true":1,"false":1,"null":1}},c:[hljs.ASM,hljs.QSM,hljs.CLCM,hljs.CBLCLM,hljs.CNM,{b:"("+hljs.RSR+"|case|return|throw)\\s*",k:{"return":1,"throw":1,"case":1},c:[hljs.CLCM,hljs.CBLCLM,{cN:"regexp",b:"/",e:"/[gim]*",c:[{b:"\\\\/"}]}],r:0},{cN:"function",b:"\\bfunction\\b",e:"{",k:{"function":1},c:[{cN:"title",b:"[A-Za-z$_][0-9A-Za-z$_]*"},{cN:"params",b:"\\(",e:"\\)",c:[hljs.ASM,hljs.QSM,hljs.CLCM,hljs.CBLCLM]}]}]}};hljs.LANGUAGES.cpp=function(){var a={keyword:{"false":1,"int":1,"float":1,"while":1,"private":1,"char":1,"catch":1,"export":1,virtual:1,operator:2,sizeof:2,dynamic_cast:2,typedef:2,const_cast:2,"const":1,struct:1,"for":1,static_cast:2,union:1,namespace:1,unsigned:1,"long":1,"throw":1,"volatile":2,"static":1,"protected":1,bool:1,template:1,mutable:1,"if":1,"public":1,friend:2,"do":1,"return":1,"goto":1,auto:1,"void":2,"enum":1,"else":1,"break":1,"new":1,extern:1,using:1,"true":1,"class":1,asm:1,"case":1,typeid:1,"short":1,reinterpret_cast:2,"default":1,"double":1,register:1,explicit:1,signed:1,typename:1,"try":1,"this":1,"switch":1,"continue":1,wchar_t:1,inline:1,"delete":1,alignof:1,char16_t:1,char32_t:1,constexpr:1,decltype:1,noexcept:1,nullptr:1,static_assert:1,thread_local:1,restrict:1,_Bool:1,complex:1},built_in:{std:1,string:1,cin:1,cout:1,cerr:1,clog:1,stringstream:1,istringstream:1,ostringstream:1,auto_ptr:1,deque:1,list:1,queue:1,stack:1,vector:1,map:1,set:1,bitset:1,multiset:1,multimap:1,unordered_set:1,unordered_map:1,unordered_multiset:1,unordered_multimap:1,array:1,shared_ptr:1}};return{dM:{k:a,i:"",k:a,r:10,c:["self"]}]}}}();hljs.LANGUAGES.perl=function(){var d={getpwent:1,getservent:1,quotemeta:1,msgrcv:1,scalar:1,kill:1,dbmclose:1,undef:1,lc:1,ma:1,syswrite:1,tr:1,send:1,umask:1,sysopen:1,shmwrite:1,vec:1,qx:1,utime:1,local:1,oct:1,semctl:1,localtime:1,readpipe:1,"do":1,"return":1,format:1,read:1,sprintf:1,dbmopen:1,pop:1,getpgrp:1,not:1,getpwnam:1,rewinddir:1,qq:1,fileno:1,qw:1,endprotoent:1,wait:1,sethostent:1,bless:1,s:1,opendir:1,"continue":1,each:1,sleep:1,endgrent:1,shutdown:1,dump:1,chomp:1,connect:1,getsockname:1,die:1,socketpair:1,close:1,flock:1,exists:1,index:1,shmget:1,sub:1,"for":1,endpwent:1,redo:1,lstat:1,msgctl:1,setpgrp:1,abs:1,exit:1,select:1,print:1,ref:1,gethostbyaddr:1,unshift:1,fcntl:1,syscall:1,"goto":1,getnetbyaddr:1,join:1,gmtime:1,symlink:1,semget:1,splice:1,x:1,getpeername:1,recv:1,log:1,setsockopt:1,cos:1,last:1,reverse:1,gethostbyname:1,getgrnam:1,study:1,formline:1,endhostent:1,times:1,chop:1,length:1,gethostent:1,getnetent:1,pack:1,getprotoent:1,getservbyname:1,rand:1,mkdir:1,pos:1,chmod:1,y:1,substr:1,endnetent:1,printf:1,next:1,open:1,msgsnd:1,readdir:1,use:1,unlink:1,getsockopt:1,getpriority:1,rindex:1,wantarray:1,hex:1,system:1,getservbyport:1,endservent:1,"int":1,chr:1,untie:1,rmdir:1,prototype:1,tell:1,listen:1,fork:1,shmread:1,ucfirst:1,setprotoent:1,"else":1,sysseek:1,link:1,getgrgid:1,shmctl:1,waitpid:1,unpack:1,getnetbyname:1,reset:1,chdir:1,grep:1,split:1,require:1,caller:1,lcfirst:1,until:1,warn:1,"while":1,values:1,shift:1,telldir:1,getpwuid:1,my:1,getprotobynumber:1,"delete":1,and:1,sort:1,uc:1,defined:1,srand:1,accept:1,"package":1,seekdir:1,getprotobyname:1,semop:1,our:1,rename:1,seek:1,"if":1,q:1,chroot:1,sysread:1,setpwent:1,no:1,crypt:1,getc:1,chown:1,sqrt:1,write:1,setnetent:1,setpriority:1,foreach:1,tie:1,sin:1,msgget:1,map:1,stat:1,getlogin:1,unless:1,elsif:1,truncate:1,exec:1,keys:1,glob:1,tied:1,closedir:1,ioctl:1,socket:1,readlink:1,"eval":1,xor:1,readline:1,binmode:1,setservent:1,eof:1,ord:1,bind:1,alarm:1,pipe:1,atan2:1,getgrent:1,exp:1,time:1,push:1,setgrent:1,gt:1,lt:1,or:1,ne:1,m:1};var f={cN:"subst",b:"[$@]\\{",e:"\\}",k:d,r:10};var c={cN:"variable",b:"\\$\\d"};var b={cN:"variable",b:"[\\$\\%\\@\\*](\\^\\w\\b|#\\w+(\\:\\:\\w+)*|[^\\s\\w{]|{\\w+}|\\w+(\\:\\:\\w*)*)"};var h=[hljs.BE,f,c,b];var g={b:"->",c:[{b:hljs.IR},{b:"{",e:"}"}]};var e={cN:"comment",b:"^(__END__|__DATA__)",e:"\\n$",r:5};var a=[c,b,hljs.HCM,e,g,{cN:"string",b:"q[qwxr]?\\s*\\(",e:"\\)",c:h,r:5},{cN:"string",b:"q[qwxr]?\\s*\\[",e:"\\]",c:h,r:5},{cN:"string",b:"q[qwxr]?\\s*\\{",e:"\\}",c:h,r:5},{cN:"string",b:"q[qwxr]?\\s*\\|",e:"\\|",c:h,r:5},{cN:"string",b:"q[qwxr]?\\s*\\<",e:"\\>",c:h,r:5},{cN:"string",b:"qw\\s+q",e:"q",c:h,r:5},{cN:"string",b:"'",e:"'",c:[hljs.BE],r:0},{cN:"string",b:'"',e:'"',c:h,r:0},{cN:"string",b:"`",e:"`",c:[hljs.BE]},{cN:"string",b:"{\\w+}",r:0},{cN:"string",b:"-?\\w+\\s*\\=\\>",r:0},{cN:"number",b:"(\\b0[0-7_]+)|(\\b0x[0-9a-fA-F_]+)|(\\b[1-9][0-9_]*(\\.[0-9_]+)?)|[0_]\\b",r:0},{b:"("+hljs.RSR+"|split|return|print|reverse|grep)\\s*",k:{split:1,"return":1,print:1,reverse:1,grep:1},r:0,c:[hljs.HCM,e,{cN:"regexp",b:"(s|tr|y)/(\\\\.|[^/])*/(\\\\.|[^/])*/[a-z]*",r:10},{cN:"regexp",b:"(m|qr)?/",e:"/[a-z]*",c:[hljs.BE],r:0}]},{cN:"sub",b:"\\bsub\\b",e:"(\\s*\\(.*?\\))?[;{]",k:{sub:1},r:5},{cN:"operator",b:"-\\w\\b",r:0},{cN:"pod",b:"\\=\\w",e:"\\=cut"}];f.c=a;g.c[1].c=a;return{dM:{k:d,c:a}}}();hljs.LANGUAGES.ruby=function(){var a="[a-zA-Z_][a-zA-Z0-9_]*(\\!|\\?)?";var j="[a-zA-Z_]\\w*[!?=]?|[-+~]\\@|<<|>>|=~|===?|<=>|[<>]=?|\\*\\*|[-/+%^&*~`|]|\\[\\]=?";var f={keyword:{and:1,"false":1,then:1,defined:1,module:1,"in":1,"return":1,redo:1,"if":1,BEGIN:1,retry:1,end:1,"for":1,"true":1,self:1,when:1,next:1,until:1,"do":1,begin:1,unless:1,END:1,rescue:1,nil:1,"else":1,"break":1,undef:1,not:1,"super":1,"class":1,"case":1,require:1,yield:1,alias:1,"while":1,ensure:1,elsif:1,or:1,def:1},keymethods:{__id__:1,__send__:1,abort:1,abs:1,"all?":1,allocate:1,ancestors:1,"any?":1,arity:1,assoc:1,at:1,at_exit:1,autoload:1,"autoload?":1,"between?":1,binding:1,binmode:1,"block_given?":1,call:1,callcc:1,caller:1,capitalize:1,"capitalize!":1,casecmp:1,"catch":1,ceil:1,center:1,chomp:1,"chomp!":1,chop:1,"chop!":1,chr:1,"class":1,class_eval:1,"class_variable_defined?":1,class_variables:1,clear:1,clone:1,close:1,close_read:1,close_write:1,"closed?":1,coerce:1,collect:1,"collect!":1,compact:1,"compact!":1,concat:1,"const_defined?":1,const_get:1,const_missing:1,const_set:1,constants:1,count:1,crypt:1,"default":1,default_proc:1,"delete":1,"delete!":1,delete_at:1,delete_if:1,detect:1,display:1,div:1,divmod:1,downcase:1,"downcase!":1,downto:1,dump:1,dup:1,each:1,each_byte:1,each_index:1,each_key:1,each_line:1,each_pair:1,each_value:1,each_with_index:1,"empty?":1,entries:1,eof:1,"eof?":1,"eql?":1,"equal?":1,"eval":1,exec:1,exit:1,"exit!":1,extend:1,fail:1,fcntl:1,fetch:1,fileno:1,fill:1,find:1,find_all:1,first:1,flatten:1,"flatten!":1,floor:1,flush:1,for_fd:1,foreach:1,fork:1,format:1,freeze:1,"frozen?":1,fsync:1,getc:1,gets:1,global_variables:1,grep:1,gsub:1,"gsub!":1,"has_key?":1,"has_value?":1,hash:1,hex:1,id:1,include:1,"include?":1,included_modules:1,index:1,indexes:1,indices:1,induced_from:1,inject:1,insert:1,inspect:1,instance_eval:1,instance_method:1,instance_methods:1,"instance_of?":1,"instance_variable_defined?":1,instance_variable_get:1,instance_variable_set:1,instance_variables:1,"integer?":1,intern:1,invert:1,ioctl:1,"is_a?":1,isatty:1,"iterator?":1,join:1,"key?":1,keys:1,"kind_of?":1,lambda:1,last:1,length:1,lineno:1,ljust:1,load:1,local_variables:1,loop:1,lstrip:1,"lstrip!":1,map:1,"map!":1,match:1,max:1,"member?":1,merge:1,"merge!":1,method:1,"method_defined?":1,method_missing:1,methods:1,min:1,module_eval:1,modulo:1,name:1,nesting:1,"new":1,next:1,"next!":1,"nil?":1,nitems:1,"nonzero?":1,object_id:1,oct:1,open:1,pack:1,partition:1,pid:1,pipe:1,pop:1,popen:1,pos:1,prec:1,prec_f:1,prec_i:1,print:1,printf:1,private_class_method:1,private_instance_methods:1,"private_method_defined?":1,private_methods:1,proc:1,protected_instance_methods:1,"protected_method_defined?":1,protected_methods:1,public_class_method:1,public_instance_methods:1,"public_method_defined?":1,public_methods:1,push:1,putc:1,puts:1,quo:1,raise:1,rand:1,rassoc:1,read:1,read_nonblock:1,readchar:1,readline:1,readlines:1,readpartial:1,rehash:1,reject:1,"reject!":1,remainder:1,reopen:1,replace:1,require:1,"respond_to?":1,reverse:1,"reverse!":1,reverse_each:1,rewind:1,rindex:1,rjust:1,round:1,rstrip:1,"rstrip!":1,scan:1,seek:1,select:1,send:1,set_trace_func:1,shift:1,singleton_method_added:1,singleton_methods:1,size:1,sleep:1,slice:1,"slice!":1,sort:1,"sort!":1,sort_by:1,split:1,sprintf:1,squeeze:1,"squeeze!":1,srand:1,stat:1,step:1,store:1,strip:1,"strip!":1,sub:1,"sub!":1,succ:1,"succ!":1,sum:1,superclass:1,swapcase:1,"swapcase!":1,sync:1,syscall:1,sysopen:1,sysread:1,sysseek:1,system:1,syswrite:1,taint:1,"tainted?":1,tell:1,test:1,"throw":1,times:1,to_a:1,to_ary:1,to_f:1,to_hash:1,to_i:1,to_int:1,to_io:1,to_proc:1,to_s:1,to_str:1,to_sym:1,tr:1,"tr!":1,tr_s:1,"tr_s!":1,trace_var:1,transpose:1,trap:1,truncate:1,"tty?":1,type:1,ungetc:1,uniq:1,"uniq!":1,unpack:1,unshift:1,untaint:1,untrace_var:1,upcase:1,"upcase!":1,update:1,upto:1,"value?":1,values:1,values_at:1,warn:1,write:1,write_nonblock:1,"zero?":1,zip:1}};var c={cN:"yardoctag",b:"@[A-Za-z]+"};var k=[{cN:"comment",b:"#",e:"$",c:[c]},{cN:"comment",b:"^\\=begin",e:"^\\=end",c:[c],r:10},{cN:"comment",b:"^__END__",e:"\\n$"}];var d={cN:"subst",b:"#\\{",e:"}",l:a,k:f};var i=[hljs.BE,d];var b=[{cN:"string",b:"'",e:"'",c:i,r:0},{cN:"string",b:'"',e:'"',c:i,r:0},{cN:"string",b:"%[qw]?\\(",e:"\\)",c:i,r:10},{cN:"string",b:"%[qw]?\\[",e:"\\]",c:i,r:10},{cN:"string",b:"%[qw]?{",e:"}",c:i,r:10},{cN:"string",b:"%[qw]?<",e:">",c:i,r:10},{cN:"string",b:"%[qw]?/",e:"/",c:i,r:10},{cN:"string",b:"%[qw]?%",e:"%",c:i,r:10},{cN:"string",b:"%[qw]?-",e:"-",c:i,r:10},{cN:"string",b:"%[qw]?\\|",e:"\\|",c:i,r:10}];var h={cN:"function",b:"\\bdef\\s+",e:" |$|;",l:a,k:f,c:[{cN:"title",b:j,l:a,k:f},{cN:"params",b:"\\(",e:"\\)",l:a,k:f}].concat(k)};var g={cN:"identifier",b:a,l:a,k:f,r:0};var e=k.concat(b.concat([{cN:"class",b:"\\b(class|module)\\b",e:"$|;",k:{"class":1,module:1},c:[{cN:"title",b:"[A-Za-z_]\\w*(::\\w+)*(\\?|\\!)?",r:0},{cN:"inheritance",b:"<\\s*",c:[{cN:"parent",b:"("+hljs.IR+"::)?"+hljs.IR}]}].concat(k)},h,{cN:"constant",b:"(::)?([A-Z]\\w*(::)?)+",r:0},{cN:"symbol",b:":",c:b.concat([g]),r:0},{cN:"number",b:"(\\b0[0-7_]+)|(\\b0x[0-9a-fA-F_]+)|(\\b[1-9][0-9_]*(\\.[0-9_]+)?)|[0_]\\b",r:0},{cN:"number",b:"\\?\\w"},{cN:"variable",b:"(\\$\\W)|((\\$|\\@\\@?)(\\w+))"},g,{b:"("+hljs.RSR+")\\s*",c:k.concat([{cN:"regexp",b:"/",e:"/[a-z]*",i:"\\n",c:[hljs.BE]}]),r:0}]));d.c=e;h.c[1].c=e;return{dM:{l:a,k:f,c:e}}}();hljs.LANGUAGES.sql={cI:true,dM:{i:"[^\\s]",c:[{cN:"operator",b:"(begin|start|commit|rollback|savepoint|lock|alter|create|drop|rename|call|delete|do|handler|insert|load|replace|select|truncate|update|set|show|pragma|grant)\\b",e:";|$",k:{keyword:{all:1,partial:1,global:1,month:1,current_timestamp:1,using:1,go:1,revoke:1,smallint:1,indicator:1,"end-exec":1,disconnect:1,zone:1,"with":1,character:1,assertion:1,to:1,add:1,current_user:1,usage:1,input:1,local:1,alter:1,match:1,collate:1,real:1,then:1,rollback:1,get:1,read:1,timestamp:1,session_user:1,not:1,integer:1,bit:1,unique:1,day:1,minute:1,desc:1,insert:1,execute:1,like:1,ilike:2,level:1,decimal:1,drop:1,"continue":1,isolation:1,found:1,where:1,constraints:1,domain:1,right:1,national:1,some:1,module:1,transaction:1,relative:1,second:1,connect:1,escape:1,close:1,system_user:1,"for":1,deferred:1,section:1,cast:1,current:1,sqlstate:1,allocate:1,intersect:1,deallocate:1,numeric:1,"public":1,preserve:1,full:1,"goto":1,initially:1,asc:1,no:1,key:1,output:1,collation:1,group:1,by:1,union:1,session:1,both:1,last:1,language:1,constraint:1,column:1,of:1,space:1,foreign:1,deferrable:1,prior:1,connection:1,unknown:1,action:1,commit:1,view:1,or:1,first:1,into:1,"float":1,year:1,primary:1,cascaded:1,except:1,restrict:1,set:1,references:1,names:1,table:1,outer:1,open:1,select:1,size:1,are:1,rows:1,from:1,prepare:1,distinct:1,leading:1,create:1,only:1,next:1,inner:1,authorization:1,schema:1,corresponding:1,option:1,declare:1,precision:1,immediate:1,"else":1,timezone_minute:1,external:1,varying:1,translation:1,"true":1,"case":1,exception:1,join:1,hour:1,"default":1,"double":1,scroll:1,value:1,cursor:1,descriptor:1,values:1,dec:1,fetch:1,procedure:1,"delete":1,and:1,"false":1,"int":1,is:1,describe:1,"char":1,as:1,at:1,"in":1,varchar:1,"null":1,trailing:1,any:1,absolute:1,current_time:1,end:1,grant:1,privileges:1,when:1,cross:1,check:1,write:1,current_date:1,pad:1,begin:1,temporary:1,exec:1,time:1,update:1,catalog:1,user:1,sql:1,date:1,on:1,identity:1,timezone_hour:1,natural:1,whenever:1,interval:1,work:1,order:1,cascade:1,diagnostics:1,nchar:1,having:1,left:1,call:1,"do":1,handler:1,load:1,replace:1,truncate:1,start:1,lock:1,show:1,pragma:1},aggregate:{count:1,sum:1,min:1,max:1,avg:1}},c:[{cN:"string",b:"'",e:"'",c:[hljs.BE,{b:"''"}],r:0},{cN:"string",b:'"',e:'"',c:[hljs.BE,{b:'""'}],r:0},{cN:"string",b:"`",e:"`",c:[hljs.BE]},hljs.CNM,{b:"\\n"}]},hljs.CBLCLM,{cN:"comment",b:"--",e:"$"}]}};hljs.LANGUAGES.php={cI:true,dM:{k:{and:1,include_once:1,list:1,"abstract":1,global:1,"private":1,echo:1,"interface":1,as:1,"static":1,endswitch:1,array:1,"null":1,"if":1,endwhile:1,or:1,"const":1,"for":1,endforeach:1,self:1,"var":1,"while":1,isset:1,"public":1,"protected":1,exit:1,foreach:1,"throw":1,elseif:1,"extends":1,include:1,__FILE__:1,empty:1,require_once:1,"function":1,"do":1,xor:1,"return":1,"implements":1,parent:1,clone:1,use:1,__CLASS__:1,__LINE__:1,"else":1,"break":1,print:1,"eval":1,"new":1,"catch":1,__METHOD__:1,"class":1,"case":1,exception:1,php_user_filter:1,"default":1,die:1,require:1,__FUNCTION__:1,enddeclare:1,"final":1,"try":1,"this":1,"switch":1,"continue":1,endfor:1,endif:1,declare:1,unset:1,"true":1,"false":1,namespace:1,trait:1,"goto":1,"instanceof":1,__DIR__:1,__NAMESPACE__:1,__halt_compiler:1},c:[hljs.CLCM,hljs.HCM,{cN:"comment",b:"/\\*",e:"\\*/",c:[{cN:"phpdoc",b:"\\s@[A-Za-z]+"}]},{cN:"comment",eB:true,b:"__halt_compiler[^;]+;",e:"[\\n\\r]$"},hljs.CNM,hljs.BNM,hljs.inherit(hljs.ASM,{i:null}),hljs.inherit(hljs.QSM,{i:null}),{cN:"string",b:'b"',e:'"',c:[hljs.BE]},{cN:"string",b:"b'",e:"'",c:[hljs.BE]},{cN:"string",b:"<<<['\"]?\\w+['\"]?$",e:"^\\w+;",c:[hljs.BE]},{cN:"variable",b:"\\$+[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*"},{cN:"preprocessor",b:"<\\?php",r:10},{cN:"preprocessor",b:"\\?>"}]}};hljs.LANGUAGES.diff={cI:true,dM:{c:[{cN:"chunk",b:"^\\@\\@ +\\-\\d+,\\d+ +\\+\\d+,\\d+ +\\@\\@$",r:10},{cN:"chunk",b:"^\\*\\*\\* +\\d+,\\d+ +\\*\\*\\*\\*$",r:10},{cN:"chunk",b:"^\\-\\-\\- +\\d+,\\d+ +\\-\\-\\-\\-$",r:10},{cN:"header",b:"Index: ",e:"$"},{cN:"header",b:"=====",e:"=====$"},{cN:"header",b:"^\\-\\-\\-",e:"$"},{cN:"header",b:"^\\*{3} ",e:"$"},{cN:"header",b:"^\\+\\+\\+",e:"$"},{cN:"header",b:"\\*{5}",e:"\\*{5}$"},{cN:"addition",b:"^\\+",e:"$"},{cN:"deletion",b:"^\\-",e:"$"},{cN:"change",b:"^\\!",e:"$"}]}}; --------------------------------------------------------------------------------