qa-rails 0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: []