homeland 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- data/README.markdown +32 -0
- data/app/assets/images/reply.png +0 -0
- data/app/assets/javascripts/homeland.coffee +48 -0
- data/app/assets/stylesheets/homeland.scss +84 -0
- data/app/controllers/homeland/application_controller.rb +14 -0
- data/app/controllers/homeland/replies_controller.rb +20 -0
- data/app/controllers/homeland/topics_controller.rb +124 -0
- data/app/helpers/homeland/application_helper.rb +27 -0
- data/app/helpers/homeland/topics_helper.rb +8 -0
- data/app/models/homeland/node.rb +24 -0
- data/app/models/homeland/reply.rb +29 -0
- data/app/models/homeland/section.rb +17 -0
- data/app/models/homeland/topic.rb +43 -0
- data/app/views/homeland/replies/edit.html.erb +28 -0
- data/app/views/homeland/shared/_error_messages.html.erb +11 -0
- data/app/views/homeland/topics/_base.html.erb +4 -0
- data/app/views/homeland/topics/_form.html.erb +23 -0
- data/app/views/homeland/topics/_sidebar.html.erb +65 -0
- data/app/views/homeland/topics/_topic.html.erb +26 -0
- data/app/views/homeland/topics/edit.html.erb +11 -0
- data/app/views/homeland/topics/feed.builder +20 -0
- data/app/views/homeland/topics/index.html.erb +51 -0
- data/app/views/homeland/topics/new.html.erb +11 -0
- data/app/views/homeland/topics/show.html.erb +88 -0
- data/config/routes.rb +16 -0
- data/lib/generators/homeland/install/templates/initializer.rb +8 -0
- data/lib/generators/homeland/install/templates/javascripts/jquery.hotkeys.js +99 -0
- data/lib/generators/homeland/install/templates/javascripts/jquery.timeago.js +147 -0
- data/lib/generators/homeland/install/templates/locales/homeland.zh-CN.yml +14 -0
- data/lib/generators/homeland/install_generator.rb +45 -0
- data/lib/generators/homeland/views_generator.rb +23 -0
- data/lib/homeland.rb +6 -0
- data/lib/homeland/engine.rb +32 -0
- data/lib/homeland/setting.rb +37 -0
- metadata +91 -0
data/README.markdown
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
A new style forum for small communitys as a Rails Engine. you can mount this in you any apps.
|
2
|
+
|
3
|
+
## Install
|
4
|
+
|
5
|
+
* You need install *Ruby 1.9.2*, *Rubygems* and *Rails 3.1* and *Mongoid* first.
|
6
|
+
|
7
|
+
1. in you Gemfile:
|
8
|
+
|
9
|
+
gem "homeland"
|
10
|
+
|
11
|
+
1. Install thought this commands:
|
12
|
+
|
13
|
+
rails g homeland:install
|
14
|
+
|
15
|
+
2. change you application.html.erb
|
16
|
+
|
17
|
+
before:
|
18
|
+
|
19
|
+
<%= link_to "Home", root_path %>
|
20
|
+
<%= link_to "Posts", posts_path %>
|
21
|
+
|
22
|
+
after:
|
23
|
+
|
24
|
+
<%= link_to "Home", main_app.root_path %>
|
25
|
+
<%= link_to "Home", main_app.posts_path %>
|
26
|
+
|
27
|
+
|
28
|
+
## Generate views to custom
|
29
|
+
|
30
|
+
rails g homeland:views
|
31
|
+
|
32
|
+
Thanks [V2EX](http://v2ex.com) forum idea.
|
Binary file
|
@@ -0,0 +1,48 @@
|
|
1
|
+
#= require jquery.hotkeys
|
2
|
+
#= require jquery.timeago
|
3
|
+
#= require_self
|
4
|
+
# TopicsController 下所有页面的 JS 功能
|
5
|
+
window.Topics =
|
6
|
+
# 往话题编辑器里面插入图片代码
|
7
|
+
appendImageFromUpload : (srcs) ->
|
8
|
+
txtBox = $(".topic_body_text_area")
|
9
|
+
for src in srcs
|
10
|
+
txtBox.val("#{txtBox.val()}[img]#{src}[/img]\n")
|
11
|
+
txtBox.focus()
|
12
|
+
$("#add_image").jDialog.close()
|
13
|
+
|
14
|
+
# 上传图片
|
15
|
+
addImageClick : () ->
|
16
|
+
opts =
|
17
|
+
title:"插入图片"
|
18
|
+
width: 350
|
19
|
+
height: 145
|
20
|
+
content: '<iframe src="/photos/tiny_new" frameborder="0" style="width:330px; height:145px;"></iframe>',
|
21
|
+
close_on_body_click : false
|
22
|
+
|
23
|
+
$("#add_image").jDialog(opts)
|
24
|
+
return false
|
25
|
+
|
26
|
+
# 回复
|
27
|
+
reply : (floor,login) ->
|
28
|
+
reply_body = $("#reply_body")
|
29
|
+
new_text = "##{floor}楼 @#{login} "
|
30
|
+
if reply_body.val().trim().length == 0
|
31
|
+
new_text += ''
|
32
|
+
else
|
33
|
+
new_text = "\n#{new_text}"
|
34
|
+
reply_body.focus().val(reply_body.val() + new_text)
|
35
|
+
return false
|
36
|
+
|
37
|
+
# 高亮楼层
|
38
|
+
hightlightReply : (floor) ->
|
39
|
+
$("#replies .reply").removeClass("light")
|
40
|
+
$("#reply"+floor).addClass("light")
|
41
|
+
|
42
|
+
# Page.ready
|
43
|
+
$(document).ready ->
|
44
|
+
$("abbr.timeago").timeago()
|
45
|
+
$("textarea").bind "keydown","ctrl+return",(el) ->
|
46
|
+
if $(el.target).val().trim().length > 0
|
47
|
+
$("#reply form").submit()
|
48
|
+
|
@@ -0,0 +1,84 @@
|
|
1
|
+
.topics .topic .title a:link,
|
2
|
+
.topics .topic .title a:visited,
|
3
|
+
.node_topics .title a:link,
|
4
|
+
.node_topics .title a:visited {color:#2c64d7; text-decoration: none;}
|
5
|
+
.topics .topic .title a:hover,
|
6
|
+
.node_topics .title a:hover { text-decoration: underline; }
|
7
|
+
|
8
|
+
.sidebar p { margin-bottom:8px;}
|
9
|
+
|
10
|
+
.topics { }
|
11
|
+
.topics .topic { }
|
12
|
+
.topics .topic_line {border-top:1px solid #DDD; padding:10px 0; margin:0 -10px; vertical-align:top; }
|
13
|
+
#main .topics .topic h1 { font-size:22px; color:#333; margin-top:6px; text-align: left; line-height:100%;margin-bottom:8px;}
|
14
|
+
.topics .topic .avatar_large { width:80px; }
|
15
|
+
.topics .topic .avatar { width:56px; margin-left:10px; }
|
16
|
+
.topics .topic .infos { padding-right:90px;}
|
17
|
+
.topics .topic .right_info { padding-left:66px;}
|
18
|
+
.topics .topic .title { font-size:14px; }
|
19
|
+
.topics .topic .info { color:#999; margin-bottom:2px; }
|
20
|
+
.topics .topic .info a:link,
|
21
|
+
.topics .topic .info a:visited { color:#999; text-decoration: underline; }
|
22
|
+
.topics .body { line-height:160%; font-size:14px; margin-top:8px;}
|
23
|
+
.topics .body p { margin-bottom:10px; }
|
24
|
+
.topics .tools { text-align:right; }
|
25
|
+
.topics .tools a {margin-left:6px;}
|
26
|
+
.topics .topic .replies { width:70px; margin-right:10px; text-align:right; vertical-align:middle; }
|
27
|
+
.topics .topic .replies a.count,
|
28
|
+
.topics .topic .replies a.count:visited { line-height: 12px; font-weight: bold; color: white;
|
29
|
+
background-color: #1c7fdb; display: inline-block;
|
30
|
+
padding: 2px 10px 2px 10px;
|
31
|
+
-moz-border-radius: 12px; -webkit-border-radius: 12px;
|
32
|
+
margin: 18px 12px 0px 0px; text-decoration: none; }
|
33
|
+
.topics .topic .replies a.count:hover { color:write; background-color:#1F8AEE; }
|
34
|
+
.topics .topic .replies a.state0:link,
|
35
|
+
.topics .topic .replies a.state0:visited { background:#DDD; }
|
36
|
+
.topics .topic .replies a.state0:hover { background:#1dc115;}
|
37
|
+
.topics .topic .replies a.state2:link,
|
38
|
+
.topics .topic .replies a.state2:visited { background:#22D419; }
|
39
|
+
.topics .topic .replies a.state2:hover {background:#1dc115; }
|
40
|
+
.topics tr.topic td.replies { text-align:right; width:50px; font-size:14px; color:green;font-weight:bold; }
|
41
|
+
.more { padding:8px; border-top:1px solid #DDD; font-size:14px; text-align:right; }
|
42
|
+
|
43
|
+
/* topics list in node page */
|
44
|
+
.node_topics { margin-bottom:16px;}
|
45
|
+
.node_topics tr td { padding:8px; }
|
46
|
+
.node_topics tr.head td { padding-top:14px; color:#CCC; font-weight:bold; font-size:12px; }
|
47
|
+
.node_topics tr.topic td.title { font-size:14px; font-weight:bold; }
|
48
|
+
.node_topics tr.topic td.author { width:80px;}
|
49
|
+
.node_topics tr.topic td.author a { color:#666; font-weight:bold; }
|
50
|
+
|
51
|
+
.no_result { margin-bottom:20px; text-align:center; }
|
52
|
+
|
53
|
+
#node_info { padding:8px;margin-bottom:8px;padding-top:0px; }
|
54
|
+
#node_info h1 { font-size:18px; color:000; display:inline; margin-right:5px; }
|
55
|
+
#node_info p { margin-bottom:8px; }
|
56
|
+
#node_info p.summary { color:#999; }
|
57
|
+
|
58
|
+
#replies {}
|
59
|
+
#replies .total { padding-bottom:6px; color:#999; }
|
60
|
+
#replies .reply { border-top:1px solid #DDD; padding:8px 10px; margin:0 -10px;}
|
61
|
+
#replies .reply.light { background:#EFFCEE;}
|
62
|
+
#replies .reply .face { padding-right:8px; width:48px; margin-left:0; }
|
63
|
+
#replies .reply .infos { padding-left:58px;}
|
64
|
+
#replies .reply .info { margin-top:4px; color:#999;height:20px;}
|
65
|
+
#replies .reply .info a:link,
|
66
|
+
#replies .reply .info a:visited { color:#999; text-decoration: underline;}
|
67
|
+
#replies .reply .info .name { float:left; }
|
68
|
+
#replies .reply .info .time { float:right; text-align:right; }
|
69
|
+
#replies .reply .info a.reply_link img { vertical-align:middle;}
|
70
|
+
#replies .reply .body { line-height:180%; font-size:14px; text-overflow: ellipsis;}
|
71
|
+
#replies .reply .body img { max-width:622px;}
|
72
|
+
|
73
|
+
#reply.form textarea { width:578px; }
|
74
|
+
|
75
|
+
.edit_tools { text-align:right; margin-bottom:3px;}
|
76
|
+
|
77
|
+
.totals ul li { line-height:150%;}
|
78
|
+
|
79
|
+
#topic_new_tip li { margin-bottom:6px;}
|
80
|
+
|
81
|
+
.body a.at_floor { color:#376B43; }
|
82
|
+
.body a.at_user { }
|
83
|
+
|
84
|
+
.topic_body_text_area { width:600px;}
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
class Homeland::ApplicationController < ApplicationController
|
3
|
+
helper_method :current_user
|
4
|
+
|
5
|
+
alias_method :origin_current_user, Homeland.current_user_method.to_sym
|
6
|
+
def current_user
|
7
|
+
origin_current_user
|
8
|
+
end
|
9
|
+
|
10
|
+
def homeland_require_user
|
11
|
+
self.send(Homeland.require_user_method)
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
module Homeland
|
3
|
+
class RepliesController < Homeland::ApplicationController
|
4
|
+
before_filter :homeland_require_user
|
5
|
+
|
6
|
+
def edit
|
7
|
+
@reply = current_user.replies.find(params[:id])
|
8
|
+
end
|
9
|
+
|
10
|
+
def update
|
11
|
+
@reply = current_user.replies.find(params[:id])
|
12
|
+
|
13
|
+
if @reply.update_attributes(params[:reply])
|
14
|
+
redirect_to(topic_path(@reply.topic_id), :notice => '回帖删除成功.')
|
15
|
+
else
|
16
|
+
render :action => "edit"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
module Homeland
|
3
|
+
class TopicsController < Homeland::ApplicationController
|
4
|
+
before_filter :homeland_require_user, :only => [:new,:edit,:create,:update,:destroy,:reply]
|
5
|
+
before_filter :init_list_sidebar, :only => [:index,:recent,:show,:cate,:search]
|
6
|
+
caches_page :feed, :expires_in => 1.hours
|
7
|
+
|
8
|
+
private
|
9
|
+
def init_list_sidebar
|
10
|
+
if !fragment_exist? "topic/init_list_sidebar/hot_nodes"
|
11
|
+
@hot_nodes = Node.hots.limit(20)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
public
|
16
|
+
# GET /topics
|
17
|
+
# GET /topics.xml
|
18
|
+
def index
|
19
|
+
@topics = Topic.last_actived.limit(10)
|
20
|
+
@sections = Section.all
|
21
|
+
|
22
|
+
end
|
23
|
+
|
24
|
+
def feed
|
25
|
+
@topics = Topic.recent.limit(20)
|
26
|
+
response.headers['Content-Type'] = 'application/rss+xml'
|
27
|
+
render :layout => false
|
28
|
+
end
|
29
|
+
|
30
|
+
def node
|
31
|
+
@node = Node.find(params[:id])
|
32
|
+
@topics = @node.topics.last_actived.paginate(:page => params[:page],:per_page => 50)
|
33
|
+
|
34
|
+
render :action => "index"
|
35
|
+
end
|
36
|
+
|
37
|
+
def recent
|
38
|
+
@topics = Topic.recent.limit(50)
|
39
|
+
|
40
|
+
render :action => "index"
|
41
|
+
end
|
42
|
+
|
43
|
+
def search
|
44
|
+
result = Redis::Search.query("Topic", params[:key], :limit => 500)
|
45
|
+
ids = result.collect { |r| r["id"] }
|
46
|
+
@topics = Topic.find(ids).paginate(:page => params[:page], :per_page => 20)
|
47
|
+
|
48
|
+
render :action => "index"
|
49
|
+
end
|
50
|
+
|
51
|
+
def show
|
52
|
+
@topic = Topic.find(params[:id])
|
53
|
+
@topic.hit!
|
54
|
+
@node = @topic.node
|
55
|
+
@replies = @topic.replies.all
|
56
|
+
|
57
|
+
end
|
58
|
+
|
59
|
+
# GET /topics/new
|
60
|
+
# GET /topics/new.xml
|
61
|
+
def new
|
62
|
+
@topic = Topic.new
|
63
|
+
@topic.node_id = params[:node]
|
64
|
+
@node = Node.find(params[:node])
|
65
|
+
end
|
66
|
+
|
67
|
+
def reply
|
68
|
+
@topic = Topic.find(params[:id])
|
69
|
+
@reply = @topic.replies.build(params[:reply])
|
70
|
+
@reply.user_id = current_user.id
|
71
|
+
if @reply.save
|
72
|
+
flash[:notice] = "回复成功。"
|
73
|
+
else
|
74
|
+
flash[:notice] = @reply.errors.full_messages.join("<br />")
|
75
|
+
end
|
76
|
+
redirect_to topic_path(params[:id],:anchor => 'reply')
|
77
|
+
end
|
78
|
+
|
79
|
+
# GET /topics/1/edit
|
80
|
+
def edit
|
81
|
+
@topic = current_user.topics.find(params[:id])
|
82
|
+
@node = @topic.node
|
83
|
+
end
|
84
|
+
|
85
|
+
# POST /topics
|
86
|
+
# POST /topics.xml
|
87
|
+
def create
|
88
|
+
pt = params[:topic]
|
89
|
+
@topic = Topic.new(pt)
|
90
|
+
@topic.user_id = current_user.id
|
91
|
+
@topic.node_id = params[:node] || pt[:node_id]
|
92
|
+
|
93
|
+
if @topic.save
|
94
|
+
redirect_to(topic_path(@topic.id), :notice => '帖子创建成功.')
|
95
|
+
else
|
96
|
+
render :action => "new"
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# PUT /topics/1
|
101
|
+
# PUT /topics/1.xml
|
102
|
+
def update
|
103
|
+
@topic = current_user.topics.find(params[:id])
|
104
|
+
pt = params[:topic]
|
105
|
+
@topic.node_id = pt[:node_id]
|
106
|
+
@topic.title = pt[:title]
|
107
|
+
@topic.body = pt[:body]
|
108
|
+
|
109
|
+
if @topic.save
|
110
|
+
redirect_to(topic_path(@topic.id), :notice => '帖子修改成功.')
|
111
|
+
else
|
112
|
+
render :action => "edit"
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
# DELETE /topics/1
|
117
|
+
# DELETE /topics/1.xml
|
118
|
+
def destroy
|
119
|
+
@topic = current_user.topics.find(params[:id])
|
120
|
+
@topic.destroy
|
121
|
+
redirect_to(topics_path, :notice => '帖子删除成功.')
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
module Homeland
|
3
|
+
module ApplicationHelper
|
4
|
+
def owner?(item)
|
5
|
+
return false if item.blank?
|
6
|
+
return if current_user.blank?
|
7
|
+
item.user_id == current_user.id
|
8
|
+
end
|
9
|
+
|
10
|
+
def timeago(time, options = {})
|
11
|
+
options[:class] ||= "timeago"
|
12
|
+
content_tag(:abbr, time.to_s, options.merge(:title => time.getutc.iso8601)) if time
|
13
|
+
end
|
14
|
+
|
15
|
+
def user_name_tag(user,options = {})
|
16
|
+
return "" if user.blank?
|
17
|
+
link_to_if(Homeland.user_path_method, user.name, [main_app, user])
|
18
|
+
end
|
19
|
+
|
20
|
+
def user_avatar_tag(user)
|
21
|
+
return "" if user.blank?
|
22
|
+
img = image_tag(user.try(Homeland.user_avatar_method.to_sym))
|
23
|
+
link_to_if(Homeland.user_path_method, raw(img), [main_app, user])
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
module Homeland
|
3
|
+
class Node
|
4
|
+
include Mongoid::Document
|
5
|
+
include Mongoid::Timestamps
|
6
|
+
|
7
|
+
field :name
|
8
|
+
field :summary
|
9
|
+
field :sort, :type => Integer, :default => 0
|
10
|
+
field :topics_count, :type => Integer, :default => 0
|
11
|
+
|
12
|
+
has_many :topics, :class_name => "Homeland::Topic"
|
13
|
+
belongs_to :section, :class_name => "Homeland::Section"
|
14
|
+
|
15
|
+
validates_presence_of :name, :summary
|
16
|
+
validates_uniqueness_of :name
|
17
|
+
|
18
|
+
index :topics_count
|
19
|
+
index :sort
|
20
|
+
|
21
|
+
scope :hots, desc(:topics_count)
|
22
|
+
scope :sorted, desc(:sort)
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
module Homeland
|
3
|
+
class Reply
|
4
|
+
include Mongoid::Document
|
5
|
+
include Mongoid::Timestamps
|
6
|
+
include Mongoid::Paranoia
|
7
|
+
|
8
|
+
field :body
|
9
|
+
|
10
|
+
belongs_to :user, :inverse_of => :replies, :class_name => Homeland.user_class.to_s
|
11
|
+
belongs_to :topic, :inverse_of => :replies, :class_name => "Homeland::Topic"
|
12
|
+
|
13
|
+
attr_protected :user_id, :topic_id
|
14
|
+
|
15
|
+
index :topic_id
|
16
|
+
index :user_id
|
17
|
+
|
18
|
+
validates_presence_of :body
|
19
|
+
scope :recent, desc(:_id)
|
20
|
+
|
21
|
+
after_create :update_parent_last_replied
|
22
|
+
def update_parent_last_replied
|
23
|
+
self.topic.replied_at = Time.now
|
24
|
+
self.topic.last_reply_user_id = self.user_id
|
25
|
+
self.topic.replies_count = self.topic.replies.count
|
26
|
+
self.topic.save
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
module Homeland
|
3
|
+
class Section
|
4
|
+
include Mongoid::Document
|
5
|
+
include Mongoid::Timestamps
|
6
|
+
|
7
|
+
field :name
|
8
|
+
field :sort, :type => Integer, :default => 0
|
9
|
+
has_many :nodes, :dependent => :destroy, :class_name => "Homeland::Node"
|
10
|
+
|
11
|
+
validates_presence_of :name
|
12
|
+
validates_uniqueness_of :name
|
13
|
+
|
14
|
+
|
15
|
+
default_scope desc(:sort)
|
16
|
+
end
|
17
|
+
end
|