From 43f40c9825b1960ee4ae297ced6b827132c0fa4c Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 19 May 2015 15:45:19 +1000 Subject: [PATCH] work in progress --- .../topic-after-cooked/solved-panel.hbs | 5 + .../extend-for-solved-button.js.es6 | 101 +++++++++++++++ assets/stylesheets/solutions.scss | 15 +++ config/locales/server.en.yml | 3 + config/settings.yml | 4 + plugin.rb | 118 ++++++++++++++++++ 6 files changed, 246 insertions(+) create mode 100644 assets/javascripts/discourse/connectors/topic-after-cooked/solved-panel.hbs create mode 100644 assets/javascripts/discourse/initializers/extend-for-solved-button.js.es6 create mode 100644 assets/stylesheets/solutions.scss create mode 100644 config/locales/server.en.yml create mode 100644 config/settings.yml create mode 100644 plugin.rb diff --git a/assets/javascripts/discourse/connectors/topic-after-cooked/solved-panel.hbs b/assets/javascripts/discourse/connectors/topic-after-cooked/solved-panel.hbs new file mode 100644 index 0000000..b0740e3 --- /dev/null +++ b/assets/javascripts/discourse/connectors/topic-after-cooked/solved-panel.hbs @@ -0,0 +1,5 @@ +{{#if topic.accepted_answer}} +

+ Solved by {{topic.accepted_answer.username}} in post #{{topic.accepted_answer.post_number}} +

+{{/if}} diff --git a/assets/javascripts/discourse/initializers/extend-for-solved-button.js.es6 b/assets/javascripts/discourse/initializers/extend-for-solved-button.js.es6 new file mode 100644 index 0000000..39776a0 --- /dev/null +++ b/assets/javascripts/discourse/initializers/extend-for-solved-button.js.es6 @@ -0,0 +1,101 @@ +import PostMenuView from 'discourse/views/post-menu'; +import PostView from 'discourse/views/post'; +import { Button } from 'discourse/views/post-menu'; +import Topic from 'discourse/models/topic'; + +export default { + name: 'extend-for-solved-button', + initialize: function() { + + Topic.reopen({ + // keeping this here cause there is complex localization + acceptedAnswerHtml: function(){ + return I18n.t("") + }.property('accepted_answer') + }); + + PostView.reopen({ + classNameBindings: ['post.accepted_answer:accepted-answer'] + }); + + PostMenuView.registerButton(function(visibleButtons){ + if (this.get('post.can_accept_answer')) { + visibleButtons.splice(0,0,new Button('acceptAnswer', 'accepted_answer.accept_answer', 'check-square-o', {className: 'unaccepted'})); + } + if (this.get('post.can_unaccept_answer')) { + visibleButtons.splice(0,0,new Button('unacceptAnswer', 'accepted_answer.unaccept_answer', 'check-square', {className: 'accepted'})); + } + }); + + PostMenuView.reopen({ + acceptedChanged: function(){ + this.rerender(); + }.observes('post.accepted_answer'), + + clickUnacceptAnswer: function(){ + this.set('post.can_accept_answer', true); + this.set('post.can_unaccept_answer', false); + this.set('post.topic.has_accepted_answer', false); + + Discourse.ajax("/solution/unaccept", { + type: 'POST', + data: { + id: this.get('post.id') + } + }).then(function(){ + // + }).catch(function(error){ + var message = I18n.t("generic_error"); + try { + message = $.parseJSON(error.responseText).errors; + } catch (e) { + // nothing we can do + } + bootbox.alert(message); + }); + }, + + clearAcceptedAnswer: function(){ + const posts = this.get('post.topic.postStream.posts'); + posts.forEach(function(post){ + if (post.get('post_number') > 1 ) { + post.set('accepted_answer',false); + post.set('can_accept_answer',true); + post.set('can_unaccept_answer',false); + } + }); + }, + + clickAcceptAnswer: function(){ + + this.clearAcceptedAnswer(); + + this.set('post.can_unaccept_answer', true); + this.set('post.can_accept_answer', false); + this.set('post.accepted_answer', true); + + this.set('post.topic.accepted_answer', { + username: this.get('post.username'), + post_number: this.get('post.post_number') + }); + + Discourse.ajax("/solution/accept", { + type: 'POST', + data: { + id: this.get('post.id') + } + }).then(function(){ + // + }).catch(function(error){ + var message = I18n.t("generic_error"); + try { + message = $.parseJSON(error.responseText).errors; + } catch (e) { + // nothing we can do + } + bootbox.alert(message); + }); + } + }); + } +}; diff --git a/assets/stylesheets/solutions.scss b/assets/stylesheets/solutions.scss new file mode 100644 index 0000000..5c7544f --- /dev/null +++ b/assets/stylesheets/solutions.scss @@ -0,0 +1,15 @@ +.post-controls .accepted, .fa.accepted { + color: green; +} + +.topic-post.accepted-answer .topic-body { + background-color: #E9FFE0; +} + +.cooked .solved { + margin-top: 20px; + margin-bottom: 0px; + padding: 4px 10px; + border: 1px solid #ddd; + background-color: #E9FFE0; +} diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml new file mode 100644 index 0000000..4ca7911 --- /dev/null +++ b/config/locales/server.en.yml @@ -0,0 +1,3 @@ +en: + site_settings: + categories_with_solved_button: "List of categories where solved button is allowed" diff --git a/config/settings.yml b/config/settings.yml new file mode 100644 index 0000000..5c9fc46 --- /dev/null +++ b/config/settings.yml @@ -0,0 +1,4 @@ +uncategorized: + categories_with_solved_button: + default: '' + client: true diff --git a/plugin.rb b/plugin.rb new file mode 100644 index 0000000..0e83891 --- /dev/null +++ b/plugin.rb @@ -0,0 +1,118 @@ +# name: discourse-solved-button +# about: Add a solved button to answers on Discourse +# version: 0.1 +# authors: Sam Saffron + +PLUGIN_NAME = "discourse_solved_button".freeze + +register_asset 'stylesheets/solutions.scss' + +after_initialize do + + module ::DiscourseSolvedButton + class Engine < ::Rails::Engine + engine_name PLUGIN_NAME + isolate_namespace DiscourseSolvedButton + end + end + + require_dependency "application_controller" + class DiscourseSolvedButton::AnswerController < ::ApplicationController + def accept + + post = Post.find(params[:id].to_i) + + accepted_id = post.topic.custom_fields["has_accepted_answer"].to_i + if accepted_id + if p2 = Post.find_by(id: accepted_id) + p2.custom_fields["is_accepted_answer"] = nil + p2.save! + end + end + + post.custom_fields["is_accepted_answer"] = "true" + post.topic.custom_fields["accepted_answer_post_id"] = post.id + post.topic.save! + post.save! + + render json: success_json + end + + def unaccept + post = Post.find(params[:id].to_i) + post.custom_fields["is_accepted_answer"] = nil + post.topic.custom_fields["accepted_answer_post_id"] = nil + post.topic.save! + post.save! + + render json: success_json + end + end + + DiscourseSolvedButton::Engine.routes.draw do + post "/accept" => "answer#accept" + post "/unaccept" => "answer#unaccept" + end + + Discourse::Application.routes.append do + mount ::DiscourseSolvedButton::Engine, at: "solution" + end + + TopicView.add_post_custom_fields_whitelister do |user| + ["is_accepted_answer"] + end + + require_dependency 'topic_view_serializer' + class ::TopicViewSerializer + attributes :accepted_answer + + def include_accepted_answer? + accepted_answer_post_id + end + + def accepted_answer + if info = accepted_answer_post_info + { + post_number: info[0], + username: info[1], + } + end + end + + def accepted_answer_post_info + # TODO: we may already have it in the stream ... so bypass query here + + Post.where(id: accepted_answer_post_id, topic_id: object.topic.id) + .joins(:user) + .pluck('post_number, username') + .first + end + + def accepted_answer_post_id + id = object.topic.custom_fields["accepted_answer_post_id"] + id && id.to_i + end + + end + + require_dependency 'post_serializer' + class ::PostSerializer + attributes :can_accept_answer, :can_unaccept_answer, :accepted_answer + + def can_accept_answer + topic = (topic_view && topic_view.topic) || object.topic + if topic + object.post_number > 1 && !accepted_answer + end + end + + def can_unaccept_answer + post_custom_fields["is_accepted_answer"] + end + + def accepted_answer + post_custom_fields["is_accepted_answer"] + end + + end +end