├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── app
├── controllers
│ └── replies_controller.rb
├── models
│ └── reply.rb
└── views
│ └── replies
│ ├── _create.html.erb
│ ├── _form.html.erb
│ ├── _list.html.erb
│ ├── edit.html.erb
│ ├── index.html.erb
│ └── preview.html.erb
├── assets
├── javascripts
│ └── replies.js
└── stylesheets
│ └── replies.css
├── config
├── locales
│ ├── en.yml
│ ├── fr.yml
│ └── ru.yml
└── routes.rb
├── db
└── migrate
│ ├── 001_create_replies.rb
│ └── 002_add_is_public_to_replies_table.rb
├── doc
└── images
│ ├── access-quick-replies.png
│ └── managing-quick-replies.png
├── init.rb
├── lib
├── redmine_quick_replies.rb
└── redmine_quick_replies
│ ├── hooks
│ └── add_replies_link.rb
│ └── patches
│ ├── user_patch.rb
│ └── wiki_formatting_patch.rb
└── test
├── fixtures
└── replies.yml
├── functional
└── replies_controller_test.rb
└── test_helper.rb
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
4 |
5 | ## v1.3.1
6 |
7 | ### Updated
8 |
9 | * Update how the plugin patch wiki_formatting for better compatibility with other plugins
10 |
11 | ## v1.3.0
12 |
13 | ### Added
14 |
15 | * Allow public replies to be created
16 | * Add french translations
17 |
18 | ## v1.2.0
19 |
20 | ### Added
21 |
22 | * Add support for Redmine 4.1
23 |
24 | ## v1.1.0
25 |
26 | ### Added
27 |
28 | * Add support for Redmine 4
29 | * Explain how to use the plugin in the README
30 |
31 | ### Changed
32 |
33 | * Order quick replies in alphabetical order
34 | * Truncate quick replies with too long name
35 |
36 | ### Fixed
37 |
38 | * Hide quick replies button if none are available
39 |
40 | ## v1.0.0
41 |
42 | 🎉 Initial release!
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
6 |
7 | ## Our Standards
8 |
9 | Examples of behavior that contributes to creating a positive environment include:
10 |
11 | * Using welcoming and inclusive language
12 | * Being respectful of differing viewpoints and experiences
13 | * Gracefully accepting constructive criticism
14 | * Focusing on what is best for the community
15 | * Showing empathy towards other community members
16 |
17 | Examples of unacceptable behavior by participants include:
18 |
19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances
20 | * Trolling, insulting/derogatory comments, and personal or political attacks
21 | * Public or private harassment
22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission
23 | * Other conduct which could reasonably be considered inappropriate in a professional setting
24 |
25 | ## Our Responsibilities
26 |
27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
28 |
29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
30 |
31 | ## Scope
32 |
33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
34 |
35 | ## Enforcement
36 |
37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at info@exolnet.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
38 |
39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
40 |
41 | ## Attribution
42 |
43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
44 |
45 | [homepage]: http://contributor-covenant.org
46 | [version]: http://contributor-covenant.org/version/1/4/
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | Contributions are **welcome** and will be fully **credited**.
4 |
5 | We accept contributions via Pull Requests on [Github](https://github.com/eXolnet/redmine_quick_replies).
6 |
7 |
8 | ## Pull Requests
9 |
10 | - **[Ruby Coding Standard](https://github.com/styleguide/ruby)** - Check the code style before commiting.
11 |
12 | - **Add tests!** - Your patch won't be accepted if it doesn't have tests.
13 |
14 | - **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date.
15 |
16 | - **Consider our release cycle** - We try to follow [SemVer v2.0.0](http://semver.org/). Randomly breaking public APIs is not an option.
17 |
18 | - **Create feature branches** - Don't ask us to pull from your master branch.
19 |
20 | - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests.
21 |
22 | - **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](http://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting.
23 |
24 |
25 | ## Running Tests
26 |
27 | ``` bash
28 | $ bundle exec rake redmine:plugins:test NAME=redmine_quick_replies
29 | ```
30 |
31 | **Happy coding**!
32 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2019-present eXolnet Inc. (https://www.exolnet.com/)
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Redmine Quick Replies
2 |
3 | [](https://github.com/eXolnet/redmine_quick_replies/releases)
4 | 
5 | [](LICENSE)
6 | [](https://github.com/eXolnet/redmine_quick_replies/actions?query=workflow%3Atests)
7 | [](https://codeclimate.com/github/eXolnet/redmine_quick_replies/maintainability)
8 |
9 | Save time by creating quick replies that could be reused in any WYSIWYG editors.
10 |
11 | ## Compatibility
12 |
13 | This plugin version is compatible only with Redmine 4.2 and later.
14 |
15 | ## Installation
16 |
17 | 1. Download the .ZIP archive, extract files and copy the plugin directory to `#{REDMINE_ROOT}/plugins/redmine_quick_replies`.
18 |
19 | 2. Make a backup of your database, then run the following command to update it:
20 |
21 | ```bash
22 | bundle exec rake redmine:plugins:migrate NAME=redmine_quick_replies RAILS_ENV=production
23 | ```
24 |
25 | 3. Restart Redmine.
26 |
27 | ### Uninstall
28 |
29 | 1. Make a backup of your database, then rollback the migrations:
30 |
31 | ```bash
32 | bundle exec rake redmine:plugins:migrate NAME=redmine_quick_replies VERSION=0 RAILS_ENV=production
33 | ```
34 |
35 | 2. Remove the plugin's folder from `#{REDMINE_ROOT}/plugins`.
36 |
37 | 3. Restart Redmine.
38 |
39 | ## Usage
40 |
41 | ### Using quick replies
42 |
43 | Quick replies could be used on any WYSIWYG editor available on Redmine:
44 |
45 | 1. Click on the "Quick Replies" button;
46 | 2. Select the quick reply that you want to append by its name.
47 |
48 | 
49 |
50 | ### Managing quick replies
51 |
52 | Access your quick replies through your account:
53 |
54 | 1. Click on the "My account" on Redmine's top menu;
55 | 2. Click on the "Quick replies";
56 | 3. Create, edit or delete your quick replies though this page.
57 |
58 | 
59 |
60 | ## Testing
61 |
62 | Run tests using the following command:
63 |
64 | ```bash
65 | bundle exec rake redmine:plugins:test NAME=redmine_quick_replies RAILS_ENV=test
66 | ```
67 |
68 | ## Contributing
69 |
70 | Please see [CONTRIBUTING](CONTRIBUTING.md) and [CODE OF CONDUCT](CODE_OF_CONDUCT.md) for details.
71 |
72 | ## Security
73 |
74 | If you discover any security related issues, please email security@exolnet.com instead of using the issue tracker.
75 |
76 | ## Credits
77 |
78 | - [Alexandre D'Eschambeault](https://github.com/xel1045)
79 | - [All Contributors](../../contributors)
80 |
81 | ## License
82 |
83 | Copyright © [eXolnet](https://www.exolnet.com). All rights reserved.
84 |
85 | This code is licensed under the [MIT license](http://choosealicense.com/licenses/mit/).
86 | Please see the [license file](LICENSE) for more information.
87 |
--------------------------------------------------------------------------------
/app/controllers/replies_controller.rb:
--------------------------------------------------------------------------------
1 | class RepliesController < ApplicationController
2 | before_action :require_login, :find_user
3 | before_action :build_new_reply_from_params, :only => [:index, :create]
4 | before_action :find_reply, :only => [:edit, :update, :destroy]
5 | before_action :find_replies, :only => [:index, :create]
6 | before_action :update_reply_from_params, :only => [:update]
7 |
8 | def index
9 | #
10 | end
11 |
12 | def create
13 | unless @reply.save
14 | render :action => 'index'
15 | return
16 | end
17 |
18 | flash[:notice] = l(:notice_reply_successful_create)
19 | redirect_to replies_path
20 | end
21 |
22 | def preview
23 | @body = params[:reply] ? params[:reply][:body] : nil
24 |
25 | render :layout => false
26 | end
27 |
28 | def edit
29 | #
30 | end
31 |
32 | def update
33 | unless @reply.save
34 | render :action => 'edit'
35 | return
36 | end
37 |
38 | flash[:notice] = l(:notice_reply_successful_update)
39 | redirect_to replies_path
40 | end
41 |
42 | def destroy
43 | @reply.destroy
44 |
45 | flash[:notice] = l(:notice_reply_successful_delete)
46 |
47 | redirect_to replies_path
48 | end
49 |
50 | private
51 |
52 | def find_user
53 | render_403 unless User.current.allowed_to_create_replies?
54 |
55 | @user = User.current
56 | end
57 |
58 | def build_new_reply_from_params
59 | @reply = Reply.new
60 | @reply.user ||= User.current
61 |
62 | update_reply_from_params
63 | end
64 |
65 | def update_reply_from_params
66 | attrs = (params[:reply] || {}).deep_dup
67 |
68 | @reply.safe_attributes = attrs
69 | end
70 |
71 | def find_reply
72 | reply_id = params[:reply_id] || params[:id]
73 |
74 | @reply = Reply.find(reply_id)
75 | raise Unauthorized unless @reply.editable?
76 | rescue ActiveRecord::RecordNotFound
77 | render_404
78 | end
79 |
80 | def find_replies
81 | @replies = Reply.editable.sorted
82 | end
83 | end
84 |
--------------------------------------------------------------------------------
/app/models/reply.rb:
--------------------------------------------------------------------------------
1 | class Reply < ActiveRecord::Base
2 | include Redmine::SafeAttributes
3 |
4 | NAME_LENGTH_LIMIT = 60
5 |
6 | ## Attributes
7 | attr_protected :id, :user_id if ActiveRecord::VERSION::MAJOR <= 4
8 |
9 | safe_attributes :name, :body
10 |
11 | safe_attributes :is_public, :if => lambda {|reply, user| user.allowed_to_manage_public_replies?}
12 |
13 | ## Relations
14 | belongs_to :user
15 |
16 | ## Validations
17 | validates :user_id, presence: true
18 |
19 | validates :name, presence: true,
20 | uniqueness: { case_sensitive: true, scope: :user_id },
21 | length: { maximum: NAME_LENGTH_LIMIT }
22 |
23 | validates :body, presence: true
24 |
25 | ## Scopes
26 | scope :all_public, lambda { where(:is_public => true) }
27 |
28 | scope :author, lambda {|*args|
29 | user = args.first || User.current
30 |
31 | where(:user_id => user.id)
32 | }
33 |
34 | scope :editable, lambda {|*args|
35 | user = args.first || User.current
36 |
37 | return visible if user.allowed_to_manage_public_replies?
38 |
39 | author(user)
40 | }
41 |
42 | scope :sorted, lambda { order(:name, :id) }
43 |
44 | scope :visible, lambda {|*args|
45 | user = args.first || User.current
46 |
47 | where("(#{table_name}.is_public=? OR #{table_name}.user_id=?)", true, user.id)
48 | }
49 |
50 | def author?(user=User.current)
51 | self.user == user
52 | end
53 |
54 | def editable?(user=User.current)
55 | author?(user) || (is_public? && user.allowed_to_manage_public_replies?)
56 | end
57 |
58 | def is_private?
59 | !is_public?
60 | end
61 |
62 | def is_public?
63 | is_public
64 | end
65 |
66 | def visible?(user=User.current)
67 | is_public? || author?(user)
68 | end
69 | end
70 |
--------------------------------------------------------------------------------
/app/views/replies/_create.html.erb:
--------------------------------------------------------------------------------
1 | <%= labelled_form_for reply, :url => replies_path, :html => {:id => 'reply-form'} do |f| %>
2 | <%= render :partial => 'form', :locals => {:f => f} %>
3 |
4 | <%= submit_tag l(:button_create), :name => 'create_button' %>
5 | <% if Redmine::VERSION.to_s < '4.0' %>
6 | <%= preview_link preview_reply_path, 'reply-form' %> |
7 | <% end %>
8 | <%= link_to l(:button_cancel), my_account_path %>
9 | <% end %>
10 |
11 |
12 |
--------------------------------------------------------------------------------
/app/views/replies/_form.html.erb:
--------------------------------------------------------------------------------
1 | <%= error_messages_for 'reply' %>
2 |
3 |
4 |
<%= f.text_field :name, :required => true, :size => 60 %>
5 |
6 | <% if User.current.allowed_to_manage_public_replies? %>
7 |
<%= f.check_box :is_public %>
8 | <% end %>
9 |
10 |
<%= f.text_area :body, :required => true, :cols => 60, :rows => 7, :label => :label_reply, :class => 'reply-edit', :placeholder => l(:label_leave_comment), :id => 'reply_body', :style => 'width: 97%;' %>
11 |
12 |
13 | <% if Redmine::VERSION.to_s >= '4.0' %>
14 | <%= wikitoolbar_for 'reply_body', preview_reply_path %>
15 | <% else %>
16 | <%= wikitoolbar_for 'reply_body' %>
17 | <% end %>
18 |
--------------------------------------------------------------------------------
/app/views/replies/_list.html.erb:
--------------------------------------------------------------------------------
1 | <% unless replies.empty? %>
2 |
3 |
4 |
5 | <%=l(:label_reply)%> |
6 | <%=l(:field_is_public)%> |
7 | |
8 |
9 |
10 |
11 |
12 | <% replies.each do |reply| %>
13 |
14 |
15 | <%= reply.name %>
16 | <%= reply.body %>
17 | |
18 |
19 | <%= checked_image reply.is_public? %> |
20 |
21 |
22 | <%= link_to l(:button_edit), edit_reply_path(:id => reply), :class => 'icon icon-edit' %>
23 | <%= delete_link reply_path(reply) %>
24 | |
25 |
26 | <% end %>
27 |
28 |
29 | <% else %>
30 | <%= l(:label_no_quick_reply) %>
31 | <% end %>
--------------------------------------------------------------------------------
/app/views/replies/edit.html.erb:
--------------------------------------------------------------------------------
1 | <%= l(:label_edit_quick_reply) %>
2 |
3 | <%= labelled_form_for @reply, :html => {:id => 'reply-form'} do |f| %>
4 | <%= render :partial => 'form', :locals => {:f => f} %>
5 |
6 | <%= submit_tag l(:button_update), :name => 'update_button' %>
7 | <% if Redmine::VERSION.to_s < '4.0' %>
8 | <%= preview_link preview_reply_path, 'reply-form' %> |
9 | <% end %>
10 | <%= link_to l(:button_cancel), replies_path %>
11 | <% end %>
12 |
13 | <% if Redmine::VERSION.to_s < '4.0' %>
14 |
15 | <% end %>
16 |
--------------------------------------------------------------------------------
/app/views/replies/index.html.erb:
--------------------------------------------------------------------------------
1 | <%= l(:label_quick_replies) %>
2 |
3 | <%= render :partial => 'list', :locals => {:replies => @replies} %>
4 |
5 | <%= l(:label_new_quick_reply) %>
6 | <%= render :partial => 'create', :locals => {:reply => @reply} %>
7 |
--------------------------------------------------------------------------------
/app/views/replies/preview.html.erb:
--------------------------------------------------------------------------------
1 | <% if @body %>
2 |
5 | <% end %>
6 |
--------------------------------------------------------------------------------
/assets/javascripts/replies.js:
--------------------------------------------------------------------------------
1 | (function() {
2 | if (redmine_quick_replies.length === 0) {
3 | return;
4 | }
5 |
6 | jsToolBar.prototype.elements.replies = {
7 | type: 'button',
8 | title: 'Replies',
9 | fn: {
10 | wiki: function() {
11 | var editor = this,
12 | dropdown = $('');
13 |
14 | for (var i = 0; i < redmine_quick_replies.length; i++) {
15 | var reply = redmine_quick_replies[i],
16 | li = $('');
17 |
18 | li.data('reply', reply);
19 | li.html($('').text(reply.name));
20 | li.attr('title', reply.name);
21 | li.appendTo(dropdown)
22 |
23 | li.mousedown(function() {
24 | var body = $(this).data('reply').body;
25 |
26 | editor.encloseSelection(body);
27 | });
28 | }
29 |
30 | dropdown.menu().width(200).position({
31 | my: 'left top',
32 | at: 'left bottom',
33 | of: this.toolNodes.replies
34 | });
35 |
36 | $(document).on('mousedown', function() {
37 | dropdown.remove();
38 | });
39 |
40 | $('body').append(dropdown);
41 | }
42 | }
43 | };
44 | })();
45 |
--------------------------------------------------------------------------------
/assets/stylesheets/replies.css:
--------------------------------------------------------------------------------
1 | .jstb_replies {
2 | background-image: url(../../../images/comment.png);
3 | }
4 |
5 | .quick-replies__menu {
6 | max-width: 225px;
7 | position: absolute;
8 | }
9 |
10 | .quick-replies__menu li {
11 |
12 | overflow: hidden;
13 | text-overflow: ellipsis;
14 | white-space: nowrap;
15 | }
16 |
17 | .reply__excerpt {
18 | color: #aaa;
19 | font-size: 90%;
20 | }
21 |
--------------------------------------------------------------------------------
/config/locales/en.yml:
--------------------------------------------------------------------------------
1 | # English strings go here for Rails i18n
2 | en:
3 | activerecord:
4 | attributes:
5 | reply:
6 | body: "Reply"
7 | label_quick_replies: "Quick replies"
8 | label_no_quick_reply: "You don't have any quick reply saved."
9 | label_new_quick_reply: "New quick reply"
10 | label_edit_quick_reply: "Edit quick reply"
11 | label_reply: Reply
12 | notice_reply_successful_create: "Quick reply successfully created."
13 | notice_reply_successful_update: "Quick reply successfully updated."
14 | notice_reply_successful_delete: "Quick reply successfully deleted."
15 | text_reply_destroy_confirmation: "Are you sure you want to delete this quick reply?"
--------------------------------------------------------------------------------
/config/locales/fr.yml:
--------------------------------------------------------------------------------
1 | # French strings go here for Rails i18n
2 | fr:
3 | activerecord:
4 | attributes:
5 | reply:
6 | body: "Réponse"
7 | label_quick_replies: "Réponses rapides"
8 | label_no_quick_reply: "Vous n'avez pas de réponse rapide enregistrée."
9 | label_new_quick_reply: "Nouvelle réponse rapide"
10 | label_edit_quick_reply: "Modifier la réponse rapide"
11 | label_reply: Réponse
12 | notice_reply_successful_create: "La réponse rapide a été créée avec succès."
13 | notice_reply_successful_update: "La réponse rapide a été modifiée avec succès."
14 | notice_reply_successful_delete: "La réponse rapide a été supprimée avec succès."
15 | text_reply_destroy_confirmation: "Voulez-vous vraiment supprimer cette réponse rapide?"
--------------------------------------------------------------------------------
/config/locales/ru.yml:
--------------------------------------------------------------------------------
1 | # Russian strings go here for Rails i18n
2 | ru:
3 | activerecord:
4 | attributes:
5 | reply:
6 | body: "Ответ"
7 | label_quick_replies: "Быстрые ответы"
8 | label_no_quick_reply: "У вас нет сохраненных быстрых ответов."
9 | label_new_quick_reply: "Новый быстрый ответ"
10 | label_edit_quick_reply: "Редактировать быстрый ответ"
11 | label_reply: Ответ
12 | notice_reply_successful_create: "Быстрый ответ успешно создан."
13 | notice_reply_successful_update: "Быстрый ответ успешно обновлён."
14 | notice_reply_successful_delete: "Быстрый ответ успешно удалён."
15 | text_reply_destroy_confirmation: "Вы уверены, что хотите удалить этот быстрый ответ?"
16 |
--------------------------------------------------------------------------------
/config/routes.rb:
--------------------------------------------------------------------------------
1 | # Plugin's routes
2 | # See: http://guides.rubyonrails.org/routing.html
3 |
4 | scope 'my' do
5 | match 'replies/preview', :to => 'replies#preview', :as => 'preview_reply', :via => [:get, :post, :put, :patch]
6 | resources :replies, controller: 'replies'
7 | end
--------------------------------------------------------------------------------
/db/migrate/001_create_replies.rb:
--------------------------------------------------------------------------------
1 | migration_class = ActiveRecord::VERSION::MAJOR >= 5 ? ActiveRecord::Migration[4.2] : ActiveRecord::Migration
2 |
3 | class CreateReplies < migration_class
4 | def change
5 | create_table :replies do |t|
6 | t.column :user_id, :integer, :null => false
7 | t.string :name, :null => false
8 | t.column :body, :text
9 | end
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/db/migrate/002_add_is_public_to_replies_table.rb:
--------------------------------------------------------------------------------
1 | migration_class = ActiveRecord::VERSION::MAJOR >= 5 ? ActiveRecord::Migration[4.2] : ActiveRecord::Migration
2 |
3 | class AddIsPublicToRepliesTable < migration_class
4 | def self.up
5 | add_column :replies, :is_public, :boolean, :default => false, :null => false
6 | end
7 |
8 | def self.down
9 | remove_column :replies, :is_public
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/doc/images/access-quick-replies.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eXolnet/redmine_quick_replies/00a94e6f43990234be438aa1c60b4b144a8b5b04/doc/images/access-quick-replies.png
--------------------------------------------------------------------------------
/doc/images/managing-quick-replies.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eXolnet/redmine_quick_replies/00a94e6f43990234be438aa1c60b4b144a8b5b04/doc/images/managing-quick-replies.png
--------------------------------------------------------------------------------
/init.rb:
--------------------------------------------------------------------------------
1 | require 'redmine'
2 |
3 | REDMINE_QUICK_REPLIES_VERSION = '1.4.1'
4 |
5 | Redmine::Plugin.register :redmine_quick_replies do
6 | name 'Quick Replies'
7 | author 'eXolnet'
8 | description 'Save time by creating quick replies that could be reused in any WYSIWYG editors.'
9 | version REDMINE_QUICK_REPLIES_VERSION
10 | url 'https://github.com/eXolnet/redmine_quick_replies'
11 | author_url 'https://www.exolnet.com'
12 |
13 | requires_redmine :version_or_higher => '4.2'
14 |
15 | permission :create_replies, replies: [:index, :create, :edit, :update, :destroy], require: :loggedin
16 | permission :manage_public_replies, {}
17 | end
18 |
19 | require File.dirname(__FILE__) + '/lib/redmine_quick_replies'
20 |
--------------------------------------------------------------------------------
/lib/redmine_quick_replies.rb:
--------------------------------------------------------------------------------
1 | require File.dirname(__FILE__) + '/redmine_quick_replies/hooks/add_replies_link'
2 |
3 | require File.dirname(__FILE__) + '/redmine_quick_replies/patches/user_patch'
4 | require File.dirname(__FILE__) + '/redmine_quick_replies/patches/wiki_formatting_patch'
5 |
6 | module RedmineQuickReplies
7 | class << self
8 | def setup
9 | Redmine::WikiFormatting::format_names.each do |format|
10 | unless Redmine::WikiFormatting::helper_for(format).included_modules.include? RedmineQuickReplies::Patches::WikiFormattingPatch
11 | Redmine::WikiFormatting::helper_for(format).send(:include, RedmineQuickReplies::Patches::WikiFormattingPatch)
12 | end
13 | end
14 | end
15 | end
16 | end
17 |
18 | if Rails.version > '6.0' && Rails.autoloaders.zeitwerk_enabled?
19 | Rails.application.config.after_initialize do
20 | RedmineQuickReplies.setup
21 | end
22 | else
23 | Rails.configuration.to_prepare do
24 | RedmineQuickReplies.setup
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/lib/redmine_quick_replies/hooks/add_replies_link.rb:
--------------------------------------------------------------------------------
1 | module RedmineQuickReplies
2 | module Hooks
3 | class AddRepliesLink < Redmine::Hook::ViewListener
4 | def view_my_account_contextual(context)
5 | user = context[:user]
6 | link_to(l(:label_quick_replies), replies_path, class: 'icon icon-comment') if user.allowed_to_create_replies?
7 | end
8 |
9 | def self.default_url_options
10 | {:script_name => Redmine::Utils.relative_url_root}
11 | end
12 | end
13 | end
14 | end
--------------------------------------------------------------------------------
/lib/redmine_quick_replies/patches/user_patch.rb:
--------------------------------------------------------------------------------
1 | require_dependency 'user'
2 |
3 | module RedmineQuickReplies
4 | module Patches
5 | module UserPatch
6 | def self.included(base) # :nodoc:
7 | base.send(:include, InstanceMethods)
8 | end
9 |
10 | module InstanceMethods
11 | def allowed_to_create_replies?
12 | allowed_to?(:create_replies, nil, global: true)
13 | end
14 |
15 | def allowed_to_manage_public_replies?
16 | allowed_to?(:manage_public_replies, nil, global: true)
17 | end
18 | end
19 | end
20 | end
21 | end
22 |
23 | unless User.included_modules.include?(RedmineQuickReplies::Patches::UserPatch)
24 | User.send(:include, RedmineQuickReplies::Patches::UserPatch)
25 | end
26 |
--------------------------------------------------------------------------------
/lib/redmine_quick_replies/patches/wiki_formatting_patch.rb:
--------------------------------------------------------------------------------
1 | module RedmineQuickReplies
2 | module Patches
3 | module WikiFormattingPatch
4 | extend ActiveSupport::Concern
5 |
6 | included do
7 | prepend InstanceOverwriteMethods
8 | end
9 |
10 | module InstanceOverwriteMethods
11 | def heads_for_wiki_formatter
12 | super
13 |
14 | return if @heads_for_wiki_redmine_quick_replies_included
15 |
16 | content_for :header_tags do
17 | replies = Reply.visible.sorted.to_json
18 |
19 | o = javascript_tag("redmine_quick_replies = " + replies + ";")
20 | o << javascript_include_tag('replies', :plugin => 'redmine_quick_replies')
21 | o << stylesheet_link_tag('replies', :plugin => 'redmine_quick_replies')
22 | o.html_safe
23 | end
24 |
25 | @heads_for_wiki_redmine_quick_replies_included = true
26 | end
27 | end
28 | end
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/test/fixtures/replies.yml:
--------------------------------------------------------------------------------
1 | ---
2 | replies_001:
3 | id: 1
4 | user_id: 1
5 | name: Reply 1
6 | body: Dapibus ligula phasellus
7 | is_public: false
8 | replies_002:
9 | id: 2
10 | user_id: 2
11 | name: Reply 2
12 | body: Metus tellus conubia
13 | is_public: false
14 | replies_003:
15 | id: 3
16 | user_id: 1
17 | name: Reply 3
18 | body: Mollis facilisis convallis
19 | is_public: true
20 |
--------------------------------------------------------------------------------
/test/functional/replies_controller_test.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path('../../test_helper', __FILE__)
2 |
3 | class RepliesControllerTest < ActionController::TestCase
4 | fixtures :users,
5 | :replies
6 |
7 | def setup
8 | @request.session[:user_id] = 1
9 | end
10 |
11 | def test_index
12 | compatible_request :get, :index
13 |
14 | assert_response :success
15 |
16 | assert_select 'table.replies' do
17 | assert_select 'tbody tr' do
18 | assert_select 'a[href="/my/replies/1/edit"]'
19 | assert_select 'a[href="/my/replies/1"][data-method="delete"]'
20 | end
21 |
22 | assert_select 'a[href="/my/replies/2"]', 0
23 | end
24 | end
25 |
26 | def test_index_loads_quick_replies_in_wiki_editor
27 | compatible_request :get, :index
28 |
29 | assert_response :success
30 |
31 | assert_select 'script[src*="/plugin_assets/redmine_quick_replies/javascripts/replies.js"]'
32 | assert_select 'link[href*="/plugin_assets/redmine_quick_replies/stylesheets/replies.css"]'
33 | end
34 |
35 | def test_create_reply_with_valid_fields
36 | reply = new_record(Reply) do
37 | compatible_request :post, :create, :reply => { :name => 'Quick Reply Name', :body => 'Body' }
38 |
39 | assert_response 302
40 | assert_redirected_to :controller => 'replies', :action => 'index'
41 | end
42 |
43 | assert User.current, reply.user
44 | end
45 |
46 | def test_create_reply_public
47 | reply = new_record(Reply) do
48 | compatible_request :post, :create, :reply => { :name => 'Quick Reply Name', :is_public => '1', :body => 'Body' }
49 |
50 | assert_response 302
51 | assert_redirected_to :controller => 'replies', :action => 'index'
52 | end
53 |
54 | assert User.current, reply.user
55 | assert true, reply.is_public
56 | end
57 |
58 | def test_create_reply_should_validate_required_fields_on_empty_post
59 | compatible_request :post, :create
60 |
61 | assert_response :success
62 |
63 | assert_select_error(/Name cannot be blank/i)
64 | assert_select_error(/Reply cannot be blank/i)
65 | end
66 |
67 | def test_create_reply_should_validate_required_fields
68 | compatible_request :post, :create, :reply => { :name => '', :body => '' }
69 |
70 | assert_response :success
71 |
72 | assert_select_error(/Name cannot be blank/i)
73 | assert_select_error(/Reply cannot be blank/i)
74 | end
75 |
76 | def test_create_reply_without_manage_public_permission
77 | @request.session[:user_id] = 2
78 |
79 | compatible_request :post, :create, :reply => { :name => 'Quick Reply Name', :is_public => '1', :body => 'Body' }
80 |
81 | assert_response 403
82 | end
83 |
84 | def test_edit_should_load_content
85 | compatible_request :get, :edit, :id => 1
86 |
87 | assert_response :success
88 |
89 | assert_select 'input[type="text"][value="Reply 1"]'
90 | end
91 |
92 | def test_edit_for_non_existing_reply
93 | compatible_request :get, :edit, :id => 999
94 |
95 | assert_response 404
96 | end
97 |
98 | def test_edit_for_another_user_reply
99 | compatible_request :get, :edit, :id => 2
100 |
101 | assert_response 403
102 | end
103 |
104 | def test_update_reply_with_valid_fields
105 | compatible_request :put, :update, :id => 1, :reply => { :name => 'Quick Reply Name', :body => 'Body' }
106 |
107 | assert_response 302
108 | assert_redirected_to :controller => 'replies', :action => 'index'
109 | end
110 |
111 | def test_update_for_non_existing_reply
112 | compatible_request :put, :update, :id => 999
113 |
114 | assert_response 404
115 | end
116 |
117 | def test_update_for_another_user_reply
118 | compatible_request :put, :update, :id => 2
119 |
120 | assert_response 403
121 | end
122 |
123 | def test_update_reply_should_keep_reply_unchanged_on_empty_post
124 | compatible_request :put, :update, :id => 1
125 |
126 | assert_response 302
127 | assert_redirected_to :controller => 'replies', :action => 'index'
128 | end
129 |
130 | def test_update_reply_should_validate_required_fields
131 | compatible_request :put, :update, :id => 1, :reply => { :name => '', :body => '' }
132 |
133 | assert_response :success
134 |
135 | assert_select_error(/Name cannot be blank/i)
136 | assert_select_error(/Reply cannot be blank/i)
137 | end
138 |
139 | def test_update_without_manage_public_permission
140 | @request.session[:user_id] = 2
141 |
142 | compatible_request :put, :update, :id => 1, :reply => { :name => 'Quick Reply Name', :is_public => '1', :body => 'Body' }
143 |
144 | assert_response 403
145 | end
146 |
147 | def test_destroy_reply
148 | compatible_request :delete, :destroy, :id => 1
149 |
150 | assert_response 302
151 | assert_redirected_to :controller => 'replies', :action => 'index'
152 | assert ! Reply.find_by_id(1)
153 | end
154 |
155 | def test_destroy_for_non_existing_reply
156 | compatible_request :delete, :destroy, :id => 999
157 |
158 | assert_response 404
159 | end
160 |
161 | def test_destroy_for_another_user_reply
162 | compatible_request :delete, :destroy, :id => 2
163 |
164 | assert_response 403
165 | end
166 | end
167 |
--------------------------------------------------------------------------------
/test/test_helper.rb:
--------------------------------------------------------------------------------
1 | $VERBOSE = nil # for hide ruby warnings
2 |
3 | # Load the Redmine helper
4 | require File.expand_path(File.dirname(__FILE__) + '/../../../test/test_helper')
5 |
6 | # Enable project fixtures
7 | ActiveRecord::FixtureSet.create_fixtures(File.dirname(__FILE__) + '/fixtures/', [:replies])
8 |
9 | module RedmineQuickReplies
10 | module TestHelper
11 | def compatible_request(type, action, parameters = {})
12 | return send(type, action, :params => parameters) if Rails.version >= '5.1'
13 | send(type, action, parameters)
14 | end
15 |
16 | def compatible_xhr_request(type, action, parameters = {})
17 | return send(type, action, :params => parameters, :xhr => true) if Rails.version >= '5.1'
18 | xhr type, action, parameters
19 | end
20 |
21 | def compatible_api_request(type, action, parameters = {}, headers = {})
22 | return send(type, action, :params => parameters, :headers => headers) if Redmine::VERSION.to_s >= '3.4'
23 | send(type, action, parameters, headers)
24 | end
25 | end
26 | end
27 |
28 | include RedmineQuickReplies::TestHelper
29 |
--------------------------------------------------------------------------------