Skip to content
Snippets Groups Projects
Commit bd3d99fb authored by milan.brabec's avatar milan.brabec
Browse files

Initial commit

parents
No related branches found
No related tags found
No related merge requests found
Showing
with 465 additions and 0 deletions
= Sperling Sparkline Plugin pro Redmine
Plugin zobrazuje vývoje plnění úkolu v čase pomocí grafu sparkline.
Sparkline se zobrazí vpravo od grafiky míry splnění úkolu v procentech (sloupec "Hotovo").
Konfigurace:
- barva grafu (CSS jméno barvy, např. <tt>lightblue</tt>, <tt>darkgreen</tt>)
- barva výplně pod křivkou
- minimum časové osy: počátek úkolu nebo určený počet předcházejících dnů
- maximum časové osy: dnes nebo konec úkolu
Konfigurace je globální i na úrovni projektu. Porjektová přebírá nastavení z globálního (jako default).
class SparklineSetting
def self.[](name, project_id=nil)
if project_id
project_settings = settings[name.to_s + '-' + project_id.to_s]
end
if project_settings.blank?
project_settings = settings[name.to_s]
end
return project_settings
end
def self.x_axis_min_options
{
offset: I18n.t('sparkline_settings.offset'),
issue_created: I18n.t('sparkline_settings.issue_created')
}
end
def self.x_axis_max_options
{
today: I18n.t('sparkline_settings.today'),
issue_due: I18n.t('sparkline_settings.issue_due')
}
end
private
def self.settings
Setting[:plugin_sparkline].blank? ? {} : Setting[:plugin_sparkline]
end
end
<% content_for :header_tags do %>
<%= javascript_include_tag 'jquery.sparkline', plugin: 'sparkline' %>
<%= javascript_include_tag 'sparkline', plugin: 'sparkline' %>
<%= stylesheet_link_tag 'sparkline', plugin: 'sparkline' %>
<% end %>
<datalist id='sparkline-list'>
<% @issues.each do |issue| %>
<%#= issue.id %>
<% result = IssueSparkline.new(issue).to_tag(:option) %>
<%= result %>
<% end %>
</datalist>
<%= render 'hooks/sparkline_import' %>
<script type="text/javascript">
issues_list_sparkline();
</script>
\ No newline at end of file
<datalist id='sparkline-list'>
<%= IssueSparkline.new(@issue).to_tag(:option) %>
</datalist>
<%= render 'hooks/sparkline_import' %>
<script type="text/javascript">
issues_show_sparkline();
</script>
<%= title [l(:label_plugins), {:controller => 'admin', :action => 'plugins'}], :sparkline %>
<div id="settings">
<%= form_tag({:action => 'plugin', :controller => 'settings'}) do %>
<div class="box tabular settings">
<%= render :partial => 'settings/sparkline/sparkline', :locals => {:settings => (Setting.send "plugin_sparkline")}%>
</div>
<%= submit_tag l(:button_apply) %>
<% end %>
</div>
<% project_suffix = @project ? '-' + @project.id.to_s : '' %>
<% @settings ||= settings %>
<% if @project %>
<h3><%= @project.name %></h3>
<%= hidden_field_tag 'project_id', @project.identifier %>
<% else %>
<h3> <%= t('sparkline_settings.global_plugin_settings') %></h3>
<p>
<%= label_tag 'line_color', t('sparkline_settings.line_color') %>
<%= text_field_tag 'settings[line_color]',
@settings[:line_color.to_s],
size: 30,
id: 'line_color' %>
</p>
<p>
<%= label_tag 'fill_color', t('sparkline_settings.fill_color') %>
<%= text_field_tag 'settings[fill_color]',
@settings[:fill_color.to_s],
size: 30,
id: 'fill_color' %>
</p>
<hr />
<h3> <%= t('sparkline_settings.default_plugin_settings') %></h3>
<% end %>
<p>
<%= label_tag 'settings[x_min]', t('sparkline_settings.min_x') %>
<%= select_tag 'settings[x_min' + project_suffix + ']',
options_from_collection_for_select(SparklineSetting.x_axis_min_options, :first, :last, @settings[:x_min.to_s + project_suffix].blank? ? @settings[:x_min.to_s] : @settings[:x_min.to_s + project_suffix]),
id: 'x_min',
include_blank: false
%>
</p>
<p>
<%= label_tag 'settings[x_max]', t('sparkline_settings.max_x') %>
<%= select_tag 'settings[x_max' + project_suffix + ']',
options_from_collection_for_select(SparklineSetting.x_axis_max_options, :first, :last, @settings[:x_max.to_s + project_suffix].blank? ? @settings[:x_max.to_s] : @settings[:x_max.to_s + project_suffix]),
id: 'x_max',
include_blank: false
%>
</p>
<p>
<%= label_tag 'time_offset', t('sparkline_settings.days_offset') %>
<%= number_field_tag 'settings[time_offset' + project_suffix + ']',
@settings[:time_offset.to_s + project_suffix].blank? ? @settings[:time_offset.to_s] : @settings[:time_offset.to_s + project_suffix],
size: 30,
id: 'time_offset',
min: 1,
disabled: (@settings[:x_min.to_s + project_suffix].blank? ? @settings[:x_min.to_s] : @settings[:x_min.to_s + project_suffix]) != "offset" %>
</p>
<script type="application/javascript">
$("#x_min").change(function () {
if ($("#x_min").val() == "offset") {
$("#time_offset").removeAttr("disabled");
} else {
$("#time_offset").attr("disabled", true);
}
});
</script>
This diff is collapsed.
This diff is collapsed.
const sparkline_selector = '.done_ratio_sparkline';
function draw() {
$(sparkline_selector).sparkline('html', {enableTagOptions: true});
};
function move(tag, targetSelector) {
$(sparkline_selector)
// .children()
.each(function (i, v) {
let elem = document.createElement(tag);
elem = $(elem);
var attributes = $(v).prop("attributes");
$.each(attributes, function() {
elem.attr(this.name, this.value);
});
$(elem).insertAfter($(targetSelector(v)));
});
};
const issues_list_selector = function (v) {
return "tr[id='issue-" + $(v).attr("issueid") + "'] td.done_ratio"
};
const issues_show_selector = function (v) {
return ".progress.attribute .percent"
};
function issues_list_sparkline() {
$("th.done_ratio").attr('colspan', 2);
move("td", issues_list_selector);
draw();
}
function issues_show_sparkline() {
move("div", issues_show_selector);
draw();
}
\ No newline at end of file
# Czech strings go here for Rails i18n
cs:
sparkline: "Sparkline"
sparkline_settings:
global_plugin_settings: "Globální nastavení"
line_color: "Barva grafu"
fill_color: "Barva výplně pod grafem"
default_plugin_settings: "Defautní nastavení projektu"
min_x: "Minimum (osa x)"
max_x: "Maximum (osa x)"
days_offset: "Časový posun ve dnech"
offset: "Před několika dny"
issue_created: "Počátek úkolu"
today: "Dnes"
issue_due: "Termín úkolu"
# English strings go here for Rails i18n
en:
sparkline: "Sparkline"
sparkline_settings:
global_plugin_settings: "Global settings"
line_color: "Line color"
fill_color: "Fill color"
default_plugin_settings: "Default project settings"
min_x: "Minimum (axis x)"
max_x: "Maximum (axis x)"
days_offset: "Time offset in days"
offset: "Offset days ago"
issue_created: "Issue created date"
today: "Today"
issue_due: "Issue due date"
# Plugin's routes
# See: http://guides.rubyonrails.org/routing.html
# match 'settings/plugin/:id', :controller => 'sparkline_settings', :action => 'plugin_project', :via => [:get, :post], :as => 'plugin_settings'
# match 'sparkline_settings/plugin_project', :controller => 'sparkline_settings', :action => 'plugin_project', :via => [:get, :post], :as => 'plugin_settings'
# match 'settings/plugin(/:project_id)', :controller => 'settings', :action => 'plugin', :via => [:get, :post], :as => 'plugin_settings_witproject'
match 'projects/:project_id/settings(/:tab)', :controller => 'projects', :action => 'settings', :via => [:get, :post], :as => 'plugin_settings_project'
init.rb 0 → 100644
Redmine::Plugin.register :sparkline do
name 'Sparkline plugin'
author 'Sperling'
description 'Sparkline plugin for done ratio of issues'
version '1.0.0'
url ''
author_url 'https://www.sperling.cz/'
require_dependency 'sparkline_hook_listener'
project_module :sparkline do
settings default: {
x_min: "offset",
x_max: "today",
time_offset: 7,
line_color: "blue",
fill_color: "skyblue",
}, partial: 'settings/sparkline/sparkline'
permission :sparkline, {
settings: [ :plugin ]
}
end
end
require 'action_view/helpers/tag_helper'
class IssueSparkline
include ActionView::Helpers::TagHelper
def initialize(issue, options={})
@issue = issue
@min_x = issue.created_on.to_date
@values = Hash.new
issue.journals.each do |journal|
details = journal.visible_details.select { |det| det.prop_key == 'done_ratio' }
details.each do |detail|
if @values.blank?
@values[@min_x] = detail.old_value
else
@values[journal.created_on.to_date] = detail.value
end
end
end
@max_x = Time.now.to_date
@values[@max_x] = issue.done_ratio.to_s
case SparklineSetting[:x_min, issue.project.id]
when "offset"
@min_x = (Time.now - SparklineSetting[:time_offset, issue.project.id].to_i * 60 * 60 * 24).to_date
last = nil
@values.each do |key,value|
if key < @min_x
last = [key, value]
@values.delete(key)
end
end
unless last.nil? || @values[@min_x]
first = @values.first
vali = last[1].to_i + (last[1].to_i - first[1].to_i)/(last[0] - first[0])*(@min_x - last[0])
@values[@min_x] = vali.to_i.to_s
end
when "issue_created"
else
end
case SparklineSetting[:x_max, issue.project.id]
when "today"
when "issue_due"
unless issue.due_date.blank?
@max_x = issue.due_date.to_date
end
else
end
end
def to_tag(tag=:div)
string_values = []
@values.sort.to_h.each {|key, value| string_values.append "#{key.to_time.to_i}:#{value}" }
content_tag(tag,
"",
values: string_values.join(","),
class: 'done_ratio_sparkline',
issueId: @issue.id,
sparkType: 'line',
sparkWidth: '150',
sparkHeight: '25',
sparkChartRangeMin: '0',
sparkChartRangeMax: '100',
sparkChartRangeMinX: @min_x.to_time.to_i.to_s,
sparkChartRangeMaxX: @max_x.to_time.to_i.to_s
)
end
end
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006-2019 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
module ProjectsHelperPatch
def self.included(base)
# :nodoc:
base.extend(ClassMethods)
base.send(:include, InstanceMethods)
# Same as typing in the class
base.class_eval do
unloadable # Send unloadable so it will not be unloaded in development
alias_method :project_settings_tabs_without_sparkline, :project_settings_tabs
alias_method :project_settings_tabs, :project_settings_tabs_with_sparkline
end
end
module ClassMethods
end
module InstanceMethods
def project_settings_tabs_with_sparkline
tabs = project_settings_tabs_without_sparkline
tabs.push ( {:name => 'sparkline', :action => :sparkline, :partial => 'settings/plugin_sparkline', :label => :sparkline})
tabs
end
end
end
unless ProjectsHelper.included_modules.include? ProjectsHelperPatch
ProjectsHelper.send(:include, ProjectsHelperPatch)
end
module SettingsControllerPatch
def self.included(base)
# :nodoc:
base.extend(ClassMethods)
base.send(:include, InstanceMethods)
# Same as typing in the class
base.class_eval do
unloadable # Send unloadable so it will not be unloaded in development , :authorize
before_action :find_project, :authorize, :only => [:plugin], unless: -> { params[:project_id].blank? }
alias_method :old_plugin, :plugin
alias_method :plugin, :new_plugin
end
end
module ClassMethods
end
module InstanceMethods
def new_plugin
unless params[:id] == :sparkline
return old_plugin
end
@plugin = Redmine::Plugin.find(params[:id])
unless @plugin.configurable?
render_404
return
end
if request.post?
setting = params[:settings] ? params[:settings].permit!.to_h : {}
orig_setting = Setting.send "plugin_#{@plugin.id}"
orig_setting = orig_setting.merge!(setting) { |key, old, new| new }
Setting.send "plugin_#{@plugin.id.to_s}=", orig_setting
flash[:notice] = l(:notice_successful_update)
if @project.blank?
redirect_to plugin_settings_path
else
redirect_to plugin_settings_project_path(@project.identifier, @plugin.id)
end
else
@partial = @plugin.settings[:partial]
@settings = Setting.send "plugin_#{@plugin.id}"
end
rescue Redmine::PluginNotFound
render_404
end
def find_project
# @project variable must be set before calling the authorize filter
@project = Project.find(params[:project_id]) unless params[:project_id].blank?
end
end
end
unless SettingsController.included_modules.include? SettingsControllerPatch
SettingsController.send(:include, SettingsControllerPatch)
end
\ No newline at end of file
class SparklineHookListener < Redmine::Hook::ViewListener
render_on :view_issues_index_bottom, partial: "hooks/sparkline_issues_list"
render_on :view_issues_show_details_bottom, partial: "hooks/sparkline_issues_show"
end
\ No newline at end of file
require File.expand_path('../../test_helper', __FILE__)
class SparklineControllerTest < ActionController::TestCase
# Replace this with your real tests.
def test_truth
assert true
end
end
# Load the Redmine helper
require File.expand_path(File.dirname(__FILE__) + '/../../../test/test_helper')
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment