├── COPYRIGHT.txt ├── CREDITS.txt ├── GPL.txt ├── README.rdoc ├── Rakefile ├── VERSION ├── app ├── controllers │ └── system_reports_controller.rb ├── helpers │ └── system_reports_helper.rb ├── models │ ├── activity_report.rb │ └── completion_count.rb └── views │ ├── settings │ └── _redmine_reports_settings.html.erb │ └── system_reports │ ├── _billing_export.html.erb │ ├── _completion_count_for_user.html.erb │ ├── _menu.html.erb │ ├── activity_report.html.erb │ ├── completion_count.html.erb │ ├── index.html.erb │ └── quickbooks.html.erb ├── assets ├── images │ ├── chart_line.png │ ├── chart_organisation.png │ ├── house.png │ └── user_comment.png └── stylesheets │ └── redmine_reports.css ├── config └── locales │ └── en.yml ├── features ├── activity_report.feature ├── step_definitions │ ├── system_report_steps.rb │ └── webrat_steps.rb ├── support │ ├── env.rb │ ├── paths.rb │ └── permissions.rb ├── system_report_completion_count.feature ├── system_report_menu_item.feature ├── system_report_overview.feature └── system_report_quickbooks.feature ├── init.rb ├── lang └── en.yml ├── lib ├── ephemeral_model.rb └── reports │ ├── activity_report.rb │ ├── completion_count.rb │ ├── quickbooks.rb │ └── report_helper.rb ├── rails └── init.rb ├── redmine_reports.gemspec ├── spec ├── controllers │ ├── empty │ ├── system_reports_activity_report_spec.rb │ ├── system_reports_completion_count_spec.rb │ ├── system_reports_overview_spec.rb │ └── system_reports_quickbooks_spec.rb ├── models │ ├── activity_report_spec.rb │ └── completion_count_spec.rb ├── rcov.opts ├── sanity_spec.rb ├── spec.opts ├── spec_helper.rb └── views │ └── empty └── test ├── functional └── system_reports_completion_count_test.rb ├── test_helper.rb └── unit ├── completion_count_test.rb ├── plugin_configuration_test.rb └── sanity_test.rb /COPYRIGHT.txt: -------------------------------------------------------------------------------- 1 | Redmine Reports is a plugin that contains a variety of reports for Redmine. 2 | 3 | Copyright (C) 2009 Eric Davis, Little Stream Software 4 | 5 | This program is free software; you can redistribute it and/or 6 | modify it under the terms of the GNU General Public License 7 | as published by the Free Software Foundation; either version 2 8 | of the License, or (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program; if not, write to the Free Software 17 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 18 | -------------------------------------------------------------------------------- /CREDITS.txt: -------------------------------------------------------------------------------- 1 | Thanks go to the following people for patches and contributions: 2 | 3 | * Eric Davis of Little Stream Software - Project Maintainer 4 | * Peter Chester of Shane and Peter, Inc - Project Sponsor 5 | * Shane Pearlman of Shane and Peter, Inc - Project Sponsor 6 | * Bill Tihen of Leysin American School in Switzerland - Project Sponsor 7 | -------------------------------------------------------------------------------- /GPL.txt: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | = Redmine Reports 2 | 3 | Redmine Reports is a plugin that contains a variety of reports for Redmine. 4 | 5 | == Features 6 | 7 | * __FEATURE__ 8 | 9 | == Getting the plugin 10 | 11 | A copy of the plugin can be downloaded from {Little Stream Software}[https://projects.littlestreamsoftware.com/projects/redmine-reports/files] or from {GitHub}[http://github.com/edavis10/redmine_reports] 12 | 13 | 14 | == Installation and Setup 15 | 16 | 1. Follow the Redmine plugin installation steps at: http://www.redmine.org/wiki/redmine/Plugins 17 | 2. Run the plugin migrations +rake db:migrate_plugins+ 18 | 3. Restart your Redmine web servers (e.g. mongrel, thin, mod_rails) 19 | 4. Go to the Reports link on the upper leftof the menu. 20 | 21 | == Usage 22 | 23 | TODO 24 | 25 | == License 26 | 27 | This plugin is licensed under the GNU GPL v2. See COPYRIGHT.txt and GPL.txt for details. 28 | 29 | == Project help 30 | 31 | If you need help you can contact the maintainer at the Bug Tracker. The bug tracker is located at https://projects.littlestreamsoftware.com 32 | 33 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'redmine_plugin_support' 3 | 4 | Dir[File.expand_path(File.dirname(__FILE__)) + "/lib/tasks/**/*.rake"].sort.each { |ext| load ext } 5 | 6 | RedminePluginSupport::Base.setup do |plugin| 7 | plugin.project_name = 'redmine_reports' 8 | plugin.default_task = [:spec, :features] 9 | plugin.tasks = [:doc, :release, :clean, :test, :spec, :features] 10 | plugin.redmine_root = File.expand_path(File.dirname(__FILE__) + '/../../../') 11 | end 12 | 13 | begin 14 | require 'jeweler' 15 | Jeweler::Tasks.new do |s| 16 | s.name = "redmine_reports" 17 | s.summary = "This is a plugin for Redmine reports" 18 | s.email = "edavis@littlestreamsoftware.com" 19 | s.homepage = "https://projects.littlestreamsoftware.com/projects/TODO" 20 | s.description = "This is a plugin for Redmine reports" 21 | s.authors = ["Eric Davis"] 22 | s.rubyforge_project = "redmine_reports" # TODO 23 | s.files = FileList[ 24 | "[A-Z]*", 25 | "init.rb", 26 | "rails/init.rb", 27 | "{bin,generators,lib,test,app,assets,config,lang}/**/*", 28 | 'lib/jeweler/templates/.gitignore' 29 | ] 30 | end 31 | Jeweler::GemcutterTasks.new 32 | Jeweler::RubyforgeTasks.new do |rubyforge| 33 | rubyforge.doc_task = "rdoc" 34 | end 35 | rescue LoadError 36 | puts "Jeweler, or one of its dependencies, is not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com" 37 | end 38 | 39 | 40 | # Adding Rails's database rake tasks, we need the db:test:reset one in 41 | # order to clear the test database. 42 | # TODO: extract to the redmine_plugin_support gem 43 | namespace :db do 44 | task :load_config => :rails_env do 45 | require 'active_record' 46 | ActiveRecord::Base.configurations = Rails::Configuration.new.database_configuration 47 | end 48 | 49 | namespace :create do 50 | desc 'Create all the local databases defined in config/database.yml' 51 | task :all => :load_config do 52 | ActiveRecord::Base.configurations.each_value do |config| 53 | # Skip entries that don't have a database key, such as the first entry here: 54 | # 55 | # defaults: &defaults 56 | # adapter: mysql 57 | # username: root 58 | # password: 59 | # host: localhost 60 | # 61 | # development: 62 | # database: blog_development 63 | # <<: *defaults 64 | next unless config['database'] 65 | # Only connect to local databases 66 | local_database?(config) { create_database(config) } 67 | end 68 | end 69 | end 70 | 71 | desc 'Create the database defined in config/database.yml for the current RAILS_ENV' 72 | task :create => :load_config do 73 | create_database(ActiveRecord::Base.configurations[RAILS_ENV]) 74 | end 75 | 76 | def create_database(config) 77 | begin 78 | if config['adapter'] =~ /sqlite/ 79 | if File.exist?(config['database']) 80 | $stderr.puts "#{config['database']} already exists" 81 | else 82 | begin 83 | # Create the SQLite database 84 | ActiveRecord::Base.establish_connection(config) 85 | ActiveRecord::Base.connection 86 | rescue 87 | $stderr.puts $!, *($!.backtrace) 88 | $stderr.puts "Couldn't create database for #{config.inspect}" 89 | end 90 | end 91 | return # Skip the else clause of begin/rescue 92 | else 93 | ActiveRecord::Base.establish_connection(config) 94 | ActiveRecord::Base.connection 95 | end 96 | rescue 97 | case config['adapter'] 98 | when 'mysql' 99 | @charset = ENV['CHARSET'] || 'utf8' 100 | @collation = ENV['COLLATION'] || 'utf8_unicode_ci' 101 | begin 102 | ActiveRecord::Base.establish_connection(config.merge('database' => nil)) 103 | ActiveRecord::Base.connection.create_database(config['database'], :charset => (config['charset'] || @charset), :collation => (config['collation'] || @collation)) 104 | ActiveRecord::Base.establish_connection(config) 105 | rescue 106 | $stderr.puts "Couldn't create database for #{config.inspect}, charset: #{config['charset'] || @charset}, collation: #{config['collation'] || @collation} (if you set the charset manually, make sure you have a matching collation)" 107 | end 108 | when 'postgresql' 109 | @encoding = config[:encoding] || ENV['CHARSET'] || 'utf8' 110 | begin 111 | ActiveRecord::Base.establish_connection(config.merge('database' => 'postgres', 'schema_search_path' => 'public')) 112 | ActiveRecord::Base.connection.create_database(config['database'], config.merge('encoding' => @encoding)) 113 | ActiveRecord::Base.establish_connection(config) 114 | rescue 115 | $stderr.puts $!, *($!.backtrace) 116 | $stderr.puts "Couldn't create database for #{config.inspect}" 117 | end 118 | end 119 | else 120 | $stderr.puts "#{config['database']} already exists" 121 | end 122 | end 123 | 124 | namespace :drop do 125 | desc 'Drops all the local databases defined in config/database.yml' 126 | task :all => :load_config do 127 | ActiveRecord::Base.configurations.each_value do |config| 128 | # Skip entries that don't have a database key 129 | next unless config['database'] 130 | # Only connect to local databases 131 | local_database?(config) { drop_database(config) } 132 | end 133 | end 134 | end 135 | 136 | desc 'Drops the database for the current RAILS_ENV' 137 | task :drop => :load_config do 138 | config = ActiveRecord::Base.configurations[RAILS_ENV || 'development'] 139 | begin 140 | drop_database(config) 141 | rescue Exception => e 142 | puts "Couldn't drop #{config['database']} : #{e.inspect}" 143 | end 144 | end 145 | 146 | def local_database?(config, &block) 147 | if %w( 127.0.0.1 localhost ).include?(config['host']) || config['host'].blank? 148 | yield 149 | else 150 | puts "This task only modifies local databases. #{config['database']} is on a remote host." 151 | end 152 | end 153 | 154 | 155 | desc "Migrate the database through scripts in db/migrate and update db/schema.rb by invoking db:schema:dump. Target specific version with VERSION=x. Turn off output with VERBOSE=false." 156 | task :migrate => :environment do 157 | ActiveRecord::Migration.verbose = ENV["VERBOSE"] ? ENV["VERBOSE"] == "true" : true 158 | ActiveRecord::Migrator.migrate("db/migrate/", ENV["VERSION"] ? ENV["VERSION"].to_i : nil) 159 | Rake::Task["db:schema:dump"].invoke if ActiveRecord::Base.schema_format == :ruby 160 | end 161 | 162 | namespace :migrate do 163 | desc 'Rollbacks the database one migration and re migrate up. If you want to rollback more than one step, define STEP=x. Target specific version with VERSION=x.' 164 | task :redo => :environment do 165 | if ENV["VERSION"] 166 | Rake::Task["db:migrate:down"].invoke 167 | Rake::Task["db:migrate:up"].invoke 168 | else 169 | Rake::Task["db:rollback"].invoke 170 | Rake::Task["db:migrate"].invoke 171 | end 172 | end 173 | 174 | desc 'Resets your database using your migrations for the current environment' 175 | task :reset => ["db:drop", "db:create", "db:migrate"] 176 | 177 | desc 'Runs the "up" for a given migration VERSION.' 178 | task :up => :environment do 179 | version = ENV["VERSION"] ? ENV["VERSION"].to_i : nil 180 | raise "VERSION is required" unless version 181 | ActiveRecord::Migrator.run(:up, "db/migrate/", version) 182 | Rake::Task["db:schema:dump"].invoke if ActiveRecord::Base.schema_format == :ruby 183 | end 184 | 185 | desc 'Runs the "down" for a given migration VERSION.' 186 | task :down => :environment do 187 | version = ENV["VERSION"] ? ENV["VERSION"].to_i : nil 188 | raise "VERSION is required" unless version 189 | ActiveRecord::Migrator.run(:down, "db/migrate/", version) 190 | Rake::Task["db:schema:dump"].invoke if ActiveRecord::Base.schema_format == :ruby 191 | end 192 | end 193 | 194 | desc 'Rolls the schema back to the previous version. Specify the number of steps with STEP=n' 195 | task :rollback => :environment do 196 | step = ENV['STEP'] ? ENV['STEP'].to_i : 1 197 | ActiveRecord::Migrator.rollback('db/migrate/', step) 198 | Rake::Task["db:schema:dump"].invoke if ActiveRecord::Base.schema_format == :ruby 199 | end 200 | 201 | desc 'Drops and recreates the database from db/schema.rb for the current environment and loads the seeds.' 202 | task :reset => [ 'db:drop', 'db:setup' ] 203 | 204 | desc "Retrieves the charset for the current environment's database" 205 | task :charset => :environment do 206 | config = ActiveRecord::Base.configurations[RAILS_ENV || 'development'] 207 | case config['adapter'] 208 | when 'mysql' 209 | ActiveRecord::Base.establish_connection(config) 210 | puts ActiveRecord::Base.connection.charset 211 | when 'postgresql' 212 | ActiveRecord::Base.establish_connection(config) 213 | puts ActiveRecord::Base.connection.encoding 214 | else 215 | puts 'sorry, your database adapter is not supported yet, feel free to submit a patch' 216 | end 217 | end 218 | 219 | desc "Retrieves the collation for the current environment's database" 220 | task :collation => :environment do 221 | config = ActiveRecord::Base.configurations[RAILS_ENV || 'development'] 222 | case config['adapter'] 223 | when 'mysql' 224 | ActiveRecord::Base.establish_connection(config) 225 | puts ActiveRecord::Base.connection.collation 226 | else 227 | puts 'sorry, your database adapter is not supported yet, feel free to submit a patch' 228 | end 229 | end 230 | 231 | desc "Retrieves the current schema version number" 232 | task :version => :environment do 233 | puts "Current version: #{ActiveRecord::Migrator.current_version}" 234 | end 235 | 236 | desc "Raises an error if there are pending migrations" 237 | task :abort_if_pending_migrations => :environment do 238 | if defined? ActiveRecord 239 | pending_migrations = ActiveRecord::Migrator.new(:up, 'db/migrate').pending_migrations 240 | 241 | if pending_migrations.any? 242 | puts "You have #{pending_migrations.size} pending migrations:" 243 | pending_migrations.each do |pending_migration| 244 | puts ' %4d %s' % [pending_migration.version, pending_migration.name] 245 | end 246 | abort %{Run "rake db:migrate" to update your database then try again.} 247 | end 248 | end 249 | end 250 | 251 | desc 'Create the database, load the schema, and initialize with the seed data' 252 | task :setup => [ 'db:create', 'db:schema:load', 'db:seed' ] 253 | 254 | desc 'Load the seed data from db/seeds.rb' 255 | task :seed => :environment do 256 | seed_file = File.join(Rails.root, 'db', 'seeds.rb') 257 | load(seed_file) if File.exist?(seed_file) 258 | end 259 | 260 | namespace :fixtures do 261 | desc "Load fixtures into the current environment's database. Load specific fixtures using FIXTURES=x,y. Load from subdirectory in test/fixtures using FIXTURES_DIR=z. Specify an alternative path (eg. spec/fixtures) using FIXTURES_PATH=spec/fixtures." 262 | task :load => :environment do 263 | require 'active_record/fixtures' 264 | ActiveRecord::Base.establish_connection(Rails.env) 265 | base_dir = ENV['FIXTURES_PATH'] ? File.join(Rails.root, ENV['FIXTURES_PATH']) : File.join(Rails.root, 'test', 'fixtures') 266 | fixtures_dir = ENV['FIXTURES_DIR'] ? File.join(base_dir, ENV['FIXTURES_DIR']) : base_dir 267 | 268 | (ENV['FIXTURES'] ? ENV['FIXTURES'].split(/,/).map {|f| File.join(fixtures_dir, f) } : Dir.glob(File.join(fixtures_dir, '*.{yml,csv}'))).each do |fixture_file| 269 | Fixtures.create_fixtures(File.dirname(fixture_file), File.basename(fixture_file, '.*')) 270 | end 271 | end 272 | 273 | desc "Search for a fixture given a LABEL or ID. Specify an alternative path (eg. spec/fixtures) using FIXTURES_PATH=spec/fixtures." 274 | task :identify => :environment do 275 | require "active_record/fixtures" 276 | 277 | label, id = ENV["LABEL"], ENV["ID"] 278 | raise "LABEL or ID required" if label.blank? && id.blank? 279 | 280 | puts %Q(The fixture ID for "#{label}" is #{Fixtures.identify(label)}.) if label 281 | 282 | base_dir = ENV['FIXTURES_PATH'] ? File.join(Rails.root, ENV['FIXTURES_PATH']) : File.join(Rails.root, 'test', 'fixtures') 283 | Dir["#{base_dir}/**/*.yml"].each do |file| 284 | if data = YAML::load(ERB.new(IO.read(file)).result) 285 | data.keys.each do |key| 286 | key_id = Fixtures.identify(key) 287 | 288 | if key == label || key_id == id.to_i 289 | puts "#{file}: #{key} (#{key_id})" 290 | end 291 | end 292 | end 293 | end 294 | end 295 | end 296 | 297 | namespace :schema do 298 | desc "Create a db/schema.rb file that can be portably used against any DB supported by AR" 299 | task :dump => :environment do 300 | require 'active_record/schema_dumper' 301 | File.open(ENV['SCHEMA'] || "#{RAILS_ROOT}/db/schema.rb", "w") do |file| 302 | ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, file) 303 | end 304 | Rake::Task["db:schema:dump"].reenable 305 | end 306 | 307 | desc "Load a schema.rb file into the database" 308 | task :load => :environment do 309 | file = ENV['SCHEMA'] || "#{RAILS_ROOT}/db/schema.rb" 310 | if File.exists?(file) 311 | load(file) 312 | else 313 | abort %{#{file} doesn't exist yet. Run "rake db:migrate" to create it then try again. If you do not intend to use a database, you should instead alter #{RAILS_ROOT}/config/environment.rb to prevent active_record from loading: config.frameworks -= [ :active_record ]} 314 | end 315 | end 316 | end 317 | 318 | namespace :structure do 319 | desc "Dump the database structure to a SQL file" 320 | task :dump => :environment do 321 | abcs = ActiveRecord::Base.configurations 322 | case abcs[RAILS_ENV]["adapter"] 323 | when "mysql", "oci", "oracle" 324 | ActiveRecord::Base.establish_connection(abcs[RAILS_ENV]) 325 | File.open("#{RAILS_ROOT}/db/#{RAILS_ENV}_structure.sql", "w+") { |f| f << ActiveRecord::Base.connection.structure_dump } 326 | when "postgresql" 327 | ENV['PGHOST'] = abcs[RAILS_ENV]["host"] if abcs[RAILS_ENV]["host"] 328 | ENV['PGPORT'] = abcs[RAILS_ENV]["port"].to_s if abcs[RAILS_ENV]["port"] 329 | ENV['PGPASSWORD'] = abcs[RAILS_ENV]["password"].to_s if abcs[RAILS_ENV]["password"] 330 | search_path = abcs[RAILS_ENV]["schema_search_path"] 331 | search_path = "--schema=#{search_path}" if search_path 332 | `pg_dump -i -U "#{abcs[RAILS_ENV]["username"]}" -s -x -O -f db/#{RAILS_ENV}_structure.sql #{search_path} #{abcs[RAILS_ENV]["database"]}` 333 | raise "Error dumping database" if $?.exitstatus == 1 334 | when "sqlite", "sqlite3" 335 | dbfile = abcs[RAILS_ENV]["database"] || abcs[RAILS_ENV]["dbfile"] 336 | `#{abcs[RAILS_ENV]["adapter"]} #{dbfile} .schema > db/#{RAILS_ENV}_structure.sql` 337 | when "sqlserver" 338 | `scptxfr /s #{abcs[RAILS_ENV]["host"]} /d #{abcs[RAILS_ENV]["database"]} /I /f db\\#{RAILS_ENV}_structure.sql /q /A /r` 339 | `scptxfr /s #{abcs[RAILS_ENV]["host"]} /d #{abcs[RAILS_ENV]["database"]} /I /F db\ /q /A /r` 340 | when "firebird" 341 | set_firebird_env(abcs[RAILS_ENV]) 342 | db_string = firebird_db_string(abcs[RAILS_ENV]) 343 | sh "isql -a #{db_string} > #{RAILS_ROOT}/db/#{RAILS_ENV}_structure.sql" 344 | else 345 | raise "Task not supported by '#{abcs["test"]["adapter"]}'" 346 | end 347 | 348 | if ActiveRecord::Base.connection.supports_migrations? 349 | File.open("#{RAILS_ROOT}/db/#{RAILS_ENV}_structure.sql", "a") { |f| f << ActiveRecord::Base.connection.dump_schema_information } 350 | end 351 | end 352 | end 353 | 354 | namespace :test do 355 | desc "Recreate the test database from the current schema.rb" 356 | task :load => 'db:test:purge' do 357 | ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations['test']) 358 | ActiveRecord::Schema.verbose = false 359 | Rake::Task["db:schema:load"].invoke 360 | end 361 | 362 | desc "Recreate the test database from the current environment's database schema" 363 | task :clone => %w(db:schema:dump db:test:load) 364 | 365 | desc "Recreate the test databases from the development structure" 366 | task :clone_structure => [ "db:structure:dump", "db:test:purge" ] do 367 | abcs = ActiveRecord::Base.configurations 368 | case abcs["test"]["adapter"] 369 | when "mysql" 370 | ActiveRecord::Base.establish_connection(:test) 371 | ActiveRecord::Base.connection.execute('SET foreign_key_checks = 0') 372 | IO.readlines("#{RAILS_ROOT}/db/#{RAILS_ENV}_structure.sql").join.split("\n\n").each do |table| 373 | ActiveRecord::Base.connection.execute(table) 374 | end 375 | when "postgresql" 376 | ENV['PGHOST'] = abcs["test"]["host"] if abcs["test"]["host"] 377 | ENV['PGPORT'] = abcs["test"]["port"].to_s if abcs["test"]["port"] 378 | ENV['PGPASSWORD'] = abcs["test"]["password"].to_s if abcs["test"]["password"] 379 | `psql -U "#{abcs["test"]["username"]}" -f #{RAILS_ROOT}/db/#{RAILS_ENV}_structure.sql #{abcs["test"]["database"]}` 380 | when "sqlite", "sqlite3" 381 | dbfile = abcs["test"]["database"] || abcs["test"]["dbfile"] 382 | `#{abcs["test"]["adapter"]} #{dbfile} < #{RAILS_ROOT}/db/#{RAILS_ENV}_structure.sql` 383 | when "sqlserver" 384 | `osql -E -S #{abcs["test"]["host"]} -d #{abcs["test"]["database"]} -i db\\#{RAILS_ENV}_structure.sql` 385 | when "oci", "oracle" 386 | ActiveRecord::Base.establish_connection(:test) 387 | IO.readlines("#{RAILS_ROOT}/db/#{RAILS_ENV}_structure.sql").join.split(";\n\n").each do |ddl| 388 | ActiveRecord::Base.connection.execute(ddl) 389 | end 390 | when "firebird" 391 | set_firebird_env(abcs["test"]) 392 | db_string = firebird_db_string(abcs["test"]) 393 | sh "isql -i #{RAILS_ROOT}/db/#{RAILS_ENV}_structure.sql #{db_string}" 394 | else 395 | raise "Task not supported by '#{abcs["test"]["adapter"]}'" 396 | end 397 | end 398 | 399 | desc "Empty the test database" 400 | task :purge => :environment do 401 | abcs = ActiveRecord::Base.configurations 402 | case abcs["test"]["adapter"] 403 | when "mysql" 404 | ActiveRecord::Base.establish_connection(:test) 405 | ActiveRecord::Base.connection.recreate_database(abcs["test"]["database"], abcs["test"]) 406 | when "postgresql" 407 | ActiveRecord::Base.clear_active_connections! 408 | drop_database(abcs['test']) 409 | create_database(abcs['test']) 410 | when "sqlite","sqlite3" 411 | dbfile = abcs["test"]["database"] || abcs["test"]["dbfile"] 412 | File.delete(dbfile) if File.exist?(dbfile) 413 | when "sqlserver" 414 | dropfkscript = "#{abcs["test"]["host"]}.#{abcs["test"]["database"]}.DP1".gsub(/\\/,'-') 415 | `osql -E -S #{abcs["test"]["host"]} -d #{abcs["test"]["database"]} -i db\\#{dropfkscript}` 416 | `osql -E -S #{abcs["test"]["host"]} -d #{abcs["test"]["database"]} -i db\\#{RAILS_ENV}_structure.sql` 417 | when "oci", "oracle" 418 | ActiveRecord::Base.establish_connection(:test) 419 | ActiveRecord::Base.connection.structure_drop.split(";\n\n").each do |ddl| 420 | ActiveRecord::Base.connection.execute(ddl) 421 | end 422 | when "firebird" 423 | ActiveRecord::Base.establish_connection(:test) 424 | ActiveRecord::Base.connection.recreate_database! 425 | else 426 | raise "Task not supported by '#{abcs["test"]["adapter"]}'" 427 | end 428 | end 429 | 430 | desc 'Check for pending migrations and load the test schema' 431 | task :prepare => 'db:abort_if_pending_migrations' do 432 | if defined?(ActiveRecord) && !ActiveRecord::Base.configurations.blank? 433 | Rake::Task[{ :sql => "db:test:clone_structure", :ruby => "db:test:load" }[ActiveRecord::Base.schema_format]].invoke 434 | end 435 | end 436 | end 437 | 438 | namespace :sessions do 439 | desc "Creates a sessions migration for use with ActiveRecord::SessionStore" 440 | task :create => :environment do 441 | raise "Task unavailable to this database (no migration support)" unless ActiveRecord::Base.connection.supports_migrations? 442 | require 'rails_generator' 443 | require 'rails_generator/scripts/generate' 444 | Rails::Generator::Scripts::Generate.new.run(["session_migration", ENV["MIGRATION"] || "CreateSessions"]) 445 | end 446 | 447 | desc "Clear the sessions table" 448 | task :clear => :environment do 449 | ActiveRecord::Base.connection.execute "DELETE FROM #{session_table_name}" 450 | end 451 | end 452 | end 453 | 454 | def drop_database(config) 455 | case config['adapter'] 456 | when 'mysql' 457 | ActiveRecord::Base.establish_connection(config) 458 | ActiveRecord::Base.connection.drop_database config['database'] 459 | when /^sqlite/ 460 | FileUtils.rm(File.join(RAILS_ROOT, config['database'])) 461 | when 'postgresql' 462 | ActiveRecord::Base.establish_connection(config.merge('database' => 'postgres', 'schema_search_path' => 'public')) 463 | ActiveRecord::Base.connection.drop_database config['database'] 464 | end 465 | end 466 | 467 | def session_table_name 468 | ActiveRecord::Base.pluralize_table_names ? :sessions : :session 469 | end 470 | 471 | def set_firebird_env(config) 472 | ENV["ISC_USER"] = config["username"].to_s if config["username"] 473 | ENV["ISC_PASSWORD"] = config["password"].to_s if config["password"] 474 | end 475 | 476 | def firebird_db_string(config) 477 | FireRuby::Database.db_string_for(config.symbolize_keys) 478 | end 479 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.1.0 2 | -------------------------------------------------------------------------------- /app/controllers/system_reports_controller.rb: -------------------------------------------------------------------------------- 1 | begin 2 | require_dependency 'application' 3 | rescue LoadError 4 | require_dependency 'application_controller' # Rails 2.3 5 | end 6 | 7 | class SystemReportsController < ApplicationController 8 | unloadable 9 | before_filter :check_permissions 10 | 11 | cattr_accessor :reports 12 | cattr_accessor :admin_required 13 | 14 | self.admin_required = [] 15 | 16 | def index 17 | end 18 | 19 | def self.add_report(name, report_module, options={}) 20 | self.send(:include, report_module) if report_module 21 | self.reports ||= [] 22 | report = { 23 | :name => name, 24 | :menu_name => options[:menu_name] || name, 25 | :action => options[:action] || name, 26 | :label => options[:label] || :reports_unnamed, 27 | :class => options[:class] 28 | } 29 | 30 | self.reports << report 31 | self.reports.uniq! 32 | self 33 | end 34 | 35 | def self.require_admin(action) 36 | self.admin_required ||= [] 37 | self.admin_required << action.to_sym 38 | self.admin_required.uniq! 39 | end 40 | 41 | private 42 | 43 | def check_permissions 44 | if SystemReportsController.admin_required.include?(params[:action].to_sym) 45 | return require_admin 46 | end 47 | 48 | if params[:controller] == 'system_reports' && params[:action] == 'index' 49 | return require_login 50 | else 51 | return authorize_global 52 | end 53 | end 54 | end 55 | 56 | SystemReportsController.add_report(:index, nil, :label => :reports_overview, :class => 'icon-overview') 57 | -------------------------------------------------------------------------------- /app/helpers/system_reports_helper.rb: -------------------------------------------------------------------------------- 1 | module SystemReportsHelper 2 | def report_menu 3 | returning [] do |menu| 4 | SystemReportsController.reports.each do |report| 5 | menu << link_to(l(report[:label]), 6 | { 7 | :controller => 'system_reports', 8 | :action => report[:action] 9 | }, 10 | :class => "icon #{report[:class]}" 11 | ) 12 | end 13 | 14 | menu << link_to(l(:reports_all_issues), { :controller => 'issues' }, :class => 'icon icon-issue') 15 | menu << link_to(l(:reports_system_activity), { :controller => 'activities', :action => 'index' }, :class => 'icon icon-activity') 16 | 17 | menu << link_to(l(:reports_spent_time_details), {:controller => 'time_entries', :action => 'index'}, :class => 'icon icon-time') 18 | menu << link_to(l(:reports_spent_time_reports), {:controller => 'time_entry_reports', :action => 'report'}, :class => 'icon icon-time') 19 | 20 | 21 | menu << link_to(l(:label_calendar), {:controller => 'calendars', :action => 'show'}, :class => 'icon icon-calendar') 22 | menu << link_to(l(:label_gantt), {:controller => 'gantts', :action => 'show'}, :class => 'icon icon-gantt') 23 | 24 | if Redmine::Plugin.registered_plugins.keys.include? :redmine_graphs 25 | menu << link_to(l(:label_graphs_old_issues), {:controller => 'graphs', :action => 'old_issues'}, :class => 'icon icon-redmine-graphs') 26 | menu << link_to(l(:label_graphs_issue_growth), {:controller => 'graphs', :action => 'issue_growth'}, :class => 'icon icon-redmine-graphs') 27 | end 28 | 29 | 30 | end 31 | end 32 | 33 | def select_size 34 | select_size = Setting.plugin_redmine_reports['select_size'] || 5 35 | select_size.to_i 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /app/models/activity_report.rb: -------------------------------------------------------------------------------- 1 | class ActivityReport < EphemeralModel 2 | include Reports::ReportHelper 3 | 4 | attr_accessor :fetcher 5 | attr_accessor :events 6 | attr_accessor :events_by_user 7 | 8 | def fetch 9 | @fetcher = Redmine::Activity::Fetcher.new(User.current, {:with_subprojects => Setting.display_subprojects_issues?}) 10 | @fetcher.scope = :all 11 | # Need to add one day to the ending date because Fetcher doesn't 12 | # include the ending date. 13 | patched_end_date = end_date.to_date + 1 14 | @events = @fetcher.events(start_date, patched_end_date.to_s).inject([]) do |matching_events, event| 15 | if selected_users_or_all_users.include?(event.event_author) 16 | matching_events << event 17 | end 18 | matching_events 19 | end 20 | @events 21 | end 22 | 23 | def group_events_by_user 24 | @events_by_user ||= @events.group_by(&:event_author).sort 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /app/models/completion_count.rb: -------------------------------------------------------------------------------- 1 | class CompletionCount < EphemeralModel 2 | include Reports::ReportHelper 3 | 4 | def total_incoming 5 | Issue.visible.count(:conditions => 6 | ["#{Issue.table_name}.created_on >= (?) and #{Issue.table_name}.created_on <= (?) and #{Issue.table_name}.status_id IN (?)", 7 | start_date, 8 | end_date, 9 | included_status_ids]) 10 | end 11 | 12 | def total_completed 13 | Issue.visible.count(:conditions => 14 | ["#{Issue.table_name}.updated_on >= (?) and #{Issue.table_name}.updated_on <= (?) and #{Issue.table_name}.status_id IN (?)", 15 | start_date, 16 | end_date, 17 | included_status_ids(:is_closed => true) 18 | ]) 19 | 20 | end 21 | 22 | def total_excluded 23 | if Setting.plugin_redmine_reports['completion_count'].present? && Setting.plugin_redmine_reports['completion_count']['exclude_statuses'].present? 24 | Issue.visible.count(:conditions => 25 | ["#{Issue.table_name}.created_on >= (?) and #{Issue.table_name}.created_on <= (?) and #{Issue.table_name}.status_id IN (?)", 26 | start_date, 27 | end_date, 28 | Setting.plugin_redmine_reports['completion_count']['exclude_statuses'].collect(&:to_i)]) 29 | else 30 | return 0 31 | end 32 | end 33 | 34 | def total_by_tracker_for_user(tracker, user_id) 35 | Issue.visible.count(:conditions => 36 | ["#{Issue.table_name}.updated_on >= (?) and #{Issue.table_name}.updated_on <= (?) and #{Issue.table_name}.tracker_id = (?) and #{Issue.table_name}.assigned_to_id = (?) and #{Issue.table_name}.status_id IN (?)", 37 | start_date, 38 | end_date, 39 | tracker.id, 40 | user_id, 41 | included_status_ids(:is_closed => true) 42 | ]) 43 | end 44 | 45 | def total_closed_for_user(user_id) 46 | Issue.visible.count(:conditions => 47 | ["#{Issue.table_name}.updated_on >= (?) and #{Issue.table_name}.updated_on <= (?) and #{Issue.table_name}.status_id IN (?) and #{Issue.table_name}.assigned_to_id = (?)", 48 | start_date, 49 | end_date, 50 | included_status_ids(:is_closed => true), 51 | user_id 52 | ]) 53 | end 54 | 55 | private 56 | def included_status_ids(conditions={}) 57 | all_statuses = IssueStatus.all(:conditions => conditions).collect(&:id) 58 | 59 | if Setting.plugin_redmine_reports['completion_count'].present? && Setting.plugin_redmine_reports['completion_count']['exclude_statuses'].present? 60 | return all_statuses - Setting.plugin_redmine_reports['completion_count']['exclude_statuses'].collect(&:to_i) 61 | else 62 | return all_statuses 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /app/views/settings/_redmine_reports_settings.html.erb: -------------------------------------------------------------------------------- 1 |

2 | <%= content_tag(:label, l(:reports_label_select_size)) %> 3 | <%= text_field_tag 'settings[select_size]', @settings['select_size'] %> 4 |

5 | 6 |
7 | <%= l(:reports_completion_count) %> 8 | 9 |

10 | <%= label_tag(l(:reports_text_exclude_statuses)) %> 11 | 12 | <% selected_statuses = @settings['completion_count']['exclude_statuses'].collect(&:to_i) unless @settings['completion_count'].blank? %> 13 | <%= select_tag('settings[completion_count][exclude_statuses]', 14 | options_from_collection_for_select(IssueStatus.all, :id, :name, selected_statuses), 15 | :multiple => true, 16 | :size => 5) %> 17 |

18 |
19 | -------------------------------------------------------------------------------- /app/views/system_reports/_billing_export.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | <% items.each do |item| %> 7 | <% next if item[1].to_f == 0.0 %> 8 | 9 | 10 | 11 | 12 | <% end %> 13 |
<%= l(:label_total)%><%= number_to_currency total %>
<%= h item[0] %><%= number_to_currency(item[1]) %>
14 | 15 | 16 | -------------------------------------------------------------------------------- /app/views/system_reports/_completion_count_for_user.html.erb: -------------------------------------------------------------------------------- 1 | <% user = completion_count_for_user %> 2 | <% if completion_count.total_closed_for_user(user.id) > 0 %> 3 | 4 | 5 | 6 | 7 | 8 | <% trackers.each do |tracker| %> 9 | 10 | 11 | 12 | 13 | <% end %> 14 | 15 | 16 | 17 | 18 | 19 |
<%= h user.name %>
<%= h tracker.name %><%= completion_count.total_by_tracker_for_user(tracker, user.id) %>
<%= l(:reports_completion_count_closed) %><%= completion_count.total_closed_for_user(user.id) %>
20 | <% else %> 21 |

22 | <%= h user.name -%> (<%= l(:label_none) %>) 23 |

24 | <% end %> 25 |
26 | -------------------------------------------------------------------------------- /app/views/system_reports/_menu.html.erb: -------------------------------------------------------------------------------- 1 |
2 | <%= report_menu.join(" ") %> 3 |
4 | -------------------------------------------------------------------------------- /app/views/system_reports/activity_report.html.erb: -------------------------------------------------------------------------------- 1 | <%= render :partial => 'menu' %> 2 | 3 |

<%= l(:reports_activity_report) %>

4 | <% labelled_tabular_form_for :activity_report, @activity_report, {} do |f| %> 5 | <%= error_messages_for 'activity_report' %> 6 | 7 |

8 | <%= f.text_field :start_date, :size => 10 %><%= calendar_for('activity_report_start_date') %> 9 | <%= f.text_field :end_date, :size => 10 %><%= calendar_for('activity_report_end_date') %> 10 |

11 |

<%= f.select :user_ids, @activity_report.default_users.collect {|u| [u.name, u.id]}, {:prompt => 'All techs', :selected => @activity_report.selected_user_ids}, {:multiple => true, :size => select_size } %>

12 |

<%= f.select :role_ids, Role.all.collect {|r| [r.name,r.id]}, {:selected => @activity_report.role_ids.collect(&:to_i)}, {:multiple => true, :size => select_size } %>

13 |

<%= submit_tag l(:button_apply) %>

14 | <% end %> 15 | 16 |
17 | 18 | <% if request.post? && @activity_report.valid? %> 19 |
20 | 21 | 22 | 23 | 26 | <% (@activity_report.start_date.to_date..@activity_report.end_date.to_date).each do |day| %> 27 | 30 | <% end %> 31 | 32 | 33 | 34 | <% @events_by_user.each do |user, activities| %> 35 | 36 | 40 | <% activities_grouped_by_day = activities.group_by(&:event_date) %> 41 | <% (@activity_report.start_date.to_date..@activity_report.end_date.to_date).each do |column_day| %> 42 | 43 | <% if activities_grouped_by_day[column_day].nil? %> 44 | 45 | <% else %> 46 | 56 | <% end %> 57 | 58 | <% end %> 59 | 60 | <% end %> 61 | 62 |
24 | <%= l(:field_user) %> 25 | 28 | <%= format_activity_day(day) %> 29 |
37 | <%= avatar(user, :size => "64") %> 38 | <%= h user %> 39 | 47 | <% activities_grouped_by_day[column_day].sort {|x,y| x.event_datetime <=> y.event_datetime }.each do |e| -%> 48 |
49 | <%= format_time(e.event_datetime, false) %> 50 | <%= content_tag('span', h(e.project), :class => 'project') %> 51 | <%= link_to format_activity_title(e.event_title), e.event_url %> 52 |
<%= format_activity_description(e.event_description) %>
53 |
54 | <% end -%> 55 |
63 |
64 | <% end %> 65 | <% content_for :header_tags do %> 66 | <%= stylesheet_link_tag "redmine_reports.css", :plugin => "redmine_reports", :media => 'all' %> 67 | <% end %> 68 | -------------------------------------------------------------------------------- /app/views/system_reports/completion_count.html.erb: -------------------------------------------------------------------------------- 1 | <%= render :partial => 'menu' %> 2 | 3 |

<%= l(:reports_completion_count) %>

4 | <% labelled_tabular_form_for :completion_count, @completion_count, {} do |f| %> 5 | <%= error_messages_for 'completion_count' %> 6 | 7 |

8 | <%= f.text_field :start_date, :size => 10 %><%= calendar_for('completion_count_start_date') %> 9 | <%= f.text_field :end_date, :size => 10 %><%= calendar_for('completion_count_end_date') %> 10 |

11 |

<%= f.select :user_ids, @completion_count.default_users.collect {|u| [u.name, u.id]}, {:prompt => 'All techs', :selected => @completion_count.selected_user_ids}, {:multiple => true, :size => select_size } %>

12 |

<%= f.select :role_ids, Role.all.collect {|r| [r.name,r.id]}, {:selected => @completion_count.role_ids.collect(&:to_i)}, {:multiple => true, :size => select_size } %>

13 | 14 |

<%= submit_tag l(:button_apply) %>

15 | <% end %> 16 | 17 |
18 | 19 | <% if request.post? && @completion_count.valid? %> 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 |
<%= l(:reports_completion_count_all_users)%>
<%= l(:reports_completion_count_incoming) %><%= @completion_count.total_incoming %>
<%= l(:reports_completion_count_completed) %><%= @completion_count.total_completed %>
<%= l(:reports_completion_count_difference) %><%= @completion_count.total_incoming - @completion_count.total_completed %>
<%= l(:reports_completion_count_excluded) %><%= @completion_count.total_excluded %>
44 |
45 | 46 | <%= render :partial => 'completion_count_for_user', :collection => @completion_count.selected_users_or_all_users, :locals => {:trackers => Tracker.all, :completion_count => @completion_count } %> 47 | <% end %> 48 | 49 | <% content_for :header_tags do %> 50 | <%= stylesheet_link_tag "redmine_reports.css", :plugin => "redmine_reports", :media => 'all' %> 51 | <% end %> 52 | -------------------------------------------------------------------------------- /app/views/system_reports/index.html.erb: -------------------------------------------------------------------------------- 1 | <%= render :partial => 'menu' %> 2 | 3 | <%= content_tag(:h1, l(:reports_title)) %> 4 | 5 |
6 |
    7 | <% report_menu.each do |report_link| %> 8 | <%= content_tag(:li, report_link) %> 9 | <% end %> 10 |
11 |
12 | 13 | <% content_for :header_tags do %> 14 | <%= stylesheet_link_tag "redmine_reports.css", :plugin => "redmine_reports", :media => 'all' %> 15 | <% end %> 16 | -------------------------------------------------------------------------------- /app/views/system_reports/quickbooks.html.erb: -------------------------------------------------------------------------------- 1 | <%= render :partial => 'menu' %> 2 | 3 |

<%= l(:reports_text_total_po) %>

4 | 5 | <%= render(:partial => 'billing_export', 6 | :locals => {:total => @total_po_total, :items => @total_po, :label => 'total_po'}) %> 7 | 8 |

<%= l(:reports_text_unspent_labor) %>

9 | 10 | <%= render(:partial => 'billing_export', 11 | :locals => {:total => @unspent_labor_total, :items => @unspent_labor, :label => 'unspent_labor'}) %> 12 | 13 |

<%= l(:reports_text_unbilled_labor) %>

14 | 15 | <%= render(:partial => 'billing_export', 16 | :locals => {:total => @unbilled_labor_total, :items => @unbilled_labor, :label => 'unbilled_labor'}) %> 17 | 18 | 19 | <% content_for(:header_tags) do %> 20 | 28 | <% end %> 29 | -------------------------------------------------------------------------------- /assets/images/chart_line.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edavis10/redmine_reports/5fa452492dda2f4f5acaa5e148505e97a3295d90/assets/images/chart_line.png -------------------------------------------------------------------------------- /assets/images/chart_organisation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edavis10/redmine_reports/5fa452492dda2f4f5acaa5e148505e97a3295d90/assets/images/chart_organisation.png -------------------------------------------------------------------------------- /assets/images/house.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edavis10/redmine_reports/5fa452492dda2f4f5acaa5e148505e97a3295d90/assets/images/house.png -------------------------------------------------------------------------------- /assets/images/user_comment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edavis10/redmine_reports/5fa452492dda2f4f5acaa5e148505e97a3295d90/assets/images/user_comment.png -------------------------------------------------------------------------------- /assets/stylesheets/redmine_reports.css: -------------------------------------------------------------------------------- 1 | .contextual {text-align:right; width: 80%; white-space: normal;} 2 | .icon {margin-left: 5px;} 3 | .icon-overview { background-image: url(../images/house.png); } 4 | .icon-issue { background-image: url(../../../images/ticket.png); } 5 | .icon-issue-complete { background-image: url(../../../images/ticket_checked.png); } 6 | .icon-calendar { background-image: url(../../../images/calendar.png); } 7 | .icon-gantt { background-image: url(../images/chart_organisation.png); } 8 | .icon-redmine-graphs { background-image: url(../images/chart_line.png); } 9 | .icon-activity { background-image: url(../images/user_comment.png); } 10 | 11 | #reports-list ul { list-style: none;} 12 | #reports-list ul { padding: 2px 0; } 13 | 14 | /* Form */ 15 | form.tabular p {float:left; clear: none; padding-left: 0;} 16 | form.tabular label {float: none; margin-left: 5px; text-align: left; display: block;} 17 | form.tabular p.submit {clear: both; } 18 | 19 | /* Completion Count */ 20 | tr.total-closed {font-weight: bold;} 21 | 22 | /* Activity Report */ 23 | #content {overflow-x:scroll;} 24 | #activity { margin: 10px 0;} 25 | #activity table td {min-width: 200px; } 26 | #activity table td.user-column {min-width: 100px; text-align: center; } 27 | #activity table td.user-column gravatar {display: block; } 28 | 29 | div#activity table td div.details { background-repeat: no-repeat; padding-left: 20px; background-position: 0 0%; margin: 10px 0; border-top: 1px dotted #000000;} 30 | div#activity gravatar {float:left;} 31 | div#activity .time { color: #777; font-size: 80%; } 32 | div#activity .description { padding-top: 15px; } 33 | 34 | div#activity div.issue { background-image: url(../../../images/ticket.png); } 35 | div#activity div.issue-edit { background-image: url(../../../images/ticket_edit.png); } 36 | div#activity div.issue-closed { background-image: url(../../../images/ticket_checked.png); } 37 | div#activity div.issue-note { background-image: url(../../../images/ticket_note.png); } 38 | div#activity div.changeset { background-image: url(../../../images/changeset.png); } 39 | div#activity div.news { background-image: url(../../../images/news.png); } 40 | div#activity div.message { background-image: url(../../../images/message.png); } 41 | div#activity div.reply { background-image: url(../../../images/comments.png); } 42 | div#activity div.wiki-page { background-image: url(../../../images/wiki_edit.png); } 43 | div#activity div.attachment { background-image: url(../../../images/attachment.png); } 44 | div#activity div.document { background-image: url(../../../images/document.png); } 45 | div#activity div.project { background-image: url(../../../images/projects.png); } 46 | 47 | /* Clear out the Issue Details css */ 48 | div.issue {background: none; border: none; margin-bottom: 0; padding: 0px} -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | reports_title: Reports 3 | reports_menu: Reports 4 | reports_overview: Overview 5 | reports_quickbooks: Quickbooks 6 | reports_text_unbilled_po: Unbilled PO 7 | reports_text_unspent_labor: Unspent Labor 8 | reports_text_unbilled_labor: Unbilled Labor 9 | reports_text_total_po: Total PO 10 | reports_completion_count: Completion Report 11 | reports_completion_count_incoming: Incoming 12 | reports_completion_count_completed: Completed 13 | reports_completion_count_difference: Difference 14 | reports_completion_count_all_users: All Users 15 | reports_completion_count_closed: Total Closed Issues 16 | field_end_date: End date 17 | field_user_ids: Users 18 | field_role_ids: Roles 19 | reports_activity_report: User Activity Report 20 | reports_system_activity: System Activity List 21 | reports_spent_time_details: Spent Time Details 22 | reports_spent_time_reports: Spent Time Summary Reports 23 | reports_all_issues: All Issues 24 | reports_label_select_size: Size of select fields 25 | reports_completion_count_excluded: Excluded 26 | reports_text_exclude_statuses: Excluded Statuses 27 | -------------------------------------------------------------------------------- /features/activity_report.feature: -------------------------------------------------------------------------------- 1 | Feature: Activity Report 2 | As a User 3 | I want to see reports on the activities of users 4 | So I can see what people are doing 5 | 6 | Scenario: See link to the Activity report 7 | Given I am logged in as a user with permission to "run activity report" 8 | And I am on the system report overview page 9 | 10 | Then I should see "Reports" 11 | And I should see a link "Activity Report" 12 | 13 | Scenario: Open link to the Activity report 14 | Given I am logged in as a user with permission to "run activity report" 15 | And I am on the system report overview page 16 | 17 | When I follow "Activity Report" 18 | 19 | Then I am on the "activity report" page 20 | 21 | Scenario: Activity Report Report as normal user 22 | Given I am logged in as a User 23 | When I visit the "activity report" page 24 | Then I should be denied access 25 | 26 | Scenario: Completion Count Report as an anonymous user 27 | Given I am not logged in 28 | When I visit the "activity report" page 29 | Then I should go to the login page 30 | 31 | Scenario: Report form 32 | Given I am logged in as a user with permission to "run activity report" 33 | When I visit the "activity report" page 34 | 35 | Then I should see "Activity Report" 36 | And I should see "Start" 37 | And I should see "End date" 38 | 39 | Scenario: Run report 40 | Given I am logged in as a user with permission to "run activity report" 41 | When I visit the "activity report" page 42 | And I select some valid values for the "activity" report 43 | And I press "Apply" 44 | 45 | Then I see the activity report for each user 46 | 47 | -------------------------------------------------------------------------------- /features/step_definitions/system_report_steps.rb: -------------------------------------------------------------------------------- 1 | # Steps for Reports 2 | Before do 3 | User.destroy_all 4 | @current_user = User.new(:mail => 'test@example.com', :firstname => 'Feature', :lastname => 'Test') 5 | @current_user.login = 'feature_test' 6 | @current_user.save! 7 | end 8 | 9 | def total_po_data 10 | [ 11 | ["Project A", 100.0], 12 | ["Project B", 350.43], 13 | ["Project C", 23.45], 14 | ["Project D", 0] 15 | ] 16 | end 17 | 18 | def setup_total_po_data 19 | @total_pos = total_po_data 20 | @total_po_total = 473.88 21 | total_po_data.each do |project_po| 22 | project = Project.new(:name => project_po[0], :identifier => project_po[0], :total_value => project_po[1]) 23 | project.save(false) 24 | end 25 | end 26 | 27 | def unspent_labor_data 28 | [ 29 | ["Project A", 1000.0], 30 | ["Project B", 1234.56], 31 | ["Project C", 123.45], 32 | ["Project D", 0] 33 | ] 34 | end 35 | 36 | def setup_unspent_labor_data 37 | @unspent_labor = unspent_labor_data 38 | @unspent_labor_total = 2358.01 39 | end 40 | 41 | def unbilled_labor_data 42 | [ 43 | ["User A", 2000.0], 44 | ["User B", 2234.56], 45 | ["User C", 223.45], 46 | ["User D", 0] 47 | ] 48 | end 49 | 50 | def setup_unbilled_labor_data 51 | @unbilled_labor = unbilled_labor_data 52 | @unbilled_labor_total = 4458.01 53 | end 54 | 55 | 56 | Given /^I am logged in as an Administrator$/ do 57 | @current_user.stubs(:admin?).returns(true) 58 | User.stubs(:current).returns(@current_user) 59 | end 60 | 61 | Given /^I am logged in as a User$/ do 62 | @current_user.stubs(:admin?).returns(false) 63 | @current_user.stubs(:allowed_to?).returns(false) 64 | User.stubs(:current).returns(@current_user) 65 | end 66 | 67 | Given /^I am logged in as a user with permission to "(.*)"$/ do |permission| 68 | Given "I am logged in as a User" 69 | permission_name = permission.gsub(' ','_').downcase.to_sym 70 | @current_user.stubs(:allowed_to?).with(permission_name_to_path(permission_name), nil, {:global => true}).returns(true) 71 | end 72 | 73 | Given /^I am not logged in$/ do 74 | User.stubs(:current).returns(User.anonymous) 75 | end 76 | 77 | Given /^I am on the (.*)$/ do |page_name| 78 | visit path_to(page_name) 79 | assert_response :success 80 | end 81 | 82 | Given /^billing data is in the system$/ do 83 | setup_total_po_data 84 | setup_unspent_labor_data 85 | setup_unbilled_labor_data 86 | end 87 | 88 | When /^I visit the "(.*)" page$/ do |page_name| 89 | visit path_to(page_name) 90 | end 91 | 92 | When /^I select some valid values for the report$/ do 93 | When 'I fill in "Start" with "2009-01-01"' 94 | When 'I fill in "End date" with "2009-01-06"' 95 | end 96 | 97 | When /^I select some valid values for the "activity" report$/ do 98 | When 'I fill in "Start" with "2009-01-01"' 99 | When 'I fill in "End date" with "2009-01-06"' 100 | end 101 | 102 | Then /^I should see a menu called "(.*)"$/ do |named| 103 | response.should have_tag("div.contextual##{named}") 104 | end 105 | 106 | Then /^I should see a link "(.*)"$/ do |text| 107 | response.should have_tag("a", /#{text}/i) 108 | end 109 | 110 | Then /^I should be on the "quickbooks" page$/ do 111 | current_url.should =~ %r{/quickbooks$} 112 | end 113 | 114 | Then /^I should be denied access$/ do 115 | response.should_not be_success 116 | response.code.should eql("403") 117 | response.should render_template('common/403') 118 | end 119 | 120 | Then /^I should go to the login page$/ do 121 | response.request.path_parameters["action"].should eql("login") 122 | end 123 | 124 | Then /^I should see the "Total PO" total$/ do 125 | response.should have_tag("tr#total_po_total", /#{@total_po_total}/) 126 | end 127 | 128 | Then /^I should see the "Total PO" subtotals$/ do 129 | response.should have_tag("table#total_po") do 130 | @total_pos.each do |po| 131 | with_tag("td.total_po_amount",/#{po[1]}/) 132 | end 133 | end 134 | end 135 | 136 | Then /^I should see the "Unspent Labor" total$/ do 137 | # TODO: find actual amounts 138 | response.should have_tag("tr#unspent_labor_total") 139 | end 140 | 141 | # TODO: Needs actual amounts in order to test 142 | # Then /^I should see the "Unspent Labor" subtotals$/ do 143 | # response.should have_tag("table#unspent_labor") do 144 | # @unspent_labor.each do |labor| 145 | # with_tag("td.unspent_labor_amount") 146 | # end 147 | # end 148 | # end 149 | 150 | 151 | Then /^I should see the "Unbilled Labor" total$/ do 152 | # TODO: find actual amounts 153 | response.should have_tag("tr#unbilled_labor_total") 154 | end 155 | 156 | # TODO: Needs actual amounts in order to test 157 | # Then /^I should see the "Unbilled Labor" subtotals$/ do 158 | # response.should have_tag("table#unbilled_labor") do 159 | # @unbilled_labor.each do |labor| 160 | # with_tag("td.unbilled_labor_amount") 161 | # end 162 | # end 163 | # end 164 | 165 | Then /^I see the totals$/ do 166 | response.should have_tag("table#totals") do 167 | with_tag('tr.incoming') 168 | with_tag('tr.completed') 169 | with_tag('tr.difference') 170 | end 171 | end 172 | 173 | Then /^I see a subreport for each user$/ do 174 | response.should have_tag("table.user-report", :count => User.active.count) 175 | end 176 | 177 | Then /^I see the activity report for each user$/ do 178 | response.should have_tag("table#activity-report-results") do 179 | with_tag("tr") 180 | end 181 | end 182 | -------------------------------------------------------------------------------- /features/step_definitions/webrat_steps.rb: -------------------------------------------------------------------------------- 1 | # Commonly used webrat steps 2 | # http://github.com/brynary/webrat 3 | 4 | When /^I press "(.*)"$/ do |button| 5 | click_button(button) 6 | end 7 | 8 | When /^I follow "(.*)"$/ do |link| 9 | click_link(link) 10 | end 11 | 12 | When /^I fill in "(.*)" with "(.*)"$/ do |field, value| 13 | fill_in(field, :with => value) 14 | end 15 | 16 | When /^I select "(.*)" from "(.*)"$/ do |value, field| 17 | select(value, :from => field) 18 | end 19 | 20 | # Use this step in conjunction with Rail's datetime_select helper. For example: 21 | # When I select "December 25, 2008 10:00" as the date and time 22 | When /^I select "(.*)" as the date and time$/ do |time| 23 | select_datetime(time) 24 | end 25 | 26 | # Use this step when using multiple datetime_select helpers on a page or 27 | # you want to specify which datetime to select. Given the following view: 28 | # <%= f.label :preferred %>
29 | # <%= f.datetime_select :preferred %> 30 | # <%= f.label :alternative %>
31 | # <%= f.datetime_select :alternative %> 32 | # The following steps would fill out the form: 33 | # When I select "November 23, 2004 11:20" as the "Preferred" data and time 34 | # And I select "November 25, 2004 10:30" as the "Alternative" data and time 35 | When /^I select "(.*)" as the "(.*)" date and time$/ do |datetime, datetime_label| 36 | select_datetime(datetime, :from => datetime_label) 37 | end 38 | 39 | # Use this step in conjuction with Rail's time_select helper. For example: 40 | # When I select "2:20PM" as the time 41 | # Note: Rail's default time helper provides 24-hour time-- not 12 hour time. Webrat 42 | # will convert the 2:20PM to 14:20 and then select it. 43 | When /^I select "(.*)" as the time$/ do |time| 44 | select_time(time) 45 | end 46 | 47 | # Use this step when using multiple time_select helpers on a page or you want to 48 | # specify the name of the time on the form. For example: 49 | # When I select "7:30AM" as the "Gym" time 50 | When /^I select "(.*)" as the "(.*)" time$/ do |time, time_label| 51 | select_time(time, :from => time_label) 52 | end 53 | 54 | # Use this step in conjuction with Rail's date_select helper. For example: 55 | # When I select "February 20, 1981" as the date 56 | When /^I select "(.*)" as the date$/ do |date| 57 | select_date(date) 58 | end 59 | 60 | # Use this step when using multiple date_select helpers on one page or 61 | # you want to specify the name of the date on the form. For example: 62 | # When I select "April 26, 1982" as the "Date of Birth" date 63 | When /^I select "(.*)" as the "(.*)" date$/ do |date, date_label| 64 | select_date(date, :from => date_label) 65 | end 66 | 67 | When /^I check "(.*)"$/ do |field| 68 | check(field) 69 | end 70 | 71 | When /^I uncheck "(.*)"$/ do |field| 72 | uncheck(field) 73 | end 74 | 75 | When /^I choose "(.*)"$/ do |field| 76 | choose(field) 77 | end 78 | 79 | When /^I attach the file at "(.*)" to "(.*)" $/ do |path, field| 80 | attach_file(field, path) 81 | end 82 | 83 | Then /^I should see "(.*)"$/ do |text| 84 | response.body.should =~ /#{text}/m 85 | end 86 | 87 | Then /^I should not see "(.*)"$/ do |text| 88 | response.body.should_not =~ /#{text}/m 89 | end 90 | 91 | Then /^the "(.*)" checkbox should be checked$/ do |label| 92 | field_labeled(label).should be_checked 93 | end 94 | -------------------------------------------------------------------------------- /features/support/env.rb: -------------------------------------------------------------------------------- 1 | # Sets up the Rails environment for Cucumber 2 | ENV["RAILS_ENV"] = "test" 3 | require File.expand_path(File.dirname(__FILE__) + '/../../../../../config/environment') 4 | require 'cucumber/rails/world' 5 | Cucumber::Rails.use_transactional_fixtures 6 | 7 | require 'webrat/rails' 8 | 9 | # Comment out the next two lines if you're not using RSpec's matchers (should / should_not) in your steps. 10 | require 'cucumber/rails/rspec' 11 | require 'webrat/rspec-rails' 12 | 13 | require 'ruby-debug' 14 | 15 | # require the entire app if we're running under coverage testing, 16 | # so we measure 0% covered files in the report 17 | # 18 | # http://www.pervasivecode.com/blog/2008/05/16/making-rcov-measure-your-whole-rails-app-even-if-tests-miss-entire-source-files/ 19 | if defined?(Rcov) 20 | all_app_files = Dir.glob('{app,lib}/**/*.rb') 21 | all_app_files.each{|rb| require rb} 22 | end 23 | -------------------------------------------------------------------------------- /features/support/paths.rb: -------------------------------------------------------------------------------- 1 | def path_to(page_name) 2 | case page_name 3 | 4 | when /homepage/i 5 | url_for(:controller => 'welcome') 6 | when /system report overview/i 7 | url_for(:controller => 'system_reports') 8 | when /quickbooks/i 9 | url_for(:controller => 'system_reports', :action => 'quickbooks') 10 | when /completion count/i 11 | url_for(:controller => 'system_reports', :action => 'completion_count') 12 | when /activity report/i 13 | url_for(:controller => 'system_reports', :action => 'activity_report') 14 | else 15 | raise "Can't find mapping from \"#{page_name}\" to a path." 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /features/support/permissions.rb: -------------------------------------------------------------------------------- 1 | def permission_name_to_path(permission) 2 | case permission 3 | when :run_completion_count 4 | {:controller => 'system_reports', :action => 'completion_count'} 5 | when :run_activity_report 6 | {:controller => 'system_reports', :action => 'activity_report'} 7 | else 8 | raise "Can't find mapping from \"#{permission}\" to a path." 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /features/system_report_completion_count.feature: -------------------------------------------------------------------------------- 1 | Feature: Completion Count Report 2 | As a User 3 | I want to see reports on the number of issues opened and closed 4 | So I can see trends on how the projects are doing 5 | 6 | Scenario: See link to the Completion Count report 7 | Given I am logged in as a user with permission to "run completion count" 8 | And I am on the system report overview page 9 | 10 | Then I should see "Reports" 11 | And I should see a link "Completion Report" 12 | 13 | Scenario: Open link to the Completion Count report 14 | Given I am logged in as a user with permission to "run completion count" 15 | And I am on the system report overview page 16 | 17 | When I follow "Completion Report" 18 | 19 | Then I am on the "completion count" page 20 | 21 | Scenario: Completion Count Report as normal user 22 | Given I am logged in as a User 23 | When I visit the "completion count" page 24 | Then I should be denied access 25 | 26 | Scenario: Completion Count Report as an anonymous user 27 | Given I am not logged in 28 | When I visit the "completion count" page 29 | Then I should go to the login page 30 | 31 | Scenario: Report form 32 | Given I am logged in as a user with permission to "run completion count" 33 | When I visit the "completion count" page 34 | 35 | Then I should see "Completion Report" 36 | And I should see "Start" 37 | And I should see "End date" 38 | And I should see "Users" 39 | 40 | Scenario: Run report 41 | Given I am logged in as a user with permission to "run completion count" 42 | When I visit the "completion count" page 43 | And I select some valid values for the report 44 | And I press "Apply" 45 | 46 | Then I see the totals 47 | And I see a subreport for each user 48 | -------------------------------------------------------------------------------- /features/system_report_menu_item.feature: -------------------------------------------------------------------------------- 1 | Feature: System Report Menu Item 2 | As an visitor 3 | I want to see a link to the Reports on the top menu 4 | So I access the reports easily 5 | 6 | Scenario: See the Reports link as a User 7 | Given I am logged in as a User 8 | And I am on the homepage 9 | 10 | Then I should see "Reports" 11 | 12 | Scenario: See the Reports link as an anonymous User 13 | Given I am not logged in 14 | And I am on the homepage 15 | 16 | Then I should not see "Reports" 17 | 18 | -------------------------------------------------------------------------------- /features/system_report_overview.feature: -------------------------------------------------------------------------------- 1 | Feature: Report Overview 2 | As an visitor 3 | I want to see a list of reports 4 | So I can check them easily 5 | 6 | Scenario: See the Reports link 7 | Given I am logged in as a User 8 | And I am on the homepage 9 | 10 | Then I should see "Reports" 11 | 12 | Scenario: See the Reports link 13 | Given I am not logged in 14 | And I am on the homepage 15 | 16 | Then I should not see "Reports" 17 | 18 | Scenario: View Report Overviews as anonymous user 19 | Given I am not logged in 20 | When I visit the "system report overview" page 21 | Then I should go to the login page 22 | 23 | 24 | Scenario: See a list of reports 25 | Given I am logged in as a User 26 | And I am on the system report overview page 27 | 28 | Then I should see "Reports" 29 | And I should see a menu called "reports-menu" 30 | -------------------------------------------------------------------------------- /features/system_report_quickbooks.feature: -------------------------------------------------------------------------------- 1 | Feature: Quickbooks Report 2 | As an Administrator 3 | I want to see reports on Quickbooks and other financial data 4 | So I can make financial decisions 5 | 6 | Scenario: See link to the Quickbooks report 7 | Given I am logged in as an Administrator 8 | And I am on the system report overview page 9 | 10 | Then I should see "Reports" 11 | And I should see a link "Quickbooks" 12 | 13 | Scenario: Open link to the Quickbooks report 14 | Given I am logged in as an Administrator 15 | And I am on the system report overview page 16 | 17 | When I follow "Quickbooks" 18 | 19 | Then I should be on the "quickbooks" page 20 | 21 | Scenario: See Total PO amounts 22 | Given I am logged in as an Administrator 23 | And billing data is in the system 24 | And I am on the system report quickbooks page 25 | 26 | Then I should see "Total PO" 27 | And I should see the "Total PO" total 28 | And I should see the "Total PO" subtotals 29 | 30 | Scenario: See Unspent Labor amounts 31 | Given I am logged in as an Administrator 32 | And billing data is in the system 33 | And I am on the system report quickbooks page 34 | 35 | Then I should see "Unspent Labor" 36 | And I should see the "Unspent Labor" total 37 | And I should see the "Unspent Labor" subtotals 38 | 39 | Scenario: See Unbilled Labor amounts 40 | Given I am logged in as an Administrator 41 | And billing data is in the system 42 | And I am on the system report quickbooks page 43 | 44 | Then I should see "Unbilled Labor" 45 | And I should see the "Unbilled Labor" total 46 | And I should see the "Unbilled Labor" subtotals 47 | 48 | Scenario: Run Quickbooks Report as normal user 49 | Given I am logged in as a User 50 | When I visit the "quickbooks" page 51 | Then I should be denied access 52 | 53 | Scenario: Run Quickbooks Report as an anonymous user 54 | Given I am not logged in 55 | When I visit the "quickbooks" page 56 | Then I should go to the login page 57 | 58 | -------------------------------------------------------------------------------- /init.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + "/rails/init" 2 | -------------------------------------------------------------------------------- /lang/en.yml: -------------------------------------------------------------------------------- 1 | reports_menu: Reports 2 | reports_overview: Overview 3 | reports_quickbooks: Quickbooks 4 | reports_text_total_po: Total PO 5 | reports_text_unbilled_po: Unbilled PO 6 | reports_text_unspent_labor: Unspent Labor 7 | reports_text_unbilled_labor: Unbilled Labor 8 | reports_text_total_po: Total PO 9 | -------------------------------------------------------------------------------- /lib/ephemeral_model.rb: -------------------------------------------------------------------------------- 1 | # From: http://www.pervasivecode.com/blog/2007/06/08/non-persistent-rails-model-classes-for-easier-validation/ 2 | # Code based on code found at http://www.railsweenie.com/forums/2/topics/724 3 | class EphemeralModel < ActiveRecord::Base 4 | def self.columns() @columns ||= []; end 5 | def self.column(name, sql_type = nil, default = nil, null = true) 6 | columns << ActiveRecord::ConnectionAdapters::Column.new(name.to_s, default, sql_type.to_s, null) 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/reports/activity_report.rb: -------------------------------------------------------------------------------- 1 | module Reports 2 | module ActivityReport 3 | def activity_report 4 | @activity_report = ::ActivityReport.new(params[:activity_report]) 5 | if request.post? && @activity_report.valid? 6 | @activity_report.fetch 7 | @events_by_user = @activity_report.group_events_by_user 8 | end 9 | end 10 | end 11 | end 12 | 13 | # Added the report and configure it's permissions 14 | require 'dispatcher' 15 | Dispatcher.to_prepare do 16 | SystemReportsController.add_report(:activity_report, Reports::ActivityReport, {:action => :activity_report, :label => :reports_activity_report, :class => 'icon-activity'}) 17 | 18 | # TODO: A better core API? 19 | Redmine::AccessControl.map {|map| 20 | map.permission(:run_activity_report, {:system_reports => [:activity_report]}) 21 | } if Redmine::AccessControl.permission(:run_activity_report).nil? 22 | 23 | end 24 | -------------------------------------------------------------------------------- /lib/reports/completion_count.rb: -------------------------------------------------------------------------------- 1 | module Reports 2 | module CompletionCount 3 | def completion_count 4 | @completion_count = ::CompletionCount.new(params[:completion_count]) 5 | if request.post? 6 | @completion_count.valid? 7 | end 8 | end 9 | end 10 | end 11 | 12 | # Added the report and configure it's permissions 13 | require 'dispatcher' 14 | Dispatcher.to_prepare do 15 | SystemReportsController.add_report(:completion_count, Reports::CompletionCount, {:action => :completion_count, :label => :reports_completion_count, :class => 'icon-issue-complete'}) 16 | 17 | # TODO: A better core API? 18 | Redmine::AccessControl.map {|map| 19 | map.permission(:run_completion_count, {:system_reports => [:completion_count]}) 20 | } if Redmine::AccessControl.permission(:run_completion_count).nil? 21 | 22 | end 23 | -------------------------------------------------------------------------------- /lib/reports/quickbooks.rb: -------------------------------------------------------------------------------- 1 | if Redmine::Plugin.registered_plugins.keys.include? :redmine_billing 2 | 3 | module Reports 4 | module Quickbooks 5 | def quickbooks 6 | @total_po = [] 7 | Project.find(:all).collect do |project| 8 | @total_po << [project.name, project.total_value] unless project.total_value.nil? 9 | end 10 | 11 | # Sort by name 12 | @total_po.sort! do |a,b| 13 | a[0] <=> b[0] 14 | end 15 | @total_po_total = @total_po.collect {|po_item| po_item[1].to_f}.sum 16 | 17 | @unspent_labor = BillingExport.unspent_labor 18 | @unspent_labor_total = @unspent_labor.collect {|labor| labor[1].to_f}.sum 19 | 20 | @unbilled_labor = BillingExport.unbilled_labor 21 | @unbilled_labor_total = @unbilled_labor.collect {|labor| labor[1].to_f}.sum 22 | end 23 | end 24 | end 25 | 26 | # Added the report and configure it's permissions 27 | require 'dispatcher' 28 | Dispatcher.to_prepare do 29 | SystemReportsController.add_report(:quickbooks, Reports::Quickbooks, {:action => :quickbooks, :label => :reports_quickbooks}) 30 | SystemReportsController.require_admin(:quickbooks) 31 | end 32 | else 33 | Rails.logger.info("*** redmine_reports: Redmine billing plugin not installed, Quickbooks report unavailable") 34 | end 35 | -------------------------------------------------------------------------------- /lib/reports/report_helper.rb: -------------------------------------------------------------------------------- 1 | module Reports 2 | module ReportHelper 3 | def self.included(base) # :nodoc: 4 | base.class_eval do 5 | column :start_date, :string 6 | column :end_date, :string 7 | 8 | attr_accessor :selected_role_ids 9 | 10 | has_many :users 11 | has_many :roles 12 | 13 | validates_presence_of :start_date 14 | validates_presence_of :end_date 15 | validate :start_date_is_before_end_date 16 | 17 | # Isn't working when defined in InstanceMethods 18 | def role_ids 19 | selected_role_ids || [] 20 | end 21 | 22 | 23 | # Adds users based on which roles a User has. 24 | def role_ids=(v) 25 | v.each do |id| 26 | role = Role.find_by_id(id.to_i) 27 | if role 28 | self.users += role.members.collect(&:user).uniq.compact 29 | @selected_role_ids ||= [] 30 | @selected_role_ids << role.id 31 | end 32 | end 33 | end 34 | end 35 | 36 | base.extend(ClassMethods) 37 | 38 | base.send(:include, InstanceMethods) 39 | end 40 | 41 | module ClassMethods 42 | end 43 | 44 | module InstanceMethods 45 | def start_date_is_before_end_date 46 | if self.end_date && self.start_date && self.end_date < self.start_date 47 | errors.add :end_date, :greater_than_start_date 48 | end 49 | end 50 | 51 | def default_users 52 | User.active.sort 53 | end 54 | 55 | def selected_users_or_all_users 56 | users.blank? ? User.active.sort : users.sort 57 | end 58 | 59 | def selected_user_ids 60 | users.collect(&:id).collect(&:to_i) if users 61 | end 62 | end 63 | end 64 | end 65 | 66 | -------------------------------------------------------------------------------- /rails/init.rb: -------------------------------------------------------------------------------- 1 | require 'redmine' 2 | 3 | Dir[directory + '/lib/reports/**/*.rb'].each do |report| 4 | require report 5 | end 6 | 7 | Redmine::Plugin.register :redmine_reports do 8 | name 'Redmine Reports plugin' 9 | author 'Eric Davis' 10 | url 'https://projects.littlestreamsoftware.com/projects/redmine-reports' 11 | author_url 'http://www.littlestreamsoftware.com' 12 | description 'This is a plugin for Redmine reports' 13 | version '0.1.0' 14 | 15 | requires_redmine :version_or_higher => '0.8.0' 16 | 17 | menu :top_menu, :reports, { :controller => 'system_reports', :action => 'index'}, :caption => :reports_menu, :if => Proc.new{User.current.logged?} 18 | 19 | settings(:default => { 20 | 'select_size' => '5', 21 | 'completion_count' => { 22 | 'exclude_statuses' => [] 23 | } 24 | }, 25 | :partial => 'settings/redmine_reports_settings') 26 | end 27 | -------------------------------------------------------------------------------- /redmine_reports.gemspec: -------------------------------------------------------------------------------- 1 | # Generated by jeweler 2 | # DO NOT EDIT THIS FILE 3 | # Instead, edit Jeweler::Tasks in Rakefile, and run `rake gemspec` 4 | # -*- encoding: utf-8 -*- 5 | 6 | Gem::Specification.new do |s| 7 | s.name = %q{redmine_reports} 8 | s.version = "0.1.0" 9 | 10 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= 11 | s.authors = ["Eric Davis"] 12 | s.date = %q{2009-10-14} 13 | s.description = %q{This is a plugin for Redmine reports} 14 | s.email = %q{edavis@littlestreamsoftware.com} 15 | s.extra_rdoc_files = [ 16 | "README.rdoc" 17 | ] 18 | s.files = [ 19 | "COPYRIGHT.txt", 20 | "CREDITS.txt", 21 | "GPL.txt", 22 | "README.rdoc", 23 | "Rakefile", 24 | "VERSION", 25 | "app/controllers/system_reports_controller.rb", 26 | "app/helpers/system_reports_helper.rb", 27 | "app/models/activity_report.rb", 28 | "app/models/completion_count.rb", 29 | "app/views/settings/_redmine_reports_settings.html.erb", 30 | "app/views/system_reports/_billing_export.html.erb", 31 | "app/views/system_reports/_completion_count_for_user.html.erb", 32 | "app/views/system_reports/_menu.html.erb", 33 | "app/views/system_reports/activity_report.html.erb", 34 | "app/views/system_reports/completion_count.html.erb", 35 | "app/views/system_reports/index.html.erb", 36 | "app/views/system_reports/quickbooks.html.erb", 37 | "assets/images/chart_line.png", 38 | "assets/images/chart_organisation.png", 39 | "assets/images/house.png", 40 | "assets/images/user_comment.png", 41 | "assets/stylesheets/redmine_reports.css", 42 | "config/locales/en.yml", 43 | "init.rb", 44 | "lang/en.yml", 45 | "lib/ephemeral_model.rb", 46 | "lib/reports/activity_report.rb", 47 | "lib/reports/completion_count.rb", 48 | "lib/reports/quickbooks.rb", 49 | "lib/reports/report_helper.rb", 50 | "lib/tasks/plugin_stat.rake", 51 | "rails/init.rb" 52 | ] 53 | s.homepage = %q{https://projects.littlestreamsoftware.com/projects/TODO} 54 | s.rdoc_options = ["--charset=UTF-8"] 55 | s.require_paths = ["lib"] 56 | s.rubyforge_project = %q{redmine_reports} 57 | s.rubygems_version = %q{1.3.5} 58 | s.summary = %q{This is a plugin for Redmine reports} 59 | s.test_files = [ 60 | "spec/spec_helper.rb", 61 | "spec/models/completion_count_spec.rb", 62 | "spec/models/activity_report_spec.rb", 63 | "spec/controllers/system_reports_completion_count_spec.rb", 64 | "spec/controllers/system_reports_overview_spec.rb", 65 | "spec/controllers/system_reports_activity_report_spec.rb", 66 | "spec/controllers/system_reports_quickbooks_spec.rb", 67 | "spec/sanity_spec.rb" 68 | ] 69 | 70 | if s.respond_to? :specification_version then 71 | current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION 72 | s.specification_version = 3 73 | 74 | if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then 75 | else 76 | end 77 | else 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /spec/controllers/empty: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edavis10/redmine_reports/5fa452492dda2f4f5acaa5e148505e97a3295d90/spec/controllers/empty -------------------------------------------------------------------------------- /spec/controllers/system_reports_activity_report_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../spec_helper' 2 | 3 | describe SystemReportsController, "with a anonymous user visiting" do 4 | describe "#activity_report" do 5 | integrate_views 6 | 7 | def do_request 8 | get :activity_report 9 | end 10 | 11 | it_should_behave_like "login_required" 12 | end 13 | 14 | end 15 | 16 | describe SystemReportsController, "with an unauthorized user visiting" do 17 | describe "#activity_report" do 18 | integrate_views 19 | 20 | def do_request 21 | get :activity_report 22 | end 23 | 24 | before(:each) do 25 | logged_in_as_user 26 | @current_user.stub!(:allowed_to?).and_return(false) 27 | end 28 | 29 | it_should_behave_like "denied_access" 30 | end 31 | 32 | end 33 | 34 | describe SystemReportsController, "GET #activity_report" do 35 | integrate_views 36 | 37 | before(:each) do 38 | logged_in_as_admin 39 | end 40 | 41 | it 'should be successful' do 42 | get :activity_report 43 | response.should be_success 44 | end 45 | 46 | it 'should render the activity report template' do 47 | get :activity_report 48 | response.should render_template('activity_report') 49 | end 50 | end 51 | 52 | describe SystemReportsController, "POST #activity_report" do 53 | integrate_views 54 | 55 | before(:each) do 56 | logged_in_as_admin 57 | end 58 | 59 | describe 'with valid data' do 60 | def data(additional_data={}) 61 | { 62 | "start_date"=>"2009-07-01", 63 | "end_date"=>"2009-07-31" 64 | }.merge(additional_data) 65 | end 66 | 67 | it 'should be successful' do 68 | post :activity_report, :activity_report => data 69 | response.should be_success 70 | end 71 | 72 | it 'should render the activity_report template' do 73 | post :activity_report, :activity_report => data 74 | response.should render_template('activity_report') 75 | end 76 | 77 | describe 'results' do 78 | before(:each) do 79 | @another_user = mock_model(User, 80 | :admin? => false, 81 | :logged? => true, 82 | :anonymous? => false, 83 | :name => "Another User", 84 | :projects => Project, 85 | :time_zone => ActiveSupport::TimeZone.all.first, 86 | :language => 'en') 87 | 88 | @events = [ 89 | mock_model(Issue, 90 | :event_url => 'issues/show/1001', 91 | :event_title => 'Entered new issue', 92 | :event_type => 'issue-new', 93 | :event_description => "", 94 | :event_datetime => DateTime.new(2009, 7, 10), 95 | :event_date => Date.new(2009, 7, 10), 96 | :event_author => @current_user, 97 | :project => mock_model(Project)), 98 | mock_model(Issue, 99 | :event_url => 'issues/show/1002', 100 | :event_title => 'Entered another new issue', 101 | :event_type => 'issue-new', 102 | :event_description => "", 103 | :event_datetime => DateTime.new(2009, 7, 11), 104 | :event_date => Date.new(2009, 7, 10), 105 | :event_author => @current_user, 106 | :project => mock_model(Project)), 107 | mock_model(Journal, 108 | :event_url => 'issues/show/1000', 109 | :event_title => 'Entered new journal', 110 | :event_type => 'issue-edit', 111 | :event_description => "", 112 | :event_datetime => DateTime.new(2009, 7, 12), 113 | :event_date => Date.new(2009, 7, 10), 114 | :event_author => @another_user, 115 | :project => mock_model(Project)), 116 | mock_model(Journal, 117 | :project => nil, 118 | :event_url => 'issues/show/1000', 119 | :event_title => 'Entered another journal', 120 | :event_description => "", 121 | :event_type => 'issue-edit', 122 | :event_datetime => DateTime.new(2009, 7, 19), 123 | :event_date => Date.new(2009, 7, 10), 124 | :event_author => @current_user, 125 | :project => mock_model(Project)) 126 | ] 127 | @events_grouped_by_user = @events.group_by(&:event_author) 128 | end 129 | 130 | it 'should fetch all Activity data in the date range' do 131 | activity_report = ActivityReport.new 132 | activity_report.stub!(:default_users).and_return([@current_user, @another_user]) 133 | activity_report.stub!(:start_date).and_return(data["start_date"]) 134 | activity_report.stub!(:end_date).and_return(data["end_date"]) 135 | activity_report.stub!(:valid?).and_return(true) 136 | 137 | activity_report.should_receive(:fetch).and_return(@events) 138 | activity_report.should_receive(:group_events_by_user).and_return(@events_grouped_by_user) 139 | ActivityReport.should_receive(:new).and_return(activity_report) 140 | 141 | post :activity_report, :activity_report => data 142 | end 143 | 144 | it 'should contain all of the days horizonatally' 145 | 146 | it 'should list each user separately' 147 | end 148 | 149 | end 150 | 151 | describe 'with invalid data' do 152 | it 'should render the activity_report template' do 153 | post :activity_report, {} 154 | response.should render_template('activity_report') 155 | end 156 | 157 | it 'should display the errors' do 158 | post :activity_report, {} 159 | assigns[:activity_report].should have(1).errors_on(:start_date) 160 | assigns[:activity_report].should have(1).errors_on(:end_date) 161 | end 162 | end 163 | end 164 | 165 | -------------------------------------------------------------------------------- /spec/controllers/system_reports_completion_count_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../spec_helper' 2 | 3 | describe SystemReportsController, "with a anonymous user visiting" do 4 | describe "#completion_count" do 5 | integrate_views 6 | 7 | def do_request 8 | get :completion_count 9 | end 10 | 11 | it_should_behave_like "login_required" 12 | end 13 | 14 | end 15 | 16 | describe SystemReportsController, "with an unauthorized user visiting" do 17 | describe "#completion_count" do 18 | integrate_views 19 | 20 | def do_request 21 | get :completion_count 22 | end 23 | 24 | before(:each) do 25 | logged_in_as_user 26 | @current_user.stub!(:allowed_to?).and_return(false) 27 | end 28 | 29 | it_should_behave_like "denied_access" 30 | end 31 | 32 | end 33 | 34 | describe SystemReportsController, "GET #completion_count" do 35 | integrate_views 36 | 37 | before(:each) do 38 | logged_in_as_admin 39 | end 40 | 41 | it 'should be successful' do 42 | get :completion_count 43 | response.should be_success 44 | end 45 | 46 | it 'should render the completion_count template' do 47 | get :completion_count 48 | response.should render_template('completion_count') 49 | end 50 | end 51 | 52 | describe SystemReportsController, "POST #completion_count" do 53 | integrate_views 54 | 55 | def user_mocks 56 | @users = [ 57 | mock_model(User, :id => 13, :name => 'Test 13', :valid? => true, :<=> => -1), 58 | mock_model(User, :id => 14, :name => 'Test 14', :valid? => true, :<=> => 1) 59 | ] 60 | @users.stub!(:sort).and_return(@users) 61 | 62 | User.stub!(:find).and_return(@users) 63 | end 64 | 65 | before(:each) do 66 | logged_in_as_admin 67 | user_mocks 68 | end 69 | 70 | describe 'with valid data' do 71 | def data(additional_data={}) 72 | { 73 | "start_date"=>"2009-07-01", 74 | "end_date"=>"2009-07-31", 75 | "user_ids"=>["13", "14"] 76 | }.merge(additional_data) 77 | end 78 | 79 | it 'should be successful' do 80 | post :completion_count, :completion_count => data 81 | response.should be_success 82 | end 83 | 84 | it 'should render the completion_count template' do 85 | post :completion_count, :completion_count => data 86 | response.should render_template('completion_count') 87 | end 88 | 89 | describe 'summary section' do 90 | before(:each) do 91 | @completion_count = CompletionCount.new(data) 92 | CompletionCount.should_receive(:new).and_return(@completion_count) 93 | end 94 | 95 | it 'should show the total incoming' do 96 | @completion_count.should_receive(:total_incoming).twice.and_return(42) 97 | post :completion_count, :completion_count => data 98 | response.should have_tag("tr.incoming td", "42") 99 | end 100 | 101 | it 'should show the total completed' do 102 | @completion_count.should_receive(:total_completed).twice.and_return(154) 103 | post :completion_count, :completion_count => data 104 | response.should have_tag("tr.completed td", "154") 105 | 106 | end 107 | 108 | it 'should show the difference' do 109 | @completion_count.should_receive(:total_incoming).twice.and_return(42) 110 | @completion_count.should_receive(:total_completed).twice.and_return(154) 111 | post :completion_count, :completion_count => data 112 | response.should have_tag("tr.difference td", "-112") 113 | end 114 | 115 | end 116 | 117 | describe 'user section' do 118 | it 'should show the sum of each tracker for each user' do 119 | @bug = mock_model(Tracker, :name => 'Bug') 120 | @feature = mock_model(Tracker, :name => 'Feature') 121 | Tracker.should_receive(:all).at_least(:once).and_return do 122 | [@bug, @feature] 123 | end 124 | 125 | @completion_count = CompletionCount.new(data) 126 | CompletionCount.should_receive(:new).and_return(@completion_count) 127 | 128 | @completion_count.should_receive(:total_by_tracker_for_user).with(@bug, 13).and_return(12) 129 | @completion_count.should_receive(:total_by_tracker_for_user).with(@feature, 13).and_return(16) 130 | @completion_count.should_receive(:total_by_tracker_for_user).with(@bug, 14).and_return(24) 131 | @completion_count.should_receive(:total_by_tracker_for_user).with(@feature, 14).and_return(56) 132 | @completion_count.stub!(:total_closed_for_user).with(13).and_return(100) 133 | @completion_count.stub!(:total_closed_for_user).with(14).and_return(100) 134 | 135 | post :completion_count, :completion_count => data 136 | 137 | response.should have_tag("#user-13") do 138 | with_tag("tr#tracker-#{@bug.id}") do 139 | with_tag('td', '12') 140 | end 141 | end 142 | 143 | response.should have_tag("#user-13") do 144 | with_tag("tr#tracker-#{@feature.id}") do 145 | with_tag('td', '16') 146 | end 147 | end 148 | 149 | response.should have_tag("#user-14") do 150 | with_tag("tr#tracker-#{@bug.id}") do 151 | with_tag('td', '24') 152 | end 153 | end 154 | 155 | response.should have_tag("#user-14") do 156 | with_tag("tr#tracker-#{@feature.id}") do 157 | with_tag('td', '56') 158 | end 159 | end 160 | 161 | end 162 | 163 | it 'should show the total sum of closed issues for each user' do 164 | @completion_count = CompletionCount.new(data) 165 | CompletionCount.should_receive(:new).and_return(@completion_count) 166 | 167 | @completion_count.should_receive(:total_closed_for_user).with(13).at_least(:once).and_return(185) 168 | @completion_count.should_receive(:total_closed_for_user).with(14).at_least(:once).and_return(214) 169 | 170 | post :completion_count, :completion_count => data 171 | 172 | response.should have_tag("#user-13") do 173 | with_tag("tr.total-closed") do 174 | with_tag('td', '185') 175 | end 176 | end 177 | 178 | response.should have_tag("#user-14") do 179 | with_tag("tr.total-closed") do 180 | with_tag('td', '214') 181 | end 182 | end 183 | end 184 | 185 | end 186 | end 187 | 188 | describe 'with invalid data' do 189 | it 'should render the completion_count template' do 190 | post :completion_count, {} 191 | response.should render_template('completion_count') 192 | end 193 | 194 | it 'should display the errors' do 195 | post :completion_count, {} 196 | assigns[:completion_count].should have(1).errors_on(:start_date) 197 | assigns[:completion_count].should have(1).errors_on(:end_date) 198 | 199 | end 200 | end 201 | end 202 | 203 | -------------------------------------------------------------------------------- /spec/controllers/system_reports_overview_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../spec_helper' 2 | 3 | describe SystemReportsController, "#index" do 4 | integrate_views 5 | 6 | before(:each) do 7 | logged_in_as_user 8 | @current_user.stub!(:allowed_to?).and_return(false) 9 | end 10 | 11 | it 'should be successful' do 12 | get :index 13 | response.should be_success 14 | end 15 | 16 | it 'should render the index template' do 17 | get :index 18 | response.should render_template('index') 19 | end 20 | end 21 | 22 | describe SystemReportsController, "with a anonymous user visiting" do 23 | describe "#index" do 24 | integrate_views 25 | 26 | def do_request 27 | get :index 28 | end 29 | 30 | it_should_behave_like "login_required" 31 | end 32 | 33 | end 34 | -------------------------------------------------------------------------------- /spec/controllers/system_reports_quickbooks_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../spec_helper' 2 | 3 | if Redmine::Plugin.registered_plugins.keys.include? :redmine_billing 4 | 5 | describe SystemReportsController, "#quickbooks" do 6 | integrate_views 7 | include ActionView::Helpers::NumberHelper 8 | 9 | def total_po_data 10 | { 11 | 'a' => 100.0, 12 | 'b' => 350.43, 13 | 'c' => 23.45, 14 | 'd' => 0 15 | } 16 | end 17 | 18 | def total_po_projects 19 | project_a = mock_model(Project, :total_value => total_po_data['a'], :name => "Project A") 20 | project_b = mock_model(Project, :total_value => total_po_data['b'], :name => "Project B") 21 | project_c = mock_model(Project, :total_value => total_po_data['c'], :name => "Project C") 22 | project_d = mock_model(Project, :total_value => total_po_data['d'], :name => "Project D") 23 | 24 | [project_a, project_b, project_c, project_d] 25 | end 26 | 27 | def unspent_labor_data 28 | [ 29 | ["Project A", 1000.0], 30 | ["Project B", 3500.43], 31 | ["Project C", 230.45], 32 | ["Project D", 0] 33 | ] 34 | end 35 | 36 | def unbilled_labor_data 37 | [ 38 | ["User A", 10.0], 39 | ["User B", 35.43], 40 | ["User C", 2.45], 41 | ["User D", 0] 42 | ] 43 | end 44 | 45 | before(:each) do 46 | logged_in_as_admin 47 | Project.stub!(:all).and_return([]) # Project jump box conflicts with mocks below 48 | Project.stub!(:find).with(:all).and_return(total_po_projects) 49 | BillingExport.stub!(:unspent_labor).and_return(unspent_labor_data) 50 | BillingExport.stub!(:unbilled_labor).and_return(unbilled_labor_data) 51 | end 52 | 53 | it 'should be successful' do 54 | get :quickbooks 55 | response.should be_success 56 | end 57 | 58 | it 'should render the index template' do 59 | get :quickbooks 60 | response.should render_template('quickbooks') 61 | end 62 | 63 | describe 'exporting the total po from the Billing plugin' do 64 | it 'should get the total value for each project' do 65 | Project.should_receive(:find).with(:all).and_return(total_po_projects) 66 | get :quickbooks 67 | end 68 | 69 | it 'should show the total of the total po' do 70 | get :quickbooks 71 | total = total_po_data.collect {|project, value| value }.sum 72 | response.should have_tag("tr#total_po_total", /#{total.to_s}/) 73 | end 74 | 75 | it 'should show the amounts for each project as a currency' do 76 | get :quickbooks 77 | response.should have_tag("table#total_po") do 78 | with_tag("td.total_po_amount",'$100.00') 79 | with_tag("td.total_po_amount",'$350.43') 80 | with_tag("td.total_po_amount",'$23.45') 81 | end 82 | end 83 | 84 | it 'should not show the amount for a project with 0' do 85 | get :quickbooks 86 | response.should_not have_tag("td.total_po_project",'Project D') 87 | response.should_not have_tag("td.total_po_amount",'$0') 88 | end 89 | end 90 | 91 | describe 'exporting the unspent labor from the Billing plugin' do 92 | it 'should use BillingExport#unspent_labor' do 93 | BillingExport.should_receive(:unspent_labor).and_return(unspent_labor_data) 94 | get :quickbooks 95 | end 96 | 97 | it 'should show the total of the unspent labor' do 98 | get :quickbooks 99 | total = unspent_labor_data.collect {|item| item[1] }.sum 100 | response.should have_tag("tr#unspent_labor_total", /#{number_with_delimiter(total)}/) 101 | end 102 | 103 | it 'should show the amounts for each project as a currency' do 104 | get :quickbooks 105 | response.should have_tag("table#unspent_labor") do 106 | with_tag("td.unspent_labor_amount",'$1,000.00') 107 | with_tag("td.unspent_labor_amount",'$3,500.43') 108 | with_tag("td.unspent_labor_amount",'$230.45') 109 | end 110 | end 111 | 112 | it 'should not show the amount for a project with 0' do 113 | get :quickbooks 114 | response.should_not have_tag("td.unspent_labor_user",'Project D') 115 | response.should_not have_tag("td.unspent_labor_amount",'$0') 116 | end 117 | end 118 | 119 | describe 'exporting the unbilled labor from the Billing plugin' do 120 | it 'should use BillingExport#unbilled_labor' do 121 | BillingExport.should_receive(:unbilled_labor).and_return(unbilled_labor_data) 122 | get :quickbooks 123 | end 124 | 125 | it 'should show the total of the unbilled labor' do 126 | get :quickbooks 127 | total = unbilled_labor_data.collect {|item| item[1] }.sum 128 | response.should have_tag("tr#unbilled_labor_total", /#{number_with_delimiter(total)}/) 129 | end 130 | 131 | it 'should show the amounts for each user as a currency' do 132 | get :quickbooks 133 | response.should have_tag("table#unbilled_labor") do 134 | with_tag("td.unbilled_labor_amount",'$10.00') 135 | with_tag("td.unbilled_labor_amount",'$35.43') 136 | with_tag("td.unbilled_labor_amount",'$2.45') 137 | end 138 | end 139 | 140 | it 'should not show the amount for a user with 0' do 141 | get :quickbooks 142 | response.should_not have_tag("td.unspent_labor_user",'User D') 143 | response.should_not have_tag("td.unspent_labor_amount",'$0') 144 | end 145 | end 146 | end 147 | 148 | describe SystemReportsController, "with a anonymous user visiting" do 149 | describe "#quickbooks" do 150 | integrate_views 151 | 152 | def do_request 153 | get :quickbooks 154 | end 155 | 156 | it_should_behave_like "login_required" 157 | end 158 | 159 | end 160 | 161 | describe SystemReportsController, "with an unauthorized user visiting" do 162 | describe "#quickbooks" do 163 | integrate_views 164 | 165 | def do_request 166 | get :quickbooks 167 | end 168 | 169 | before(:each) do 170 | logged_in_as_user 171 | @current_user.stub!(:allowed_to?).and_return(false) 172 | end 173 | 174 | it_should_behave_like "denied_access" 175 | end 176 | 177 | end 178 | 179 | else 180 | puts "*** Skipping Quickbooks test" 181 | end 182 | -------------------------------------------------------------------------------- /spec/models/activity_report_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../spec_helper' 2 | 3 | def data(additional_data={}) 4 | { 5 | "start_date"=>"2009-07-01", 6 | "end_date"=>"2009-07-31", 7 | "user_ids" => [""], 8 | }.merge(additional_data) 9 | end 10 | 11 | 12 | 13 | describe ActivityReport, '#fetch' do 14 | before(:each) do 15 | @current_user = mock_model(User, :id => 1) 16 | @another_user = mock_model(User, :id => 2) 17 | @users = [@current_user, @another_user] 18 | @users.stub!(:sort).and_return(@users) 19 | 20 | @events = [ 21 | mock_model(Issue, :event_author => @current_user), 22 | mock_model(Issue, :event_author => @current_user), 23 | mock_model(Journal, :event_author => @another_user), 24 | mock_model(Journal, :event_author => @current_user) 25 | ] 26 | User.stub!(:active).and_return(@users) 27 | end 28 | 29 | def mock_fetcher 30 | @fetcher = Redmine::Activity::Fetcher.new(User.current) 31 | Redmine::Activity::Fetcher.should_receive(:new).and_return(@fetcher) 32 | @fetcher.stub!(:events).and_return(@events) 33 | end 34 | 35 | it 'should fetch all Activity data in the date range' do 36 | mock_fetcher 37 | @fetcher.should_receive(:events).with(data['start_date'], '2009-08-01').and_return(@events) 38 | activity = ActivityReport.new(data) 39 | activity.fetch 40 | end 41 | 42 | it 'should set @fetcher to the Redmine Activity Fetcher' do 43 | mock_fetcher 44 | activity = ActivityReport.new(data) 45 | activity.fetch 46 | activity.fetcher.should eql(@fetcher) 47 | end 48 | 49 | it 'should set @events' do 50 | mock_fetcher 51 | activity = ActivityReport.new(data) 52 | activity.fetch 53 | activity.events.should_not be_nil 54 | activity.events.size.should eql(4) 55 | end 56 | 57 | end 58 | 59 | describe ActivityReport, '#group_events_by_user' do 60 | 61 | it 'should set return the events grouped by user' 62 | 63 | end 64 | -------------------------------------------------------------------------------- /spec/models/completion_count_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../spec_helper' 2 | 3 | describe CompletionCount, '#role_ids=' do 4 | before(:each) do 5 | @user1 = mock_model(User, :id => 1, :quoted_id => '1') 6 | @user2 = mock_model(User, :id => 2) 7 | 8 | 9 | @role1 = mock_model(Role) 10 | @role1.stub!(:members).and_return do 11 | [ 12 | mock_model(Member, :user => @user1), 13 | mock_model(Member, :user => @user1), 14 | mock_model(Member, :user => @user1) 15 | ] 16 | end 17 | 18 | @role3 = mock_model(Role) 19 | @role3.stub!(:members).and_return do 20 | [ 21 | mock_model(Member, :user => @user1), 22 | mock_model(Member, :user => @user2) 23 | ] 24 | end 25 | end 26 | 27 | it 'should find all the Roles' do 28 | Role.should_receive(:find_by_id).with(1).and_return(@role1) 29 | Role.should_receive(:find_by_id).with(3).and_return(@role3) 30 | @completion_count = CompletionCount.new(:role_ids => ['1',3]) 31 | end 32 | 33 | it 'should add all of the users with the Roles to the users list' do 34 | Role.should_receive(:find_by_id).with(1).and_return(@role1) 35 | Role.should_receive(:find_by_id).with(3).and_return(@role3) 36 | @completion_count = CompletionCount.new(:role_ids => ['1',3]) 37 | 38 | @completion_count.users.should include(@user1) 39 | @completion_count.users.should include(@user2) 40 | end 41 | 42 | it 'should not duplicate users' do 43 | Role.should_receive(:find_by_id).with(1).and_return(@role1) 44 | Role.should_receive(:find_by_id).with(3).and_return(@role3) 45 | @completion_count = CompletionCount.new(:role_ids => ['1',3]) 46 | 47 | @completion_count.users.size.should eql(2) 48 | end 49 | end 50 | 51 | 52 | describe CompletionCount, '#total_incoming' do 53 | it 'should get a count of the number of issues created in the date range' do 54 | start_date = Date.yesterday 55 | end_date = Date.today 56 | @completion_count = CompletionCount.new(:start_date => start_date, :end_date => end_date) 57 | Issue.should_receive(:visible).and_return(Issue) 58 | Issue.should_receive(:count).with(:conditions => ["#{Issue.table_name}.created_on >= (?) and #{Issue.table_name}.created_on <= (?)", 59 | start_date, 60 | end_date]).and_return(120) 61 | 62 | @completion_count.total_incoming.should eql(120) 63 | end 64 | end 65 | 66 | describe CompletionCount, '#total_completed' do 67 | it 'should get a count of the number of tasks that have been closed in the date range' do 68 | start_date = Date.yesterday 69 | end_date = Date.today 70 | @completion_count = CompletionCount.new(:start_date => start_date, :end_date => end_date) 71 | 72 | IssueStatus.should_receive(:all).with(:conditions => {:is_closed => true}).and_return do 73 | [mock_model(IssueStatus, :id => 4), mock_model(IssueStatus, :id => 5), mock_model(IssueStatus, :id => 7)] 74 | end 75 | 76 | Issue.should_receive(:visible).and_return(Issue) 77 | conditions = ["#{Issue.table_name}.updated_on >= (?) and #{Issue.table_name}.updated_on <= (?) and #{Issue.table_name}.status_id IN (?)", 78 | start_date, 79 | end_date, 80 | [4,5,7] 81 | ] 82 | 83 | Issue.should_receive(:count).with(:conditions => conditions).and_return(100) 84 | 85 | @completion_count.total_completed.should eql(100) 86 | 87 | end 88 | end 89 | 90 | describe CompletionCount, '#total_by_tracker_for_user' do 91 | it 'should get a count of the number of closed tasks that are in the tracker for the user' do 92 | start_date = Date.yesterday 93 | end_date = Date.today 94 | IssueStatus.should_receive(:all).with(:conditions => {:is_closed => true}).and_return do 95 | [mock_model(IssueStatus, :id => 4), mock_model(IssueStatus, :id => 5), mock_model(IssueStatus, :id => 7)] 96 | end 97 | 98 | @completion_count = CompletionCount.new(:start_date => start_date, :end_date => end_date) 99 | 100 | @tracker = mock_model(Tracker) 101 | 102 | Issue.should_receive(:visible).and_return(Issue) 103 | conditions = ["#{Issue.table_name}.updated_on >= (?) and #{Issue.table_name}.updated_on <= (?) and #{Issue.table_name}.tracker_id = (?) and #{Issue.table_name}.assigned_to_id = (?) and #{Issue.table_name}.status_id IN (?)", 104 | start_date, 105 | end_date, 106 | @tracker.id, 107 | 123, 108 | [4,5,7] 109 | ] 110 | 111 | Issue.should_receive(:count).with(:conditions => conditions).and_return(1200) 112 | 113 | @completion_count.total_by_tracker_for_user(@tracker, 123).should eql(1200) 114 | end 115 | end 116 | 117 | describe CompletionCount, "#total_closed_for_user" do 118 | it 'should get a count of the number of closed tasks for the user' do 119 | start_date = Date.yesterday 120 | end_date = Date.today 121 | IssueStatus.should_receive(:all).with(:conditions => {:is_closed => true}).and_return do 122 | [mock_model(IssueStatus, :id => 4), mock_model(IssueStatus, :id => 5), mock_model(IssueStatus, :id => 7)] 123 | end 124 | 125 | @completion_count = CompletionCount.new(:start_date => start_date, :end_date => end_date) 126 | 127 | @tracker = mock_model(Tracker) 128 | 129 | Issue.should_receive(:visible).and_return(Issue) 130 | conditions = ["#{Issue.table_name}.updated_on >= (?) and #{Issue.table_name}.updated_on <= (?) and #{Issue.table_name}.status_id IN (?) and #{Issue.table_name}.assigned_to_id = (?)", 131 | start_date, 132 | end_date, 133 | [4,5,7], 134 | 123 135 | ] 136 | 137 | Issue.should_receive(:count).with(:conditions => conditions).and_return(1100) 138 | 139 | @completion_count.total_closed_for_user(123).should eql(1100) 140 | end 141 | end 142 | -------------------------------------------------------------------------------- /spec/rcov.opts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edavis10/redmine_reports/5fa452492dda2f4f5acaa5e148505e97a3295d90/spec/rcov.opts -------------------------------------------------------------------------------- /spec/sanity_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/spec_helper' 2 | 3 | describe Class do 4 | it "should be a class of Class" do 5 | Class.class.should eql(Class) 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/spec.opts: -------------------------------------------------------------------------------- 1 | --colour 2 | --format 3 | progress 4 | --loadby 5 | mtime 6 | --reverse 7 | --backtrace -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # This file is copied to ~/spec when you run 'ruby script/generate rspec' 2 | # from the project root directory. 3 | ENV["RAILS_ENV"] = "test" 4 | require File.expand_path(File.dirname(__FILE__) + "/../../../../config/environment") 5 | require 'spec' 6 | require 'spec/rails' 7 | require 'ruby-debug' 8 | 9 | Spec::Runner.configure do |config| 10 | # If you're not using ActiveRecord you should remove these 11 | # lines, delete config/database.yml and disable :active_record 12 | # in your config/boot.rb 13 | config.use_transactional_fixtures = true 14 | config.use_instantiated_fixtures = false 15 | config.fixture_path = RAILS_ROOT + '/spec/fixtures/' 16 | 17 | # == Fixtures 18 | # 19 | # You can declare fixtures for each example_group like this: 20 | # describe "...." do 21 | # fixtures :table_a, :table_b 22 | # 23 | # Alternatively, if you prefer to declare them only once, you can 24 | # do so right here. Just uncomment the next line and replace the fixture 25 | # names with your fixtures. 26 | # 27 | # config.global_fixtures = :table_a, :table_b 28 | # 29 | # If you declare global fixtures, be aware that they will be declared 30 | # for all of your examples, even those that don't use them. 31 | # 32 | # == Mock Framework 33 | # 34 | # RSpec uses it's own mocking framework by default. If you prefer to 35 | # use mocha, flexmock or RR, uncomment the appropriate line: 36 | # 37 | # config.mock_with :mocha 38 | # config.mock_with :flexmock 39 | # config.mock_with :rr 40 | end 41 | 42 | # require the entire app if we're running under coverage testing, 43 | # so we measure 0% covered files in the report 44 | # 45 | # http://www.pervasivecode.com/blog/2008/05/16/making-rcov-measure-your-whole-rails-app-even-if-tests-miss-entire-source-files/ 46 | if defined?(Rcov) 47 | all_app_files = Dir.glob('{app,lib}/**/*.rb') 48 | all_app_files.each{|rb| require rb} 49 | end 50 | 51 | module SystemReportsSpecHelper 52 | def logged_in_as_admin 53 | @current_user = mock_model(User, 54 | :admin? => true, 55 | :logged? => true, 56 | :anonymous? => false, 57 | :active? => true, 58 | :name => "Administrator", 59 | :projects => Project, 60 | :time_zone => ActiveSupport::TimeZone.all.first, 61 | :language => 'en') 62 | @current_user.stub!(:allowed_to?).and_return(true) 63 | 64 | User.stub!(:current).and_return(@current_user) 65 | return @current_user 66 | end 67 | 68 | def logged_in_as_user 69 | @current_user = mock_model(User, 70 | :admin? => false, 71 | :logged? => true, 72 | :anonymous? => false, 73 | :active? => true, 74 | :name => "User", 75 | :projects => Project, 76 | :time_zone => ActiveSupport::TimeZone.all.first, 77 | :language => 'en') 78 | User.stub!(:current).and_return(@current_user) 79 | return @current_user 80 | end 81 | end 82 | 83 | include SystemReportsSpecHelper 84 | 85 | 86 | describe "login_required", :shared => true do 87 | it 'should redirect' do 88 | do_request 89 | response.should be_redirect 90 | end 91 | 92 | it 'should redirect to the login page' do 93 | do_request 94 | response.should redirect_to(:controller => 'account', :action => 'login', :back_url => controller.url_for(params)) 95 | end 96 | 97 | end 98 | 99 | describe "denied_access", :shared => true do 100 | it 'should not be successful' do 101 | do_request 102 | response.should_not be_success 103 | end 104 | 105 | it 'should return a 403 status code' do 106 | do_request 107 | response.code.should eql("403") 108 | end 109 | 110 | it 'should display the standard unauthorized page' do 111 | do_request 112 | response.should render_template('common/403') 113 | end 114 | 115 | end 116 | -------------------------------------------------------------------------------- /spec/views/empty: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edavis10/redmine_reports/5fa452492dda2f4f5acaa5e148505e97a3295d90/spec/views/empty -------------------------------------------------------------------------------- /test/functional/system_reports_completion_count_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../test_helper' 2 | require 'system_reports_controller' 3 | 4 | # Re-raise errors caught by the controller. 5 | class SystemReportsController; def rescue_action(e) raise e end; end 6 | 7 | class SystemReportsControllerCompletionCountTest < ActionController::TestCase 8 | def setup 9 | @controller = SystemReportsController.new 10 | @request = ActionController::TestRequest.new 11 | @response = ActionController::TestResponse.new 12 | build_anonymous_role 13 | setup_plugin_configuration 14 | end 15 | 16 | context "GET :completion_count" do 17 | context "as an anonymous user" do 18 | should 'be tested' 19 | end 20 | 21 | context "as an unauthorized user" do 22 | should 'be tested' 23 | end 24 | 25 | context "logged in as admin" do 26 | setup do 27 | @user = User.generate_with_protected!(:admin => true) 28 | @request.session[:user_id] = @user.id 29 | 30 | get :completion_count 31 | end 32 | 33 | should_respond_with :success 34 | should_render_template :completion_count 35 | 36 | end 37 | end 38 | 39 | context "POST :completion_count as an anonymous user" do 40 | should 'be tested' 41 | end 42 | 43 | context "POST :completion_count as an unauthorized user" do 44 | should 'be tested' 45 | end 46 | 47 | context "POST :completion_count as an admin" do 48 | setup do 49 | @user = User.generate_with_protected!(:admin => true) 50 | @request.session[:user_id] = @user.id 51 | end 52 | 53 | context "with valid data" do 54 | setup do 55 | @project = Project.generate! 56 | 57 | @user1 = User.generate_with_protected!(:admin => true) 58 | @user2 = User.generate_with_protected!(:admin => true) 59 | @bug = Tracker.generate!(:name => 'Bug') 60 | @feature = Tracker.generate!(:name => 'Feature') 61 | @project.trackers << @bug 62 | @project.trackers << @feature 63 | @open = IssueStatus.generate! 64 | @closed = IssueStatus.generate!(:is_closed => true) 65 | 66 | # Bug, Incoming, Completed, User1 67 | Issue.generate_for_project!(@project, { 68 | :assigned_to => @user1, 69 | :tracker => @bug, 70 | :created_on => 2.days.ago, 71 | :updated_on => 1.day.ago, 72 | :status => @closed 73 | }) 74 | 75 | # Bug, Incoming, Open, User2 76 | Issue.generate_for_project!(@project, { 77 | :assigned_to => @user2, 78 | :tracker => @bug, 79 | :created_on => 2.days.ago, 80 | :updated_on => 1.day.ago, 81 | :status => @open 82 | }) 83 | 84 | # Feature, Incoming, Completed, User2 85 | Issue.generate_for_project!(@project, { 86 | :assigned_to => @user2, 87 | :tracker => @feature, 88 | :created_on => 2.days.ago, 89 | :updated_on => 1.day.ago, 90 | :status => @closed 91 | }) 92 | 93 | # Feature, Excluded User2 94 | Issue.generate_for_project!(@project, { 95 | :assigned_to => @user2, 96 | :tracker => @feature, 97 | :created_on => 2.days.ago, 98 | :updated_on => 1.day.ago, 99 | :status => @excluded_status1 100 | }) 101 | 102 | # Feature, out of date range 103 | Issue.generate_for_project!(@project, { 104 | :assigned_to => @user2, 105 | :tracker => @feature, 106 | :created_on => 2.weeks.ago, 107 | :updated_on => 9.days.ago, 108 | :status => @closed 109 | }) 110 | 111 | post :completion_count, :completion_count => { 112 | "start_date"=> 1.week.ago.to_date.to_s, 113 | "end_date"=> Date.today.to_s, 114 | "user_ids"=>[@user1.id, @user2.id] 115 | } 116 | end 117 | 118 | should_respond_with :success 119 | should_render_template :completion_count 120 | 121 | context 'summary section' do 122 | should 'show the total incoming' do 123 | assert_select 'td#total-incoming', '3' 124 | end 125 | 126 | should 'show the total completed' do 127 | assert_select 'td#total-completed', '2' 128 | end 129 | 130 | should 'show the difference' do 131 | assert_select 'td#total-difference', '1' 132 | end 133 | 134 | should 'show the excluded count' do 135 | assert_select 'td#total-excluded', '1' 136 | end 137 | end 138 | 139 | context 'user section' do 140 | should 'show the sum of each tracker for each user' do 141 | assert_select "#user-#{@user1.id}" do 142 | assert_select "tr#tracker-#{@bug.id}" do 143 | assert_select 'td','1' 144 | end 145 | end 146 | 147 | assert_select "#user-#{@user2.id}" do 148 | assert_select "tr#tracker-#{@bug.id}" do 149 | assert_select 'td','0' 150 | end 151 | end 152 | 153 | assert_select "#user-#{@user2.id}" do 154 | assert_select "tr#tracker-#{@feature.id}" do 155 | assert_select 'td','1' 156 | end 157 | end 158 | end 159 | 160 | should 'show the total sum of closed issues for each user' do 161 | assert_select("#user-#{@user1.id}") do 162 | assert_select("tr.total-closed") do 163 | assert_select('td', '1') 164 | end 165 | end 166 | 167 | assert_select("#user-#{@user2.id}") do 168 | assert_select("tr.total-closed") do 169 | assert_select('td', '1') 170 | end 171 | end 172 | 173 | end 174 | 175 | end 176 | end 177 | 178 | context 'with invalid data' do 179 | setup do 180 | post :completion_count, {} 181 | end 182 | 183 | should_respond_with :success 184 | 185 | should 'display the errors' do 186 | assert assigns['completion_count'].errors.on(:start_date) 187 | assert assigns['completion_count'].errors.on(:end_date) 188 | end 189 | end 190 | end 191 | end 192 | 193 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | # Load the normal Rails helper 2 | require File.expand_path(File.dirname(__FILE__) + '/../../../../test/test_helper') 3 | 4 | # Ensure that we are using the temporary fixture path 5 | Engines::Testing.set_fixture_path 6 | 7 | # Helpers 8 | class ActiveSupport::TestCase 9 | def configure_plugin(fields={}) 10 | Setting.plugin_redmine_reports = fields.stringify_keys 11 | end 12 | 13 | def setup_plugin_configuration 14 | @excluded_status1 = IssueStatus.generate!(:is_closed => true) 15 | configure_plugin({ 16 | 'select_size' => '5', 17 | 'completion_count' => { 18 | 'exclude_statuses' => [@excluded_status1.id.to_s] 19 | } 20 | }) 21 | end 22 | 23 | def build_anonymous_role 24 | @role = Role.generate! 25 | @role.update_attribute(:builtin, Role::BUILTIN_ANONYMOUS) 26 | end 27 | 28 | end 29 | -------------------------------------------------------------------------------- /test/unit/completion_count_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../test_helper' 2 | 3 | class CompletionCountTest < ActiveSupport::TestCase 4 | def setup 5 | build_anonymous_role 6 | setup_plugin_configuration 7 | @admin = User.generate_with_protected!(:admin => true) 8 | User.current = @admin 9 | @start_date = Date.yesterday 10 | @end_date = Date.today 11 | @date_inside = @start_date + 1.hour 12 | @project = Project.generate! 13 | @closed = IssueStatus.generate!(:is_closed => true) 14 | @completion_count = CompletionCount.new(:start_date => @start_date, :end_date => @end_date) 15 | end 16 | 17 | context "#total_incoming" do 18 | should 'get a count of the number of issues created in the date range' do 19 | Issue.generate_for_project!(@project, :created_on => @date_inside) 20 | Issue.generate_for_project!(@project, :created_on => @date_inside) 21 | Issue.generate_for_project!(@project, :created_on => @date_inside) 22 | Issue.generate_for_project!(@project, :created_on => @date_inside) 23 | 24 | assert_equal 4, @completion_count.total_incoming 25 | end 26 | 27 | should 'exclude issues with the "exclude status"' do 28 | Issue.generate_for_project!(@project, :created_on => @date_inside) 29 | Issue.generate_for_project!(@project, :created_on => @date_inside) 30 | Issue.generate_for_project!(@project, :created_on => @date_inside, :status => @excluded_status1) 31 | 32 | assert_equal 2, @completion_count.total_incoming 33 | end 34 | end 35 | 36 | context "#total_completed" do 37 | should 'get the count of the number of tasks closed in the date range' do 38 | Issue.generate_for_project!(@project, :updated_on => @date_inside, :status => @closed) 39 | Issue.generate_for_project!(@project, :updated_on => @date_inside, :status => @closed) 40 | Issue.generate_for_project!(@project, :updated_on => @date_inside, :status => @closed) 41 | Issue.generate_for_project!(@project, :updated_on => @date_inside, :status => @closed) 42 | 43 | assert_equal 4, @completion_count.total_completed 44 | 45 | end 46 | 47 | should 'exclude issues with the "exclude status"' do 48 | Issue.generate_for_project!(@project, :updated_on => @date_inside, :status => @closed) 49 | Issue.generate_for_project!(@project, :updated_on => @date_inside, :status => @closed) 50 | Issue.generate_for_project!(@project, :created_on => @date_inside, :status => @excluded_status1) 51 | 52 | assert_equal 2, @completion_count.total_completed 53 | end 54 | end 55 | 56 | context "#total_by_tracker_for_user" do 57 | setup do 58 | @project.trackers << @tracker = Tracker.generate! 59 | end 60 | should 'count the number of closed issues in that tracker for the user' do 61 | Issue.generate_for_project!(@project, :updated_on => @date_inside, :status => @closed, :assigned_to => @admin) do |issue| 62 | issue.tracker = @tracker 63 | end 64 | Issue.generate_for_project!(@project, :updated_on => @date_inside, :status => @closed, :assigned_to => @admin) do |issue| 65 | issue.tracker = @tracker 66 | end 67 | Issue.generate_for_project!(@project, :updated_on => @date_inside, :status => @closed, :assigned_to => @admin) do |issue| 68 | issue.tracker = @tracker 69 | end 70 | Issue.generate_for_project!(@project, :updated_on => @date_inside, :status => @closed, :assigned_to => @admin) 71 | 72 | assert_equal 3, @completion_count.total_by_tracker_for_user(@tracker, @admin) 73 | end 74 | 75 | should 'exclude issues with the "exclude status"' do 76 | Issue.generate_for_project!(@project, :updated_on => @date_inside, :status => @closed, :assigned_to => @admin) do |issue| 77 | issue.tracker = @tracker 78 | end 79 | 80 | Issue.generate_for_project!(@project, :updated_on => @date_inside, :status => @excluded_status1, :assigned_to => @admin) do |issue| 81 | issue.tracker = @tracker 82 | end 83 | 84 | assert_equal 1, @completion_count.total_by_tracker_for_user(@tracker, @admin) 85 | end 86 | end 87 | 88 | context '#total_closed_for_user' do 89 | should 'count the number of closed issues' do 90 | Issue.generate_for_project!(@project, :updated_on => @date_inside, :status => @closed, :assigned_to => @admin) 91 | Issue.generate_for_project!(@project, :updated_on => @date_inside, :status => @closed, :assigned_to => @admin) 92 | Issue.generate_for_project!(@project, :updated_on => @date_inside, :status => @closed, :assigned_to => @admin) 93 | Issue.generate_for_project!(@project, :updated_on => @date_inside, :status => @closed, :assigned_to => @admin) 94 | 95 | assert_equal 4, @completion_count.total_closed_for_user(@admin) 96 | 97 | end 98 | 99 | should 'exclude issues with the "exclude status"' do 100 | Issue.generate_for_project!(@project, :updated_on => @date_inside, :status => @closed, :assigned_to => @admin) 101 | Issue.generate_for_project!(@project, :updated_on => @date_inside, :status => @excluded_status1, :assigned_to => @admin) 102 | 103 | assert_equal 1, @completion_count.total_closed_for_user(@admin) 104 | 105 | end 106 | end 107 | end 108 | -------------------------------------------------------------------------------- /test/unit/plugin_configuration_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../test_helper' 2 | 3 | class PluginConfigurationTest < ActiveSupport::TestCase 4 | test "nothing yet" do 5 | assert true 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /test/unit/sanity_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../test_helper' 2 | 3 | class SanityTest < ActiveSupport::TestCase 4 | def test_is_sane 5 | assert true 6 | end 7 | 8 | should "be true" do 9 | assert true 10 | end 11 | 12 | should "mixin ObjectDaddy" do 13 | assert User.included_modules.include?(ObjectDaddy) 14 | end 15 | 16 | should "connect to database" do 17 | User.generate_with_protected!(:firstname => 'Testing connection') 18 | assert_equal 1, User.count(:all, :conditions => {:firstname => 'Testing connection'}) 19 | end 20 | end 21 | --------------------------------------------------------------------------------