├── screenshot.png
├── test
├── test_helper.rb
└── functional
│ └── custom_reports_controller_test.rb
├── config
├── routes.rb
├── database-postgresql-travis.yml
├── database-mysql-travis.yml
└── locales
│ ├── es.yml
│ ├── en.yml
│ ├── tr.yml
│ ├── ru.yml
│ ├── hu.yml
│ └── pt-br.yml
├── assets
├── stylesheets
│ ├── custom_report.css
│ └── nv.d3.css
└── javascripts
│ ├── custom_report_edit.js
│ ├── custom_report_charts.js
│ └── d3.v2.min.js
├── lib
├── redmine_custom_reports
│ ├── user_patch.rb
│ └── project_patch.rb
└── redmine_custom_reports.rb
├── db
└── migrate
│ ├── 20121212125003_remove_filters_from_custom_reports.rb
│ ├── 20121212125002_create_custom_report_series.rb
│ └── 20121212125001_create_custom_reports.rb
├── app
├── views
│ └── custom_reports
│ │ ├── _links.html.erb
│ │ ├── new.html.erb
│ │ ├── edit.html.erb
│ │ ├── _series.html.erb
│ │ ├── _sidebar.html.erb
│ │ ├── show.html.erb
│ │ ├── index.html.erb
│ │ ├── _form.html.erb
│ │ └── _series_filters.html.erb
├── models
│ ├── query_ext.rb
│ ├── custom_report_series.rb
│ └── custom_report.rb
├── helpers
│ └── custom_reports_helper.rb
└── controllers
│ └── custom_reports_controller.rb
├── init.rb
├── .travis.yml
└── README.md
/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Restream/redmine_custom_reports/HEAD/screenshot.png
--------------------------------------------------------------------------------
/test/test_helper.rb:
--------------------------------------------------------------------------------
1 | # Load the normal Rails helper
2 | require File.expand_path(File.dirname(__FILE__) + '/../../../test/test_helper')
3 |
--------------------------------------------------------------------------------
/config/routes.rb:
--------------------------------------------------------------------------------
1 | RedmineApp::Application.routes.draw do
2 | resources :projects do
3 | resources :custom_reports
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/config/database-postgresql-travis.yml:
--------------------------------------------------------------------------------
1 | # https://docs.travis-ci.com/user/database-setup/#PostgreSQL
2 | test:
3 | adapter: postgresql
4 | database: redmine
5 | username: postgres
6 |
--------------------------------------------------------------------------------
/config/database-mysql-travis.yml:
--------------------------------------------------------------------------------
1 | # https://docs.travis-ci.com/user/database-setup/#MySQL
2 | test:
3 | adapter: mysql2
4 | database: redmine
5 | username: travis
6 | encoding: utf8
7 |
--------------------------------------------------------------------------------
/assets/stylesheets/custom_report.css:
--------------------------------------------------------------------------------
1 | .custom-report {
2 | width: 100%;
3 | }
4 |
5 | .custom-report-chart {
6 | float: left;
7 | }
8 |
9 | .custom-report-chart svg {
10 | height: 500px;
11 | overflow: visible;
12 | }
13 |
--------------------------------------------------------------------------------
/lib/redmine_custom_reports/user_patch.rb:
--------------------------------------------------------------------------------
1 | require_dependency 'user'
2 |
3 | module RedmineCustomReports
4 | module UserPatch
5 | def self.included(base)
6 | base.send :has_many, :custom_reports, dependent: :destroy
7 | end
8 | end
9 | end
10 |
11 | unless User.included_modules.include? RedmineCustomReports::UserPatch
12 | User.send :include, RedmineCustomReports::UserPatch
13 | end
--------------------------------------------------------------------------------
/lib/redmine_custom_reports/project_patch.rb:
--------------------------------------------------------------------------------
1 | require_dependency 'project'
2 |
3 | module RedmineCustomReports
4 | module ProjectPatch
5 | def self.included(base)
6 | base.send :has_many, :custom_reports, dependent: :destroy
7 | end
8 | end
9 | end
10 |
11 | unless Project.included_modules.include? RedmineCustomReports::ProjectPatch
12 | Project.send :include, RedmineCustomReports::ProjectPatch
13 | end
--------------------------------------------------------------------------------
/db/migrate/20121212125003_remove_filters_from_custom_reports.rb:
--------------------------------------------------------------------------------
1 | class RemoveFiltersFromCustomReports < ActiveRecord::Migration
2 | def self.up
3 | if column_exists? :custom_reports, :filters
4 | remove_column :custom_reports, :filters
5 | end
6 | end
7 |
8 | def self.down
9 | unless column_exists? :custom_reports, :filters
10 | add_column :custom_reports, :filters, :string
11 | end
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/app/views/custom_reports/_links.html.erb:
--------------------------------------------------------------------------------
1 | <% if custom_reports && custom_reports.any? %>
2 | <% custom_reports.each do |custom_report| %>
3 | <%= link_to custom_report.name,
4 | { controller: 'custom_reports',
5 | action: 'show',
6 | project_id: @project,
7 | id: custom_report.id } %>
8 | <% end %>
9 | <% else %>
10 | <%= l(:label_no_data) %>
11 | <% end %>
12 |
--------------------------------------------------------------------------------
/app/views/custom_reports/new.html.erb:
--------------------------------------------------------------------------------
1 |
<%= @custom_report.description %>
27 | 28 | <%= content_tag :div, 29 | class: 'custom-report', 30 | 'data-custom_report_info' => @custom_report.info.to_json do %> 31 | <% if @custom_report.multi_series? %> 32 | <% content_tag :div, 33 | class: 'custom-report-chart', 34 | style: width_style_for_series(@custom_report), 35 | 'data-chart_data' => @custom_report.data.to_json do %> 36 | 37 | <% end %> 38 | <% else %> 39 | <% @custom_report.series.each do |series| %> 40 | <%= content_tag :div, 41 | class: 'custom-report-chart', 42 | style: width_style_for_series(@custom_report), 43 | 'data-chart_data' => [series.data].to_json do %> 44 || <%= l(:field_name) %> | 16 |<%= l(:field_description) %> | 17 |<%= l(:field_chart_type) %> | 18 | <% if User.current.allowed_to?(:manage_custom_reports, @project) %> 19 |<%= l(:field_is_public) %> | 20 | <% end %> 21 |22 | 23 | 24 | <% @custom_reports.each do |custom_report| %> 25 | |
|---|---|---|---|---|
|
27 | <%= link_to custom_report.name,
28 | { controller: 'custom_reports',
29 | action: 'show',
30 | project_id: @project,
31 | id: custom_report.id } %> 32 | |
33 | <%= custom_report.description %> | 34 |<%= custom_report.chart_type %> | 35 | <% if User.current.allowed_to?(:manage_custom_reports, @project) %> 36 |<%= custom_report.is_public %> | 37 | <% end %> 38 |39 | <% if custom_report.allowed_to_manage? %> 40 | <%= link_to(l(:button_edit), 41 | edit_project_custom_report_path(@project, custom_report), 42 | class: 'icon icon-edit') %> 43 | 44 | <%= link_to(l(:button_delete), 45 | project_custom_report_path(@project, custom_report), 46 | confirm: l(:text_are_you_sure), 47 | method: :delete, 48 | class: 'icon icon-del') %> 49 | <% end %> 50 | | 51 |
15 | <%= f.text_field :name, size: 80, required: true %> 16 |
17 | 18 |19 | 20 | <%= text_field 'custom_report', 'description', size: 80 %> 21 |
22 | 23 |24 | 25 | <%= select 'custom_report', 'chart_type', 26 | CustomReport::CHART_TYPES.collect { |ct| [l("label_chart_type_#{ct}"), ct] }, 27 | include_blank: false %> 28 |
29 | 30 | <% if User.current.admin? || User.current.allowed_to?(:manage_public_custom_reports, @project) %> 31 |32 | 33 | <%= check_box 'custom_report', 'is_public', 34 | onchange: (User.current.admin? ? nil : 'if (this.checked) {$("custom_report_is_for_all").checked = false; $("custom_report_is_for_all").disabled = true;} else {$("custom_report_is_for_all").disabled = false;}') %> 35 |
36 | <% end %> 37 | 38 |39 | 40 | <%= select 'custom_report', 'group_by', 41 | @custom_report.groupable_columns.collect { |c| [c.caption, c.name.to_s] }, 42 | include_blank: false %> 43 |
44 | 45 |46 | 47 | <%= text_field 'custom_report', 'null_text', size: 80 %> 48 |
49 |
6 |
|
107 | 108 | <%= label_tag("#{series_id}_add_filter_select", l(:label_filter_add)) %> 109 | <%= select_tag "#{series_id}_add_filter_select", 110 | query_options_for_select(query), 111 | class: 'add_series_filter', 112 | name: nil %> 113 | | 114 |
0&&(e=r)}return e}function _r(e,t){return e.x-t.x}function Dr(e,t){return t.x-e.x}function Pr(e,t){return e.depth-t.depth}function Hr(e,t){function n(e,r){var i=e.children;if(i&&(a=i.length)){var s,o=null,u=-1,a;while(++u=0)s=r[i]._tree,s.prelim+=t,s.mod+=t,t+=s.shift+(n+=s.change)}function jr(e,t,n){e=e._tree,t=t._tree;var r=n/(t.number-e.number);e.change+=r,t.change-=r,t.shift+=n,t.prelim+=n,t.mod+=n}function Fr(e,t,n){return e._tree.ancestor.parent==t.parent?e._tree.ancestor:n}function Ir(e){return{x:e.x,y:e.y,dx:e.dx,dy:e.dy}}function qr(e,t){var n=e.x+t[3],r=e.y+t[0],i=e.dx-t[1]-t[3],s=e.dy-t[0]-t[2];return i<0&&(n+=i/2,i=0),s<0&&(r+=s/2,s=0),{x:n,y:r,dx:i,dy:s}}function Rr(e,t){function n(e,r){d3.text(e,t,function(e){r(e&&n.parse(e))})}function r(t){return t.map(i).join(e)}function i(e){return o.test(e)?'"'+e.replace(/\"/g,'""')+'"':e}var s=new RegExp("\r\n|["+e+"\r\n]","g"),o=new RegExp('["'+e+"\n]"),u=e.charCodeAt(0);return n.parse=function(e){var t;return n.parseRows(e,function(e,n){if(n){var r={},i=-1,s=t.length;while(++i=e.length)return i;if(l)return l=!1,r;var t=s.lastIndex;if(e.charCodeAt(t)===34){var n=t;while(n++s&&(i=s),o1);return e+t*n*Math.sqrt(-2*Math.log(i)/i)}},logNormal:function(e,t){var n=arguments.length;n<2&&(t=1),n<1&&(e=0);var r=d3.random.normal();return function(){return Math.exp(e+t*r())}},irwinHall:function(e){return function(){for(var t=0,n=0;n=^]))?([+\- ])?(#)?(0)?([0-9]+)?(,)?(\.[0-9]+)?([a-zA-Z%])?/,rs=d3.map({g:function(e,t){return e.toPrecision(t)},e:function(e,t){return e.toExponential(t)},f:function(e,t){return e.toFixed(t)},r:function(e,t){return d3.round(e,t=m(e,t)).toFixed(Math.max(0,Math.min(20,t)))}}),is=["y","z","a","f","p","n","μ","m","","k","M","G","T","P","E","Z","Y"].map(b);d3.formatPrefix=function(e,t){var n=0;return e&&(e<0&&(e*=-1),t&&(e=d3.round(e,m(e,t))),n=1+Math.floor(1e-12+Math.log(e)/Math.LN10),n=Math.max(-24,Math.min(24,Math.floor((n<=0?n+1:n-1)/3)*3))),is[8+n/3]};var ss=T(2),os=T(3),us=function(){return x},as=d3.map({linear:us,poly:T,quad:function(){return ss},cubic:function(){return os},sin:function(){return N},exp:function(){return C},circle:function(){return k},elastic:L,back:A,bounce:function(){return O}}),fs=d3.map({"in":x,out:E,"in-out":S,"out-in":function(e){return S(E(e))}});d3.ease=function(e){var t=e.indexOf("-"),n=t>=0?e.substring(0,t):e,r=t>=0?e.substring(t+1):"in";return n=as.get(n)||us,r=fs.get(r)||x,w(r(n.apply(null,Array.prototype.slice.call(arguments,1))))},d3.event=null,d3.transform=function(e){var t=document.createElementNS(d3.ns.prefix.svg,"g");return(d3.transform=function(e){t.setAttribute("transform",e);var n=t.transform.baseVal.consolidate();return new P(n?n.matrix:cs)})(e)},P.prototype.toString=function(){return"translate("+this.translate+")rotate("+this.rotate+")skewX("+this.skew+")scale("+this.scale+")"};var ls=180/Math.PI,cs={a:1,b:0,c:0,d:1,e:0,f:0};d3.interpolate=function(e,t){var n=d3.interpolators.length,r;while(--n>=0&&!(r=d3.interpolators[n](e,t)));return r},d3.interpolateNumber=function(e,t){return t-=e,function(n){return e+t*n}},d3.interpolateRound=function(e,t){return t-=e,function(n){return Math.round(e+t*n)}},d3.interpolateString=function(e,t){var n,r,i,s=0,o=0,u=[],a=[],f,l;hs.lastIndex=0;for(r=0;n=hs.exec(t);++r)n.index&&u.push(t.substring(s,o=n.index)),a.push({i:u.length,x:n[0]}),u.push(null),s=hs.lastIndex;sn.dx)f=n.dx;while(++i50?n:s<-140?r:o<21?i:t)(e)}var t=d3.geo.albers(),n=d3.geo.albers().origin([-160,60]).parallels([55,65]),r=d3.geo.albers().origin([-160,20]).parallels([8,18]),i=d3.geo.albers().origin([-60,10]).parallels([8,18]);return e.scale=function(s){return arguments.length?(t.scale(s),n.scale(s*.6),r.scale(s),i.scale(s*1.5),e.translate(t.translate())):t.scale()},e.translate=function(s){if(!arguments.length)return t.translate();var o=t.scale()/1e3,u=s[0],a=s[1];return t.translate(s),n.translate([u-400*o,a+170*o]),r.translate([u-190*o,a+200*o]),i.translate([u+580*o,a+430*o]),e},e.scale(t.scale())},d3.geo.bonne=function(){function e(e){var u=e[0]*mo-r,a=e[1]*mo-i;if(s){var f=o+s-a,l=u*Math.cos(a)/f;u=f*Math.sin(l),a=f*Math.cos(l)-o}else u*=Math.cos(a),a*=-1;return[t*u+n[0],t*a+n[1]]}var t=200,n=[480,250],r,i,s,o;return e.invert=function(e){var i=(e[0]-n[0])/t,u=(e[1]-n[1])/t;if(s){var a=o+u,f=Math.sqrt(i*i+a*a);u=o+s-f,i=r+f*Math.atan2(i,a)/Math.cos(u)}else u*=-1,i/=Math.cos(u);return[i/mo,u/mo]},e.parallel=function(t){return arguments.length?(o=1/Math.tan(s=t*mo),e):s/mo},e.origin=function(t){return arguments.length?(r=t[0]*mo,i=t[1]*mo,e):[r/mo,i/mo]},e.scale=function(
4 | n){return arguments.length?(t=+n,e):t},e.translate=function(t){return arguments.length?(n=[+t[0],+t[1]],e):n},e.origin([0,0]).parallel(45)},d3.geo.equirectangular=function(){function e(e){var r=e[0]/360,i=-e[1]/360;return[t*r+n[0],t*i+n[1]]}var t=500,n=[480,250];return e.invert=function(e){var r=(e[0]-n[0])/t,i=(e[1]-n[1])/t;return[360*r,-360*i]},e.scale=function(n){return arguments.length?(t=+n,e):t},e.translate=function(t){return arguments.length?(n=[+t[0],+t[1]],e):n},e},d3.geo.mercator=function(){function e(e){var r=e[0]/360,i=-(Math.log(Math.tan(Math.PI/4+e[1]*mo/2))/mo)/360;return[t*r+n[0],t*Math.max(-0.5,Math.min(.5,i))+n[1]]}var t=500,n=[480,250];return e.invert=function(e){var r=(e[0]-n[0])/t,i=(e[1]-n[1])/t;return[360*r,2*Math.atan(Math.exp(-360*i*mo))/mo-90]},e.scale=function(n){return arguments.length?(t=+n,e):t},e.translate=function(t){return arguments.length?(n=[+t[0],+t[1]],e):n},e},d3.geo.path=function(){function e(e,t){typeof s=="function"&&(o=zr(s.apply(this,arguments))),f(e);var n=a.length?a.join(""):null;return a=[],n}function t(e){return u(e).join(",")}function n(e){var t=i(e[0]),n=0,r=e.length;while(++n