📜 ⬆️ ⬇️

Writing a Redmine Plugin

Probably many have heard about the Redmine project management system, and some may even have used it in their work. Redmine is a fairly flexible cross-platform system written in the well-known Ruby on Rails framework. Like most similar systems, Redmine allows you to extend its functionality through third-party plug-ins. At the moment there are already more than a thousand of such plug-ins for different taste and color. I want to talk about one of them and how to write a plug-in to Redmine using his example.

Repository plugin - a plugin that allows you to download files and folders from the repository (repository) of the project in one zip-archive. Redmine offers a visual guide to creating plugins.
So, let's start writing the plugin. Everything that is written below concerns redmine installed on a web server with Linux.
First, we move to the directory where redmine is installed (for example / var / www / redmine):

$ cd /var/www/redmine

Set the environment variable RAILS_ENV:

$ export RAILS_ENV="production"

Now we can generate the necessary files for our plugin:
')
$ ruby script/generate redmine_plugin repository
create vendor/plugins/redmine_repository/app/controllers
create vendor/plugins/redmine_repository/app/helpers
create vendor/plugins/redmine_repository/app/models
create vendor/plugins/redmine_repository/app/views
create vendor/plugins/redmine_repository/db/migrate
create vendor/plugins/redmine_repository/lib/tasks
create vendor/plugins/redmine_repository/assets/images
create vendor/plugins/redmine_repository/assets/javascripts
create vendor/plugins/redmine_repository/assets/stylesheets
create vendor/plugins/redmine_repository/lang
create vendor/plugins/redmine_repository/README
create vendor/plugins/redmine_repository/init.rb
create vendor/plugins/redmine_repository/lang/en.yml

Edit the file vendor / plugins / redmine_repository / init.rb, specifying
it contains the name, description of the plugin, the author and the minimum version of redmine for which the plugin is written:
 require 'redmine' require 'dispatcher' Redmine::Plugin.register :redmine_repository do name 'Redmine Repository plugin' author 'Sanny' description 'This is a reposirory plugin for Redmine' version '0.0.2' requires_redmine :version_or_higher => '1.1.2' end 

Our plugin should allow you to choose which files to download from the repository. Therefore, it is necessary to add checkboxes to the existing storage type
image
Because It is necessary to change the existing type of storage, the easiest way is to copy the files that are responsible for the appearance of the storage into the appropriate directory of our plugin and then edit these files:

$ cp app/views/repositories/_dir_lsit.rhtml vendor/plugins/redmine_repository/app/views/repositories
$ cp app/views/repositories/_dir_lsit_content.rhtml vendor/plugins/redmine_repository/app/views/repositories
$ cp app/views/repositories/show.rhtml vendor/plugins/redmine_repository/app/views/repositories

The _dir_lsit.rhtml file is responsible for displaying the storage file system tree. There we will add a “Download” button to download:
 <% if authorize_for('repositories', 'entries_operation') %> <div style="float: right;"> <%= submit_tag(l(:Download), :name => "download_entries") %> </div> <% end %> 

In order for the button to work, we wrap all the code in form_tag:
 <% form_tag({:action => "entries_operation"}, :method => :post, :id => "Entries") do %> ... <% if authorize_for('repositories', 'entries_operation') %> <div style="float: right;"> <%= submit_tag(l(:Download), :name => "download_entries") %> </div> <% end %> <p> </p> <% end %> 

Now add a checkbox to each entry in the vault mapping. To do this, make the following change to the _dir_lsit_content.rhtml file:
instead
 <% if entry.is_dir? %> <span class="expander" onclick="<%= remote_function :url => {:action => 'show', :id => @project, :path => to_path_param(ent_path), :rev => @rev, :depth => (depth + 1), :parent_id => tr_id}, :method => :get, :update => { :success => tr_id }, :position => :after, :success => "scmEntryLoaded('#{tr_id}')", :condition => "scmEntryClick('#{tr_id}')"%>"> </span> <% end %> 

write
 <% if entry.is_dir? %> <span class="expander" onclick="<%= remote_function :url => {:action => 'show', :id => @project, :path => to_path_param(entry.path), :rev => @rev, :depth => (depth + 1), :parent_id => tr_id, :p arent_val => entry.path}, :method => :get, :update => { :success => tr_id }, :position => :after, :success => "scmEntryLoaded('#{tr_id}')", :complete => "checkBranch('#{tr_id}', getParentNodeChecked('#{params[:parent_val]}'))", :condition => "scmEntryClick('#{tr_id}')"%>"> </span> <span><%= check_box_tag("folders[]", entry.path, false, :id => params[:parent_id], :onclick => "checkBranch('#{tr_id}', this.checked);" ) if authorize_for('repositories', 'entries_operation') %></span> <% else %> <span style="padding-left: 8px"> </span> <span><%= check_box_tag("files[]", entry.path, false, :id => params[:parent_id]) if authorize_for('repositories', 'entries_operation') %></span> <% end %> 

Here two self-written javascript functions are used:
getParentNodeChecked(parentVal) - determines the state of the checkbox of the parent of this tree node,
checkBranch(parentId, checked) - sets the checkboxes of the descendants of the tree node to the state of the parent (roughly speaking, if a directory is selected, then all the subdirectories and files in this directory are highlighted).
Create them in the new assets / javascripts / repository.js file
 function getParentNodeChecked(parentVal) { var form = document.getElementById('Entries'); for (var i=0;i<form.elements.length;i++) { var e = form.elements[i]; if(e.type=='checkbox' && e.value == parentVal) return e.checked; } } function checkBranch(parentId, checked) { var allchecked = true; var has_check_tree = false; var form = document.getElementById('Entries'); for (var i=0;i<form.elements.length;i++) { var e = form.elements[i]; if(e.type=='checkbox') { if(e.id == parentId) { e.checked = !checked; e.click(); } allchecked = allchecked && e.checked; if(e.name == "check_tree") has_check_tree = true; } } if(has_check_tree) form.check_tree.checked = allchecked; } 

Let's connect our repository.js to our new repository mapping (in the show.rhtml file):
 ... <% content_for :header_tags do %> <%= stylesheet_link_tag "scm" %> <%= javascript_include_tag "repository.js", :plugin => "redmine_repository" %> <% end %> 

It remains to add the function of archiving the selected folders and files. The implementation will be done in ruby. Additionally, to support zip archives, you need to install the rubyzip package (gem install rubyzip).
Create a RepositoryZip class that will directly archive (app / helpers / repository_zip.rb file):
 require 'zip/zip' require 'zip/zipfilesystem' class RepositoryZip attr_reader :file_count def initialize() @zip = Tempfile.new(["repository_zip",".zip"]) @zip_file = Zip::ZipOutputStream.new(@zip.path) @file_count = 0 end def finish @zip_file.close unless @zip_file.nil? @zip.path unless @zip.nil? end def close @zip_file.close unless @zip_file.nil? @zip.close unless @zip.nil? end def add_file(file, cat) @zip_file.put_next_entry(file) @zip_file.write(cat) @file_count += 1 end def add_folder(folder) @zip_file.put_next_entry(folder + "/") end end 

Now let's “patch” the class RepositoriesController existing in redmine by adding several methods to it (file lib / repositories_controller_patch.rb). I will not dwell on the logic of this “patch”, just give the code:
 require 'tree' require_dependency 'application_controller' require_dependency 'repositories_controller' require_dependency 'repository_zip' module RepositoriesControllerPatch def self.included(base) # :nodoc: base.extend(ClassMethods) base.send(:include, InstanceMethods) base.class_eval do unloadable #  unloadable      end end module ClassMethods end module InstanceMethods def entries_operation selected_folders = params[:folders].nil? ? [] : params[:folders] selected_files = params[:files].nil? ? [] : params[:files] if selected_folders.empty? && selected_files.empty? flash[:warning] = l(:warning_no_entries_selected) redirect_to :action => "show", :id => @project, :path => @path return end # make a selected files and folders tree selected_tree = Tree::TreeNode.new(".", "root") selected_files.each do |file| folder = Pathname.new(file).dirname.to_s selected_tree_node = selected_tree if !folder.match(/^\.+$/) folder.split("/").each do if selected_tree_node[folder].nil? selected_tree_node = selected_tree_node.add(Tree::TreeNode.new(folder, "folder")) else selected_tree_node = selected_tree_node[folder] end end selected_tree_node << Tree::TreeNode.new(file, "file") else selected_tree << Tree::TreeNode.new(file, "file") end end selected_folders.each do |folder| selected_tree_node = selected_tree folder.split("/").each do if selected_tree_node[folder].nil? selected_tree_node = selected_tree_node.add(Tree::TreeNode.new(folder, "folder")) else selected_tree_node = selected_tree_node[folder] end end selected_tree_node << Tree::TreeNode.new(file, "file") else selected_tree << Tree::TreeNode.new(file, "file") end end selected_folders.each do |folder| selected_tree_node = selected_tree folder.split("/").each do if selected_tree_node[folder].nil? selected_tree_node = selected_tree_node.add(Tree::TreeNode.new(folder, "folder")) else selected_tree_node = selected_tree_node[folder] end end end begin if !params[:email_entries].blank? email_entries(selected_tree) else download_entries(selected_tree) end rescue => e flash[:warning] = l(:error_in_getting_files) + " (" + e.message + ")" redirect_to :action => "show", :id => @project end end def download_entries(selected_tree) zip = RepositoryZip.new zip_entries(zip, selected_tree) send_file(zip.finish, :filename => filename_for_content_disposition(@project.name + "-" + DateTime.now.strftime("%y%m%d%H%M%S") + ".zip"), :type => "application/zip", :disposition => "attachment") ensure zip.close unless zip.nil? end def zip_entries(zip, selected_tree) selected_tree.children.each do |node| if node.content == "file" zip.add_file(node.name, @repository.cat(node.name, @rev)) else zip.add_folder(node.name) if node.hasChildren? # add selected subfolders zip_entries(zip, node) else # add all subfolders with files entries = @repository.entries(node.name, @rev) entries.each do |entry| node << Tree::TreeNode.new(entry.path, entry.is_dir? ? "folder" : "file") end end zip_entries(zip, node) end end zip end end # of InstaceMethods end # of module RepositoriesController.send(:include, RepositoriesControllerPatch) 

The entries_operation method entries_operation responsible for generating a list of files and folders for archiving.
The download_entries method is responsible for downloading the archive by the user.
The zip_entries method zip_entries responsible for archiving the selected files and folders.

That's all for all. All that remains is to add permissions to download files to users of the redmine system and localize the plugin. Edit the init.rb file to add permissions:
 require 'redmine' require 'dispatcher' require 'repositories_controller_patch' Redmine::Plugin.register :redmine_repository do name 'Redmine Repository plugin' author 'Sanny' description 'This is a reposirory plugin for Redmine' version '0.0.2' requires_redmine :version_or_higher => '1.1.2' end Redmine::AccessControl.map do |map| map.project_module :repository do |map| map.permission :operations, :repositories => [:entries_operation] end end 

Localization files are located in the config / locales directory. For the Russian language create a file ru.yml:
 ru: Download: "Download" warning_no_entries_selected: "  " error_in_getting_files: "    " permission_operations: "    " 

Restart redmine, set permissions for downloading files with one archive for different users of the system and use.

Source: https://habr.com/ru/post/130088/


All Articles