├── .gitignore ├── COPYRIGHT.txt ├── CREDITS.txt ├── GPL.txt ├── Gemfile ├── README.rdoc ├── Rakefile ├── VERSION ├── app ├── controllers │ └── timesheet_controller.rb ├── helpers │ └── timesheet_helper.rb ├── models │ └── timesheet.rb └── views │ ├── settings │ └── _timesheet_settings.rhtml │ └── timesheet │ ├── _by_issue.rhtml │ ├── _form.rhtml │ ├── _issue_time_entries.rhtml │ ├── _time_entry.rhtml │ ├── _timesheet_group.rhtml │ ├── context_menu.html.erb │ ├── index.rhtml │ ├── no_projects.rhtml │ ├── report.rhtml │ └── timelog.rhtml ├── assets ├── images │ ├── csv.png │ ├── toggle-arrow-closed.gif │ └── toggle-arrow-open.gif ├── javascripts │ └── timesheet.js └── stylesheets │ └── timesheet.css ├── autotest └── discover.rb ├── config ├── locales │ ├── ca.yml │ ├── cs.yml │ ├── da.yml │ ├── de.yml │ ├── en.yml │ ├── es.yml │ ├── fr.yml │ ├── hu.yml │ ├── hy.yml │ ├── it.yml │ ├── ja.yml │ ├── lt.yml │ ├── pl.yml │ ├── pt-br.yml │ ├── ru.yml │ ├── sr.yml │ ├── sv.yml │ └── uk.yml └── routes.rb ├── init.rb ├── lang ├── ca.yml ├── cs.yml ├── da.yml ├── de.yml ├── en.yml ├── es.yml ├── fr.yml ├── hu.yml ├── hy.yml ├── it.yml ├── ja.yml ├── lt.yml ├── pl.yml ├── pt-br.yml ├── ru.yml ├── sr.yml ├── sv.yml └── uk.yml ├── lib ├── timesheet_compatibility.rb └── timesheet_plugin │ └── patches │ ├── project_patch.rb │ └── user_patch.rb ├── rails └── init.rb ├── test ├── functional │ └── timesheet_controller_test.rb ├── integration │ ├── configuration_test.rb │ ├── filter_allowed_projects_by_status_test.rb │ ├── session_storage_test.rb │ └── timesheet_menu_test.rb ├── test_helper.rb └── unit │ ├── lib │ └── timesheet_plugin │ │ └── patches │ │ └── user_patch_test.rb │ ├── sanity_test.rb │ └── timesheet_test.rb └── timesheet_plugin.gemspec /.gitignore: -------------------------------------------------------------------------------- 1 | *.tar.gz 2 | *.zip 3 | .DS_Store 4 | coverage 5 | coverage 6 | doc 7 | pkg 8 | rdoc 9 | -------------------------------------------------------------------------------- /COPYRIGHT.txt: -------------------------------------------------------------------------------- 1 | Redmine Timesheets is a Redmine plugin to create timesheets from all projects. 2 | Copyright (C) 2007 Eric Davis, Little Stream Software 3 | 4 | This program is free software; you can redistribute it and/or 5 | modify it under the terms of the GNU General Public License 6 | as published by the Free Software Foundation; either version 2 7 | of the License, or (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with this program; if not, write to the Free Software 16 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 17 | -------------------------------------------------------------------------------- /CREDITS.txt: -------------------------------------------------------------------------------- 1 | Thanks go to the following people for patches and contributions: 2 | 3 | Maintainers 4 | 5 | * Eric Davis 6 | 7 | Contributors 8 | 9 | * Morten Krogh Andersen 10 | * Peter Chester of Shane and Peter, Inc - Feature sponsorship 11 | * Anton Dollmaier 12 | * Mischa The Evil 13 | * Michele Franzin 14 | * Fabrice HELMBACHER 15 | * Sergej Jegorov 16 | * Gergő Jónás 17 | * Vladimir Oleynik 18 | * Shane Pearlman of Shane and Peter, Inc - Feature sponsorship 19 | * Michael Pirogov 20 | * Krzysztof Podejma 21 | * Pau Garcia i Quiles 22 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | group :test do 2 | gem 'webrat' 3 | end 4 | -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | = Redmine Timesheet plugin 2 | 3 | A plugin to show and filter timelogs across all projects in Redmine. 4 | 5 | == Features 6 | 7 | * Filtering and sum of timelogs by: 8 | * Date range 9 | * Projects 10 | * Activities 11 | * Users 12 | * Grouping of timelogs by: 13 | * Project 14 | * Issue 15 | * User 16 | * Access control based on the user's Projects and Roles 17 | * "Project Timesheet" permission to allow a user to see all timelogs on a project 18 | * Permalinks to reports 19 | * Plugin hook support for changing the behavior of the plugin 20 | * User configurable precision for hours 21 | * CSV exports 22 | 23 | == Getting the plugin 24 | 25 | A copy of the released version can be downloaded from {Little Stream Software}[https://projects.littlestreamsoftware.com/projects/redmine-timesheet/files]. A development copy can be gotton from {GitHub}[http://github.com/edavis10/redmine-timesheet-plugin/tree/master] 26 | 27 | == Installation and Setup 28 | 29 | 1. Follow the Redmine plugin installation steps at: http://www.redmine.org/wiki/redmine/Plugins Make sure the plugin is installed to +vendor/plugins/timesheet_plugin+ 30 | 2. Restart your Redmine web servers (e.g. mongrel, thin, mod_rails) 31 | 3. Login and click the Timesheet Link in the top left menu 32 | 33 | == Upgrade 34 | 35 | === Zip or tar files 36 | 37 | 1. Download the latest file as described in Getting the plugin 38 | 2. Extract the file to your Redmine into vendor/plugins 39 | 3. Restart your Redmine 40 | 41 | === Git 42 | 43 | 1. Open a shell to your Redmine's vendor/plugins/timesheet_plugin folder 44 | 2. Update your Git copy with `git pull` 45 | 3. Restart your Redmine 46 | 47 | == License 48 | 49 | This plugin is licensed under the GNU GPL v2. See LICENSE.txt and GPL.txt for details. 50 | 51 | == Project help 52 | 53 | If you need help you can contact the maintainer at his email address (See CREDITS.txt) or create an issue in the Bug Tracker. 54 | 55 | === Bug tracker 56 | 57 | If you would like to report a bug or request a new feature the bug tracker is located at: https://projects.littlestreamsoftware.com/projects/redmine-timesheet 58 | -------------------------------------------------------------------------------- /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 = 'timesheet_plugin' 8 | plugin.default_task = [:test] 9 | plugin.tasks = [:doc, :release, :clean, :stats, :test, :db] 10 | # TODO: gem not getting this automaticly 11 | plugin.redmine_root = File.expand_path(File.dirname(__FILE__) + '/../../../') 12 | end 13 | 14 | begin 15 | require 'jeweler' 16 | Jeweler::Tasks.new do |s| 17 | s.name = "timesheet_plugin" 18 | s.summary = "A Timesheet plugin for Redmine to show timelogs for all projects" 19 | s.email = "edavis@littlestreamsoftware.com" 20 | s.homepage = "https://projects.littlestreamsoftware.com/projects/redmine-timesheet" 21 | s.description = "A plugin to show and filter timelogs across all projects in Redmine." 22 | s.authors = ["Eric Davis"] 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 | rescue LoadError 32 | puts "Jeweler, or one of its dependencies, is not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com" 33 | end 34 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.6.0 2 | -------------------------------------------------------------------------------- /app/controllers/timesheet_controller.rb: -------------------------------------------------------------------------------- 1 | class TimesheetController < ApplicationController 2 | unloadable 3 | 4 | layout 'base' 5 | before_filter :get_list_size 6 | before_filter :get_precision 7 | before_filter :get_activities 8 | 9 | helper :sort 10 | include SortHelper 11 | helper :issues 12 | include ApplicationHelper 13 | helper :timelog 14 | 15 | SessionKey = 'timesheet_filter' 16 | 17 | verify :method => :delete, :only => :reset, :render => {:nothing => true, :status => :method_not_allowed } 18 | 19 | def index 20 | load_filters_from_session 21 | unless @timesheet 22 | @timesheet ||= Timesheet.new 23 | end 24 | @timesheet.allowed_projects = allowed_projects 25 | 26 | if @timesheet.allowed_projects.empty? 27 | render :action => 'no_projects' 28 | return 29 | end 30 | end 31 | 32 | def report 33 | if params && params[:timesheet] 34 | @timesheet = Timesheet.new( params[:timesheet] ) 35 | else 36 | redirect_to :action => 'index' 37 | return 38 | end 39 | 40 | @timesheet.allowed_projects = allowed_projects 41 | 42 | if @timesheet.allowed_projects.empty? 43 | render :action => 'no_projects' 44 | return 45 | end 46 | 47 | if !params[:timesheet][:projects].blank? 48 | @timesheet.projects = @timesheet.allowed_projects.find_all { |project| 49 | params[:timesheet][:projects].include?(project.id.to_s) 50 | } 51 | else 52 | @timesheet.projects = @timesheet.allowed_projects 53 | end 54 | 55 | call_hook(:plugin_timesheet_controller_report_pre_fetch_time_entries, { :timesheet => @timesheet, :params => params }) 56 | 57 | save_filters_to_session(@timesheet) 58 | 59 | @timesheet.fetch_time_entries 60 | 61 | # Sums 62 | @total = { } 63 | unless @timesheet.sort == :issue 64 | @timesheet.time_entries.each do |project,logs| 65 | @total[project] = 0 66 | if logs[:logs] 67 | logs[:logs].each do |log| 68 | @total[project] += log.hours 69 | end 70 | end 71 | end 72 | else 73 | @timesheet.time_entries.each do |project, project_data| 74 | @total[project] = 0 75 | if project_data[:issues] 76 | project_data[:issues].each do |issue, issue_data| 77 | @total[project] += issue_data.collect(&:hours).sum 78 | end 79 | end 80 | end 81 | end 82 | 83 | @grand_total = @total.collect{|k,v| v}.inject{|sum,n| sum + n} 84 | 85 | respond_to do |format| 86 | format.html { render :action => 'details', :layout => false if request.xhr? } 87 | format.csv { send_data @timesheet.to_csv, :filename => 'timesheet.csv', :type => "text/csv" } 88 | end 89 | end 90 | 91 | def context_menu 92 | @time_entries = TimeEntry.find(:all, :conditions => ['id IN (?)', params[:ids]]) 93 | render :layout => false 94 | end 95 | 96 | def reset 97 | clear_filters_from_session 98 | redirect_to :action => 'index' 99 | end 100 | 101 | private 102 | def get_list_size 103 | @list_size = Setting.plugin_timesheet_plugin['list_size'].to_i 104 | end 105 | 106 | def get_precision 107 | precision = Setting.plugin_timesheet_plugin['precision'] 108 | 109 | if precision.blank? 110 | # Set precision to a high number 111 | @precision = 10 112 | else 113 | @precision = precision.to_i 114 | end 115 | end 116 | 117 | def get_activities 118 | @activities = TimeEntryActivity.all(:conditions => 'parent_id IS NULL') 119 | end 120 | 121 | def allowed_projects 122 | if User.current.admin? 123 | Project.timesheet_order_by_name 124 | elsif Setting.plugin_timesheet_plugin['project_status'] == 'all' 125 | Project.timesheet_order_by_name.timesheet_with_membership(User.current) 126 | else 127 | Project.timesheet_order_by_name.all(:conditions => Project.visible_by(User.current)) 128 | end 129 | end 130 | 131 | def clear_filters_from_session 132 | session[SessionKey] = nil 133 | end 134 | 135 | def load_filters_from_session 136 | if session[SessionKey] 137 | @timesheet = Timesheet.new(session[SessionKey]) 138 | # Default to free period 139 | @timesheet.period_type = Timesheet::ValidPeriodType[:free_period] 140 | end 141 | 142 | if session[SessionKey] && session[SessionKey]['projects'] 143 | @timesheet.projects = allowed_projects.find_all { |project| 144 | session[SessionKey]['projects'].include?(project.id.to_s) 145 | } 146 | end 147 | end 148 | 149 | def save_filters_to_session(timesheet) 150 | if params[:timesheet] 151 | # Check that the params will fit in the session before saving 152 | # prevents an ActionController::Session::CookieStore::CookieOverflow 153 | encoded = Base64.encode64(Marshal.dump(params[:timesheet])) 154 | if encoded.size < 2.kilobytes # Only use 2K of the cookie 155 | session[SessionKey] = params[:timesheet] 156 | end 157 | end 158 | 159 | if timesheet 160 | session[SessionKey] ||= {} 161 | session[SessionKey]['date_from'] = timesheet.date_from 162 | session[SessionKey]['date_to'] = timesheet.date_to 163 | end 164 | end 165 | end 166 | -------------------------------------------------------------------------------- /app/helpers/timesheet_helper.rb: -------------------------------------------------------------------------------- 1 | module TimesheetHelper 2 | def showing_users(users) 3 | l(:timesheet_showing_users) + users.collect(&:name).join(', ') 4 | end 5 | 6 | def permalink_to_timesheet(timesheet) 7 | link_to(l(:timesheet_permalink), 8 | :controller => 'timesheet', 9 | :action => 'report', 10 | :timesheet => timesheet.to_param) 11 | end 12 | 13 | def link_to_csv_export(timesheet) 14 | link_to('CSV', 15 | { 16 | :controller => 'timesheet', 17 | :action => 'report', 18 | :format => 'csv', 19 | :timesheet => timesheet.to_param 20 | }, 21 | :method => 'post', 22 | :class => 'icon icon-timesheet') 23 | end 24 | 25 | def toggle_issue_arrows(issue_id) 26 | js = "toggleTimeEntries('#{issue_id}'); return false;" 27 | 28 | return toggle_issue_arrow(issue_id, 'toggle-arrow-closed.gif', js, false) + 29 | toggle_issue_arrow(issue_id, 'toggle-arrow-open.gif', js, true) 30 | end 31 | 32 | def toggle_issue_arrow(issue_id, image, js, hide=false) 33 | style = "display:none;" if hide 34 | style ||= '' 35 | 36 | content_tag(:span, 37 | link_to_function(image_tag(image, :plugin => "timesheet_plugin"), js), 38 | :class => "toggle-" + issue_id.to_s, 39 | :style => style 40 | ) 41 | 42 | end 43 | 44 | def displayed_time_entries_for_issue(time_entries) 45 | time_entries.collect(&:hours).sum 46 | end 47 | 48 | def project_options(timesheet) 49 | available_projects = timesheet.allowed_projects 50 | selected_projects = timesheet.projects.collect(&:id) 51 | selected_projects = available_projects.collect(&:id) if selected_projects.blank? 52 | 53 | options_from_collection_for_select(available_projects, 54 | :id, 55 | :name, 56 | selected_projects) 57 | end 58 | 59 | def activity_options(timesheet, activities) 60 | options_from_collection_for_select(activities, :id, :name, timesheet.activities) 61 | end 62 | 63 | def user_options(timesheet) 64 | available_users = Timesheet.viewable_users.sort { |a,b| a.to_s.downcase <=> b.to_s.downcase } 65 | selected_users = timesheet.users 66 | 67 | options_from_collection_for_select(available_users, 68 | :id, 69 | :name, 70 | selected_users) 71 | 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /app/models/timesheet.rb: -------------------------------------------------------------------------------- 1 | class Timesheet 2 | attr_accessor :date_from, :date_to, :projects, :activities, :users, :allowed_projects, :period, :period_type 3 | 4 | # Time entries on the Timesheet in the form of: 5 | # project.name => {:logs => [time entries], :users => [users shown in logs] } 6 | # project.name => {:logs => [time entries], :users => [users shown in logs] } 7 | # project.name could be the parent project name also 8 | attr_accessor :time_entries 9 | 10 | # Array of TimeEntry ids to fetch 11 | attr_accessor :potential_time_entry_ids 12 | 13 | # Sort time entries by this field 14 | attr_accessor :sort 15 | ValidSortOptions = { 16 | :project => 'Project', 17 | :user => 'User', 18 | :issue => 'Issue' 19 | } 20 | 21 | ValidPeriodType = { 22 | :free_period => 0, 23 | :default => 1 24 | } 25 | 26 | def initialize(options = { }) 27 | self.projects = [ ] 28 | self.time_entries = options[:time_entries] || { } 29 | self.potential_time_entry_ids = options[:potential_time_entry_ids] || [ ] 30 | self.allowed_projects = options[:allowed_projects] || [ ] 31 | 32 | unless options[:activities].nil? 33 | self.activities = options[:activities].collect do |activity_id| 34 | # Include project-overridden activities 35 | activity = TimeEntryActivity.find(activity_id) 36 | project_activities = TimeEntryActivity.all(:conditions => ['parent_id IN (?)', activity.id]) if activity.parent_id.nil? 37 | project_activities ||= [] 38 | 39 | [activity.id.to_i] + project_activities.collect(&:id) 40 | end.flatten.uniq.compact 41 | else 42 | self.activities = TimeEntryActivity.all.collect { |a| a.id.to_i } 43 | end 44 | 45 | unless options[:users].nil? 46 | self.users = options[:users].collect { |u| u.to_i } 47 | else 48 | self.users = Timesheet.viewable_users.collect {|user| user.id.to_i } 49 | end 50 | 51 | if !options[:sort].nil? && options[:sort].respond_to?(:to_sym) && ValidSortOptions.keys.include?(options[:sort].to_sym) 52 | self.sort = options[:sort].to_sym 53 | else 54 | self.sort = :project 55 | end 56 | 57 | self.date_from = options[:date_from] || Date.today.to_s 58 | self.date_to = options[:date_to] || Date.today.to_s 59 | 60 | if options[:period_type] && ValidPeriodType.values.include?(options[:period_type].to_i) 61 | self.period_type = options[:period_type].to_i 62 | else 63 | self.period_type = ValidPeriodType[:free_period] 64 | end 65 | self.period = options[:period] || nil 66 | end 67 | 68 | # Gets all the time_entries for all the projects 69 | def fetch_time_entries 70 | self.time_entries = { } 71 | case self.sort 72 | when :project 73 | fetch_time_entries_by_project 74 | when :user 75 | fetch_time_entries_by_user 76 | when :issue 77 | fetch_time_entries_by_issue 78 | else 79 | fetch_time_entries_by_project 80 | end 81 | end 82 | 83 | def period=(period) 84 | return if self.period_type == Timesheet::ValidPeriodType[:free_period] 85 | # Stolen from the TimelogController 86 | case period.to_s 87 | when 'today' 88 | self.date_from = self.date_to = Date.today 89 | when 'yesterday' 90 | self.date_from = self.date_to = Date.today - 1 91 | when 'current_week' # Mon -> Sun 92 | self.date_from = Date.today - (Date.today.cwday - 1)%7 93 | self.date_to = self.date_from + 6 94 | when 'last_week' 95 | self.date_from = Date.today - 7 - (Date.today.cwday - 1)%7 96 | self.date_to = self.date_from + 6 97 | when '7_days' 98 | self.date_from = Date.today - 7 99 | self.date_to = Date.today 100 | when 'current_month' 101 | self.date_from = Date.civil(Date.today.year, Date.today.month, 1) 102 | self.date_to = (self.date_from >> 1) - 1 103 | when 'last_month' 104 | self.date_from = Date.civil(Date.today.year, Date.today.month, 1) << 1 105 | self.date_to = (self.date_from >> 1) - 1 106 | when '30_days' 107 | self.date_from = Date.today - 30 108 | self.date_to = Date.today 109 | when 'current_year' 110 | self.date_from = Date.civil(Date.today.year, 1, 1) 111 | self.date_to = Date.civil(Date.today.year, 12, 31) 112 | when 'all' 113 | self.date_from = self.date_to = nil 114 | end 115 | self 116 | end 117 | 118 | def to_param 119 | { 120 | :projects => projects.collect(&:id), 121 | :date_from => date_from, 122 | :date_to => date_to, 123 | :activities => activities, 124 | :users => users, 125 | :sort => sort 126 | } 127 | end 128 | 129 | def to_csv 130 | returning '' do |out| 131 | FCSV.generate out do |csv| 132 | csv << csv_header 133 | 134 | # Write the CSV based on the group/sort 135 | case sort 136 | when :user, :project 137 | time_entries.sort.each do |entryname, entry| 138 | entry[:logs].each do |e| 139 | csv << time_entry_to_csv(e) 140 | end 141 | end 142 | when :issue 143 | time_entries.sort.each do |project, entries| 144 | entries[:issues].sort {|a,b| a[0].id <=> b[0].id}.each do |issue, time_entries| 145 | time_entries.each do |e| 146 | csv << time_entry_to_csv(e) 147 | end 148 | end 149 | end 150 | end 151 | end 152 | end 153 | end 154 | 155 | def self.viewable_users 156 | if Setting['plugin_timesheet_plugin'].present? && Setting['plugin_timesheet_plugin']['user_status'] == 'all' 157 | user_scope = User.all 158 | else 159 | user_scope = User.active 160 | end 161 | 162 | user_scope.select {|user| 163 | user.allowed_to?(:log_time, nil, :global => true) 164 | } 165 | end 166 | 167 | protected 168 | 169 | def csv_header 170 | csv_data = [ 171 | '#', 172 | l(:label_date), 173 | l(:label_member), 174 | l(:label_activity), 175 | l(:label_project), 176 | l(:label_issue), 177 | "#{l(:label_issue)} #{l(:field_subject)}", 178 | l(:field_comments), 179 | l(:field_hours) 180 | ] 181 | Redmine::Hook.call_hook(:plugin_timesheet_model_timesheet_csv_header, { :timesheet => self, :csv_data => csv_data}) 182 | return csv_data 183 | end 184 | 185 | def time_entry_to_csv(time_entry) 186 | csv_data = [ 187 | time_entry.id, 188 | time_entry.spent_on, 189 | time_entry.user.name, 190 | time_entry.activity.name, 191 | time_entry.project.name, 192 | ("#{time_entry.issue.tracker.name} ##{time_entry.issue.id}" if time_entry.issue), 193 | (time_entry.issue.subject if time_entry.issue), 194 | time_entry.comments, 195 | time_entry.hours 196 | ] 197 | Redmine::Hook.call_hook(:plugin_timesheet_model_timesheet_time_entry_to_csv, { :timesheet => self, :time_entry => time_entry, :csv_data => csv_data}) 198 | return csv_data 199 | end 200 | 201 | # Array of users to find 202 | # String of extra conditions to add onto the query (AND) 203 | def conditions(users, extra_conditions=nil) 204 | if self.potential_time_entry_ids.empty? 205 | if self.date_from.present? && self.date_to.present? 206 | conditions = ["spent_on >= (:from) AND spent_on <= (:to) AND #{TimeEntry.table_name}.project_id IN (:projects) AND user_id IN (:users) AND activity_id IN (:activities)", 207 | { 208 | :from => self.date_from, 209 | :to => self.date_to, 210 | :projects => self.projects, 211 | :activities => self.activities, 212 | :users => users 213 | }] 214 | else # All time 215 | conditions = ["#{TimeEntry.table_name}.project_id IN (:projects) AND user_id IN (:users) AND activity_id IN (:activities)", 216 | { 217 | :projects => self.projects, 218 | :activities => self.activities, 219 | :users => users 220 | }] 221 | end 222 | else 223 | conditions = ["user_id IN (:users) AND #{TimeEntry.table_name}.id IN (:potential_time_entries)", 224 | { 225 | :users => users, 226 | :potential_time_entries => self.potential_time_entry_ids 227 | }] 228 | end 229 | 230 | if extra_conditions 231 | conditions[0] = conditions.first + ' AND ' + extra_conditions 232 | end 233 | 234 | Redmine::Hook.call_hook(:plugin_timesheet_model_timesheet_conditions, { :timesheet => self, :conditions => conditions}) 235 | return conditions 236 | end 237 | 238 | def includes 239 | includes = [:activity, :user, :project, {:issue => [:tracker, :assigned_to, :priority]}] 240 | Redmine::Hook.call_hook(:plugin_timesheet_model_timesheet_includes, { :timesheet => self, :includes => includes}) 241 | return includes 242 | end 243 | 244 | private 245 | 246 | 247 | def time_entries_for_all_users(project) 248 | return project.time_entries.find(:all, 249 | :conditions => self.conditions(self.users), 250 | :include => self.includes, 251 | :order => "spent_on ASC") 252 | end 253 | 254 | def time_entries_for_current_user(project) 255 | return project.time_entries.find(:all, 256 | :conditions => self.conditions(User.current.id), 257 | :include => self.includes, 258 | :include => [:activity, :user, {:issue => [:tracker, :assigned_to, :priority]}], 259 | :order => "spent_on ASC") 260 | end 261 | 262 | def issue_time_entries_for_all_users(issue) 263 | return issue.time_entries.find(:all, 264 | :conditions => self.conditions(self.users), 265 | :include => self.includes, 266 | :include => [:activity, :user], 267 | :order => "spent_on ASC") 268 | end 269 | 270 | def issue_time_entries_for_current_user(issue) 271 | return issue.time_entries.find(:all, 272 | :conditions => self.conditions(User.current.id), 273 | :include => self.includes, 274 | :include => [:activity, :user], 275 | :order => "spent_on ASC") 276 | end 277 | 278 | def time_entries_for_user(user, options={}) 279 | extra_conditions = options.delete(:conditions) 280 | 281 | return TimeEntry.find(:all, 282 | :conditions => self.conditions([user], extra_conditions), 283 | :include => self.includes, 284 | :order => "spent_on ASC" 285 | ) 286 | end 287 | 288 | def fetch_time_entries_by_project 289 | self.projects.each do |project| 290 | logs = [] 291 | users = [] 292 | if User.current.admin? 293 | # Administrators can see all time entries 294 | logs = time_entries_for_all_users(project) 295 | users = logs.collect(&:user).uniq.sort 296 | elsif User.current.allowed_to_on_single_potentially_archived_project?(:see_project_timesheets, project) 297 | # Users with the Role and correct permission can see all time entries 298 | logs = time_entries_for_all_users(project) 299 | users = logs.collect(&:user).uniq.sort 300 | elsif User.current.allowed_to_on_single_potentially_archived_project?(:view_time_entries, project) 301 | # Users with permission to see their time entries 302 | logs = time_entries_for_current_user(project) 303 | users = logs.collect(&:user).uniq.sort 304 | else 305 | # Rest can see nothing 306 | end 307 | 308 | # Append the parent project name 309 | if project.parent.nil? 310 | unless logs.empty? 311 | self.time_entries[project.name] = { :logs => logs, :users => users } 312 | end 313 | else 314 | unless logs.empty? 315 | self.time_entries[project.parent.name + ' / ' + project.name] = { :logs => logs, :users => users } 316 | end 317 | end 318 | end 319 | end 320 | 321 | def fetch_time_entries_by_user 322 | self.users.each do |user_id| 323 | logs = [] 324 | if User.current.admin? 325 | # Administrators can see all time entries 326 | logs = time_entries_for_user(user_id) 327 | elsif User.current.id == user_id 328 | # Users can see their own their time entries 329 | logs = time_entries_for_user(user_id) 330 | elsif User.current.allowed_to_on_single_potentially_archived_project?(:see_project_timesheets, nil, :global => true) 331 | # User can see project timesheets in at least once place, so 332 | # fetch the user timelogs for those projects 333 | logs = time_entries_for_user(user_id, :conditions => Project.allowed_to_condition(User.current, :see_project_timesheets)) 334 | else 335 | # Rest can see nothing 336 | end 337 | 338 | unless logs.empty? 339 | user = User.find_by_id(user_id) 340 | self.time_entries[user.name] = { :logs => logs } unless user.nil? 341 | end 342 | end 343 | end 344 | 345 | # project => { :users => [users shown in logs], 346 | # :issues => 347 | # { issue => {:logs => [time entries], 348 | # issue => {:logs => [time entries], 349 | # issue => {:logs => [time entries]} 350 | # 351 | def fetch_time_entries_by_issue 352 | self.projects.each do |project| 353 | logs = [] 354 | users = [] 355 | project.issues.each do |issue| 356 | if User.current.admin? 357 | # Administrators can see all time entries 358 | logs << issue_time_entries_for_all_users(issue) 359 | elsif User.current.allowed_to_on_single_potentially_archived_project?(:see_project_timesheets, project) 360 | # Users with the Role and correct permission can see all time entries 361 | logs << issue_time_entries_for_all_users(issue) 362 | elsif User.current.allowed_to_on_single_potentially_archived_project?(:view_time_entries, project) 363 | # Users with permission to see their time entries 364 | logs << issue_time_entries_for_current_user(issue) 365 | else 366 | # Rest can see nothing 367 | end 368 | end 369 | 370 | logs.flatten! if logs.respond_to?(:flatten!) 371 | logs.uniq! if logs.respond_to?(:uniq!) 372 | 373 | unless logs.empty? 374 | users << logs.collect(&:user).uniq.sort 375 | 376 | 377 | issues = logs.collect(&:issue).uniq 378 | issue_logs = { } 379 | issues.each do |issue| 380 | issue_logs[issue] = logs.find_all {|time_log| time_log.issue == issue } # TimeEntry is for this issue 381 | end 382 | 383 | # TODO: TE without an issue 384 | 385 | self.time_entries[project] = { :issues => issue_logs, :users => users} 386 | end 387 | end 388 | end 389 | 390 | 391 | def l(*args) 392 | I18n.t(*args) 393 | end 394 | end 395 | -------------------------------------------------------------------------------- /app/views/settings/_timesheet_settings.rhtml: -------------------------------------------------------------------------------- 1 |

<%= text_field_tag 'settings[list_size]', @settings['list_size'] %>

2 | 3 |

<%= text_field_tag 'settings[precision]', @settings['precision'] %>

4 | 5 |

6 | 7 | <%= select_tag('settings[project_status]', 8 | options_for_select({ 9 | l(:text_active_projects) => 'active', 10 | l(:text_all_projects) => 'all'}, @settings['project_status'])) %> 11 |

12 | 13 |

14 | 15 | <%= select_tag('settings[user_status]', 16 | options_for_select({ 17 | l(:text_active_users) => 'active', 18 | l(:text_all_users) => 'all'}, @settings['user_status'])) %> 19 |

20 | -------------------------------------------------------------------------------- /app/views/timesheet/_by_issue.rhtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 10 | 11 | 15 | 16 | <%= render :partial => "issue_time_entries", :collection => entry[:issues] %> 17 | 18 |
4 | <%= link_to image_tag('toggle_check.png'), {}, :onclick => 'toggleIssuesSelection(Element.up(this, "table")); return false;', 5 | :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}", :class => 'toggle-all' %> 6 |  <%= l(:label_date) %><%= l(:label_member) %><%= l(:label_issue) %> / <%= l(:field_comments) %><%= l(:field_hours) %> 12 | <%= Redmine::Hook.call_hook(:plugin_timesheet_views_timesheet_group_header, { }) %> 13 | 14 |
19 |
20 | 36 | -------------------------------------------------------------------------------- /app/views/timesheet/_form.rhtml: -------------------------------------------------------------------------------- 1 |
2 |
3 | <% form_for :timesheet, :url =>{:action => 'report'} do |f| %> 4 | 5 |

6 |
7 | <%= radio_button_tag 'timesheet[period_type]', '1', @timesheet.period_type == Timesheet::ValidPeriodType[:default] %> 8 | <%= select_tag 'timesheet[period]', options_for_period_select((params[:timesheet].nil? ? nil : params[:timesheet][:period])), 9 | :onfocus => '$("timesheet_period_type_1").checked = true;' %> 10 |

11 | 12 | <%= radio_button_tag 'timesheet[period_type]', '2', @timesheet.period_type == Timesheet::ValidPeriodType[:free_period] %> 13 | 14 |
15 | <%= f.text_field "date_from", :size => 10 %><%= calendar_for('timesheet_date_from') %>
16 | 17 |
18 | <%= f.text_field "date_to", :size => 10 %><%= calendar_for('timesheet_date_to') %>

19 |
20 |

21 | 22 |

23 |
24 | <%= select_tag("timesheet[sort]", options_for_select(Timesheet::ValidSortOptions.invert, @timesheet.sort)) %> 25 | 26 |

27 | 28 |

29 |
30 | <%= select_tag 'timesheet[projects][]', project_options(@timesheet), { :multiple => true, :size => @list_size} %> 31 |

32 | 33 | 34 |

35 |
36 | <%= select_tag 'timesheet[activities][]', activity_options(@timesheet, @activities), { :multiple => true, :size => @list_size} %> 37 |

38 | 39 |

40 |
41 | <%= select_tag 'timesheet[users][]', user_options(@timesheet), { :multiple => true, :size => @list_size} %> 42 |

43 | 44 | <%# TODO: Typo on hook %> 45 | <%= call_hook(:plugin_timesheet_view_timesheet_form, { :timesheet => @timesheet, :params => params, :list_size => @list_size }) %> 46 | <%= call_hook(:plugin_timesheet_views_timesheet_form, { :timesheet => @timesheet, :params => params, :list_size => @list_size }) %> 47 | 48 |
49 | <%= submit_tag l(:button_apply),:class => 'button-small' -%> 50 | 51 | <% end %> 52 | <%= button_to(l(:button_reset), {:controller => 'timesheet', :action => 'reset'}, :method => 'delete') %> 53 |
54 |
55 | -------------------------------------------------------------------------------- /app/views/timesheet/_issue_time_entries.rhtml: -------------------------------------------------------------------------------- 1 | <% issue = issue_time_entries[0] %> 2 | <% time_entries = issue_time_entries[1] %> 3 | <% unless issue.nil? %> 4 | "> 5 | 6 | <%= link_to image_tag('toggle_check.png'), {}, :onclick => "toggleTimeEntriesSelection('#{issue.id}'); return false;", 7 | :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}", :class => 'toggle-all' %> 8 | 9 | 10 | <%= toggle_issue_arrows(issue.id) %> 11 | 12 | 13 | <%= l(:field_assigned_to) %>:
<%= issue.assigned_to.to_s %> 14 | 15 |
16 | <%= link_to_issue issue %> 17 | 18 | <%= render_issue_tooltip issue %> 19 | 20 |
21 | 22 | <%= number_with_precision(displayed_time_entries_for_issue(time_entries), @precision) %> 23 | <%= Redmine::Hook.call_hook(:plugin_timesheet_views_timesheet_time_entry_sum, {:issue => issue, :time_entries => time_entries, :precision => @precision }) %> 24 | 25 | 26 | <% time_entries.each do |time_entry| %> 27 | <%# TODO: Typo on hook %> 28 | issue-time-entry-<%= issue.id -%> hascontextmenu <%= call_hook(:plugin_timesheet_view_timesheets_time_entry_row_class, {:time_entry => time_entry }) %> <%= call_hook(:plugin_timesheet_views_timesheets_time_entry_row_class, {:time_entry => time_entry }) %>" style="display:none;"> 29 | 30 | <%= check_box_tag 'ids[]', time_entry.id, false, { :class => 'checkbox' } %> 31 | 32 | 33 | <%= format_date(time_entry.spent_on) %> 34 | <%= time_entry.user.name %> 35 | <%= h time_entry.comments %> 36 | <%= number_with_precision(time_entry.hours, @precision) %> 37 | <%= Redmine::Hook.call_hook(:plugin_timesheet_views_timesheet_time_entry, {:time_entry => time_entry, :precision => @precision }) %> 38 | 39 | <% if time_entry.editable_by?(User.current) -%> 40 | <%= link_to image_tag('edit.png'), {:controller => 'timelog', :action => 'edit', :id => time_entry}, 41 | :title => l(:button_edit) %> 42 | <%= link_to image_tag('delete.png'), {:controller => 'timelog', :action => 'destroy', :id => time_entry}, 43 | :confirm => l(:text_are_you_sure), 44 | :method => :post, 45 | :title => l(:button_delete) %> 46 | <% end -%> 47 | 48 | 49 | <% end %> 50 | <% end %> 51 | -------------------------------------------------------------------------------- /app/views/timesheet/_time_entry.rhtml: -------------------------------------------------------------------------------- 1 | <%# TODO: Typo on hook %> 2 | hascontextmenu <%= call_hook(:plugin_timesheet_view_timesheets_time_entry_row_class, {:time_entry => time_entry }) %> <%= call_hook(:plugin_timesheet_views_timesheets_time_entry_row_class, {:time_entry => time_entry }) %>"> 3 | <%= check_box_tag 'ids[]', time_entry.id, false, { :class => 'checkbox' } %> 4 | <%= format_date(time_entry.spent_on) %> 5 | <%= time_entry.user.name %> 6 | <%= time_entry.activity.name %> 7 | <%= time_entry.project.name %> 8 | 9 | <% if time_entry.issue %> 10 |
11 | <%= link_to_issue time_entry.issue %> 12 | 13 | <%= render_issue_tooltip time_entry.issue %> 14 | 15 |
16 | <% end %> 17 | 18 | <%=h time_entry.comments %> 19 | <%= number_with_precision(time_entry.hours, @precision) %> 20 | <%= Redmine::Hook.call_hook(:plugin_timesheet_views_timesheet_time_entry, {:time_entry => time_entry, :precision => @precision }) %> 21 | 22 | <% if time_entry.editable_by?(User.current) -%> 23 | <%= link_to image_tag('edit.png'), {:controller => 'timelog', :action => 'edit', :id => time_entry}, 24 | :title => l(:button_edit) %> 25 | <%= link_to image_tag('delete.png'), {:controller => 'timelog', :action => 'destroy', :id => time_entry}, 26 | :confirm => l(:text_are_you_sure), 27 | :method => :delete, 28 | :title => l(:button_delete) %> 29 | <% end -%> 30 | 31 | 32 | -------------------------------------------------------------------------------- /app/views/timesheet/_timesheet_group.rhtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | <%= Redmine::Hook.call_hook(:plugin_timesheet_views_timesheet_group_header, { }) %> 15 | 16 | 17 | 18 | <%= render :partial => "time_entry", :collection => entry[:logs] %> 19 | 20 |
4 | <%= link_to image_tag('toggle_check.png'), {}, :onclick => 'toggleIssuesSelection(Element.up(this, "table")); return false;', 5 | :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}", :class => 'toggle-all' %> 6 | <%= l(:label_date) %><%= l(:label_member) %><%= l(:label_activity) %><%= l(:label_project) %><%= l(:label_issue) %><%= l(:field_comments) %><%= l(:field_hours) %>
21 |
22 | -------------------------------------------------------------------------------- /app/views/timesheet/context_menu.html.erb: -------------------------------------------------------------------------------- 1 | <%# TODO: Typo on hook %> 2 | <% entries = call_hook(:plugin_timesheet_view_timesheets_context_menu, { :time_entries => @time_entries }) %> 3 | <% entries += call_hook(:plugin_timesheet_views_timesheets_context_menu, { :time_entries => @time_entries }) %> 4 | <%= content_tag(:ul, entries) unless entries.empty? %> 5 | -------------------------------------------------------------------------------- /app/views/timesheet/index.rhtml: -------------------------------------------------------------------------------- 1 |

<%= l(:timesheet_title)%>

2 | 3 | <%= render :partial => 'form' %> 4 | 5 | <% content_for(:header_tags) do %> 6 | <%= stylesheet_link_tag "timesheet.css", :plugin => "timesheet_plugin", :media => 'all' %> 7 | <%= javascript_include_tag 'timesheet.js', :plugin => 'timesheet_plugin' %> 8 | <% end %> 9 | -------------------------------------------------------------------------------- /app/views/timesheet/no_projects.rhtml: -------------------------------------------------------------------------------- 1 |

<%= l(:timesheet_title)%>

2 | 3 |

4 | <%= l(:label_no_data) %> 5 |

6 | -------------------------------------------------------------------------------- /app/views/timesheet/report.rhtml: -------------------------------------------------------------------------------- 1 |
2 | <%= link_to_csv_export(@timesheet) %> 3 | <%= permalink_to_timesheet(@timesheet) %> 4 |
5 | 6 |

<%= l(:timesheet_title)%>

7 | 8 | <%= render :partial => 'form' %> 9 | 10 | <%= call_hook(:plugin_timesheet_views_timesheets_report_before_time_entries, { :timesheet => @timesheet }) %> 11 | 12 | <% form_tag({}, { :id => 'time_entries'}) do -%> 13 | <% if @timesheet.time_entries.length > 0 %> 14 |

<%= l(:label_spent_time) %> (<%= h(number_with_precision(@grand_total, @precision)) -%> <%= h(l(:field_hours)) -%>)

15 | 16 | <% @timesheet.time_entries.each do |entryname,entry| 17 | case @timesheet.sort 18 | when :user %> 19 |

<%= h entryname -%> (<%= h number_with_precision(@total[entryname], @precision) %> <%= h(l(:field_hours)) -%>)

20 | <%= render :partial => 'timesheet_group', :locals => {:entry => entry, :name => entryname, :total => @total[entryname] } %> 21 | <% when :issue %> 22 |

<%= h entryname -%> (<%= h number_with_precision(@total[entryname], @precision) %> <%= h(l(:field_hours)) -%>)

23 | <%= render :partial => 'by_issue', :locals => {:entry => entry, :name => entryname, :total => 0 } %> 24 | <% else %> 25 | <%# Default to :project %> 26 |

<%= h entryname -%> (<%= h number_with_precision(@total[entryname], @precision) %> <%= h(l(:field_hours)) -%>) <%= showing_users(entry[:users]) %>

27 | <%= render :partial => 'timesheet_group', :locals => {:entry => entry, :name => entryname, :total => @total[entryname] } %> 28 | 29 | <% end 30 | end # each 31 | end # length 32 | end # form_tag 33 | -%> 34 | 35 | <% content_for(:header_tags) do %> 36 | <%= javascript_include_tag 'context_menu' %> 37 | <%= stylesheet_link_tag 'context_menu' %> 38 | <%= stylesheet_link_tag "timesheet.css", :plugin => "timesheet_plugin", :media => 'all' %> 39 | <%# TODO: Typo on hook %> 40 | <%= call_hook(:plugin_timesheet_view_timesheets_report_header_tags, { :timesheet => @timesheet }) %> 41 | <%= call_hook(:plugin_timesheet_views_timesheets_report_header_tags, { :timesheet => @timesheet }) %> 42 | <% end %> 43 | 44 | 45 | <%= javascript_tag "new ContextMenu('#{url_for(:controller => 'timesheet', :action => 'context_menu')}')" %> 46 | 47 | <%# TODO: Typo on hook %> 48 | <%= call_hook(:plugin_timesheet_view_timesheets_report_bottom, { :timesheet => @timesheet }) %> 49 | <%= call_hook(:plugin_timesheet_views_timesheets_report_bottom, { :timesheet => @timesheet }) %> 50 | -------------------------------------------------------------------------------- /app/views/timesheet/timelog.rhtml: -------------------------------------------------------------------------------- 1 |

<%= l(:label_spent_time) %>

2 | 3 | <% @entries.each do |entryname,entry| -%> 4 | <%= render :partial => 'project_timesheet', :locals => {:entry => entry, :name => entryname} %> 5 | <% end -%> 6 | -------------------------------------------------------------------------------- /assets/images/csv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edavis10/redmine-timesheet-plugin/e0c362447d90a009853e2e1c659dc79d7db01995/assets/images/csv.png -------------------------------------------------------------------------------- /assets/images/toggle-arrow-closed.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edavis10/redmine-timesheet-plugin/e0c362447d90a009853e2e1c659dc79d7db01995/assets/images/toggle-arrow-closed.gif -------------------------------------------------------------------------------- /assets/images/toggle-arrow-open.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edavis10/redmine-timesheet-plugin/e0c362447d90a009853e2e1c659dc79d7db01995/assets/images/toggle-arrow-open.gif -------------------------------------------------------------------------------- /assets/javascripts/timesheet.js: -------------------------------------------------------------------------------- 1 | function targetField(label_element) { 2 | return $(label_element.attributes.for.value); 3 | } 4 | 5 | function selectAllOptions(element) { 6 | for (var i = 0; i < element.options.length; i++) { 7 | element.options[i].selected = true; 8 | } 9 | } 10 | 11 | Event.observe(window, 'load', 12 | function() { 13 | $$('label.select_all').each(function(element) { 14 | Event.observe(element, 'click', function (e) { selectAllOptions(targetField(this)); }); 15 | }); 16 | } 17 | ); 18 | -------------------------------------------------------------------------------- /assets/stylesheets/timesheet.css: -------------------------------------------------------------------------------- 1 | div#timesheet-form p { padding:0px 10px; float:left; } 2 | .icon-timesheet { background-image: url(../images/csv.png); } 3 | 4 | #date-options { margin-left: 10px; } 5 | #date-options input[type='radio'] { margin-left: -20px; } 6 | #timesheet-form .button-to div {display:inline; } 7 | -------------------------------------------------------------------------------- /autotest/discover.rb: -------------------------------------------------------------------------------- 1 | Autotest.add_discovery do 2 | "rails" 3 | end 4 | -------------------------------------------------------------------------------- /config/locales/ca.yml: -------------------------------------------------------------------------------- 1 | ca: 2 | timesheet_title: Full de temps 3 | timesheet_date_from_label: De 4 | timesheet_date_to_label: A 5 | timesheet_project_label: Projecte 6 | timesheet_activities_label: Activitat 7 | timesheet_users_label: Usuaris 8 | timesheet_showing_users: 'mostrant temps per a: ' 9 | timesheet_permalink: "(Enllaç permanent a aquest full de temps)" 10 | -------------------------------------------------------------------------------- /config/locales/cs.yml: -------------------------------------------------------------------------------- 1 | cs: 2 | timesheet_title: Souhrnná tabulka stráveného času 3 | timesheet_date_from_label: Od 4 | timesheet_date_to_label: do 5 | timesheet_project_label: Projekt 6 | timesheet_activities_label: Aktivita 7 | timesheet_users_label: Uživatelé 8 | timesheet_showing_users: 'zobrazuji časy pro: ' 9 | timesheet_permalink: '(Permalink k tomuto přehledu práce)' 10 | timesheet_group_by: Seskupit dle 11 | -------------------------------------------------------------------------------- /config/locales/da.yml: -------------------------------------------------------------------------------- 1 | da: 2 | timesheet_title: Timeseddel 3 | timesheet_date_from_label: Fra 4 | timesheet_date_to_label: Til 5 | timesheet_project_label: Projekt 6 | timesheet_activities_label: Aktivitet 7 | timesheet_users_label: Brugere 8 | timesheet_showing_users: 'viser tidsregistreringer for: ' 9 | timesheet_permalink: '(Permanent link til denne timeseddel)' 10 | timesheet_group_by: Gruppér efter 11 | -------------------------------------------------------------------------------- /config/locales/de.yml: -------------------------------------------------------------------------------- 1 | de: 2 | timesheet_title: Stundenzettel 3 | timesheet_date_from_label: Von 4 | timesheet_date_to_label: Bis 5 | timesheet_project_label: Projekt 6 | timesheet_activities_label: Aktivität 7 | timesheet_users_label: Benutzer 8 | timesheet_showing_users: 'zeige Zeit für: ' 9 | timesheet_permalink: '(Permalink zu diesem Stundenzettel)' 10 | timesheet_group_by: Gruppiere nach 11 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | timesheet_title: Timesheet 3 | timesheet_date_from_label: From 4 | timesheet_date_to_label: To 5 | timesheet_project_label: Project 6 | timesheet_activities_label: Activity 7 | timesheet_users_label: Users 8 | timesheet_showing_users: 'showing time for: ' 9 | timesheet_permalink: '(Permalink to this timesheet)' 10 | timesheet_group_by: Group by 11 | label_show_project_status: "Projects with the status:" 12 | text_active_projects: "Active" 13 | text_all_projects: "All (active and archived)" 14 | label_user_status: "Show users with the status of" 15 | text_active_users: "Active only" 16 | text_all_users: "All" 17 | 18 | -------------------------------------------------------------------------------- /config/locales/es.yml: -------------------------------------------------------------------------------- 1 | es: 2 | timesheet_title: Hoja de tiempos 3 | timesheet_date_from_label: De 4 | timesheet_date_to_label: A 5 | timesheet_project_label: Proyecto 6 | timesheet_activities_label: Actividad 7 | timesheet_users_label: Usuarios 8 | timesheet_showing_users: 'mostrando tiempos para: ' 9 | timesheet_permalink: '(Permalink a esta hoja de tiempos)' 10 | -------------------------------------------------------------------------------- /config/locales/fr.yml: -------------------------------------------------------------------------------- 1 | fr: 2 | timesheet_title: Feuille de temps 3 | timesheet_date_from_label: De 4 | timesheet_date_to_label: A 5 | timesheet_project_label: Projet 6 | timesheet_activities_label: Activite 7 | timesheet_users_label: Utilisateurs 8 | timesheet_showing_users: 'showing time for: ' 9 | timesheet_permalink: '(Permalink to this timesheet)' 10 | timesheet_group_by: Group by 11 | -------------------------------------------------------------------------------- /config/locales/hu.yml: -------------------------------------------------------------------------------- 1 | hu: 2 | timesheet_title: Időlap 3 | timesheet_date_from_label: Kezdő dátum 4 | timesheet_date_to_label: Vég dátum 5 | timesheet_project_label: Projekt 6 | timesheet_activities_label: Aktivitás 7 | timesheet_users_label: Felhasználók 8 | timesheet_showing_users: 'mutatás ettől a felhasználótól: ' 9 | timesheet_permalink: '(Permalink erre az időlapra)' 10 | timesheet_group_by: Csoportosítás 11 | -------------------------------------------------------------------------------- /config/locales/hy.yml: -------------------------------------------------------------------------------- 1 | "hy": 2 | timesheet_title: Ծախսված ժամանակի աղյուսակ 3 | timesheet_date_from_label: Սկսած 4 | timesheet_date_to_label: մինչև 5 | timesheet_project_label: Նախագիծ 6 | timesheet_activities_label: Ակտիվությունը 7 | timesheet_users_label: Օգտագործողներ 8 | timesheet_permalink: '(Մշտական հղում դեպի այս աղյուսակը)' 9 | timesheet_group_by: Դասավորել ըստ 10 | -------------------------------------------------------------------------------- /config/locales/it.yml: -------------------------------------------------------------------------------- 1 | it: 2 | timesheet_title: Consuntivo tempi 3 | timesheet_date_from_label: Da 4 | timesheet_date_to_label: A 5 | timesheet_project_label: Progetto 6 | timesheet_activities_label: Attività 7 | timesheet_users_label: Utenti 8 | -------------------------------------------------------------------------------- /config/locales/ja.yml: -------------------------------------------------------------------------------- 1 | ja: 2 | timesheet_title: 記録時間表 3 | timesheet_date_from_label: 期間 4 | timesheet_date_to_label: から 5 | timesheet_project_label: プロジェクト 6 | timesheet_activities_label: 活動 7 | timesheet_users_label: ユーザ 8 | timesheet_showing_users: 'ユーザ: ' 9 | timesheet_permalink: '(この記録時間表へ直接リンク)' 10 | timesheet_group_by: グループ化 11 | -------------------------------------------------------------------------------- /config/locales/lt.yml: -------------------------------------------------------------------------------- 1 | lt: 2 | timesheet_title: Sunaudoto laiko suvestinė lentelė 3 | timesheet_date_from_label: Pradedant nuo 4 | timesheet_date_to_label: iki 5 | timesheet_project_label: Projektas 6 | timesheet_activities_label: Aktivumas 7 | timesheet_users_label: Vartotojai 8 | timesheet_permalink: '(Pastovi nuoroda į šį lentelę)' 9 | timesheet_group_by: Grupuoti pagal 10 | -------------------------------------------------------------------------------- /config/locales/pl.yml: -------------------------------------------------------------------------------- 1 | pl: 2 | timesheet_title: Czas Pracy 3 | timesheet_date_from_label: Od 4 | timesheet_date_to_label: Do 5 | timesheet_project_label: Projekt 6 | timesheet_activities_label: Aktywności 7 | timesheet_users_label: Użytkownicy 8 | timesheet_showing_users: 'czas pracy użytkownika: ' 9 | timesheet_permalink: '(Permalink do tego czasu pracy)' 10 | timesheet_group_by: Grupuj 11 | -------------------------------------------------------------------------------- /config/locales/pt-br.yml: -------------------------------------------------------------------------------- 1 | pt-BR: 2 | timesheet_title: Relatório de Apontamentos 3 | timesheet_date_from_label: De 4 | timesheet_date_to_label: Até 5 | timesheet_project_label: Projeto 6 | timesheet_activities_label: Atividades 7 | timesheet_users_label: Usuários 8 | timesheet_showing_users: 'Exibindo usuários: ' 9 | timesheet_permalink: 'Link permanente' 10 | timesheet_group_by: 'Agrupar por' 11 | -------------------------------------------------------------------------------- /config/locales/ru.yml: -------------------------------------------------------------------------------- 1 | ru: 2 | timesheet_title: Сводная таблица затраченного времени 3 | timesheet_date_from_label: Начиная с 4 | timesheet_date_to_label: по 5 | timesheet_project_label: Проект 6 | timesheet_activities_label: Активность 7 | timesheet_users_label: Пользователи 8 | timesheet_showing_users: 'сводка для пользователя: ' 9 | timesheet_permalink: '(Постоянная ссылка на эту таблицу)' 10 | timesheet_group_by: Группировать по 11 | -------------------------------------------------------------------------------- /config/locales/sr.yml: -------------------------------------------------------------------------------- 1 | "sr": 2 | timesheet_title: Satnica 3 | timesheet_date_from_label: Od 4 | timesheet_date_to_label: Do 5 | timesheet_project_label: Projekat 6 | timesheet_activities_label: Aktivnost 7 | timesheet_users_label: Korisnici 8 | timesheet_showing_users: 'pokazujem vreme za: ' 9 | timesheet_permalink: '(Stalni link za ovu satnicu)' 10 | timesheet_group_by: Grupiši po 11 | -------------------------------------------------------------------------------- /config/locales/sv.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edavis10/redmine-timesheet-plugin/e0c362447d90a009853e2e1c659dc79d7db01995/config/locales/sv.yml -------------------------------------------------------------------------------- /config/locales/uk.yml: -------------------------------------------------------------------------------- 1 | uk: 2 | timesheet_title: Таблиця витрат часу 3 | timesheet_date_from_label: Починаючи з 4 | timesheet_date_to_label: до 5 | timesheet_project_label: Проект 6 | timesheet_activities_label: Активність 7 | timesheet_users_label: Користувачі 8 | timesheet_showing_users: 'час витрачений користувачем: ' 9 | timesheet_permalink: '(Постійний лінк на цю таблицю)' 10 | timesheet_group_by: Групувати за 11 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | ActionController::Routing::Routes.draw do |map| 2 | map.connect 'timesheet/index', :controller => 'timesheet', :action => 'index' 3 | map.connect 'timesheet/context_menu', :controller => 'timesheet', :action => 'context_menu' 4 | map.connect 'timesheet/report.:format', :controller => 'timesheet', :action => 'report' 5 | map.connect 'timesheet/reset', :controller => 'timesheet', :action => 'reset', :conditions => { :method => :delete } 6 | end 7 | -------------------------------------------------------------------------------- /init.rb: -------------------------------------------------------------------------------- 1 | require 'redmine' 2 | 3 | # Taken from lib/redmine.rb 4 | if RUBY_VERSION < '1.9' 5 | require 'faster_csv' 6 | else 7 | require 'csv' 8 | FCSV = CSV 9 | end 10 | 11 | require 'dispatcher' 12 | Dispatcher.to_prepare :timesheet_plugin do 13 | 14 | require_dependency 'principal' 15 | require_dependency 'user' 16 | User.send(:include, TimesheetPlugin::Patches::UserPatch) 17 | 18 | require_dependency 'project' 19 | Project.send(:include, TimesheetPlugin::Patches::ProjectPatch) 20 | # Needed for the compatibility check 21 | begin 22 | require_dependency 'time_entry_activity' 23 | rescue LoadError 24 | # TimeEntryActivity is not available 25 | end 26 | end 27 | 28 | 29 | unless Redmine::Plugin.registered_plugins.keys.include?(:timesheet_plugin) 30 | Redmine::Plugin.register :timesheet_plugin do 31 | name 'Timesheet Plugin' 32 | author 'Eric Davis of Little Stream Software' 33 | description 'This is a Timesheet plugin for Redmine to show timelogs for all projects' 34 | url 'https://projects.littlestreamsoftware.com/projects/redmine-timesheet' 35 | author_url 'http://www.littlestreamsoftware.com' 36 | 37 | version '0.6.0' 38 | requires_redmine :version_or_higher => '0.9.0' 39 | 40 | settings(:default => { 41 | 'list_size' => '5', 42 | 'precision' => '2', 43 | 'project_status' => 'active', 44 | 'user_status' => 'active' 45 | }, :partial => 'settings/timesheet_settings') 46 | 47 | permission :see_project_timesheets, { }, :require => :member 48 | 49 | menu(:top_menu, 50 | :timesheet, 51 | {:controller => 'timesheet', :action => 'index'}, 52 | :caption => :timesheet_title, 53 | :if => Proc.new { 54 | User.current.allowed_to?(:see_project_timesheets, nil, :global => true) || 55 | User.current.allowed_to?(:view_time_entries, nil, :global => true) || 56 | User.current.admin? 57 | }) 58 | 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /lang/ca.yml: -------------------------------------------------------------------------------- 1 | timesheet_title: Full de temps 2 | timesheet_date_from_label: De 3 | timesheet_date_to_label: A 4 | timesheet_project_label: Projecte 5 | timesheet_activities_label: Activitat 6 | timesheet_users_label: Usuaris 7 | timesheet_showing_users: 'mostrant temps per a: ' 8 | timesheet_permalink: "(Enllaç permanent a aquest full de temps)" 9 | -------------------------------------------------------------------------------- /lang/cs.yml: -------------------------------------------------------------------------------- 1 | timesheet_title: Souhrnná tabulka stráveného času 2 | timesheet_date_from_label: Od 3 | timesheet_date_to_label: do 4 | timesheet_project_label: Projekt 5 | timesheet_activities_label: Aktivita 6 | timesheet_users_label: Uživatelé 7 | timesheet_showing_users: 'zobrazuji časy pro: ' 8 | timesheet_permalink: '(Permalink k tomuto přehledu práce)' 9 | timesheet_group_by: Seskupit dle 10 | -------------------------------------------------------------------------------- /lang/da.yml: -------------------------------------------------------------------------------- 1 | timesheet_title: Timeseddel 2 | timesheet_date_from_label: Fra 3 | timesheet_date_to_label: Til 4 | timesheet_project_label: Projekt 5 | timesheet_activities_label: Aktivitet 6 | timesheet_users_label: Brugere 7 | timesheet_showing_users: 'viser tidsregistreringer for: ' 8 | timesheet_permalink: '(Permanent link til denne timeseddel)' 9 | timesheet_group_by: Gruppér efter 10 | -------------------------------------------------------------------------------- /lang/de.yml: -------------------------------------------------------------------------------- 1 | timesheet_title: Stundenzettel 2 | timesheet_date_from_label: Von 3 | timesheet_date_to_label: Bis 4 | timesheet_project_label: Projekt 5 | timesheet_activities_label: Aktivität 6 | timesheet_users_label: Benutzer 7 | timesheet_showing_users: 'zeige Zeit für: ' 8 | timesheet_permalink: '(Permalink zu diesem Stundenzettel)' 9 | timesheet_group_by: Gruppiere nach -------------------------------------------------------------------------------- /lang/en.yml: -------------------------------------------------------------------------------- 1 | timesheet_title: Timesheet 2 | timesheet_date_from_label: From 3 | timesheet_date_to_label: To 4 | timesheet_project_label: Project 5 | timesheet_activities_label: Activity 6 | timesheet_users_label: Users 7 | timesheet_showing_users: 'showing time for: ' 8 | timesheet_permalink: '(Permalink to this timesheet)' 9 | timesheet_group_by: Group by 10 | -------------------------------------------------------------------------------- /lang/es.yml: -------------------------------------------------------------------------------- 1 | timesheet_title: Hoja de tiempos 2 | timesheet_date_from_label: De 3 | timesheet_date_to_label: A 4 | timesheet_project_label: Proyecto 5 | timesheet_activities_label: Actividad 6 | timesheet_users_label: Usuarios 7 | timesheet_showing_users: 'mostrando tiempos para: ' 8 | timesheet_permalink: '(Permalink a esta hoja de tiempos)' 9 | -------------------------------------------------------------------------------- /lang/fr.yml: -------------------------------------------------------------------------------- 1 | timesheet_title: Feuille de temps 2 | timesheet_date_from_label: De 3 | timesheet_date_to_label: A 4 | timesheet_project_label: Projet 5 | timesheet_activities_label: Activite 6 | timesheet_users_label: Utilisateurs 7 | timesheet_showing_users: 'showing time for: ' 8 | timesheet_permalink: '(Permalink to this timesheet)' 9 | timesheet_group_by: Group by 10 | -------------------------------------------------------------------------------- /lang/hu.yml: -------------------------------------------------------------------------------- 1 | timesheet_title: Időlap 2 | timesheet_date_from_label: Kezdő dátum 3 | timesheet_date_to_label: Vég dátum 4 | timesheet_project_label: Projekt 5 | timesheet_activities_label: Aktivitás 6 | timesheet_users_label: Felhasználók 7 | timesheet_showing_users: 'mutatás ettől a felhasználótól: ' 8 | timesheet_permalink: '(Permalink erre az időlapra)' 9 | timesheet_group_by: Csoportosítás 10 | -------------------------------------------------------------------------------- /lang/hy.yml: -------------------------------------------------------------------------------- 1 | timesheet_title: Ծախսված ժամանակի աղյուսակ 2 | timesheet_date_from_label: Սկսած 3 | timesheet_date_to_label: մինչև 4 | timesheet_project_label: Նախագիծ 5 | timesheet_activities_label: Ակտիվությունը 6 | timesheet_users_label: Օգտագործողներ 7 | timesheet_permalink: '(Մշտական հղում դեպի այս աղյուսակը)' 8 | timesheet_group_by: Դասավորել ըստ 9 | -------------------------------------------------------------------------------- /lang/it.yml: -------------------------------------------------------------------------------- 1 | timesheet_title: Consuntivo tempi 2 | timesheet_date_from_label: Da 3 | timesheet_date_to_label: A 4 | timesheet_project_label: Progetto 5 | timesheet_activities_label: Attività 6 | timesheet_users_label: Utenti 7 | -------------------------------------------------------------------------------- /lang/ja.yml: -------------------------------------------------------------------------------- 1 | timesheet_title: 記録時間表 2 | timesheet_date_from_label: 期間 3 | timesheet_date_to_label: から 4 | timesheet_project_label: プロジェクト 5 | timesheet_activities_label: 活動 6 | timesheet_users_label: ユーザ 7 | timesheet_showing_users: 'ユーザ: ' 8 | timesheet_permalink: '(この記録時間表へ直接リンク)' 9 | timesheet_group_by: グループ化 10 | -------------------------------------------------------------------------------- /lang/lt.yml: -------------------------------------------------------------------------------- 1 | timesheet_title: Sunaudoto laiko suvestinė lentelė 2 | timesheet_date_from_label: Pradedant nuo 3 | timesheet_date_to_label: iki 4 | timesheet_project_label: Projektas 5 | timesheet_activities_label: Aktivumas 6 | timesheet_users_label: Vartotojai 7 | timesheet_permalink: '(Pastovi nuoroda į šį lentelę)' 8 | timesheet_group_by: Grupuoti pagal 9 | -------------------------------------------------------------------------------- /lang/pl.yml: -------------------------------------------------------------------------------- 1 | timesheet_title: Czas Pracy 2 | timesheet_date_from_label: Od 3 | timesheet_date_to_label: Do 4 | timesheet_project_label: Projekt 5 | timesheet_activities_label: Aktywności 6 | timesheet_users_label: Użytkownicy 7 | timesheet_showing_users: 'czas pracy użytkownika: ' 8 | timesheet_permalink: '(Permalink do tego czasu pracy)' 9 | timesheet_group_by: Grupuj 10 | -------------------------------------------------------------------------------- /lang/pt-br.yml: -------------------------------------------------------------------------------- 1 | timesheet_title: Relatório de Apontamentos 2 | timesheet_date_from_label: De 3 | timesheet_date_to_label: Até 4 | timesheet_project_label: Projeto 5 | timesheet_activities_label: Atividades 6 | timesheet_users_label: Usuários 7 | timesheet_showing_users: 'Exibindo usuários: ' 8 | timesheet_permalink: 'Link permanente' 9 | timesheet_group_by: 'Agrupar por' 10 | -------------------------------------------------------------------------------- /lang/ru.yml: -------------------------------------------------------------------------------- 1 | timesheet_title: Сводная таблица затраченного времени 2 | timesheet_date_from_label: Начиная с 3 | timesheet_date_to_label: по 4 | timesheet_project_label: Проект 5 | timesheet_activities_label: Активность 6 | timesheet_users_label: Пользователи 7 | timesheet_showing_users: 'сводка для пользователя: ' 8 | timesheet_permalink: '(Постоянная ссылка на эту таблицу)' 9 | timesheet_group_by: Группировать по 10 | -------------------------------------------------------------------------------- /lang/sr.yml: -------------------------------------------------------------------------------- 1 | timesheet_title: Satnica 2 | timesheet_date_from_label: Od 3 | timesheet_date_to_label: Do 4 | timesheet_project_label: Projekat 5 | timesheet_activities_label: Aktivnost 6 | timesheet_users_label: Korisnici 7 | timesheet_showing_users: 'pokazujem vreme za: ' 8 | timesheet_permalink: '(Stalni link za ovu satnicu)' 9 | timesheet_group_by: Grupiši po 10 | -------------------------------------------------------------------------------- /lang/sv.yml: -------------------------------------------------------------------------------- 1 | timesheet_title: "Tidkort" 2 | timesheet_date_from_label: "Från" 3 | timesheet_date_to_label: "Till" 4 | timesheet_project_label: "Projekt" 5 | timesheet_activities_label: "Aktivitet" 6 | timesheet_users_label: "Användare" 7 | timesheet_showing_users: 'visar tid för: ' 8 | timesheet_permalink: '(Permalänk till detta tidkort)' 9 | timesheet_group_by: "Gruppera efter" 10 | -------------------------------------------------------------------------------- /lang/uk.yml: -------------------------------------------------------------------------------- 1 | timesheet_title: Таблиця витрат часу 2 | timesheet_date_from_label: Починаючи з 3 | timesheet_date_to_label: до 4 | timesheet_project_label: Проект 5 | timesheet_activities_label: Активність 6 | timesheet_users_label: Користувачі 7 | timesheet_showing_users: 'час витрачений користувачем: ' 8 | timesheet_permalink: '(Постійний лінк на цю таблицю)' 9 | timesheet_group_by: Групувати за 10 | -------------------------------------------------------------------------------- /lib/timesheet_compatibility.rb: -------------------------------------------------------------------------------- 1 | # Wrappers around the Redmine core API changes between versions 2 | module TimesheetCompatibility 3 | end 4 | -------------------------------------------------------------------------------- /lib/timesheet_plugin/patches/project_patch.rb: -------------------------------------------------------------------------------- 1 | module TimesheetPlugin 2 | module Patches 3 | module ProjectPatch 4 | def self.included(base) 5 | base.extend(ClassMethods) 6 | 7 | base.send(:include, InstanceMethods) 8 | base.class_eval do 9 | unloadable 10 | 11 | # Prefix our named_scopes to prevent collusion 12 | named_scope :timesheet_order_by_name, :order => 'name ASC' 13 | named_scope :timesheet_with_membership, lambda {|user| 14 | # Similar to Project.visible_by but without the STATUS check 15 | if user && user.memberships.any? 16 | 17 | # Principal#members gets all projects, but #memberships will only 18 | # get the active ones 19 | if Setting.plugin_timesheet_plugin['project_status'] == 'all' 20 | project_ids = user.members.collect{|m| m.project_id} 21 | else 22 | project_ids = user.memberships.collect{|m| m.project_id} 23 | end 24 | 25 | { 26 | :conditions => [ 27 | "#{Project.table_name}.is_public = :true or #{Project.table_name}.id IN (:project_ids)", 28 | { 29 | :true => true, 30 | :project_ids => project_ids 31 | } 32 | ] 33 | } 34 | else 35 | { 36 | :conditions => { :is_public => true } 37 | } 38 | end 39 | } 40 | end 41 | end 42 | 43 | module ClassMethods 44 | end 45 | 46 | module InstanceMethods 47 | end 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/timesheet_plugin/patches/user_patch.rb: -------------------------------------------------------------------------------- 1 | module TimesheetPlugin 2 | module Patches 3 | module UserPatch 4 | def self.included(base) 5 | base.extend(ClassMethods) 6 | 7 | base.send(:include, InstanceMethods) 8 | base.class_eval do 9 | unloadable 10 | end 11 | end 12 | 13 | module ClassMethods 14 | end 15 | 16 | module InstanceMethods 17 | 18 | # Wrapper for User#allowed_to? that doesn't reject archived projects 19 | # automatically 20 | def allowed_to_on_single_potentially_archived_project?(action, context, options={}) 21 | if Setting.plugin_timesheet_plugin['project_status'] == 'all' && context && context.is_a?(Project) && !context.active? 22 | # Duplicated from User#allowed_to? but without the archived project guard 23 | # No action allowed on disabled modules 24 | return false unless context.allows_to?(action) 25 | # Admin users are authorized for anything else 26 | return true if admin? 27 | 28 | roles = roles_for_project_with_potentially_archived_project(context) 29 | return false unless roles 30 | roles.detect {|role| (context.is_public? || role.member?) && role.allowed_to?(action)} 31 | 32 | else 33 | allowed_to?(action, context, options) 34 | end 35 | end 36 | 37 | 38 | # Similar to User#roles_for_project but doesn't reject archived projects 39 | def roles_for_project_with_potentially_archived_project(project) 40 | roles = [] 41 | if logged? 42 | # Find project membership 43 | # ED: use members because membership is only for unarchived projects 44 | membership = members.detect {|m| m.project_id == project.id} 45 | if membership 46 | roles = membership.roles 47 | else 48 | @role_non_member ||= Role.non_member 49 | roles << @role_non_member 50 | end 51 | else 52 | @role_anonymous ||= Role.anonymous 53 | roles << @role_anonymous 54 | end 55 | roles 56 | end 57 | 58 | end 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /rails/init.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + "/../init" 2 | -------------------------------------------------------------------------------- /test/functional/timesheet_controller_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../test_helper' 2 | class ActiveSupport::TestCase 3 | def self.use_timesheet_controller_shared(&block) 4 | should 'should set @timesheet.allowed_projects to the list of current projects the user is a member of' do 5 | Member.destroy_all # clear any setup memberships 6 | 7 | project1 = Project.generate! 8 | project2 = Project.generate! 9 | projects = [project1, project2] 10 | 11 | projects.each do |project| 12 | Member.generate!(:principal => @current_user, :project => project, :roles => [@normal_role]) 13 | end 14 | 15 | instance_eval &block 16 | 17 | assert_equal projects, assigns['timesheet'].allowed_projects 18 | end 19 | 20 | should 'include public projects in @timesheet.allowed_projects' do 21 | project1 = Project.generate!(:is_public => true) 22 | project2 = Project.generate!(:is_public => true) 23 | projects = [project1, project2] 24 | 25 | instance_eval &block 26 | 27 | assert_contains assigns['timesheet'].allowed_projects, project1 28 | assert_contains assigns['timesheet'].allowed_projects, project2 29 | end 30 | 31 | should 'should set @timesheet.allowed_projects to all the projects if the user is an admin' do 32 | Member.destroy_all # clear any setup memberships 33 | 34 | @current_user.admin = true 35 | project1, _ = *generate_project_membership(@current_user) 36 | project2, _ = *generate_project_membership(@current_user) 37 | projects = [project1, project2] 38 | 39 | instance_eval &block 40 | 41 | assert_equal projects, assigns['timesheet'].allowed_projects 42 | end 43 | 44 | should 'should get the list size from the settings' do 45 | settings = { 'list_size' => 10, 'precision' => '2' } 46 | Setting.plugin_timesheet_plugin = settings 47 | 48 | instance_eval &block 49 | assert_equal 10, assigns['list_size'] 50 | end 51 | 52 | should 'should get the precision from the settings' do 53 | settings = { 'list_size' => 10, 'precision' => '2' } 54 | Setting.plugin_timesheet_plugin = settings 55 | 56 | instance_eval &block 57 | assert_equal 2, assigns['precision'] 58 | end 59 | 60 | should 'should create a new @timesheet' do 61 | instance_eval &block 62 | assert assigns['timesheet'] 63 | end 64 | end 65 | end 66 | 67 | 68 | class TimesheetControllerTest < ActionController::TestCase 69 | def generate_and_login_user(options = {}) 70 | @current_user = User.generate_with_protected!(:admin => false) 71 | @request.session[:user_id] = @current_user.id 72 | end 73 | 74 | def generate_project_membership(user) 75 | @project = Project.generate!(:is_public => false) 76 | @member = Member.generate!(:principal => user, :project => @project, :roles => [@normal_role]) 77 | [@project, @member] 78 | end 79 | 80 | def setup 81 | @normal_role = Role.generate!(:name => 'Normal User', :permissions => [:view_time_entries]) 82 | end 83 | 84 | context "#index with GET request" do 85 | setup do 86 | generate_and_login_user 87 | generate_project_membership(@current_user) 88 | get 'index' 89 | end 90 | 91 | use_timesheet_controller_shared do 92 | get 'index' 93 | end 94 | 95 | should_render_template :index 96 | 97 | should 'have no timelog entries' do 98 | assert assigns['timesheet'].time_entries.empty? 99 | end 100 | end 101 | 102 | context "#index with GET request and a session" do 103 | 104 | should 'should read the session data' do 105 | generate_and_login_user 106 | @current_user.admin = true 107 | @current_user.save! 108 | 109 | projects = [] 110 | 4.times do |i| 111 | projects << Project.generate! 112 | end 113 | 114 | session[TimesheetController::SessionKey] = HashWithIndifferentAccess.new( 115 | :projects => projects.collect(&:id).collect(&:to_s), 116 | :date_to => '2009-01-01', 117 | :date_from => '2009-01-01' 118 | ) 119 | 120 | get :index 121 | assert_equal '2009-01-01', assigns['timesheet'].date_from 122 | assert_equal '2009-01-01', assigns['timesheet'].date_to 123 | assert_equal projects, assigns['timesheet'].projects 124 | end 125 | end 126 | 127 | context "#index with GET request from an Anonymous user" do 128 | setup do 129 | get 'index' 130 | end 131 | 132 | should_render_template :no_projects 133 | 134 | end 135 | 136 | context "#report with GET request from an Anonymous user" do 137 | setup do 138 | get :report 139 | end 140 | 141 | should_respond_with :redirect 142 | should_redirect_to('index') {{:action => 'index'}} 143 | end 144 | 145 | context "#report with POST request from an Anonymous user" do 146 | setup do 147 | post :report 148 | end 149 | 150 | should_respond_with :redirect 151 | should_redirect_to('index') {{:action => 'index'}} 152 | 153 | end 154 | 155 | context "#report with POST request" do 156 | setup do 157 | generate_and_login_user 158 | end 159 | 160 | use_timesheet_controller_shared do 161 | post :report, :timesheet => {} 162 | end 163 | 164 | should 'should only allow the allowed projects into @timesheet.projects' do 165 | project1 = Project.generate!(:is_public => false) 166 | project2 = Project.generate!(:is_public => false) 167 | projects = [project1, project2] 168 | 169 | Member.generate!(:principal => @current_user, :project => project1, :roles => [@normal_role]) 170 | 171 | post :report, :timesheet => { :projects => [project1.id.to_s, project2.id.to_s] } 172 | 173 | assert_equal [project1], assigns['timesheet'].projects 174 | end 175 | 176 | should 'include public projects' do 177 | project1 = Project.generate!(:is_public => true) 178 | project2 = Project.generate!(:is_public => true) 179 | projects = [project1, project2] 180 | 181 | post :report, :timesheet => { :projects => [project1.id.to_s, project2.id.to_s] } 182 | 183 | assert_contains assigns['timesheet'].allowed_projects, project1 184 | assert_contains assigns['timesheet'].allowed_projects, project2 185 | end 186 | 187 | should 'should save the session data' do 188 | generate_project_membership(@current_user) 189 | post :report, :timesheet => { :projects => ['1'] } 190 | 191 | assert @request.session[TimesheetController::SessionKey] 192 | assert @request.session[TimesheetController::SessionKey].keys.include?('projects') 193 | assert_equal ['1'], @request.session[TimesheetController::SessionKey]['projects'] 194 | end 195 | 196 | context ":csv format" do 197 | setup do 198 | generate_project_membership(@current_user) 199 | post :report, :timesheet => {:projects => ['1']}, :format => 'csv' 200 | end 201 | 202 | should_respond_with_content_type 'text/csv' 203 | should_respond_with :success 204 | end 205 | end 206 | 207 | context "#report with request with no data" do 208 | setup do 209 | generate_and_login_user 210 | end 211 | 212 | context 'should redirect to the index' do 213 | context "from a GET request" do 214 | setup do 215 | get 'report', { } 216 | end 217 | 218 | should_respond_with :redirect 219 | should_redirect_to('index') {{:action => 'index' }} 220 | end 221 | 222 | context "from a POST request" do 223 | setup do 224 | post 'report', { } 225 | end 226 | 227 | should_respond_with :redirect 228 | should_redirect_to('index') {{:action => 'index' }} 229 | end 230 | end 231 | end 232 | 233 | context "DELETE to :reset" do 234 | setup do 235 | generate_and_login_user 236 | @current_user.admin = true 237 | @current_user.save! 238 | 239 | @project = Project.generate! 240 | session[TimesheetController::SessionKey] = HashWithIndifferentAccess.new( 241 | :projects => [@project.id.to_s], 242 | :date_to => '2009-01-01', 243 | :date_from => '2009-01-01' 244 | ) 245 | 246 | delete :reset 247 | end 248 | 249 | should_respond_with :redirect 250 | should_redirect_to('index') {{:action => 'index'}} 251 | should 'clear the session' do 252 | assert session[TimesheetController::SessionKey].blank? 253 | end 254 | 255 | end 256 | end 257 | -------------------------------------------------------------------------------- /test/integration/configuration_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class ConfigurationTest < ActionController::IntegrationTest 4 | def setup 5 | @user = User.generate!(:password => 'test', :password_confirmation => 'test', :admin => true) 6 | end 7 | 8 | should "add a plugin configuration panel" do 9 | login_as(@user.login, 'test') 10 | visit_home 11 | click_link 'Administration' 12 | assert_response :success 13 | 14 | click_link 'Plugins' 15 | assert_response :success 16 | 17 | click_link 'Configure' 18 | assert_response :success 19 | end 20 | 21 | should "be able to configure the list size" do 22 | login_as(@user.login, 'test') 23 | visit_configuration_panel 24 | 25 | fill_in "List size", :with => '10' 26 | click_button 'Apply' 27 | 28 | assert_equal '10', plugin_configuration['list_size'] 29 | end 30 | 31 | should "be able to configure the number precision" do 32 | login_as(@user.login, 'test') 33 | visit_configuration_panel 34 | 35 | fill_in "Number precision", :with => '10' 36 | click_button 'Apply' 37 | 38 | assert_equal '10', plugin_configuration['precision'] 39 | end 40 | 41 | should "be able to configure the project status" do 42 | login_as(@user.login, 'test') 43 | visit_configuration_panel 44 | 45 | select "All (active and archived)", :from => 'settings_project_status' 46 | click_button 'Apply' 47 | 48 | assert_equal 'all', plugin_configuration['project_status'] 49 | end 50 | 51 | should "be able to configure what types of users are shown" do 52 | login_as(@user.login, 'test') 53 | visit_configuration_panel 54 | 55 | select "All", :from => 'settings_user_status' 56 | click_button 'Apply' 57 | 58 | assert_equal 'all', plugin_configuration['user_status'] 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /test/integration/filter_allowed_projects_by_status_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class FilterAllowedProjectsByStatusTest < ActionController::IntegrationTest 4 | def setup 5 | @active_project1 = Project.generate! 6 | @active_project2 = Project.generate! 7 | @archived_project1 = Project.generate!(:status => Project::STATUS_ARCHIVED) 8 | @archived_private_project1 = Project.generate!(:status => Project::STATUS_ARCHIVED, :is_public => false) 9 | assert !@archived_project1.active? 10 | assert !@archived_private_project1.active? 11 | 12 | @admin_user = User.generate_with_protected!(:login => 'theadmin', :admin => true, :password => 'testing', :password_confirmation => 'testing') 13 | @user = User.generate_with_protected!(:login => 'theuser', :admin => false, :password => 'testing', :password_confirmation => 'testing') 14 | @role = Role.generate!(:permissions => [:view_time_entries]) 15 | @project = Project.generate! 16 | Member.generate!(:principal => @user, :project => @project, :roles => [@role]) 17 | 18 | Member.generate!(:principal => @user, :project => @archived_private_project1, :roles => [@role]) 19 | end 20 | 21 | context "with project_status configured to all" do 22 | setup do 23 | Setting.plugin_timesheet_plugin = {'project_status' => 'all'} 24 | end 25 | 26 | context "as an admin" do 27 | setup do 28 | log_user(@admin_user.login, 'testing') 29 | follow_redirect! 30 | click_link "Timesheet" 31 | end 32 | 33 | should "see archived projects in the list" do 34 | assert_select "#timesheet_projects_" do 35 | assert_select 'option[value=?]', @active_project1.id 36 | assert_select 'option[value=?]', @active_project2.id 37 | assert_select 'option[value=?]', @archived_project1.id 38 | assert_select 'option[value=?]', @archived_private_project1.id 39 | end 40 | end 41 | end 42 | 43 | context "as a regular user" do 44 | setup do 45 | log_user(@user.login, 'testing') 46 | follow_redirect! 47 | click_link "Timesheet" 48 | end 49 | 50 | should "see archived projects in the list" do 51 | assert_select "#timesheet_projects_" do 52 | assert_select 'option[value=?]', @active_project1.id 53 | assert_select 'option[value=?]', @active_project2.id 54 | assert_select 'option[value=?]', @archived_project1.id 55 | assert_select 'option[value=?]', @archived_private_project1.id 56 | end 57 | end 58 | 59 | end 60 | end 61 | 62 | context "with project_status configured to active" do 63 | setup do 64 | Setting.plugin_timesheet_plugin = {'project_status' => 'active'} 65 | end 66 | 67 | context "as an admin" do 68 | setup do 69 | log_user(@admin_user.login, 'testing') 70 | follow_redirect! 71 | click_link "Timesheet" 72 | end 73 | 74 | should "see archived projects in the list" do 75 | assert_select "#timesheet_projects_" do 76 | assert_select 'option[value=?]', @active_project1.id 77 | assert_select 'option[value=?]', @active_project2.id 78 | assert_select 'option[value=?]', @archived_project1.id 79 | end 80 | end 81 | end 82 | 83 | context "as a regular user" do 84 | setup do 85 | log_user(@user.login, 'testing') 86 | follow_redirect! 87 | click_link "Timesheet" 88 | end 89 | 90 | should "not see archived projects in the list" do 91 | assert_select "#timesheet_projects_" do 92 | assert_select 'option[value=?]', @active_project1.id 93 | assert_select 'option[value=?]', @active_project2.id 94 | assert_select 'option[value=?]', @archived_project1.id, :count => 0 95 | end 96 | end 97 | 98 | end 99 | end 100 | 101 | end 102 | 103 | -------------------------------------------------------------------------------- /test/integration/session_storage_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class SessionStorageTest < ActionController::IntegrationTest 4 | def setup 5 | @project1 = Project.generate! 6 | @project2 = Project.generate! 7 | 8 | @admin_user = User.generate!(:login => 'theadmin', :admin => true, :password => 'testing', :password_confirmation => 'testing') 9 | @role = Role.generate!(:permissions => [:view_time_entries]) 10 | @project = Project.generate! 11 | Member.generate!(:principal => @admin_user, :project => @project, :roles => [@role]) 12 | end 13 | 14 | context "when running a report" do 15 | setup do 16 | login_as(@admin_user.login, "testing") 17 | end 18 | 19 | should "save the timesheet params to the session" 20 | should "reuse the session params when loading a fresh timesheet" 21 | should "not save the timesheet params if it would overflow the cookie store" do 22 | # Since sessions are 4K, make a ton of Activities to load into the session (they are faster than Projects/Users) 23 | 1000.times {|i| self.instance_variable_set("@activity_#{i}", TimeEntryActivity.generate!.reload) } 24 | click_link "Timesheet" 25 | choose "timesheet_period_type_1" # Pre-defined 26 | select "all time", :from => 'timesheet_period' 27 | select "Project", :from => 'timesheet_sort' 28 | select @project1.name, :from => 'Project:' 29 | select @project2.name, :from => 'Project:' 30 | 1000.times {|i| 31 | select(self.instance_variable_get("@activity_#{i}").name, :from => "timesheet_activities_") 32 | } 33 | 34 | assert_nothing_raised do 35 | click_button 'Apply' 36 | 37 | click_link "Timesheet" 38 | assert_response :success # loads the cookie 39 | end 40 | end 41 | 42 | end 43 | 44 | end 45 | -------------------------------------------------------------------------------- /test/integration/timesheet_menu_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class TimesheetMenuTest < ActionController::IntegrationTest 4 | context "as an admin" do 5 | setup do 6 | @admin_user = User.generate_with_protected!(:admin => true, :password => 'testing', :password_confirmation => 'testing') 7 | log_user(@admin_user.login, 'testing') 8 | end 9 | 10 | should_see_the_timesheet_menu 11 | end 12 | 13 | context "as a user with See Project Timesheets on a project" do 14 | setup do 15 | @manager_user = User.generate_with_protected!(:admin => false, :password => 'testing', :password_confirmation => 'testing') 16 | 17 | @manager_role = Role.generate!(:permissions => [:view_time_entries, :see_project_timesheets]) 18 | @project = Project.generate! 19 | Member.generate!(:principal => @manager_user, :project => @project, :roles => [@manager_role]) 20 | 21 | log_user(@manager_user.login, 'testing') 22 | end 23 | 24 | should_see_the_timesheet_menu 25 | end 26 | 27 | context "as a user with View Time Entries on a project" do 28 | setup do 29 | @user = User.generate_with_protected!(:admin => false, :password => 'testing', :password_confirmation => 'testing') 30 | 31 | @role = Role.generate!(:permissions => [:view_time_entries]) 32 | @project = Project.generate! 33 | Member.generate!(:principal => @user, :project => @project, :roles => [@role]) 34 | 35 | log_user(@user.login, 'testing') 36 | end 37 | 38 | should_see_the_timesheet_menu 39 | end 40 | 41 | context "as a user with without See Project Timesheets or View Time Entries on a project" do 42 | setup do 43 | @user = User.generate_with_protected!(:admin => false, :password => 'testing', :password_confirmation => 'testing') 44 | log_user(@user.login, 'testing') 45 | end 46 | 47 | should_not_see_the_timesheet_menu 48 | end 49 | 50 | context "as the anonymous user" do 51 | should_not_see_the_timesheet_menu 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /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 | require "webrat" 8 | 9 | Webrat.configure do |config| 10 | config.mode = :rails 11 | end 12 | 13 | # shoulda 14 | class Test::Unit::TestCase 15 | def self.should_see_the_timesheet_menu 16 | should "see the timesheet menu" do 17 | get '/' 18 | 19 | assert_select '#top-menu a.timesheet' 20 | end 21 | end 22 | 23 | def self.should_not_see_the_timesheet_menu 24 | should "not see the timesheet menu" do 25 | get '/' 26 | 27 | assert_select '#top-menu a.timesheet', :count => 0 28 | end 29 | end 30 | end 31 | 32 | def User.add_to_project(user, project, role) 33 | Member.generate!(:principal => user, :project => project, :roles => [role]) 34 | end 35 | 36 | module ChiliProjectIntegrationTestHelper 37 | def login_as(user="existing", password="existing") 38 | visit "/logout" # Make sure the session is cleared 39 | 40 | visit "/login" 41 | fill_in 'Login', :with => user 42 | fill_in 'Password', :with => password 43 | click_button 'Login' 44 | assert_response :success 45 | assert User.current.logged? 46 | end 47 | 48 | def visit_home 49 | visit '/' 50 | assert_response :success 51 | end 52 | 53 | def visit_project(project) 54 | visit '/' 55 | assert_response :success 56 | 57 | click_link 'Projects' 58 | assert_response :success 59 | 60 | click_link project.name 61 | assert_response :success 62 | end 63 | 64 | def visit_issue_page(issue) 65 | visit '/issues/' + issue.id.to_s 66 | end 67 | 68 | def visit_issue_bulk_edit_page(issues) 69 | visit url_for(:controller => 'issues', :action => 'bulk_edit', :ids => issues.collect(&:id)) 70 | end 71 | end 72 | 73 | module TimesheetIntegrationTestHelper 74 | def visit_configuration_panel 75 | visit_home 76 | click_link 'Administration' 77 | assert_response :success 78 | 79 | click_link 'Plugins' 80 | assert_response :success 81 | 82 | click_link 'Configure' 83 | assert_response :success 84 | end 85 | 86 | end 87 | 88 | class ActionController::IntegrationTest 89 | include ChiliProjectIntegrationTestHelper 90 | include TimesheetIntegrationTestHelper 91 | end 92 | 93 | class ActiveSupport::TestCase 94 | def assert_forbidden 95 | assert_response :forbidden 96 | assert_template 'common/403' 97 | end 98 | 99 | def configure_plugin(configuration_change={}) 100 | Setting.plugin_timesheet_plugin = { 101 | 'list_size' => '5', 102 | 'precision' => '2', 103 | 'project_status' => 'active', 104 | 'user_status' => 'active' 105 | }.merge(configuration_change) 106 | end 107 | 108 | def reconfigure_plugin(configuration_change) 109 | Setting['plugin_timesheet_plugin'] = Setting['plugin_timesheet_plugin'].merge(configuration_change) 110 | end 111 | 112 | def plugin_configuration 113 | Setting.plugin_timesheet_plugin 114 | end 115 | end 116 | -------------------------------------------------------------------------------- /test/unit/lib/timesheet_plugin/patches/user_patch_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../../../../test_helper' 2 | 3 | class TimesheetPlugin::Patches::UserTest < ActionController::TestCase 4 | 5 | should "be tested" 6 | end 7 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /test/unit/timesheet_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../test_helper' 2 | 3 | module TimesheetSpecHelper 4 | def timesheet_factory(options={ }) 5 | timesheet = Timesheet.new(options) 6 | timesheet.date_from ||= Date.today.to_s 7 | timesheet.date_to ||= Date.today.to_s 8 | timesheet.allowed_projects = options[:projects] if options[:projects] 9 | timesheet.projects = options[:projects] if options[:projects] 10 | 11 | return timesheet 12 | end 13 | 14 | def project_factory(id, options = { }) 15 | object_options = { 16 | :id => id, 17 | :trackers => [@tracker] 18 | }.merge(options) 19 | 20 | project = Project.generate!(object_options) 21 | project_te1 = TimeEntry.generate!(:project => project, :hours => '5', :activity => @activity, :spent_on => Date.today) 22 | project_te2 = TimeEntry.generate!(:project => project, :hours => '10', :activity => @activity, :spent_on => Date.today) 23 | 24 | return project 25 | end 26 | 27 | def time_entry_factory(id, options = { }) 28 | object_options = { 29 | :id => id, 30 | :spent_on => Date.today, 31 | }.merge(options) 32 | 33 | time_entry = TimeEntry.generate!(object_options) 34 | return time_entry 35 | end 36 | 37 | def stub_non_member_user(projects) 38 | @current_user = User.generate_with_protected!(:admin => false, :firstname => "Non", :lastname => "Member") 39 | User.current = @current_user 40 | end 41 | 42 | def stub_normal_user(projects) 43 | @current_user = User.generate_with_protected!(:admin => false, :firstname => "Non", :lastname => "Member") 44 | projects.each do |project| 45 | Member.generate!(:principal => @current_user, :project => project, :roles => [@normal_role]) 46 | end 47 | User.current = @current_user 48 | end 49 | 50 | def stub_manager_user(projects) 51 | @current_user = User.generate_with_protected!(:admin => false, :firstname => "Non", :lastname => "Member") 52 | projects.each do |project| 53 | Member.generate!(:principal => @current_user, :project => project, :roles => [@manager_role]) 54 | end 55 | User.current = @current_user 56 | end 57 | 58 | def stub_admin_user 59 | @current_user = User.generate_with_protected!(:admin => true, :firstname => "Administrator", :lastname => "Bob") 60 | assert @current_user.admin? 61 | User.current = @current_user 62 | end 63 | 64 | def stub_common_csv_records(options={}) 65 | @csv_tracker = @tracker 66 | @csv_project = options[:project] || Project.generate!(:name => 'Project Name', :trackers => [@csv_tracker]) 67 | @csv_issue = options[:issue] # || Issue.generate_for_project!(@csv_project, :tracker => @csv_tracker, :priority => @issue_priority) 68 | @csv_activity ||= options[:activity] || TimeEntryActivity.generate!(:name => 'activity') 69 | @csv_user = options[:user] || User.current 70 | { 71 | :user => @csv_user, 72 | :activity => @csv_activity, 73 | :spent_on => '2009-04-05', 74 | :project => @csv_project, 75 | :comments => 'comments', 76 | :hours => 10.0, 77 | :issue => @csv_issue 78 | } 79 | end 80 | end 81 | 82 | class TimesheetTest < ActiveSupport::TestCase 83 | include TimesheetSpecHelper 84 | 85 | def setup 86 | @issue_priority = IssuePriority.generate!(:name => 'common_csv_records') 87 | @tracker = Tracker.generate!(:name => 'Tracker') 88 | @activity = TimeEntryActivity.generate!(:name => 'activity') 89 | @normal_role = Role.generate!(:name => 'Normal User', :permissions => [:view_time_entries, :log_time]) 90 | @manager_role = Role.generate!(:permissions => [:view_time_entries, :see_project_timesheets]) 91 | end 92 | 93 | should 'not be an ActiveRecord class' do 94 | assert !Timesheet.new.is_a?(ActiveRecord::Base) 95 | end 96 | 97 | context "initializing" do 98 | should 'should initialize time_entries to an empty Hash' do 99 | timesheet = Timesheet.new 100 | assert_kind_of Hash, timesheet.time_entries 101 | assert timesheet.time_entries.empty? 102 | end 103 | 104 | should 'should initialize projects to an empty Array' do 105 | timesheet = Timesheet.new 106 | assert_kind_of Array, timesheet.projects 107 | assert timesheet.projects.empty? 108 | end 109 | 110 | should 'should initialize allowed_projects to an empty Array' do 111 | timesheet = Timesheet.new 112 | assert_kind_of Array, timesheet.allowed_projects 113 | assert timesheet.allowed_projects.empty? 114 | end 115 | 116 | should 'should initialize activities to an Array' do 117 | timesheet = Timesheet.new 118 | assert_kind_of Array, timesheet.activities 119 | end 120 | 121 | context "users" do 122 | setup do 123 | project = Project.generate! 124 | @user_with_permission1 = User.generate_with_protected! 125 | @user_with_permission2 = User.generate_with_protected! 126 | @user_without_permission = User.generate_with_protected! 127 | 128 | Member.generate!(:principal => @user_with_permission1, :project => project, :roles => [@normal_role]) 129 | Member.generate!(:principal => @user_with_permission2, :project => project, :roles => [@normal_role]) 130 | 131 | @timesheet = Timesheet.new 132 | end 133 | 134 | should 'initialize users to an Array' do 135 | assert_kind_of Array, @timesheet.users 136 | end 137 | 138 | should 'only include users who have the "log time" permission' do 139 | assert_contains @timesheet.users, @user_with_permission1.id 140 | assert_contains @timesheet.users, @user_with_permission2.id 141 | end 142 | 143 | end 144 | 145 | should 'should initialize sort to :project' do 146 | timesheet = Timesheet.new 147 | assert_equal :project, timesheet.sort 148 | end 149 | 150 | should 'should initialize time_entries to the passed in options' do 151 | data = { :test => true } 152 | timesheet = Timesheet.new({ :time_entries => data }) 153 | assert !timesheet.time_entries.empty? 154 | assert_equal data, timesheet.time_entries 155 | end 156 | 157 | should 'should initialize allowed_projects to the passed in options' do 158 | data = ['project1', 'project2'] 159 | timesheet = Timesheet.new({ :allowed_projects => data }) 160 | assert !timesheet.allowed_projects.empty? 161 | assert_equal data, timesheet.allowed_projects 162 | end 163 | 164 | should 'should initialize activities to the integers of the passed in options' do 165 | act1 = TimeEntryActivity.generate! 166 | act2 = TimeEntryActivity.generate! 167 | 168 | data = [act1.id, act2.id] 169 | timesheet = Timesheet.new({ :activities => data }) 170 | assert !timesheet.activities.empty? 171 | assert_equal [act1.id, act2.id], timesheet.activities 172 | end 173 | 174 | should 'should initialize users to the ids of the passed in options' do 175 | user1 = User.generate_with_protected! 176 | user2 = User.generate_with_protected! 177 | data = [user1.id, user2.id] 178 | 179 | timesheet = Timesheet.new({ :users => data }) 180 | assert !timesheet.users.empty? 181 | assert_equal [user1.id, user2.id], timesheet.users 182 | end 183 | 184 | should 'should initialize sort to the :user option when passed :user' do 185 | timesheet = Timesheet.new({ :sort => :user }) 186 | assert_equal :user, timesheet.sort 187 | end 188 | 189 | should 'should initialize sort to the :project option when passed :project' do 190 | timesheet = Timesheet.new({ :sort => :project }) 191 | assert_equal :project, timesheet.sort 192 | end 193 | 194 | should 'should initialize sort to the :issue option when passed :issue' do 195 | timesheet = Timesheet.new({ :sort => :issue }) 196 | assert_equal :issue, timesheet.sort 197 | end 198 | 199 | should 'should initialize sort to the :project option when passed an invalid sort' do 200 | timesheet = Timesheet.new({ :sort => :invalid }) 201 | assert_equal :project, timesheet.sort 202 | end 203 | end 204 | 205 | context "#viewable_users" do 206 | setup do 207 | @active_user = User.generate! 208 | @active2_user = User.generate! 209 | @inactive_user = User.generate!(:status => User::STATUS_LOCKED) 210 | @no_permission_user = User.generate! 211 | @role = Role.generate!(:permissions => [:log_time]) 212 | @project = Project.generate! 213 | User.add_to_project(@active_user, @project, @role) 214 | User.add_to_project(@active2_user, @project, @role) 215 | User.add_to_project(@inactive_user, @project, @role) 216 | end 217 | 218 | context "with the user_status configured to all" do 219 | setup do 220 | reconfigure_plugin('user_status' => 'all') 221 | end 222 | 223 | should "show return all users with the log_time permission" do 224 | assert Timesheet.viewable_users.include?(@active_user) 225 | assert Timesheet.viewable_users.include?(@active2_user) 226 | assert Timesheet.viewable_users.include?(@inactive_user) 227 | assert !Timesheet.viewable_users.include?(@no_permission_user) 228 | end 229 | end 230 | 231 | context "with the user_status configured to active" do 232 | setup do 233 | reconfigure_plugin('user_status' => 'active') 234 | end 235 | 236 | should "show return active users with the log_time permission" do 237 | assert Timesheet.viewable_users.include?(@active_user) 238 | assert Timesheet.viewable_users.include?(@active2_user) 239 | assert !Timesheet.viewable_users.include?(@inactive_user) 240 | assert !Timesheet.viewable_users.include?(@no_permission_user) 241 | end 242 | end 243 | 244 | context "with the user_status not configured" do 245 | setup do 246 | reconfigure_plugin('user_status' => '') 247 | end 248 | 249 | should "show default to the 'active' option and return active users with the log_time permission" do 250 | assert Timesheet.viewable_users.include?(@active_user) 251 | assert Timesheet.viewable_users.include?(@active2_user) 252 | assert !Timesheet.viewable_users.include?(@inactive_user) 253 | assert !Timesheet.viewable_users.include?(@no_permission_user) 254 | end 255 | end 256 | 257 | end 258 | 259 | context "#fetch_time_entries" do 260 | setup do 261 | stub_admin_user 262 | @project1 = Project.generate!(:name => 'Project 1') 263 | @te1 = TimeEntry.generate!(:project => @project1, :activity => @activity, :spent_on => Date.today, :user => @current_user) 264 | 265 | @project2 = Project.generate!(:name => 'Project 2') 266 | @te2 = TimeEntry.generate!(:project => @project2, :activity => @activity, :spent_on => Date.today, :user => @current_user) 267 | 268 | @timesheet = timesheet_factory(:activities => [@activity.id], :projects => [@project1, @project2]) 269 | end 270 | 271 | should 'should clear .time_entries' do 272 | timesheet = Timesheet.new 273 | timesheet.time_entries = { :filled => 'data' } 274 | 275 | previous = timesheet.time_entries 276 | 277 | timesheet.fetch_time_entries 278 | 279 | assert_not_same previous, timesheet.time_entries 280 | end 281 | 282 | should 'should add a time_entry Hash for each project' do 283 | @timesheet.fetch_time_entries 284 | 285 | assert !@timesheet.time_entries.empty? 286 | assert_equal 2, @timesheet.time_entries.size 287 | end 288 | 289 | should 'should use the project name for each time_entry key' do 290 | @timesheet.fetch_time_entries 291 | 292 | assert_contains @timesheet.time_entries.keys, "Project 1" 293 | assert_contains @timesheet.time_entries.keys, "Project 2" 294 | end 295 | 296 | should 'should add the parent project name for each time_entry array for sub-projects' do 297 | @project2.set_parent!(@project1) 298 | 299 | @timesheet.fetch_time_entries 300 | 301 | assert_contains @timesheet.time_entries.keys, "Project 1" 302 | assert_contains @timesheet.time_entries.keys, "Project 1 / Project 2" 303 | end 304 | 305 | should 'should fetch all the time entries on a project in the date range' 306 | should 'should fetch all the time entries on a project matching the activities' 307 | should 'should fetch all the time entries on a project matching the users' 308 | end 309 | 310 | context "#fetch_time_entries with user sorting" do 311 | setup do 312 | @project = Project.generate!(:trackers => [@tracker], :name => 'Project Name') 313 | stub_admin_user 314 | @timesheet = timesheet_factory(:sort => :user, :users => [User.current.id], :projects => [@project], :activities => [@activity.id]) 315 | 316 | TimeEntry.generate!(:user => User.current, :project => @project, :activity => @activity) 317 | TimeEntry.generate!(:user => User.current, :project => @project, :activity => @activity) 318 | TimeEntry.generate!(:user => User.current, :project => @project, :activity => @activity) 319 | 320 | end 321 | 322 | should 'should clear .time_entries' do 323 | @timesheet.time_entries = { :filled => 'data' } 324 | 325 | previous = @timesheet.time_entries 326 | @timesheet.fetch_time_entries 327 | assert_not_same previous, @timesheet.time_entries 328 | end 329 | 330 | should 'should add a time_entry array for each user' do 331 | @timesheet.fetch_time_entries 332 | 333 | assert !@timesheet.time_entries.empty? 334 | assert_equal 1, @timesheet.time_entries.size # One user 335 | end 336 | 337 | should 'should use the user name for each time_entry array' do 338 | @timesheet.fetch_time_entries 339 | 340 | assert_contains @timesheet.time_entries.keys, "Administrator Bob" 341 | end 342 | end 343 | 344 | context '#fetch_time_entries with issue sorting' do 345 | setup do 346 | stub_admin_user 347 | @project = project_factory(1) 348 | @timesheet = timesheet_factory(:sort => :issue, :users => [User.current.id]) 349 | @timesheet.projects = [@project] 350 | 351 | @issue1 = Issue.generate_for_project!(@project, :priority => @issue_priority) 352 | @issue2 = Issue.generate_for_project!(@project, :priority => @issue_priority) 353 | @issue3 = Issue.generate_for_project!(@project, :priority => @issue_priority) 354 | 355 | TimeEntry.generate!(:user => User.current, :project => @project, :activity => @activity, :issue => @issue1) 356 | TimeEntry.generate!(:user => User.current, :project => @project, :activity => @activity, :issue => @issue1) 357 | TimeEntry.generate!(:user => User.current, :project => @project, :activity => @activity, :issue => @issue2) 358 | TimeEntry.generate!(:user => User.current, :project => @project, :activity => @activity, :issue => @issue2) 359 | TimeEntry.generate!(:user => User.current, :project => @project, :activity => @activity, :issue => @issue3) 360 | 361 | end 362 | 363 | should 'should clear .time_entries' do 364 | @timesheet.time_entries = { :filled => 'data' } 365 | 366 | previous = @timesheet.time_entries 367 | 368 | @timesheet.fetch_time_entries 369 | 370 | assert_not_same previous, @timesheet.time_entries 371 | end 372 | 373 | should 'should add a time_entry array for each project' do 374 | @timesheet.fetch_time_entries 375 | 376 | assert !@timesheet.time_entries.empty? 377 | assert_equal 1, @timesheet.time_entries.size 378 | end 379 | 380 | should 'should use the project for each time_entry array' do 381 | @timesheet.fetch_time_entries 382 | assert_contains @timesheet.time_entries.keys, @project 383 | end 384 | end 385 | 386 | context "#fetch_time_entries as an administrator" do 387 | 388 | should 'should collect time entries for all users on each project' do 389 | project1 = Project.generate!(:name => "Project 1", :trackers => [@tracker]) 390 | project2 = Project.generate!(:name => "Project 2", :trackers => [@tracker]) 391 | project3 = Project.generate!(:name => "Project 3", :trackers => [@tracker]) 392 | 393 | stub_admin_user 394 | @other_user = User.generate_with_protected!(:admin => false, :firstname => "Non", :lastname => "Member") 395 | 396 | timesheet = timesheet_factory(:activities => [@activity.id], :projects => [project1, project2, project3], :users => [User.current.id, @other_user.id]) 397 | 398 | @te1 = TimeEntry.generate!(:project => project1, :hours => 5, :activity => @activity, :spent_on => Date.today, :user => @current_user) 399 | @te2 = TimeEntry.generate!(:project => project2, :hours => 5, :activity => @activity, :spent_on => Date.today, :user => @other_user) 400 | @te3 = TimeEntry.generate!(:project => project3, :hours => 5, :activity => @activity, :spent_on => Date.today, :user => @other_user) 401 | 402 | timesheet.fetch_time_entries 403 | 404 | assert timesheet.time_entries.present? 405 | assert_same_elements timesheet.time_entries.keys, ["Project 1", "Project 2", "Project 3"] 406 | logs1 = timesheet.time_entries["Project 1"][:logs] 407 | assert_equal 1, logs1.size 408 | assert_same_elements logs1, [@te1] 409 | logs2 = timesheet.time_entries["Project 2"][:logs] 410 | assert_equal 1, logs2.size 411 | assert_same_elements logs2, [@te2] 412 | logs3 = timesheet.time_entries["Project 3"][:logs] 413 | assert_equal 1, logs3.size 414 | assert_same_elements logs3, [@te3] 415 | 416 | users1 = timesheet.time_entries["Project 1"][:users] 417 | assert_equal 1, users1.size 418 | assert_same_elements users1, [User.current] 419 | users2 = timesheet.time_entries["Project 2"][:users] 420 | assert_equal 1, users2.size 421 | assert_same_elements users2, [@other_user] 422 | users3 = timesheet.time_entries["Project 3"][:users] 423 | assert_equal 1, users3.size 424 | assert_same_elements users3, [@other_user] 425 | end 426 | end 427 | 428 | context '#fetch_time_entries as a user with see_project_timesheet permission on a project' do 429 | 430 | should 'should collect time entries for all users' do 431 | project1 = Project.generate!(:name => "Project 1", :trackers => [@tracker]) 432 | project2 = Project.generate!(:name => "Project 2", :trackers => [@tracker]) 433 | project3 = Project.generate!(:name => "Project 3", :trackers => [@tracker]) 434 | 435 | stub_manager_user([project1, project2]) 436 | @other_user = User.generate_with_protected!(:admin => false, :firstname => "Non", :lastname => "Member") 437 | 438 | timesheet = timesheet_factory(:activities => [@activity.id], :projects => [project1, project2, project3], :users => [User.current.id, @other_user.id]) 439 | 440 | @te1 = TimeEntry.generate!(:project => project1, :hours => 5, :activity => @activity, :spent_on => Date.today, :user => @current_user) 441 | @te2 = TimeEntry.generate!(:project => project2, :hours => 5, :activity => @activity, :spent_on => Date.today, :user => @other_user) 442 | @te3 = TimeEntry.generate!(:project => project3, :hours => 5, :activity => @activity, :spent_on => Date.today, :user => @other_user) 443 | 444 | timesheet.fetch_time_entries 445 | 446 | assert timesheet.time_entries.present? 447 | assert_same_elements timesheet.time_entries.keys, ["Project 1", "Project 2"] 448 | logs1 = timesheet.time_entries["Project 1"][:logs] 449 | assert_equal 1, logs1.size 450 | assert_same_elements logs1, [@te1] 451 | logs2 = timesheet.time_entries["Project 2"][:logs] 452 | assert_equal 1, logs2.size 453 | assert_same_elements logs2, [@te2] 454 | 455 | users1 = timesheet.time_entries["Project 1"][:users] 456 | assert_equal 1, users1.size 457 | assert_same_elements users1, [User.current] 458 | users2 = timesheet.time_entries["Project 2"][:users] 459 | assert_equal 1, users2.size 460 | assert_same_elements users2, [@other_user] 461 | end 462 | 463 | context "with the 'see project timesheet' permission" do 464 | should 'xxx return the time entries for all users on that project' do 465 | manager_project = Project.generate!(:name => "Manager Project", :trackers => [@tracker]) 466 | user_project = Project.generate!(:name => "User Project", :trackers => [@tracker]) 467 | 468 | stub_manager_user([manager_project]) 469 | Member.generate!(:principal => @current_user, :project => user_project, :roles => [@normal_role]) 470 | 471 | other_user = User.generate_with_protected!(:admin => false, :firstname => "Other", :lastname => "Member") 472 | 473 | @timesheet = timesheet_factory(:sort => :user, :activities => [@activity.id], :projects => [manager_project, user_project], :users => [User.current.id, other_user.id]) 474 | 475 | @te1 = TimeEntry.generate!(:project => manager_project, :hours => 5, :activity => @activity, :spent_on => Date.today, :user => @current_user) 476 | @te2 = TimeEntry.generate!(:project => manager_project, :hours => 5, :activity => @activity, :spent_on => Date.today, :user => other_user) 477 | @te3 = TimeEntry.generate!(:project => user_project, :hours => 5, :activity => @activity, :spent_on => Date.today, :user => other_user) 478 | 479 | @timesheet.fetch_time_entries 480 | 481 | assert @timesheet.time_entries.present? 482 | assert_same_elements @timesheet.time_entries.keys, [@current_user.name, other_user.name] 483 | 484 | current_user_logs = @timesheet.time_entries[@current_user.name][:logs] 485 | assert_equal 1, current_user_logs.size 486 | assert_same_elements current_user_logs, [@te1] 487 | 488 | other_user_logs = @timesheet.time_entries[other_user.name][:logs] 489 | assert_equal 1, other_user_logs.size 490 | assert_same_elements other_user_logs, [@te2] 491 | assert !other_user_logs.include?(@te3), "Showing other user logs for project which the user doesn't have permission to see" 492 | end 493 | end 494 | end 495 | 496 | context '#fetch_time_entries as a user with view_time_entries permission on a project' do 497 | 498 | setup do 499 | @project1 = project_factory(1, :name => 'Project 1') 500 | @project2 = project_factory(2, :name => 'Project 2') 501 | @archived_project = project_factory(3, :name => 'Archived Project', :status => Project::STATUS_ARCHIVED) 502 | @timesheet = timesheet_factory(:activities => [@activity.id], :projects => [@project1, @project2, @archived_project]) 503 | stub_normal_user([@project1, @project2, @archived_project]) 504 | @te1 = TimeEntry.generate!(:project => @project1, :hours => 5, :activity => @activity, :spent_on => Date.today, :user => @current_user) 505 | @te2 = TimeEntry.generate!(:project => @project1, :hours => 5, :activity => @activity, :spent_on => Date.today, :user => @current_user) 506 | @te_on_archived_project = TimeEntry.generate!(:project => @archived_project, :hours => 5, :activity => @activity, :spent_on => Date.today, :user => @current_user) 507 | 508 | end 509 | 510 | should 'should collect time entries for only themself' do 511 | @timesheet.fetch_time_entries 512 | 513 | assert @timesheet.time_entries.present? 514 | assert_equal ["Project 1"], @timesheet.time_entries.keys 515 | logs = @timesheet.time_entries["Project 1"][:logs] 516 | assert_equal 2, logs.size 517 | assert_same_elements logs, [@te1, @te2] 518 | users = @timesheet.time_entries["Project 1"][:users] 519 | assert_equal 1, users.size 520 | assert_same_elements users, [User.current] 521 | 522 | end 523 | 524 | context "with project status set to all" do 525 | setup do 526 | Setting.plugin_timesheet_plugin['project_status'] = 'all' 527 | end 528 | 529 | should 'collect time entries for archived projects' do 530 | @timesheet.fetch_time_entries 531 | 532 | assert @timesheet.time_entries.present? 533 | assert_equal ["Project 1", "Archived Project"], @timesheet.time_entries.keys 534 | 535 | archived_time_logs = @timesheet.time_entries["Archived Project"][:logs] 536 | assert_equal 1, archived_time_logs.size 537 | assert_same_elements archived_time_logs, [@te_on_archived_project] 538 | 539 | end 540 | end 541 | end 542 | 543 | context '#fetch_time_entries as a non-member of a project' do 544 | 545 | should 'should get no time entries' do 546 | timesheet = timesheet_factory 547 | 548 | project1 = project_factory(1, :name => 'Proejct 1') 549 | project2 = project_factory(2, :name => 'Project 2') 550 | 551 | stub_non_member_user([project1, project2]) 552 | timesheet.projects = [project1, project2] 553 | 554 | timesheet.fetch_time_entries 555 | assert timesheet.time_entries.empty? 556 | end 557 | end 558 | 559 | context '#period=' do 560 | 561 | context 'should set the date_to and date_from for' do 562 | setup do 563 | @date = Date.new(2009,2,4) 564 | Date.stubs(:today).returns(@date) 565 | @timesheet = Timesheet.new(:period_type => Timesheet::ValidPeriodType[:default]) 566 | end 567 | 568 | should 'today' do 569 | @timesheet.period = 'today' 570 | assert_equal @date, @timesheet.date_from 571 | assert_equal @date, @timesheet.date_to 572 | end 573 | 574 | should 'yesterday' do 575 | @timesheet.period = 'yesterday' 576 | assert_equal @date.yesterday, @timesheet.date_from 577 | assert_equal @date.yesterday, @timesheet.date_to 578 | end 579 | 580 | should 'current_week' do 581 | @timesheet.period = 'current_week' 582 | assert_equal Date.new(2009,2,2), @timesheet.date_from 583 | assert_equal Date.new(2009,2,8), @timesheet.date_to 584 | end 585 | 586 | should 'last_week' do 587 | @timesheet.period = 'last_week' 588 | assert_equal Date.new(2009,1,26), @timesheet.date_from 589 | assert_equal Date.new(2009,2,1), @timesheet.date_to 590 | end 591 | 592 | should '7_days' do 593 | @timesheet.period = '7_days' 594 | assert_equal @date - 7, @timesheet.date_from 595 | assert_equal @date, @timesheet.date_to 596 | end 597 | 598 | should 'current_month' do 599 | @timesheet.period = 'current_month' 600 | assert_equal Date.new(2009,2,1), @timesheet.date_from 601 | assert_equal Date.new(2009,2,28), @timesheet.date_to 602 | end 603 | 604 | should 'last_month' do 605 | @timesheet.period = 'last_month' 606 | assert_equal Date.new(2009,1,1), @timesheet.date_from 607 | assert_equal Date.new(2009,1,31), @timesheet.date_to 608 | end 609 | 610 | should '30_days' do 611 | @timesheet.period = '30_days' 612 | assert_equal @date - 30, @timesheet.date_from 613 | assert_equal @date, @timesheet.date_to 614 | end 615 | 616 | should 'current_year' do 617 | @timesheet.period = 'current_year' 618 | assert_equal Date.new(2009,1,1), @timesheet.date_from 619 | assert_equal Date.new(2009,12,31), @timesheet.date_to 620 | end 621 | 622 | should 'all' do 623 | @timesheet.period = 'all' 624 | assert_equal nil, @timesheet.date_from 625 | assert_equal nil, @timesheet.date_to 626 | end 627 | end 628 | end 629 | 630 | context '#to_csv' do 631 | setup do 632 | stub_admin_user 633 | @another_user = User.generate_with_protected!(:admin => true, :firstname => 'Another', :lastname => 'user') 634 | @project = Project.generate!(:trackers => [@tracker], :name => 'Project Name') 635 | end 636 | 637 | context "sorted by :user" do 638 | should "should return a csv grouped by user" do 639 | timesheet = timesheet_factory(:sort => :user, :users => [User.current.id, @another_user.id], :projects => [@project.id], :activities => [@activity.id], :date_from => '2009-04-05', :date_to => '2009-04-05') 640 | issue = Issue.generate_for_project!(@project, :tracker => @tracker, :priority => @issue_priority, :subject => 'Issue 1') 641 | 642 | 643 | time_entries = [ 644 | time_entry_factory(1, stub_common_csv_records(:activity => @activity, :project => @project,:issue => issue).merge({})), 645 | time_entry_factory(3, stub_common_csv_records(:activity => @activity, :project => @project,:issue => issue).merge({})), 646 | time_entry_factory(4, stub_common_csv_records(:activity => @activity, :project => @project,:issue => issue).merge({})), 647 | time_entry_factory(5, stub_common_csv_records(:activity => @activity, :project => @project,:issue => nil)) 648 | ] 649 | 650 | time_entries_another_user = [ 651 | time_entry_factory(2, stub_common_csv_records(:project => @project, :issue => issue).merge({:user => @another_user })) 652 | ] 653 | 654 | timesheet.fetch_time_entries 655 | 656 | # trailing newline 657 | assert_equal [ 658 | "#,Date,Member,Activity,Project,Issue,Issue Subject,Comment,Hours", 659 | "1,2009-04-05,Administrator Bob,activity,Project Name,Tracker #1,Issue 1,comments,10.0", 660 | "3,2009-04-05,Administrator Bob,activity,Project Name,Tracker #1,Issue 1,comments,10.0", 661 | "4,2009-04-05,Administrator Bob,activity,Project Name,Tracker #1,Issue 1,comments,10.0", 662 | "5,2009-04-05,Administrator Bob,activity,Project Name,,,comments,10.0", 663 | "2,2009-04-05,Another user,activity,Project Name,Tracker #1,Issue 1,comments,10.0", 664 | ].join("\n") + "\n", timesheet.to_csv 665 | 666 | end 667 | end 668 | 669 | context "sorted by :project" do 670 | should "should return a csv grouped by project" do 671 | 672 | another_project = Project.generate!(:trackers => [@tracker], :name => 'Another Project') 673 | timesheet = timesheet_factory(:sort => :project, :users => [User.current.id, @another_user.id], :projects => [@project, another_project], :activities => [@activity.id], :date_from => '2009-04-05', :date_to => '2009-04-05') 674 | issue = Issue.generate_for_project!(@project, :tracker => @tracker, :priority => @issue_priority, :subject => 'Issue 1') 675 | another_issue = Issue.generate_for_project!(another_project, :tracker => @tracker, :priority => @issue_priority, :subject => 'Issue 2') 676 | 677 | project_a_time_entries = [ 678 | time_entry_factory(1, stub_common_csv_records({:activity => @activity, :project => @project,:issue => issue})), 679 | time_entry_factory(3, stub_common_csv_records({:activity => @activity, :project => @project,:issue => issue})), 680 | time_entry_factory(5, stub_common_csv_records({:activity => @activity, :project => @project,:issue => nil})) 681 | ] 682 | 683 | another_project_time_entries = [ 684 | time_entry_factory(2, stub_common_csv_records({:activity => @activity,:user => @another_user, :project => another_project,:issue => another_issue })), 685 | time_entry_factory(4, stub_common_csv_records({:activity => @activity, :project => another_project,:issue => another_issue})) 686 | 687 | ] 688 | 689 | timesheet.fetch_time_entries 690 | # trailing newline 691 | assert_equal [ 692 | "#,Date,Member,Activity,Project,Issue,Issue Subject,Comment,Hours", 693 | "2,2009-04-05,Another user,activity,Another Project,Tracker #2,Issue 2,comments,10.0", 694 | "4,2009-04-05,Administrator Bob,activity,Another Project,Tracker #2,Issue 2,comments,10.0", 695 | "1,2009-04-05,Administrator Bob,activity,Project Name,Tracker #1,Issue 1,comments,10.0", 696 | "3,2009-04-05,Administrator Bob,activity,Project Name,Tracker #1,Issue 1,comments,10.0", 697 | "5,2009-04-05,Administrator Bob,activity,Project Name,,,comments,10.0", 698 | ].join("\n") + "\n", timesheet.to_csv 699 | end 700 | end 701 | 702 | context "sorted by :issue" do 703 | should "should return a csv grouped by issue" do 704 | another_project = Project.generate!(:trackers => [@tracker], :name => 'Another Project') 705 | 706 | @issue1 = Issue.generate_for_project!(@project, :tracker => @tracker, :priority => @issue_priority, :subject => 'Issue 1') 707 | @issue1.time_entries << time_entry_factory(1, stub_common_csv_records({:activity => @activity, :project => @project})) 708 | 709 | @issue2 = Issue.generate_for_project!(@project, :tracker => @tracker, :priority => @issue_priority, :subject => 'Issue 2') 710 | @issue2.time_entries << time_entry_factory(3, stub_common_csv_records({:activity => @activity, :project => @project})) 711 | 712 | @issue3 = Issue.generate_for_project!(another_project, :tracker => @tracker, :priority => @issue_priority, :subject => 'Issue 3') 713 | @issue3.time_entries << time_entry_factory(2, stub_common_csv_records({:user => @another_user, :activity => @activity, :project => another_project})) 714 | 715 | @issue4 = Issue.generate_for_project!(another_project, :tracker => @tracker, :priority => @issue_priority, :subject => 'Issue 4') 716 | @issue4.time_entries << time_entry_factory(4, stub_common_csv_records({:activity => @activity, :project => another_project})) 717 | 718 | timesheet = timesheet_factory(:sort => :issue, :users => [User.current.id, @another_user.id], :projects => [@project, another_project], :activities => [@activity.id], :date_from => '2009-04-05', :date_to => '2009-04-05') 719 | 720 | timesheet.fetch_time_entries 721 | assert_equal [ 722 | "#,Date,Member,Activity,Project,Issue,Issue Subject,Comment,Hours", 723 | "2,2009-04-05,Another user,activity,Another Project,Tracker #3,Issue 3,comments,10.0", 724 | "4,2009-04-05,Administrator Bob,activity,Another Project,Tracker #4,Issue 4,comments,10.0", 725 | "1,2009-04-05,Administrator Bob,activity,Project Name,Tracker #1,Issue 1,comments,10.0", 726 | "3,2009-04-05,Administrator Bob,activity,Project Name,Tracker #2,Issue 2,comments,10.0", 727 | ].join("\n") + "\n", timesheet.to_csv 728 | 729 | end 730 | end 731 | end 732 | end 733 | -------------------------------------------------------------------------------- /timesheet_plugin.gemspec: -------------------------------------------------------------------------------- 1 | # Generated by jeweler 2 | # DO NOT EDIT THIS FILE DIRECTLY 3 | # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command 4 | # -*- encoding: utf-8 -*- 5 | 6 | Gem::Specification.new do |s| 7 | s.name = %q{timesheet_plugin} 8 | s.version = "0.6.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{2010-03-18} 13 | s.description = %q{A plugin to show and filter timelogs across all projects in Redmine.} 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/timesheet_controller.rb", 26 | "app/helpers/timesheet_helper.rb", 27 | "app/models/timesheet.rb", 28 | "app/views/settings/_timesheet_settings.rhtml", 29 | "app/views/timesheet/_by_issue.rhtml", 30 | "app/views/timesheet/_form.rhtml", 31 | "app/views/timesheet/_issue_time_entries.rhtml", 32 | "app/views/timesheet/_time_entry.rhtml", 33 | "app/views/timesheet/_timesheet_group.rhtml", 34 | "app/views/timesheet/context_menu.html.erb", 35 | "app/views/timesheet/index.rhtml", 36 | "app/views/timesheet/no_projects.rhtml", 37 | "app/views/timesheet/report.rhtml", 38 | "app/views/timesheet/timelog.rhtml", 39 | "assets/images/csv.png", 40 | "assets/images/toggle-arrow-closed.gif", 41 | "assets/images/toggle-arrow-open.gif", 42 | "assets/javascripts/timesheet.js", 43 | "assets/stylesheets/timesheet.css", 44 | "config/locales/ca.yml", 45 | "config/locales/cs.yml", 46 | "config/locales/da.yml", 47 | "config/locales/de.yml", 48 | "config/locales/en.yml", 49 | "config/locales/es.yml", 50 | "config/locales/fr.yml", 51 | "config/locales/hu.yml", 52 | "config/locales/hy.yml", 53 | "config/locales/it.yml", 54 | "config/locales/ja.yml", 55 | "config/locales/lt.yml", 56 | "config/locales/pl.yml", 57 | "config/locales/pt-br.yml", 58 | "config/locales/ru.yml", 59 | "config/locales/sr.yml", 60 | "config/locales/sv.yml", 61 | "config/locales/uk.yml", 62 | "config/routes.rb", 63 | "init.rb", 64 | "lang/ca.yml", 65 | "lang/cs.yml", 66 | "lang/da.yml", 67 | "lang/de.yml", 68 | "lang/en.yml", 69 | "lang/es.yml", 70 | "lang/fr.yml", 71 | "lang/hu.yml", 72 | "lang/hy.yml", 73 | "lang/it.yml", 74 | "lang/ja.yml", 75 | "lang/lt.yml", 76 | "lang/pl.yml", 77 | "lang/pt-br.yml", 78 | "lang/ru.yml", 79 | "lang/sr.yml", 80 | "lang/sv.yml", 81 | "lang/uk.yml", 82 | "lib/timesheet_compatibility.rb", 83 | "rails/init.rb", 84 | "test/functional/timesheet_controller_test.rb", 85 | "test/integration/timesheet_menu_test.rb", 86 | "test/test_helper.rb", 87 | "test/unit/sanity_test.rb", 88 | "test/unit/timesheet_test.rb" 89 | ] 90 | s.homepage = %q{https://projects.littlestreamsoftware.com/projects/redmine-timesheet} 91 | s.rdoc_options = ["--charset=UTF-8"] 92 | s.require_paths = ["lib"] 93 | s.rubygems_version = %q{1.3.5} 94 | s.summary = %q{A Timesheet plugin for Redmine to show timelogs for all projects} 95 | s.test_files = [ 96 | "test/test_helper.rb", 97 | "test/integration/timesheet_menu_test.rb", 98 | "test/unit/timesheet_test.rb", 99 | "test/unit/sanity_test.rb", 100 | "test/functional/timesheet_controller_test.rb" 101 | ] 102 | 103 | if s.respond_to? :specification_version then 104 | current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION 105 | s.specification_version = 3 106 | 107 | if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then 108 | else 109 | end 110 | else 111 | end 112 | end 113 | 114 | --------------------------------------------------------------------------------