qa-rails 0.1

Sign up to get free protection for your applications and to get access to all the features.
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,7 @@
1
+ //= require jquery
2
+ //= require underscore
3
+ //= require backbone
4
+ //= require backbone_rails_sync
5
+ //= require backbone_datalink
6
+
7
+ //= require qa-rails
@@ -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,8 @@
1
+ # encoding: UTF-8
2
+
3
+ class QAReply < ActiveRecord::Base
4
+ acts_as_paranoid :column => 'is_delete', :column_type => 'boolean'
5
+
6
+ include QA_Rails::ContentMarkdownCache
7
+
8
+ 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,2 @@
1
+ - a = QATopic.where(:category_id => params[:category_id]).page(params[:page] ||= 1).per(params[:per] ||= 5)
2
+ %div= paginate a
@@ -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,11 @@
1
+ Rails.application.routes.draw do
2
+ namespace :qa do
3
+ resources :topics do
4
+ resources :replies
5
+ collection do
6
+ get :new_topics_count
7
+ end
8
+ end
9
+ resources :markdown
10
+ end
11
+ end
@@ -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__)
@@ -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: []