diff --git a/.travis.yml b/.travis.yml index ba949e8..4f5461c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,6 +7,7 @@ rvm: services: - postgresql + - elasticsearch addons: postgresql: "9.5" diff --git a/Gemfile b/Gemfile index 5528071..12a79e7 100644 --- a/Gemfile +++ b/Gemfile @@ -69,6 +69,10 @@ gem 'byebug', platform: :mri # for corss-site communication gem 'rest-client' +# Elasticsearch +gem 'elasticsearch-model' +gem 'elasticsearch-rails' + group :development, :test do gem 'ffaker' gem 'factory_girl_rails' diff --git a/Gemfile.lock b/Gemfile.lock index 2df9f09..71cc958 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,6 +1,6 @@ GIT - remote: https://github.com/seuros/capistrano-puma.git - revision: 1d7cca2e431c23943ebcde19588169ed12025833 + remote: git://github.com/seuros/capistrano-puma.git + revision: 00708fa18a029cb34f46aef60f920106c0c00107 specs: capistrano3-puma (3.1.0) capistrano (~> 3.7) @@ -8,7 +8,7 @@ GIT puma (~> 3.4) GIT - remote: https://github.com/shouya/cant_cant_cant.git + remote: git://github.com/shouya/cant_cant_cant.git revision: 2dc86d3238febe461cfea48b8eeef38d0482c6d5 specs: cant_cant_cant (0.1.10) @@ -114,6 +114,19 @@ GEM docile (1.1.5) domain_name (0.5.20170404) unf (>= 0.0.5, < 1.0.0) + elasticsearch (5.0.4) + elasticsearch-api (= 5.0.4) + elasticsearch-transport (= 5.0.4) + elasticsearch-api (5.0.4) + multi_json + elasticsearch-model (5.0.1) + activesupport (> 3) + elasticsearch (~> 5) + hashie + elasticsearch-rails (5.0.1) + elasticsearch-transport (5.0.4) + faraday + multi_json enumerize (2.1.0) activesupport (>= 3.2) erubis (2.7.0) @@ -123,6 +136,8 @@ GEM factory_girl_rails (4.8.0) factory_girl (~> 4.8.0) railties (>= 3.0.0) + faraday (0.12.1) + multipart-post (>= 1.2, < 3) ffaker (2.5.0) ffi (1.9.18) formatador (0.2.5) @@ -142,6 +157,7 @@ GEM guard (~> 2.1) guard-compat (~> 1.1) rspec (>= 2.99.0, < 4.0) + hashie (3.5.5) http-cookie (1.0.3) domain_name (~> 0.5) i18n (0.8.1) @@ -186,6 +202,7 @@ GEM mini_portile2 (2.1.0) minitest (5.10.1) multi_json (1.12.1) + multipart-post (2.0.0) nenv (0.3.0) net-scp (1.2.1) net-ssh (>= 2.6.5) @@ -359,6 +376,8 @@ DEPENDENCIES carrierwave codeclimate-test-reporter coffee-rails (~> 4.2) + elasticsearch-model + elasticsearch-rails enumerize factory_girl_rails ffaker @@ -396,4 +415,4 @@ DEPENDENCIES web-console BUNDLED WITH - 1.13.6 + 1.14.6 diff --git a/app/controllers/api/v1/admin/posts_controller.rb b/app/controllers/api/v1/admin/posts_controller.rb index 228cdf9..417a504 100644 --- a/app/controllers/api/v1/admin/posts_controller.rb +++ b/app/controllers/api/v1/admin/posts_controller.rb @@ -1,7 +1,7 @@ module API::V1::Admin class PostsController < AdminController resource_description { short '管理介面文章/視頻 API' } - before_action find_record, only: %i(destroy show update publish) + before_action find_record, only: %i(destroy show update publish toggle_recommended) api :GET, '/admin/posts', 'List or filter on posts' param :title, String, desc: '标题' @@ -62,6 +62,12 @@ def unpublish updated end + api :POST, '/admin/posts/:post_id/toggle_recommended', 'toggle推荐文章' + def toggle_recommended + @post.toggle!(:recommended) + updated + end + def close @post.close! updated diff --git a/app/controllers/api/v1/admin/users_controller.rb b/app/controllers/api/v1/admin/users_controller.rb index ffe1a3a..ba968db 100644 --- a/app/controllers/api/v1/admin/users_controller.rb +++ b/app/controllers/api/v1/admin/users_controller.rb @@ -11,5 +11,10 @@ def permissions } end end + + api :GET, '/admin/info', 'Get current user\' info' + def info + success current_user_state + end end end diff --git a/app/controllers/api/v1/comments_controller.rb b/app/controllers/api/v1/comments_controller.rb index d2d5a56..6eccab8 100644 --- a/app/controllers/api/v1/comments_controller.rb +++ b/app/controllers/api/v1/comments_controller.rb @@ -2,11 +2,12 @@ module API::V1 class CommentsController < APIController resource_description { short '評論' } before_action :find_commentable + before_action :find_comment, only: %i(like unlike) api :GET, '/(posts|ads)/:id/comments', 'List all comments under given post or ad' def index - success do + success(user_id: current_user_id) do paginated(@commentable.comments.normal) end end @@ -21,6 +22,28 @@ def create created(comment) end + api :POST, '/posts/:post_id/comments/:comment_id/like', 'like specfic comment' + param :post_id, Integer, desc: 'post id', required: true + param :comment_id, Integer, desc: 'comment id', required: true + def like + if @comment.like(current_user_id) + render json: { message: 'success' } + else + render json: { error: 'already liked' } + end + end + + api :POST, '/posts/:post_id/comments/:comment_id/unlike', 'unlike specfic comment' + param :post_id, Integer, desc: 'post id', required: true + param :comment_id, Integer, desc: 'comment id', required: true + def unlike + if @comment.unlike(current_user_id) + render json: { message: 'success' } + else + render json: { error: 'already unliked' } + end + end + private def find_commentable @@ -28,6 +51,10 @@ def find_commentable Ad. find_by(id: params[:ad_id]) end + def find_comment + @comment = Comment.find_by(id: params[:comment_id]) + end + def comment_params params.permit(:content, :parent_id) end diff --git a/app/controllers/api/v1/posts_controller.rb b/app/controllers/api/v1/posts_controller.rb index da13ba8..6a10289 100644 --- a/app/controllers/api/v1/posts_controller.rb +++ b/app/controllers/api/v1/posts_controller.rb @@ -1,7 +1,7 @@ module API::V1 class PostsController < APIController resource_description { short '文章/視頻 API' } - before_action find_record, only: :show + before_action find_record, only: %i(show like unlike) api :GET, '/posts', 'List all posts' def index @@ -10,7 +10,8 @@ def index api :GET, '/posts/:id', 'Show specific post' def show - success(@post) + @post.increment + success(@post, serializer: ::PostSerializer, user_id: current_user_id) end api :GET, '/posts/hot_in_week', 'Show a list of hot posts in recent 7-day' @@ -23,5 +24,23 @@ def index_by_tag tag = params[:tag] success { Post.with_tag(tag).published } end + + api :POST, '/posts/:id/like', 'Like specfic post' + def like + if @post.like(current_user_id) + render json: { message: 'success' } + else + render json: { error: 'already liked' } + end + end + + api :POST, '/posts/:id/unlike', 'Unlike specfic post' + def unlike + if @post.unlike(current_user_id) + render json: { message: 'success' } + else + render json: { error: 'already liked' } + end + end end end diff --git a/app/models/comment.rb b/app/models/comment.rb index 547dcc6..c7231cd 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -10,7 +10,6 @@ # depth :integer default(0) # commentable_type :string # commentable_id :integer -# author :string # parent_id :integer # created_at :datetime not null # updated_at :datetime not null @@ -27,8 +26,11 @@ class Comment < ApplicationRecord include SmartFilterable + include Likeable acts_as_paranoid + add_likeable + belongs_to :commentable, polymorphic: true belongs_to :parent, diff --git a/app/models/concerns/likeable.rb b/app/models/concerns/likeable.rb new file mode 100644 index 0000000..d55fb85 --- /dev/null +++ b/app/models/concerns/likeable.rb @@ -0,0 +1,35 @@ +module Likeable + extend ActiveSupport::Concern + + include AccountAPIHelper + + class_methods do + def add_likeable + define_method 'like' do |user_id| + unless find_like(user_id) + Like.create(user_id: user_id, target: self) + true + end + end + + define_method 'unlike' do |user_id| + if find_like(user_id) + find_like(user_id).destroy + true + end + end + + define_method 'find_like' do |user_id| + @first_like_cached ||= Like.find_by(user_id: user_id, target: self) + end + end + end + + def liked + !!(Like.find_by(user_id: @instance_options[:user_id], target: object)) + end + + def like_count + Like.where(target: object).count + end +end diff --git a/app/models/like.rb b/app/models/like.rb new file mode 100644 index 0000000..43c909d --- /dev/null +++ b/app/models/like.rb @@ -0,0 +1,19 @@ +# == Schema Information +# +# Table name: likes +# +# id :integer not null, primary key +# user_id :string +# target_type :string +# target_id :string +# created_at :datetime not null +# updated_at :datetime not null +# +# Indexes +# +# index_likes_on_user_id_and_target_type_and_target_id (user_id,target_type,target_id) +# + +class Like < ApplicationRecord + belongs_to :target, polymorphic: true +end diff --git a/app/models/post.rb b/app/models/post.rb index 911d836..cf31733 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -22,6 +22,8 @@ # created_at :datetime not null # updated_at :datetime not null # deleted_at :datetime +# views :integer +# recommended :boolean default(FALSE) # # Indexes # @@ -38,9 +40,14 @@ class Post < ApplicationRecord include SmartFilterable include Countable include ProcessPostMeta + include Likeable + include Elasticsearch::Model + include Elasticsearch::Model::Callbacks acts_as_paranoid + add_likeable + add_instance_counter_for :click add_instance_counter_for :publishing add_instance_counter_for :sharing @@ -74,6 +81,11 @@ class Post < ApplicationRecord scope :with_tag, ->(tag) { where('? = ANY(tags)', tag) } scope :with_cover, -> { includes(:cover) } + mapping do + indexes :title, type: :string, analyzer: :smartcn + indexes :content_rendered, type: :string, analyzer: :smartcn + end + def article? !video? end @@ -117,6 +129,41 @@ def close! save end + def increment(by = 1) + self.views ||= 0 + self.views += by + save + end + + def img_count + Nokogiri::HTML(content_rendered).css('img').length + end + + def h2_list + Nokogiri::HTML(content_rendered).css('h2').map(&:text) + end + + def related_posts(size = 4) + opts = { + query: { + more_like_this: { + fields: [:title, :content_rendered], + docs: [ + { + _index: self.class.index_name, + _type: self.class.document_type, + _id: id + } + ], + min_term_freq: 2, + min_doc_freq: 5 + } + }, + size: size + } + self.class.__elasticsearch__.search(opts).records.to_a + end + private def render_content diff --git a/app/serializers/admin_short_post_serializer.rb b/app/serializers/admin_short_post_serializer.rb index ef30f90..433ea4d 100644 --- a/app/serializers/admin_short_post_serializer.rb +++ b/app/serializers/admin_short_post_serializer.rb @@ -1,5 +1,12 @@ class AdminShortPostSerializer < ApplicationSerializer - attributes :id, :title, :abstract, :state, :published_at + attributes :id, + :title, + :abstract, + :state, + :published_at, + :views, + :recommended + attribute :column_title do object.column.title end diff --git a/app/serializers/application_serializer.rb b/app/serializers/application_serializer.rb index 4c7ea38..e7b7155 100644 --- a/app/serializers/application_serializer.rb +++ b/app/serializers/application_serializer.rb @@ -1,3 +1,4 @@ class ApplicationSerializer < ActiveModel::Serializer include RolePredicates + include Likeable end diff --git a/app/serializers/comment_serializer.rb b/app/serializers/comment_serializer.rb index cd26f97..106033e 100644 --- a/app/serializers/comment_serializer.rb +++ b/app/serializers/comment_serializer.rb @@ -23,5 +23,11 @@ # class CommentSerializer < ApplicationSerializer - attributes :id, :content, :commenter, :parent_id, :created_at + attributes :id, + :content, + :commenter, + :parent_id, + :created_at, + :liked, + :like_count end diff --git a/app/serializers/post_serializer.rb b/app/serializers/post_serializer.rb index 678741d..fa58864 100644 --- a/app/serializers/post_serializer.rb +++ b/app/serializers/post_serializer.rb @@ -38,8 +38,12 @@ class PostSerializer < ApplicationSerializer :cover_url, :source, :link, + :liked, :tags, - :published_at + :published_at, + :like_count, + :img_count, + :h2_list has_one :column end diff --git a/config/cant_cant_cant.yml b/config/cant_cant_cant.yml index 214f002..800fcfd 100644 --- a/config/cant_cant_cant.yml +++ b/config/cant_cant_cant.yml @@ -111,6 +111,8 @@ admin: &admin api/v1/admin/collections#reset_members: deny api/v1/admin/collections#update: allow + api/v1/admin/users#info: allow + dev: &dev <<: *all <<: *visitor diff --git a/config/deploy/sandbox.rb b/config/deploy/sandbox.rb index 32f137d..fe7e5aa 100644 --- a/config/deploy/sandbox.rb +++ b/config/deploy/sandbox.rb @@ -15,7 +15,7 @@ set :repo_url, "file:///home/#{fetch(:user)}/gitrepo/#{fetch(:application)}.git" set :deploy_to, "/home/#{fetch(:user)}/projects/#{fetch(:application)}" -set :puma_bind, "unix://#{shared_path}/tmp/sockets/#{fetch(:application)}-puma.sock" +set :puma_bind, "unix://#{shared_path}/tmp/sockets/puma.sock" set :puma_state, "#{shared_path}/tmp/pids/puma.state" set :puma_pid, "#{shared_path}/tmp/pids/puma.pid" set :puma_access_log, "#{release_path}/log/puma.error.log" diff --git a/config/routes.rb b/config/routes.rb index 2dc7609..8647227 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -4,10 +4,19 @@ namespace :api do namespace :v1, except: [:new, :edit] do root to: 'home#web_index' + + resources :ads, only: [:index] + resources :collections, only: [:index, :show] resources :posts, only: [:index, :show] do - resources :comments, only: [:index, :create] + post 'like', to: 'posts#like' + post 'unlike', to: 'posts#unlike' + + resources :comments, only: [:index, :create] do + post 'like', to: 'comments#like' + post 'unlike', to: 'comments#unlike' + end get 'by-tag/:tag', to: 'posts#index_by_tag', on: :collection end resources :columns, only: [:index, :show] @@ -15,6 +24,7 @@ namespace :admin do resources :posts do patch :publish, :draft, :close + post :toggle_recommended get :filter, :today_statistics, on: :collection get :comments, to: 'comments#index_for_commentable' @@ -22,6 +32,7 @@ get 'tags', to: 'tags#index' get 'permissions', to: 'users#permissions' + get 'info', to: 'users#info' resources :ads do get :comments, to: 'comments#index_for_commentable' diff --git a/db/migrate/20170531061242_create_likes.rb b/db/migrate/20170531061242_create_likes.rb new file mode 100644 index 0000000..9e46766 --- /dev/null +++ b/db/migrate/20170531061242_create_likes.rb @@ -0,0 +1,13 @@ +class CreateLikes < ActiveRecord::Migration[5.0] + def change + create_table :likes do |t| + t.string :user_id + t.string :target_type + t.string :target_id + + t.timestamps + end + + add_index :likes, [:user_id, :target_type, :target_id] + end +end diff --git a/db/migrate/20170601021434_add_fields_to_posts.rb b/db/migrate/20170601021434_add_fields_to_posts.rb new file mode 100644 index 0000000..cb9c4e6 --- /dev/null +++ b/db/migrate/20170601021434_add_fields_to_posts.rb @@ -0,0 +1,6 @@ +class AddFieldsToPosts < ActiveRecord::Migration[5.0] + def change + add_column :posts, :views, :integer + add_column :posts, :recommended, :boolean, index: true, default: false + end +end diff --git a/db/schema.rb b/db/schema.rb index f963ae3..5fc2002 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20170410165313) do +ActiveRecord::Schema.define(version: 20170601021434) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -72,7 +72,6 @@ t.integer "depth", default: 0 t.string "commentable_type" t.integer "commentable_id" - t.string "author" t.integer "parent_id" t.datetime "created_at", null: false t.datetime "updated_at", null: false @@ -100,6 +99,15 @@ t.datetime "updated_at", null: false end + create_table "likes", force: :cascade do |t| + t.string "user_id" + t.string "target_type" + t.string "target_id" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["user_id", "target_type", "target_id"], name: "index_likes_on_user_id_and_target_type_and_target_id", using: :btree + end + create_table "posts", force: :cascade do |t| t.string "title" t.text "abstract" @@ -120,6 +128,8 @@ t.datetime "created_at", null: false t.datetime "updated_at", null: false t.datetime "deleted_at" + t.integer "views" + t.boolean "recommended", default: false t.index ["authors"], name: "index_posts_on_authors", using: :btree t.index ["column_id"], name: "index_posts_on_column_id", using: :btree t.index ["deleted_at"], name: "index_posts_on_deleted_at", using: :btree