qa-rails 0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.markdown +27 -0
- data/app/assets/images/askbg.png +0 -0
- data/app/assets/images/dot2.png +0 -0
- data/app/assets/images/learn_btn.png +0 -0
- data/app/assets/javascripts/qa-rails-link.js +7 -0
- data/app/assets/javascripts/qa-rails.js +273 -0
- data/app/controllers/qa/markdown_controller.rb +24 -0
- data/app/controllers/qa/replies_controller.rb +23 -0
- data/app/controllers/qa/topics_controller.rb +42 -0
- data/app/controllers/qa_controller.rb +34 -0
- data/app/models/qa_reply.rb +8 -0
- data/app/models/qa_topic.rb +24 -0
- data/app/views/qa/_paginate.html.haml +2 -0
- data/app/views/qa/_qa.html.haml +156 -0
- data/app/views/qa/_textarea.html.haml +25 -0
- data/config/routes.rb +11 -0
- data/db/migrate/20130408062843_create_qa.rb +34 -0
- data/lib/qa-rails.rb +43 -0
- data/lib/rails_engine.rb +16 -0
- data/qa-rails.gemspec +20 -0
- metadata +176 -0
data/README.markdown
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
Rails 迷你问答论坛插件
|
2
|
+
==================================================================
|
3
|
+
|
4
|
+
### 使用
|
5
|
+
1. 添加qa-rails数据库表,bundle exec rake db:migrate
|
6
|
+
2. 在view里引用论坛插件,示例:
|
7
|
+
|
8
|
+
```ruby
|
9
|
+
qa_setup "#content .SectionBox .SectionList .forum",
|
10
|
+
@current_lesson.id,
|
11
|
+
:jsfun_user_avatar_url => "eoe.jsfun_user_avatar_url",
|
12
|
+
:jsfun_user_info => "function() { return {uid:eoe.uid, uname:eoe.uname}; }",
|
13
|
+
:empty_topics_text => 'Hey, 还没有问题,如果你有和本课相关但还不明白的问题,请在这里提问 :)',
|
14
|
+
:notice_div => '$(".SectionBox[anchor=qa] .SectionHead .hasNew")'
|
15
|
+
```
|
16
|
+
|
17
|
+
### 其他论坛参考
|
18
|
+
* https://github.com/radar/forem
|
19
|
+
* https://github.com/discourse/discourse vendor下的gems值得一看
|
20
|
+
|
21
|
+
### 贡献人员名单
|
22
|
+
* 产品: @iceskysl
|
23
|
+
* rails, javascript: @mvj3
|
24
|
+
* css: @xiang97
|
25
|
+
|
26
|
+
### License
|
27
|
+
MIT
|
Binary file
|
Binary file
|
Binary file
|
@@ -0,0 +1,273 @@
|
|
1
|
+
$(document).ready(function() {
|
2
|
+
if (!$.fn.autosize) { console.log("warning: please use git://github.com/jackmoore/autosize.git to auto resize textarea"); $.fn.autosize = function() {}; }
|
3
|
+
if (!$.fn.timeago) { console.log("warning: please use git://github.com/ashchan/smart-time-ago.git to compute time ago"); $.fn.timeago = function() {}; }
|
4
|
+
|
5
|
+
// textarea 自适应高度
|
6
|
+
var autosize = function() {
|
7
|
+
setTimeout(function() {
|
8
|
+
$("#content .forum textarea").autosize();
|
9
|
+
}, 5000);
|
10
|
+
};
|
11
|
+
|
12
|
+
var scroll_to_div = function(_f) {
|
13
|
+
// 用dom高度做动态计算
|
14
|
+
var dom = $(QARails.domid).closest(".SectionList");
|
15
|
+
var _top = dom.offset().top - 50;
|
16
|
+
var _micro_seconds = dom.height() - 500;
|
17
|
+
// 自动滚动到论坛顶部,以防止帖子超过一屏
|
18
|
+
$("html, body").animate({ scrollTop: _top}, _micro_seconds);
|
19
|
+
autosize();
|
20
|
+
};
|
21
|
+
var is_first_time_load_qa = true;
|
22
|
+
|
23
|
+
// 设定 检查新内容的时间相关
|
24
|
+
var now_int = function() {
|
25
|
+
return Math.round(new Date().getTime() / 1000);
|
26
|
+
};
|
27
|
+
var last_checked_at = now_int();
|
28
|
+
|
29
|
+
// 论坛框架视图
|
30
|
+
var QARootView = Backbone.View.extend({
|
31
|
+
events: {
|
32
|
+
"click .convert_tags span": "edit_or_preview",
|
33
|
+
"click .markdownBox a.reply": "comment",
|
34
|
+
"click .markdownBox a.ask": "ask",
|
35
|
+
"click .pagination span": "ajax_paginate"
|
36
|
+
},
|
37
|
+
// 编辑或预览
|
38
|
+
edit_or_preview: function(event) {
|
39
|
+
event.preventDefault();
|
40
|
+
if (!this.process_data(event)) { return false; }
|
41
|
+
var dom = $(event.target);
|
42
|
+
// 不能重复点击
|
43
|
+
if (dom.hasClass('on')) { return false; }
|
44
|
+
|
45
|
+
// 引用相对的输入框
|
46
|
+
var textarea_dom = dom.closest(".convert_tags").siblings(".textareaBox");
|
47
|
+
|
48
|
+
// 切换显示
|
49
|
+
textarea_dom.find(".preview_box, .textarea_text").toggleClass('hide');
|
50
|
+
this.$el.find(".convert_tags span").toggleClass('on');
|
51
|
+
var that = this;
|
52
|
+
|
53
|
+
// 判断是预览
|
54
|
+
if (dom.hasClass('preview_button')) {
|
55
|
+
// 传输数据到markdown渲染接口
|
56
|
+
var _data = {data: this.content};
|
57
|
+
$.post("/qa/markdown", _data, function(opts) {
|
58
|
+
console.log(opts);
|
59
|
+
// 渲染结果
|
60
|
+
textarea_dom.find('.preview_content').html(opts.data);
|
61
|
+
});
|
62
|
+
}
|
63
|
+
},
|
64
|
+
btn_event: undefined,
|
65
|
+
current_textarea_dom: undefined,
|
66
|
+
markdown_editor_type: undefined,
|
67
|
+
content: undefined,
|
68
|
+
title: undefined,
|
69
|
+
last_ask_title: undefined,
|
70
|
+
last_comment_content: undefined,
|
71
|
+
process_data: function(event) {
|
72
|
+
this.btn_event = event;
|
73
|
+
var dom = $(this.btn_event.target).closest(".markdownBox").children(".convert_tags").siblings(".textareaBox");
|
74
|
+
this.markdown_editor_type = dom.find('.btn').attr('type');
|
75
|
+
this.current_textarea_dom = dom.find('textarea');
|
76
|
+
var v = this.current_textarea_dom.val().split("\n");
|
77
|
+
|
78
|
+
// remove blank line at the beign and the end
|
79
|
+
var _length_pre, _length;
|
80
|
+
while (_length_pre != v.length) {
|
81
|
+
_length_pre = v.length;
|
82
|
+
if (_.isEmpty(v[0])) { v.shift(); }
|
83
|
+
if (_.isEmpty(v[v.length-1])) { v.pop(); }
|
84
|
+
}
|
85
|
+
|
86
|
+
switch(this.markdown_editor_type) {
|
87
|
+
case 'ask':
|
88
|
+
this.content = v.slice(1, v.length).join("\n");
|
89
|
+
this.title = v[0];
|
90
|
+
dom.find('.preview_title').html(this.title);
|
91
|
+
if (($.trim(this.title).length === 0) || ($.trim(this.content).length === 0)) {
|
92
|
+
alert("至少包含标题和内容的两行内容!");
|
93
|
+
return false;
|
94
|
+
}
|
95
|
+
break;
|
96
|
+
case 'reply':
|
97
|
+
this.content = v.join("\n");
|
98
|
+
if ($.trim(this.content).length === 0) {
|
99
|
+
alert("回复内容不能为空!");
|
100
|
+
return false;
|
101
|
+
}
|
102
|
+
break;
|
103
|
+
}
|
104
|
+
|
105
|
+
return true;
|
106
|
+
},
|
107
|
+
ask: function(event) {
|
108
|
+
if (!this.process_data(event)) { return false; }
|
109
|
+
|
110
|
+
// no dup ask
|
111
|
+
if (QARootView.last_ask_title === this.title) { alert("不能重复提问!"); return false; }
|
112
|
+
QARootView.last_ask_title = this.title;
|
113
|
+
|
114
|
+
var data = $.extend(QARails.default_ajax_data, {title: this.title, content: this.content});
|
115
|
+
var that = this;
|
116
|
+
$.post("/qa/topics.json", data).done(function(data) {
|
117
|
+
QARootView.render();
|
118
|
+
});
|
119
|
+
|
120
|
+
},
|
121
|
+
comment: function(event) {
|
122
|
+
if (!this.process_data(event)) { return false; }
|
123
|
+
// no dup ask
|
124
|
+
if (QARootView.last_comment_content === this.content) { alert("不能重复回复!"); return false; }
|
125
|
+
QARootView.last_comment_content = this.content;
|
126
|
+
|
127
|
+
var topic_id = $(event.target).closest('.show').attr('topic_id');
|
128
|
+
var data = $.extend(QARails.default_ajax_data, {topic_id: topic_id, content: this.content});
|
129
|
+
$.ajax({
|
130
|
+
url: "/qa/topics/" + topic_id + "/replies.json",
|
131
|
+
type: "POST",
|
132
|
+
data: data
|
133
|
+
}).done(function(data) {
|
134
|
+
QAShowView.render(data);
|
135
|
+
});
|
136
|
+
},
|
137
|
+
// ajax分页
|
138
|
+
ajax_paginate: function(event) {
|
139
|
+
var dom = $(event.target);
|
140
|
+
if (!dom.is('a')) { dom = dom.find('a'); }
|
141
|
+
var url = "/qa/topics.json?" + dom.attr('href').split('?')[1];
|
142
|
+
|
143
|
+
$.getJSON(url).done(function(data) {
|
144
|
+
QAListTopicView.render(data);
|
145
|
+
scroll_to_div();
|
146
|
+
});
|
147
|
+
|
148
|
+
return false;
|
149
|
+
},
|
150
|
+
template: _.template($("#qa_list_template").html()),
|
151
|
+
render: function() {
|
152
|
+
this.$el.html(this.template());
|
153
|
+
return this;
|
154
|
+
}
|
155
|
+
});
|
156
|
+
/* 渲染列表视图 */
|
157
|
+
QARootView.render = function(page) {
|
158
|
+
page = page || QARails.kaminari_config.page;
|
159
|
+
var url = "/qa/topics.json?category_id=" +QARails.category_id + "&page=" + page + "&per=" + QARails.kaminari_config.per;
|
160
|
+
$.getJSON(url, function(data) {
|
161
|
+
console.log("qa-rails", data);
|
162
|
+
$(QARails.domid).html((new QARootView()).render().el);
|
163
|
+
QAListTopicView.render(data);
|
164
|
+
|
165
|
+
setTimeout(function() {
|
166
|
+
$("#qa .loading").hide();
|
167
|
+
$('#qa .topic_list').removeClass('hide');
|
168
|
+
|
169
|
+
if (!is_first_time_load_qa) {
|
170
|
+
scroll_to_div();
|
171
|
+
is_first_time_load_qa = false;
|
172
|
+
}
|
173
|
+
}, 1000);
|
174
|
+
|
175
|
+
// 重新更新时间点
|
176
|
+
last_checked_at = now_int();
|
177
|
+
});
|
178
|
+
};
|
179
|
+
|
180
|
+
/* 论坛主题列表渲染视图 */
|
181
|
+
var QAListTopicView = Backbone.View.extend({
|
182
|
+
events: {
|
183
|
+
"click li": 'topic_show'
|
184
|
+
},
|
185
|
+
/* 点击显示主题详情 */
|
186
|
+
topic_show: function(event) {
|
187
|
+
var topic_id = $(event.target).closest('li').attr('topic_id');
|
188
|
+
$.getJSON("/qa/topics/" + topic_id + ".json", function(data) {
|
189
|
+
QAShowView.render(data).show();
|
190
|
+
scroll_to_div();
|
191
|
+
});
|
192
|
+
},
|
193
|
+
template: _.template($("#qa_list_topic_template").html()),
|
194
|
+
initialize: function(opts) {
|
195
|
+
this.topics = opts.topics;
|
196
|
+
return this;
|
197
|
+
},
|
198
|
+
render: function() {
|
199
|
+
this.$el.html(this.template());
|
200
|
+
return this;
|
201
|
+
}
|
202
|
+
});
|
203
|
+
QAListTopicView.render = function(data) {
|
204
|
+
var list_topic_view = new QAListTopicView(data);
|
205
|
+
$("#qa ul").html(list_topic_view.render().el);
|
206
|
+
$('#qa ul .timeago').timeago();
|
207
|
+
$('#qa #kaminari').html(data.paginate_html);
|
208
|
+
return list_topic_view;
|
209
|
+
};
|
210
|
+
|
211
|
+
|
212
|
+
/* 论坛主题回复渲染视图 */
|
213
|
+
var QAShowView = Backbone.View.extend({
|
214
|
+
events: {
|
215
|
+
"click .back_to_list" : "back_to_list"
|
216
|
+
},
|
217
|
+
show: function() {
|
218
|
+
$("#qa .topic_list, #qa .topic_show").toggleClass('hide');
|
219
|
+
},
|
220
|
+
back_to_list: function() {
|
221
|
+
var page = parseInt($(QARails.domid).find('.pagination span.current').text(), 10);
|
222
|
+
QARootView.render(page);
|
223
|
+
},
|
224
|
+
template: _.template($("#qa_show_template").html()),
|
225
|
+
initialize: function(opts) {
|
226
|
+
this.topic = opts.topic;
|
227
|
+
this.replies = opts.replies;
|
228
|
+
return this;
|
229
|
+
},
|
230
|
+
render: function() {
|
231
|
+
this.$el.html(this.template());
|
232
|
+
return this;
|
233
|
+
}
|
234
|
+
});
|
235
|
+
QAShowView.render = function(data) {
|
236
|
+
var show_view = new QAShowView(data);
|
237
|
+
$("#qa .topic_show").html(show_view.render().el);
|
238
|
+
$("#qa .topic_show .timeago").timeago();
|
239
|
+
return show_view;
|
240
|
+
};
|
241
|
+
|
242
|
+
// 初始化
|
243
|
+
QARootView.render();
|
244
|
+
autosize();
|
245
|
+
|
246
|
+
// 问答新内容的轮询检查
|
247
|
+
// 流程: 1, 设当前次刷新topic list的时间为最后时间点,然后轮询检查该时间点到现在有无新内容。2, 如果有,更新内容后才又开始 1 步骤; 如果无,继续轮询。
|
248
|
+
setTimeout(function() {
|
249
|
+
var notice_div = $(QARails.notice_div);
|
250
|
+
if (!_.isEmpty(notice_div)) {
|
251
|
+
// 点击加载新列表
|
252
|
+
notice_div.click(function() {
|
253
|
+
QARootView.render();
|
254
|
+
notice_div.html("");
|
255
|
+
});
|
256
|
+
|
257
|
+
// 轮询检查
|
258
|
+
setInterval(function() {
|
259
|
+
var url = "/qa/topics/new_topics_count.json?category_id=" + QARails.category_id + '&time_begin=' + last_checked_at;
|
260
|
+
$.getJSON(url, function(data) {
|
261
|
+
if (data.new_topcis_count > 0) {
|
262
|
+
notice_div.html("有" + data.new_topcis_count + "条新内容 点击查看");
|
263
|
+
} else {
|
264
|
+
notice_div.html("");
|
265
|
+
console.log(url + " 没有新内容");
|
266
|
+
}
|
267
|
+
});
|
268
|
+
}, 60*1000);
|
269
|
+
}
|
270
|
+
}, 60*1000);
|
271
|
+
|
272
|
+
|
273
|
+
});
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
class Qa::MarkdownController < QaController
|
4
|
+
# 接口参数,接受一个data键值,以POST form形式提交,例如
|
5
|
+
# data = 'markdown document'
|
6
|
+
def create
|
7
|
+
@success = false
|
8
|
+
begin
|
9
|
+
if params[:data].blank?
|
10
|
+
@data = "data参数不能为空"
|
11
|
+
else
|
12
|
+
@data = "<div class='markdown'>#{MarkdownTopicConverter.format(params[:data].to_s)}</div>"
|
13
|
+
@success = true
|
14
|
+
end
|
15
|
+
rescue => e
|
16
|
+
logger.info [e, e.backtrace].flatten.join("\n")
|
17
|
+
end
|
18
|
+
|
19
|
+
respond_to do |format|
|
20
|
+
format.json { render :json => {:data => @data, :status => !!@success} }
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
class Qa::RepliesController < QaController
|
4
|
+
|
5
|
+
def index
|
6
|
+
end
|
7
|
+
|
8
|
+
def create
|
9
|
+
(render :nothing => true; return false) if params[:content].blank?
|
10
|
+
|
11
|
+
@topic = QATopic.find(params[:topic_id])
|
12
|
+
@reply = QAReply.create(default_data.merge(:topic_id => @topic.id))
|
13
|
+
|
14
|
+
@replies = @topic.replies.map {|reply| reply.content = reply._content_markdown_cache; reply }
|
15
|
+
@topic.update_attributes :replies_count => @replies.count
|
16
|
+
@topic.content = @topic._content_markdown_cache # after update_attributes
|
17
|
+
|
18
|
+
respond_to do |format|
|
19
|
+
format.json { render :json => {:topic => @topic, :replies => @topic.replies}, :status => 200}
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
class Qa::TopicsController < QaController
|
4
|
+
|
5
|
+
def index
|
6
|
+
@qa_options = {:category_id => params[:category_id]}
|
7
|
+
|
8
|
+
fetch_topic_list
|
9
|
+
end
|
10
|
+
|
11
|
+
def create
|
12
|
+
(render :nothing => true; return false) if (params[:title].blank? || params[:content].blank?)
|
13
|
+
|
14
|
+
@topic = QATopic.create(default_data.merge(:title => params[:title], :category_id => params[:category_id]))
|
15
|
+
|
16
|
+
fetch_topic_list
|
17
|
+
end
|
18
|
+
|
19
|
+
def show
|
20
|
+
@topic = QATopic.find(params[:id])
|
21
|
+
QATopic.increment_counter :view_count, @topic.id
|
22
|
+
@topic.view_count += 1
|
23
|
+
@topic.content = @topic._content_markdown_cache
|
24
|
+
@replies = @topic.replies.map {|reply| reply.content = reply._content_markdown_cache; reply }
|
25
|
+
|
26
|
+
respond_to do |format|
|
27
|
+
format.json { render :json => {:topic => @topic, :replies => @replies}, :status => 200}
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def destroy
|
32
|
+
end
|
33
|
+
|
34
|
+
def new_topics_count
|
35
|
+
@new_topcis_count = QATopic.where(:category_id => params[:category_id]).where("created_at >= ?", Time.at(params[:time_begin].to_i)).count
|
36
|
+
|
37
|
+
respond_to do |format|
|
38
|
+
format.json { render :json => {:new_topcis_count => @new_topcis_count}, :status => 200 }
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
class QaController < ApplicationController
|
4
|
+
before_filter do
|
5
|
+
(render :nothing => true, :status => 403; return false) if user_login?
|
6
|
+
# TODO auth user in category_id
|
7
|
+
end
|
8
|
+
|
9
|
+
private
|
10
|
+
def default_data
|
11
|
+
{:uid => current_user.uid, :uname => current_user.uname, :content => params[:content]}
|
12
|
+
end
|
13
|
+
|
14
|
+
def fetch_topic_list
|
15
|
+
self.formats = [:html]
|
16
|
+
@paginate_html = render_to_string(:partial => "qa/paginate")
|
17
|
+
self.formats = [:json]
|
18
|
+
|
19
|
+
@topics = QATopic.where(:category_id => params[:category_id]).order("created_at DESC").page(params[:page] || 1).per(params[:per] || 5)
|
20
|
+
@topics.map do |topic|
|
21
|
+
topic.content = topic._content_markdown_cache
|
22
|
+
end
|
23
|
+
|
24
|
+
respond_to do |format|
|
25
|
+
format.json { render :json => {:topics => @topics.as_json(:methods => [:last_reply]), :paginate_html => @paginate_html}, :status => 200}
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def user_login?
|
30
|
+
current_user.uid.to_i.zero?
|
31
|
+
end
|
32
|
+
|
33
|
+
|
34
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
class QATopic < ActiveRecord::Base
|
4
|
+
acts_as_paranoid :column => 'is_delete', :column_type => 'boolean'
|
5
|
+
attr_accessible(*self.column_names)
|
6
|
+
|
7
|
+
has_many :replies, :class_name => QAReply, :foreign_key => :topic_id
|
8
|
+
|
9
|
+
# TODO expire from list if reply is delete
|
10
|
+
def last_reply reload = false
|
11
|
+
_r = Rails.cache.fetch self.last_reply_cache_key do
|
12
|
+
_last_reply.to_json
|
13
|
+
end
|
14
|
+
JSON.parse(_r)
|
15
|
+
end
|
16
|
+
def _last_reply
|
17
|
+
self.replies.last || {}
|
18
|
+
end
|
19
|
+
|
20
|
+
def last_reply_cache_key; "qa:topic_#{self.id}:last_reply" end
|
21
|
+
def t; self.created_at.strftime("%Y%m%d-%H%M%S") end
|
22
|
+
|
23
|
+
include QA_Rails::ContentMarkdownCache
|
24
|
+
end
|
@@ -0,0 +1,156 @@
|
|
1
|
+
- if protect_against_forgery?
|
2
|
+
= javascript_tag "var AUTH_TOKEN = #{form_authenticity_token.to_json};"
|
3
|
+
|
4
|
+
%script{:type=>"text/template", :id=>"qa_list_template"}
|
5
|
+
%div#qa
|
6
|
+
%div.loading
|
7
|
+
%div.topic_list.hide
|
8
|
+
%div.topic_content.fix
|
9
|
+
%ul
|
10
|
+
#kaminari.fix
|
11
|
+
= render :partial => "qa/textarea", :locals => {:is_ask => true}
|
12
|
+
%div.topic_show.hide
|
13
|
+
|
14
|
+
%script{:type=>"text/template", :id=>"qa_list_topic_template"}
|
15
|
+
= raw("<% if (_.isEmpty(this.topics)) { %>")
|
16
|
+
%div.empty_text
|
17
|
+
= raw("<%= QARails.empty_topics_text %>")
|
18
|
+
= raw("<% } else { %>")
|
19
|
+
= raw("<% _.each(this.topics, function(topic) { %>")
|
20
|
+
%li{:topic_id => raw("<%= topic.id %>")}
|
21
|
+
%a.user
|
22
|
+
%span.avatar
|
23
|
+
%img{:src => raw("<%= QARails.jsfun_user_avatar_url(topic.uid) %>")}
|
24
|
+
%div.box
|
25
|
+
%span.title.pointer= raw("<%= QARails.escapeHTML(topic.title) %>")
|
26
|
+
%div
|
27
|
+
%a.uname
|
28
|
+
= raw("<%= topic.uname %>")
|
29
|
+
|
30
|
+
= raw("<% if (_.isEmpty(topic.last_reply)) { %>")
|
31
|
+
= "于"
|
32
|
+
%time.timeago{:datetime => raw("<%= topic.created_at %>")}
|
33
|
+
= "发布"
|
34
|
+
= raw("<% } else { %>")
|
35
|
+
= "最后由"
|
36
|
+
= raw("<%= topic.last_reply.uname %>")
|
37
|
+
= "于"
|
38
|
+
%time.timeago{:datetime => raw("<%= topic.last_reply.created_at %>")}
|
39
|
+
= "回复"
|
40
|
+
= raw("<% } %>")
|
41
|
+
|
42
|
+
%em= raw("<%= topic.replies_count %>")
|
43
|
+
= raw("<% }); %>")
|
44
|
+
= raw("<% } %>")
|
45
|
+
|
46
|
+
|
47
|
+
%script{:type=>"text/template", :id=>"qa_show_template"}
|
48
|
+
%div.back_to_list.pointer 后退
|
49
|
+
%div.show{:topic_id => raw("<%= this.topic.id %>")}
|
50
|
+
%div.head
|
51
|
+
%div.left
|
52
|
+
%div.title= raw("<%= QARails.escapeHTML(this.topic.title) %>")
|
53
|
+
%div.detail
|
54
|
+
%span.uname= raw("<%= this.topic.uname %>")
|
55
|
+
%span.time
|
56
|
+
%span 于
|
57
|
+
%time.timeago{:datetime => raw("<%= this.topic.created_at %>")}
|
58
|
+
%span 发布
|
59
|
+
%span.view_count= raw("<%= this.topic.view_count %>次阅读")
|
60
|
+
%div.right
|
61
|
+
%span.avatar
|
62
|
+
%img{:src => raw("<%= QARails.jsfun_user_avatar_url(this.topic.uid) %>")}
|
63
|
+
%div.body
|
64
|
+
%div.content= raw("<%= this.topic.content %>")
|
65
|
+
%div.comments
|
66
|
+
= raw("<% _.each(this.replies, function(reply) { %>")
|
67
|
+
%div.comment
|
68
|
+
%span.avatar
|
69
|
+
%img{:src => raw("<%= QARails.jsfun_user_avatar_url(reply.uid) %>")}
|
70
|
+
%span.user
|
71
|
+
%span.uname= raw("<%= reply.uname %>")
|
72
|
+
%span.content= raw("<%= reply.content %>")
|
73
|
+
%span.time
|
74
|
+
%time.timeago{:datetime => raw("<%= reply.created_at %>")}
|
75
|
+
= raw("<% }); %>")
|
76
|
+
= render :partial => "qa/textarea", :locals => {:is_ask => false}
|
77
|
+
|
78
|
+
|
79
|
+
:css
|
80
|
+
/* facebox-rails.gem */
|
81
|
+
#qa .loading {background-image: url(/assets/facebox/loading.gif); background-repeat: no-repeat; background-position: center center; height: 50px;}
|
82
|
+
|
83
|
+
#qa .topic_list {margin-top:-20px;}
|
84
|
+
#qa .hide {display:none;}
|
85
|
+
#qa .pointer {cursor:pointer}
|
86
|
+
#qa .highlighttable{display:block;width:100%;margin:10px 0;overflow-x:auto;overflow-y:hidden;border:1px solid #ccc;}
|
87
|
+
|
88
|
+
|
89
|
+
#qa .topic_content{margin:10px 0;}
|
90
|
+
#qa .topic_content li{float:left;width:100%;padding:10px 0;overflow:hidden;border-bottom:1px solid #ccc;}
|
91
|
+
#qa .topic_content li ul li,#qa .topic_content li ol li{float:none;width:auto;padding:0;border:none;}
|
92
|
+
#qa .topic_content .user{float:left;width:48px;margin-left:25px;}
|
93
|
+
#qa .topic_content .user span.avatar{display:block;height:48px;overflow:hidden;}
|
94
|
+
#qa .topic_content .user span.uname, .markdownBox .uname{display:block;width:48px;height:24px;line-height:24px;overflow:hidden;text-align:center;color:#555;}
|
95
|
+
#qa .topic_content .box{float:right;width:800px;height:auto !important;max-height:150px;overflow:hidden;font-size:12px;margin-right:25px;position:relative;}
|
96
|
+
#qa .topic_content .box .title{color:#0073de;font-size:14px;}
|
97
|
+
#qa .topic_content .box a.uname{color:#999;}
|
98
|
+
#qa .topic_content .box em{display:inline-block;padding:0 8px;color:#fff;font-size:14px;font-style:normal;cursor:pointer;background-color:#1c7fdb;border-radius:12px;position:absolute;right:0;top:10px;}
|
99
|
+
#qa .topic_content .box .view_count{float:right;padding:0 5px;margin-left:10px;font-size:14px;color:#fff;border-radius:4px;background-color:#1c7fdb;}
|
100
|
+
#qa .topic_content .box .content{font-family:"宋体";line-height:24px;margin-top:5px;}
|
101
|
+
#qa .topic_content .empty_text{padding:50px 0;text-align:center;font-size:16px;}
|
102
|
+
|
103
|
+
#qa .pagination{float:right;margin-top:10px;margin-right:25px;border:1px solid #e6e6e6;border-left:none;}
|
104
|
+
#qa .pagination span{cursor:pointer;display:inline-block;border-left:1px solid #e6e6e6;padding:5px 10px;}
|
105
|
+
#qa .pagination span.page a{color:#0073de;}
|
106
|
+
#qa .pagination span.current{color:#999;}
|
107
|
+
|
108
|
+
#qa .topic_show .back_to_list{width:16px;height:16px;overflow:hidden;text-indent:-9999px;background:url(/assets/learn_btn.png) -212px 0 no-repeat;position:absolute;left:10px;top:14px;}
|
109
|
+
#qa .topic_show .head{padding:0 35px 10px;overflow:hidden;border-bottom:1px solid #ddd;}
|
110
|
+
#qa .topic_show .head .left{float:left;width:530px;}
|
111
|
+
#qa .topic_show .head .left .detail{margin-top:10px;}
|
112
|
+
#qa .topic_show .head .left .title{font-size:20px;color:#0073de;}
|
113
|
+
#qa .topic_show .head .left .uname{color:#666;font-weight:bold;}
|
114
|
+
#qa .topic_show .head .left .time{color:#999;padding:0 8px;}
|
115
|
+
#qa .topic_show .head .right{float:right;width:48px;height:48px;overflow:hidden;}
|
116
|
+
#qa .topic_show .body,.topic_show .comment{padding:15px 25px;overflow:hidden;border-bottom:1px solid #ddd;}
|
117
|
+
#qa .topic_show .body .content code,#qa .topic_content .box .content code,#qa .preview_box .preview_content code{padding:1px 3px;border:1px solid #dedede;border-radius:3px;background-color:#fafafa;}
|
118
|
+
#qa .topic_show .comment{border-bottom:1px dashed #ddd;padding:15px 0;margin:0 25px;}
|
119
|
+
#qa .topic_show .comment .avatar,#qa .comment .avatar,#qa .textareaBox .avatar{float:left;width:48px;height:48px;overflow:hidden;margin-right:15px;_display:inline;}
|
120
|
+
#qa .topic_show .comment .user{float:left;width:710px;}
|
121
|
+
#qa .topic_show .comment .user .uname{color:#666;font-weight:bold;}
|
122
|
+
#qa .topic_show .comment .user .content{display:block;line-height:24px;}
|
123
|
+
#qa .topic_show .comment .time{float:right;color:#999;}
|
124
|
+
|
125
|
+
/*提问框*/
|
126
|
+
#qa .textarea_text,#qa .preview_box,#qa.replay_textarea{float:left;margin-right:15px;border:1px solid #ccc;position:relative;zoom:1;}
|
127
|
+
#qa .textarea_text textarea,#qa .replay_textarea textarea{float:left;display:block;width:720px;height:80px;resize:none;margin:0;padding:0;border:none;}
|
128
|
+
#qa .markdownBox a.btn {float:left;width:62px;height:32px;overflow:hiddn;line-height:32px;text-indent:-9000px;background:url(/assets/learn_btn.png) -115px 0 no-repeat;}
|
129
|
+
#qa .markdownBox a.btn.reply {background-position:-53px 0;}
|
130
|
+
#qa .markdownBox{margin:20px 0 0;padding:36px 15px 20px;border-radius: 0 0 4px 4px;position:relative;}
|
131
|
+
#qa .markdownBox .ask_nav span{float:left;width:127px;height:46px;padding-left:30px;font-size:16px;line-height:38px;text-align:left;color:#fff;background:url(/assets/askbg.png) 0 0 no-repeat;position:absolute;left:-5px;top:-5px;}
|
132
|
+
#qa .markdownBox .convert_tags{margin:10px 0 5px 83px;}
|
133
|
+
#qa .markdownBox .convert_tags span{padding:2px 8px;border-radius:3px;cursor:pointer;}
|
134
|
+
#qa .markdownBox .convert_tags span:hover,#qa .markdownBox .convert_tags span.on{background-color:#b1b1b1;color:#fff;}
|
135
|
+
#qa .markdownBox .textareaBox{margin:0 10px;}
|
136
|
+
#qa .markdownBox .textarea_exp{margin-left:73px;margin-top:5px;}
|
137
|
+
#qa .markdownBox .textarea_exp li{list-style:none;padding-left:15px;line-height:22px;background:url(/assets/dot2.png) 0 11px no-repeat;}
|
138
|
+
#qa .markdownBox .textarea_exp li code{border:1px solid #dedede;background-color:#f8f8f8;border-radius:3px;}
|
139
|
+
#qa .preview_box{width:720px;height:auto;min-height:80px;background-color:#fff;}
|
140
|
+
#qa .preview_box .preview_title{color:#0073de;font-size:14px;}
|
141
|
+
#qa .preview_box .preview_content{font-family:"宋体";line-height:24px;color:#555;font-size:14px;}
|
142
|
+
|
143
|
+
|
144
|
+
:javascript
|
145
|
+
var QARails = {};
|
146
|
+
QARails.domid = #{@qa_options[:domid].to_json};
|
147
|
+
QARails.category_id = #{@qa_options[:category_id].to_json};
|
148
|
+
QARails.jsfun_user_avatar_url = #{@qa_options[:jsfun_user_avatar_url]};
|
149
|
+
QARails.jsfun_user_info = #{@qa_options[:jsfun_user_info]};
|
150
|
+
QARails.kaminari_config = {page: #{params[:page] || 1}, per: #{params[:per] || 5}};
|
151
|
+
QARails.default_ajax_data = {authenticity_token: AUTH_TOKEN, category_id: QARails.category_id};
|
152
|
+
QARails.escapeHTML = function(text) { return $('<div/>').text(text).html(); }
|
153
|
+
QARails.empty_topics_text = #{(@qa_options[:empty_topics_text] || '还没有相关讨论,等你来提问哦 :)').to_json};
|
154
|
+
QARails.notice_div = #{@qa_options[:notice_div]};
|
155
|
+
|
156
|
+
= javascript_include_tag "qa-rails"
|
@@ -0,0 +1,25 @@
|
|
1
|
+
%div.markdownBox
|
2
|
+
%div.ask_nav.fix
|
3
|
+
%span= is_ask ? "我要提问" : "添加回复"
|
4
|
+
%div.convert_tags.fix
|
5
|
+
%span.edit_button.on 编辑
|
6
|
+
%span.preview_button 预览
|
7
|
+
%div.textareaBox.fix
|
8
|
+
%span.avatar
|
9
|
+
%img{:src => raw("<%= QARails.jsfun_user_avatar_url(" + (is_ask ? "eoe.uid" : "QARails.jsfun_user_info().uid") + ") %>")}
|
10
|
+
%div.textarea_text
|
11
|
+
= text_area_tag (is_ask ? "post_topic" : "post_comment")
|
12
|
+
%div.preview_box.hide
|
13
|
+
%p.preview_title
|
14
|
+
%div.preview_content
|
15
|
+
- tmp = is_ask ? "ask" : "reply"
|
16
|
+
%a.btn.pointer{:class => tmp, :type => tmp}= is_ask ? "提问" : "回复"
|
17
|
+
%ol.textarea_exp
|
18
|
+
- if is_ask
|
19
|
+
%li 第一行是标题
|
20
|
+
%li
|
21
|
+
= "支持 Markdown 格式,"
|
22
|
+
%strong **粗体**
|
23
|
+
= "、~~删除线~~、"
|
24
|
+
%code `单行代码`
|
25
|
+
="、![Alt text](http://foo.com/bar.jpg) 显示图片"
|
data/config/routes.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
class CreateQa < ActiveRecord::Migration
|
4
|
+
def up
|
5
|
+
create_table :qa_topics, :options => 'ENGINE=Innodb DEFAULT CHARSET=utf8', :comment => "问答 主题" do |t|
|
6
|
+
t.integer :category_id, :comment => "分类ID", :default => 0
|
7
|
+
t.integer :uid, :comment => "作者ID", :default => 0
|
8
|
+
t.string :uname, :comment => "作者名字"
|
9
|
+
t.string :title, :comment => "主题的标题"
|
10
|
+
t.text :content, :comment => "主题的内容"
|
11
|
+
t.integer :replies_count, :comment => "回复数", :default => 0
|
12
|
+
t.integer :view_count, :comment => "阅读数", :default => 0
|
13
|
+
t.boolean :is_delete, :comment => "是否删除", :default => false
|
14
|
+
t.boolean :is_resolved, :comment => "是否解决", :default => false
|
15
|
+
t.timestamps
|
16
|
+
end
|
17
|
+
add_index :qa_topics, [:is_delete, :category_id, :updated_at]
|
18
|
+
|
19
|
+
create_table :qa_replies, :options => 'ENGINE=Innodb DEFAULT CHARSET=utf8', :comment => "问答 回复" do |t|
|
20
|
+
t.integer :topic_id, :comment => "主题 外键", :default => 0
|
21
|
+
t.integer :uid, :comment => "作者ID", :default => 0
|
22
|
+
t.string :uname, :comment => "作者名字"
|
23
|
+
t.text :content, :comment => "回复 内容"
|
24
|
+
t.boolean :is_delete, :comment => "是否删除", :default => false
|
25
|
+
t.timestamps
|
26
|
+
end
|
27
|
+
add_index :qa_replies, [:is_delete, :topic_id, :created_at]
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
def down
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
data/lib/qa-rails.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
require 'markdown-ruby-china'
|
4
|
+
|
5
|
+
module QA_Rails
|
6
|
+
module ContentMarkdownCache
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
def _content_markdown_cache
|
9
|
+
seconds = Rails.env == 'development' ? 1.second : 5.minutes
|
10
|
+
key = "/json/#{self.class.table_name}/#{self.id}/_content_markdown_cache"
|
11
|
+
Rails.cache.fetch(key, :expires_in => seconds) do
|
12
|
+
c = MarkdownTopicConverter.format(self.content.to_s)
|
13
|
+
puts self.content.inspect, " - "*8, c
|
14
|
+
c
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
module ApplicationHelper
|
21
|
+
# Setup QA mini forum
|
22
|
+
#
|
23
|
+
# domid - which dom you want to append
|
24
|
+
# category_id - board_id
|
25
|
+
#
|
26
|
+
# options[:jsfun_user_avatar_url] =>
|
27
|
+
# function(uid) { return "http://gravater.com/" + uid + ".png" };
|
28
|
+
#
|
29
|
+
def qa_setup domid, category_id = 1, options = {}
|
30
|
+
@qa_options = options
|
31
|
+
@qa_options[:domid] = domid
|
32
|
+
@qa_options[:category_id] = category_id
|
33
|
+
params[:category_id] = category_id
|
34
|
+
|
35
|
+
raise "Your should setup jsfun_user_avatar_url" if not @qa_options[:jsfun_user_avatar_url]
|
36
|
+
raise "Your should setup jsfun_user_info" if not @qa_options[:jsfun_user_info]
|
37
|
+
|
38
|
+
render :partial => "qa/qa"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
|
43
|
+
require File.expand_path('../rails_engine.rb', __FILE__)
|
data/lib/rails_engine.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module QA_Rails
|
4
|
+
|
5
|
+
class Engine < Rails::Engine
|
6
|
+
initializer "qa.load_app_instance_data" do |app|
|
7
|
+
app.class.configure do
|
8
|
+
['db/migrate', 'app/assets', 'app/models', 'app/controllers', 'app/views'].each do |path|
|
9
|
+
config.paths[path] ||= []
|
10
|
+
config.paths[path] += QA_Rails::Engine.paths[path].existent
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
data/qa-rails.gemspec
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = 'qa-rails'
|
3
|
+
s.version = '0.1'
|
4
|
+
s.date = '2013-04-08'
|
5
|
+
s.summary = File.read("README.markdown").split(/===+/)[0].strip
|
6
|
+
s.description = s.summary
|
7
|
+
s.authors = ["David Chen"]
|
8
|
+
s.email = 'mvjome@gmail.com'
|
9
|
+
s.homepage = 'http://github.com/eoecn/qa-rails'
|
10
|
+
|
11
|
+
s.add_dependency "rails"
|
12
|
+
s.add_dependency "haml"
|
13
|
+
s.add_dependency "kaminari"
|
14
|
+
s.add_dependency "cells", ">= 3.8.2"
|
15
|
+
s.add_dependency "markdown-ruby-china"
|
16
|
+
s.add_dependency 'facebox-rails'
|
17
|
+
s.add_dependency 'acts_as_paranoid_boolean_column'
|
18
|
+
|
19
|
+
s.files = `git ls-files`.split("\n")
|
20
|
+
end
|
metadata
ADDED
@@ -0,0 +1,176 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: qa-rails
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: '0.1'
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- David Chen
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-04-08 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rails
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: haml
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: kaminari
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :runtime
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: cells
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ! '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: 3.8.2
|
70
|
+
type: :runtime
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: 3.8.2
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: markdown-ruby-china
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ! '>='
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '0'
|
86
|
+
type: :runtime
|
87
|
+
prerelease: false
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ! '>='
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '0'
|
94
|
+
- !ruby/object:Gem::Dependency
|
95
|
+
name: facebox-rails
|
96
|
+
requirement: !ruby/object:Gem::Requirement
|
97
|
+
none: false
|
98
|
+
requirements:
|
99
|
+
- - ! '>='
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '0'
|
102
|
+
type: :runtime
|
103
|
+
prerelease: false
|
104
|
+
version_requirements: !ruby/object:Gem::Requirement
|
105
|
+
none: false
|
106
|
+
requirements:
|
107
|
+
- - ! '>='
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0'
|
110
|
+
- !ruby/object:Gem::Dependency
|
111
|
+
name: acts_as_paranoid_boolean_column
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
113
|
+
none: false
|
114
|
+
requirements:
|
115
|
+
- - ! '>='
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :runtime
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
none: false
|
122
|
+
requirements:
|
123
|
+
- - ! '>='
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: '0'
|
126
|
+
description: Rails 迷你问答论坛插件
|
127
|
+
email: mvjome@gmail.com
|
128
|
+
executables: []
|
129
|
+
extensions: []
|
130
|
+
extra_rdoc_files: []
|
131
|
+
files:
|
132
|
+
- README.markdown
|
133
|
+
- app/assets/images/askbg.png
|
134
|
+
- app/assets/images/dot2.png
|
135
|
+
- app/assets/images/learn_btn.png
|
136
|
+
- app/assets/javascripts/qa-rails-link.js
|
137
|
+
- app/assets/javascripts/qa-rails.js
|
138
|
+
- app/controllers/qa/markdown_controller.rb
|
139
|
+
- app/controllers/qa/replies_controller.rb
|
140
|
+
- app/controllers/qa/topics_controller.rb
|
141
|
+
- app/controllers/qa_controller.rb
|
142
|
+
- app/models/qa_reply.rb
|
143
|
+
- app/models/qa_topic.rb
|
144
|
+
- app/views/qa/_paginate.html.haml
|
145
|
+
- app/views/qa/_qa.html.haml
|
146
|
+
- app/views/qa/_textarea.html.haml
|
147
|
+
- config/routes.rb
|
148
|
+
- db/migrate/20130408062843_create_qa.rb
|
149
|
+
- lib/qa-rails.rb
|
150
|
+
- lib/rails_engine.rb
|
151
|
+
- qa-rails.gemspec
|
152
|
+
homepage: http://github.com/eoecn/qa-rails
|
153
|
+
licenses: []
|
154
|
+
post_install_message:
|
155
|
+
rdoc_options: []
|
156
|
+
require_paths:
|
157
|
+
- lib
|
158
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
159
|
+
none: false
|
160
|
+
requirements:
|
161
|
+
- - ! '>='
|
162
|
+
- !ruby/object:Gem::Version
|
163
|
+
version: '0'
|
164
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
165
|
+
none: false
|
166
|
+
requirements:
|
167
|
+
- - ! '>='
|
168
|
+
- !ruby/object:Gem::Version
|
169
|
+
version: '0'
|
170
|
+
requirements: []
|
171
|
+
rubyforge_project:
|
172
|
+
rubygems_version: 1.8.25
|
173
|
+
signing_key:
|
174
|
+
specification_version: 3
|
175
|
+
summary: Rails 迷你问答论坛插件
|
176
|
+
test_files: []
|