pax_global_header00006660000000000000000000000064147302662340014521gustar00rootroot0000000000000052 comment=dd4d7b3daf02cb458505d5c7f67e768b8ac3f308 TaskJuggler-3.8.1/000077500000000000000000000000001473026623400137545ustar00rootroot00000000000000TaskJuggler-3.8.1/.gemtest000066400000000000000000000000001473026623400154130ustar00rootroot00000000000000TaskJuggler-3.8.1/.gitignore000066400000000000000000000010161473026623400157420ustar00rootroot00000000000000benchmarks/css benchmarks/icons benchmarks/scripts CHANGELOG doc lib/css lib/*.html lib/icons lib/scripts lib/tags lib/test* lib/*.tjp manual/html pkg Rakefile rcov refman test/TestSuite/HTML-Reports/css test/TestSuite/HTML-Reports/*.html test/TestSuite/HTML-Reports/icons test/TestSuite/HTML-Reports/scripts test/TestSuite/Syntax/Correct/css test/TestSuite/Syntax/Correct/icons test/TestSuite/Syntax/Correct/scripts test/TestSuite/Syntax/Correct/*.html test/TestSuite/Syntax/Correct/*.csv test/TestSuite/Syntax/Correct/*.xml TaskJuggler-3.8.1/COPYING000066400000000000000000000432541473026623400150170ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. TaskJuggler-3.8.1/README.rdoc000066400000000000000000000126631473026623400155720ustar00rootroot00000000000000= About {TaskJuggler}[http://www.taskjuggler.org] TaskJuggler is a modern and powerful, Free and Open Source Software project management tool. Its new approach to project planning and tracking is more flexible and superior to the commonly used Gantt chart editing tools. TaskJuggler is project management software for serious project managers. It covers the complete spectrum of project management tasks from the first idea to the completion of the project. It assists you during project scoping, resource assignment, cost and revenue planning, risk management, and communication management. TaskJuggler provides an optimizing scheduler that computes your project time lines and resource assignments based on the project outline and the constraints that you have provided. The built-in resource balancer and consistency checker will offload you from having to worry about irrelevant details and it will ring the alarm if the project gets out of hand. The flexible as-many-details-as-necessary approach allows you to plan your project as you go, making it also ideal for new management strategies such as Extreme Programming and Agile Project Management. If you are about to build a skyscraper or just want to put together the release schedule of your open source project, TaskJuggler is the right tool for you. If you just want to draw nice looking Gantt charts to impress your boss or your investors, TaskJuggler might not be right for you. You can of course create nice looking Gantt charts. But it takes a little more effort to master its power. If you are up for this, TaskJuggler will become a companion that you don't want to miss anymore. TaskJuggler is written in {Ruby}[http://www.ruby-lang.org/en/] and should be easily installable and usable on all popular operating systems. It may sound surprising at first, but this software does not need a graphical user interface. A command shell, a plain text editor (no word processor!), and a web browser is all you need for your work. = Features and Highlights == Basic Properties * Manages tasks, resources and accounts of your project * Powerful to-do list management * Detailed reference manual * Simple installation * Runs on all Linux, Unix, Windows, MacOS and several other operating systems * Full integration with Vim text editor == Advanced Scheduling * Automatic resource leveling and tasks conflict resolution * Unlimited number of scenarios (baselines) of the same project for what-if analysis * Flexible working hours and leave management * Support for shift working * Multiple time zone support == Accounting * Tasks may have initial costs, finishing costs * Resources may have usage based costs * Task and/or resource base cost models * Support for profit/loss analysis == Reporting * Comprehensive and flexible reports so you can find the information you need when you need it * Powerful filtering functions to provide the right amount of detail to the right audience * Time and status sheet reporting infrastructure * Project tracking and status reporting with dashboard support == Scaling and Enterprise Features * Projects can be combined into larger projects * Support for central resource allocation database * Manages roles and complex reporting lines * Powerful project description language with macro support * Scales well on multi-core or multi-CPU systems * Support for project management teams and revision control systems * Data export to Microsoft Project and Computer Associates Clarity == Web Publishing and Groupware Functions * HTML reports for web publishing * CSV data export for exchange with popular office software * iCalendar export for data exchange with calendar and productivity applications * Built-in web server for dynamic and interactive reports * Server based time sheet system for status and actual work reporting = Installation Installation instructions can be found {here}[http://www.taskjuggler.org/tj3/manual/Installation.html#Installation]. = Introduction and Tutorial To learn more about how to use TaskJuggler please see the {user manual}[http://www.taskjuggler.org/tj3/manual/index.html]. It also contains a {tutorial}[http://www.taskjuggler.org/tj3/manual/Tutorial.html#The_Tutorial_Your_first_Project] to get you started. It will tell you how to generate HTML reports like {these}[http://www.taskjuggler.org/tj3/examples/Tutorial/Overview.html] from such a {project description}[http://www.taskjuggler.org/tj3/examples/Tutorial/tutorial.tjp]. = Getting Help and reporting Bugs There are several mailing list for TaskJuggler users and developers. Please see {this page}[http://www.taskjuggler.org/contact.html] for details. If you have a question about TaskJuggler usage, the user list is the best place to go. Please subscribe to the mailing list and help other users when you get more proficient in TaskJuggler. If you think that you have found a bug in the software, please view the {bug reporting guidelines}[http://www.taskjuggler.org/tj3/manual/Reporting_Bugs.html#Reporting_Bugs_and_Sending_Feedback]. = Copyright and License TaskJuggler is (c) 2006, 2007, 2008, 2009, 2010, 2011 by Chris Schlaeger This program is free software; you can redistribute it and/or modify it under the terms of {version 2 of the GNU General Public License}[http://www.gnu.org/licenses/old-licenses/gpl-2.0.html] as published by the Free Software Foundation. You accept the terms of this license by distributing or using this software. TaskJuggler[http://www.taskjuggler.org] is a trademark of Chris Schlaeger. TaskJuggler-3.8.1/Rakefile000066400000000000000000000010171473026623400154200ustar00rootroot00000000000000$:.unshift File.join(File.dirname(__FILE__)) # Add the lib directory to the search path if it isn't included already lib = File.expand_path('../lib', __FILE__) $:.unshift lib unless $:.include?(lib) require 'rake' require 'rspec' require 'rake/clean' require 'bundler/gem_tasks' Dir.glob( 'tasks/*.rake').each do |fn| begin load fn; rescue LoadError puts "#{fn.split('/')[1]} tasks unavailable: #{$!}" end end task :default => [ :test ] desc 'Run all unit and spec tests' task :test => [ :unittest, :spec ] TaskJuggler-3.8.1/TODO000066400000000000000000000032771473026623400144550ustar00rootroot00000000000000= Todo list for TaskJuggler 3.x This is a possibly still incomplete list of things that still could or should be done for TaskJuggler 3.x. == Missing features for 2.x compatibility * Add missing functions for logical expressions. * Missing reports (calendar) * More unit tests for core classes. * Lots more test cases for the test suite. == New features (probably after first stable release) * Make web server reports more dynamic with controller elements. * Add support for an optional account attribute to be used instead of the defined charge set. task "Foo" { charge 2000 onstart { account someacc } } * Evaluate if account name space should be hierarchical. * Delta reports that show only the differences between 2 arbitrary scenarios. * Per scenario purging * User defined numerical attributes that add-up. * Critical path detection after scheduling. * SVG reports * Pert charts * Highlight dependency arrows of a task with mouse-over effect. * Make gap ends align with certain times or days of week, day of weeks or day of months. = Discontinued 2.x features TaskJuggler III should be mostly compatible with 2.x. Existing projects should be usable with TaskJuggler III with no to minimal effort. Nevertheless we will use the transition to TaskJuggler III as an opportunity to drop some things that have been badly designed or are of too little use to justify the porting efforts. There is currently no plan to port the TaskJuggler 2.x GUI due to lack of time/resources. Help in this area will be greatly appreciated. * XML file reading. * htmlstatusreport. This report can now be created with composed reports. * The syntax for reports has changed somewhat. See manual for details. TaskJuggler-3.8.1/benchmarks/000077500000000000000000000000001473026623400160715ustar00rootroot00000000000000TaskJuggler-3.8.1/benchmarks/666tasks.tjp000066400000000000000000001527201473026623400202060ustar00rootroot00000000000000project "Benchmark" 2010-06-19 +3y shift parttime "Part Time" { workinghours mon, wed, fri 8:00 - 12:00 } resource a "A" resource b "B" resource c "C" resource d "D" resource e "E" resource f "F" resource g "G" resource h "H" task g1 "Group 1" { task sg1 "Sub Group 1" { task t1 "Task 1" { effort 2d allocate a } task t2 "Task 2" { length 2d priority 600 allocate b depends !t1 } task t3 "Task 3" { duration 4d allocate a { alternative b } priority 200 } task t4 "Task 4" { depends !t1 length 5d allocate c priority 800 limits { dailymax 2h } } task t5 "Task 5" { effort 10d allocate d } task t6 "Task 6" { depends !t5 effort 5d allocate e { mandatory } allocate f { alternative g } } task t7 "Task 7" { duration 5d depends !t1 allocate e priority 900 shifts parttime } task t8 "Task 8" { depends !t4 milestone } task t9 "Task 9" { depends !t6 } task t10 "Task 10" { effort 3d allocate h { alternative a } } } task sg2 "Sub Group 2" { depends !sg1 task t1 "Task 1" { effort 2d allocate a } task t2 "Task 2" { length 2d priority 600 allocate b depends !t1 } task t3 "Task 3" { duration 4d allocate a { alternative b } priority 200 } task t4 "Task 4" { depends !t1 length 5d allocate c priority 800 limits { dailymax 2h } } task t5 "Task 5" { effort 10d allocate d } task t6 "Task 6" { depends !t5 effort 5d allocate e { mandatory } allocate f { alternative g } } task t7 "Task 7" { duration 5d depends !t1 } task t8 "Task 8" { depends !t4 milestone } task t9 "Task 9" { depends !t6 } task t10 "Task 10" { effort 3d allocate h { alternative a } } } task sg3 "Sub Group 3" { task t1 "Task 1" { effort 2d allocate a } task t2 "Task 2" { length 2d priority 600 allocate b depends !t1 } task t3 "Task 3" { duration 15d allocate a { alternative b, c, d, e, f, g } priority 200 } task t4 "Task 4" { depends !t1 length 5d allocate c priority 800 limits { dailymax 2h } } task t5 "Task 5" { effort 10d allocate d } task t6 "Task 6" { depends !t5 effort 5d allocate e { mandatory } allocate f { alternative g } } task t7 "Task 7" { duration 5d depends !t1 } task t8 "Task 8" { depends !t4 milestone } task t9 "Task 9" { depends !t6 } task t10 "Task 10" { effort 3d allocate h { alternative a } } } task sg4 "Sub Group 4" { depends !sg2 task t1 "Task 1" { effort 2d allocate a } task t2 "Task 2" { length 2d priority 600 allocate b depends !t1 } task t3 "Task 3" { duration 4d allocate a { alternative b } priority 200 } task t4 "Task 4" { depends !t1 length 5d allocate c priority 800 limits { dailymax 2h } } task t5 "Task 5" { effort 10d allocate d } task t6 "Task 6" { depends !t5 effort 5d allocate e { mandatory } allocate f { alternative g } } task t7 "Task 7" { duration 5d depends !t1 allocate e priority 900 shifts parttime } task t8 "Task 8" { depends !t4 milestone } task t9 "Task 9" { depends !t6 } task t10 "Task 10" { effort 3d allocate h { alternative a } } } task sg5 "Sub Group 5" { task t1 "Task 1" { effort 2d allocate a } task t2 "Task 2" { length 2d priority 600 allocate b depends !t1 } task t3 "Task 3" { duration 4d } task t4 "Task 4" { depends !t1 length 5d allocate c priority 800 limits { dailymax 2h } } task t5 "Task 5" { effort 10d allocate d } task t6 "Task 6" { depends !t5 effort 5d allocate e { mandatory } allocate f { alternative g } } task t7 "Task 7" { effort 5d depends !t1 allocate e priority 900 shifts parttime } task t8 "Task 8" { depends !t4 milestone } task t9 "Task 9" { depends !t6 } task t10 "Task 10" { effort 3d allocate h { alternative a } } } task sg6 "Sub Group 6" { depends !sg4 task t1 "Task 1" { effort 2d allocate a } task t2 "Task 2" { length 2d priority 600 allocate b depends !t1 } task t3 "Task 3" { duration 4d allocate a { alternative b } priority 200 } task t4 "Task 4" { depends !t1 length 5d allocate c priority 800 limits { dailymax 2h } } task t5 "Task 5" { effort 10d allocate d } task t6 "Task 6" { depends !t5 effort 5d allocate e { mandatory } allocate f { alternative g } } task t7 "Task 7" { duration 5d depends !t1 allocate e priority 900 shifts parttime } task t8 "Task 8" { depends !t4 milestone } task t9 "Task 9" { depends !t6 } task t10 "Task 10" { effort 3d allocate h { alternative a } } } task sg7 "Sub Group 7" { task t1 "Task 1" { effort 2d allocate a } task t2 "Task 2" { length 2d priority 600 allocate b depends !t1 } task t3 "Task 3" { duration 4d } task t4 "Task 4" { depends !t1 length 5d allocate c priority 800 limits { dailymax 2h } } task t5 "Task 5" { effort 10d allocate d } task t6 "Task 6" { depends !t5 effort 5d allocate e { mandatory } allocate f { alternative g } } task t7 "Task 7" { duration 5d depends !t1 allocate e priority 900 shifts parttime } task t8 "Task 8" { depends !t4 milestone } task t9 "Task 9" { depends !t6 } task t10 "Task 10" { effort 3d allocate h { alternative a } } } task sg8 "Sub Group 8" { depends !sg7 task t1 "Task 1" { effort 2d allocate a } task t2 "Task 2" { length 2d priority 600 allocate b depends !t1 } task t3 "Task 3" { duration 4d allocate a { alternative b } priority 200 } task t4 "Task 4" { depends !t1 length 5d allocate c priority 800 limits { dailymax 2h } } task t5 "Task 5" { effort 10d allocate d } task t6 "Task 6" { depends !t5 effort 5d allocate e { mandatory } allocate f { alternative g } } task t7 "Task 7" { duration 15d depends !t1 allocate e, f, g, h priority 900 shifts parttime } task t8 "Task 8" { depends !t4 milestone } task t9 "Task 9" { depends !t6 } task t10 "Task 10" { effort 3d allocate h { alternative a } } } task sg9 "Sub Group 9" { task t1 "Task 1" { effort 2d allocate a } task t2 "Task 2" { length 2d priority 600 allocate b depends !t1 } task t3 "Task 3" { duration 20d allocate a { alternative b, c, d, e, g, h } priority 200 } task t4 "Task 4" { depends !t1 length 5d allocate c priority 800 limits { dailymax 2h } } task t5 "Task 5" { effort 10d allocate d } task t6 "Task 6" { depends !t5 effort 5d allocate e { mandatory } allocate f { alternative g } } task t7 "Task 7" { effort 5d depends !t1 allocate e, f, g, h priority 900 shifts parttime } task t8 "Task 8" { depends !t4 milestone } task t9 "Task 9" { depends !t6 } task t10 "Task 10" { effort 3d allocate h { alternative a } } } task sg10 "Sub Group 10" { depends !sg6 task t1 "Task 1" { effort 2d allocate a } task t2 "Task 2" { length 2d priority 600 allocate b depends !t1 } task t3 "Task 3" { duration 4d allocate a { alternative b } priority 200 } task t4 "Task 4" { depends !t1 length 5d allocate c priority 800 limits { dailymax 2h } } task t5 "Task 5" { effort 10d allocate d } task t6 "Task 6" { depends !t5 effort 5d allocate e { mandatory } allocate f { alternative g } } task t7 "Task 7" { duration 5d depends !t1 allocate e priority 900 shifts parttime } task t8 "Task 8" { depends !t4 milestone } task t9 "Task 9" { depends !t6 } task t10 "Task 10" { effort 3d allocate h { alternative a } } } } task g2 "Group 2" { depends !g1 task sg1 "Sub Group 1" { task t1 "Task 1" { effort 2d allocate a } task t2 "Task 2" { length 2d priority 600 allocate b depends !t1 } task t3 "Task 3" { duration 4d } task t4 "Task 4" { depends !t1 length 5d allocate c priority 800 limits { dailymax 2h } } task t5 "Task 5" { effort 10d allocate d } task t6 "Task 6" { depends !t5 effort 5d allocate e { mandatory } allocate f { alternative g } } task t7 "Task 7" { duration 5d depends !t1 allocate e priority 900 shifts parttime } task t8 "Task 8" { depends !t4 milestone } task t9 "Task 9" { depends !t6 } task t10 "Task 10" { effort 3d allocate h { alternative a } } } task sg2 "Sub Group 2" { depends !sg1 task t1 "Task 1" { effort 2d allocate a } task t2 "Task 2" { length 2d priority 600 allocate b depends !t1 } task t3 "Task 3" { duration 4d allocate a { alternative b } priority 200 } task t4 "Task 4" { depends !t1 length 5d allocate c priority 800 limits { dailymax 2h } } task t5 "Task 5" { effort 10d allocate d } task t6 "Task 6" { depends !t5 effort 5d allocate e { mandatory } allocate f { alternative g } } task t7 "Task 7" { duration 5d depends !t1 } task t8 "Task 8" { depends !t4 milestone } task t9 "Task 9" { depends !t6 } task t10 "Task 10" { effort 3d allocate h { alternative a } } } task sg3 "Sub Group 3" { task t1 "Task 1" { effort 2d allocate a } task t2 "Task 2" { length 2d priority 600 allocate b depends !t1 } task t3 "Task 3" { duration 4d } task t4 "Task 4" { depends !t1 length 5d allocate c priority 800 limits { dailymax 2h } } task t5 "Task 5" { effort 10d allocate d } task t6 "Task 6" { depends !t5 effort 5d allocate e { mandatory } allocate f { alternative g } } task t7 "Task 7" { duration 5d depends !t1 allocate e priority 900 shifts parttime } task t8 "Task 8" { depends !t4 milestone } task t9 "Task 9" { depends !t6 } task t10 "Task 10" { effort 3d allocate h { alternative a } } } task sg4 "Sub Group 4" { depends !sg2 task t1 "Task 1" { effort 2d allocate a } task t2 "Task 2" { length 2d priority 600 allocate b depends !t1 } task t3 "Task 3" { duration 20d allocate a { alternative b, c, d, f, g, h } priority 400 } task t4 "Task 4" { depends !t1 length 5d allocate c priority 800 limits { dailymax 2h } } task t5 "Task 5" { effort 10d allocate d } task t6 "Task 6" { depends !t5 effort 5d allocate e { mandatory } allocate f { alternative g } } task t7 "Task 7" { duration 5d depends !t1 allocate e priority 900 shifts parttime } task t8 "Task 8" { depends !t4 milestone } task t9 "Task 9" { depends !t6 } task t10 "Task 10" { effort 3d allocate h { alternative a } } } task sg5 "Sub Group 5" { task t1 "Task 1" { effort 2d allocate a } task t2 "Task 2" { length 2d priority 600 allocate b depends !t1 } task t3 "Task 3" { duration 15d allocate a { alternative b, c, d, e, f, g } priority 200 } task t4 "Task 4" { depends !t1 length 5d allocate c priority 800 limits { dailymax 2h } } task t5 "Task 5" { effort 10d allocate d } task t6 "Task 6" { depends !t5 effort 5d allocate e { mandatory } allocate f { alternative g } } task t7 "Task 7" { duration 5d depends !t1 } task t8 "Task 8" { depends !t4 milestone } task t9 "Task 9" { depends !t6 } task t10 "Task 10" { effort 3d allocate h { alternative a } } } task sg6 "Sub Group 6" { depends !sg4 task t1 "Task 1" { effort 2d allocate a } task t2 "Task 2" { length 2d priority 600 allocate b depends !t1 } task t3 "Task 3" { duration 4d allocate a { alternative b } priority 200 } task t4 "Task 4" { depends !t1 length 5d allocate c priority 800 limits { dailymax 2h } } task t5 "Task 5" { effort 10d allocate d } task t6 "Task 6" { depends !t5 effort 5d allocate e { mandatory } allocate f { alternative g } } task t7 "Task 7" { duration 5d depends !t1 allocate e priority 900 shifts parttime } task t8 "Task 8" { depends !t4 milestone } task t9 "Task 9" { depends !t6 } task t10 "Task 10" { effort 3d allocate h { alternative a } } } task sg7 "Sub Group 7" { task t1 "Task 1" { effort 2d allocate a } task t2 "Task 2" { length 2d priority 600 allocate b depends !t1 } task t3 "Task 3" { duration 4d } task t4 "Task 4" { depends !t1 length 5d allocate c priority 800 limits { dailymax 2h } } task t5 "Task 5" { effort 10d allocate d } task t6 "Task 6" { depends !t5 effort 5d allocate e { mandatory } allocate f { alternative g } } task t7 "Task 7" { duration 5d depends !t1 allocate e priority 900 shifts parttime } task t8 "Task 8" { depends !t4 milestone } task t9 "Task 9" { depends !t6 } task t10 "Task 10" { effort 3d allocate h { alternative a } } } task sg8 "Sub Group 8" { depends !sg7 task t1 "Task 1" { effort 2d allocate a } task t2 "Task 2" { length 2d priority 600 allocate b depends !t1 } task t3 "Task 3" { duration 4d allocate a { alternative b } priority 200 } task t4 "Task 4" { depends !t1 length 5d allocate c priority 800 limits { dailymax 2h } } task t5 "Task 5" { effort 10d allocate d } task t6 "Task 6" { depends !t5 effort 5d allocate e { mandatory } allocate f { alternative g } } task t7 "Task 7" { duration 25d depends !t1 allocate e, a, b, c, d, f priority 900 shifts parttime } task t8 "Task 8" { depends !t4 milestone } task t9 "Task 9" { depends !t6 } task t10 "Task 10" { effort 3d allocate h { alternative a } } } task sg9 "Sub Group 9" { task t1 "Task 1" { effort 2d allocate a } task t2 "Task 2" { length 2d priority 600 allocate b depends !t1 } task t3 "Task 3" { duration 20d allocate a { alternative b, c, d, f, h } priority 500 } task t4 "Task 4" { depends !t1 length 5d allocate c priority 800 limits { dailymax 2h } } task t5 "Task 5" { effort 10d allocate d } task t6 "Task 6" { depends !t5 effort 5d allocate e { mandatory } allocate f { alternative g } } task t7 "Task 7" { duration 5d depends !t1 allocate e, a, b, c, d priority 900 shifts parttime } task t8 "Task 8" { depends !t4 milestone } task t9 "Task 9" { depends !t6 } task t10 "Task 10" { effort 3d allocate h { alternative a } } } task sg10 "Sub Group 10" { depends !sg6 task t1 "Task 1" { effort 2d allocate a } task t2 "Task 2" { length 2d priority 600 allocate b depends !t1 } task t3 "Task 3" { duration 4d allocate a { alternative b } priority 200 } task t4 "Task 4" { depends !t1 length 5d allocate c priority 800 limits { dailymax 2h } } task t5 "Task 5" { effort 10d allocate d } task t6 "Task 6" { depends !t5 effort 5d allocate e { mandatory } allocate f { alternative g } } task t7 "Task 7" { duration 5d depends !t1 allocate e priority 900 shifts parttime } task t8 "Task 8" { depends !t4 milestone } task t9 "Task 9" { depends !t6 } task t10 "Task 10" { effort 3d allocate h { alternative a } } } } task g3 "Group 3" { task sg1 "Sub Group 1" { task t1 "Task 1" { effort 2d allocate a } task t2 "Task 2" { length 2d priority 600 allocate b depends !t1 } task t3 "Task 3" { duration 4d } task t4 "Task 4" { depends !t1 length 5d allocate c priority 800 limits { dailymax 2h } } task t5 "Task 5" { effort 10d allocate d } task t6 "Task 6" { depends !t5 effort 5d allocate e { mandatory } allocate f { alternative g } } task t7 "Task 7" { duration 5d depends !t1 allocate e priority 900 shifts parttime } task t8 "Task 8" { depends !t4 milestone } task t9 "Task 9" { depends !t6 } task t10 "Task 10" { effort 3d allocate h { alternative a } } } task sg2 "Sub Group 2" { depends !sg1 task t1 "Task 1" { effort 2d allocate a } task t2 "Task 2" { length 2d priority 600 allocate b depends !t1 } task t3 "Task 3" { duration 4d allocate a { alternative b } priority 200 } task t4 "Task 4" { depends !t1 length 5d allocate c priority 800 limits { dailymax 2h } } task t5 "Task 5" { effort 10d allocate d } task t6 "Task 6" { depends !t5 effort 5d allocate e { mandatory } allocate f { alternative g } } task t7 "Task 7" { duration 5d depends !t1 allocate e priority 900 shifts parttime } task t8 "Task 8" { depends !t4 milestone } task t9 "Task 9" { depends !t6 } task t10 "Task 10" { effort 3d allocate h { alternative a } } } task sg3 "Sub Group 3" { task t1 "Task 1" { effort 2d allocate a } task t2 "Task 2" { length 2d priority 600 allocate b depends !t1 } task t3 "Task 3" { duration 4d } task t4 "Task 4" { depends !t1 length 5d allocate c priority 800 limits { dailymax 2h } } task t5 "Task 5" { effort 10d allocate d } task t6 "Task 6" { depends !t5 effort 5d allocate e { mandatory } allocate f { alternative g } } task t7 "Task 7" { duration 5d } task t8 "Task 8" { depends !t4 milestone } task t9 "Task 9" { depends !t6 } task t10 "Task 10" { effort 3d allocate h { alternative a } } } task sg4 "Sub Group 4" { depends !sg2 task t1 "Task 1" { effort 2d allocate a } task t2 "Task 2" { length 2d priority 600 allocate b depends !t1 } task t3 "Task 3" { duration 10d allocate a { alternative b, c, d, e, g } priority 200 } task t4 "Task 4" { depends !t1 length 5d allocate c priority 800 limits { dailymax 2h } } task t5 "Task 5" { effort 10d allocate d } task t6 "Task 6" { depends !t5 effort 5d allocate e { mandatory } allocate f { alternative g } } task t7 "Task 7" { duration 5d } task t8 "Task 8" { depends !t4 milestone } task t9 "Task 9" { depends !t6 } task t10 "Task 10" { effort 3d allocate h { alternative a } } } task sg5 "Sub Group 5" { task t1 "Task 1" { effort 2d allocate a } task t2 "Task 2" { length 2d priority 600 allocate b depends !t1 } task t3 "Task 3" { duration 4d } task t4 "Task 4" { depends !t1 length 5d allocate c priority 800 limits { dailymax 2h } } task t5 "Task 5" { effort 10d allocate d } task t6 "Task 6" { depends !t5 effort 5d allocate e { mandatory } allocate f { alternative g } } task t7 "Task 7" { duration 5d depends !t1 allocate e priority 900 shifts parttime } task t8 "Task 8" { depends !t4 milestone } task t9 "Task 9" { depends !t6 } task t10 "Task 10" { effort 3d allocate h { alternative a } } } task sg6 "Sub Group 6" { depends !sg4 task t1 "Task 1" { effort 2d allocate a } task t2 "Task 2" { length 2d priority 600 allocate b depends !t1 } task t3 "Task 3" { duration 4d } task t4 "Task 4" { depends !t1 length 5d allocate c, d, f, g, h priority 800 limits { dailymax 2h } } task t5 "Task 5" { effort 10d allocate d } task t6 "Task 6" { depends !t5 effort 5d allocate e { mandatory } allocate f { alternative g } } task t7 "Task 7" { duration 15d depends !t1 allocate e, f, g, h priority 900 shifts parttime } task t8 "Task 8" { depends !t4 milestone } task t9 "Task 9" { depends !t6 } task t10 "Task 10" { effort 3d allocate h { alternative a } } } task sg7 "Sub Group 7" { task t1 "Task 1" { effort 2d allocate a } task t2 "Task 2" { length 2d priority 600 allocate b depends !t1 } task t3 "Task 3" { duration 4d } task t4 "Task 4" { depends !t1 length 5d allocate c priority 800 limits { dailymax 2h } } task t5 "Task 5" { effort 10d allocate d } task t6 "Task 6" { depends !t5 effort 5d allocate e { mandatory } allocate f { alternative g } } task t7 "Task 7" { duration 5d } task t8 "Task 8" { depends !t4 milestone } task t9 "Task 9" { depends !t6 } task t10 "Task 10" { effort 3d allocate h { alternative a } } } task sg8 "Sub Group 8" { depends !sg7 task t1 "Task 1" { effort 2d allocate a } task t2 "Task 2" { length 2d priority 600 allocate b depends !t1 } task t3 "Task 3" { duration 4d allocate a { alternative b } priority 200 } task t4 "Task 4" { depends !t1 length 5d allocate c priority 800 limits { dailymax 2h } } task t5 "Task 5" { effort 10d allocate d } task t6 "Task 6" { depends !t5 effort 5d allocate e { mandatory } allocate f { alternative g } } task t7 "Task 7" { duration 5d depends !t1 allocate e priority 900 shifts parttime } task t8 "Task 8" { depends !t4 milestone } task t9 "Task 9" { depends !t6 } task t10 "Task 10" { effort 3d allocate h { alternative a } } } task sg9 "Sub Group 9" { task t1 "Task 1" { effort 2d allocate a } task t2 "Task 2" { length 2d priority 600 allocate b depends !t1 } task t3 "Task 3" { duration 4d } task t4 "Task 4" { depends !t1 length 5d allocate c priority 800 limits { dailymax 2h } } task t5 "Task 5" { effort 10d allocate d } task t6 "Task 6" { depends !t5 effort 5d allocate e { mandatory } allocate f { alternative g } } task t7 "Task 7" { } task t8 "Task 8" { depends !t4 milestone } task t9 "Task 9" { depends !t6 } task t10 "Task 10" { effort 3d allocate h { alternative a } } } task sg10 "Sub Group 10" { depends !sg6 task t1 "Task 1" { effort 2d allocate a } task t2 "Task 2" { length 2d priority 600 allocate b depends !t1 } task t3 "Task 3" { duration 4d allocate a { alternative b } priority 200 } task t4 "Task 4" { depends !t1 length 5d allocate c priority 800 limits { dailymax 2h } } task t5 "Task 5" { effort 10d allocate d } task t6 "Task 6" { depends !t5 effort 5d allocate e { mandatory } allocate f { alternative g } } task t7 "Task 7" { duration 5d depends !t1 allocate e priority 900 shifts parttime } task t8 "Task 8" { depends !t4 milestone } task t9 "Task 9" { depends !t6 } task t10 "Task 10" { effort 3d allocate h { alternative a } } } } task g4 "Group 4" { depends !g1 task sg1 "Sub Group 1" { task t1 "Task 1" { effort 2d allocate a } task t2 "Task 2" { length 2d priority 600 allocate b depends !t1 } task t3 "Task 3" { duration 10d allocate a { alternative b, c, d, e, f } priority 200 } task t4 "Task 4" { depends !t1 length 5d allocate c priority 800 limits { dailymax 2h } } task t5 "Task 5" { effort 10d allocate d } task t6 "Task 6" { depends !t5 effort 5d allocate e { mandatory } allocate f { alternative g } } task t7 "Task 7" { duration 5d depends !t1 allocate e priority 900 shifts parttime } task t8 "Task 8" { depends !t4 milestone } task t9 "Task 9" { depends !t6 } task t10 "Task 10" { effort 3d allocate h { alternative a } } } task sg2 "Sub Group 2" { depends !sg1 task t1 "Task 1" { effort 2d allocate a } task t2 "Task 2" { length 2d priority 600 allocate b depends !t1 } task t3 "Task 3" { duration 4d allocate a { alternative b } priority 200 } task t4 "Task 4" { depends !t1 length 5d allocate c priority 800 limits { dailymax 2h } } task t5 "Task 5" { effort 10d allocate d } task t6 "Task 6" { depends !t5 effort 5d allocate e { mandatory } allocate f { alternative g } } task t7 "Task 7" { duration 5d depends !t1 allocate e priority 900 shifts parttime } task t8 "Task 8" { depends !t4 milestone } task t9 "Task 9" { depends !t6 } task t10 "Task 10" { effort 3d allocate h { alternative a } } } task sg3 "Sub Group 3" { task t1 "Task 1" { effort 2d allocate a } task t2 "Task 2" { length 2d priority 600 allocate b depends !t1 } task t3 "Task 3" { duration 9d } task t4 "Task 4" { depends !t1 length 5d allocate c priority 800 limits { dailymax 2h } } task t5 "Task 5" { effort 10d allocate d } task t6 "Task 6" { depends !t5 effort 5d allocate e { mandatory } allocate f { alternative g } } task t7 "Task 7" { duration 5d } task t8 "Task 8" { depends !t4 milestone } task t9 "Task 9" { depends !t6 } task t10 "Task 10" { effort 3d allocate h { alternative a } } } task sg4 "Sub Group 4" { depends !sg2 task t1 "Task 1" { effort 2d allocate a } task t2 "Task 2" { length 2d priority 600 allocate b depends !t1 } task t3 "Task 3" { duration 4d } task t4 "Task 4" { depends !t1 length 5d allocate c priority 800 limits { dailymax 2h } } task t5 "Task 5" { effort 10d allocate d } task t6 "Task 6" { depends !t5 effort 5d allocate e { mandatory } allocate f { alternative g } } task t7 "Task 7" { duration 5d depends !t1 allocate e priority 900 shifts parttime } task t8 "Task 8" { depends !t4 milestone } task t9 "Task 9" { depends !t6 } task t10 "Task 10" { effort 3d allocate h { alternative a } } } task sg5 "Sub Group 5" { task t1 "Task 1" { effort 2d allocate a } task t2 "Task 2" { length 2d priority 600 allocate b depends !t1 } task t3 "Task 3" { duration 4d } task t4 "Task 4" { depends !t1 length 5d allocate c priority 800 limits { dailymax 2h } } task t5 "Task 5" { effort 10d allocate d } task t6 "Task 6" { depends !t5 effort 5d allocate e { mandatory } allocate f { alternative g } } task t7 "Task 7" { duration 5d depends !t1 allocate e priority 900 shifts parttime } task t8 "Task 8" { depends !t4 milestone } task t9 "Task 9" { depends !t6 } task t10 "Task 10" { effort 3d allocate h { alternative a } } } task sg6 "Sub Group 6" { depends !sg4 task t1 "Task 1" { effort 2d allocate a } task t2 "Task 2" { length 2d priority 600 allocate b depends !t1 } task t3 "Task 3" { duration 4d allocate a { alternative b } priority 200 } task t4 "Task 4" { depends !t1 length 5d allocate c priority 800 limits { dailymax 2h } } task t5 "Task 5" { effort 10d allocate d } task t6 "Task 6" { depends !t5 effort 5d allocate e { mandatory } allocate f { alternative g } } task t7 "Task 7" { duration 5d depends !t1 allocate e priority 900 shifts parttime } task t8 "Task 8" { depends !t4 milestone } task t9 "Task 9" { depends !t6 } task t10 "Task 10" { effort 3d allocate h { alternative a } } } task sg7 "Sub Group 7" { task t1 "Task 1" { effort 2d allocate a } task t2 "Task 2" { length 2d priority 600 allocate b depends !t1 } task t3 "Task 3" { duration 4d } task t4 "Task 4" { depends !t1 length 5d allocate c priority 800 limits { dailymax 2h } } task t5 "Task 5" { effort 10d allocate d } task t6 "Task 6" { depends !t5 effort 5d allocate e { mandatory } allocate f { alternative g } } task t7 "Task 7" { duration 5d depends !t1 allocate e priority 900 shifts parttime } task t8 "Task 8" { depends !t4 milestone } task t9 "Task 9" { depends !t6 } task t10 "Task 10" { effort 3d allocate h { alternative a } } } task sg8 "Sub Group 8" { depends !sg7 task t1 "Task 1" { effort 2d allocate a } task t2 "Task 2" { length 2d priority 600 allocate b depends !t1 } task t3 "Task 3" { duration 4d allocate a { alternative b } priority 200 } task t4 "Task 4" { depends !t1 length 5d allocate c priority 800 limits { dailymax 2h } } task t5 "Task 5" { effort 10d allocate d } task t6 "Task 6" { depends !t5 effort 5d allocate e { mandatory } allocate f { alternative g } } task t7 "Task 7" { duration 5d depends !t1 allocate e priority 900 shifts parttime } task t8 "Task 8" { depends !t4 milestone } task t9 "Task 9" { depends !t6 } task t10 "Task 10" { effort 3d allocate h { alternative a } } } task sg9 "Sub Group 9" { task t1 "Task 1" { effort 2d allocate a } task t2 "Task 2" { length 2d priority 600 allocate b depends !t1 } task t3 "Task 3" { duration 4d allocate a { alternative b } priority 800 } task t4 "Task 4" { depends !t1 length 5d allocate c priority 800 limits { dailymax 2h } } task t5 "Task 5" { effort 10d allocate d } task t6 "Task 6" { depends !t5 effort 5d allocate e { mandatory } allocate f { alternative g } } task t7 "Task 7" { duration 5d depends !t1 } task t8 "Task 8" { depends !t4 milestone } task t9 "Task 9" { depends !t6 } task t10 "Task 10" { effort 3d allocate h { alternative a } } } task sg10 "Sub Group 10" { depends !sg6 task t1 "Task 1" { effort 2d allocate a } task t2 "Task 2" { length 2d priority 600 allocate b depends !t1 } task t3 "Task 3" { duration 4d allocate a { alternative b } priority 200 } task t4 "Task 4" { depends !t1 length 5d allocate c priority 800 limits { dailymax 2h } } task t5 "Task 5" { effort 10d allocate d } task t6 "Task 6" { depends !t5 effort 5d allocate e { mandatory } allocate f { alternative g } } task t7 "Task 7" { duration 5d depends !t1 allocate e priority 900 shifts parttime } task t8 "Task 8" { depends !t4 milestone } task t9 "Task 9" { depends !t6 } task t10 "Task 10" { effort 3d allocate h { alternative a } } } } task g5 "Group 5" { depends !g2 task sg1 "Sub Group 1" { task t1 "Task 1" { effort 2d allocate a } task t2 "Task 2" { length 2d priority 600 allocate b depends !t1 } task t3 "Task 3" { duration 4d allocate a { alternative b } priority 200 } task t4 "Task 4" { depends !t1 length 5d allocate c priority 800 limits { dailymax 2h } } task t5 "Task 5" { effort 10d allocate d } task t6 "Task 6" { depends !t5 effort 5d allocate e { mandatory } allocate f { alternative g } } task t7 "Task 7" { duration 5d depends !t1 allocate e priority 900 shifts parttime } task t8 "Task 8" { depends !t4 milestone } task t9 "Task 9" { depends !t6 } task t10 "Task 10" { effort 3d allocate h { alternative a } } } task sg2 "Sub Group 2" { depends !sg1 task t1 "Task 1" { effort 2d allocate a } task t2 "Task 2" { length 2d priority 600 allocate b depends !t1 } task t3 "Task 3" { duration 4d allocate a { alternative b } priority 200 } task t4 "Task 4" { depends !t1 length 5d allocate c priority 800 limits { dailymax 2h } } task t5 "Task 5" { effort 10d allocate d } task t6 "Task 6" { depends !t5 effort 5d allocate e { mandatory } allocate f { alternative g } } task t7 "Task 7" { duration 5d depends !t1 allocate e priority 900 shifts parttime } task t8 "Task 8" { depends !t4 milestone } task t9 "Task 9" { depends !t6 } task t10 "Task 10" { effort 3d allocate h { alternative a } } } task sg3 "Sub Group 3" { task t1 "Task 1" { effort 2d allocate a } task t2 "Task 2" { length 2d priority 600 allocate b depends !t1 } task t3 "Task 3" { duration 4d } task t4 "Task 4" { depends !t1 length 5d allocate c priority 800 limits { dailymax 2h } } task t5 "Task 5" { effort 10d allocate d } task t6 "Task 6" { depends !t5 effort 5d allocate e { mandatory } allocate f { alternative g } } task t7 "Task 7" { duration 5d depends !t1 allocate e priority 900 shifts parttime } task t8 "Task 8" { depends !t4 milestone } task t9 "Task 9" { depends !t6 } task t10 "Task 10" { effort 3d allocate h { alternative a } } } task sg4 "Sub Group 4" { depends !sg2 task t1 "Task 1" { effort 2d allocate a } task t2 "Task 2" { length 2d priority 600 allocate b depends !t1 } task t3 "Task 3" { duration 4d allocate a { alternative b } priority 200 } task t4 "Task 4" { depends !t1 length 5d allocate c priority 800 limits { dailymax 2h } } task t5 "Task 5" { effort 10d allocate d } task t6 "Task 6" { depends !t5 effort 5d allocate e { mandatory } allocate f { alternative g } } task t7 "Task 7" { duration 5d depends !t1 allocate e priority 900 shifts parttime } task t8 "Task 8" { depends !t4 milestone } task t9 "Task 9" { depends !t6 } task t10 "Task 10" { effort 3d allocate h { alternative a } } } task sg5 "Sub Group 5" { task t1 "Task 1" { effort 2d allocate a } task t2 "Task 2" { length 2d priority 600 allocate b depends !t1 } task t3 "Task 3" { duration 4d } task t4 "Task 4" { depends !t1 length 5d allocate c priority 800 limits { dailymax 2h } } task t5 "Task 5" { effort 10d allocate d } task t6 "Task 6" { depends !t5 effort 5d allocate e { mandatory } allocate f { alternative g } } task t7 "Task 7" { } task t8 "Task 8" { depends !t4 milestone } task t9 "Task 9" { depends !t6 } task t10 "Task 10" { effort 3d allocate h { alternative a } } } task sg6 "Sub Group 6" { depends !sg4 task t1 "Task 1" { effort 2d allocate a } task t2 "Task 2" { length 2d priority 600 allocate b depends !t1 } task t3 "Task 3" { duration 4d allocate a { alternative b } priority 200 } task t4 "Task 4" { depends !t1 length 5d allocate c priority 800 limits { dailymax 2h } } task t5 "Task 5" { effort 10d allocate d } task t6 "Task 6" { depends !t5 effort 5d allocate e { mandatory } allocate f { alternative g } } task t7 "Task 7" { duration 5d depends !t1 allocate e priority 900 shifts parttime } task t8 "Task 8" { depends !t4 milestone } task t9 "Task 9" { depends !t6 } task t10 "Task 10" { effort 3d allocate h { alternative a } } } task sg7 "Sub Group 7" { task t1 "Task 1" { effort 2d allocate a } task t2 "Task 2" { length 2d priority 600 allocate b depends !t1 } task t3 "Task 3" { duration 4d } task t4 "Task 4" { depends !t1 length 5d allocate c priority 800 limits { dailymax 2h } } task t5 "Task 5" { effort 10d allocate d } task t6 "Task 6" { depends !t5 effort 5d allocate e { mandatory } allocate f { alternative g } } task t7 "Task 7" { duration 5d depends !t1 allocate e priority 900 shifts parttime } task t8 "Task 8" { depends !t4 milestone } task t9 "Task 9" { depends !t6 } task t10 "Task 10" { effort 3d allocate h { alternative a } } } task sg8 "Sub Group 8" { depends !sg7 task t1 "Task 1" { effort 2d allocate a } task t2 "Task 2" { length 2d priority 600 allocate b depends !t1 } task t3 "Task 3" { duration 4d allocate a { alternative b } priority 200 } task t4 "Task 4" { depends !t1 length 5d allocate c priority 800 limits { dailymax 2h } } task t5 "Task 5" { effort 10d allocate d } task t6 "Task 6" { depends !t5 effort 5d allocate e { mandatory } allocate f { alternative g } } task t7 "Task 7" { duration 5d depends !t1 allocate e priority 900 shifts parttime } task t8 "Task 8" { depends !t4 milestone } task t9 "Task 9" { depends !t6 } task t10 "Task 10" { effort 3d allocate h { alternative a } } } task sg9 "Sub Group 9" { task t1 "Task 1" { effort 2d allocate a } task t2 "Task 2" { length 2d priority 600 allocate b depends !t1 } task t3 "Task 3" { duration 4d allocate a { alternative b, g, f, h } priority 200 } task t4 "Task 4" { depends !t1 length 5d allocate c priority 800 limits { dailymax 2h } } task t5 "Task 5" { effort 10d allocate d } task t6 "Task 6" { depends !t5 effort 5d allocate e { mandatory } allocate f { alternative g } } task t7 "Task 7" { } task t8 "Task 8" { depends !t4 milestone } task t9 "Task 9" { depends !t6 } task t10 "Task 10" { effort 3d allocate h { alternative a } } } task sg10 "Sub Group 10" { depends !sg6 task t1 "Task 1" { effort 2d allocate a } task t2 "Task 2" { length 2d priority 600 allocate b depends !t1 } task t3 "Task 3" { duration 4d allocate a { alternative b } priority 200 } task t4 "Task 4" { depends !t1 length 5d allocate c priority 800 limits { dailymax 2h } } task t5 "Task 5" { effort 10d allocate d } task t6 "Task 6" { depends !t5 effort 5d allocate e { mandatory } allocate f { alternative g } } task t7 "Task 7" { duration 5d depends !t1 allocate e priority 900 shifts parttime } task t8 "Task 8" { depends !t4 milestone } task t9 "Task 9" { depends !t6 } task t10 "Task 10" { effort 3d allocate h { alternative a } } } } task g6 "Group 6" { depends !g5 task sg1 "Sub Group 1" { task t1 "Task 1" { effort 2d allocate a } task t2 "Task 2" { length 2d priority 600 allocate b depends !t1 } task t3 "Task 3" { duration 4d allocate a { alternative b } priority 200 } task t4 "Task 4" { depends !t1 length 5d allocate c priority 800 limits { dailymax 2h } } task t5 "Task 5" { effort 10d allocate d } task t6 "Task 6" { depends !t5 effort 5d allocate e { mandatory } allocate f { alternative g } } task t7 "Task 7" { duration 5d depends !t1 allocate e priority 900 shifts parttime } task t8 "Task 8" { depends !t4 milestone } task t9 "Task 9" { depends !t6 } task t10 "Task 10" { effort 3d allocate h { alternative a } } } task sg2 "Sub Group 2" { depends !sg1 task t1 "Task 1" { effort 2d allocate a } task t2 "Task 2" { length 2d priority 600 allocate b depends !t1 } task t3 "Task 3" { duration 4d allocate a { alternative b } priority 200 } task t4 "Task 4" { depends !t1 length 5d allocate c priority 800 limits { dailymax 2h } } task t5 "Task 5" { effort 10d allocate d } task t6 "Task 6" { depends !t5 effort 5d allocate e { mandatory } allocate f { alternative g } } task t7 "Task 7" { duration 5d depends !t1 allocate e priority 900 shifts parttime } task t8 "Task 8" { depends !t4 milestone } task t9 "Task 9" { depends !t6 } task t10 "Task 10" { effort 3d allocate h { alternative a } } } task sg3 "Sub Group 3" { task t1 "Task 1" { effort 2d allocate a } task t2 "Task 2" { length 2d priority 600 allocate b depends !t1 } task t3 "Task 3" { duration 4d } task t4 "Task 4" { depends !t1 length 5d allocate c priority 800 limits { dailymax 2h } } task t5 "Task 5" { effort 10d allocate d } task t6 "Task 6" { depends !t5 effort 5d allocate e { mandatory } allocate f { alternative g } } task t7 "Task 7" { duration 5d depends !t1 allocate e priority 900 shifts parttime } task t8 "Task 8" { depends !t4 milestone } task t9 "Task 9" { depends !t6 } task t10 "Task 10" { effort 3d allocate h { alternative a } } } task sg4 "Sub Group 4" { depends !sg2 task t1 "Task 1" { effort 2d allocate a } task t2 "Task 2" { length 2d priority 600 allocate b depends !t1 } task t3 "Task 3" { duration 4d allocate a { alternative b } priority 200 } task t4 "Task 4" { depends !t1 length 5d allocate c priority 800 limits { dailymax 2h } } task t5 "Task 5" { effort 10d allocate d } task t6 "Task 6" { depends !t5 effort 5d allocate e { mandatory } allocate f { alternative g } } task t7 "Task 7" { duration 5d depends !t1 allocate e priority 900 shifts parttime } task t8 "Task 8" { depends !t4 milestone } task t9 "Task 9" { depends !t6 } task t10 "Task 10" { effort 3d allocate h { alternative a } } } task sg5 "Sub Group 5" { task t1 "Task 1" { effort 2d allocate a } task t2 "Task 2" { length 2d priority 600 allocate b depends !t1 } task t3 "Task 3" { duration 4d } task t4 "Task 4" { depends !t1 length 5d allocate c priority 800 limits { dailymax 2h } } task t5 "Task 5" { effort 10d allocate d } task t6 "Task 6" { depends !t5 effort 5d allocate e { mandatory } allocate f { alternative g } } task t7 "Task 7" { duration 5d depends !t1 allocate e priority 900 shifts parttime } task t8 "Task 8" { depends !t4 milestone } task t9 "Task 9" { depends !t6 } task t10 "Task 10" { effort 3d allocate h { alternative a } } } task sg6 "Sub Group 6" { depends !sg4 task t1 "Task 1" { effort 2d allocate a } task t2 "Task 2" { length 2d priority 600 allocate b depends !t1 } task t3 "Task 3" { duration 4d allocate a { alternative b } priority 200 } task t4 "Task 4" { depends !t1 length 5d allocate c priority 800 limits { dailymax 2h } } task t5 "Task 5" { effort 10d allocate d } task t6 "Task 6" { depends !t5 effort 5d allocate e { mandatory } allocate f { alternative g } } task t7 "Task 7" { duration 5d depends !t1 allocate e priority 900 shifts parttime } task t8 "Task 8" { depends !t4 milestone } task t9 "Task 9" { depends !t6 } task t10 "Task 10" { effort 3d allocate h { alternative a } } } task sg7 "Sub Group 7" { task t1 "Task 1" { effort 2d allocate a } task t2 "Task 2" { length 2d priority 600 allocate b depends !t1 } task t3 "Task 3" { duration 4d } task t4 "Task 4" { depends !t1 length 5d allocate c priority 800 limits { dailymax 2h } } task t5 "Task 5" { effort 10d allocate d } task t6 "Task 6" { depends !t5 effort 5d allocate e { mandatory } allocate f { alternative g } } task t7 "Task 7" { duration 5d depends !t1 allocate e priority 900 shifts parttime } task t8 "Task 8" { depends !t4 milestone } task t9 "Task 9" { depends !t6 } task t10 "Task 10" { effort 3d allocate h { alternative a } } } task sg8 "Sub Group 8" { depends !sg7 task t1 "Task 1" { effort 2d allocate a } task t2 "Task 2" { length 2d priority 600 allocate b depends !t1 } task t3 "Task 3" { duration 4d allocate a { alternative b } priority 200 } task t4 "Task 4" { depends !t1 length 5d allocate c priority 800 limits { dailymax 2h } } task t5 "Task 5" { effort 10d allocate d } task t6 "Task 6" { depends !t5 effort 5d allocate e { mandatory } allocate f { alternative g } } task t7 "Task 7" { duration 5d depends !t1 allocate e priority 900 shifts parttime } task t8 "Task 8" { depends !t4 milestone } task t9 "Task 9" { depends !t6 } task t10 "Task 10" { effort 3d allocate h { alternative a } } } task sg9 "Sub Group 9" { task t1 "Task 1" { effort 2d allocate a } task t2 "Task 2" { length 2d priority 600 allocate b depends !t1 } task t3 "Task 3" { duration 4d } task t4 "Task 4" { depends !t1 length 5d allocate c priority 800 limits { dailymax 2h } } task t5 "Task 5" { effort 10d allocate d } task t6 "Task 6" { depends !t5 effort 5d allocate e { mandatory } allocate f { alternative g } } task t7 "Task 7" { duration 5d depends !t1 allocate e priority 900 shifts parttime } task t8 "Task 8" { depends !t4 milestone } task t9 "Task 9" { depends !t6 } task t10 "Task 10" { effort 3d allocate h { alternative a } } } task sg10 "Sub Group 10" { depends !sg6 task t1 "Task 1" { effort 2d allocate a } task t2 "Task 2" { length 2d priority 600 allocate b depends !t1 } task t3 "Task 3" { duration 4d allocate a { alternative b } priority 200 } task t4 "Task 4" { depends !t1 length 5d allocate c priority 800 limits { dailymax 2h } } task t5 "Task 5" { effort 10d allocate d } task t6 "Task 6" { depends !t5 effort 5d allocate e { mandatory } allocate f { alternative g } } task t7 "Task 7" { duration 5d depends !t1 allocate e priority 900 shifts parttime } task t8 "Task 8" { depends !t4 milestone } task t9 "Task 9" { depends !t6 } task t10 "Task 10" { effort 3d allocate h { alternative a } } } } TaskJuggler-3.8.1/benchmarks/allocate.tjp000066400000000000000000000010471473026623400203760ustar00rootroot00000000000000project test "Allocation Performance Test" "1.0" 2007-08-25 +3m macro r_and_t [ resource r${1} "Resource ${1}" task t${1} "Task ${1}" { period ${projectstart} - ${projectend} allocate r${1} } ] macro r_and_t_10 [ ${r_and_t "${1}0"} ${r_and_t "${1}1"} ${r_and_t "${1}2"} ${r_and_t "${1}3"} ${r_and_t "${1}4"} ${r_and_t "${1}5"} ${r_and_t "${1}6"} ${r_and_t "${1}7"} ${r_and_t "${1}8"} ${r_and_t "${1}9"} ] ${r_and_t_10 "0"} ${r_and_t_10 "1"} ${r_and_t_10 "2"} ${r_and_t_10 "3"} ${r_and_t_10 "4"} ${r_and_t_10 "5"} TaskJuggler-3.8.1/benchmarks/allocatedSlots.tjp000066400000000000000000000674771473026623400216120ustar00rootroot00000000000000project "Benchmark" 2010-06-19 +3y shift parttime "Part Time" { workinghours mon, wed, fri 8:00 - 12:00 } resource a "A" resource b "B" resource c "C" resource d "D" resource e "E" resource f "F" resource g "G" resource h "H" task g1 "Group 1" { task sg1 "Sub Group 1" { task t1 "Task 1" { effort 2d allocate a } task t2 "Task 2" { length 2d priority 600 allocate b depends !t1 } task t3 "Task 3" { duration 8d allocate a { alternative b, c, d } priority 200 } task t4 "Task 4" { depends !t1 length 5d allocate c priority 800 limits { dailymax 2h } } task t5 "Task 5" { effort 10d allocate d } task t6 "Task 6" { depends !t5 effort 5d allocate e { mandatory } allocate f { alternative g } } task t7 "Task 7" { duration 10d depends !t1 allocate e, f, g priority 900 shift parttime } task t8 "Task 8" { depends !t4 milestone } task t9 "Task 9" { depends !t6 } task t10 "Task 10" { effort 3d allocate h { alternative a } } } task sg2 "Sub Group 2" { depends !sg1 task t1 "Task 1" { effort 2d allocate a } task t2 "Task 2" { length 2d priority 600 allocate b depends !t1 } task t3 "Task 3" { duration 4d allocate a { alternative b } priority 200 } task t4 "Task 4" { depends !t1 length 5d allocate c priority 800 limits { dailymax 2h } } task t5 "Task 5" { effort 10d allocate d } task t6 "Task 6" { depends !t5 effort 5d allocate e { mandatory } allocate f { alternative g } } task t7 "Task 7" { duration 5d depends !t1 allocate e priority 900 shift parttime } task t8 "Task 8" { depends !t4 milestone } task t9 "Task 9" { depends !t6 } task t10 "Task 10" { effort 3d allocate h { alternative a } } } task sg3 "Sub Group 3" { task t1 "Task 1" { effort 2d allocate a } task t2 "Task 2" { length 2d priority 600 allocate b depends !t1 } task t3 "Task 3" { duration 8d allocate a { alternative b, c, d, e, f, g } priority 200 } task t4 "Task 4" { depends !t1 length 5d allocate c priority 800 limits { dailymax 2h } } task t5 "Task 5" { effort 10d allocate d } task t6 "Task 6" { depends !t5 effort 5d allocate e { mandatory } allocate f { alternative g } } task t7 "Task 7" { duration 10d depends !t1 allocate e priority 900 shift parttime } task t8 "Task 8" { depends !t4 milestone } task t9 "Task 9" { depends !t6 } task t10 "Task 10" { effort 3d allocate h { alternative a } } } task sg4 "Sub Group 4" { depends !sg2 task t1 "Task 1" { effort 2d allocate a } task t2 "Task 2" { length 2d priority 600 allocate b depends !t1 } task t3 "Task 3" { duration 4d allocate a { alternative b } priority 200 } task t4 "Task 4" { depends !t1 length 5d allocate c priority 800 limits { dailymax 2h } } task t5 "Task 5" { effort 10d allocate d } task t6 "Task 6" { depends !t5 effort 5d allocate e { mandatory } allocate f { alternative g } } task t7 "Task 7" { duration 5d depends !t1 allocate e priority 900 shift parttime } task t8 "Task 8" { depends !t4 milestone } task t9 "Task 9" { depends !t6 } task t10 "Task 10" { effort 3d allocate h { alternative a } } } task sg5 "Sub Group 5" { task t1 "Task 1" { effort 2d allocate a } task t2 "Task 2" { length 2d priority 600 allocate b depends !t1 } task t3 "Task 3" { duration 20d allocate a { alternative b, c, d, e, f, g } priority 200 } task t4 "Task 4" { depends !t1 length 5d allocate c priority 800 limits { dailymax 2h } } task t5 "Task 5" { effort 10d allocate d } task t6 "Task 6" { depends !t5 effort 5d allocate e { mandatory } allocate f { alternative g } } task t7 "Task 7" { duration 5d depends !t1 allocate e, f, g priority 900 shift parttime } task t8 "Task 8" { depends !t4 milestone } task t9 "Task 9" { depends !t6 } task t10 "Task 10" { effort 3d allocate h { alternative a } } } task sg6 "Sub Group 6" { depends !sg4 task t1 "Task 1" { effort 2d allocate a } task t2 "Task 2" { length 2d priority 600 allocate b depends !t1 } task t3 "Task 3" { duration 4d allocate a { alternative b } priority 200 } task t4 "Task 4" { depends !t1 length 5d allocate c priority 800 limits { dailymax 2h } } task t5 "Task 5" { effort 10d allocate d } task t6 "Task 6" { depends !t5 effort 5d allocate e { mandatory } allocate f { alternative g } } task t7 "Task 7" { duration 5d depends !t1 allocate e priority 900 shift parttime } task t8 "Task 8" { depends !t4 milestone } task t9 "Task 9" { depends !t6 } task t10 "Task 10" { effort 3d allocate h { alternative a } } } task sg7 "Sub Group 7" { task t1 "Task 1" { effort 2d allocate a } task t2 "Task 2" { length 2d priority 600 allocate b depends !t1 } task t3 "Task 3" { duration 11d allocate a { alternative b, c, d, e, f, g } priority 200 } task t4 "Task 4" { depends !t1 length 5d allocate c priority 800 limits { dailymax 2h } } task t5 "Task 5" { effort 10d allocate d } task t6 "Task 6" { depends !t5 effort 5d allocate e { mandatory } allocate f { alternative g } } task t7 "Task 7" { duration 10d depends !t1 allocate e, f, g, h priority 900 shift parttime } task t8 "Task 8" { depends !t4 milestone } task t9 "Task 9" { depends !t6 } task t10 "Task 10" { effort 3d allocate h { alternative a } } } task sg8 "Sub Group 8" { depends !sg7 task t1 "Task 1" { effort 2d allocate a } task t2 "Task 2" { length 2d priority 600 allocate b depends !t1 } task t3 "Task 3" { duration 8d allocate a { alternative b, c, d, e, f } priority 200 } task t4 "Task 4" { depends !t1 length 5d allocate c priority 800 limits { dailymax 2h } } task t5 "Task 5" { effort 10d allocate d } task t6 "Task 6" { depends !t5 effort 5d allocate e { mandatory } allocate f { alternative g } } task t7 "Task 7" { duration 5d depends !t1 allocate e, f, g priority 900 shift parttime } task t8 "Task 8" { depends !t4 milestone } task t9 "Task 9" { depends !t6 } task t10 "Task 10" { effort 3d allocate h { alternative a } } } task sg9 "Sub Group 9" { task t1 "Task 1" { effort 2d allocate a } task t2 "Task 2" { length 2d priority 600 allocate b depends !t1 } task t3 "Task 3" { duration 8d } task t4 "Task 4" { depends !t1 length 5d allocate c priority 800 limits { dailymax 2h } } task t5 "Task 5" { effort 10d allocate d } task t6 "Task 6" { depends !t5 effort 5d allocate e { mandatory } allocate f { alternative g } } task t7 "Task 7" { duration 5d depends !t1 allocate e, f, g priority 900 shift parttime } task t8 "Task 8" { depends !t4 milestone } task t9 "Task 9" { depends !t6 } task t10 "Task 10" { effort 3d allocate h { alternative a } } } task sg10 "Sub Group 10" { depends !sg6 task t1 "Task 1" { effort 2d allocate a } task t2 "Task 2" { length 2d priority 600 allocate b depends !t1 } task t3 "Task 3" { duration 4d allocate a { alternative b } priority 200 } task t4 "Task 4" { depends !t1 length 5d allocate c priority 800 limits { dailymax 2h } } task t5 "Task 5" { effort 10d allocate d } task t6 "Task 6" { depends !t5 effort 5d allocate e { mandatory } allocate f { alternative g } } task t7 "Task 7" { duration 5d depends !t1 allocate e priority 900 shift parttime } task t8 "Task 8" { depends !t4 milestone } task t9 "Task 9" { depends !t6 } task t10 "Task 10" { effort 3d allocate h { alternative a } } } } task g2 "Group 2" { depends !g1 task sg1 "Sub Group 1" { task t1 "Task 1" { effort 2d allocate a } task t2 "Task 2" { length 2d priority 600 allocate b depends !t1 } task t3 "Task 3" { duration 4d allocate a { alternative b } priority 200 } task t4 "Task 4" { depends !t1 length 5d allocate c priority 800 limits { dailymax 2h } } task t5 "Task 5" { effort 10d allocate d } task t6 "Task 6" { depends !t5 effort 5d allocate e { mandatory } allocate f { alternative g } } task t7 "Task 7" { duration 5d depends !t1 allocate e priority 900 shift parttime } task t8 "Task 8" { depends !t4 milestone } task t9 "Task 9" { depends !t6 } task t10 "Task 10" { effort 3d allocate h { alternative a } } } task sg2 "Sub Group 2" { depends !sg1 task t1 "Task 1" { effort 2d allocate a } task t2 "Task 2" { length 2d priority 600 allocate b depends !t1 } task t3 "Task 3" { duration 4d allocate a { alternative b } priority 200 } task t4 "Task 4" { depends !t1 length 5d allocate c priority 800 limits { dailymax 2h } } task t5 "Task 5" { effort 10d allocate d } task t6 "Task 6" { depends !t5 effort 5d allocate e { mandatory } allocate f { alternative g } } task t7 "Task 7" { duration 5d depends !t1 allocate e priority 900 shift parttime } task t8 "Task 8" { depends !t4 milestone } task t9 "Task 9" { depends !t6 } task t10 "Task 10" { effort 3d allocate h { alternative a } } } task sg3 "Sub Group 3" { task t1 "Task 1" { effort 2d allocate a } task t2 "Task 2" { length 2d priority 600 allocate b depends !t1 } task t3 "Task 3" { effort 2d allocate a { alternative b } priority 200 } task t4 "Task 4" { depends !t1 length 5d allocate c priority 800 limits { dailymax 2h } } task t5 "Task 5" { effort 10d allocate d } task t6 "Task 6" { depends !t5 effort 5d allocate e { mandatory } allocate f { alternative g } } task t7 "Task 7" { duration 5d depends !t1 allocate e priority 900 shift parttime } task t8 "Task 8" { depends !t4 milestone } task t9 "Task 9" { depends !t6 } task t10 "Task 10" { effort 3d allocate h { alternative a } } } task sg4 "Sub Group 4" { depends !sg2 task t1 "Task 1" { effort 2d allocate a } task t2 "Task 2" { length 2d priority 600 allocate b depends !t1 } task t3 "Task 3" { duration 4d allocate a { alternative b } priority 200 } task t4 "Task 4" { depends !t1 length 5d allocate c priority 800 limits { dailymax 2h } } task t5 "Task 5" { effort 10d allocate d } task t6 "Task 6" { depends !t5 effort 5d allocate e { mandatory } allocate f { alternative g } } task t7 "Task 7" { duration 5d depends !t1 allocate e priority 900 shift parttime } task t8 "Task 8" { depends !t4 milestone } task t9 "Task 9" { depends !t6 } task t10 "Task 10" { effort 3d allocate h { alternative a } } } task sg5 "Sub Group 5" { task t1 "Task 1" { effort 2d allocate a } task t2 "Task 2" { length 2d priority 600 allocate b depends !t1 } task t3 "Task 3" { duration 10d allocate a { alternative b, c, d, e, f } priority 200 } task t4 "Task 4" { depends !t1 length 5d allocate c priority 800 limits { dailymax 2h } } task t5 "Task 5" { effort 10d allocate d } task t6 "Task 6" { depends !t5 effort 5d allocate e { mandatory } allocate f { alternative g } } task t7 "Task 7" { duration 5d depends !t1 allocate e priority 900 shift parttime } task t8 "Task 8" { depends !t4 milestone } task t9 "Task 9" { depends !t6 } task t10 "Task 10" { effort 3d allocate h { alternative a } } } task sg6 "Sub Group 6" { depends !sg4 task t1 "Task 1" { effort 2d allocate a } task t2 "Task 2" { length 2d priority 600 allocate b depends !t1 } task t3 "Task 3" { duration 4d allocate a { alternative b } priority 200 } task t4 "Task 4" { depends !t1 length 5d allocate c priority 800 limits { dailymax 2h } } task t5 "Task 5" { effort 10d allocate d } task t6 "Task 6" { depends !t5 effort 5d allocate e { mandatory } allocate f { alternative g } } task t7 "Task 7" { effort 5d depends !t1 allocate e priority 900 shift parttime } task t8 "Task 8" { depends !t4 milestone } task t9 "Task 9" { depends !t6 } task t10 "Task 10" { effort 3d allocate h { alternative a } } } task sg7 "Sub Group 7" { task t1 "Task 1" { effort 2d allocate a } task t2 "Task 2" { length 2d priority 600 allocate b depends !t1 } task t3 "Task 3" { duration 8d allocate a { alternative b, d, e, f } priority 200 } task t4 "Task 4" { depends !t1 length 5d allocate c priority 800 limits { dailymax 2h } } task t5 "Task 5" { effort 10d allocate d } task t6 "Task 6" { depends !t5 effort 5d allocate e { mandatory } allocate f { alternative g } } task t7 "Task 7" { duration 5d depends !t1 allocate e priority 900 shift parttime } task t8 "Task 8" { depends !t4 milestone } task t9 "Task 9" { depends !t6 } task t10 "Task 10" { effort 3d allocate h { alternative a } } } task sg8 "Sub Group 8" { depends !sg7 task t1 "Task 1" { effort 2d allocate a } task t2 "Task 2" { length 2d priority 600 allocate b depends !t1 } task t3 "Task 3" { duration 4d allocate a { alternative b } priority 200 } task t4 "Task 4" { depends !t1 length 5d allocate c priority 800 limits { dailymax 2h } } task t5 "Task 5" { effort 10d allocate d } task t6 "Task 6" { depends !t5 effort 5d allocate e { mandatory } allocate f { alternative g } } task t7 "Task 7" { duration 5d depends !t1 allocate e priority 900 shift parttime } task t8 "Task 8" { depends !t4 milestone } task t9 "Task 9" { depends !t6 } task t10 "Task 10" { effort 3d allocate h { alternative a } } } task sg9 "Sub Group 9" { task t1 "Task 1" { effort 2d allocate a } task t2 "Task 2" { length 2d priority 600 allocate b depends !t1 } task t3 "Task 3" { duration 8d allocate a { alternative b, c, e, f, h } priority 200 } task t4 "Task 4" { depends !t1 length 5d allocate c priority 800 limits { dailymax 2h } } task t5 "Task 5" { effort 10d allocate d } task t6 "Task 6" { depends !t5 effort 5d allocate e { mandatory } allocate f { alternative g } } task t7 "Task 7" { duration 5d depends !t1 allocate e priority 900 shift parttime } task t8 "Task 8" { depends !t4 milestone } task t9 "Task 9" { depends !t6 } task t10 "Task 10" { effort 3d allocate h { alternative a } } } task sg10 "Sub Group 10" { depends !sg6 task t1 "Task 1" { effort 2d allocate a } task t2 "Task 2" { length 2d priority 600 allocate b depends !t1 } task t3 "Task 3" { duration 4d allocate a { alternative b } priority 200 } task t4 "Task 4" { depends !t1 length 5d allocate c priority 800 limits { dailymax 2h } } task t5 "Task 5" { effort 10d allocate d } task t6 "Task 6" { depends !t5 effort 5d allocate e { mandatory } allocate f { alternative g } } task t7 "Task 7" { duration 5d depends !t1 allocate e priority 900 shift parttime } task t8 "Task 8" { depends !t4 milestone } task t9 "Task 9" { depends !t6 } task t10 "Task 10" { effort 3d allocate h { alternative a } } } } task g3 "Group 3" { task sg1 "Sub Group 1" { task t1 "Task 1" { effort 2d allocate a } task t2 "Task 2" { length 2d priority 600 allocate b depends !t1 } task t3 "Task 3" { effort 4d allocate a { alternative b, c, d, e } priority 400 } task t4 "Task 4" { depends !t1 length 5d allocate c priority 800 limits { dailymax 2h } } task t5 "Task 5" { effort 10d allocate d } task t6 "Task 6" { depends !t5 effort 5d allocate e { mandatory } allocate f { alternative g } } task t7 "Task 7" { duration 5d depends !t1 allocate e priority 900 shift parttime } task t8 "Task 8" { depends !t4 milestone } task t9 "Task 9" { depends !t6 } task t10 "Task 10" { effort 3d allocate h { alternative a } } } task sg2 "Sub Group 2" { depends !sg1 task t1 "Task 1" { effort 2d allocate a } task t2 "Task 2" { length 2d priority 600 allocate b depends !t1 } task t3 "Task 3" { duration 4d allocate a { alternative b } priority 200 } task t4 "Task 4" { depends !t1 length 5d allocate c priority 800 limits { dailymax 2h } } task t5 "Task 5" { effort 10d allocate d } task t6 "Task 6" { depends !t5 effort 5d allocate e { mandatory } allocate f { alternative g } } task t7 "Task 7" { duration 5d depends !t1 allocate e priority 900 shift parttime } task t8 "Task 8" { depends !t4 milestone } task t9 "Task 9" { depends !t6 } task t10 "Task 10" { effort 3d allocate h { alternative a } } } task sg3 "Sub Group 3" { task t1 "Task 1" { effort 2d allocate a } task t2 "Task 2" { length 2d priority 600 allocate b depends !t1 } task t3 "Task 3" { duration 10d allocate a { alternative b, c, d, e, f, g, h } priority 200 } task t4 "Task 4" { depends !t1 length 5d allocate c priority 800 limits { dailymax 2h } } task t5 "Task 5" { effort 10d allocate d } task t6 "Task 6" { depends !t5 effort 5d allocate e { mandatory } allocate f { alternative g } } task t7 "Task 7" { duration 15d depends !t1 allocate e, f, g, h priority 900 shift parttime } task t8 "Task 8" { depends !t4 milestone } task t9 "Task 9" { depends !t6 } task t10 "Task 10" { effort 3d allocate h { alternative a } } } task sg4 "Sub Group 4" { depends !sg2 task t1 "Task 1" { effort 2d allocate a } task t2 "Task 2" { length 2d priority 600 allocate b depends !t1 } task t3 "Task 3" { duration 4d allocate a { alternative b } priority 200 } task t4 "Task 4" { depends !t1 length 5d allocate c priority 800 limits { dailymax 2h } } task t5 "Task 5" { effort 10d allocate d } task t6 "Task 6" { depends !t5 effort 5d allocate e { mandatory } allocate f { alternative g } } task t7 "Task 7" { duration 5d depends !t1 allocate e priority 900 shift parttime } task t8 "Task 8" { depends !t4 milestone } task t9 "Task 9" { depends !t6 } task t10 "Task 10" { effort 3d allocate h { alternative a } } } task sg5 "Sub Group 5" { task t1 "Task 1" { effort 2d allocate a } task t2 "Task 2" { length 2d priority 600 allocate b depends !t1 } task t3 "Task 3" { duration 4d } task t4 "Task 4" { depends !t1 length 5d allocate c priority 800 limits { dailymax 2h } } task t5 "Task 5" { effort 10d allocate d } task t6 "Task 6" { depends !t5 effort 5d allocate e { mandatory } allocate f { alternative g } } task t7 "Task 7" { duration 15d depends !t1 allocate e, f, g, h priority 900 shift parttime } task t8 "Task 8" { depends !t4 milestone } task t9 "Task 9" { depends !t6 } task t10 "Task 10" { effort 3d allocate h { alternative a } } } task sg6 "Sub Group 6" { depends !sg4 task t1 "Task 1" { effort 2d allocate a } task t2 "Task 2" { length 2d priority 600 allocate b depends !t1 } task t3 "Task 3" { duration 4d allocate a { alternative b } priority 200 } task t4 "Task 4" { depends !t1 length 5d allocate c priority 800 limits { dailymax 2h } } task t5 "Task 5" { effort 10d allocate d } task t6 "Task 6" { depends !t5 effort 5d allocate e { mandatory } allocate f { alternative g } } task t7 "Task 7" { duration 5d depends !t1 } task t8 "Task 8" { depends !t4 milestone } task t9 "Task 9" { depends !t6 } task t10 "Task 10" { effort 3d allocate h { alternative a } } } task sg7 "Sub Group 7" { task t1 "Task 1" { effort 2d allocate a } task t2 "Task 2" { length 2d priority 600 allocate b depends !t1 } task t3 "Task 3" { effort 1d allocate a { alternative b, c, d, e, f, h } priority 200 } task t4 "Task 4" { depends !t1 length 5d allocate c priority 800 limits { dailymax 2h } } task t5 "Task 5" { effort 10d allocate d } task t6 "Task 6" { depends !t5 effort 5d allocate e { mandatory } allocate f { alternative g } } task t7 "Task 7" { effort 1d depends !t1 allocate e priority 900 shift parttime } task t8 "Task 8" { depends !t4 milestone } task t9 "Task 9" { depends !t6 } task t10 "Task 10" { effort 3d allocate h { alternative a } } } task sg8 "Sub Group 8" { depends !sg7 task t1 "Task 1" { effort 2d allocate a } task t2 "Task 2" { length 2d priority 600 allocate b depends !t1 } task t3 "Task 3" { duration 4d allocate a { alternative b } priority 200 } task t4 "Task 4" { depends !t1 length 5d allocate c priority 800 limits { dailymax 2h } } task t5 "Task 5" { effort 10d allocate d } task t6 "Task 6" { depends !t5 effort 5d allocate e { mandatory } allocate f { alternative g } } task t7 "Task 7" { duration 5d depends !t1 allocate e priority 900 shift parttime } task t8 "Task 8" { depends !t4 milestone } task t9 "Task 9" { depends !t6 } task t10 "Task 10" { effort 3d allocate h { alternative a } } } task sg9 "Sub Group 9" { task t1 "Task 1" { effort 2d allocate a } task t2 "Task 2" { length 2d priority 600 allocate b depends !t1 } task t3 "Task 3" { effort 4d allocate a { alternative b } priority 200 } task t4 "Task 4" { depends !t1 length 5d allocate c priority 800 limits { dailymax 2h } } task t5 "Task 5" { effort 10d allocate d } task t6 "Task 6" { depends !t5 effort 5d allocate e { mandatory } allocate f { alternative g } } task t7 "Task 7" { effort 1d depends !t1 allocate e, f, g, h priority 900 shift parttime } task t8 "Task 8" { depends !t4 milestone } task t9 "Task 9" { depends !t6 } task t10 "Task 10" { effort 3d allocate h { alternative a } } } task sg10 "Sub Group 10" { depends !sg6 task t1 "Task 1" { effort 2d allocate a } task t2 "Task 2" { length 2d priority 600 allocate b depends !t1 } task t3 "Task 3" { duration 4d allocate a { alternative b } priority 200 } task t4 "Task 4" { depends !t1 length 5d allocate c priority 800 limits { dailymax 2h } } task t5 "Task 5" { effort 10d allocate d } task t6 "Task 6" { depends !t5 effort 5d allocate e { mandatory } allocate f { alternative g } } task t7 "Task 7" { duration 5d depends !t1 allocate e priority 900 shift parttime } task t8 "Task 8" { depends !t4 milestone } task t9 "Task 9" { depends !t6 } task t10 "Task 10" { effort 3d allocate h { alternative a } } } } taskreport "allocatedSlots" { formats csv columns name, daily } TaskJuggler-3.8.1/benchmarks/booking.tjp000066400000000000000000000057401473026623400202460ustar00rootroot00000000000000project test "Booking Performance Test" "1.0" 2008-01-01 +1m { timezone 'UTC' } macro r_and_t [ resource r${1} "Resource ${1}" task t${1} "Task ${1}" { period ${projectstart} - ${projectend} booking r${1} 2008-01-01-00:00:00-+0000 - 2008-01-02 { sloppy 2 } booking r${1} 2008-01-02-00:00:00-+0000 - 2008-01-03 { sloppy 2 } booking r${1} 2008-01-03-00:00:00-+0000 - 2008-01-04 { sloppy 2 } booking r${1} 2008-01-04-00:00:00-+0000 - 2008-01-05 { sloppy 2 } booking r${1} 2008-01-05-00:00:00-+0000 - 2008-01-06 { sloppy 2 } booking r${1} 2008-01-06-00:00:00-+0000 - 2008-01-07 { sloppy 2 } booking r${1} 2008-01-07-00:00:00-+0000 - 2008-01-08 { sloppy 2 } booking r${1} 2008-01-08-00:00:00-+0000 - 2008-01-09 { sloppy 2 } booking r${1} 2008-01-09-00:00:00-+0000 - 2008-01-10 { sloppy 2 } booking r${1} 2008-01-10-00:00:00-+0000 - 2008-01-11 { sloppy 2 } booking r${1} 2008-01-11-00:00:00-+0000 - 2008-01-12 { sloppy 2 } booking r${1} 2008-01-12-00:00:00-+0000 - 2008-01-13 { sloppy 2 } booking r${1} 2008-01-13-00:00:00-+0000 - 2008-01-14 { sloppy 2 } booking r${1} 2008-01-14-00:00:00-+0000 - 2008-01-15 { sloppy 2 } booking r${1} 2008-01-15-00:00:00-+0000 - 2008-01-16 { sloppy 2 } booking r${1} 2008-01-16-00:00:00-+0000 - 2008-01-17 { sloppy 2 } booking r${1} 2008-01-17-00:00:00-+0000 - 2008-01-18 { sloppy 2 } booking r${1} 2008-01-18-00:00:00-+0000 - 2008-01-19 { sloppy 2 } booking r${1} 2008-01-19-00:00:00-+0000 - 2008-01-20 { sloppy 2 } booking r${1} 2008-01-20-00:00:00-+0000 - 2008-01-21 { sloppy 2 } booking r${1} 2008-01-21-00:00:00-+0000 - 2008-01-22 { sloppy 2 } booking r${1} 2008-01-22-00:00:00-+0000 - 2008-01-23 { sloppy 2 } booking r${1} 2008-01-23-00:00:00-+0000 - 2008-01-24 { sloppy 2 } booking r${1} 2008-01-24-00:00:00-+0000 - 2008-01-25 { sloppy 2 } booking r${1} 2008-01-25-00:00:00-+0000 - 2008-01-26 { sloppy 2 } booking r${1} 2008-01-26-00:00:00-+0000 - 2008-01-27 { sloppy 2 } booking r${1} 2008-01-27-00:00:00-+0000 - 2008-01-28 { sloppy 2 } booking r${1} 2008-01-28-00:00:00-+0000 - 2008-01-29 { sloppy 2 } booking r${1} 2008-01-29-00:00:00-+0000 - 2008-01-30 { sloppy 2 } } ] macro r_and_t_10 [ ${r_and_t "${1}0"} ${r_and_t "${1}1"} ${r_and_t "${1}2"} ${r_and_t "${1}3"} ${r_and_t "${1}4"} ${r_and_t "${1}5"} ${r_and_t "${1}6"} ${r_and_t "${1}7"} ${r_and_t "${1}8"} ${r_and_t "${1}9"} ] macro r_and_t_100 [ ${r_and_t_10 "${1}0"} ${r_and_t_10 "${1}1"} ${r_and_t_10 "${1}2"} ${r_and_t_10 "${1}3"} ${r_and_t_10 "${1}4"} ${r_and_t_10 "${1}5"} ${r_and_t_10 "${1}6"} ${r_and_t_10 "${1}7"} ${r_and_t_10 "${1}8"} ${r_and_t_10 "${1}9"} ] ${r_and_t_100 "0"} ${r_and_t_100 "1"} ${r_and_t_100 "2"} ${r_and_t_100 "3"} ${r_and_t_100 "4"} ${r_and_t_100 "5"} ${r_and_t_100 "6"} ${r_and_t_100 "7"} ${r_and_t_100 "8"} ${r_and_t_100 "9"} TaskJuggler-3.8.1/benchmarks/depends.tjp000066400000000000000000000045111473026623400202330ustar00rootroot00000000000000project test "Dependency Performance Test" "1.0" 2007-08-25 +3m macro leaftask [ task l${1} "Leaftask ${1}" { length 1d } ] macro subtasks [ task s${1} "Subtasks ${1}" { ${leaftask "0"} ${leaftask "1"} ${leaftask "2"} ${leaftask "3"} ${leaftask "4"} ${leaftask "5"} ${leaftask "6"} ${leaftask "7"} ${leaftask "8"} ${leaftask "9"} } ] macro tasktree [ task t${1} "Task ${1}" { start ${projectstart} ${subtasks "0"} ${subtasks "1"} ${subtasks "2"} ${subtasks "3"} ${subtasks "4"} ${subtasks "5"} ${subtasks "6"} ${subtasks "7"} ${subtasks "8"} ${subtasks "9"} } ] macro depends [ supplement task ${1} { depends ${2} } ] macro dependsXY [ ${depends "t1.s${2}.l${2}" "t0.s${1}.l${1}"} ] macro dependLeaf [ ${depends "${1}.l1" "!l0"} ${depends "${1}.l2" "!l1"} ${depends "${1}.l3" "!l2"} ${depends "${1}.l4" "!l3"} ${depends "${1}.l5" "!l4"} ${depends "${1}.l6" "!l5"} ${depends "${1}.l7" "!l6"} ${depends "${1}.l8" "!l6"} ${depends "${1}.l9" "!l8"} ] ${tasktree "0"} ${tasktree "1"} ${dependsXY "1" "0"} ${dependsXY "2" "1"} ${dependsXY "3" "2"} ${dependsXY "4" "3"} ${dependsXY "5" "4"} ${dependsXY "6" "5"} ${dependsXY "7" "6"} ${dependsXY "8" "7"} ${dependsXY "9" "8"} ${dependLeaf "t0.s0"} ${dependLeaf "t0.s1"} ${dependLeaf "t0.s2"} ${dependLeaf "t0.s4"} ${dependLeaf "t0.s5"} ${dependLeaf "t0.s6"} ${dependLeaf "t0.s8"} ${dependLeaf "t0.s9"} ${dependLeaf "t1.s1"} ${dependLeaf "t1.s2"} ${dependLeaf "t1.s3"} ${dependLeaf "t1.s4"} ${dependLeaf "t1.s5"} ${dependLeaf "t1.s7"} ${dependLeaf "t1.s8"} ${dependLeaf "t1.s9"} ${depends "t0.s3.l2" "t1.s1.l1"} ${depends "t0.s2.l3" "t1.s2.l9"} ${depends "t0.s2.l8" "t1.s3.l3"} ${depends "t0.s3.l2" "t1.s7.l2"} ${depends "t0.s3.l9" "t0.s1.l5"} ${depends "t0.s4.l2" "t0.s1.l8"} ${depends "t0.s1.l1" "t0.s0.l5"} ${depends "t0.s4.l2" "t0.s2.l7"} ${depends "t1.s9.l2" "t1.s7.l4"} ${depends "t1.s7.l1" "t1.s2.l8"} ${depends "t0.s8.l0" "t0.s4.l5"} ${depends "t1.s4.l1" "t0.s6.l8"} ${depends "t1.s4.l4" "t0.s5.l4"} ${depends "t0.s5.l3" "t0.s3.l9"} ${depends "t1.s8.l0" "t0.s2.l8"} ${depends "t1.s9.l1" "t1.s6.l6"} ${depends "t0.s7.l7" "t0.s5.l9"} ${depends "t0.s5.l2" "t0.s1.l4"} ${depends "t1.s2.l1" "t0.s7.l7"} taskreport depends "depends" { formats html columns no, name, id, chart { scale week } } TaskJuggler-3.8.1/benchmarks/gantt.tjp000066400000000000000000000020671473026623400177320ustar00rootroot00000000000000project test "HTML Task Report Performance Test" "1.0" 2007-08-25 +3m macro leaftask [ task leaf${1} "Leaftask ${1}" { duration 1d depends ${2} } ] macro subtasks [ task s${1} "Subtasks ${1}" { ${leaftask "0" "start"} ${leaftask "1" "!leaf0"} ${leaftask "2" "!leaf1, !leaf0"} ${leaftask "3" "!leaf0, !leaf1, !leaf2"} ${leaftask "4" "!leaf3"} ${leaftask "5" "start" } ${leaftask "6" "!leaf5"} ${leaftask "7" "!leaf5, !leaf6"} ${leaftask "8" "!leaf5, !leaf6, !leaf7"} ${leaftask "9" "!leaf8"} } ] macro tasktree [ task t${1} "Task ${1}" { ${subtasks "0"} ${subtasks "1"} ${subtasks "2"} ${subtasks "3"} ${subtasks "4"} ${subtasks "5"} ${subtasks "6"} ${subtasks "7"} ${subtasks "8"} ${subtasks "9"} } ] task start "Start" ${tasktree "0"} ${tasktree "1"} ${tasktree "2"} ${tasktree "3"} ${tasktree "4"} ${tasktree "5"} ${tasktree "6"} ${tasktree "7"} ${tasktree "8"} ${tasktree "9"} ${tasktree "10"} taskreport "gantt" { formats html columns no, name, start, end, chart } TaskJuggler-3.8.1/benchmarks/htmltaskreport.tjp000066400000000000000000000023131473026623400216720ustar00rootroot00000000000000project test "HTML Task Report Performance Test" "1.0" 2007-08-25 +3m macro leaftask [ task leaf${1} "Leaftask ${1}" { period ${projectstart} - ${projectend} } ] macro subtasks [ task s${1} "Subtasks ${1}" { ${leaftask "0"} ${leaftask "1"} ${leaftask "2"} ${leaftask "3"} ${leaftask "4"} ${leaftask "5"} ${leaftask "6"} ${leaftask "7"} ${leaftask "8"} ${leaftask "9"} } ] macro tasktree [ task t${1} "Task ${1}" { ${subtasks "0"} ${subtasks "1"} ${subtasks "2"} ${subtasks "3"} ${subtasks "4"} ${subtasks "5"} ${subtasks "6"} ${subtasks "7"} ${subtasks "8"} ${subtasks "9"} } ] ${tasktree "0"} ${tasktree "1"} ${tasktree "2"} ${tasktree "3"} ${tasktree "4"} ${tasktree "5"} ${tasktree "6"} ${tasktree "7"} ${tasktree "8"} ${tasktree "9"} ${tasktree "10"} taskreport "htmltaskreport-1" { formats html columns no, name, start, end, chart } taskreport "htmltaskreport-2" { formats html columns no, name, start, end, daily } taskreport "htmltaskreport-3" { formats html columns no, name, start, end, weekly } taskreport "htmltaskreport-4" { formats html columns no, name, start, end, daily sorttasks id.up } TaskJuggler-3.8.1/benchmarks/runbench.rb000066400000000000000000000014601473026623400202230ustar00rootroot00000000000000# # runbench.rb - TaskJuggler # # Copyright (c) 2007 by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # $:.unshift File.join(File.dirname(__FILE__), '..', 'lib') require 'benchmark' require 'taskjuggler/TaskJuggler' require 'taskjuggler/Tj3Config' AppConfig.appName = 'taskjuggler3' ENV['TASKJUGGLER_DATA_PATH'] = '../' Benchmark.bm(25) do |x| Dir.glob('*.tjp').each do |f| x.report(f) do tj = TaskJuggler.new(true) tj.parse([ f ]) tj.schedule tj.generateReports unless tj.project.reports.empty? end end Dir.glob('*.html').each { |f| File.delete(f) } Dir.glob('*.csv').each { |f| File.delete(f) } end TaskJuggler-3.8.1/bin/000077500000000000000000000000001473026623400145245ustar00rootroot00000000000000TaskJuggler-3.8.1/bin/tj3000077500000000000000000000001761473026623400151560ustar00rootroot00000000000000#!/usr/bin/env ruby $:.unshift File.join(File.dirname(File.realpath(__FILE__)), '..', 'lib') require File.basename(__FILE__) TaskJuggler-3.8.1/bin/tj3client000077500000000000000000000001761473026623400163550ustar00rootroot00000000000000#!/usr/bin/env ruby $:.unshift File.join(File.dirname(File.realpath(__FILE__)), '..', 'lib') require File.basename(__FILE__) TaskJuggler-3.8.1/bin/tj3d000077500000000000000000000001761473026623400153220ustar00rootroot00000000000000#!/usr/bin/env ruby $:.unshift File.join(File.dirname(File.realpath(__FILE__)), '..', 'lib') require File.basename(__FILE__) TaskJuggler-3.8.1/bin/tj3man000077500000000000000000000001761473026623400156520ustar00rootroot00000000000000#!/usr/bin/env ruby $:.unshift File.join(File.dirname(File.realpath(__FILE__)), '..', 'lib') require File.basename(__FILE__) TaskJuggler-3.8.1/bin/tj3ss_receiver000077500000000000000000000001761473026623400174100ustar00rootroot00000000000000#!/usr/bin/env ruby $:.unshift File.join(File.dirname(File.realpath(__FILE__)), '..', 'lib') require File.basename(__FILE__) TaskJuggler-3.8.1/bin/tj3ss_sender000077500000000000000000000001761473026623400170640ustar00rootroot00000000000000#!/usr/bin/env ruby $:.unshift File.join(File.dirname(File.realpath(__FILE__)), '..', 'lib') require File.basename(__FILE__) TaskJuggler-3.8.1/bin/tj3ts_receiver000077500000000000000000000001761473026623400174110ustar00rootroot00000000000000#!/usr/bin/env ruby $:.unshift File.join(File.dirname(File.realpath(__FILE__)), '..', 'lib') require File.basename(__FILE__) TaskJuggler-3.8.1/bin/tj3ts_sender000077500000000000000000000001761473026623400170650ustar00rootroot00000000000000#!/usr/bin/env ruby $:.unshift File.join(File.dirname(File.realpath(__FILE__)), '..', 'lib') require File.basename(__FILE__) TaskJuggler-3.8.1/bin/tj3ts_summary000077500000000000000000000001761473026623400173020ustar00rootroot00000000000000#!/usr/bin/env ruby $:.unshift File.join(File.dirname(File.realpath(__FILE__)), '..', 'lib') require File.basename(__FILE__) TaskJuggler-3.8.1/bin/tj3webd000077500000000000000000000001761473026623400160200ustar00rootroot00000000000000#!/usr/bin/env ruby $:.unshift File.join(File.dirname(File.realpath(__FILE__)), '..', 'lib') require File.basename(__FILE__) TaskJuggler-3.8.1/data/000077500000000000000000000000001473026623400146655ustar00rootroot00000000000000TaskJuggler-3.8.1/data/css/000077500000000000000000000000001473026623400154555ustar00rootroot00000000000000TaskJuggler-3.8.1/data/css/tjmanual.css000066400000000000000000000025771473026623400200150ustar00rootroot00000000000000pre { font-size:16px; font-family: Courier; padding-left:8px; padding-right:8px; padding-top:0px; padding-bottom:0px; } p { margin-top:8px; margin-bottom:8px; } code { font-size:16px; font-family: Courier; } .table { background-color:#ABABAB; width:100%; } .tag { background-color:#E0E0F0; font-size:16px; font-weight:bold; padding-left:8px; padding-right:8px; padding-top:5px; padding-bottom:5px; } .descr { background-color:#F0F0F0; font-size:16px; padding-left:8px; padding-right:8px; padding-top:5px; padding-bottom:5px; } .attrtable { background-color:#ABABAB; } .attrtag { background-color:#F0F0F0; font-family: Courier; padding-left:6px; padding-right:6px; padding-top:3px; padding-bottom:3px; } .attrdescr { background-color:#F0F0F0; padding-left:6px; padding-right:6px; padding-top:3px; padding-bottom:3px; width:100%; } .codeframe{ border-width:2px; border-color:#ABABAB; border-style:solid; background-color:#F0F0F0; margin-top:8px; margin-bottom:8px; } .code { padding-left:15px; padding-right:15px; padding-top:0px; padding-bottom:0px; } div[codesection] { border-width:2px; border-color:#ABABAB; border-style:solid; background-color:#F0F0F0; margin-top:8px; margin-bottom:8px; } pre[codesection] { padding-left:15px; padding-right:15px; padding-top:0px; padding-bottom:0px; } TaskJuggler-3.8.1/data/css/tjreport.css000066400000000000000000000217261473026623400200500ustar00rootroot00000000000000/* WARNING: This is an automatically generated file. DO NOT EDIT IT! * If you want to use your own style sheet, keep a master copy * somewhere else and copy it over this file if needed. This file will * be overwritten whenever the stylesheet that comes with TaskJuggler * has been modified. */ body { font-family:Bitstream Vera Sans, Tahoma, sans-serif; font-size:15px; } h1, h2, table, tr, td, div, span { } table { } /* Treat images in tables as block and not line elements. This will * eliminate surprising space at the bottom. */ td img {display: block;} p { font-size:15px; } td, div { padding:0px; margin:0px; } h1 { font-size:22px; } h2 { font-size:18px; } h3 { font-size:16px; } .tj_journal { font-size:11px; } i.tj_journal { font-size:9px; } h1.tj_journal { font-size:16px; margin-left:0px; } h2.tj_journal { font-size:14px; margin-left:10px; } h3.tj_journal { margin-top:5px; font-size:13px; margin-bottom:1px; margin-left:20px; } h4.tj_journal { font-size:12px; margin-left:30px; } p.tj_journal { margin-top:1px; margin-bottom:5px; margin-left:30px; } /* The basic elements of a text report page. */ .tj_text_page { width:100%; border-spacing:0px; } .tj_text_row { } .tj_column_left { vertical-align:top; } .tj_column_center { vertical-align:top; } .tj_column_right { vertical-align:top; } /* The top-level page layout */ .tj_page { margin: 35px 5% 25px 5%; } /* The container that holds report tables */ .tj_table_frame { margin-left:auto; margin-right:auto; text-align:center; background-color:#9a9a9a; margin-top:15px; margin-bottom:15px; border-spacing:1px; font-size:13px; } /* The headline box for report tables */ .tj_table_headline { font-size:16px; font-weight:bold; white-space:nowrap; padding:5px; margin:1px; text-align:center; color:#000000; background-color:#d4dde6; } .tj_table { background-color:#9a9a9a; margin:0px; border-spacing:1px; } /* The cells of the table header. */ .tj_table_header_cell { padding:1px 3px 1px 3px; white-space:nowrap; border-spacing:0px; color:#ffffff; overflow:hidden; } /* A regular table cell. It usually contains the cell icon, the text * label and a tooltip trigger. */ .tj_table_cell { font-size:13px; vertical-align:top; padding:1px 3px 1px 3px; margin:0px; width:100%; border-spacing:0px; position: relative; overflow:hidden; } /* The symbol is the icon to the left of the text label in a table * cell. */ .tj_table_cell_icon { vertical-align:top; text-align:right; padding:1px 3px 0px 0px; width:19px; } /* This is the text label of a cell. */ .tj_table_cell_label { font-size:12px; vertical-align:top; padding-top:1px; } /* The box around the icon to the right of the text label. This is * optional and triggers the tooltip with the full text of the cell in * case the cell is not large enough to show everything. */ .tj_table_cell_tooltip { font-size:13px; vertical-align:top; padding:2px 0px 0px 3px; } /* The container that holds the invisible tooltips. */ .tj_tooltip_box{ position:fixed; top:0px; left:0px; display:none; visibility:hidden; } /* The caption box for report tables */ .tj_table_caption { padding: 5px 13px 5px 13px; background-color:#ebf2ff; text-align:left; white-space:normal; margin:1px; font-size:13px } .tj_table_legend_frame { padding:5px; margin:1px; background-color:#d4dde6; } /* The legend of reports with calendar and Gantt charts */ .tj_table_legend { margin-left:auto; margin-right:auto; text-align:center; font-size:11px; color:#000000; border-spacing:1px; } /* A row of the table legend */ .tj_legend_row { height:19px; } /* Headlines used for the legend when both chart types are used in a * report */ .tj_legend_headline { font-size:12px; font-weight:bold; } /* A legend row has 3 items. An item contains a label and a symbol */ .tj_legend_item { } .tj_legend_symbol { position: relative; width:45px; height:19px; } .tj_legend_label { text-align: left; } .tj_legend_spacer { width:30px; } .tj_gantt_jag { position:absolute; border-style: solid; width: 0px; height: 0px; line-height: 0px; border-top: 5px solid black; border-left: 5px solid transparent; border-right: 5px solid transparent; border-bottom: none } .tj_diamond_top { position:absolute; border-style: solid; width: 0px; height: 0px; line-height: 0px; border-top: none; border-left: 7px solid transparent; border-right: 7px solid transparent; border-bottom: 7px solid black; } .tj_diamond_bottom { position:absolute; border-style: solid; width: 0px; height: 0px; line-height: 0px; border-top: 7px solid black; border-left: 7px solid transparent; border-right: 7px solid transparent; border-bottom: none; } .tj_arrow_head { position:absolute; border-style: solid; width: 0px; height: 0px; line-height: 0px; border-top: 5px solid transparent; border-left: 5px solid black; border-right: none; border-bottom: 5px solid transparent; } .tabback { background-color:#9a9a9a; overflow:visible; } .tabfront { background-color:#d4dde6; } .tabhead { white-space:nowrap; background-color:#7a7a7a; color:#ffffff; text-align:center; } .tabhead_offduty { white-space:nowrap; background-color:#bdbdaa; color:#000000; } .tabfooter { white-space:nowrap; background-color:#9a9a9a; color:#ffffff; text-align:center; } .headercelldiv { padding-top:1px; padding-right:3px; padding-left:3px; padding-bottom:0px; white-space:nowrap; overflow:hidden; } .celldiv { padding:1px 3px 2px 3px; white-space:nowrap; overflow:hidden; position: relative; } .tabline { color:#000000 } .tabcell { white-space:nowrap; overflow:hidden; padding:0px; } .costaccountcell1 { background-color:#fff2eb; white-space:nowrap; padding:0px; } .costaccountcell2 { background-color:#ebdfd9; white-space:nowrap; padding:0px; } .revenueaccountcell1 { background-color:#cbffcc; white-space:nowrap; padding:0px; } .revenueaccountcell2 { background-color:#a6d0a6; white-space:nowrap; padding:0px; } .accountcell1 { background-color:#ebf2ff; white-space:nowrap; padding:0px; } .accountcell2 { background-color:#d9dfeb; white-space:nowrap; padding:0px; } .taskcell1 { background-color:#ebf2ff; white-space:nowrap; padding:0px; } .taskcell2 { background-color:#d9dfeb; white-space:nowrap; padding:0px; } .resourcecell1 { background-color:#fff2eb; white-space:nowrap; padding:0px; } .resourcecell2 { background-color:#ebdfd9; white-space:nowrap; padding:0px; } /* The *2 versions have a 20 points less HSV value of the *1 versions*/ .busy1 { background-color:#ff3b3b; } .busy2 { background-color:#eb3636; } .loaded1 { background-color:#ff9b9b; } .loaded2 { background-color:#eb8f8f; } .free1 { background-color:#a5ffb4; } .free2 { background-color:#98eba6; } .offduty1 { background-color:#bdbdaa; } .offduty2 { background-color:#a9a999; } .calconttask1 { background-color:#abbeae; } .calconttask2 { background-color:#99aa9c; } .caltask1 { background-color:#2050e5; } .caltask2 { background-color:#1c4ad1; } .todo1 { background-color:#beabab; } .todo2 { background-color:#aa9999; } .tabvline { background-color:#9a9a9a; position:absolute; } .tj_gantt_frame { position:absolute; /* Make sure this element is above all other elements */ z-index:100; } .containerbar { background-color:#09090a; position:absolute; } .taskbarframe { background-color:#09090a; position:absolute; } .taskbar { background-color:#2f57ea; position:absolute; } .progressbar { background-color:#36363f; position:absolute; } .milestone { background-color:#09090a; position:absolute; } .loadstackframe { background-color:#452a2a; position:absolute; } .free { background-color:#a5ffb5; position:absolute; } .busy { background-color:#ff9b9b; position:absolute; } .assigned { background-color:#ff3b3b; position:absolute; } .offduty { background-color:#bdbdaa; white-space:nowrap; position:absolute; } .depline { background-color:#000000; position:absolute; } .nowline { background-color:#EE0000; position:absolute; } .markdateline { background-color:#000000; position:absolute; } .white { background-color:#FFFFFF; position:absolute; } .navbar_topruler { margin:7px 0px 0px 0px; } .navbar_midruler { margin:5px 0px 0px 0px; } .navbar_bottomruler { margin:5px 0px 7px 0px; } .navbar_current { background-color:#606060; font-size:13px; font-weight:bold; padding:5px; color:#FFFFFF; } .navbar_other { background-color:#FFFFFF; font-size:13px; font-weight:bold; padding:2px; } .navbar { font-size:20px; font-weight:bold; padding:0px; } .copyright { font-size:9px; color:#101010; text-align:center; margin-top:10px; } div[codesection] { border-width:2px; border-color:#ABABAB; border-style:solid; background-color:#F0F0F0; margin-top:8px; margin-bottom:8px; } pre[codesection] { padding-left:15px; padding-right:15px; padding-top:0px; padding-bottom:0px; } TaskJuggler-3.8.1/data/icons/000077500000000000000000000000001473026623400160005ustar00rootroot00000000000000TaskJuggler-3.8.1/data/icons/details.png000066400000000000000000000013501473026623400201320ustar00rootroot00000000000000PNG  IHDR;֕JsRGBbKGD pHYs  tIME  :^HhIDAT(u_ha?Mha9"XM֔դą 7Db4 MZcsFNg"{?yy_ԽgR2aK`cK,QPF$͔3KmHǴǝb /B;yc#Hs/ԑGǥ?7S czo/3p[Vxa~i8Y+}[7rۿvI/8eR y=!dJ.ߝ=܉n {88R*b {﵈%L]3a:lhu j@24?wIENDB`TaskJuggler-3.8.1/data/icons/flag-red.png000066400000000000000000000011021473026623400201610ustar00rootroot00000000000000PNG  IHDR;֕JsRGBbKGD pHYs7\7\ǤtIME  )IDAT(ϝMkSA{ܐmըE)]WܸFDRHbTJ臘6)6Mn" $fSY <Jҋ[wg8/k5k7<'owxC  ~Y= r vU:{q2KFak-9jTpҜB7 SDR 0fgB =O }ErIENDB`TaskJuggler-3.8.1/data/icons/flag-yellow.png000066400000000000000000000011011473026623400207210ustar00rootroot00000000000000PNG  IHDR;֕JsRGBbKGD pHYs7\7\ǤtIME  [.9IDAT(ϝMkA3;ɺtcb%x/~ ?7/**^z[xGAAJb)-l`6ٙCКKayghW\ƜJ{54@(~yਙW( kT|toeC5?~ V7t?u. X垦5-cVJVOJgGx5%5- Q<1_R *T~\|$y2 !9D,h!ˆt:}vJ.V(ZEeaE&& =Nk=go,׷6N |mߺ9|5RUDU@`'.24z}uln3+cs'wxZs|pe(]pxťS@^$[>P.< (OzIENDB`TaskJuggler-3.8.1/data/icons/resource.png000066400000000000000000000013221473026623400203330ustar00rootroot00000000000000PNG  IHDR;֕JsRGB pHYs  tIME ȏ`dIDAT(uKTQǿ͛y31&ǩ&C[ƅR`AfEmEPAXgFDaY 5lqF̼wo KFcŹ|?/vEU@MdgWS__?v\ŭ^sg_DkN>Zf8r`'CKuqGJ, Qe9iy>gpԚ9G2UHBSpE F.99'9p/yAi #vI8mg(z f m ?bo}xrb 3bfb+ cadl55]rr[o ˧ 3 f@A"GUEV^+/.nkn:"D0" 7ŭܷӇ{=2d,!bݞ\K?0=0U}܊ A\ `:(G.tFwx0uGI HD)s$l0 DžAz> q YHpEsϮ%pm%W :\,}1jˉwq&5TF7z{]8ސ00fg7BF'\ (gIENDB`TaskJuggler-3.8.1/data/icons/resourcegroup.png000066400000000000000000000015421473026623400214140ustar00rootroot00000000000000PNG  IHDR;֕JsRGB pHYs  tIME 4$MIDAT(]]h[e䜓&'icf4 WuuBEA MŁ/AB'"N/J2vXZs:ɺ,ےiNNr~>/p~^O=6>O}0сswOo:ʝ}UY:Y宅e*[f>3 [/{ߓָ w~)ޔ>P[zg/m/^2|V9PM)&CD: A@j@^|P3[H)w祕Xm/"LTo'g;wmkj.e'KNbEI4v{JmfruMGu]nS݁؃hQL~U;DnV)#8ev#")f:@$* ǎ!I]*5S|T:PAra φLd˅ 7gc/o9QOAٟm_WmM zk{6! 2KZc/y&^cIENDB`TaskJuggler-3.8.1/data/icons/taskgroup.png000066400000000000000000000007501473026623400205270ustar00rootroot00000000000000PNG  IHDR醟sRGB pHYs  tIME 67ftEXtCommentCreated with GIMPWUIDAT8˭JQ= ^/ ,}Bl7lE,D,, +kVEh3(**|p\爙}reg%]t y7: \VdfTu&} Vm_Jq֛|Oٵ=0#2,; P_Li79HEkIENDB`TaskJuggler-3.8.1/data/icons/trend-down.png000066400000000000000000000013641473026623400205730ustar00rootroot00000000000000PNG  IHDR;֕JsRGBbKGD pHYs^tIME 7VDtIDAT(ύ=LSQϽVPj+1$ƁM\\4#1qq0 ~ :"1U ! M *5@j$}}:hRxƓ'CP3zд|J閾ʬTi, f( L+hdv*B(#7LBN/Av0E^@;ryұy#۱횢fɛd` s˩O $jz{jY"-A7L˜9C1mY4PzUږ5,1s{dOͲ}0 *QBe 6i0sr4bx:wklw\DsUCU58$b. _fgsSω$Pg`Fѯ=b1fC`:lPNp[!Xͦ7pȹ}Už!S#ɅGx ]-uCr2ndݘ-@(dZ5KǪ7n9e$4148S?+=b &t$BQ0>\E)JolȾKW]>ZOv7=xYuT.dQ5tM៣MIENDB`TaskJuggler-3.8.1/data/icons/trend-flat.png000066400000000000000000000013371473026623400205520ustar00rootroot00000000000000PNG  IHDR;֕JsRGBbKGD pHYs^tIME 6H2_IDAT(ϕMHTQsϹW&jifL!CB #Irf 7A G]#bbCEd!)*Mo9(Ȟ~/M}&T5t{>0w]rGXuk3*{t7Z.mő3 f-Uepg&(ͽYxA2o+g7y?t`QZtL `h>2!wGZF>Nfl\&ˠf؜B-8NKVvxeBXP !>!-e4p|3?K|=u+ljz`8$qUTUR eAI(BX -'a2Wf2-yS pGDeHB `r$F U#ɗ5[Z#or B"XT!T`6$ [׃ޱ"r E K\<$%> ŧ v=V Ug7lrMum'=muHrtQ# K;*j B ν.mr%n+,[h_Lz"wF ;O'IENDB`TaskJuggler-3.8.1/data/icons/trend-up.png000066400000000000000000000014001473026623400202370ustar00rootroot00000000000000PNG  IHDR;֕JsRGBbKGD pHYs^tIME 8.وJIDAT(}[Hqƿ3ά]L /i%EU 1("" EI#)}z Q =ASD)IÖζٝv~VR 8?8}录b9K y挎,̫l?|mX):w27DIyTYzS'E'a)?4+Xqpө`qeõ4 @X0=1M}l[ɸw<|eO%%b@.+}? /0(B%*eDXxI,9T^aj&,ɻV0/ӳ)a SrZgI/iMIENDB`TaskJuggler-3.8.1/data/scripts/000077500000000000000000000000001473026623400163545ustar00rootroot00000000000000TaskJuggler-3.8.1/data/scripts/wz_tooltip.js000066400000000000000000001073421473026623400211330ustar00rootroot00000000000000/* This notice must be untouched at all times. Copyright (c) 2002-2008 Walter Zorn. All rights reserved. wz_tooltip.js v. 5.31 The latest version is available at http://www.walterzorn.com or http://www.devira.com or http://www.walterzorn.de Created 1.12.2002 by Walter Zorn (Web: http://www.walterzorn.com ) Last modified: 7.11.2008 Easy-to-use cross-browser tooltips. Just include the script at the beginning of the section, and invoke Tip('Tooltip text') to show and UnTip() to hide the tooltip, from the desired HTML eventhandlers. Example: My home page No container DIV required. By default, width and height of tooltips are automatically adapted to content. Is even capable of dynamically converting arbitrary HTML elements to tooltips by calling TagToTip('ID_of_HTML_element_to_be_converted') instead of Tip(), which means you can put important, search-engine-relevant stuff into tooltips. Appearance & behaviour of tooltips can be individually configured via commands passed to Tip() or TagToTip(). Tab Width: 4 LICENSE: LGPL This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License (LGPL) as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For more details on the GNU Lesser General Public License, see http://www.gnu.org/copyleft/lesser.html */ var config = new Object(); //=================== GLOBAL TOOLTIP CONFIGURATION =========================// var tt_Debug = false // false or true - recommended: false once you release your page to the public var tt_Enabled = true // Allows to (temporarily) suppress tooltips, e.g. by providing the user with a button that sets this global variable to false var TagsToTip = true // false or true - if true, HTML elements to be converted to tooltips via TagToTip() are automatically hidden; // if false, you should hide those HTML elements yourself // For each of the following config variables there exists a command, which is // just the variablename in uppercase, to be passed to Tip() or TagToTip() to // configure tooltips individually. Individual commands override global // configuration. Order of commands is arbitrary. // Example: onmouseover="Tip('Tooltip text', LEFT, true, BGCOLOR, '#FF9900', FADEIN, 400)" config. Above = false // false or true - tooltip above mousepointer config. BgColor = '#C3C8DA' // Background colour (HTML colour value, in quotes) config. BgImg = '' // Path to background image, none if empty string '' config. BorderColor = '#999999' config. BorderStyle = 'solid' // Any permitted CSS value, but I recommend 'solid', 'dotted' or 'dashed' config. BorderWidth = 1 config. CenterMouse = true // false or true - center the tip horizontally below (or above) the mousepointer config. ClickClose = false // false or true - close tooltip if the user clicks somewhere config. ClickSticky = true // false or true - make tooltip sticky if user left-clicks on the hovered element while the tooltip is active config. CloseBtn = true // false or true - closebutton in titlebar config. CloseBtnColors = ['#505050', '#FFFFFF', '#A0A0A0', '#000000'] // [Background, text, hovered background, hovered text] - use empty strings '' to inherit title colours config. CloseBtnText = ' X ' // Close button text (may also be an image tag) config. CopyContent = true // When converting a HTML element to a tooltip, copy only the element's content, rather than converting the element by its own config. Delay = 750 // Time span in ms until tooltip shows up config. Duration = 0 // Time span in ms after which the tooltip disappears; 0 for infinite duration, < 0 for delay in ms _after_ the onmouseout until the tooltip disappears config. Exclusive = false // false or true - no other tooltip can appear until the current one has actively been closed config. FadeIn = 400 // Fade-in duration in ms, e.g. 400; 0 for no animation config. FadeOut = 400 config. FadeInterval = 30 // Duration of each fade step in ms (recommended: 30) - shorter is smoother but causes more CPU-load config. Fix = null // Fixated position, two modes. Mode 1: x- an y-coordinates in brackets, e.g. [210, 480]. Mode 2: Show tooltip at a position related to an HTML element: [ID of HTML element, x-offset, y-offset from HTML element], e.g. ['SomeID', 10, 30]. Value null (default) for no fixated positioning. config. FollowMouse = false // false or true - tooltip follows the mouse config. FontColor = '#000044' config. FontFace = 'Verdana,Geneva,sans-serif' config. FontSize = '8pt' // E.g. '9pt' or '12px' - unit is mandatory config. FontWeight = 'normal' // 'normal' or 'bold'; config. Height = 0 // Tooltip height; 0 for automatic adaption to tooltip content, < 0 (e.g. -100) for a maximum for automatic adaption config. JumpHorz = false // false or true - jump horizontally to other side of mouse if tooltip would extend past clientarea boundary config. JumpVert = true // false or true - jump vertically " config. Left = false // false or true - tooltip on the left of the mouse config. OffsetX = 14 // Horizontal offset of left-top corner from mousepointer config. OffsetY = 4 // Vertical offset config. Opacity = 100 // Integer between 0 and 100 - opacity of tooltip in percent config. Padding = 10 // Spacing between border and content config. Shadow = true // false or true config. ShadowColor = '#A0A0A0' config. ShadowWidth = 4 config. Sticky = false // false or true - fixate tip, ie. don't follow the mouse and don't hide on mouseout config. TextAlign = 'left' // 'left', 'right' or 'justify' config. Title = '' // Default title text applied to all tips (no default title: empty string '') config. TitleAlign = 'center' // 'left' or 'right' - text alignment inside the title bar config. TitleBgColor = '#7A7A7A' // If empty string '', BorderColor will be used config. TitleFontColor = '#FFFFFF' // Color of title text - if '', BgColor (of tooltip body) will be used config. TitleFontFace = '' // If '' use FontFace (boldified) config. TitleFontSize = '' // If '' use FontSize config. TitlePadding = 2 config. Width = -600 // Tooltip width; 0 for automatic adaption to tooltip content; < -1 (e.g. -240) for a maximum width for that automatic adaption; // -1: tooltip width confined to the width required for the titlebar //======= END OF TOOLTIP CONFIG, DO NOT CHANGE ANYTHING BELOW ==============// //===================== PUBLIC =============================================// function Tip() { tt_Tip(arguments, null); } function TagToTip() { var t2t = tt_GetElt(arguments[0]); if(t2t) tt_Tip(arguments, t2t); } function UnTip() { tt_OpReHref(); if(tt_aV[DURATION] < 0 && (tt_iState & 0x2)) tt_tDurt.Timer("tt_HideInit()", -tt_aV[DURATION], true); else if(!(tt_aV[STICKY] && (tt_iState & 0x2))) tt_HideInit(); } //================== PUBLIC PLUGIN API =====================================// // Extension eventhandlers currently supported: // OnLoadConfig, OnCreateContentString, OnSubDivsCreated, OnShow, OnMoveBefore, // OnMoveAfter, OnHideInit, OnHide, OnKill var tt_aElt = new Array(10), // Container DIV, outer title & body DIVs, inner title & body TDs, closebutton SPAN, shadow DIVs, and IFRAME to cover windowed elements in IE tt_aV = new Array(), // Caches and enumerates config data for currently active tooltip tt_sContent, // Inner tooltip text or HTML tt_t2t, tt_t2tDad, // Tag converted to tip, and its DOM parent element tt_musX, tt_musY, tt_over, tt_x, tt_y, tt_w, tt_h; // Position, width and height of currently displayed tooltip function tt_Extension() { tt_ExtCmdEnum(); tt_aExt[tt_aExt.length] = this; return this; } function tt_SetTipPos(x, y) { var css = tt_aElt[0].style; tt_x = x; tt_y = y; css.left = x + "px"; css.top = y + "px"; if(tt_ie56) { var ifrm = tt_aElt[tt_aElt.length - 1]; if(ifrm) { ifrm.style.left = css.left; ifrm.style.top = css.top; } } } function tt_HideInit() { if(tt_iState) { tt_ExtCallFncs(0, "HideInit"); tt_iState &= ~(0x4 | 0x8); if(tt_flagOpa && tt_aV[FADEOUT]) { tt_tFade.EndTimer(); if(tt_opa) { var n = Math.round(tt_aV[FADEOUT] / (tt_aV[FADEINTERVAL] * (tt_aV[OPACITY] / tt_opa))); tt_Fade(tt_opa, tt_opa, 0, n); return; } } tt_tHide.Timer("tt_Hide();", 1, false); } } function tt_Hide() { if(tt_db && tt_iState) { tt_OpReHref(); if(tt_iState & 0x2) { tt_aElt[0].style.visibility = "hidden"; tt_ExtCallFncs(0, "Hide"); } tt_tShow.EndTimer(); tt_tHide.EndTimer(); tt_tDurt.EndTimer(); tt_tFade.EndTimer(); if(!tt_op && !tt_ie) { tt_tWaitMov.EndTimer(); tt_bWait = false; } if(tt_aV[CLICKCLOSE] || tt_aV[CLICKSTICKY]) tt_RemEvtFnc(document, "mouseup", tt_OnLClick); tt_ExtCallFncs(0, "Kill"); // In case of a TagToTip tip, hide converted DOM node and // re-insert it into DOM if(tt_t2t && !tt_aV[COPYCONTENT]) tt_UnEl2Tip(); tt_iState = 0; tt_over = null; tt_ResetMainDiv(); if(tt_aElt[tt_aElt.length - 1]) tt_aElt[tt_aElt.length - 1].style.display = "none"; } } function tt_GetElt(id) { return(document.getElementById ? document.getElementById(id) : document.all ? document.all[id] : null); } function tt_GetDivW(el) { return(el ? (el.offsetWidth || el.style.pixelWidth || 0) : 0); } function tt_GetDivH(el) { return(el ? (el.offsetHeight || el.style.pixelHeight || 0) : 0); } function tt_GetScrollX() { return(window.pageXOffset || (tt_db ? (tt_db.scrollLeft || 0) : 0)); } function tt_GetScrollY() { return(window.pageYOffset || (tt_db ? (tt_db.scrollTop || 0) : 0)); } function tt_GetClientW() { return tt_GetWndCliSiz("Width"); } function tt_GetClientH() { return tt_GetWndCliSiz("Height"); } function tt_GetEvtX(e) { return (e ? ((typeof(e.pageX) != tt_u) ? e.pageX : (e.clientX + tt_GetScrollX())) : 0); } function tt_GetEvtY(e) { return (e ? ((typeof(e.pageY) != tt_u) ? e.pageY : (e.clientY + tt_GetScrollY())) : 0); } function tt_AddEvtFnc(el, sEvt, PFnc) { if(el) { if(el.addEventListener) el.addEventListener(sEvt, PFnc, false); else el.attachEvent("on" + sEvt, PFnc); } } function tt_RemEvtFnc(el, sEvt, PFnc) { if(el) { if(el.removeEventListener) el.removeEventListener(sEvt, PFnc, false); else el.detachEvent("on" + sEvt, PFnc); } } function tt_GetDad(el) { return(el.parentNode || el.parentElement || el.offsetParent); } function tt_MovDomNode(el, dadFrom, dadTo) { if(dadFrom) dadFrom.removeChild(el); if(dadTo) dadTo.appendChild(el); } //====================== PRIVATE ===========================================// var tt_aExt = new Array(), // Array of extension objects tt_db, tt_op, tt_ie, tt_ie56, tt_bBoxOld, // Browser flags tt_body, tt_ovr_, // HTML element the mouse is currently over tt_flagOpa, // Opacity support: 1=IE, 2=Khtml, 3=KHTML, 4=Moz, 5=W3C tt_maxPosX, tt_maxPosY, tt_iState = 0, // Tooltip active |= 1, shown |= 2, move with mouse |= 4, exclusive |= 8 tt_opa, // Currently applied opacity tt_bJmpVert, tt_bJmpHorz,// Tip temporarily on other side of mouse tt_elDeHref, // The tag from which we've removed the href attribute // Timer tt_tShow = new Number(0), tt_tHide = new Number(0), tt_tDurt = new Number(0), tt_tFade = new Number(0), tt_tWaitMov = new Number(0), tt_bWait = false, tt_u = "undefined"; function tt_Init() { tt_MkCmdEnum(); // Send old browsers instantly to hell if(!tt_Browser() || !tt_MkMainDiv()) return; tt_IsW3cBox(); tt_OpaSupport(); tt_AddEvtFnc(document, "mousemove", tt_Move); // In Debug mode we search for TagToTip() calls in order to notify // the user if they've forgotten to set the TagsToTip config flag if(TagsToTip || tt_Debug) tt_SetOnloadFnc(); // Ensure the tip be hidden when the page unloads tt_AddEvtFnc(window, "unload", tt_Hide); } // Creates command names by translating config variable names to upper case function tt_MkCmdEnum() { var n = 0; for(var i in config) eval("window." + i.toString().toUpperCase() + " = " + n++); tt_aV.length = n; } function tt_Browser() { var n, nv, n6, w3c; n = navigator.userAgent.toLowerCase(), nv = navigator.appVersion; tt_op = (document.defaultView && typeof(eval("w" + "indow" + "." + "o" + "p" + "er" + "a")) != tt_u); tt_ie = n.indexOf("msie") != -1 && document.all && !tt_op; if(tt_ie) { var ieOld = (!document.compatMode || document.compatMode == "BackCompat"); tt_db = !ieOld ? document.documentElement : (document.body || null); if(tt_db) tt_ie56 = parseFloat(nv.substring(nv.indexOf("MSIE") + 5)) >= 5.5 && typeof document.body.style.maxHeight == tt_u; } else { tt_db = document.documentElement || document.body || (document.getElementsByTagName ? document.getElementsByTagName("body")[0] : null); if(!tt_op) { n6 = document.defaultView && typeof document.defaultView.getComputedStyle != tt_u; w3c = !n6 && document.getElementById; } } tt_body = (document.getElementsByTagName ? document.getElementsByTagName("body")[0] : (document.body || null)); if(tt_ie || n6 || tt_op || w3c) { if(tt_body && tt_db) { if(document.attachEvent || document.addEventListener) return true; } else tt_Err("wz_tooltip.js must be included INSIDE the body section," + " immediately after the opening tag.", false); } tt_db = null; return false; } function tt_MkMainDiv() { // Create the tooltip DIV if(tt_body.insertAdjacentHTML) tt_body.insertAdjacentHTML("afterBegin", tt_MkMainDivHtm()); else if(typeof tt_body.innerHTML != tt_u && document.createElement && tt_body.appendChild) tt_body.appendChild(tt_MkMainDivDom()); if(window.tt_GetMainDivRefs /* FireFox Alzheimer */ && tt_GetMainDivRefs()) return true; tt_db = null; return false; } function tt_MkMainDivHtm() { return( '
' + (tt_ie56 ? ('') : '') ); } function tt_MkMainDivDom() { var el = document.createElement("div"); if(el) el.id = "WzTtDiV"; return el; } function tt_GetMainDivRefs() { tt_aElt[0] = tt_GetElt("WzTtDiV"); if(tt_ie56 && tt_aElt[0]) { tt_aElt[tt_aElt.length - 1] = tt_GetElt("WzTtIfRm"); if(!tt_aElt[tt_aElt.length - 1]) tt_aElt[0] = null; } if(tt_aElt[0]) { var css = tt_aElt[0].style; css.visibility = "hidden"; css.position = "absolute"; css.overflow = "hidden"; return true; } return false; } function tt_ResetMainDiv() { tt_SetTipPos(0, 0); tt_aElt[0].innerHTML = ""; tt_aElt[0].style.width = "0px"; tt_h = 0; } function tt_IsW3cBox() { var css = tt_aElt[0].style; css.padding = "10px"; css.width = "40px"; tt_bBoxOld = (tt_GetDivW(tt_aElt[0]) == 40); css.padding = "0px"; tt_ResetMainDiv(); } function tt_OpaSupport() { var css = tt_body.style; tt_flagOpa = (typeof(css.KhtmlOpacity) != tt_u) ? 2 : (typeof(css.KHTMLOpacity) != tt_u) ? 3 : (typeof(css.MozOpacity) != tt_u) ? 4 : (typeof(css.opacity) != tt_u) ? 5 : (typeof(css.filter) != tt_u) ? 1 : 0; } // Ported from http://dean.edwards.name/weblog/2006/06/again/ // (Dean Edwards et al.) function tt_SetOnloadFnc() { tt_AddEvtFnc(document, "DOMContentLoaded", tt_HideSrcTags); tt_AddEvtFnc(window, "load", tt_HideSrcTags); if(tt_body.attachEvent) tt_body.attachEvent("onreadystatechange", function() { if(tt_body.readyState == "complete") tt_HideSrcTags(); } ); if(/WebKit|KHTML/i.test(navigator.userAgent)) { var t = setInterval(function() { if(/loaded|complete/.test(document.readyState)) { clearInterval(t); tt_HideSrcTags(); } }, 10); } } function tt_HideSrcTags() { if(!window.tt_HideSrcTags || window.tt_HideSrcTags.done) return; window.tt_HideSrcTags.done = true; if(!tt_HideSrcTagsRecurs(tt_body)) tt_Err("There are HTML elements to be converted to tooltips.\nIf you" + " want these HTML elements to be automatically hidden, you" + " must edit wz_tooltip.js, and set TagsToTip in the global" + " tooltip configuration to true.", true); } function tt_HideSrcTagsRecurs(dad) { var ovr, asT2t; // Walk the DOM tree for tags that have an onmouseover or onclick attribute // containing a TagToTip('...') call. // (.childNodes first since .children is bugous in Safari) var a = dad.childNodes || dad.children || null; for(var i = a ? a.length : 0; i;) {--i; if(!tt_HideSrcTagsRecurs(a[i])) return false; ovr = a[i].getAttribute ? (a[i].getAttribute("onmouseover") || a[i].getAttribute("onclick")) : (typeof a[i].onmouseover == "function") ? (a[i].onmouseover || a[i].onclick) : null; if(ovr) { asT2t = ovr.toString().match(/TagToTip\s*\(\s*'[^'.]+'\s*[\),]/); if(asT2t && asT2t.length) { if(!tt_HideSrcTag(asT2t[0])) return false; } } } return true; } function tt_HideSrcTag(sT2t) { var id, el; // The ID passed to the found TagToTip() call identifies an HTML element // to be converted to a tooltip, so hide that element id = sT2t.replace(/.+'([^'.]+)'.+/, "$1"); el = tt_GetElt(id); if(el) { if(tt_Debug && !TagsToTip) return false; else el.style.display = "none"; } else tt_Err("Invalid ID\n'" + id + "'\npassed to TagToTip()." + " There exists no HTML element with that ID.", true); return true; } function tt_Tip(arg, t2t) { if(!tt_db || (tt_iState & 0x8)) return; if(tt_iState) tt_Hide(); if(!tt_Enabled) return; tt_t2t = t2t; if(!tt_ReadCmds(arg)) return; tt_iState = 0x1 | 0x4; tt_AdaptConfig1(); tt_MkTipContent(arg); tt_MkTipSubDivs(); tt_FormatTip(); tt_bJmpVert = false; tt_bJmpHorz = false; tt_maxPosX = tt_GetClientW() + tt_GetScrollX() - tt_w - 1; tt_maxPosY = tt_GetClientH() + tt_GetScrollY() - tt_h - 1; tt_AdaptConfig2(); // Ensure the tip be shown and positioned before the first onmousemove tt_OverInit(); tt_ShowInit(); tt_Move(); } function tt_ReadCmds(a) { var i; // First load the global config values, to initialize also values // for which no command is passed i = 0; for(var j in config) tt_aV[i++] = config[j]; // Then replace each cached config value for which a command is // passed (ensure the # of command args plus value args be even) if(a.length & 1) { for(i = a.length - 1; i > 0; i -= 2) tt_aV[a[i - 1]] = a[i]; return true; } tt_Err("Incorrect call of Tip() or TagToTip().\n" + "Each command must be followed by a value.", true); return false; } function tt_AdaptConfig1() { tt_ExtCallFncs(0, "LoadConfig"); // Inherit unspecified title formattings from body if(!tt_aV[TITLEBGCOLOR].length) tt_aV[TITLEBGCOLOR] = tt_aV[BORDERCOLOR]; if(!tt_aV[TITLEFONTCOLOR].length) tt_aV[TITLEFONTCOLOR] = tt_aV[BGCOLOR]; if(!tt_aV[TITLEFONTFACE].length) tt_aV[TITLEFONTFACE] = tt_aV[FONTFACE]; if(!tt_aV[TITLEFONTSIZE].length) tt_aV[TITLEFONTSIZE] = tt_aV[FONTSIZE]; if(tt_aV[CLOSEBTN]) { // Use title colours for non-specified closebutton colours if(!tt_aV[CLOSEBTNCOLORS]) tt_aV[CLOSEBTNCOLORS] = new Array("", "", "", ""); for(var i = 4; i;) {--i; if(!tt_aV[CLOSEBTNCOLORS][i].length) tt_aV[CLOSEBTNCOLORS][i] = (i & 1) ? tt_aV[TITLEFONTCOLOR] : tt_aV[TITLEBGCOLOR]; } // Enforce titlebar be shown if(!tt_aV[TITLE].length) tt_aV[TITLE] = " "; } // Circumvents broken display of images and fade-in flicker in Geckos < 1.8 if(tt_aV[OPACITY] == 100 && typeof tt_aElt[0].style.MozOpacity != tt_u && !Array.every) tt_aV[OPACITY] = 99; // Smartly shorten the delay for fade-in tooltips if(tt_aV[FADEIN] && tt_flagOpa && tt_aV[DELAY] > 100) tt_aV[DELAY] = Math.max(tt_aV[DELAY] - tt_aV[FADEIN], 100); } function tt_AdaptConfig2() { if(tt_aV[CENTERMOUSE]) { tt_aV[OFFSETX] -= ((tt_w - (tt_aV[SHADOW] ? tt_aV[SHADOWWIDTH] : 0)) >> 1); tt_aV[JUMPHORZ] = false; } } // Expose content globally so extensions can modify it function tt_MkTipContent(a) { if(tt_t2t) { if(tt_aV[COPYCONTENT]) tt_sContent = tt_t2t.innerHTML; else tt_sContent = ""; } else tt_sContent = a[0]; tt_ExtCallFncs(0, "CreateContentString"); } function tt_MkTipSubDivs() { var sCss = 'position:relative;margin:0px;padding:0px;border-width:0px;left:0px;top:0px;line-height:normal;width:auto;', sTbTrTd = ' cellspacing="0" cellpadding="0" border="0" style="' + sCss + '">' + '' + tt_aV[TITLE] + '' + (tt_aV[CLOSEBTN] ? ('') : '') + '
' + '' + tt_aV[CLOSEBTNTEXT] + '
') : '') + '
' + '' + tt_sContent + '
' + (tt_aV[SHADOW] ? ('
' + '
') : '') ); tt_GetSubDivRefs(); // Convert DOM node to tip if(tt_t2t && !tt_aV[COPYCONTENT]) tt_El2Tip(); tt_ExtCallFncs(0, "SubDivsCreated"); } function tt_GetSubDivRefs() { var aId = new Array("WzTiTl", "WzTiTlTb", "WzTiTlI", "WzClOsE", "WzBoDy", "WzBoDyI", "WzTtShDwB", "WzTtShDwR"); for(var i = aId.length; i; --i) tt_aElt[i] = tt_GetElt(aId[i - 1]); } function tt_FormatTip() { var css, w, h, pad = tt_aV[PADDING], padT, wBrd = tt_aV[BORDERWIDTH], iOffY, iOffSh, iAdd = (pad + wBrd) << 1; //--------- Title DIV ---------- if(tt_aV[TITLE].length) { padT = tt_aV[TITLEPADDING]; css = tt_aElt[1].style; css.background = tt_aV[TITLEBGCOLOR]; css.paddingTop = css.paddingBottom = padT + "px"; css.paddingLeft = css.paddingRight = (padT + 2) + "px"; css = tt_aElt[3].style; css.color = tt_aV[TITLEFONTCOLOR]; if(tt_aV[WIDTH] == -1) css.whiteSpace = "nowrap"; css.fontFamily = tt_aV[TITLEFONTFACE]; css.fontSize = tt_aV[TITLEFONTSIZE]; css.fontWeight = "bold"; css.textAlign = tt_aV[TITLEALIGN]; // Close button DIV if(tt_aElt[4]) { css = tt_aElt[4].style; css.background = tt_aV[CLOSEBTNCOLORS][0]; css.color = tt_aV[CLOSEBTNCOLORS][1]; css.fontFamily = tt_aV[TITLEFONTFACE]; css.fontSize = tt_aV[TITLEFONTSIZE]; css.fontWeight = "bold"; } if(tt_aV[WIDTH] > 0) tt_w = tt_aV[WIDTH]; else { tt_w = tt_GetDivW(tt_aElt[3]) + tt_GetDivW(tt_aElt[4]); // Some spacing between title DIV and closebutton if(tt_aElt[4]) tt_w += pad; // Restrict auto width to max width if(tt_aV[WIDTH] < -1 && tt_w > -tt_aV[WIDTH]) tt_w = -tt_aV[WIDTH]; } // Ensure the top border of the body DIV be covered by the title DIV iOffY = -wBrd; } else { tt_w = 0; iOffY = 0; } //-------- Body DIV ------------ css = tt_aElt[5].style; css.top = iOffY + "px"; if(wBrd) { css.borderColor = tt_aV[BORDERCOLOR]; css.borderStyle = tt_aV[BORDERSTYLE]; css.borderWidth = wBrd + "px"; } if(tt_aV[BGCOLOR].length) css.background = tt_aV[BGCOLOR]; if(tt_aV[BGIMG].length) css.backgroundImage = "url(" + tt_aV[BGIMG] + ")"; css.padding = pad + "px"; css.textAlign = tt_aV[TEXTALIGN]; if(tt_aV[HEIGHT]) { css.overflow = "auto"; if(tt_aV[HEIGHT] > 0) css.height = (tt_aV[HEIGHT] + iAdd) + "px"; else tt_h = iAdd - tt_aV[HEIGHT]; } // TD inside body DIV css = tt_aElt[6].style; css.color = tt_aV[FONTCOLOR]; css.fontFamily = tt_aV[FONTFACE]; css.fontSize = tt_aV[FONTSIZE]; css.fontWeight = tt_aV[FONTWEIGHT]; css.textAlign = tt_aV[TEXTALIGN]; if(tt_aV[WIDTH] > 0) w = tt_aV[WIDTH]; // Width like title (if existent) else if(tt_aV[WIDTH] == -1 && tt_w) w = tt_w; else { // Measure width of the body's inner TD, as some browsers would expand // the container and outer body DIV to 100% w = tt_GetDivW(tt_aElt[6]); // Restrict auto width to max width if(tt_aV[WIDTH] < -1 && w > -tt_aV[WIDTH]) w = -tt_aV[WIDTH]; } if(w > tt_w) tt_w = w; tt_w += iAdd; //--------- Shadow DIVs ------------ if(tt_aV[SHADOW]) { tt_w += tt_aV[SHADOWWIDTH]; iOffSh = Math.floor((tt_aV[SHADOWWIDTH] * 4) / 3); // Bottom shadow css = tt_aElt[7].style; css.top = iOffY + "px"; css.left = iOffSh + "px"; css.width = (tt_w - iOffSh - tt_aV[SHADOWWIDTH]) + "px"; css.height = tt_aV[SHADOWWIDTH] + "px"; css.background = tt_aV[SHADOWCOLOR]; // Right shadow css = tt_aElt[8].style; css.top = iOffSh + "px"; css.left = (tt_w - tt_aV[SHADOWWIDTH]) + "px"; css.width = tt_aV[SHADOWWIDTH] + "px"; css.background = tt_aV[SHADOWCOLOR]; } else iOffSh = 0; //-------- Container DIV ------- tt_SetTipOpa(tt_aV[FADEIN] ? 0 : tt_aV[OPACITY]); tt_FixSize(iOffY, iOffSh); } // Fixate the size so it can't dynamically change while the tooltip is moving. function tt_FixSize(iOffY, iOffSh) { var wIn, wOut, h, add, pad = tt_aV[PADDING], wBrd = tt_aV[BORDERWIDTH], i; tt_aElt[0].style.width = tt_w + "px"; tt_aElt[0].style.pixelWidth = tt_w; wOut = tt_w - ((tt_aV[SHADOW]) ? tt_aV[SHADOWWIDTH] : 0); // Body wIn = wOut; if(!tt_bBoxOld) wIn -= (pad + wBrd) << 1; tt_aElt[5].style.width = wIn + "px"; // Title if(tt_aElt[1]) { wIn = wOut - ((tt_aV[TITLEPADDING] + 2) << 1); if(!tt_bBoxOld) wOut = wIn; tt_aElt[1].style.width = wOut + "px"; tt_aElt[2].style.width = wIn + "px"; } // Max height specified if(tt_h) { h = tt_GetDivH(tt_aElt[5]); if(h > tt_h) { if(!tt_bBoxOld) tt_h -= (pad + wBrd) << 1; tt_aElt[5].style.height = tt_h + "px"; } } tt_h = tt_GetDivH(tt_aElt[0]) + iOffY; // Right shadow if(tt_aElt[8]) tt_aElt[8].style.height = (tt_h - iOffSh) + "px"; i = tt_aElt.length - 1; if(tt_aElt[i]) { tt_aElt[i].style.width = tt_w + "px"; tt_aElt[i].style.height = tt_h + "px"; } } function tt_DeAlt(el) { var aKid; if(el) { if(el.alt) el.alt = ""; if(el.title) el.title = ""; aKid = el.childNodes || el.children || null; if(aKid) { for(var i = aKid.length; i;) tt_DeAlt(aKid[--i]); } } } // This hack removes the native tooltips over links in Opera function tt_OpDeHref(el) { if(!tt_op) return; if(tt_elDeHref) tt_OpReHref(); while(el) { if(el.hasAttribute && el.hasAttribute("href")) { el.t_href = el.getAttribute("href"); el.t_stats = window.status; el.removeAttribute("href"); el.style.cursor = "hand"; tt_AddEvtFnc(el, "mousedown", tt_OpReHref); window.status = el.t_href; tt_elDeHref = el; break; } el = tt_GetDad(el); } } function tt_OpReHref() { if(tt_elDeHref) { tt_elDeHref.setAttribute("href", tt_elDeHref.t_href); tt_RemEvtFnc(tt_elDeHref, "mousedown", tt_OpReHref); window.status = tt_elDeHref.t_stats; tt_elDeHref = null; } } function tt_El2Tip() { var css = tt_t2t.style; // Store previous positioning tt_t2t.t_cp = css.position; tt_t2t.t_cl = css.left; tt_t2t.t_ct = css.top; tt_t2t.t_cd = css.display; // Store the tag's parent element so we can restore that DOM branch // when the tooltip is being hidden tt_t2tDad = tt_GetDad(tt_t2t); tt_MovDomNode(tt_t2t, tt_t2tDad, tt_aElt[6]); css.display = "block"; css.position = "static"; css.left = css.top = css.marginLeft = css.marginTop = "0px"; } function tt_UnEl2Tip() { // Restore positioning and display var css = tt_t2t.style; css.display = tt_t2t.t_cd; tt_MovDomNode(tt_t2t, tt_GetDad(tt_t2t), tt_t2tDad); css.position = tt_t2t.t_cp; css.left = tt_t2t.t_cl; css.top = tt_t2t.t_ct; tt_t2tDad = null; } function tt_OverInit() { if(window.event) tt_over = window.event.target || window.event.srcElement; else tt_over = tt_ovr_; tt_DeAlt(tt_over); tt_OpDeHref(tt_over); } function tt_ShowInit() { tt_tShow.Timer("tt_Show()", tt_aV[DELAY], true); if(tt_aV[CLICKCLOSE] || tt_aV[CLICKSTICKY]) tt_AddEvtFnc(document, "mouseup", tt_OnLClick); } function tt_Show() { var css = tt_aElt[0].style; // Override the z-index of the topmost wz_dragdrop.js D&D item css.zIndex = Math.max((window.dd && dd.z) ? (dd.z + 2) : 0, 1010); if(tt_aV[STICKY] || !tt_aV[FOLLOWMOUSE]) tt_iState &= ~0x4; if(tt_aV[EXCLUSIVE]) tt_iState |= 0x8; if(tt_aV[DURATION] > 0) tt_tDurt.Timer("tt_HideInit()", tt_aV[DURATION], true); tt_ExtCallFncs(0, "Show") css.visibility = "visible"; tt_iState |= 0x2; if(tt_aV[FADEIN]) tt_Fade(0, 0, tt_aV[OPACITY], Math.round(tt_aV[FADEIN] / tt_aV[FADEINTERVAL])); tt_ShowIfrm(); } function tt_ShowIfrm() { if(tt_ie56) { var ifrm = tt_aElt[tt_aElt.length - 1]; if(ifrm) { var css = ifrm.style; css.zIndex = tt_aElt[0].style.zIndex - 1; css.display = "block"; } } } function tt_Move(e) { if(e) tt_ovr_ = e.target || e.srcElement; e = e || window.event; if(e) { tt_musX = tt_GetEvtX(e); tt_musY = tt_GetEvtY(e); } if(tt_iState & 0x4) { // Prevent jam of mousemove events if(!tt_op && !tt_ie) { if(tt_bWait) return; tt_bWait = true; tt_tWaitMov.Timer("tt_bWait = false;", 1, true); } if(tt_aV[FIX]) { tt_iState &= ~0x4; tt_PosFix(); } else if(!tt_ExtCallFncs(e, "MoveBefore")) tt_SetTipPos(tt_Pos(0), tt_Pos(1)); tt_ExtCallFncs([tt_musX, tt_musY], "MoveAfter") } } function tt_Pos(iDim) { var iX, bJmpMod, cmdAlt, cmdOff, cx, iMax, iScrl, iMus, bJmp; // Map values according to dimension to calculate if(iDim) { bJmpMod = tt_aV[JUMPVERT]; cmdAlt = ABOVE; cmdOff = OFFSETY; cx = tt_h; iMax = tt_maxPosY; iScrl = tt_GetScrollY(); iMus = tt_musY; bJmp = tt_bJmpVert; } else { bJmpMod = tt_aV[JUMPHORZ]; cmdAlt = LEFT; cmdOff = OFFSETX; cx = tt_w; iMax = tt_maxPosX; iScrl = tt_GetScrollX(); iMus = tt_musX; bJmp = tt_bJmpHorz; } if(bJmpMod) { if(tt_aV[cmdAlt] && (!bJmp || tt_CalcPosAlt(iDim) >= iScrl + 16)) iX = tt_PosAlt(iDim); else if(!tt_aV[cmdAlt] && bJmp && tt_CalcPosDef(iDim) > iMax - 16) iX = tt_PosAlt(iDim); else iX = tt_PosDef(iDim); } else { iX = iMus; if(tt_aV[cmdAlt]) iX -= cx + tt_aV[cmdOff] - (tt_aV[SHADOW] ? tt_aV[SHADOWWIDTH] : 0); else iX += tt_aV[cmdOff]; } // Prevent tip from extending past clientarea boundary if(iX > iMax) iX = bJmpMod ? tt_PosAlt(iDim) : iMax; // In case of insufficient space on both sides, ensure the left/upper part // of the tip be visible if(iX < iScrl) iX = bJmpMod ? tt_PosDef(iDim) : iScrl; return iX; } function tt_PosDef(iDim) { if(iDim) tt_bJmpVert = tt_aV[ABOVE]; else tt_bJmpHorz = tt_aV[LEFT]; return tt_CalcPosDef(iDim); } function tt_PosAlt(iDim) { if(iDim) tt_bJmpVert = !tt_aV[ABOVE]; else tt_bJmpHorz = !tt_aV[LEFT]; return tt_CalcPosAlt(iDim); } function tt_CalcPosDef(iDim) { return iDim ? (tt_musY + tt_aV[OFFSETY]) : (tt_musX + tt_aV[OFFSETX]); } function tt_CalcPosAlt(iDim) { var cmdOff = iDim ? OFFSETY : OFFSETX; var dx = tt_aV[cmdOff] - (tt_aV[SHADOW] ? tt_aV[SHADOWWIDTH] : 0); if(tt_aV[cmdOff] > 0 && dx <= 0) dx = 1; return((iDim ? (tt_musY - tt_h) : (tt_musX - tt_w)) - dx); } function tt_PosFix() { var iX, iY; if(typeof(tt_aV[FIX][0]) == "number") { iX = tt_aV[FIX][0]; iY = tt_aV[FIX][1]; } else { if(typeof(tt_aV[FIX][0]) == "string") el = tt_GetElt(tt_aV[FIX][0]); // First slot in array is direct reference to HTML element else el = tt_aV[FIX][0]; iX = tt_aV[FIX][1]; iY = tt_aV[FIX][2]; // By default, vert pos is related to bottom edge of HTML element if(!tt_aV[ABOVE] && el) iY += tt_GetDivH(el); for(; el; el = el.offsetParent) { iX += el.offsetLeft || 0; iY += el.offsetTop || 0; } } // For a fixed tip positioned above the mouse, use the bottom edge as anchor // (recommended by Christophe Rebeschini, 31.1.2008) if(tt_aV[ABOVE]) iY -= tt_h; tt_SetTipPos(iX, iY); } function tt_Fade(a, now, z, n) { if(n) { now += Math.round((z - now) / n); if((z > a) ? (now >= z) : (now <= z)) now = z; else tt_tFade.Timer( "tt_Fade(" + a + "," + now + "," + z + "," + (n - 1) + ")", tt_aV[FADEINTERVAL], true ); } now ? tt_SetTipOpa(now) : tt_Hide(); } function tt_SetTipOpa(opa) { // To circumvent the opacity nesting flaws of IE, we set the opacity // for each sub-DIV separately, rather than for the container DIV. tt_SetOpa(tt_aElt[5], opa); if(tt_aElt[1]) tt_SetOpa(tt_aElt[1], opa); if(tt_aV[SHADOW]) { opa = Math.round(opa * 0.8); tt_SetOpa(tt_aElt[7], opa); tt_SetOpa(tt_aElt[8], opa); } } function tt_OnCloseBtnOver(iOver) { var css = tt_aElt[4].style; iOver <<= 1; css.background = tt_aV[CLOSEBTNCOLORS][iOver]; css.color = tt_aV[CLOSEBTNCOLORS][iOver + 1]; } function tt_OnLClick(e) { // Ignore right-clicks e = e || window.event; if(!((e.button && e.button & 2) || (e.which && e.which == 3))) { if(tt_aV[CLICKSTICKY] && (tt_iState & 0x4)) { tt_aV[STICKY] = true; tt_iState &= ~0x4; } else if(tt_aV[CLICKCLOSE]) tt_HideInit(); } } function tt_Int(x) { var y; return(isNaN(y = parseInt(x)) ? 0 : y); } Number.prototype.Timer = function(s, iT, bUrge) { if(!this.value || bUrge) this.value = window.setTimeout(s, iT); } Number.prototype.EndTimer = function() { if(this.value) { window.clearTimeout(this.value); this.value = 0; } } function tt_GetWndCliSiz(s) { var db, y = window["inner" + s], sC = "client" + s, sN = "number"; if(typeof y == sN) { var y2; return( // Gecko or Opera with scrollbar // ... quirks mode ((db = document.body) && typeof(y2 = db[sC]) == sN && y2 && y2 <= y) ? y2 // ... strict mode : ((db = document.documentElement) && typeof(y2 = db[sC]) == sN && y2 && y2 <= y) ? y2 // No scrollbar, or clientarea size == 0, or other browser (KHTML etc.) : y ); } // IE return( // document.documentElement.client+s functional, returns > 0 ((db = document.documentElement) && (y = db[sC])) ? y // ... not functional, in which case document.body.client+s // is the clientarea size, fortunately : document.body[sC] ); } function tt_SetOpa(el, opa) { var css = el.style; tt_opa = opa; if(tt_flagOpa == 1) { if(opa < 100) { // Hacks for bugs of IE: // 1.) Once a CSS filter has been applied, fonts are no longer // anti-aliased, so we store the previous 'non-filter' to be // able to restore it if(typeof(el.filtNo) == tt_u) el.filtNo = css.filter; // 2.) A DIV cannot be made visible in a single step if an // opacity < 100 has been applied while the DIV was hidden var bVis = css.visibility != "hidden"; // 3.) In IE6, applying an opacity < 100 has no effect if the // element has no layout (position, size, zoom, ...) css.zoom = "100%"; if(!bVis) css.visibility = "visible"; css.filter = "alpha(opacity=" + opa + ")"; if(!bVis) css.visibility = "hidden"; } else if(typeof(el.filtNo) != tt_u) // Restore 'non-filter' css.filter = el.filtNo; } else { opa /= 100.0; switch(tt_flagOpa) { case 2: css.KhtmlOpacity = opa; break; case 3: css.KHTMLOpacity = opa; break; case 4: css.MozOpacity = opa; break; case 5: css.opacity = opa; break; } } } function tt_Err(sErr, bIfDebug) { if(tt_Debug || !bIfDebug) alert("Tooltip Script Error Message:\n\n" + sErr); } //============ EXTENSION (PLUGIN) MANAGER ===============// function tt_ExtCmdEnum() { var s; // Add new command(s) to the commands enum for(var i in config) { s = "window." + i.toString().toUpperCase(); if(eval("typeof(" + s + ") == tt_u")) { eval(s + " = " + tt_aV.length); tt_aV[tt_aV.length] = null; } } } function tt_ExtCallFncs(arg, sFnc) { var b = false; for(var i = tt_aExt.length; i;) {--i; var fnc = tt_aExt[i]["On" + sFnc]; // Call the method the extension has defined for this event if(fnc && fnc(arg)) b = true; } return b; } tt_Init(); TaskJuggler-3.8.1/examples/000077500000000000000000000000001473026623400155725ustar00rootroot00000000000000TaskJuggler-3.8.1/examples/Fedora-20/000077500000000000000000000000001473026623400172115ustar00rootroot00000000000000TaskJuggler-3.8.1/examples/Fedora-20/f-20.tjp000066400000000000000000002265511473026623400204070ustar00rootroot00000000000000# Fedora 20 # # Copyright 2011 John Poelstra # Copyright 2011 Robyn Bergeron # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation files # (the "Software"), to deal in the Software without restriction, # including without limitation the rights to use, copy, modify, merge, # publish, distribute, sublicense, and/or sell copies of the Software, # and to permit persons to whom the Software is furnished to do so, # subject to the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # # Current release version macro major [20] # Last release version macro previous_major[19] # Next release version macro next_major [21] # do not change this macro content [f] # do not change this macro content_title [Fedora] # GA date of previous release macro start_date [2013-04-30] # set to one day after "start_date" macro start_work [2013-05-01] macro end_date [2014-12-31] macro prior_project [f19] project ${content}${major} "${content_title}" "${major}" ${start_date} - ${end_date} { timeformat "%Y-%b-%d" # Based on Eastern time zone in USA timezone "America/New_York" # Setup scenarios scenario plan "Original Plan" { scenario actual "Actual" } # Limit working days workinghours sat,sun off } # ========= Cornerstones of Fedora Schedules ============= # This section also serves as a "style guide" for the source file too /* 1) EVERY entry should be in the following line order and indented consistently with the rest of the file. This makes the source file consistent and easier to read. a) task name and description b) dependency (if applicable) c) length (not "duration") (if applicable) d) flags (if applicable) 2) Official Release Engineering Composes happen on Thursdays, except for the final release 3) Syncing releases to mirrors ideally happens on Thursdays... end of day on Friday is worst case 4) Most tasks are scheduled with 'length' instead of 'duration' to get a sense of "work days" required--using 'length' one week = 5 days --an eternal debate could be held to discuss whether or not schedule calculations should include weekends because community members work at all times and not within strict work days. 5) Use "length" everywhere and "duration" only when something must take into account a weekend day 6) All "Blocker Bug Days" should be on Fridays 7) If a task takes one day or less--schedule with no length--this way it shows up as a milestone and gets included in iCalendar (ics) file 8) Because of bugs in the way TaskJuggler ics files get rendered in some calendars (e.g. Zimbra) we only include zero length (milestone) tasks in the ics file. As a result there are several duplicate tasks with no length that have been added so they appear in the ics file. 9) Use core schedule milestones (flags = key) as anchor points (depends/precedes) for tasks in this schedule file instead of more transient tasks like meetings, compose dates, etc. that may change (or slip) from release to release. This makes building new release schedules easier and require less maintenance and updating. 10) In ALL but limited cases task beginning and ending should be automatically calculated based on logic in this file. When using hard coded dates, explicitly call them out with a comment to highlight their existence. Hard coded dates are particularly troublesome when slipping a schedule or branching the file to create a new release schedule because they must be adjusted and recalculated manually 11) TaskJuggler does not provide an easy way (that I am aware of) to schedule tasks to happen *before* other tasks. I've created a hack/methodology I call "shadow" tasks. These are unreported tasks that go backwards a certain period of time and serve as an anchor or starting point for the actual task to be reported. 12) The "milestone" declaration is NOT used in this source file. It is redundant and unnecessary. Do not include it. All tasks without 'length' or 'duration' are automatically considered "milestone" tasks. */ # Define flags for filtered reporting flags ambassadors flags bugtriage flags blocker # use to proof blocker meeting placement flags design flags devel flags docs flags elections flags fpl flags hidden # used to hide tasks that we do not displayed in Fedora reports flags infrastructure flags interface flags key /* use for report of key tasks/high level overview--reflected on Fedora wiki, e.g. https://fedoraproject.org/wiki/Releases/15/Schedule */ flags proto # used for drafting new schedules and shows tasks useful for doing this flags pm flags pr flags marketing flags roadmap # major milestones flags translation flags quality flags releng flags spins flags web task ${content}${major} "${content_title} ${major}" { start ${start_work} task first_day "First Day of Development" { flags hidden } task PlanningPhase "Planning Phase" { # add for ical task start_features_cal "Start Feature Submission" { flags key, pm, roadmap } task rawhide_spins "Start Nightly Spins Compose Based on Rawhide" { depends !!first_day {gaplength 5d} flags spins } # ADJUST FOR NEW RELEASE # HARDCODED date--approximately 2 weeks after GA of previous release task file_ticket "File ticket with RHT Eng-ops for Fedora 17 EOL bugzilla closure" { start 2013-05-15 flags pm } # ADJUST FOR NEW RELEASE # HARDCODED date--approximately 4 weeks after GA of previous release # this date is approved by FESCo in accordance with https://fedoraproject.org/wiki/LifeCycle # The process behind this task is at: https://fedoraproject.org/wiki/BugZappers/HouseKeeping task fedora17_eol "RHT Eng-Ops Fedora 17 EOL auto closure" { start 2013-05-29 flags pm, key } task clean_market_wiki "Cleanup Marketing wiki from previous releases" { depends !start_features_cal {gaplength 25d} length 5d flags marketing } task cycle_market_wiki "Cycle Marketing wiki pages for current release" { depends !start_features_cal {gaplength 25d} length 5d flags marketing } task bug_trackers "Create Tracker Bugs" { flags pm note "See details at https://fedoraproject.org/wiki/BugZappers/HouseKeeping/FirstDayDevel" } task design_concept "Conceptual Design Phase" { length 30d flags design } # ADJUST FOR NEW RELEASE # a solid start for the wallpaper is the goal for alpha # adjust length so packaging end date lands on Alpha Deadline task wallpaper_design "Wallpaper Design for Alpha" { depends !design_concept length 35d flags design } } # E N D of PlanningPhase task supplement_wallpaper "Supplemental Wallpaper Process" { flags design # ADJUST FOR NEW RELEASE # adjust length of this task so that 'package_supplemental_wallpaper' ends on Beta Deadline task supplement_wallpaper_submit "Supplemental Wallpaper Submission Period" { length 82d } task decide_supplement_wallpaper "Select Official Supplemental Wallpaper" { depends !supplement_wallpaper_submit } task supplement_license_review "Verify Supplemental Wallpaper Licenses" { depends !decide_supplement_wallpaper {gaplength 1d} length 10d } task package_supplemental_wallpaper "Package Supplemental Wallpaper" { depends !supplement_license_review length 2d } } # end supplemental wallpaper task DevelopmentPhase "Development Phase" { start ${start_work} task devel_start "Start Development" { flags devel } # ADJUST FOR NEW RELEASE--IMPORTANT # The length of this task DRIVES THE ENTIRE SCHEDULE # and determines when the testing phases starts. The testing # phase of the schedule is static (and completely automatically # generated by TaskJuggler) from release to release, but the # number of days of development varies depending when the previous # release ended (GA). As a result the length of this task also # influences where the GA date lands. # Each "5d" (5 days) is equivalent to one week. task develop "Packaging and Development (precedes Alpha)" { length 70d flags devel, proto } } # E N D of DevelopmentPhase task TestingPhase "Testing Phase" { task alpha "Alpha Release" { start ${start_work} task shadow_alpha_blocker "SHADOW: anchor for first blocker meeting" { precedes !feature_freeze {gaplength 11d} flags hidden } task remind_alpha_blocker1 "Reminder: Alpha Blocker Meeting (${content}${major}alpha) #1" { depends !shadow_alpha_blocker {gaplength 3d} flags pm } task alpha_blocker1 "Alpha Blocker Meeting (${content}${major}alpha) #1" { depends !shadow_alpha_blocker {gaplength 5d} flags releng, quality, devel, blocker, pm } task remind_alpha_blocker2 "Reminder: Alpha Blocker Meeting (${content}${major}alpha) #2" { depends !alpha_blocker1 {gaplength 3d} flags pm } task alpha_blocker2 "Alpha Blocker Meeting (${content}${major}alpha) #2" { depends !alpha_blocker1 {gaplength 5d} flags releng, quality, devel, blocker, pm } # raise awareness one week before Alpha compose task daily_alpha_blocker "Daily Review & Notification of Open Alpha Blocker Bugs" { depends !alpha_blocker2 {gaplength 1d} length 4d flags releng, quality, devel, pm, blocker } task remind_alpha_blocker3 "Reminder: Alpha Blocker Meeting (${content}${major}alpha) #3" { depends !alpha_blocker2 {gaplength 3d} flags pm } task alpha_blocker3 "Alpha Blocker Meeting (${content}${major}alpha) #3" { depends !alpha_blocker2 {gaplength 5d} flags releng, quality, devel, pm, blocker } task remind_alpha_blocker4 "Reminder: Alpha Blocker Meeting (${content}${major}alpha) #4" { depends !alpha_blocker3 {gaplength 3d} flags pm } task alpha_blocker4 "Alpha Blocker Meeting (${content}${major}alpha) #4" { depends !alpha_blocker3 {gaplength 5d} flags releng, quality, devel, pm, blocker } # placeholder if slip--otherwise comment out /* task alpha_blocker5 "Alpha Blocker Meeting (${content}${major}alpha) #5" { depends !alpha_blocker4 {gaplength 5d} flags releng, quality, devel, pm, blocker } */ # Feature Freeze is one week before Alpha Change Deadline # Automatically calculated based on Alpha Change Deadline task shadow_feature_freeze "SHADOW: Anchor Feature Freeze" { precedes !alpha_deadline {gaplength 6d} flags hidden } task feature_freeze "Feature Freeze (Testable|Complete)" { depends !shadow_feature_freeze flags releng, quality, pm, proto, devel, key, marketing, roadmap, fpl } task feature_freeze_deadline_announce "Announce Feature Freeze Reached" { depends !feature_freeze flags pm } task alpha_deadline_remind "Remind Alpha Deadline in 1 week" { depends !feature_freeze flags pm } task spins_freeze "Spins Freeze--All ${content_title} ${major} Spins Identified" { depends !shadow_feature_freeze flags releng, quality, pm, proto, devel, key, marketing, spins, fpl } task talking_points "Create Talking Points" { depends !feature_freeze {gaplength 6d} length 5d flags marketing } task feature_profiles "Feature Profiles" { depends !talking_points length 20d flags marketing } # Branch on the same Tuesday as Feature Freeze task branch_rawhide "Branch Fedora ${major} from Rawhide" { depends !shadow_feature_freeze flags releng, devel, pm, proto, key, roadmap, fpl } task bugzilla_description "Reflect supported versions in Bugzilla product description" { depends !shadow_feature_freeze flags pm } task rawhide_rebase "Rebase Rawhide bugs to Fedora ${major}" { depends !shadow_feature_freeze flags pm } # Create anchor for three weeks before for Feature Submission task shadow_feature_submit_remind_3_weeks "SHADOW: Three Weeks Before Feature Submission" { precedes !feature_submission_deadline {gaplength 15d} flags hidden } # Three weeks before Feature Submission Deadline task feature_check_remind "Request Feature Status Updates + Remind Submit Deadline" { depends !shadow_feature_submit_remind_3_weeks flags devel, pm } task alpha_releng_tickets "File All Release Engineering Tickets for ${content_title} ${major} Alpha" { depends !shadow_feature_submit_remind_3_weeks {gaplength 3d} flags releng } task feature_submit_remind_2_weeks "Feature Submission Deadline Two Weeks away" { depends !shadow_feature_submit_remind_3_weeks {gaplength 6d} flags devel, pm } #two weeks before spins submission deadline get wiki pages in order task spins_wiki_update "Update All Spins Wiki Pages From Previous Releases" { depends !shadow_feature_submit_remind_3_weeks flags spins } task feature_submit_remind_1_week "Feature Submission Deadline One Week away" { depends !shadow_feature_submit_remind_3_weeks {gaplength 11d} flags devel, pm } # One day before compose task alpha_installer_build1 "Submit Installer Build for QA Compose" { depends !feature_submit_remind_1_week {gaplength 1d} flags devel } # Thursday before Feature Submission deadline task qa_alpha_compose1 "Create Installable Images for QA testing #1" { depends !feature_submit_remind_1_week {gaplength 2d} flags releng } task alpha_rawhide_install1 "Pre-Alpha Rawhide Acceptance Test Plan #1" { depends !qa_alpha_compose1 length 6d flags quality } # Create anchor for two weeks before Feature Freeze task shadow_feature_submission_deadline "SHADOW: Two weeks before Feature Freeze" { precedes !feature_freeze {gaplength 10d} flags hidden } task feature_submission_deadline "Feature Submission Deadline" { depends !shadow_feature_submission_deadline flags releng, quality, pm, proto, devel, key, roadmap, fpl } task feature_submission_deadline_announce "Announce Feature Submission Closed" { depends !shadow_feature_submission_deadline flags pm } task spins_submission_deadline "Custom Spins Submission Deadline" { depends !shadow_feature_submission_deadline flags pm, proto, key, spins, fpl } task warn_rawhide_rebase "Rawhide Rebase Warning to Package Maintainers" { depends !shadow_feature_submission_deadline flags pm } task ticket_rawhide_rebase "File Rawhide Rebase ticket with RHT Eng-ops" { depends !shadow_feature_submission_deadline flags pm } # One day before compose task alpha_installer_build2 "Submit Installer Build for QA Compose" { depends !feature_submission_deadline {gaplength 2d} flags devel } task qa_alpha_compose2 "Create Installable Images for QA testing #2" { depends !feature_submission_deadline {gaplength 3d} flags releng } task alpha_rawhide_install2 "Pre-Alpha Rawhide Acceptance Test Plan #2" { depends !qa_alpha_compose2 length 5d flags quality } # One day before compose task alpha_installer_build3 "Submit Installer Build for QA Compose" { depends !qa_alpha_compose2 {gaplength 4d} flags devel } task qa_alpha_compose3 "Create Installable Images for QA testing #3" { depends !alpha_rawhide_install2 flags releng } task alpha_rawhide_install3 "Pre-Alpha Rawhide Acceptance Test Plan #3" { depends !qa_alpha_compose3 length 5d flags quality } task feature_incomplete_nag "Remind < 85% complete Feature Owners" { depends !feature_freeze {gaplength 1d} flags pm } task feature_incomplete_fesco "Deliver Incomplete Features to FESCo " { depends !feature_freeze {gaplength 6d} flags pm } # KEY ADJUSTMENT POINT # Date of Alpha Deadline depends on length of development task alpha_deadline "Alpha Change Deadline" { depends !!!DevelopmentPhase.develop flags releng, quality, pm, devel, key, roadmap, proto, blocker, spins } task alpha_deadline_announce "Announce Alpha Change Deadline Reached" { depends !alpha_deadline flags pm } # KEY ADJUSTMENT POINT--manually adjust if length of time before Alpha ships changes task alpha_infrastructure_freeze "Alpha Infrastructure Change Freeze" { depends !alpha_deadline length 10d flags infrastructure } task alpha_spins_ks "Build spin-kickstarts package from master" { depends !alpha_deadline flags spins } # Happens the same day as Feature Freeze task orphan "Orphan Rawhide Packages" { depends !feature_freeze flags releng, devel } task finalize_alpha_wallpaper "Finalize Alpha Wallpaper" { depends !!!PlanningPhase.wallpaper_design flags design, pm length 3d } task alpha_wallpaper_deadline "Alpha Wallpaper Deadline" { depends !finalize_alpha_wallpaper flags design } task blog_alpha_wallpaper "Blog About Alpha Wallpaper" { depends !finalize_alpha_wallpaper flags design } # must land a few days before Alpha compose by Releng task package_alpha_wallpaper "Package Alpha Wallpaper" { depends !finalize_alpha_wallpaper length 2d flags design } task alpha_wallpaper_feedback "Solicit Feedback on Alpha Wallpaper" { depends !package_alpha_wallpaper length 10d flags design } # depends on nearly complete version of the wallpaper, otherwise # you have to create the splashes twice # work on until Beta freeze task start_splash_screens "Create Splash Screens" { depends !alpha_drop length 9d flags design } # for ics file task start_splash_screens_cal "Start Splash Screens" { depends !alpha_drop flags design } # work on until Beta freeze task finalize_splash_screens "Finalize Splash Screens" { depends !start_splash_screens length 4d flags design } # work on until Beta freeze task beta_wallpaper "Prepare wallpaper for Beta" { depends !alpha_drop length 13d flags design } # send reminder on Monday before Wednesday meeting task remind_alpha_go_not "Reminder: ${content_title} ${major} Alpha Go/No-Go Meeting" { depends !create_alpha_compose {gaplength 2d} flags pm } task alpha_go_not "${content_title} ${major} Alpha Go/No-Go Meeting (17:00 US Eastern)" { depends !create_alpha_compose {gaplength 4d} flags releng, quality, devel, pm, proto, blocker } task trans_software_rebuild1 "Remind f-dev-announce to Rebuild All Translated Packages" { depends !feature_freeze {gaplength 5d } flags translation } task software_string_freeze "Software String Freeze" { depends !feature_freeze {gaplength 6d } flags devel, translation, pm, proto, releng, key, roadmap } task announce_software_string_freeze "Announce Software String Freeze Reached" { depends !feature_freeze {gaplength 6d } flags pm } task software_translation "Software Translation"{ # KEY ADJUSTMENT POINT # if the alpha slips, add additional time to this task (or maybe not - this may be attached to string freeze dates increasing - double check this! -robyn) task trans_software "Software Translation Period" { depends !!software_string_freeze length 25d flags translation } # KEY ADJUSTMENT POINT # If the alpha slips, add additional time to 'gaplength' for this task which essentially extends the freeze task remind_build_trans_software "Remind f-dev-announce to build F${major} collection pkgs for trans team" { depends !!software_string_freeze {gaplength 9d} flags translation } task request_review_image "Create Rel-Eng ticket for Live Image compose for Software Review UI" { depends !remind_build_trans_software {gaplength 4d} flags translation } task build_trans_software "Build F-${major} collection packages for all language translators" { depends !request_review_image flags releng, devel } task compose_review_image "Compose of Live Image of Software Review UI for Translation" { depends !build_trans_software flags releng } task trans_software_review "Review and correct software translation in built UI" { depends !build_trans_software {gaplength 1d} length 6d flags translation } task trans_software_rebuild2 "Remind f-dev-announce to Rebuild All Translated Packages" { depends !trans_software_review flags translation } # Should land one week before the "Beta Change Deadline"--affected by "remind_build_trans_software" # Double check after adjusting "trans_software" or length of Alpha tasks for a slip task trans_software_deadline "Software: Translation Deadline (PO Files complete)" { depends !trans_software_review flags translation, roadmap, key } # for ical file task start_trans_rebuild "Software: Start Rebuild all translated packages" { depends !trans_software_deadline flags devel } task trans_rebuild "Software: Rebuild all translated packages" { depends !trans_software_deadline length 5d flags devel } } #end software_translation # Send reminder on Monday before Thursday meeting task alpha_meeting_reminder "Reminder: Alpha Release Readiness Meeting" { depends !feature_freeze {gaplength 10d} flags pm } task alpha_meeting "${content_title} ${major} Alpha Release Readiness Meeting" { depends !alpha_meeting_reminder {gaplength 3d} flags releng, pm, quality, docs, design, translation, marketing, web } # land on a Tuesday task shadow_before_alpha_compose "SHADOW: 1.5 weeks before Alpha Compose" { precedes !create_alpha_compose {gaplength 8d} flags hidden } task create_alpha_tc "Create Alpha Test Compose (TC)" { depends !shadow_before_alpha_compose flags releng, proto } # test Wed to Wed task test_alpha_tc "Test Alpha 'Test Compose'" { depends !create_alpha_tc {gaplength 1d} length 6d flags quality, proto } task alpha_kernel_build "Submit Kernel Build for Alpha RC Compose" { depends !alpha_deadline flags devel } task alpha_installer_build "Submit Installer Build for Alpha RC Compose" { depends !alpha_deadline {gaplength 1d} flags devel } task create_alpha_compose "Compose Alpha Candidate" { depends !alpha_deadline {gaplength 2d} flags releng, proto } task test_alpha_candidate "Test Alpha Candidate" { depends !create_alpha_compose length 5d flags quality, proto } # for ical file task start_stage_alpha "Start Stage & Sync Alpha to Mirrors" { depends !test_alpha_candidate flags releng } task notify_mirrors_alpha "Notify Mirrors of ${content_title} ${major} Alpha" { depends !start_stage_alpha {gaplength 1d} flags releng } task stage_alpha "Stage & Sync Alpha to Mirrors" { depends !test_alpha_candidate length 3d flags releng, proto } task alpha_export_control "Alpha Export Control Reporting" { depends !start_stage_alpha {gaplength 1d} flags releng, pm } task alpha_announce "Create Alpha Announcement (Marketing & Docs)" { depends !alpha_meeting length 2d flags docs, marketing } task alpha_drop "Alpha Public Availability" { depends !stage_alpha flags releng, docs, quality, design, pm, proto, devel, key, marketing, roadmap, spins, blocker, infrastructure, fpl } task ambassador_start "FAmSCo heads Ambassador Wide Meetings Preparing For ${content_title} ${major}" { depends !alpha_drop {gaplength 7d} length 5d flags ambassadors } task start_swag "FAmSCo and Regional teams call for Preparation of Media/SWAG" { depends !alpha_drop {gaplength 7d} flags ambassadors } task swag_poc "Regional Team Meetings and Select POC for Swag/Media production" { depends !alpha_drop {gaplength 8d} length 5d flags ambassadors } task swag_funding_request "Regional Teams Submit Funding Request For Swag/Media Production" { depends !alpha_drop {gaplength 8d} length 5d flags ambassadors } # this task was proposed by Mike McGrath and to be performed by FES task nvr_testing "NVR Update Check testing" { depends !stage_alpha length 1d flags quality } # KEY ADJUSTMENT POINT task alpha_release_notes "Alpha Release Notes" { # create for ical task start_alpha_beats "Start Alpha Beat and Feature Page Review" { depends !!feature_freeze {gaplength 6d} flags docs, quality } task validate_beat_writers "Validate Former Beat Writers" { depends !!feature_freeze length 5d flags docs } task recruited_beat_writers "Recruit New Beat Writers" { depends !validate_beat_writers length 5d flags docs } task comb_alpha_beats "Comb Beats and Feature Pages for Alpha" { depends !start_alpha_beats length 2d flags docs, quality } task notify_devel_relnotes "Notify Development About Alpha Release Notes" { depends !!alpha_deadline flags docs } # KEY ADJUSTMENT POINT # Beta release notes depend on this task # If alpha candidate is not ready on time add extra time for the release notes here task prep_alpha_notes "Prepare Alpha Release Notes (1 page)" { depends !comb_alpha_beats length 6d flags docs, quality } task post_notes "Post Alpha Release Notes One-Page" { depends !prep_alpha_notes {gaplength 1d} flags docs } } #end alpha_release_notes # two days to create web banner # one day for websites team to add to www.fedoraproject.org # should be live one day before the release task alpha_banner "Alpha Release Banner" { precedes !alpha_drop {gaplength 3d} task alpha_create_banner "Create Alpha Website Banner" { length 2d flags design } task alpha_publish_banner "Add Alpha Banner to Website" { length 1d flags web } } # KEY ADJUSTMENT POINT # Three weeks for Alpha Testing # IF "Beta Deadline" is missed the length of this task is extended-- # in that case corresponding change needs to be made to "software_translation" task task test_alpha "Alpha Testing" { depends !stage_alpha length 15d flags quality, proto } task review_bookmarks "Review Firefox Bookmarks For Update" { depends !stage_alpha length 5d flags marketing } task update_bookmarks "Update and Package Firefox Bookmarks" { depends !review_bookmarks length 2d flags marketing } task tag_bookmarks "Tag Updated Bookmarks Package for ${content_title} ${major}" { depends !update_bookmarks flags marketing } # Explicit task to mark end of alpha # Also permits subsequent tasks to cleanly depend on it using "precedes" (because it is zero length) task alpha_end "End of Alpha Testing" { depends !test_alpha flags quality } task beta_marketing_notes "Marketing: Beta One Page Release Notes" { depends !alpha_end {gaplength 5d} length 5d flags marketing } } task beta "Beta Release" { # KEY ADJUSTMENT POINT--if the Alpha slips, make sure the blocker meeting # tasks continue to line up correctly # Once the Alpha is staged, start holding Blocker meetings for the Beta task remind_beta_blocker1 "Reminder: Beta Blocker Meeting (${content}${major}beta) #1" { depends !!alpha.create_alpha_compose {gaplength 9d } flags pm } task beta_blocker1 "Beta Blocker Meeting (${content}${major}beta) #1" { depends !!alpha.stage_alpha {gaplength 3d } flags quality, releng, devel, pm, blocker } task beta_releng_tickets "File All Release Engineering Tickets for ${content_title} ${major} Beta" { depends !!alpha.stage_alpha {gaplength 2d } flags releng } task remind_beta_blocker2 "Reminder: Beta Blocker Meeting (${content}${major}beta) #2" { depends !beta_blocker1 {gaplength 3d} flags pm } task beta_blocker2 "Beta Blocker Meeting (${content}${major}beta) #2" { depends !beta_blocker1 {gaplength 5d} flags releng, quality, devel, pm, blocker } # raise awareness one week before Beta compose task daily_beta_blocker "Daily Review & Notification of Open Beta Blocker Bugs" { depends !beta_blocker2 {gaplength 1d} length 4d flags releng, quality, devel, pm, blocker } task remind_beta_blocker3 "Reminder: Beta Blocker Meeting (${content}${major}beta) #3" { depends !beta_blocker2 {gaplength 3d} flags pm } task beta_blocker3 "Beta Blocker Meeting (${content}${major}beta) #3" { depends !beta_blocker2 {gaplength 5d} flags releng, quality, devel, pm, blocker } task remind_beta_blocker4 "Reminder: Beta Blocker Meeting (${content}${major}beta) #4" { depends !beta_blocker3 {gaplength 3d} flags pm } task beta_blocker4 "Beta Blocker Meeting (${content}${major}beta) #4" { depends !beta_blocker3 {gaplength 5d} flags releng, quality, devel, pm, blocker } # Create anchor one week before for Beta Deadline task shadow_beta_deadline "SHADOW: one week before Beta Deadline" { precedes !beta_deadline {gaplength 6d} flags hidden } task remind_beta_deadline "Remind Beta Deadline in 1 week" { depends !shadow_beta_deadline flags pm } task remind_final_features "Remind Features 100% Complete in 1 week" { depends !shadow_beta_deadline flags pm } task beta_spins_ks "Build spin-kickstarts package from master" { depends !shadow_beta_deadline flags spins } task coordinate_swag_design "FAmSCo Coordinate Media/Swag/Poster artwork with Design team" { depends !shadow_beta_deadline {gaplength 5d} length 10d flags ambassadors } # Two weeks before the public Beta Release task beta_deadline "Beta Change Deadline" { depends !!alpha.test_alpha flags releng, docs, quality, pm, proto, devel, key, marketing, spins, roadmap } task feature_complete "Features 100% Complete Deadline" { depends !!alpha.test_alpha flags releng, docs, quality, pm, proto, devel, key, marketing, roadmap, fpl } # KEY ADJUSTMENT POINT--manually adjust if length of time before Beta ships changes task beta_infrastructure_freeze "Beta Infrastructure Change Freeze" { depends !!alpha.test_alpha length 10d flags infrastructure, releng } task announce_beta_deadline "Announce Beta Deadline & Feature Complete" { depends !!alpha.test_alpha flags pm } task final_feature_fesco "Deliver features < 100% to FESCo" { depends !beta_deadline {gaplength 1d} flags pm } task brief_ambassadors "Brief Ambassadors on upcoming release" { depends !beta_deadline {gaplength 5d} length 5d flags marketing } # The Release Slogan is created by the Marketing team based on # the initial artwork theme. The Design Team needs to know # the slogan to create the release banners # prepare 6 weeks before GA (2 weeks of work) # go live 1 month before GA # KEY ADJUSTMENT POINT task create_countdown "Create Count Down Graphic" { depends !beta_deadline length 10d flags design } task publish_countdown "Publish Count Down Graphic" { depends !create_countdown length 1d flags web } task beta_release_notes "Beta Release Notes" { task unclaimed_beats "Write Unclaimed Wiki Beats" { depends !!!alpha.alpha_drop length 6d flags docs } task port_wiki_publican "Port Wiki to Publican" { depends !unclaimed_beats {gaplength 1d} length 3d flags docs } task remind_trans_beta_notes "Remind Translation: Beta Rel Notes POT Coming" { depends !unclaimed_beats flags docs } task start_release_notes_pot1 "Start nightly POT files all fed-rel-notes.rpm content" { depends !port_wiki_publican {gaplength 1d} flags docs } task release_notes_pot1 "Generate nightly POT files all fed-rel-notes.rpm content" { depends !port_wiki_publican {gaplength 1d} length 13d flags docs } task remind_devel_beta_notes"Remind announce-list & f-devel-announce: Wiki Freeze" { depends !unclaimed_beats {gaplength 1d} flags docs } task beta_wiki_freeze "Wiki Freeze: Beta Release Notes" { depends !remind_devel_beta_notes {gaplength 2d} flags docs } task trans_release_notes "Translate Beta Release Notes" { depends !port_wiki_publican {gaplength 1d} length 14d flags translation } # KEY ADJUSTMENT POINT task build_trans_review "Ongoing build translation review htmls" { depends !beta_wiki_freeze length 5d flags docs } # KEY ADJUSTMENT POINT task trans_review_beta "Review and correct Beta Release Notes (daily buids html)" { depends !beta_wiki_freeze length 5d flags translation } task trans_release_notes_deadline "Translation Deadline: Beta Release Notes (PO Files complete)" { depends !trans_review_beta flags translation, docs } task build_beta_relnotes "Build f-r-n.rpm and Push to updates-candidate" { depends !trans_release_notes_deadline length 2d flags docs, translation } task final_release_notes_reminder "Reminder: Send Project Wide-Final Release Notes Deadlines" { depends !!beta_deadline {gaplength 7d} flags docs } # one day before release which is 2D after meeting task web_notes "Build and Post Beta release-notes to docs.fedoraproject.org" { depends !!beta_meeting {gaplength 2d} flags docs } # one day before release which is 2D after meeting task tech_web_notes "Build and Post Fedora Technical Notes to docs.fedoraproject.org" { depends !!beta_meeting {gaplength 2d} flags docs } } # end beta_release_notes task splash_deadline "Deadline: Beta Splash Screens" { depends !!alpha.finalize_splash_screens flags design } task package_final_splash "Package: Beta Splash Screens" { depends !!alpha.finalize_splash_screens length 2d flags design } task package_beta_wallpaper "Package: Beta Wallpaper"{ depends !!alpha.beta_wallpaper length 2d flags design } task package_supplemental_wallpaper "Package: Supplemental Wallpaper"{ depends !!alpha.beta_wallpaper flags design } task beta_meeting_announce "Announce: Beta Release Readiness Meeting" { flags pm } # KEY ADJUSTMENT POINT if Beta Deadline changes task beta_meeting_reminder "Reminder: Beta Release Readiness Meeting" { depends !beta_deadline {gaplength 4d} flags pm } task beta_meeting "${content_title} ${major} Beta Release Readiness Meeting" { depends !beta_meeting_reminder {gaplength 3d} flags releng, pm, quality, docs, design, translation, marketing, web } task beta_announce "Create Beta Announcement (Docs & Marketing)" { depends !beta_meeting length 2d flags docs, marketing } # placeholder if slip # task beta_blocker4 "Beta Blocker Day (${content}${major}beta) #4" { # depends !!beta_blocker3 {gaplength 5d} # flags releng, quality, devel, pm, blocker # } task shadow_before_beta_compose "SHADOW: 1.5 weeks before Beta Compose" { precedes !create_beta_compose {gaplength 9d} # with 'precedes', gaplength pushes date backwards flags hidden } task beta_installer_build1 "Submit Installer Build for Beta TC Compose" { depends !shadow_before_beta_compose flags devel } task create_beta_tc "Create Beta Test Compose (TC)" { depends !shadow_before_beta_compose {gaplength 2d} flags releng, proto } task test_beta_tc "Test Beta 'Test Compose'" { depends !create_beta_tc length 6d flags quality, proto } task beta_rawhide_install "Pre-Beta Acceptance Test Plan" { precedes !create_beta_tc length 5d flags quality } task remind_beta_go_not "Reminder: ${content_title} ${major} Beta Go/No-Go Meeting" { depends !create_beta_compose {gaplength 2d} flags pm } task beta_go_not "${content_title} ${major} Beta Go/No-Go Meeting (17:00 US Eastern)" { depends !create_beta_compose {gaplength 4d} flags releng, quality, devel, pm, proto, blocker } task beta_kernel_build "Submit Kernel Build for Beta RC Compose" { depends !beta_deadline flags devel } task beta_installer_build "Submit Installer Build for Beta RC Compose" { depends !beta_deadline {gaplength 1d} flags devel } # KEY ADJUSTMENT POINT if Beta release date slips--add more time to this task task create_beta_compose "Compose Beta Candidate" { depends !beta_deadline {gaplength 2d} flags releng, proto } task call_for_events "FAmSCo and Regional Teams Call for Release Events" { depends !beta_deadline {gaplength 12d} flags ambassadors } task logistics_budget "Regional Teams Plan Regional Logistics for Release Events & File Budget Requests" { depends !call_for_events length 10d flags ambassadors } task test_beta2 "Test Beta Candidate" { depends !create_beta_compose length 5d flags quality, proto } task start_stage_beta "Start Stage & Sync Beta to Mirrors" { depends !test_beta2 flags releng } task notify_mirrors_beta "Notify Mirrors of ${content_title} ${major} Beta" { depends !start_stage_beta {gaplength 1d} flags releng } task stage_beta "Stage & Sync Beta to Mirrors" { depends !test_beta2 length 3d flags releng, proto } task beta_export_control "Beta Export Control Reporting" { depends !start_stage_beta {gaplength 1d} flags releng, pm } # two days to create web banner # one day for websites team to add to www.fedoraproject.org # should be live one day before the release task beta_banner "Beta Release Banner" { precedes !beta_drop {gaplength 3d} task beta_create_banner "Create Beta Website Banner" { length 2d flags design } task beta_publish_banner "Add Beta Banner to Website" { length 1d flags web } } task shadow_before_beta_drop "SHADOW: One Day before Public Beta release" { precedes !beta_drop {gaplength 2d} flags hidden } # Five weeks prior to GA task beta_drop "Beta Release Public Availability" { depends !stage_beta flags docs, releng, quality, pm, translation, proto, design, devel, key, marketing, roadmap, blocker, spins, infrastructure, fpl } task event_deadline "Release Event Submission Deadline" { depends !logistics_budget {gaplength 1d} flags ambassadors } task budget_allocations "FAmSCo Review Budget Allocations" { depends !event_deadline flags ambassadors } task irc_sessions "FAmSCo Regional IRC town halls" { depends !beta_drop {gaplength 8d} length 10d flags ambassadors } # Three weeks of public testing # Ends on a Monday and is followed by Final Release Deadline task beta_test "Beta Testing" { depends !stage_beta length 14d flags quality, proto } task websites_trans_reminder "Reminder to f-websites-list about POT/PO dates in 7 days" { depends !beta_drop flags translation, web } # two weeks to create # should be completely done and ready for hand off to Ambassadors two weeks before GA # Ambassadors should have made prior arrangements to flip artwork over to media producer # at this time. task media "Create DVD/CD label and sleeve artwork" { # not a "great" place to anchor to as it could move # needs to start four weeks before GA depends !beta_drop length 10d flags design } task rc_rawhide_install "Pre-RC Acceptance Test Plan" { depends !stage_beta {gaplength 7d} length 4d flags quality } task testmile "End of Beta Testing" { depends !beta_test flags quality } } } # E N D of TestingPhase task LaunchPhase "Launch Phase" { # four weeks before GA, ambassadors create release posters # two weeks before GA to art team does final polish to posters # posters are ready on release day task release_posters "Release Party Posters" { depends !!TestingPhase.beta.beta_drop task create_posters "FAmSCo with Design Team Create Release Party Posters" { length 10d flags ambassadors } task polish_poster "Polish/Finalize Release Party Posters" { depends !create_posters length 9d flags design } } task screenshots "Update and freeze the screenshots page" { depends !!TestingPhase.beta.stage_beta {gaplength 5d} length 5d flags marketing } task final_screenshots "Marketing: Final Screen Shots" { depends !screenshots length 5d flags marketing } task final_marketing_notes "Marketing: Final One Page Release Notes" { depends !screenshots length 5d flags marketing } task briefings "Brief news distribution network" { depends !screenshots length 5d flags marketing } task monitor "Monitor news sites to provide corrections & info" { depends !screenshots length 29d flags marketing } task rc "Release Candidate" { task final_releng_tickets "File All Release Engineering Tickets for ${content_title} ${major} GA" { depends !!!TestingPhase.beta.stage_beta {gaplength 2d } flags releng } task remind_ga_blocker1 "Reminder: Final Blocker Meeting (${content}${major}blocker) #1" { depends !!!TestingPhase.beta.create_beta_compose {gaplength 4d} flags pm } task ga_blocker1 "Final Blocker Meeting (${content}${major}blocker) #1" { depends !!!TestingPhase.beta.start_stage_beta {gaplength 1d} flags releng, quality, devel, pm, blocker } task remind_ga_blocker2 "Reminder: Final Blocker Meeting (${content}${major}blocker) #2" { depends !ga_blocker1 {gaplength 3d} flags pm } task ga_blocker2 "Final Blocker Meeting (${content}${major}blocker) #2" { depends !ga_blocker1 {gaplength 5d} flags releng, quality, devel, pm, blocker } task remind_ga_blocker3 "Reminder: Final Blocker Meeting (${content}${major}blocker) #3" { depends !ga_blocker2 {gaplength 3d} flags pm } task ga_blocker3 "Final Blocker Meeting (${content}${major}blocker) #3" { depends !ga_blocker2 {gaplength 5d} flags releng, quality, devel, pm, blocker } # two days before Final Deadline task shadow_before_final_deadline "SHADOW: one day before Final Deadline" { precedes !final_change_deadline {gaplength 2d} flags hidden } task kernel_debug "Disable Kernel debug and submit new Kernel build for RC" { depends !shadow_before_final_deadline flags devel } task final_change_deadline "Final Change Deadline" { depends !!!TestingPhase.beta.beta_test flags releng, devel, proto, pm, key, spins } task check_swag "FAmSCo and Regional Teams Meet to Address Unresolved Events/Media/Swag Issues" { depends !final_change_deadline {gaplength 1d} flags ambassadors } # one day before Final Deadline task final_wallpaper "Package Final Wallpaper" { depends !shadow_before_final_deadline flags design } # one day before Final Deadline task final_splash "Package Final Splash Screens" { depends !shadow_before_final_deadline flags design } task announce_final_change_deadline "Announce Final Freeze & Implications" { depends !final_change_deadline flags pm } # ADJUST FOR NEW RELEASE--EOL Version task eol_warning "File RHT Eng-ops ticket for Fedora 15 EOL Bugzilla warning" { depends !final_change_deadline flags pm } # KEY ADJUSTMENT POINT--manually adjust if length of time before Final release ships changes task final_infrastructure_freeze "Final Infrastructure Change Freeze" { depends !!!TestingPhase.beta.beta_test {gaplength 1d} length 10d flags infrastructure, releng } task remind_ga_blocker4 "Reminder: Final Blocker Meeting (${content}${major}blocker) #4" { depends !ga_blocker3 {gaplength 3d} flags pm } task ga_blocker4 "Final Blocker Meeting (${content}${major}blocker) #4" { depends !ga_blocker3 {gaplength 5d} flags releng, quality, devel, pm, blocker } # raise awareness one week before final compose task daily_ga_blocker "Daily Review & Notification of Open Final Blocker Bugs" { depends !ga_blocker3 {gaplength 1d} length 4d flags releng, quality, devel, pm, blocker } task ga_blocker5 "Final Blocker Meeting (${content}${major}blocker)--Blocks RC Compose" { depends !ga_blocker4 {gaplength 1d} flags releng, quality, devel, pm, blocker } task ga_release_notes "Final Release Notes" { # One day before public beta release task final_release_note_wiki_reminder "Reminder to Development: Wiki Freeze in 7 days" { depends !!!!TestingPhase.beta.shadow_before_beta_drop flags docs } task prep_ga_notes "Prepare GA Release Notes" { depends !!!!TestingPhase.beta.beta_drop flags docs, quality } task ga_release_notes_freeze "String Freeze: GA Release Notes" { depends !prep_ga_notes {gaplength 4d} flags docs } task wiki_ga_port "Port diff wiki content to Publican" { depends !ga_release_notes_freeze length 5d flags docs } task remind_trans_ga_notes "Remind Translation: RPM Freeze (no more POTs) in 5 days" { depends !ga_release_notes_freeze {gaplength 2d} flags docs } # KEY ADJUSTMENT POINT--if length of Beta changes this task length needs to change too task ga_pot_trans "Translate Final Release Notes (POT to PO)" { depends !!!!TestingPhase.beta.beta_release_notes.trans_release_notes_deadline {gaplength 1d} length 24d flags translation } task ga_release_notes_pot "Generate GA Release Notes POT files for Translation" { depends !wiki_ga_port flags docs } task build_trans_review_final "Build GA release note htmls for Translation" { depends !ga_release_notes_pot {gaplength 1d} length 4d flags docs } task build_ga_trans_review "Review and correct GA Release Notes (daily builds html)" { depends !ga_release_notes_pot {gaplength 1d} length 4d flags docs, translation } task remind_ga_trans_deadline "Remind Translators of GA Release Notes Deadline in 4 days" { depends !ga_release_notes_pot {gaplength 3d} flags docs } task ga_release_notes_po "Translation Deadline: GA rel-notes (PO Files complete)" { depends !ga_pot_trans flags translation } task ga_release_notes_rpm "Build fedora-release-notes.rpm" { depends !ga_release_notes_po length 2d flags docs } } #end final release notes # Three banners are created for the GA release (based on the Slogan from Marketing) # 1) large banner--fedoraproject.org front page # 2) "the release is out, go get it"--fedoraproject.org front page # 3) release name on start.fedoraproject.org # banners take one week to complete # banners should be completed one week before GA # banners are translated the week up until GA # translated during the week up until GA task ga_create_banners "Create Final Release Banners" { depends !!!TestingPhase.beta.testmile length 9d flags design } # Start one day before Final Change Deadline task create_ga_announce "Create GA Announcement (Docs & Marketing)" { depends !!!LaunchPhase.rc.shadow_before_final_deadline length 7d flags docs, marketing } task translate_ga_announce "GA Announcement available for translation (optional)" { depends !create_ga_announce length 5d flags translation } task ga_publish_banners "Add Final Release Banners to Website" { depends !ga_create_banners length 1d flags web } # web properties need to be updated and translated # Tasks start at time Beta Release goes out # http://fedoraproject.org/en/index # http://fedoraproject.org/en/get-fedora # http://fedoraproject.org/en/join-fedora # http://fedoraproject.org/en/get-help task web_content_update "Update Website Content" { depends !!!TestingPhase.beta.beta_drop length 5d flags web } task web_freeze "Website String Freeze" { depends !web_content_update flags web } task web_create_pot "Create Website POT Files" { depends !web_freeze length 1d flags web } task trans_web "Translation Period for Website (POT to PO)" { depends !web_create_pot length 9d flags translation } task review_trans_web "Review and correct Website translations" { depends !trans_web length 4d flags translation, web } task finish_trans_web "Translation Deadline: Websites (POs done)" { depends !review_trans_web flags translation } task publish_trans_web "Publish Translations on Website (fedoraproject.org)" { depends !review_trans_web length 1d flags web } task final_meeting_reminder "Reminder: Final Release Readiness Meeting" { depends !!!TestingPhase.beta.beta_test {gaplength 5d} flags pm } task ga_meeting "${content_title} ${major} Final Release Readiness Meeting" { depends !final_meeting_reminder {gaplength 3d} flags releng, pm, quality, docs, design, marketing, translation, web } task shadow_before_final_compose "SHADOW: one week before RC Compose" { precedes !start_final_compose {gaplength 8d} flags hidden } task final_installer_build1 "Submit Installer Build for Final TC Compose" { depends !shadow_before_final_compose flags devel } task create_final_tc "Create 'Final' Test Compose (TC)" { depends !shadow_before_final_compose {gaplength 2d} flags releng, proto } task test_final_tc "Test 'Final' Test Compose (TC)" { depends !create_final_tc length 4d flags quality, proto } task final_installer_build "Submit Installer Build for Final RC Compose" { depends !final_change_deadline flags devel } task start_final_compose "Compose 'Final' RC: DVD, Live, Spins" { depends !final_change_deadline {gaplength 1d} length 1d flags releng, key, roadmap, proto } task early_iso "Regional Teams Obtain Final Release ISOs from Release Engineering for duplication" { depends !test_final {gaplength 2d} length 3d flags ambassadors } task regional_marketing "Regional Coordination with Marketing for Release Events" { depends !test_final {gaplength 2d} length 5d flags ambassadors } task deliver_final "Deliver RC to QA for Testing" { depends !start_final_compose flags releng, proto } task test_final "Test 'Final' RC" { depends !deliver_final length 4d flags quality } # for ics file task start_stage_final "Start Stage & Sync RC to Mirrors" { depends !test_final {gaplength 2d} flags releng } task notify_mirrors_final "Notify Mirrors of ${content_title} ${major} Final" { depends !start_stage_final {gaplength 1d} flags releng } task stage_final "Stage & Sync RC to Mirrors" { depends !test_final {gaplength 2d} length 3d flags releng, proto } task package_spins_ks "Branch spin-kickstarts and build package from new branch" { depends !create_final_tc flags spins } task freeze_spins_ks "Spins kickstart package Freeze" { depends !create_final_tc flags spins } task enable_updates "Enable ${content_title} ${major} Updates" { depends !!!TestingPhase.beta.beta_test {gaplength 2d} flags releng } task remind_final_go_not "Reminder: ${content_title} ${major} Final Go/No-Go Meeting" { depends !start_final_compose {gaplength 1d} flags pm } # Hold on Tuesday instead of Wednesday (Alpha and Beta)--this provides a little cushion # if something goes wrong. Mirrors do not have to start sync until Thursday task final_go_not "${content_title} ${major} Final Go/No-Go Meeting (17:00 US Eastern)" { depends !start_final_compose {gaplength 4d} flags releng, quality, docs, pm, proto, blocker } task final_export_control "Final Export Control Reporting" { depends !start_stage_final {gaplength 1d} flags releng, pm } # Zero-day tasks should start two Fridays before GA and finish the day before GA task zero_day_relnotes "Zero Day Release Notes" { task shadow_zero_day "SHADOW: Seven work week days before GA" { precedes !!!final {gaplength 7d} flags hidden } task zero_day_web "0-Day rel-notes update docs.fp.org" { depends !shadow_zero_day length 6d flags docs } task zero_day_rpm "0-Day rel-notes build updated rpm" { depends !shadow_zero_day length 6d flags docs } task zero_day_pot "0-Day rel-notes generate POT" { depends !shadow_zero_day length 6d flags docs } task zero_day_trans "Translate 0-Day Release Notes" { depends !shadow_zero_day length 6d flags translation } task zero_day_deadline "Translation Deadline: 0-Day (PO Files complete)" { depends !zero_day_trans flags translation } task web_post "Add translated zero-day updates to docs.fp.org" { depends !zero_day_trans flags docs } task post_tech_notes "Update and post Fedora Technical Notes to docs.fedoraproject.org" { depends !!!final flags docs } # Monday after Tuesday GA task push_updates_rpm "Push updated rel-notes RPMs to Updates repo" { depends !!!final {gaplength 4d} flags docs } } # zero_day_relnotes } # end of rc task task bugzilla_description "Reflect supported versions in Bugzilla product description" { depends !rc.stage_final flags pm } task final "Final (GA) Release" { depends !rc.stage_final flags quality, releng, docs, design, pm, translation, proto, devel, key, marketing, roadmap, spins, infrastructure, fpl } # ADJUST FOR NEW RELEASE--EOL Version task remind_eol "Send Email Reminder About Fedora 16 EOL Activities" { depends !final {gaplength 2d} flags releng, pm, devel } task event_reports "Hold Release Events and Publish Event Reports" { depends !final length 23d flags ambassadors } task spins_ga_ks "Build new spin-kickstarts package for updates (if necessary)" { depends !rc.stage_final flags spins } task marketing_post "Marketing Retrospective" { depends !final length 10d flags marketing } } # Starting in Fedora 13, the docs group wanted the Guides work to span almost # the entire release cycle--they don't fit nicely into different phases. # So we put them all here in their own block. # Run from Start until one week before GA task all_guides "${content_title} ${major} Guides" { # Run from Start until "Branch Guides" task continue_guides_trans "Continue translation of guides in branch of previous release " { length 70d flags translation } task test_branch_guides "Test master branches of guides against Alpha and correct" { depends !!TestingPhase.alpha.stage_alpha length 10d flags docs } task branch_guides "Branch Guides" { depends !test_branch_guides flags docs } task guides_pot "Create POT files for All Guides" { depends !branch_guides flags docs } task notify_trans "Notify trans that new Guide POT files available " { depends !guides_pot flags docs } task trans_all_guides "Translate All Guides (POT to PO)" { depends !guides_pot flags docs } task publish_draft "Publish draft guides" { depends !branch_guides flags docs } task announce_publish_draft "Notify announce-list and f-devel-list draft guides available" { depends !publish_draft flags docs } # ADJUST FOR NEW RELEASE--make sure this task lands one day before GA task guides_trans "Translate All Guides (POT to PO)" { depends !guides_pot length 39d flags translation } task remind_trans_pot "Reminder to Trans that new POT files are coming for all guides" { depends !!TestingPhase.alpha.stage_alpha {gaplength 8d} flags docs } # Wednesday, one week after Beta Change Deadline task srpm_review "Remind new guide owners SRPM package review" { depends !!TestingPhase.beta.beta_deadline {gaplength 6d} flags docs } task shadow_before_beta_deadline "SHADOW: for Friday before Beta deadline" { precedes !!TestingPhase.beta.beta_deadline {gaplength 1d} flags hidden } # Friday before Final Change Deadline task remind_trans "Reminder to Trans that Final Guides POT files are coming" { depends !shadow_before_beta_deadline flags docs } task guides_string_freeze "String Freeze All Guides" { depends !!LaunchPhase.rc.final_change_deadline flags docs } task generate_final_pot "Generate final POT files for Guides" { depends !guides_string_freeze flags docs } task notify_trans_final "Notify Trans of Final Guides POT availability" { depends !guides_string_freeze flags docs } # Monday to Friday, two weeks before GA task build_daily "Daily builds of Final guides for Translation" { length 9d depends !!LaunchPhase.rc.final_change_deadline flags docs } # Monday to Friday, two weeks before GA task review_daily "Review and correct Final Translated Guides (daily builds html)" { depends !!LaunchPhase.rc.final_change_deadline length 9d flags translation } # Friday before GA task shadow_guides_trans_deadline "SHADOW: Translation Deadline: All Final Guides" { precedes !!LaunchPhase.final {gaplength 2d} flags hidden } # zero duration tasks (milestones/deadlines) need a shadow precedes task so they report correctly # Also needed for ICS files which only report milestones task guides_trans_deadline "Translation Deadline: All Final Guides" { depends !shadow_guides_trans_deadline flags translation } task test_guides_beta "Test guides against Beta and correct" { depends !!TestingPhase.beta.beta_drop length 4d flags docs } task refresh_pot "Refresh POT files for all guides against Beta" { depends !test_guides_beta flags docs } task notify_trans_refresh "Notify trans that POT files updated against Beta" { depends !refresh_pot flags docs } task republish_draft "Republish draft guides for Beta" { depends !test_guides_beta flags docs } task notify_revised_draft "Notify announce-list and f-devel-list revised draft guides available" { depends !republish_draft flags docs } # Friday before GA until the day before GA task guides_final_build "Final Build All Guides: All Languages" { depends !srpm_review length 3d flags docs } # Day before GA task shadow_publish_guides "SHADOW: Publish all guides to docs.fp.o (html,html-single,pdf)" { precedes !!LaunchPhase.final {gaplength 2d} flags hidden } task publish_guides "Publish all guides to docs.fp.o (html,html-single,pdf)" { depends !shadow_publish_guides flags docs } } # end of "all_guides" # Elections http://fedoraproject.org/wiki/Elections # Board & FESCo are reelected in every release # FAmSCo Elections are held once a year near the Halloween release # ADJUST FOR NEW RELEASE task elections "${content_title} ${next_major} Election Coordination" { # manually set start date that is five weeks before GA date (on a Tuesday) # this is cleaner than using 'precedes' # use 'duration' to make it easier for some tasks to land on weekends start 2013-09-24 flags elections, fpl, pm # Don't forget to do this! Or else bad things happen. task remind "Remind advisory-board list of upcoming election schedule" { } task solicit "Solicit volunteers for questionnaire and town halls" { depends !remind duration 7d } task wiki_update "Update wiki page https://fedoraproject.org/wiki/Elections with required information" { depends !solicit } task advertise_elections "Advertise elections schedule and pages" { depends !solicit } task announce_nominations "FPL/designee announces opening of nominations" { depends !solicit {gapduration 25d} } task open_questions "Questionnaire wrangler announces opening for questions"{ depends !announce_nominations } task collect_questions "Collect question on the wiki"{ depends !announce_nominations duration 8d } task collect_answers "Candidates write questionnaire answers" { depends !collect_questions duration 7d } task announce_town "Town hall wrangler announces schedule for town hall meetings" { depends !announce_nominations } task question_deadline "Questionnaire answers due from candidates" { depends !collect_answers } # five days before GA task present_answers "Wrangler presents questionnaire answers" { depends !collect_answers {gapduration 1d} duration 2d } task post_questions "All answers posted to questionnaire page, advertise to voters" { depends !present_answers } task town_hall "Town hall period" { depends !post_questions {gapduration 1d} duration 6d } task voting_application "Finalize Voting Application" { duration 1d depends !town_hall } task voting_start "Voting Begins" { depends !voting_application } task voting "Voting for general elections" { depends !voting_application duration 6d } task voting_end "Voting Ends" { depends !voting } task announce_results "Announce Results" { depends !voting_end {gapduration 1d} } } # end elections # The release naming process for the next release should end three weeks prior # to the end of the current release # It is easiest to start this task by hard coding the start date to be 6 weeks before GA # 2010-09-14 # 1 week -- collect names # 1 week --fedora board reviews names # 2 weeks -- names reviewed by RHT legal # 1 week -- community vote on names # ADJUST FOR NEW RELEASE task naming "Name the ${content_title} ${next_major} Release" { start 2013-09-17 # manually set this date flags pm, fpl task gather "Collect ${content_title} ${next_major} Names on Wiki" { length 5.5d } task board "Board Review of Proposed ${content_title} ${next_major} Names" { depends !gather length 3d } task legal "Legal Review" { depends !board length 5d } task vote "Voting" { depends !legal length 4d } task announce_name "${content_title} ${next_major} Release Name Announced" { depends !vote {gapduration 1d} flags design, pm } } # end release naming task pr "Public Relations" { task video "Creative team videos" { flags fpl, pr task video_schedule "Meet w/Creative to schedule videos" { precedes !video1.review_spotlight1_video {gaplength 25d} length 5d } task video1 "Make spotlight video #1" { task review_spotlight1_video "Review video #1" { precedes !release_spotlight1_video length 15d } task release_spotlight1_video "Publish spotlight video #1" { depends !!!spotlight_feature_blogs.spotlight_feature1.spotlight_feature1_drop } } task video2 "Make spotlight video #2" { task review_spotlight2_video "Review video #2" { precedes !release_spotlight2_video length 15d } task release_spotlight2_video "Publish spotlight video #2" { depends !!!spotlight_feature_blogs.spotlight_feature3.spotlight_feature3_drop } } task release_video "Make release video" { task review_release_video "Review release video" { precedes !release_video length 20d } task release_video "Publish release video" { depends !!!!LaunchPhase.final } } } #end video task beta_release_blog "Beta press blog entry" { flags fpl, pr /** * By building the final task to be a 0-duration event * (milestone), depending on another given task date like a * release date, and having each other task below precede the * one following, these tasks should auto-adjust to fall on a * given date. */ task beta_release_blog_draft "Start drafting Beta blog" { precedes !beta_release_blog_legal length 10d } task beta_release_blog_legal "Red Hat PR send Beta blog to Legal" { precedes !beta_release_blog_intl length 5d } task beta_release_blog_intl "Red Hat PR send Beta blog to intl-pr list" { precedes !beta_release_blog_drop length 6d } task beta_release_blog_drop "Red Hat PR publish and send Beta blog to media contacts" { depends !!!TestingPhase.beta.beta_drop } } task spotlight_feature_blogs "Spotlight feature press blogs" { task spotlight_feature1 "Spotlight feature #1" { task spotlight_feature1_draft "Draft spotlight #1 blog entry" { precedes !spotlight_feature1_legal length 5d flags fpl, pr } task spotlight_feature1_legal "Red Hat PR send spotlight #1 blog entry draft to legal" { precedes !spotlight_feature1_drop length 5d flags fpl, pr } task shadow_spotlight_feature1 "SHADOW: Three weeks until GA" { precedes !!!!LaunchPhase.final { gaplength 16d } length 1d flags hidden } task spotlight_feature1_drop "Red Hat PR publish spotlight #1 blog entry" { depends !shadow_spotlight_feature1 flags fpl, pr } } task spotlight_feature2 "Spotlight feature #2" { task spotlight_feature2_draft "Draft spotlight #2 blog entry" { precedes !spotlight_feature2_legal length 5d flags fpl, pr } task spotlight_feature2_legal "Red Hat PR send spotlight #2 blog entry draft to legal" { precedes !spotlight_feature2_drop length 5d flags fpl, pr } task spotlight_feature2_drop "Red Hat PR publish spotlight #2 blog entry" { depends !shadow_spotlight_feature2 flags fpl, pr } task shadow_spotlight_feature2 "SHADOW: Two weeks until GA" { precedes !!!!LaunchPhase.final { gaplength 11d } length 1d flags hidden } } task spotlight_feature3 "Spotlight feature #3" { task spotlight_feature3_draft "Draft spotlight #3 blog entry" { precedes !spotlight_feature3_legal length 5d flags fpl, pr } task spotlight_feature3_legal "Red Hat PR send spotlight #3 blog entry draft to legal" { precedes !spotlight_feature3_drop length 5d flags fpl, pr } task spotlight_feature3_drop "Red Hat PR publish spotlight #3 blog entry" { depends !shadow_spotlight_feature3 flags fpl, pr } task shadow_spotlight_feature3 "SHADOW: One week until GA" { precedes !!!!LaunchPhase.final { gaplength 6d } duration 1d flags hidden } } } task usb_keys_prebriefs "USB Keys and media pre-briefs" { flags fpl, pr task buy_usb_keys "Purchase USB Keys" { precedes !prepare_usb_keys length 5d } task assess_press_kit "Check LiveUSB press review sheet for readiness" { precedes !update_press_one_sheet length 5d } task update_press_one_sheet "Update LiveUSB press review sheet" { precedes !send_usb_keys length 5d } task prepare_usb_keys "Prep USB keys with pre-release" { precedes !send_usb_keys length 5d } task send_usb_keys "Send USB keys to Red Hat PR for distribution" { precedes !media_prebriefs {gaplength 10d} length 3d } task distribute_usb_keys "Red Hat PR distribute USB keys to media contacts" { precedes !media_prebriefs {gaplength 7d} length 5d } task media_prebriefs "Hold media prebrief interviews" { precedes !!!LaunchPhase.final length 6d } } task redhat_com_update "Update Red Hat web site" { flags fpl, pr task web_graphics_discuss "Schedule meeting with Red Hat web team to plan launch" { length 5d precedes !web_promo_to_brand } task web_promo_to_brand "Send web promo ideas to Brand" { length 5d precedes !web_copy_review } task web_copy_review "Review and update www.redhat.com/Fedora copy" { length 5d precedes !web_copy_send_update } task web_copy_send_update "Send updated copy to Web team" { precedes !rh_web_goes_live { gaplength 11d } length 5d } task rh_web_goes_live "Red Hat website changes go live" { depends !!!LaunchPhase.final } } task ga_press_release "GA press release" { flags fpl, pr task ga_press_release_draft "Start drafting GA press release" { length 10d precedes !ga_press_release_legal } task ga_press_release_legal "Red Hat PR send GA press release to Legal" { length 5d precedes !ga_press_release_intl } task ga_press_release_intl "Red Hat PR send GA press release to intl-pr list" { length 6d precedes !ga_press_release_drop } task ga_press_release_drop "Red Hat PR publish and send GA press release to media contacts" { depends !!!LaunchPhase.final } } task ceo_blog "CEO press blog entry" { flags fpl, pr task ceo_prepare_final_rc "Prepare a final RC on USB for CEO" { precedes !ceo_send_final_rc length 2d } task ceo_send_final_rc "Send final RC USB key to CEO" { precedes !ceo_solicit_feedback length 2d } task ceo_solicit_feedback "Solicit CEO feedback on pre-release" { precedes !ceo_blog_draft length 4d } task ceo_blog_draft "Draft CEO blog" { precedes !ceo_blog_legal length 3d } task ceo_blog_legal "Red Hat PR send CEO blog to Legal" { precedes !ceo_blog_drop length 3d } task ceo_blog_drop "Red Hat PR publish and send CEO blog to media contacts" { depends !!!LaunchPhase.final {gaplength 1d} } } } } # Bitter End include "reports.tji" tagfile "tags" TaskJuggler-3.8.1/examples/Fedora-20/icons/000077500000000000000000000000001473026623400203245ustar00rootroot00000000000000TaskJuggler-3.8.1/examples/Fedora-20/icons/fedoralogo.png000066400000000000000000000112301473026623400231500ustar00rootroot00000000000000PNG  IHDR2zGsRGBbKGD pHYs B Bd+_tIMEM6|IDATx{\U"@`@䭠U(pGTfFBQ$"8fAW|0#"V $z DbgII]֭[U!LR{duUݺܳϷђBpZ` dZ"?g-doqm(I;ff;Y]Wh%{_laum#gmOr}=]WyK,ibׯ0ZPٶ>vv=T,(޵z=RYKh>pI|X|$PQFqL<>a ̛pyp4#0 P(ޥo$̌g]kfێ]L4[ݐs_a{ Ԣ+-y5d|O_ C˿1#+^~錛9/v^ ]kVӬK_`PUY|fƌQY{^D5*qk^gN;nywaA0{yk9BN.;.F5r [+Oآ[Ű3-2b/Qcώl-JK^ 9& *P(+ŌB_,`6bt)Xdז'Q -y5db1z &vWL;l<&t\s{U!6T P)سmSԨ[dt}2#O >iO^ I=mBo}l+![~J `<ΝHG5^I?gr j.@Ǟ|:ҏ=1<66=Dŭ kkbO?̞[^ja`~k`BvҀ<׹xvd̈1P9ut[%ILEǴъQf &3n($q9(7n}֏bCAiXn%Փ'Rٛ>P+\yson?ȣF+Ƹ(+\?b#~rݔI* yكU߫mޯt%}x 8*u+m;P{_\}Đڬݳ/=h<^[JT+5<W[ί#J۱uXJe7*k$X빎 nrf>9⫖Aq>2E c*}H=/}o_f'x[y)\ny:ǜ+j ॰U*'M=ߔ Lew~0TJ#Uv ϝ?Q b3>Yebuk6bĆ)ovY$ҹ(uNS ή$q,nQO -|&&fmd#|t+w{Iioѽ7nP(Dp?75+֗?2l6 [r!ε_@Q{WF(}{o@ .vCЭ+2Tv 3[ _\sO6-i}>e<,-'ڌ"e;|1B `q><K%.lUC^ g%HFHN>܂v}5./ZAKswD*t%Ň>$dms593%(, gy78s1YX X N%\itnUS=ILrW|!u1nO|`Y9eIN%oj Rӄ?4AFJbVǢOF`ji1۫~_+Stc!}k{&qdL`bB>a1qk =1I.u%HRC2  gAJ.DאpEN+*\݌g4DٕjR~i ڣGs )ǒ,EKܱ "ތǀ`Sl*33I\Hot3YKQ*UP ^Oeoj4xs_4@ss6sTvM}-a@acp~=Ks1JC|T"=o$TPddO.dXX_0-*x>N tx*;`NHsi!ИAz\%=H玉kD:{7n;ΞA,z+Eiq`:7 6I=fWԋ1ްI,X"SlT=B43PR[\oƯDڌ@MnHs]kzawr6{'fߺsajQ6&m(< ~E0 x53C8R~*eFXdiRNZ 8O˿\t5\u ̋sWWbU/*'5<\QdL` b*4/:\I#>L#3u{%tE~**!: =߬}H8%u4XlUQ!ި-2( l3~jJ|.3}sp;]%]ᾖc\Pto&FJe_[<7q~,L?ViYx*h2Ζ&I4+c6 l&+ &s )ǷtwIp3^#uCď{$uHכ /y /˻ NOj{+<ۛF܈xG':2H;I>J(ʞ&f Mx ĭ?P # Copyright 2011 Robyn Bergeron # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation files # (the "Software"), to deal in the Software without restriction, # including without limitation the rights to use, copy, modify, merge, # publish, distribute, sublicense, and/or sell copies of the Software, # and to permit persons to whom the Software is furnished to do so, # subject to the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # # This project schedule was derived from the original Fedora 20 # schedule way before the actual project start. By the time you are # looking at this file, the real schedule might have been updated # already. This is just an example file with example dates. The real # schedule can be found at http://fedoraproject.org/wiki/Schedule # These macros are set (edited) manually each week to create # the individual team schedule reminders, created by running # $ make calendar # Sunday macro weekly_start [2013-05-05] # Wednesday falling two weeks after starting Sunday macro weekly_end [2013-05-22] navigator menu macro all_navbar [ header -8<- == [[File:icons/fedoralogo.png|bottom]] ${content_title}-${major} Project Plan == <[navigator id="menu"]> ->8- ] macro FilePrefix [${content}-${major}-] macro HLPrefix [${content_title} ${major} ] textreport "Public Reports" { formats html ${all_navbar} timeformat "%a %Y-%m-%d" scenarios actual taskreport "index" { title "Project Overview" headline "${HLPrefix} Task Overview" columns name, start, end, duration, chart { scale month } sorttasks tree, actual.start.up, seqno.up hidetask hidden taskreport "${FilePrefix}planning-phase" { title "Planning Phase" headline "${HLPrefix} Planning Phase" taskroot f20.PlanningPhase } taskreport "${FilePrefix}development-phase" { title "Development Phase" headline "${HLPrefix} Development Phase" taskroot f20.DevelopmentPhase } taskreport "${FilePrefix}alpha-phase" { title "Alpha Testing Phase" headline "${HLPrefix} Alpha Testing Phase" taskroot f20.TestingPhase.alpha } taskreport "${FilePrefix}beta-phase" { title "Beta Testing Phase" headline "${HLPrefix} Beta Testing Phase" taskroot f20.TestingPhase.beta } taskreport "${FilePrefix}launch-phase" { title "Launch Phase" headline "${HLPrefix} Launch Phase" taskroot f20.LaunchPhase } hidetask hidden | isleaf() } taskreport "${content}-${major}-all-tasks" { title "All Tasks" headline "${content_title} ${major} Tasks" columns name, start, end, duration sorttasks tree, actual.start.up, seqno.up hidetask hidden } # key tasks # this report mirrors what should be on the wiki # https://fedoraproject.org/wiki/Releases/14/Schedule taskreport "${content}-${major}-key-tasks" { title "Milestones" headline "${content_title} ${major} Key Tasks & Milestones" columns name, start, end, chart sorttasks actual.start.up hidetask ~key } textreport "Team Reports" { sorttasks tree, actual.start.up columns no, name, start, end, duration timeformat "%a %Y-%m-%d" scenarios actual # Ambassador Team Reports taskreport "${content}-${major}-ambassadors-tasks" { title "Ambassador" headline "${content_title} ${major} Ambassadors Team Tasks" hidetask ~ambassadors } # Design Team Reports taskreport "${content}-${major}-design-tasks.html" { title "Design" headline "${content_title} ${major} Design Team Tasks" hidetask ~design } # Development Team Reports taskreport "${content}-${major}-devel-tasks" { title "Development" headline "${content_title} ${major} Development Team Tasks" hidetask ~devel } # Docs Team Reports taskreport "${content}-${major}-docs-tasks" { title "Docs" headline "${content_title} ${major} Docs Team Tasks" hidetask ~docs } # Marketing Team Reports taskreport "${content}-${major}-marketing-tasks" { title "Marketing" headline "${content_title} ${major} Marketing Team Tasks" hidetask ~marketing } # Release Engineering Team Reports taskreport "${content}-${major}-releng-tasks" { title "Release Engineering" headline "${content_title} ${major} Releng Team Tasks" hidetask ~releng } # Quality Team Reports taskreport "${content}-${major}-quality-tasks" { title "Quality" headline "${content_title} ${major} Quality Tasks" hidetask ~quality } # Spins SIG Reports taskreport "${content}-${major}-spins-tasks" { title "Spins SIG" headline "${content_title} ${major} Spins SIG Tasks" hidetask ~spins } # Translation Team Reports taskreport "${content}-${major}-trans-tasks" { title "Translation" headline "${content_title} ${major} Translation Tasks" hidetask ~translation } # Web Team Reports taskreport "${content}-${major}-web-tasks" { title "Web" headline "${content_title} ${major} Web Team Tasks" hidetask ~web & ~infrastructure } # This report is just a menu entry, not a real report. purge formats } textreport "Administrative Items" { sorttasks tree, actual.start.up columns no, name, start, end, duration timeformat "%a %Y-%m-%d" scenarios actual # Elections taskreport "${content}-${major}-elections" { title "Elections" headline "${content_title} ${major} Elections " hidetask ~elections } # FPL taskreport "${content}-${major}-fpl" { title "FPL" headline "${content_title} ${major} FPL" hidetask ~fpl } # PR taskreport "${content}-${major}-pr" { title "PR" headline "${content_title} ${major} Media/PR" hidetask ~pr } # Infrastructure taskreport "${content}-${major}-infrastructure" { title "Infrastructure" headline "${content_title} ${major} Infrastructure Freezes" hidetask ~infrastructure } purge formats } ### Miscelaneous reports ### textreport "Miscellaneous Reports" { sorttasks actual.start.up columns no, name, start, end, duration taskreport "${content}-${major}-pm-tasks" { title "PM" headline "${content_title} ${major} Project Management Tasks" hidetask ~pm } taskreport "${content}-${major}-blocker-meetings" { title "Blocker" headline "${content_title} ${major} Blocker Meeting" hidetask ~blocker & ~key } taskreport "${content}-${major}-draft-schedule" { title "Primary Tasks" headline "${content_title} ${major} Primary Tasks" sorttasks actual.start.up hidetask ~proto } purge formats } purge formats } textreport "Weekly CSV Reports" { formats csv start ${weekly_start} end ${weekly_end} sorttasks actual.start.up columns start, end, name timeformat "%m/%d/%Y" scenarios actual # Reports for weekly emails to team lists # These reports are post-processed by 'format-weekly-calendar.py' taskreport "${content}-${major}-ambassadors-weekly" { hidetask ~ambassadors } taskreport "${content}-${major}-design-weekly" { hidetask ~design } taskreport "${content}-${major}-devel-weekly" { hidetask ~devel } taskreport "${content}-${major}-docs-weekly" { hidetask ~docs } taskreport "${content}-${major}-fpl-weekly" { hidetask ~fpl } taskreport "${content}-${major}-infrastructure-weekly" { hidetask ~infrastructure } taskreport "${content}-${major}-marketing-weekly" { hidetask ~marketing } taskreport "${content}-${major}-pm-weekly" { hidetask ~pm } taskreport "${content}-${major}-pr-weekly" { hidetask ~pr } taskreport "${content}-${major}-quality-weekly" { hidetask ~quality } taskreport "${content}-${major}-releng-weekly" { hidetask ~releng & ~devel } taskreport "${content}-${major}-spins-weekly" { hidetask ~spins } taskreport "${content}-${major}-trans-weekly" { hidetask ~translation } taskreport "${content}-${major}-web-weekly" { hidetask ~web } purge formats } textreport "Miscellaneous CSV Reports" { formats csv scenarios actual timeformat "%m/%d/%Y" # Text export of entire schedule for outside parsing and analysis # This is required for other teams--DO NOT REMOVE taskreport "${content}-${major}" { columns name, id, note, resources, start, end, duration } # reports for new schedule prototyping and planning # not used once the release gets rolling taskreport "${content}-${major}-plan" { sorttasks actual.start.up columns start, end, name hidetask ~releng & ~quality } taskreport "${content}-${major}-devel" { sorttasks actual.start.up columns start, end, name # TODO: tj3 does not yet support the separator attribute #separator " " hidetask ~devel } purge formats } icalreport "${content}-${major}-all-milestones" { hidetask ~ismilestone(actual) scenario actual } icalreport "${content}-${major}-ambassadors-ics" { hidetask (~ismilestone(actual) | ~ambassadors) scenario actual } icalreport "${content}-${major}-design-ics" { hidetask (~ismilestone(actual) | ~design) scenario actual } icalreport "${content}-${major}-devel-ics" { hidetask (~ismilestone(actual) | ~devel) scenario actual } icalreport "${content}-${major}-docs-ics" { hidetask (~ismilestone(actual) | ~docs) scenario actual } icalreport "${content}-${major}-fpl-ics" { scenario actual hidetask ~fpl } icalreport "${content}-${major}-pr-ics" { hidetask ~pr scenario actual } icalreport "${content}-${major}-key-ics" { hidetask (~ismilestone(actual) | ~key) scenario actual } icalreport "${content}-${major}-marketing-ics" { hidetask (~ismilestone(actual) | ~marketing) scenario actual } icalreport "${content}-${major}-releng-ics" { hidetask (~ismilestone(actual) | ~releng) scenario actual } icalreport "${content}-${major}-quality-ics" { hidetask (~ismilestone(actual) | ~quality) scenario actual } icalreport "${content}-${major}-spins-ics" { hidetask (~ismilestone(actual) | ~spins) scenario actual } icalreport "${content}-${major}-translation-ics" { hidetask (~ismilestone(actual) | ~translation) scenario actual } icalreport "${content}-${major}-web-ics" { hidetask (~ismilestone(actual) | ~web) scenario actual } icalreport "${content}-${major}-pm-ics" { hidetask (~ismilestone(actual) | ~pm) scenario actual } TaskJuggler-3.8.1/examples/ProjectTemplate/000077500000000000000000000000001473026623400206745ustar00rootroot00000000000000TaskJuggler-3.8.1/examples/ProjectTemplate/template.tjp000066400000000000000000000220511473026623400232260ustar00rootroot00000000000000/* * This file contains a project skeleton. It is part of the * TaskJuggler project management tool. You can use this as a basis to * start your own project file. */ project your_project_id "Your Project Title" 2011-11-11-0:00--0500 +4m { # Set the default time zone for the project. If not specified, UTC # is used. timezone "America/New_York" # Hide the clock time. Only show the date. timeformat "%Y-%m-%d" # Use US format for numbers numberformat "-" "" "," "." 1 # Use US financial format for currency values. Don't show cents. currencyformat "(" ")" "," "." 0 # Pick a day during the project that will be reported as 'today' in # the project reports. If not specified, the current day will be # used, but this will likely be outside of the project range, so it # can't be seen in the reports. now 2011-12-24 # The currency for all money values is the Euro. currency "USD" # You can define multiple scenarios here if you need them. #scenario plan "Plan" { # scenario actual "Actual" #} # You can define your own attributes for tasks and resources. This # is handy to capture additional information about the project that # is not directly impacting the project schedule, but which you like to # keep in one place. #extend task { # reference spec "Link to Wiki page" #} #extend resource { # text Phone "Phone" #} } copyright "Claim your rights here" # If you have any text block that you need multiple times to describe # your project, you should define a macro for it. Macros can even have # variable segments that you can set upon calling the macro. # # macro Task [ # task "A ${1} task" { # } # ] # # Can be called as # ${Task "big"} # to generate # task "A big task" { # } # You can attach flags to accounts, resources and tasks. These can be # used to filter out subsets of them during reporting. flags important, hidden # If you want to do budget planning for your project, you need to # define some accounts. account cost "Project Cost" { account dev "Development" account doc "Documentation" } account rev "Customer Payments" # The Profit & Loss analysis should be rev - cost accounts. balance cost rev # Define your public holidays here. vacation "New Year's Day" 2012-01-02 vacation "Birthday of Martin Luther King, Jr." 2012-01-16 vacation "Washington's Birthday" 2012-02-20 vacation "Memorial Day" 2012-05-28 vacation "Independence Day" 2012-07-04 vacation "Labor Day" 2012-09-03 vacation "Columbus Day" 2012-10-08 vacation "Veterans Day" 2012-11-12 vacation "Thanksgiving Day" 2012-11-22 vacation "Christmas Day" 2012-12-25 # The daily default rate of all resources. This can be overridden for each # resource. We specify this so we can do a good calculation of # the costs of the project. rate 400.0 # This is a set of example resources. resource r1 "Resource 1" resource t1 "Team 1" { managers r1 resource r2 "Resource 2" resource r3 "Resource 3" } # This is a resource that does not do any work. resource s1 "System 1" { efficiency 0.0 rate 600.0 } task project "Project" { task wp1 "Workpackage 1" { task t1 "Task 1" task t2 "Task 2" } task wp2 "Work package 2" { depends !wp1 task t1 "Task 1" task t2 "Task 2" } task deliveries "Deliveries" { task "Item 1" { depends !!wp1 } task "Item 2" { depends !!wp2 } } } # Now the project has been specified completely. Stopping here would # result in a valid TaskJuggler file that could be processed and # scheduled. Here reports will be generated to visualize the # results. navigator navbar { hidereport 0 } macro TaskTip [ tooltip istask() -8<- '''Start: ''' <-query attribute='start'-> '''End: ''' <-query attribute='end'-> ---- '''Resources:''' <-query attribute='resources'-> ---- '''Precursors: ''' <-query attribute='precursors'-> ---- '''Followers: ''' <-query attribute='followers'-> ->8- ] textreport frame "" { header -8<- == TaskJuggler Project Template == <[navigator id="navbar"]> ->8- footer "----" textreport index "Overview" { formats html center '<[report id="overview"]>' } textreport "Status" { formats html center -8<- <[report id="status.dashboard"]> ---- <[report id="status.completed"]> ---- <[report id="status.ongoing"]> ---- <[report id="status.future"]> ->8- } textreport wps "Work packages" { textreport wp1 "Work package 1" { formats html center '<[report id="wp1"]>' } textreport wp2 "Work package 2" { formats html center '<[report id="wp2"]>' } } textreport "Deliveries" { formats html center '<[report id="deliveries"]>' } textreport "ContactList" { formats html title "Contact List" center '<[report id="contactList"]>' } textreport "ResourceGraph" { formats html title "Resource Graph" center '<[report id="resourceGraph"]>' } } # A traditional Gantt chart with a project overview. taskreport overview "" { header -8<- === Project Overview === The project is structured into 2 work packages. # Specification # <-reportlink id='frame.wps.wp1'-> # <-reportlink id='frame.wps.wp2'-> # Testing === Original Project Plan === ->8- columns bsi { title 'WBS' }, name, start, end, effort, cost, revenue, chart { ${TaskTip} } # For this report we like to have the abbreviated weekday in front # of the date. %a is the tag for this. timeformat "%a %Y-%m-%d" loadunit days hideresource 1 balance cost rev caption 'All effort values are in man days.' footer -8<- === Staffing === All project phases are properly staffed. See [[ResourceGraph]] for detailed resource allocations. === Current Status === Some blurb about the current situation. ->8- } # Macro to set the background color of a cell according to the alert # level of the task. macro AlertColor [ cellcolor plan.alert = 0 "#90FF90" # green cellcolor plan.alert = 1 "#FFFF90" # yellow cellcolor plan.alert = 2 "#FF9090" # red ] taskreport status "" { columns bsi { width 50 title 'WBS' }, name { width 150 }, start { width 100 }, end { width 100 }, effort { width 100 }, alert { tooltip plan.journal != '' "<-query attribute='journal'->" width 150 }, status { width 150 } taskreport dashboard "" { headline "Project Dashboard (<-query attribute='now'->)" columns name { title "Task" ${AlertColor} width 200}, resources { width 200 ${AlertColor} listtype bullets listitem "<-query attribute='name'->" start ${projectstart} end ${projectend} }, alerttrend { title "Trend" ${AlertColor} width 50 }, journal { width 350 ${AlertColor} } journalmode status_up journalattributes headline, author, date, summary, details hidetask ~hasalert(0) sorttasks alert.down, plan.end.up period %{${now} - 1w} +1w } taskreport completed "" { headline "Already completed tasks" hidetask ~(plan.end <= ${now}) } taskreport ongoing "" { headline "Ongoing tasks" hidetask ~((plan.start <= ${now}) & (plan.end > ${now})) } taskreport future "" { headline "Future tasks" hidetask ~(plan.start > ${now}) } } # A list of tasks showing the resources assigned to each task. taskreport wp1 "" { headline "Work package 1 - Resource Allocation Report" columns bsi { title 'WBS' }, name, start, end, effort { title "Work" }, duration, chart { ${TaskTip} scale day width 500 } timeformat "%Y-%m-%d" hideresource ~(isleaf() & isleaf_()) sortresources name.up taskroot project.wp1 } # A list of tasks showing the resources assigned to each task. taskreport wp2 "" { headline "Work package 2 - Resource Allocation Report" columns bsi { title 'WBS' }, name, start, end, effort { title "Work" }, duration, chart { ${TaskTip} scale day width 500 } timeformat "%Y-%m-%d" hideresource ~(isleaf() & isleaf_()) sortresources name.up taskroot project.wp2 } # A list of all tasks with the percentage completed for each task taskreport deliveries "" { headline "Project Deliverables" columns bsi { title 'WBS' }, name, start, end, note { width 150 }, complete, chart { ${TaskTip} } taskroot project.deliveries hideresource 1 } # A list of all employees with their contact details. resourcereport contactList "" { headline "Contact list and duty plan" columns name, email { celltext 1 "[mailto:<-email-> <-email->]" }, managers { title "Manager" }, chart { scale day } hideresource ~isleaf() sortresources name.up hidetask 1 } # A graph showing resource allocation. It identifies whether each # resource is under- or over-allocated for. resourcereport resourceGraph "" { headline "Resource Allocation Graph" columns no, name, effort, rate, weekly { ${TaskTip} } loadunit shortauto # We only like to show leaf tasks for leaf resources. hidetask ~(isleaf() & isleaf_()) sorttasks plan.start.up } TaskJuggler-3.8.1/examples/Scrum/000077500000000000000000000000001473026623400166635ustar00rootroot00000000000000TaskJuggler-3.8.1/examples/Scrum/Product Burndown.csv000066400000000000000000000006551473026623400226050ustar00rootroot00000000000000"Date";"product:plan.opentasks" "2012-02-01";14 "2012-02-02";14 "2012-02-03";14 "2012-02-06";14 "2012-02-07";14 "2012-02-08";13 "2012-02-09";13 "2012-02-10";13 "2012-02-13";13 "2012-02-14";13 "2012-02-15";13 "2012-02-16";13 "2012-02-17";12 "2012-02-20";12 "2012-02-21";12 "2012-02-22";11 "2012-02-23";11 "2012-02-24";11 "2012-02-27";11 "2012-02-28";10 "2012-02-29";10 "2012-03-01";10 "2012-03-02";9 "2012-03-05";9 "2012-03-06";9 TaskJuggler-3.8.1/examples/Scrum/Sprint 1 Burndown.csv000066400000000000000000000006321473026623400225600ustar00rootroot00000000000000"Date";"product.s1:plan.opentasks" "2012-02-01";4 "2012-02-02";4 "2012-02-03";4 "2012-02-06";4 "2012-02-07";4 "2012-02-08";3 "2012-02-09";3 "2012-02-10";3 "2012-02-13";3 "2012-02-14";3 "2012-02-15";3 "2012-02-16";3 "2012-02-17";2 "2012-02-20";2 "2012-02-21";2 "2012-02-22";1 "2012-02-23";1 "2012-02-24";1 "2012-02-27";1 "2012-02-28";0 "2012-02-29";0 "2012-03-01";0 "2012-03-02";0 "2012-03-05";0 "2012-03-06";0 TaskJuggler-3.8.1/examples/Scrum/Sprint 2 Burndown.csv000066400000000000000000000006321473026623400225610ustar00rootroot00000000000000"Date";"product.s2:plan.opentasks" "2012-02-01";5 "2012-02-02";5 "2012-02-03";5 "2012-02-06";5 "2012-02-07";5 "2012-02-08";5 "2012-02-09";5 "2012-02-10";5 "2012-02-13";5 "2012-02-14";5 "2012-02-15";5 "2012-02-16";5 "2012-02-17";5 "2012-02-20";5 "2012-02-21";5 "2012-02-22";5 "2012-02-23";5 "2012-02-24";5 "2012-02-27";5 "2012-02-28";5 "2012-02-29";5 "2012-03-01";5 "2012-03-02";4 "2012-03-05";4 "2012-03-06";4 TaskJuggler-3.8.1/examples/Scrum/Sprint 3 Burndown.csv000066400000000000000000000006321473026623400225620ustar00rootroot00000000000000"Date";"product.s3:plan.opentasks" "2012-02-01";5 "2012-02-02";5 "2012-02-03";5 "2012-02-06";5 "2012-02-07";5 "2012-02-08";5 "2012-02-09";5 "2012-02-10";5 "2012-02-13";5 "2012-02-14";5 "2012-02-15";5 "2012-02-16";5 "2012-02-17";5 "2012-02-20";5 "2012-02-21";5 "2012-02-22";5 "2012-02-23";5 "2012-02-24";5 "2012-02-27";5 "2012-02-28";5 "2012-02-29";5 "2012-03-01";5 "2012-03-02";5 "2012-03-05";5 "2012-03-06";5 TaskJuggler-3.8.1/examples/Scrum/scrum.tjp000066400000000000000000000047771473026623400205520ustar00rootroot00000000000000project "Scrum Example Project" 2012-02-01 +4m { now 2012-03-07 } resource r1 "R1" resource r2 "R2" resource r3 "R3" # This example uses a very simple WBS that groups tasks by sprint. For # larger projects, a classical WBS that breaks tasks into smaller # tasks and so on is probably more appropriate. The reports can then # select the sprint context by date. task product "Product" { task s1 "Sprint 1" { task t1 "T1" { effort 5d allocate r1 } task t2 "T2" { effort 3d allocate r1 depends !t1 } task t3 "T3" { effort 7d allocate r1 } task t4 "T4" { effort 4d allocate r2 depends !t2 } } task s2 "Sprint 2" { depends !s1 task t1 "T1" { effort 3d allocate r2 } task t2 "T2" { effort 4d allocate r3 depends !t1 } task t3 "T3" { effort 6d allocate r1 } task t4 "T4" { effort 5d allocate r3 depends !t3 } task t5 "T5" { effort 3d allocate r2 depends !t1 } } task s3 "Sprint 3" { depends !s2 task t1 "T1" { effort 6d allocate r1 depends product.s1.t2 } task t2 "T2" { effort 4d allocate r3 depends !t1 } task t3 "T3" { effort 4d allocate r1 depends product.s2.t4 } task t4 "T4" { effort 7d allocate r2 depends !t3 } task t5 "T5" { effort 5d allocate r2 depends !t1 } } } navigator menu textreport "" { header -8<- == Scrum Example Project == <[navigator id='menu']> ->8- formats html textreport "" { columns name, status, effort, resources taskreport "Product Backlog" { } taskreport "Sprint 1 Backlog" { taskroot product.s1 } taskreport "Sprint 2 Backlog" { taskroot product.s2 } taskreport "Sprint 3 Backlog" { taskroot product.s3 } title "Backlogs" purge formats } textreport "" { sorttasks id.up width 800 tracereport "Product Burndown" { columns opentasks hidetask plan.id != "product" } tracereport "Sprint 1 Burndown" { columns opentasks hidetask plan.id != "product.s1" } tracereport "Sprint 2 Burndown" { columns opentasks hidetask plan.id != "product.s2" } tracereport "Sprint 3 Burndown" { columns opentasks hidetask plan.id != "product.s3" } title "Burndown Charts" purge formats } purge formats } TaskJuggler-3.8.1/examples/ToDo-List/000077500000000000000000000000001473026623400173505ustar00rootroot00000000000000TaskJuggler-3.8.1/examples/ToDo-List/todolist.tjp000066400000000000000000000035021473026623400217300ustar00rootroot00000000000000project "My TODO List" 2011-01-01 +5y { # The now date is only set to keep the reports constant. In a real # list you would _not_ set a now date. now 2011-12-20 } task "Errands" { priority 5 task "By some milk" { end 2011-12-13 complete 100 priority 7 } task "Pickup Jacket from dry cleaner" { end 2011-12-18 complete 0 note "Smith Dry Cleaners" } task "Buy present for wife" { end 2011-12-23 note "Have a good idea first" journalentry 2011-12-10 "Maybe a ring?" journalentry 2011-12-14 "Too expensive. Some book?" } } task "Long term projects" { priority 3 task "Buy new car" { end 2011-05-11 complete 100 priority 6 } task "Build boat" { end 2013-04-01 complete 42 } } macro cellcol [ cellcolor (plan.end < ${now}) & (plan.gauge = "behind schedule") "#FF0000" cellcolor plan.gauge = "behind schedule" "#FFFF00" ] navigator navbar textreport frame "" { formats html header -8<- == My ToDo List for ${today} == <[navigator id="navbar"]> ->8- footer "----" columns name, end { title "Due Date" ${cellcol} }, complete, priority, note, journal { celltext 1 "" tooltip 1 "<-query attribute='journal'->" width 70 } taskreport "TODOs due today" { hidetask (plan.complete >= 100) | (plan.end > %{${now} +1d}) journalattributes date, headline, summary, details } taskreport "TODOs due within a week" { hidetask (plan.complete >= 100) | (plan.end > %{${now} +1w}) journalattributes date, headline, summary, details } taskreport "All open TODOs" { hidetask plan.complete >= 100 journalattributes date, headline, summary, details } taskreport "Completed TODOs" { hidetask plan.complete < 100 } purge formats } TaskJuggler-3.8.1/examples/Tutorial/000077500000000000000000000000001473026623400173755ustar00rootroot00000000000000TaskJuggler-3.8.1/examples/Tutorial/tutorial.tjp000066400000000000000000000352341473026623400217660ustar00rootroot00000000000000/* * This file contains an example project. It is part of the * TaskJuggler project management tool. It uses a made up software * development project to demonstrate some of the basic features of * TaskJuggler. Please see the TaskJuggler manual for a more detailed * description of the various syntax elements. */ project acso "Accounting Software" 2002-01-16 +4m { # Set the default time zone for the project. If not specified, UTC # is used. timezone "Europe/Paris" # Hide the clock time. Only show the date. timeformat "%Y-%m-%d" # Use US format for numbers numberformat "-" "" "," "." 1 # Use US financial format for currency values. Don't show cents. currencyformat "(" ")" "," "." 0 # Pick a day during the project that will be reported as 'today' in # the project reports. If not specified, the current day will be # used, but this will likely be outside of the project range, so it # can't be seen in the reports. now 2002-03-05-13:00 # The currency for all money values is the US Dollar. currency "USD" # We want to compare the baseline scenario to one with a slightly # delayed start. scenario plan "Plan" { scenario delayed "Delayed" } extend resource { text Phone "Phone" } } # This is not a real copyright for this file. It's just used as an example. copyright "© 2002 Crappy Software, Inc." # The daily default rate of all resources. This can be overridden for each # resource. We specify this, so that we can do a good calculation of # the costs of the project. rate 390.0 # Register Good Friday as a global holiday for all resources. leaves holiday "Good Friday" 2002-03-29 flags team # This is one way to form teams macro allocate_developers [ allocate dev1 allocate dev2 allocate dev3 ] # In order to do a simple profit and loss analysis of the project we # specify accounts. One for the development costs, one for the # documentation costs, and one account to credit the customer payments # to. account cost "Project Cost" { account dev "Development" account doc "Documentation" } account rev "Payments" # The Profit&Loss analysis should be rev - cost accounts. balance cost rev resource boss "Paul Henry Bullock" { email "phb@crappysoftware.com" Phone "x100" rate 480 } resource dev "Developers" { managers boss resource dev1 "Paul Smith" { email "paul@crappysoftware.com" Phone "x362" rate 350.0 } resource dev2 "Sébastien Bono" { email "SBono@crappysoftware.com" Phone "x234" } resource dev3 "Klaus Müller" { email "Klaus.Mueller@crappysoftware.com" Phone "x490" leaves annual 2002-02-01 - 2002-02-05 } flags team } resource misc "The Others" { managers boss resource test "Peter Murphy" { email "murphy@crappysoftware.com" Phone "x666" limits { dailymax 6.4h } rate 310.0 } resource doc "Dim Sung" { email "sung@crappysoftware.com" Phone "x482" rate 300.0 leaves annual 2002-03-11 - 2002-03-16 } flags team } # Now we specify the work packages. The whole project is described as # a task that contains subtasks. These subtasks are then broken down # into smaller tasks and so on. The innermost tasks describe the real # work and have resources allocated to them. Many attributes of tasks # are inherited from the enclosing task. This saves you a lot of typing. task AcSo "Accounting Software" { # All work-related costs will be booked to this account unless the # subtasks specify something different. chargeset dev # For the duration of the project we have running cost that are not # included in the labor cost. charge 170 perday responsible boss task spec "Specification" { # The effort to finish this task is 20 man-days. effort 20d # Now we use the macro declared above to allocate the resources # for this task. Because they can work in parallel, they may finish this # task earlier than in 20 working-days. ${allocate_developers} # Each task without subtasks must have a start or an end # criterion and a duration. For this task we use a reference to a # milestone defined further below as the start criterion. So this task # can not start before the specified milestone has been reached. # References to other tasks may be relative. Each exclamation mark (!) # means 'in the scope of the enclosing task'. To descent into a task, the # fullstop (.) together with the id of the tasks have to be specified. depends !deliveries.start } task software "Software Development" { # The software is the most critical task of the project. So we set # the priority of this task (and all its subtasks) to 1000, the top # priority. The higher the priority, the more likely the task will # get the requested resources. priority 1000 # All subtasks depend on the specification task. depends !spec responsible dev1 task database "Database coupling" { effort 20d allocate dev1, dev2 journalentry 2002-02-03 "Problems with the SQL Libary" { author dev1 alert yellow summary -8<- We ran into some compatibility problems with the SQL Library. ->8- details -8<- We have already contacted the vendor and are now waiting for their advise. ->8- } } task gui "Graphical User Interface" { effort 35d # This task has taken 5 man-days more than originally planned. # We record this as well, so that we can generate reports that # compare the delayed schedule of the project to the original plan. delayed:effort 40d depends !database, !backend allocate dev2, dev3 # Resource dev2 should only work 6 hours per day on this task. limits { dailymax 6h { resources dev2 } } } task backend "Back-End Functions" { effort 30d # This task is behind schedule, because it should have been # finished already. To document this, we specify that the task # is 95% completed. If nothing is specified, TaskJuggler assumes # that the task is on schedule and computes the completion rate # according to the current day and the plan data. complete 95 depends !database allocate dev1, dev2 } } task test "Software testing" { task alpha "Alpha Test" { # Efforts can not only be specified as man-days, but also as # man-weeks, man-hours, etc. By default, TaskJuggler assumes # that a man-week is 5 man-days or 40 man-hours. These values # can be changed, of course. effort 1w # This task depends on a task in the scope of the enclosing # task's enclosing task. So we need two exclamation marks (!!) # to get there. depends !!software allocate test, dev2 note "Hopefully most bugs will be found and fixed here." journalentry 2002-03-01 "Contract with Peter not yet signed" { author boss alert red summary -8<- The paperwork is stuck with HR and I can't hunt it down. ->8- details -8<- If we don't get the contract closed within the next week, the start of the testing is at risk. ->8- } } task beta "Beta Test" { effort 4w depends !alpha allocate test, dev1 } } task manual "Manual" { effort 10w depends !deliveries.start allocate doc, dev3 purge chargeset chargeset doc journalentry 2002-02-28 "User manual completed" { author boss summary "The doc writers did a really great job to finish on time." } } task deliveries "Milestones" { # Some milestones have customer payments associated with them. We # credit these payments to the 'rev' account. purge chargeset chargeset rev task start "Project start" { # A task that has no duration is a milestone. It only needs a # start or end criterion. All other tasks depend on this task. # Here we use the built-in macro ${projectstart} to align the # start of the task with the above specified project time frame. start ${projectstart} # For some reason the actual start of the project got delayed. # We record this, so that we can compare the planned run to the # delayed run of the project. delayed:start 2002-01-20 # At the beginning of this task we receive a payment from the # customer. This is credited to the account associated with this # task when the task starts. charge 21000.0 onstart } task prev "Technology Preview" { depends !!software.backend charge 31000.0 onstart note "All '''major''' features should be usable." } task beta "Beta version" { depends !!test.alpha charge 13000.0 onstart note "Fully functional, may contain bugs." } task done "Ship Product to Customer" { # The next line can be uncommented to trigger a warning about # the project being late. For all tasks, limits for the start and # end values can be specified. Those limits are checked after the # project has been scheduled. For all violated limits a warning # is issued. # maxend 2002-04-17 depends !!test.beta, !!manual charge 33000.0 onstart note "All priority 1 and 2 bugs must be fixed." } } } # Now the project has been specified completely. Stopping here would # result in a valid TaskJuggler file that could be processed and # scheduled. But no reports would be generated to visualize the # results. navigator navbar { hidereport @none } macro TaskTip [ tooltip istask() -8<- '''Start: ''' <-query attribute='start'-> '''End: ''' <-query attribute='end'-> ---- '''Resources:''' <-query attribute='resources'-> ---- '''Precursors: ''' <-query attribute='precursors'-> ---- '''Followers: ''' <-query attribute='followers'-> ->8- ] textreport frame "" { header -8<- == Accounting Software Project == <[navigator id="navbar"]> ->8- footer "----" textreport index "Overview" { formats html center '<[report id="overview"]>' } textreport "Status" { formats html center -8<- <[report id="status.dashboard"]> ---- <[report id="status.completed"]> ---- <[report id="status.ongoing"]> ---- <[report id="status.future"]> ->8- } textreport development "Development" { formats html center '<[report id="development"]>' } textreport "Deliveries" { formats html center '<[report id="deliveries"]>' } textreport "ContactList" { formats html title "Contact List" center '<[report id="contactList"]>' } textreport "ResourceGraph" { formats html title "Resource Graph" center '<[report id="resourceGraph"]>' } } # A traditional Gantt chart with a project overview. taskreport overview "" { header -8<- === Project Overview === The project is structured into 3 phases. # Specification # <-reportlink id='frame.development'-> # Testing === Original Project Plan === ->8- columns bsi { title 'WBS' }, name, start, end, effort, cost, revenue, chart { ${TaskTip} } # For this report we like to have the abbreviated weekday in front # of the date. %a is the tag for this. timeformat "%a %Y-%m-%d" loadunit days hideresource @all balance cost rev caption 'All effort values are in man days.' footer -8<- === Staffing === All project phases are properly staffed. See [[ResourceGraph]] for detailed resource allocations. === Current Status === The project started off with a delay of 4 days. This slightly affected the original schedule. See [[Deliveries]] for the impact on the delivery dates. ->8- } # Macro to set the background color of a cell according to the alert # level of the task. macro AlertColor [ cellcolor plan.alert = 0 "#90FF90" # green cellcolor plan.alert = 1 "#FFFF90" # yellow cellcolor plan.alert = 2 "#FF9090" # red ] taskreport status "" { columns bsi { width 50 title 'WBS' }, name { width 150 }, start { width 100 }, end { width 100 }, effort { width 100 }, alert { tooltip plan.journal != '' "<-query attribute='journal'->" width 150 }, status { width 150 } scenarios delayed taskreport dashboard "" { headline "Project Dashboard (<-query attribute='now'->)" columns name { title "Task" ${AlertColor} width 200}, resources { width 200 ${AlertColor} listtype bullets listitem "<-query attribute='name'->" start ${projectstart} end ${projectend} }, alerttrend { title "Trend" ${AlertColor} width 50 }, journal { width 350 ${AlertColor} } journalmode status_up journalattributes headline, author, date, summary, details hidetask ~hasalert(0) sorttasks alert.down, delayed.end.up period %{${now} - 1w} +1w } taskreport completed "" { headline "Already completed tasks" hidetask ~(delayed.end <= ${now}) } taskreport ongoing "" { headline "Ongoing tasks" hidetask ~((delayed.start <= ${now}) & (delayed.end > ${now})) } taskreport future "" { headline "Future tasks" hidetask ~(delayed.start > ${now}) } } # A list of tasks showing the resources assigned to each task. taskreport development "" { scenarios delayed headline "Development - Resource Allocation Report" columns bsi { title 'WBS' }, name, start, end, effort { title "Work" }, duration, chart { ${TaskTip} scale day width 500 } timeformat "%Y-%m-%d" hideresource ~(isleaf() & isleaf_()) sortresources name.up } # A list of all tasks with the percentage completed for each task taskreport deliveries "" { headline "Project Deliverables" columns bsi { title 'WBS' }, name, start, end, note { width 150 }, complete, chart { ${TaskTip} } taskroot AcSo.deliveries hideresource @all scenarios plan, delayed } # A list of all employees with their contact details. resourcereport contactList "" { scenarios delayed headline "Contact list and duty plan" columns name, email { celltext 1 "[mailto:<-email-> <-email->]" }, Phone, managers { title "Manager" }, chart { scale day } hideresource ~isleaf() sortresources name.up hidetask @all } # A graph showing resource allocation. It identifies whether each # resource is under- or over-allocated for. resourcereport resourceGraph "" { scenarios delayed headline "Resource Allocation Graph" columns no, name, effort, rate, weekly { ${TaskTip} } loadunit shortauto # We only like to show leaf tasks for leaf resources. hidetask ~(isleaf() & isleaf_()) sorttasks plan.start.up } TaskJuggler-3.8.1/h2m/000077500000000000000000000000001473026623400144425ustar00rootroot00000000000000TaskJuggler-3.8.1/h2m/tj3.h2m000066400000000000000000000012501473026623400155500ustar00rootroot00000000000000[NAME] tj3 \- schedules tj3 projects and generates reports [ENVIRONMENT] .TP \fBTASKJUGGLER_DATA_PATH\fR Override the path to the TaskJuggler data folder. The data folder contains the css, icons and scripts for TaskJuggler reports. .TP \fBTZ\fR The POSIX Time Zone environment variable. Any environment variable may be used in a TaskJuggler file using the expression $(VAR_NAME). [EXAMPLES] tj3 tutorial.tjp [SEE ALSO] tj3client(1), tj3d(1), tj3man(1), tj3ss_receiver(1), tj3ss_sender(1), tj3ts_receiver(1), tj3ts_sender(1), tj3ts_summary(1), tj3webd(1) The full TaskJuggler manual is available online at http://www.taskjuggler.org/tj3/manual/, or via the tj3man command. TaskJuggler-3.8.1/h2m/tj3client.h2m000066400000000000000000000020611473026623400167500ustar00rootroot00000000000000[NAME] tj3client \- send commands and data to the TaskJuggler daemon [ENVIRONMENT] .TP \fBHOME\fR The user's home folder. Used to search for configuration file if not specified. [FILES] .TP \fB.taskjugglerrc\fR or \fBtaskjuggler.rc\fR tj3d searches for a config file named .taskjugglerrc or taskjuggler.rc in the current path, the user's home path as specified by the HOME environment variable or /etc/. At a minimum the file must contain: _global: authKey: ******** (the user should specify their own auth key and set file permissions accordingly). An alternative config file location may be specified using the -c, --config FILE option. [EXAMPLES] .TP Load a project: tj3client add yourproject.tjp .PP .TP List available reports for a project: tj3client list-reports .PP .TP Generate a report: tj3client report .PP .TP Terminate a running instance of the server: tj3client terminate [SEE ALSO] tj3d(1) The full TaskJuggler manual is available online at http://www.taskjuggler.org/tj3/manual/, or via the tj3man command. TaskJuggler-3.8.1/h2m/tj3d.h2m000066400000000000000000000020431473026623400157150ustar00rootroot00000000000000[NAME] tjd3 \- the TaskJuggler daemon [ENVIRONMENT] .TP \fBHOME\fR The user's home folder. Used to search for configuration file if not specified. [FILES] .TP \fB.taskjugglerrc\fR or \fBtaskjuggler.rc\fR tj3d searches for a config file named .taskjugglerrc or taskjuggler.rc in the current path, the user's home path as specified by the HOME environment variable or /etc/. At a minimum the file must contain: _global: authKey: ******** (the user should specify their own auth key and set file permissions accordingly). An alternative config file location may be specified using the -c, --config FILE option. .TP \fBtj3d.log\fR The tj3d log file, created in the working directory. Location can be overridden using the --logfile FILE option. [SECURITY] The author advises: "the daemon has not received any kind of security review ... only use the daemon in a trusted environment with only trusted users!" [SEE ALSO] tj3client(1) The full TaskJuggler manual is available online at http://www.taskjuggler.org/tj3/manual/, or via the tj3man command. TaskJuggler-3.8.1/h2m/tj3man.h2m000066400000000000000000000005551473026623400162530ustar00rootroot00000000000000[NAME] tj3man \- get documentation for TaskJuggler keywords or generate user manual [ENVIRONMENT] .TP \fBBROWSER\fR The web browser used to display the user manual as HTML. [EXAMPLES] tj3man shift .br tj3man --html shift.timesheet .br tj3man --html [SEE ALSO] tj3(1) The TaskJuggler manual is also available online at http://www.taskjuggler.org/tj3/manual/. TaskJuggler-3.8.1/h2m/tj3ss_receiver.h2m000066400000000000000000000022651473026623400200110ustar00rootroot00000000000000[NAME] tj3ss_receiver \- receive filled-out status sheets via email [ENVIRONMENT] .TP \fBHOME\fR The user's home folder. Used to search for configuration file if not specified. [FILES] .TP \fB.taskjugglerrc\fR or \fBtaskjuggler.rc\fR tj3d searches for a config file named .taskjugglerrc or taskjuggler.rc in the current path, the user's home path as specified by the HOME environment variable or /etc/. At a minimum the file must configure an e-mail delivery method and sender e-mail e.g.: _global: emailDeliveryMethod: smtp smtpServer: smtp.your_company.com .br _statussheets: senderEmail: 'TaskJuggler ' An alternative config file location may be specified using the -c, --config FILE option. .TP \fBstatussheets.log\fR The statussheets log file, created in the working directory. .TP \fBStatusSheets/FailedMails/\fR Directory created in the working directory to store the failed emails. .TP \fBStatusSheets/FailedSheets/\fR Directory created in the working directory to store the failed status sheets. [SEE ALSO] tj3ss_sender(1) The full TaskJuggler manual is available online at http://www.taskjuggler.org/tj3/manual/, or via the tj3man command. TaskJuggler-3.8.1/h2m/tj3ss_sender.h2m000066400000000000000000000022471473026623400174650ustar00rootroot00000000000000[NAME] tj3ss_sender \- send out status sheets templates via email [ENVIRONMENT] .TP \fBHOME\fR The user's home folder. Used to search for configuration file if not specified. [FILES] \fB.taskjugglerrc\fR or \fBtaskjuggler.rc\fR tj3d searches for a config file named .taskjugglerrc or taskjuggler.rc in the current path, the user's home path as specified by the HOME environment variable or /etc/. At a minimum the file must configure an authentication key, an e-mail delivery method and sender e-mail e.g.: _global: authKey: ******** smtpServer: smtp.your_company.com .br _statussheets: senderEmail: 'TaskJuggler ' (the user should specify their own auth key and set file permissions accordingly). An alternative config file location may be specified using the -c, --config FILE option. .TP \fBstatussheets.log\fR The statussheets log file, created in the working directory. .TP \fBStatusSheetTemplates\fR Base directory of the sheet templates, created in the working directory. [SEE ALSO] tj3ss_receiver(1), tj3d(1) The full TaskJuggler manual is available online at http://www.taskjuggler.org/tj3/manual/, or via the tj3man command. TaskJuggler-3.8.1/h2m/tj3ts_receiver.h2m000066400000000000000000000024101473026623400200020ustar00rootroot00000000000000[NAME] tj3ts_receiver \- receive filled-out time sheets via email [ENVIRONMENT] .TP \fBHOME\fR The user's home folder. Used to search for configuration file if not specified. [FILES] .TP \fB.taskjugglerrc\fR or \fBtaskjuggler.rc\fR tj3d searches for a config file named .taskjugglerrc or taskjuggler.rc in the current path, the user's home path as specified by the HOME environment variable or /etc/. At a minimum the file must configure an authentication key, an e-mail delivery method and sender e-mail e.g.: _global: authKey: ******** smtpServer: smtp.your_company.com _timesheets: senderEmail: 'TaskJuggler ' (the user should specify their own auth key and set file permissions accordingly). An alternative config file location may be specified using the -c, --config FILE option. .TP \fBtimesheets.log\fR The statussheets log file, created in the working directory. .TP \fBTimeSheets/FailedMails/\fR Directory created in the working directory to store the failed emails. .TP \fBTimeSheets/FailedSheets/\fR Directory created in the working directory to store the failed status sheets. [SEE ALSO] tj3ts_sender(1) The full TaskJuggler manual is available online at http://www.taskjuggler.org/tj3/manual/, or via the tj3man command. TaskJuggler-3.8.1/h2m/tj3ts_sender.h2m000066400000000000000000000022451473026623400174640ustar00rootroot00000000000000[NAME] tj3ts_sender \- send out time sheets templates via email [ENVIRONMENT] .TP \fBHOME\fR The user's home folder. Used to search for configuration file if not specified. [FILES] \fB.taskjugglerrc\fR or \fBtaskjuggler.rc\fR tj3d searches for a config file named .taskjugglerrc or taskjuggler.rc in the current path, the user's home path as specified by the HOME environment variable or /etc/. At a minimum the file must configure an authentication key, an e-mail delivery method and sender e-mail e.g.: _global: authKey: ******** smtpServer: smtp.your_company.com projectId: acso .br _timesheets: senderEmail: 'TaskJuggler ' (the user should specify their own auth key and set file permissions accordingly). An alternative config file location may be specified using the -c, --config FILE option. .TP \fBtimesheets.log\fR The timesheets log file, created in the working directory. .TP \fBTimeSheetTemplates\fR Base directory of the sheet templates, created in the working directory. [SEE ALSO] tj3ts_receiver(1) The full TaskJuggler manual is available online at http://www.taskjuggler.org/tj3/manual/, or via the tj3man command. TaskJuggler-3.8.1/h2m/tj3ts_summary.h2m000066400000000000000000000024511473026623400177000ustar00rootroot00000000000000[NAME] tj3ts_summary \- send out individual copies and a summary of accepted time sheets [ENVIRONMENT] .TP \fBHOME\fR The user's home folder. Used to search for configuration file if not specified. [FILES] \fB.taskjugglerrc\fR or \fBtaskjuggler.rc\fR tj3d searches for a config file named .taskjugglerrc or taskjuggler.rc in the current path, the user's home path as specified by the HOME environment variable or /etc/. At a minimum the file must configure an authentication key, an e-mail delivery method, project id, sender e-mail and receipients e.g.: _global: authKey: ******** smtpServer: smtp.your_company.com projectId: acso .br _timesheets: senderEmail: 'TaskJuggler ' _summary: sheetRecipients: - team@your_company.com (the user should specify their own auth key and set file permissions accordingly). An alternative config file location may be specified using the -c, --config FILE option. .TP \fBtimesheets.log\fR The timesheets log file, created in the working directory. .TP \fBTimeSheetTemplates\fR Base directory of the sheet templates, created in the working directory. [SEE ALSO] tj3ts_receiver(1) tj3ts_sender(1) The full TaskJuggler manual is available online at http://www.taskjuggler.org/tj3/manual/, or via the tj3man command. TaskJuggler-3.8.1/h2m/tj3webd.h2m000066400000000000000000000013761473026623400164230ustar00rootroot00000000000000[NAME] tj3webd \- TaskJuggler reports web server [ENVIRONMENT] .TP \fBHOME\fR The user's home folder. Used to search for configuration file if not specified. [FILES] .TP \fB.taskjugglerrc\fR or \fBtaskjuggler.rc\fR tj3d searches for a config file named .taskjugglerrc or taskjuggler.rc in the current path, the user's home path as specified by the HOME environment variable or /etc/. At a minimum the file must contain: _global: authKey: ******** (the user should specify their own auth key and set file permissions accordingly). An alternative config file location may be specified using the -c, --config FILE option. [SEE ALSO] tj3d(1) The full TaskJuggler manual is available online at http://www.taskjuggler.org/tj3/manual/, or via the tj3man command. TaskJuggler-3.8.1/lib/000077500000000000000000000000001473026623400145225ustar00rootroot00000000000000TaskJuggler-3.8.1/lib/header.tmpl000066400000000000000000000006471473026623400166570ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = $FILE -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # TaskJuggler-3.8.1/lib/taskjuggler/000077500000000000000000000000001473026623400170445ustar00rootroot00000000000000TaskJuggler-3.8.1/lib/taskjuggler/Account.rb000066400000000000000000000030051473026623400207630ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = Account.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/PropertyTreeNode' require 'taskjuggler/AccountScenario' class TaskJuggler # An Account is an object to record financial transactions. Alternatively, an # Account can just be a container for a set of Accounts. In this case it # cannot directly record any transactions. class Account < PropertyTreeNode def initialize(project, id, name, parent) super(project.accounts, id, name, parent) project.addAccount(self) @data = Array.new(@project.scenarioCount, nil) @project.scenarioCount.times do |i| AccountScenario.new(self, i, @scenarioAttributes[i]) end end # Many Account functions are scenario specific. These functions are # provided by the class AccountScenario. In case we can't find a # function called for the Account class we try to find it in # AccountScenario. def method_missing(func, scenarioIdx, *args) @data[scenarioIdx].method(func).call(*args) end # Return a reference to the _scenarioIdx_-th scenario. def scenario(scenarioIdx) return @data[scenarioIdx] end end end TaskJuggler-3.8.1/lib/taskjuggler/AccountCredit.rb000066400000000000000000000012301473026623400221140ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = AccountCredit.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # class TaskJuggler class AccountCredit attr_reader :date, :description, :amount def initialize(date, description, amount) @date = date @description = description @amount = amount end end end TaskJuggler-3.8.1/lib/taskjuggler/AccountScenario.rb000066400000000000000000000066031473026623400224560ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = AccountScenario.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/ScenarioData' class TaskJuggler # This class handles the scenario specific features of a Account object. class AccountScenario < ScenarioData def initialize(account, scenarioIdx, attributes) super %w( credits ).each do |attr| @property[attr, @scenarioIdx] end end def query_balance(query) # The account balance is the turnover from project start (index 0) to # the start of the query period. It's the start because that's what the # label in the column header says. startIdx = 0 endIdx = @project.dateToIdx(query.start) query.sortable = query.numerical = amount = turnover(startIdx, endIdx) query.string = query.currencyFormat.format(amount) end def query_turnover(query) startIdx = @project.dateToIdx(query.start) endIdx = @project.dateToIdx(query.end) query.sortable = query.numerical = amount = turnover(startIdx, endIdx) query.string = query.currencyFormat.format(amount) end private # Compute the turnover for the period between _startIdx_ end _endIdx_. # TODO: This method is horribly inefficient! def turnover(startIdx, endIdx) amount = 0.0 # Accumulate the amounts that were directly credited to the account # during the given interval. unless @credits.empty? # For this, we need the real dates again. Conver the indices back to # dates. startDate = @project.idxToDate(startIdx) endDate = @project.idxToDate(endIdx) @credits.each do |credit| if startDate <= credit.date && credit.date < endDate amount += credit.amount end end end if @property.container? if @property.adoptees.empty? # Normal case. Accumulate turnover of child accounts. @property.children.each do |child| amount += child.turnover(@scenarioIdx, startIdx, endIdx) end else # Special case for meta account that is used to calculate a balance. # The first adoptee is the top-level cost account, the second the # top-level revenue account. amount += -@property.adoptees[0].turnover(@scenarioIdx, startIdx, endIdx) + @property.adoptees[1].turnover(@scenarioIdx, startIdx, endIdx) end else case @property.get('aggregate') when :tasks @project.tasks.each do |task| amount += task.turnover(@scenarioIdx, startIdx, endIdx, @property, nil, false) end when :resources @project.resources.each do |resource| next unless resource.leaf? amount += resource.turnover(@scenarioIdx, startIdx, endIdx, @property, nil, false) end else raise "Unknown aggregation type #{@property.get('aggregate')}" end end amount end end end TaskJuggler-3.8.1/lib/taskjuggler/AlertLevelDefinitions.rb000066400000000000000000000057221473026623400236320ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = AlertLevelDefinitions.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # class TaskJuggler # This class holds all information to describe a alert level as used by # TaskJuggler. A level has a unique ID, a unique name and a unique color. # Colors are stored as HTML compatible strings, e. g. "#RGB" where R, G, B # are a single or two-digit hex value. class AlertLevelDefinition < Struct.new(:id, :name, :color) def to_s "#{id} '#{name}' '#{color}'" end end # This class holds a list of AlertLevelDefinition objects. There are 3 # default levels. If they are changed, the :modified flag will indicate # this. class AlertLevelDefinitions def initialize # By default, we have a green, a yellow and a red level defined. @levels = [] add(AlertLevelDefinition.new('green', 'Green', '#008000')) add(AlertLevelDefinition.new('yellow', 'Yellow', '#BEA800')) add(AlertLevelDefinition.new('red', 'Red', '#C00000')) # Since those are the default values, we reset the modified flag. @modified = false end # Remove all AlertLevelDefinition objects from the list. def clear @levels = [] @modified = true end # Add a new AlertLevelDefinition. def add(level) raise ArgumentError unless level.is_a?(AlertLevelDefinition) if indexById(level.id) || indexByName(level.name) raise ArgumentError, "ID and name must be unique" end @levels << level @modified = true end # Return true if the alert levels are no longer the default ones, # otherwise return false. def modified? @modified end # Try to match _id_ to a defined alert level ID and return the # index of it. If no level is found, nil is returned. def indexById(id) @levels.index { |level| id == level.id } end # Try to match _name_ to a defined alert level ID and return the # index of it. If no level is found, nil is returned. def indexByName(name) @levels.index { |level| name == level.name } end # Try to match _color_ to a defined alert level ID and return the # index of it. If no level is found, nil is returned. def indexByColor(color) @levels.index { |level| color == level.color } end # Return the AlertLevelDefinition at _index_ or nil if _index_ is out of # range. def [](index) @levels[index] end # Pass map call to @levels. def map(&block) @levels.map(&block) end # Return the definition of the alert levels in TJP syntax. def to_tjp "alertlevels #{@levels.join(",\n")}" end end end TaskJuggler-3.8.1/lib/taskjuggler/AlgorithmDiff.rb000066400000000000000000000160251473026623400221140ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = AlgorithmDiff.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # # This class is an implementation of the classic UNIX diff functionality. It's # based on an original implementation by Lars Christensen, which based his # version on the Perl Algorithm::Diff implementation. This is largely a # from-scratch implementation that tries to have a less intrusive and more # user-friendly interface. But some code fragments are very similar to the # original and are copyright (C) 2001 Lars Christensen. class Diff # A Hunk stores all information about a contiguous change of the destination # list. It stores the inserted and deleted values as well as their positions # in the A and B list. class Hunk attr_reader :insertValues, :deleteValues attr_accessor :aIdx, :bIdx # Create a new Hunk. _aIdx_ is the index in the A list. _bIdx_ is the # index in the B list. def initialize(aIdx, bIdx) @aIdx = aIdx # A list of values to be deleted from the A list starting at aIdx. @deleteValues = [] @bIdx = bIdx # A list of values to be inserted into the B list at bIdx. @insertValues = [] end # Has the Hunk any values to insert? def insert? !@insertValues.empty? end # Has the Hunk any values to be deleted? def delete? !@deleteValues.empty? end def to_s str = '' showSeparator = false if insert? && delete? str << "#{aRange}c#{bRange}\n" showSeparator = true elsif insert? str << "#{aIdx}a#{bRange}\n" else str << "#{aRange}d#{bIdx}\n" end @deleteValues.each { |value| str << "< #{value}\n" } str << "---\n" if showSeparator @insertValues.each { |value| str << "> #{value}\n" } str end def inspect puts to_s end private def aRange range(@aIdx + 1, @aIdx + @deleteValues.length) end def bRange range(@bIdx + 1, @bIdx + @insertValues.length) end def range(startIdx, endIdx) if (startIdx == endIdx) "#{startIdx}" else "#{startIdx},#{endIdx}" end end end # Create a new Diff between the _a_ list and _b_ list. def initialize(a, b) @hunks = [] diff(a, b) end # Modify the _values_ list according to the stored diff information. def patch(values) res = values.dup @hunks.each do |hunk| if hunk.delete? res.slice!(hunk.bIdx, hunk.deleteValues.length) end if hunk.insert? res.insert(hunk.bIdx, *hunk.insertValues) end end res end def editScript script = [] @hunks.each do |hunk| if hunk.delete? script << "#{hunk.aIdx + 1}d#{hunk.deleteValues.length}" end if hunk.insert? script << "#{hunk.bIdx + 1}i#{hunk.insertValues.join(',')}" end end script end # Return the diff list as standard UNIX diff output. def to_s str = '' @hunks.each { |hunk| str << hunk.to_s } str end def inspect puts to_s end private def diff(a, b) indexTranslationTable = computeIndexTranslations(a, b) ai = bi = 0 tableLength = indexTranslationTable.length while ai < tableLength do # Check if value from index ai should be included in B. destIndex = indexTranslationTable[ai] if destIndex # Yes, it needs to go to position destIndex. All values from bi to # newIndex - 1 are new values in B, not in A. while bi < destIndex insertElement(ai, bi, b[bi]) bi += 1 end bi += 1 else # No, it's not in B. Put it onto the deletion list. deleteElement(ai, bi, a[ai]) end ai += 1 end # The remainder of the A list has to be deleted. while ai < a.length deleteElement(ai, bi, a[ai]) ai += 1 end # The remainder of the B list are new values. while bi < b.length insertElement(ai, bi, b[bi]) bi += 1 end end def computeIndexTranslations(a, b) aEndIdx = a.length - 1 bEndIdx = b.length - 1 startIdx = 0 indexTranslationTable = [] while (startIdx < aEndIdx && startIdx < bEndIdx && a[startIdx] == b[startIdx]) indexTranslationTable[startIdx] = startIdx startIdx += 1 end while (aEndIdx >= startIdx && bEndIdx >= startIdx && a[aEndIdx] == b[bEndIdx]) indexTranslationTable[aEndIdx] = bEndIdx aEndIdx -= 1 bEndIdx -= 1 end return indexTranslationTable if startIdx >= aEndIdx && startIdx >= bEndIdx links = [] thresholds = [] bHashesToIndicies = reverseHash(b, startIdx, bEndIdx) startIdx.upto(aEndIdx) do |ai| aValue = a[ai] next unless bHashesToIndicies.has_key? aValue k = nil bHashesToIndicies[aValue].each do |bi| if k && (thresholds[k] > bi) && (thresholds[k - 1] < bi) thresholds[k] = bi else k = replaceNextLarger(thresholds, bi, k) end links[k] = [ k == 0 ? nil : links[k - 1], ai, bi ] if k end end if !thresholds.empty? link = links[thresholds.length - 1] while link indexTranslationTable[link[1]] = link[2] link = link[0] end end return indexTranslationTable end def reverseHash(values, startIdx, endIdx) hash = {} startIdx.upto(endIdx) do |i| element = values[i] if hash.has_key?(element) hash[element].insert(0, i) else hash[element] = [ i ] end end hash end def replaceNextLarger(ary, value, high = nil) high ||= ary.length if ary.empty? || value > ary[-1] ary.push value return high end low = 0 while low < high index = (high + low) / 2 found = ary[index] return nil if value == found if value > found low = index + 1 else high = index end end ary[low] = value low end def deleteElement(aIdx, bIdx, value) if @hunks.empty? || @hunks.last.aIdx + @hunks.last.deleteValues.length != aIdx @hunks << (hunk = Hunk.new(aIdx, bIdx)) else hunk = @hunks.last end hunk.deleteValues << value end def insertElement(aIdx, bIdx, value) if @hunks.empty? || @hunks.last.bIdx + @hunks.last.insertValues.length != bIdx @hunks << (hunk = Hunk.new(aIdx, bIdx)) else hunk = @hunks.last end hunk.insertValues << value end end module Diffable def diff(b) Diff.new(self, b) end def patch(diff) diff.patch(self) end end module DiffableString def diff(b) split("\n").extend(Diffable).diff(b.split("\n")) end def patch(hunks) split("\n").extend(Diffable).patch(hunks).join("\n") + "\n" end end TaskJuggler-3.8.1/lib/taskjuggler/Allocation.rb000066400000000000000000000113751473026623400214650ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = Allocation.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/Resource' require 'taskjuggler/Shift' class TaskJuggler # The Allocation is key object in TaskJuggler. It contains a description how # Resources are assigned to a Task. Each allocation holds a non-empty list of # candidate resources. For each time slot one candidate will be assigned if # any are available. A selectionMode controls the order in which the resources # are checked for availability. The first available one is selected. class Allocation attr_reader :selectionMode attr_accessor :atomic, :persistent, :mandatory, :shifts, :lockedResource # Create an Allocation object. The _candidates_ list must at least contain # one Resource reference. def initialize(candidates, selectionMode = 1, persistent = false, mandatory = false, atomic = false) @candidates = candidates # The selection mode determines how the candidate is selected from the # list of candidates. # 0 : 'order' : select by order of list # 1 : 'minallocated' : select candidate with lowest allocation # probability # 2 : 'minloaded' : select candidate with lowest allocated overall # load # 3 : 'maxloaded' : select candidate with highest allocated overall # load # 4 : 'random' : select a random candidate @selectionMode = selectionMode @atomic = atomic @persistent = persistent @mandatory = mandatory @shifts = nil @staticCandidates = nil end # Set the selection mode identified by name specified in _str_. For # efficiency reasons, we turn the name into an Integer value. def setSelectionMode(str) modes = %w( order minallocated minloaded maxloaded random ) @selectionMode = modes.index(str) raise "Unknown selection mode #{str}" if @selectionMode.nil? end # Append another candidate to the candidates list. def addCandidate(candidate) @candidates << candidate end # Returns true if we either have no shifts defined or the defined shifts # are active at date specified by global scoreboard index _sbIdx_. def onShift?(sbIdx) return @shifts.onShift?(sbIdx) if @shifts true end # Return the candidate list sorted according to the selectionMode. def candidates(scenarioIdx = nil) # In case we have selection criteria that results in a static list, we # can use the previously determined list. return @staticCandidates if @staticCandidates if scenarioIdx.nil? || @selectionMode == 0 # declaration order return @candidates end if @selectionMode == 4 # random # For a random sorting we put the candidates in a hash with a random # number as key. Then we sort the hash according to the random keys an # use the resuling sequence of the values. hash = {} @candidates.each { |c| hash[rand] = c } twinList = hash.sort { |x, y| x[0] <=> y[0] } list = [] twinList.each { |k, v| list << v } return list end list = @candidates.sort do |x, y| case @selectionMode when 1 # lowest alloc probability if @persistent # For persistent resources we use a more sophisticated heuristic # than just the criticalness of the resource. Instead, we # look at the already allocated slots of the resource. This will # reduce the probability to pick a persistent resource that was # already allocated for a higher priority or more critical task. if (cmp = x.bookedEffort(scenarioIdx) <=> y.bookedEffort(scenarioIdx)) == 0 x['criticalness', scenarioIdx] <=> y['criticalness', scenarioIdx] else cmp end else x['criticalness', scenarioIdx] <=> y['criticalness', scenarioIdx] end when 2 # lowest allocated load x.bookedEffort(scenarioIdx) <=> y.bookedEffort(scenarioIdx) when 3 # hightes allocated load y.bookedEffort(scenarioIdx) <=> x.bookedEffort(scenarioIdx) else raise "Unknown selection mode #{@selectionMode}" end end @staticCandidates = list if @selectionMode == 1 && !@persistent list end end end TaskJuggler-3.8.1/lib/taskjuggler/AppConfig.rb000066400000000000000000000070051473026623400212410ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = AppConfig.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'rbconfig' # This class provides central management of configuration data to an # application. It stores the version number, the name of the application and # the suite it belongs to. It also holds copyright and license information. # These infos have to be set in the main module of the application right after # launch. Then, all other modules can retrieve them from the global instance # as needed. class AppConfig def initialize @@version = '0.0.0' @@packageName = 'unnamed' @@softwareName = 'unnamed' @@packageInfo = 'no info' @@appName = 'unnamed' @@authors = [] @@copyright = [] @@contact = 'not specified' @@license = 'no license' end def AppConfig.version=(version) @@version = version end def AppConfig.version @@version end def AppConfig.packageName=(name) @@packageName = name end def AppConfig.packageName @@packageName end def AppConfig.softwareName=(name) @@softwareName = name end def AppConfig.softwareName @@softwareName end def AppConfig.packageInfo=(info) @@packageInfo = info end def AppConfig.packageInfo @@packageInfo end def AppConfig.appName=(name) @@appName = name end def AppConfig.appName @@appName end def AppConfig.authors=(authors) @@authors = authors end def AppConfig.authors @@authors end def AppConfig.copyright=(copyright) @@copyright = copyright end def AppConfig.copyright @@copyright end def AppConfig.contact=(contact) @@contact = contact end def AppConfig.contact @@contact end def AppConfig.license=(license) @@license = license end def AppConfig.license @@license end def AppConfig.dataDirs(baseDir = 'data') dirs = dataSearchDirs(baseDir) # Remove non-existing directories from the list again dirs.delete_if do |dir| !File.exist?(dir) end dirs end def AppConfig.dataSearchDirs(baseDir = 'data') rubyLibDir = RbConfig::CONFIG['rubylibdir'] rubyBaseDir, versionDir = rubyLibDir.scan(/(.*\/)(.*)/)[0] dirs = [] if ENV['TASKJUGGLER_DATA_PATH'] ENV['TASKJUGGLER_DATA_PATH'].split(':').each do |path| dirs << path + "/#{baseDir}/" end end # Find the data dir relative to the source of this file. This should # always work. dirs << File.join(File.dirname(__FILE__), '..', '..', baseDir) # This hopefully works for all setups. Otherwise we have to add more # alternative pathes. # This one is for RPM based distros like Novell dirs << rubyBaseDir + "gems/" + versionDir + '/gems/' \ + @@packageName + '-' + @@version + "/#{baseDir}/" # This one is for Debian based distros dirs << rubyLibDir + '/gems/' \ + @@packageName + '-' + @@version + "/#{baseDir}/" dirs end def AppConfig.dataFiles(fileName) files = [] dirs = dataDirs dirs.each { |d| files << d + fileName if File.exist?(d + fileName) } files end def AppConfig.dataFile(fileName) dirs = dataDirs dirs.each { |d| return d + fileName if File.exist?(d + fileName) } nil end end TaskJuggler-3.8.1/lib/taskjuggler/AttributeBase.rb000066400000000000000000000125121473026623400221300ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = AttributeBase.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2019 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/deep_copy' class TaskJuggler # This class is the base for all property attribute types. Each property can # have multiple attributes of different type. For each type, there must be a # special Ruby class. Each of these classes must be derived from this class. # The class holds information like a reference to the property that owns the # attribute and the type of the attribute. # # The class can track wheter the attribute value was provided by the project # file, inherited from another property or computed during scheduling. # # Attributes that are of an inherited type will be copied from a parent # property or the global scope. class AttributeBase attr_reader :property, :type, :provided, :inherited # The mode is flag that controls how value assignments affect the flags. @@mode = 0 # Create a new AttributeBase object. _type_ specifies the specific type of # the object. _property_ is the PropertyTreeNode object this attribute # belongs to. def initialize(property, type, container) @type = type @property = property @container = container reset end # Reset the attribute value to the default value. def reset @inherited = false # Flag that marks whether the value of this attribute was provided by the # user (in contrast to being calculated). @provided = false # If type is an AttributeDefinition, create the initial value according # to the specified default for this type. Otherwise type is the initial # value. if @type.is_a?(AttributeDefinition) @container.instance_variable_set(('@' + type.id).intern, @type.default.deep_clone) else @container.instance_variable_set(('@' + type.id).intern, @type) end end # Call this function to inherit _value_ from the parent property. It is # very important that the values are deep copied as they may be modified # later on. def inherit(value) @inherited = true @container.instance_variable_set(('@' + type.id).intern, value.deep_clone) end # Return the current attribute setting mode. def AttributeBase.mode @@mode end # Change the @@mode. 0 means values are provided, 1 means values are # inherited, any other value means calculated. def AttributeBase.setMode(mode) @@mode = mode end # Return the ID of the attribute. def id type.id end # Return the name of the attribute. def name type.name end # Set the value of the attribute. Depending on the mode we are in, the flags # are updated accordingly. def set(value) case @@mode when 0 @provided = true when 1 @inherited = true end # Store the value in an instance variable in the PropertyTreeNode or # ScenarioData object referred to by @container. @container.instance_variable_set(('@' + type.id).intern, value) end # Return the attribute value. def get @container.instance_variable_get(('@' + type.id).intern) end # For legacy purposes we provide another name for get(). alias value get # Check whether the value is uninitialized or nil. def nil? if (v = get).is_a?(Array) v.empty? else v.nil? end end def isList? false end # We overwrite this for ListAttributeBase. def AttributeBase::isList? false end # Return the value as String. def to_s(query = nil) get.to_s end def to_num v = get if v.is_a?(Integer) || v.is_a?(Float) v else nil end end def to_sort v = get if v.is_a?(Integer) || v.is_a?(Float) v elsif v.respond_to?('to_s') if v.respond_to?('join') # If the attribute is an Array we convert it to a comma separated # list. v.join(', ') else v.to_s end else nil end end def to_rti(query) get.is_a?(RichTextIntermediate) ? !value : nil end # Return the value in TJP file syntax. def to_tjp @type.id + " " + get.to_s end private def quotedString(str) if str.include?("\n") "-8<-\n#{str}\n->8-" else "\"#{str.gsub("\"", '\"')}\"" end end end # The ListAttributeBase is a specialized form of AttributeBase for a list of # values instead of a single value. It will be used as a base class for all # attributes that hold lists. class ListAttributeBase < AttributeBase def initialize(property, type, container) super end def to_s get.join(', ') end def isList? true end # We overwrite this for ListAttributeBase. def ListAttributeBase::isList? true end end class AttributeOverwrite < ArgumentError end end TaskJuggler-3.8.1/lib/taskjuggler/AttributeDefinition.rb000066400000000000000000000044061473026623400233510ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = AttributeDefinition.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # class TaskJuggler # The AttributeDefinition describes the meta information of a PropertyTreeNode # attribute. It contains various information about the attribute. Based on # these bits of information, the PropertySet objects generate the attribute # lists for each PropertyTreeNode upon creation of the node. class AttributeDefinition attr_reader :id, :name, :objClass, :inheritedFromParent, :inheritedFromProject, :scenarioSpecific, :userDefined, :default # Create a new AttributeDefinition. _id_ is the ID of the attribute. It must # be unique within the PropertySet where it is used. _name_ is a more # descriptive text that will be used in report columns and the like. # _objClass_ is a reference to the class (not the object itself) of the # attribute. The possible classes all have names ending in Attribute. # _inheritedFromParent_ is a boolean flag that needs to be true if the # node can inherit the setting from the attribute of the parent node. # _inheritedFromProject_ is a boolen flag that needs to be true if the # node can inherit the setting from an attribute in the global scope. # _scenarioSpecific_ is a boolean flag that is set to true if the attribute # can have different values for each scenario. _default_ is the default # value that is set upon creation of the attribute. def initialize(id, name, objClass, inheritedFromParent, inheritedFromProject, scenarioSpecific, default, userDefined = false) @id = id @name = name @objClass = objClass @inheritedFromParent = inheritedFromParent @inheritedFromProject = inheritedFromProject @scenarioSpecific = scenarioSpecific @default = default @userDefined = userDefined # Prevent objects from being deep copied. freeze end end end TaskJuggler-3.8.1/lib/taskjuggler/Attributes.rb000066400000000000000000000323021473026623400215170ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = Attributes.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/Allocation' require 'taskjuggler/AttributeBase' require 'taskjuggler/Charge' require 'taskjuggler/ChargeSet' require 'taskjuggler/Limits' require 'taskjuggler/LogicalOperation' require 'taskjuggler/ShiftAssignments' require 'taskjuggler/WorkingHours' class TaskJuggler class AccountAttribute < AttributeBase def initialize(property, type, container) super end def AccountAttribute::tjpId 'account' end def to_s(query = nil) (v = get) ? v.id : '' end def to_tjp (v = get)? v.id : '' end end class AccountCreditListAttribute < ListAttributeBase def initialize(property, type, container) super set(Array.new) end def AccountCreditListAttribute::tjpId 'credits' end end class AllocationAttribute < ListAttributeBase def initialize(property, type, container) super set(Array.new) end def AllocationAttribute::tjpId 'allocation' end def to_tjp out = [] get.each do |allocation| out.push("allocate #{allocation.to_tjp}\n") # TODO: incomplete end out end def to_s(query = nil) out = '' first = true get.each do |allocation| if first first = false else out << "\n" end out << '[ ' firstR = true allocation.candidates.each do |resource| if firstR firstR = false else out << ', ' end out << resource.fullId end modes = %w(order lowprob lowload hiload random) out << " ] select by #{modes[allocation.selectionMode]} " out << 'mandatory ' if allocation.mandatory out << 'persistent ' if allocation.persistent end out end end class BookingListAttribute < ListAttributeBase def initialize(property, type, container) super end def BookingListAttribute::tjpId 'bookinglist' end def to_s(query = nil) get.collect{ |x| x.to_s }.join(', ') end def to_tjp raise "Don't call this method. This needs to be a special case." end end class BooleanAttribute < AttributeBase def initialize(property, type, container) super end def BooleanAttribute::tjpId 'boolean' end def to_s(query = nil) get ? 'true' : 'false' end def to_tjp @type.id + ' ' + (get ? 'yes' : 'no') end end class ChargeListAttribute < ListAttributeBase def initialize(property, type, container) super end def ChargeListAttribute::tjpId 'charge' end def to_s(query = nil) get.join(', ') end end # A ChargeSetListAttribute encapsulates a list of ChargeSet objects as # PropertyTreeNode attributes. class ChargeSetListAttribute < ListAttributeBase def initialize(property, type, container) super end def ChargeSetListAttribute::tjpId 'chargeset' end def to_s(query = nil) out = [] get.each { |i| out << i.to_s } out.join(", ") end def to_tjp out = [] get.each { |i| out << i.to_s } @type.id + " " + out.join(', ') end end class ColumnListAttribute < ListAttributeBase def initialize(property, type, container) super end def ColumnListAttribute::tjpId 'columns' end def to_s(query = nil) "TODO" end end class DateAttribute < AttributeBase def initialize(property, type, container) super end def to_s(query = nil) if (v = get) v.to_s(query ? query.timeFormat : '%Y-%m-%d') else 'Error' end end def DateAttribute::tjpId 'date' end end class DefinitionListAttribute < ListAttributeBase def initialize(property, type, container) super end end class DependencyListAttribute < ListAttributeBase def initialize(property, type, container) super end def DependencyListAttribute::tjpId 'dependencylist' end def to_s(query = nil) out = [] get.each { |t| out << t.task.fullId if t.task } out.join(', ') end def to_tjp out = [] get.each { |taskDep| out << taskDep.task.fullId } @type.id + " " + out.join(', ') end end class DurationAttribute < AttributeBase def initialize(property, type, container) super end def DurationAttribute::tjpId 'duration' end def to_tjp @type.id + ' ' + get.to_s + 'h' end def to_s(query = nil) query ? query.scaleDuration(query.project.slotsToDays(get)) : get.to_s end end class IntegerAttribute < AttributeBase def initialize(property, type, container) super end def IntegerAttribute::tjpId 'integer' end end class FlagListAttribute < ListAttributeBase def initialize(property, type, container) super end def FlagListAttribute::tjpId 'flaglist' end def to_s(query = nil) get.join(', ') end def to_tjp "flags #{get.join(', ')}" end end class FloatAttribute < AttributeBase def initialize(property, type, container) super end def FloatAttribute::tjpId 'number' end def to_tjp id + ' ' + get.to_s end end class FormatListAttribute < ListAttributeBase def initialize(property, type, container) super end def to_s(query = nil) get.join(', ') end end class JournalSortListAttribute < ListAttributeBase def initialize(property, type, container) super end def JournalSortListAttribute::tjpId 'journalsorting' end end class TimeIntervalListAttribute < ListAttributeBase def initialize(property, type, container) super end def TimeIntervalListAttribute::tjpId 'intervallist' end def to_s(query = nil) out = [] get.each { |i| out << i.to_s } out.join(", ") end def to_tjp out = [] get.each { |i| out << i.to_s } @type.id + " " + out.join(', ') end end class LeaveAllowanceListAttribute < ListAttributeBase def initialize(property, type, container) super end end class LeaveListAttribute < ListAttributeBase def initialize(property, type, container) super end def LeaveListAttribute::tjpId 'leave' end def to_tjp "leaves #{get.join(",\n")}" end end class LimitsAttribute < AttributeBase def initialize(property, type, container) super v = get v.setProject(property.project) if v end def LimitsAttribute::tjpId 'limits' end def to_tjp 'This code is still missing!' end end class LogicalExpressionAttribute < AttributeBase def initialize(property, type, container) super end def LogicalExpressionAttribute::tjpId 'logicalexpressions' end end class LogicalExpressionListAttribute < ListAttributeBase def initialize(property, type, container) super end def LogicalExpressionListAttribute::tjpId 'logicalexpressions' end end class NodeListAttribute < ListAttributeBase def initialize(property, type, container) super end end class PropertyAttribute < AttributeBase def initialize(property, type, container) super end def PropertyAttribute::tjpId 'property' end end class RealFormatAttribute < AttributeBase def initialize(property, type, container) super end end class ReferenceAttribute < AttributeBase def initialize(property, type, container) super end def ReferenceAttribute::tjpId 'reference' end def to_s(query = nil) url || '' end def to_rti(query) return nil unless get rText = RichText.new("[#{url} #{label}]") rText.generateIntermediateFormat end def to_tjp "#{@type.id} \"#{url}\"#{label ? " { label \"#{label}\" }" : ''}" end def url (v = get) ? v[0] : nil end def label (v = get) ? (v[1] ? v[1][0] : v[0]) : nil end end class ResourceListAttribute < ListAttributeBase def initialize(property, type, container) super end def ResourceListAttribute::tjpId 'resourcelist' end def to_s(query = nil) out = [] get.each { |r| out << r.fullId } out.join(", ") end def to_rti(query = nil) out = [] if query get.each do |r| if query.listItem rti = RichText.new(query.listItem, RTFHandlers.create(r.project)). generateIntermediateFormat q = query.dup q.property = r rti.setQuery(q) out << "#{rti.to_s}" else out << "#{r.name}" end end query.assignList(out) else get.each { |r| out << r.name } rText = RichText.new(out.join(', ')) rText.generateIntermediateFormat end end def to_tjp out = [] get.each { |r| out << r.fullId } @type.id + " " + out.join(', ') end end class RichTextAttribute < AttributeBase def initialize(property, type, container) super end def inputText (v = get) ? v.richText.inputText : '' end def RichTextAttribute::tjpId 'richtext' end def to_s(query = nil) (v = get) ? v.to_s : '' end def to_tjp "#{@type.id} #{quotedString(get.richText.inputText)}" end end class ScenarioListAttribute < ListAttributeBase def initialize(property, type, container) super end def ScenarioListAttribute::tjpId 'scenarios' end def to_s(query = nil) get.join(', ') end end class ShiftAssignmentsAttribute < AttributeBase def initialize(property, type, container) super v = get v.project = property.project if v end def ShiftAssignmentsAttribute::tjpId 'shifts' end def to_tjp v = get first = true str = 'shifts ' v.assignments.each do |sa| if first first = false else str += ",\n" end str += "#{sa.shiftScenario.property.fullId} #{sa.interval}" end str end end class SortListAttribute < ListAttributeBase def initialize(property, type, container) super end def SortListAttribute::tjpId 'sorting' end end class StringAttribute < AttributeBase def initialize(property, type, container) super end def StringAttribute::tjpId 'text' end def to_tjp "#{@type.id} #{quotedString(get)}" end end class SymbolAttribute < AttributeBase def initialize(property, type, container) super end def SymbolAttribute::tjpId 'symbol' end end class SymbolListAttribute < ListAttributeBase def initialize(property, type, container) super end def SymbolListAttribute::tjpId 'symbollist' end end class TaskDepListAttribute < ListAttributeBase def initialize(property, type, container) super end def TaskDepListAttribute::tjpId 'taskdeplist' end def to_s(query = nil) out = [] get.each { |t, onEnd| out << t.fullId } out.join(", ") end def to_tjp out = [] get.each { |t, onEnd| out << t.fullId } @type.id + " " + out.join(', ') end end class TaskListAttribute < ListAttributeBase def initialize(property, type, container) super end def TaskListAttribute::tjpId 'tasklist' end def to_s(query = nil) out = [] get.each { |t| out << t.fullId } out.join(", ") end def to_tjp out = [] get.each { |t| out << t.fullId } @type.id + " " + out.join(', ') end end class WorkingHoursAttribute < AttributeBase def initialize(property, type, container) super end def WorkingHoursAttribute::tjpId 'workinghours' end def to_tjp dayNames = %w( sun mon tue wed thu fri sat ) str = '' 7.times do |day| str += "workinghours #{dayNames[day]} " whs = get.getWorkingHours(day) if whs.empty? str += "off" str += "\n" if day < 6 next end first = true whs.each do |iv| if first first = false else str += ', ' end str += "#{iv[0] / 3600}:#{iv[0] % 3600 == 0 ? '00' : (iv[0] % 3600) / 60} - " + "#{iv[1] / 3600}:#{iv[1] % 3600 == 0 ? '00' : (iv[1] % 3600) / 60}" end str += "\n" if day < 6 end str end end end TaskJuggler-3.8.1/lib/taskjuggler/BatchProcessor.rb000066400000000000000000000310431473026623400223130ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = BatchProcessor.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'thread' require 'monitor' class TaskJuggler # The JobInfo class is just a storage container for some batch job related # pieces of information. It contains things like a job id, the process id, # the stdout data and the like. class JobInfo attr_reader :jobId, :block, :tag attr_accessor :pid, :retVal, :stdoutP, :stdoutC, :stdout, :stdoutEOT, :stderrP, :stderrC, :stderr, :stderrEOT def initialize(jobId, block, tag) # The job id. A unique number that is used by the BatchProcessor objects # to indentify jobs. @jobId = jobId # This the the block of code to be run as external process. @block = block # The tag can really be anything that the user of BatchProcessor needs # to uniquely identify the job. @tag = tag # The pipe to transfer stdout data from the child to the parent. @stdoutP, @stdoutC = nil # The stdout output of the child @stdout = '' # This flag is set to true when the EOT character has been received. @stdoutEOF = false # The pipe to transfer stderr data from the child to the parent. @stderrP, @stderrC = nil # The stderr output of the child @stderr = '' # This flag is set to true when the EOT character has been received. @stderrEOT = false end def openPipes @stdoutP, @stdoutC = IO.pipe @stderrP, @stderrC = IO.pipe end end # The BatchProcessor class can be used to run code blocks of the program as # a separate process. Mulitple pieces of code can be submitted to be # executed in parallel. The number of CPU cores to use is limited at object # creation time. The submitted jobs will be queued and scheduled to the # given number of CPUs. The usage model is simple. Create an BatchProcessor # object. Use BatchProcessor#queue to submit all the jobs and then use # BatchProcessor#wait to wait for completion and to process the results. class BatchProcessor # Create a BatchProcessor object. +maxCpuCores+ limits the number of # simultaneously spawned processes. def initialize(maxCpuCores) @maxCpuCores = maxCpuCores # Jobs submitted by calling queue() are put in the @toRunQueue. The # launcher Thread will pick them up and fork them off into another # process. @toRunQueue = [ ] # A hash that maps the JobInfo objects of running jobs by their PID. @runningJobs = { } # A list of jobs that wait to complete their writing. @spoolingJobs = [ ] # The wait() method will then clean the @toDropQueue, executes the post # processing block and removes all JobInfo related objects. @toDropQueue = [] # A semaphore to guard accesses to @runningJobs, @spoolingJobs and # following shared data structures. @lock = Monitor.new # We count the submitted and completed jobs. The @jobsIn counter also # doubles as a unique job ID. @jobsIn = @jobsOut = 0 # An Array that holds all the IO objects to receive data from. @pipes = [] # A hash that maps IO objects to JobInfo objects @pipeToJob = {} # This global flag is set to true to signal the threads to terminate. @terminate = false # Sleep time of the threads when no data is pending. This value must be # large enough to allow for a context switch between the sending # (forked-off) process and this process. If it's too large, throughput # will suffer. @timeout = 0.02 Thread.abort_on_exception = true end # Add a new job the job queue. +tag+ is some data that the caller can use # to identify the job upon completion. +block+ is a Ruby code block to be # executed in a separate process. def queue(tag = nil, &block) # Create a new JobInfo object for the job and push it to the @toRunQueue. @lock.synchronize do raise 'You cannot call queue() while wait() is running!' if @jobsOut > 0 # If this is the first queued job for this run, we have to start the # helper threads. if @jobsIn == 0 # The JobInfo objects in the @toRunQueue are processed by the # launcher thread. It forkes off processes to execute the code # block associated with the JobInfo. @launcher = Thread.new { launcher } # The receiver thread waits for terminated child processes and picks # up the results. @receiver = Thread.new { receiver } # The grabber thread collects $stdout and $stderr data from each # child process and stores them in the corresponding JobInfo. @grabber = Thread.new { grabber } end # To track a job through the queues, we use a JobInfo object to hold # all data associated with a job. job = JobInfo.new(@jobsIn, block, tag) # Increase job counter @jobsIn += 1 # Push the job to the toRunQueue. @toRunQueue.push(job) end end # Wait for all jobs to complete. The code block will get the JobInfo # objects for each job to pick up the results. def wait # Don't wait if there are no jobs. return if @jobsIn == 0 # When we have received as many jobs in the @toDropQueue than we have # started then we're done. while @lock.synchronize { @jobsOut < @jobsIn } job = nil @lock.synchronize do if !@toDropQueue.empty? && (job = @toDropQueue.pop) # Call the post-processing block that was passed to wait() with # the JobInfo object as argument. @jobsOut += 1 yield(job) end end unless job sleep(@timeout) end end # Signal threads to stop @terminate = true # Wait for treads to finish @launcher.join @receiver.join @grabber.join # Reset some variables so we can reuse the object for further job runs. @jobsIn = @jobsOut = 0 @terminate = false # Make sure all data structures are empty and clean. check end private # This function runs in a separate thread to pop JobInfo items from the # @toRunQueue and create child processes for them. def launcher # Run until the terminate flag is set. until @terminate job = nil unless @lock.synchronize { @runningJobs.length < @maxCpuCores && (job = @toRunQueue.pop) } # We have no jobs in the @toRunQueue or all CPU cores in use already. sleep(@timeout) else @lock.synchronize do job.openPipes # Add the receiver end of the pipe to the pipes Arrays. @pipes << job.stdoutP @pipes << job.stderrP # Map the pipe end to this JobInfo object. @pipeToJob[job.stdoutP] = job @pipeToJob[job.stderrP] = job pid = fork do # This is the child process now. Connect $stdout and $stderr to # the pipes. $stdout.reopen(job.stdoutC) job.stdoutC.close $stderr.reopen(job.stderrC) job.stderrC.close # Call the Ruby code block retVal = job.block.call # Send EOT character to mark the end of the text. $stdout.putc 4 $stdout.close $stderr.putc 4 $stderr.close # Now exit the child process and return the return value of the # block as process return value. exit retVal end job.pid = pid # Save the process ID in the PID to JobInfo hash. @runningJobs[pid] = job end end end end # This function runs in a separate thread to wait for completed jobs. It # waits for the process completion and stores the result in the # corresponding JobInfo object. Aborted jobs are pushed to the # @toDropQueue while completed jobs are pushed to the @spoolingJobs queue. def receiver until @terminate pid = retVal = nil begin # Wait for the next job to complete. pid, retVal = Process.wait2 rescue Errno::ECHILD # No running jobs. Wait a bit. sleep(@timeout) end if pid && retVal job = nil @lock.synchronize do # Get the JobInfo object that corresponds to the process ID. The # blocks passed to queue() or wait() may fork child processes as # well. If we get their PID, we can just ignore them. next if (job = @runningJobs[pid]).nil? # Remove the job from the @runningJobs Hash. @runningJobs.delete(pid) # Save the return value. job.retVal = retVal.exitstatus if retVal.signaled? cleanPipes(job) # Aborted jobs will probably not send an EOT. So we fastrack # them to the toDropQueue. @toDropQueue.push(job) else # Push the job into the @spoolingJobs list to wait for it to # finish writing IO. @spoolingJobs << job end end end end end # This function runs in a separate thread to pick up the $stdout and # $stderr outputs of the child processes. It stores them in the JobInfo # object that corresponds to each child process. def grabber until @terminate # Wait for output in any of the pipes or a timeout. To make sure that # we get all output, we remain in the loop until the select() call # times out. res = nil begin @lock.synchronize do if (res = IO.select(@pipes, nil, nil, @timeout)) # We have output data from at least one child. Check which pipe # actually triggered the select. res[0].each do |pipe| # Find the corresponding JobInfo object. job = @pipeToJob[pipe] # Store the standard output. if pipe == job.stdoutP # Look for the EOT character to signal the end of the text. if pipe.closed? || (c = pipe.read_nonblock(1)) == ?\004 job.stdoutEOT = true else job.stdout << c end end # Store the error output. if pipe == job.stderrP # Look for the EOT character to signal the end of the text. if pipe.closed? || (c = pipe.read_nonblock(1)) == ?\004 job.stderrEOT = true else job.stderr << c end end end end end sleep(@timeout) unless res end while res # Search the @spoolingJobs list for jobs that have completed IO and # push them to the @toDropQueue. @lock.synchronize do @spoolingJobs.each do |job| # Both stdout and stderr need to have reached the end of text. if job.stdoutEOT && job.stderrEOT @spoolingJobs.delete(job) cleanPipes(job) @toDropQueue.push(job) # Since we deleted a list item during an iterator run, we # terminate the iterator. break end end end end end def cleanPipes(job) @pipes.delete(job.stdoutP) @pipeToJob.delete(job.stdoutP) @pipes.delete(job.stderrP) @pipeToJob.delete(job.stderrP) job.stdoutC.close job.stdoutP.close job.stderrC.close job.stderrP.close job.stdoutC = job.stderrC = nil job.stdoutP = job.stderrP = nil end def check raise "toRunQueue not empty!" unless @toRunQueue.empty? raise "runningJobs list not empty!" unless @runningJobs.empty? raise "spoolingJobs list not empty!" unless @spoolingJobs.empty? raise "toDropQueue not empty!" unless @toDropQueue.empty? raise "pipe list not empty!" unless @pipes.empty? raise "pipe map not empty!" unless @pipeToJob.empty? end end end TaskJuggler-3.8.1/lib/taskjuggler/Booking.rb000066400000000000000000000025441473026623400207660ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = Booking.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # class TaskJuggler class Booking attr_reader :resource, :task, :intervals attr_accessor :sourceFileInfo, :overtime, :sloppy def initialize(resource, task, intervals) @resource = resource @task = task @intervals = intervals @sourceFileInfo = nil @overtime = 0 @sloppy = 0 end def to_s out = "#{@resource.fullId} " first = true @intervals.each do |iv| if first first = false else out += ", " end out += "#{iv.start} + #{(iv.end - iv.start) / 3600}h" end end def to_tjp(taskMode) out = taskMode ? "#{@task.fullId} " : "#{@resource.fullId} " first = true @intervals.each do |iv| if first first = false else out += ",\n" end out += "#{iv.start} + #{(iv.end - iv.start) / 3600}h" end out += ' { overtime 2 }' end end end TaskJuggler-3.8.1/lib/taskjuggler/Charge.rb000066400000000000000000000040361473026623400205650ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = Charge.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/TjTime' class TaskJuggler # This class describes a one-time or per time charge that can be associated # with a Task. The charge can take effect either on starting the task, # finishing it, or per time interval. class Charge # Create a new Charge object. _amount_ is either the one-time charge or the # per-day-rate. _task_ is the Task that owns this charge. _scenarioIdx_ is # the index of the scenario this Charge belongs to. def initialize(amount, mode, task, scenarioIdx) @amount = amount unless [ :onStart, :onEnd, :perDiem ].include?(mode) raise "Unsupported mode #{mode}" end @mode = mode @task = task @scenarioIdx = scenarioIdx end # Compute the total charge for the TimeInterval described by _period_. def turnover(period) case @mode when :onStart return period.contains?(@task['start', @scenarioIdx]) ? @amount : 0.0 when :onEnd return period.contains?(@task['end', @scenarioIdx]) ? @amount : 0.0 else iv = period.intersection(TimeInterval.new(@task['start', @scenarioIdx], @task['end', @scenarioIdx])) if iv return (iv.duration / (60 * 60 * 24)) * @amount else return 0.0 end end end # Dump object in human readable form. def to_s case @mode when :onStart mode = 'on start' when :onEnd mode = 'on end' when :perDiem mode = 'per day' else mode = 'unknown' end "#{@amount} #{mode}" end end end TaskJuggler-3.8.1/lib/taskjuggler/ChargeSet.rb000066400000000000000000000076421473026623400212470ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = ChargeSet.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/TjException' class TaskJuggler # A charge set describes how a given amount is distributed over a set of # accounts. It stores the percentage share for each account. The accumulated # percentages must always be 100% for a valid charge set. For consistency # reasons, accounts must always be leaf accounts of the same top-level # account. Percentage values must range from 0.0 to 1.0. class ChargeSet attr_reader :master # Create a new ChargeSet object. def initialize @set = {} @master = nil end # Add a new account to the set. Accounts and share rates must meet a number # of requirements. This method does some error checking and raises a # TjException in case of problems. It cannot check everything. Accounts can # later be turned into group accounts or the total share sum may not be # 100%. This needs to be checked at a later stage. Accounts may have a share # of nil. This will be set in ChargeSet#complete later. def addAccount(account, share) unless account.leaf? raise TjException.new, "Account #{account.fullId} is a group account and cannot be used " + "in a chargeset." end if @set.include?(account) raise TjException.new, "Account #{account.fullId} is already a member of the charge set." end if @master.nil? @master = account.root elsif @master != account.root raise TjException.new, "All members of this charge set must belong to the " + "#{@master.fullId} account. #{account.fullId} belongs to " + "#{account.root.fullId}." end if account.container? raise TjException.new, "#{account.fullId} is a group account. Only leaf accounts are " + "allowed for a charge set." end if share && (share < 0.0 || share > 1.0) raise TjException.new, "Charge set shares must be between 0 and 100%" end @set[account] = share end def each @set.each do |account, share| yield account, share end end # Check for accounts that don't have a share yet and distribute the # remainder to 100% evenly accross them. def complete # Calculate the current total share. totalPercent = 0.0 undefined = 0 @set.each_value do |share| if share totalPercent += share else undefined += 1 end end # Must be less than 100%. if totalPercent > 1.0 raise TjException.new, "Total share of this set (#{totalPercent * 100}%) excedes 100%." end if undefined > 0 commonShare = (1.0 - totalPercent) / undefined if commonShare <= 0 raise TjException.new, "Total share is 100% but #{undefined} account(s) still exist." end @set.each do |account, share| if share.nil? @set[account] = commonShare end end elsif totalPercent != 1.0 raise TjException.new, "Total share of this set is #{totalPercent * 100} instead of 100%." end end # Return the share percentage for a given Account _account_. def share(account) @set[account] end # Return the set as comma separated list of account ID + share pairs. def to_s str = '(' @set.each do |account, share| str += ', ' unless str == '(' str += "#{account.fullId} #{share * 100}%" end str += ')' end end end TaskJuggler-3.8.1/lib/taskjuggler/DataCache.rb000066400000000000000000000104601473026623400211670ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = DataCache.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'time' require 'singleton' class TaskJuggler # These are the entries in the DataCache. They store a value and an access # counter. The counter can be read and written externally. class DataCacheEntry attr_reader :unhashedKey attr_accessor :hits # Create a new DataCacheEntry for the _value_. We also store the unhashed # key to be able to detect hash collisions. The access counter is set to 1 # to increase the chance that it is not flushed immedidate. def initialize(unhashedKey, value) @unhashedKey = unhashedKey @value = value @hits = 1 end # Return the value and increase the access counter by 1. def value if @hits <= 0 @hits = 1 else @hits += 1 end @value end end # This class provides a global data cache that can be used to store and # retrieve values indexed by a key. The cache is size limited. When maximum # capacity is reached, a certain percentage of the least requested values is # dropped from the cache. The primary purpose of this global cache is to # store values that are expensive to compute but may be need on several # occasions during the program execution. class DataCache include Singleton def initialize resize flush # Counter for the number of writes to the cache. @stores = 0 # Counter for the number of found values. @hits = 0 # Counter for the number of not found values. @misses = 0 # Counter for hash collisions @collisions = 0 end # For now, we use this randomly determined size. def resize(size = 100000) @highWaterMark = size # Flushing out the least used entries is fairly expensive. So we only # want to do this once in a while. The lowWaterMark determines how much # of the entries will survive the flush. @lowWaterMark = size * 0.9 end # Completely flush the cache. The statistic counters will remain intact, # but all data values are lost. def flush @entries = {} end if RUBY_VERSION < '1.9.0' # Ruby 1.8 has a buggy hash key generation algorithm that leads to many # hash collisions. We completely disable caching on 1.8. def cached(*args) yield end else # _args_ is a set of arguments that unambigously identify the data entry. # It's converted into a hash to store or recover a previously stored # entry. If we have a value for the key, return the value. Otherwise call # the block to compute the value, store it and return it. def cached(*args) key = args.hash if @entries.has_key?(key) e = @entries[key] if e.unhashedKey != args # Two different args produce the same hash key. This should be a # very rare event! @collisions += 1 yield else @hits += 1 e.value end else @misses += 1 store(yield, args, key) end end end def to_s <<"EOT" Entries: #{@entries.size} Stores: #{@stores} Collisions: #{@collisions} Hits: #{@hits} Misses: #{@misses} Hit Rate: #{@hits * 100.0 / (@hits + @misses)}% EOT end private # Store _value_ into the cache using _key_ to tag it. _key_ must be unique # and must be used to load the value from the cache again. You cannot # store nil values! def store(value, unhashedKey, key) @stores += 1 if @entries.size > @highWaterMark while @entries.size > @lowWaterMark # How many entries do we need to delete to get to the low watermark? toDelete = @entries.size - @lowWaterMark @entries.delete_if do |foo, e| # Hit counts age with every cleanup. (e.hits -= 1) < 0 && (toDelete -= 1) >= 0 end end end @entries[key] = DataCacheEntry.new(unhashedKey, value) value end end end TaskJuggler-3.8.1/lib/taskjuggler/FileList.rb000066400000000000000000000031641473026623400211100ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = FileList.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # class TaskJuggler # The FileRecord stores the name of a file and the modification time. class FileRecord def initialize(fileName) @name = fileName.dup @mtime = File.mtime(@name) end def modified? File.mtime(@name) > @mtime end end # The FileList class stores a list of file names. Each file name is unique # and more information about the file is contained in FileRecord entries. class FileList # Create a new, empty FileList. def initialize @files = {} end # Add the file with _fileName_ to the list. If it's already in the list, # it will not be added again. def <<(fileName) return if fileName == '.' || @files.include?(fileName) @files[fileName] = FileRecord.new(fileName) end # Return the name of the master file or nil of the master file was stdin. def masterFile @files.each_key do |file| return file if file[-4, 4] == '.tjp' end nil end # Return true if any of the files in the list have been modified after # they were added to the list. def modified? @files.each_value do |f| return true if f.modified? end false end end end TaskJuggler-3.8.1/lib/taskjuggler/HTMLDocument.rb000066400000000000000000000055601473026623400216420ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = HTMLDocument.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/XMLDocument' require 'taskjuggler/HTMLElements' class TaskJuggler # HTMLDocument objects are a specialized form of XMLDocument objects. All # mandatory elements of a proper HTML document will be added automatically # upon object creation. class HTMLDocument < XMLDocument include HTMLElements attr_reader :html # When creating a HTMLDocument the caller can specify the type of HTML that # will be used. The constructor then generates the proper XML declaration # for it. :strict, :transitional and :frameset are supported for _docType_. def initialize(docType = :html5, &block) super(&block) unless docType == :html5 @elements << XMLBlob.new('') case docType when :strict dtdRef = 'Strict' url = 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd' when :transitional dtdRef = 'Transitional' url = 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd' when :frameset dtdRef = 'Frameset' url = 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd' else raise "Unsupported docType" end @elements << XMLBlob.new( '") else @elements << XMLBlob.new('') end @elements << XMLComment.new('This file has been generated by ' + "#{AppConfig.appName} v#{AppConfig.version}") attrs = { 'xml:lang' => 'en', 'lang' => 'en' } attrs['xmlns'] ='http://www.w3.org/1999/xhtml' unless docType == :html5 @elements << (@html = HTML.new(attrs)) end # Generate the 'head' section of an HTML page. def generateHead(title, metaTags = {}, blob = nil) @html << HEAD.new { e = [ TITLE.new { title }, META.new({ 'http-equiv' => 'Content-Type', 'content' => 'text/html; charset=utf-8' }), # Ugly hack to force IE into IE-9 mode. META.new({ 'http-equiv' => 'X-UA-Compatible', 'content' => 'IE=9' }) ] # Include optional meta tags. metaTags.each do |name, content| e << META.new({ 'name' => name, 'content' => content }) end # Add a raw HTML blob into the header if provided. e << XMLBlob.new(blob) if blob e } end end end TaskJuggler-3.8.1/lib/taskjuggler/HTMLElements.rb000066400000000000000000000024651473026623400216410ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = HTMLElements.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/XMLElement' class TaskJuggler module HTMLElements # A list of supported HTML tags. htmlTags = %w( a b body br code col colgroup div em frame frameset footer h1 h2 h3 head html hr meta p pre span table td title tr ) # A list of HTML tags that are self-closing. closureTags = %w( area base basefont br hr input img link meta ) # For every HTML tag, we generate a class with the equivalent uppercase # name. This class is derived off of XMLElement. This makes creating HTML # code a lot simpler. Instead of # XMLElement.new('h1') # we now can write # H1.new htmlTags.each do |tag| class_eval <<"EOT" class #{tag.upcase} < XMLElement def initialize(attrs = {}, &block) super("#{tag}", attrs, #{closureTags.include?(tag)}) end end EOT end end end TaskJuggler-3.8.1/lib/taskjuggler/ICalendar.rb000066400000000000000000000143531473026623400212210ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = ICalendar.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/Tj3Config' class TaskJuggler # This class implements a very basic RFC5545 compliant iCalendar file # generator. It currently only supports a very small subset of the tags that # are needed for TaskJuggler. class ICalendar # The maximum allowed length of a content line without line end character. LINELENGTH = 75 # Utility class to store name and email of a person. class Person < Struct.new(:name, :email) end # Base class for all ICalendar components. class Component attr_accessor :description, :relatedTo, :organizer attr_reader :uid def initialize(ical, uid, summary, startDate) @ical = ical @type = self.class.to_s.split('::').last.upcase @uid = uid + "-#{@type}" @summary = summary @startDate = startDate # Optional attributes @description = nil @relatedTo = nil @organizer = nil @attendees = [] end def setOrganizer(name, email) @organizer = Person.new(name, email) end def addAttendee(name, email) @attendees << Person.new(name, email) end def to_s str = <<"EOT" BEGIN:V#{@type} DTSTAMP:#{dateTime(TjTime.new.utc)} CREATED:#{dateTime(@ical.creationDate)} UID:#{@uid} LAST-MODIFIED:#{dateTime(@ical.lastModified)} SUMMARY:#{quoted(@summary)} DTSTART:#{dateTime(@startDate)} EOT str += "DESCRIPTION:#{quoted(@description)}\n" if @description str += "RELATED-TO:#{@relatedTo}\n" if @relatedTo if @organizer str += "ORGANIZER;CN=#{@organizer.name}:mailto:#{@organizer.email}\n" end @attendees.each do |attendee| str += "ATTENDEE;CN=#{attendee.name}:mailto:#{attendee.email}\n" end str += yield if block_given? str += "END:V#{@type}\n\n" end private def dateTime(date) @ical.dateTime(date) end def quoted(str) str.gsub(/([;,"\\])/, '\\\\\1').gsub(/\n/, '\n') end end # Stores the data of an VTODO component and can generate one. class Todo < Component attr_accessor :priority, :percentComplete # Create the Todo object with some mandatory data. _ical_ is a reference # to the parent ICalendar object. _uid_ is a unique pattern used to # generate the UID tag. _summary_ is a String for SUMMARY. _startDate_ # is used to generate DTSTART. _endDate_ is used to either generate # the COMPLETED or DUE tag. def initialize(ical, uid, summary, startDate, endDate) super(ical, uid, summary, startDate) # Mandatory attributes @ical.addTodo(self) @endDate = endDate # Priority value (0 - 9) @priority = 0 @percentComplete = -1 end # Generate the VTODO record as String. def to_s super do str = '' if @percentComplete < 100.0 str += "DUE:#{dateTime(@endDate)}\n" else str += "COMPLETED:#{dateTime(@endDate)}\n" end str += "PERCENT-COMPLETE:#{@percentComplete}\n" end end end # Stores the data of an VTODO component and can generate one. class Event < Component # Create the Event object with some mandatory data. _ical_ is a # reference to the parent ICalendar object. _uid_ is a unique pattern # used to generate the UID tag. _summary_ is a String for SUMMARY. # _startDate_ is used to generate DTSTART. _endDate_ is used to either # generate the COMPLETED or DUE tag. def initialize(ical, uid, summary, startDate, endDate) super(ical, uid, summary, startDate) @ical.addEvent(self) # Mandatory attributes @endDate = endDate # Optional attributes @priority = 1 end # Generate the VEVENT record as String. def to_s super do <<"EOT" PRIORITY:#{@priority} DTEND:#{dateTime(@endDate)} TRANSP:TRANSPARENT EOT end end end class Journal < Component def initialize(ical, uid, summary, startDate) super @ical.addJournal(self) end def to_s super end end attr_reader :uid attr_accessor :creationDate, :lastModified def initialize(uid) @uid = "#{AppConfig.packageName}-#{uid}" @creationDate = @lastModified = TjTime.new.utc @todos = [] @events = [] @journals = [] end # Add a new VTODO component. For internal use only! def addTodo(todo) @todos << todo end # Add a new VEVENT component. For internal use only! def addEvent(event) @events << event end # Add a new VJOURNAL component. For internal user only! def addJournal(journal) @journals << journal end def to_s str = <<"EOT" BEGIN:VCALENDAR PRODID:-//The #{AppConfig.softwareName} Project/NONSGML #{AppConfig.softwareName} #{AppConfig.version}//EN VERSION:2.0 EOT @todos.each { |todo| str += todo.to_s } @events.each { |event| str += event.to_s } @journals.each { |journal| str += journal.to_s } str << <<"EOT" END:VCALENDAR EOT foldLines(str) end def dateTime(date) date.to_s("%Y%m%dT%H%M%SZ", 'UTC') end private # Make sure that no line is longer than LINELENTH octets (excl. the # newline character) def foldLines(str) newStr = '' str.each_line do |line| bytes = 0 line.each_utf8_char do |c| # Make sure we support Ruby 1.8 and 1.9 String handling. cBytes = c.bytesize if bytes + cBytes > LINELENGTH && c != "\n" newStr += "\n " bytes = 0 else bytes += cBytes end newStr << c end end # Convert line ends to CR+LF newStr.gsub(/\n/, "\r\n") end end end TaskJuggler-3.8.1/lib/taskjuggler/Interval.rb000066400000000000000000000211611473026623400211560ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = Interval.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/TjTime' class TaskJuggler # This is the based class used to store several kinds of intervals in # derived classes. class Interval attr_reader :start, :end # Create a new Interval object. _s_ is the interval start, _e_ the # interval end (not included). def initialize(s, e) @start = s @end = e # The end must not be before the start. if @end < @start raise ArgumentError, "Invalid interval (#{s} - #{e})" end end # Return true if _arg_ is contained within the Interval. It can either # be a single TjTime or another Interval. def contains?(arg) if arg.is_a?(Interval) raise ArgumentError, "Class mismatch" if self.class != arg.class return @start <= arg.start && arg.end <= @end else raise ArgumentError, "Class mismatch" if @start.class != arg.class return @start <= arg && arg < @end end end # Check whether the Interval _arg_ overlaps with this Interval. def overlaps?(arg) if arg.is_a?(Interval) raise ArgumentError, "Class mismatch" if self.class != arg.class return (@start <= arg.start && arg.start < @end) || (arg.start <= @start && @start < arg.end) else raise ArgumentError, "Class mismatch" if @start.class != arg.class return @start <= arg && arg < @end end end # Return a new Interval that contains the overlap of self and the Interval # _iv_. In case there is no overlap, nil is returned. def intersection(iv) raise ArgumentError, "Class mismatch" if self.class != iv.class newStart = @start > iv.start ? @start : iv.start newEnd = @end < iv.end ? @end : iv.end newStart < newEnd ? self.class.new(newStart, newEnd) : nil end # Append or prepend the Interval _iv_ to self. If _iv_ does not directly # attach to self, just return self. def combine(iv) raise ArgumentError, "Class mismatch" if self.class != iv.class if iv.end == @start # Prepend iv Array.new self.class.new(iv.start, @end) elsif @end == iv.start # Append iv Array.new self.class.new(@start, iv.end) else self end end # Compare self with Interval _iv_. This function only works for # non-overlapping Interval objects. def <=>(iv) raise ArgumentError, "Class mismatch" if self.class != iv.class if @end < iv.start -1 elsif iv.end < @start 1 end 0 end # Return true if the Interval _iv_ describes an identical time period. def ==(iv) raise ArgumentError, "Class mismatch" if self.class != iv.class @start == iv.start && @end == iv.end end end # The TimeInterval class provides objects that model a time interval. The # start end end time are represented as seconds after Jan 1, 1970. The start # is part of the interval, the end is not. class TimeInterval < Interval attr_accessor :start, :end # Create a new TimeInterval. _args_ can be three different kind of arguments. # # a and b should be TjTime objects. # # TimeInterval.new(a, b) | -> Interval(a, b) # TimeInterval.new(a) | -> Interval(a, a) # TimeInterval.new(iv) | -> Interval(iv.start, iv.end) # def initialize(*args) if args.length == 1 if args[0].is_a?(TjTime) # Just one argument, a date super(args[0], args[0]) elsif args[0].is_a?(TimeInterval) # Just one argument, a TimeInterval super(args[0].start, args[0].end) else raise ArgumentError, "Illegal argument 1: #{args[0].class}" end elsif args.length == 2 # Two arguments, a start and end date unless args[0].is_a?(TjTime) raise ArgumentError, "Interval start must be a date, not a " + "#{args[0].class}" end unless args[1].is_a?(TjTime) raise ArgumentError, "Interval end must be a date, not a" + "#{args[1].class}" end super(args[0], args[1]) else raise ArgumentError, "Too many arguments: #{args.length}" end end # Return the duration of the TimeInterval. def duration @end - @start end # Turn the TimeInterval into a human readable form. def to_s @start.to_s + ' - ' + @end.to_s end end # This class describes an interval of a scoreboard. The start and end of the # interval are stored as indexes but can always be converted back to TjTime # objects if needed. class ScoreboardInterval < Interval attr_reader :sbStart, :slotDuration # Create a new ScoreboardInterval. _args_ can be three different kind of # arguments. # # sbStart must be a TjTime of the scoreboard start # slotDuration must be the duration of the scoreboard slots in seconds # a and b should be TjTime or Integer objects that describe the start and # end time or index of the interval. # # TimeInterval.new(iv) # TimeInterval.new(sbStart, slotDuration, a) # TimeInterval.new(sbStart, slotDuration, a, b) # def initialize(*args) case args.length when 1 # If there is only one argument, it must be a ScoreboardInterval. if args[0].is_a?(ScoreboardInterval) @sbStart = args[0].sbStart @slotDuration = args[0].slotDuration # Just one argument, a TimeInterval super(args[0].start, args[0].end) else raise ArgumentError, "Illegal argument 1: #{args[0].class}" end when 3 @sbStart = args[0] @slotDuration = args[1] # If the third argument is a date we convert it to a scoreboard index. args[2] = dateToIndex(args[2]) if args[2].is_a?(TjTime) if args[2].is_a?(Integer) super(args[2], args[2]) else raise ArgumentError, "Illegal argument 3: #{args[0].class}" end when 4 @sbStart = args[0] @slotDuration = args[1] # If the third and forth arguments are a date we convert them to a # scoreboard index. args[2] = dateToIndex(args[2]) if args[2].is_a?(TjTime) args[3] = dateToIndex(args[3]) if args[3].is_a?(TjTime) if !(args[2].is_a?(Integer)) raise ArgumentError, "Interval start must be an index or TjTime, " + "not a #{args[2].class}" end if !(args[3].is_a?(Integer)) raise ArgumentError, "Interval end must be an index or TjTime, " + "not a #{args[3].class}" end super(args[2], args[3]) else raise ArgumentError, "Wrong number of arguments: #{args.length}" end unless @sbStart.is_a?(TjTime) raise ArgumentError, "sbStart must be a TjTime object, not a" + "#{@sbStart.class}" end unless @slotDuration.is_a?(Integer) raise ArgumentError, "slotDuration must be an Integer, not a " + "#{@slotDuration.class}" end end # Assign the start of the interval. +arg+ can be an Integer or # TjTime object. def start=(arg) case arg when Integer @start = arg when TjTime @start = dateToIndex(arg) else raise ArgumentError, "Unsupported class #{arg.class}" end end # Assign the start of the interval. +arg+ can be an Integer or # TjTime object. def end=(arg) case arg when Integer @end = arg when TjTime @end = dateToIndex(arg) else raise ArgumentError, "Unsupported class #{arg.class}" end end # Return the interval start as TjTime object. def startDate indexToDate(@start) end # Return the interval end as TjTime object. def endDate indexToDate(@end) end # Return the duration of the ScoreboardInterval. def duration indexToDate(@end) - indexToDate(@start) end # Turn the ScoreboardInterval into a human readable form. def to_s indexToDate(@start).to_s + ' - ' + indexToDate(@end).to_s end private def dateToIndex(date) (date - @sbStart).to_i / @slotDuration end def indexToDate(index) @sbStart + (index * @slotDuration) end end end TaskJuggler-3.8.1/lib/taskjuggler/IntervalList.rb000066400000000000000000000061221473026623400220120ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = IntervalList.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/Interval' class TaskJuggler # A list of Intervals. The intervals in the list must not overlap and must # be in ascending order. class IntervalList < Array alias append << def &(list) res = IntervalList.new si = li = 0 while si < length && li < list.length do if self[si].start < list[li].start # The current Interval of self starts earlier than the current # Interval of list. if self[si].end <= list[li].start # self[si] does not overlap with list[li]. Ignore it. si += 1 elsif self[si].end < list[li].end # self[si] does overlap with list[li] but list[li] goes further res << self[si].class.new(list[li].start, self[si].end) si += 1 else # self[si] does overlap with list[li] but self[si] goes further res << self[si].class.new(list[li].start, list[li].end) li += 1 end elsif list[li].start < self[si].start # The current Interval of list starts earlier than the current # Interval of self. if list[li].end <= self[si].start # list[li] does not overlap with self[si]. Ignore it. li += 1 elsif list[li].end < self[si].end # list[li] does overlap with self[si] but self[si] goes further res << self[si].class.new(self[si].start, list[li].end) li += 1 else # list[li] does overlap with self[si] but list[li] goes further res << self[si].class.new(self[si].start, self[si].end) si += 1 end else # self[si].start and list[li].start are identical if self[si].end == list[li].end # self[si] and list[li] are identical. Add the Interval and # increase both pointers. res << self[si] li += 1 si += 1 elsif self[si].end < list[li].end # self[si] ends earlier. res << self[si] si += 1 else # list[li] ends earlier. res << list[li] li += 1 end end end res end # Append the Interval _iv_. If the start of _iv_ matches the end of the # list list item, _iv_ is merged with the last item. def <<(iv) if last if last.end > iv.start raise "Intervals may not overlap and must be added in " + "ascending order." elsif last.end == iv.start self[-1] = last.class.new(last.start, iv.end) return self end end append(iv) end end end TaskJuggler-3.8.1/lib/taskjuggler/Journal.rb000066400000000000000000000644171473026623400210170ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = Journal.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/MessageHandler' class TaskJuggler # A JournalEntry stores some RichText strings to describe a status or a # property of the project at a certain point in time. Additionally, the # entry can contain a reference to a Resource as author and an alert level. # The text is structured in 3 different elements, a headline, a short # summary and a longer text segment. The headline is mandatory, the # summary and details sections are optional. class JournalEntry attr_reader :date, :headline, :property, :sourceFileInfo attr_accessor :author, :moderators, :summary, :details, :alertLevel, :flags, :timeSheetRecord # Create a new JournalEntry object. def initialize(journal, date, headline, property, sourceFileInfo = nil) # A reference to the Journal object this entry belongs to. @journal = journal # The date of the entry. @date = date # A very short description. Should not be longer than about 40 # characters. @headline = headline # A reference to a PropertyTreeNode object. @property = property # Source file location of this entry of type SourceFileInfo @sourceFileInfo = sourceFileInfo # A reference to a Resource. @author = nil # A list of Resource objects that have moderated this entry. @moderators = [] # An introductory or summarizing RichText paragraph. @summary = nil # A RichText of arbitrary length. @details = nil # The alert level. @alertLevel = 0 # A list of flags. @flags = [] # A reference to a time sheet record that was used to create this # JournalEntry object. @timeSheetRecord = nil # Add the new entry to the journal. @journal.addEntry(self) end # Convert the entry into a RichText string. The formatting is controlled # by the Query parameters. def to_rText(query) # We use the alert level a sortable and numerical result. if query.journalAttributes.include?('alert') levelRecord = query.project['alertLevels'][alertLevel] if query.selfContained alertName = "[" + "#{levelRecord.name}]" else alertName = "[[File:icons/flag-#{levelRecord.id}.png|" + "alt=[#{levelRecord.name}]|text-bottom]] " end else alertName = '' end # The String that will hold the result as RichText markup. rText = '' # Markup to use for headlines. hlMark = '===' if query.journalAttributes.include?('property') && @property if @property.is_a?(Task) # Include the alert level, task name and ID. rText += "#{hlMark} #{alertName} #{@property.name}" if query.journalAttributes.include?('propertyid') rText += " (ID: #{@property.fullId})" end rText += " #{hlMark}\n\n" if query.journalAttributes.include?('timesheet') && @timeSheetRecord # Include the reported time sheet data for this task. rText += "'''Work:''' #{@timeSheetRecord.actualWorkPercent.to_i}% " if @timeSheetRecord.actualWorkPercent != @timeSheetRecord.planWorkPercent rText += "(#{@timeSheetRecord.planWorkPercent.to_i}%) " end if @timeSheetRecord.remaining rText += "'''Remaining:''' #{@timeSheetRecord.actualRemaining}d " if @timeSheetRecord.actualRemaining != @timeSheetRecord.planRemaining rText += "(#{@timeSheetRecord.planRemaining}d) " end else rText += "'''End:''' " + "#{@timeSheetRecord.actualEnd.to_s(query.timeFormat)} " if @timeSheetRecord.actualEnd != @timeSheetRecord.planEnd rText += "(#{@timeSheetRecord.planEnd.to_s(query.timeFormat)}) " end end rText += "\n\n" end elsif !(@timeSheetRecord = @timeSheetRecord).nil? && @timeSheetRecord.task.is_a?(String) # There is only an entry in the timesheet, but we don't have a # corresponding Task in the Project. This must be a new task created # by the timesheet submitter. rText += "#{hlMark} #{alertName} [New Task] " + "#{@timeSheetRecord.name}" if query.journalAttributes.include?('propertyid') rText += " (ID: #{@timeSheetRecord.task})" end rText += " #{hlMark}\n\n" if query.journalAttributes.include?('timesheet') && @timeSheetRecord # We don't have any plan data since it's a new task. Just include # the reported time sheet actuals. rText += "'''Work:''' #{@timeSheetRecord.actualWorkPercent}% " if @timeSheetRecord.remaining rText += "'''Remaining:''' #{@timeSheetRecord.actualRemaining}d " else rText += "'''End:''' " + "#{@timeSheetRecord.actualEnd.to_s(query.timeFormat)} " end rText += "\n\n" end else # Property must be a Resource rText += "#{hlMark} #{alertName} Personal Notes #{hlMark}\n\n" end # We've shown the alert now. Don't show it again with the headline. alertName = '' # Increase level for subsequent headlines. hlMark += '=' end if query.journalAttributes.include?('headline') rText += "#{hlMark} #{alertName}" + @headline + " #{hlMark}\n\n" end showDate = query.journalAttributes.include?('date') showAuthor = query.journalAttributes.include?('author') && @author if showDate || showAuthor rText += "''Reported " end if showDate rText += "on #{@date.to_s(query.timeFormat)} " end if showAuthor rText += "by #{@author.name}" end rText += "''\n\n" if showDate || showAuthor if query.journalAttributes.include?('flags') && !@flags.empty? rText += "''Flags:'' #{@flags.join(', ')}\n\n" end if query.journalAttributes.include?('summary') && @summary rText += @summary.richText.inputText + "\n\n" end if query.journalAttributes.include?('details') && @details rText += @details.richText.inputText + "\n\n" end rText end # Just for debugging def to_s # :nodoc: "Headline: #{@headline}\nProperty: #{@property.class}: #{@property.fullId}" end end # The JournalEntryList is an Array with a twist. Before any data retrieval # function is called, the list of JournalEntry objects will be sorted by # date. This is a utility class only. Use Journal to store a journal. class JournalEntryList attr_reader :entries JournalEntryList::SortingAttributes = [ :alert, :date, :seqno ] def initialize @entries = [] @sorted = false @sortBy = [ [ :date, 1 ], [ :alert, 1 ], [ :seqno, 1 ] ] end def setSorting(by) by.each do |attr, direction| unless SortingAttributes.include?(attr) raise ArgumentError, "Unknown attribute #{attr}" end if (direction != 1) && (direction != -1) raise ArgumentError, "Unknown direction #{direction}" end end @sortBy = by end # Return the number of entries. def count @entries.length end # Add a new JournalEntry to the list. The list will be marked as unsorted. def <<(entry) @entries << entry @sorted = false end # Add a list of JournalEntry objects to the existing list. The list will # be marked unsorted. def +(list) @entries += list.entries @sorted = false self end # Return the _index_-th entry. def[](index) sort! @entries[index] end # The well known iterator. The list will be sorted first. def each sort! @entries.each do |entry| yield entry end end # Like Array::delete def delete(e) @entries.delete(e) end # Like Array::delete_if def delete_if @entries.delete_if { |e| yield(e) } end # Like Array::empty? def empty? @entries.empty? end # Like Array:length def length @entries.length end # Like Array::include? def include?(entry) @entries.include?(entry) end # Like Array::first but list is first sorted. def first sort! @entries.first end # Returns the last elements (by date) if date is nil or the last elements # right before the given _date_. If there are multiple entries with # exactly the same date, all are returned. Otherwise the result Array will # only contain one element. In case no matching entry is found, the Array # will be empty. def last(date = nil) result = JournalEntryList.new sort! @entries.reverse_each do |e| if result.empty? # We haven't found any yet. So add the first one we find before the # cut-off date. result << e if e.date <= date elsif result.first.date == e.date # Now we only accept other entries with the exact same date. result << e else # We've found all entries we are looking for. break end end result.sort! end # Sort the list of entries. First by ascending by date, than by alertLevel # and finally by PropertyTreeNode sequence number. def sort! if block_given? @entries.sort! { |a, b| yield(a, b) } else return self if @sorted @entries.sort! do |a, b| res = 0 @sortBy.each do |attr, direction| res = case attr when :date a.date <=> b.date when :alert a.alertLevel <=> b.alertLevel when :seqno a.property.sequenceNo <=> b.property.sequenceNo end * direction break if res != 0 end res end end @sorted = true self end # Eliminate duplicate entries. def uniq! @entries.uniq! end end # A Journal is a list of JournalEntry objects. It provides methods to add # JournalEntry objects and retrieve specific entries or other processed # information. class Journal include MessageHandler # Create a new Journal object. def initialize # This list holds all entries. @entries = JournalEntryList.new # This hash holds a list of entries for each property. @propertyToEntries = {} end # Add a new JournalEntry to the Journal. def addEntry(entry) return if @entries.include?(entry) @entries << entry return if entry.property.nil? # When we store the property into the @propertyToEntries hash, we need # to make sure that we store the PropertyTreeNode object and not a # PTNProxy object. unless @propertyToEntries.include?(entry.property.ptn) @propertyToEntries[entry.property.ptn] = JournalEntryList.new end @propertyToEntries[entry.property.ptn] << entry end def getEntries(property) @propertyToEntries[property.ptn] end # Delete all entries of the Journal for which the block yields true. def delete_if @entries.delete_if do |e| res = yield(e) @propertyToEntries[e.property.ptn].delete(e) if res res end end def to_rti(query) entries = JournalEntryList.new case query.journalMode when :journal # This is the regular journal. It contains all journal entries that # are dated in the query interval. If a property is given, only # entries of this property are included. if query.property if query.property.is_a?(Task) entries = entriesByTask(query.property, query.start, query.end, query.hideJournalEntry) elsif query.property.is_a?(Resource) entries = entriesByResource(query.property, query.start, query.end, query.hideJournalEntry) end else entries = self.entries(query.start, query.end, query.hideJournalEntry) end when :journal_sub # This mode also contains all journal entries that are dated in the # query interval. A property must be given and only entries of this # property and all its children are included. if query.property.is_a?(Task) entries = entriesByTaskR(query.property, query.start, query.end, query.hideJournalEntry) end when :status_up # In this mode only the last entries before the query end date for # each task are included. An entry is not included if any of the # parent tasks has a more recent entry that is still before the query # end date. if query.property if query.property.is_a?(Task) entries += currentEntries(query.end, query.property, 0, query.start, query.hideJournalEntry) end else query.project.tasks.each do |task| # We only care about top-level tasks. next if task.parent entries += currentEntries(query.end, task, 0, query.start, query.hideJournalEntry) # Eliminate duplicates due to entries from adopted tasks entries.uniq! end end when :status_down, :status_dep # In this mode only the last entries before the query end date for # each task (incl. sub tasks) are included. if query.property if query.property.is_a?(Task) entries += currentEntriesR(query.end, query.property, 0, query.start, query) end else query.project.tasks.each do |task| # We only care about top-level tasks. next if task.parent entries += currentEntriesR(query.end, task, 0, query.start, query) # Eliminate duplicates due to entries from adopted tasks entries.uniq! end end when :alerts_down, :alerts_dep # In this mode only the last entries before the query end date for # each task (incl. sub tasks) and only the ones with the highest alert # level are included. if query.property if query.property.is_a?(Task) entries += alertEntries(query.end, query.property, 1, query.start, query) end else query.project.tasks.each do |task| # We only care about top-level tasks. next if task.parent entries += alertEntries(query.end, task, 1, query.start, query) # Eliminate duplicates due to entries from adopted tasks entries.uniq! end end else raise "Unknown jourmal mode: #{query.journalMode}" end # Sort entries according to the user specified sorting criteria. entries.setSorting(query.sortJournalEntries) entries.sort! # The components of the message are either UTF-8 text or RichText. For # the RichText components, we use the originally provided markup since # we compose the result as RichText markup first. rText = '' entries.each do |entry| rText += entry.to_rText(query) end # Now convert the RichText markup String into RichTextIntermediate # format. unless (rti = RichText.new(rText, RTFHandlers.create(query.project)). generateIntermediateFormat) warning('ptn_journal', "Syntax error in journal: #{rText}") return nil end # No section numbers, please! rti.sectionNumbers = false # We use a special class to allow CSS formating. rti.cssClass = 'tj_journal' query.rti = rti end # Return a list of all JournalEntry objects for the given _resource_ that # are dated between _startDate_ and _endDate_, are not hidden by their # flags matching _logExp_, are for Task _task_ and have at least the alert # level _alertLevel. If an optional parameter is nil, it always matches # the entry. def entriesByResource(resource, startDate = nil, endDate = nil, logExp = nil, task = nil, alertLevel = nil) list = JournalEntryList.new @entries.each do |entry| if entry.author == resource.ptn && (startDate.nil? || entry.date > startDate) && (endDate.nil? || entry.date <= endDate) && (task.nil? || entry.property == task.ptn) && (alertLevel.nil? || entry.alertLevel >= alertLevel) && !entry.headline.empty? && !hidden(entry, logExp) list << entry end end list end # Return a list of all JournalEntry objects for the given _task_ that are # dated between _startDate_ and _endDate_ (end date not included), are not # hidden by their flags matching _logExp_ are from Author _resource_ and # have at least the alert level _alertLevel. If an optional parameter is # nil, it always matches the entry. def entriesByTask(task, startDate = nil, endDate = nil, logExp = nil, resource = nil, alertLevel = nil) list = JournalEntryList.new @entries.each do |entry| if entry.property == task.ptn && (startDate.nil? || entry.date >= startDate) && (endDate.nil? || entry.date < endDate) && (resource.nil? || entry.author == resource) && (alertLevel.nil? || entry.alertLevel >= alertLevel) && !entry.headline.empty? && !hidden(entry, logExp) list << entry end end list end # Return a list of all JournalEntry objects for the given _task_ or any of # its sub tasks that are dated between _startDate_ and _endDate_, are not # hidden by their flags matching _logExp_, are from Author _resource_ and # have at least the alert level _alertLevel. If an optional parameter is # nil, it always matches the entry. def entriesByTaskR(task, startDate = nil, endDate = nil, logExp = nil, resource = nil, alertLevel = nil) list = entriesByTask(task, startDate, endDate, logExp, resource, alertLevel) task.kids.each do |t| list += entriesByTaskR(t, startDate, endDate, logExp, resource, alertLevel) end list end def entries(startDate = nil, endDate = nil, logExp = nil, property = nil, alertLevel = nil) list = JournalEntryList.new @entries.each do |entry| if (startDate.nil? || startDate <= entry.date) && (endDate.nil? || endDate >= entry.date) && (property.nil? || property.ptn == entry.property || entry.property.isChildOf?(property.ptn)) && (alertLevel.nil? || alertLevel == entry.alertLevel) && !hidden(entry, logExp) list << entry end end list end # Determine the alert level for the given _property_ at the given _date_. # If the property does not have any JournalEntry objects or they are out # of date compared to the child properties, the level is computed based on # the highest level of the children. Only take the entries that are not # filtered by _query_.hideJournalEntry into account. def alertLevel(date, property, query) maxLevel = 0 # Gather all the current (as of the specified _date_) JournalEntry # objects for the property and than find the highest level. currentEntriesR(date, property, 0, nil, query).each do |e| maxLevel = e.alertLevel if maxLevel < e.alertLevel end maxLevel end # Return the list of JournalEntry objects that are dated at or before # _date_, are for _property_ or any of its childs, have at least _level_ # alert and are after _minDate_. We only return those entries with the # highest overall alert level. def alertEntries(date, property, minLevel, minDate, query) maxLevel = 0 entries = [] # Gather all the current (as of the specified _date_) JournalEntry # objects for the property and than find the highest level. currentEntriesR(date, property, minLevel, minDate, query).each do |e| if maxLevel < e.alertLevel maxLevel = e.alertLevel entries = [ e ] elsif maxLevel == e.alertLevel entries << e end end entries end # This function returns a list of entries that have all the exact same # date and are the last entries before the deadline _date_. Only messages # with at least the required alert level _minLevel_ are returned. Messages # with alert level _minLevel_ or higher must be newer than _minDate_. def currentEntries(date, property, minLevel, minDate, logExp) pEntries = getEntries(property) ? getEntries(property).last(date) : JournalEntryList.new # Remove entries below the minium alert level or before the timeout # date. pEntries.delete_if do |e| e.headline.empty? || e.alertLevel < minLevel || (e.alertLevel >= minLevel && minDate && e.date < minDate) end unless pEntries.empty? # Check parents for a more important or more up-to-date message. p = property.parent while p do ppEntries = getEntries(p) ? getEntries(p).last(date) : JournalEntryList.new # A parent has a more up-to-date message. if !ppEntries.empty? && ppEntries.first.date >= pEntries.first.date return JournalEntryList.new end p = p.parent end end # Remove all entries that are filtered by logExp. if logExp pEntries.delete_if { |e| hidden(e, logExp) } end pEntries end # This function recursively traverses a tree of PropertyTreeNode objects # from bottom to top. It returns the last entries before _date_ for each # property unless there is a property in the sub-tree specified by the # root _property_ with more up-to-date entries. The result is a # JournalEntryList. def currentEntriesR(date, property, minLevel, minDate, query) DataCache.instance.cached(self, :currentEntriesR, date, property, minLevel, minDate, query) do # See if this property has any current JournalEntry objects. pEntries = getEntries(property) ? getEntries(property).last(date) : JournalEntryList.new # Remove entries below the minium alert level or before the timeout # date. pEntries.delete_if do |e| e.headline.empty? || e.alertLevel < minLevel || (e.alertLevel == minLevel && minDate && e.date < minDate) end # Determine the highest alert level of the pEntries. maxPAlertLevel = 0 pEntries.each do |e| maxPAlertLevel = e.alertLevel if e.alertLevel > maxPAlertLevel end cEntries = JournalEntryList.new latestDate = nil maxAlertLevel = 0 # If we have an entry from this property, we only care about child # entries that are from a later date. minDate = pEntries.first.date + 1 unless pEntries.empty? # Now gather all current entries of the child properties and find the # date that is closest to and right before the given _date_. property.kids.each do |p| currentEntriesR(date, p, minLevel, minDate, query).each do |e| # Find the date of the most recent entry. latestDate = e.date if latestDate.nil? || e.date > latestDate # Find the highest alert level. maxAlertLevel = e.alertLevel if e.alertLevel > maxAlertLevel cEntries << e unless cEntries.include?(e) end end # Only Task properties have dependencies. if (query.journalMode == :status_dep || query.journalMode == :alerts_dep) && property.is_a?(Task) # Now gather all current entries of the dependency properties and find # the date that is closest to and right before the given _date_. property['startpreds', query.scenarioIdx].each do |p, onEnd| # We only follow end->start dependencies. next unless onEnd currentEntriesR(date, p, minLevel, minDate, query).each do |e| # Find the date of the most recent entry. latestDate = e.date if latestDate.nil? || e.date > latestDate # Find the highest alert level. maxAlertLevel = e.alertLevel if e.alertLevel > maxAlertLevel cEntries << e unless cEntries.include?(e) end end end if !pEntries.empty? && (maxPAlertLevel > maxAlertLevel || latestDate.nil? || pEntries.first.date >= latestDate) # If no child property has a more current JournalEntry or one with a # higher alert level than this property and this property has # JournalEntry objects, than those are taken. entries = pEntries else # Otherwise we take the entries from the kids. entries = cEntries end # Remove all entries that are filtered by query.hideJournalEntry. if query.hideJournalEntry entries.delete_if { |e| hidden(e, query.hideJournalEntry) } end # Otherwise return the list provided by the childen. entries end end private def hidden(entry, logExp) logExp.nil? ? false : logExp.eval(entry) end end end TaskJuggler-3.8.1/lib/taskjuggler/KateSyntax.rb000066400000000000000000000162771473026623400215010ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = KateSyntax.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/SyntaxReference' class TaskJuggler # This class is a generator for Kate (http://kate-editor.org/) TaskJuggler syntax # highlighting files. class KateSyntax # Create a generator object. def initialize # Create a syntax reference for all current keywords. @reference = SyntaxReference.new(nil, true) @properties = [] @attributes = [] @reference.keywords.each_value do |kw| if kw.isProperty? @properties << kw else @attributes << kw end end @file = nil end # Generate the Kate syntax file into _file_. def generate(file) @file = File.open(file, 'w') header keywords contexts highlights footer @file.close end private def header # Generate the header section. Mostly consists of comments and # description attributes @file.write <<"EOT" EOT end def footer # Generate the footer section. Mostly consists of closing tags. @file.write <<"EOT" EOT end def contexts @file.write <<'EOT' EOT #syn match tjparg contained /\${.*}/ end def keywords @file.puts "" %w( macro project supplement include supplement ).each do |kw| @file.puts " #{kw} " end @file.puts "" # Property keywords @file.puts "" @properties.each do |kw| kw.names.each do |name| # Ignore the 'supplement' entries. They are not real properties. next if name == 'supplement' @file.puts " #{name} " end end @file.puts "" # Attribute keywords @file.puts "" @attributes.each do |kw| next if %w( resourcereport taskreport textreport ).include?(kw.keyword) single = kw.names.length == 1 kw.names.each do |name| break if [ '%', '(', '~', 'include', 'macro', 'project', 'supplement' ].include?(name) @file.puts " #{name} " end end @file.puts "" end def highlights @file.write <<'EOT' EOT end end end TaskJuggler-3.8.1/lib/taskjuggler/KeywordArray.rb000066400000000000000000000015141473026623400220150ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = KeywordArray.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # class TaskJuggler # This class is a specialized version of Array. It stores a list of # keywords as String objects. The entry '*' is special. It means all # keywords of a particular set are included. '*' must be the first entry if # it is present. class KeywordArray < Array alias a_include? include? def include?(keyword) (self[0] == '*') || a_include?(keyword) end end end TaskJuggler-3.8.1/lib/taskjuggler/KeywordDocumentation.rb000066400000000000000000000610141473026623400235510ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = KeywordDocumentation.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'term/ansicolor' require 'taskjuggler/MessageHandler' require 'taskjuggler/HTMLDocument' require 'taskjuggler/RichText' require 'taskjuggler/TjpExample' require 'taskjuggler/TextFormatter' require 'taskjuggler/Project' class TaskJuggler # The textual TaskJuggler Project description consists of many keywords. The # parser has built-in support to document the meaning and usage of these # keywords. Most keywords are unique, but there can be exceptions. To # resolve ambiguoties the keywords can be prefixed by a scope. The scope is # usually a keyword that describes the context that the ambiguous keyword is # used in. This class stores the keyword, the corresponding # TextParser::Pattern and the context that the keyword is used in. It also # stores information such as the list of optional attributes (keywords used # in the context of the current keyword) and whether the keyword is scenario # specific or not. class KeywordDocumentation include HTMLElements include Term::ANSIColor include MessageHandler attr_reader :keyword, :names, :pattern, :references, :optionalAttributes attr_accessor :contexts, :scenarioSpecific, :inheritedFromProject, :inheritedFromParent, :predecessor, :successor # Construct a new KeywordDocumentation object. _rule_ is the # TextParser::Rule and _pattern_ is the corresponding TextParser::Pattern. # _syntax_ is an expanded syntax representation of the _pattern_. _args_ # is an Array of TextParser::TokenDoc that describe the arguments of the # _pattern_. _optAttrPatterns_ is an Array with references to # TextParser::Patterns that are optional attributes to this keyword. def initialize(rule, pattern, syntax, args, optAttrPatterns, manual) @rule = rule @pattern = pattern # The unique identifier. Usually the attribute or property name. To # disambiguate a . can be added. @keyword = pattern.keyword # Similar to @keyword, but without the scope. Since there could be # several, this is an Array of String objects. @names = [] @syntax = syntax @args = args @manual = manual # Hash that maps patterns of optional attributes to a boolean value. It # is true if the pattern is a scenario specific attribute. @optAttrPatterns = optAttrPatterns # The above hash is later converted into a list that points to the # keyword documentation of the optional attribute. @optionalAttributes = [] @scenarioSpecific = false @inheritedFromProject= false @inheritedFromParent = false @contexts = [] @seeAlso = [] # The following are references to the neighboring keyword in an # alphabetically sorted list. @predecessor = nil @successor = nil # Array to collect all references to other RichText objects. @references = [] end # Returns true of the KeywordDocumentation is documenting a TJP property # (task, resources, etc.). A TJP property can be nested. def isProperty? # I haven't found a good way to automatically detect all the various # report types as properties. They don't directly include themselves as # attributes. return true if %w( accountreport export nikureport resourcereport taskreport textreport timesheetreport statussheetreport).include?(keyword) @optionalAttributes.include?(self) end # Returns true of the keyword can be used outside of any other keyword # context. def globalScope? return true if @contexts.empty? @contexts.each do |context| return true if context.keyword == 'properties' end false end # Post process the class member to set cross references to other # KeywordDocumentation items. def crossReference(keywords, rules) # Get the attribute or property name of the Keyword. This is not unique # like @keyword since it's got no scope. @pattern.terminalTokens(rules).each do |tok| # Ignore patterns that don't have a real name. break if tok[0] == '{' @names << tok[0] end # Some arguments are references to other patterns. The current keyword # is added as context to such patterns. @args.each do |arg| if arg.pattern && checkReference(arg.pattern) kwd = keywords[arg.pattern.keyword] kwd.contexts << self unless kwd.contexts.include?(self) end end # Optional attributes are treated similarly. In addition we add them to # the @optionalAttributes list of this keyword. @optAttrPatterns.each do |pattern, scenarioSpecific| next unless checkReference(pattern) # Check if all the attributes are documented. We ignore undocumented # keywords that are deprecated or removed. if (kwd = keywords[pattern.keyword]).nil? unless [ :deprecated, :removed ].include?(pattern.supportLevel) token = pattern.terminalTokens(rules) $stderr.puts "Keyword #{keyword} has undocumented optional " + "attribute #{token[0]}" end else @optionalAttributes << kwd kwd.contexts << self unless kwd.contexts.include?(self) kwd.scenarioSpecific = true if scenarioSpecific end end # Resolve the seeAlso patterns to keyword references. @pattern.seeAlso.sort.each do |also| if keywords[also].nil? raise "See also reference #{also} of #{@pattern} is unknown" end @seeAlso << keywords[also] end end def listAttribute? if (propertySet = findPropertySet) keyword = @keyword keyword = keyword.split('.')[0] if keyword.include?('.') return propertySet.listAttribute?(keyword) end false end def computeInheritance if (propertySet = findPropertySet) keyword = @keyword keyword = keyword.split('.')[0] if keyword.include?('.') @inheritedFromProject = propertySet.inheritedFromProject?(keyword) @inheritedFromParent = propertySet.inheritedFromParent?(keyword) end end # Return the keyword name in a more readable form. E.g. 'foo.bar' is # returned as 'foo (bar)'. 'foo' will remain 'foo'. def title kwTokens = @keyword.split('.') if kwTokens.size == 1 title = @keyword else title = "#{kwTokens[0]} (#{kwTokens[1]})" end title end # Return the complete documentation of this keyword as formatted text # string. def to_s tagW = 13 textW = 79 # Top line with multiple elements str = "#{blue('Keyword:')} #{bold(@keyword)}" + "#{listAttribute? ? ' (List Attribute)' : '' }\n\n" if @pattern.supportLevel != :supported msg = supportLevelMessage if [ :deprecated, :removed ].include?(@pattern.supportLevel) && @seeAlso.length > 0 msg += "\n\nPlease use " alsoStr = '' @seeAlso.each do |also| unless alsoStr.empty? alsoStr += ', ' end alsoStr += also.keyword end msg += "#{alsoStr} instead!" end str += red("Warning: #{format(tagW, msg, textW)}\n") end # Don't show further details if the keyword has been removed. return str if @pattern.supportLevel == :removed str += blue('Purpose:') + " #{format(tagW, newRichText(@pattern.doc).to_s, textW)}\n" if @syntax != '[{ }]' str += blue('Syntax:') + " #{format(tagW, @syntax, textW)}\n" str += blue('Arguments:') + " " if @args.empty? str += format(tagW, "none\n", textW) else argStr = '' @args.each do |arg| argText = newRichText(arg.text || "See '#{arg.name}' for details.").to_s if arg.typeSpec.nil? || ("<#{arg.name}>") == arg.typeSpec indent = arg.name.length + 2 argStr += "#{arg.name}: " + "#{format(indent, argText, textW - tagW)}\n" else typeSpec = arg.typeSpec typeSpec[0] = '[' typeSpec[-1] = ']' indent = arg.name.length + typeSpec.size + 3 argStr += "#{arg.name} #{typeSpec}: " + "#{format(indent, argText, textW - tagW)}\n" end end str += indent(tagW, argStr) end str += "\n" end str += blue('Context:') + ' ' if @contexts.empty? str += format(tagW, 'Global scope', textW) else cxtStr = '' @contexts.each do |context| unless cxtStr.empty? cxtStr += ', ' end cxtStr += context.keyword end str += format(tagW, cxtStr, textW) end str += "\n#{blue('Attributes:')} " if @optionalAttributes.empty? str += "none\n\n" else attrStr = '' @optionalAttributes.sort! do |a, b| a.keyword <=> b.keyword end showLegend = false @optionalAttributes.each do |attr| unless attrStr.empty? attrStr += ', ' end attrStr += attr.keyword if attr.scenarioSpecific || attr.inheritedFromProject || attr.inheritedFromParent first = true showLegend = true tag = '[' if attr.scenarioSpecific tag += 'sc' first = false end if attr.inheritedFromProject tag += ':' unless first tag += 'ig' first = false end if attr.inheritedFromParent tag += ':' unless first tag += 'ip' end tag += ']' attrStr += cyan(tag) end end if showLegend attrStr += "\n\n#{cyan('[sc]')} : Attribute is scenario specific" + "\r#{cyan('[ig]')} : " + "Value can be inherited from global setting" + "\r#{cyan('[ip]')} : " + "Value can be inherited from parent property" end str += format(tagW, attrStr, textW) str += "\n" end unless @seeAlso.empty? str += blue('See also:') + " " alsoStr = '' @seeAlso.each do |also| unless alsoStr.empty? alsoStr += ', ' end alsoStr += also.keyword end str += format(tagW, alsoStr, textW) str += "\n" end # str += "Rule: #{@rule.name}\n" if @rule # str += "Pattern: #{@pattern.tokens.join(' ')}\n" if @pattern str end # Return a String that represents the keyword documentation in an XML # formatted form. def generateHTML(directory) html = HTMLDocument.new head = html.generateHead(keyword, { 'description' => 'The TaskJuggler Manual', 'keywords' => 'taskjuggler, project, management' }) head << @manual.generateStyleSheet html.html << BODY.new do [ @manual.generateHTMLHeader, generateHTMLNavigationBar, DIV.new('style' => 'margin-left:5%; margin-right:5%') do [ generateHTMLKeywordBox, generateHTMLSupportLevel, generateHTMLDescriptionBox, generateHTMLOptionalAttributesBox, generateHTMLExampleBox ] end, generateHTMLNavigationBar, @manual.generateHTMLFooter ] end if directory html.write(directory + "#{keyword}.html") else puts html.to_s end end private def checkReference(pattern) if pattern.keyword.nil? $stderr.puts "Pattern #{pattern} is undocumented but referenced by " + "#{@keyword}." false end true end def indent(width, str) TextFormatter.new(80, width).indent(str)[width..-1] end # Generate the navigation bar. def generateHTMLNavigationBar @manual.generateHTMLNavigationBar( @predecessor ? @predecessor.title : nil, @predecessor ? "#{@predecessor.keyword}.html" : nil, @successor ? @successor.title : nil, @successor ? "#{@successor.keyword}.html" : nil) end # Return a HTML object with a link to the manual page for the keyword. def keywordHTMLRef(parent, keyword) parent << XMLNamedText.new(keyword.title, 'a', 'href' => "#{keyword.keyword}.html") end # This function is primarily a wrapper around the RichText constructor. It # catches all RichTextScanner processing problems and converts the exception # data into an error message. def newRichText(text) rText = RichText.new(text, []) unless (rti = rText.generateIntermediateFormat) error('rich_text', "Error in RichText of rule #{@keyword}") end @references += rti.internalReferences rti end # Utility function to turn a list of keywords into a comma separated list # of HTML references to the files of these keywords. All embedded in a # table cell element. _list_ is the KeywordDocumentation list. _width_ is # the percentage width of the cell. def listHTMLAttributes(list, width) td = XMLElement.new('td', 'class' => 'descr', 'style' => "width:#{width}%") first = true list.each do |attr| if first first = false else td << XMLText.new(', ') end keywordHTMLRef(td, attr) end td end def format(indent, str, width) TextFormatter.new(width, indent).format(str)[indent..-1] end def generateHTMLSupportLevel if @pattern.supportLevel != :supported [ P.new do newRichText("#{supportLevelMessage}").to_html end, [ :deprecated, :removed ].include?(@pattern.supportLevel) ? (P.new { useInsteadMessage }) : nil ] else nil end end def generateHTMLKeywordBox # Box with keyword name. P.new do TABLE.new('align' => 'center', 'class' => 'table') do TR.new('align' => 'left') do [ TD.new({ 'class' => 'tag', 'style' => 'width:16%'}) { 'Keyword' }, TD.new({ 'class' => 'descr', 'style' => 'width:84%' }) do [ B.new() { title }, listAttribute? ? A.new({ 'href' => "List_Attributes.html" }) { "List Attribute" } : "" ] end ] end end end end def generateHTMLDescriptionBox return nil if @pattern.supportLevel == :removed # Box with purpose, syntax, arguments and context. P.new do TABLE.new({ 'align' => 'center', 'class' => 'table' }) do [ COLGROUP.new do [ COL.new('width' => '16%'), COL.new('width' => '24%'), COL.new('width' => '60%') ] end, generateHTMLPurposeLine, generateHTMLSyntaxLine, generateHTMLArgumentsLine, generateHTMLContextLine, generateHTMLAlsoLine ] end end end def generateHTMLPurposeLine generateHTMLTableLine('Purpose', newRichText(@pattern.doc).to_html) end def generateHTMLSyntaxLine if @syntax != '[{ }]' generateHTMLTableLine('Syntax', CODE.new { @syntax }) end end def generateHTMLArgumentsLine return nil unless @syntax != '[{ }]' if @args.empty? generateHTMLTableLine('Arguments', 'none') else rows = [] first = true @args.each do |arg| if first col1 = 'Arguments' col1rows = @args.length first = false else col1 = col1rows = nil end if arg.typeSpec.nil? || ('<' + arg.name + '>') == arg.typeSpec col2 = "#{arg.name}" else typeSpec = arg.typeSpec typeName = typeSpec[1..-2] typeSpec[0] = '[' typeSpec[-1] = ']' col2 = [ "#{arg.name} [", A.new('href' => "The_TaskJuggler_Syntax.html" + "\##{typeName}") { typeName }, ']' ] end col3 = newRichText(arg.text || "See [[#{arg.name}]] for details.").to_html rows << generateHTMLTableLine(col1, col2, col3, col1rows) end rows end end def generateHTMLContextLine descr = [] @contexts.each do |c| next if [ :deprecated, :removed ].include?(c.pattern.supportLevel) descr << ', ' unless descr.empty? descr << A.new('href' => "#{c.keyword}.html") { c.title } end if descr.empty? descr = A.new('href' => 'Getting_Started.html#Structure_of_a_TJP_File') do 'Global scope' end end generateHTMLTableLine('Context', descr) end def generateHTMLAlsoLine unless @seeAlso.empty? descr = [] @seeAlso.each do |a| next if [ :deprecated, :removed ].include?(a.pattern.supportLevel) descr << ', ' unless descr.empty? descr << A.new('href' => "#{a.keyword}.html") { a.title } end generateHTMLTableLine('See also', descr) end end def generateHTMLTableLine(col1, col2, col3 = nil, col1rows = nil) return nil if @pattern.supportLevel == :removed TR.new('align' => 'left') do columns = [] attrs = { 'class' => 'tag' } attrs['rowspan'] = col1rows.to_s if col1rows columns << TD.new(attrs) { col1 } if col1 attrs = { 'class' => 'descr' } attrs['colspan'] = '2' unless col3 columns << TD.new(attrs) { col2 } columns << TD.new('class' => 'descr') { col3 } if col3 columns end end def generateHTMLOptionalAttributesBox return nil if @pattern.supportLevel == :removed # Box with attributes. unless @optionalAttributes.empty? @optionalAttributes.sort! do |a, b| a.keyword <=> b.keyword end showDetails = false @optionalAttributes.each do |attr| if attr.scenarioSpecific || attr.inheritedFromProject || attr.inheritedFromParent showDetails = true break end end P.new do TABLE.new('align' => 'center', 'class' => 'table') do if showDetails # Table of all attributes with checkmarks for being scenario # specific, inherited from parent and inherited from global # scope. rows = [] rows << COLGROUP.new do [ 16, 24, 20, 20, 20 ].map { |p| COL.new('width' => "#{p}%") } end rows << TR.new('align' => 'left') do [ TD.new('class' => 'tag', 'rowspan' => "#{@optionalAttributes.length + 1}") do 'Attributes' end, TD.new('class' => 'tag') { 'Name' }, TD.new('class' => 'tag') { 'Scen. spec.' }, TD.new('class' => 'tag') { 'Inh. fm. Global' }, TD.new('class' => 'tag') { 'Inh. fm. Parent' } ] end @optionalAttributes.each do |attr| if [ :deprecated, :removed ].include?(attr.pattern.supportLevel) next end rows << TR.new('align' => 'left') do [ TD.new('align' => 'left', 'class' => 'descr') do A.new('href' => "#{attr.keyword}.html") { attr.title } end, TD.new('align' => 'center', 'class' => 'descr') do 'x' if attr.scenarioSpecific end, TD.new('align' => 'center', 'class' => 'descr') do 'x' if attr.inheritedFromProject end, TD.new('align' => 'center', 'class' => 'descr') do 'x' if attr.inheritedFromParent end ] end end rows else # Comma separated list of all attributes. TR.new('align' => 'left') do [ TD.new('class' => 'tag', 'style' => 'width:16%') do 'Attributes' end, TD.new('class' => 'descr', 'style' => 'width:84%') do list = [] @optionalAttributes.each do |attr| if [ :deprecated, :removed ]. include?(attr.pattern.supportLevel) next end list << ', ' unless list.empty? list << A.new('href' => "#{attr.keyword}.html") do attr.title end end list end ] end end end end end end def generateHTMLExampleBox return nil if @pattern.supportLevel == :removed if @pattern.exampleFile exampleDir = File.join(AppConfig.dataDirs('test')[0], 'TestSuite', 'Syntax', 'Correct') example = TjpExample.new fileName = "#{exampleDir}/#{@pattern.exampleFile}.tjp" example.open(fileName) unless (text = example.to_s(@pattern.exampleTag)) raise "There is no tag '#{@pattern.exampleTag}' in file " + "#{fileName}." end DIV.new('class' => 'codeframe') do PRE.new('class' => 'code') { text } end end end def supportLevelMessage case @pattern.supportLevel when :experimental "This keyword is currently in an experimental state. " + "The implementation is probably still incomplete and " + "use of this keyword may lead to wrong results. Do not " + "use this keyword unless you were specifically directed " + "by the developers to try it." when :beta "This keyword has not yet been fully tested yet. You are " + "welcome to try it, but it may lead to wrong results. " + "The syntax may still change with future versions. " + "The developers appreciate any feedback on this keyword." when :deprecated "This keyword should no longer be used. It will be removed " + "in future versions of this software." when :removed "This keyword is no longer supported." end end def useInsteadMessage return nil if @seeAlso.empty? descr = [ 'Use ' ] @seeAlso.each do |a| descr << ', ' unless descr.length <= 1 descr << A.new('href' => "#{a.keyword}.html") { a.title } end descr << " instead." end def findPropertySet property = nil @contexts.each do |kwd| if %w( task resource account shift scenario accountreport resourcereport taskreport textreport ). include?(kwd.keyword) property = kwd.keyword break end end if property project = Project.new('id', 'dummy', '1.0') case property when 'task' project.tasks when 'resource' project.resources when 'account' project.accounts when 'shift' project.shifts when 'scenario' project.scenarios else project.reports end else nil end end end end TaskJuggler-3.8.1/lib/taskjuggler/LeaveList.rb000066400000000000000000000051131473026623400212610ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = LeaveList.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # class TaskJuggler # This class describes a leave. class Leave attr_reader :interval, :type, :reason # This Hash defines the supported leave types. It maps the symbol to its # index. The index sequence is important when multiple leaves are defined # for the same time slot. A subsequent definition with a type with a # larger index will override the old leave type. Types = { :project => 1, :annual => 2, :special => 3, :sick => 4, :unpaid => 5, :holiday => 6, :unemployed => 7 } # Create a new Leave object. _interval_ should be an Interval describing # the leave period. _type_ must be one of the supported leave types # (:holiday, :annual, :special, :unpaid, :sick and :project ). The # _reason_ is an optional String that describes the leave reason. def initialize(type, interval, reason = nil) unless Types[type] raise ArgumentError, "Unsupported leave type #{type}" end @type = type @interval = interval @reason = reason end def typeIdx Types[@type] end def to_s "#{@type} #{@reason ? "\"#{@reason}\"" : ""} #{@interval}" end end # A list of leaves. class LeaveList < Array def initialize(*args) super(*args) end end class LeaveAllowance < Struct.new(:type, :date, :slots) def initialize(type, date, slots) unless Leave::Types[type] raise ArgumentError, "Unsupported leave type #{type}" end super end end # The LeaveAllowanceList can store lists of LeaveAllowance objects. # Allowances are counted in time slots and can be negative to substract # expired allowances. class LeaveAllowanceList < Array # Create a new empty LeaveAllowanceList. def initialize(*args) super(*args) end def balance(type, startDate, endDate) unless Leave::Types[type] raise ArgumentError, "Unsupported leave type #{type}" end balance = 0.0 each do |al| balance += al.slots if al.type == type && al.date >= startDate && al.date < endDate end balance end end end TaskJuggler-3.8.1/lib/taskjuggler/Limits.rb000066400000000000000000000232661473026623400206430ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = Limits.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/Scoreboard' class TaskJuggler # This class holds a set of limits. Each limit can be created individually and # must have unique name. The Limit objects are created when an upper or lower # limit is set. All upper or lower limits can be tested with a single function # call. class Limits # This class implements a mechanism that can be used to limit certain events # within a certain time period. It supports an upper and a lower limit. class Limit attr_accessor :resource attr_reader :name, :interval, :upper # To create a new Limit object, the Interval +interval+ and the # period duration (+period+ in seconds) must be specified. This creates # a counter for each period within the overall interval. +value+ is the # value of the limit. +upper+ specifies whether the limit is an upper or # lower limit. The limit can also be restricted to certain a Resource # specified by +resource+. def initialize(name, interval, period, value, upper, resource) @name = name @interval = interval @period = period @value = value @upper = upper @resource = resource # To avoid multiple resets of untouched scoreboards we keep this dirty # flag. It's set whenever a counter is increased. @dirty = true reset end # Returns a deep copy of the class instance. def copy Limit.new(@name, @interval, @period, @value, @upper, @resource) end # This function can be used to reset the counter for a specific period # specified by +index+ or to reset all counters. def reset(index = nil) return unless @dirty if index.nil? @scoreboard = Scoreboard.new(@interval.startDate, @interval.endDate, @period, 0) else return unless @interval.contains?(index) # The scoreboard may be just a subset of the @interval period. @scoreboard[idxToSbIdx(index)] = 0 end @dirty = false end # Increase the counter if the _index_ matches the @interval. The # relationship between @resource and _resource_ is described below. # @r \ _r_ nil y # nil inc inc # x - if x==y inc else - def inc(index, resource) if @interval.contains?(index) && (@resource.nil? || @resource == resource) # The condition is met, increment the counter for the interval. @dirty = true @scoreboard[idxToSbIdx(index)] += 1 end end # Decrease the counter if the _index_ matches the @interval. The # relationship between @resource and _resource_ is described below. # @r \ _r_ nil y # nil inc inc # x - if x==y inc else - def dec(index, resource) if @interval.contains?(index) && (@resource.nil? || @resource == resource) # The condition is met, decrement the counter for the interval. @dirty = true @scoreboard[idxToSbIdx(index)] -= 1 end end # Returns true if the counter for the time slot specified by +index+ or # all counters are within the limit. If +upper+ is true, only upper # limits are checked. If not, only lower limits are checked. The # dependency between _resource_ and @resource is described in the matrix # below: # @r \ _r_ nil y # nil test true # x true if x==y test else true def ok?(index, upper, resource) # if @upper does not match or the provided resource does not match, # we can ignore this limit. return true if @upper != upper || (@resource && @resource != resource) if index.nil? # No index given. We need to check all counters. @scoreboard.each do |i| return false if @upper ? i >= @value : i < @value end return true else # If the index is outside the interval we don't have to check # anything. Everything is ok. return true if !@interval.contains?(index) sbVal = @scoreboard[idxToSbIdx(index)] return @upper ? (sbVal < @value) : (sbVal >= @value) end end private # The project scoreboard and the Limit scoreboard differ from each # other. The Limit scoreboard may only be a subset of the project # scoreboard interval. And the Limit scoreboard has a larger slot # duration that depends on what kind of limit it is (daily, weekly, # etc.). Therefor, we have to use this method to translate project # scoreboard indexes to Limit scoreboard indexes. def idxToSbIdx(index) (index - @interval.start) * @interval.slotDuration / @period end end attr_reader :project, :limits # Create a new Limits object. If an argument is passed, it acts as a copy # contructor. def initialize(limits = nil) if limits.nil? # Normal initialization @limits = [] @project = nil else # Deep copy content from other instance. @limits = [] limits.limits.each do |name, limit| @limits << limit.copy end @project = limits.project end end # The objects need access to some project specific data like the project # period. def setProject(project) unless @limits.empty? raise "Cannot change project after limits have been set!" end @project = project end # Reset all counter for all limits. def reset @limits.each { |limit| limit.reset } end # Call this function to create or change a limit. The limit is uniquely # identified by the combination of +name+, +interval+ and +resource+. # +value+ is the new limit value (in time slots). In case the interval # is nil, the complete project time frame is used. def setLimit(name, value, interval = nil, resource = nil) iv = interval || ScoreboardInterval.new(@project['start'], @project['scheduleGranularity'], @project['start'], @project['end']) unless iv.is_a?(ScoreboardInterval) raise ArgumentError, "interval must be of class ScoreboardInterval" end # The known ivs are aligned to start at their respective start. iv.start = iv.startDate.midnight iv.end = iv.endDate.midnight case name when 'dailymax' period = 60 * 60 * 24 upper = true when 'dailymin' period = 60 * 60 * 24 upper = false when 'weeklymax' iv.start = iv.startDate.beginOfWeek( @project['weekStartsMonday']) iv.end = iv.endDate.beginOfWeek(@project['weekStartsMonday']) period = 60 * 60 * 24 * 7 upper = true when 'weeklymin' iv.start = iv.startDate.beginOfWeek( @project['weekStartsMonday']) iv.end = iv.endDate.beginOfWeek(@project['weekStartsMonday']) period = 60 * 60 * 24 * 7 upper = false when 'monthlymax' iv.start = iv.startDate.beginOfMonth iv.end = iv.endDate.beginOfMonth # We use 30 days ivs here. This will cause the iv to drift # away from calendar months. But it's better than using 30.4167 which # does not align with day boundaries. period = 60 * 60 * 24 * 30 upper = true when 'monthlymin' iv.start = iv.startDate.beginOfMonth iv.end = iv.endDate.beginOfMonth # We use 30 days ivs here. This will cause the iv to drift # away from calendar months. But it's better than using 30.4167 which # does not align with day boundaries. period = 60 * 60 * 24 * 30 upper = false when 'maximum' period = iv.duration upper = true when 'minimum' period = iv.duration upper = false else raise "Limit period undefined" end # If we have already a limit for the name + interval + resource # combination, we delete it first. @limits.delete_if do |l| l.name == name && l.interval.startDate == iv.startDate && l.interval.endDate == iv.endDate && l.resource == resource end @limits << Limit.new(name, iv, period, value, upper, resource) end # This function increases the counters for all limits for a specific # interval identified by _index_. def inc(index, resource = nil) @limits.each do |limit| limit.inc(index, resource) end end # This function decreases the counters for all limits for a specific # interval identified by _index_. def dec(index, resource = nil) @limits.each do |limit| limit.dec(index, resource) end end # Check all upper limits and return true if none is exceeded. If an # _index_ is specified only the counters for that specific period are # tested. Otherwise all periods are tested. If _resource_ is nil, only # non-resource-specific counters are checked, otherwise only the ones that # match the _resource_. def ok?(index = nil, upper = true, resource = nil) @limits.each do |limit| return false unless limit.ok?(index, upper, resource) end true end end end TaskJuggler-3.8.1/lib/taskjuggler/Log.rb000066400000000000000000000125431473026623400201170ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = Log.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'singleton' require 'monitor' require 'term/ansicolor' class TaskJuggler # The Log class implements a filter for segmented execution traces. The # trace messages are filtered based on their segment name and the nesting # level of the segments. The class is a Singleton, so there is only one # instance in the program. class Log < Monitor include Singleton @@level = 0 @@stack = [] @@segments = [] @@silent = true @@progress = 0 @@progressMeter = '' # Set the maximum nesting level that should be shown. Segments with a # nesting level greater than _l_ will be silently dropped. def Log.level=(l) @@level = l end # The trace output can be limited to a list of segments. Messages not in # these segments will be ignored. Messages from segments that are nested # into the shown segments will be shown for the next @@level nested # segments. def Log.segments=(s) @@segments = s end # if +s+ is true, progress information will not be shown. def Log.silent=(s) @@silent = s end # Return the @@silent value. def Log.silent @@silent end # This function is used to open a new segment. +segment+ is the name of # the segment and +message+ is a description of it. def Log.enter(segment, message) return if @@level == 0 @@stack << segment Log.msg { ">> [#{segment}] #{message}" } end # This function is used to close an open segment. To make this mechanism a # bit more robust, it will search the stack of open segments for a segment # with that name and will close all nested segments as well. def Log.exit(segment, message = nil) return if @@level == 0 Log.msg { "<< [#{segment}] #{message}" } if message if @@stack.include?(segment) loop do m = @@stack.pop break if m == segment end end end # Use this function to show a log message within the currently active # segment. The message is the result of the passed block. The block will # only be evaluated if the message will actually be shown. def Log.msg(&block) return if @@level == 0 offset = 0 unless @@segments.empty? showMessage = false @@stack.each do |segment| # If a segment list is used to filter the output, we look for the # first listed segments on the stack. This and all nested segments # will be shown. if @@segments.include?(segment) offset = @@stack.index(segment) showMessage = true break end end return unless showMessage end if @@stack.length - offset < @@level $stderr.puts ' ' * (@@stack.length - offset) + yield(block) end end # Print out a status message unless we are in silent mode. def Log.status(message) return if @@silent $stdout.puts message end # The progress meter can be a textual progress bar or some animated # character sequence that informs the user about ongoing activities. Call # this function to start the progress meter display or to change the info # +text+. The the meter is active the text cursor is always returned to # the start of the same line. Consequent output will overwrite the last # meter text. def Log.startProgressMeter(text) return if @@silent maxlen = 60 text = text.ljust(maxlen) text = text[0..maxlen - 1] if text.length_utf8 > maxlen @@progressMeter = text $stdout.print("#{@@progressMeter} ...\r") end # This sets the progress meter status to "done" and puts the cursor into # the next line again. def Log.stopProgressMeter return if @@silent $stdout.print("#{@@progressMeter} [ " + Term::ANSIColor.green("Done") + " ]\n") end # This function may only be called when Log#startProgressMeter has been # called before. It updates the progress indicator to the next symbol to # visualize ongoing activity. def Log.activity return if @@silent indicator = %w( - \\ | / ) @@progress = (@@progress.to_i + 1) % indicator.length $stdout.print("#{@@progressMeter} [#{indicator[@@progress]}]\r") end # This function may only be called when Log#startProgressMeter has been # called before. It updates the progress bar to the given +percent+ # completion value. The value should be between 0.0 and 1.0. def Log.progress(percent) return if @@silent percent = 0.0 if percent < 0.0 percent = 1.0 if percent > 1.0 @@progress = percent length = 16 full = (length * percent).to_i bar = '=' * full + ' ' * (length - full) label = (percent * 100.0).to_i.to_s + '%' bar[length / 2 - label.length / 2, label.length] = label $stdout.print("#{@@progressMeter} [" + Term::ANSIColor.green("#{bar}") + "]\r") end end end TaskJuggler-3.8.1/lib/taskjuggler/LogicalExpression.rb000066400000000000000000000041641473026623400230300ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = LogicalExpression.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/LogicalOperation' require 'taskjuggler/Attributes' require 'taskjuggler/LogicalFunction' class TaskJuggler # A LogicalExpression is an object that describes tree of LogicalOperation # objects and the context that it should be evaluated in. class LogicalExpression attr_reader :query, :sourceFileInfo # Create a new LogicalExpression object. _op_ must be a LogicalOperation. # _sourceFileInfo_ is the file position where expression started. It may be # nil if not available. def initialize(op, sourceFileInfo = nil) @operation = op @sourceFileInfo = sourceFileInfo @query = nil end # This function triggers the evaluation of the expression. _property_ is the # PropertyTreeNode that should be used for the evaluation. _scopeProperty_ # is the PropertyTreeNode that describes the scope. It may be nil. def eval(query) @query = query res = @operation.eval(self) return res if res.is_a?(TrueClass) || res.is_a?(FalseClass) || res.is_a?(String) # In TJP syntax 'non 0' means false. return res != 0 end # Dump the LogicalExpression as a String. If _query_ is provided, it will # show the actual values, otherwise just the variable names. def to_s(query = nil) if @sourceFileInfo.nil? "#{@operation.to_s(query)}" else "#{@sourceFileInfo} #{@operation.to_s(query)}" end end # This is an internal function. It's called by the LogicalOperation methods # in case something went wrong during an evaluation. def error(text) # :nodoc: raise TjException.new, "#{to_s}\nLogical expression error: #{text}" end end end TaskJuggler-3.8.1/lib/taskjuggler/LogicalFunction.rb000066400000000000000000000216761473026623400224650ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = LogicalFunction.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/LogicalOperation' class TaskJuggler # The LogicalFunction is a specialization of the LogicalOperation. It models a # function call in a LogicalExpression. class LogicalFunction attr_accessor :name, :arguments # A map with the names of the supported functions and the number of # arguments they require. @@functions = { 'hasalert' => 1, 'isactive' => 1, 'ischildof' => 1, 'isdependencyof' => 3, 'isdutyof' => 2, 'isfeatureof' => 2, 'isleaf' => 0, 'ismilestone' => 1, 'isongoing' => 1, 'isresource' => 0, 'isresponsibilityof' => 2, 'istask' => 0, 'isvalid' => 1, 'treelevel' => 0 } # Create a new LogicalFunction. _opnd_ is the name of the function. def initialize(opnd) if opnd[-1] == ?_ # Function names with a trailing _ are like their counterparts without # the _. But during evaluation the property and the scope properties # will be switched. @name = opnd[0..-2] @invertProperties = true else @name = opnd @invertProperties = false end @arguments = [] end # Register the arguments of the function and check if the name is a known # function and the number of arguments match this function. If not, return # an [ id, message ] error. Otherwise nil. def setArgumentsAndCheck(args) unless @@functions.include?(@name) return [ 'unknown_function', "Unknown function #{@name} used in logical expression." ] end if @@functions[@name] != args.length return [ 'wrong_no_func_arguments', "Wrong number of arguments for function #{@name}. Got " + "#{args.length} instead of #{@@functions[@name]}." ] end @arguments = args nil end # Evaluate the function by calling it with the arguments. def eval(expr) # Call the function and return the result. send(name, expr, @arguments) end # Return a textual expression of the function call. def to_s "#{@name}(#{@arguments.join(', ')})" end private # Return the property and scope property as determined by the # @invertProperties setting. def properties(expr) if @invertProperties return expr.query.scopeProperty, nil else return expr.query.property, expr.query.scopeProperty end end def hasalert(expr, args) property = properties(expr)[0] query = expr.query project = property.project !project['journal'].currentEntries(query.end, property, args[0], query.start, query.hideJournalEntry).empty? end def isactive(expr, args) property, scopeProperty = properties(expr) # The result can only be true when called for a Task property. return false unless property.is_a?(Task) || property.is_a?(Resource) project = property.project # 1st arg must be a scenario index. if (scenarioIdx = project.scenarioIdx(args[0])).nil? expr.error("Unknown scenario '#{args[0]}' used for function isactive()") end query = expr.query property.getAllocatedTime(scenarioIdx, query.startIdx, query.endIdx, scopeProperty) > 0.0 end def ischildof(expr, args) # The the context property. property = properties(expr)[0] # Find the prospective parent ID in the current PropertySet. return false unless (parent = property.propertySet[args[0]]) property.isChildOf?(parent) end def isdependencyof(expr, args) property = properties(expr)[0] # The result can only be true when called for a Task property. return false unless property.is_a?(Task) project = property.project # 1st arg must be a task ID. return false if (task = project.task(args[0])).nil? # 2nd arg must be a scenario index. return false if (scenarioIdx = project.scenarioIdx(args[1])).nil? # 3rd arg must be an integer number. return false unless args[2].is_a?(Integer) property.isDependencyOf(scenarioIdx, task, args[2]) end def isdutyof(expr, args) property = properties(expr)[0] # The result can only be true when called for a Task property. return false unless (task = property).is_a?(Task) project = task.project # 1st arg must be a resource ID. return false if (resource = project.resource(args[0])).nil? # 2nd arg must be a scenario index. return false if (scenarioIdx = project.scenarioIdx(args[1])).nil? task['assignedresources', scenarioIdx].include?(resource) end def isfeatureof(expr, args) property = properties(expr)[0] # The result can only be true when called for a Task property. return false unless property.is_a?(Task) project = property.project # 1st arg must be a task ID. return false if (task = project.task(args[0])).nil? # 2nd arg must be a scenario index. return false if (scenarioIdx = project.scenarioIdx(args[1])).nil? property.isFeatureOf(scenarioIdx, task) end def isleaf(expr, args) property = properties(expr)[0] return false unless property property.leaf? end def ismilestone(expr, args) property = properties(expr)[0] return false unless property # 1st arg must be a scenario index. return false if (scenarioIdx = property.project.scenarioIdx(args[0])).nil? property.is_a?(Task) && property['milestone', scenarioIdx] end def isongoing(expr, args) property = properties(expr)[0] # The result can only be true when called for a Task property. return false unless (task = property).is_a?(Task) project = task.project # 1st arg must be a scenario index. if (scenarioIdx = project.scenarioIdx(args[0])).nil? expr.error("Unknown scenario '#{args[0]}' used for function " + "isongoing()") end query = expr.query iv1 = TimeInterval.new(query.start, query.end) tStart = task['start', scenarioIdx] tEnd = task['end', scenarioIdx] # This helps to show tasks with scheduling errors. return true unless tStart && tEnd iv2 = TimeInterval.new(tStart, tEnd) return iv1.overlaps?(iv2) end def isresource(expr, args) property = properties(expr)[0] return false unless property property.is_a?(Resource) end def isresponsibilityof(expr, args) property = properties(expr)[0] # The result can only be true when called for a Task property. return false unless (task = property).is_a?(Task) project = task.project # 1st arg must be a resource ID. return false if (resource = project.resource(args[0])).nil? # 2nd arg must be a scenario index. return false if (scenarioIdx = project.scenarioIdx(args[1])).nil? task['responsible', scenarioIdx].include?(resource) end def istask(expr, args) property = properties(expr)[0] return false unless property property.is_a?(Task) end def isvalid(expr, args) property = properties(expr)[0] project = property.project attr = args[0] scenario, attr = args[0].split('.') if attr.nil? attr = scenario scenario = nil end expr.error("Argument must not be empty") unless attr if scenario && (scenarioIdx = project.scenarioIdx(scenario)).nil? expr.error("Unknown scenario '#{scenario}' used for function " + "isvalid()") end unless property.propertySet.knownAttribute?(attr) expr.error("Unknown attribute '#{attr}' used for function " + "isvalid()") end if scenario unless property.attributeDefinition(attr).scenarioSpecific expr.error("Attribute '#{attr}' of property '#{property.fullId}' " + "is not scenario specific. Don't provide a scenario ID!") end !property[attr, scenarioIdx].nil? else if property.attributeDefinition(attr).scenarioSpecific expr.error("Attribute '#{attr}' of property '#{property.fullId}' " + "is scenario specific. Please provide a scenario ID!") end !property.get(attr).nil? end end def treelevel(expr, args) property = properties(expr)[0] return 0 unless property property.level + 1 end end end TaskJuggler-3.8.1/lib/taskjuggler/LogicalOperation.rb000066400000000000000000000206111473026623400226240ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = LogicalOperation.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2019 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/TjTime' require 'taskjuggler/RichText' require 'taskjuggler/TjException' class TaskJuggler # A LogicalOperation is the basic building block for a LogicalExpression. A # logical operation has one or two operands and an operator. The operands can # be LogicalOperation objects, fixed values or references to project data. The # LogicalOperation can be evaluated in a certain context. This contexts # determines the actual values of the project data references. # The evaluation is done by calling LogicalOperation#eval. The result must be # of a type that responds to all the operators that are used in the eval # method. class LogicalOperation attr_reader :operand1 attr_accessor :operand2, :operator # Create a new LogicalOperation object. _opnd1_ is the mandatory operand. # The @operand2 and the @operator can be set later. def initialize(opnd1, operator = nil, opnd2 = nil) @operand1 = opnd1 @operand2 = opnd2 @operator = operator end # Evaluate the expression in a given context represented by _expr_ of type # LogicalExpression. The result must be of a type that responds to all the # operators of this function. def eval(expr) case @operator when nil if @operand1.respond_to?(:eval) # An operand can be a fixed value or another term. This could be a # LogicalOperation, LogicalFunction or anything else that provides # an appropriate eval() method. return @operand1.eval(expr) else return @operand1 end when '~' return !coerceBoolean(@operand1.eval(expr), expr) when '>', '>=', '=', '<', '<=', '!=' # Evaluate the operation for all 2 operand operations that can be # either interpreted as date, numbers or Strings. opnd1 = @operand1.eval(expr) opnd2 = @operand2.eval(expr) if opnd1.is_a?(TjTime) res= evalBinaryOperation(opnd1, operator, opnd2) do |o| coerceTime(o, expr) end return res elsif opnd1.is_a?(Integer) || opnd1.is_a?(Float) return evalBinaryOperation(opnd1, operator, opnd2) do |o| coerceNumber(o, expr) end elsif opnd1.is_a?(RichTextIntermediate) return evalBinaryOperation(opnd1.to_s, operator, opnd2) do |o| coerceString(o, expr) end elsif opnd1.is_a?(String) return evalBinaryOperation(opnd1, operator, opnd2) do |o| coerceString(o, expr) end else expr.error("First operand of a binary operation must be a date, " + "a number or a string: #{opnd1.class}") end when '&' return coerceBoolean(@operand1.eval(expr), expr) && coerceBoolean(@operand2.eval(expr), expr) when '|' return coerceBoolean(@operand1.eval(expr), expr) || coerceBoolean(@operand2.eval(expr), expr) else expr.error("Unknown operator #{@operator} in logical expression") end end # Convert the operation into a textual representation. def to_s(query) if @operator.nil? operand_to_s(@operand1, query) elsif @operand2.nil? @operator + operand_to_s(@operand1, query) else "(#{operand_to_s(@operand1, query)} #{@operator} " + "#{operand_to_s(@operand2, query)})" end end private def operand_to_s(operand, query) if operand.is_a?(LogicalOperation) operand.to_s(query) elsif operand.is_a?(String) "'#{operand}'" else operand.to_s end end # We need to do binary operator evaluation with various coerce functions. # This function does the evaluation of _opnd1_ and _opnd2_ with the # operation specified by _operator_. The operands are first coerced into # the proper format by calling the block. def evalBinaryOperation(opnd1, operator, opnd2) case operator when '>' return yield(opnd1) > yield(opnd2) when '>=' return yield(opnd1) >= yield(opnd2) when '=' return yield(opnd1) == yield(opnd2) when '<' return yield(opnd1) < yield(opnd2) when '<=' return yield(opnd1) <= yield(opnd2) when '!=' return yield(opnd1) != yield(opnd2) else raise "Operator error" end end # Force the _val_ into a boolean value. def coerceBoolean(val, expr) # First the obvious ones. return val if val.class == TrueClass || val.class == FalseClass # An empty String means false, else true. return !val.empty? if val.is_a?(String) # In TJP logic 'non 0' means false. return val != 0 if val.is_a?(Integer) expr.error("Operand #{val} can't be evaluated to true or false.") end # Force the _val_ into a number. In case this fails, an exception is raised. def coerceNumber(val, expr) unless val.is_a?(Integer) || val.is_a?(Float) expr.error("Operand #{val} of type #{val.class} must be a number.") end val end # Force the _val_ into a String. In case this fails, an exception is raised. def coerceString(val, expr) unless val.respond_to?('to_s') expr.error("Operand #{val} of type #{val.class} can't be converted " + "into a string") end val end # Force the _val_ into a String. In case this fails, an exception is raised. def coerceTime(val, expr) unless val.is_a?(TjTime) expr.error("Operand #{val} of type #{val.class} can't be converted " + "into a date") end val end end # This class handles operands that are property attributes. They are # addressed by attribute ID and scenario index. The expression provides the # property reference. class LogicalAttribute < LogicalOperation def initialize(attribute, scenario) @scenario = scenario super end # To evaluate a property attribute we use the Query mechanism to retrieve # the value. def eval(expr) query = expr.query.dup query.scenarioIdx = @scenario.sequenceNo - 1 query.attributeId = @operand1 query.process if query.ok # The logical expressions are mostly about comparing values. So we use # the sortableResult of the Query. This creates some challenges for load # values, as the user is not accustomed to the internal representation # of those. # Convert nil results into empty Strings if necessary query.result || '' else expr.error(query.errorMessage) query.errorMessage end end # Dumps the LogicalOperation as String. If _query_ is nil, the variable # names are shown, otherwise their values. def to_s(query) if query query = query.dup query.scenarioIdx = @scenario.sequenceNo - 1 query.attributeId = @operand1 query.process unless query.ok return "Error in conversion to String: #{query.errorMessage}" end query.to_s else "#{@scenario.fullId}.#{@operand1}" end end end # This class handles operands that represent flags. The operation evaluates # to true if the property provided by the expression has the flag assigned. class LogicalFlag < LogicalOperation def initialize(opnd) super end # Return true if the property has the flag assigned. def eval(expr) if expr.query.is_a?(Query) # This is used for Project or PTN related Queries expr.query.property['flags', 0].include?(@operand1) else # This is used for Journal objects. expr.query.flags.include?(@operand1) end end def to_s(query) if query if query.is_a?(Query) query.property['flags', 0].include?(@operand1) ? 'true' : 'false' else query.flags.include?(@operand1) ? 'true' : 'false' end else @operand1 end end end end TaskJuggler-3.8.1/lib/taskjuggler/MessageHandler.rb000066400000000000000000000260171473026623400222610ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = MessageHandler.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # if RUBY_VERSION < "1.9.0" require 'rubygems' end require 'singleton' require 'term/ansicolor' require 'taskjuggler/TextParser/SourceFileInfo' class TaskJuggler class TjRuntimeError < RuntimeError end # The Message object can store several classes of messages that the # application can send out. class Message include Term::ANSIColor attr_reader :type, :id, :message, :line attr_accessor :sourceFileInfo # Create a new Message object. The _type_ specifies what tpye of message # this is. The following types are supported: fatal, error, warning, info # and debug. _id_ is a String that must uniquely identify the source of # the Message. _message_ is a String with the actual message. # _sourceLineInfo_ is a SourceLineInfo object that can reference a # location in a specific file. _line_ is a String of that file. _data_ can # be any context sensitive data. _sceneario_ specifies the Scenario in # which the message originated. def initialize(type, id, message, sourceFileInfo, line, data, scenario) unless [ :fatal, :error, :warning, :info, :debug ]. include?(type) raise "Unknown message type: #{type}" end @type = type @id = id if message && !message.is_a?(String) raise "String object expected as message but got #{message.class}" end @message = message if sourceFileInfo && !sourceFileInfo.is_a?(TextParser::SourceFileInfo) raise "SourceFileInfo object expected but got #{sourceFileInfo.class}" end @sourceFileInfo = sourceFileInfo if line && !line.is_a?(String) raise "String object expected as line but got #{line.class}" end @line = line @data = data if scenario && !scenario.is_a?(Scenario) raise "Scenario object expected by got #{scenario.class}" end @scenario = scenario end # Convert the Message into a String that can be printed to the console. def to_s str = "" # The SourceFileInfo is printed as :line: if @sourceFileInfo str += "#{@sourceFileInfo.fileName}:#{sourceFileInfo.lineNo}: " end if @scenario tag = "#{@type.to_s.capitalize} in scenario #{@scenario.id}: " else tag = "#{@type.to_s.capitalize}: " end colors = { :fatal => red, :error => red, :warning => magenta, :info => blue, :debug => green } str += colors[@type] + tag + @message + reset str += "\n" + @line if @line str end # Convert the Message into a String that can be stored in a log file. def to_log str = "" # The SourceFileInfo is printed as :line: if @sourceFileInfo str += "#{@sourceFileInfo.fileName}:#{sourceFileInfo.lineNo}: " end str += "Scenario #{@scenario.id}: " if @scenario str += @message str end end # The MessageHandler can display and store application messages. Depending # on the type of the message, a TjExeption can be raised (:error), or the # program can be immedidately aborted (:fatal). Other types will just # continue the program flow. class MessageHandlerInstance include Singleton attr_reader :messages, :errors attr_accessor :logFile, :appName, :abortOnWarning LogLevels = { :none => 0, :fatal => 1, :error => 2, :critical => 2, :warning => 3, :info => 4, :debug => 5 } # Initialize the MessageHandler. def initialize reset end # Reset the MessageHandler to the initial state. All messages will be # purged and the error counter set to 0. def reset # This setting controls what type of messages will be written to the # console. @outputLevel = 4 # This setting controls what type of messages will be written to the log # file. @logLevel = 3 # The full file name of the log file. @logFile = nil # Toggle if scenario ids are included in the messages or not. @hideScenario = true # The name of the current application @appName = 'unknown' # Set to true if program should be exited on warnings. @abortOnWarning = false # A SourceFileInfo object that will be used to baseline the provided # source file infos of the messages. We use a Hash to keep per Thread # values. @baselineSFI = {} # Each tread can request to only throw a TjRuntimeError instead of # using exit(). This hash keeps a flag for each thread using the # object_id of the Thread object as key. @trapSetup = {} clear end def baselineSFI=(line) @baselineSFI[Thread.current.object_id] = line end def trapSetup=(enable) @trapSetup[Thread.current.object_id] = enable end # Clear the error log. def clear # A counter for messages of type error. @errors = 0 # A list of all generated messages. @messages = [] end # Set the console output level. def outputLevel=(level) @outputLevel = checkLevel(level) end # Set the log output level. def logLevel=(level) @logLevel = checkLevel(level) end def hideScenario=(yesNo) @hideScenario = yesNo end # Generate a fatal message that will abort the application. def fatal(id, message, sourceFileInfo = nil, line = nil, data = nil, scenario = nil) addMessage(:fatal, id, message, sourceFileInfo, line, data, scenario) end # Generate an error message. def error(id, message, sourceFileInfo = nil, line = nil, data = nil, scenario = nil) addMessage(:error, id, message, sourceFileInfo, line, data, scenario) end # Generate an critical message. def critical(id, message, sourceFileInfo = nil, line = nil, data = nil, scenario = nil) addMessage(:critical, id, message, sourceFileInfo, line, data, scenario) end # Generate a warning. def warning(id, message, sourceFileInfo = nil, line = nil, data = nil, scenario = nil) addMessage(:warning, id, message, sourceFileInfo, line, data, scenario) end # Generate an info message. def info(id, message, sourceFileInfo = nil, line = nil, data = nil, scenario = nil) addMessage(:info, id, message, sourceFileInfo, line, data, scenario) end # Generate a debug message. def debug(id, message, sourceFileInfo = nil, line = nil, data = nil, scenario = nil) addMessage(:debug, id, message, sourceFileInfo, line, data, scenario) end # Convert all messages into a single String. def to_s text = '' @messages.each { |msg| text += msg.to_s } text end private def checkLevel(level) if level.is_a?(Integer) if level < 0 || level > 5 raise ArgumentError, "Unsupported level #{level}" end else unless (level = LogLevels[level]) raise ArgumentError, "Unsupported level #{level}" end end level end def log(type, message) return unless @logFile timeStamp = Time.new.strftime("%Y-%m-%d %H:%M:%S") begin File.open(@logFile, 'a') do |f| f.write("#{timeStamp} #{type} #{@appName}[#{Process.pid}]: " + "#{message}\n") end rescue $stderr.puts "Cannot write to log file #{@logFile}: #{$!}" end end # Generate a message by specifying the _type_. def addMessage(type, id, message, sourceFileInfo = nil, line = nil, data = nil, scenario = nil) # If we have a SourceFileInfo and a baseline SFI, we correct the # sourceFileInfo accordingly. baselineSFI = @baselineSFI[Thread.current.object_id] if sourceFileInfo && baselineSFI sourceFileInfo = TextParser::SourceFileInfo.new( baselineSFI.fileName, sourceFileInfo.lineNo + baselineSFI.lineNo - 1, sourceFileInfo.columnNo) end # Treat criticals like errors but without generating another # exception. msg = Message.new(type == :critical ? :error : type, id, message, sourceFileInfo, line, data, @hideScenario ? nil : scenario) @messages << msg # Append the message to the log file if requested by the user. log(type, msg.to_log) if @logLevel >= LogLevels[type] # Print the message to $stderr if requested by the user. $stderr.puts msg.to_s if @outputLevel >= LogLevels[type] case type when :warning raise TjException.new, '' if @abortOnWarning when :critical # Increase the error counter. @errors += 1 when :error # Increase the error counter. @errors += 1 if @trapSetup[Thread.current.object_id] raise TjRuntimeError else exit 1 end when :fatal raise RuntimeError end end end module MessageHandler # Generate a fatal message that will abort the application. def fatal(id, message, sourceFileInfo = nil, line = nil, data = nil, scenario = nil) MessageHandlerInstance.instance.fatal(id, message, sourceFileInfo, line, data, scenario) end # Generate an error message. def error(id, message, sourceFileInfo = nil, line = nil, data = nil, scenario = nil) MessageHandlerInstance.instance.error(id, message, sourceFileInfo, line, data, scenario) end # Generate an critical message. def critical(id, message, sourceFileInfo = nil, line = nil, data = nil, scenario = nil) MessageHandlerInstance.instance.critical(id, message, sourceFileInfo, line, data, scenario) end # Generate a warning. def warning(id, message, sourceFileInfo = nil, line = nil, data = nil, scenario = nil) MessageHandlerInstance.instance.warning(id, message, sourceFileInfo, line, data, scenario) end # Generate an info message. def info(id, message, sourceFileInfo = nil, line = nil, data = nil, scenario = nil) MessageHandlerInstance.instance.info(id, message, sourceFileInfo, line, data, scenario) end # Generate a debug message. def debug(id, message, sourceFileInfo = nil, line = nil, data = nil, scenario = nil) MessageHandlerInstance.instance.debug(id, message, sourceFileInfo, line, data, scenario) end end end TaskJuggler-3.8.1/lib/taskjuggler/PTNProxy.rb000066400000000000000000000067311473026623400211030ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = PTNProxy.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # class TaskJuggler # This class provides objects that represent PropertyTreeNode objects that # were adopted (directly or indirectly) in their new parental context. Such # objects are used as elements of a PropertyList which can only hold each # PropertyTreeNode objects once. By using this class, we can add such # objects more than once, each time with a new parental context that was # created by an adoption. class PTNProxy attr_reader :parent def initialize(ptn, parent) @ptn = ptn raise "Adopted properties must have a parent" unless parent @parent = parent @indext = nil @tree = nil @level = -1 end # Return the logical ID of this node respesting adoptions. For PropertySet # objects with a flat namespace, this is just the ID. Otherwise, the # logical ID is composed of all IDs from the root node to this node, # separating the IDs by a dot. In contrast to PropertyTreeNode::fullId() # the logicalId takes the aption path into account. def logicalId if @ptn.propertySet.flatNamespace @ptn.id else if (dotPos = @ptn.id.rindex('.')) id = @ptn.id[(dotPos + 1)..-1] else id = @ptn.id end @parent.logicalId + '.' + id end end def set(attribute, val) if attribute == 'index' @index = val elsif attribute == 'tree' @tree = val else @ptn.set(attribute, val) end end def get(attribute) if attribute == 'index' @index elsif attribute == 'tree' @tree else @ptn.get(attribute) end end def [](attribute, scenarioIdx) if attribute == 'index' @index elsif attribute == 'tree' @tree else @ptn[attribute, scenarioIdx] end end # Returns the level that this property is on. Top-level properties return # 0, their children 1 and so on. This value is cached internally, so it does # not have to be calculated each time the function is called. def level return @level if @level >= 0 t = self @level = 0 until (t = t.parent).nil? @level += 1 end @level end # Find out if this property is a direct or indirect child of _ancestor_. def isChildOf?(ancestor) parent = self while parent = parent.parent return true if (parent == ancestor) end false end # Return the 'index' attributes of this property, prefixed by the 'index' # attributes of all its parents. The result is an Array of Integers. def getIndicies idcs = [] p = self begin parent = p.parent idcs.insert(0, p.get('index')) p = parent end while p idcs end def method_missing(func, *args, &block) @ptn.send(func, *args, &block) end alias_method :respond_to_?, :respond_to? def respond_to?(method) respond_to_?(method) || @ptn.respond_to?(method) end def is_a?(type) @ptn.is_a?(type) end end end TaskJuggler-3.8.1/lib/taskjuggler/Painter.rb000066400000000000000000000051121473026623400207720ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = Painter.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/XMLElement' require 'taskjuggler/Painter/Primitives' require 'taskjuggler/Painter/Group' require 'taskjuggler/Painter/BasicShapes' require 'taskjuggler/Painter/Text' require 'taskjuggler/Painter/FontMetrics' class TaskJuggler # This is a vector drawing class. It can describe a canvas with lines, # rectangles, circles, ellipses and text elements on it. The elements can be # grouped. It currently only supports rendering as an SVG output. class Painter include Primitives # Create a canvas of dimension _width_ times _height_. The block can be # used to add elements to the drawing. If the block has an argument, the # block content is evaluated within the current context. If no argument is # provided, the newly created object will be the evaluation context of the # block. This will make instance variables of the caller inaccessible. # Methods of the caller will still be available. def initialize(width, height, &block) @width = width @height = height @elements = [] if block if block.arity == 1 # This is the traditional case where self is passed to the block. # All Primitives methods now must be prefixed with the block # variable to call them. yield self else # In order to have the primitives easily available in the block, we # use instance_eval to switch self to this object. But this makes the # methods of the original self no longer accessible. We work around # this by saving the original self and using method_missing to # delegate the method call to the original self. @originalSelf = eval('self', block.binding) instance_eval(&block) end end end # Delegator to @originalSelf. def method_missing(method, *args, &block) @originalSelf.send(method, *args, &block) end # Render the canvas as SVG output (tree of XMLElement objects). def to_svg XMLElement.new('svg', 'width' => "#{@width}px", 'height' => "#{@height}px") do @elements.map { |el| el.to_svg } end end end end TaskJuggler-3.8.1/lib/taskjuggler/Painter/000077500000000000000000000000001473026623400204465ustar00rootroot00000000000000TaskJuggler-3.8.1/lib/taskjuggler/Painter/BasicShapes.rb000066400000000000000000000033021473026623400231560ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = BasicShapes.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/XMLElement' require 'taskjuggler/Painter/Element' class TaskJuggler class Painter # A circle element. class Circle < Element # Create a circle with center at cx, cy and radius r. def initialize(attrs) super('circle', [ :cx, :cy, :r ] + FillAndStrokeAttrs, attrs) end end # An ellipse element. class Ellipse < Element # Create an ellipse with center at cx, cy and radiuses rx and ry. def initialize(attrs) super('ellipse', [ :cx, :cy, :rx, :ry ] + FillAndStrokeAttrs, attrs) end end # A line element. class Line < Element # Create a line from x1, y1, to x2, y2. def initialize(attrs) super('line', [ :x1, :y1, :x2, :y2 ] + StrokeAttrs, attrs) end end # A Rectangle element. class Rect < Element # Create a rectangle at x, y with width and height. def initialize(attrs) super('rect', [ :x, :y, :width, :height, :rx, :ry ] + FillAndStrokeAttrs, attrs) end end # A Polygon line element. class PolyLine < Element # Create a polygon line with the provided Points. def initialize(attrs) super('polyline', [ :points ] + FillAndStrokeAttrs, attrs) end end end end TaskJuggler-3.8.1/lib/taskjuggler/Painter/Color.rb000066400000000000000000000215261473026623400220570ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = Color.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # class TaskJuggler class Painter # A description of the color. The color is stored internally as an RGB # value. Common names are provided for many popular colors. class Color NamedColors = { :aliceblue => [ 240, 248, 255 ], :antiquewhite => [ 250, 235, 215 ], :aqua => [ 0, 255, 255 ], :aquamarine => [ 127, 255, 212 ], :azure => [ 240, 255, 255 ], :beige => [ 245, 245, 220 ], :bisque => [ 255, 228, 196 ], :black => [ 0, 0, 0 ], :blanchedalmond => [ 255, 235, 205 ], :blue => [ 0, 0, 255 ], :blueviolet => [ 138, 43, 226 ], :brown => [ 165, 42, 42 ], :burlywood => [ 222, 184, 135 ], :cadetblue => [ 95, 158, 160 ], :chartreuse => [ 127, 255, 0 ], :chocolate => [ 210, 105, 30 ], :coral => [ 255, 127, 80 ], :cornflowerblue => [ 100, 149, 237 ], :cornsilk => [ 255, 248, 220 ], :crimson => [ 220, 20, 60 ], :cyan => [ 0, 255, 255 ], :darkblue => [ 0, 0, 139 ], :darkcyan => [ 0, 139, 139 ], :darkgoldenrod => [ 184, 134, 11 ], :darkgray => [ 169, 169, 169 ], :darkgreen => [ 0, 100, 0 ], :darkgrey => [ 169, 169, 169 ], :darkkhaki => [ 189, 183, 107 ], :darkmagenta => [ 139, 0, 139 ], :darkolivegreen => [ 85, 107, 47 ], :darkorange => [ 255, 140, 0 ], :darkorchid => [ 153, 50, 204 ], :darkred => [ 139, 0, 0 ], :darksalmon => [ 233, 150, 122 ], :darkseagreen => [ 143, 188, 143 ], :darkslateblue => [ 72, 61, 139 ], :darkslategray => [ 47, 79, 79 ], :darkslategrey => [ 47, 79, 79 ], :darkturquoise => [ 0, 206, 209 ], :darkviolet => [ 148, 0, 211 ], :deeppink => [ 255, 20, 147 ], :deepskyblue => [ 0, 191, 255 ], :dimgray => [ 105, 105, 105 ], :dimgrey => [ 105, 105, 105 ], :dodgerblue => [ 30, 144, 255 ], :firebrick => [ 178, 34, 34 ], :floralwhite => [ 255, 250, 240 ], :forestgreen => [ 34, 139, 34 ], :fuchsia => [ 255, 0, 255 ], :gainsboro => [ 220, 220, 220 ], :ghostwhite => [ 248, 248, 255 ], :gold => [ 255, 215, 0 ], :goldenrod => [ 218, 165, 32 ], :gray => [ 128, 128, 128 ], :grey => [ 128, 128, 128 ], :green => [ 0, 128, 0 ], :greenyellow => [ 173, 255, 47 ], :honeydew => [ 240, 255, 240 ], :hotpink => [ 255, 105, 180 ], :indianred => [ 205, 92, 92 ], :indigo => [ 75, 0, 130 ], :ivory => [ 255, 255, 240 ], :khaki => [ 240, 230, 140 ], :lavender => [ 230, 230, 250 ], :lavenderblush => [ 255, 240, 245 ], :lawngreen => [ 124, 252, 0 ], :lemonchiffon => [ 255, 250, 205 ], :lightblue => [ 173, 216, 230 ], :lightcoral => [ 240, 128, 128 ], :lightcyan => [ 224, 255, 255 ], :lightgoldenrodyellow => [ 250, 250, 210 ], :lightgray => [ 211, 211, 211 ], :lightgreen => [ 144, 238, 144 ], :lightgrey => [ 211, 211, 211 ], :lightpink => [ 255, 182, 193 ], :lightsalmon => [ 255, 160, 122 ], :lightseagreen => [ 32, 178, 170 ], :lightskyblue => [ 135, 206, 250 ], :lightslategray => [ 119, 136, 153 ], :lightslategrey => [ 119, 136, 153 ], :lightsteelblue => [ 176, 196, 222 ], :lightyellow => [ 255, 255, 224 ], :lime => [ 0, 255, 0 ], :limegreen => [ 50, 205, 50 ], :linen => [ 250, 240, 230 ], :magenta => [ 255, 0, 255 ], :maroon => [ 128, 0, 0 ], :mediumaquamarine => [ 102, 205, 170 ], :mediumblue => [ 0, 0, 205 ], :mediumorchid => [ 186, 85, 211 ], :mediumpurple => [ 147, 112, 219 ], :mediumseagreen => [ 60, 179, 113 ], :mediumslateblue => [ 123, 104, 238 ], :mediumspringgreen => [ 0, 250, 154 ], :mediumturquoise => [ 72, 209, 204 ], :mediumvioletred => [ 199, 21, 133 ], :midnightblue => [ 25, 25, 112 ], :mintcream => [ 245, 255, 250 ], :mistyrose => [ 255, 228, 225 ], :moccasin => [ 255, 228, 181 ], :navajowhite => [ 255, 222, 173 ], :navy => [ 0, 0, 128 ], :oldlace => [ 253, 245, 230 ], :olive => [ 128, 128, 0 ], :olivedrab => [ 107, 142, 35 ], :orange => [ 255, 165, 0 ], :orangered => [ 255, 69, 0 ], :orchid => [ 218, 112, 214 ], :palegoldenrod => [ 238, 232, 170 ], :palegreen => [ 152, 251, 152 ], :paleturquoise => [ 175, 238, 238 ], :palevioletred => [ 219, 112, 147 ], :papayawhip => [ 255, 239, 213 ], :peachpuff => [ 255, 218, 185 ], :peru => [ 205, 133, 63 ], :pink => [ 255, 192, 203 ], :plum => [ 221, 160, 221 ], :powderblue => [ 176, 224, 230 ], :purple => [ 128, 0, 128 ], :red => [ 255, 0, 0 ], :rosybrown => [ 188, 143, 143 ], :royalblue => [ 65, 105, 225 ], :saddlebrown => [ 139, 69, 19 ], :salmon => [ 250, 128, 114 ], :sandybrown => [ 244, 164, 96 ], :seagreen => [ 46, 139, 87 ], :seashell => [ 255, 245, 238 ], :sienna => [ 160, 82, 45 ], :silver => [ 192, 192, 192 ], :skyblue => [ 135, 206, 235 ], :slateblue => [ 106, 90, 205 ], :slategray => [ 112, 128, 144 ], :slategrey => [ 112, 128, 144 ], :snow => [ 255, 250, 250 ], :springgreen => [ 0, 255, 127 ], :steelblue => [ 70, 130, 180 ], :tan => [ 210, 180, 140 ], :teal => [ 0, 128, 128 ], :thistle => [ 216, 191, 216 ], :tomato => [ 255, 99, 71 ], :turquoise => [ 64, 224, 208 ], :violet => [ 238, 130, 238 ], :wheat => [ 245, 222, 179 ], :white => [ 255, 255, 255 ], :whitesmoke => [ 245, 245, 245 ], :yellow => [ 255, 255, 0 ], :yellowgreen => [ 154, 205, 50 ] } # Create a new Color object. def initialize(*args) if args.length == 1 unless NamedColors.include?(args[0]) raise "Unknown color name #{args[0]}" end @r, @g, @b = NamedColors[args[0]] elsif args.length == 3 args.each do |v| unless v >= 0 && v < 256 raise ArgumentError, "RGB values (#{args.join(', ')}) must " + "be between 0 and 255." end end @r, @g, @b = args elsif args.length == 4 unless args[0] >= 0 && args[0] < 360 raise ArgumentError, "Hue value must be between 0 and 360" end unless args[1] >= 0 && args[1] <= 255 raise ArgumentError, "Saturation value (#{args[1]}) must be " + "between 0 and 255" end unless args[2] >= 0 && args[2] <= 255 raise ArgumentError, "Color value (#{args[2]}) must be " + "between 0 and 255" end @r, @g, @b = hsvToRgb(*args[0..2]) else raise ArgumentError end end def to_rgb [ @r, @g, @b ] end def to_hsv rgbToHsv(@r, @g, @b) end # Convert the RGB value into a String format that is used in HTML. def to_s format("#%02x%02x%02x", @r, @g, @b) end private def hsvToRgb(h, s, v) hi = (h / 60.0).floor % 6 f = (h / 60.0) - (h / 60.0).floor p = (v * (1.0 - s / 255.0)).to_i q = (v * (1.0 - (f * s / 255.0))).to_i t = (v * (1.0 - ((1.0 - f) * s / 255.0))).to_i [ [ v, t, p ], [ q, v, p ], [ p, v, t ], [ p, q, v ], [ t, p, v ], [ v, p, q ] ][hi] end def rgbToHsv(*rgb) max = rgb.max min = rgb.min chroma = (max - min).to_f v = max if (max != 0.0) s = chroma / max * 255.0 else s = 0.0 end r, g, b = rgb if (s == 0.0) h = 0.0 else if (r == max) h = (g - b) / chroma elsif (g == max) h = 2.0 + (b - r) / chroma elsif (b == max) h = 4.0 + (r - g) / chroma end h *= 60.0 h += 360.0 if h < 0.0 end [ h.to_i, s.to_i, v.to_i ] end end end end TaskJuggler-3.8.1/lib/taskjuggler/Painter/Element.rb000066400000000000000000000025411473026623400223660ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = Line.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/Painter/SVGSupport' require 'taskjuggler/Painter/Primitives' class TaskJuggler class Painter # The base class for all drawable elements. class Element include SVGSupport include Primitives # Create a new Element. _type_ specifies the type of the element. # _attrs_ is a list of the supported attributes. _values_ is a hash of # the provided attributes. def initialize(type, attrs, values) @type = type @attributes = attrs @values = {} @text = nil values.each do |k, v| unless @attributes.include?(k) raise ArgumentError, "Unsupported attribute #{k}" end @values[k] = v end end # Convert the Element into an XMLElement tree using SVG syntax. def to_svg el = XMLElement.new(@type, valuesToSVG) el << XMLText.new(@text) if @text el end end end end TaskJuggler-3.8.1/lib/taskjuggler/Painter/FontData.rb000066400000000000000000000336031473026623400225000ustar00rootroot00000000000000 class TaskJuggler class Painter class FontMetrics Font_LiberationSans_normal = Painter::FontMetricsData.new('LiberationSans', :normal, 24, 26.088, @charWidth = { ' ' => 6.648, '!' => 6.648, '"' => 8.496, '#' => 13.344, '$' => 13.344, '%' => 21.336, '&' => 15.984, '\'' => 4.560, '(' => 7.992, ')' => 7.992, '*' => 9.336, '+' => 13.992, ',' => 6.648, '-' => 7.992, '.' => 6.648, '/' => 6.648, '0' => 13.344, '1' => 13.344, '2' => 13.344, '3' => 13.344, '4' => 13.344, '5' => 13.344, '6' => 13.344, '7' => 13.344, '8' => 13.344, '9' => 13.344, ':' => 6.648, ';' => 6.648, '<' => 13.992, '=' => 13.992, '>' => 13.992, '?' => 13.344, '@' => 24.360, 'A' => 15.984, 'B' => 15.984, 'C' => 17.328, 'D' => 17.328, 'E' => 15.984, 'F' => 14.640, 'G' => 18.648, 'H' => 17.328, 'I' => 6.648, 'J' => 12.000, 'K' => 15.984, 'L' => 13.344, 'M' => 19.992, 'N' => 17.328, 'O' => 18.648, 'P' => 15.984, 'Q' => 18.648, 'R' => 17.328, 'S' => 15.984, 'T' => 14.640, 'U' => 17.328, 'V' => 15.984, 'W' => 22.632, 'X' => 15.984, 'Y' => 15.984, 'Z' => 14.640, '[' => 6.648, '\\' => 6.648, ']' => 6.648, '^' => 11.256, '_' => 13.344, '`' => 7.992, 'a' => 13.344, 'b' => 13.344, 'c' => 12.000, 'd' => 13.344, 'e' => 13.344, 'f' => 6.648, 'g' => 13.344, 'h' => 13.344, 'i' => 5.328, 'j' => 5.328, 'k' => 12.000, 'l' => 5.328, 'm' => 19.992, 'n' => 13.344, 'o' => 13.344, 'p' => 13.344, 'q' => 13.344, 'r' => 7.992, 's' => 12.000, 't' => 6.648, 'u' => 13.344, 'v' => 12.000, 'w' => 17.328, 'x' => 12.000, 'y' => 12.000, 'z' => 12.000, '{' => 7.992, '|' => 6.216, '}' => 7.992, '~' => 13.992, }, @kerningDelta = { ' A' => -1.324, ' T' => -0.434, ' Y' => -0.434, '11' => -1.781, 'A ' => -1.324, 'AT' => -1.781, 'AV' => -1.781, 'AW' => -0.891, 'AY' => -1.781, 'Av' => -0.434, 'Aw' => -0.434, 'Ay' => -0.434, 'F,' => -2.660, 'F.' => -2.660, 'FA' => -1.324, 'L ' => -0.891, 'LT' => -1.781, 'LV' => -1.781, 'LW' => -1.781, 'LY' => -1.781, 'Ly' => -0.891, 'P ' => -0.434, 'P,' => -3.094, 'P.' => -3.094, 'PA' => -1.781, 'RT' => -0.434, 'RV' => -0.434, 'RW' => -0.434, 'RY' => -0.434, 'T ' => -0.434, 'T,' => -2.660, 'T-' => -1.324, 'T.' => -2.660, 'T:' => -2.660, 'T;' => -2.660, 'TA' => -1.781, 'TO' => -0.434, 'Ta' => -2.660, 'Tc' => -2.660, 'Te' => -2.660, 'Ti' => -0.891, 'To' => -2.660, 'Tr' => -0.891, 'Ts' => -2.660, 'Tu' => -0.891, 'Tw' => -1.324, 'Ty' => -1.324, 'V,' => -2.203, 'V-' => -1.324, 'V.' => -2.203, 'V:' => -0.891, 'V;' => -0.891, 'VA' => -1.781, 'Va' => -1.781, 'Ve' => -1.324, 'Vi' => -0.434, 'Vo' => -1.324, 'Vr' => -0.891, 'Vu' => -0.891, 'Vy' => -0.891, 'W,' => -1.324, 'W-' => -0.434, 'W.' => -1.324, 'W:' => -0.434, 'W;' => -0.434, 'WA' => -0.891, 'Wa' => -0.891, 'We' => -0.434, 'Wo' => -0.434, 'Wr' => -0.434, 'Wu' => -0.434, 'Wy' => -0.211, 'Y ' => -0.434, 'Y,' => -3.094, 'Y-' => -2.203, 'Y.' => -3.094, 'Y:' => -1.324, 'Y;' => -1.559, 'YA' => -1.781, 'Ya' => -1.781, 'Ye' => -2.203, 'Yi' => -0.891, 'Yo' => -2.203, 'Yp' => -1.781, 'Yq' => -2.203, 'Yu' => -1.324, 'Yv' => -1.324, 'ff' => -0.434, 'r,' => -1.324, 'r.' => -1.324, 'v,' => -1.781, 'v.' => -1.781, 'w,' => -1.324, 'w.' => -1.324, 'y,' => -1.781, 'y.' => -1.781, } ) Font_LiberationSans_italic = Painter::FontMetricsData.new('LiberationSans', :italic, 24, 26.016, @charWidth = { ' ' => 6.648, '!' => 6.648, '"' => 8.496, '#' => 13.344, '$' => 13.344, '%' => 21.336, '&' => 15.984, '\'' => 4.560, '(' => 7.992, ')' => 7.992, '*' => 9.336, '+' => 13.992, ',' => 6.648, '-' => 7.992, '.' => 6.648, '/' => 6.648, '0' => 13.344, '1' => 13.344, '2' => 13.344, '3' => 13.344, '4' => 13.344, '5' => 13.344, '6' => 13.344, '7' => 13.344, '8' => 13.344, '9' => 13.344, ':' => 6.648, ';' => 6.648, '<' => 13.992, '=' => 13.992, '>' => 13.992, '?' => 13.344, '@' => 24.360, 'A' => 15.984, 'B' => 15.984, 'C' => 17.328, 'D' => 17.328, 'E' => 15.984, 'F' => 14.640, 'G' => 18.648, 'H' => 17.328, 'I' => 6.648, 'J' => 12.000, 'K' => 15.984, 'L' => 13.344, 'M' => 19.992, 'N' => 17.328, 'O' => 18.648, 'P' => 15.984, 'Q' => 18.648, 'R' => 17.328, 'S' => 15.984, 'T' => 14.640, 'U' => 17.328, 'V' => 15.984, 'W' => 22.632, 'X' => 15.984, 'Y' => 15.984, 'Z' => 14.640, '[' => 6.648, '\\' => 6.648, ']' => 6.648, '^' => 11.256, '_' => 13.344, '`' => 7.992, 'a' => 13.344, 'b' => 13.344, 'c' => 12.000, 'd' => 13.344, 'e' => 13.344, 'f' => 6.648, 'g' => 13.344, 'h' => 13.344, 'i' => 5.328, 'j' => 5.328, 'k' => 12.000, 'l' => 5.328, 'm' => 19.992, 'n' => 13.344, 'o' => 13.344, 'p' => 13.344, 'q' => 13.344, 'r' => 7.992, 's' => 12.000, 't' => 6.648, 'u' => 13.344, 'v' => 12.000, 'w' => 17.328, 'x' => 12.000, 'y' => 12.000, 'z' => 12.000, '{' => 7.992, '|' => 6.216, '}' => 7.992, '~' => 13.992, }, @kerningDelta = { ' A' => -0.891, ' Y' => -0.434, '11' => -1.781, 'A ' => -0.891, 'AT' => -1.781, 'AV' => -1.324, 'AW' => -0.434, 'AY' => -1.781, 'Av' => -0.434, 'Aw' => -0.434, 'Ay' => -0.211, 'F ' => -0.434, 'F,' => -3.094, 'F.' => -3.094, 'FA' => -1.781, 'L ' => -0.434, 'LT' => -1.781, 'LV' => -1.324, 'LW' => -0.891, 'LY' => -2.203, 'Ly' => -0.434, 'P ' => -0.891, 'P,' => -3.094, 'P.' => -3.094, 'PA' => -1.781, 'RT' => -0.434, 'RV' => -0.434, 'RW' => -0.434, 'RY' => -0.891, 'T,' => -2.203, 'T-' => -2.203, 'T.' => -2.203, 'T:' => -1.781, 'T;' => -1.781, 'TA' => -1.781, 'TO' => -0.434, 'Ta' => -2.203, 'Tc' => -2.203, 'Te' => -2.203, 'Ti' => -0.211, 'To' => -2.203, 'Tr' => -1.781, 'Ts' => -2.203, 'Tu' => -1.781, 'Tw' => -1.781, 'Ty' => -1.781, 'V,' => -1.781, 'V-' => -0.891, 'V.' => -1.781, 'V:' => -0.434, 'V;' => -0.434, 'VA' => -1.324, 'Va' => -0.891, 'Ve' => -0.891, 'Vi' => -0.434, 'Vo' => -0.891, 'Vr' => -0.434, 'Vu' => -0.434, 'Vy' => -0.434, 'W,' => -0.891, 'W-' => -0.434, 'W.' => -0.891, 'WA' => -0.434, 'Wa' => -0.434, 'We' => -0.434, 'Wi' => -0.211, 'Y ' => -0.434, 'Y,' => -2.203, 'Y-' => -1.781, 'Y.' => -2.203, 'Y:' => -0.891, 'Y;' => -0.891, 'YA' => -1.324, 'Ya' => -1.781, 'Ye' => -1.324, 'Yi' => -0.434, 'Yo' => -1.324, 'Yp' => -1.324, 'Yq' => -1.324, 'Yu' => -0.891, 'Yv' => -0.891, 'r,' => -1.324, 'r-' => -0.434, 'r.' => -0.891, 'v,' => -1.781, 'v.' => -1.781, 'w,' => -1.324, 'w.' => -1.324, 'y,' => -1.781, 'y.' => -1.781, } ) Font_LiberationSans_bold = Painter::FontMetricsData.new('LiberationSans', :bold, 24, 26.088, @charWidth = { ' ' => 6.648, '!' => 7.992, '"' => 11.376, '#' => 13.344, '$' => 13.344, '%' => 21.336, '&' => 17.328, '\'' => 5.688, '(' => 7.992, ')' => 7.992, '*' => 9.336, '+' => 13.992, ',' => 6.648, '-' => 7.992, '.' => 6.648, '/' => 6.648, '0' => 13.344, '1' => 13.344, '2' => 13.344, '3' => 13.344, '4' => 13.344, '5' => 13.344, '6' => 13.344, '7' => 13.344, '8' => 13.344, '9' => 13.344, ':' => 7.992, ';' => 7.992, '<' => 13.992, '=' => 13.992, '>' => 13.992, '?' => 14.640, '@' => 23.400, 'A' => 17.328, 'B' => 17.328, 'C' => 17.328, 'D' => 17.328, 'E' => 15.984, 'F' => 14.640, 'G' => 18.648, 'H' => 17.328, 'I' => 6.648, 'J' => 13.344, 'K' => 17.328, 'L' => 14.640, 'M' => 19.992, 'N' => 17.328, 'O' => 18.648, 'P' => 15.984, 'Q' => 18.648, 'R' => 17.328, 'S' => 15.984, 'T' => 14.640, 'U' => 17.328, 'V' => 15.984, 'W' => 22.632, 'X' => 15.984, 'Y' => 15.984, 'Z' => 14.640, '[' => 7.992, '\\' => 6.648, ']' => 7.992, '^' => 13.992, '_' => 13.344, '`' => 7.992, 'a' => 13.344, 'b' => 14.640, 'c' => 13.344, 'd' => 14.640, 'e' => 13.344, 'f' => 7.992, 'g' => 14.640, 'h' => 14.640, 'i' => 6.648, 'j' => 6.648, 'k' => 13.344, 'l' => 6.648, 'm' => 21.336, 'n' => 14.640, 'o' => 14.640, 'p' => 14.640, 'q' => 14.640, 'r' => 9.336, 's' => 13.344, 't' => 7.992, 'u' => 14.640, 'v' => 13.344, 'w' => 18.648, 'x' => 13.344, 'y' => 13.344, 'z' => 12.000, '{' => 9.336, '|' => 6.696, '}' => 9.336, '~' => 13.992, }, @kerningDelta = { ' A' => -0.891, ' Y' => -0.434, '11' => -1.324, 'A ' => -0.891, 'AT' => -1.781, 'AV' => -1.781, 'AW' => -1.324, 'AY' => -2.203, 'Av' => -0.891, 'Aw' => -0.434, 'Ay' => -0.891, 'F,' => -2.660, 'F.' => -2.660, 'FA' => -1.324, 'L ' => -0.434, 'LT' => -1.781, 'LV' => -1.781, 'LW' => -1.324, 'LY' => -2.203, 'Ly' => -0.891, 'P ' => -0.434, 'P,' => -3.094, 'P.' => -3.094, 'PA' => -1.781, 'RV' => -0.434, 'RW' => -0.434, 'RY' => -0.891, 'T,' => -2.660, 'T-' => -1.324, 'T.' => -2.660, 'T:' => -2.660, 'T;' => -2.660, 'TA' => -1.781, 'TO' => -0.434, 'Ta' => -1.781, 'Tc' => -1.781, 'Te' => -1.781, 'Ti' => -0.434, 'To' => -1.781, 'Tr' => -1.324, 'Ts' => -1.781, 'Tu' => -1.781, 'Tw' => -1.781, 'Ty' => -1.781, 'V,' => -2.203, 'V-' => -1.324, 'V.' => -2.203, 'V:' => -1.324, 'V;' => -1.324, 'VA' => -1.781, 'Va' => -1.324, 'Ve' => -1.324, 'Vi' => -0.434, 'Vo' => -1.781, 'Vr' => -1.324, 'Vu' => -0.891, 'Vy' => -0.891, 'W,' => -1.324, 'W-' => -0.480, 'W.' => -1.324, 'W:' => -0.434, 'W;' => -0.434, 'WA' => -1.324, 'Wa' => -0.891, 'We' => -0.434, 'Wi' => -0.211, 'Wo' => -0.434, 'Wr' => -0.434, 'Wu' => -0.434, 'Wy' => -0.434, 'Y ' => -0.434, 'Y,' => -2.660, 'Y-' => -1.324, 'Y.' => -2.660, 'Y:' => -1.781, 'Y;' => -1.781, 'YA' => -2.203, 'Ya' => -1.324, 'Ye' => -1.324, 'Yi' => -0.891, 'Yo' => -1.781, 'Yp' => -1.324, 'Yq' => -1.781, 'Yu' => -1.324, 'Yv' => -1.324, 'r,' => -1.324, 'r.' => -1.324, 'v,' => -1.781, 'v.' => -1.781, 'w,' => -0.891, 'w.' => -0.891, 'y,' => -1.781, 'y.' => -1.781, } ) Font_LiberationSans_bold_italic = Painter::FontMetricsData.new('LiberationSans', :bold_italic, 24, 26.088, @charWidth = { ' ' => 6.648, '!' => 7.992, '"' => 11.376, '#' => 13.344, '$' => 13.344, '%' => 21.336, '&' => 17.328, '\'' => 5.688, '(' => 7.992, ')' => 7.992, '*' => 9.336, '+' => 13.992, ',' => 6.648, '-' => 7.992, '.' => 6.648, '/' => 6.648, '0' => 13.344, '1' => 13.344, '2' => 13.344, '3' => 13.344, '4' => 13.344, '5' => 13.344, '6' => 13.344, '7' => 13.344, '8' => 13.344, '9' => 13.344, ':' => 7.992, ';' => 7.992, '<' => 13.992, '=' => 13.992, '>' => 13.992, '?' => 14.640, '@' => 23.400, 'A' => 17.328, 'B' => 17.328, 'C' => 17.328, 'D' => 17.328, 'E' => 15.984, 'F' => 14.640, 'G' => 18.648, 'H' => 17.328, 'I' => 6.648, 'J' => 13.344, 'K' => 17.328, 'L' => 14.640, 'M' => 19.992, 'N' => 17.328, 'O' => 18.648, 'P' => 15.984, 'Q' => 18.648, 'R' => 17.328, 'S' => 15.984, 'T' => 14.640, 'U' => 17.328, 'V' => 15.984, 'W' => 22.632, 'X' => 15.984, 'Y' => 15.984, 'Z' => 14.640, '[' => 7.992, '\\' => 6.648, ']' => 7.992, '^' => 13.992, '_' => 13.344, '`' => 7.992, 'a' => 13.344, 'b' => 14.640, 'c' => 13.344, 'd' => 14.640, 'e' => 13.344, 'f' => 7.992, 'g' => 14.640, 'h' => 14.640, 'i' => 6.648, 'j' => 6.648, 'k' => 13.344, 'l' => 6.648, 'm' => 21.336, 'n' => 14.640, 'o' => 14.640, 'p' => 14.640, 'q' => 14.640, 'r' => 9.336, 's' => 13.344, 't' => 7.992, 'u' => 14.640, 'v' => 13.344, 'w' => 18.648, 'x' => 13.344, 'y' => 13.344, 'z' => 12.000, '{' => 9.336, '|' => 6.696, '}' => 9.336, '~' => 13.992, }, @kerningDelta = { ' A' => -0.891, ' Y' => -0.434, '11' => -1.781, 'A ' => -0.891, 'AT' => -1.781, 'AV' => -1.781, 'AW' => -1.324, 'AY' => -1.781, 'F,' => -2.660, 'F.' => -2.660, 'FA' => -1.324, 'L ' => -0.434, 'LT' => -1.781, 'LV' => -1.324, 'LW' => -1.324, 'LY' => -1.781, 'P ' => -0.891, 'P,' => -3.094, 'P.' => -3.094, 'PA' => -1.781, 'RT' => -0.434, 'RW' => -0.434, 'RY' => -0.434, 'T,' => -1.781, 'T-' => -1.324, 'T.' => -1.781, 'T:' => -1.781, 'T;' => -1.781, 'TA' => -1.781, 'TO' => -0.434, 'Ta' => -0.891, 'Tc' => -0.891, 'Te' => -0.891, 'Ti' => -0.434, 'To' => -0.891, 'Tr' => -0.434, 'Ts' => -0.891, 'Tu' => -0.434, 'Tw' => -0.891, 'Ty' => -0.891, 'V,' => -2.203, 'V-' => -0.891, 'V.' => -2.203, 'V:' => -0.891, 'V;' => -0.891, 'VA' => -1.781, 'Va' => -0.891, 'Ve' => -0.891, 'Vi' => -0.891, 'Vo' => -0.891, 'Vr' => -0.434, 'Vu' => -0.434, 'Vy' => -0.434, 'W,' => -1.781, 'W-' => -0.891, 'W.' => -1.781, 'W:' => -0.891, 'W;' => -0.891, 'WA' => -1.324, 'Wa' => -0.434, 'We' => -0.434, 'Wi' => -0.211, 'Wo' => -0.434, 'Wr' => -0.434, 'Wu' => -0.434, 'Wy' => -0.434, 'Y ' => -0.434, 'Y,' => -2.203, 'Y-' => -1.781, 'Y.' => -2.203, 'Y:' => -1.324, 'Y;' => -1.324, 'YA' => -1.781, 'Ya' => -0.891, 'Ye' => -0.891, 'Yi' => -0.891, 'Yo' => -0.891, 'Yp' => -0.891, 'Yq' => -0.891, 'Yu' => -0.891, 'Yv' => -0.891, 'ff' => -0.434, 'r,' => -1.324, 'r.' => -1.324, 'v,' => -1.324, 'v.' => -1.324, 'w,' => -0.891, 'w.' => -0.891, 'y,' => -0.891, 'y.' => -0.891, } ) end end end TaskJuggler-3.8.1/lib/taskjuggler/Painter/FontMetrics.rb000066400000000000000000000076341473026623400232420ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = Painter.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # # Set this flag to true to generate FontData.rb. This will require the prawn # gem to be installed. For normal operation, this flag must be set to false. GeneratorMode = false if GeneratorMode # Only required to generate the font metrics data. require 'prawn' end require 'taskjuggler/Painter/FontMetricsData' unless GeneratorMode require 'taskjuggler/Painter/FontData' end class TaskJuggler class Painter # Class to compute or store the raw data for glyph size and kerning # infomation. Developers can use it to generate FontData.rb. This file # contains pre-computed font metrics data for some selected fonts. This # data can then be used to determine the width and height of a bounding # box of a given String. # # Developers can also use this file to generate FontData.rb using prawn as # a back-end. We currently do not want to have prawn as a runtime # dependency for TaskJuggler. class FontMetrics # Initialize the FontMetrics object. def initialize() @fonts = {} # We currently only support the LiberationSans font which is metric # compatible to Arial. @fonts['Arial'] = @fonts['LiberationSans'] = Font_LiberationSans_normal @fonts['Arial-Italic'] = @fonts['LiberationSans-Italic'] = Font_LiberationSans_italic @fonts['Arial-Bold'] = @fonts['LiberationSans-Bold'] = Font_LiberationSans_bold @fonts['Arial-BoldItalic'] = @fonts['LiberationSans-BoldItalic'] = Font_LiberationSans_bold_italic end # Return the height of the _font_ with _ptSize_ points in screen pixels. def height(font, ptSize) checkFontName(font) # Calculate resulting height scaled to the font size and convert to # screen pixels instead of points. (@fonts[font].height * (ptSize.to_f / @fonts[font].ptSize) * (4.0 / 3.0)).to_i end # Return the width of the string in screen pixels when using the font # _font_ with _ptSize_ points. def width(font, ptSize, str) checkFontName(font) w = 0 lastC = nil str.each_char do |c| cw = @fonts[font].glyphWidth(c) w += cw || @font[font].averageWidth if lastC delta = @fonts[font].kerningDelta[lastC + c] w += delta if delta end lastC = c end # Calculate resulting width scaled to the font size and convert to # screen pixels instead of points. (w * (ptSize.to_f / @fonts[font].ptSize) * (4.0 / 3.0)).to_i end private def checkFontName(font) unless @fonts.include?(font) raise ArgumentError, "Unknown font '#{font}'!" end end end end if GeneratorMode File.open('FontData.rb', 'w') do |f| f.puts <<'EOT' class TaskJuggler class Painter class FontMetrics EOT font = 'LiberationSans' f.puts Painter::FontMetricsData.new(font, :normal).to_ruby f.puts Painter::FontMetricsData.new(font, :italic).to_ruby f.puts Painter::FontMetricsData.new(font, :bold).to_ruby f.puts Painter::FontMetricsData.new(font, :bold_italic).to_ruby #font = 'Helvetica' #f.puts Painter::FontMetricsData.new(font, :normal).to_ruby #f.puts Painter::FontMetricsData.new(font, :italic).to_ruby #f.puts Painter::FontMetricsData.new(font, :bold).to_ruby #f.puts Painter::FontMetricsData.new(font, :bold_italic).to_ruby f.puts <<'EOT' end end end EOT end end end TaskJuggler-3.8.1/lib/taskjuggler/Painter/FontMetricsData.rb000066400000000000000000000110511473026623400240200ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = Painter.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # class TaskJuggler class Painter # The FontMetricsData objects generate and store the font metrics data for # a particular font. The glyph set is currently restricted to US ASCII # characters. class FontMetricsData MIN_GLYPH_INDEX = 32 MAX_GLYPH_INDEX = 126 attr_reader :ptSize, :charWidth, :height, :kerningDelta # The constructor can be used in two different modes. If all font data # is supplied, the object just stores the supplied font data. If only # the font name is given, the class uses the prawn library to generate # the font metrics for the requested font. def initialize(fontName, type = :normal, ptSize = 24, height = nil, wData = nil, kData = nil) @fontName = fontName @type = type @height = height @ptSize = ptSize @averageWidth = 0.0 if wData && kData @charWidth = wData @kerningDelta = kData else generateMetrics end end # Return the width of the glyph _c_. This must be a single character # String. If the glyph is not known, nil is returned. def glyphWidth(c) return @charWidth[c] end # The average with of all glyphs. def averageWidth return (@averageWidth * (3.0 / 4.0)).to_i end # Generate the FontMetricsData initialization code for the particular # font. The output will be Ruby syntax. def to_ruby indent = ' ' * 6 s = "#{indent}Font_#{@fontName.gsub(/-/, '_')}_#{@type} = " + "Painter::FontMetricsData.new('#{@fontName}', :#{@type}, " + "#{@ptSize}, #{"%.3f" % @height},\n" s << "#{indent} @charWidth = {" i = 0 @charWidth.each do |c, w| s << (i % 4 == 0 ? "\n#{indent} " : ' ') i += 1 s << "'#{escapedChars(c)}' => #{"%0.3f" % w}," end s << "\n#{indent} },\n" s << "#{indent} @kerningDelta = {" i = 0 @kerningDelta.each do |cp, w| s << (i % 4 == 0 ? "\n#{indent} " : ' ') i += 1 s << "'#{cp}' => #{"%.3f" % w}," end s << "\n#{indent} }\n#{indent})\n" end private def escapedChars(c) c.gsub(/\\/, '\\\\\\\\').gsub(/'/, '\\\\\'') end def generateMetrics @pdf = Prawn::Document.new ttfDir = "/usr/share/fonts/truetype/" @pdf.font_families.update( "LiberationSans" => { :bold => "#{ttfDir}LiberationSans-Bold.ttf", :italic => "#{ttfDir}LiberationSans-Italic.ttf", :bold_italic => "#{ttfDir}LiberationSans-BoldItalic.ttf", :normal => "#{ttfDir}LiberationSans-Regular.ttf" } ) @pdf.font(@fontName, :size => @ptSize, :style => @type) # Determine the height of the font. @height = @pdf.height_of("jjggMMWW") @charWidth = {} @averageWidth = 0.0 MIN_GLYPH_INDEX.upto(MAX_GLYPH_INDEX) do |c| char = "" << c begin @charWidth[char] = (w = @pdf.width_of(char)) rescue # the glyph is not in this font. end @averageWidth += w end @averageWidth /= (MAX_GLYPH_INDEX - MIN_GLYPH_INDEX) @kerningDelta = {} MIN_GLYPH_INDEX.upto(MAX_GLYPH_INDEX) do |c1| char1 = "" << c1 next unless (cw1 = glyphWidth(char1)) MIN_GLYPH_INDEX.upto(MAX_GLYPH_INDEX) do |c2| char2 = "" << c2 next unless (cw2 = glyphWidth(char2)) chars = char1 + char2 # The kerneing delta is the difference between the computed width # of the combined characters and the sum of the individual # character widths. delta = @pdf.width_of(chars, :kerning => true) - (cw1 + cw2) # We ususally don't use Strings longer than 100 characters. So we # can ignore kerning deltas below a certain threshhold. if delta.abs > 0.001 @kerningDelta[chars] = delta end end end end end end end TaskJuggler-3.8.1/lib/taskjuggler/Painter/Group.rb000066400000000000000000000043351473026623400220740ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = Group.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/Painter/Primitives' require 'taskjuggler/Painter/SVGSupport' class TaskJuggler class Painter # The Group can be used to group Elements together and define common # attributes in a single place. class Group include Primitives include SVGSupport def initialize(values, &block) @attributes = [ :fill, :font_family, :font_size, :stroke, :stroke_width ] values.each do |k, v| unless @attributes.include?(k) raise ArgumentError, "Unsupported attribute #{k}. " + "Use one of #{@attributes.join(', ')}." end end @values = values @elements = [] if block if block.arity == 1 # This is the traditional case where self is passed to the block. # All Primitives methods now must be prefixed with the block # variable to call them. yield self else # In order to have the primitives easily available in the block, # we use instance_eval to switch self to this object. But this # makes the methods of the original self no longer accessible. We # work around this by saving the original self and using # method_missing to delegate the method call to the original self. @originalSelf = eval('self', block.binding) instance_eval(&block) end end end # Delegator to @originalSelf. def method_missing(method, *args, &block) @originalSelf.send(method, *args, &block) end # Convert the Group into an XMLElement tree using SVG syntax. def to_svg XMLElement.new('g', valuesToSVG) do @elements.map { |el| el.to_svg } end end end end end TaskJuggler-3.8.1/lib/taskjuggler/Painter/Points.rb000066400000000000000000000022271473026623400222520ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = Points.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # class TaskJuggler class Painter # Utility class to describe a list of x, y coordinates. Each coordinate is # an Array with 2 elements. The whole list is another Array. class Points # Store the list after doing some error checking. def initialize(arr) arr.each do |point| unless point.is_a?(Array) && point.length == 2 raise ArgumentError, 'Points must be an Array with 2 coordinates' end end @points = arr end # Conver the list of coordinates into a String that is compatible with # SVG syntax. def to_s str = '' @points.each do |point| str += "#{point[0]},#{point[1]} " end str end end end end TaskJuggler-3.8.1/lib/taskjuggler/Painter/Primitives.rb000066400000000000000000000046551473026623400231400ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = Primitives.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/Painter/Color' require 'taskjuggler/Painter/Points' class TaskJuggler class Painter # This module contains utility methods to create the canvas Elements with # minimal overhead. The element is added to it's current parent and # mandatory arguments are enforced. It also eliminates the need to call # 'new' methods of each Element. module Primitives unless defined?(StrokeAttrs) StrokeAttrs = [ :stroke, :stroke_opacity, :stroke_width ] FillAttrs = [ :fill, :fill_opacity ] FillAndStrokeAttrs = StrokeAttrs + FillAttrs TextAttrs = FillAndStrokeAttrs + [ :font_family, :font_size ] end def color(*args) Color.new(*args) end def points(arr) Points.new(arr) end def group(attrs = {}, &block) @elements << (g = Group.new(attrs, &block)) g end def circle(cx, cy, r, attrs = {}) attrs[:cx] = cx attrs[:cy] = cy attrs[:r] = r @elements << (c = Circle.new(attrs)) c end def ellipse(cx, cy, rx, ry, attrs = {}) attrs[:cx] = cx attrs[:cy] = cy attrs[:rx] = rx attrs[:ry] = ry @elements << (e = Ellipse.new(attrs)) e end def line(x1, y1, x2, y2, attrs = {}) attrs[:x1] = x1 attrs[:y1] = y1 attrs[:x2] = x2 attrs[:y2] = y2 @elements << (l = Line.new(attrs)) l end def polyline(points, attrs = {}) attrs[:points] = points.is_a?(Array) ? Points.new(points) : points @elements << (l = PolyLine.new(attrs)) l end def rect(x, y, width, height, attrs = {}) attrs[:x] = x attrs[:y] = y attrs[:width] = width attrs[:height] = height @elements << (r = Rect.new(attrs)) r end def text(x, y, str, attrs = {}) attrs[:x] = x attrs[:y] = y @elements << (t = Text.new(str, attrs)) t end end end end TaskJuggler-3.8.1/lib/taskjuggler/Painter/SVGSupport.rb000066400000000000000000000015331473026623400230310ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = SVGSupport.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # class TaskJuggler class Painter # Utility module to convert the attributes into SVG compatible syntax. module SVGSupport def valuesToSVG values = {} @values.each do |k, v| unit = k == :font_size ? 'pt' : '' # Convert the underscores to dashes and the symbols to Strings. values[k.to_s.gsub(/[_]/, '-')] = v.to_s + unit end values end end end end TaskJuggler-3.8.1/lib/taskjuggler/Painter/Text.rb000066400000000000000000000013651473026623400217240ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = Text.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/XMLElement' require 'taskjuggler/Painter/Element' class TaskJuggler class Painter # A text element. class Text < Element # Create a text of _str_ at x, y coordinates. def initialize(str, attrs) super('text', [ :x, :y ] + TextAttrs, attrs) @text = str end end end end TaskJuggler-3.8.1/lib/taskjuggler/Project.rb000066400000000000000000001440201473026623400210000ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = Project.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/TjException' require 'taskjuggler/MessageHandler' require 'taskjuggler/FileList' require 'taskjuggler/TjTime' require 'taskjuggler/AlertLevelDefinitions' require 'taskjuggler/AccountCredit' require 'taskjuggler/Booking' require 'taskjuggler/DataCache' require 'taskjuggler/LeaveList' require 'taskjuggler/PropertySet' require 'taskjuggler/Attributes' require 'taskjuggler/RealFormat' require 'taskjuggler/PropertyList' require 'taskjuggler/TaskDependency' require 'taskjuggler/Scenario' require 'taskjuggler/Shift' require 'taskjuggler/Account' require 'taskjuggler/Task' require 'taskjuggler/Resource' require 'taskjuggler/reports/Report' require 'taskjuggler/ShiftAssignments' require 'taskjuggler/WorkingHours' require 'taskjuggler/TimeSheets' require 'taskjuggler/ProjectFileParser' require 'taskjuggler/BatchProcessor' require 'taskjuggler/Journal' require 'taskjuggler/KeywordArray' class TaskJuggler # This class implements objects that hold all project properties. Project # generally consist of resources, tasks and a number of other optional # properties. Tasks, Resources, Accounts and Shifts are all build on the # same underlying storage class PropertyTreeNode. Properties of the same # kind are kept in PropertySet objects. There is only one PropertySet for # each type of property. Additionally, each property may belong to various # PropertyList objects. In contrast to PropertySet objects, PropertyList # object have well defined sorting order and no information about the # attributes of each type of property. The PropertySet holds the blueprints # for the data construction inside the PropertyTreeNode objects. It contains # the list of known Attributes. class Project include MessageHandler attr_reader :accounts, :shifts, :tasks, :resources, :scenarios, :timeSheets, :reports, :inputFiles attr_accessor :reportContexts, :outputDir, :warnTsDeltas # Create a project with the specified +id+, +name+ and +version+. # The constructor will set default values for all project attributes. def initialize(id, name, version) AttributeBase.setMode(0) @attributes = { # This nested Array defines the supported alert levels. The lowest # level comes first at index 0 and the level rises from there on. # Currently, these levels are hardcoded. Each level entry has 3 # members: the tjp syntax token, the user visible name and the # associated color as RGB byte array. 'alertLevels' => AlertLevelDefinitions.new, 'auxdir' => '', 'copyright' => nil, 'costaccount' => nil, 'currency' => "EUR", 'currencyFormat' => RealFormat.new([ '-', '', '', ',', 2 ]), 'dailyworkinghours' => 8.0, 'end' => nil, 'markdate' => nil, 'flags' => [], 'journal' => Journal.new, 'limits' => nil, 'leaves' => LeaveList.new, 'loadUnit' => :days, 'name' => name, 'navigators' => {}, 'now' => TjTime.new.align(3600), 'numberFormat' => RealFormat.new([ '-', '', '', '.', 1]), 'priority' => 500, 'projectid' => id || "prj", 'projectids' => [ id ], 'rate' => 0.0, 'revenueaccount' => nil, 'scheduleGranularity' => Project.maxScheduleGranularity, 'shortTimeFormat' => "%H:%M", 'start' => nil, 'timeFormat' => "%Y-%m-%d", 'timezone' => TjTime.timeZone, 'trackingScenarioIdx' => nil, 'version' => version || "1.0", 'weekStartsMonday' => true, 'workinghours' => nil, 'yearlyworkingdays' => 260.714 } # Before we can add any properties to this project, we need to define the # attributes that each of the property types will be using. In TaskJuggler # lingo, properties of a project are resources, tasks, accounts, shifts # and scenarios. Each of these properties can have lots of further # information attached to it. These bits of information are called # attributes. An attribute is defined by the AttributeDefinition class. # The PropertySet objects need to be fed with a list of such attribute # definitions to register the attributes with the properties. @scenarios = PropertySet.new(self, true) attrs = [ # ID Name Type # Inh. Inh.Prj Scen. Default [ 'active', 'Enabled', BooleanAttribute, true, false, false, true ], [ 'id', 'ID', StringAttribute, false, false, false, nil ], [ 'name', 'Name', StringAttribute, false, false, false, nil ], [ 'ownbookings', 'Own Bookings', BooleanAttribute, false, false, false, true ], [ 'projection', 'Projection Mode', BooleanAttribute, true, false, false, false ], [ 'seqno', 'No', IntegerAttribute, false, false, false, nil ], ] attrs.each { |a| @scenarios.addAttributeType(AttributeDefinition.new(*a)) } @shifts = PropertySet.new(self, true) attrs = [ # ID Name Type # Inh. Inh.Prj Scen. Default [ 'bsi', 'BSI', StringAttribute, false, false, false, "" ], [ 'id', 'ID', StringAttribute, false, false, false, nil ], [ 'index', 'Index', IntegerAttribute, false, false, false, -1 ], [ 'leaves', 'Leaves', LeaveListAttribute, true, true, true, LeaveList.new ], [ 'name', 'Name', StringAttribute, false, false, false, nil ], [ 'replace', 'Replace', BooleanAttribute, true, false, true, false ], [ 'seqno', 'No', IntegerAttribute, false, false, false, nil ], [ 'timezone', 'Time Zone', StringAttribute, true, true, true, TjTime.timeZone ], [ 'tree', 'Tree Index', StringAttribute, false, false, false, "" ], [ 'workinghours', 'Working Hours', WorkingHoursAttribute, true, true, true, nil ] ] attrs.each { |a| @shifts.addAttributeType(AttributeDefinition.new(*a)) } @accounts = PropertySet.new(self, true) attrs = [ # ID Name Type # Inh. Inh.Prj Scen. Default [ 'aggregate', 'Aggregate', SymbolAttribute, true, false, false, :tasks ], [ 'bsi', 'BSI', StringAttribute, false, false, false, "" ], [ 'credits', 'Credits', AccountCreditListAttribute, false, false, true, [] ], [ 'id', 'ID', StringAttribute, false, false, false, nil ], [ 'index', 'Index', IntegerAttribute, false, false, false, -1 ], [ 'flags', 'Flags', FlagListAttribute, true, false, true, [] ], [ 'name', 'Name', StringAttribute, false, false, false, nil ], [ 'seqno', 'No', IntegerAttribute, false, false, false, nil ], [ 'tree', 'Tree Index', StringAttribute, false, false, false, "" ] ] attrs.each { |a| @accounts.addAttributeType(AttributeDefinition.new(*a)) } @resources = PropertySet.new(self, true) attrs = [ # ID Name Type # Inh. Inh.Prj Scen. Default [ 'alloctdeffort', 'Alloctd. Effort', FloatAttribute, false, false, true, 0.0 ], [ 'bsi', 'BSI', StringAttribute, false, false, false, "" ], [ 'chargeset', 'Charge Sets', ChargeSetListAttribute, true, false, true, [] ], [ 'criticalness', 'Criticalness', FloatAttribute, false, false, true, 0.0 ], [ 'duties', 'Duties', TaskListAttribute, false, false, true, [] ], [ 'directreports', 'Direct Reports', ResourceListAttribute, false, false, true, [] ], [ 'efficiency','Efficiency', FloatAttribute, true, false, true, 1.0 ], [ 'effort', 'Total Effort', IntegerAttribute, false, false, true, 0 ], [ 'email', 'Email', StringAttribute, false, false, false, nil ], [ 'fail', 'Failure Conditions', LogicalExpressionListAttribute, false, false, false, [] ], [ 'flags', 'Flags', FlagListAttribute, true, false, true, [] ], [ 'index', 'Index', IntegerAttribute, false, false, false, -1 ], [ 'leaveallowances', 'Leave Allowances', LeaveAllowanceListAttribute, true, false, true, LeaveAllowanceList.new ], [ 'leaves', 'Leaves', LeaveListAttribute, true, true, true, LeaveList.new ], [ 'limits', 'Limits', LimitsAttribute, true, true, true, nil ], [ 'managers', 'Managers', ResourceListAttribute, true, false, true, [] ], [ 'rate', 'Rate', FloatAttribute, true, true, true, 0.0 ], [ 'reports', 'Reports', ResourceListAttribute, false, false, true, [] ], [ 'seqno', 'No', IntegerAttribute, false, false, false, nil ], [ 'shifts', 'Shifts', ShiftAssignmentsAttribute, true, false, true, nil ], [ 'tree', 'Tree Index', StringAttribute, false, false, false, "" ], [ 'warn', 'Warning Condition', LogicalExpressionListAttribute, false, false, false, [] ], [ 'workinghours', 'Working Hours', WorkingHoursAttribute, true, true, true, nil ] ] attrs.each { |a| @resources.addAttributeType(AttributeDefinition.new(*a)) } @tasks = PropertySet.new(self, false) attrs = [ # ID Name Type # Inh. Inh.Prj Scen. Default [ 'allocate', 'Allocations', AllocationAttribute, true, false, true, [] ], [ 'assignedresources', 'Assigned Resources', ResourceListAttribute, false, false, true, [] ], [ 'booking', 'Bookings', BookingListAttribute, false, false, true, [] ], [ 'bsi', 'BSI', StringAttribute, false, false, false, "" ], [ 'charge', 'Charges', ChargeListAttribute, false, false, true, [] ], [ 'chargeset', 'Charge Sets', ChargeSetListAttribute, true, false, true, [] ], [ 'complete', 'Completion', FloatAttribute, false, false, true, nil ], [ 'competitors', 'Competitors', TaskListAttribute, false, false, true, [] ], [ 'criticalness', 'Criticalness', FloatAttribute, false, false, true, 0.0 ], [ 'depends', 'Preceding tasks', DependencyListAttribute, true, false, true, [] ], [ 'duration', 'Duration', DurationAttribute, false, false, true, 0 ], [ 'effort', 'Effort', DurationAttribute, false, false, true, 0 ], [ 'effortdone', 'Completed Effort', IntegerAttribute, false, false, true, nil ], [ 'effortleft', 'Remaining Effort', IntegerAttribute, false, false, true, nil ], [ 'end', 'End', DateAttribute, false, false, true, nil ], [ 'endpreds', 'End Preds.', TaskDepListAttribute, false, false, true, [] ], [ 'endsuccs', 'End Succs.', TaskDepListAttribute, false, false, true, [] ], [ 'fail', 'Failure Conditions', LogicalExpressionListAttribute, false, false, false, [] ], [ 'flags', 'Flags', FlagListAttribute, true, false, true, [] ], [ 'forward', 'Scheduling', BooleanAttribute, true, false, true, true ], [ 'gauge', 'Schedule gauge', StringAttribute, false, false, true, nil ], [ 'id', 'ID', StringAttribute, false, false, false, nil ], [ 'index', 'Index', IntegerAttribute, false, false, false, -1 ], [ 'length', 'Length', DurationAttribute, false, false, true, 0 ], [ 'limits', 'Limits', LimitsAttribute, false, false, true, nil ], [ 'maxend', 'Max. End', DateAttribute, true, false, true, nil ], [ 'maxstart', 'Max. Start', DateAttribute, false, false, true, nil ], [ 'milestone', 'Milestone', BooleanAttribute, false, false, true, false ], [ 'minend', 'Min. End', DateAttribute, false, false, true, nil ], [ 'minstart', 'Min. Start', DateAttribute, true, false, true, nil ], [ 'name', 'Name', StringAttribute, false, false, false, nil ], [ 'note', 'Note', RichTextAttribute, false, false, false, nil ], [ 'pathcriticalness', 'Path Criticalness', FloatAttribute, false, false, true, 0.0 ], [ 'precedes', 'Following tasks', DependencyListAttribute, true, false, true, [] ], [ 'priority', 'Priority', IntegerAttribute, true, true, true, 500 ], [ 'projectid', 'Project ID', SymbolAttribute, true, true, true, nil ], [ 'responsible', 'Responsible', ResourceListAttribute, true, false, true, [] ], [ 'scheduled', 'Scheduled', BooleanAttribute, true, false, true, false ], [ 'projectionmode', 'Projection Mode', BooleanAttribute, true, false, true, false ], [ 'seqno', 'No', IntegerAttribute, false, false, false, nil ], [ 'shifts', 'Shifts', ShiftAssignmentsAttribute, true, false, true, nil ], [ 'start', 'Start', DateAttribute, false, false, true, nil ], [ 'startpreds', 'Start Preds.', TaskDepListAttribute, false, false, true, [] ], [ 'startsuccs', 'Start Succs.', TaskDepListAttribute, false, false, true, [] ], [ 'status', 'Task Status', StringAttribute, false, false, true, "" ], [ 'tree', 'Tree Index', StringAttribute, false, false, false, "" ], [ 'warn', 'Warning Condition', LogicalExpressionListAttribute, false, false, false, [] ] ] attrs.each { |a| @tasks.addAttributeType(AttributeDefinition.new(*a)) } @reports = PropertySet.new(self, false) attrs = [ # ID Name Type # Inh. Inh.Prj Scen. Default [ 'accountroot', 'Account Root', PropertyAttribute, true, false, false, nil ], [ 'auxdir', 'Auxiliary files directory', StringAttribute, true, true, false, '' ], [ 'bsi', 'BSI', StringAttribute, false, false, false, '' ], [ 'caption', 'Caption', RichTextAttribute, true, false, false, nil ], [ 'center', 'Center', RichTextAttribute, true, false, false, nil ], [ 'columns', 'Columns', ColumnListAttribute, true, false, false, [] ], [ 'costaccount', 'Cost Account', AccountAttribute, true, true, false, nil ], [ 'currencyFormat', 'Currency Format', RealFormatAttribute, true, true, false, nil ], [ 'definitions', 'Definitions', DefinitionListAttribute, true, false, false, KeywordArray.new([ '*' ]) ], [ 'end', 'End', DateAttribute, true, true, false, nil ], [ 'markdate', 'Markdate', DateAttribute, true, true, false, nil ], [ 'epilog', 'Epilog', RichTextAttribute, true, false, false, nil ], [ 'flags', 'Flags', FlagListAttribute, true, false, true, [] ], [ 'footer', 'Footer', RichTextAttribute, true, false, false, nil ], [ 'formats', 'Formats', FormatListAttribute, true, false, false, [] ], [ 'ganttBars', 'Gantt Bars', BooleanAttribute, true, false, false, true ], [ 'header', 'Header', RichTextAttribute, true, false, false, nil ], [ 'headline', 'Headline', RichTextAttribute, true, false, false, nil ], [ 'hideAccount', 'Hide Account', LogicalExpressionAttribute, true, false, false, nil ], [ 'hideJournalEntry', 'Hide JournalEntry', LogicalExpressionAttribute, true, false, false, nil ], [ 'hideResource', 'Hide Resource', LogicalExpressionAttribute, true, false, false, nil ], [ 'hideTask', 'Hide Task', LogicalExpressionAttribute, true, false, false, nil ], [ 'height', 'Height', IntegerAttribute, false, false, false, 480 ], [ 'id', 'ID', StringAttribute, false, false, false, nil ], [ 'index', 'Index', IntegerAttribute, false, false, false, -1 ], [ 'interactive', 'Interactive', BooleanAttribute, false, false, false, false ], [ 'journalAttributes', 'Journal Attributes', SymbolListAttribute, true, false, false, KeywordArray.new([ '*' ]) ], [ 'journalMode', 'Journal Mode', SymbolAttribute, true, false, false, :journal ], [ 'left', 'Left', RichTextAttribute, true, false, false, nil ], [ 'loadUnit', 'Load Unit', StringAttribute, true, true, false, nil ], [ 'name', 'Name', StringAttribute, false, false, false, nil ], [ 'now', 'Now', DateAttribute, true, true, false, nil ], [ 'numberFormat', 'Number Format', RealFormatAttribute, true, true, false, nil ], [ 'openNodes', 'Open Nodes', NodeListAttribute, false, false, false, nil ], [ 'prolog', 'Prolog', RichTextAttribute, true, false, false, nil ], [ 'rawHtmlHead', 'Raw HTML Header', StringAttribute, true, false, false, nil ], [ 'resourceAttributes', 'Resource Attributes', FormatListAttribute, true, false, false, KeywordArray.new([ '*' ]) ], [ 'resourceroot', 'resource Root', PropertyAttribute, true, false, false, nil ], [ 'revenueaccount', 'Revenue Account', AccountAttribute, true, true, false, nil ], [ 'right', 'Right', RichTextAttribute, true, false, false, nil ], [ 'rollupAccount', 'Rollup Account', LogicalExpressionAttribute, true, false, false, nil ], [ 'rollupResource', 'Rollup Resource', LogicalExpressionAttribute, true, false, false, nil ], [ 'rollupTask', 'Rollup Task', LogicalExpressionAttribute, true, false, false, nil ], [ 'scenarios', 'Scenarios', ScenarioListAttribute, true, false, false, [ 0 ] ], [ 'selfcontained', 'Selfcontained', BooleanAttribute, true, false, false, false ], [ 'seqno', 'No', IntegerAttribute, false, false, false, nil ], [ 'shortTimeFormat', 'Short Time Format', StringAttribute, true, true, false, nil ], [ 'sortAccounts', 'Sort Accounts', SortListAttribute, true, false, false, [[ 'seqno', true, -1 ]] ], [ 'sortJournalEntries', 'Sort Journal Entries', JournalSortListAttribute, true, false, false, [[ :alert, 1 ], [ :date, 1 ], [ :seqno, 1 ]] ], [ 'sortResources', 'Sort Resources', SortListAttribute, true, false, false, [[ 'seqno', true, -1 ]] ], [ 'sortTasks', 'Sort Tasks', SortListAttribute, true, false, false, [[ 'seqno', true, -1 ]] ], [ 'start', 'Start', DateAttribute, true, true, false, nil ], [ 'taskAttributes', 'Task Attributes', FormatListAttribute, true, false, false, KeywordArray.new([ '*' ]) ], [ 'taskroot', 'Task Root', PropertyAttribute, true, false, false, nil ], [ 'timeFormat', 'Time Format', StringAttribute, true, true, false, nil ], [ 'timeOffId', 'Time Off ID', StringAttribute, false, false, false, nil ], [ 'timeOffName', 'Time Off Name', StringAttribute, false, false, false, nil ], [ 'timezone', 'Time Zone', StringAttribute, true, true, false, TjTime.timeZone ], [ 'title', 'Title', StringAttribute, true, false, false, nil ], [ 'tree', 'Tree Index', StringAttribute, false, false, false, "" ], [ 'weekStartsMonday', 'Week Starts Monday', BooleanAttribute, true, true, false, false ], [ 'width', 'Width', IntegerAttribute, true, false, false, 640 ], [ 'novevents', 'No vevents in icalreports', BooleanAttribute, true, false, false, false ] ] attrs.each { |a| @reports.addAttributeType(AttributeDefinition.new(*a)) } Scenario.new(self, 'plan', 'Plan Scenario', nil) # A list of files that contained the project data. @inputFiles = FileList.new @timeSheets = TimeSheets.new # A scoreboard that reflects the global working hours and leaves. @scoreboard = nil # A scoreboard that reflects the global working hours but no leaves. @scoreboardNoLeaves = nil # The ReportContext provides additional settings to the report that can # complement or replace the report attributes. Reports can include other # reports. During report generation, only one context is active, but the # context of enclosing reports needs to be preserved. Therefor we use a # stack to implement this. @reportContexts = [] @outputDir = './' @warnTsDeltas = false end # Overload the deep_clone function so that references to the project don't # lead to deep copying of the whole project. def deep_clone self end # Query the value of a Project attribute. _name_ is the ID of the attribute. def [](name) if !@attributes.has_key?(name) raise "Unknown project attribute #{name}" end @attributes[name] end # Set the Project attribute with ID _name_ to _value_. def []=(name, value) if !@attributes.has_key?(name) raise "Unknown project attribute #{name}" end @attributes[name] = value # If the start, end or schedule granularity have been changed, we have # to reset the working hours. if %w(start end scheduleGranularity timezone timingresolution). include?(name) if @attributes['start'] && @attributes['end'] @attributes['workinghours'] = WorkingHours.new(@attributes['scheduleGranularity'], @attributes['start'], @attributes['end'], @attributes['timezone']) end end value end # Return the number of defined scenarios for the project. def scenarioCount @scenarios.items end # Return the average number of working hours per day. This defaults to 8 but # can be set to other values by the user. def dailyWorkingHours @attributes['dailyworkinghours'].to_f end def weeklyWorkingDays @attributes['workinghours'].weeklyWorkingHours / @attributes['dailyworkinghours'] end # Return the average number of working days per month. def monthlyWorkingDays @attributes['yearlyworkingdays'] / 12.0 end # Return the average number of working days per year. def yearlyWorkingDays @attributes['yearlyworkingdays'].to_f end # Convert timeSlots to working days. def slotsToDays(slots) slots * @attributes['scheduleGranularity'] / (60 * 60 * dailyWorkingHours) end # call-seq: # scenario(index) -> Scenario # scenario(id) -> Scenario # # Return the Scenario with the given _id_ or _index_. def scenario(arg) if arg.is_a?(Integer) @scenarios.each do |sc| return sc if sc.sequenceNo - 1 == arg end else return @scenarios[arg] end nil end # call-seq: # scenarioIdx(scenario) # scenarioIdx(id) # # Return the index of the given Scenario specified by _scenario_ or _id_. def scenarioIdx(sc) if sc.is_a?(Scenario) return sc.sequenceNo - 1 elsif @scenarios[sc].nil? return nil else return @scenarios[sc].sequenceNo - 1 end end # Return the Shift with the ID _id_ or return nil if it does not exist. def shift(id) @shifts[id] end # Return the Account with the ID _id_ or return nil if it does not exist. def account(id) @accounts[id] end # Return the Task with the ID _id_ or return nil if it does not exist. def task(id) @tasks[id] end # Return the Resource with the ID _id_ or return nil if it does not exist. def resource(id) @resources[id] end # Return the Report with the ID +id+ or return nil if it does not exist. def report(id) @reports[id] end # Return the Report with the name +name+ or return nil if it does not # exist. def reportByName(name) @reports.each do |report| return report if report.name == name end nil end # This function must be called after the Project data structures have been # filled with data. It schedules all scenario and stores the result in the # data structures again. def schedule initScoreboards [ @accounts, @shifts, @resources, @tasks ].each do |p| # Set all index counters to their proper values. p.index end if @tasks.empty? error('no_tasks', "No tasks defined") end @scenarios.each do |sc| # Skip disabled scenarios next unless sc.get('active') scIdx = scenarioIdx(sc) # All user provided values are set now. The next step is to # propagate inherited values. These values must be marked as # inherited by setting the mode to 1. As we always call # PropertyTreeNode#inherit this is just a safeguard. AttributeBase.setMode(1) prepareScenario(scIdx) # Now change to mode 2 so all values that are modified are marked # as computed. AttributeBase.setMode(2) # Schedule the scenario. scheduleScenario(scIdx) # Complete the data sets, and check the result. finishScenario(scIdx) end resources.each do |resource| resource.checkFailsAndWarnings end tasks.each do |task| task.checkFailsAndWarnings end @timeSheets.warnOnDelta if @warnTsDeltas true end # Add the CSV output format to all reports of type 'tracereport' if # _enable_ is true. Otherwise remove all CSV output formats. def enableTraceReports(enable) @reports.each do |report| next unless report.typeSpec == :tracereport if enable # Enable the CSV format for the tracereport unless report.get('formats').include?(:csv) report.get('formats') << :csv end else # Disabe CSV format for the tracereport report.get('formats').delete(:csv) end end end # Make sure that we have a least one report defined that has an output # format. def checkReports if @reports.empty? warning('no_report_defined', "This project has no reports defined. " + "No output data will be generated.") end unless @accounts.empty? @reports.each do |report| if (report.typeSpec != :accountreport) && (report.get('costaccount').nil? || report.get('revenueaccount').nil?) warning('report_without_balance', "The report #{report.fullId} has no 'balance' defined. " + "No cost or revenue computation will be possible.", report.sourceFileInfo) end end end @reports.each do |report| return unless report.get('formats').empty? end warning('all_formats_empty', "None of the reports has a 'formats' attribute. " + "No output data will be generated.") end # Call this function to generate the reports based on the scheduling result. # This function may only be called after Project#schedule has been called. def generateReports(maxCpuCores) @reports.index if maxCpuCores == 1 @reports.each do |report| # Skip reports that don't have any format specified or trace reports # when generateTraces is false. next if report.get('formats').empty? Log.startProgressMeter("Report #{report.name}") @reportContexts.push(ReportContext.new(self, report)) report.generate @reportContexts.pop Log.stopProgressMeter end else # Kickoff the generation of all reports by pushing the jobs into the # BatchProcessor queue. bp = BatchProcessor.new(maxCpuCores) @reports.each do |report| # Skip reports that don't have any format specified or trace reports # when generateTraces is false. next if report.get('formats').empty? || (report.typeSpec == :trace && !generateTraces) bp.queue(report) { @reportContexts.push(ReportContext.new(self, report)) res = report.generate @reportContexts.pop res } end # Now wait for all the jobs to finish. bp.wait do |report| Log.startProgressMeter("Report #{report.tag.name}") $stdout.print(report.stdout) $stderr.print(report.stderr) if report.retVal.signaled? error('rg_signal', "Signal raised") end unless report.retVal.success? error('rg_abort', "Process aborted") end Log.stopProgressMeter end end DataCache.instance.flush end def generateReport(reportId, regExpMode, formats = nil, dynamicAttributes = nil) reportList = regExpMode ? reportList = matchingReports(reportId) : [ reportId ] reportList.each do |id| unless (report = @reports[id]) error('unknown_report_id', "Request to generate unknown report #{id}") end if formats.nil? && report.get('formats').empty? error('formats_empty', "The report #{report.fullId} has no 'formats' attribute. " + "No output data will be generated.", report.sourceFileInfo) end Log.startProgressMeter("Report #{report.name}") @reportContexts.push(context = ReportContext.new(self, report)) # If we have dynamic attributes we need to backup the old attributes # first, then parse the dynamicAttributes String replacing the # original values. if dynamicAttributes unless dynamicAttributes.empty? context.attributeBackup = report.backupAttributes parser = ProjectFileParser.new parser.parseReportAttributes(report, dynamicAttributes) end report.set('interactive', true) end report.generate(formats) if dynamicAttributes && !dynamicAttributes.empty? report.restoreAttributes(context.attributeBackup) end @reportContexts.pop Log.stopProgressMeter end end def listReports(reportId, regExpMode) reportList = regExpMode ? reportList = matchingReports(reportId) : @reports[reportId] ? [ reportId ] : [] puts "No match for #{reportId}" if reportList.empty? reportList.each do |id| report = @reports[id] formats = report.get('formats') next if formats.empty? puts sprintf("%s\t%s\t%s", id, formats.join(', '), report.name) end end def checkTimeSheets @timeSheets.check end #################################################################### # The following functions are not intended to be called from outside # the TaskJuggler library. There is no guarantee that these # functions will be usable or present in future releases. #################################################################### def addScenario(scenario) # :nodoc: @scenarios.addProperty(scenario) end def addShift(shift) # :nodoc: @shifts.addProperty(shift) end def addAccount(account) # :nodoc: @accounts.addProperty(account) end def addTask(task) # :nodoc: @tasks.addProperty(task) end def addResource(resource) # :nodoc: @resources.addProperty(resource) end def addReport(report) # :nodoc: @reports.addProperty(report) end def removeAccount(account) # :nodoc: @accounts.removeProperty(account) end # call-seq: # isWorkingTime(slotIdx) -> true or false # isWorkingTime(slot) -> true or false # isWorkingTime(startTime, endTime) -> true or false # isWorkingTime(interval) -> true or false # # Return true if the slot or interval is within globally defined working # time or false if not. If the argument is a TimeInterval, all slots of # the interval must be working time to return true as result. Global work # time means, no global leaves defined and the slot lies within a # defined global working time period. def isWorkingTime(*args) # Normalize argument(s) to TimeInterval if args.length == 1 if args[0].is_a?(Integer) return @scoreboard[args[0]].nil? elsif args[0].is_a?(TjTime) return @scoreboard[dateToIdx(args[0])].nil? elsif args[0].is_a?(TimeInterval) startIdx = dateToIdx(args[0].start) endIdx = dateToIdx(args[0].end) else raise ArgumentError, "Unsupported argument type #{args[0].class}" end else startIdx = dateToIdx(args[0]) endIdx = dateToIdx(args[1]) end startIdx.upto(endIdx) do |idx| return false if @scoreboard[idx] end true end # call-seq: # hasWorkingTime(startTime, endTime) -> true or false # hasWorkingTime(interval) -> true or false # # Return true if the interval overlaps with a globally defined working # time or false if not. Global work time means, no global leaves defined # and the slot lies within a defined global working time period. def hasWorkingTime(*args) # Normalize argument(s) to TimeInterval if args.length == 1 if args[0].is_a?(TimeInterval) startIdx = dateToIdx(args[0].start) endIdx = dateToIdx(args[0].end) else raise ArgumentError, "Unsupported argument type #{args[0].class}" end else startIdx = dateToIdx(args[0]) endIdx = dateToIdx(args[1]) end startIdx.upto(endIdx) do |idx| return true if @scoreboard[idx] end false end # Convert working _seconds_ to working days. The result depends on the # setting of the global 'dailyworkinghours' attribute. def convertToDailyLoad(seconds) seconds / (@attributes['dailyworkinghours'] * 3600.0) end # Many internal data structures use Scoreboard objects to keep track of # scheduling data. These have one entry for every schedulable time slot in # the project time frame. This functions returns the number of entries in # the scoreboards. def scoreboardSize ((@attributes['end'] - @attributes['start']) / @attributes['scheduleGranularity']).to_i end # Convert a Scoreboard index to the equivalent date. _idx_ is the index and # it must be within the range of the Scoreboard objects. If not, an # exception is raised. def idxToDate(idx) if $DEBUG && (idx < 0 || idx > scoreboardSize) raise "Scoreboard index out of range" end @attributes['start'] + idx * @attributes['scheduleGranularity'] end # Convert a _date_ (TjTime) to the equivalent Scoreboard index. If # _forceIntoProject_ is true, the date will be pushed into the project time # frame. def dateToIdx(date, forceIntoProject = true) if (date < @attributes['start'] || date > @attributes['end']) # Date is out of range. if forceIntoProject return 0 if date < @attributes['start'] return scoreboardSize - 1 if date > @attributes['end'] else raise "Date #{date} is out of project time range " + "(#{@attributes['start']} - #{@attributes['end']})" end end # Calculate the corresponding index. ((date - @attributes['start']) / @attributes['scheduleGranularity']).to_i end def collectTimeOffIntervals(iv, minDuration) @scoreboard.collectIntervals(iv, minDuration) do |val| val.is_a?(Integer) && (val & 0x3E) != 0 end end # Return the number of working days (ignoring global leaves) during the # given _interval_. def workingDays(interval) startIdx = dateToIdx(interval.start) endIdx = dateToIdx(interval.end) slots = 0 startIdx.upto(endIdx) do |idx| slots += 1 unless @scoreboardNoLeaves[idx] end slotsToDays(slots) end # Return the number of global working slots during the given time interval # specified by _startIdx_ and _endIdx_. This method takes global leaves # into account. def getWorkSlots(startIdx, endIdx) slots = 0 startIdx.upto(endIdx) do |idx| slots += 1 unless @scoreboard[idx] end slots end # Return true if for the date specified by the global scoreboard index # _sbIdx_ there is any resource that is available. def anyResourceAvailable?(sbIdx) @resourceAvailability[sbIdx] end # TaskJuggler keeps all times in UTC. All time values must be multiples of # the used scheduling granularity. If the local time zone is not # hour-aligned to UTC, the maximum allowed schedule granularity is # reduced. def Project.maxScheduleGranularity refTime = Time.gm(2000, 1, 1, 0, 0, 0) case (min = refTime.getlocal.min) when 0 # We are hour-aligned to UTC; scheduleGranularity is 1 hour 60 * 60 when 30 # We are half-hour off from UTC; scheduleGranularity is 30 minutes 30 * 60 when 15, 45 # We are 15 or 45 minutes off from UTC; scheduleGranularity is 15 # minutes 15 * 60 else raise "Unknown Time zone alignment #{min}" end end # Return the name of the attribute _id_. Since we don't know whether we # are looking for a task, resource, etc. attribute, we prefer tasks over # resources here. def attributeName(id) # We have to see if the attribute id is a task or resource attribute and # return it's name. (name = @tasks.attributeName(id)).nil? && (name = @resources.attributeName(id)).nil? name end def journal(query) @attributes['journal'].to_rti(query) end # Print the attribute values. It's used for debugging only. def to_s #raise "STOP!" str = '' @attributes.each do |attribute, value| if value str += "#{attribute}: " + "#{value.is_a?(PropertyTreeNode) ? value.fullId : value}" end end str end protected def prepareScenario(scIdx) Log.enter('prepareScenario', "Finishing scenario #{scenario(scIdx).get('name')}") Log.startProgressMeter("Preparing scenario " + "#{scenario(scIdx).get('name')}") resources = PropertyList.new(@resources) tasks = PropertyList.new(@tasks) # Compile a list of leaf resources that are actually used in this # project. usedResources = [] tasks.each do |task| task.candidates(scIdx).each do |resource| usedResources << resource unless usedResources.include?(resource) end end total = usedResources.length i = 0 usedResources.each do |resource| resource.prepareScheduling(scIdx) resource.preScheduleCheck(scIdx) i += 1 Log.progress((i.to_f / total) * 0.8) end resources.each { |resource| resource.setDirectReports(scIdx) } resources.each { |resource| resource.setReports(scIdx) } computeResourceAvailabilities(scIdx, usedResources) Log.progress(0.81) tasks.each { |task| task.prepareScheduling(scIdx) } Log.progress(0.82) tasks.each { |task| task.Xref(scIdx) } tasks.each { |task| task.propagateInitialValues(scIdx) } Log.progress(0.83) tasks.each { |task| task.preScheduleCheck(scIdx) } Log.progress(0.84) # Check for dependency loops in the task graph. tasks.each { |task| task.resetLoopFlags(scIdx) } tasks.each do |task| task.checkForLoops(scIdx, [], false, true, true) if task.parent.nil? end Log.progress(0.85) tasks.each { |task| task.resetLoopFlags(scIdx) } tasks.each do |task| task.checkForLoops(scIdx, [], true, true, false) if task.parent.nil? end Log.progress(0.87) # Compute the criticalness of the tasks and their pathes. tasks.each { |task| task.countResourceAllocations(scIdx) } Log.progress(0.88) resources.each { |resource| resource.calcCriticalness(scIdx) } Log.progress(0.9) tasks.each { |task| task.calcCriticalness(scIdx) } Log.progress(0.95) tasks.each { |task| task.calcPathCriticalness(scIdx) } Log.progress(0.99) @timeSheets.check Log.progress(1.0) Log.stopProgressMeter # This is used for debugging only if false resources.each do |resource| puts "#{resource}" end tasks.each do |task| puts "#{task}" end end Log.exit('prepareScenario', "Preparing scenario #{scenario(scIdx).get('name')} completed") end def finishScenario(scIdx) Log.enter('finishScenario', "Finishing scenario #{scenario(scIdx).get('name')}") Log.startProgressMeter("Checking scenario #{scenario(scIdx).get('name')}") @tasks.each do |task| # Recursively traverse the top-level tasks to finish all tasks. task.finishScheduling(scIdx) unless task.parent end @resources.each do |resource| # Recursively traverse the top-level resources to finish them all. resource.finishScheduling(scIdx) unless resource.parent end i = 0 total = @tasks.items @tasks.each do |task| task.postScheduleCheck(scIdx) if task.parent.nil? i += 1 Log.progress(i.to_f / total) end Log.stopProgressMeter Log.exit('finishScenario', "Finishing scenario #{scenario(scIdx).get('name')} completed") end # Schedule all tasks for the given Scenario with index +scIdx+. def scheduleScenario(scIdx) tasks = PropertyList.new(@tasks) # Only care about leaf tasks that are not milestones and aren't # scheduled already (marked with the 'scheduled' attribute). tasks.delete_if { |task| !task.leaf? || task['milestone', scIdx] || task['scheduled', scIdx] } Log.enter('scheduleScenario', "#{tasks.length} leaf tasks") # The sorting of the work item list determines which tasks will get their # resources first. The first sorting criterium is the user specified task # priority. The second criterium is the scheduler determined priority # stored in the pathcriticalness attribute. That way, the user can always # override the scheduler determined priority. To always have a defined # order, the third criterium is the sequence number. tasks.setSorting([ [ 'priority', false, scIdx ], [ 'pathcriticalness', false, scIdx ], [ 'seqno', true, -1 ] ]) tasks.sort! totalTasks = tasks.length # Enter the main scheduling loop. This loop is only terminated when all # tasks have been scheduled or another thread has set the breakFlag to # true. Log.startProgressMeter("Scheduling scenario " + "#{scenario(scIdx).get('name')}") failedTasks = [] while !tasks.empty? # Only update the progress bar every 10 completed tasks. if tasks.length % 10 == 0 percentComplete = (totalTasks - tasks.length).to_f / totalTasks Log.progress(percentComplete) end # Now find the task with the highest priority that can be scheduled # and schedule it. taskToRemove = nil tasks.each do |task| # Task not ready? Ignore it. next unless task.readyForScheduling?(scIdx) unless task.schedule(scIdx) failedTasks << task end # The task has been completed or failed. But we can remove it from # the todo list. taskToRemove = task # The scheduling of this task may cause other higher priority tasks # to be ready now. So we terminate the inner loop and start at the # top of the list again. break end # There should always be a task to be removed. If not, the scheduler # has found a set of tasks that deadlock each other. if taskToRemove tasks.delete(taskToRemove) elsif failedTasks.empty? warning('deadlock', 'Some tasks reference each other but don\'t provide ' + 'enough information to start the scheduling. The ' + 'scheduler does not know where to start scheduling ' + 'these tasks. You need to provide more fixed dates ' + 'or dependencies on already scheduled tasks.') failedTasks = tasks break else # We have some tasks that cannot be scheduled. break end end # Check for failed tasks and report the first 10 of them as # warnings. unless failedTasks.empty? warning('unscheduled_tasks', "#{failedTasks.length} tasks could not be scheduled") i = 0 failedTasks.each do |t| warning('unscheduled_task', "Task #{t.fullId}: " + "#{t['start', scIdx] ? t['start', scIdx] : ''} -> " + "#{t['end', scIdx] ? t['end', scIdx] : ''}", t.sourceFileInfo) i += 1 break if i >= 10 end Log.stopProgressMeter return false end Log.stopProgressMeter Log.exit('scheduleScenario', "Scheduling of scenario #{scIdx} finished") true end private def initScoreboards # Create scoreboard and mark all slots as unavailable @scoreboard = Scoreboard.new(@attributes['start'], @attributes['end'], @attributes['scheduleGranularity'], 2) # And the same for another scoreboard. @scoreboardNoLeaves = Scoreboard.new(@attributes['start'], @attributes['end'], @attributes['scheduleGranularity'], 2) workinghours = @attributes['workinghours'] # Change all work time slots to nil (available) again. date = @scoreboard.idxToDate(0) delta = @attributes['scheduleGranularity'] scoreboardSize.times do |i| if workinghours.onShift?(date) @scoreboard[i] = nil @scoreboardNoLeaves[i] = nil end date += delta end # Mark all global leave slots as such @attributes['leaves'].each do |leave| startIdx = @scoreboard.dateToIdx(leave.interval.start) endIdx = @scoreboard.dateToIdx(leave.interval.end) startIdx.upto(endIdx - 1) do |i| # If the slot is nil or set to 4 then don't set the time-off bit. sb = @scoreboard[i] @scoreboard[i] = ((sb.nil? || sb == 4) ? 0 : 2) | (1 << 2) end end end def computeResourceAvailabilities(scIdx, usedResources) @resourceAvailability = Scoreboard.new(@attributes['start'], @attributes['end'], @attributes['scheduleGranularity']) @resourceAvailability.each_index do |idx| usedResources.each do |resource| if resource.available?(scIdx, idx) @resourceAvailability[idx] = true break end end end end def matchingReports(reportId) list = [] @reports.each do |report| id = report.fullId list << id if Regexp.new(reportId) =~ id end list end end end TaskJuggler-3.8.1/lib/taskjuggler/ProjectFileParser.rb000066400000000000000000000446221473026623400227640ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = ProjectFileParser.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/TextParser' require 'taskjuggler/ProjectFileScanner' require 'taskjuggler/TjpSyntaxRules' require 'taskjuggler/RichText' require 'taskjuggler/RichText/RTFHandlers' class TaskJuggler # This class specializes the TextParser class for use with TaskJuggler project # files (TJP Files). The primary purpose is to provide functionality that make # it more comfortable to define the TaskJuggler syntax in a form that is human # creatable but also powerful enough to define the data structures the parser # needs to understand the syntax. # # By adding some additional information to the syntax rules, we can also # generate the complete reference manual from this rule set. class ProjectFileParser < TextParser include TjpSyntaxRules # Create the parser object. def initialize super # Define the token types that the ProjectFileScanner may return for # variable elements. @variables = [ :INTEGER, :FLOAT, :DATE, :TIME, :STRING, :LITERAL, :ID, :ID_WITH_COLON, :ABSOLUTE_ID, :MACRO ] initRules updateParserTables @project = nil end # Call this function with the master file to start processing a TJP file or # a set of TJP files. def open(file, master, fileNameIsBuffer = false) @scanner = ProjectFileScanner.new(file) # We need the ProjectFileScanner object for error reporting. if master && !fileNameIsBuffer && file != '.' && file[-4, 4] != '.tjp' error('illegal_extension', "Project file name must end with " + '\'.tjp\' extension') end @scanner.open(fileNameIsBuffer) @property = nil @scenarioIdx = 0 initFileStack # Stack for property IDs. Needed to handle nested 'supplement' # statements. @idStack = [] end # Call this function to cleanup the parser structures after the file # processing has been completed. def close @scanner.close end # This function will deliver the next token from the scanner. A token is a # two element Array that contains the ID or type of the token as well as the # text string of the token. def nextToken @scanner.nextToken end # This function can be used to return tokens. Returned tokens will be pushed # on a LIFO stack. To preserve the order of the original tokens the last # token must be returned first. This mechanism is used to implement # look-ahead functionality. def returnToken(token) @scanner.returnToken(token) end # A set of standard marcros is defined in all files as soon as the project # header has been read. Calling this functions gets the values from @project # and inserts the Macro objects into the ProjectFileScanner. def setGlobalMacros @scanner.addMacro(Macro.new('projectstart', @project['start'].to_s, @scanner.sourceFileInfo)) @scanner.addMacro(Macro.new('projectend', @project['end'].to_s, @scanner.sourceFileInfo)) @scanner.addMacro(Macro.new('now', @project['now'].to_s, @scanner.sourceFileInfo)) @scanner.addMacro(Macro.new('today', @project['now']. to_s(@project['timeFormat']), @scanner.sourceFileInfo)) end def parseReportAttributes(report, attributes) open(attributes, false, true) @property = report @project = report.project parse(:dynamicAttributes) end private # Utility function that convers English weekday names into their index # number and does some error checking. It returns 0 for 'sun', 1 for 'mon' # and so on. def weekDay(name) names = %w( sun mon tue wed thu fri sat ) if (day = names.index(@val[0])).nil? error('weekday', "Weekday name expected (#{names.join(', ')})") end day end # Make sure that certain attributes are not used after sub properties have # been added to a property. def checkContainer(attribute) if @property.container? error('container_attribute', "The attribute #{attribute} may not be used for this property " + 'after sub properties have been added.', @sourceFileInfo[0], @property) end end # Convenience function to check that an TimeInterval fits completely # within the project time frame. def checkInterval(iv) # Make sure the interval is within the project time frame. if iv.start < @project['start'] || iv.start >= @project['end'] error('interval_start_in_range', "Start date #{iv.start} must be within the project time frame " + "(#{@project['start']} - #{@project['end']})") end if iv.end <= @project['start'] || iv.end > @project['end'] error('interval_end_in_range', "End date #{iv.end} must be within the project time frame " + "(#{@project['start']} - #{@project['end']})") end end # Convenience function to check the integrity of a booking statement. def checkBooking(task, resource) unless task.leaf? error('booking_no_leaf', "#{task.fullId} is not a leaf task", @sourceFileInfo[0], task) end if task['milestone', @scenarioIdx] error('booking_milestone', "You cannot add bookings to a milestone", @sourceFileInfo[0], task) end unless resource.leaf? error('booking_group', "You cannot book a group resource", @sourceFileInfo[0], task) end end # The TaskJuggler syntax can be extended by the user when the properties are # extended with user-defined attributes. These attribute definitions # introduce keywords that have to be processed like the build-in keywords. # The parser therefor needs to adapt on the fly to the new syntax. By # calling this function, a TaskJuggler property can be extended with a new # attribute. @propertySet determines what property should be extended. # _type_ is the attribute type, _default_ is the default value. def extendPropertySetDefinition(type, default) if @propertySet.knownAttribute?(@val[1]) error('extend_redefinition', "The extended attribute #{@val[1]} has already been defined.") end # Determine the values for scenarioSpecific and inheritable. inherit = false scenarioSpecific = false unless @val[3].nil? @val[3].each do |option| case option when 'inherit' inherit = true when 'scenariospecific' scenarioSpecific = true end end end # Register the new Attribute type with the Property set it should belong # to. @propertySet.addAttributeType(AttributeDefinition.new( @val[1], @val[2], type, inherit, false, scenarioSpecific, default, true)) # Add the new user-defined attribute as reportable attribute to the parser # rule. oldCurrentRule = @cr @cr = @rules[:reportableAttributes] unless @cr.include?(@val[1]) singlePattern('_' + @val[1]) descr(@val[2]) end @cr = oldCurrentRule scenarioSpecific end # This function is primarily a wrapper around the RichText constructor. It # catches all RichTextScanner processing problems and converts the exception # data into a MessageHandler message that points to the correct location. # This is necessary, because the RichText parser knows nothing about the # actual input file. So we have to map the error location in the RichText # input stream back to the position in the project file. _sfi_ is the # SourceFileInfo of the input string. To limit the supported set of # variable tokens, a subset can be provided by _tokenSet_. def newRichText(text, sfi, tokenSet = nil) rText = RichText.new(text, RTFHandlers.create(@project, sfi)) # The RichText is processed by a separate parser. Messages will not have # the proper source file info unless we baseline them with the original # source file info. mh = MessageHandlerInstance.instance mh.baselineSFI = sfi rti = rText.generateIntermediateFormat( [ 0, 0, 0 ], tokenSet) # Reset the baseline again. mh.baselineSFI = nil rti.sectionNumbers = false if rti rti end # This method is a convenience wrapper around Report.new. It checks if # the report name already exists. It also triggers the attribute # inheritance. +name+ is the name of the report, +type+ is the report # type. +sourceFileInfo+ is a SourceFileInfo of the report definition. The # method returns the newly created Report. def newReport(id, name, type, sourceFileInfo) # If there is no parent property and the report prefix is not empty, the # reportprefix defines the parent property. if @property.nil? && !@reportprefix.empty? @property = @project.report(@reportprefix) end # Report IDs must be unique. If an ID was provided, check if it exists # already. if id # If we have a scope property, we need to prepend the ID of the scope # property to the provided ID. id = (@property ? @property.fullId + '.' : '') + @val[1] if @project.report(id) error('report_exists', "report #{id} has already been defined.", sourceFileInfo, @property) end end @reportCounter += 1 if name != '.' && name != '' if @project.reportByName(name) error('report_redefinition', "A report with the name #{name} has already been defined.") end end @property = Report.new(@project, id || "report#{@reportCounter}", name, @property) @property.typeSpec = type @property.sourceFileInfo = sourceFileInfo @property.inheritAttributes if block_given? # The default attribute values for this report type have to be set in # 'inherited' mode since they are not user provided. AttributeBase.setMode(1) yield AttributeBase.setMode(0) end end # If the @limitResources list is not empty, we have to create a Limits # object for each Resource. Otherwise, one Limits object is enough. def setLimit(name, value, interval) if @limitResources.empty? @limits.setLimit(name, value, interval) else @limitResources.each do |resource| @limits.setLimit(name, value, interval, resource) end end end # Set the _attribute_ to _value_ and reset all other duration attributes. def setDurationAttribute(attribute, value = true) checkContainer(attribute) { 'milestone' => false, 'duration' => 0, 'length' => 0, 'effort' => 0 }.each do |attr, val| if attribute == attr @property[attr, @scenarioIdx] = value else if @property.getAttribute(attr, @scenarioIdx).provided error('multiple_durations', "This duration criteria is overwriting a previously " + "provided criteria (duration, effort, length or milestone).") end @property[attr, @scenarioIdx] = val end end end # The following functions are mostly conveniance functions to simplify the # syntax tree definition. The *Rule functions may only be used in _rule # functions. And only one function call per _rule function is allowed. # This function creates a set of rules to describe a list of keywords. # _name_ is the name of the top-level rule and _items_ can be a Hash or # Array. The array just contains the allowed keywords, the Hash contains # keyword/description pairs. The description is used to describe # the keyword in the manual. The syntax supports two special cases. A '*' # means all items in the list and '-' means the list is empty. def allOrNothingListRule(name, items) newRule(name) { # A '*' means all possible items should be in the list. pattern(%w( _* ), lambda { KeywordArray.new([ '*' ]) }) descr('A shortcut for all items') # A '-' means the list should be empty. pattern([ '_-' ], lambda { KeywordArray.new }) descr('No items') # Or the list consists of one or more comma separated keywords. pattern([ "!#{name}_AoN_ruleItems" ], lambda { KeywordArray.new(@val[0]) }) } # Create the rule for the comma separated list. newRule("#{name}_AoN_ruleItems") { listRule("more#{name}_AoN_ruleItems", "!#{name}_AoN_ruleItem") } # Create the rule for the keywords with their description. newRule("#{name}_AoN_ruleItem") { if items.is_a?(Array) items.each { |keyword| singlePattern('_' + keyword) } else items.each do |keyword, description| singlePattern('_' + keyword) descr(description) if description end end } end def listRule(name, listItem) pattern([ "#{listItem}", "!#{name}" ], lambda { if @val[1] && @val[1].include?(@val[0]) error('duplicate_in_list', "Duplicate items in list.") end [ @val[0] ] + (@val[1].nil? ? [] : @val[1]) }) newRule(name) { commaListRule(listItem) } end def commaListRule(listItem) optional repeatable pattern([ '_,', "#{listItem}" ], lambda { @val[1] }) end # Create pattern that turns the rule into the definition for optional # attributes. _attributes_ is the rule that lists these attributes. def optionsRule(attributes) optional pattern([ '_{', "!#{attributes}", '_}' ], lambda { @val[1] }) end # Create a pattern with just a single _item_. The pattern returns the value # of that item. def singlePattern(item) pattern([ item ], lambda { @val[0] }) end # Add documentation for the current pattern of the currently processed rule. def doc(keyword, text) @cr.setDoc(keyword, text) end # Add documentation for patterns that only consists of a single terminal # token. def descr(text) if @cr.patterns[-1].length != 1 || (@cr.patterns[-1][0][0] != :literal && @cr.patterns[-1][0][0] != :variable) raise 'descr() may only be used for patterns with terminal tokens.' end arg(0, nil, text) end # Add documentation for the arguments with index _idx_ of the current # pattern of the currently processed rule. _name_ is that should be used for # this variable. _text_ is the documentation text. def arg(idx, name, text) @cr.setArg(idx, TextParser::TokenDoc.new(name, text)) end # Restrict the syntax documentation of the previously defined pattern to # the first +idx+ tokens. def lastSyntaxToken(idx) @cr.setLastSyntaxToken(idx) end # Specify the support level for the current pattern. def level(level) @cr.setSupportLevel(level) end # Add a reference to another pattern. This information is only used to # generate the documentation for the patterns of this rule. def also(seeAlso) seeAlso = [ seeAlso ] unless seeAlso.is_a?(Array) @cr.setSeeAlso(seeAlso) end # Add a TJP file or parts of it as an example. The TJP _file_ must be in the # directory test/TestSuite/Syntax/Correct. _tag_ can be used to identify # that only a part of the file should be included. def example(file, tag = nil) @cr.setExample(file, tag) end # Determine the title of the column with the ID _colId_. The title may be # from the static set or be from a user defined attribute. def columnTitle(colId) if @property.typeSpec == :tracereport "<-id->:<-scenario->.#{colId}" else TableReport.defaultColumnTitle(colId) || @project.attributeName(colId) end end # To manage certain variables that have file scope throughout a hierachie # of nested include files, we use a @fileStack to track those variables. # The values primarily live in their class instance variables. But upon # return from an included file, we need to restore the old values. This # function creates or resets the stack. def initFileStack @fileStackVariables = %w( taskprefix reportprefix resourceprefix accountprefix ) stackEntry = {} @fileStackVariables.each do |var| stackEntry[var] = '' instance_variable_set('@' + var, '') end @fileStack = [ stackEntry ] end # Push a new set of variables onto the @fileStack. def pushFileStack stackEntry = {} @fileStackVariables.each do |var| stackEntry[var] = instance_variable_get('@' + var) end @fileStack << stackEntry end # Pop the last stack entry from the @fileStack and restore the class # variables according to the now top-entry. def popFileStack stackEntry = @fileStack.pop @fileStackVariables.each do |var| instance_variable_set('@' + var, stackEntry[var]) end # Include files can only occur at global level or in the project header. # In both cases, the @property was nil on including and must be reset to # nil again after the include file. @property = nil end # This method most be used instead of the += operator for all list # attributes. += will always return an Array object. This will cause # trouble with the list attributes that are not plain Arrays. def appendScListAttribute(attrId, list) list.each do |v| @property[attrId, @scenarioIdx] << v end # The << operator does not set the 'provided' flag. Just do a self # assignment to trigget the flag to get set. begin @property[attrId, @scenarioIdx] = @property[attrId, @scenarioIdx] rescue AttributeOverwrite # Overwrites are ok here. end end end end TaskJuggler-3.8.1/lib/taskjuggler/ProjectFileScanner.rb000066400000000000000000000325201473026623400231130ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = ProjectFileScanner.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/TextParser/Scanner' class TaskJuggler # This class specializes the TextParser::Scanner class to detect the tokens # of the TJP syntax. class ProjectFileScanner < TextParser::Scanner def initialize(masterFile) tokenPatterns = [ # Any white spaces [ nil, /\s+/, :tjp, method('newPos') ], # Single line comments starting with # [ nil, /#.*\n?/, :tjp, method('newPos') ], # C++ style single line comments starting with // [ nil, /\/\/.*\n?/, :tjp, method('newPos') ], # C style single line comment /* .. */. [ nil, /\/\*.*\*\//, :tjp, method('newPos') ], # C style multi line comment: We need three patterns here. The first # one is for the start of the string. It switches the scanner mode to # the :cppComment mode. [ nil, /\/\*([^*]*[^\/]|.*)\n/, :tjp, method('startComment') ], # This is the string end pattern. It switches back to tjp mode. [ nil, /.*\*\//, :cppComment, method('endComment') ], # This pattern matches string lines that contain neither the start, # nor the end of the string. [ nil, /^.*\n/, :cppComment ], # Macro Call: This case is more complicated because we want to replace # macro calls inside of numbers, strings and identifiers. For this to # work, macro calls may have a prefix that looks like a number, a part # of a string or an identifier. This prefix is preserved and # re-injected into the scanner together with the expanded text. Macro # calls may span multiple lines. The ${ and the macro name must be in # the first line. Arguments that span multiple lines are not # supported. As above, we need rules for the start, the end and lines # with neither start nor end. Macro calls inside of strings need a # special start pattern that is active in the string modes. Both # patterns switch the scanner to macroCall mode. [ nil, /([-a-zA-Z_0-9>:.+]*|"(\\"|[^"])*?|'(\\'|[^'])*?)?\$\{\s*(\??[a-zA-Z_]\w*)(\s*"(\\"|[^"])*")*/, :tjp, method('startMacroCall') ], # This pattern is similar to the previous one, but is active inside of # multi-line strings. The corresponding rule for sizzors strings # can be found below. [ nil, /(\\"|[^"])*?\$\{\s*(\??[a-zA-Z_]\w*)(\s*"(\\"|[^"])*")*/, :dqString, method('startMacroCall') ], [ nil, /(\\'|[^'])*?\$\{\s*(\??[a-zA-Z_]\w*)(\s*"(\\"|[^"])*")*/, :sqString, method('startMacroCall') ], # This pattern matches the end of a macro call. It injects the prefix # and the expanded macro into the scanner again. The mode is restored # to the previous mode. [ nil, /(\s*"(\\"|[^"])*")*\s*\}/, :macroCall, method('endMacroCall') ], # This pattern collects macro call arguments in lines that contain # neither the start nor the end of the macro. [ nil, /.*\n/, :macroCall, method('midMacroCall') ], # Environment variable reference. This is similar to the macro call, # but the it can only extend within the starting line. [ nil, /([-a-zA-Z_0-9>:.+]*|"(\\"|[^"])*?|'(\\'|[^'])*?)?\$\([A-Z_][A-Z_0-9]*\)/, :tjp, method('environmentVariable') ], # An ID with a colon suffix: foo: [ :ID_WITH_COLON, /[a-zA-Z_]\w*:/, :tjp, method('chop') ], # An absolute ID: a.b.c [ :ABSOLUTE_ID, /[a-zA-Z_]\w*(\.[a-zA-Z_]\w*)+/ ], # A normal ID: bar [ :ID, /[a-zA-Z_]\w*/ ], # A date [ :DATE, /\d{4}-\d{1,2}-\d{1,2}(-\d{1,2}:\d{1,2}(:\d{1,2})?(-[-+]?\d{4})?)?/, :tjp, method('to_date') ], # A time of day [ :TIME, /\d{1,2}:\d{2}/, :tjp, method('to_time') ], # A floating point number (e. g. 3.143) [ :FLOAT, /\d*\.\d+/, :tjp, method('to_f') ], # An integer number [ :INTEGER, /\d+/, :tjp, method('to_i') ], # Multi line string enclosed with double quotes. The string may # contain double quotes prefixed by a backslash. The first rule # switches the scanner to dqString mode. [ 'nil', /"(\\"|[^"])*/, :tjp, method('startStringDQ') ], # Any line not containing the start or end. [ 'nil', /^(\\"|[^"])*\n/, :dqString, method('midStringDQ') ], # The end of the string. [ :STRING, /(\\"|[^"])*"/, :dqString, method('endStringDQ') ], # Multi line string enclosed with single quotes. [ 'nil', /'(\\'|[^'])*/, :tjp, method('startStringSQ') ], # Any line not containing the start or end. [ 'nil', /^(\\'|[^'])*\n/, :sqString, method('midStringSQ') ], # The end of the string. [ :STRING, /(\\'|[^'])*'/, :sqString, method('endStringSQ') ], # Scizzors marked string -8<- ... ->8-: The opening mark must be the # last thing in the line. The indentation of the first line after the # opening mark determines the indentation for all following lines. So, # we first switch the scanner to szrString1 mode. [ 'nil', /-8<-.*\n/, :tjp, method('startStringSZR') ], # Since the first line can be the last line (empty string case), we # need to detect the end in szrString1 and szrString mode. The # patterns switch the scanner back to tjp mode. [ :STRING, /\s*->8-/, :szrString1, method('endStringSZR') ], [ :STRING, /\s*->8-/, :szrString, method('endStringSZR') ], # This rule handles macros inside of sizzors strings. [ nil, /.*?\$\{\s*(\??[a-zA-Z_]\w*)(\s*"(\\"|[^"])*")*/, [ :szrString, :szrString1 ], method('startMacroCall') ], # Any line not containing the start or end. [ 'nil', /.*\n/, :szrString1, method('firstStringSZR') ], [ 'nil', /.*\n/, :szrString, method('midStringSZR') ], # Single line macro definition [ :MACRO, /\[.*\]\n/, :tjp, method('chop2nl') ], # Multi line macro definition: The pattern switches the scanner into # macroDef mode. [ nil, /\[.*\n/, :tjp, method('startMacroDef') ], # The end of the macro is marked by a ']' that is immediately followed # by a line break. It switches the scanner back to tjp mode. [ :MACRO, /.*\]\n/, :macroDef, method('endMacroDef') ], # Any line not containing the start or end. [ nil, /.*\n/, :macroDef, method('midMacroDef') ], # Some multi-char literals. [ :LITERAL, /<=?/ ], [ :LITERAL, />=?/ ], [ :LITERAL, /!=?/ ], # Everything else is returned as a single-char literal. [ :LITERAL, /./ ] ] super(masterFile, Log, tokenPatterns, :tjp) end private def to_i(type, match) [ type, match.to_i ] end def to_f(type, match) [ type, match.to_f ] end def to_time(type, match) h, m = match.split(':') h = h.to_i if h < 0 || h > 24 error('time_bad_hour', "Hour #{h} out of range (0 - 24)") end m = m.to_i if m < 0 || h > 59 error('time_bad_minute', "Minute #{m} out of range (0 - 59)") end if h == 24 && m != 0 error('time_bad_time', "Time #{match} cannot be larger then 24:00") end [ type, (h * 60 + m) * 60 ] end def to_date(type, match) begin [ type, TjTime.new(match) ] rescue TjException => msg error('time_error', msg.message) end end def newPos(type, match) @startOfToken = sourceFileInfo [ nil, '' ] end def chop(type, match) [ type, match[0..-2] ] end def chop2(type, match) # Remove first and last character. [ type, match[1..-2] ] end def chop2nl(type, match) # remove first and last \n (if it exists) and the last character. if match[-1] == ?\n [ type, match[1..-3] ] else [ type, match[1..-2] ] end end def startComment(type, match) self.mode = :cppComment [ nil, '' ] end def endComment(type, match) self.mode = :tjp [ nil, '' ] end def startStringDQ(type, match) self.mode = :dqString # Remove the opening " and remove the backslashes from escaped ". @string = match[1..-1].gsub(/\\"/, '"') [ nil, '' ] end def midStringDQ(type, match) # Remove the backslashes from escaped ". @string += match.gsub(/\\"/, '"') [ nil, '' ] end def endStringDQ(type, match) self.mode = :tjp # Remove the trailing " and remove the backslashes from escaped ". @string += match[0..-2].gsub(/\\"/, '"') [ :STRING, @string ] end def startStringSQ(type, match) self.mode = :sqString # Remove the opening ' and remove the backslashes from escaped '. @string = match[1..-1].gsub(/\\'/, "'") [ nil, '' ] end def midStringSQ(type, match) # Remove the backslashes from escaped '. @string += match.gsub(/\\'/, "'") [ nil, '' ] end def endStringSQ(type, match) self.mode = :tjp # Remove the trailing ' and remove the backslashes from escaped '. @string += match[0..-2].gsub(/\\'/, "'") [ :STRING, @string ] end def startStringSZR(type, match) # There should be a line break after the cut mark, but we allow some # spaces between the mark and the line break as well. if match.length != 5 && /-8<-\s*\n$/.match(match).nil? @lineDelta = 1 error('junk_after_cut', 'The cut mark -8<- must be immediately followed by a ' + 'line break.') end self.mode = :szrString1 @startOfToken = sourceFileInfo @string = '' [ nil, '' ] end def firstStringSZR(type, match) self.mode = :szrString # Split the leading indentation and the rest of the string. @indent, @string = */(\s*)(.*\n)/.match(match)[1, 2] [ nil, '' ] end def midStringSZR(type, match) # Ignore all the characters from the begining of match that are the same # in @indent. i = 0 while i < @indent.length && @indent[i] == match[i] i += 1 end @string += match[i..-1] [ nil, '' ] end def endStringSZR(type, match) self.mode = :tjp [ :STRING, @string ] end def environmentVariable(type, match) # Store any characters that precede the $( in prefix and remove it from # @macroCall. if (start = match.index('$(')) > 0 prefix = match[0..(start - 1)] envRef = match[start..-1] else prefix = '' envRef = match end # Remove '$(' and ')' varName = envRef[2..-2] if (value = ENV[varName]) @cf.injectText(prefix + value, envRef.length) else error('unknown_env_var', "Unknown environment variable '#{varName}'") end [ nil, '' ] end def startMacroDef(type, match) self.mode = :macroDef # Remove the opening '[' @macroDef = match[1..-1] [ nil, '' ] end def midMacroDef(type, match) @macroDef += match [ nil, '' ] end def endMacroDef(type, match) self.mode = :tjp # Remove "](\n|$)" if match[-1] == ?\n @macroDef += match[0..-3] else @macroDef += match[0..-2] end [ :MACRO, @macroDef ] end def startMacroCall(type, match) @macroCallPreviousMode = @scannerMode self.mode = :macroCall @macroCall = match [ nil, '' ] end def midMacroCall(type, match) @macroCall += match [ nil, '' ] end def endMacroCall(type, match) self.mode = @macroCallPreviousMode @macroCall += match # Store any characters that precede the ${ in prefix and remove it from # @macroCall. if (macroStart = @macroCall.index('${')) > 0 prefix = @macroCall[0..(macroStart - 1)] @macroCall = @macroCall[macroStart..-1] else prefix = '' end macroCallLength = @macroCall.length # Remove '${' and '}' and white spaces at begin and end argsStr = @macroCall[2..-2].sub(/^[ \t\n]*(.*?)[ \t\n]*$/, '\1') # Extract the macro name. if argsStr.index(' ').nil? expandMacro(prefix, [ argsStr ], macroCallLength) else macroName = argsStr[0, argsStr.index(' ')] # Remove the name part from argsStr argsStr = argsStr[macroName.length..-1] # Array to hold the arguments args = [] # We use another StringScanner to clean the double quotes. scanner = StringScanner.new(argsStr) while (scanner.scan(/\s*"/)) args << scanner.scan(/(\\"|[^"])*/).gsub(/\\"/, '"') scanner.scan(/"\s*/) end unless scanner.eos? error('junk_at_eom', "Junk found at end of macro: #{scanner.post_match}") end # Expand the macro and inject it into the scanner. expandMacro(prefix, [ macroName ] + args, macroCallLength) end [ nil, '' ] end end end TaskJuggler-3.8.1/lib/taskjuggler/PropertyList.rb000066400000000000000000000240011473026623400220460ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = PropertyList.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2019 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/PTNProxy' require 'taskjuggler/MessageHandler' class TaskJuggler # The PropertyList is a utility class that can be used to hold a list of # properties. It's derived from an Array, so it can hold the properties in a # well defined order. The order can be determined by an arbitrary number of # sorting levels. A sorting level specifies an attribute who's value should # be used for sorting, a scenario index if necessary and the sorting # direction (up/down). All nodes in the PropertyList must belong to the same # PropertySet. class PropertyList include MessageHandler attr_writer :query attr_reader :propertySet, :query, :sortingLevels, :sortingCriteria, :sortingUp, :scenarioIdx # A PropertyList is always bound to a certain PropertySet. All properties # in the list must be of that set. def initialize(arg, copyItems = true) @items = copyItems ? arg.to_ary : [] if arg.is_a?(PropertySet) # Create a PropertyList from the given PropertySet. @propertySet = arg # To keep the list sorted, we may have to access Property attributes. # Pre-scheduling, we can only use static attributes. Post-scheduling, # we can include dynamic attributes as well. This query template will # be used to query attributes when it has been set. Otherwise the list # can only be sorted by static attributes. @query = nil resetSorting addSortingCriteria('seqno', true, -1) sort! else # Create a PropertyList from a given other PropertyList. @propertySet = arg.propertySet @query = arg.query ? arg.query.dup : nil @sortingLevels = arg.sortingLevels @sortingCriteria = arg.sortingCriteria.dup @sortingUp = arg.sortingUp.dup @scenarioIdx = arg.scenarioIdx.dup end end # This class should be a derived class of Array. But since it re-defines # sort!() and still needs to call Array::sort!() I took a different route. # All missing methods will be propagated to the @items Array. def method_missing(func, *args, &block) @items.method(func).call(*args, &block) end def includeAdopted adopted = [] @items.each do |p| p.adoptees.each do |ap| adopted += includeAdoptedR(ap, p) end end append(adopted) end # Make sure that the list does not contain the same PropertyTreeNode more # than once. This could happen for adopted tasks. If you use # includeAdopted(), you should call this method after filtering to see if # the filter was strict enough. def checkForDuplicates(sourceFileInfo) ptns = {} @items.each do |i| if ptns.include?(i.ptn) error('proplist_duplicate', "An adopted property is included as #{i.logicalId} and " + "as #{ptns[i.ptn].logicalId}. Please use stronger filtering " + 'to avoid including the property more than once!', sourceFileInfo) end ptns[i.ptn] = i end end # Specialized version of Array::include? that also matches adopted tasks. def include?(node) !@items.find { |p| p.ptn == node.ptn }.nil? end def [](node) @items.find { |n| n.ptn == node.ptn } end def to_ary @items.dup end # Set all sorting levels as Array of triplets. def setSorting(modes) resetSorting modes.each do |mode| addSortingCriteria(*mode) end end # Clear all sorting levels. def resetSorting @sortingLevels = 0 @sortingCriteria = [] @sortingUp = [] @scenarioIdx = [] end # Append another Array of PropertyTreeNodes or a PropertyList to this. The # list will be sorted again. def append(list) if $DEBUG list.each do |node| unless node.propertySet == @propertySet raise "Fatal Error: All nodes must belong to the same PropertySet." end end end @items.concat(list) raise "Duplicate items" if @items != @items.uniq sort! end # If the first sorting level is 'tree' the breakdown structure of the # list is preserved. This is a somewhat special mode and this function # returns true if the mode is set. def treeMode? @sortingLevels > 0 && @sortingCriteria[0] == 'tree' end # Sort the properties according to the currently defined sorting criteria. def sort! if treeMode? # Tree sorting is somewhat complex. It will be based on the 'tree' # attribute of the PropertyTreeNodes but we have to update them first # based on the other sorting criteria. # Remove the tree sorting mode first. sc = @sortingCriteria.delete_at(0) su = @sortingUp.delete_at(0) si = @scenarioIdx.delete_at(0) @sortingLevels -= 1 # Sort the list based on the rest of the modes. sortInternal # The update the 'index' attributes of the PropertyTreeNodes. index # An then the 'tree' attributes. indexTree # Restore the 'tree' sorting mode again. @sortingCriteria.insert(0, sc) @sortingUp.insert(0, su) @scenarioIdx.insert(0, si) @sortingLevels += 1 # Sort again, now based on the updated 'tree' attributes. sortInternal else sortInternal end # Update indexes. index end # Return the Array index of _item_ or nil. def itemIndex(item) @items.index(item) end # This function sets the index attribute of all the properties in the list. # The index starts with 0 and increases for each property. def index i = 0 @items.each do |p| p.force('index', i += 1) end end # Turn the list into a String. This is only used for debugging. def to_s # :nodoc: res = "Sorting: " @sortingLevels.times do |i| res += "#{@sortingCriteria[i]}/#{@sortingUp[i] ? 'up' : 'down'}/" + "#{@scenarioIdx[i]}, " end res += "\n#{@items.length} properties:" @items.each { |i| res += "#{i.get('id')}: #{i.get('name')}\n" } res end private def includeAdoptedR(property, parent) # Create a proxy for the current PropertyTreeNode and add it to a list. adopted = [ parentProxy = PTNProxy.new(property, parent) ] # Add proxies for all children (adopted or not) and their children. property.kids.each do |p| adopted += includeAdoptedR(p, parentProxy) end adopted end # Append a new sorting level to the existing levels. def addSortingCriteria(criteria, up, scIdx) unless @propertySet.knownAttribute?(criteria) || @propertySet.hasQuery?(criteria, scIdx) raise TjException.new, "Unknown attribute '#{criteria}' used for sorting criterium" end if @propertySet.scenarioSpecific?(criteria) if @propertySet.project.scenario(scIdx).nil? raise TjException.new, "Unknown scenario index '#{scIdx}' used." end else scIdx == -1 end @sortingCriteria.push(criteria) @sortingUp.push(up) @scenarioIdx.push(scIdx) @sortingLevels += 1 end # Update the 'tree' indicies that are needed for the 'tree' sorting mode. def indexTree @items.each do |property| # The indicies are an Array if the 'index' attributes for this # property and all its parents. treeIdcs = property.getIndicies # Now convert them to a String. tree = '' treeIdcs.each do |idx| # Prefix the level index with zeros so that we always have a 6 # digit long String. 6 digits should be large enough for all # real-world projects. tree += idx.to_s.rjust(6, '0') end property.force('tree', tree) end end def sortInternal @items.sort! do |a, b| res = 0 @sortingLevels.times do |i| if @query && @sortingCriteria[i] != 'tree' # In case we have a Query reference, we get the two values with this # query. @query.scenarioIdx = @scenarioIdx[i] < 0 ? nil : @scenarioIdx[i] @query.attributeId = @sortingCriteria[i] @query.property = a @query.process unless @query.ok fatal "List sort failed: #{@query.errorMessage}" end aVal = @query.to_sort @query.property = b @query.process unless @query.ok fatal "List sort failed: #{@query.errorMessage}" end bVal = @query.to_sort else # In case we don't have a query, we use the static mechanism. # If the scenario index is negative we have a non-scenario-specific # attribute. if @scenarioIdx[i] < 0 if @sortingCriteria[i] == 'id' aVal = a.fullId bVal = b.fullId else aVal = a.get(@sortingCriteria[i]) bVal = b.get(@sortingCriteria[i]) end else aVal = a[@sortingCriteria[i], @scenarioIdx[i]] bVal = b[@sortingCriteria[i], @scenarioIdx[i]] end end res = aVal <=> bVal # Invert the result if we have to sort in decreasing order. res = -res unless @sortingUp[i] # If the two elements are equal on this compare level we try the next # level. break if res != 0 end res end end end end TaskJuggler-3.8.1/lib/taskjuggler/PropertySet.rb000066400000000000000000000234521473026623400216770ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = PropertySet.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/AttributeDefinition' require 'taskjuggler/PropertyTreeNode' class TaskJuggler # A PropertySet is a collection of properties of the same kind. Properties can # be Task, Resources, Scenario, Shift or Accounts objects. All properties of # the same kind belong to the same PropertySet. A property may only belong to # one PropertySet in the Project. The PropertySet holds the definitions for # the attributes. All Properties of the set will have a set of these # attributes. class PropertySet attr_reader :project, :flatNamespace, :attributeDefinitions def initialize(project, flatNamespace) if $DEBUG && project.nil? raise "project parameter may not be NIL" end # Indicates whether the namespace of this PropertySet is flat or not. In a # flat namespace all property IDs must be unique. Otherwise only the IDs # within a group of siblings must be unique. The full ID of the Property # is then composed of the siblings ID prefixed by the parent ID. ID fields # are separated by dots. @flatNamespace = flatNamespace # The main Project data structure reference. @project = project # A list of all PropertyTreeNodes in this set. @properties = Array.new # A hash of all PropertyTreeNodes in this set, hashed by their ID. This is # the same data as in @properties, but hashed by ID for faster access. @propertyMap = Hash.new # This is the blueprint for PropertyTreeNode attribute sets. Whever a new # PropertTreeNode is created, an attribute is created for each definition # in this list. @attributeDefinitions = Hash.new [ [ 'id', 'ID', StringAttribute, false, false, false, '' ], [ 'name', 'Name', StringAttribute, false, false, false, '' ], [ 'seqno', 'Seq. No', IntegerAttribute, false, false, false, 0 ] ].each { |a| addAttributeType(AttributeDefinition.new(*a)) } end # Use the function to declare the various attributes that properties of this # PropertySet can have. The attributes must be declared before the first # property is added to the set. def addAttributeType(attributeType) if !@properties.empty? raise "Fatal Error: Attribute types must be defined before " + "properties are added." end @attributeDefinitions[attributeType.id] = attributeType end # Iterate over all attribute definitions. def eachAttributeDefinition @attributeDefinitions.sort.each do |key, value| yield(value) end end # Return true if there is an AttributeDefinition for _attrId_. def knownAttribute?(attrId) @attributeDefinitions.include?(attrId) end # Check whether the PropertyTreeNode has a calculated attribute with the # ID _attrId_. For scenarioSpecific attributes _scenarioIdx_ needs to be # provided. def hasQuery?(attrId, scenarioIdx = nil) return false if @properties.empty? property = @properties.first methodName = 'query_' + attrId # First we check for non-scenario-specific query functions. if property.respond_to?(methodName) return true elsif scenarioIdx # Then we check for scenario-specific ones via the @data member. return property.data[scenarioIdx].respond_to?(methodName) end false end # Return whether the attribute with _attrId_ is scenario specific or not. def scenarioSpecific?(attrId) if @attributeDefinitions[attrId] # Check the 'scenarioSpecific' flag of the attribute definition. @attributeDefinitions[attrId].scenarioSpecific elsif (property = @properties.first) && property && property.data && property.data[0].respond_to?("query_#{attrId}") # We've found a query_ function for the attrId that is scenario # specific. true else # All hardwired, non-existing and non-scenario-specific query_ # candidates. false end end # Return whether the attribute with _attrId_ is inherited from the global # scope. def inheritedFromProject?(attrId) # All hardwired attributes are not inherited. return false if @attributeDefinitions[attrId].nil? @attributeDefinitions[attrId].inheritedFromProject end # Return whether the attribute with _attrId_ is inherited from parent. def inheritedFromParent?(attrId) # All hardwired attributes are not inherited. return false if @attributeDefinitions[attrId].nil? @attributeDefinitions[attrId].inheritedFromParent end # Return whether or not the attribute was user defined. def userDefined?(attrId) return false if @attributeDefinitions[attrId].nil? @attributeDefinitions[attrId].userDefined end def listAttribute?(attrId) (ad = @attributeDefinitions[attrId]) && ad.objClass.isList? end # Return the default value of the attribute. def defaultValue(attrId) return nil if @attributeDefinitions[attrId].nil? @attributeDefinitions[attrId].default end # Returns the name (human readable description) of the attribute with the # Id specified by _attrId_. def attributeName(attrId) # Some attributes are hardwired into the properties. These need to be # treated separately. if @attributeDefinitions.include?(attrId) return @attributeDefinitions[attrId].name end nil end # Return the type of the attribute with the Id specified by _attrId_. def attributeType(attrId) # Hardwired attributes need special treatment. if @attributeDefinitions.has_key?(attrId) @attributeDefinitions[attrId].objClass else nil end end # Add the new PropertyTreeNode object _property_ to the set. The set is # indexed by ID. In case an object with the same ID already exists in the # set it will be overwritten. # # Whenever the set has been extended, the 'bsi' and 'tree' attributes of the # properties are no longer up-to-date. You must call index() before using # these attributes. def addProperty(property) # The PropertyTreeNode objects are indexed by ID or hierachical ID # depending on the name space setting of this set. @propertyMap[property.id] = property @properties << property end # Remove the PropertyTreeNode (and all its children) object from the set. # _prop_ can either be a property ID or a reference to the PropertyTreeNode. # # TODO: This function does not take care of references to this PTN! def removeProperty(prop) if prop.is_a?(String) property = @propertyMap[prop] else property = prop end # Iterate over all properties and eliminate references to this the # PropertyTreeNode to be removed. @properties.each do |p| p.removeReferences(p) end # Recursively remove all sub-nodes. The children list is modified during # the call, so we can't use an iterator here. until property.children.empty? do removeProperty(property.children.first) end @properties.delete(property) @propertyMap.delete(property.fullId) # Remove this node from the child list of the parent node. property.parent.children.delete(property) if property.parent property end # Call this function to delete all registered properties. def clearProperties @properties.clear @propertyMap.clear end # Return the PropertyTreeNode object with ID _id_ from the set or nil if not # present. def [](id) @propertyMap[id] end # Update the breakdown structure indicies (bsi). This method needs to # be called whenever the set has been modified. def index each do |p| bsIdcs = p.getBSIndicies bsi = "" first = true bsIdcs.each do |idx| if first first = false else bsi += '.' end bsi += idx.to_s end p.force('bsi', bsi) end end # Return the index of the top-level _property_ in the set. def levelSeqNo(property) seqNo = 1 @properties.each do |p| unless p.parent return seqNo if p == property seqNo += 1 end end raise "Fatal Error: Unknow property #{property}" end # Return the maximum used number of breakdown levels. A flat list has a # maxDepth of 1. A list with one sub level has a maxDepth of 2 and so on. def maxDepth md = 0 each do |p| md = p.level if p.level > md end md + 1 end # Return the number of PropertyTreeNode objects in this set. def items @properties.length end alias length items # Return true if the set is empty. def empty? @properties.empty? end # Return the number of top-level PropertyTreeNode objects. Top-Level items # are no children. def topLevelItems items = 0 @properties.each do |p| items += 1 unless p.parent end items end # Iterator over all PropertyTreeNode objects in this set. def each @properties.each do |value| yield(value) end end # Return the set of PropertyTreeNode objects as flat Array. def to_ary @properties.dup end def to_s PropertyList.new(self).to_s end end end TaskJuggler-3.8.1/lib/taskjuggler/PropertyTreeNode.rb000066400000000000000000000631001473026623400226430ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = PropertyTreeNode.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2019 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/MessageHandler' class TaskJuggler # This class is the base object for all Project properties. A Project property # is a e. g. a Task, a Resource or other objects. Such properties can be # arranged in tree form by assigning child properties to an existing property. # The parent object needs to exist at object creation time. The # PropertyTreeNode class holds all data and methods that are common to the # different types of properties. Each property can have a set of predifined # attributes. The PropertySet class holds collections of the same # PropertyTreeNode objects and the defined attributes. # Each PropertySet has a predefined set of attributes, but the attribute set # can be extended by the user. E.g. a task has the predefined attribute # 'start' and 'end' date. The user can extend tasks with a user defined # attribute like an URL that contains more details about the task. class PropertyTreeNode include MessageHandler attr_reader :propertySet, :id, :subId, :parent, :project, :sequenceNo, :children, :adoptees attr_accessor :name, :sourceFileInfo attr_reader :data # Create a new PropertyTreeNode object. _propertySet_ is the PropertySet # that this PropertyTreeNode object belongs to. The PropertySet determines # the attributes that are common to all Nodes in the set. _id_ is a String # that is unique in the namespace of the set. _name_ is a user readable, # short description of the object. _parent_ is the PropertyTreeNode that # sits above this node in the object hierachy. A root object has a _parent_ # of nil. For sets with hierachical name spaces, parent can be nil and # specified by a hierachical _id_ (e. g. 'father.son'). def initialize(propertySet, id, name, parent) @propertySet = propertySet @project = propertySet.project @parent = parent # Scenario specific data @data = nil # Attributes are created on-demand. We need to be careful that a pure # check for existance does not create them unecessarily. @attributes = Hash.new do |hash, attributeId| unless (aType = attributeDefinition(attributeId)) raise ArgumentError, "Unknown attribute '#{attributeId}' requested for " + "#{self.class.to_s.sub(/TaskJuggler::/, '')} '#{fullId}'" end unless aType.scenarioSpecific hash[attributeId] = aType.objClass.new(@propertySet, aType, self) else raise ArgumentError, "Attribute '#{attributeId}' is scenario specific" end end @scenarioAttributes = Array.new(@project.scenarioCount) do |scenarioIdx| Hash.new do |hash, attributeId| unless (aType = attributeDefinition(attributeId)) raise ArgumentError, "Unknown attribute '#{attributeId}' requested for " + "#{self.class.to_s.sub(/TaskJuggler::/, '')} '#{fullId}'" end if aType.scenarioSpecific hash[attributeId] = aType.objClass.new(@propertySet, aType, @data[scenarioIdx]) else raise ArgumentError, "Attribute '#{attributeId}' is not scenario specific" end end end # If _id_ is still nil, we generate a unique id. unless id tag = self.class.to_s.gsub(/TaskJuggler::/, '') id = '_' + tag + '_' + (propertySet.items + 1).to_s id = parent.fullId + '.' + id if !@propertySet.flatNamespace && parent end if !@propertySet.flatNamespace && id.include?('.') parentId = id[0..(id.rindex('.') - 1)] # Set parent to the parent property if it's still nil. @parent = @propertySet[parentId] unless @parent if $DEBUG if !@parent || !@propertySet[@parent.fullId] raise "Fatal Error: parent must be member of same property set" end if parentId != @parent.fullId raise "Fatal Error: parent (#{@parent.fullId}) and parent ID " + "(#{@parentId}) don't match" end end @subId = id[(id.rindex('.') + 1).. -1] else @subId = id end # The attribute 'id' is either the short ID or the full hierarchical ID. set('id', fullId) # The name of the property. @name = name set('name', name) @level = -1 @sourceFileInfo = nil @sequenceNo = @propertySet.items + 1 set('seqno', @sequenceNo) # This is a list of the real sub nodes of this PropertyTreeNode. @children = [] # This is a list of the adopted sub nodes of this PropertyTreeNode. @adoptees = [] # In case we have a parent object, we register this object as child of # the parent. if (@parent) @parent.addChild(self) end # This is a list of the PropertyTreeNode objects that have adopted this # node. @stepParents = [] end # We only use deep_clone for attributes, never for properties. Since # attributes may reference properties these references should remain # references. def deep_clone self end # We often use PTNProxy objects to represent PropertyTreeNode objects. The # proxy usually does a good job acting like a PropertyTreeNode. But in # some situations, we want to make sure to operate on the PropertyTreeNode # and not the PTNProxy. Both classes provide a ptn() method that always # return the PropertyTreeNode. def ptn self end # Adopt _property_ as a step child. Also register the new relationship # with the child. def adopt(property) # A property cannot adopt itself. if self == property error('adopt_self', 'A property cannot adopt itself') end # A top level task must never contain the same leaf task more then once! allOfRoot = root.all property.allLeaves.each do |adoptee| if allOfRoot.include?(adoptee) error('adopt_duplicate_child', "The task '#{adoptee.fullId}' has already been adopted by " + "property '#{root.fullId}' or any of its sub-properties.") end end @adoptees << property property.getAdopted(self) end # Get adopted by _property_. Also register the new relationship with the # step parent. This method is for internal use only. Other classes should # alway use PropertyTreeNode::adopt(). def getAdopted(property) # :nodoc: return if @stepParents.include?(property) @stepParents << property end # Return a list of all children including adopted kids. def kids @children + @adoptees end # Return a list of all parents including step parents. def parents (@parent ? [ @parent ] : []) + @stepParents end # This method creates a shallow copy of all attributes and returns them as # an Array that can be used with restoreAttributes(). def backupAttributes [ @attributes.clone, @scenarioAttributes.clone ] end # Restore the attributes to a previously saved state. _backup_ is an Array # generated by backupAttributes(). def restoreAttributes(backup) @attributes, @scenarioAttributes = backup end # Remove any references in the stored data that references the _property_. def removeReferences(property) @children.delete(property) @adoptees.delete(property) @stepParents.delete(property) end # Return the index of the child _node_. def levelSeqNo(node) @children.index(node) + 1 end # Inherit values for the attributes from the parent node or the Project. def inheritAttributes # Inherit non-scenario-specific values @propertySet.eachAttributeDefinition do |attrDef| next if attrDef.scenarioSpecific || !attrDef.inheritedFromParent aId = attrDef.id if parent # Inherit values from parent property if parent.provided(aId) || parent.inherited(aId) @attributes[aId].inherit(parent.get(aId)) end else # Inherit selected values from project if top-level property if attrDef.inheritedFromProject if @project[aId] @attributes[aId].inherit(@project[aId]) end end end end # Inherit scenario-specific values @propertySet.eachAttributeDefinition do |attrDef| next if !attrDef.scenarioSpecific || !attrDef.inheritedFromParent @project.scenarioCount.times do |scenarioIdx| if parent # Inherit scenario specific values from parent property if parent.provided(attrDef.id, scenarioIdx) || parent.inherited(attrDef.id, scenarioIdx) @scenarioAttributes[scenarioIdx][attrDef.id].inherit( parent[attrDef.id, scenarioIdx]) end else # Inherit selected values from project if top-level property if attrDef.inheritedFromProject if @project[attrDef.id] && @scenarioAttributes[scenarioIdx][attrDef.id] @scenarioAttributes[scenarioIdx][attrDef.id].inherit( @project[attrDef.id]) end end end end end end # Returns a list of this node and all transient sub nodes. def all res = [ self ] kids.each do |c| res = res.concat(c.all) end res end # Return a list of all leaf nodes of this node. def allLeaves(withoutSelf = false) res = [] if leaf? res << self unless withoutSelf else kids.each do |c| res += c.allLeaves end end res end def logicalId fullId end # Return the full id of this node. For PropertySet objects with a flat # namespace, this is just the ID. Otherwise, the full ID is composed of all # IDs from the root node to this node, separating the IDs by a dot. def fullId res = @subId unless @propertySet.flatNamespace t = self until (t = t.parent).nil? res = t.subId + "." + res end end res end # Returns the level that this property is on. Top-level properties return # 0, their children 1 and so on. This value is cached internally, so it does # not have to be calculated each time the function is called. def level return @level if @level >= 0 t = self @level = 0 until (t = t.parent).nil? @level += 1 end @level end # Return the hierarchical index of this node. In project management lingo # this is called the Breakdown Structure Index (BSI). The result is an Array # with an index for each level from the root to this node. def getBSIndicies idcs = [] p = self begin parent = p.parent idcs.insert(0, parent ? parent.levelSeqNo(p) : @propertySet.levelSeqNo(p)) p = parent end while p idcs end # Return the 'index' attributes of this property, prefixed by the 'index' # attributes of all its parents. The result is an Array of Integers. def getIndicies idcs = [] p = self begin parent = p.parent idcs.insert(0, p.get('index')) p = parent end while p idcs end # Add _child_ node as child to this node. def addChild(child) if $DEBUG && child.propertySet != @propertySet raise "Child nodes must belong to the same property set as the parent" end @children.push(child) end # Find out if this property is a direct or indirect child of _ancestor_. def isChildOf?(ancestor) parent = self while parent = parent.parent return true if (parent == ancestor) end false end # Return true if the node is a leaf node (has no children). def leaf? @children.empty? && @adoptees.empty? end # Return true if the node has children. def container? !@children.empty? || !@adoptees.empty? end # Return a list with all parent nodes of this node. def ancestors(includeStepParents = false) nodes = [] if includeStepParents parents.each do |parent| nodes << parent nodes += parent.ancestors(true) end else n = self while n.parent nodes << (n = n.parent) end end nodes end # Return the top-level node for this node. def root n = self while n.parent n = n.parent end n end # Return the type of the attribute with ID _attributeId_. def attributeDefinition(attributeId) @propertySet.attributeDefinitions[attributeId] end # Return the value of the non-scenario-specific attribute with ID # _attributeId_. This method works for built-in attributes as well. # In case the attribute does not exist, an exception is raised. def get(attributeId) # Make sure the attribute gets created if it doesn't exist already. @attributes[attributeId] instance_variable_get(('@' + attributeId).intern) end # Return the value of the attribute with ID _attributeId_. This method # works for built-in attributes as well. In case this is a # scenario-specific attribute, the scenario index needs to be provided by # _scenarioIdx_, otherwise it must be nil. In case the attribute does not # exist, an exception is raised. def getAttribute(attributeId, scenarioIdx = nil) if scenarioIdx @scenarioAttributes[scenarioIdx][attributeId] else @attributes[attributeId] end end # Set the non-scenario-specific attribute with ID _attributeId_ to # _value_. No further checks are done. def force(attributeId, value) @attributes[attributeId].set(value) end # Set the non-scenario-specific attribute with ID _attributeId_ to _value_. # In case an already provided value is overwritten again, an exeception is # raised. def set(attributeId, value) attr = @attributes[attributeId] # Assignments to list attributes always append. We don't # consider this an overwrite. overwrite = attr.provided && !attr.isList? attr.set(value) # We only raise the overwrite error after the value has been set. if overwrite raise AttributeOverwrite, "Overwriting a previously provided value for attribute " + "#{attributeId}" end end # Set the scenario specific attribute with ID _attributeId_ for the # scenario with index _scenario_ to _value_. If _scenario_ is nil, the # attribute must not be scenario specific. In case the attribute does not # exist, an exception is raised. def []=(attributeId, scenario, value) overwrite = false if scenario if AttributeBase.mode == 0 # If we get values in 'provided' mode, we copy them immedidately to # all derived scenarios. @project.scenario(scenario).all.each do |sc| scenarioIdx = @project.scenarioIdx(sc) attr = @scenarioAttributes[scenarioIdx][attributeId] if attr.provided && !attr.isList? # Assignments to list attributes always append. We don't # consider this an overwrite. overwrite = true end if scenarioIdx == scenario attr.set(value) else attr.inherit(value) end end else attr = @scenarioAttributes[scenario][attributeId] overwrite = attr.provided && !attr.isList? attr.set(value) end else attr = @attributes[attributeId] overwrite = attr.provided && !attr.isList? attr.set(value) end # We only raise the overwrite error after all scenarios have been # set. For some attributes the overwrite is actually allowed. if overwrite raise AttributeOverwrite, "Overwriting a previously provided value for attribute " + "#{attributeId}" end end # Return the value of the attribute with ID _attributeId_. For # scenario-specific attributes, _scenario_ must indicate the index of the # Scenario. def [](attributeId, scenario) @scenarioAttributes[scenario][attributeId] @data[scenario].instance_variable_get(('@' + attributeId).intern) end # Returns true if the value of the attribute _attributeId_ (in scenario # _scenarioIdx_) has been provided by the user. def provided(attributeId, scenarioIdx = nil) if scenarioIdx unless @scenarioAttributes[scenarioIdx].has_key?(attributeId) return false end @scenarioAttributes[scenarioIdx][attributeId].provided else return false unless @attributes.has_key?(attributeId) @attributes[attributeId].provided end end # Returns true if the value of the attribute _attributeId_ (in scenario # _scenarioIdx_) has been inherited from a parent node or scenario. def inherited(attributeId, scenarioIdx = nil) if scenarioIdx unless @scenarioAttributes[scenarioIdx].has_key?(attributeId) return false end @scenarioAttributes[scenarioIdx][attributeId].inherited else return false unless @attributes.has_key?(attributeId) @attributes[attributeId].inherited end end def modified?(attributeId, scenarioIdx = nil) if scenarioIdx unless @scenarioAttributes[scenarioIdx].has_key?(attributeId) return false end @scenarioAttributes[scenarioIdx][attributeId].provided || @scenarioAttributes[scenarioIdx][attributeId].inherited else return false unless @attributes.has_key?(attributeId) @attributes[attributeId].provided || @attributes[attributeId].inherited end end def checkFailsAndWarnings if @attributes.has_key?('fail') || @attributes.has_key?('warn') propertyType = case self when Task 'task' when Resource 'resource' else 'unknown' end queryAttrs = { 'project' => @project, 'property' => self, 'scopeProperty' => nil, 'start' => @project['start'], 'end' => @project['end'], 'loadUnit' => :days, 'numberFormat' => @project['numberFormat'], 'timeFormat' => nil, 'currencyFormat' => @project['currencyFormat'] } query = Query.new(queryAttrs) if @attributes['fail'] @attributes['fail'].get.each do |expr| if expr.eval(query) error("#{propertyType}_fail_check", "User defined check failed for #{propertyType} " + "#{fullId} \n" + "Condition: #{expr.to_s}\n" + "Result: #{expr.to_s(query)}") end end end if @attributes['warn'] @attributes['warn'].get.each do |expr| if expr.eval(query) warning("#{propertyType}_warn_check", "User defined warning triggered for #{propertyType} " + "#{fullId} \n" + "Condition: #{expr.to_s}\n" + "Result: #{expr.to_s(query)}") end end end end end def query_children(query) list = [] kids.each do |property| if query.listItem rti = RichText.new(query.listItem, RTFHandlers.create(@project)). generateIntermediateFormat q = query.dup q.property = property rti.setQuery(q) list << "#{rti.to_s}" else list << "#{property.name} (#{property.fullId})" end end query.assignList(list) end def query_journal(query) @project['journal'].to_rti(query) end def query_alert(query) journal = @project['journal'] query.sortable = query.numerical = alert = journal.alertLevel(query.end, self, query) alertLevel = @project['alertLevels'][alert] query.string = alertLevel.name rText = "#{alertLevel.name}" + "" unless (rti = RichText.new(rText, RTFHandlers.create(@project)). generateIntermediateFormat) warning('ptn_journal', "Syntax error in journal message") return nil end rti.blockMode = false query.rti = rti end def query_alertmessages(query) journalMessages(@project['journal'].alertEntries(query.end, self, 1, query.start, query), query, true) end def query_alertsummaries(query) journalMessages(@project['journal'].alertEntries(query.end, self, 1, query.start, query), query, false) end def query_journalmessages(query) journalMessages(@project['journal'].currentEntries(query.end, self, 0, query.start, query.hideJournalEntry), query, true) end def query_journalsummaries(query) journalMessages(@project['journal'].currentEntries(query.end, self, 0, query.start, query.hideJournalEntry), query, false) end def query_alerttrend(query) journal = @project['journal'] startAlert = journal.alertLevel(query.start, self, query) endAlert = journal.alertLevel(query.end, self, query) if startAlert < endAlert query.sortable = 0 query.string = 'Up' elsif startAlert > endAlert query.sortable = 2 query.string = 'Down' else query.sortable = 1 query.string = 'Flat' end end # Dump the class data in human readable form. Used for debugging only. def to_s # :nodoc: res = "#{self.class} #{fullId} \"#{@name}\"\n" + " Sequence No: #{@sequenceNo}\n" res += " Parent: #{@parent.fullId}\n" if @parent children = "" @children.each do |c| children += ', ' unless children.empty? children += c.fullId end res += ' Children: ' + children + "\n" unless children.empty? @attributes.sort.each do |key, attr| res += indent(" #{key}: ", attr.to_s) end unless @scenarioAttributes.empty? project.scenarioCount.times do |sc| break if @scenarioAttributes[sc].nil? headerShown = false @scenarioAttributes[sc].sort.each do |key, attr| unless headerShown res += " Scenario #{project.scenario(sc).get('id')} (#{sc})\n" headerShown = true end res += indent(" #{key}: ", attr.to_s) end end end res += '-' * 75 + "\n" end alias to_str to_s # Many PropertyTreeNode functions are scenario specific. These functions are # provided by the class *Scenario classes. In case we can't find a function # called for the base class we try to find it in corresponding *Scenario # class. def method_missing(func, scenarioIdx = 0, *args, &block) @data[scenarioIdx].send(func, *args, &block) end private # Create a blog-style list of all alert messages that match the Query. def journalMessages(entries, query, longVersion) # The components of the message are either UTF-8 text or RichText. For # the RichText components, we use the originally provided markup since # we compose the result as RichText markup first. rText = '' entries.each do |entry| rText += "==== " + entry.headline + " ====\n" rText += "''Reported on #{entry.date.to_s(query.timeFormat)}'' " if entry.author rText += "''by #{entry.author.name}''" end rText += "\n\n" unless entry.flags.empty? rText += "''Flags:'' #{entry.flags.join(', ')}\n\n" end if entry.summary rText += entry.summary.richText.inputText + "\n\n" end if longVersion && entry.details rText += entry.details.richText.inputText + "\n\n" end end # Now convert the RichText markup String into RichTextIntermediate # format. unless (rti = RichText.new(rText, RTFHandlers.create(@project)). generateIntermediateFormat) warning('ptn_journal', "Syntax error in journal message") return nil end # No section numbers, please! rti.sectionNumbers = false # We use a special class to allow CSS formating. rti.cssClass = 'tj_journal' query.rti = rti end def indent(tag, str) tag + str.gsub(/\n/, "\n#{' ' * tag.length}") + "\n" end end end TaskJuggler-3.8.1/lib/taskjuggler/Query.rb000066400000000000000000000345161473026623400205070ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = Query.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/TjException' class TaskJuggler # A query can be used to retrieve any property attribute after the scheduling # run has been completed. It is possible to make a Query before the scheduling # run has been completed, but it only produces good results for static # attributes. And for such queries, the PropertyTreeNode.get and [] functions # are a lot more efficient. # # When constructing a Query, a set of variables need to be set that is # sufficient enough to identify a unique attribute. Some attribute are # computed dynamically and further variables such as a start and end time will # be incorporated into the result computation. # # The result is returned as String (Query#result), in numerical form # (Query#numericalResult) if available as number, and as an entity that can be # used for sorting (Query#sortableResult). To get the result, Query#process # needs to be called. In case an error occured, Query#ok is set to false and # Query#errorMessage contains an error message. class Query @@ps = %w( project propertyType propertyId property scopePropertyType scopePropertyId scopeProperty attributeId scenario scenarioIdx loadUnit numberFormat currencyFormat timeFormat listItem listType hideJournalEntry journalMode journalAttributes sortJournalEntries costAccount revenueAccount selfContained ) @@ps.each do |p| attr_accessor p.to_sym end attr_accessor :ok, :errorMessage attr_reader :end, :endIdx, :start, :startIdx attr_writer :sortable, :numerical, :string, :rti # Create a new Query object. The _parameters_ need to be sufficent to # uniquely identify an attribute. def initialize(parameters = { }) @selfContained = false @@ps.each do |p| instance_variable_set('@' + p, parameters[p] ? parameters[p] : nil) end # instance_variable_set does not call writer functions. So we need to # handle @start, @end, @startIdx and @endIdx separately. %w( end endIdx start startIdx ).each do |p| send(p + '=', parameters[p]) if parameters[p] end # The custom data hash can be filled with results to be returned for # special attributes that are not directly property attributes or # computed attributes. @customData = {} reset end # We probably need the start and end dates as TjTime and Scoreboard index. # We store both, but we need to assure they are always in sync. def start=(date) if date.is_a?(TjTime) @start = date else raise "Unsupported type #{date.class}" end @startIdx = @project.dateToIdx(@start) end def startIdx=(idx) if idx.is_a?(Integer) @startIdx = idx @start = @project.idxToDate(idx) else raise "Unsupported type #{idx.class}" end end def end=(date) if date.is_a?(TjTime) @end = date else raise "Unsupported type #{date.class}" end @endIdx = @project.dateToIdx(@end) end def endIdx=(idx) if idx.is_a?(Integer) @endIdx = idx @end = @project.idxToDate(idx) else raise "Unsupported type #{idx.class}" end end # Set a custom data entry. _name_ is the name of the pseudo attribute. # _data_ must be a Hash that contains the value for :numberical, :string, # :sortable or :rti results. def setCustomData(name, data) @customData[name] = data end # This method tries to resolve the query and return a result. In case it # finds an attribute that matches the query, it returns true; false # otherwise. The actual result data is stored in the Query object. It can # then be retrieved by the caller with the methods to_s(), to_num(), # to_sort() and result(). def process reset begin # Resolve property reference from property ID. if @propertyId && (@property.nil? || @propertyId[0] == '!') @property = resolvePropertyId(@propertyType, @propertyId) unless @property @errorMessage = "Unknown property '#{@propertyId}' queried" return @ok = false end end unless @property # No property was provided. We are looking for a project attribute. supportedAttrs = %w( copyright currency end journal name now projectid start version ) unless supportedAttrs.include?(@attributeId) @errorMessage = "Unsupported project attribute '#{@attributeId}'" return @ok = false end if @project.respond_to?(attributeId) @project.send(attributeId, self) else attr = @project[@attributeId] end if attr.is_a?(TjTime) @sortable = @numerical = attr @string = attr.to_s(@timeFormat) else @sortable = @string = attr end return @ok = true end # Same for the scope property. if !@scopeProperty.nil? && !@scopePropertyId.nil? @scopeProperty = resolvePropertyId(@scopePropertyType, @scopePropertyId) unless @scopeProperty @errorMessage = "Unknown scope property #{@scopePropertyId} queried" return @ok = false end end # Make sure the have a reference to the project. @project = @property.project unless @project if @scenario && !@scenarioIdx @scenarioIdx = @project.scenarioIdx(@scenario) unless @scenarioIdx raise "Query cannot resolve scenario '#{@scenario}'" end end queryMethodName = 'query_' + @attributeId # First we check for non-scenario-specific query functions. if (data = @customData[@attributeId]) @sortable = data[:sortable] @numerical = data[:numerical] @string = data[:string] @rti = data[:rti] elsif @property.respond_to?(queryMethodName) @property.send(queryMethodName, self) elsif @scenarioIdx && @property.data && @property.data[@scenarioIdx].respond_to?(queryMethodName) # Then we check for scenario-specific ones via the @data member. @property.send(queryMethodName, @scenarioIdx, self) else # The result is a BaseAttribute begin # The user may also provide a scenario index for # non-scenario-specific values. We need to check if the attribute # is really scenario specific or not because # PropertyTreeNode::getAttribute can only handle an index for # scenario-specific attributs. aType = @property.attributeDefinition(@attributeId) raise ArgumentError unless aType scIdx = aType.scenarioSpecific ? @scenarioIdx : nil @attr = @property.getAttribute(@attributeId, scIdx) if @attr.nil? && @attr.is_a?(DateAttribute) @errorMessage = "Attribute '#{@attributeId}' of property " + "'#{@property.fullId}' has undefined value." return @ok = false end rescue ArgumentError @errorMessage = "Unknown attribute '#{@attributeId}' queried" return @ok = false end end rescue TjException @errorMessage = $!.message return @ok = false end @ok = true end # Converts the String items in _listItems_ into a RichTextIntermediate # objects and assigns it as result of the query. def assignList(listItems) list = '' listItems.each do |item| case @listType when nil, :comma list += ', ' unless list.empty? list += item when :bullets list += "* #{item}\n" when :numbered list += "# #{item}\n" end end @sortable = @string = list rText = RichText.new(list) @rti = rText.generateIntermediateFormat end # Return the result of the Query as String. The result may be nil. def to_s @attr ? @attr.to_s(self) : (@rti ? @rti.to_s : (@string || '')) end # Return the result of the Query as Integer or Float. The result may be # nil. def to_num @attr ? @attr.to_num : @numerical end # Return the result in the best suited type and format for sorting. The # result may be nil. def to_sort @attr ? @attr.to_sort : @sortable end # Return the result as RichTextIntermediate object. The result may be nil. def to_rti return @attr.value if @attr.is_a?(RichTextAttribute) @attr ? @attr.to_rti(self) : @rti end # Return the result in the orginal form. It may be nil. def result if @attr if @attr.value && @attr.is_a?(ReferenceAttribute) @attr.value[0] else @attr.value end elsif @numerical @numerical elsif @rti @rti else @string end end # Convert a duration to the format specified by @loadUnit. _value_ is the # duration effort in days. The return value is the converted value with # optional unit as a String. def scaleDuration(value) scaleValue(value, [ 24 * 60, 24, 1, 1.0 / 7, 1.0 / 30.42, 1.0 / 91.25, 1.0 / 365 ]) end # Convert a load or effort value to the format specified by @loadUnit. # _work_ is the effort in man days. The return value is the converted value # with optional unit as a String. def scaleLoad(value) scaleValue(value, [ @project.dailyWorkingHours * 60, @project.dailyWorkingHours, 1.0, 1.0 / @project.weeklyWorkingDays, 1.0 / @project.monthlyWorkingDays, 1.0 / (@project.yearlyWorkingDays / 4), 1.0 / @project.yearlyWorkingDays ]) end private def resolvePropertyId(pType, pId) unless @project raise "Need Project reference to process the query" end if pId[0] == '!' # This is the case where the property ID is just a sequence of # exclamation marks. Each one moves the scope 1 level up from the # current level. pId.each_utf8_char do |c| if c == '!' @property = @property.parent end break unless @property end @property else case pType when :Account @project.account(pId) when :Task @project.task(pId) when:Resource @project.resource(pId) else raise "Unknown property type #{pType}" end end end # This function converts number to strings that may include a unit. The # unit is determined by @loadUnit. In the automatic modes, the shortest # possible result is shown and the unit is always appended. _value_ is the # value to convert. _factors_ determines the conversion factors for the # different units. def scaleValue(value, factors) if @loadUnit == :shortauto || @loadUnit == :longauto # We try all possible units and store the resulting strings here. options = [] # For each option we also save the delta between the String value and # the original value. delta = [] # For each of the units we can define a maximum value that the value # should not exceed. nil means no limit. Never use quarters since it's # pretty uncommon to use. max = [ 60, 48, nil, 8, 24, 0, nil ] stdFormat = RealFormat.new([ '-', '', '', '.', @numberFormat.fractionDigits ]) i = 0 fSep = @numberFormat.fractionSeparator factors.each do |factor| scaledValue = value * factor str = @numberFormat.format(scaledValue) stdStr = stdFormat.format(scaledValue) delta[i] = (scaledValue - stdStr.to_f).abs # We ignore results that are 0 or exceed the maximum. To ensure that # we have at least one result the unscaled value is always taken. if (factor != 1.0 && /^[0.]*$/ =~ stdStr) || (max[i] && scaledValue > max[i]) options << nil else options << str end i += 1 end # Find the value that is the closest to the original value. This will be # the default if all values have the same length. shortest = 2 delta.length.times do |j| shortest = j if options[j] && delta[j] < delta[shortest] end # Find the shortest option. 6.times do |j| shortest = j if options[j] && options[j][0, 2] != '0' + fSep && options[j].length < options[shortest].length end str = options[shortest] if @loadUnit == :longauto # For the long units we handle singular and plural properly. For # English we just need to append an 's', but this code will work for # other languages as well. units = [] if str == "1" units = %w( minute hour day week month quarter year ) else units = %w( minutes hours days weeks months quarters years ) end str += ' ' + units[shortest] else str += %w( min h d w m q y )[shortest] end else # For fixed units we just need to do the conversion. No unit is # included. units = [ :minutes, :hours, :days, :weeks, :months, :quarters, :years ] str = @numberFormat.format(value * factors[units.index(@loadUnit)]) end str end private # Queries object can be reused. Calling this function will clear the query # result data. def reset @attr = @numerical = @sortable = @string = @rti = nil @ok = true @errorMessage = nil end end end TaskJuggler-3.8.1/lib/taskjuggler/RealFormat.rb000066400000000000000000000063531473026623400214340ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = RealFormat.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # class TaskJuggler # This class provides the functionality to format a Float according to certain # rules. These rules determine how negative values are represented, how the # fractional part is shown and how to structure the mantissa. The result is # always a String. # # The class uses the following parameters to control the formating. # signPrefix: Prefix used for negative numbers. (String) # signSuffix: Suffix used for negative numbers. (String) # thousandsSeparator: Separator used after 3 integer digits. (String) # fractionSeparator: Separator used between the inter part and the # fractional part. (String) # fractionDigits: Number of fractional digits to show. (Integer) class RealFormat attr_reader :signPrefix, :signSuffix, :thousandsSeparator, :fractionSeparator, :fractionDigits # Create a new RealFormat object and define the formating rules. def initialize(args) iVars = %w( @signPrefix @signSuffix @thousandsSeparator @fractionSeparator @fractionDigits ) if args.is_a?(RealFormat) # The argument is another RealFormat object. iVars.each do |iVar| instance_variable_set(iVar, args.instance_variable_get(iVar)) end elsif args.length == 5 # The argument is a list of values. args.length.times do |i| instance_variable_set(iVars[i], args[i]) end else raise RuntimeError, "Bad number of parameters #{args.length}" end end # Converts the Float _number_ into a String representation according to the # formating rules. def format(number) # Check for negative number. Continue with the absolute part. if number < 0 negate = true number = -number else negate = false end # Determine the integer part. intNumber = (number * (10 ** @fractionDigits)).round.to_i.to_s if intNumber.length <= @fractionDigits intNumber = '0' * (@fractionDigits - intNumber.length + 1) + intNumber end intPart = intNumber[0..-(@fractionDigits + 1)] # Determinate the fractional part fracPart = @fractionDigits > 0 ? @fractionSeparator + intNumber[-(@fractionDigits)..-1] : '' if @thousandsSeparator.empty? out = intPart else out = '' 1.upto(intPart.length) do |i| out = intPart[-i, 1] + out out = @thousandsSeparator + out if i % 3 == 0 && i < intPart.length end end out += fracPart # Now compose the result. out = @signPrefix + out + @signSuffix if negate out end def to_s [ @signPrefix, @signSuffix, @thousandsSeparator, @fractionSeparator, @fractionDigits ].collect { |s| "\"#{s}\"" }.join(' ') end end end TaskJuggler-3.8.1/lib/taskjuggler/Resource.rb000066400000000000000000000064341473026623400211670ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = Resource.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2019 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/PropertyTreeNode' require 'taskjuggler/ResourceScenario' class TaskJuggler class Resource < PropertyTreeNode def initialize(project, id, name, parent) super(project.resources, id, name, parent) project.addResource(self) @data = Array.new(@project.scenarioCount, nil) @project.scenarioCount.times do |i| ResourceScenario.new(self, i, @scenarioAttributes[i]) end end # Just a shortcut to avoid the slower calls via method_missing. def book(scenarioIdx, sbIdx, task) @data[scenarioIdx].book(sbIdx, task) end # Many Resource functions are scenario specific. These functions are # provided by the class ResourceScenario. In case we can't find a # function called for the Resource class we try to find it in # ResourceScenario. def method_missing(func, scenarioIdx = 0, *args, &block) @data[scenarioIdx].method(func).call(*args, &block) end def query_dashboard(query) dashboard(query) end private # Create a dashboard-like list of all task that have a current alert # status. def dashboard(query) scenarioIdx = @project['trackingScenarioIdx'] taskList = [] unless scenarioIdx rText = "No 'trackingscenario' defined." else @project.tasks.each do |task| if task['responsible', scenarioIdx].include?(self) && !@project['journal'].currentEntries(query.end, task, 0, query.start, query.hideJournalEntry).empty? taskList << task end end end if taskList.empty? rText = "We have no current status for any task that #{name} " + "is responsible for." else # The components of the message are either UTF-8 text or RichText. For # the RichText components, we use the originally provided markup since # we compose the result as RichText markup first. rText = '' taskList.each do |task| rText += "=== [" + "#{task.query_alert(query).richText.inputText}" + "] Task: #{task.name} " + "(#{task.fullId}) ===\n\n" rText += task.query_journalmessages(query).richText.inputText + "\n\n" end end # Now convert the RichText markup String into RichTextIntermediate # format. unless (rti = RichText.new(rText, RTFHandlers.create(@project)). generateIntermediateFormat) warning('res_dashboard', 'Syntax error in dashboard text') return nil end # No section numbers, please! rti.sectionNumbers = false # We use a special class to allow CSS formating. rti.cssClass = 'tj_journal' query.rti = rti end end end TaskJuggler-3.8.1/lib/taskjuggler/ResourceScenario.rb000066400000000000000000001070311473026623400226460ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = ResourceScenario.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2020 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/ScenarioData' class TaskJuggler class ResourceScenario < ScenarioData def initialize(resource, scenarioIdx, attributes) super # Scoreboard may be nil, a Task, or a bit vector encoded as an Integer # nil: Value has not been determined yet. # Task: A reference to a Task object # Bit 0: Reserved # Bit 1: 0: Work time (as defined by working hours) # 1: No work time (as defined by working hours) # Bit 2 - 5: See Leave class for acutal values. # Bit 6 - 7: Reserved # Bit 8: 0: No global override # 1: Override global setting # The scoreboard is only created when needed to save memory for projects # which read-in the coporate employee database but only need a small # subset. @scoreboard = nil # The index of the earliest booked time slot. @firstBookedSlot = nil # Same but for each assigned resource. @firstBookedSlots = {} # The index of the last booked time Slot. @lastBookedSlot = nil # Same but for each assigned resource. @lastBookedSlots = {} # First available slot of the resource. @minslot = nil # Last available slot of the resource. @maxslot = nil # Attributed are only really created when they are accessed the first # time. So make sure some needed attributes really exist so we don't # have to check for existance each time we access them. %w( alloctdeffort chargeset criticalness directreports duties efficiency effort limits managers rate reports shifts leaves leaveallowances workinghours ).each do |attr| @property[attr, @scenarioIdx] end @dCache = DataCache.instance end # This method must be called at the beginning of each scheduling run. It # initializes variables used during the scheduling process. def prepareScheduling @effort = 0 initScoreboard if @property.leaf? end # The criticalness of a resource is a measure for the probabilty that all # allocations can be fullfilled. The smaller the value, the more likely # will the tasks get the resource. A value above 1.0 means that # statistically some tasks will not get their resources. A value between # 0 and 1 implies no guarantee, though. def calcCriticalness if @scoreboard.nil? # Resources that are not allocated are not critical at all. @criticalness = 0.0 else freeSlots = 0 @scoreboard.each do |slot| freeSlots += 1 if slot.nil? end @criticalness = freeSlots == 0 ? 1.0 : @alloctdeffort / freeSlots end end def setDirectReports @managers.map! do |managerId| # First we need to map 'managerId' to an existing Resource. if (manager = @project.resource(managerId)).nil? error('resource_id_expected', "#{managerId} is not a defined " + 'resource.', @sourceFileInfo) end unless manager.leaf? error('manager_is_group', "Resource #{@property.fullId} has group #{manager.fullId} " + "assigned as manager. Managers must be leaf resources.") end if manager == @property error('manager_is_self', "Resource #{@property.fullId} cannot manage itself.") end # Only leaf resources have reporting lines. if @property.leaf? # The 'directreports' attribute is the reverse link for the 'managers' # attribute. In contrast to the 'managers' attribute, the # 'directreports' list has no duplicate entries. unless manager['directreports', @scenarioIdx].include?(@property) manager['directreports', @scenarioIdx] << @property end end manager end @managers.uniq! end def setReports return unless @directreports.empty? @managers.each do |r| r.setReports_i(@scenarioIdx, [ @property ]) end end def preScheduleCheck end # This method does some housekeeping work after the scheduling is # completed. It's meant to be called for top-level resources and then # recursively descends into all child resources. def finishScheduling # Recursively descend into all child resources. @property.children.each do |resource| resource.finishScheduling(@scenarioIdx) end # Add the parent tasks of each task to the duties list. @duties.each do |task| task.ancestors(true).each do |pTask| @duties << pTask unless @duties.include?(pTask) end end # Add the assigned task to the parent(s) resource duties list. @property.parents.each do |pResource| @duties.each do |task| unless pResource['duties', @scenarioIdx].include?(task) pResource['duties', @scenarioIdx] << task end end end end # Returns true if the resource is available at the time specified by # _sbIdx_. def available?(sbIdx) return false unless @scoreboard[sbIdx].nil? limits = @limits return false if limits && !limits.ok?(sbIdx) true end # Return true if the resource is booked for a tasks at the time specified by # _sbIdx_. def booked?(sbIdx) @scoreboard[sbIdx].is_a?(Task) end # Return the Task that this resource is booked for at the time specified # by _sbIdx_. If not booked to a task, nil is returned. def bookedTask(sbIdx) return nil unless (sb = @scoreboard[sbIdx]).is_a?(Task) sb end # Book the slot indicated by the scoreboard index +sbIdx+ for Task +task+. # If +force+ is true, overwrite the existing booking for this slot. The # method returns true if the slot was available. def book(sbIdx, task, force = false) return false if !force && !available?(sbIdx) # Make sure the task is in the list of duties. unless @duties.include?(task) @duties << task end #puts "Booking resource #{@property.fullId} at " + # "#{@scoreboard.idxToDate(sbIdx)}/#{sbIdx} for task #{task.fullId}\n" @scoreboard[sbIdx] = task # Track the total allocated slots for this resource. @effort += @efficiency @limits.inc(sbIdx) if @limits task.incLimits(@scenarioIdx, sbIdx, @property) # Scoreboard iterations are fairly expensive but they are very frequent # operations in later processing. To limit the interations to the # relevant intervals, we store the interval for all bookings and for # each individual task. if @firstBookedSlot.nil? || @firstBookedSlot > sbIdx @firstBookedSlot = @firstBookedSlots[task] = sbIdx elsif @firstBookedSlots[task].nil? || @firstBookedSlots[task] > sbIdx @firstBookedSlots[task] = sbIdx end if @lastBookedSlot.nil? || @lastBookedSlot < sbIdx @lastBookedSlot = @lastBookedSlots[task] = sbIdx elsif @lastBookedSlots[task].nil? || @lastBookedSlots[task] < sbIdx @lastBookedSlots[task] = sbIdx end true end def bookBooking(sbIdx, booking) initScoreboard if @scoreboard.nil? unless @scoreboard[sbIdx].nil? if booked?(sbIdx) error('booking_conflict', "Resource #{@property.fullId} has multiple conflicting " + "bookings for #{@scoreboard.idxToDate(sbIdx)}. The " + "conflicting tasks are #{@scoreboard[sbIdx].fullId} and " + "#{booking.task.fullId}.", booking.sourceFileInfo) end val = @scoreboard[sbIdx] if ((val & 2) != 0 && booking.overtime < 1) # The booking is blocked due to the overtime attribute. Now let's # see if the user wants to be warned about it. if booking.sloppy < 1 error('booking_no_duty', "Resource #{@property.fullId} has no duty at " + "#{@scoreboard.idxToDate(sbIdx)}.", booking.sourceFileInfo) end return false end if ((val & 0x3C) != 0 && booking.overtime < 2) # The booking is blocked due to the overtime attribute. Now let's # see if the user wants to be warned about it. if booking.sloppy < 2 error('booking_on_vacation', "Resource #{@property.fullId} is on vacation at " + "#{@scoreboard.idxToDate(sbIdx)}.", booking.sourceFileInfo) end return false end end book(sbIdx, booking.task, true) end # @effort only trackes the already allocated effort for leaf resources. It's # too expensive to propagate this to the group resources on every booking. # If a value for a group effort is needed, it's computed here. def bookedEffort if @property.leaf? @effort else effort = 0 @property.kids.each do |r| effort += r.bookedEffort(@scenarioIdx) end effort end end # Compute the annual leave days within the period specified by the # _query_. The result is in days. def query_annualleave(query) query.sortable = query.numerical = val = getLeave(query.startIdx, query.endIdx, :annual) query.string = query.scaleLoad(val) end # Compute a list of the annual leave days within the period specified by # the _query_. The result is a list of dates and how much of that day was # taken. def query_annualleavelist(query) iv_list = collectLeaveIntervals(Interval.new(query.start, query.end), :annual) iv_list.map! do |iv| # The interval is at most one working day. We list the date of that # day. day = iv.start.strftime(@project['timeFormat']) # And how much of the working day was taken. A full working day is # 1.0, a half working day 0.5. days = (iv.end - iv.start) / (60 * 60 * @project['dailyworkinghours']) "#{day} (#{'%.1f' % days})" end query.assignList(iv_list) end def query_annualleavebalance(query) if @property.leaf? leave = getLeave(query.startIdx, query.endIdx, :annual) allowanceSlots = @leaveallowances.balance(:annual, query.start, query.end) allowance = @project.slotsToDays(allowanceSlots) query.sortable = query.numerical = val = allowance - leave query.string = query.scaleLoad(val) end end # Compute the cost generated by this Resource for a given Account during a # given interval. If a Task is provided as scopeProperty only the turnover # directly assiciated with the Task is taken into account. def query_cost(query) if query.costAccount query.sortable = query.numerical = cost = turnover(query.startIdx, query.endIdx, query.costAccount, query.scopeProperty, true) query.string = query.currencyFormat.format(cost) else query.string = 'No \'balance\' defined!' end end # A list of the tasks that the resource has been allocated to work on in # the report time frame. def query_duties(query) list = [] iv = TimeInterval.new(query.start, query.end) @duties.each do |task| if task.hasResourceAllocated?(@scenarioIdx, iv, @property) if query.listItem rti = RichText.new(query.listItem, RTFHandlers.create(@project)). generateIntermediateFormat unless rti error('bad_resource_ts_query', "Syntax error in query statement for task attribute " + "'resources'.") end q = query.dup q.property = task q.scopeProperty = @property rti.setQuery(q) list << "#{rti.to_s}" else list << "#{task.name} (#{task.id})" end end end query.assignList(list) end # The effort allocated to the Resource in the specified interval. In case a # Task is given as scope property only the effort allocated to this Task is # taken into account. def query_effort(query) query.sortable = query.numerical = effort = getEffectiveWork(query.startIdx, query.endIdx, query.scopeProperty) query.string = query.scaleLoad(effort) end # The completed (as of 'now') effort allocated for the resource in the # specified interval. In case a Task is given as scope property only # the effort allocated for this Task is taken into account. def query_effortdone(query) # For this query, we always override the query period. query.sortable = query.numerical = effort = getEffectiveWork(@project.dateToIdx(@project['start'], false), @project.dateToIdx(@project['now']), query.scopeProperty) query.string = query.scaleLoad(effort) end # The remaining (as of 'now') effort allocated for the resource in the # specified interval. In case a Task is given as scope property only # the effort allocated for this Task is taken into account. def query_effortleft(query) # For this query, we always override the query period. query.sortable = query.numerical = effort = getEffectiveWork(@project.dateToIdx(@project['now']), @project.dateToIdx(@project['end'], false), query.scopeProperty) query.string = query.scaleLoad(effort) end # The unallocated work time of the Resource during the specified interval. def query_freetime(query) query.sortable = query.numerical = time = getEffectiveFreeTime(query.startIdx, query.endIdx) / (60 * 60 * 24) query.string = query.scaleDuration(time) end # The unallocated effort of the Resource during the specified interval. def query_freework(query) query.sortable = query.numerical = work = getEffectiveFreeWork(query.startIdx, query.endIdx) query.string = query.scaleLoad(work) end # The the Full-time equivalent for the resource or group. def query_fte(query) fte = 0.0 if @property.container? # Accumulate the FTEs of all sub-resources. @property.kids.each do |resource| resource.query_fte(@scenarioIdx, query) fte += query.to_num end else # TODO: Getting the globalWorkSlots is relatively expensive. We # probably don't need to compute this for every resource. globalWorkSlots = @project.getWorkSlots(query.startIdx, query.endIdx) workSlots = getWorkSlots(query.startIdx, query.endIdx) if globalWorkSlots > 0 fte = (workSlots.to_f / globalWorkSlots) * @efficiency end end query.sortable = query.numerical = fte query.string = query.numberFormat.format(fte) end # The headcount of the resource or group. def query_headcount(query) headcount = 0 if @property.container? @property.kids.each do |resource| resource.query_headcount(@scenarioIdx, query) headcount += query.to_num end else if employed?(@project.dateToIdx(query.start)) # We only count headcount that is employed at the start date of the # query interval. headcount += @efficiency.round end end query.sortable = query.numerical = headcount query.string = query.numberFormat.format(headcount) end # Get the rate of the resource. def query_rate(query) query.sortable = query.numerical = r = rate query.string = query.currencyFormat.format(r) end # Compute the revenue generated by this Resource for a given Account during # a given interval. If a Task is provided as scopeProperty only the # revenue directly associated to this Task is taken into account. def query_revenue(query) if query.revenueAccount query.sortable = query.numerical = revenue = turnover(query.startIdx, query.endIdx, query.revenueAccount, query.scopeProperty) query.string = query.currencyFormat.format(revenue) else query.string = 'No \'balance\' defined!' end end # Compute the sick leave days within the period specified by the # _query_. The result is in days. def query_sickleave(query) query.sortable = query.numerical = val = getLeave(query.startIdx, query.endIdx, :sick) query.string = query.scaleLoad(val) end # Compute the special leave days within the period specified by the # _query_. The result is in days. def query_specialleave(query) query.sortable = query.numerical = val = getLeave(query.startIdx, query.endIdx, :special) query.string = query.scaleLoad(val) end # The work time of the Resource that was blocked by leaves during the # specified TimeInterval. The result is in working days (effort). def query_timeoffdays(query) query.sortable = query.numerical = time = getTimeOffDays(query.startIdx, query.endIdx) query.string = query.scaleLoad(time) end # Compute the unpaid leave days within the period specified by the # _query_. The result is in days. def query_unpaidleave(query) query.sortable = query.numerical = val = getLeave(query.startIdx, query.endIdx, :unpaid) query.string = query.scaleLoad(val) end # A generic tree iterator that recursively accumulates the result of the # block for each leaf object. def treeSum(startIdx, endIdx, *args, &block) cacheTag = "#{self.class}.#{caller[0][/`.*'/][1..-2]}" treeSumR(cacheTag, startIdx, endIdx, *args, &block) end # Recursing method for treeSum. def treeSumR(cacheTag, startIdx, endIdx, *args, &block) # Check if the value to be computed is already in the data cache. If so, # return it. Otherwise we have to compute it first and then store it in # the cache. We use the signature of the method that called treeSum() # and its arguments together with 'self' as index to the cache. @dCache.cached(self, cacheTag, startIdx, endIdx, *args) do if @property.container? sum = 0.0 # Iterate over all the kids and accumulate the result of the # recursively called method. @property.kids.each do |resource| sum += resource.treeSumR(@scenarioIdx, cacheTag, startIdx, endIdx, *args, &block) end sum else instance_eval(&block) end end end # Returns the number of leave days for the period described by _startIdx_ # and _endIdx_ for the given _type_ of leave. def getLeave(startIdx, endIdx, type) treeSum(startIdx, endIdx, type) do @project.convertToDailyLoad(@project['scheduleGranularity'] * getLeaveSlots(startIdx, endIdx, type)) end end # Returns the work of the resource (and its children) weighted by their # efficiency. If _task_ is provided, only the work for this task and all # its sub tasks are being counted. def getEffectiveWork(startIdx, endIdx, task = nil) # Make sure we have the real Task and not a proxy. task = task.ptn if task # There can't be any effective work if the start is after the end or the # todo list doesn't contain the specified task. return 0.0 if startIdx >= endIdx || (task && !@duties.include?(task)) # Temporary workaround until @duties is fixed again. # The unique key we use to address the result in the cache. @dCache.cached(self, :ResourceScenarioGetEffectiveWork, startIdx, endIdx, task) do work = 0.0 if @property.container? @property.kids.each do |resource| work += resource.getEffectiveWork(@scenarioIdx, startIdx, endIdx, task) end else unless @scoreboard.nil? work = @project.convertToDailyLoad( getAllocatedSlots(startIdx, endIdx, task) * @project['scheduleGranularity']) * @efficiency end end work end end # Returns the allocated accumulated time of this resource and its children. def getAllocatedTime(startIdx, endIdx, task = nil) treeSum(startIdx, endIdx, task) do return 0 if @scoreboard.nil? @project.convertToDailyLoad(@project['scheduleGranularity'] * getAllocatedSlots(startIdx, endIdx, task)) end end # Return the unallocated work time (in seconds) of the resource and its # children. def getEffectiveFreeTime(startIdx, endIdx) treeSum(startIdx, endIdx) do getFreeSlots(startIdx, endIdx) * @project['scheduleGranularity'] end end # Return the unallocated work of the resource and its children weighted by # their efficiency. def getEffectiveFreeWork(startIdx, endIdx) treeSum(startIdx, endIdx) do @project.convertToDailyLoad(getFreeSlots(startIdx, endIdx) * @project['scheduleGranularity']) * @efficiency end end # Return the number of working days that are blocked by leaves. def getTimeOffDays(startIdx, endIdx) treeSum(startIdx, endIdx) do @project.convertToDailyLoad(getTimeOffSlots(startIdx, endIdx) * @project['scheduleGranularity']) * @efficiency end end def turnover(startIdx, endIdx, account, task = nil, includeKids = false) amount = 0.0 if @property.container? && includeKids @property.kids.each do |child| amount += child.turnover(@scenarioIdx, startIdx, endIdx, account, task) end else if task # If we have a known task, we only include the amount that is # specific to this resource, this task and the chargeset of the # task. amount += task.turnover(@scenarioIdx, startIdx, endIdx, account, @property) elsif !@chargeset.empty? # If no tasks was provided, we include the amount of this resource, # weighted by the chargeset of this resource. totalResourceCost = cost(startIdx, endIdx) @chargeset.each do |set| set.each do |accnt, share| if share > 0.0 && (accnt == account || accnt.isChildOf?(account)) amount += totalResourceCost * share end end end end end amount end # Returns the cost for using this resource during the specified # TimeInterval _period_. If a Task _task_ is provided, only the work on # this particular task is considered. def cost(startIdx, endIdx, task = nil) getAllocatedTime(startIdx, endIdx, task) * @rate end # Returns true if the resource or any of its children is allocated during # the period specified with the TimeInterval _iv_. If task is not nil # only allocations to this tasks are respected. def allocated?(iv, task = nil) return false if task && !@duties.include?(task) startIdx = @project.dateToIdx(iv.start) endIdx = @project.dateToIdx(iv.end) startIdx, endIdx = fitIndicies(startIdx, endIdx, task) return false if startIdx >= endIdx return allocatedSub(startIdx, endIdx, task) end # Iterate over the scoreboard and turn its content into a set of Bookings. # _iv_ can be a TimeInterval to limit the bookings within the provided # period. if _hashByTask_ is true, the result is a Hash of Arrays with # bookings hashed by Task. Otherwise it's just a plain Array with # Bookings. def getBookings(iv = nil, hashByTask = true) bookings = hashByTask ? {} : [] return bookings if @property.container? || @scoreboard.nil? || @firstBookedSlot.nil? || @lastBookedSlot.nil? # To speedup the collection we start with the first booked slot and end # with the last booked slot. startIdx = @firstBookedSlot endIdx = @lastBookedSlot + 1 # If the user provided a TimeInterval, we only return bookings within # this TimeInterval. if iv ivStartIdx = @project.dateToIdx(iv.start) ivEndIdx = @project.dateToIdx(iv.end) startIdx = ivStartIdx if ivStartIdx > startIdx endIdx = ivEndIdx if ivEndIdx < endIdx end lastTask = nil bookingStart = nil startIdx.upto(endIdx) do |idx| task = @scoreboard[idx] # Now we watch for task changes. if task != lastTask || (task.is_a?(Task) && (lastTask.nil? || idx == endIdx)) if lastTask # We've found the end of a task booking series. # If we don't have a Booking for the task yet, we create one. if hashByTask if bookings[lastTask].nil? bookings[lastTask] = Booking.new(@property, lastTask, []) end # Append the new interval to the Booking. bookings[lastTask].intervals << TimeInterval.new(@scoreboard.idxToDate(bookingStart), @scoreboard.idxToDate(idx)) else if bookings.empty? || bookings.last.task != lastTask bookings << Booking.new(@property, lastTask, []) end # Append the new interval to the Booking. bookings.last.intervals << TimeInterval.new(@scoreboard.idxToDate(bookingStart), @scoreboard.idxToDate(idx)) end end # Get ready for the next task booking interval if task.is_a?(Task) lastTask = task bookingStart = idx else lastTask = bookingStart = nil end end end bookings end # Return a list of scoreboard intervals that are at least _minDuration_ long # and contain only off-duty and leave slots. The result is an Array of # [ start, end ] TjTime values. def collectTimeOffIntervals(iv, minDuration) # Time-off intervals are only useful for leaf resources. Group resources # would just default to the global working hours. return [] unless @property.leaf? initScoreboard if @scoreboard.nil? @scoreboard.collectIntervals(iv, minDuration) do |val| val.is_a?(Integer) && (val & 0x3E) != 0 end end # Return a list of scoreboard intervals that are at least _minDuration_ long # and only contain leave slots of the given type. The result is an Array of # [ start, end ] TjTime values. def collectLeaveIntervals(iv, type) # Time-off intervals are only useful for leaf resources. Group resources # would just default to the global working hours. return [] unless @property.leaf? initScoreboard if @scoreboard.nil? @scoreboard.collectIntervals(iv, 60 * 60) do |val| val.is_a?(Integer) && (val & 0x3E) == (Leave::Types[type] << 2) end end # Count the booked slots between the start and end index. If _task_ is not # nil count only those slots that are assigned to this particular task or # any of its sub tasks. def getAllocatedSlots(startIdx, endIdx, task = nil) # If there is no scoreboard, we don't have any allocations. return 0 unless @scoreboard startIdx, endIdx = fitIndicies(startIdx, endIdx, task) return 0 if startIdx >= endIdx bookedSlots = 0 taskList = task ? task.all : [] @scoreboard.each(startIdx, endIdx) do |slot| if slot.is_a?(Task) && (task.nil? || taskList.include?(slot)) bookedSlots += 1 end end bookedSlots end # Count the number of slots betweend the _startIdx_ and _endIdx_ that can # be used for work def getWorkSlots(startIdx, endIdx) countSlots(startIdx, endIdx) do |val| # We count free slots and assigned slots. val.nil? || val.is_a?(Task) end end # Count the number of slots that are work time slots but marked as annual # leave. def getLeaveSlots(startIdx, endIdx, type) countSlots(startIdx, endIdx) do |val| val.is_a?(Integer) && (val & 0x3E) == (Leave::Types[type] << 2) end end # Count the free slots between the start and end index. def getFreeSlots(startIdx, endIdx) countSlots(startIdx, endIdx) do |val| val.nil? end end # Count the regular work time slots between the start and end index that # have been blocked by leaves. def getTimeOffSlots(startIdx, endIdx) countSlots(startIdx, endIdx) do |val| # Bit 1 needs to be unset and the leave bits must not be 0. val.is_a?(Integer) && (val & 0x2) == 0 && (val & 0x3C) != 0 end end # Get the first available slot of the resource. def getMinSlot initScoreboard unless @minslot @minslot end # Get the last available slot of the resource. def getMaxSlot initScoreboard unless @maxslot @maxslot end private def initScoreboard # Create scoreboard and mark all slots as non-working-time. @scoreboard = Scoreboard.new(@project['start'], @project['end'], @project['scheduleGranularity'], 2) # Change all work time slots to nil (available) again. @project.scoreboardSize.times do |i| @scoreboard[i] = nil if onShift?(i) end # Mark all global leave slots as such @project['leaves'].each do |leave| startIdx = @scoreboard.dateToIdx(leave.interval.start) endIdx = @scoreboard.dateToIdx(leave.interval.end) startIdx.upto(endIdx - 1) do |i| sb = @scoreboard[i] # We preseve the work-time bit (#1). @scoreboard[i] = (sb.nil? ? 0 : 2) | (leave.typeIdx << 2) end end # Mark all resource specific leave slots as such @leaves.each do |leave| startIdx = @scoreboard.dateToIdx(leave.interval.start) endIdx = @scoreboard.dateToIdx(leave.interval.end) startIdx.upto(endIdx - 1) do |i| if (sb = @scoreboard[i]) # The slot is already marked as non-working slot. We override the # leave type if the new type is larger than the old one. leaveIdx = (sb & 0x3C) >> 2 if leave.typeIdx > leaveIdx # The work-time bit (#1) is preserved. @scoreboard[i] = (sb & 0x2) | (leave.typeIdx << 2) end else # This marks a working time slot as a leave slot. Since bit 1 is # not set, we still know that this could be a working slot. @scoreboard[i] = leave.typeIdx << 2 end end end unless @shifts.nil? # Mark the leaves from all the shifts the resource is assigned to. @project.scoreboardSize.times do |i| v = @shifts.getSbSlot(i) # Make sure a shift is actually assigned. next unless v if (v & (1 << 8)) != 0 # Check if the leave replacement bit (#8) is set. In that case we # copy the whole interval over to the resource scoreboard # overriding any global leaves. @scoreboard[i] = (v & 0x3E == 0) ? nil : (v & 0x3D) elsif ((sb = @scoreboard[i]).nil? || ((sb & 0x3C) < (v & 0x3C))) && (v & 0x3C) != 0 # In merge mode, we only add the shift leaves with higher type # index or unassigned slots. @scoreboard[i] = v & 0x3E end end end # Set minimum and maximum availability idx = 0 while idx < @scoreboard.size if available?(idx) @minslot = idx break end idx += 1 end idx = @scoreboard.size - 1 while idx >= 0 if available?(idx) @maxslot = idx break end idx -= 1 end end def countSlots(startIdx, endIdx) return 0 if startIdx >= endIdx initScoreboard unless @scoreboard slots = 0 startIdx.upto(endIdx - 1) do |idx| slots += 1 if yield(@scoreboard[idx]) end slots end # Limit the _startIdx_ and _endIdx_ to the actually assigned interval. # If _task_ is provided, fit it for the bookings of this particular task. def fitIndicies(startIdx, endIdx, task = nil) if task startIdx = @firstBookedSlots[task] if @firstBookedSlots[task] && startIdx < @firstBookedSlots[task] endIdx = @lastBookedSlots[task] + 1 if @lastBookedSlots[task] && endIdx > @lastBookedSlots[task] + 1 else startIdx = @firstBookedSlot if @firstBookedSlot && startIdx < @firstBookedSlot endIdx = @lastBookedSlot + 1 if @lastBookedSlot && endIdx > @lastBookedSlot + 1 end [ startIdx, endIdx ] end def setReports_i(reports) if reports.include?(@property) # A manager must never show up in the list of his/her own reports. error('manager_loop', "Management loop detected. #{@property.fullId} has self " + "in list of reports") end @reports += reports # Resources can end up multiple times in the list if they have multiple # reporting chains. We only need them once in the list. @reports.uniq! @managers.each do |r| r.setReports_i(@scenarioIdx, @reports) end end def onShift?(sbIdx) if @shifts && @shifts.assigned?(sbIdx) return @shifts.onShift?(sbIdx) else @workinghours.onShift?(sbIdx) end end def employed?(sbIdx) initScoreboard unless @scoreboard val = @scoreboard[sbIdx] return true unless val.is_a?(Integer) leave_type = (val >> 2) & 0xF leave_type < Leave::Types[:unemployed] end # Returns true if the resource or any of its children is allocated during # the period specified with _startIdx_ and _endIdx_. If task is not nil # only allocations to this tasks are respected. def allocatedSub(startIdx, endIdx, task) if @property.container? @property.kids.each do |resource| return true if resource.allocatedSub(@scenarioIdx, startIdx, endIdx, task) end else return false unless @scoreboard && @duties.include?(task) startIdx, endIdx = fitIndicies(startIdx, endIdx, task) return false if startIdx >= endIdx startIdx.upto(endIdx - 1) do |idx| return true if @scoreboard[idx] == task end end false end # Return the daily cost of a resource or resource group. def rate if @property.container? dailyRate = 0.0 @property.kids.each do |resource| dailyRate += resource.rate(@scenarioIdx) end dailyRate else @rate end end end end TaskJuggler-3.8.1/lib/taskjuggler/RichText.rb000066400000000000000000000176531473026623400211370ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = RichText.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/RichText/Element' require 'taskjuggler/RichText/Parser' require 'taskjuggler/MessageHandler' class TaskJuggler # RichText is a MediaWiki markup parser and HTML generator implemented in # pure Ruby. It can also generate plain text versions of the original markup # text. It is based on the TextParser class to implement the # RichTextParser. The scanner is implemented in the RichTextScanner class. # The read-in text is converted into a tree of RichTextElement objects. # These can then be turned into HTML element trees modelled by XMLElement or # plain text. # # This class supports the following mark-ups: # # The following markups are block commands and must start at the beginning of # the line. # # == Headline 1 == # === Headline 2 === # ==== Headline 3 ==== # # ---- creates a horizontal line # # * Bullet 1 # ** Bullet 2 # *** Bullet 3 # # # Enumeration Level 1 # ## Enumeration Level 2 # ### Enumeration Level 3 # # Preformatted text start with # a single space at the start of # each line. # # # The following are in-line mark-ups and can occur within any text block # # This is an ''italic'' word. # This is a '''bold''' word. # This is a ''''monospaced'''' word. This is not part of the original # MediaWiki markup, but we needed monospaced as well. # This is a '''''italic and bold''''' word. # # Linebreaks are ignored if not followed by a blank line. # # [http://www.taskjuggler.org] A web link # [http://www.taskjuggler.org The TaskJuggler Web Site] another link # # [[item]] site internal internal reference (in HTML .html gets appended # automatically) # [[item An item]] another internal reference # [[function:path arg1 arg2 ...]] # # ... Disable markup interpretation for the enclosed # portion of text. # class RichText attr_reader :inputText # The Parser uses complex to setup data structures that are identical for # all RichText instances. So, we'll share them across the instances. @@parser = nil # Create a rich text object by passing a String with markup elements to it. # _text_ must be plain text with MediaWiki compatible markup elements. In # case an error occurs, an exception of type TjException will be raised. # _functionHandlers_ is a Hash that maps RichTextFunctionHandler objects # by their function name. def initialize(text, functionHandlers = []) # Keep a copy of the original text. @inputText = text @functionHandlers = functionHandlers end # Convert the @inputText into an abstract syntax tree that can then be # converted into the various output formats. _sectionCounter_ is an Array # that holds the initial values for the section counters. def generateIntermediateFormat(sectionCounter = [ 0, 0, 0], tokenSet = nil) rti = RichTextIntermediate.new(self) # Copy the function handlers. @functionHandlers.each do |h| rti.registerFunctionHandler(h) end # We'll setup the RichTextParser once and share it across all instances. if @@parser # We already have a RichTextParser that we can reuse. @@parser.reuse(rti, sectionCounter, tokenSet) else # There is no RichTextParser yet, create one. @@parser = RichTextParser.new(rti, sectionCounter, tokenSet) end @@parser.open(@inputText) # Parse the input text and convert it to the intermediate representation. return nil if (tree = @@parser.parse(:richtext)) == false # In case the result is empty, use an empty RichTextElement as result tree = RichTextElement.new(rti, :richtext, nil) unless tree tree.cleanUp rti.tree = tree rti end # Return the RichTextFunctionHandler for the function _name_. _block_ # specifies whether we are looking for a block or inline function. def functionHandler(name, block) @functionHandlers.each do |handler| return handler if handler.function == name && handler.blockFunction == block end nil end private end # The RichTextIntermediate is a container for the intermediate # representation of a RichText object. By calling the to_* members it can be # converted into the respective formats. A RichTextIntermediate object is # generated by RichText::generateIntermediateFormat. class RichTextIntermediate attr_reader :richText, :functionHandlers attr_accessor :blockMode, :sectionNumbers, :lineWidth, :indent, :titleIndent, :parIndent, :listIndent, :preIndent, :linkTarget, :cssClass, :tree def initialize(richText) # A reference to the corresponding RichText object the RTI is derived # from. @richText = richText # The root of the generated intermediate format. This is a # RichTextElement. @tree = nil # The blockMode specifies whether the RichText should be interpreted as # a line of text or a block (default). @blockMode = true # Set this to false to disable automatically generated section numbers. @sectionNumbers = true # Set this to the maximum width used for text output. @lineWidth = 80 # The indentation used for all text output. @indent = 0 # Additional indentation used for titles in text output. @titleIndent = 0 # Additional indentation used for paragraph text output. @parIndent = 0 # Additional indentation used for lists in text output. @listIndent = 1 # Additional indentation used for
 sections in text output.
      @preIndent = 0
      # The target used for hypertext links.
      @linkTarget = nil
      # The CSS class used for some key HTML elements.
      @cssClass = nil
      # These are the RichTextFunctionHandler objects to handle references with
      # a function specification.
      @functionHandlers = {}
    end

    # Use this function to register new RichTextFunctionHandler objects with
    # this class.
    def registerFunctionHandler(functionHandler)
      raise "Bad function handler" unless functionHandler
      @functionHandlers[functionHandler.function] = functionHandler.dup
    end

    # Return the handler for the given _function_ or raise an exception if it
    # does not exist.
    def functionHandler(function)
      @functionHandlers[function]
    end

    # Return true if the RichText has no content.
    def empty?
      @tree.empty?
    end

    # Recursively extract the section headings from the RichTextElement and
    # build the TableOfContents _toc_ with the gathered sections.  _fileName_
    # is the base name (without .html or other suffix) of the file the
    # TOCEntries should point to.
    def tableOfContents(toc, fileName)
      @tree.tableOfContents(toc, fileName)
    end

    # Return an Array with all other snippet names that are referenced by
    # internal references in this RichTextElement.
    def internalReferences
      @tree.internalReferences
    end

    # Convert the intermediate format into a plain text String object.
    def to_s
      str = @tree.to_s
      str.chomp! while str[-1] == ?\n
      str
    end

    # Convert the intermediate format into a XMLElement objects tree.
    def to_html
      html = @tree.to_html
      html.chomp! while html[-1] == ?\n
      html
    end

    # Convert the intermediate format into a tagged syntax String object.
    def to_tagged
      @tree.to_tagged
    end

  end

end

TaskJuggler-3.8.1/lib/taskjuggler/RichText/000077500000000000000000000000001473026623400205765ustar00rootroot00000000000000TaskJuggler-3.8.1/lib/taskjuggler/RichText/Document.rb000066400000000000000000000113221473026623400227000ustar00rootroot00000000000000#!/usr/bin/env ruby -w
# encoding: UTF-8
#
# = Document.rb -- The TaskJuggler III Project Management Software
#
# Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014
#               by Chris Schlaeger 
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of version 2 of the GNU General Public License as
# published by the Free Software Foundation.
#

require 'taskjuggler/RichText/Snip'
require 'taskjuggler/RichText/TableOfContents'
require 'taskjuggler/RichText/FunctionHandler'

class TaskJuggler

  # A RichTextDocument object collect a set of structured text files into a
  # single document. This document may have a consistent table of contents
  # across all files and can be turned into a set of corresponding HTML files.
  # This class is an abstract class. To use it, a derrived class must define
  # the functions generateHTMLCover, generateStyleSheet, generateHTMLHeader
  # and generateHTMLFooter.
  class RichTextDocument

    attr_reader :functionHandlers

    # Create a new empty RichTextDocument object.
    def initialize
      @functionHandlers = []
      @snippets = []
      @dirty = false
      @sectionCounter = [ 0, 0, 0 ]
      @linkTarget = nil
      @toc = nil
      @anchors = []
    end

    # Register a new RichTextFunctionHandler for this document.
    def registerFunctionHandler(handler)
      @functionHandlers << handler
    end

    # Add a new structured text file to the document. _file_ must be the name of
    # a file with RichText compatible syntax elements.
    def addSnip(file)
      @snippets << (snippet = RichTextSnip.new(self, file, @sectionCounter))
      snippet.linkTarget = @linkTarget
      @dirty = true
      snippet
    end

    # Call this method to generate a table of contents for all files that were
    # registered so far. The table of contents is stored internally and will be
    # used when the document is produced in a new format. This function also
    # collects a list of all snip names to @anchors and gathers a list of
    # all references to other snippets in @references. As these two lists will
    # be used by RichTextDocument#checkInternalReferences this function must be
    # called first.
    def tableOfContents
      @toc = TableOfContents.new
      @references = {}
      @anchors = []
      # Collect all file names as potentencial anchors.
      @snippets.each do |snip|
        snip.tableOfContents(@toc, snip.name)
        @anchors << snip.name
        (refs = snip.internalReferences).empty? || @references[snip.name] = refs
      end
      # Then add all section entries as well. We use the HTML style
      # # notation.
      @toc.each do |tocEntry|
        @anchors << tocEntry.file + '#' + tocEntry.tag
      end
    end

    # Make sure that all internal references only point to known snippets.
    def checkInternalReferences
      @references.each do |snip, refs|
        refs.each do |reference|
          unless @anchors.include?(reference)
            # TODO: Probably an Exception is cleaner here.
            puts "Warning: Rich text file #{snip} references unknown " +
                 "object #{reference}"
          end
        end
      end
    end

    # Generate HTML files for all registered text files. The files have the same
    # name as the orginal files with '.html' appended. The files will be
    # generated into the _directory_. _directory_ must be empty or a valid path
    # name that is terminated with a '/'. A table of contense is generated into
    # a file called 'toc.html'.
    def generateHTML(directory = '')
      crossReference

      generateHTMLTableOfContents(directory)

      @snippets.each do |snip|
        snip.generateHTML(directory)
      end
    end

  private

    # Register the previous and next file with each of the text files. This
    # function is used by the output generators to have links to the next and
    # previous file in the sequence embedded into the generated files.
    def crossReference
      return unless @dirty

      prevSnip = nil
      @snippets.each do |snip|
        if prevSnip
          snip.prevSnip = prevSnip
          prevSnip.nextSnip = snip
        end
        prevSnip = snip
      end

      @dirty = false
    end

    # Generate a HTML file with the table of contense for all registered files.
    def generateHTMLTableOfContents(directory)
      html = HTMLDocument.new
      head = html.generateHead('Index')
      html.html << (body = XMLElement.new('body'))

      body << generateHTMLCover <<
        @toc.to_html <<
        XMLElement.new('br', {}, true) <<
        XMLElement.new('hr', {}, true) <<
        XMLElement.new('br', {}, true) <<
        generateHTMLFooter

      html.write(directory + 'toc.html')
    end

  end

end

TaskJuggler-3.8.1/lib/taskjuggler/RichText/Element.rb000066400000000000000000000431351473026623400225220ustar00rootroot00000000000000#!/usr/bin/env ruby -w
# encoding: UTF-8
#
# = Element.rb -- The TaskJuggler III Project Management Software
#
# Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014
#               by Chris Schlaeger 
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of version 2 of the GNU General Public License as
# published by the Free Software Foundation.
#

require 'taskjuggler/UTF8String'
require 'taskjuggler/TjException'
require 'taskjuggler/XMLElement'
require 'taskjuggler/TextFormatter'

class TaskJuggler

  class RichTextImage

    attr_reader :fileName
    attr_accessor :altText, :verticalAlign

    def initialize(fileName)
      @fileName = fileName
      @altText = nil
      @verticalAlign = nil
    end

  end

  # The RichTextElement class models the nodes of the intermediate
  # representation that the RichTextParser generates. Each node can reference an
  # Array of other RichTextElement nodes, building a tree that represents the
  # syntactical structure of the parsed RichText. Each node has a certain
  # category that identifies the content of the node.
  class RichTextElement

    attr_reader :richText, :category, :children
    attr_writer :data
    attr_accessor :appendSpace

    # Create a new RichTextElement node. _rt_ is the RichText object this
    # element belongs to. _category_ is the type of the node. It can be :title,
    # :bold, etc. _arg_ is an overloaded argument. It can either be another node
    # or an Array of RichTextElement nodes.
    def initialize(rt, category, arg = nil)
      @richText = rt
      @category = category
      if arg
        if arg.is_a?(Array)
          @children = arg
        else
          unless arg.is_a?(RichTextElement) || arg.is_a?(String)
            raise "Element must be of type RichTextElement instead of " +
                  "#{arg.class}"
          end
          @children = [ arg ]
        end
      else
        @children = []
      end

      # Certain elements such as titles,references and numbered bullets can be
      # require additional data. This variable is used for this. It can hold an
      # Array of counters or a link label.
      @data = nil
      @appendSpace = false
    end

    # Remove a paragraph node from the richtext node if it is the only node in
    # richtext. The paragraph children will become richtext children.
    def cleanUp
      if @category == :richtext && @children.length == 1 &&
         @children[0].category == :paragraph
         @children = @children[0].children
      end
      self
    end

    # Return true of the node contains an empty RichText tree.
    def empty?
      @category == :richtext && @children.empty?
    end


    # Recursively extract the section headings from the RichTextElement and
    # build the TableOfContents _toc_ with the gathered sections.  _fileName_
    # is the base name (without .html or other suffix) of the file the
    # TOCEntries should point to.
    def tableOfContents(toc, fileName)
      number = nil
      case @category
      when :title1
        number = "#{@data[0]} "
      when :title2
        number = "#{@data[0]}.#{@data[1]} "
      when :title3
        number = "#{@data[0]}.#{@data[1]}.#{@data[2]} "
      when :title4
        number = "#{@data[0]}.#{@data[1]}.#{@data[2]}.#{@data[3]} "
      end
      if number
        # We've found a section heading. The String value of the Element is the
        # title.
        title = children_to_s
        tag = convertToID(title)
        toc.addEntry(TOCEntry.new(number, title, fileName, tag))
      else
        # Recursively extract the TOC from the child objects.
        @children.each do |el|
          el.tableOfContents(toc, fileName) if el.is_a?(RichTextElement)
        end
      end

      toc
    end

    # Return an Array with all other snippet names that are referenced by
    # internal references in this RichTextElement.
    def internalReferences
      references = []
      if @category == :ref && !@data.include?(':')
          references << @data
      else
        @children.each do |el|
          if el.is_a?(RichTextElement)
            references += el.internalReferences
          end
        end
      end

      # We only need each reference once.
      references.uniq
    end

    # Conver the intermediate representation into a plain text again. All
    # elements that can't be represented in plain text easily will be ignored or
    # just their value will be included.
    def to_s
      pre = ''
      post = ''
      case @category
      when :richtext
      when :title1
        return textBlockFormat(@richText.indent + @richText.titleIndent,
                               sTitle(1), children_to_s,
                               @richText.lineWidth) + "\n"
      when :title2
        return textBlockFormat(@richText.indent + @richText.titleIndent,
                               sTitle(2), children_to_s,
                               @richText.lineWidth) + "\n"
      when :title3
        return textBlockFormat(@richText.indent + @richText.titleIndent,
                               sTitle(3), children_to_s,
                               @richText.lineWidth) + "\n"
      when :title4
        return textBlockFormat(@richText.indent + @richText.titleIndent,
                               sTitle(4), children_to_s,
                               @richText.lineWidth) + "\n"
      when :hline
        return "#{' ' * @richText.indent}" +
               "#{'-' * (@richText.lineWidth - @richText.indent)}\n"
      when :paragraph
        return textBlockFormat(@richText.indent + @richText.parIndent,
                               '', children_to_s, @richText.lineWidth) + "\n"
      when :pre
        return TextFormatter.new(@richText.lineWidth,
                                 @richText.indent + @richText.preIndent).
          indent(children_to_s) + "\n"
      when :bulletlist1
      when :bulletitem1
        return textBlockFormat(@richText.indent + @richText.listIndent,
                               '* ', children_to_s,
                               @richText.lineWidth) + "\n"
      when :bulletlist2
      when :bulletitem2
        return textBlockFormat(@richText.indent + @richText.listIndent * 2,
                               '* ', children_to_s,
                               @richText.lineWidth) + "\n"
      when :bulletlist3
      when :bulletitem3
        return textBlockFormat(@richText.indent + @richText.listIndent * 3,
                               '* ', children_to_s,
                               @richText.lineWidth) + "\n"
      when :bulletlist4
      when :bulletitem4
        return textBlockFormat(@richText.indent + @richText.listIndent * 4,
                               '* ', children_to_s,
                               @richText.lineWidth) + "\n"
      when :numberlist1
      when :numberitem1
        return textBlockFormat(@richText.indent + @richText.listIndent,
                               "#{@data[0]}. ", children_to_s,
                               @richText.lineWidth) + "\n"
      when :numberlist2
      when :numberitem2
        return textBlockFormat(@richText.indent + @richText.listIndent,
                               "#{@data[0]}.#{@data[1]} ", children_to_s,
                               @richText.lineWidth) + "\n"
      when :numberlist3
      when :numberitem3
        return textBlockFormat(@richText.indent + @richText.listIndent,
                               "#{@data[0]}.#{@data[1]}.#{@data[2]} ",
                               children_to_s, @richText.lineWidth) + "\n"
      when :numberlist4
      when :numberitem4
        return textBlockFormat(@richText.indent + @richText.listIndent,
                               "#{@data[0]}.#{@data[1]}.#{@data[2]}." +
                               "#{@data[3]} ",
                               children_to_s, @richText.lineWidth) + "\n"
      when :img
        pre = @data.altText if @data.altText
      when :ref
      when :href
      when :blockfunc
      when :inlinefunc
        checkHandler
        pre = @richText.functionHandler(@data[0]).to_s(@data[1])
      when :italic
      when :bold
      when :fontCol
      when :code
      when :text
      when :htmlblob
        return ''
      else
        raise "Unknown RichTextElement category #{@category}"
      end

      pre + children_to_s + post
    end

    # Convert the tree of RichTextElement nodes into an XML like text
    # representation. This is primarily intended for unit testing. The tag names
    # are similar to HTML tags, but this is not meant to be valid HTML.
    def to_tagged
      pre = ''
      post = ''
      case @category
      when :richtext
        pre = '
' post = '
' when :title1 pre = "

#{@data[0]} " post = "

\n\n" when :title2 pre = "

#{@data[0]}.#{@data[1]} " post = "

\n\n" when :title3 pre = "

#{@data[0]}.#{@data[1]}.#{@data[2]} " post = "

\n\n" when :title4 pre = "

#{@data[0]}.#{@data[1]}.#{@data[2]}.#{@data[3]} " post = "

\n\n" when :hline pre = '
' post = "\n" when :paragraph pre = '

' post = "

\n\n" when :pre pre = '
'
        post = "
\n\n" when :bulletlist1 pre = '
    ' post = '
' when :bulletitem1 pre = '
  • * ' post = "
  • \n" when :bulletlist2 pre = '
      ' post = '
    ' when :bulletitem2 pre = '
  • * ' post = "
  • \n" when :bulletlist3 pre = '
      ' post = '
    ' when :bulletitem3 pre = '
  • * ' post = "
  • \n" when :bulletlist4 pre = '
      ' post = '
    ' when :bulletitem4 pre = '
  • * ' post = "
  • \n" when :numberlist1 pre = '
      ' post = '
    ' when :numberitem1 pre = "
  • #{@data[0]} " post = "
  • \n" when :numberlist2 pre = '
      ' post = '
    ' when :numberitem2 pre = "
  • #{@data[0]}.#{@data[1]} " post = "
  • \n" when :numberlist3 pre = '
      ' post = '
    ' when :numberitem3 pre = "
  • #{@data[0]}.#{@data[1]}.#{@data[2]} " post = "
  • \n" when :numberlist4 pre = '
      ' post = '
    ' when :numberitem4 pre = "
  • #{@data[0]}.#{@data[1]}.#{@data[2]}.#{@data[3]} " post = "
  • \n" when :img pre = "" when :ref pre = "" post = '' when :href pre = "" post = '' when :blockfunc pre = " href) when :href a = XMLElement.new('a', 'href' => @data.to_s) a['target'] = @richText.linkTarget if @richText.linkTarget a when :blockfunc noChilds = true checkHandler @richText.functionHandler(@data[0]).to_html(@data[1]) when :inlinefunc noChilds = true checkHandler @richText.functionHandler(@data[0]).to_html(@data[1]) when :italic XMLElement.new('i') when :bold XMLElement.new('b') when :fontCol XMLElement.new('span', 'style' => "color:#{@data}") when :code XMLElement.new('code', attrs) when :htmlblob noChilds = true XMLBlob.new(@children[0]) when :text noChilds = true XMLText.new(@children[0]) else raise "Unknown RichTextElement category #{@category}" end # Some elements never have leaves. return html if noChilds @children.each do |el_i| html << el_i.to_html html << XMLText.new(' ') if el_i.appendSpace end html end # Convert all childern into a single plain text String. def children_to_s text = '' @children.each do |c| text << c.to_s + (c.is_a?(RichTextElement) && c.appendSpace ? ' ' : '') end text end def checkHandler unless @data[0] && @data[0].is_a?(String) raise "Bad RichText function '#{@data[0]}' requested" end if @richText.functionHandler(@data[0]).nil? raise "No handler for #{@data[0]} registered" end end # This function converts a String into a new String that only contains # characters that are acceptable for HTML tag IDs. def convertToID(text) out = '' text.each_utf8_char do |c| out << c if (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') out << '_' if c == ' ' end out.chomp('_') end private def sTitle(level) s = '' if @richText.sectionNumbers 1.upto(level) do |i| s += '.' unless s.empty? s += "#{@data[i - 1]}" end s += ') ' end s end def htmlTitle(level) attrs = { 'id' => convertToID(children_to_s) } attrs['class'] = @richText.cssClass if @richText.cssClass el = XMLElement.new("h#{level}", attrs) if @richText.sectionNumbers s = '' 1.upto(level) do |i| s += '.' unless s.empty? s += "#{@data[i - 1]}" end s += ' ' el << XMLText.new(s) end el end def htmlObject fileTypes = { 'png' => { 'type' => 'image/png' }, 'gif' => { 'type' => 'image/gif' }, 'jpg' => { 'type' => 'image/jpg' }, 'svg' => { 'type' => 'image/svg+xml', 'class' => 'img' }} # Error checking must have been done in the parser! # File types must be in sync with # RichTextSyntaxRules::rule_plainTextWithLinks return nil unless (index = @data.fileName.rindex('.')) extension = @data.fileName[index + 1..-1].downcase return nil unless (attributes = fileTypes[extension]) attributes['data'] = @data.fileName el = XMLElement.new('object', attributes) el['alt'] = @data.altText if @data.altText if @data.verticalAlign el['style'] = "vertical-align:#{@data.verticalAlign}; " end el end def textBlockFormat(indent, label, str, width) labLen = label.length TextFormatter.new(width, indent + labLen, indent).format(label + str) end def textBlockIndent(indent, str, width) TextFormatter.new(width, indent).indent(str) end end end TaskJuggler-3.8.1/lib/taskjuggler/RichText/FunctionExample.rb000066400000000000000000000034071473026623400242300ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = FunctionExample.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/RichText/FunctionHandler' require 'taskjuggler/TjpExample' require 'taskjuggler/XMLElement' class TaskJuggler # This class is a specialized RichTextFunctionHandler that turns references to # TJP example code in the test/TestSuite/Syntax/Correct directory into # embedded example code. It currently only supports HTML. class RichTextFunctionExample < RichTextFunctionHandler def initialize super('example') @blockFunction = true end # Not supported for this function def to_s(args) '' end # Return a XMLElement tree that represents the example file as HTML code. def to_html(args) unless (file = args['file']) raise "'file' argument missing" end tag = args['tag'] example = TjpExample.new fileName = File.join(AppConfig.dataDirs('test')[0], 'TestSuite', 'Syntax', 'Correct', "#{file}.tjp") example.open(fileName) frame = XMLElement.new('div', 'class' => 'codeframe') frame << (pre = XMLElement.new('pre', 'class' => 'code')) unless (text = example.to_s(tag)) raise "There is no tag '#{tag}' in file " + "#{fileName}." end pre << XMLText.new(text) frame end # Not supported for this function. def to_tagged(args) nil end end end TaskJuggler-3.8.1/lib/taskjuggler/RichText/FunctionHandler.rb000066400000000000000000000026061473026623400242120ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = FunctionHandler.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/MessageHandler' class TaskJuggler # This class is the abstract base class for all RichText function handlers. # A function handler is responsible for a certain function such as 'example' # or 'query'. functions are used in internal RichText references such as # '[[example:Allocation 2]]'. 'example' is the function, 'Allocation' is the # path and '2' is the first argument. Arguments are optional. The function # handler can turn such internal references into Strings or XMLElement # trees. Therefor, each derived handler needs to implement a to_s, to_html # and to_tagged method that takes two parameter. The first is the path, the # second is the argument Array. class RichTextFunctionHandler include MessageHandler attr_reader :function, :blockFunction def initialize(function, sourceFileInfo = nil) @function = function @blockFunction = false @sourceFileInfo = sourceFileInfo end end end TaskJuggler-3.8.1/lib/taskjuggler/RichText/Parser.rb000066400000000000000000000060451473026623400223640ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = Parser.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/TextParser' require 'taskjuggler/RichText/Scanner' require 'taskjuggler/RichText/SyntaxRules' require 'taskjuggler/Log' class TaskJuggler # This is the parser class used by the RichText class to convert the input # text into an intermediate representation. Most of the actual work is done # by the generic TextParser class. The syntax description for the markup # language is provided by the RichTextSyntaxRules module. To break the input # String into tokens, the RichTextScanner class is used. class RichTextParser < TextParser include RichTextSyntaxRules # Create the parser and initialize the rule set. _rt_ is the RichText object # the resulting tree of RichTextElement objects should belong to. def initialize(rti, sectionCounter = [ 0, 0, 0, 0 ], tokenSet = nil) super() @richTextI = rti # These are the tokens that can be returned by the RichTextScanner. @variables = [ :LINEBREAK, :SPACE, :WORD, :BOLD, :ITALIC, :CODE, :BOLDITALIC, :PRE, :HREF, :HREFEND, :REF, :REFEND, :HLINE, :HTMLBLOB, :FCOLSTART, :FCOLEND, :QUERY, :INLINEFUNCSTART, :INLINEFUNCEND, :BLOCKFUNCSTART, :BLOCKFUNCEND, :ID, :STRING, :TITLE1, :TITLE2, :TITLE3, :TITLE4, :TITLE1END, :TITLE2END, :TITLE3END, :TITLE4END, :BULLET1, :BULLET2, :BULLET3, :BULLET4, :NUMBER1, :NUMBER2, :NUMBER3, :NUMBER4 ] limitTokenSet(tokenSet) # Load the rule set into the parser. initRules updateParserTables # The sections and numbered list can each nest 3 levels deep. We use these # counter Arrays to generate proper 1.2.3 type labels. @sectionCounter = sectionCounter @numberListCounter = [ 0, 0, 0, 0 ] end def reuse(rti, sectionCounter = [ 0, 0, 0, 0], tokenSet = nil) @blockedVariables = {} @stack = nil @richTextI = rti @sectionCounter = sectionCounter limitTokenSet(tokenSet) end # Construct the parser and get ready to read. def open(text) # Make sure that the last line is properly terminated with a newline. # Multiple newlines at the end are simply ignored. @scanner = RichTextScanner.new(text + "\n\n", Log) @scanner.open(true) end # Get the next token from the scanner. def nextToken @scanner.nextToken end # Return the last fetch token again to the scanner. def returnToken(token) @scanner.returnToken(token) end end end TaskJuggler-3.8.1/lib/taskjuggler/RichText/RTFHandlers.rb000066400000000000000000000020231473026623400232340ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = RTFHandlers.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/RichText/RTFNavigator' require 'taskjuggler/RichText/RTFQuery' require 'taskjuggler/RichText/RTFReport' require 'taskjuggler/RichText/RTFReportLink' class TaskJuggler # This convenience class creates an Array containing all RichTextFunction # objects used by TaskJuggler. class RTFHandlers def RTFHandlers.create(project, sourceFileInfo = nil) [ RTFNavigator.new(project, sourceFileInfo), RTFQuery.new(project, sourceFileInfo), RTFReport.new(project, sourceFileInfo), RTFReportLink.new(project, sourceFileInfo) ] end end end TaskJuggler-3.8.1/lib/taskjuggler/RichText/RTFNavigator.rb000066400000000000000000000030161473026623400234310ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = RTFNavigator.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/RichText/FunctionHandler' require 'taskjuggler/XMLElement' require 'taskjuggler/reports/Navigator' class TaskJuggler # This class is a specialized RichTextFunctionHandler that generates a # navigation bar for all reports that match the specified LogicalExpression. # It currently only supports HTML. class RTFNavigator < RichTextFunctionHandler def initialize(project, sourceFileInfo = nil) @project = project super('navigator', sourceFileInfo) @blockFunction = true end # Not supported for this function def to_s(args) '' end # Return a XMLElement tree that represents the navigator in HTML code. def to_html(args) if args.nil? || (id = args['id']).nil? error('rtf_nav_id_missing', "Argument 'id' missing to specify the navigator to be used.") end unless (navBar = @project['navigators'][id]) error('rtf_nav_unknown_id', "Unknown navigator #{id}") end navBar.to_html end # Not supported for this function. def to_tagged(args) nil end end end TaskJuggler-3.8.1/lib/taskjuggler/RichText/RTFQuery.rb000066400000000000000000000165011473026623400226070ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = RTFQuery.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/RichText/RTFWithQuerySupport' require 'taskjuggler/XMLElement' require 'taskjuggler/Query' class TaskJuggler # This class is a specialized RichTextFunctionHandler that can be used to # query the value of a project or property attribute. class RTFQuery < RTFWithQuerySupport def initialize(project, sourceFileInfo = nil) @project = project super('query', sourceFileInfo) @blockMode = false end # Return the result of the query as String. def to_s(args) return '' unless (query = prepareQuery(args)) if query.ok query.to_s else error('query_error', query.errorMessage + recreateQuerySyntax(args)) 'Query Error: ' + query.errorMessage end end # Return a XMLElement tree that represents the navigator in HTML code. def to_html(args) return nil unless (query = prepareQuery(args)) if query.ok if (rti = query.to_rti) rti.to_html elsif (str = query.to_s) XMLText.new(str) else nil end else error('query_error', query.errorMessage + recreateQuerySyntax(args)) font = XMLElement.new('font', 'color' => '#FF0000') font << XMLText.new('Query Error: ' + query.errorMessage) font end end # Not supported for this function. def to_tagged(args) nil end private def prepareQuery(args) unless @query raise "No Query has been registered for this RichText yet!" end query = @query.dup # Check the user provided arguments. Only the following list is allowed. validArgs = %w( attribute currencyformat end family journalattributes journalmode loadunit numberformat property scenario scopeproperty start timeformat ) expandedArgs = {} args.each do |arg, value| unless validArgs.include?(arg) error('bad_query_parameter', "Unknown query parameter '#{arg}'. " + "Use one of #{validArgs.join(', ')}!") return nil end expandedArgs[arg] = SimpleQueryExpander.new(value, @query, @sourceFileInfo).expand end if ((expandedArgs['property'] && expandedArgs['property'][0] != '!') || expandedArgs['scopeproperty']) && !(expandedArgs['family'] || @query.propertyType) error('missing_family', "If you provide a property or scope property you need to " + "provide a family type as well.") end # Every provided query parameter will overwrite the corresponding value # in the Query that was provided by the ReportContext. The name of the # arguments don't always exactly match the Query variables Let's start # with the easy ones. if expandedArgs['property'] query.propertyId = expandedArgs['property'] query.property = nil unless query.propertyId[0] == '!' end if expandedArgs['scopeproperty'] query.scopePropertyId = expandedArgs['scopeproperty'] query.scopeProperty = nil end query.attributeId = expandedArgs['attribute'] if expandedArgs['attribute'] query.start = TjTime.new(expandedArgs['start']) if expandedArgs['start'] query.end = TjTime.new(expandedArgs['end']) if expandedArgs['end'] if expandedArgs['numberformat'] query.numberFormat = expandedArgs['numberformat'] end query.timeFormat = expandedArgs['timeformat'] if expandedArgs['timeformat'] if expandedArgs['currencyformat'] query.currencyFormat = expandedArgs['currencyformat'] end query.project = @project # And now the slighly more complicated ones. setScenarioIdx(query, expandedArgs) setPropertyType(query, expandedArgs) setLoadUnit(query, expandedArgs) setJournalMode(query, expandedArgs) setJournalAttributes(query, expandedArgs) # Now that we have put together the query, we can process it and return # the query object for result extraction. query.process query end # Regenerate the original query text based on the argument list. def recreateQuerySyntax(args) queryText = "\n<-query" args.each do |a, v| queryText += " #{a}=\"#{v}\"" end queryText += "->" end def setPropertyType(query, args) validTypes = { 'account' => :Account, 'task' => :Task, 'resource' => :Resource } if args['family'] unless validTypes[args['family']] error('rtfq_bad_query_family', "Unknown query family type '#{args['family']}'. " + "Use one of #{validTypes.keys.join(', ')}!") end query.propertyType = validTypes[args['family']] if query.propertyType == :Task query.scopePropertyType = :Resource elsif query.propertyType == :Resource query.scopePropertyType = :Task end end end def setLoadUnit(query, args) units = { 'days' => :days, 'hours' => :hours, 'longauto' => :longauto, 'minutes' => :minutes, 'months' => :months, 'quarters' => :quarters, 'shortauto' => :shortauto, 'weeks' => :weeks, 'years' => :years } query.loadUnit = units[args['loadunit']] if args['loadunit'] end def setScenarioIdx(query, args) if args['scenario'] scenarioIdx = @project.scenarioIdx(args['scenario']) unless scenarioIdx error('rtfq_bad_scenario', "Unknown scenario #{args['scenario']}") end query.scenarioIdx = scenarioIdx end # Default to 0 in case no scenario was provided. query.scenarioIdx = 0 unless query.scenarioIdx end def setJournalMode(query, args) if (mode = args['journalmode']) validModes = %w( journal journal_sub status_up status_down alerts_down ) unless validModes.include?(mode) error('rtfq_bad_journalmode', "Unknown journalmode #{mode}. Must be one of " + "#{validModes.join(', ')}.") end query.journalMode = mode.intern elsif !query.journalMode query.journalMode = :journal end end def setJournalAttributes(query, args) if (attrListStr = args['journalattributes']) attrs = attrListStr.split(', ').map { |a| a.delete(' ') } query.journalAttributes = [] validAttrs = %w( author date details flags headline property propertyid summary timesheet ) attrs.each do |attr| if validAttrs.include?(attr) query.journalAttributes << attr else error('rtfq_bad_journalattr', "Unknown journalattribute #{attr}. Must be one of " + "#{validAttrs.join(', ')}.") end end elsif !query.journalAttributes query.journalAttributes = %w( date summary details ) end end end end TaskJuggler-3.8.1/lib/taskjuggler/RichText/RTFReport.rb000066400000000000000000000043631473026623400227600ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = RTFReport.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/RichText/FunctionHandler' require 'taskjuggler/XMLElement' class TaskJuggler # This class is a specialized RichTextFunctionHandler that includes a # report into the RichText output for supported formats. class RTFReport < RichTextFunctionHandler def initialize(project, sourceFileInfo = nil) @project = project super('report', sourceFileInfo) @blockFunction = true end # Not supported for this function def to_s(args) '' end # Return a HTML tree for the report. def to_html(args) if args.nil? || (id = args['id']).nil? error('rtp_report_id', "Argument 'id' missing to specify the report to be used.") return nil end unless (report = @project.report(id)) error('rtp_report_unknown_id', "Unknown report #{id}") return nil end # Detect recursive nesting found = false @project.reportContexts.each do |c| if c.report == report found = true break end end if found stack = "" @project.reportContexts.each do |context| stack += ' -> ' unless stack.empty? stack += '[ ' if context.report == report stack += context.report.fullId end stack += " -> #{report.fullId} ] ..." error('rtp_report_recursion', "Recursive nesting of reports detected: #{stack}") return nil end # Create a new context for the report. @project.reportContexts.push(ReportContext.new(@project, report)) # Generate the report with the new context report.generateIntermediateFormat html = report.to_html @project.reportContexts.pop html end # Not supported for this function. def to_tagged(args) nil end end end TaskJuggler-3.8.1/lib/taskjuggler/RichText/RTFReportLink.rb000066400000000000000000000045101473026623400235700ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = RTFReportLink.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/RichText/RTFWithQuerySupport' require 'taskjuggler/XMLElement' require 'taskjuggler/URLParameter' require 'taskjuggler/SimpleQueryExpander' class TaskJuggler # This class is a specialized RichTextFunctionHandler that generates a link # to another report. It's not available on all output formats. class RTFReportLink < RTFWithQuerySupport def initialize(project, sourceFileInfo = nil) @project = project super('reportlink', sourceFileInfo) @blockFunction = false @query = nil end # Not supported for this function def to_s(args) report = checkArgs(args) report.name end # Return a HTML tree for the report. def to_html(args) report = checkArgs(args) # The URL for interactive reports is different than for static reports. if report.interactive? # The project and report ID must be provided as query. url = "taskjuggler?project=#{@project['projectid']};" + "report=#{report.fullId}" if args['attributes'] qEx = SimpleQueryExpander.new(args['attributes'], @query, @sourceFileInfo) url += ";attributes=" + URLParameter.encode(qEx.expand) end else # The report name just gets a '.html' extension. url = report.name + ".html" end a = XMLElement.new('a', 'href'=> url) a << XMLText.new(report.name) a end # Not supported for this function. def to_tagged(args) nil end private def checkArgs(args) if args.nil? || (id = args['id']).nil? error('rtp_report_id', "Argument 'id' missing to specify the report to be used.") return nil end unless (report = @project.report(id)) error('rtp_report_unknown_id', "Unknown report #{id}") return nil end report end end end TaskJuggler-3.8.1/lib/taskjuggler/RichText/RTFWithQuerySupport.rb000066400000000000000000000021321473026623400250330ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = RTFWithQuerySupport.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/RichText/FunctionHandler' class TaskJuggler class RTFWithQuerySupport < RichTextFunctionHandler def initialize(type, sourceFileInfo = nil) super @query = nil end # This function must be called to register the Query object that will be # used to resolve the queries. It will create a copy of the object since # it will modify it. def setQuery(query) @query = query.dup end end class RichTextIntermediate def setQuery(query) @functionHandlers.each_value do |handler| if handler.respond_to?('setQuery') handler.setQuery(query) end end end end end TaskJuggler-3.8.1/lib/taskjuggler/RichText/Scanner.rb000066400000000000000000000174551473026623400225300ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = Scanner.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/UTF8String' require 'taskjuggler/TextParser/Scanner' class TaskJuggler # The RichTextScanner is used by the RichTextParser to chop the input text # into digestable tokens. It specializes the TextScanner class for RichText # syntax. The scanner can operate in various modes. The current mode is # context dependent. The following modes are supported: # # :bop : at the begining of a paragraph. # :bol : at the begining of a line. # :inline : in the middle of a line # :nowiki : ignoring all MediaWiki special tokens # :html : read anything until # :ref : inside of a REF [[ .. ]] # :href : inside of an HREF [ .. ] # :func : inside of a block <[ .. ]> or inline <- .. -> function class RichTextScanner < TextParser::Scanner def initialize(masterFile, log) tokenPatterns = [ # :bol mode rules [ :LINEBREAK, /\s*\n/, :bol, method('linebreak') ], [ nil, /\s+/, :bol, method('inlineMode') ], # :bop mode rules [ :PRE, / [^\n]+\n?/, :bop, method('pre') ], [ nil, /\s*\n/, :bop, method('linebreak') ], # :inline mode rules [ :SPACE, /[ \t\n]+/, :inline, method('space') ], # :bop and :bol mode rules [ :INLINEFUNCSTART, /<-/, [ :bop, :bol, :inline ], method('functionStart') ], [ :BLOCKFUNCSTART, /<\[/, [ :bop, :bol ], method('functionStart') ], [ ':TITLE*', /={2,5}/, [ :bop, :bol ], method('titleStart') ], [ 'TITLE*END', /={2,5}/, :inline, method('titleEnd') ], [ 'BULLET*', /\*{1,4}[ \t]+/, [ :bop, :bol ], method('bullet') ], [ 'NUMBER*', /\#{1,4}[ \t]+/, [ :bop, :bol ], method('number') ], [ :HLINE, /----/, [ :bop, :bol ], method('inlineMode') ], # :bop, :bol and :inline mode rules # The token puts the scanner into :nowiki mode. [ nil, //, [ :bop, :bol, :inline ], method('nowikiStart') ], [ nil, //, [ :bop, :bol, :inline ], method('htmlStart') ], [ :FCOLSTART, //, [ :bop, :bol, :inline ], method('fontColorStart') ], [ :FCOLEND, /<\/fcol>/, [ :bop, :bol, :inline ], method('fontColorEnd') ], [ :QUOTES, /'{2,5}/, [ :bop, :bol, :inline ], method('quotes') ], [ :REF, /\[\[/, [ :bop, :bol, :inline ], method('refStart') ], [ :HREF, /\[/, [ :bop, :bol, :inline], method('hrefStart') ], [ :WORD, /.[^ \n\t\[<']*/, [ :bop, :bol, :inline ], method('inlineMode') ], # :nowiki mode rules [ nil, /<\/nowiki>/, :nowiki, method('nowikiEnd') ], [ :WORD, /(<(?!\/nowiki>)|[^ \t\n<])+/, :nowiki ], [ :SPACE, /[ \t]+/, :nowiki ], [ :LINEBREAK, /\s*\n/, :nowiki ], # :html mode rules [ :HTMLBLOB, /(.|\n)*<\/html>/ , :html, method('htmlEnd') ], [ :HTMLBLOB, /.*\n/ , :html ], # :ref mode rules [ :REFEND, /\]\]/, :ref, method('refEnd') ], [ :WORD, /(<(?!-)|(\](?!\])|[^|<\]]))+/, :ref ], [ :QUERY, /<-\w+->/, :ref, method('query') ], [ :LITERAL, /./, :ref ], # :href mode rules [ :HREFEND, /\]/, :href, method('hrefEnd') ], [ :WORD, /(<(?!-)|[^ \t\n\]<])+/, :href ], [ :QUERY, /<-\w+->/, :href, method('query') ], [ :SPACE, /[ \t\n]+/, :href ], # :func mode rules [ :INLINEFUNCEND, /->/ , :func, method('functionEnd') ], [ :BLOCKFUNCEND, /\]>/, :func, method('functionEnd') ], [ :ID, /[a-zA-Z_]\w*/, :func ], [ :STRING, /"(\\"|[^"])*"/, :func, method('dqString') ], [ :STRING, /'(\\'|[^'])*'/, :func, method('sqString') ], [ nil, /[ \t\n]+/, :func ], [ :LITERAL, /./, :func ] ] super(masterFile, log, tokenPatterns, :bop) end private def space(type, match) if match.index("\n") # If the match contains a linebreak we switch to :bol mode. self.mode = :bol # And return an empty string. match = '' end [ type, match ] end def linebreak(type, match) self.mode = :bop [ type, match ] end def inlineMode(type, match) self.mode = :inline [ type, match ] end def titleStart(type, match) self.mode = :inline [ "TITLE#{match.length - 1}".intern, match ] end def titleEnd(type, match) [ "TITLE#{match.length - 1}END".intern, match ] end def bullet(type, match) self.mode = :inline [ "BULLET#{match.count('*')}".intern, match ] end def number(type, match) self.mode = :inline [ "NUMBER#{match.count('#')}".intern, match ] end def fontColorStart(type, match) self.mode = :inline # Extract color name from colName = match[6..-2] if colName =~ /#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})/ # We've got a valid hex number. else validColors = %w( black maroon green olive navy purple teal silver gray red lime yellow blue fuchsia aqua white ) unless validColors.include?(colName) error('bad_color_name', "#{colName} is not a supported color. Use one of " + "#{validColors.join(', ')} or #RGB where 'R', 'G' and 'B' " + "are one or two digit hexadecimal numbers.") end end [ type, colName ] end def fontColorEnd(type, match) [ type, match ] end def quotes(type, match) self.mode = :inline types = [ nil, nil, :ITALIC, :BOLD , :CODE, :BOLDITALIC ] [ types[match.length], match ] end def htmlStart(type, match) self.mode = :html [ type, match ] end def htmlEnd(type, match) self.mode = :inline [ type, match[0..-8] ] end def nowikiStart(type, match) self.mode = :nowiki [ type, match ] end def nowikiEnd(type, match) self.mode = :inline [ type, match ] end def functionStart(type, match) # When restoring :bol or :bop mode, we need to switch to :inline mode. @funcLastMode = (@scannerMode == :bop || @scannerMode == :bol) ? :inline : @scannerMode self.mode = :func [ type, match ] end def functionEnd(type, match) self.mode = @funcLastMode @funcLastMode = nil [ type, match ] end def pre(type, match) [ type, match[1..-1] ] end def dqString(type, match) # Remove first and last character and remove backslashes from quoted # double quotes. [ type, match[1..-2].gsub(/\\"/, '"') ] end def sqString(type, match) # Remove first and last character and remove backslashes from quoted # single quotes. [ type, match[1..-2].gsub(/\\'/, "'") ] end def query(type, match) # Remove <- and ->. [ type, match[2..-3] ] end def hrefStart(type, match) # When restoring :bol or :bop mode, we need to switch to :inline mode. @hrefLastMode = (@scannerMode == :bop || @scannerMode == :bol) ? :inline : @scannerMode self.mode = :href [ type, match ] end def hrefEnd(type, match) self.mode = @hrefLastMode @hrefLastMode = nil [ type, match ] end def refStart(type, match) self.mode = :ref [ type, match ] end def refEnd(type, match) self.mode = :inline [ type, match ] end end end TaskJuggler-3.8.1/lib/taskjuggler/RichText/Snip.rb000066400000000000000000000064311473026623400220400ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = Snip.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/RichText' require 'taskjuggler/HTMLDocument' class TaskJuggler # A RichTextSnip is a building block for a RichTextDocument. It represents # the contense of a text file that contains structured text using the # RichText syntax. The class can read-in such a text file and generate an # equivalent HTML version. class RichTextSnip attr_reader :name attr_accessor :prevSnip, :nextSnip # Create a RichTextSnip object. _document_ is a reference to the # RichTextDocument. _fileName_ is the name of the structured text file # using RichText syntax. _sectionCounter_ is an 3 item Integer Array. These # 3 numbers are used to store the section counters over multiple # RichTextSnip objects. def initialize(document, fileName, sectionCounter) @document = document # Strip any directories from fileName. @name = fileName.index('/') ? fileName[fileName.rindex('/') + 1 .. -1] : fileName text = '' File.open(fileName) do |file| file.each_line { |line| text += line } end rText = RichText.new(text, @document.functionHandlers) unless (@richText = rText.generateIntermediateFormat(sectionCounter)) exit end @prevSnip = @nextSnip = nil end # Set the target for all anchor links in the document. def linkTarget=(target) @richText.linkTarget = target end # Set the CSS class. def cssClass=(css) @richText.cssClass = css end # Generate a TableOfContents object from the section headers of the # RichTextSnip. def tableOfContents(toc, fileName) @richText.tableOfContents(toc, fileName) end # Return an Array with all other snippet names that are referenced by # internal references in this snip. def internalReferences @richText.internalReferences end # Generate a HTML version of the structured text. The base file name is the # same as the original file. _directory_ is the name of the output # directory. def generateHTML(directory = '') html = HTMLDocument.new head = html.generateHead(@name) head << @document.generateStyleSheet html.html << (body = XMLElement.new('body')) body << @document.generateHTMLHeader body << generateHTMLNavigationBar body << (div = XMLElement.new('div', 'style' => 'width:90%; margin-left:5%; margin-right:5%')) div << @richText.to_html body << generateHTMLNavigationBar body << @document.generateHTMLFooter html.write(directory + @name + '.html') end private def generateHTMLNavigationBar @document.generateHTMLNavigationBar( @prevSnip ? @prevSnip.name : nil, @prevSnip ? "#{prevSnip.name}.html" : nil, @nextSnip ? @nextSnip.name : nil, @nextSnip ? "#{nextSnip.name}.html" : nil) end end end TaskJuggler-3.8.1/lib/taskjuggler/RichText/SyntaxRules.rb000066400000000000000000000325241473026623400234320ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = SyntaxRules.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # class TaskJuggler # This modules contains the syntax definition for the RichTextParser. The # defined syntax aims to be compatible to the most commonly used markup # elements of the MediaWiki system. See # http://en.wikipedia.org/wiki/Wikipedia:Cheatsheet for details. # # Linebreaks are treated just like spaces as word separators unless it is # followed by another newline or any of the start-of-line special # characters. These characters start sequences that mark headlines, bullet # items and such. The special meaning only gets activated when used at the # start of the line. # # The parser traverses the input text and creates a tree of RichTextElement # objects. This is the intermediate representation that can be converted to # the final output format. module RichTextSyntaxRules # This is the entry node. def rule_richtext pattern(%w( !sections . ), lambda { RichTextElement.new(@richTextI, :richtext, @val[0]) }) end def rule_sections optional repeatable pattern(%w( !section !blankLines ), lambda { @val[0] }) end # The following syntax elements are all block elements that can span # multiple lines. def rule_section pattern(%w( !headlines ), lambda { @val[0] }) pattern(%w( $HLINE ), lambda { RichTextElement.new(@richTextI, :hline, @val[0]) }) pattern(%w( !paragraph ), lambda { @val[0] }) pattern(%w( !pre ), lambda { RichTextElement.new(@richTextI, :pre, @val[0].join) }) pattern(%w( !bulletList1 ), lambda { RichTextElement.new(@richTextI, :bulletlist1, @val[0]) }) pattern(%w( !numberList1 ), lambda { @numberListCounter = [ 0, 0, 0, 0 ] RichTextElement.new(@richTextI, :numberlist1, @val[0]) }) pattern(%w( !blockFunction ), lambda { @val[0] }) end def rule_headlines pattern(%w( !title1 ), lambda { @val[0] }) pattern(%w( !title2 ), lambda { @val[0] }) pattern(%w( !title3 ), lambda { @val[0] }) pattern(%w( !title4 ), lambda { @val[0] }) end def rule_pre repeatable pattern(%w( $PRE ), lambda { @val[0] }) end def rule_title1 pattern(%w( $TITLE1 !space !text $TITLE1END ), lambda { el = RichTextElement.new(@richTextI, :title1, @val[2]) @sectionCounter[0] += 1 @sectionCounter[1] = @sectionCounter[2] = 0 el.data = @sectionCounter.dup el }) end def rule_title2 pattern(%w( $TITLE2 !space !text $TITLE2END ), lambda { el = RichTextElement.new(@richTextI, :title2, @val[2]) @sectionCounter[1] += 1 @sectionCounter[2] = 0 el.data = @sectionCounter.dup el }) end def rule_title3 pattern(%w( $TITLE3 !space !text $TITLE3END ), lambda { el = RichTextElement.new(@richTextI, :title3, @val[2]) @sectionCounter[2] += 1 @sectionCounter[3] = 0 el.data = @sectionCounter.dup el }) end def rule_title4 pattern(%w( $TITLE4 !space !text $TITLE4END ), lambda { el = RichTextElement.new(@richTextI, :title4, @val[2]) @sectionCounter[3] += 1 el.data = @sectionCounter.dup el }) end def rule_bulletList1 optional repeatable pattern(%w( $BULLET1 !text ), lambda { RichTextElement.new(@richTextI, :bulletitem1, @val[1]) }) pattern(%w( !bulletList2 ), lambda { RichTextElement.new(@richTextI, :bulletlist2, @val[0]) }) end def rule_bulletList2 repeatable pattern(%w( $BULLET2 !text ), lambda { RichTextElement.new(@richTextI, :bulletitem2, @val[1]) }) pattern(%w( !bulletList3 ), lambda { RichTextElement.new(@richTextI, :bulletlist3, @val[0]) }) end def rule_bulletList3 repeatable pattern(%w( $BULLET3 !text ), lambda { RichTextElement.new(@richTextI, :bulletitem3, @val[1]) }) pattern(%w( !bulletList4 ), lambda { RichTextElement.new(@richTextI, :bulletlist4, @val[0]) }) end def rule_bulletList4 repeatable pattern(%w( $BULLET4 !text ), lambda { RichTextElement.new(@richTextI, :bulletitem4, @val[1]) }) end def rule_numberList1 repeatable pattern(%w( $NUMBER1 !text !blankLines ), lambda { el = RichTextElement.new(@richTextI, :numberitem1, @val[1]) @numberListCounter[0] += 1 el.data = @numberListCounter.dup el }) pattern(%w( !numberList2 ), lambda { @numberListCounter[1, 2] = [ 0, 0 ] RichTextElement.new(@richTextI, :numberlist2, @val[0]) }) end def rule_numberList2 repeatable pattern(%w( $NUMBER2 !text !blankLines ), lambda { el = RichTextElement.new(@richTextI, :numberitem2, @val[1]) @numberListCounter[1] += 1 el.data = @numberListCounter.dup el }) pattern(%w( !numberList3 ), lambda { @numberListCounter[2] = 0 RichTextElement.new(@richTextI, :numberlist3, @val[0]) }) end def rule_numberList3 repeatable pattern(%w( $NUMBER3 !text !blankLines ), lambda { el = RichTextElement.new(@richTextI, :numberitem3, @val[1]) @numberListCounter[2] += 1 el.data = @numberListCounter.dup el }) pattern(%w( !numberList4 ), lambda { @numberListCounter[3] = 0 RichTextElement.new(@richTextI, :numberlist4, @val[0]) }) end def rule_numberList4 repeatable pattern(%w( $NUMBER4 !text !blankLines ), lambda { el = RichTextElement.new(@richTextI, :numberitem4, @val[1]) @numberListCounter[3] += 1 el.data = @numberListCounter.dup el }) end def rule_paragraph pattern(%w( !text ), lambda { RichTextElement.new(@richTextI, :paragraph, @val[0]) }) end def rule_text pattern(%w( !textWithSpace ), lambda { @val[0].last.appendSpace = false @val[0] }) end def rule_textWithSpace repeatable pattern(%w( !plainTextWithLinks ), lambda { @val[0] }) pattern(%w( !inlineFunction ), lambda { @val[0] }) pattern(%w( $ITALIC !space !plainTextWithLinks $ITALIC !space ), lambda { el = RichTextElement.new(@richTextI, :italic, @val[2]) # Since the italic end marker will disappear we need to make sure # there was no space before it. @val[2].last.appendSpace = false if @val[2].last el.appendSpace = !@val[4].nil? el }) pattern(%w( $BOLD !space !plainTextWithLinks $BOLD !space ), lambda { el = RichTextElement.new(@richTextI, :bold, @val[2]) @val[2].last.appendSpace = false if @val[2].last el.appendSpace = !@val[4].nil? el }) pattern(%w( $CODE !space !plainTextWithLinks $CODE !space ), lambda { el = RichTextElement.new(@richTextI, :code, @val[2]) @val[2].last.appendSpace = false if @val[2].last el.appendSpace = !@val[4].nil? el }) pattern(%w( $BOLDITALIC !space !plainTextWithLinks $BOLDITALIC !space ), lambda { el = RichTextElement.new(@richTextI, :bold, RichTextElement.new(@richTextI, :italic, @val[2])) @val[2].last.appendSpace = false if @val[2].last el.appendSpace = !@val[4].nil? el }) pattern(%w( $FCOLSTART !space !plainTextWithLinks $FCOLEND !space ), lambda { el = RichTextElement.new(@richTextI, :fontCol, @val[2]) el.data = @val[0] el.appendSpace = !@val[4].nil? el }) end def rule_plainTextWithLinks pattern(%w( !plainText ), lambda { @val[0] }) pattern(%w( $REF !refToken !moreRefToken $REFEND !space ), lambda { v1 = @val[1].join if v1.index(':') protocol, locator = v1.split(':') else protocol = nil end el = nil if protocol == 'File' el = RichTextElement.new(@richTextI, :img) unless (index = locator.rindex('.')) error('rt_file_no_ext', "File name without extension: #{locator}") end extension = locator[index + 1..-1].downcase unless %w( jpg gif png svg ).include?(extension) error('rt_file_bad_ext', "Unsupported file type: #{extension}") end el.data = img = RichTextImage.new(locator) if @val[2] @val[2].each do |token| if token[0, 4] == 'alt=' img.altText = token[4..-1] elsif %w( top middle bottom baseline sub super text-top text-bottom ).include?(token) img.verticalAlign = token else error('rt_bad_file_option', "Unknown option '#{token}' for file reference " + "#{v1}.") end end end else val = @val[2] || v1 el = RichTextElement.new(@richTextI, :ref, RichTextElement.new(@richTextI, :text, val)) el.data = v1 el.appendSpace = !@val[4].nil? end el }) pattern(%w( $HREF !wordWithQueries !space !plainTextWithQueries $HREFEND !space ), lambda { el = RichTextElement.new(@richTextI, :href, @val[3] || @val[1]) el.data = RichTextElement.new(@richTextI, :richtext, @val[1]) el.appendSpace = !@val[5].nil? el }) end def rule_moreRefToken repeatable optional pattern(%w( _| !refToken ), lambda { @val[1].join }) end def rule_refToken repeatable pattern(%w( $WORD ), lambda { @val[0] }) end def rule_wordWithQueries repeatable pattern(%w( $WORD ), lambda { RichTextElement.new(@richTextI, :text, @val[0]) }) pattern(%w( $QUERY ), lambda { # The <-attributeID-> syntax is a shortcut for an embedded query # inline function. It can only be used within a ReportTableCell # context that provides a property and a scope property. el = RichTextElement.new(@richTextI, :inlinefunc) # Data is a 2 element Array with the function name and a Hash for the # arguments. el.data = ['query', { 'attribute' => @val[0] } ] el }) end def rule_plainText repeatable optional pattern(%w( !htmlBlob !space ), lambda { el = RichTextElement.new(@richTextI, :htmlblob, @val[0].join) el.appendSpace = !@val[1].nil? el }) pattern(%w( $WORD !space ), lambda { el = RichTextElement.new(@richTextI, :text, @val[0]) el.appendSpace = !@val[1].nil? el }) end def rule_plainTextWithQueries repeatable optional pattern(%w( !wordWithQueries !space ), lambda { @val[0][-1].appendSpace = true if @val[1] @val[0] }) end def rule_htmlBlob repeatable pattern(%w( $HTMLBLOB ), lambda { @val[0] }) end def rule_space optional repeatable pattern(%w( $SPACE ), lambda { true }) end def rule_blankLines optional repeatable pattern(%w( $LINEBREAK )) pattern(%w( $SPACE )) end def rule_blockFunction pattern(%w( $BLOCKFUNCSTART $ID !functionArguments $BLOCKFUNCEND ), lambda { args = {} @val[2].each { |arg| args[arg[0]] = arg[1] } if @val[2] el = RichTextElement.new(@richTextI, :blockfunc) # Data is a 2 element Array with the function name and a Hash for the # arguments. unless @richTextI.richText.functionHandler(@val[1], true) error('bad_block_function', "Unsupported block function #{@val[1]}") end el.data = [@val[1], args ] el }) end def rule_inlineFunction pattern(%w( $INLINEFUNCSTART $ID !functionArguments $INLINEFUNCEND !space ), lambda { args = {} @val[2].each { |arg| args[arg[0]] = arg[1] } if @val[2] el = RichTextElement.new(@richTextI, :inlinefunc) # Data is a 2 element Array with the function name and a Hash for the # arguments. unless @richTextI.richText.functionHandler(@val[1], false) error('bad_inline_function', "Unsupported inline function #{@val[1]}") end el.data = [@val[1], args ] el.appendSpace = !@val[4].nil? el }) end def rule_functionArguments optional repeatable pattern(%w( $ID _= $STRING ), lambda { [ @val[0], @val[2] ] }) end end end TaskJuggler-3.8.1/lib/taskjuggler/RichText/TOCEntry.rb000066400000000000000000000051651473026623400226010ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = TOCEntry.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/UTF8String' require 'taskjuggler/XMLElement' class TaskJuggler # A TOCEntry object is used to store the data of an entry in a TableOfContents # object. It stores the section number, the title, the file name and the name # of the tag in this file. The tag is optional and may be nil. The object can # be turned into an HTML tree. class TOCEntry attr_reader :number, :title, :file, :tag # Create a TOCEntry object. # _number_: The section number as String, e. g. '1.2.3' or 'A.3'. # _title_: The section title as String. # _file_: The name of the file. # _tag_: An optional tag within the file. def initialize(number, title, file, tag = nil) @number = number @title = title @file = file @tag = tag end # Return the TOCEntry as equivalent HTML elements. The result is an Array of # XMLElement objects. def to_html html = [] if level == 0 # A another table line for some extra distance above main chapters. html << (tr = XMLElement.new('tr')) tr << (td = XMLElement.new('td')) td << XMLElement.new('div', 'style' => 'height:10px') end # Use a different font size depending on the element level. fontSizes = [ 20, 17, 15, 14, 14 ] tr = XMLElement.new('tr', 'style' => "font-size:#{fontSizes[level]}px;") tr << (td = XMLElement.new('td', 'style' => "width:30px;")) # Top-level headings have their number in the left column. td << XMLText.new(@number) if level == 0 tr << (td = XMLElement.new('td')) if level > 0 # Lower level headings have their number in the right column with the # heading text. td << XMLElement.new('span', 'style' => 'padding-right:15px') do XMLText.new(@number) end end tag = @tag ? "##{@tag}" : '' td << (a = XMLElement.new('a', 'href' => "#{@file}.html#{tag}")) a << XMLText.new(@title) html << tr html end private # Returns the level of the section. It simply counts the number of dots in # the section number. def level lev = 0 @number.each_utf8_char { |c| lev += 1 if c == '.' } lev end end end TaskJuggler-3.8.1/lib/taskjuggler/RichText/TableOfContents.rb000066400000000000000000000026421473026623400241610ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = TableOfContents.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/XMLElement' require 'taskjuggler/RichText/TOCEntry' class TaskJuggler # This class can be used to store a table of contents. It's just an Array of # TOCEntry objects. Each TOCEntry objects represents the title of a section. class TableOfContents # Create an empty TableOfContents object. def initialize @entries = [] end # This method must be used to add new TOCEntry objects to the # TableOfContents. _entry_ must be a TOCEntry object reference. def addEntry(entry) @entries << entry end def each @entries.each { |e| yield e } end # Return HTML elements that represent the content of the TableOfContents # object. The result is a tree of XMLElement objects. def to_html div = XMLElement.new('div', 'style' => 'margin-left:15%; margin-right:15%;') div << (table = XMLElement.new('table')) @entries.each { |e| table << e.to_html } div end end end TaskJuggler-3.8.1/lib/taskjuggler/RuntimeConfig.rb000066400000000000000000000046311473026623400221460ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = RuntimeConfig.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'yaml' # The RuntimeConfig searches for a YAML config file in a list of directories. # When a file is found it is read-in. The read-in config values are grouped in # a tree of sections. The values of a section can then be used to overwrite # the instance variable of a passed object. class RuntimeConfig attr_accessor :debugMode def initialize(appName, configFile = nil) @appName = appName @config = nil @debugMode = false if configFile # Read user specified config file. unless loadConfigFile(configFile) error("Config file #{configFile} not found!") end else # Search config files in certain directories. [ '.', ENV['HOME'], '/etc' ].each do |path| # Try UNIX style hidden file first, then .rc. [ "#{path}/.#{appName}rc", "#{path}/#{appName}.rc" ].each do |file| break if loadConfigFile(file) end end end end def configure(object, section) debug("Configuring object of type #{object.class}") sections = section.split('.') return false unless (p = @config) sections.each do |sec| p = p['_' + sec] unless p && p.is_a?(Hash) debug("Section #{section} not found in config file") return false end end object.instance_variables.each do |iv| ivName = iv[1..-1] debug("Processing class variable #{ivName}") if p.include?(ivName) debug("Setting @#{ivName} to #{p[ivName]}") object.instance_variable_set(iv, p[ivName]) end end true end private def loadConfigFile(fileName) if File.exist?(fileName) debug("Loading #{fileName}") begin @config = YAML::load(File.read(fileName)) rescue error("Error in config file #{fileName}: #{$!}") end debug(@config.to_s) return true end false end def debug(message) return unless @debugMode puts message end def error(message) $stderr.puts message exit 1 end end TaskJuggler-3.8.1/lib/taskjuggler/Scenario.rb000066400000000000000000000012411473026623400211320ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = Scenario.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/PropertyTreeNode' class TaskJuggler class Scenario < PropertyTreeNode def initialize(project, id, name, parent) super(project.scenarios, id, name, parent) project.addScenario(self) end end end TaskJuggler-3.8.1/lib/taskjuggler/ScenarioData.rb000066400000000000000000000035351473026623400217340ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = ScenarioData.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/TjException' require 'taskjuggler/MessageHandler' class TaskJuggler class ScenarioData attr_reader :property def initialize(property, idx, attributes) @property = property @project = property.project @scenarioIdx = idx @attributes = attributes @messageHandler = MessageHandlerInstance.instance # Register the scenario with the Task. @property.data[idx] = self end # We only use deep_clone for attributes, never for properties. Since # attributes may reference properties these references should remain # references. def deep_clone self end def a(attributeName) @attributes[attributeName].get end def error(id, text, sourceFileInfo = nil, property = nil) @messageHandler.error( id, text, sourceFileInfo || @property.sourceFileInfo, nil, property || @property, @project.scenario(@scenarioIdx)) end def warning(id, text, sourceFileInfo = nil, property = nil) @messageHandler.warning( id, text, sourceFileInfo || @property.sourceFileInfo, nil, property || @property, @project.scenario(@scenarioIdx)) end def info(id, text, sourceFileInfo = nil, property = nil) @messageHandler.info( id, text, sourceFileInfo || @property.sourceFileInfo, nil, property || @property, @project.scenario(@scenarioIdx)) end end end TaskJuggler-3.8.1/lib/taskjuggler/Scoreboard.rb000066400000000000000000000134161473026623400214610ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = Scoreboard.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2019 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/IntervalList' class TaskJuggler # Scoreboard objects are instrumental during the scheduling process. The # project time span is divided into discrete time slots by the scheduling # resolution. This class models the resulting time slots with an array that # spans from project start o project end. Each slot has an index start with 0 # at the project start. class Scoreboard attr_reader :startDate, :endDate, :resolution, :size # Create the scoreboard based on the the given _startDate_, _endDate_ and # timing _resolution_. The resolution must be specified in seconds. # Optionally you can provide an initial value for the scoreboard cells. def initialize(startDate, endDate, resolution, initVal = nil) @startDate = startDate @endDate = endDate @resolution = resolution @size = ((endDate - startDate) / resolution).ceil + 1 clear(initVal) end # Erase all values and set them to nil or a new initial value. def clear(initVal = nil) @sb = Array.new(@size, initVal) end # Converts a scroreboard index to the corresponding date. You can optionally # sanitize the _idx_ value by forcing it into the project range. def idxToDate(idx, forceIntoProject = false) if forceIntoProject return @startDate if kdx < 0 return @endDate if @size - 1 if idx >= @size elsif idx < 0 || idx >= @size raise "Index #{idx} is out of scoreboard range (#{size - 1})" end @startDate + idx * @resolution end # Converts a date to the corresponding scoreboard index. You can optionally # sanitize the _date_ by forcing it into the project time span. def dateToIdx(date, forceIntoProject = true) idx = ((date - @startDate) / @resolution).to_i if forceIntoProject return 0 if idx < 0 return @size - 1 if idx >= @size elsif (idx < 0 || idx >= @size) raise "Date #{date} is out of project time range " + "(#{@startDate} - #{@endDate})" end idx end # Iterate over all scoreboard entries. def each(startIdx = 0, endIdx = @size) if startIdx != 0 || endIdx != @size startIdx.upto(endIdx - 1) do |i| yield @sb[i] end else @sb.each do |entry| yield entry end end end # Iterate over all scoreboard entries by index. def each_index @sb.each_index do |index| yield index end end # Assign result of block to each element. def collect! @sb.collect! { |x| yield x } end # Get the value at index _idx_. def [](idx) @sb[idx] end # Set the _value_ at index _idx_. def []=(idx, value) @sb[idx] = value end # Get the value corresponding to _date_. def get(date) @sb[dateToIdx(date)] end # Set the _value_ corresponding to _date_. def set(date, value) @sb[dateToIdx(date)] = value end # Return a list of intervals that describe a contiguous part of the # scoreboard that contains only the values that yield true for the passed # block. The intervals must be within the interval described by _iv_ and # must be at least _minDuration_ long. The return value is an # IntervalList. def collectIntervals(iv, minDuration) # Determine the start and stop index for the scoreboard search. We save # the original values for later use as well. startIdx = sIdx = dateToIdx(iv.start) endIdx = eIdx = dateToIdx(iv.end) # Convert the minDuration into number of slots. minDuration /= @resolution minDuration = 1 if minDuration <= 0 # Expand the interval with the minDuration to both sides. This will # reduce the failure to detect intervals at the iv boundary. However, # this will not prevent undetected intervals at the project time frame # boundaries. startIdx -= minDuration startIdx = 0 if startIdx < 0 endIdx += minDuration endIdx = @size - 1 if endIdx > @size - 1 # This is collects the resulting intervals. intervals = IntervalList.new # The duration counter for the currently analyzed interval and the start # index. duration = start = 0 idx = startIdx loop do # Check whether the scoreboard slot matches any of the target values # and we have not yet reached the last slot. if yield(@sb[idx]) && idx < endIdx # If so, save the start position if this is the first slot and start # counting the matching slots. start = idx if start == 0 duration += 1 else # If we don't have a match or are at the end of the interval, check # if we've just finished a matching interval. if duration > 0 if duration >= minDuration # Make sure that all intervals are within the originally # requested Interval. start = sIdx if start < sIdx idx = eIdx if idx > eIdx intervals << TimeInterval.new(idxToDate(start), idxToDate(idx)) end duration = start = 0 end end break if (idx += 1) > endIdx end intervals end def inspect s = '' 0.upto(@sb.length - 1) do |i| s << "#{idxToDate(i)}: #{@sb[i]}" end s end end end TaskJuggler-3.8.1/lib/taskjuggler/SheetHandlerBase.rb000066400000000000000000000204601473026623400225340ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = SheetHandlerBase.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'mail' require 'taskjuggler/UTF8String' require 'taskjuggler/RichText' require 'taskjuggler/HTMLDocument' class TaskJuggler class SheetHandlerBase attr_accessor :workingDir, :dryRun def initialize(appName) @appName = appName # User specific settings @emailDeliveryMethod = 'smtp' @smtpServer = nil @senderEmail = nil @workingDir = nil @scmCommand = nil # The default project ID @projectId = 'prj' # Controls the amount of output that is sent to the terminal. # 0: No output # 1: only errors # 2: errors and warnings # 3: All messages @outputLevel = 2 # Controls the amount of information that is added to the log file. The # levels are identical to @outputLevel. @logLevel = 3 # Set to true to not send any emails. Instead the email (header + body) is # printed to the terminal. @dryRun = false @logFile = 'timesheets.log' @emailFailure = false end # Extract the text between the cut-marker lines and remove any email # quotes from the beginnin of the line. def cutOut(text) # Pattern for the section start marker mark1 = /(.*)# --------8<--------8<--------/ # Pattern for the section end marker mark2 = /# -------->8-------->8--------/ # The cutOut section cutOutText = nil quoteLen = 0 quoteMarks = emptyLine = '' text.each_line do |line| if cutOutText.nil? # We are looking for the line with the start marker (mark1) if (matches = mark1.match(line)) quoteMarks = matches[1] quoteLen = quoteMarks.length # Special case for quoted empty lines without trailing spaces. emptyLine = quoteMarks.chomp.chomp(' ') + "\n" cutOutText = line[quoteLen..-1] end else # Remove quote marks from the beginning of the line. line = line[quoteLen..-1] if line[0, quoteLen] == quoteMarks line = "\n" if line == emptyLine cutOutText << line # We are gathering text until we hit the end marker (mark2) return cutOutText if mark2.match(line) end end # There are no cut markers. We just return the original text. text end def setWorkingDir # Make sure the user has provided a properly setup config file. case @emailDeliveryMethod when 'smtp' error('\'smtpServer\' not configured') unless @smtpServer when 'sendmail' # nothing to check else error("Unknown emailDeliveryMethod #{@emailDeliveryMethod}") end error('\'senderEmail\' not configured') unless @senderEmail # Change into the specified working directory begin Dir.chdir(@workingDir) if @workingDir rescue error("Working directory #{@workingDir} not found") end end def addToScm(message, fileName) return unless @scmCommand cmd = @scmCommand.gsub(/%m/, message) cmd.gsub!(/%f/, fileName) unless @dryRun `#{cmd}` if $? == 0 info("Added #{fileName} to SCM") else error("SCM command #{cmd} failed: #{$?.class}") end end end def info(message) puts message if @outputLevel >= 3 log('INFO', message) if @logLevel >= 3 end def warning(message) puts message if @outputLevel >= 2 log('WARN', message) if @logLevel >= 2 end def error(message) $stderr.puts message if @outputLevel >= 1 log("ERROR", message) if @logLevel >= 1 raise TjRuntimeError end def log(type, message) timeStamp = Time.new.strftime("%Y-%m-%d %H:%M:%S") File.open(@logFile, 'a') do |f| f.write("#{timeStamp} #{type} #{@appName}: #{message}\n") end end # Like SheetHandlerBase::sendEmail but interpretes the _message_ as # RichText markup. The generated mail will have a text/plain and a # text/html part. def sendRichTextEmail(to, subject, message, attachment = nil, from = nil, inReplyTo = nil) rti = RichText.new(message).generateIntermediateFormat rti.lineWidth = 72 rti.indent = 2 rti.titleIndent = 0 rti.listIndent = 2 rti.parIndent = 2 rti.preIndent = 4 rti.sectionNumbers = false # Send out the email. sendEmail(to, subject, rti, attachment, from, inReplyTo) end def sendEmail(to, subject, message, attachment = nil, from = nil, inReplyTo = nil) case @emailDeliveryMethod when 'smtp' settings_dto = { :address => @smtpServer, :port => 25, } Mail.defaults do delivery_method :smtp, settings_dto end when 'sendmail' Mail.defaults do delivery_method :sendmail end else raise "Unknown email delivery method: #{@emailDeliveryMethod}" end begin self_ = self mail = Mail.new do subject subject text_part do content_type [ 'text', 'plain', { 'charset' => 'UTF-8' } ] content_transfer_encoding 'base64' body message.to_s.to_base64 end if message.is_a?(RichTextIntermediate) html_part do content_type 'text/html; charset=UTF-8' content_transfer_encoding 'base64' body self_.htmlMailBody(message).to_base64 end end end mail.to = to mail.from = from || @senderEmail mail.in_reply_to = inReplyTo if inReplyTo mail['User-Agent'] = "#{AppConfig.softwareName}/#{AppConfig.version}" mail['X-TaskJuggler'] = @appName if attachment mail.add_file ({ :filename => File.basename(attachment), :content => File.read(attachment) }) end #raise "Mail header problem" unless mail.errors.empty? rescue @emailFailure = true error("Email processing failed: #{$!}") end if @dryRun # For testing and debugging, we only print out the email. puts "-- Email Start #{'-' * 60}\n#{mail.to_s}-- Email End #{'-' * 62}" log('INFO', "Show email '#{subject}' to #{to}") else # Actually send out the email. begin mail.deliver rescue # We try to send out another email. If that fails again, we abort # without further attempts. if @emailFailure log('ERROR', "Email double fault: #{$!}") raise TjRuntimeError else @emailFailure = true error("Email transmission failed: #{$!}") end end log('INFO', "Sent email '#{subject}' to #{to}") end end def htmlMailBody(message) html = HTMLDocument.new head = html.generateHead("TaskJuggler Report - #{@name}", 'description' => 'TaskJuggler Report', 'keywords' => 'taskjuggler, project, management') auxSrcDir = AppConfig.dataDirs('data/css')[0] cssFileName = (auxSrcDir ? auxSrcDir + '/tjreport.css' : '') # Raise an error if we haven't found the data directory if auxSrcDir.nil? || !File.exist?(cssFileName) dataDirError(cssFileName) end cssFile = IO.read(cssFileName) if cssFile.empty? raise TjException.new, <<"EOT" Cannot read '#{cssFileName}'. Make sure the file is not empty and you have read access permission. EOT end head << XMLElement.new('meta', 'http-equiv' => 'Content-Style-Type', 'content' => 'text/css; charset=utf-8') head << (style = XMLElement.new('style', 'type' => 'text/css')) style << XMLBlob.new("\n" + cssFile) html.html << (body = XMLElement.new('body')) body << message.to_html html.to_s end end end TaskJuggler-3.8.1/lib/taskjuggler/SheetReceiver.rb000066400000000000000000000304201473026623400221250ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = SheetReceiver.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'digest/md5' require 'mail' require 'yaml' require 'taskjuggler/apps/Tj3Client' require 'taskjuggler/StdIoWrapper' require 'taskjuggler/SheetHandlerBase' require 'taskjuggler/RichText' class TaskJuggler class SheetReceiver < SheetHandlerBase include StdIoWrapper def initialize(appName, type) super(appName) @sheetType = type # The following settings must be set by the deriving class. # Sheet type specific option for tj3client @tj3clientOption = nil # Base directory to store received sheets @sheetDir = nil # Base directory where to find the resource file. @templateDir = nil # Directory to store the failed emails. @failedMailsDir = nil # Directory to store the failed sheets @failedSheetsDir = nil # File that holds the acceptable signatures. @signatureFile = nil # The log file @logFile = nil # The subject of the confirmation email @emailSubject = nil # Regular expressions to identify a sheet. @sheetHeader = nil # Regular expression to extract the sheet signature (date). @signatureFilter = nil # The email address of the submitter of the sheet. @submitter = nil # The resource ID of the submitter. @resourceId = nil # The stdout content from tj3client @report = nil # The stderr content from tj3client @warnings = nil # The extracted sheet text. @sheet = nil # Will indicate whether the sheet was attached or in mail body @sheetWasAttached = true # The end date of the reporting period. @date = nil # The id of the incomming message. @messageId = nil end # Read the sheet from $stdin in email format. Extract the sheet from the # attachments or body and check it. If ok, send back a summary, otherwise # the error message. # The actual check is done by a tj3 server process that is accessed via # tj3client. def processEmail setWorkingDir createDirectories begin # Read the RFC 822 compliant mail from STDIN. rawMail = $stdin.read rawMail = rawMail.forceUTF8Encoding mail = Mail.new(rawMail) rescue # In certain cases, Mail will fail to create the Mail object. Since we # don't have the email sender yet, we have to try to extract it # ourself. fromLine = nil rawMail.each_line do |line| unless fromLine matches = line.match('^From: .*') if matches fromLine = matches[0] break end end end # Try to extract the mail sender the dirty way so we can at least send # a response to the submitter. @submitter = fromLine[6..-1] if fromLine && fromLine.is_a?(String) error("Incoming mail could not be processed: #{$!}") end # Who sent this email? @submitter = mail.from.respond_to?('[]') ? mail.from[0] : mail.from # Getting the message ID. @messageId = mail.message_id || 'unknown' @idDigest = Digest::MD5.hexdigest(@messageId) info("Processing #{@sheetType} mail from #{@submitter} " + "with ID #{@messageId} (#{@idDigest})") # Store the mail in the failedMailsDir in case something goes wrong. File.open("#{@failedMailsDir}/#{@idDigest}", 'w') do |f| f.write(mail) end # First we search the attachments and then the body. mail.attachments.each do |attachment| # We are looking for an attached file with a .tji extension. fileName = attachment.filename next unless fileName && fileName[-4..-1] == '.tji' # Further inspect the attachment. If we could process it, we are done. return true if processSheet(attachment.body.decoded) end # None of the attachements worked, so let's try the mail body. @sheetWasAttached = false return true if processSheet(mail.body.decoded) error(<<"EOT" No #{@sheetType} sheet found in email. Please make sure the header syntax is correct and contained in a single line that starts at the begining of the line. If you had the #{@sheetType} sheet attached, the file name must have a '.tji' extension to be found. EOT ) end private # Isolate the actual syntax from _sheet_ and process it. def processSheet(sheet) begin @sheet = sheet.forceUTF8Encoding rescue error($!.message) end # If the sheet contains special cut markers, we extract only the content # within those markers. @sheet = cutOut(@sheet) # A valid sheet must have the poper header line. if @sheetHeader.match(@sheet) checkSignature(@sheet) # Extract the resource ID and the end date from the sheet. matches = @sheetHeader.match(@sheet) @resourceId, @date = matches[1..2] # Email answers will only go the email address on file! @submitter = getResourceEmail(@resourceId) info("Found #{@sheetWasAttached ? 'attached ' : ''}sheet for " + "#{@resourceId} dated #{@date}") # Ok, found. Now check the full sheet. if checkSheet(@sheet) # Everything is fine. Store it away. fileSheet(@sheet) # Remove the mail from the failedMailsDir File.delete("#{@failedMailsDir}/#{@idDigest}") info("Accepted sheet for #{@resourceId} dated #{@date}") return true end end end def checkSheet(sheet) res = nil begin # Save a copy of the sheet for debugging purposes. File.open("#{@failedSheetsDir}/#{@resourceId}-#{@date}.tji", 'w') do |f| f.write(sheet) end command = [ '--unsafe', '--silent', *@tj3clientOption.split(' '), @projectId, '.' ] # Send the report to the tj3client process via stdin. res = stdIoWrapper(sheet) do Tj3Client.new.main(command) end # Without errors, the incoming report is pretty printed and returned # in RichText format. @report = res.stdOut @warnings = res.stdErr rescue fatal("Cannot check #{@sheetType} sheet: #{$!}") end if res.returnValue == 0 File.delete("#{@failedSheetsDir}/#{@resourceId}-#{@date}.tji") return true end # The exit status was not 0. The stderr output should not be empty and # will contain error and warning messages. error(@warnings) end def fileSheet(sheet) # Create the appropriate directory structure if it doesn't exist. dir = "#{@sheetDir}/#{@date}" fileName = "#{dir}/#{@resourceId}_#{@date}.tji" newDir = false begin unless File.directory?(dir) Dir.mkdir(dir) addToScm('Adding new directory', dir) newDir = true end File.open(fileName, 'w') { |f| f.write(sheet) } addToScm("Adding/updating #{fileName}", fileName) rescue fatal("Cannot store #{@sheetType} sheet #{fileName}: #{$!}") return end # Create or update the file that includes all *.tji in the directory. generateInclusionFile(dir) if newDir # Add the new directory to the parent all.tji file. allFile = "#{@sheetDir}/all.tji" File.open(allFile, 'a') do |f| f.write("\ninclude '#{@date}/all.tji' { }") end addToScm('Adding new directory to all.tji', allFile) end text = <<"EOT" == Report from #{getResourceName} for the period ending #{@date} == EOT # Add warnings if we had any. unless @warnings.empty? text += <<"EOT" ---- Your report does contain some issues that you may want to fix or address with your manager or project manager: #{@warnings} ---- EOT end # Append the pretty printed version of the submitted sheet. text += @report # Send out the email. sendRichTextEmail(@submitter, sprintf(@emailSubject, getResourceName, @date), text, nil, nil, @messageId) true end # Generate or update a file the contains 'include' statements for all the # .tji files in the provided directory. The generated file will be in this # directory as well. def generateInclusionFile(dir) pwd = Dir.pwd begin Dir.chdir(dir) File.open('all.tji', 'w') do |file| Dir.glob('*.tji').each do |tji| file.puts("include '#{tji}' { }") unless tji == 'all.tji' end end rescue error("Can't create inclusion file: #{$!}") ensure Dir.chdir(pwd) end # Report the change to the SCM handler. addToScm('Adding/updating summary include file.', "#{dir}/all.tji") end def checkSignature(sheet) if matches = @signatureFilter.match(sheet) interval = matches[1] else fatal("No #{@sheetType}sheet header found") end acceptedSignatures = [] if File.exist?(@signatureFile) File.open(@signatureFile, 'r') do |file| acceptedSignatures = file.readlines end acceptedSignatures.map! { |s| s.chomp } acceptedSignatures.delete_if { |s| s.chomp.empty? } else error("#{@signatureFile} does not exist yet.") end unless acceptedSignatures.include?(interval) error(<<"EOT" The reporting period #{interval} was not accepted! Either you have modified the sheet header, you are submitting the sheet too late or too early. EOT ) end end def createDirectories [ @sheetDir, @failedMailsDir, @failedSheetsDir ].each do |dir| unless File.directory?(dir) info("Creating directory #{dir}") Dir.mkdir(dir) end end end def error(message) $stderr.puts message if @outputLevel >= 1 log('ERROR', "#{message}") if @logLevel >= 1 # Append the submitted sheet for further tries. We may run into encoding # errors here. In this case we send the answer without the incoming time # sheet. begin message += "\n" + @sheet if @sheet && !@sheetWasAttached rescue end sendEmail(@submitter, "Your #{@sheetType} sheet submission failed!", message) raise TjRuntimeError end def fatal(message) log('FATAL', "#{message}") # Append the submitted sheet for further tries. message += "\n" + @sheet if @sheet sendEmail(@submitter, 'Temporary server error', <<"EOT" We are sorry! The #{@sheetType} sheet server detected a configuration problem and is temporarily out of service. The administrator has been notified and will try to rectify the situation as soon as possible. Please re-submit your #{@sheetType} sheet later! EOT ) raise TjRuntimeError end # Load tye resources.yml YAML file into the @resourceList variable. # The format is Array with one entry per resource. The entry is an Array # with 3 fields: ID, name and email. All fields are String objects. def getResourceList fatal('@date not set') unless @date fileName = "#{@templateDir}/#{@date}/resources.yml" begin @resourceList = YAML.load(File.read(fileName)) info("#{@resourceList.length} resources loaded") rescue error("Cannot read resource file #{fileName}: #{$!}") end @resourceList end def getResourceEmail(id = @resourceId) getResourceList unless @resourceList @resourceList.each do |resource| return resource[2] if resource[0] == id end error("Resource ID '#{id}' not found in list") end def getResourceName(id = @resourceId) getResourceList unless @resourceList @resourceList.each do |resource| return resource[1] if resource[0] == id end error("Resource ID '#{id}' not found in list") end end end TaskJuggler-3.8.1/lib/taskjuggler/SheetSender.rb000066400000000000000000000213571473026623400216120ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = SheetSender.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'mail' require 'yaml' require 'taskjuggler/StdIoWrapper' require 'taskjuggler/SheetHandlerBase' require 'taskjuggler/reports/CSVFile' require 'taskjuggler/apps/Tj3Client' class TaskJuggler # A base class for sheet senders. class SheetSender < SheetHandlerBase attr_accessor :force, :intervalDuration include StdIoWrapper def initialize(appName, type) super(appName) @sheetType = type # The following settings must be provided by the deriving class. # This is a LogicalExpression string that controls what resources should # not be getting a report sheet template. @hideResource = nil # This file contains the signature (date or interval) that the # SheetReceiver will accept as a valid signature. @signatureFile = nil # The base directory of the sheet templates. @templateDir = nil # When true, existing templates will be regenerated and send out again. # Otherwise the existing template will not be send out again. @force = false @signatureFilter = nil # The subject of the template email. @mailSubject = nil # The into text of the template email. @introText = nil # The end date of the reported interval. @date = Time.new.strftime('%Y-%m-%d') # Determines the length of the reported interval. @intervalDuration = '1w' # We need this to determine if we already sent out a report. @timeStamp = Time.new end # Send out report templates to a list of project resources. The resources # are selected by the @hideResource filter expression and can be further # limited with a list of resource IDs passed by _resourceList_. def sendTemplates(resourceList) setWorkingDir createDirectories resources = genResourceList(resourceList) genTemplates(resources) sendReportTemplates(resources) end private def createDirectories unless File.directory?(@templateDir) warning("Creating directory #{@templateDir}") Dir.mkdir(@templateDir) end @templateDir += "/#{@date}" unless File.directory?(@templateDir) Dir.mkdir(@templateDir) end end def genResourceList(resourceList) list = [] info('Retrieving resource list...') # Create a TJP report definition for a CSV report that contains the id, # name, email, effort and free work for each resource that is not hidden # by @hideResource. reportDef = <<"EOF" resourcereport rl_21497214 '.' { formats csv columns id, name, email, effort, freework, efficiency hideresource #{@hideResource} sortresources id.up loadunit days period %{#{@date} - 1w} +1w } EOF report = generateReport('rl_21497214', reportDef) # Parse the CSV report into an Array of Arrays csv = CSVFile.new.parse(report) # Get rid of the column title line csv.delete_at(0) # Process the CSV report line by line csv.each do |id, name, email, effort, free, efficiency| if email.nil? || email.empty? error("Resource '#{id}' must have a valid email address") end # Ignore resources that are on leave for the whole period. if effort == 0.0 && free == 0.0 && efficiency != 0.0 info("Resource '#{id}' was on leave the whole period") next end list << [ id, name, email, effort, free ] end # Save the resource list to a file. We'll need it in the receiver again. begin fileName = @templateDir + '/resources.yml' File.open(fileName, 'w') do |file| YAML.dump(list, file) end rescue error("Saving of #{fileName} failed: #{$!}") end unless resourceList.empty? # When the user specified resource list is empty, we generate templates # for all users that don't match the @hideResource filter. Otherwise we # only generate templates for those in the list and that are not hidden # by the filter. list.delete_if { |item| !resourceList.include?(item[0]) } end error('genResourceList: list is empty') if list.empty? info("#{list.length} resources found") list end def genTemplates(resources) firstTemplateFile = nil resources.each do |resInfo| res = resInfo[0] info("Generating template for #{res}...") reportId = "sheet_template_#{res}" templateFile = "#{@templateDir}/#{res}_#{@date}" # We use the first template file to get the sheet interval. firstTemplateFile = templateFile + '.tji' unless firstTemplateFile # Don't re-generate already existing templates unless we are in force # mode. We probably have sent them out earlier with a manual trigger. if !@force && File.exist?(templateFile + '.tji') info("Skipping already existing #{templateFile}.tji.") next end reportDef = <<"EOT" #{@sheetType}sheetreport #{reportId} \"#{templateFile}\" { hideresource ~(plan.id = \"#{res}\") period %{#{@date} - #{@intervalDuration}} +#{@intervalDuration} sorttasks id.up } EOT generateReport(reportId, reportDef) end unless firstTemplateFile error("No #{@sheetType} sheet templates found in #{@templateDir}") end enableSignatureForReporting(firstTemplateFile) end def sendReportTemplates(resources) resources.each do |id, name, email| attachment = "#{@templateDir}/#{id}_#{@date}.tji" unless File.exist?(attachment) error("sendReportTemplates: " + "#{@sheetType} sheet #{attachment} for #{name} not found") end # Don't send out old templates again. @timeStamp has a higher # resolution. We add 1s to avoid truncation errors. if (File.mtime(attachment) + 1) < @timeStamp info("Old template #{attachment} found. Not sending it out.") next end message = " Hello #{name}!\n\n#{@introText}" + File.read(attachment) sendEmail(email, sprintf(@mailSubject, @date), message, attachment) end end def enableSignatureForReporting(templateFile) signature = nil # That's a pretty bad hack to make reasonably certain that the tj3 server # process has put the complete file into the file system. i = 0 begin if File.exist?(templateFile) File.open(templateFile, 'r') do |file| while (line = file.gets) if matches = @signatureFilter.match(line) signature = matches[1] end end end end i += 1 # If the file doesn't exist yet or the cannot yet be read, wait for # 300ms. We try this 100 times. sleep(0.3) unless signature end while signature.nil? && i < 100 unless signature error("enableSignatureForReporting: Cannot find signature in file " + "#{templateFile}") end acceptedSignatures = [] if File.exist?(@signatureFile) File.open(@signatureFile, 'r') do |file| acceptedSignatures = file.readlines end acceptedSignatures.map! { |s| s.chomp } acceptedSignatures.delete_if { |s| s.chomp.empty? } else info("#{@signatureFile} does not exist yet.") end unless acceptedSignatures.include?(signature) # Add the new signature info("Adding #{signature} to #{@signatureFile}") acceptedSignatures << signature # And write back the adapted file. File.open(@signatureFile, 'w') do |file| acceptedSignatures.each do |iv| file.write("#{iv}\n") end end else info("Signature #{signature} is already listed in #{@signatureFile}") end end def generateReport(id, reportDef) out = '' err = '' res = nil begin command = [ '--unsafe', '--silent', 'report', @projectId, id, '=', '.' ] # Send the report definition to the tj3client process via stdin. res = stdIoWrapper(reportDef) do Tj3Client.new.main(command) end out = res.stdOut err = res.stdErr if res.returnValue != 0 error("generateReport: #{err}") end rescue error("generateReport: Report generation failed: #{$!}") end out end end end TaskJuggler-3.8.1/lib/taskjuggler/Shift.rb000066400000000000000000000027151473026623400204530ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = Shift.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2019 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/PropertyTreeNode' require 'taskjuggler/ShiftScenario' class TaskJuggler # A shift is a definition of working hours for each day of the week. It may # also contain a list of intervals that define off-duty periods or leaves. class Shift < PropertyTreeNode def initialize(project, id, name, parent) super(project.shifts, id, name, parent) project.addShift(self) @data = Array.new(@project.scenarioCount, nil) @project.scenarioCount.times do |i| ShiftScenario.new(self, i, @scenarioAttributes[i]) end end # Many Shift functions are scenario specific. These functions are # provided by the class ShiftScenario. In case we can't find a # function called for the Shift class we try to find it in # ShiftScenario. def method_missing(func, scenarioIdx = 0, *args) @data[scenarioIdx].method(func).call(*args) end # Return a reference to the _scenarioIdx_-th scenario. def scenario(scenarioIdx) return @data[scenarioIdx] end end end TaskJuggler-3.8.1/lib/taskjuggler/ShiftAssignments.rb000066400000000000000000000232241473026623400226650ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = ShiftAssignments.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'monitor' require 'taskjuggler/Scoreboard' class TaskJuggler # A ShiftAssignment associate a specific defined shift with a time interval # where the shift should be active. class ShiftAssignment attr_reader :shiftScenario attr_accessor :interval def initialize(shiftScenario, interval) @shiftScenario = shiftScenario @interval = interval end def hashKey return "#{@shiftScenario.object_id}|#{@interval.start}|#{@interval.end}" end # Return a deep copy of self. def copy ShiftAssignment.new(@shiftScenario, TimeInterval.new(@interval)) end # Return true if the _iv_ interval overlaps with the assignment interval. def overlaps?(iv) @interval.overlaps?(iv) end # Returns true if the shift is active and requests to replace global # leave settings. def replace?(date) @interval.start <= date && date < @interval.end && @shiftScenario.replace? end # Check if date is withing the assignment period. def assigned?(date) @interval.start <= date && date < @interval.end end # Returns true if the shift has working hours defined for the _date_. def onShift?(date) @shiftScenario.onShift?(date) end # Returns true if the shift has a leave defined for the _date_. def onLeave?(date) @shiftScenario.onLeave?(date) end # Primarily used for debugging def to_s "#{@shiftScenario.property.id} #{interval}" end end # This class manages a list of ShiftAssignment elements. The intervals of the # assignments must not overlap. # # Since it is fairly costly to determine the onShift and onLeave values # for a given date we use a scoreboard to cache all computed values. # Changes to the assigment set invalidate the cache again. # # To optimize memory usage and computation time the Scoreboard objects for # similar ShiftAssignments are shared. # # Scoreboard may be nil or a bit vector encoded as an Integer # nil: Value has not been determined yet. # Bit 0: 0: No assignment # 1: Has assignement # Bit 1: 0: Work time (as defined by working hours) # 1: No work time (as defined by working hours) # Bit 2 - 5: 0: No holiday or leave time # 1: Public holiday (holiday) # 2: Annual leave # 3: Special leave # 4: Sick leave # 5: unpaid leave # 6: blocked for other projects # 7 - 15: Reserved # Bit 6 - 7: Reserved # Bit 8: 0: No global override # 1: Override global setting class ShiftAssignments < Monitor include ObjectSpace attr_accessor :project attr_reader :assignments # This class is sharing the Scoreboard instances for ShiftAssignments that # have identical assignment data. This class variable holds a Hash with # records for each unique Scoreboard. A record is an array of references # to the owning ShiftAssignments objects and a reference to the Scoreboard # object. @@scoreboards = {} def initialize(sa = nil) define_finalizer(self, self.class.method(:deleteScoreboard).to_proc) # An Array of ShiftAssignment objects. @assignments = [] # A String that uniquely identifies the content of this ShiftAssignment # object. @hashKey = nil if sa # A ShiftAssignments object was passed to the contructor. We create a # deep copy of it. @project = sa.project sa.assignments.each do |assignment| @assignments << assignment.copy end # Create a new ScoreBoard or share one with a ShiftAssignments object # that has the same set of shift assignments. @scoreboard = newScoreboard else @project = nil @scoreboard = nil end end # Add a new assignment to the list. In case there was no overlap the # function returns true. Otherwise false. def addAssignment(shiftAssignment) # Make sure we don't insert overlapping assignments. return false if overlaps?(shiftAssignment.interval) @assignments << shiftAssignment @scoreboard = newScoreboard true end # This function returns the entry in the scoreboard that corresponds to # _idx_. If the slot has not yet been determined, it's calculated first. def getSbSlot(idx) # Check if we have a value already for this slot. return @scoreboard[idx] unless @scoreboard[idx].nil? date = @scoreboard.idxToDate(idx) # If not, compute it. @assignments.each do |sa| next unless sa.assigned?(date) # Mark the slot as 'assigned'. Meaning, the rest of the bits are valid # for this time slot. @scoreboard[idx] = 1 # Set bit 1 if the shift is not active @scoreboard[idx] |= 1 << 1 unless sa.onShift?(date) # Set bits 2 - 5 to 1 if it's a leave slot. @scoreboard[idx] |= 1 << 3 if sa.onLeave?(date) # Set the 8th bit if the shift replaces global leaves. @scoreboard[idx] |= 1 << 8 if sa.replace?(date) return @scoreboard[idx] end # The slot is not covered by any assignment. @scoreboard[idx] = 0 end # Returns true if any of the defined shift periods overlaps with the date or # interval specified by _idx_. def assigned?(idx) (getSbSlot(idx) & 1) == 1 end # Returns true if any of the defined shift periods contains the date # specified by the scoreboard index _idx_ and the shift has working hours # defined for that date. def onShift?(idx) (getSbSlot(idx) & (1 << 1)) == 0 end # Returns true if any of the defined shift periods contains the date # specified by the scoreboard index _idx_ and the shift has a leave # defined or all off hours defined for that date. def timeOff?(idx) (getSbSlot(idx) & 0x3E) != 0 end # Returns true if any of the defined shift periods contains the date # specified by the scoreboard index _idx_ and if the shift has a leave # defined for the date. def onLeave?(idx) (getSbSlot(idx) & 0x3C) != 0 end # Return a list of intervals that lay within _iv_ and are at least # minDuration long and contain no working time. def collectTimeOffIntervals(iv, minDuration) @scoreboard.collectIntervals(iv, minDuration) do |val| (val & 0x3E) != 0 end end def ShiftAssignments.scoreboards @@scoreboards end def ShiftAssignments.sbClear @@scoreboards = {} end # This function is primarily used for debugging purposes. def to_s return '' if @assignments.empty? out = "shifts " first = true @assignments.each do |sa| if first first = false else out += ', ' end out += sa.to_s end out end def hashKey @hashKey if @hashKey @hashKey = "#{@project.object_id}|" @assignments.sort! { |a, b| a.interval.start <=> b.interval.start } @assignments.each { |a| @hashKey += a.hashKey + '||' } @hashKey end private # This function either returns a new Scoreboard or a reference to an # existing one in case we already have one for the same assigment patterns. def newScoreboard if (record = @@scoreboards[hashKey]) # If we already have a Scoreboard object for the hashKey of this # ShiftAssignments object, we can re-use this. We just need to # register the object as a user of it. record[0] << object_id # Return the re-used Scoreboard object. return record[1] end # We have not found a matching scoreboard, so we have to create a new one. newSb = Scoreboard.new(@project['start'], @project['end'], @project['scheduleGranularity']) # Create a new record for it and register the ShiftAssignments object as # first user. Add the record to the @@scoreboards list. @@scoreboards[hashKey] = [ [ object_id ], newSb ] # Append the new record to the list. return newSb end # This function is called whenever a ShiftAssignments object gets destroyed # by the GC. def ShiftAssignments.deleteScoreboard(objId) # Attention: Due to the way this class is called, there will be no # visible exceptions here. All runtime errors will go unnoticed! # # We'll search the @@scoreboards for an entry that holds a reference to # the deleted ShiftAssignments object. If it's the last in the record, # we delete the whole record. If not, we'll just remove the reference # form the record. @@scoreboards.each_value do |record| if record[0].include?(objId) # Remove the ShiftAssignments object as user of this Scoreboard # object. record[0].delete(objId) # We've found what we were looking for. break end end # Delete all entries which have empty reference lists. @@scoreboards.delete_if { |key, record| record[0].empty? } end # Returns true if the interval overlaps with any of the assignment periods. def overlaps?(iv) @assignments.each do |sa| return true if sa.overlaps?(iv) end false end end end TaskJuggler-3.8.1/lib/taskjuggler/ShiftScenario.rb000066400000000000000000000021271473026623400221340ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = ShiftScenario.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/ScenarioData' class TaskJuggler # This class handles the scenario specific features of a Shift object. class ShiftScenario < ScenarioData def initialize(resource, scenarioIdx, attributes) super end # Returns true if the shift has working time defined for the _date_. def onShift?(date) a('workinghours').onShift?(date) end def replace? a('replace') end # Returns true if the shift has a vacation defined for the _date_. def onLeave?(date) a('leaves').each do |leave| if leave.interval.contains?(date) return true end end false end end end TaskJuggler-3.8.1/lib/taskjuggler/SimpleQueryExpander.rb000066400000000000000000000041471473026623400233450ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = SimpleQueryExpander.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'strscan' require 'taskjuggler/MessageHandler' class TaskJuggler # The SimpleQueryExpander class is used to replace embedded attribute # queries in a string with the value of the attribute. The embedded queries # must have the form <-name-> where name is the name of the attribute. The # Query class is used to determine the value of the attribute within the # context of the query. class SimpleQueryExpander include MessageHandler # _inputStr_ is the String with the embedded queries. _query_ is the Query # with that provides the evaluation context. _messageHandle_ is a # MessageHandler that will be used for error reporting. _sourceFileInfo_ # is a SourceFileInfo reference used for error reporting. def initialize(inputStr, query, sourceFileInfo) @inputStr = inputStr @query = query.dup @sourceFileInfo = sourceFileInfo end def expand # Create a copy of the input string since we will modify it. str = @inputStr.dup # The scenario name is not an attribute that can be queried. We need to # handle this separately if @query.scenarioIdx str.gsub!(/<-scenario->/, @query.project.scenario(@query.scenarioIdx).id) end # Replace all occurences of <-name->. str.gsub!(/<-[a-zA-Z][_a-zA-Z]*->/) do |match| attribute = match[2..-3] @query.attributeId = attribute @query.process if @query.ok @query.to_s else # The query failed. We report an error. error('sqe_expand_failed', "Unknown attribute #{attribute}", @sourceFileInfo) end end str end end end TaskJuggler-3.8.1/lib/taskjuggler/StatusSheetReceiver.rb000066400000000000000000000030371473026623400233350ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = StatusSheetReceiver.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/SheetReceiver' class TaskJuggler # This class specializes SheetReceiver to process status sheets. class StatusSheetReceiver < SheetReceiver def initialize(appName) super(appName, 'status') @tj3clientOption = 'check-ss' # File name and directory settings. @sheetDir = 'StatusSheets' @templateDir = 'StatusSheetTemplates' @failedMailsDir = "#{@sheetDir}/FailedMails" @failedSheetsDir = "#{@sheetDir}/FailedSheets" # This file contains the time intervals that the StatusSheetReceiver will # accept as a valid interval. @signatureFile = "#{@templateDir}/acceptable_intervals" # The log file @logFile = 'statussheets.log' # Regular expression to identify status sheets. @sheetHeader = /^[ ]*statussheet\s([a-zA-Z_][a-zA-Z0-9_]*)\s[0-9\-:+]*\s-\s([0-9]*-[0-9]*-[0-9]*)/ # Regular expression to extract the sheet signature (time period). @signatureFilter = /^[ ]*statussheet\s[a-zA-Z_][a-zA-Z0-9_]*\s([0-9:\-+]*\s-\s[0-9:\-+]*)/ @emailSubject = "Status report from %s for %s" end end end TaskJuggler-3.8.1/lib/taskjuggler/StatusSheetSender.rb000066400000000000000000000110121473026623400230010ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = StatusSheetSender.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/SheetSender' class TaskJuggler # The StatusSheetSender class generates status sheet templates for the current # week and sends them out to the managers. For this to work, the resources # must provide the 'Email' custom attribute with their email address. The # actual project data is accessed via tj3client on a tj3 server process. class StatusSheetSender < SheetSender attr_accessor :date, :hideResource def initialize(appName) super(appName, 'status') # This is a LogicalExpression string that controls what resources should # not be getting a status sheet. @hideResource = '0' # The base directory of the status sheet templates. @templateDir = 'StatusSheetTemplates' # The base directory of the received time sheets. @timeSheetDir = 'TimeSheets' # This file contains the time intervals that the StatusSheetReceiver will # accept as a valid interval. @signatureFile = "#{@templateDir}/acceptable_intervals" # The log file @logFile = 'statussheets.log' @signatureFilter = /^[ ]*statussheet\s[a-zA-Z_][a-zA-Z0-9_]*\s([0-9:\-+]*\s-\s[0-9:\-+]*)/ @introText = <<'EOT' Please find enclosed your weekly status report template. Please fill out the form and send it back to the sender of this email. You can either use the attached file or the body of the email. In case you send it in the body of the email, make sure it only contains the 'statussheet' syntax. It must be plain text, UTF-8 encoded and the status sheet header from 'statussheet' to the period end date must be in a single line that starts at the beginning of the line. EOT # tj3ts_summary generates a list of resources that have not submitted # their reports yet. If you want to generate the warning below, make # sure you run tj3ts_summary immediately before you sent the status sheet # templates. defaulters = defaulterList unless defaulters.empty? @introText += <<"EOT" =============================== W A R N I N G ============================== The following people have not submitted their report yet. The status reports for the work they have done is not included in this template! You can either manually add their status to the tasks or asked them to send their time sheet immediately and re-request this template. #{defaulters.join} EOT end @mailSubject = "Your weekly status report template for %s" end def defaulterList dirs = Dir.glob("#{@timeSheetDir}/????-??-??").sort tsDir = nil # The status sheet intervals and the time sheet intervals are not # identical. The status sheet interval can be smaller and is somewhat # later. But it always includes the end date of the corresponding time # sheet period. To get the file with the IDs of the resources that have # not submitted their report, we need to find the time sheet directory # that is within the status sheet period. repDate = Time.local(*@date.split('-')) dirs.each do |dir| dirDate = Time.local(*dir[-10..-1].split('-')) if dirDate < repDate tsDir = dir else break end end # Check if there is a time sheet directory. return [] unless tsDir missingFile = "#{tsDir}/missing-reports" # Check if it's got a missing-reports file. return [] if !File.exist?(missingFile) # The sheet could have been submitted after tj3ts_summary was run. We # ignore the entry if a time sheet file now exists. There is a race # condition here. The file may exist, but it may not yet be loaded for # the current project that is used to generate the status report. There # is a race condition here. The file may exist, but it may not yet be # loaded for the current project that is used to generate the status # report. list = File.readlines(missingFile) list.delete_if do |resource| tsDate = tsDir[-10..-1] File.exist?("#{tsDir}/#{resource.chomp}_#{tsDate}.tji") end # Return the content of the file. list end end end TaskJuggler-3.8.1/lib/taskjuggler/StdIoWrapper.rb000066400000000000000000000033431473026623400217570ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = StdIoWrapper.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # class TaskJuggler # This module provides just one method to run the passed block. It will # capture all content that will be send to $stdout and $stderr by the block. # I can also feed the String provided by _stdIn_ to $stdin of the block. module StdIoWrapper Results = Struct.new(:returnValue, :stdOut, :stdErr) def stdIoWrapper(stdIn = nil) # Save the old $stdout and $stderr and replace them with StringIO # objects to capture the output. oldStdOut = $stdout oldStdErr = $stderr $stdout = (out = StringIO.new) $stderr = (err = StringIO.new) # If the caller provided a String to feed into $stdin, we replace that # as well. if stdIn oldStdIn = $stdin $stdin = StringIO.new(stdIn) end begin # Call the block with the hooked up IOs. res = yield rescue RuntimeError # Blocks that are called this way usually return 0 on success and 1 on # errors. res = 1 ensure # Restore the stdio channels no matter what. $stdout = oldStdOut $stderr = oldStdErr $stdin = oldStdIn if stdIn end # Return the return value of the block and the $stdout and $stderr # captures. Results.new(res, out.string, err.string) end end end TaskJuggler-3.8.1/lib/taskjuggler/SyntaxReference.rb000066400000000000000000000254551473026623400225110ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = SyntaxReference.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/KeywordDocumentation' require 'taskjuggler/ProjectFileParser' require 'taskjuggler/HTMLDocument' require 'taskjuggler/MessageHandler' class TaskJuggler # This class can traverse the syntax rules of the ProjectFileParser and # extract all documented keywords including their arguments and relations. # All this work in done in the contructor. The documentation can then be # generated for all found keyword or just a single one. Currently plain text # output as well as HTML files are supported. class SyntaxReference attr_reader :keywords # The constructor is the most important function of this class. It creates # a parser object and then traverses all rules and extracts the documented # patterns. In a second pass the extracted KeywordDocumentation objects # are then cross referenced to capture their relationships. _manual_ is an # optional reference to the UserManual object that uses this # SyntaxReference. def initialize(manual = nil, ignoreOld = false) @manual = manual @parser = ProjectFileParser.new @parser.updateParserTables # This hash stores all documented keywords using the keyword as # index. @keywords = {} @parser.rules.each_value do |rule| rule.patterns.each do |pattern| # Only patterns that are documented are of interest. next if pattern.doc.nil? # Ignore deprecated and removed keywords if requested next if ignoreOld && [ :deprecated, :removed ].include?(pattern.supportLevel) # Make sure each keyword is unique. if @keywords.include?(pattern.keyword) raise "Multiple patterns have the same keyword #{pattern.keyword}" end argDocs = [] # Create a new KeywordDocumentation object and fill-in all extracted # values. kwd = KeywordDocumentation.new(rule, pattern, pattern.to_syntax(argDocs, @parser.rules), argDocs, optionalAttributes(pattern, {}), @manual) @keywords[pattern.keyword] = kwd end end # Make sure all references to other keywords are present. @keywords.each_value do |kwd| kwd.crossReference(@keywords, @parser.rules) end # Figure out whether the keyword describes an inheritable attribute or # not. @keywords.each_value do |kwd| kwd.computeInheritance end end # Return a sorted Array with all keywords (as String objects). def all sorted = @keywords.keys.sort # Register the neighbours with each keyword so we can use this info in # navigation bars. pred = nil sorted.each do |kwd| keyword = @keywords[kwd] pred.successor = keyword if pred keyword.predecessor = pred pred = keyword end end # Generate entries for a TableOfContents for each of the keywords. The # entries are appended to the TableOfContents _toc_. _sectionPrefix_ is the # prefix that is used for the chapter numbers. In case we have 20 keywords # and _sectionPrefix_ is 'A', the keywords will be enumerated 'A.1' to # 'A.20'. def tableOfContents(toc, sectionPrefix) keywords = all # Set the chapter name to 'Syntax Reference' with a link to the first # keyword. toc.addEntry(TOCEntry.new(sectionPrefix, 'Syntax Reference', keywords[0])) i = 1 keywords.each do |keyword| title = @keywords[keyword].title toc.addEntry(TOCEntry.new("#{sectionPrefix}.#{i}", title, keyword)) i += 1 end end def internalReferences references = {} @keywords.each_value do |keyword| (refs = keyword.references.uniq).empty? || references[keyword.keyword] = refs end references end # Generate a documentation for the keyword or an error message. The result # is a multi-line plain text String for known keywords. In case of an error # the result is empty but an error message will be send to $stderr. def to_s(keyword) if checkKeyword(keyword) @keywords[keyword].to_s else '' end end # Generate a documentation for the keyword or an error message. The result # is a XML String for known keywords. In case of an error the result is # empty but an error message will be send to $stderr. def generateHTMLreference(directory, keyword) if checkKeyword(keyword) @keywords[keyword].generateHTML(directory) else '' end end # Generate 2 files named navbar.html and alphabet.html. They are used to # support navigating through the syntax reference. def generateHTMLnavbar(directory, keywords) html = HTMLDocument.new head = html.generateHead('TaskJuggler Syntax Reference Navigator') head << XMLElement.new('base', 'target' => 'display') html.html << (body = XMLElement.new('body')) body << XMLNamedText.new('Table Of Contents', 'a', 'href' => 'toc.html') body << XMLElement.new('br', {}, true) normalizedKeywords = {} keywords.each do |keyword| normalizedKeywords[@keywords[keyword].title] = keyword end letter = nil letters = [] normalizedKeywords.keys.sort!.each do |normalized| if normalized[0, 1] != letter letter = normalized[0, 1] letters << letter body << (h = XMLElement.new('h3')) h << XMLNamedText.new(letter.upcase, 'a', 'name' => letter) end keyword = normalizedKeywords[normalized] body << XMLNamedText.new("#{normalized}", 'a', 'href' => "#{keyword}.html") body << XMLElement.new('br', {}, true) end html.write(directory + 'navbar.html') html = HTMLDocument.new head = html.generateHead('TaskJuggler Syntax Reference Navigator') head << XMLElement.new('base', 'target' => 'navigator') html.html << (body = XMLElement.new('body')) body << (divf = XMLElement.new('div')) divf << (form = XMLElement.new( 'form', 'action' => 'http://www.google.com/search', 'method' => "get", 'target' => '_blank', 'style' => 'margin:0')) form << XMLElement.new('input', 'type' => 'text', 'value' => '', 'maxlength' => '255', 'size' => '25', 'name' => 'q') form << XMLElement.new('input', 'type' => 'submit', 'value' => 'Search') form << XMLElement.new('input', 'type' => 'hidden', 'value' => 'taskjuggler.org/manual', 'name' => 'sitesearch') body << (h3 = XMLElement.new('h3')) letters.each do |l| h3 << XMLNamedText.new(l.upcase, 'a', 'href' => "navbar.html##{l}") end html.write(directory + 'alphabet.html') end private # Find optional attributes and return them hashed by the defining pattern. def optionalAttributes(pattern, stack) # If we hit an endless recursion we won't find any attributes. So we push # each pattern we process on the 'stack'. If we hit it again, we just # return an empty hash. return {} if stack[pattern] # If we hit a pattern that is documented, we ignore it. return {} if !stack.empty? && pattern.doc # Push pattern onto 'stack'. stack[pattern] = true if pattern[0][1] == '{' && pattern[2][1] == '}' # We have found an optional attribute pattern! return attributes(pattern[1], false) end # If a token of the pattern is a reference, we recursively # follow the reference to the next pattern. pattern.each do |type, name| if type == :reference rule = @parser.rules[name] # Rules with multiple patterns won't lead to attributes. next if rule.patterns.length > 1 attrs = optionalAttributes(rule.patterns[0], stack) return attrs unless attrs.empty? end end {} end # For the rule referenced by token all patterns are collected that define # the terminal token of each first token of each pattern of the specified # rule. The patterns are returned as a hash. For each pattern the hashed # boolean value specifies whether the attribute is scenario specific or not. def attributes(token, scenarioSpecific) raise "Token #{token} must reference a rule" if token[0] != :reference token = token[1] # Find the matching rule. rule = @parser.rules[token] attrs = {} # Now we look at the first token of each pattern. rule.patterns.each do |pattern| if pattern[0][0] == :literal # If it's a terminal symbol, we found what we are looking for. We add # it to the attrs hash and mark it as non scenario specific. attrs[pattern] = scenarioSpecific elsif pattern[0][0] == :reference && pattern[0][1] == :scenarioIdCol # A reference to the !scenarioId rule marks the next token of the # pattern as a reference to a rule with all scenario specific # attributes. attrs.merge!(attributes(pattern[1], true)) elsif pattern[0][0] == :reference # In case we have a reference to another rule, we just follow the # reference. If the pattern is documented we don't have to follow the # reference. We can use the pattern instead. if pattern.doc.nil? attrs.merge!(attributes(pattern[0], scenarioSpecific)) else attrs[pattern] = scenarioSpecific end else raise "Hit unknown token #{token}" end end attrs end def checkKeyword(keyword) if keyword.nil? || @keywords[keyword].nil? unless keyword.nil? $stderr.puts "ERROR: #{keyword} is not a known keyword.\n\n" end # Create list of top-level keywords. kwdStr = '' @keywords.each_value do |kwd| if kwd.contexts.empty? || (kwd.contexts.length == 1 && kwd.contexts[0] == kwd) kwdStr += ', ' unless kwdStr.empty? kwdStr += kwd.keyword end end $stderr.puts "Try one of the following keywords as argument to this " + "program:\n" $stderr.puts "#{kwdStr}" return false end true end end end TaskJuggler-3.8.1/lib/taskjuggler/TableColumnDefinition.rb000066400000000000000000000075071473026623400236200ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = TableColumnDefinition.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # class TaskJuggler # A CellSettingPattern is used to store alternative settings for # ReportTableCell settings. These could be the cell text, the tooltip or a # color setting. The user can provide multiple options and the # LogicalExpression is used to select the pattern for a given cell. class CellSettingPattern attr_reader :setting, :logExpr def initialize(setting, logExpr) @setting = setting @logExpr = logExpr end end # The CellSettingPatternList holds a list of possible test pattern for a cell # or tooltip. The first entry who's LogicalExpression matches is used. class CellSettingPatternList def initialize @patterns = [] end # Add a new pattern to the list. def addPattern(pattern) @patterns << pattern end # Get the RichText that matches the _property_ and _scopeProperty_. def getPattern(query) @patterns.each do |pattern| if pattern.logExpr.eval(query) return pattern.setting end end nil end end # This class holds the definition of a column of a report. This is the user # specified data that is later used to generate the actual ReportTableColumn. # The column is uniquely identified by an ID. class TableColumnDefinition attr_reader :id, :cellText, :tooltip, :hAlign, :cellColor, :fontColor attr_accessor :title, :start, :end, :scale, :listItem, :listType, :width, :content, :column, :timeformat1, :timeformat2 def initialize(id, title) # The column ID. It must be unique within the report. @id = id # An alternative title for the column header. @title = title # An alternative start date for columns with time-variant values. @start = nil # An alternative end date for columns with time-variant values. @end = nil # For regular columns (non-calendar and non-chart) the user can override # the actual cell content. @cellText = CellSettingPatternList.new # The content attribute is only used for calendar columns. It specifies # what content should be displayed in the calendar columns. @content = 'load' # Horizontal alignment of the cell content. @hAlign = CellSettingPatternList.new # An alternative content for the tooltip message. It should be a # RichText object. @tooltip = CellSettingPatternList.new # An alternative background color for the cell. The color setting is # stored as "#RGB" or "#RRGGBB" String. @cellColor = CellSettingPatternList.new # An alternative font color for the cell. The format is equivalent to # the @cellColor setting. @fontColor = CellSettingPatternList.new # Specifies a RichText pattern to be used to generate the text of the # individual list items. @listItem = nil # Specifies whether list items are comma separated, bullet or numbered # list. @listType = nil # The scale attribute is only used for Gantt chart columns. It specifies # the minimum resolution of the chart. @scale = 'week' # The width of columns. @width = nil # Format of the upper calendar header line @timeformat1 = nil # Format of the lower calendar header line @timeformat2 = nil # Reference to the ReportTableColumn object that was created based on this # definition. @column = nil end end end TaskJuggler-3.8.1/lib/taskjuggler/TableColumnSorter.rb000066400000000000000000000054141473026623400230010ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = TableColumnSorter.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # class TaskJuggler # This class can rearrange the columns of a table according to a new order # determined by an Array of table headers. The table is an Array of table # lines. Each line is another Array. The first line of the table is an Array # of the headers of the columns. class TableColumnSorter attr_reader :discontinuedColumns # Register a new table for rearranging. def initialize(table) @oldTable = table @discontinuedColumns = nil end # Rearrange the registered table. The old table won't be modified. The # method returns a new table (Array of Arrays). _newHeaders_ is an Array # that represents the new column headers. The columns that are not in the # new header will be the last columns of the new table. def sort(newHeaders) # Maps old index to new index. columnIdxMap = {} newHeaderIndex = newHeaders.length oldHeaders = @oldTable[0] discontinuedHeaders = [] oldHeaders.length.times do |i| if (ni = newHeaders.index(oldHeaders[i])) # This old column is still in the new header columnIdxMap[i] = ni else # This old column is no longer contained in the new header. We # append it at the end. columnIdxMap[i] = newHeaderIndex discontinuedHeaders << oldHeaders[i] newHeaderIndex += 1 end end # We construct a new table from scratch. All values from the old table # are copied over. columns in the new table that were not contained in # the old table will be filled with nil. newTable = [] @oldTable.length.times do |lineIdx| oldLine = @oldTable[lineIdx] if lineIdx == 0 # Insert the new headers. The discontinued ones will be added below. newTable[0] = newHeaders else # Add a line of nils to the new table. newTable[lineIdx] = Array.new(newHeaderIndex, nil) end # Copy the old column to the new position. columnIdxMap.each do |oldColIdx, newColIdx| newTable[lineIdx][newColIdx] = oldLine[oldColIdx] end end # Now we need to add the new column headers that were not in the old # headers. #newTable[0] += discontinuedHeaders @discontinuedColumns = discontinuedHeaders.length newTable end end end TaskJuggler-3.8.1/lib/taskjuggler/Task.rb000066400000000000000000000075031473026623400203000ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = Task.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/PropertyTreeNode' require 'taskjuggler/TaskScenario' class TaskJuggler class Task < PropertyTreeNode def initialize(project, id, name, parent) super(project.tasks, id, name, parent) project.addTask(self) @data = Array.new(@project.scenarioCount, nil) @project.scenarioCount.times do |i| TaskScenario.new(self, i, @scenarioAttributes[i]) end end def readyForScheduling?(scenarioIdx) @data[scenarioIdx].readyForScheduling? end private # Create a blog-style list of all alert messages that match the Query. def journalText(query, longVersion, recursive) # The components of the message are either UTF-8 text or RichText. For # the RichText components, we use the originally provided markup since # we compose the result as RichText markup first. rText = '' if recursive list = @project['journal'].entriesByTaskR(self, query.start, query.end, query.hideJournalEntry) else list = @project['journal'].entriesByTask(self, query.start, query.end, query.hideJournalEntry) end list.setSorting([ [ :alert, -1 ], [ :date, -1 ], [ :seqno, 1 ] ]) list.sort! list.each do |entry| tsRecord = entry.timeSheetRecord if entry.property.is_a?(Task) levelRecord = @project['alertLevels'][entry.alertLevel] if query.selfContained alertName = "[#{levelRecord.name}]" else alertName = "[[File:icons/flag-#{levelRecord.id}.png|" + "alt=[#{levelRecord.name}]|text-bottom]]" end rText += "== #{alertName} #{entry.headline} ==\n" + "''Reported on #{entry.date.to_s(query.timeFormat)}'' " if entry.author rText += "''by #{entry.author.name}''" end rText += "\n\n" unless entry.flags.empty? rText += "'''Flags:''' #{entry.flags.join(', ')}\n\n" end if tsRecord rText += "'''Work:''' #{tsRecord.actualWorkPercent.to_i}% " if tsRecord.remaining rText += "'''Remaining:''' #{tsRecord.actualRemaining}d " else rText += "'''End:''' " + "#{tsRecord.actualEnd.to_s(query.timeFormat)} " end rText += "\n\n" end end unless entry.headline.empty? rText += "'''#{entry.headline}'''\n\n" end if entry.summary rText += entry.summary.richText.inputText + "\n\n" end if longVersion && entry.details rText += entry.details.richText.inputText + "\n\n" end end # Don't generate a RichText object for an empty String. return if rText.empty? # Now convert the RichText markup String into RichTextIntermediate # format. unless (rti = RichText.new(rText, RTFHandlers.create(@project)). generateIntermediateFormat) warning('task_journal_text', 'Syntax error in journal text') return nil end # No section numbers, please! rti.sectionNumbers = false # We use a special class to allow CSS formating. rti.cssClass = 'tj_journal' query.rti = rti end end end TaskJuggler-3.8.1/lib/taskjuggler/TaskDependency.rb000066400000000000000000000022771473026623400223020ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = TaskDependency.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # class TaskJuggler class TaskDependency attr_accessor :onEnd, :gapDuration, :gapLength attr_reader :taskId, :task def initialize(taskId, onEnd) @taskId = taskId @task = nil # Specifies whether the dependency is relative to the start or the # end of the dependent task. @onEnd = onEnd # The gap duration is stored in seconds of calendar time. @gapDuration = 0 # The gap length is stored in number of scheduling slots. @gapLength = 0 end def ==(dep) @taskId == dep.taskId && @task == dep.task && @onEnd == dep.onEnd && @gapDuration == dep.gapDuration && @gapLength == dep.gapLength end def resolve(project) @task = project.task(@taskId) end end end TaskJuggler-3.8.1/lib/taskjuggler/TaskJuggler.rb000066400000000000000000000312651473026623400216220ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = TaskJuggler.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'drb' # Only needed during profiling. #require 'ruby-prof' require 'taskjuggler/Project' require 'taskjuggler/MessageHandler' require 'taskjuggler/Log' # The TaskJuggler class models the object that provides access to the # fundamental features of the TaskJuggler software. It can read project # files, schedule them and generate the reports. class TaskJuggler include MessageHandler attr_reader :project attr_accessor :maxCpuCores, :warnTsDeltas, :generateTraces # Create a new TaskJuggler object. _console_ is a boolean that determines # whether or not messages can be written to $stderr. def initialize @project = nil @parser = nil @maxCpuCores = 1 @warnTsDeltas = false @generateTraces = false TjTime.setTimeZone('UTC') end # Read in the files passed as file names in _files_, parse them and # construct a Project object. In case of success true is returned. # Otherwise false. def parse(files, keepParser = false) # Reset the MessageHandler to clear all errors. MessageHandlerInstance.instance.clear Log.enter('parser', 'Parsing files ...') master = true @project = nil #RubyProf.start @parser = ProjectFileParser.new files.each do |file| begin @parser.open(file, master) rescue TjException => msg if msg.message && !msg.message.empty? critical('parse', msg.message) end Log.exit('parser') return false end if master # The first file is considered the master file. if (@project = @parser.parse(:project)) == false Log.exit('parser') return false end master = false else # All other files. @parser.setGlobalMacros if @parser.parse(:propertiesFile) == false Log.exit('parser') return false end end @project.inputFiles << file @parser.close end #profile = RubyProf.stop #printer = RubyProf::GraphHtmlPrinter.new(profile) #File.open("profile.html", "w") do |file| # printer.print(file) #end #printer = RubyProf::CallTreePrinter.new(profile) #File.open("profile.clt", "w") do |file| # printer.print(file) #end # For the report server mode we may need to keep the parser. Otherwise, # destroy it. @parser = nil unless keepParser Log.exit('parser') MessageHandlerInstance.instance.errors == 0 end # Parse a file and add the content to the existing project. _fileName_ is # the name of the file. _rule_ is the TextParser::Rule to start with. def parseFile(fileName, rule) begin @parser.open(fileName, false) @project.inputFiles << fileName rescue TjException => msg if msg.message && !msg.message.empty? critical('parse_file', msg.message) end return nil end @parser.setGlobalMacros return nil if (res = @parser.parse(rule)) == false @parser.close res end # Schedule all scenarios in the project. Return true if no error was # detected, false otherwise. def schedule Log.enter('scheduler', 'Scheduling project ...') #puts @project.to_s @project.warnTsDeltas = @warnTsDeltas begin res = @project.schedule rescue TjException => msg if msg.message && !msg.message.empty? critical('scheduling_error', msg.message) end return false end @project.enableTraceReports(@generateTraces) Log.exit('scheduler') res end # Generate all specified reports. The project must have been scheduled before # this method can be called. It returns true if no error occured, false # otherwise. def generateReports(outputDir = nil) @project.checkReports if outputDir # Make sure the output directory path always ends with a '/' unless empty. outputDir += '/' unless outputDir.empty? || outputDir[-1] == '/' @project.outputDir = outputDir end Log.enter('reports', 'Generating reports ...') begin #RubyProf.start @project.generateReports(@maxCpuCores) #profile = RubyProf.stop #printer = RubyProf::GraphHtmlPrinter.new(profile) #File.open("profile.html", "w") do |file| # printer.print(file) #end #printer = RubyProf::CallTreePrinter.new(profile) #File.open("profile.clt", "w") do |file| # printer.print(file) #end rescue TjException => msg if msg.message && !msg.message.empty? critical('generate_reports', msg.message) end return false end Log.exit('reports') true end # Generate the report with the ID _reportId_. If _regExpMode_ is true, # _reportId_ is interpreted as a Regular Expression and all reports with # matching IDs are generated. _formats_ is a list of formats (e. g. :html, # :csv, etc.). _dynamicAtributes_ is a String that may contain attributes to # supplement the report definition. The String must be in TJP format and may # be nil if no additional attributes are provided. def generateReport(reportId, regExpMode, formats = nil, dynamicAttributes = nil) begin Log.enter('generateReport', 'Generating report #{reportId} ...') @project.generateReport(reportId, regExpMode, formats, dynamicAttributes) rescue TjException => msg if msg.message && !msg.message.empty? critical('generate_report', msg.message) end Log.exit('generateReport') return false end Log.exit('generateReport') true end # List the details of the report with _reportId_ or if _regExpMode_ the # reports that match the regular expression in _reportId_. def listReports(reportId, regExpMode) begin Log.enter('listReports', 'Generating report list for #{reportId} ...') @project.listReports(reportId, regExpMode) rescue TjException => msg if msg.message && !msg.message.empty? critical('list_reports', msg.message) end Log.exit('listReports') return false end Log.exit('listReports') true end # Generate an export report definition for bookings up to the _freezeDate_. def freeze(freezeDate, taskBookings) begin # Check the master file is really a file and not stdin. unless (masterFile = @project.inputFiles.masterFile) error('cannot_freeze_stdin', "The project freeze feature can only be used when the " + "master file is a real file, not standard input.") end # Derive the file names for the header and bookings file from the base # name of the master file. masterFileBase = Dir.pwd + '/' + File.basename(masterFile, '.tjp') headerFile = masterFileBase + '-header.tji' bookingsFileBase = masterFileBase + '-bookings' bookingsFile = bookingsFileBase + '.tji' if !File.exist?(bookingsFile) || !File.exist?(headerFile) info('incl_freeze_files', "Please make sure you include #{headerFile} at " + "the end of the project header and " + "#{bookingsFile} at the end of #{masterFile}.") end # Generate the project header include file with the new 'now' date. begin File.open(headerFile, 'w') do |f| f.puts("now #{freezeDate}") end rescue error('write_header_incl', "Cannote write header include file " + "#{headerFile}") end # Generate an export report for the bookings. report = Report.new(@project, '_bookings_', bookingsFileBase, nil) report.typeSpec = :export report.set('formats', [ :tjp ]) report.inheritAttributes # We export only the tracking scenario. unless (trackingScenarioIdx = @project['trackingScenarioIdx']) error('no_tracking_scen', 'No trackingscenario defined') end report.set('scenarios', [ trackingScenarioIdx ]) # Only generate bookings up to the freeze date. report.set('end', freezeDate) # Show all tasks, sorted by seqno-up. report.set('hideTask', LogicalExpression.new(LogicalOperation.new(0))) report.set('sortTasks', [ [ 'seqno', true, -1 ] ]) # Show all resources, sorted by seqno-up. report.set('hideResource', LogicalExpression.new(LogicalOperation.new(0))) report.set('sortResources', [ [ 'seqno', true, -1 ] ]) # Only generate bookings, no other attributes or definitions. report.set('definitions', []) # We group the bookings by task or by resource depending on the user # request. if taskBookings report.set('taskAttributes', [ 'booking' ]) report.set('resourceAttributes', []) else report.set('taskAttributes', []) report.set('resourceAttributes', [ 'booking' ]) end rescue TjException return false end true end # Check the content of the file _fileName_ and interpret it as a time sheet. # If the sheet is syntactically correct and matches the loaded project, true # is returned. Otherwise false. def checkTimeSheet(fileName) begin Log.enter('checkTimeSheet', 'Parsing #{fileName} ...') # To use this feature, the user must have specified which scenario is # the tracking scenario. unless @project['trackingScenarioIdx'] raise TjException.new, 'No trackingscenario defined' end # Make sure we don't use data from old time sheets or Journal entries. @project.timeSheets.clear @project['journal'] = Journal.new return false unless (ts = parseFile(fileName, :timeSheetFile)) return false unless @project.checkTimeSheets queryAttrs = { 'project' => @project, 'property' => ts.resource, 'scopeProperty' => nil, 'scenarioIdx' => @project['trackingScenarioIdx'], 'start' => ts.interval.start, 'end' => ts.interval.end, 'journalMode' => :journal, 'journalAttributes' => %w( alert property propertyid headline flags timesheet summary details ), 'sortJournalEntries' => [ [ :seqno, 1 ] ], 'timeFormat' => '%Y-%m-%d', 'selfContained' => true } query = Query.new(queryAttrs) puts ts.resource.query_journal(query).richText.inputText rescue TjException => msg if msg.message && !msg.message.empty? critical('check_time_sheet', msg.message) end Log.exit('checkTimeSheet') return false end Log.exit('checkTimeSheet') true end # Check the content of the file _fileName_ and interpret it as a status # sheet. If the sheet is syntactically correct and matches the loaded # project, true is returned. Otherwise false. def checkStatusSheet(fileName) begin Log.enter('checkStatusSheet', 'Parsing #{fileName} ...') # To use this feature, the user must have specified which scenario is # the tracking scenario. unless @project['trackingScenarioIdx'] raise TjException.new, 'No trackingscenario defined' end return false unless (ss = parseFile(fileName, :statusSheetFile)) queryAttrs = { 'project' => @project, 'property' => ss[0], 'scopeProperty' => nil, 'scenarioIdx' => @project['trackingScenarioIdx'], 'start' => ss[1], 'end' => ss[2], 'timeFormat' => '%Y-%m-%d', 'selfContained' => true } query = Query.new(queryAttrs) puts ss[0].query_dashboard(query).richText.inputText rescue TjException => msg if msg.message && !msg.message.empty? critical('check_status_sheet', msg.message) end Log.exit('checkStatusSheet') return false end Log.exit('checkStatusSheet') true end # Return the ID of the project or nil if no project has been loaded yet. def projectId return nil if @project.nil? @project['projectid'] end # Return the name of the project or nil if no project has been loaded yet. def projectName return nil if @project.nil? @project['name'] end # Return the number of errors that had been reported during processing. def errors MessageHandlerInstance.instance.errors end end TaskJuggler-3.8.1/lib/taskjuggler/TaskScenario.rb000066400000000000000000003104371473026623400217670ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = TaskScenario.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/ScenarioData' require 'taskjuggler/DataCache' class TaskJuggler class TaskScenario < ScenarioData attr_reader :isRunAway, :hasDurationSpec # Create a new TaskScenario object. def initialize(task, scenarioIdx, attributes) super # Attributed are only really created when they are accessed the first # time. So make sure some needed attributes really exist so we don't # have to check for existance each time we access them. %w( allocate assignedresources booking charge chargeset complete competitors criticalness depends duration effort effortdone effortleft end forward gauge length maxend maxstart minend minstart milestone pathcriticalness precedes priority projectionmode responsible scheduled shifts start status ).each do |attr| @property[attr, @scenarioIdx] end unless @property.parent # The projectionmode attributes is a scenario specific attribute that # can be inherited from the project. The normal inherit-from-project # mechanism does not support scenario specific inheritance. We have to # deal with this separately here. To make it look like a regularly # inherted value, we need to switch the AttributeBase mode and restore # it afterwards. mode = AttributeBase.mode AttributeBase.setMode(1) @property['projectionmode', @scenarioIdx] = @project.scenario(@scenarioIdx).get('projection') AttributeBase.setMode(mode) end # A list of all allocated leaf resources. @candidates = [] @dCache = DataCache.instance end def markAsScheduled return if @scheduled @scheduled = true Log.msg do if @milestone typename = 'Milestone' elsif @property.leaf? typename = 'Task' else typename = 'Container' end "#{typename} #{@property.fullId} has been scheduled." end end # Call this function to reset all scheduling related data prior to # scheduling. def prepareScheduling @property['startpreds', @scenarioIdx] = [] @property['startsuccs', @scenarioIdx] = [] @property['endpreds', @scenarioIdx] = [] @property['endsuccs', @scenarioIdx] = [] @isRunAway = false # And as global scoreboard index @currentSlotIdx = nil # The 'done' variables count scheduled values in number of time slots. @doneDuration = 0 @doneLength = 0 # Due to the 'efficiency' factor the effort slots must be a float. @doneEffort = 0.0 @nowIdx = @project.dateToIdx(@project['now']) @startIsDetermed = nil @endIsDetermed = nil # To avoid multiple calls to propagateDate() we use these flags to know # when we've done it already. @startPropagated = false @endPropagated = false @durationType = if @effort > 0 @hasDurationSpec = true :effortTask elsif @length > 0 @hasDurationSpec = true :lengthTask elsif @duration > 0 @hasDurationSpec = true :durationTask else # If the task is set as milestone it has a duration spec. @hasDurationSpec = @milestone :startEndTask end markAsMilestone # For start-end-tasks without allocation, we don't have to do # anything but to set the 'scheduled' flag. if @durationType == :startEndTask && @start && @end && @allocate.empty? markAsScheduled end # Collect the limits of this task and all parent tasks into a single # Array. @allLimits = [] task = @property # Reset the counters of all limits of this task. task['limits', @scenarioIdx].reset if task['limits', @scenarioIdx] until task.nil? if task['limits', @scenarioIdx] @allLimits << task['limits', @scenarioIdx] end task = task.parent end @contendedResources = Hash.new { |hash, key| hash[key] = Hash.new(0) } # Collect the mandatory allocations. @mandatories = [] @allocate.each do |allocation| @mandatories << allocation if allocation.mandatory allocation.lockedResource = nil end bookBookings if @durationType == :startEndTask @startIdx = @project.dateToIdx(@start) if @start @endIdx = @project.dateToIdx(@end) if @end end end # The parser only stores the full task IDs for each of the dependencies. # This function resolves them to task references and checks them. In # addition to the 'depends' and 'precedes' property lists we also keep 4 # additional lists. # startpreds: All precedessors to the start of this task # startsuccs: All successors to the start of this task # endpreds: All predecessors to the end of this task # endsuccs: All successors to the end of this task # Each list element consists of a reference/boolean pair. The reference # points to the dependent task and the boolean specifies whether the # dependency originates from the end of the task or not. def Xref @depends.each do |dependency| depTask = checkDependency(dependency, 'depends') @startpreds.push([ depTask, dependency.onEnd ]) depTask[dependency.onEnd ? 'endsuccs' : 'startsuccs', @scenarioIdx]. push([ @property, false ]) end @precedes.each do |dependency| predTask = checkDependency(dependency, 'precedes') @endsuccs.push([ predTask, dependency.onEnd ]) predTask[dependency.onEnd ? 'endpreds' : 'startpreds', @scenarioIdx]. push([@property, true ]) end end # Return true of this Task has a dependency [ _target_, _onEnd_ ] in the # dependency category _depType_. def hasDependency?(depType, target, onEnd) a(depType).include?([target, onEnd]) end def propagateInitialValues unless @startPropagated if @start propagateDate(@start, false, true) elsif @property.parent.nil? && @property.canInheritDate?(@scenarioIdx, false) propagateDate(@project['start'], false, true) end end unless @endPropagated if @end propagateDate(@end, true, true) elsif @property.parent.nil? && @property.canInheritDate?(@scenarioIdx, true) propagateDate(@project['end'], true, true) end end end # Before the actual scheduling work can be started, we need to do a few # consistency checks on the task. def preScheduleCheck # Accounts can have sub accounts added after being used in a chargetset. # So we need to re-test here. @chargeset.each do |chargeset| chargeset.each do |account, share| unless account.leaf? error('account_no_leaf', "Chargesets may not include group account #{account.fullId}.") end end end @responsible.map! do |resourceId| # 'resource' is still just an ID and needs to be converted into a # Resource. if (resource = @project.resource(resourceId)).nil? error('resource_id_expected', "#{resourceId} is not a defined " + 'resource.', @sourceFileInfo) end resource end @responsible.uniq! # Leaf tasks can be turned into containers after bookings have been added. # We need to check for this. unless @property.leaf? || @booking.empty? error('container_booking', "Container task #{@property.fullId} may not have bookings.") end # Milestones may not have bookings. if @milestone && !@booking.empty? error('milestone_booking', "Milestone #{@property.fullId} may not have bookings.") end # All 'scheduled' tasks must have a fixed start and end date. if @scheduled && (@start.nil? || @end.nil?) error('not_scheduled', "Task #{@property.fullId} is marked as scheduled but does not " + 'have a fixed start and end date.') end # If an effort has been specified resources must be allocated as well. if @effort > 0 && @allocate.empty? error('effort_no_allocations', "Task #{@property.fullId} has an effort but no resource " + "allocations.") end durationSpecs = 0 durationSpecs += 1 if @effort > 0 durationSpecs += 1 if @length > 0 durationSpecs += 1 if @duration > 0 durationSpecs += 1 if @milestone # The rest of this function performs a number of plausibility tests with # regards to task start and end critiria. To explain the various cases, # the following symbols are used: # # |: fixed start or end date # -: no fixed start or end date # M: Milestone # D: start or end dependency # x->: ASAP task with duration criteria # <-x: ALAP task with duration criteria # -->: ASAP task without duration criteria # <--: ALAP task without duration criteria if @property.container? if durationSpecs > 0 error('container_duration', "Container task #{@property.fullId} may not have a duration " + "or be marked as milestones.") end elsif @milestone if durationSpecs > 1 error('milestone_duration', "Milestone task #{@property.fullId} may not have a duration.") end # Milestones can have the following cases: # # | M - ok |D M - ok - M - err1 -D M - ok # | M | err2 |D M | err2 - M | ok -D M | ok # | M -D ok |D M -D ok - M -D ok -D M -D ok # | M |D err2 |D M |D err2 - M |D ok -D M |D ok # err1: no start and end # already handled by 'start_undetermed' or 'end_undetermed' # err2: differnt start and end dates if @start && @end && @start != @end error('milestone_start_end', "Start (#{@start}) and end (#{@end}) dates of " + "milestone task #{@property.fullId} must be identical.") end else # Error table for non-container, non-milestone tasks: # AMP: Automatic milestone promotion for underspecified tasks when # no bookings or allocations are present. # AMPi: Automatic milestone promotion when no bookings or # allocations are present. When no bookings but allocations are # present the task inherits start and end date. # Ref. implicitXref()| # inhS: Inherit start date from parent task or project # inhE: Inherit end date from parent task or project # # | x-> - ok |D x-> - ok - x-> - inhS -D x-> - ok # | x-> | err1 |D x-> | err1 - x-> | inhS -D x-> | err1 # | x-> -D ok |D x-> -D ok - x-> -D inhS -D x-> -D ok # | x-> |D err1 |D x-> |D err1 - x-> |D inhS -D x-> |D err1 # | --> - AMP |D --> - AMP - --> - AMPi -D --> - AMP # | --> | ok |D --> | ok - --> | inhS -D --> | ok # | --> -D ok |D --> -D ok - --> -D inhS -D --> -D ok # | --> |D ok |D --> |D ok - --> |D inhS -D --> |D ok # | <-x - inhE |D <-x - inhE - <-x - inhE -D <-x - inhE # | <-x | err1 |D <-x | err1 - <-x | ok -D <-x | ok # | <-x -D err1 |D <-x -D err1 - <-x -D ok -D <-x -D ok # | <-x |D err1 |D <-x |D err1 - <-x |D ok -D <-x |D ok # | <-- - inhE |D <-- - inhE - <-- - AMP -D <-- - inhE # | <-- | ok |D <-- | ok - <-- | AMP -D <-- | ok # | <-- -D ok |D <-- -D ok - <-- -D AMP -D <-- -D ok # | <-- |D ok |D <-- |D ok - <-- |D AMP -D <-- |D ok # These cases are normally autopromoted to milestones or inherit their # start or end dates. But this only works for tasks that have no # allocations or bookings. # - --> - # | --> - # |D --> - # -D --> - # - <-- - # - <-- | # - <-- -D # - <-- |D if durationSpecs == 0 && ((@forward && @end.nil? && !hasDependencies(true)) || (!@forward && @start.nil? && !hasDependencies(false))) error('task_underspecified', "Task #{@property.fullId} has too few specifations to be " + "scheduled.") end # err1: Overspecified (12 cases) # | x-> | # | <-x | # | x-> |D # | <-x |D # |D x-> | # |D <-x | # |D <-x |D # |D x-> |D # -D x-> | # -D x-> |D # |D <-x -D # | <-x -D if durationSpecs > 1 error('multiple_durations', "Tasks may only have either a duration, length or effort or " + "be a milestone.") end startSpeced = @property.provided('start', @scenarioIdx) endSpeced = @property.provided('end', @scenarioIdx) if ((startSpeced && endSpeced) || (hasDependencies(false) && @forward && endSpeced) || (hasDependencies(true) && !@forward && startSpeced)) && durationSpecs > 0 && !@property.provided('scheduled', @scenarioIdx) error('task_overspecified', "Task #{@property.fullId} has a start, an end and a " + 'duration specification.') end end if !@booking.empty? && !@forward && !@scheduled error('alap_booking', 'A task scheduled in ALAP mode may only have bookings if it ' + 'has been marked as fully scheduled. Keep in mind that ' + 'certain attributes like \'end\' or \'precedes\' automatically ' + 'switch the task to ALAP mode.') end @startsuccs.each do |task, onEnd| unless task['forward', @scenarioIdx] task.data[@scenarioIdx].error( 'onstart_wrong_direction', 'Tasks with on-start dependencies must be ASAP scheduled') end end @endpreds.each do |task, onEnd| if task['forward', @scenarioIdx] task.data[@scenarioIdx].error( 'onend_wrong_direction', 'Tasks with on-end dependencies must be ALAP scheduled') end end end # When the actual scheduling process has been completed, this function must # be called to do some more housekeeping. It computes some derived data # based on the just scheduled values. def finishScheduling # Recursively descend into all child tasks. @property.children.each do |task| task.finishScheduling(@scenarioIdx) end @property.parents.each do |pTask| # Add the assigned resources to the parent task's list. @assignedresources.each do |resource| unless pTask['assignedresources', @scenarioIdx].include?(resource) pTask['assignedresources', @scenarioIdx] << resource end end end # These lists are no longer needed, so let's save some memory. Set it to # nil so we can detect accidental use. @candidates = nil @mandatories = nil @allLimits = nil end # This function is not essential but does perform a large number of # consistency checks. It should be called after the scheduling run has been # finished. def postScheduleCheck @errors = 0 @property.children.each do |task| @errors += 1 unless task.postScheduleCheck(@scenarioIdx) end # There is no point to check the parent if the child(s) have errors. return false if @errors > 0 # Same for runaway tasks. They have already been reported. if @isRunAway error('sched_runaway', "Some tasks did not fit into the project time " + "frame.") end # Make sure the task is marked complete unless @scheduled error('not_scheduled', "Task #{@property.fullId} has not been marked as scheduled.") end # If the task has a follower or predecessor that is a runaway this task # is also incomplete. (@startsuccs + @endsuccs).each do |task, onEnd| return false if task.isRunAway(@scenarioIdx) end (@startpreds + @endpreds).each do |task, onEnd| return false if task.isRunAway(@scenarioIdx) end # Check if the start time is ok if @start.nil? error('task_start_undef', "Task #{@property.fullId} has undefined start time") end if @start < @project['start'] || @start > @project['end'] error('task_start_range', "The start time (#{@start}) of task #{@property.fullId} " + "is outside the project interval (#{@project['start']} - " + "#{@project['end']})") end if !@minstart.nil? && @start < @minstart warning('minstart', "The start time (#{@start}) of task #{@property.fullId} " + "is too early. Must be after #{@minstart}.") end if !@maxstart.nil? && @start > @maxstart warning('maxstart', "The start time (#{@start}) of task #{@property.fullId} " + "is too late. Must be before #{@maxstart}.") end # Check if the end time is ok error('task_end_undef', "Task #{@property.fullId} has undefined end time") if @end.nil? if @end < @project['start'] || @end > @project['end'] error('task_end_range', "The end time (#{@end}) of task #{@property.fullId} " + "is outside the project interval (#{@project['start']} - " + "#{@project['end']})") end if !@minend.nil? && @end < @minend warning('minend', "The end time (#{@end}) of task #{@property.fullId} " + "is too early. Must be after #{@minend}.") end if !@maxend.nil? && @end > @maxend warning('maxend', "The end time (#{@end}) of task #{@property.fullId} " + "is too late. Must be before #{@maxend}.") end # Make sure the start is before the end if @start > @end error('start_after_end', "The start time (#{@start}) of task #{@property.fullId} " + "is after the end time (#{@end}).") end # Check that tasks fits into parent task. unless (parent = @property.parent).nil? || parent['start', @scenarioIdx].nil? || parent['end', @scenarioIdx].nil? if @start < parent['start', @scenarioIdx] error('task_start_in_parent', "The start date (#{@start}) of task #{@property.fullId} " + "is before the start date (#{parent['start', @scenarioIdx]}) " + "of the enclosing task.") end if @end > parent['end', @scenarioIdx] error('task_end_in_parent', "The end date (#{@end}) of task #{@property.fullId} " + "is after the end date (#{parent['end', @scenarioIdx]}) " + "of the enclosing task.") end end # Check that all preceding tasks start/end before this task. @depends.each do |dependency| task = dependency.task limit = task[dependency.onEnd ? 'end' : 'start', @scenarioIdx] next if limit.nil? if @start < limit || (dependency.gapDuration > 0 && limit + dependency.gapDuration > @start) || (dependency.gapLength > 0 && calcLength(limit, @start) < dependency.gapLength) error('task_pred_before', "Task #{@property.fullId} (#{@start}) must start " + (dependency.gapDuration > 0 ? "#{dependency.gapDuration / (60 * 60 * 24)} days " : (dependency.gapLength > 0 ? "#{@project.slotsToDays(dependency.gapLength)} " + "working days " : '')) + "after " + "#{dependency.onEnd ? 'end' : 'start'} (#{limit}) of task " + "#{task.fullId}. This condition could not be met.") end end # Check that all following tasks end before this task @precedes.each do |dependency| task = dependency.task limit = task[dependency.onEnd ? 'end' : 'start', @scenarioIdx] next if limit.nil? if limit < @end || (dependency.gapDuration > 0 && limit - dependency.gapDuration < @end) || (dependency.gapLength > 0 && calcLength(@end, limit) < dependency.gapLength) error('task_succ_after', "Task #{@property.fullId} (#{@end}) must end " + (dependency.gapDuration > 0 ? "#{dependency.gapDuration / (60 * 60 * 24)} days " : (dependency.gapLength > 0 ? "#{@project.slotsToDays(dependency.gapLength)} " + "working days " : '')) + "before " + "#{dependency.onEnd ? 'end' : 'start'} (#{limit}) of task " + "#{task.fullId}. This condition could not be met.") end end if @milestone && @start != @end error('milestone_times_equal', "Milestone #{@property.fullId} must have identical start and " + "end date.") end if @property.leaf? && @effort == 0 && !@milestone && !@allocate.empty? && @assignedresources.empty? # The user used an 'allocate' for the task, but did not specify any # 'effort'. Actual allocations will only happen when resources are # available by chance. If there are no assigned resources, we generate # a warning as this is probably not what the user intended. warning('allocate_no_assigned', "Task #{@property.id} has resource allocation requested, but " + "did not get any resources assigned. Either use 'effort' " + "to ensure allocations or use a higher 'priority'.") end thieves = [] @competitors.each do |t| thieves << t if t['priority', @scenarioIdx] < @priority end unless thieves.empty? warning('priority_inversion', "Due to a mix of ALAP and ASAP scheduled tasks or a " + "dependency on a lower priority tasks the following " + "task#{thieves.length > 1 ? 's' : ''} stole resources from " + "#{@property.fullId} despite having a lower priority:") thieves.each do |t| info('priority_inversion_info', "Task #{t.fullId}", t.sourceFileInfo) end end @errors == 0 end def resetLoopFlags @deadEndFlags = Array.new(4, false) end # To ensure that we can properly schedule the project, we need to make # sure that it does not contain any circular dependencies. This method # recursively checks for such loops by remembering the _path_. Each entry # is marks the start or end of a task. _atEnd_ specifies whether we are # currently at the start or end of the task. _fromOutside_ specifies # whether we are coming from a inside or outside that tasks. See # specification below. _forward_ specifies whether we are checking the # dependencies from start to end or in the opposite direction. If we are # moving forward, we only move from start to end of ASAP tasks, not ALAP # tasks and vice versa. For milestones, we ignore the scheduling # direction. def checkForLoops(path, atEnd, fromOutside, forward) # Check if we have been here before on this path. if path.include?([ @property, atEnd ]) warning('loop_detected', "Dependency loop detected at #{atEnd ? 'end' : 'start'} " + "of task #{@property.fullId}", false) skip = true path.each do |t, e| if t == @property && e == atEnd skip = false next end next if skip info("loop_at_#{e ? 'end' : 'start'}", "Loop ctnd. at #{e ? 'end' : 'start'} of task #{t.fullId}", t.sourceFileInfo) end error('loop_end', "Aborting") end # Used for debugging only if false pathText = '' path.each do |t, e| pathText += "#{t.fullId}(#{e ? 'end' : 'start'}) -> " end pathText += "#{@property.fullId}(#{atEnd ? 'end' : 'start'})" puts pathText end return if @deadEndFlags[(atEnd ? 2 : 0) + (fromOutside ? 1 : 0)] path << [ @property, atEnd ] # To find loops we have to traverse the graph in a certain order. When we # enter a task we can either come from outside or inside. The following # graph explains these definitions: # # | / \ | # outside v / \ v outside # +------------------------------+ # | / Task \ | # -->| o <--- ---> o |<-- # |/ Start End \| # /+------------------------------+\ # / ^ ^ \ # | inside | # # At the top we have the parent task. At the botton the child tasks. # The horizontal arrors are start predecessors or end successors. # As the graph is doubly-linked, we need to becareful to only find real # loops. When coming from outside, we only continue to the inside and vice # versa. Horizontal moves are only made when we are in a leaf task. unless atEnd if fromOutside if @property.container? # # | # v # +-------- # -->| o--+ # +----|--- # | # V # @property.children.each do |child| child.checkForLoops(@scenarioIdx, path, false, true, forward) end else # | # v # +-------- # -->| o----> # +-------- # if (forward && @forward) || @milestone checkForLoops(path, true, false, true) end end else if @startpreds.empty? # # ^ # | # +-|------ # | o <-- # +-------- # ^ # | # if @property.parent @property.parent.checkForLoops(@scenarioIdx, path, false, false, forward) end else # +-------- # <---- o <-- # +-------- # ^ # | # @startpreds.each do |task, targetEnd| task.checkForLoops(@scenarioIdx, path, targetEnd, true, forward) end end end else if fromOutside if @property.container? # # | # v # --------+ # +--o |<-- # ---|----+ # | # v # @property.children.each do |child| child.checkForLoops(@scenarioIdx, path, true, true, forward) end else # | # v # --------+ # <----o |<-- # --------+ # if (!forward && !@forward) || @milestone checkForLoops(path, false, false, false) end end else if @endsuccs.empty? # # ^ # | # ------|-+ # --> o | # --------+ # ^ # | # if @property.parent @property.parent.checkForLoops(@scenarioIdx, path, true, false, forward) end else # --------+ # --> o----> # --------+ # ^ # | # @endsuccs.each do |task, targetEnd| task.checkForLoops(@scenarioIdx, path, targetEnd, true, forward) end end end end path.pop @deadEndFlags[(atEnd ? 2 : 0) + (fromOutside ? 1 : 0)] = true # puts "Finished with #{@property.fullId} #{atEnd ? 'end' : 'start'} " + # "#{fromOutside ? 'outside' : 'inside'}" end # This function must be called before prepareScheduling(). It compiles the # list of leaf resources that are allocated to this task. def candidates @candidates = [] @allocate.each do |allocation| allocation.candidates.each do |candidate| candidate.allLeaves.each do |resource| @candidates << resource unless @candidates.include?(resource) end end end @candidates end # This function does some prep work for other functions like # calcCriticalness. It compiles a list of all allocated leaf resources and # stores it in @candidates. It also adds the allocated effort to # the 'alloctdeffort' counter of each resource. def countResourceAllocations return if @candidates.empty? || @effort <= 0 avgEffort = @effort / @candidates.length @candidates.each do |resource| resource['alloctdeffort', @scenarioIdx] += avgEffort end end # Determine the criticalness of the individual task. This is a measure for # the likelyhood that this task will get the resources that it needs to # complete the effort. Tasks without effort are not cricital. The only # exception are milestones which get an arbitrary value between 0 and 2 # based on their priority. def calcCriticalness @criticalness = 0.0 @pathcriticalness = nil # Users feel that milestones are somewhat important. So we use an # arbitrary value larger than 0 for them. We make it priority dependent, # so the user has some control over it. Priority 0 is 0, 500 is 1.0 and # 1000 is 2.0. These values are pretty much randomly picked and probably # require some more tuning based on real projects. if @milestone @criticalness = @priority / 500.0 end # Task without efforts of allocations are not critical. return if @effort <= 0 || @candidates.empty? # Determine the average criticalness of all allocated resources. criticalness = 0.0 @candidates.each do |resource| criticalness += resource['criticalness', @scenarioIdx] end criticalness /= @candidates.length # The task criticalness is the product of effort and average resource # criticalness. @criticalness = @effort * criticalness end # The path criticalness is a measure for the overall criticalness of the # task taking the dependencies into account. The fact that a task is part # of a chain of effort-based task raises all the task in the chain to a # higher criticalness level than the individual tasks. In fact, the path # criticalness of this chain is equal to the sum of the individual # criticalnesses of the tasks. def calcPathCriticalness(atEnd = false) # If we have computed this already, just return the value. If we are only # at the end of the task, we do not include the criticalness of this task # as it is not really part of the path. if @pathcriticalness return @pathcriticalness - (atEnd ? 0 : @criticalness) end maxCriticalness = 0.0 if atEnd # At the end, we only care about pathes through the successors of this # task or its parent tasks. if (criticalness = calcPathCriticalnessEndSuccs) > maxCriticalness maxCriticalness = criticalness end else # At the start of the task, we have two options. if @property.container? # For container tasks, we ignore all dependencies and check the pathes # through all the children. @property.children.each do |task| if (criticalness = task.calcPathCriticalness(@scenarioIdx, false)) > maxCriticalness maxCriticalness = criticalness end end else # For leaf tasks, we check all pathes through the start successors and # then the pathes through the end successors of this task and all its # parent tasks. @startsuccs.each do |task, onEnd| if (criticalness = task.calcPathCriticalness(@scenarioIdx, onEnd)) > maxCriticalness maxCriticalness = criticalness end end if (criticalness = calcPathCriticalnessEndSuccs) > maxCriticalness maxCriticalness = criticalness end maxCriticalness += @criticalness end end @pathcriticalness = maxCriticalness end # Check if the task is ready to be scheduled. For this it needs to have at # least one specified end date and a duration criteria or the other end # date. def readyForScheduling? # If the tasks has already been scheduled, we still call it 'ready' so # it will be removed from the todo list. return true if @scheduled return false if @isRunAway if @forward return true if @start && (@hasDurationSpec || @end) else return true if @end && (@hasDurationSpec || @start) end false end # This function is the entry point for the core scheduling algorithm. It # schedules the task to completion. The function returns true if a start # or end date has been determined and other tasks may be ready for # scheduling now. def schedule # Check if the task has already been scheduled e. g. by propagateDate(). return true if @scheduled logTag = "schedule_#{@property.id}" Log.enter(logTag, "Scheduling task #{@property.id}") # Compute the date of the next slot this task wants to have scheduled. # This must either be the first slot ever or it must be directly # adjecent to the previous slot. If this task has not yet been scheduled # at all, @currentSlotIdx is still nil. Otherwise it contains the index # of the last scheduled slot. if @forward # On first call, the @currentSlotIdx is not set yet. We set it to the # start slot index or the 'now' slot if we are in projection mode and # the tasks has allocations. if @currentSlotIdx.nil? @currentSlotIdx = @project.dateToIdx( @projectionmode && (@project['now'] > @start) && !@allocate.empty? ? @project['now'] : @start) end else # On first call, the @currentSlotIdx is not set yet. We set it to the # slot index of the slot before the end slot. if @currentSlotIdx.nil? @currentSlotIdx = @project.dateToIdx(@end) - 1 end end # Schedule all time slots from slot in the scheduling direction until # the task is completed or a problem has been found. # The task may not excede the project interval. lowerLimit = @project.dateToIdx(@project['start']) upperLimit = @project.dateToIdx(@project['end']) delta = @forward ? 1 : -1 while scheduleSlot @currentSlotIdx += delta if @currentSlotIdx < lowerLimit || upperLimit < @currentSlotIdx markAsRunaway Log.exit(logTag, "Scheduling of task #{@property.id} failed") return false end end Log.exit(logTag, "Scheduling of task #{@property.id} completed") true end # Set a new start or end date and propagate the value to all other # task ends that have a direct dependency to this end of the task. def propagateDate(date, atEnd, ignoreEffort = false) logTag = "propagateDate_#{@property.id}_#{atEnd ? 'end' : 'start'}" Log.enter(logTag, "Propagating #{atEnd ? 'end' : 'start'} date " + "to task #{@property.id}") thisEnd = atEnd ? 'end' : 'start' otherEnd = atEnd ? 'start' : 'end' #puts "Propagating #{thisEnd} date #{date} to #{@property.fullId} " + # "#{ignoreEffort ? "ignoring effort" : "" }" # These flags are just used to avoid duplicate calls of this function # during propagateInitialValues(). if atEnd @endPropagated = true else @startPropagated = true end # For leaf tasks, propagate start may set the date. Container task dates # are only set in scheduleContainer(). if @property.leaf? # If we already have a date, we will only shrink the task period with # the new date. if (setDate = instance_variable_get('@' + thisEnd)) && atEnd ? date > setDate : date < setDate Log.msg { "Preserving #{thisEnd} date of #{typename} " + "#{@property.fullId}: #{setDate}" } return end instance_variable_set(('@' + thisEnd).intern, date) typename = 'Task' if @durationType == :startEndTask instance_variable_set(('@' + thisEnd + 'Idx').intern, @project.dateToIdx(date)) if @milestone typename = 'Milestone' end end Log.msg { "Update #{typename} #{@property.fullId}: #{period_to_s}" } end if @milestone # Start and end date of a milestone are identical. markAsScheduled if a(otherEnd).nil? propagateDate(a(thisEnd), !atEnd) end elsif !@scheduled && @start && @end && !(@length == 0 && @duration == 0 && @effort == 0 && !@allocate.empty?) markAsScheduled end # Propagate date to all dependent tasks. Don't do this for start # successors or end predecessors if this task is effort based. In this # case, the date might still change to align with the first/last # allocation. In these cases, bookResource() has to propagate the final # date. if atEnd if ignoreEffort || @effort == 0 @endpreds.each do |task, onEnd| propagateDateToDep(task, onEnd) end end @endsuccs.each do |task, onEnd| propagateDateToDep(task, onEnd) end else if ignoreEffort || @effort == 0 @startsuccs.each do |task, onEnd| propagateDateToDep(task, onEnd) end end @startpreds.each do |task, onEnd| propagateDateToDep(task, onEnd) end end # Propagate date to sub tasks which have only an implicit # dependency on the parent task and no other criteria for this end of # the task. @property.children.each do |task| if task.canInheritDate?(@scenarioIdx, atEnd) task.propagateDate(@scenarioIdx, date, atEnd) end end # The date propagation might have completed the date set of the enclosing # containter task. If so, we can schedule it as well. @property.parents.each do |parent| parent.scheduleContainer(@scenarioIdx) end Log.exit(logTag, "Finished propagation of " + "#{atEnd ? 'end' : 'start'} date " + "to task #{@property.id}") end # This function determines if a task can inherit the start or end date # from a parent task or the project time frame. +atEnd+ specifies whether # the check should be done for the task end (true) or task start (false). def canInheritDate?(atEnd) # Inheriting a start or end date from the enclosing task or the project # is allowed for the following scenarios: # - --> - inhS*1 - <-- - inhE*1 # - --> | inhS | <-- - inhE # - x-> - inhS - <-x - inhE # - x-> | inhS | <-x - inhE # - x-> -D inhS -D <-x - inhE # - x-> |D inhS |D <-x - inhE # - --> -D inhS -D <-- - inhE # - --> |D inhS |D <-- - inhE # - <-- | inhS | --> - inhE # # *1 when no bookings but allocations are present thisEnd, thatEnd = atEnd ? [ 'end', 'start' ] : [ 'start', 'end' ] # Return false if we already have a date for this end or if we have a # strong dependency for this end. return false if instance_variable_get('@' + thisEnd) || hasStrongDeps?(atEnd) # Containter task can inherit the date if they have no dependencies at # this end. return true if @property.container? hasThatSpec = !instance_variable_get('@' + thatEnd).nil? || hasStrongDeps?(!atEnd) # Check for tasks that have no start and end spec, no duration spec but # allocates. They can inherit the start and end date. return true if hasThatSpec && !@hasDurationSpec && !@allocate.empty? if @forward ^ atEnd # the scheduling direction is pointing away from this end return true if @hasDurationSpec || !@booking.empty? return hasThatSpec else # the scheduling direction is pointing towards this end return !instance_variable_get('@' + thatEnd).nil? && !@hasDurationSpec && @booking.empty? #&& @allocate.empty? end end # Find the smallest possible interval that encloses all child tasks. Abort # the operation if any of the child tasks are not yet scheduled. def scheduleContainer return if @scheduled || !@property.container? nStart = nil nEnd = nil @property.kids.each do |task| # Abort if a child has not yet been scheduled. Since we haven't done # the consistency check yet, we can't rely on start and end being set # if 'scheduled' is set. return if (!task['scheduled', @scenarioIdx] || task['start', @scenarioIdx].nil? || task['end', @scenarioIdx].nil?) if nStart.nil? || task['start', @scenarioIdx] < nStart nStart = task['start', @scenarioIdx] end if nEnd.nil? || task['end', @scenarioIdx] > nEnd nEnd = task['end', @scenarioIdx] end end startSet = endSet = false # Propagate the dates to other dependent tasks. if @start.nil? || @start > nStart @start = nStart startSet = true end if @end.nil? || @end < nEnd @end = nEnd endSet = true end unless @start && @end raise "Start (#{@start}) and end (#{@end}) must be set" end Log.msg { "Container task #{@property.fullId} completed: #{period_to_s}" } markAsScheduled # If we have modified the start or end date, we need to communicate this # new date to surrounding tasks. propagateDate(nStart, false) if startSet propagateDate(nEnd, true) if endSet end # Find the earliest possible start date for the task. This date must be # after the end date of all the task that this task depends on. # Dependencies may also require a minimum gap between the tasks. def earliestStart # This is the date that we will return. startDate = nil @depends.each do |dependency| potentialStartDate = dependency.task[dependency.onEnd ? 'end' : 'start', @scenarioIdx] return nil if potentialStartDate.nil? # Determine the end date of a 'length' gap. dateAfterLengthGap = potentialStartDate gapLength = dependency.gapLength while gapLength > 0 && dateAfterLengthGap < @project['end'] do if @project.isWorkingTime(dateAfterLengthGap) gapLength -= 1 end dateAfterLengthGap += @project['scheduleGranularity'] end # Determine the end date of a 'duration' gap. if dateAfterLengthGap > potentialStartDate + dependency.gapDuration potentialStartDate = dateAfterLengthGap else potentialStartDate += dependency.gapDuration end startDate = potentialStartDate if startDate.nil? || startDate < potentialStartDate end # If any of the parent tasks has an explicit start date, the task must # start at or after this date. task = @property while (task = task.parent) do if task['start', @scenarioIdx] && (startDate.nil? || task['start', @scenarioIdx] > startDate) startDate = task['start', @scenarioIdx] break end end # When the computed start date is after the already determined end date # of the task, the start dependencies were too weak. This happens when # task B depends on A and they are specified this way: # task A: | --> D- # task B: -D <-- | if @end && (startDate.nil? || startDate > @end) error('impossible_start_dep', "Task #{@property.fullId} has start date dependencies " + "that conflict with the end date #{@end}.") end startDate end # Find the latest possible end date for the task. This date must be # before the start date of all the task that this task precedes. # Dependencies may also require a minimum gap between the tasks. def latestEnd # This is the date that we will return. endDate = nil @precedes.each do |dependency| potentialEndDate = dependency.task[dependency.onEnd ? 'end' : 'start', @scenarioIdx] return nil if potentialEndDate.nil? # Determine the end date of a 'length' gap. dateBeforeLengthGap = potentialEndDate gapLength = dependency.gapLength while gapLength > 0 && dateBeforeLengthGap > @project['start'] do if @project.isWorkingTime(dateBeforeLengthGap - @project['scheduleGranularity']) gapLength -= 1 end dateBeforeLengthGap -= @project['scheduleGranularity'] end # Determine the end date of a 'duration' gap. if dateBeforeLengthGap < potentialEndDate - dependency.gapDuration potentialEndDate = dateBeforeLengthGap else potentialEndDate -= dependency.gapDuration end endDate = potentialEndDate if endDate.nil? || endDate > potentialEndDate end # If any of the parent tasks has an explicit end date, the task must end # at or before this date. task = @property while (task = task.parent) do if task['end', @scenarioIdx] && (endDate.nil? || task['end', @scenarioIdx] < endDate) endDate = task['end', @scenarioIdx] break end end # When the computed end date is before the already determined start date # of the task, the end dependencies were too weak. This happens when # task A precedes B and they are specified this way: # task A: | --> D- # task B: -D <-- | if @start && (endDate.nil? || endDate < @start) error('impossible_end_dep', "Task #{@property.fullId} has end date dependencies " + "that conflict with the start date #{@start}.") end endDate end def addBooking(booking) # This append operation will not trigger a copy to sub-scenarios. # Bookings are only valid for the scenario they are defined in. @booking << booking end def query_activetasks(query) count = activeTasks(query) query.sortable = query.numerical = count # For the string output, we only use integer numbers. query.string = "#{count.to_i}" end def query_closedtasks(query) count = closedTasks(query) query.sortable = query.numerical = count # For the string output, we only use integer numbers. query.string = "#{count.to_i}" end def query_competitorcount(query) query.sortable = query.numerical = @competitors.length query.string = "#{@competitors.length}" end def query_complete(query) # If we haven't calculated the value yet, calculate it first. unless @complete calcCompletion end query.sortable = query.numerical = @complete # For the string output, we only use integer numbers. query.string = "#{@complete.to_i}%" end # Compute the cost generated by this Task for a given Account during a given # interval. If a Resource is provided as scopeProperty only the cost # directly generated by the resource is taken into account. def query_cost(query) if query.costAccount query.sortable = query.numerical = cost = turnover(query.startIdx, query.endIdx, query.costAccount, query.scopeProperty) query.string = query.currencyFormat.format(cost) else query.string = 'No \'balance\' defined!' end end # The duration of the task. After scheduling, it can be determined for # all tasks. Also for those who did not have a 'duration' attribute. def query_duration(query) query.sortable = query.numerical = duration = (@end - @start) / (60 * 60 * 24) query.string = query.scaleDuration(duration) end # The completed (as of 'now') effort allocated for the task in the # specified interval. In case a Resource is given as scope property only # the effort allocated for this resource is taken into account. def query_effortdone(query) if @effortdone effort = @project.convertToDailyLoad(@effortdone * @project['scheduleGranularity']) else # For this query, we always override the query period. effort = getEffectiveWork(@project.dateToIdx(@project['start'], false), @project.dateToIdx(@project['now']), query.scopeProperty) end query.sortable = query.numerical = effort query.string = query.scaleLoad(effort) end # The remaining (as of 'now') effort allocated for the task in the # specified interval. In case a Resource is given as scope property only # the effort allocated for this resource is taken into account. def query_effortleft(query) # For this query, we always override the query period. query.sortable = query.numerical = effort = getEffectiveWork(@project.dateToIdx(@project['now']), @project.dateToIdx(@project['end'], false), query.scopeProperty) query.string = query.scaleLoad(effort) end # The effort allocated for the task in the specified interval. In case a # Resource is given as scope property only the effort allocated for this # resource is taken into account. def query_effort(query) query.sortable = query.numerical = work = getEffectiveWork(query.startIdx, query.endIdx, query.scopeProperty) query.string = query.scaleLoad(work) end def query_followers(query) list = [] # First gather the task that depend on the start of this task. @startsuccs.each do |task, onEnd| if onEnd date = task['end', query.scenarioIdx].to_s(query.timeFormat) dep = "[->]" else date = task['start', query.scenarioIdx].to_s(query.timeFormat) dep = "[->[" end list << generateDepencyListItem(query, task, dep, date) end # Than add the tasks that depend on the end of this task. @endsuccs.each do |task, onEnd| if onEnd date = task['end', query.scenarioIdx].to_s(query.timeFormat) dep = "]->]" else date = task['start', query.scenarioIdx].to_s(query.timeFormat) dep = "]->[" end list << generateDepencyListItem(query, task, dep, date) end query.assignList(list) end def query_gauge(query) # If we haven't calculated the schedule status yet, calculate it first. calcGauge unless @gauge query.string = @gauge end # The number of different resources assigned to the task during the query # interval. Each resource is counted based on their mathematically rounded # efficiency. def query_headcount(query) headcount = 0 assignedResources(Interval.new(query.start, query.end)).each do |res| headcount += res['efficiency', @scenarioIdx].round end query.sortable = query.numerical = headcount query.string = query.numberFormat.format(headcount) end def query_inputs(query) inputList = PropertyList.new(@project.tasks, false) inputs(inputList, true) inputList.delete(@property) inputList.setSorting([['start', true, @scenarioIdx], ['seqno', true, -1 ]]) inputList.sort! query.assignList(generateTaskList(inputList, query)) end def query_maxend(query) queryDateLimit(query, @maxend) end def query_maxstart(query) queryDateLimit(query, @maxstart) end def query_minend(query) queryDateLimit(query, @minend) end def query_minstart(query) queryDateLimit(query, @minstart) end def query_opentasks(query) count = openTasks(query) query.sortable = query.numerical = count # For the string output, we only use integer numbers. query.string = "#{count.to_i}" end def query_precursors(query) list = [] # First gather the task that depend on the start of this task. @startpreds.each do |task, onEnd| if onEnd date = task['end', query.scenarioIdx].to_s(query.timeFormat) dep = "]->[" else date = task['start', query.scenarioIdx].to_s(query.timeFormat) dep = "[->[" end list << generateDepencyListItem(query, task, dep, date) end # Than add the tasks that depend on the end of this task. @endpreds.each do |task, onEnd| if onEnd date = task['end', query.scenarioIdx].to_s(query.timeFormat) dep = "]->]" else date = task['start', query.scenarioIdx].to_s(query.timeFormat) dep = "[->]" end list << generateDepencyListItem(query, task, dep, date) end query.assignList(list) end # A list of the resources that have been allocated to work on the task in # the report time frame. def query_resources(query) list = [] iv = TimeInterval.new(query.start, query.end) assignedResources(iv).each do |resource| if resource.allocated?(@scenarioIdx, iv, @property) if query.listItem rti = RichText.new(query.listItem, RTFHandlers.create(@project)). generateIntermediateFormat unless rti error('bad_resource_ts_query', "Syntax error in query statement for task attribute " + "'resources'.") end q = query.dup q.property = resource q.scopeProperty = @property rti.setQuery(q) list << "#{rti.to_s}" else list << "#{resource.name} (#{resource.fullId})" end end end query.assignList(list) end # Compute the revenue generated by this Task for a given Account during a # given interval. If a Resource is provided as scopeProperty only the # revenue directly generated by the resource is taken into account. def query_revenue(query) if query.revenueAccount query.sortable = query.numerical = revenue = turnover(query.startIdx, query.endIdx, query.revenueAccount, query.scopeProperty) query.string = query.currencyFormat.format(revenue) else query.string = 'No \'balance\' defined!' end end def query_scheduling(query) query.string = @forward ? 'ASAP' : 'ASAP' if @property.leaf? end def query_status(query) # If we haven't calculated the completion yet, calculate it first. calcStatus if @status.empty? query.string = @status end def query_targets(query) targetList = PropertyList.new(@project.tasks, false) targets(targetList, true) targetList.delete(@property) targetList.setSorting([['start', true, @scenarioIdx], ['seqno', true, -1 ]]) targetList.sort! query.assignList(generateTaskList(targetList, query)) end # Compute the total time _resource_ or all resources are allocated during # interval specified by _startIdx_ and _endIdx_. def getAllocatedTime(startIdx, endIdx, resource = nil) return 0.0 if @milestone || startIdx >= endIdx || (resource && !@assignedresources.include?(resource)) @dCache.cached(self, :TaskScenarioAllocatedTime, startIdx, endIdx, resource) do allocatedTime = 0.0 if @property.container? @property.kids.each do |task| allocatedTime += task.getAllocatedTime(@scenarioIdx, startIdx, endIdx, resource) end else if resource allocatedTime += resource.getAllocatedTime(@scenarioIdx, startIdx, endIdx, @property) else @assignedresources.each do |r| allocatedTime += r.getAllocatedTime(@scenarioIdx, startIdx, endIdx, @property) end end end allocatedTime end end # Compute the effective work a _resource_ or all resources do during the # interval specified by _startIdx_ and _endIdx_. The effective work is the # actual work multiplied by the efficiency of the resource. def getEffectiveWork(startIdx, endIdx, resource = nil) # Make sure we have the real Resource and not a proxy. resource = resource.ptn if resource return 0.0 if @milestone || startIdx >= endIdx || (resource && !@assignedresources.include?(resource)) @dCache.cached(self, :TaskScenarioEffectiveWork, startIdx, endIdx, resource) do workLoad = 0.0 if @property.container? @property.kids.each do |task| workLoad += task.getEffectiveWork(@scenarioIdx, startIdx, endIdx, resource) end else if resource workLoad += resource.getEffectiveWork(@scenarioIdx, startIdx, endIdx, @property) else @assignedresources.each do |r| workLoad += r.getEffectiveWork(@scenarioIdx, startIdx, endIdx, @property) end end end workLoad end end # Return a list of intervals that lay within _iv_ and are at least # minDuration long and contain no working time. def collectTimeOffIntervals(iv, minDuration) # This function is often called recursively for the same parameters. We # store the results in the cache to avoid repeated computations of the # same results. @dCache.cached(self, :TaskScenarioCollectTimeOffIntervals, iv, minDuration) do il = IntervalList.new il << TimeInterval.new(@project['start'], @project['end']) if @property.leaf? unless (resources = @assignedresources).empty? # The task has assigned resources, so we can use their common time # off intervals. resources.each do |resource| il &= resource.collectTimeOffIntervals(@scenarioIdx, iv, minDuration) end else # The task has no assigned resources. We simply use the global time # off intervals. il &= @project.collectTimeOffIntervals(iv, minDuration) end else @property.kids.each do |task| il &= task.collectTimeOffIntervals(@scenarioIdx, iv, minDuration) end end il end end # Check if the Task _task_ depends on this task. _depth_ specifies how # many dependent task are traversed at max. A value of 0 means no limit. # TODO: Change this to a non-recursive implementation. def isDependencyOf(task, depth, list = []) return true if task == @property # If this task is already in the list of traversed task, we can ignore # it. return false if list.include?(@property) list << @property @startsuccs.each do |t, onEnd| unless onEnd # must be a start->start dependency return true if t.isDependencyOf(@scenarioIdx, task, depth, list) end end # For task to depend on this task, the start of task must be after the # end of this task. if task['start', @scenarioIdx] && @end return false if task['start', @scenarioIdx] < @end end # Check if any of the parent tasks is a dependency of _task_. t = @property.parent while t # If the parent is a dependency, than all childs are as well. return true if t.isDependencyOf(@scenarioIdx, task, depth, list) t = t.parent end return false if depth == 1 @endsuccs.each do |ta, onEnd| unless onEnd # must be an end->start dependency return true if ta.isDependencyOf(@scenarioIdx, task, depth - 1, list) end end false end # If _task_ or any of its sub-tasks depend on this task or any of its # sub-tasks, we call this task a feature of _task_. def isFeatureOf(task) sources = @property.all destinations = task.all sources.each do |s| destinations.each do |d| return true if s.isDependencyOf(@scenarioIdx, d, 0) end end false end # Returns true of the _resource_ is assigned to this task or any of its # children. def hasResourceAllocated?(interval, resource) return false unless @assignedresources.include?(resource) if @property.leaf? return resource.allocated?(@scenarioIdx, interval, @property) else @property.kids.each do |t| return true if t.hasResourceAllocated?(@scenarioIdx, interval, resource) end end false end # Gather a list of Resource objects that have been assigned to the task # (including sub tasks) for the given Interval _interval_. def assignedResources(interval = nil) interval = Interval.new(a('start'), a('end')) unless interval list = [] if @property.container? @property.kids.each do |task| list += task.assignedResources(@scenarioIdx, interval) end list.uniq! else @assignedresources.each do |resource| if resource.allocated?(@scenarioIdx, interval, @property) list << resource end end end list end private def scheduleSlot # Tasks must always be scheduled in a single contigous fashion. # Depending on the scheduling direction the next slot must be scheduled # either right before or after this slot. If the current slot is not # directly aligned, we'll wait for another call with a proper slot. The # function returns false if the task has been completely scheduled. case @durationType when :effortTask bookResources if @doneEffort < @effort if @doneEffort >= @effort # The specified effort has been reached. The task has been fully # scheduled now. if @forward propagateDate(@project.idxToDate(@currentSlotIdx + 1), true, true) else propagateDate(@project.idxToDate(@currentSlotIdx), false, true) end return false end when :lengthTask bookResources # The doneLength is only increased for global working time slots. @doneLength += 1 if onShift?(@currentSlotIdx) # If we have reached the specified duration or lengths, we set the end # or start date and propagate the value to neighbouring tasks. if @doneLength >= @length if @forward propagateDate(@project.idxToDate(@currentSlotIdx + 1), true) else propagateDate(@project.idxToDate(@currentSlotIdx), false) end return false end when :durationTask # The doneDuration counts the number of scheduled slots. It is increased # by one with every scheduled slot. bookResources @doneDuration += 1 # If we have reached the specified duration or lengths, we set the end # or start date and propagate the value to neighbouring tasks. if @doneDuration >= @duration if @forward propagateDate(@project.idxToDate(@currentSlotIdx + 1), true) else propagateDate(@project.idxToDate(@currentSlotIdx), false) end return false end when :startEndTask # Task with start and end date but no duration criteria bookResources # Depending on the scheduling direction we can mark the task as # scheduled once we have reached the other end. if (@forward && @currentSlotIdx >= @endIdx) || (!@forward && @currentSlotIdx <= @startIdx) markAsScheduled @property.parents.each do |parent| parent.scheduleContainer(@scenarioIdx) end return false end else raise "Unknown task duration type #{@durationType}" end true end def bookResources # First check if there is any resource at all for this slot. return if !@project.anyResourceAvailable?(@currentSlotIdx) || (@projectionmode && (@nowIdx > @currentSlotIdx)) # If the task has resource independent allocation limits we need to make # sure that none of them is already exceeded. return unless limitsOk?(@currentSlotIdx) # If the task has shifts to limit the allocations, we check that we are # within a defined shift interval. If yes, we need to be on shift to # continue. if @shifts && @shifts.assigned?(@currentSlotIdx) return if !@shifts.onShift?(@currentSlotIdx) end # We first have to make sure that if there are mandatory resources # that these are all available for the time slot. takenMandatories = [] @mandatories.each do |allocation| return unless allocation.onShift?(@currentSlotIdx) # For mandatory allocations with alternatives at least one of the # alternatives must be available. found = false allocation.candidates(@scenarioIdx).each do |candidate| # When a resource group is marked mandatory, all members of the # group must be available. allAvailable = true candidate.allLeaves.each do |resource| if !limitsOk?(@currentSlotIdx, resource) || !resource.available?(@scenarioIdx, @currentSlotIdx) || takenMandatories.include?(resource) # We've found a mandatory resource that is not available for # the slot. allAvailable = false break else takenMandatories << resource end end if allAvailable found = true break end end # At least one mandatory resource is not available. We cannot continue. return unless found end @allocate.each do |allocation| next unless allocation.onShift?(@currentSlotIdx) # In case we have a persistent allocation we need to check if there # is already a locked resource and use it. locked_candidate = allocation.lockedResource if locked_candidate next if bookResource(locked_candidate) if allocation.atomic && locked_candidate.bookedTask(@scenarioIdx, @currentSlotIdx) rollbackBookings return end if @forward next if @currentSlotIdx < locked_candidate.getMaxSlot(@scenarioIdx) else next if @currentSlotIdx > locked_candidate.getMinSlot(@scenarioIdx) end # Persistent candidate is gone for the rest of the project! # Warn and assign somebody else, if available! warning('broken_persistence', "Persistence broken for Task #{@property.fullId} " + "- resource #{locked_candidate.name} is gone") allocation.lockedResource = nil end # Create a list of candidates in the proper order and # assign the first one available. allocation.candidates(@scenarioIdx).each do |candidate| if bookResource(candidate) allocation.lockedResource = candidate if allocation.persistent break end end end end def bookResource(resource) booked = false resource.allLeaves.each do |r| # Prevent overbooking when multiple resources are allocated and # available. If the task has allocation limits we need to make sure # that none of them is already exceeded. break if (@effort > 0 && r['efficiency', @scenarioIdx] > 0.0 && @doneEffort >= @effort) || !limitsOk?(@currentSlotIdx, r) if r.book(@scenarioIdx, @currentSlotIdx, @property) # This method is _very_ performance sensitive. Uncomment this log # message only if you really need it. #Log.msg { "Book #{resource.name} on task #{@property.fullId}" } # For effort based task we adjust the the start end (as defined by # the scheduling direction) to align with the first booked time # slot. if @effort > 0 && @doneEffort == 0 if @forward propagateDate(@project.idxToDate(@currentSlotIdx), false, true) Log.msg { "Task #{@property.fullId} first assignment: " + "#{period_to_s}" } else propagateDate(@project.idxToDate(@currentSlotIdx + 1), true, true) Log.msg { "Task #{@property.fullId} last assignment: " + "#{period_to_s}" } end end @doneEffort += r['efficiency', @scenarioIdx] unless @assignedresources.include?(r) @assignedresources << r end booked = true elsif (competitor = r.bookedTask(@scenarioIdx, @currentSlotIdx)) # Keep a list of all the Tasks that have successfully competed for # the same resources and are potentially delaying the progress of # this Task. @competitors << competitor unless @competitors.include?(competitor) @contendedResources[competitor][r] += 1 end end booked end def onShift?(sbIdx) if @shifts && @shifts.assigned?(sbIdx) return @shifts.onShift?(sbIdx) else return @project.isWorkingTime(sbIdx) end end # Check if all of the task limits are not exceded at the given _sbIdx_. If # a _resource_ is provided, the limit for that particular resource is # checked. If no resource is provided, only non-resource-specific limits # are checked. def limitsOk?(sbIdx, resource = nil) @allLimits.each do |limit| return false unless limit.ok?(sbIdx, true, resource) end true end # Limits do not take efficiency into account. Limits are usage limits, not # effort limits. def incLimits(sbIdx, resource = nil) @allLimits.each do |limit| limit.inc(sbIdx, resource) end end # Calculate the number of general working time slots between the TjTime # objects _d1_ and _d2_. def calcLength(d1, d2) slots = 0 while d1 < d2 slots += 1 if @project.isWorkingTime(d1) d1 += @project['scheduleGranularity'] end slots end # Register the user provided bookings with the Resource scoreboards. A # booking describes the assignment of a Resource to a certain Task for a # specified TimeInterval. def bookBookings firstSlotIdx = nil lastSlotIdx = nil if @effortdone || @effortleft # Force task to be scheduled in ASAP mode. @forward = true unless @start error('effort_done_left_start_missing', "Task #{@property.fullId} has 'effortdone' or 'effortleft' " + "attribute but no start date specified.") end unless @effort > 0 error('effort_missing', "Task #{@property.fullId} has 'effortdone' or " + "'effortleft' attribute but no 'effort'.") end if @effortdone if @effortdone > @effort error('effort_done_larger_effort', "Task #{@property.fullId} has larger 'effortdone' " + "than 'effort'.") end @doneEffort = @effortdone if @effortleft error('effort_done_and_left', "A task cannot have the 'effortdone' and 'effortleft' " + "attribute.") end else if @effortleft > @effort error('effort_left_larger_effort', "Task #{@property.fullId} has larger 'effortleft' " + "than 'effort'.") end @doneEffort = @effort - @effortleft end firstSlotIdx = @project.dateToIdx(@start) lastSlotIdx = @project.dateToIdx(@project['now']) end unless (bookings = findBookings).empty? if @effortdone || @effortleft error('bookings_and_effort', "Bookings cannot be used together with 'effortdone' or " + "'effortleft' attributes.") end bookings.each do |booking| unless booking.resource.leaf? error('booking_resource_not_leaf', "Booked resources may not be group resources", booking.sourceFileInfo) end unless @forward || @scheduled error('booking_forward_only', "Only forward scheduled tasks may have booking statements.") end booked = false booking.intervals.each do |interval| startIdx = @project.dateToIdx(interval.start, false) endIdx = @project.dateToIdx(interval.end, false) startIdx.upto(endIdx - 1) do |idx| if booking.resource.bookBooking(@scenarioIdx, idx, booking) # Booking was successful for this time slot. @doneEffort += booking.resource['efficiency', @scenarioIdx] booked = true # Store the indexes of the first slot and the slot after the # last slot. firstSlotIdx = idx if !firstSlotIdx || firstSlotIdx > idx lastSlotIdx = idx if !lastSlotIdx || lastSlotIdx < idx end end end if booked && !@assignedresources.include?(booking.resource) @assignedresources << booking.resource end end end # For effort based tasks, or tasks without a start date, with bookings # that have not yet been marked as scheduled we set the start date to # the date of the first booked slot. if (@start.nil? || (@doneEffort > 0 && @effort > 0)) && !@scheduled && firstSlotIdx firstSlotDate = @project.idxToDate(firstSlotIdx) if @start.nil? || firstSlotDate > @start @start = firstSlotDate Log.msg { "Task #{@property.fullId} first booking: #{period_to_s}" } end end # Check if the the duration criteria has already been reached by the # supplied bookings and set the task end to the last booked slot. # Also the task is marked as scheduled. if lastSlotIdx && !@scheduled tentativeEnd = @project.idxToDate(lastSlotIdx + 1) slotDuration = @project['scheduleGranularity'] if @effort > 0 if @doneEffort >= @effort @end = tentativeEnd markAsScheduled end elsif @length > 0 @doneLength = 0 startIdx = @project.dateToIdx(date = @start) endIdx = @project.dateToIdx(@project['now']) startIdx.upto(endIdx) do |idx| @doneLength += 1 if onShift?(idx) date += slotDuration # Continue not only until the @length has been reached, but also # the tentativeEnd date. This allows us to detect overbookings. if @doneLength >= @length && date >= tentativeEnd endDate = @project.idxToDate(idx + 1) @end = [ endDate, tentativeEnd ].max markAsScheduled break end end elsif @duration > 0 @doneDuration = ((tentativeEnd - @start) / slotDuration).to_i if @doneDuration >= @duration @end = tentativeEnd markAsScheduled elsif @duration * slotDuration < (@project['now'] - @start) # This handles the case where the bookings don't provide enough # @doneDuration to reach @duration, but the now date would be # after the @start + @duration date. @end = @start + @duration * slotDuration markAsScheduled end end end # If the task has bookings, we assume that the bookings describe all # work up to the 'now' date. if @doneEffort > 0 @currentSlotIdx = @project.dateToIdx(@project['now']) end # Finally, we check if the bookings caused more effort, length or # duration than was requested by the user. This is only flagged as a # warning. if @effort > 0 effort = @project.slotsToDays(@doneEffort) effortHours = effort * @project['dailyworkinghours'] requestedEffort = @project.slotsToDays(@effort) requestedEffortHours = requestedEffort * @project['dailyworkinghours'] if effort > requestedEffort warning('overbooked_effort', "The total effort (#{effort}d or #{effortHours}h) of the " + "provided bookings for task #{@property.fullId} exceeds " + "the specified effort of #{requestedEffort}d or " + "#{requestedEffortHours}h.") end end if @length > 0 && @doneLength > @length length = @project.slotsToDays(@doneLength) requestedLength = @project.slotsToDays(@length) warning('overbooked_length', "The total length (#{length}d) of the provided bookings " + "for task #{@property.fullId} exceeds the specified length " + "of #{requestedLength}d.") end if @duration > 0 && @doneDuration > @duration duration = @doneDuration * @project['scheduleGranularity'] / (60.0 * 60 * 24) requestedDuration = @duration * @project['scheduleGranularity'] / (60.0 * 60 * 24) warning('overbooked_duration', "The total duration (#{duration}d) of the provided bookings " + "for task #{@property.fullId} exceeds the specified duration " + "of #{requestedDuration}d.") end # If a task has only bookings but no effort, length or duration and is # not a milestone but marked as being scheduled, we set the start and # end date according to the first/last booking date unless the date has # been set already. if @scheduled && @effort == 0 && @length == 0 && @duration == 0 && !@milestone unless @start || !firstSlotIdx @start = @project.idxToDate(firstSlotIdx) end unless @end || !lastSlotIdx @end = @project.idxToDate(lastSlotIdx + 1) end end end def rollbackBookings @doneEffort = 0.0 @allocate.each do |allocation| allocation.lockedResource = nil allocation.candidates(@scenarioIdx).each do |resource| resource.allLeaves.each do |r| r.rollbackBookings(@scenarioIdx, @property) end end end end # This function checks if the task has a dependency on another task or # fixed date for a certain end. If +atEnd+ is true, the task end will be # checked. Otherwise the start. def hasDependencies(atEnd) thisEnd = atEnd ? 'end' : 'start' !a(thisEnd + 'succs').empty? || !a(thisEnd + 'preds').empty? end # Return true if this task or any of its parent tasks has at least one # predecessor task. def hasPredecessors t = @property while t return true unless t['startpreds', @scenarioIdx].empty? t = t.parent end false end # Return true if this task or any of its parent tasks has at least one # sucessor task. def hasSuccessors t = @property while t return true unless t['endsuccs', @scenarioIdx].empty? t = t.parent end false end # Return true if the task has a 'strong' dependency at the start if # _atEnd_ is false or at the end. A 'strong' dependency is an outer # dependency. At the start a predecessor is strong, and the end a # successor. start successors or end predecessors are considered weak # dependencies since this task will always have to get the date first and # then pass it on to the inner dependencies. def hasStrongDeps?(atEnd) if atEnd return !@endsuccs.empty? else return !@startpreds.empty? end end def markAsRunaway @isRunAway = true remainingEffort = @project.convertToDailyLoad(@project['scheduleGranularity'] * (@effort - @doneEffort)) warning('runaway', "#{remainingEffort}d of effort of task " + "#{@property.fullId} " + "does not fit into the project time frame. ") unless @competitors.empty? multi = @competitors.length > 1 info('runaway_tasks', "The following task#{multi ? 's' : ''} " + "compete#{multi ? '' : 's'} for the same resources that " + "this task is requesting: ") @competitors.each do |t| res = @contendedResources[t].to_a res.sort! { |i, j| j[1] <=> i[1] } resList = res.map { |r| "#{r[0].id}(#{r[1]})" }.join(', ') info('runaway_competitor', "Task #{t.fullId} has conflicts for the following " + "resource#{res.length > 1 ? 's' : ''}: #{resList}", t.sourceFileInfo) end end end # This function determines if a task is a milestones and marks it # accordingly. def markAsMilestone # Containers may not be milestones if @milestone && @property.container? error('container_milestone', "Container task #{@property.fullId} may not be marked " + "as a milestone.") end return if @property.container? || @hasDurationSpec || !@booking.empty? || !@allocate.empty? # The following cases qualify for an automatic milestone promotion. # - --> - # | --> - # |D --> - # -D --> - # - <-- - # - <-- | # - <-- -D # - <-- |D hasStartSpec = !@start.nil? || !@depends.empty? hasEndSpec = !@end.nil? || !@precedes.empty? @milestone = (hasStartSpec && @forward && !hasEndSpec) || (!hasStartSpec && !@forward && hasEndSpec) || (!hasStartSpec && !hasEndSpec) # Milestones may only have start or end date even when the 'scheduled' # attribute is set. For further processing, we need to add the missing # date. if @milestone @hasDurationSpec = true @end = @start if @start && !@end @start = @end if !@start && @end Log.msg { "Mark as milestone #{@property.fullId}" } end end def checkDependency(dependency, depType) depList = instance_variable_get(('@' + depType).intern) if (depTask = dependency.resolve(@project)).nil? # Remove the broken dependency. It could cause trouble later on. depList.delete(dependency) error('task_depend_unknown', "Task #{@property.fullId} has unknown #{depType} " + "#{dependency.taskId}") end if depTask == @property # Remove the broken dependency. It could cause trouble later on. depList.delete(dependency) error('task_depend_self', "Task #{@property.fullId} cannot " + "depend on self") end if depTask.isChildOf?(@property) # Remove the broken dependency. It could cause trouble later on. depList.delete(dependency) error('task_depend_child', "Task #{@property.fullId} cannot depend on child " + "#{depTask.fullId}") end if @property.isChildOf?(depTask) # Remove the broken dependency. It could cause trouble later on. depList.delete(dependency) error('task_depend_parent', "Task #{@property.fullId} cannot depend on parent " + "#{depTask.fullId}") end depList.each do |dep| if dep.task == depTask && !dep.equal?(dependency) # Remove the broken dependency. It could cause trouble later on. depList.delete(dependency) error('task_depend_multi', "No need to specify dependency #{depTask.fullId} multiple " + "times for task #{@property.fullId}.") end end depTask end # Set @startIsDetermed or @endIsDetermed (depending on _setStart) to # _value_. def setDetermination(setStart, value) setStart ? @startIsDetermed = value : @endIsDetermed = value end # This function is called to propagate the start or end date of the # current task to a dependend Task +task+. If +atEnd+ is true, the date # should be propagated to the end of the +task+, otherwise to the start. def propagateDateToDep(task, atEnd) #puts "Propagate #{atEnd ? 'end' : 'start'} to dep. #{task.fullId}" # Don't propagate if the task is already completely scheduled or is a # container. return if task['scheduled', @scenarioIdx] || task.container? # Don't propagate if the task already has a date for that end. return unless task[atEnd ? 'end' : 'start', @scenarioIdx].nil? # Don't propagate if the task has a duration or is a milestone and the # task end to set is in the scheduling direction. return if task.hasDurationSpec(@scenarioIdx) && !(atEnd ^ task['forward', @scenarioIdx]) # Check if all other dependencies for that task end have been determined # already and use the latest or earliest possible date. Don't propagate # if we don't have all dates yet. return if (nDate = (atEnd ? task.latestEnd(@scenarioIdx) : task.earliestStart(@scenarioIdx))).nil? # Looks like it is ok to propagate the date. task.propagateDate(@scenarioIdx, nDate, atEnd) #puts "Propagate #{atEnd ? 'end' : 'start'} to dep. #{task.fullId} done" end # This is a helper function for calcPathCriticalness(). It computes the # larges criticalness of the pathes through the end-successors of this task # and all its parent tasks. def calcPathCriticalnessEndSuccs maxCriticalness = 0.0 # Gather a list of all end-successors of this task and its parent task. tList = [] depStruct = Struct.new(:task, :onEnd) p = @property while (p) p['endsuccs', @scenarioIdx].each do |task, onEnd| dep = depStruct.new(task, onEnd) tList << dep unless tList.include?(dep) end p = p.parent end tList.each do |dep| criticalness = dep.task.calcPathCriticalness(@scenarioIdx, dep.onEnd) maxCriticalness = criticalness if criticalness > maxCriticalness end maxCriticalness end # Calculate the current completion degree for tasks that have no user # specified completion value. def calcCompletion # If we already have a value for @complete, we don't need to calculate # anything. return @complete if @complete # We cannot compute a completion degree without a start or end date. if @start.nil? || @end.nil? @complete = 0.0 return nil end @complete = calcTaskCompletion end def calcTaskCompletion completion = 0.0 if @property.container? # For container task the completion degree is the average of the # sub tasks. @property.kids.each do |child| return nil unless (comp = child.calcCompletion(@scenarioIdx)) completion += comp end completion /= @property.kids.length else # For leaf tasks we first compare the start and end dates against the # current date. if @end <= @project['now'] # The task has ended already. It's 100% complete. completion = 100.0 elsif @project['now'] <= @start # The task has not started yet. Its' 0% complete. completion = 0.0 elsif @effort > 0 # Effort based leaf tasks. The completion degree is the percentage # of effort that has been done already. done = getEffectiveWork(@project.dateToIdx(@start, false), @project.dateToIdx(@project['now'])) total = @project.convertToDailyLoad( @effort * @project['scheduleGranularity']) completion = done / total * 100.0 else # Length/duration leaf tasks. completion = ((@project['now'] - @start) / (@end - @start)) * 100.0 end end completion end # Calculate the status of the task based on the 'complete' attribute. def calcStatus # If the completion degree is not yet available, we need to calculate it # first. calcCompletion unless @complete if @complete @status = if @complete == 0.0 # Milestones are reached, normal tasks started. @milestone ? 'not reached' : 'not started' elsif @complete >= 100.0 'done' else 'in progress' end else # The completion degree could not be calculated due to errors. We set # the state to unknown. @status = 'unknown' end end # The gauge shows if a task is ahead, behind or on schedule. The measure # is based on the provided 'complete' value and the current date. def calcGauge # If the completion degree is not yet available, we need to calculate it # first. calcCompletion unless @complete return @gauge if @gauge if @property.container? states = [ 'on schedule', 'ahead of schedule', 'behind schedule', 'unknown' ] gauge = 0 @property.kids.each do |child| if (idx = states.index(child.calcGauge(@scenarioIdx))) > gauge gauge = idx end end @gauge = states[gauge] else @gauge = if (calculatedComplete = calcTaskCompletion).nil? # The completion degree could not be calculated due to errors. We # set the state to unknown. 'unknown' elsif @complete == calculatedComplete 'on schedule' elsif @complete < calculatedComplete 'behind schedule' else 'ahead of schedule' end end end def activeTasks(query) return 0 unless TimeInterval.new(@start, @end). overlaps?(TimeInterval.new(query.start, query.end)) if @property.leaf? now = @project['now'] return @start <= now && now < @end ? 1 : 0 else cnt = 0 @property.kids.each do |task| cnt += task.closedTasks(@scenarioIdx, query) end return cnt end end def closedTasks(query) return 0 unless TimeInterval.new(@start, @end). overlaps?(TimeInterval.new(query.start, query.end)) if @property.leaf? return @end <= @project['now'] ? 1 : 0 else cnt = 0 @property.kids.each do |task| cnt += task.closedTasks(@scenarioIdx, query) end return cnt end end def openTasks(query) return 0 unless TimeInterval.new(@start, @end). overlaps?(TimeInterval.new(query.start, query.end)) if @property.leaf? return @end > @project['now'] ? 1 : 0 else cnt = 0 @property.kids.each do |task| cnt += task.openTasks(@scenarioIdx, query) end return cnt end end # Recursively compile a list of Task properties which depend on the # current task. def inputs(foundInputs, includeChildren, checkedTasks = {}) # Ignore tasks that we have already included in the checked tasks list. taskSignature = [ @property, includeChildren ] return if checkedTasks.include?(taskSignature) checkedTasks[taskSignature] = true # An "input" must be a leaf task that has no direct or indirect (through # parent) following tasks. Only milestones are recognized as inputs. if @property.leaf? && !hasPredecessors && @milestone foundInputs << @property return end # We also include inputs of child tasks if requested. The recursive # iteration of child tasks is limited to the tested task only. The # predecessors children are not iterated. (see further below) if includeChildren @property.kids.each do |child| child.inputs(@scenarioIdx, foundInputs, true, checkedTasks) end end # Now check the direct predecessors. @startpreds.each do |t, onEnd| t.inputs(@scenarioIdx, foundInputs, false, checkedTasks) end # Check for indirect predecessors inherited from the ancestors. if @property.parent @property.parent.inputs(@scenarioIdx, foundInputs, false, checkedTasks) end end # Recursively compile a list of Task properties which depend on the # current task. def targets(foundTargets, includeChildren, checkedTasks = {}) # Ignore tasks that we have already included in the checked tasks list. taskSignature = [ @property, includeChildren ] return if checkedTasks.include?(taskSignature) checkedTasks[taskSignature] = true # A target must be a leaf function that has no direct or indirect # (through parent) following tasks. Only milestones are recognized as # targets. if @property.leaf? && !hasSuccessors && @milestone foundTargets << @property return end @endsuccs.each do |t, onEnd| t.targets(@scenarioIdx, foundTargets, false, checkedTasks) end # Check for indirect followers. if @property.parent @property.parent.targets(@scenarioIdx, foundTargets, false, checkedTasks) end # Also include targets of child tasks. The recursive iteration of child # tasks is limited to the tested task only. The followers are not # iterated. if includeChildren @property.kids.each do |child| child.targets(@scenarioIdx, foundTargets, true, checkedTasks) end end end # Compute the turnover generated by this Task for a given Account _account_ # during the interval specified by _startIdx_ and _endIdx_. These can either # be TjTime values or Scoreboard indexes. If a Resource _resource_ is given, # only the turnover directly generated by the resource is taken into # account. def turnover(startIdx, endIdx, account, resource = nil, includeKids = true) amount = 0.0 if @property.container? && includeKids @property.kids.each do |child| amount += child.turnover(@scenarioIdx, startIdx, endIdx, account, resource) end end # If we are evaluating the task in the context of a specific resource, # we use the chargeset of that resource, not the chargeset of the task. chargeset = resource ? resource['chargeset', @scenarioIdx] : @chargeset # If there are no chargeset defined for this task, we don't need to # compute the resource related or other cost. unless chargeset.empty? resourceCost = 0.0 otherCost = 0.0 # Container tasks don't have resource cost. unless @property.container? if resource resourceCost = resource.cost(@scenarioIdx, startIdx, endIdx, @property) else @assignedresources.each do |r| resourceCost += r.cost(@scenarioIdx, startIdx, endIdx, @property) end end end unless @charge.empty? # Add one-time and periodic charges to the amount. startDate = startIdx.is_a?(TjTime) ? startIdx : @project.idxToDate(startIdx) endDate = endIdx.is_a?(TjTime) ? endIdx : @project.idxToDate(endIdx) iv = TimeInterval.new(startDate, endDate) @charge.each do |charge| otherCost += charge.turnover(iv) end end totalCost = resourceCost + otherCost # Now weight the total cost by the share of the account chargeset.each do |set| set.each do |accnt, share| if share > 0.0 && (accnt == account || accnt.isChildOf?(account)) amount += totalCost * share end end end end amount end def generateDepencyListItem(query, task, dep, date) if query.listItem rti = RichText.new(query.listItem, RTFHandlers.create(@project)). generateIntermediateFormat q = query.dup q.property = task q.setCustomData('dependency', { :string => dep }) q.setCustomData('date', { :string => date }) rti.setQuery(q) "#{rti.to_s}" else "#{task.name} (#{task.fullId}) #{dep} #{date}" end end def generateTaskList(taskList, query) list = [] taskList.each do |task| date = task['start', @scenarioIdx]. to_s(@property.project['timeFormat']) if query.listItem rti = RichText.new(query.listItem, RTFHandlers.create(@project)). generateIntermediateFormat q = query.dup q.property = task q.setCustomData('date', { :string => date }) rti.setQuery(q) list << "#{rti.to_s}" else list << "#{task.name} (#{task.fullId}) #{date}" end end list end def findBookings # Map the index back to the Scenario object. scenario = @property.project.scenario(@scenarioIdx) # Check if the current scenario should inherit its bookings from the # parent. If so, redirect 'scenario' to the parent. The top-level # scenario can never inherit bookings. while !scenario.get('ownbookings') do scenario = scenario.parent end # Return the bookings of the found scenario. @property['booking', @property.project.scenarioIdx(scenario)] end # Date limits may be nil and this is not an error. TjTime.to_s() would # report it as such if we don't use this wrapper method. def queryDateLimit(query, date) if date query.sortable = query.numerical = date query.string = date.to_s(query.timeFormat) else query.sortable = query.numerical = -1 query.string = '' end end def period_to_s "#{@start ? @start.to_s : ''} -> #{@end ? @end.to_s : ''}" end end end TaskJuggler-3.8.1/lib/taskjuggler/TernarySearchTree.rb000066400000000000000000000136421473026623400227710ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = TernarySearchTree.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/UTF8String' class TaskJuggler # Classical ternary search tree implementation. It can store any list # objects who's elements are comparable. These are usually String or Array # objects. Common elements (by value and index) are only stored once which # makes it fairly efficient for large lists that have similar start # sequences. It also provides a fast find method. class TernarySearchTree # Create a new TernarySearchTree object. The optional _arg_ can be an # element to store in the new tree or a list of elements to store. def initialize(arg = nil) clear if arg.nil? return elsif arg.is_a?(Array) sortForBalancedTree(arg).each { |elem| insert(elem) } else insert(arg) if arg end end # Stores _str_ in the tree. _index_ is for internal use only. def insert(str, index = 0) if str.nil? || str.empty? raise ArgumentError, "Cannot insert nil or empty lists" end if index > (maxIdx = str.length - 1) || index < 0 raise ArgumentError, "index out of range [0..#{maxIdx}]" end @value = str[index] unless @value if str[index] < @value @smaller = TernarySearchTree.new unless @smaller @smaller.insert(str, index) elsif str[index] > @value @larger = TernarySearchTree.new unless @larger @larger.insert(str, index) else if index == maxIdx @last = true else @equal = TernarySearchTree.new unless @equal @equal.insert(str, index + 1) end end end alias << insert # Insert the elements of _list_ into the tree. def insertList(list) list.each { |val| insert(val) } end # if _str_ is stored in the tree it returns _str_. If _partialMatch_ is # true, it returns all items that start with _str_. _index_ is for # internal use only. If nothing is found it returns either nil or an empty # list. def find(str, partialMatch = false, index = 0) return nil if str.nil? || index > (maxIdx = str.length - 1) if str[index] < @value return @smaller.find(str, partialMatch, index) if @smaller elsif str[index] > @value return @larger.find(str, partialMatch, index) if @larger else if index == maxIdx # We've reached the end of the search pattern. if partialMatch # The strange looking ('' << val) is for Ruby 1.8 compatibility. return collect { |v| str[0..-2] + ('' << v) } else return str if @last end end return @equal.find(str, partialMatch, index + 1) if @equal end nil end alias [] find # Returns the number of elements in the tree. def length result = 0 result += @smaller.length if @smaller result += 1 if @last result += @equal.length if @equal result += @larger.length if @larger result end # Return the maximum depth of the tree. def maxDepth(depth = 0) depth += 1 depths = [] depths << @smaller.maxDepth(depth) if @smaller depths << @equal.maxDepth(depth) if @equal depths << @larger.maxDepth(depth) if @larger depths << depth if @last depths.max end # Invokes _block_ for each element and returns the results as an Array. def collect(str = nil, &block) result = [] result += @smaller.collect(str, &block) if @smaller # The strange looking ('' << val) is for Ruby 1.8 compatibility. newStr = str.nil? ? ('' << @value) : str + ('' << @value) result << yield(newStr) if @last result += @equal.collect(newStr, &block) if @equal result += @larger.collect(str, &block) if @larger result end # Return an Array with all the elements stored in the tree. def to_a collect{ |x| x} end # Balance the tree for more effective data retrieval. def balance! list = sortForBalancedTree(to_a) clear list.each { |x| insert(x) } end # Return a balanced version of the tree. def balanced TernarySearchTree.new(to_a) end def inspect(prefix = ' ', indent = 0) puts "#{' ' * indent}#{prefix} #{@value} #{@last ? '!' : ''}" @smaller.inspect('<', indent + 2) if @smaller @equal.inspect('=', indent + 2) if @equal @larger.inspect('>', indent + 2) if @larger end private # Reset the node to an empty tree. def clear @smaller = @equal = @larger = @value = nil @last = false end # Split the list into the first element and the remaining ones. def split(str) # The list may not be nil or empty. This would be a bug. raise ArgumentError if str.nil? || str.empty? # The second element of the result may be nil. [ str[0], str[1..-1] ] end # Reorder the list elements so that we get a fully balanced tree when # inserting the elements from front to back. def sortForBalancedTree(list) lists = [ list.sort ] result = [] while !lists.empty? newLists = [] lists.each do |l| # Split the list in half and add the center element to the result # list. pivot = l.length / 2 result << l[pivot] # Add the two remaining sub lists to the newLists Array. newLists << l[0..pivot - 1] if pivot > 0 newLists << l[pivot + 1..-1] if pivot < l.length - 1 end lists = newLists end result end end end TaskJuggler-3.8.1/lib/taskjuggler/TextFormatter.rb000066400000000000000000000127661473026623400222150ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = TextFormatter.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/UTF8String' class TaskJuggler # This class provides a simple text block formatting function. Plain text # can be indented and limited to a given text width. class TextFormatter attr_accessor :indentation, :width, :firstLineIndent def initialize(width = 80, indentation = 0, firstLineIndent = nil) # The width of the text including the indent. @width = width # The indent for the first line of a paragraph @firstLineIndent = firstLineIndent || indentation # The indent for other lines. @indentation = indentation end # Add @indentation number of spaces at the beginning of each line. The # first line will be indented by @firstLineIndent. Lines that are longer # than @width will be clipped. def indent(str) out = '' # Indentation to be used for the currently processed line. It will be # set to nil if it was inserted already. indentBuf = ' ' * @firstLineIndent linePos = 0 # Process the input String from start to finish. str.each_utf8_char do |c| if c == "\n" # To prevent trailing white spaces we only insert a line break # instead of the indent buffer. if indentBuf out += "\n" end # The indent buffer for the next line. indentBuf = "\n" + ' ' * @indentation else # If we still have an indent buffer, we need to insert it first. if indentBuf out += indentBuf linePos = indentBuf.delete("\n").length indentBuf = nil end # Discard all characters that extend of the requested line width. if linePos < @width out << c linePos += 1 end end end # Always end with a line break out += "\n" unless out[-1] == "\n" out end # Format the String _str_ according to the class settings. def format(str) # The resulting String. @out = '' # The column of the last character of the current line. @linePos = 0 # A buffer for the currently processed word. @wordBuf = '' # True of we are at the beginning of a line. @beginOfLine = true # A buffer for the indentation to be used for the next line. @indentBuf = ' ' * @firstLineIndent # The status of the state machine. state = :beginOfParagraph # Process the input String from start to finish. str.each_utf8_char do |c| case state when :beginOfParagraph # We are currently a the beginning of a new paragraph. if c == ' ' || c == "\n" # ignore it else # A new word started. @wordBuf << c state = :inWord end when :inWord # We are in the middle of processing a word. if c == ' ' || c == "\n" # The word has ended. appendWord state = c == ' ' ? :betweenWords : :betweenWordsOrLines elsif c == "\r" # CR is used to start a new line but without starting a new # paragraph. appendWord @indentBuf = "\n" + ' ' * @firstLineIndent @beginOfLine = true state = :betweenWords else # Add the character to the word buffer. @wordBuf << c end when :betweenWords # We are in between words. if c == ' ' # ignore it elsif c == "\n" state = :betweenWordsOrLines else # A new word started. @wordBuf << c state = :inWord end when :betweenWordsOrLines if c == "\n" # The word break is really a paragraph break. @indentBuf = "\n\n" + ' ' * @firstLineIndent @beginOfLine = true state = :beginOfParagraph elsif c == ' ' state = :betweenWords else @wordBuf << c state = :inWord end else raise "Unknown state in state machine: #{state}" end end # Add any still pending word. appendWord # Always end with a line break @out += "\n" unless @out[-1] == "\n" @out end private def appendWord # Ignore empty words. wordLength = @wordBuf.length return unless wordLength > 0 # If the word does not fit into the current line anymore, we have to # start a new line. @beginOfLine = true if @linePos + 1 + wordLength > @width if @beginOfLine # Insert the content of the @indentBuf and reset @linePos and # @indentBuf. @out += @indentBuf @linePos = @indentBuf.delete("\n").length @indentBuf = "\n" + ' ' * @indentation else # Insert a space to separate the words. @out += ' ' @linePos += 1 end # Append the word and reset the @wordBuf. @out += @wordBuf @wordBuf = '' @linePos += wordLength @beginOfLine = false if @beginOfLine end end end TaskJuggler-3.8.1/lib/taskjuggler/TextParser.rb000066400000000000000000000421351473026623400214770ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = TextParser.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/TextParser/Pattern' require 'taskjuggler/TextParser/Rule' require 'taskjuggler/TextParser/StackElement' require 'taskjuggler/MessageHandler' require 'taskjuggler/TjException' require 'taskjuggler/Log' class TaskJuggler # The TextParser implements a somewhat modified LL(1) parser. It uses a # dynamically compiled state machine. Dynamically means, that the syntax can # be extended during the parse process. This allows support for languages # that can extend their syntax during the parse process. The TaskJuggler # syntax is such an beast. # # This class is just a base class. A complete parser would derive from this # class and implement the rule set and the functions _nextToken()_ and # _returnToken()_. It also needs to set the array _variables_ to declare all # variables ($SOMENAME) that the scanner may deliver. # # To describe the syntax the functions TextParser#pattern, TextParser#optional # and TextParser#repeatable can be used. When the rule set is changed during # parsing, TextParser#updateParserTables must be called to make the changes # effective. The parser can also document the syntax automatically. To # document a pattern, the functions TextParser#doc, TextParser#descr, # TextParser#also and TextParser#arg can be used. # # In contrast to conventional LL grammars, we use a slightly improved syntax # descriptions. Repeated patterns are not described by recursive call but we # use a repeat flag for syntax rules that consists of repeatable patterns. # This removes the need for recursion elimination when compiling the state # machine and makes the syntax a lot more readable. However, it adds a bit # more complexity to the state machine. Optional patterns are described by # a rule flag, not by adding an empty pattern. # # To start parsing the input the function TextParser#parse needs to be called # with the name of the start rule. class TextParser include MessageHandler # Utility class so that we can distinguish Array results from the Array # containing the results of a repeatable rule. We define some merging # method with a slightly different behaviour. class TextParserResultArray < Array def initialize super end # If there is a repeatable rule that contains another repeatable loop, the # result of the inner rule is an Array that gets put into another Array by # the outer rule. In this case, the inner Array can be merged with the # outer Array. def <<(arg) if arg.is_a?(TextParserResultArray) self.concat(arg) else super end end end attr_reader :rules # Create a new TextParser object. def initialize # This Hash will store the ruleset that the parser is operating on. @rules = { } # Array to hold the token types that the scanner can return. @variables = [] # An list of token types that are not allowed in the current context. # For performance reasons we use a hash with the token as key. The value # is irrelevant. @blockedVariables = {} # The currently processed rule. @cr = nil @states = {} # The stack used by the FSM. @stack = nil end # Limit the allowed tokens of the scanner to the subset passed by the # _tokenSet_ Array. def limitTokenSet(tokenSet) return unless tokenSet # Create a copy of all supported variables. blockedVariables = @variables.dup # Then delete all that are in the limited set. blockedVariables.delete_if { |v| tokenSet.include?(v) } # And convert the list into a Hash for faster lookups. @blockedVariables = {} blockedVariables.each { |v| @blockedVariables[v] = true } end # Call all methods that start with 'rule_' to initialize the rules. def initRules methods.each do |m| if m[0, 5] == 'rule_' # Create a new rule with the suffix of the function name as name. newRule(m[5..-1]) # Call the function. send(m) end end end # Add a new rule to the rule set. _name_ must be a unique identifier. The # function also sets the class variable @cr to the new rule. Subsequent # calls to TextParser#pattern, TextParser#optional or # TextParser#repeatable will then implicitly operate on the most recently # added rule. def newRule(name) # Use a symbol instead of a String. name = name.intern raise "Fatal Error: Rule #{name} already exists" if @rules.has_key?(name) if block_given? saveCr = @cr @rules[name] = @cr = TextParser::Rule.new(name) yield @cr = saveCr else @rules[name] = @cr = TextParser::Rule.new(name) end end # Add a new pattern to the most recently added rule. _tokens_ is an array of # strings that specify the syntax elements of the pattern. Each token must # start with an character that identifies the type of the token. The # following types are supported. # # * ! a reference to another rule # * $ a variable token as delivered by the scanner # * _ a literal token. # # _func_ is a Proc object that is called whenever the parser has completed # the processing of this rule. def pattern(tokens, func = nil) @cr.addPattern(TextParser::Pattern.new(tokens, func)) end # Identify the patterns of the most recently added rule as optional syntax # elements. def optional @cr.setOptional end # Identify the patterns of the most recently added rule as repeatable syntax # elements. def repeatable @cr.setRepeatable end # This function needs to be called whenever new rules or patterns have been # added and before the next call to TextParser#parse. It's perfectly ok to # call this function from within a parse() call as long as the states that # are currently on the stack have not been modified. def updateParserTables saveFsmStack # Invalidate some cached data. @rules.each_value { |rule| rule.flushCache } @states = {} # Generate the parser states for all patterns of all rules. @rules.each_value do |rule| rule.generateStates(@rules).each do |s| @states[[ s.rule, s.pattern, s.index ]] = s end checkRule(rule) end # Compute the transitions between the generated states. @states.each_value do |state| state.addTransitions(@states, @rules) end restoreFsmStack end # To parse the input this function needs to be called with the name of the # rule to start with. It returns the result of the processing function of # the top-level parser rule that was specified by _ruleName_. In case of # an error, the result is false. def parse(ruleName) @stack = [] @@expectedTokens = [] begin result = parseFSM(@rules[ruleName]) rescue TjException => msg if msg.message && !msg.message.empty? critical('parse', msg.message) end return false end result end # Return the SourceFileInfo of the TextScanner at the beginning of the # currently processed TextParser::Rule. Or return nil if we don't have a # current position. def sourceFileInfo return @scanner.sourceFileInfo if @stack.nil? || @stack.length <= 1 @stack.last.firstSourceFileInfo end def error(id, text, sfi = nil, data = nil) sfi ||= sourceFileInfo if @scanner # The scanner has some more context information, so we pass the error # on to the TextScanner. @scanner.error(id, text, sfi, data) else error(id, text, sfi, data) end end def warning(id, text, sfi = nil, data = nil) sfi ||= sourceFileInfo if @scanner # The scanner has some more context information, so we pass the # warning on to the TextScanner. @scanner.warning(id, text, sfi, data) else warning(id, text, sfi, data) end end private def checkRule(rule) if rule.patterns.empty? raise "Rule #{rule.name} must have at least one pattern" end rule.patterns.each do |pat| pat.each do |type, name| if type == :variable if @variables.index(name).nil? error('unsupported_token', "The token #{name} is not supported here.") end elsif type == :reference if @rules[name].nil? raise "Fatal Error: Reference to unknown rule #{name} in " + "pattern '#{pat}' of rule #{rule.name}" end end end end end def parseFSM(rule) unless (state = @states[[ rule, nil, 0 ]]) error("no_start_state", "No start state for rule #{rule.name} found") end @stack = [ TextParser::StackElement.new(nil, state) ] loop do if state.transitions.empty? # The final states of each pattern have no pre-compiled transitions. # For such a state, we don't need to get a new token. transition = token = nil else transition = state.transition(token = getNextToken) end # If we have looped-back we need to finish the pattern first. Final # tokens of repeatable rules do have transitions! if transition && transition.loopBack finishPattern(token) transition = state.transition(token = getNextToken) end if transition # Shift: This is for normal state transitions. This may be from one # token of a pattern to the next token of the same pattern or to the # start of a new pattern. The transition tells us what state we have # to process next. state = transition.state # Transitions that enter rules generate states which we need to # resume at when a rule has been completely processed. We push this # list of states on the @stack. stackElement = @stack.last first = true transition.stateStack.each do |s| checkForOldSyntax(s, token) if first && s.pattern == stackElement.state.pattern # The first state in the list may just be another state of the # current pattern. In this case, we already have the # StackElement on the @stack. We only need to update the State # for the current StackElement. stackElement.state = s else # For other patterns, we just push a new StackElement onto the # @stack. @stack.push(TextParser::StackElement.new(nil, s)) end first = false end if state.index == 0 # If we have just started with a new pattern (or loop-ed back) we # need to push a new StackEntry onto the @stack. The StackEntry # stores the result of the pattern and keeps the State that we # need to return to in case we jump to other patterns from this # pattern. checkForOldSyntax(state, token) @stack.push(TextParser::StackElement.new(state.pattern.function, state)) end # Store the token value in the result Array. @stack.last.insert(state.index, token[1], token[2], false) else # Reduce: We've reached the end of a rule. There is no pre-compiled # transition available. The current token, if we have one, is of no # use to us during this state. We just return it to the scanner. The # next state is determined by the first matching state from the # @stack. if state.noReduce # Only states that finish a rule may trigger a reduce operation. # Other states have the noReduce flag set. If a reduce for such a # state is triggered, we found a token that is not supported by # the syntax rules. error("no_reduce", "Unexpected token '#{token[1]}' found. " + "Expecting #{@stack.last.state.expectedTokens.length > 1 ? 'one of ' : ''}" + "#{@stack.last.state.expectedTokens.join(', ')}", token[2]) end if finishPattern(token) # Accept: We're done with parsing. break end state = @stack.last.state end end @stack[0].val[0] end def finishPattern(token) # The method to finish this pattern may include another file or change # the parser rules. Therefor we have to return the token to the scanner. returnToken(token) if token #dumpStack # To finish a pattern we need to pop the StackElement with the token # values from the stack. stackEntry = @stack.pop if stackEntry.nil? || @stack.empty? # Check if we have reached the bottom of the stack. token = getNextToken if token[0] == :endOfText # If the token is the end of the top-level file, we're done. We push # back the StackEntry since it holds the overall result of the # parsing. @stack.push(stackEntry) return true end # If it's not the EOF token, we found a token that violates the syntax # rules. error('unexpctd_token', "Unexpected token '#{token[1]}' found. " + "Expecting one of " + "#{stackEntry.state.expectedTokens.join(', ')}", token[2]) end # Memorize if the rule for this pattern was repeatable. Then we will # store the result of the pattern in an Array. ruleIsRepeatable = stackEntry.state.rule.repeatable state = stackEntry.state result = nil if state.pattern.function # Make the token values and their SourceFileInfo available. @val = stackEntry.val @sourceFileInfo = stackEntry.sourceFileInfo # Now call the pattern action to compute the value of the pattern. begin result = state.pattern.function.call rescue AttributeOverwrite @scanner.warning('attr_overwrite', $!.to_s) end end # We use the SourceFileInfo of the first token of the pattern to store # it with the result of the pattern. firstSourceFileInfo = stackEntry.firstSourceFileInfo # Store the result at the correct position into the next lower level of # the stack. stackEntry = @stack.last stackEntry.insert(stackEntry.state.index, result, firstSourceFileInfo, ruleIsRepeatable) false end def dumpStack #puts "Stack level #{@stack.length}" @stack.each do |sl| print "#{@stack.index(sl)}: " sl.each do |v| if v.is_a?(Array) begin print "[#{v.join('|')}]|" rescue print "[#{v[0].class}...]|" end else begin print "#{v}|" rescue print v.class end end end puts " -> #{sl.state ? sl.state.to_s(true) : 'nil'}" + "#{sl.function.nil? ? '' : '(Called)'}" end end # Check if the current token matches a deprecated or removed syntax # element. def checkForOldSyntax(state, token) if state.pattern.supportLevel == :deprecated warning('deprecated_keyword', "The keyword '#{token[1]}' has been deprecated! " + "See the reference manual for details.") end if state.pattern.supportLevel == :removed error('removed_keyword', "The keyword '#{token[1]}' is no longer supported! " + "See the reference manual for details.") end end # Convert the FSM stack state entries from State objects into [ rule, # pattern, index ] equivalents. def saveFsmStack return unless @stack @stack.each do |s| next unless (st = s.state) s.state = [ st.rule, st.pattern, st.index ] end end # Convert the FSM stack state entries from [ rule, pattern, index ] into # the respective State objects again. def restoreFsmStack return unless @stack @stack.each do |s| next unless (state = @states[s.state]) raise "Stack restore failed. Cannot find state" unless state s.state = state end end def getNextToken token = nextToken # puts "Token: [#{token[0]}][#{token[1]}]" if @blockedVariables[token[0]] error('unsupported_token', "The token #{token[1]} is not supported in this context.", token[2]) end token end end end TaskJuggler-3.8.1/lib/taskjuggler/TextParser/000077500000000000000000000000001473026623400211455ustar00rootroot00000000000000TaskJuggler-3.8.1/lib/taskjuggler/TextParser/MacroTable.rb000066400000000000000000000045361473026623400235130ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = MacroTable.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # class TaskJuggler::TextParser class Macro attr_reader :name, :value, :sourceFileInfo def initialize(name, value, sourceFileInfo) @name = name @value = value @sourceFileInfo = sourceFileInfo end end # The MacroTable is used by the TextScanner to store defined macros and # resolve them on request later on. A macro is a text pattern that has a name. # The pattern may contain variable parts that are replaced by arguments passed # during the macro call. class MacroTable def initialize @macros = {} end # Add a new macro definition to the table or replace an existing one. def add(macro) @macros[macro.name] = macro end # Remove all definitions from the table. def clear @macros = [] end # Returns true only if a macro named _name_ is defined in the table. def include?(name) @macros.include?(name) end # Returns the definition of the macro specified by name as first entry of # _args_. The other entries of _args_ are parameters that are replacing the # ${n} tokens in the macro definition. In case the macro call has less # arguments than the macro definition uses, the ${n} tokens remain # unchanged. No error is generated. def resolve(args, sourceFileInfo) name = args[0] # If the first character of the macro name is a '?', the macro may be # undefined and is silently ignored. if name[0] == ?? # Remove the '?' from the name. name = name[1..-1] return [ nil, '' ] unless @macros[name] end return nil unless @macros[name] resolved = @macros[name].value.dup i = 0 args.each do |arg| resolved.gsub!(Regexp.new("(([^$]|^))\\$\\{#{i}\\}"), "\\1#{arg}") i += 1 end # Remove the escape character from all the escaped '${...}'. resolved.gsub!('$${', '${') [ @macros[name], resolved ] end end end TaskJuggler-3.8.1/lib/taskjuggler/TextParser/Pattern.rb000066400000000000000000000373261473026623400231220ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = Pattern.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/TextParser/TokenDoc' require 'taskjuggler/TextParser/State' class TaskJuggler::TextParser # This class models the most crutial elements of a syntax description - the # pattern. A TextParserPattern primarily consists of a set of tokens. Tokens # are Strings where the first character determines the type of the token. # There are 4 known types. # # Terminal token: In the syntax declaration the terminal token is prefixed # by an underscore. Terminal tokens are terminal symbols of the syntax tree. # They just represent themselves. # # Variable token: The variable token describes values of a certain class such # as strings or numbers. In the syntax declaration the token is prefixed by # a dollar sign and the text of the token specifies the variable type. See # ProjectFileParser for a complete list of variable types. # # Reference token: The reference token specifies a reference to another parser # rule. In the syntax declaration the token is prefixed by a bang and the # text matches the name of the rule. See TextParserRule for details. # # End token: The . token marks the expected end of the input stream. # # In addition to the pure syntax tree information the pattern also holds # documentary information about the pattern. class Pattern attr_reader :keyword, :doc, :supportLevel, :seeAlso, :exampleFile, :exampleTag, :tokens, :function # Create a new Pattern object. _tokens_ must be an Array of String objects # that describe the Pattern. _function_ can be a reference to a method # that should be called when the Pattern was recognized by the parser. def initialize(tokens, function = nil) # A unique name for the pattern that is used in the documentation. @keyword = nil # Initialize pattern doc as empty. @doc = nil # A list of TokenDoc elements that describe the meaning of variable # tokens. The order of the tokens and entries in the Array must correlate. @args = [] # The syntax can evolve over time. The support level specifies which # level of support this pattern hast. Possible values are :experimental, # :beta, :supported, :deprecated, :removed @supportLevel = :supported # A list of references to other patterns that are related to this pattern. @seeAlso = [] # A reference to a file under test/TestSuite/Syntax/Correct and a tag # within that file. This identifies example TJP code to be included with # the reference manual. @exampleFile = nil @exampleTag = nil @tokens = [] tokens.each do |token| unless '!$_.'.include?(token[0]) raise "Fatal Error: All pattern tokens must start with a type " + "identifier [!$_.]: #{tokens.join(', ')}" end # For the syntax specification using a prefix character is more # convenient. But for further processing, we need to split the string # into two symbols. The prefix determines the token type, the rest is # the token name. There are 4 types of tokens: # :reference : a reference to another rule # :variable : a terminal symbol # :literal : a user defined string # :eof : marks the end of an input stream type = [ :reference, :variable, :literal, :eof ]['!$_.'.index(token[0])] # For literals we use a String to store the token content. For others, # a symbol is better suited. name = type == :literal ? token[1..-1] : (type == :eof ? '' : token[1..-1].intern) # We favor an Array to store the 2 elements over a Hash for # performance reasons. @tokens << [ type, name ] # Initialize pattern argument descriptions as empty. @args << nil end @function = function # In some cases we don't want to show all tokens in the syntax # documentation. This value specifies the index of the last shown token. @lastSyntaxToken = @tokens.length - 1 @transitions = [] end # Generate the state machine states for the pattern. _rule_ is the Rule # that the pattern belongs to. A list of generated State objects will be # returned. def generateStates(rule, rules) # The last token of a pattern must always trigger a reduce operation. # But the the last tokens of a pattern describe fully optional syntax, # the last non-optional token and all following optional tokens must # trigger a reduce operation. Here we find the index of the first token # that must trigger a reduce operation. firstReduceableToken = @tokens.length - 1 (@tokens.length - 2).downto(0).each do |i| if optionalToken(i + 1, rules) # If token i + 1 is optional, assume token i is the first one to # trigger a reduce. firstReduceableToken = i else # token i + 1 is not optional, we found the first token to trigger # the reduce. break end end states = [] @tokens.length.times do |i| states << (state = State.new(rule, self, i)) # Mark all states that are allowed to trigger a reduce operation. state.noReduce = false if i >= firstReduceableToken end states end # Add the transitions to the State objects of this pattern. _states_ is a # Hash with all State objects. _rules_ is a Hash with the Rule objects of # the syntax. _stateStack_ is an Array of State objects that have been # traversed before reaching this pattern. _sourceState_ is the State that # the transition originates from. _destRule_, this pattern and _destIndex_ # describe the State the transition is leading to. _loopBack_ is boolean # flag, set to true when the transition describes a loop back to the start # of the Rule. def addTransitionsToState(states, rules, stateStack, sourceState, destRule, destIndex, loopBack) # If we hit a token in the pattern that is optional, we need to consider # the next token of the pattern as well. loop do if destIndex >= @tokens.length if sourceState.rule == destRule if destRule.repeatable # The transition leads us back to the start of the Rule. This # will generate transitions to the first token of all patterns # of this Rule. destRule.addTransitionsToState(states, rules, [], sourceState, true) end end # We've reached the end of the pattern. No more transitions to # consider. return end # The token descriptor tells us where the transition(s) need to go to. tokenType, tokenName = @tokens[destIndex] case tokenType when :reference # The descriptor references another rule. unless (refRule = rules[tokenName]) raise "Unknown rule #{tokenName} referenced in rule #{refRule.name}" end # If we reference another rule from a pattern, we need to come back # to the pattern once we are done with the referenced rule. To be # able to come back, we collect a list of all the States that we # have passed during a reference resolution. This list forms a stack # that is popped during recude operations of the parser FSM. skippedState = states[[ destRule, self, destIndex ]] # Rules may reference themselves directly or indirectly. To avoid # endless recursions of this algorithm, we stop once we have # detected a recursion. We have already all necessary transitions # collected. The recursion will be unrolled in the parser FSM. unless stateStack.include?(skippedState) # Push the skipped state on the stateStack before recursing. stateStack.push(skippedState) refRule.addTransitionsToState(states, rules, stateStack, sourceState, loopBack) # Once we're done, remove the State from the stateStack again. stateStack.pop end # If the referenced rule is not optional, we have no further # transitions for this pattern at this destIndex. break unless refRule.optional?(rules) else unless (destState = states[[ destRule, self, destIndex ]]) raise "Destination state not found" end # We've found a transition to a terminal token. Add the transition # to the source State. sourceState.addTransition(@tokens[destIndex], destState, stateStack, loopBack) # Fixed tokens are never optional. There are no more transitions for # this pattern at this index. break end destIndex += 1 end end # Set the keyword and documentation text for the pattern. def setDoc(keyword, doc) @keyword = keyword @doc = doc end # Set the documentation text and for the idx-th variable. def setArg(idx, doc) @args[idx] = doc end # Restrict the syntax documentation to the first +idx+ tokens. def setLastSyntaxToken(idx) @lastSyntaxToken = idx end # Specify the support level of this pattern. def setSupportLevel(level) unless [ :experimental, :beta, :supported, :deprecated, :removed ].include?(level) raise "Fatal Error: Unknown support level #{level}" end @supportLevel = level end # Set the references to related patterns. def setSeeAlso(also) @seeAlso = also end # Set the file and tag for the TJP code example. def setExample(file, tag) @exampleFile = file @exampleTag = tag end # Conveniance function to access individual tokens by index. def [](i) @tokens[i] end # Iterator for tokens. def each @tokens.each { |type, name| yield(type, name) } end # Returns true of the pattern is empty. def empty? @tokens.empty? end # Returns the number of tokens in the pattern. def length @tokens.length end # Return true if all tokens of the pattern are optional. If a token # references a rule, this rule is followed for the check. def optional?(rules) @tokens.each do |type, name| if type == :literal || type == :variable return false elsif type == :reference if !rules[name].optional?(rules) return false end end end true end # Returns true if the i-th token is a terminal symbol. def terminalSymbol?(i) @tokens[i][0] == :variable || @tokens[i][0] == :literal end # Find recursively the first terminal token of this pattern. If an index is # specified start the search at this n-th pattern token instead of the # first. The return value is an Array of [ token, pattern ] tuple. def terminalTokens(rules, index = 0) type, name = @tokens[index] # Terminal token start with an underscore or dollar character. if type == :literal return [ [ name, self ] ] elsif type == :variable return [] elsif type == :reference # We have to continue the search at this rule. rule = rules[name] # The rule may only have a single pattern. If not, then this pattern # has no terminal token. tts = [] rule.patterns.each { |p| tts += p.terminalTokens(rules, 0) } return tts else raise "Unexpected token #{type} #{name}" end end # Returns a string that expresses the elements of the pattern in an EBNF # like fashion. The resolution of the pattern is done recursively. This is # just the wrapper function that sets up the stack. def to_syntax(argDocs, rules, skip = 0) to_syntax_r({}, argDocs, rules, skip) end # Generate a syntax description for this pattern. def to_syntax_r(stack, argDocs, rules, skip) # If we find ourself on the stack we hit a recursive pattern. This is used # in repetitions. if stack[self] return '[, ... ]' end # "Push" us on the stack. stack[self] = true str = '' first = true # Analyze the tokens of the pattern skipping the first 'skip' tokens. skip.upto(@lastSyntaxToken) do |i| type, name = @tokens[i] # If the first token is a _{ the pattern describes optional attributes. # They are represented by a standard idiom. if first first = false return '{ }' if name == '{' else # Separate the syntax elemens by a whitespace. str << ' ' end if @args[i] # The argument is documented in the syntax definition. We copy the # entry as we need to modify it. argDoc = @args[i].dup # A documented argument without a name is a terminal token. We use the # terminal symbol as name. if @args[i].name.nil? str << "#{name}" argDoc.name = name else str << "<#{@args[i].name}>" end addArgDoc(argDocs, argDoc) # Documented arguments don't have the type set yet. Use the token # value for that. if type == :variable argDoc.typeSpec = "<#{name}>" end else # Undocumented tokens are recursively expanded. case type when :literal # Literals are shown as such. str << name.to_s when :variable # Variables are enclosed by angle brackets. str << "<#{name}>" when :reference if rules[name].patterns.length == 1 && !rules[name].patterns[0].doc.nil? addArgDoc(argDocs, TokenDoc.new(rules[name].patterns[0].keyword, rules[name].patterns[0])) str << '<' + rules[name].patterns[0].keyword + '>' else # References are followed recursively. str << rules[name].to_syntax(stack, argDocs, rules, 0) end end end end # Remove us from the "stack" again. stack.delete(self) str end # Generate a text form of the pattern. This is similar to the syntax in # the original syntax description. def to_s str = "" @tokens.each do |type, name| case type when :reference str += "!#{name} " when :variable str += "$#{name } " when :literal str += "#{name} " when :eof str += ". " else raise "Unknown type #{type}" end end str end private def addArgDoc(argDocs, argDoc) raise 'Error' if argDoc.name.nil? argDocs.each do |ad| return if ad.name == argDoc.name end argDocs << argDoc end # Check if token with _index_ describes fully optional syntax elements. def optionalToken(index, rules) # If the token is a reference to another rule, we need to check if it's # optional. if @tokens[index][0] == :reference return rules[@tokens[index][1]].optional?(rules) end # All other token types are never optional. false end end end TaskJuggler-3.8.1/lib/taskjuggler/TextParser/Rule.rb000066400000000000000000000147641473026623400224150ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = Rule.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/TextParser/State' class TaskJuggler::TextParser # The TextParserRule holds the basic elment of the syntax description. Each # rule has a name and a set of patterns. The parser uses these rules to parse # the input files. The first token of a pattern must resolve to a terminal # token. The resolution can run transitively over a set of rules. The first # tokens of each pattern of a rule must resolve to a terminal symbol and all # terminals must be unique in the scope that they appear in. The parser uses # this first token to select the next pattern it uses for the syntactical # analysis. A rule can be marked as repeatable and/or optional. In this case # the syntax element described by the rule may occur 0 or multiple times in # the parsed file. class Rule attr_reader :name, :patterns, :optional, :repeatable, :keyword, :doc # Create a new syntax rule called +name+. def initialize(name) @name = name @patterns = [] @repeatable = false @optional = false @keyword = nil flushCache end def flushCache # A rule is considered to describe optional tokens in case the @optional # flag is set or all of the patterns reference optional rules again. # This variable caches the transitively determined optional value. @transitiveOptional = nil end # Add a new +pattern+ to the Rule. It should be of type # TextParser::Pattern. def addPattern(pattern) @patterns << pattern end def include?(token) @patterns.each { |p| return true if p[0][1] == token } false end # Mark the rule as an optional element of the syntax. def setOptional @optional = true end # Return true if the rule describes optional elements. The evaluation # recursively descends into the pattern if necessary and stores the result # to be reused for later calls. def optional?(rules) # If we have a cached result, use this. return @transitiveOptional if @transitiveOptional # If the rule is marked optional, then it is optional. if @optional return @transitiveOptional = true end # If all patterns describe optional content, then this rule is optional # as well. @transitiveOptional = true @patterns.each do |pat| return @transitiveOptional = false unless pat.optional?(rules) end end def generateStates(rules) # First, add an entry State for this rule. Entry states are never # reached by normal state transitions. They are only used as (re-)start # states. states = [ State.new(self) ] @patterns.each do |pattern| states += pattern.generateStates(self, rules) end states end # Return a Hash of all state transitions caused by the 1st token of each # pattern of this rule. def addTransitionsToState(states, rules, stateStack, sourceState, loopBack) @patterns.each do |pattern| pattern.addTransitionsToState(states, rules, stateStack.dup, sourceState, self, 0, loopBack) end end # Mark the syntax element described by this Rule as a repeatable element # that can occur once or more times in sequence. def setRepeatable @repeatable = true end # Add a description for the syntax elements of this Rule. +doc+ is a # RichText and +keyword+ is a unique name of this Rule. To avoid # ambiguouties, an optional scope can be appended, separated by a dot # (E.g. name.scope). def setDoc(keyword, doc) raise 'No pattern defined yet' if @patterns.empty? @patterns[-1].setDoc(keyword, doc) end # Add a description for a pattern element of the last added pattern. def setArg(idx, doc) raise 'No pattern defined yet' if @patterns.empty? @patterns[-1].setArg(idx, doc) end # Specify the index +idx+ of the last token to be used for the syntax # documentation. All subsequent tokens will be ignored. def setLastSyntaxToken(idx) raise 'No pattern defined yet' if @patterns.empty? raise 'Token index too large' if idx >= @patterns[-1].tokens.length @patterns[-1].setLastSyntaxToken(idx) end # Specify the support level of the current pattern. def setSupportLevel(level) raise 'No pattern defined yet' if @patterns.empty? @patterns[-1].setSupportLevel(level) end # Add a reference to another rule for documentation purposes. def setSeeAlso(also) raise 'No pattern defined yet' if @patterns.empty? @patterns[-1].setSeeAlso(also) end # Add a reference to a code example. +file+ is the name of the file. +tag+ # is a tag within the file that specifies a part of this file. def setExample(file, tag) @patterns[-1].setExample(file, tag) end # Return a reference the pattern of this Rule. def pattern(idx) @patterns[idx] end def to_syntax(stack, docs, rules, skip) str = '' str << '[' if @optional || @repeatable str << '(' if @patterns.length > 1 first = true pStr = '' @patterns.each do |pat| if first first = false else pStr << ' | ' end pStr << pat.to_syntax_r(stack, docs, rules, skip) end return '' if pStr == '' str << pStr str << '...' if @repeatable str << ')' if @patterns.length > 1 str << ']' if @optional || @repeatable str end def dump puts "Rule: #{name} #{@optional ? "[optional]" : ""} " + "#{@repeatable ? "[repeatable]" : ""}" @patterns.length.times do |i| puts " Pattern: \"#{@patterns[i]}\"" unless @transitions[i] puts "No transitions for this pattern!" next end @transitions[i].each do |key, rule| if key[0] == ?_ token = "\"" + key.slice(1, key.length - 1) + "\"" else token = key.slice(1, key.length - 1) end puts " #{token} -> #{rule.name}" end end puts end end end TaskJuggler-3.8.1/lib/taskjuggler/TextParser/Scanner.rb000066400000000000000000000455151473026623400230750ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = Scanner.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'stringio' require 'strscan' require 'taskjuggler/UTF8String' require 'taskjuggler/TextParser/SourceFileInfo' require 'taskjuggler/TextParser/MacroTable' class TaskJuggler::TextParser # The Scanner class is an abstract text scanner with support for nested # include files and text macros. The tokenizer will operate on rules that # must be provided by a derived class. The scanner is modal. Each mode # operates only with the subset of token patterns that are assigned to the # current mode. The current line is tracked accurately and can be used for # error reporting. The scanner can operate on Strings or Files. class Scanner class MacroStackEntry attr_reader :macro, :args, :text, :endPos def initialize(macro, args, text, endPos) @macro = macro @args = args @text = text @endPos = endPos end end # This class is used to handle the low-level input operations. It knows # whether it deals with a text buffer or a file and abstracts this to the # Scanner. For each nested file the scanner puts a StreamHandle on the # stack while the file is scanned. With this stack the scanner can resume # the processing of the enclosing file once the included files have been # completely processed. class StreamHandle attr_reader :fileName, :macroStack def initialize(log, textScanner) @log = log @textScanner = textScanner @fileName = nil @stream = nil @line = nil @endPos = 1 @scanner = nil @wrapped = false @macroStack = [] @nextMacroEnd = nil end def error(id, message) @textScanner.error(id, message) end def close @stream = nil end # Inject the String _text_ into the input stream at the current cursor # position. def injectText(text, callLength) # Remove the macro call from the end of the already parsed input. preCall = @scanner.pre_match[0..-(callLength + 1)] # Store the end position of the inserted macro in bytes. @nextMacroEnd = preCall.bytesize + text.bytesize # Compose the new @line from the cleaned input, the injected text and # the remainer of the old @line. @line = preCall + text + @scanner.post_match # Start the StringScanner again at the first character of the injected # text. @scanner.string = @line @scanner.pos = preCall.bytesize end def injectMacro(macro, args, text, callLength) injectText(text, callLength) # Simple detection for recursive macro calls. return false if @macroStack.length > 20 @macroStack << MacroStackEntry.new(macro, args, text, @nextMacroEnd) true end def readyNextLine # We read the file line by line with gets(). If we don't have a line # yet or we've reached the end of a line, we get the next one. if @scanner.nil? || @scanner.eos? if (@line = @stream.gets) # Update activity meter about every 1024 lines. @log.activity if (@stream.lineno & 0x3FF) == 0 else # We've reached the end of the current file. @scanner = nil return false end @scanner = StringScanner.new(@line) @wrapped = @line[-1] == ?\n end true end def scan(re) @scanner.scan(re) end def cleanupMacroStack if @nextMacroEnd pos = @scanner.pos while @nextMacroEnd && @nextMacroEnd < pos @macroStack.pop @nextMacroEnd = @macroStack.empty? ? nil : @macroStack.last.endPos end end end def peek(n) @scanner ? @scanner.peek(n) : nil end def eof? @stream.eof? && @scanner.eos? end def dirname @fileName ? File.dirname(@fileName) : '' end # Return the number of the currently processed line. def lineNo # The IO object counts the lines for us by counting the gets() calls. currentLine = @stream && @scanner ? @stream.lineno : 1 # If we've just read the LF, we have to add 1. The LF can only be the # last character of the line. currentLine += 1 if @wrapped && @line && @scanner && @scanner.eos? currentLine end # Return the already processed part of the current line. def line return '' unless @line (@scanner.pre_match || '') + (@scanner.matched || '') end end # Specialized version of StreamHandle for operations on files. class FileStreamHandle < StreamHandle attr_reader :fileName def initialize(fileName, log, textScanner) super(log, textScanner) @fileName = fileName.dup data = (fileName == '.' ? $stdin : File.new(@fileName, 'r')).read begin @stream = StringIO.new(data.forceUTF8Encoding) rescue error('fileEncoding', $!.message) end @log.msg { "Parsing file #{@fileName} ..." } @log.startProgressMeter("Reading file #{fileName}") end def close @stream.close unless @stream == $stdin super end end # Specialized version of StreamHandle for operations on Strings. class BufferStreamHandle < StreamHandle def initialize(buffer, log, textScanner) super(log, textScanner) begin @stream = StringIO.new(buffer) rescue error('bufferEncoding', $!.message) end #@log.msg { "Parsing buffer #{buffer[0, 20]} ..." } end end # Create a new instance of Scanner. _masterFile_ must be a String that # either contains the name of the file to start with or the text itself. # _messageHandler_ is a MessageHandler that is used for error messages. # _log_ is a Log to report progress and status. def initialize(masterFile, log, tokenPatterns, defaultMode) @masterFile = masterFile @messageHandler = TaskJuggler::MessageHandlerInstance.instance @log = log # This table contains all macros that may be expanded when found in the # text. @macroTable = MacroTable.new # The currently processed IO object. @cf = nil # This Array stores the currently processed nested files. It's an Array # of Arrays. The nested Array consists of 2 elements, the IO object and # the @tokenBuffer. @fileStack = [] # This flag is set if we have reached the end of a file. Since we will # only know when the next new token is requested that the file is really # done now, we have to use this flag. @finishLastFile = false # True if the scanner operates on a buffer. @fileNameIsBuffer = false # A SourceFileInfo of the start of the currently processed token. @startOfToken = nil # Line number correction for error messages. @lineDelta = 0 # Lists of regexps that describe the detectable tokens. The Arrays are # grouped by mode. @patternsByMode = { } # The currently active scanner mode. @scannerMode = nil # The mode that the scanner is in at the start and end of file @defaultMode = defaultMode # Points to the currently active pattern set as defined by the mode. @activePatterns = nil tokenPatterns.each do |pat| type = pat[0] regExp = pat[1] mode = pat[2] || :tjp postProc = pat[3] addPattern(type, regExp, mode, postProc) end self.mode = defaultMode end # Add a new pattern to the scanner. _type_ is either nil for tokens that # will be ignored, or some identifier that will be returned with each # token of this type. _regExp_ is the RegExp that describes the token. # _mode_ identifies the scanner mode where the pattern is active. If it's # only a single mode, _mode_ specifies the mode directly. For multiple # modes, it's an Array of modes. _postProc_ is a method reference. This # method is called after the token has been detected. The method gets the # type and the matching String and returns them again in an Array. def addPattern(type, regExp, mode, postProc = nil) if mode.is_a?(Array) mode.each do |m| # The pattern is active in multiple modes @patternsByMode[m] = [] unless @patternsByMode.include?(m) @patternsByMode[m] << [ type, regExp, postProc ] end else # The pattern is only active in one specific mode. @patternsByMode[mode] = [] unless @patternsByMode.include?(mode) @patternsByMode[mode] << [ type, regExp, postProc ] end end # Switch the parser to another mode. The scanner will then only detect # patterns of that _newMode_. def mode=(newMode) #puts "**** New mode: #{newMode}" @activePatterns = @patternsByMode[newMode] raise "Undefined mode #{newMode}" unless @activePatterns @scannerMode = newMode end # Start the processing. If _fileNameIsBuffer_ is true, we operate on a # String, else on a File. def open(fileNameIsBuffer = false) @fileNameIsBuffer = fileNameIsBuffer if fileNameIsBuffer @fileStack = [ [ @cf = BufferStreamHandle.new(@masterFile, @log, self), nil, nil ] ] else begin @fileStack = [ [ @cf = FileStreamHandle.new(@masterFile, @log, self), nil, nil ] ] rescue IOError, SystemCallError error('open_file', "Cannot open file #{@masterFile}: #{$!}") end end @masterPath = @cf.dirname + '/' @tokenBuffer = nil end # Finish processing and reset all data structures. def close unless @fileNameIsBuffer @log.startProgressMeter("Reading file #{@masterFile}") @log.stopProgressMeter end @fileStack = [] @cf = @tokenBuffer = nil end # Continue processing with a new file specified by _includeFileName_. When # this file is finished, we will continue in the old file after the # location where we started with the new file. The method returns the full # qualified name of the included file. def include(includeFileName, sfi, &block) if includeFileName[0] != '/' pathOfCallingFile = @fileStack.last[0].dirname path = pathOfCallingFile.empty? ? '' : pathOfCallingFile + '/' # If the included file is not an absolute name, we interpret the file # name relative to the including file. includeFileName = path + includeFileName end # Try to dectect recursive inclusions. This will not work if files are # accessed via filesystem links. @fileStack.each do |entry| if includeFileName == entry[0].fileName error('include_recursion', "Recursive inclusion of #{includeFileName} detected", sfi) end end # Save @tokenBuffer in the record of the parent file. @fileStack.last[1] = @tokenBuffer unless @fileStack.empty? @tokenBuffer = nil @finishLastFile = false # Open the new file and push the handle on the @fileStack. begin @fileStack << [ (@cf = FileStreamHandle.new(includeFileName, @log, self)), nil, block ] @log.msg { "Parsing file #{includeFileName}" } rescue StandardError error('bad_include', "Cannot open include file #{includeFileName}", sfi) end # Return the name of the included file. includeFileName end # Return SourceFileInfo for the current processing prosition. def sourceFileInfo @cf ? SourceFileInfo.new(fileName, @cf.lineNo - @lineDelta, 0) : SourceFileInfo.new(@masterFile, 0, 0) end # Return the name of the currently processed file. If we are working on a # text buffer, the text will be returned. def fileName @cf ? @cf.fileName : @masterFile end def lineNo # :nodoc: @cf ? @cf.lineNo : 0 end def columnNo # :nodoc: 0 end def line # :nodoc: @cf ? @cf.line : 0 end # Return the next token from the input stream. The result is an Array with # 3 entries: the token type, the token String and the SourceFileInfo where # the token started. def nextToken # If we have a pushed-back token, return that first. unless @tokenBuffer.nil? res = @tokenBuffer @tokenBuffer = nil return res end if @finishLastFile # The previously processed file has now really been processed to # completion. Close it and remove the corresponding entry from the # @fileStack. @finishLastFile = false #@log.msg { "Completed file #{@cf.fileName}" } # If we have a block to be executed on EOF, we call it now. onEof = @fileStack.last[2] onEof.call if onEof @cf.close if @cf @fileStack.pop if @fileStack.empty? # We are done with the top-level file now. @cf = @tokenBuffer = nil @finishLastFile = true return [ :endOfText, '', @startOfToken ] else # Continue parsing the file that included the current file. @cf, tokenBuffer = @fileStack.last @log.msg { "Parsing file #{@cf.fileName} ..." } # If we have a left over token from previously processing this file, # return it now. if tokenBuffer @finishLastFile = true if tokenBuffer[0] == :eof return tokenBuffer end end end scanToken end # Return a token to retrieve it with the next nextToken() call again. Only 1 # token can be returned before the next nextToken() call. def returnToken(token) #@log.msg { "-> Returning Token: [#{token[0]}][#{token[1]}]" } unless @tokenBuffer.nil? $stderr.puts @tokenBuffer raise "Fatal Error: Cannot return more than 1 token in a row" end @tokenBuffer = token end # Add a Macro to the macro translation table. def addMacro(macro) @macroTable.add(macro) end # Return true if the Macro _name_ has been added already. def macroDefined?(name) @macroTable.include?(name) end # Expand a macro and inject it into the input stream. _prefix_ is any # string that was found right before the macro call. We have to inject it # before the expanded macro. _args_ is an Array of Strings. The first is # the macro name, the rest are the parameters. _callLength_ is the number # of characters for the complete macro call "${...}". def expandMacro(prefix, args, callLength) # Get the expanded macro from the @macroTable. macro, text = @macroTable.resolve(args, sourceFileInfo) # If the expanded macro is empty, we can ignore it. return if text == '' unless macro && text error('undefined_macro', "Undefined macro '#{args[0]}' called") end unless @cf.injectMacro(macro, args, prefix + text, callLength) error('macro_stack_overflow', "Too many nested macro calls.") end end # Call this function to report any errors related to the parsed input. def error(id, text, sfi = nil, data = nil) message(:error, id, text, sfi, data) end def warning(id, text, sfi = nil, data = nil) message(:warning, id, text, sfi, data) end private def scanToken @startOfToken = sourceFileInfo begin match = nil loop do # First make sure that the line buffer has been filled and we have a # line to parse. unless @cf.readyNextLine if @scannerMode != @defaultMode # The stream resets the line number to 1. Since we still # know the start of the token, we setup @lineDelta so that # sourceFileInfo() returns the proper line number. @lineDelta = -(@startOfToken.lineNo - 1) error('runaway_token', "Unterminated token starting at line #{@startOfToken}") end # We've found the end of an input file. Return a special token # that describes the end of a file. @finishLastFile = true return [ :eof, '', @startOfToken ] end @activePatterns.each do |type, re, postProc| if (match = @cf.scan(re)) #raise "#{re} matches empty string" if match.empty? # If we have a post processing method, call it now. It may modify # the type or the found token String. type, match = postProc.call(type, match) if postProc break if type.nil? # Ignore certain tokens with nil type. @cf.cleanupMacroStack return [ type, match, @startOfToken ] end end if match.nil? # If we haven't found a match, we either hit EOF or a token we did # not expect. if @cf.eof? error('unexpected_eof', "Unexpected end of file found") else error('no_token_match', "Unexpected characters found: '#{@cf.peek(10)}...'") end else # Remove completely scanned expanded macros from stack. @cf.cleanupMacroStack end end rescue ArgumentError # This is triggered by StringScanner.scan, but we don't want to put # the block in the inner loops for performance reasons. error('scan_encoding_error', $!.message) end end def message(type, id, text, sfi, data) unless text.empty? line = @cf ? @cf.line : nil sfi ||= sourceFileInfo if @cf && !@cf.macroStack.empty? @messageHandler.info('macro_stack', 'Macro call history:', nil) @cf.macroStack.reverse_each do |entry| macro = entry.macro args = entry.args[1..-1] args.collect! { |a| '"' + a + '"' } @messageHandler.info('macro_stack', " ${#{macro.name}#{args.empty? ? '' : ' '}" + "#{args.join(' ')}}", macro.sourceFileInfo) end end case type when :error @messageHandler.error(id, text, sfi, line, data) when :warning @messageHandler.warning(id, text, sfi, line, data) else raise "Unknown message type #{type}" end end end end end TaskJuggler-3.8.1/lib/taskjuggler/TextParser/SourceFileInfo.rb000066400000000000000000000021101473026623400243400ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = SourceFileInfo.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # class TaskJuggler class TextParser # Simple class that holds the info about a source file reference. class SourceFileInfo attr_reader :fileName, :lineNo, :columnNo # Create a new SourceFileInfo object. _file_ is the name of the file. # _line_ is the line in this file, _col_ is the column number in the # line. def initialize(file, line, col) @fileName = file @lineNo = line @columnNo = col end # Return the info in the common "filename:line:" format. def to_s # The column is not reported for now. "#{@fileName}:#{@lineNo}:" end end end end TaskJuggler-3.8.1/lib/taskjuggler/TextParser/StackElement.rb000066400000000000000000000057141473026623400240600ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = StackElement.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # class TaskJuggler::TextParser # This class models the elements of the stack that the TextParser uses to keep # track of its state. It stores the current TextParserRule, the current # pattern position and the TextScanner position at the start of processing. It # also store the function that must be called to store the collected values. class StackElement attr_reader :val, :function, :sourceFileInfo, :firstSourceFileInfo attr_accessor :state # Create a new stack element. _rule_ is the TextParserRule that triggered # the creation of this element. _function_ is the function that will be # called at the end to store the collected data. _sourceFileInfo_ is a # SourceFileInfo reference that describes the TextScanner position when the # rule was entered. def initialize(function, state = nil) # This Array stores the collected values. @val = [] # Array to store the source file references for the collected values. @sourceFileInfo = [] # A shortcut to the first non-nil sourceFileInfo. @firstSourceFileInfo = nil # Counter used for StackElement::store() @position = 0 # The method that will process the collected values. @function = function @state = state end # Insert the value _val_ at the position _index_. It also stores the # _sourceFileInfo_ for this element. In case _multiValue_ is true, the # old value is not overwritten, but values are stored in an # TextParserResultArray object. def insert(index, val, sourceFileInfo, multiValue) if multiValue if @val[index] # We already have a value for this token position. unless @val[index].is_a?(TextParserResultArray) # This should never happen. raise "#{@val[index].class} must be an Array" end else @val[index] = TextParserResultArray.new end # Just append the value and apply the special Array merging. @val[index] << val else @val[index] = val end @sourceFileInfo[index] = sourceFileInfo # Store the first SFI for faster access. @firstSourceFileInfo = sourceFileInfo unless @firstSourceFileInfo val end # Store a collected value and move the position to the next pattern. def store(val, sourceFileInfo = nil) @val[@position] = val @sourceFileInfo[@position] = sourceFileInfo @position += 1 end def each @val.each { |x| yield x } end def length @val.length end end end TaskJuggler-3.8.1/lib/taskjuggler/TextParser/State.rb000066400000000000000000000140331473026623400225530ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = State.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # class TaskJuggler::TextParser # A StateTransition maps a token type to the next state to be # processed. A token descriptor is either a Symbol that maps to a RegExp in # the TextScanner or an expected String. The transition may also have a # list of State objects that are being activated by the transition. class StateTransition attr_reader :tokenType, :state, :stateStack, :loopBack # Create a new StateTransition object. _descriptor_ is a [ token type, # token value ] touple. _state_ is the State objects this transition # originates at. _stateStack_ is the list of State objects that have been # activated by this transition. _loopBack_ is a boolean flag that # specifies whether the transition describes a loop back to the start of # the Rule or not. def initialize(descriptor, state, stateStack, loopBack) if !descriptor.respond_to?(:length) || descriptor.length != 2 raise "Bad parameter descriptor: #{descriptor} " + "of type #{descriptor.class}" end @tokenType = descriptor[0] == :eof ? :eof : descriptor[1] if !state.is_a?(State) raise "Bad parameter state: #{state} of type #{state.class}" end @state = state if !stateStack.is_a?(Array) raise "Bad parameter stateStack: #{stateStack} " + "of type #{stateStack.class}" end @stateStack = stateStack.dup @loopBack = loopBack end # Generate a human readable form of the TransitionState date. It's only # used for debugging. def to_s str = "#{@state.rule.name}, " + "#{@state.rule.patterns.index(@state.pattern)}, #{@state.index} " unless @stateStack.empty? str += "(" @stateStack.each do |s| str += "#{s.rule.name} " end str += ")" end str += '(loop)' if @loopBack str end end # This State objects describes a state of the TextParser FSM. A State # captures the position in the syntax description that the parser is # currently at. A position is defined by the Rule, the Pattern and the index # of the current token of that Pattern. An index of 0 means, we've just read # the 1st token of the pattern. States which have no Pattern describe the # start of rule. The parser has not yet identified the first token, so it # doesn't know the Pattern yet. # # The actual data of a State is the list of possible StateTransitions to # other states and a boolean flag that specifies if Reduce operations are # valid for this State or not. The transitions are hashed by the token that # would trigger this transition. class State attr_reader :rule, :pattern, :index, :transitions attr_accessor :noReduce def initialize(rule, pattern = nil, index = 0) @rule = rule @pattern = pattern @index = index # Starting states are always reduceable. Other states may or may not be # reduceable. For now, we assume they are not. @noReduce = !pattern.nil? @transitions = {} end # Complete the StateTransition list. We can only call this function after # all State objects for the syntax have been created. So we can't make # this part of the constructor. def addTransitions(states, rules) if @pattern # This is an normal state node. @pattern.addTransitionsToState(states, rules, [], self, @rule, @index + 1, false) else # This is a start node. @rule.addTransitionsToState(states, rules, [], self, false) end end # This method adds the actual StateTransition to this State. def addTransition(token, nextState, stateStack, loopBack) tr = StateTransition.new(token, nextState, stateStack, loopBack) if @transitions.include?(tr.tokenType) raise "Ambiguous transition for #{tr.tokenType} in \n#{self}\n" + "The following transition both match:\n" + " #{tr}\n #{@transitions[tr.tokenType]}" end @transitions[tr.tokenType] = tr end # Find the transition that matches _token_. def transition(token) if token[0] == :ID # The scanner cannot differentiate between IDs and literals that look # like IDs. So we look for literals first and then for IDs. @transitions[token[1]] || @transitions[:ID] elsif token[0] == :LITERAL @transitions[token[1]] else @transitions[token[0]] end end # Return a comma separated list of token strings that would trigger # transitions for this State. def expectedTokens tokens = [] @transitions.each_key do |t| tokens << "#{t.is_a?(String) ? "'#{t}'" : ":#{t}"}" end tokens end # Convert the State data into a human readable form. Used for debugging # only. def to_s(short = false) if short if @pattern str = "#{rule.name} " + "#{rule.patterns.index(@pattern)} #{@index}" else str = "#{rule.name} (Starting Node)" end else if @pattern str = "=== State: #{rule.name} " + "#{rule.patterns.index(@pattern)} #{@index}" + " #{@noReduce ? '' : '(R)'}" + " #{'=' * 40}\nPattern: #{@pattern}\n" else str = "=== State: #{rule.name} (Starting Node) #{'=' * 30}\n" end @transitions.each do |type, target| targetStr = target ? target.to_s : "" str += " #{type.is_a?(String) ? "'#{type}'" : ":#{type}"}" + " => #{targetStr}\n" end str += "#{'=' * 76}\n" end str end end end TaskJuggler-3.8.1/lib/taskjuggler/TextParser/TokenDoc.rb000066400000000000000000000021211473026623400231740ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = TokenDoc.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # class TaskJuggler::TextParser # Utility class to store a name and a textual description of the meaning of a # token used by the parser syntax tree. A specification of the variable type # and a reference to a specific pattern are optional. class TokenDoc attr_reader :text attr_accessor :name, :typeSpec, :pattern # Construct a ParserTokenDoc object. _name_ and _text_ are Strings that # hold the name and textual description of the parser token. def initialize(name, arg) @name = name if arg.is_a?(String) @text = arg else @pattern = arg end @typeSpec = nil @pattern = nil end end end TaskJuggler-3.8.1/lib/taskjuggler/TimeSheetReceiver.rb000066400000000000000000000025731473026623400227540ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = TimeSheetReceiver.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/SheetReceiver' class TaskJuggler # This class specializes SheetReceiver to process time sheets. class TimeSheetReceiver < SheetReceiver def initialize(appName) super(appName, 'time') @tj3clientOption = 'check-ts' # File name and directory settings. @sheetDir = 'TimeSheets' @templateDir = 'TimeSheetTemplates' @failedMailsDir = "#{@sheetDir}/FailedMails" @failedSheetsDir = "#{@sheetDir}/FailedSheets" @signatureFile = "#{@templateDir}/acceptable_intervals" @logFile = 'timesheets.log' # Regular expression to identify time sheets. @sheetHeader = /^[ ]*timesheet\s([a-zA-Z_][a-zA-Z0-9_]*)\s[0-9\-:+]*\s-\s([0-9]*-[0-9]*-[0-9]*)/ # Regular expression to extract the sheet signature (time period). @signatureFilter = /^[ ]*timesheet\s[a-zA-Z_][a-zA-Z0-9_]*\s([0-9:\-+]*\s-\s[0-9:\-+]*)/ @emailSubject = "Report from %s for %s" end end end TaskJuggler-3.8.1/lib/taskjuggler/TimeSheetSender.rb000066400000000000000000000040551473026623400224250ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = TimeSheetSender.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/SheetSender' class TaskJuggler # The TimeSheetSender class generates time sheet templates for the current # week and sends them out to the project contributors. For this to work, the # resources must provide the 'Email' custom attribute with their email # address. The actual project data is accessed via tj3client on a tj3 server # process. class TimeSheetSender < SheetSender attr_accessor :date def initialize(appName) super(appName, 'time') # This is a LogicalExpression string that controls what resources should # not be getting a time sheet. @hideResource = '0' # The base directory of the time sheet templates. @templateDir = 'TimeSheetTemplates' # This file contains the time intervals that the TimeSheetReceiver will # accept as a valid interval. @signatureFile = "#{@templateDir}/acceptable_intervals" # The log file @logFile = 'timesheets.log' @signatureFilter = /^[ ]*timesheet\s[a-zA-Z_][a-zA-Z0-9_]*\s([0-9:\-+]*\s-\s[0-9:\-+]*)/ @introText = <<'EOT' Please find enclosed your weekly report template. Please fill out the form and send it back to the sender of this email. You can either use the attached file or the body of the email. In case you send it in the body of the email, make sure it only contains the 'timesheet' syntax. No quote marks are allowed. It must be plain text, UTF-8 encoded and the time sheet header from 'timesheet' to the period end date must be in a single line that starts at the beginning of the line. EOT @mailSubject = 'Your weekly time sheet template for %s' end end end TaskJuggler-3.8.1/lib/taskjuggler/TimeSheetSummary.rb000066400000000000000000000143201473026623400226360ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = TimeSheetSummary.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/SheetReceiver' class TaskJuggler # The TimeSheetSender class generates time sheet templates for the current # week and sends them out to the project contributors. For this to work, the # resources must provide the 'Email' custom attribute with their email # address. The actual project data is accessed via tj3client on a tj3 server # process. class TimeSheetSummary < SheetReceiver attr_accessor :date, :sheetRecipients, :digestRecipients def initialize super('tj3ts_summary', 'summary') # This is a LogicalExpression string that controls what resources should # not be considered in the summary. @hideResource = '0' # The base directory of the time sheet templates. @templateDir = 'TimeSheetTemplates' # The base directory of the submitted time sheets @sheetDir = 'TimeSheets' # The log file @logFile = 'timesheets.log' # A list of email addresses to send the individual sheets. The sender # will be the sheet submitter. @sheetRecipients = [] # A list of email addresses to send the summary to @digestRecipients = [] @resourceIntro = "== Weekly Report from %s ==\n" @resourceSheetSubject = "Weekly report %s" @summarySubject = "Weekly staff reports %s" @reminderSubject = "Your time sheet for the period ending %s is overdue!" @reminderText = <<'EOT' The deadline for your time sheet submission has passed but we haven't received it yet. Please submit your time sheet immediately so the content can still be included in the management reports. Please send a copy of your submission notification email to your manager. If possible, your manager will still try to include your report data in his/her report. Please be aware that post deadline submissions must be processed manually and create an additional load for your manager and/or project manager. Please try to submit in time in the future. Thanks for your cooperation! EOT @defaulterHeader = "The following %d person(s) have not yet submitted " + "their time sheets:\n\n" end def sendSummary(resourceIds) setWorkingDir summary = '' defaulterList = [] getResourceList.each do |resource| resourceId = resource[0] resourceName = resource[1] resourceEmail = resource[2] next if !resourceIds.empty? && !resourceIds.include?(resourceId) templateFile = "#{@templateDir}/#{@date}/#{resourceId}_#{@date}.tji" sheetFile = "#{@sheetDir}/#{@date}/#{resourceId}_#{@date}.tji" if File.exist?(templateFile) if File.exist?(sheetFile) # If there are no recipients specified, we don't need to compile # the summary. unless @digestRecipients.empty? && @sheetRecipients.empty? # Resource has submitted a time sheet sheet = getResourceJournal(sheetFile) summary += sprintf(@resourceIntro, resourceName) summary += sheet + "\n----\n" info("Adding report from #{resourceName} to summary") @sheetRecipients.each do |to| sendRichTextEmail(to, sprintf(@resourceSheetSubject, @date), sheet, nil, "#{resourceName} <#{resourceEmail}>") end end else defaulterList << resource # Resource did not submit a time sheet info("Report from #{resourceId} is missing") end end end unless defaulterList.empty? # Prepend the defaulter list to the summary. text = sprintf(@defaulterHeader, defaulterList.length) defaulterList.each do |resource| text += "* #{resource[1]}\n" end text += "\n----\n" summary = text + summary # Create a file with the IDs of the resources who's reports are # missing. missingFile = "#{@sheetDir}/#{@date}/missing-reports" begin File.open(missingFile, 'w') do |f| defaulterList.each { |resource| f.puts resource[0] } end rescue error("Cannot write file with missing reports (#missingFile): #{$!}") end end # Send out the summary text to the list of digest recipients. @digestRecipients.each do |to| sendRichTextEmail(to, sprintf(@summarySubject, @date), summary) end # If there is a reminder text defined, resend the template to those # individuals that have not yet submitted their report yet. if @reminderText && !@reminderText.empty? defaulterList.each do |resource| sendReminder(resource[0], resource[1], resource[2]) end end end private def sendReminder(id, name, email) attachment = "#{@templateDir}/#{@date}/#{id}_#{@date}.tji" unless File.exist?(attachment) error("sendReportTemplates: " + "#{@sheetType} sheet #{attachment} for #{name} not found") end message = "Hello #{name}!\n\n#{@reminderText}" + File.read(attachment) sendEmail(email, sprintf(@reminderSubject, @date), message, attachment) end def getResourceJournal(sheetFile) err = '' status = nil report = nil warnings = nil begin # Save a copy of the sheet for debugging purposes. command = [ '--unsafe', "--silent", "check-ts", @projectId, sheetFile ] res = stdIoWrapper do Tj3Client.new.main(command) end if res.returnValue != 0 error("summary sheets: #{err}") end report = res.stdOut warnings = res.stdErr rescue fatal("Cannot summarize sheet: #{$!}") end report end end end TaskJuggler-3.8.1/lib/taskjuggler/TimeSheets.rb000066400000000000000000000324601473026623400214500ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = TimeSheets.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # class TaskJuggler # This class holds the work related bits of a time sheet that are specific # to a single Task. This can be an existing Task or a new one identified by # it's ID String. For effort based task, it stores the remaining effort, for # other task the expected end date. For all tasks it stores the completed # work during the reporting time frame. class TimeSheetRecord include MessageHandler attr_reader :task, :work attr_accessor :sourceFileInfo, :remaining, :expectedEnd, :status, :priority, :name def initialize(timeSheet, task) # This is a reference to a Task object for existing tasks or an ID as # String for new tasks. @task = task # Add the new TimeSheetRecord to the TimeSheet it belongs to. (@timeSheet = timeSheet) << self # Work done will be measured in time slots. @work = nil # Remaining work will be measured in time slots. @remaining = nil @expectedEnd = nil # For new task, we also need to store the name. @name = nil # Reference to the JournalEntry object that holds the status for this # record. @status = nil @priority = 0 @sourceFileInfo = nil end # Store the number of worked time slots. If the value is an Integer, it can # be directly assigned. A Float is interpreted as percentage and must be # in the rage of 0.0 to 1.0. def work=(value) if value.is_a?(Integer) @work = value else # Must be percent value @work = @timeSheet.percentToSlots(value) end end # Perform all kinds of consistency checks. def check scIdx = @timeSheet.scenarioIdx taskId = @task.is_a?(Task) ? @task.fullId : @task # All TimeSheetRecords must have a 'work' attribute. if @work.nil? error('ts_no_work', "The time sheet record for task #{taskId} must " + "have a 'work' attribute to specify how much was done " + "for this task during the reported period.") end if @task.is_a?(Task) # This is already known tasks. if @task['effort', scIdx] > 0 unless @remaining error('ts_no_remaining', "The time sheet record for task #{taskId} must " + "have a 'remaining' attribute to specify how much " + "effort is left for this task.") end else unless @expectedEnd error('ts_no_expected_end', "The time sheet record for task #{taskId} must " + "have an 'end' attribute to specify the expected end " + "of this task.") end end else # This is for new tasks. if @remaining.nil? && @expectedEnd.nil? error('ts_no_rem_or_end', "New task #{taskId} requires either a 'remaining' or a " + "'end' attribute.") end end if @work >= @timeSheet.daysToSlots(1) && @status.nil? error('ts_no_status_work', "You must specify a status for task #{taskId}. It was worked " + "on for a day or more.") end if @status if @status.headline.empty? error('ts_no_headline', "You must provide a headline for the status of " + "task #{taskId}") end if @status.summary && @status.summary.richText.inputText == "A summary text\n" error('ts_default_summary', "You must change the default summary text of the status " + "for task #{taskId}.") end if @status.alertLevel > 0 && @status.summary.nil? && @status.details.nil? error('ts_alert1_more_details', "Task #{taskId} has an elevated alert level and must " + "have a summary or details section.") end if @status.alertLevel > 1 && @status.details.nil? error('ts_alert2_more_details', "Task #{taskId} has a high alert level and must have " + "a details section.") end end end def warnOnDelta(startIdx, endIdx) # Ignore personal entries. return unless @task resource = @timeSheet.resource if @task.is_a?(String) # A resource has requested a new Task to be created. warning('ts_res_new_task', "#{resource.name} is requesting a new task:\n" + " ID: #{@task}\n" + " Name: #{@name}\n" + " Work: #{@timeSheet.slotsToDays(@work)}d " + (@remaining ? "Remaining: #{@timeSheet.slotsToDays(@remaining)}d" : "End: #{@end.to_s}")) return end scenarioIdx = @timeSheet.scenarioIdx project = resource.project plannedWork = @task.getEffectiveWork(scenarioIdx, startIdx, endIdx, resource) # Convert the @work slots into a daily load. work = project.convertToDailyLoad(@work * project['scheduleGranularity']) if work != plannedWork warning('ts_res_work_delta', "#{resource.name} worked " + "#{work < plannedWork ? 'less' : 'more'} " + "on #{@task.fullId}\n" + "#{work}d instead of #{plannedWork}d") end if @task['effort', scenarioIdx] > 0 startIdx = endIdx endIdx = project.dateToIdx(@task['end', scenarioIdx]) remainingWork = @task.getEffectiveWork(scenarioIdx, startIdx, endIdx, resource) # Convert the @remaining slots into a daily load. remaining = project.convertToDailyLoad(@remaining * project['scheduleGranularity']) if remaining != remainingWork warning('ts_res_remain_delta', "#{resource.name} requests " + "#{remaining < remainingWork ? 'less' : 'more'} " + "remaining effort for task #{@task.fullId}\n" + "#{remaining}d instead of #{remainingWork}d") end else if @expectedEnd != @task['end', scenarioIdx] warning('ts_res_end_delta', "#{resource.name} requests " + "#{@expectedEnd < @task['end', scenarioIdx] ? 'earlier' : 'later'} end (#{@expectedEnd}) for task " + "#{@task.fullId}. Planned end is " + "#{@task['end', scenarioIdx]}.") end end end def taskId @task.is_a?(Task) ? @task.fullId : task end # The reported work in % (0.0 - 100.0) of the average working time. def actualWorkPercent (@work.to_f / @timeSheet.totalGrossWorkingSlots) * 100.0 end # The planned work in % (0.0 - 100.0) of the average working time. def planWorkPercent resource = @timeSheet.resource project = resource.project scenarioIdx = @timeSheet.scenarioIdx startIdx = project.dateToIdx(@timeSheet.interval.start) endIdx = project.dateToIdx(@timeSheet.interval.end) (@timeSheet.resource.getAllocatedSlots(scenarioIdx, startIdx, endIdx, @task).to_f / @timeSheet.totalGrossWorkingSlots) * 100.0 end # The reporting remaining effort in days. def actualRemaining project = @timeSheet.resource.project project.convertToDailyLoad(@remaining * project['scheduleGranularity']) end # The remaining effort according to the plan. def planRemaining resource = @timeSheet.resource project = resource.project scenarioIdx = @timeSheet.scenarioIdx startIdx = project.dateToIdx(project['now']) endIdx = project.dateToIdx(@task['end', scenarioIdx]) @task.getEffectiveWork(scenarioIdx, startIdx, endIdx, resource) end # The reported expected end of the task. def actualEnd @expectedEnd end # The planned end of the task. def planEnd @task['end', @timeSheet.scenarioIdx] end private end # The TimeSheet class stores the work related bits of a time sheet. For each # task it holds a TimeSheetRecord object. A time sheet is always bound to an # existing Resource. class TimeSheet attr_accessor :sourceFileInfo attr_reader :resource, :interval, :scenarioIdx def initialize(resource, interval, scenarioIdx) raise "Illegal resource" unless resource.is_a?(Resource) @resource = resource raise "Interval undefined" if interval.nil? @interval = interval raise "Sceneario index undefined" if scenarioIdx.nil? @scenarioIdx = scenarioIdx @sourceFileInfo = nil # This flag is set to true if at least one record was reported as # percentage. @percentageUsed = false # The TimeSheetRecord list. @records = [] @messageHandler = MessageHandlerInstance.instance end # Add a new TimeSheetRecord to the list. def<<(record) @records.each do |r| if r.task == record.task error('ts_duplicate_task', "Duplicate records for task #{r.taskId}") end end @records << record end # Perform all kinds of consitency checks. def check totalSlots = 0 @records.each do |record| record.check totalSlots += record.work end unless (scenarioIdx = @resource.project['trackingScenarioIdx']) error('ts_no_tracking_scenario', 'No trackingscenario has been defined.') end if @resource['efficiency', scenarioIdx] > 0.0 targetSlots = totalNetWorkingSlots # This is the acceptable rounding error when checking the total # reported work. delta = 1 if totalSlots < (targetSlots - delta) error('ts_work_too_low', "The total work to be reported for this time sheet " + "is #{workWithUnit(targetSlots)} but only " + "#{workWithUnit(totalSlots)} were reported.") end if totalSlots > (targetSlots + delta) error('ts_work_too_high', "The total work to be reported for this time sheet " + "is #{workWithUnit(targetSlots)} but " + "#{workWithUnit(totalSlots)} were reported.") end else if totalSlots > 0 error('ts_work_not_null', "The reported work for non-working resources must be 0.") end end end def warnOnDelta project = @resource.project startIdx = project.dateToIdx(@interval.start) endIdx = project.dateToIdx(@interval.end) @records.each do |record| record.warnOnDelta(startIdx, endIdx) end end # Compute the total number of potential working time slots during the # report period. This value is not resource specific. def totalGrossWorkingSlots project = @resource.project # Calculate the number of weeks in the report weeksToReport = (@interval.end - @interval.start).to_f / (60 * 60 * 24 * 7) daysToSlots((project.weeklyWorkingDays * weeksToReport).to_i) end # Compute the total number of actual working time slots of the # Resource. This is the sum of allocated, free time slots. def totalNetWorkingSlots project = @resource.project startIdx = project.dateToIdx(@interval.start) endIdx = project.dateToIdx(@interval.end) @resource.getAllocatedSlots(@scenarioIdx, startIdx, endIdx, nil) + @resource.getFreeSlots(@scenarioIdx, startIdx, endIdx) end # Converts allocation percentage into time slots. def percentToSlots(value) @percentageUsed = true (totalGrossWorkingSlots * value).to_i end # Computes how many percent the _slots_ are of the total working slots in # the report time frame. def slotsToPercent(slots) slots.to_f / totalGrossWorkingSlots end def slotsToDays(slots) slots * @resource.project['scheduleGranularity'] / (60 * 60 * @resource.project.dailyWorkingHours) end def daysToSlots(days) ((days * 60 * 60 * @resource.project.dailyWorkingHours) / @resource.project['scheduleGranularity']).to_i end def error(id, text, sourceFileInfo = nil) @messageHandler.error(id, text, sourceFileInfo || @sourceFileInfo, nil, @resource) end def warning(id, text, sourceFileInfo = nil) @messageHandler.warning(id, text, sourceFileInfo, nil, @resource) end private def workWithUnit(slots) if @percentageUsed "#{(slotsToPercent(slots) * 100.0).to_i}%" else "#{slotsToDays(slots)} days" end end end # A class to hold all time sheets of a project. class TimeSheets < Array def initialize super end def check each { |s| s.check } end def warnOnDelta each { |s| s.warnOnDelta } end end end TaskJuggler-3.8.1/lib/taskjuggler/Tj3AppBase.rb000066400000000000000000000150611473026623400212700ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = Tj3AppBase.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'optparse' require 'term/ansicolor' require 'taskjuggler/Tj3Config' require 'taskjuggler/RuntimeConfig' require 'taskjuggler/TjTime' require 'taskjuggler/TextFormatter' require 'taskjuggler/MessageHandler' require 'taskjuggler/Log' class TaskJuggler class Tj3AppBase include MessageHandler def initialize # Indent and width of options. The deriving class may has to change # this. @optsSummaryWidth = 22 @optsSummaryIndent = 5 # Show some progress information by default @silent = false @configFile = nil @mandatoryArgs = '' @mininumRubyVersion = '1.9.2' # If stdout is not a tty, we don't use ANSI escape sequences to color # the terminal output. Additionally, we have the --no-color option to # force colors off in case this does not work properly. Term::ANSIColor.coloring = STDOUT.tty? # Make sure the MessageHandler is set to default values. MessageHandlerInstance.instance.reset end def processArguments(argv) @opts = OptionParser.new @opts.summary_width = @optsSummaryWidth @opts.summary_indent = ' ' * @optsSummaryIndent @opts.banner = "Copyright (c) #{AppConfig.copyright.join(', ')}\n" + " by #{AppConfig.authors.join(', ')}\n\n" + "#{AppConfig.license}\n" + "For more info about #{AppConfig.softwareName} see " + "#{AppConfig.contact}\n\n" + "Usage: #{AppConfig.appName} [options] " + "#{@mandatoryArgs}\n\n" @opts.separator "\nOptions:" @opts.on('-c', '--config ', String, format('Use the specified YAML configuration file')) do |arg| @configFile = arg end @opts.on('--silent', format("Don't show program and progress information")) do @silent = true MessageHandlerInstance.instance.outputLevel = :warning TaskJuggler::Log.silent = true end @opts.on('--no-color', format(<<'EOT' Don't use ANSI contol sequences to color the terminal output. Colors should only be used when spooling to an ANSI terminal. In case the detection fails, you can use this option to force colors to be off. EOT )) do Term::ANSIColor::coloring = false end @opts.on('--debug', format('Enable Ruby debug mode')) do $DEBUG = true end yield @opts.on_tail('-h', '--help', format('Show this message')) do puts @opts.to_s quit end @opts.on_tail('--version', format('Show version info')) do # Display the software name and version in GNU format # as expected by help2man # https://www.gnu.org/prep/standards/standards.html#g_t_002d_002dversion puts "#{AppConfig.appName} (#{AppConfig.softwareName}) #{AppConfig.version}\n" # To also display the copyright and license statements in GNU format # uncomment the following and remove the equivalent statements from # --help # + #<<'EOT' #Copyright (C) 2016 Chris Schlaeger #License GPLv2: GNU GPL version 2 #This is free software; you can redistribute it and/or modify it under #the terms of version 2 of the GNU General Public License as published by the #Free Software Foundation. # #For more info about TaskJuggler see http://www.taskjuggler.org #EOT quit end begin files = @opts.parse(argv) rescue OptionParser::ParseError => msg puts @opts.to_s + "\n" error('tj3app_bad_cmd_options', msg.message) end files end def main(argv = ARGV) if Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new(@mininumRubyVersion) error('tj3app_ruby_version', 'This program requires at least Ruby version ' + "#{@mininumRubyVersion}!") end # Install signal handler to exit gracefully on CTRL-C. intHandler = Kernel.trap('INT') do begin fatal('tj3app_user_abort', "Aborting on user request!") rescue RuntimeError exit 1 end end retVal = 0 begin args = processArguments(argv) # If DEBUG mode has been enabled, we restore the INT trap handler again # to get Ruby backtraces. Kernel.trap('INT', intHandler) if $DEBUG unless @silent puts "#{AppConfig.softwareName} v#{AppConfig.version} - " + "#{AppConfig.packageInfo}\n\n" + "Copyright (c) #{AppConfig.copyright.join(', ')}\n" + " by #{AppConfig.authors.join(', ')}\n\n" + "#{AppConfig.license}\n" end @rc = RuntimeConfig.new(AppConfig.packageName, @configFile) begin MessageHandlerInstance.instance.trapSetup = true retVal = appMain(args) MessageHandlerInstance.instance.trapSetup = false rescue TjRuntimeError # We have hit a situation that we can't recover from. A message # was severed via the MessageHandler to inform the user and we now # abort the program. return 1 end rescue Exception => e if e.is_a?(SystemExit) || e.is_a?(Interrupt) # Don't show backtrace on user interrupt unless we are in debug mode. $stderr.puts e.backtrace.join("\n") if $DEBUG 1 else fatal('crash_trap', "#{e}\n#{e.backtrace.join("\n")}\n\n" + "#{'*' * 79}\nYou have triggered a bug in " + "#{AppConfig.softwareName} version #{AppConfig.version}!\n" + "Please see the user manual on how to get this bug fixed!\n" + "http://www.taskjuggler.org/tj3/manual/Reporting_Bugs.html#" + "Reporting_Bugs_and_Feature_Requests\n" + "#{'*' * 79}\n") end end # Exit value in case everything was fine. retVal end private def quit exit 0 end def format(str, indent = nil) indent = @optsSummaryWidth + @optsSummaryIndent + 1 unless indent TextFormatter.new(79, indent).format(str)[indent..-1] end end end TaskJuggler-3.8.1/lib/taskjuggler/Tj3Config.rb000066400000000000000000000020751473026623400211630ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = Tj3Config.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2016, # 2020 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/UTF8String' require 'taskjuggler/AppConfig' require 'taskjuggler/version' AppConfig.version = VERSION AppConfig.packageName = 'taskjuggler' AppConfig.softwareName = 'TaskJuggler' AppConfig.packageInfo = 'A Project Management Software' AppConfig.copyright = [ (2006..2020).to_a ] AppConfig.authors = [ 'Chris Schlaeger ' ] AppConfig.contact = 'http://www.taskjuggler.org' AppConfig.license = <<'EOT' This program is free software; you can redistribute it and/or modify it under the terms of version 2 of the GNU General Public License as published by the Free Software Foundation. EOT TaskJuggler-3.8.1/lib/taskjuggler/Tj3SheetAppBase.rb000066400000000000000000000031501473026623400222550ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = Tj3SheetAppBase.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/Tj3AppBase' class TaskJuggler class Tj3SheetAppBase < Tj3AppBase def initialize super @dryRun = false @workingDir = nil end def processArguments(argv) super do @opts.on('-d', '--directory ', String, format('Use the specified directory as working ' + 'directory')) do |arg| @workingDir = arg end @opts.on('--dryrun', format("Don't send out any emails or do SCM commits")) do @dryRun = true end yield end end def optsEndDate @opts.on('-e', '--enddate ', String, format("The end date of the reporting period. Either as " + "YYYY-MM-DD or day of week. 0: Sunday, 1: Monday and " + "so on. The default value is #{@date}.")) do |arg| ymdFilter = /([0-9]{4})-([0-9]{2})-([0-9]{2})/ if ymdFilter.match(arg) @date = Time.mktime(*(ymdFilter.match(arg)[1..3])) else @date = TjTime.new.nextDayOfWeek(arg.to_i % 7) end @date = @date.strftime('%Y-%m-%d') end end end end TaskJuggler-3.8.1/lib/taskjuggler/TjException.rb000066400000000000000000000011661473026623400216310ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = TjException.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # class TaskJuggler class TjException < RuntimeError attr_reader :error, :fatal def initialize(error = true, fatal = false) @error = error @fatal = fatal end end end TaskJuggler-3.8.1/lib/taskjuggler/TjTime.rb000066400000000000000000000377221473026623400206000ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = TjTime.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'time' require 'date' class TaskJuggler # The TjTime class extends the original Ruby class Time with lots of # TaskJuggler specific additional functionality. This is mostly for handling # time zones. class TjTime attr_reader :time # The number of days per month. Leap years are taken care of separately. MON_MAX = [ 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 ] # Initialize @@tz with the current time zone if it is set. @@tz = ENV['TZ'] # call-seq: # TjTime() -> TjTime (now) # TjTime(tjtime) -> TjTime # TjTime(time, timezone) -> TjTime # TjTime(str) -> TjTime # TjTime(secs) -> TjTime # # The constructor is overloaded and accepts 4 kinds of arguments. If _t_ # is a Time object it's assumed to be in local time. If it's a string, it # is parsed as a date. Or else it is interpreted as seconds after Epoch. def initialize(t = nil) case t when nil @time = Time.now when Time @time = t when TjTime @time = t.time when String parse(t) when Array @time = Time.mktime(*t) else @time = Time.at(t) end end # Check if +zone+ is a valid time zone. def TjTime.checkTimeZone(zone) return true if zone == 'UTC' # Valid time zones must be of the form 'Region/City' return false unless zone.include?('/') # Save curent value of TZ tz = ENV['TZ'] ENV['TZ'] = zone newZone = Time.new.zone # If the time zone is valid, the OS can convert a zone like # 'America/Denver' into 'MST'. Unknown time zones are either not # converted or cause a fallback to UTC. # Since glibc 2.10 Time.new.zone only return the region for illegal # zones instead of the full zone string like it does on earlier # versions. region = zone[0..zone.index('/') - 1] res = (newZone != zone && newZone != region && newZone != 'UTC') # Restore TZ if it was set earlier. if tz ENV['TZ'] = tz else ENV.delete('TZ') end res end # Set a new active time zone. _zone_ must be a valid String known to the # underlying operating system. def TjTime.setTimeZone(zone) unless zone && TjTime.checkTimeZone(zone) raise "Illegal time zone #{zone}" end oldTimeZone = @@tz @@tz = zone ENV['TZ'] = zone oldTimeZone end # Return the name of the currently active time zone. def TjTime.timeZone @@tz end # Align the date to a time grid. The grid distance is determined by _clock_. def align(clock) TjTime.new((localtime.to_i / clock) * clock) end # Return the time object in UTC. def utc TjTime.new(@time.dup.gmtime) end # Returns the total number of seconds of the day. The time is assumed to be # in the time zone specified by _tz_. def secondsOfDay(tz = nil) lt = localtime (lt.to_i + lt.gmt_offset) % (60 * 60 * 24) end # Add _secs_ number of seconds to the time. def +(secs) TjTime.new(@time.to_i + secs) end # Substract _arg_ number of seconds or return the number of seconds between # _arg_ and this time. def -(arg) if arg.is_a?(TjTime) @time - arg.time else TjTime.new(@time.to_i - arg) end end # Convert the time to seconds since Epoch and return the module of _val_. def %(val) @time.to_i % val end # Return true if time is smaller than _t_. def <(t) return false unless t @time < t.time end # Return true if time is smaller or equal than _t_. def <=(t) return false unless t @time <= t.time end # Return true if time is larger than _t_. def >(t) return true unless t @time > t.time end # Return true if time is larger or equal than _t_. def >=(t) return true unless t @time >= t.time end # Return true if time and _t_ are identical. def ==(t) return false unless t @time == t.time end # Coparison operator for time with another time _t_. def <=>(t) return -1 unless t @time <=> t.time end # Iterator that executes the block until time has reached _endDate_ # increasing time by _step_ on each iteration. def upto(endDate, step = 1) t = @time while t < endDate.time yield(TjTime.new(t)) t += step end end # Normalize time to the beginning of the current hour. def beginOfHour sec, min, hour, day, month, year = localtime.to_a sec = min = 0 TjTime.new([ year, month, day, hour, min, sec, 0 ]) end # Normalize time to the beginning of the current day. def midnight sec, min, hour, day, month, year = localtime.to_a sec = min = hour = 0 TjTime.new([ year, month, day, hour, min, sec, 0 ]) end # Normalize time to the beginning of the current week. _startMonday_ # determines whether the week should start on Monday or Sunday. def beginOfWeek(startMonday) t = localtime.to_a # Set time to noon, 12:00:00 t[0, 3] = [ 0, 0, 12 ] weekday = t[6] t.slice!(6, 4) t.reverse! # Substract the number of days determined by the weekday t[6] and set time # to midnight of that day. (TjTime.new(Time.local(*t)) - (weekday - (startMonday ? 1 : 0)) * 60 * 60 * 24).midnight end # Normalize time to the beginning of the current month. def beginOfMonth sec, min, hour, day, month, year = localtime.to_a sec = min = hour = 0 day = 1 TjTime.new([ year, month, day, hour, min, sec, 0 ]) end # Normalize time to the beginning of the current quarter. def beginOfQuarter sec, min, hour, day, month, year = localtime.to_a sec = min = hour = 0 day = 1 month = ((month - 1) % 3 ) + 1 TjTime.new([ year, month, day, hour, min, sec, 0 ]) end # Normalize time to the beginning of the current year. def beginOfYear sec, min, hour, day, month, year = localtime.to_a sec = min = hour = 0 day = month = 1 TjTime.new([ year, month, day, hour, min, sec, 0 ]) end # Return a new time that is _hours_ later than time. def hoursLater(hours) TjTime.new(@time + hours * 3600) end # Return a new time that is 1 hour later than time. def sameTimeNextHour hoursLater(1) end # Return a new time that is 1 day later than time but at the same time of # day. def sameTimeNextDay sec, min, hour, day, month, year = localtime.to_a if (day += 1) > lastDayOfMonth(month, year) day = 1 if (month += 1) > 12 month = 1 year += 1 end end TjTime.new([ year, month, day, hour, min, sec, 0 ]) end # Return a new time that is 1 week later than time but at the same time of # day. def sameTimeNextWeek sec, min, hour, day, month, year = localtime.to_a if (day += 7) > lastDayOfMonth(month, year) day -= lastDayOfMonth(month, year) if (month += 1) > 12 month = 1 year += 1 end end TjTime.new([ year, month, day, hour, min, sec, 0 ]) end # Return a new time that is 1 month later than time but at the same time of # day. def sameTimeNextMonth sec, min, hour, day, month, year = localtime.to_a monMax = month == 2 && leapYear?(year) ? 29 : MON_MAX[month] if (month += 1) > 12 month = 1 year += 1 end day = monMax if day >= lastDayOfMonth(month, year) TjTime.new([ year, month, day, hour, min, sec, 0 ]) end # Return a new time that is 1 quarter later than time but at the same time of # day. def sameTimeNextQuarter sec, min, hour, day, month, year = localtime.to_a if (month += 3) > 12 month -= 12 year += 1 end TjTime.new([ year, month, day, hour, min, sec, 0 ]) end # Return a new time that is 1 year later than time but at the same time of # day. def sameTimeNextYear sec, min, hour, day, month, year = localtime.to_a year += 1 TjTime.new([ year, month, day, hour, min, sec, 0]) end # Return the start of the next _dow_ day of week after _date_. _dow_ must # be 0 for Sundays, 1 for Mondays and 6 for Saturdays. If _date_ is a # Tuesday and _dow_ is 5 (Friday) the date of next Friday 0:00 will be # returned. If _date_ is a Tuesday and _dow_ is 2 (Tuesday) the date of # the next Tuesday will be returned. def nextDayOfWeek(dow) raise "Day of week must be 0 - 6." unless dow >= 0 && dow <= 6 d = midnight.sameTimeNextDay currentDoW = d.strftime('%w').to_i 1.upto((dow + 7 - currentDoW) % 7) { |i| d = d.sameTimeNextDay } d end # Return the number of hours between this time and _date_. The result is # always rounded up. def hoursTo(date) t1, t2 = order(date) ((t2 - t1) / 3600).ceil end # Return the number of days between this time and _date_. The result is # always rounded up. def daysTo(date) countIntervals(date, :sameTimeNextDay) end # Return the number of weeks between this time and _date_. The result is # always rounded up. def weeksTo(date) countIntervals(date, :sameTimeNextWeek) end # Return the number of months between this time and _date_. The result is # always rounded up. def monthsTo(date) countIntervals(date, :sameTimeNextMonth) end # Return the number of quarters between this time and _date_. The result is # always rounded up. def quartersTo(date) countIntervals(date, :sameTimeNextQuarter) end # Return the number of years between this time and _date_. The result is # always rounded up. def yearsTo(date) countIntervals(date, :sameTimeNextYear) end # This function is just a wrapper around Time.strftime(). In case @time is # nil, it returns 'unkown'. def to_s(format = nil, tz = nil) return 'unknown' if @time.nil? t = tz == 'UTC' ? gmtime : localtime if format.nil? fmt = '%Y-%m-%d-%H:%M' + (@time.sec == 0 ? '' : ':%S') + '-%z' else # Handle TJ specific extensions to the strftime format. fmt = format.sub(/%Q/, "#{((t.mon - 1) / 3) + 1}") end # Always report values in local timezone t.strftime(fmt) end # Return the seconds since Epoch. def to_i localtime.to_i end def to_a localtime.to_a end def strftime(format) localtime.strftime(format) end # Return the day of the week. 0 for Sunday, 1 for Monday and so on. def wday localtime.wday end # Return the hours of the day (0..23) def hour localtime.hour end # Return the day of the month (1..n). def day localtime.day end # Return the month of the year (1..12) def month localtime.month end alias mon month # Return the year. def year localtime.year end private def parse(t) year, month, day, time, zone = t.split('-', 5) # Check the year if year year = year.to_i if year < 1970 || year > 2035 raise TjException.new, "Year #{year} out of range (1970 - 2035)" end else raise TjException.new, "Year not specified" end # Check the month if month month = month.to_i if month < 1 || month > 12 raise TjException.new, "Month #{month} out of range (1 - 12)" end else raise TjException.new, "Month not specified" end # Check the day if day day = day.to_i maxDay = [ 0, 31, Date.gregorian_leap?(year) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 ] if month < 1 || month > maxDay[month] raise TjException.new, "Day #{day} out of range (1 - #{maxDay[month]})" end else raise TjException.new, "Day not specified" end # The time is optional. Will be expanded to 00:00:00. if time hour, minute, second = time.split(':') # Check hour if hour hour = hour.to_i if hour < 0 || hour > 23 raise TjException.new, "Hour #{hour} out of range (0 - 23)" end else raise TjException.new, "Hour not specified" end if minute minute = minute.to_i if minute < 0 || minute > 59 raise TjException.new, "Minute #{minute} out of range (0 - 59)" end else raise TjException.new, "Minute not specified" end # Check sencond. This value is optional and defaults to 0. if second second = second.to_i if second < 0 || second > 59 raise TjException.new, "Second #{second} out of range (0 - 59)" end else second = 0 end else hour = minute = second = 0 end # The zone is optional and defaults to the current time zone. if zone if zone[0] != ?- && zone[0] != ?+ raise TjException.new, "Time zone adjustment must be prefixed by " + "+ or -, not #{zone[0]}" end if zone.length != 5 raise TjException.new, "Time zone adjustment must use (+/-)HHMM format" end @time = Time.utc(year, month, day, hour, minute, second) sign = zone[0] == ?- ? -1 : 1 tzHour = zone[1..2].to_i tzMinute = zone[3..4].to_i if tzMinute < 0 || tzMinute > 59 raise TjException.new, "Time zone adjustment minute out of range " + "(0 - 59) but is #{tzMinute}" end time_offset = sign * (tzHour * 3600 + tzMinute * 60) # UTC-1200 is the most westerly time zone but UTC+1400 is the most # easterly time zone (Republic of Kiribati). if time_offset < -12 * 3600 || time_offset > 14 * 3600 raise TjException.new, "Time zone adjustment out of range " + "(-1200 - +1400} but is #{zone})" end # The time offset must be substracted from the base time to convert it # to UTC. @time -= time_offset else @time = Time.mktime(year, month, day, hour, minute, second) end end def order(date) self < date ? [ self, date ] : [ date, self ] end def countIntervals(date, stepFunc) i = 0 t1, t2 = order(date) while t1 < t2 t1 = t1.send(stepFunc) i += 1 end i end def lastDayOfMonth(month, year) month == 2 && leapYear?(year) ? 29 : MON_MAX[month] end def leapYear?(year) case when year % 400 == 0 true when year % 100 == 0 false else year % 4 == 0 end end def gmtime if @time.utc? # @time is already in the right zone (UTC) @time else # To convert a Time object from local time to UTC. @time.dup.gmtime end end def localtime if @time.utc? if @@tz == 'UTC' # @time is already in the right zone (UTC) @time else @time.dup.localtime end elsif @@tz == 'UTC' # @time is not in UTC, so convert it to local time. @time.dup.gmtime else # To convert a Time object from one local time to another, we need to # conver to UTC first and then to the new local time. @time.dup.gmtime.localtime end end end end TaskJuggler-3.8.1/lib/taskjuggler/TjpExample.rb000066400000000000000000000070541473026623400214500ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = TjpExample.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'stringio' class TaskJuggler # This class can extract snippets from an annotated TJP syntax file. The file # does not care about the TJP syntax but the annotation lines must start with # a '#' character at the begining of the line. The snippets must be enclosed # by a starting line and an ending line. Each snippet must have a unique tag # that can be used to retrieve the specific snip. # # The following line starts a snip called 'foo': # # *** EXAMPLE: foo + # # The following line ends a snip called 'foo': # # *** EXAMPLE: foo - # # The function TjpExample#to_s() can be used to get the content of the snip. # It takes the tag name as optional parameter. If no tag is specified, the # full example without the annotation lines is returned. class TjpExample # Create a new TjpExample object. def initialize @snippets = { } # Here we will store the complete example. @snippets['full text'] = [] @file = nil end # Use this function to process the file called _fileName_. def open(fileName) @file = File.open(fileName, 'r') process @file.close end # Use this function to process the String _text_. def parse(text) @file = StringIO.new(text) process end # This method returns the snip identified by _tag_. def to_s(tag = nil) tag = 'full text' unless tag return nil unless @snippets[tag] s = '' @snippets[tag].each { |l| s << l } s end private def process # This mark identifies the annotation lines. mark = '# *** EXAMPLE: ' # We need this to remember what snippets are currently active. snippetState = { } # Now process the file or String line by line. @file.each_line do |line| if line[0, mark.length] == mark # We've found an annotation line. Get the tag and indicator. tokens = line.split tag = tokens[3] indicator = tokens[4] if indicator == '+' # Start a new snip if snippetState[tag] raise "Snippet #{tag} has already been started" end snippetState[tag] = true elsif indicator == '-' # Stop an existing snip unless snippetState[tag] raise "Snippet #{tag} has not yet been started" end snippetState[tag] = false else raise "Bad indicator #{indicator}. Must be '+' or '-': #{line}" end else # Process the regular lines and add them to all currently active # snippets. snippetState.each do |t, state| if state # Create a new snip buffer if it does not yet exist. @snippets[t] = [] unless @snippets[t] # Add the line. @snippets[t] << line end end # Add all lines to this buffer. @snippets['full text'] << line end end # Remove empty lines at end of all snips @snippets.each_value do |snip| snip.delete_at(-1) if snip[-1] == "\n" end end end end TaskJuggler-3.8.1/lib/taskjuggler/TjpSyntaxRules.rb000066400000000000000000007746551473026623400224000ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = TjpSyntaxRules.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2020 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # class TaskJuggler # This module contains the rule definition for the TJP syntax. Every rule is # put in a function who's name must start with rule_. The functions are not # necessary but make the file more readable and receptable to syntax folding. module TjpSyntaxRules def rule_absoluteTaskId pattern(%w( !taskIdUnverifd ), lambda { id = (@taskprefix.empty? ? '' : @taskprefix + '.') + @val[0] if (task = @project.task(id)).nil? error('unknown_abs_task', "Unknown task #{id}", @sourceFileInfo[0]) end task }) end def rule_account pattern(%w( !accountHeader !accountBody ), lambda { @property = @property.parent }) doc('account', <<'EOT' Declares an account. Accounts can be used to calculate costs of tasks or the whole project. Account declaration may be nested, but only leaf accounts may be used to track turnover. When the cost of a task is split over multiple accounts they all must have the same top-level group account. Top-level accounts can be used for profit/loss calculations. The sub-account structure of a top-level account should be organized accordingly. Accounts have a global name space. All IDs must be unique within the accounts of the project. EOT ) example('Account', '1') end def rule_accountAttributes repeatable optional pattern(%w( !account)) pattern(%w( !accountScenarioAttributes )) pattern(%w( !scenarioIdCol !accountScenarioAttributes ), lambda { @scenarioIdx = 0 }) # Other attributes will be added automatically. end def rule_accountBody optionsRule('accountAttributes') end def rule_accountCredit pattern(%w( !valDate $STRING !number ), lambda { AccountCredit.new(@val[0], @val[1], @val[2]) }) arg(1, 'description', 'Short description of the transaction') arg(2, 'amount', 'Amount to be booked.') end def rule_accountCredits listRule('moreAccountCredits', '!accountCredit') end def rule_accountHeader pattern(%w( _account !optionalID $STRING ), lambda { if @property.nil? && !@accountprefix.empty? @property = @project.account(@accountprefix) end if @val[1] && @project.account(@val[1]) error('account_exists', "Account #{@val[1]} has already been defined.", @sourceFileInfo[1], @property) end @property = Account.new(@project, @val[1], @val[2], @property) @property.sourceFileInfo = @sourceFileInfo[0] @property.inheritAttributes @scenarioIdx = 0 }) arg(2, 'name', 'A name or short description of the account') end def rule_accountId pattern(%w( $ID ), lambda { id = @val[0] id = @accountprefix + '.' + id unless @accountprefix.empty? # In case we have a nested supplement, we need to prepend the parent ID. id = @property.fullId + '.' + id if @property && @property.is_a?(Account) if (account = @project.account(id)).nil? error('unknown_account', "Unknown account #{id}", @sourceFileInfo[0]) end account }) end def rule_accountReport pattern(%w( !accountReportHeader !reportBody ), lambda { @property = @property.parent }) level(:beta) doc('accountreport', <<'EOT' The report lists accounts and their respective values in a table. The report can operate in two modes: # Balance mode: If a [[balance]] has been set, the report will include the defined cost and revenue accounts as well as all their sub accounts. To reduce the list of included accounts, you can use the [[hideaccount]], [[rollupaccount]] or [[accountroot]] attributes. The order of the task can be controlled with [[sortaccounts]]. If the first sorting criteria is tree sorting, the parent accounts will always be included to form the tree. Tree sorting is the default. You need to change it if you do not want certain parent accounts to be included in the report. Additionally, it will contain a line at the end that lists the balance (revenue - cost). # Normal mode: All reports are listed in the order and completeness as defined by the other report attributes. No balance line will be included. EOT ) example('AccountReport') end def rule_accountReportHeader pattern(%w( _accountreport !optionalID !reportName ), lambda { newReport(@val[1], @val[2], :accountreport, @sourceFileInfo[0]) do unless @property.modified?('columns') # Set the default columns for this report. %w( bsi name monthly ).each do |col| @property.get('columns') << TableColumnDefinition.new(col, columnTitle(col)) end end # Show all accounts, sorted by tree, seqno-up. unless @property.modified?('hideAccount') @property.set('hideAccount', LogicalExpression.new(LogicalOperation.new(0))) end unless @property.modified?('sortAccounts') @property.set('sortAccounts', [ [ 'tree', true, -1 ], [ 'seqno', true, -1 ] ]) end end }) end def rule_accountScenarioAttributes pattern(%w( _aggregate !aggregate ), lambda { @property.set('aggregate', @val[1]) }) doc('aggregate', <<'EOT' Specifies whether the account is used to track task or resource specific amounts. The default is to track tasks. EOT ) example('AccountReport') pattern(%w( _credits !accountCredits ), lambda { @property['credits', @scenarioIdx] += @val[1] }) doc('credits', <<'EOT' Book the specified amounts to the account at the specified date. The desciptions are just used for documentary purposes. EOT ) example('Account', '1') pattern(%w( !flags )) doc('flags.account', <<'EOT' Attach a set of flags. The flags can be used in logical expressions to filter properties from the reports. EOT ) # Other attributes will be added automatically. end def rule_aggregate pattern(%w( _resources ), lambda { :resources }) descr('Aggregate resources') pattern(%w( _tasks ), lambda { :tasks }) descr('Aggregate tasks') end def rule_alertLevel pattern(%w( $ID ), lambda { level = @project['alertLevels'].indexById(@val[0]) unless level levels = @project['alertLevels'].map { |l| l.id } error('bad_alert', "Unknown alert level #{@val[0]}. Must be " + "one of #{levels.join(', ')}", @sourceFileInfo[0]) end level }) arg(0, 'alert level', <<'EOT' By default supported values are ''''green'''', ''''yellow'''' and ''''red''''. The default value is ''''green''''. You can define your own levels with [[alertlevels]]. EOT ) end def rule_alertLevelDefinition pattern(%w( $ID $STRING !color ), lambda { [ @val[0], @val[1], @val[2] ] }) arg(0, 'ID', "A unique ID for the alert level") arg(1, 'color name', 'A unique name of the alert level color') end def rule_alertLevelDefinitions listRule('moreAlertLevelDefinitions', '!alertLevelDefinition') end def rule_allocate pattern(%w( _allocate !allocations ), lambda { checkContainer('allocate') @property['allocate', @scenarioIdx] += @val[1] }) doc('allocate', <<'EOT' Specify which resources should be allocated to the task. The attributes provide numerous ways to control which resource is used and when exactly it will be assigned to the task. Shifts and limits can be used to restrict the allocation to certain time intervals or to limit them to a certain maximum per time period. The purge statement can be used to remove inherited allocations or flags. For effort-based tasks the task duration is clipped to only extend from the beginning of the first allocation to the end of the last allocation. This is done to optimize for an overall minimum project duration as dependent tasks can potentially use the unallocated, clipped slots. EOT ) example('Allocate-1', '1') end def rule_allocation pattern(%w( !allocationHeader !allocationBody ), lambda { @val[0] }) end def rule_allocationAttributes optional repeatable pattern(%w( _alternative !resourceId !moreAlternatives ), lambda { ([ @val[1] ] + (@val[2] ? @val[2] : [])).each do |candidate| @allocate.addCandidate(candidate) end }) doc('alternative', <<'EOT' Specify which resources should be allocated to the task. The optional attributes provide numerous ways to control which resource is used and when exactly it will be assigned to the task. Shifts and limits can be used to restrict the allocation to certain time intervals or to limit them to a certain maximum per time period. EOT ) example('Alternative', '1') pattern(%w( !limits ), lambda { limits = @property['limits', @scenarioIdx] = @val[0] @allocate.candidates.each do |resource| limits.limits.each do |l| l.resource = resource if resource.leaf? end end }) level(:removed) doc('limits.allocate', '') pattern(%w( _select !allocationSelectionMode ), lambda { @allocate.setSelectionMode(@val[1]) }) doc('select', <<'EOT' The select function controls which resource is picked from an allocation and it's alternatives. The selection is re-evaluated each time the resource used in the previous time slot becomes unavailable. Even for non-persistent allocations a change in the resource selection only happens if the resource used in the previous (or next for ASAP tasks) time slot has become unavailable. EOT ) pattern(%w( _persistent ), lambda { @allocate.persistent = true }) doc('persistent', <<'EOT' Specifies that once a resource is picked from the list of alternatives, this resource is used for the whole task. This is useful when several alternative resources have been specified. Normally the selected resource can change after each break. A break is an interval of at least one timeslot where no resources were available. EOT ) pattern(%w( _mandatory ), lambda { @allocate.mandatory = true }) doc('mandatory', <<'EOT' Makes a resource allocation mandatory. This means, that for each time slot only then resources are allocated when all mandatory resources are available. So either all mandatory resources can be allocated for the time slot, or no resource will be allocated. EOT ) pattern(%w( !allocateShiftAssignments !shiftAssignment ), lambda { begin @allocate.shifts = @shiftAssignments rescue AttributeOverwrite # Multiple shift assignments are a common idiom, so don't warn about # them. end @shiftAssignments = nil }) level(:deprecated) also('shifts.allocate') doc('shift.allocate', <<'EOT' Limits the allocations of resources during the specified interval to the specified shift. Multiple shifts can be defined, but shift intervals may not overlap. Allocation shifts are an additional restriction to the [[shifts.task|task shifts]] and [[shifts.resource|resource shifts]] or [[workinghours.resource|resource working hours]]. Allocations will only be made for time slots that are specified as duty time in all relevant shifts. The restriction to the shift is only active during the specified time interval. Outside of this interval, no restrictions apply. EOT ) pattern(%w( !allocateShiftsAssignments !shiftAssignments ), lambda { begin @allocate.shifts = @shiftAssignments rescue AttributeOverwrite # Multiple shift assignments are a common idiom, so don't warn about # them. end @shiftAssignments = nil }) doc('shifts.allocate', <<'EOT' Limits the allocations of resources during the specified interval to the specified shift. Multiple shifts can be defined, but shift intervals may not overlap. Allocation shifts are an additional restriction to the [[shifts.task|task shifts]] and [[shifts.resource|resource shifts]] or [[workinghours.resource|resource working hours]]. Allocations will only be made for time slots that are specified as duty time in all relevant shifts. The restriction to the shift is only active during the specified time interval. Outside of this interval, no restrictions apply. EOT ) end def rule_allocationBody optionsRule('allocationAttributes') end def rule_allocationHeader pattern(%w( !resourceId ), lambda { @allocate = Allocation.new([ @val[0] ]) }) end def rule_allocations listRule('moreAllocations', '!allocation') end def rule_allocationSelectionMode singlePattern('_maxloaded') descr('Pick the available resource that has been used the most so far.') singlePattern('_minloaded') descr('Pick the available resource that has been used the least so far.') singlePattern('_minallocated') descr(<<'EOT' Pick the resource that has the smallest allocation factor. The allocation factor is calculated from the various allocations of the resource across the tasks. This is the default setting. EOT ) singlePattern('_order') descr('Pick the first available resource from the list.') singlePattern('_random') descr('Pick a random resource from the list.') end def rule_allocateShiftAssignments pattern(%w( _shift ), lambda { @shiftAssignments = @allocate.shifts }) end def rule_allocateShiftsAssignments pattern(%w( _shifts ), lambda { @shiftAssignments = @allocate.shifts }) end def rule_allOrNone pattern(%w( _all ), lambda { 1 }) pattern(%w( _none ), lambda { 0 }) end def rule_argument singlePattern('$ABSOLUTE_ID') singlePattern('!date') singlePattern('$ID') singlePattern('$INTEGER') singlePattern('$FLOAT') end def rule_argumentList optional pattern(%w( _( !argumentListBody _) ), lambda { @val[1].nil? ? [] : @val[1] }) end def rule_argumentListBody optional pattern(%w( !argument !moreArguments ), lambda { [ @val[0] ] + (@val[1].nil? ? [] : @val[1]) }) end def rule_author pattern(%w( _author !resourceId ), lambda { @journalEntry.author = @val[1] }) doc('author', <<'EOT' This attribute can be used to capture the authorship or source of the information. EOT ) end def rule_balance pattern(%w( _balance !balanceAccounts ), lambda { @val[1] }) doc('balance', <<'EOT' During report generation, TaskJuggler can consider some accounts to be revenue accounts, while other can be considered cost accounts. By using the balance attribute, two top-level accounts can be designated for a profit-loss-analysis. This analysis includes all sub accounts of these two top-level accounts. To clear a previously set balance, just use a ''''-''''. EOT ) example('AccountReport') end def rule_balanceAccounts pattern(%w( !accountId !accountId ), lambda { if @val[0].parent error('cost_acct_no_top', "The cost account #{@val[0].fullId} is not a top-level account.", @sourceFileInfo[0]) end if @val[1].parent error('rev_acct_no_top', "The revenue account #{@val[1].fullId} is not a top-level " + "account.", @sourceFileInfo[1]) end if @val[0] == @val[1] error('cost_rev_same', 'The cost and revenue accounts may not be the same.', @sourceFileInfo[0]) end [ @val[0], @val[1] ] }) arg(0, 'cost account', <<'EOT' The top-level account that is used for all cost related charges. EOT ) arg(2, 'revenue account', <<'EOT' The top-level account that is used for all revenue related charges. EOT ) pattern([ '_-' ], lambda { [ nil, nil ] }) end def rule_bookingAttributes optional repeatable pattern(%w( _overtime $INTEGER ), lambda { if @val[1] < 0 || @val[1] > 2 error('overtime_range', "Overtime value #{@val[1]} out of range (0 - 2).", @sourceFileInfo[1], @property) end @booking.overtime = @val[1] }) doc('overtime.booking', <<'EOT' This attribute enables bookings during off-hours and leaves. It implicitly sets the [[sloppy.booking|sloppy]] attribute accordingly. EOT ) arg(1, 'value', <<'EOT' * '''0''': You can only book available working time. (Default) * '''1''': You can book off-hours as well. * '''2''': You can book working time, off-hours and vacation time. EOT ) pattern(%w( _sloppy $INTEGER ), lambda { if @val[1] < 0 || @val[1] > 2 error('sloppy_range', "Sloppyness value #{@val[1]} out of range (0 - 2).", @sourceFileInfo[1], @property) end @booking.sloppy = @val[1] }) doc('sloppy.booking', <<'EOT' Controls how strict TaskJuggler checks booking intervals for conflicts with working periods and leaves. This attribute only affects the check for conflicts. No assignments will be made unless the [[overtime.booking| overtime]] attribute is set accordingly. EOT ) arg(1, 'sloppyness', <<'EOT' * '''0''': Period may not contain any off-duty hours, vacation or other task assignments. (default) * '''1''': Period may contain off-duty hours, but no vacation time or other task assignments. * '''2''': Period may contain off-duty hours and vacation time, but no other task assignments. EOT ) end def rule_bookingBody optionsRule('bookingAttributes') end def rule_calendarDuration pattern(%w( !number !durationUnit ), lambda { convFactors = [ 60.0, # minutes 60.0 * 60, # hours 60.0 * 60 * 24, # days 60.0 * 60 * 24 * 7, # weeks 60.0 * 60 * 24 * 30.4167, # months 60.0 * 60 * 24 * 365 # years ] ((@val[0] * convFactors[@val[1]]) / @project['scheduleGranularity']).to_i }) arg(0, 'value', 'A floating point or integer number') end def rule_chargeset pattern(%w( _chargeset !chargeSetItem !moreChargeSetItems ), lambda { checkContainer('chargeset') items = [ @val[1] ] items += @val[2] if @val[2] chargeSet = ChargeSet.new begin items.each do |item| chargeSet.addAccount(item[0], item[1]) end chargeSet.complete rescue TjException error('chargeset', $!.message, @sourceFileInfo[0], @property) end masterAccounts = [] @property['chargeset', @scenarioIdx].each do |set| masterAccounts << set.master end if masterAccounts.include?(chargeSet.master) error('chargeset_master', "All charge sets for this property must have different " + "top-level accounts.", @sourceFileInfo[0], @property) end @property['chargeset', @scenarioIdx] = @property['chargeset', @scenarioIdx] + [ chargeSet ] }) doc('chargeset', <<'EOT' A chargeset defines how the turnover associated with the property will be charged to one or more accounts. A property may have any number of charge sets, but each chargeset must deal with a different top-level account. A charge set consists of one or more accounts. Each account must be a leaf account. The account ID may be followed by a percentage value that determines the share for this account. The total percentage of all accounts must be exactly 100%. If some accounts don't have a percentage specification, the remainder to 100% is distributed evenly between them. EOT ) end def rule_chargeMode singlePattern('_onstart') descr('Charge the amount on starting the task.') singlePattern('_onend') descr('Charge the amount on finishing the task.') singlePattern('_perhour') descr('Charge the amount for every hour the task lasts.') singlePattern('_perday') descr('Charge the amount for every day the task lasts.') singlePattern('_perweek') descr('Charge the amount for every week the task lasts.') end def rule_chargeSetItem pattern(%w( !accountId !optionalPercent ), lambda { if @property.is_a?(Task) aggregate = :tasks elsif @property.is_a?(Resource) aggregate = :resources else raise "Unknown property type #{@property.class}" end if @val[0].get('aggregate') != aggregate error('account_bad_aggregate', "The account #{@val[0].fullId} cannot aggregate amounts " + "related to #{aggregate}.") end [ @val[0], @val[1] ] }) arg(0, 'account', 'The ID of a previously defined leaf account.') arg(1, 'share', 'A percentage between 0 and 100%') end def rule_chartScale singlePattern('_hour') descr('Set chart resolution to 1 hour.') singlePattern('_day') descr('Set chart resolution to 1 day.') singlePattern('_week') descr('Set chart resolution to 1 week.') singlePattern('_month') descr('Set chart resolution to 1 month.') singlePattern('_quarter') descr('Set chart resolution to 1 quarter.') singlePattern('_year') descr('Set chart resolution to 1 year.') end def rule_color pattern(%w( $STRING ), lambda { col = @val[0] unless /#[0-9A-Fa-f]{3}/ =~ col || /#[0-9A-Fa-f]{6}/ =~ col error('bad_color', "Color values must be specified as '#RGB' or '#RRGGBB' values", @sourceFileInfo[0]) end col }) arg(0, 'color', <<'EOT' The RGB color values of the color. The following formats are supported: #RGB and #RRGGBB. Where R, G, B are hexadecimal values. See [http://en.wikipedia.org/wiki/Web_colors Wikipedia] for more details. EOT ) end def rule_columnBody optionsRule('columnOptions') end def rule_columnDef pattern(%w( !columnId !columnBody ), lambda { @val[0] }) end def rule_columnId pattern(%w( !reportableAttributes ), lambda { @column = TableColumnDefinition.new(@val[0], columnTitle(@val[0])) }) doc('columnid', <<'EOT' This is a comprehensive list of all pre-defined [[columns]]. In addition to the listed IDs all user defined attributes can be used as column IDs. EOT ) end def rule_columnOptions optional repeatable pattern(%w( _celltext !logicalExpression $STRING ), lambda { @column.cellText.addPattern( CellSettingPattern.new(newRichText(@val[2], @sourceFileInfo[2]), @val[1])) }) doc('celltext.column', <<'EOT' Specifies an alternative content that is used for the cells of the column. Usually such a text contains a query function. Otherwise all cells of the column will have the same fixed value. The logical expression specifies for which cells the text should be used. If multiple celltext patterns are provided for a column, the first matching one is taken for each cell. EOT ) arg(2, 'text', 'Alterntive cell text specified as [[Rich_Text_Attributes|Rich Text]]') pattern(%w( _cellcolor !logicalExpression !color ), lambda { @column.cellColor.addPattern( CellSettingPattern.new(@val[2], @val[1])) }) doc('cellcolor.column', <<'EOT' Specifies an alternative background color for the cells of this column. The [[logicalexpression|logical expression]] specifies for which cells the color should be used. If multiple cellcolor patterns are provided for a column, the first matching one is used for each cell. EOT ) pattern(%w( _end !date ), lambda { @column.end = @val[1] }) doc('end.column', <<'EOT' Normally, columns with calculated values take the specified report period into account when calculating their values. With this attribute, the user can specify an end date for the period that should be used when calculating the values of this column. It does not have an impact on columns with time invariant values. EOT ) pattern(%w( _fontcolor !logicalExpression !color ), lambda { @column.fontColor.addPattern( CellSettingPattern.new(@val[2], @val[1])) }) doc('fontcolor.column', <<'EOT' Specifies an alternative font color for the cells of this column. The [[logicalexpression|logical expression]] specifies for which cells the color should be used. If multiple fontcolor patterns are provided for a column, the first matching one is used for each cell. EOT ) pattern(%w( _halign !logicalExpression !hAlignment ), lambda { @column.hAlign.addPattern( CellSettingPattern.new(@val[2], @val[1])) }) doc('halign.column', <<'EOT' Specifies the horizontal alignment of the cell content. The [[logicalexpression|logical expression]] specifies for which cells the alignment setting should be used. If multiple halign patterns are provided for a column, the first matching one is used for each cell. EOT ) pattern(%w( _listitem $STRING ), lambda { @column.listItem = @val[1] }) doc('listitem.column', <<'EOT' Specifies a [[Rich_Text_Attributes|Rich Text]] pattern that is used to generate the text for the list items. The pattern should contain at least one ''''<-query attribute='XXX'->'''' element that will be replaced with the value of attribute XXX. For the replacement, the property of the query will be the list item. EOT ) pattern(%w( _listtype !listType ), lambda { @column.listType = @val[1] }) also(%w( listitem.column )) doc('listtype.column', <<'EOT' Specifies what type of list should be used. This attribute only affects columns that contain a list of items. EOT ) pattern(%w( _period !interval ), lambda { @column.start = @val[1].start @column.end = @val[1].end }) doc('period.column', <<'EOT' This property is a shortcut for setting the [[start.column|start]] and [[end.column|end]] property at the same time. EOT ) pattern(%w( _scale !chartScale ), lambda { @column.scale = @val[1] }) doc('scale.column', <<'EOT' Specifies the scale that should be used for a chart column. This value is ignored for all other columns. EOT ) pattern(%w( _start !date ), lambda { @column.start = @val[1] }) doc('start.column', <<'EOT' Normally, columns with calculated values take the specified report period into account when calculating their values. With this attribute, the user can specify a start date for the period that should be used when calculating the values of this column. It does not have an impact on columns with time invariant values. EOT ) pattern(%w( _timeformat1 $STRING ), lambda { @column.timeformat1 = @val[1] }) doc('timeformat1', <<'EOT' Specify an alternative format for the upper header line of calendar or Gantt chart columns. EOT ) arg(1, 'format', 'See [[timeformat]] for details.') pattern(%w( _timeformat2 $STRING ), lambda { @column.timeformat2 = @val[1] }) doc('timeformat2', <<'EOT' Specify an alternative format for the lower header line of calendar or Gantt chart columns. EOT ) arg(1, 'format', 'See [[timeformat]] for details.') pattern(%w( _title $STRING ), lambda { @column.title = @val[1] }) doc('title.column', <<'EOT' Specifies an alternative title for a report column. EOT ) arg(1, 'text', 'The new column title.') pattern(%w( _tooltip !logicalExpression $STRING ), lambda { @column.tooltip.addPattern( CellSettingPattern.new(newRichText(@val[2], @sourceFileInfo[2]), @val[1])) }) doc('tooltip.column', <<'EOT' Specifies an alternative content for the tooltip. This will replace the original content of the tooltip that would be available for columns with text that does not fit the column with. The [[logicalexpression|logical expression]] specifies for which cells the text should be used. If multiple tooltip patterns are provided for a column, the first matching one is taken for each cell. EOT ) arg(2, 'text', <<'EOT' The content of the tooltip. The text is interpreted as [[Rich_Text_Attributes| Rich Text]]. EOT ) pattern(%w( _width !number ), lambda { @column.width = @val[1] }) doc('width.column', <<'EOT' Specifies the maximum width of the column in screen pixels. If the content of the column does not fit into this width, it will be cut off. In some cases a scrollbar is added or a tooltip window with the complete content is shown when the mouse is moved over the column. The latter is only supported in interactive output formats. The resulting column width may be smaller if the column has a fixed width (e. g. the chart column). EOT ) end def rule_currencyFormat pattern(%w( _currencyformat $STRING $STRING $STRING $STRING $INTEGER ), lambda { RealFormat.new(@val.slice(1, 5)) }) doc('currencyformat', 'These values specify the default format used for all currency ' + 'values.') example('Currencyformat') arg(1, 'negativeprefix', 'Prefix for negative numbers') arg(2, 'negativesuffix', 'Suffix for negative numbers') arg(3, 'thousandsep', 'Separator used for every 3rd digit') arg(4, 'fractionsep', 'Separator used to separate the fraction digits') arg(5, 'fractiondigits', 'Number of fraction digits to show') end def rule_date pattern(%w( !dateCalcedOrNot ), lambda { resolution = @project.nil? ? Project.maxScheduleGranularity : @project['scheduleGranularity'] if @val[0] % resolution != 0 error('misaligned_date', "The date must be aligned to the timing resolution (" + "#{resolution / 60} min) of the project.", @sourceFileInfo[0]) end @val[0] }) doc('date', <<'EOT' A DATE is date and time specification similar to the ISO 8601 date format. Instead of the hard to read ISO notation with a ''''T'''' between the date and time sections, we simply use the more intuitive and easier to read dash: ''''YYYY-MM-DD[-hh:mm[:ss]][-TIMEZONE]''''. Hour, minutes, seconds, and the ''''TIMEZONE'''' are optional. If not specified, the values are set to 0. ''''TIMEZONE'''' must be an offset to GMT or UTC, specified as ''''+HHMM'''' or ''''-HHMM''''. Dates must always be aligned with the [[timingresolution]]. TaskJuggler also supports simple date calculations. You can add or subtract a given interval from a fixed date. %{2009-11-01 + 8m} This will result in an actual date of around 2010-07-01. Keep in mind that due to the varying lengths of months TaskJuggler cannot add exactly 8 calendar months. The date calculation functionality makes most sense when used with macros. %{${now} - 2w} This results in a date 2 weeks earlier than the current (or specified) date. See [[duration]] for a complete list of supported time intervals. Don't forget to put at least one space character after the date to prevent TaskJuggler from interpreting the interval as an hour. Date attributes may be invalid in some cases. This needs special care in [[logicalexpression|logical expressions]]. EOT ) end def rule_dateCalcedOrNot singlePattern('$DATE') pattern(%w( _% _{ $DATE !plusOrMinus !intervalDuration _} ), lambda { @val[2] + ((@val[3] == '+' ? 1 : -1) * @val[4]) }) end def rule_declareFlagList listRule('moreDeclareFlagList', '$ID') end def rule_details pattern(%w( _details $STRING ), lambda { return if @val[1].empty? rtTokenSetMore = [ :LINEBREAK, :SPACE, :WORD, :BOLD, :ITALIC, :CODE, :BOLDITALIC, :PRE, :HREF, :HREFEND, :REF, :REFEND, :HLINE, :TITLE2, :TITLE3, :TITLE4, :TITLE2END, :TITLE3END, :TITLE4END, :BULLET1, :BULLET2, :BULLET3, :BULLET4, :NUMBER1, :NUMBER2, :NUMBER3, :NUMBER4 ] if @val[1] == "Some more details\n" error('ts_default_details', "'Some more details' is not a valid value", @sourceFileInfo[1]) end @journalEntry.details = newRichText(@val[1], @sourceFileInfo[1], rtTokenSetMore) }) doc('details', <<'EOT' This is a continuation of the [[summary]] of the journal or status entry. It can be several paragraphs long. EOT ) arg(1, 'text', <<'EOT' The text will be interpreted as [[Rich_Text_Attributes|Rich Text]]. Only a subset of the markup is supported for this attribute. You can use word formatting, paragraphs, hyperlinks, lists, section and subsection headers. EOT ) end def rule_durationUnit pattern(%w( _min ), lambda { 0 }) descr('minutes') pattern(%w( _h ), lambda { 1 }) descr('hours') pattern(%w( _d ), lambda { 2 }) descr('days') pattern(%w( _w ), lambda { 3 }) descr('weeks') pattern(%w( _m ), lambda { 4 }) descr('months') pattern(%w( _y ), lambda { 5 }) descr('years') end def rule_durationUnitOrPercent pattern(%w( _% ), lambda { -1 }) descr('percentage of reported period') pattern(%w( _min ), lambda { 0 }) descr('minutes') pattern(%w( _h ), lambda { 1 }) descr('hours') pattern(%w( _d ), lambda { 2 }) descr('days') end def rule_dynamicAttributes pattern(%w( !reportAttributes . )) end def rule_export pattern(%w( !exportHeader !exportBody ), lambda { @property = @property.parent }) doc('export', <<'EOT' The export report looks like a regular TaskJuggler file with the provided input data complemented by the results of the scheduling process. The content of the report can be controlled with the [[definitions]] attribute. In case the file contains the project header, a ''''.tjp'''' extension is added to the file name. Otherwise, a ''''.tji'''' extension is used. The [[resourceattributes]] and [[taskattributes]] attributes provide even more control over the content of the file. The export report can be used to share certain tasks or milestones with other projects or to save past resource allocations as immutable part for future scheduling runs. When an export report is included the project IDs of the included tasks must be declared first with the project id property. EOT ) example('Export') end def rule_exportAttributes optional repeatable pattern(%w( _definitions !exportDefinitions ), lambda { @property.set('definitions', @val[1]) }) doc('definitions', <<"EOT" This attribute controls what definitions will be contained in the report. If the list includes ''project'', the generated file will have a ''''.tjp'''' extension. Otherwise it will have a ''''.tji'''' extension. By default, the report contains everything and the generated files have a ''''.tjp'''' extension. EOT ) allOrNothingListRule('exportDefinitions', { 'flags' => 'Include flag definitions', 'project' => 'Include project header', 'projecids' => 'Include project IDs', 'tasks' => 'Include task definitions', 'resources' => 'Include resource definitions' }) pattern(%w( _formats !exportFormats ), lambda { @property.set('formats', @val[1]) }) level(:beta) doc('formats.export', <<'EOT' This attribute defines for which output formats the export report should be generated. By default, the TJP format will be used. EOT ) pattern(%w( !hideresource )) pattern(%w( !hidetask )) pattern(%w( !loadunit )) pattern(%w( !purge )) pattern(%w( !reportEnd )) pattern(%w( !reportPeriod )) pattern(%w( !reports )) pattern(%w( !reportStart )) pattern(%w( _resourceattributes !exportableResourceAttributes ), lambda { @property.set('resourceAttributes', @val[1]) }) doc('resourceattributes', <<"EOT" Define a list of resource attributes that should be included in the report. EOT ) allOrNothingListRule('exportableResourceAttributes', { 'booking' => 'Include bookings', 'leaves' => 'Include leaves', 'workinghours' => 'Include working hours' }) pattern(%w( !rollupresource )) pattern(%w( !rolluptask )) pattern(%w( _scenarios !scenarioIdList ), lambda { # Don't include disabled scenarios in the report @val[1].delete_if { |sc| !@project.scenario(sc).get('active') } @property.set('scenarios', @val[1]) }) doc('scenarios.export', <<'EOT' List of scenarios that should be included in the report. By default, all scenarios will be included. This attribute can be used to limit the included scenarios to a defined list. EOT ) pattern(%w( _taskattributes !exportableTaskAttributes ), lambda { @property.set('taskAttributes', @val[1]) }) doc('taskattributes', <<"EOT" Define a list of task attributes that should be included in the report. EOT ) allOrNothingListRule('exportableTaskAttributes', { 'booking' => 'Include bookings', 'complete' => 'Include completion values', 'depends' => 'Include dependencies', 'flags' => 'Include flags', 'maxend' => 'Include maximum end dates', 'maxstart' => 'Include maximum start dates', 'minend' => 'Include minimum end dates', 'minstart' => 'Include minimum start dates', 'note' => 'Include notes', 'priority' => 'Include priorities', 'responsible' => 'Include responsible resource' }) pattern(%w( _taskroot !taskId), lambda { if @val[1].leaf? error('taskroot_leaf', "#{@val[1].fullId} is not a container task", @sourceFileInfo[1]) end @property.set('taskroot', @val[1]) }) level(:experimental) doc('taskroot.export', <<'EOT' Only tasks below the specified root-level tasks are exported. The exported tasks will have the ID of the root-level task stripped from their ID, so that the sub-tasks of the root-level task become top-level tasks in the report file. EOT ) example('TaskRoot') pattern(%w( _timezone !validTimeZone ), lambda { @property.set('timezone', @val[1]) }) doc('timezone.export', "Set the time zone to be used for all dates in the report.") end def rule_exportBody optionsRule('exportAttributes') end def rule_exportFormat pattern(%w( _tjp ), lambda { :tjp }) descr('Export of the scheduled project in TJP syntax.') pattern(%w( _mspxml ), lambda { :mspxml }) descr(<<'EOT' Export of the scheduled project in Microsoft Project XML format. This will export the data of the fully scheduled project. The exported data include the tasks, resources and the assignments of resources to tasks. This is only a small subset of the data that TaskJuggler can manage. This export is intended to share resource assignment data with other teams using Microsoft Project. TaskJuggler manages assignments with a larger accuracy than the Microsoft Project XML format can represent. This will inevitably lead to some rounding errors and different interpretation of the data. The numbers you will see in Microsoft Project are not necessarily an exact match of the numbers you see in TaskJuggler. The XML file format requires the sequence of the tasks in the file to follow the work breakdown structure. Hence all user provided sorting directions will be ignored for this format. EOT ) end def rule_exportFormats pattern(%w( !exportFormat !moreExportFormats ), lambda { [ @val[0] ] + (@val[1].nil? ? [] : @val[1]) }) end def rule_exportHeader pattern(%w( _export !optionalID $STRING ), lambda { newReport(@val[1], @val[2], :export, @sourceFileInfo[0]) do unless @property.modified?('formats') @property.set('formats', [ :tjp ]) end # By default, we export all scenarios. unless @property.modified?('scenarios') scenarios = Array.new(@project.scenarios.items) { |i| i } scenarios.delete_if { |sc| !@project.scenario(sc).get('active') } @property.set('scenarios', scenarios) end # Show all tasks, sorted by seqno-up. unless @property.modified?('hideTask') @property.set('hideTask', LogicalExpression.new(LogicalOperation.new(0))) end unless @property.modified?('sortTasks') @property.set('sortTasks', [ [ 'seqno', true, -1 ] ]) end # Show all resources, sorted by seqno-up. unless @property.modified?('hideResource') @property.set('hideResource', LogicalExpression.new(LogicalOperation.new(0))) end unless @property.modified?('sortResources') @property.set('sortResources', [ [ 'seqno', true, -1 ] ]) end end }) arg(2, 'file name', <<'EOT' The name of the report file to generate. It must end with a .tjp or .tji extension, or use . to use the standard output channel. EOT ) end def rule_extendAttributes optional repeatable pattern(%w( _date !extendId $STRING !extendOptionsBody ), lambda { # Extend the propertySet definition and parser rules if extendPropertySetDefinition(DateAttribute, nil) @ruleToExtendWithScenario.addPattern(TextParser::Pattern.new( [ '_' + @val[1], '!date' ], lambda { @property[@val[0], @scenarioIdx] = @val[1] })) else @ruleToExtend.addPattern(TextParser::Pattern.new( [ '_' + @val[1], '!date' ], lambda { @property.set(@val[0], @val[1]) })) end }) doc('date.extend', <<'EOT' Extend the property with a new attribute of type date. EOT ) arg(2, 'name', 'The name of the new attribute. It is used as header ' + 'in report columns and the like.') pattern(%w( _number !extendId $STRING !extendOptionsBody ), lambda { # Extend the propertySet definition and parser rules if extendPropertySetDefinition(FloatAttribute, nil) @ruleToExtendWithScenario.addPattern(TextParser::Pattern.new( [ '_' + @val[1], '!number' ], lambda { @property[@val[0], @scenarioIdx] = @val[1] })) else @ruleToExtend.addPattern(TextParser::Pattern.new( [ '_' + @val[1], '!number' ], lambda { @property.set(@val[0], @val[1]) })) end }) doc('number.extend', <<'EOT' Extend the property with a new attribute of type number. Possible values for this attribute could be integer or floating point numbers. EOT ) arg(2, 'name', 'The name of the new attribute. It is used as header ' + 'in report columns and the like.') pattern(%w( _reference !extendId $STRING !extendOptionsBody ), lambda { # Extend the propertySet definition and parser rules if extendPropertySetDefinition(ReferenceAttribute, nil) @ruleToExtendWithScenario.addPattern(TextParser::Pattern.new( [ '_' + @val[1], '$STRING', '!referenceBody' ], lambda { @property[@val[0], @scenarioIdx] = [ @val[1], @val[2] ] })) else @ruleToExtend.addPattern(TextParser::Pattern.new( [ '_' + @val[1], '$STRING', '!referenceBody' ], lambda { @property.set(@val[0], [ @val[1], @val[2] ]) })) end }) doc('reference.extend', <<'EOT' Extend the property with a new attribute of type reference. A reference is a URL and an optional text that will be shown instead of the URL if needed. EOT ) arg(2, 'name', 'The name of the new attribute. It is used as header ' + 'in report columns and the like.') pattern(%w( _richtext !extendId $STRING !extendOptionsBody ), lambda { # Extend the propertySet definition and parser rules if extendPropertySetDefinition(RichTextAttribute, nil) @ruleToExtendWithScenario.addPattern(TextParser::Pattern.new( [ '_' + @val[1], '$STRING' ], lambda { @property[@val[0], @scenarioIdx] = newRichText(@val[1], @sourceFileInfo[1]) })) else @ruleToExtend.addPattern(TextParser::Pattern.new( [ '_' + @val[1], '$STRING' ], lambda { @property.set(@val[0], newRichText(@val[1], @sourceFileInfo[1])) })) end }) doc('richtext.extend', <<'EOT' Extend the property with a new attribute of type [[Rich_Text_Attributes|Rich Text]]. EOT ) arg(2, 'name', 'The name of the new attribute. It is used as header ' + 'in report columns and the like.') pattern(%w( _text !extendId $STRING !extendOptionsBody ), lambda { # Extend the propertySet definition and parser rules if extendPropertySetDefinition(StringAttribute, nil) @ruleToExtendWithScenario.addPattern(TextParser::Pattern.new( [ '_' + @val[1], '$STRING' ], lambda { @property[@val[0], @scenarioIdx] = @val[1] })) else @ruleToExtend.addPattern(TextParser::Pattern.new( [ '_' + @val[1], '$STRING' ], lambda { @property.set(@val[0], @val[1]) })) end }) doc('text.extend', <<'EOT' Extend the property with a new attribute of type text. A text is a character sequence enclosed in single or double quotes. EOT ) arg(2, 'name', 'The name of the new attribute. It is used as header ' + 'in report columns and the like.') end def rule_extendBody optionsRule('extendAttributes') end def rule_extendId pattern(%w( $ID ), lambda { unless (?A..?Z) === @val[0][0] error('extend_id_cap', "User defined attributes IDs must start with a capital letter", @sourceFileInfo[0]) end @val[0] }) arg(0, 'id', 'The ID of the new attribute. It can be used like the ' + 'built-in IDs.') end def rule_extendOptions optional repeatable singlePattern('_inherit') doc('inherit.extend', <<'EOT' If this attribute is used, the property extension will be inherited by child properties from their parent property. EOT ) singlePattern('_scenariospecific') doc('scenariospecific.extend', <<'EOT' If this attribute is used, the property extension is scenario specific. A different value can be set for each scenario. EOT ) end def rule_extendOptionsBody optionsRule('extendOptions') end def rule_extendProperty pattern(%w( !extendPropertyId ), lambda { case @val[0] when 'task' @ruleToExtend = @rules[:taskAttributes] @ruleToExtendWithScenario = @rules[:taskScenarioAttributes] @propertySet = @project.tasks when 'resource' @ruleToExtend = @rules[:resourceAttributes] @ruleToExtendWithScenario = @rules[:resourceScenarioAttributes] @propertySet = @project.resources end }) end def rule_extendPropertyId singlePattern('_task') singlePattern('_resource') end def rule_fail pattern(%w( _fail !logicalExpression ), lambda { begin @property.set('fail', @property.get('fail') + [ @val[1] ]) rescue AttributeOverwrite end }) doc('fail', <<'EOT' The fail attribute adds a [[logicalexpression|logical expression]] to the property. The condition described by the logical expression is checked after the scheduling and an error is raised if the condition evaluates to true. This attribute is primarily intended for testing purposes. EOT ) end def rule_flag pattern(%w( $ID ), lambda { unless @project['flags'].include?(@val[0]) error('undecl_flag', "Undeclared flag '#{@val[0]}'", @sourceFileInfo[0]) end @val[0] }) end def rule_flagLogicalExpression pattern(%w( !flagOperation ), lambda { LogicalExpression.new(@val[0], sourceFileInfo) }) doc('logicalflagexpression', <<'EOT' A logical flag expression is a combination of operands and mathematical operations. The final result of a logical expression is always true or false. Logical expressions are used the reduce the properties in a report to a certain subset or to select alternatives for the cell content of a table. When the logical expression is used with attributes like [[hidejournalentry]] and evaluates to true for a certain property, this property is hidden or rolled-up in the report. Operands must be previously declared flags or another logical expression. When you combine logical operations to a more complex expression, the operators are evaluated from left to right. '''a | b & c''' is identical to '''(a | b) & c'''. It's highly recommended that you always use brackets to control the evaluation sequence. Currently, TaskJuggler does not support the concept of operator precedence or right-left associativity. This may change in the future. EOT ) also(%w( functions )) end def rule_flagOperand pattern(%w( _( !flagOperation _) ), lambda { @val[1] }) pattern(%w( _~ !flagOperand ), lambda { operation = LogicalOperation.new(@val[1]) operation.operator = '~' operation }) pattern(%w( $ID ), lambda { unless @project['flags'].include?(@val[0]) error('operand_unkn_flag', "Undeclared flag '#{@val[0]}'", @sourceFileInfo[0]) end LogicalFlag.new(@val[0]) }) end def rule_flagOperation pattern(%w( !flagOperand !flagOperationChain ), lambda { operation = LogicalOperation.new(@val[0]) if @val[1] # Further operators/operands create an operation tree. @val[1].each do |ops| operation = LogicalOperation.new(operation) operation.operator = ops[0] operation.operand2 = ops[1] end end operation }) arg(0, 'operand', <<'EOT' An operand is a declared flag. An operand can be a negated operand by prefixing a ~ character or it can be another logical expression enclosed in braces. EOT ) end def rule_flagOperationChain optional repeatable pattern(%w( !flagOperatorAndOperand), lambda { @val[0] }) end def rule_flagOperatorAndOperand pattern(%w( !flagOperator !flagOperand), lambda{ [ @val[0], @val[1] ] }) arg(1, 'operand', <<'EOT' An operand is a declared flag. An operand can be a negated operand by prefixing a ~ character or it can be another logical expression enclosed in braces. EOT ) end def rule_flagOperator singlePattern('_|') descr('The \'or\' operator') singlePattern('_&') descr('The \'and\' operator') end def rule_flags pattern(%w( _flags !flagList ), lambda { @val[1].each do |flag| next if @property['flags', @scenarioIdx].include?(flag) @property['flags', @scenarioIdx] += [ flag ] end }) end def rule_flagList listRule('moreFlagList', '!flag') end def rule_formats pattern(%w( _formats !outputFormats ), lambda { @property.set('formats', @val[1]) }) doc('formats', <<'EOT' This attribute defines for which output formats the report should be generated. By default, this list is empty. Unless a formats attribute was added to a report definition, no output will be generated for this report. As reports are composable, a report may include other report definitions. A format definition is only needed for the outermost report that includes the others. EOT ) end def rule_functions # This rule is not used by the parser. It's only for the documentation. pattern(%w( !functionsBody )) doc('functions', <<'EOT' The following functions are supported in logical expressions. These functions are evaluated in logical conditions such as hidetask or rollupresource. For the evaluation, implicit and explicit parameters are used. All functions may operate on the current property and the scope property. The scope property is the enclosing property in reports with nested properties. Imagine e. g. a task report with nested resources. When the function is called for a task line, the task is the property and we don't have a scope property. When the function is called for a resource line, the resource is the property and the enclosing task is the scope property. The number of arguments that are passed in brackets to the function depends on the specific function. See the reference for details on each function. All functions can be suffixed with an underscore character. In that case, the function is operating on the scope property as if it were the property. The original property is ignored in that case. In our task report example from above, calling a function with an appended underscore would mean that a task line would be evaluated for the enclosing resource. In the example below you can see how this can be used. To generate a task report that lists all assigned leaf resources for leaf task lines only we use the expression hideresource ~(isleaf() & isleaf_()) The tilde in front of the bracketed expression means not that expression. In other words: show resources that are leaf resources and show them for leaf tasks only. The regular form isleaf() (without the appended underscore) operates on the resource. The isleaf_() variant operates on the enclosing task. EOT ) example('LogicalFunction', '1') end def rule_functionsBody # This rule is not used by the parser. It's only for the documentation. optionsRule('functionPatterns') end def rule_functionPatterns # This rule is not used by the parser. It's only for the documentation. pattern(%w( _hasalert _( $INTEGER _, !date _) )) doc('hasalert', <<'EOT' Will evaluate to true if the current property has a current alert message within the report time frame and with at least the provided alert level. EOT ) arg(2, 'Level', 'The minimum required alert level to be considered.') pattern(%w( _isactive _( $ID _) )) doc('isactive', <<'EOT' Will evaluate to true for tasks and resources if they have bookings in the scenario during the report time frame. EOT ) arg(2, 'ID', 'A scenario ID') pattern(%w( _ischildof _( $ID _) )) doc('ischildof', <<'EOT' Will evaluate to true for tasks and resources if current property is a child of the provided parent property. EOT ) arg(2, 'ID', 'The ID of the parent') pattern(%w( _isdependencyof _( $ID _, $ID _, $INTEGER _) )) doc('isdependencyof', <<'EOT' Will evaluate to true for tasks that depend on the specified task in the specified scenario and are no more than distance tasks away. If distance is 0, all dependencies are considered independent of their distance. EOT ) arg(2, 'Task ID', 'The ID of a defined task') arg(4, 'Scenario ID', 'A scenario ID') arg(6, 'Distance', 'The maximum task distance to be considered') pattern(%w( _isdutyof _( $ID _, $ID _) )) doc('isdutyof', <<'EOT' Will evaluate to true for tasks that have the specified resource assigned to it in the specified scenario. EOT ) arg(2, 'Resource ID', 'The ID of a defined resource') arg(4, 'Scenario ID', 'A scenario ID') pattern(%w( _isfeatureof _( $ID _, $ID _) )) doc('isfeatureof', <<'EOT' If the provided task or any of its sub-tasks depend on this task or any of its sub-tasks, we call this task a feature of the provided task. EOT ) arg(2, 'Task ID', 'The ID of a defined task') arg(4, 'Scenario ID', 'A scenario ID') pattern(['_isleaf', '_(', '_)' ]) doc('isleaf', 'The result is true if the property is not a container.') pattern(%w( _ismilestone _( $ID _) )) doc('ismilestone', <<'EOT' The result is true if the property is a milestone in the provided scenario. EOT ) arg(2, 'Scenario ID', 'A scenario ID') pattern(%w( _isongoing _( $ID _) )) doc('isongoing', <<'EOT' Will evaluate to true for tasks that overlap with the report period in the given scenario. EOT ) arg(2, 'ID', 'A scenario ID') pattern(['_isresource', '_(', '_)' ]) doc('isresource', 'The result is true if the property is a resource.') pattern(%w( _isresponsibilityof _( $ID _, $ID _) )) doc('isresponsibilityof', <<'EOT' Will evaluate to true for tasks that have the specified resource assigned as [[responsible]] in the specified scenario. EOT ) arg(2, 'Resource ID', 'The ID of a defined resource') arg(4, 'Scenario ID', 'A scenario ID') pattern(['_istask', '_(', '_)' ]) doc('istask', 'The result is true if the property is a task.') pattern(%w( _isvalid _( $ID _) )) doc('isvalid', 'Returns false if argument is not an assigned or ' + 'properly computed value.') pattern(%w( _treelevel _( _) )) doc('treelevel', <<'EOT' Returns the nesting level of a property in the property tree. Top level properties have a level of 1, their children 2 and so on. EOT ) end def rule_hAlignment pattern(%w( _center ), lambda { :center }) doc('halign.center', 'Center the cell content') pattern(%w( _left ), lambda { :left }) doc('halign.left', 'Left align the cell content') pattern(%w( _right ), lambda { :right }) doc('halign.right', 'Right align the cell content') end def rule_headline pattern(%w( _headline $STRING ), lambda { @property.set('headline', newRichText(@val[1], @sourceFileInfo[1])) }) doc('headline', <<'EOT' Specifies the headline for a report. EOT ) arg(1, 'text', <<'EOT' The text used for the headline. It is interpreted as [[Rich_Text_Attributes|Rich Text]]. EOT ) end def rule_hideaccount pattern(%w( _hideaccount !logicalExpression ), lambda { @property.set('hideAccount', @val[1]) }) doc('hideaccount', <<'EOT' Do not include accounts that match the specified [[logicalexpression|logical expression]]. If the report is sorted in ''''tree'''' mode (default) then enclosing accounts are listed even if the expression matches the account. EOT ) also(%w( sortaccounts )) end def rule_hidejournalentry pattern(%w( _hidejournalentry !flagLogicalExpression ), lambda { @property.set('hideJournalEntry', @val[1]) }) doc('hidejournalentry', <<'EOT' Do not include journal entries that match the specified logical expression. EOT ) end def rule_hideresource pattern(%w( _hideresource !logicalExpression ), lambda { @property.set('hideResource', @val[1]) }) doc('hideresource', <<'EOT' Do not include resources that match the specified [[logicalexpression|logical expression]]. If the report is sorted in ''''tree'''' mode (default) then enclosing resources are listed even if the expression matches the resource. EOT ) also(%w( sortresources )) end def rule_hidetask pattern(%w( _hidetask !logicalExpression ), lambda { @property.set('hideTask', @val[1]) }) doc('hidetask', <<'EOT' Do not include tasks that match the specified [[logicalexpression|logical expression]]. If the report is sorted in ''''tree'''' mode (default) then enclosing tasks are listed even if the expression matches the task. EOT ) also(%w( sorttasks )) end def rule_iCalReport pattern(%w( !iCalReportHeader !iCalReportBody ), lambda { @property = nil }) doc('icalreport', <<'EOT' Generates an RFC5545 compliant iCalendar file. This file can be used to export task information to calendar applications or other tools that read iCalendar files. EOT ) end def rule_iCalReportBody optionsRule('iCalReportAttributes') end def rule_iCalReportAttributes optional repeatable pattern(%w( !hideresource )) pattern(%w( !hidejournalentry )) pattern(%w( !hidetask )) pattern(%w( !reportEnd )) pattern(%w( !reportPeriod )) pattern(%w( !reportStart )) pattern(%w( !rollupresource )) pattern(%w( !rolluptask )) pattern(%w( _novevents), lambda { @property.set('novevents', [ true ]) }) doc('novevents', <<'EOT' Don't add VEVENT entries to generated [[icalreport]]s. EOT ) pattern(%w( _scenario !scenarioId ), lambda { # Don't include disabled scenarios in the report sc = @val[1] unless @project.scenario(sc).get('active') warning('ical_sc_disabled', "Scenario #{sc} has been disabled") else @property.set('scenarios', [ @val[1] ]) end }) doc('scenario.ical', <<'EOT' ID of the scenario that should be included in the report. By default, the top-level scenario will be included. This attribute can be used to select another scenario. EOT ) end def rule_iCalReportHeader pattern(%w( _icalreport !optionalID $STRING ), lambda { newReport(@val[1], @val[2], :iCal, @sourceFileInfo[0]) do @property.set('formats', [ :iCal ]) # By default, we export only the first scenario. unless @project.scenario(0).get('active') @property.set('scenarios', [ 0 ]) end # Show all tasks, sorted by seqno-up. @property.set('hideTask', LogicalExpression.new(LogicalOperation.new(0))) @property.set('sortTasks', [ [ 'seqno', true, -1 ] ]) # Show all resources, sorted by seqno-up. @property.set('hideResource', LogicalExpression.new(LogicalOperation.new(0))) @property.set('sortResources', [ [ 'seqno', true, -1 ] ]) # Show all journal entries. @property.set('hideJournalEntry', LogicalExpression.new(LogicalOperation.new(0))) # Add VEVENT entries to icalreports by default @property.set('novevents', [ false ]) end }) arg(1, 'file name', <<'EOT' The name of the report file to generate without an extension. Use . to use the standard output channel. EOT ) end def rule_idOrAbsoluteId singlePattern('$ID') singlePattern('$ABSOLUTE_ID') end def rule_includeAttributes optionsRule('includeAttributesBody') end def rule_includeAttributesBody optional repeatable pattern(%w( _accountprefix !accountId ), lambda { @accountprefix = @val[1].fullId }) doc('accountprefix', <<'EOT' This attribute can be used to insert the accounts of the included file as sub-account of the account specified by ID. The parent account must already be defined. EOT ) arg(1, 'account ID', 'The absolute ID of an already defined account') pattern(%w( _reportprefix !reportId ), lambda { @reportprefix = @val[1].fullId }) doc('reportprefix', <<'EOT' This attribute can be used to insert the reports of the included file as sub-report of the report specified by ID. The parent report must already be defined. EOT ) arg(1, 'report ID', 'The absolute ID of an already defined report.') pattern(%w( _resourceprefix !resourceId ), lambda { @resourceprefix = @val[1].fullId }) doc('resourceprefix', <<'EOT' This attribute can be used to insert the resources of the included file as sub-resource of the resource specified by ID. The parent resource must already be defined. EOT ) arg(1, 'resource ID', 'The ID of an already defined resource') pattern(%w( _taskprefix !taskId ), lambda { @taskprefix = @val[1].fullId }) doc('taskprefix', <<'EOT' This attribute can be used to insert the tasks of the included file as sub-task of the task specified by ID. The parent task must already be defined. EOT ) arg(1, 'task ID', 'The absolute ID of an already defined task.') end def rule_includeFile pattern(%w( !includeFileName ), lambda { unless @project error('include_before_project', "You must declare the project header before you include other " + "files.") end @project.inputFiles << @scanner.include(@val[0], @sourceFileInfo[0]) do popFileStack end }) end def rule_includeFileName pattern(%w( $STRING ), lambda { unless @val[0][-4, 4] == '.tji' error('bad_include_suffix', "Included files must have a '.tji'" + "extension: '#{@val[0]}'", @sourceFileInfo[0]) end pushFileStack @val[0] }) arg(0, 'filename', <<'EOT' Name of the file to include. This must have a ''''.tji'''' extension. The name may have an absolute or relative path. You need to use ''''/'''' characters to separate directories. EOT ) end def rule_includeProperties pattern(%w( !includeFileName !includeAttributes ), lambda { @project.inputFiles << @scanner.include(@val[0], @sourceFileInfo[0]) do popFileStack end }) end def rule_intervalOrDate pattern(%w( !date !intervalOptionalEnd ), lambda { if @val[1] mode = @val[1][0] endSpec = @val[1][1] if mode == 0 unless @val[0] < endSpec error('start_before_end', "The end date (#{endSpec}) must be " + "after the start date (#{@val[0]}).", @sourceFileInfo[0]) end TimeInterval.new(@val[0], endSpec) else TimeInterval.new(@val[0], @val[0] + endSpec) end else TimeInterval.new(@val[0], @val[0].sameTimeNextDay) end }) doc('interval3', <<'EOT' There are three ways to specify a date interval. The first is the most obvious. A date interval consists of a start and end DATE. Watch out for end dates without a time specification! Date specifications are 0 extended. An end date without a time is expanded to midnight that day. So the day of the end date is not included in the interval! The start and end dates must be separated by a hyphen character. In the second form, the end date is omitted. A 24 hour interval is assumed. The third form specifies the start date and an interval duration. The duration must be prefixed by a plus character. EOT ) end def rule_interval pattern(%w( !date !intervalEnd ), lambda { mode = @val[1][0] endSpec = @val[1][1] if mode == 0 unless @val[0] < endSpec error('start_before_end', "The end date (#{endSpec}) must be after " + "the start date (#{@val[0]}).", @sourceFileInfo[0]) end TimeInterval.new(@val[0], endSpec) else TimeInterval.new(@val[0], @val[0] + endSpec) end }) doc('interval2', <<'EOT' There are two ways to specify a date interval. The first is the most obvious. A date interval consists of a start and end DATE. Watch out for end dates without a time specification! Date specifications are 0 extended. An end date without a time is expanded to midnight that day. So the day of the end date is not included in the interval! The start and end dates must be separated by a hyphen character. The second form specifies the start date and an interval duration. The duration must be prefixed by a plus character. EOT ) end def rule_intervalDuration pattern(%w( !number !durationUnit ), lambda { convFactors = [ 60, # minutes 60 * 60, # hours 60 * 60 * 24, # days 60 * 60 * 24 * 7, # weeks 60 * 60 * 24 * 30.4167, # months 60 * 60 * 24 * 365 # years ] if @val[0] == 0.0 error('zero_duration', "The interval duration may not be 0.", @sourceFileInfo[1]) end duration = (@val[0] * convFactors[@val[1]]).to_i resolution = @project.nil? ? 60 * 60 : @project['scheduleGranularity'] if @val[1] == 4 # If the duration unit is months, we have to align the duration with # the timing resolution of the project. duration = (duration / resolution).to_i * resolution end # Make sure the interval aligns with the timing resolution. if duration % resolution != 0 error('iv_duration_not_aligned', "The interval duration must be a multiple of the specified " + "timing resolution (#{resolution / 60} min) of the project.") end duration }) arg(0, 'duration', 'The duration of the interval. May not be 0 and must ' + 'be a multiple of [[timingresolution]].') end def rule_intervalEnd pattern([ '_-', '!date' ], lambda { [ 0, @val[1] ] }) pattern(%w( _+ !intervalDuration ), lambda { [ 1, @val[1] ] }) end def rule_intervalOptionalEnd optional pattern([ '_-', '!date' ], lambda { [ 0, @val[1] ] }) pattern(%w( _+ !intervalDuration ), lambda { [ 1, @val[1] ] }) end def rule_intervals listRule('moreIntervals', '!intervalOrDate') end def rule_intervalOptional optional singlePattern('!interval') end def rule_intervalsOptional optional singlePattern('!intervals') end def rule_journalReportAttributes pattern(%w( _journalattributes !journalReportAttributesList ), lambda { @property.set('journalAttributes', @val[1]) }) doc('journalattributes', <<'EOT' A list that determines which of the journal attributes should be included in the journal report. EOT ) allOrNothingListRule('journalReportAttributesList', { 'alert' => 'Include the alert status', 'author' => 'Include the author if known', 'date' => 'Include the date', 'details' => 'Include the details', 'flags' => 'Include the flags', 'headline' => 'Include the headline', 'property' => 'Include the task or resource name', 'propertyid' => 'Include the property ID. ' + 'Requires \'property\'.', 'summary' => 'Include the summary', 'timesheet' => 'Include the timesheet information.' + ' Requires \'property\'.'}) end def rule_journalReportMode pattern(%w( _journal ), lambda { :journal }) descr(<<'EOT' This is the regular journal. It contains all journal entries that are dated in the query interval. If a property is given, only entries of this property are included. Without a property context, all the project entries are included unless hidden by other attributes like [[hidejournalentry]]. EOT ) pattern(%w( _journal_sub ), lambda { :journal_sub }) descr(<<'EOT' This mode only yields entries if used in the context of a task. It contains all journal entries that are dated in the query interval for the task and all its sub tasks. EOT ) pattern(%w( _status_dep ), lambda { :status_dep }) descr(<<'EOT' In this mode only the last entries before the report end date for each property and all its sub-properties and their dependencies are included. If there are multiple entries at the exact same date, then all these entries are included. EOT ) pattern(%w( _status_down ), lambda { :status_down }) descr(<<'EOT' In this mode only the last entries before the report end date for each property and all its sub-properties are included. If there are multiple entries at the exact same date, then all these entries are included. EOT ) pattern(%w( _status_up ), lambda { :status_up }) descr(<<'EOT' In this mode only the last entries before the report end date for each property are included. If there are multiple entries at the exact same date, then all these entries are included. If any of the parent properties has a more recent entry that is still before the report end date, no entries will be included. EOT ) pattern(%w( _alerts_dep ), lambda { :alerts_dep }) descr(<<'EOT' In this mode only the last entries before the report end date for the context property and all its sub-properties and their dependencies are included. If there are multiple entries at the exact same date, then all these entries are included. In contrast to the ''''status_down'''' mode, only entries with an alert level above the default level, and only those with the highest overall alert level are included. EOT ) pattern(%w( _alerts_down ), lambda { :alerts_down }) descr(<<'EOT' In this mode only the last entries before the report end date for the context property and all its sub-properties is included. If there are multiple entries at the exact same date, then all these entries are included. In contrast to the ''''status_down'''' mode, only entries with an alert level above the default level, and only those with the highest overall alert level are included. EOT ) end def rule_journalEntry pattern(%w( !journalEntryHeader !journalEntryBody ), lambda { @val[0] }) doc('journalentry', <<'EOT' This attribute adds an entry to the journal of the project. A journal can be used to record events, decisions or news that happened at a particular moment during the project. Depending on the context, a journal entry may or may not be associated with a specific property or author. A journal entry can consist of up to three parts. The headline is mandatory and should be only 5 to 10 words long. The introduction is optional and should be only one or two sentences long. All other details should be put into the third part. Depending on the context, journal entries are listed with headlines only, as headlines plus introduction or in full. EOT ) end def rule_journalEntryAttributes optional repeatable pattern(%w( _alert !alertLevel ), lambda { @journalEntry.alertLevel = @val[1] }) doc('alert', <<'EOT' Specify the alert level for this entry. This attribute is intended to be used for status reporting. When used for a journal entry that is associated with a property, the value can be reported in the alert column. When multiple entries have been specified for the property, the entry with the date closest to the report end date will be used. Container properties will inherit the highest alert level of all its sub properties unless it has an own journal entry dated closer to the report end than all of its sub properties. EOT ) pattern(%w( !author )) pattern(%w( _flags !flagList ), lambda { @val[1].each do |flag| next if @journalEntry.flags.include?(flag) @journalEntry.flags << flag end }) doc('flags.journalentry', <<'EOT' Journal entries can have flags attached to them. These can be used to include only entries in a report that have a certain flag. EOT ) pattern(%w( !summary )) pattern(%w( !details )) end def rule_journalEntryBody optionsRule('journalEntryAttributes') end def rule_journalEntryHeader pattern(%w( _journalentry !valDate $STRING ), lambda { @journalEntry = JournalEntry.new(@project['journal'], @val[1], @val[2], @property, @sourceFileInfo[0]) }) arg(2, 'headline', <<'EOT' The headline of the journal entry. It will be interpreted as [[Rich_Text_Attributes|Rich Text]]. EOT ) end def rule_journalSortCriteria pattern(%w( !journalSortCriterium !moreJournalSortCriteria ), lambda { [ @val[0] ] + (@val[1].nil? ? [] : @val[1]) }) end def rule_journalSortCriterium pattern(%w( $ABSOLUTE_ID ), lambda { supported = [] JournalEntryList::SortingAttributes.each do |attr| supported << "#{attr}.up" supported << "#{attr}.down" end unless supported.include?(@val[0]) error('bad_journal_sort_criterium', "Unsupported sorting criterium #{@val[0]}. Must be one of " + "#{supported.join(', ')}.") end attr, direction = @val[0].split('.') [ attr.intern, direction == 'up' ? 1 : -1 ] }) end def rule_leafResourceId pattern(%w( !resourceId ), lambda { resource = @val[0] unless resource.leaf? error('leaf_resource_id_expected', "#{resource.id} is not a leaf resource.", @sourceFileInfo[0]) end resource }) arg(0, 'resource', 'The ID of a leaf resource') end def rule_leave pattern(%w( !leaveType !vacationName !intervalOrDate ), lambda { Leave.new(@val[0].intern, @val[2], @val[1]) }) end def rule_leaveList listRule('moreLeaveList', '!leave') end def rule_leaveName optional pattern(%w( $STRING ), lambda { @val[0] }) arg(0, 'name', 'An optional name or reason for the leave') end def rule_leaveAllowance pattern(%w( _annual !valDate !optionalMinus !nonZeroWorkingDuration ), lambda { LeaveAllowance.new(:annual, @val[1], (@val[2] ? -1 : 1) * @val[3]) }) end def rule_leaveAllowanceList listRule('moreLeaveAllowanceList', '!leaveAllowance') end def rule_leaveAllowances pattern(%w( _leaveallowances !leaveAllowanceList ), lambda { appendScListAttribute('leaveallowances', @val[1]) }) doc('leaveallowance', <<'EOT' Add or subtract leave allowances. Currently, only allowances for the annual leaves are supported. Allowances can be negative to deal with expired allowances. The ''''leaveallowancebalance'''' report [[columns|column]] can be used to report the current annual leave balance. Leaves outside of the project period are silently ignored and will not be considered in the leave balance calculation. Therefore, leave allowances are only allowed within the project period. EOT ) level(:beta) example('Leave') end def rule_leaves pattern(%w( _leaves !leaveList ), lambda { LeaveList.new(@val[1]) }) doc('leaves', <<'EOT' Describe a list of leave periods. A leave can be due to a public holiday, personal or sick leave. At global scope, the leaves determine which day is considered a working day. Subsequent resource definitions will inherit the leave list. Leaves can be defined at global level, at resource level and at shift level and intervals may overlap. The leave types have different priorities. A higher priority leave type can overwrite a lower priority type. This means that resource level leaves can overwrite global leaves when they have a higher priority. A sub resource can overwrite a leave of an enclosing resource. Leave periods outside of the project interval are silently ignored. For leave periods that are partially outside of the project period only the part inside the project period will be considered. EOT ) example('Leave') end def rule_leaveType singlePattern('_project') descr('Assignment to another project (lowest priority)') singlePattern('_annual') descr('Personal leave based on annual allowance') singlePattern('_special') descr('Personal leave based on a special occasion') singlePattern('_sick') descr('Sick leave') singlePattern('_unpaid') descr('Unpaid leave') singlePattern('_holiday') descr('Public or bank holiday') singlePattern('_unemployed') descr('Not employeed (highest priority)') end def rule_limitAttributes optionsRule('limitAttributesBody') end def rule_limitAttributesBody optional repeatable pattern(%w( _end !valDate ), lambda { @limitInterval.end = @val[1] }) doc('end.limit', <<'EOT' The end date of the limit interval. It must be within the project time frame. EOT ) pattern(%w( _period !valInterval ), lambda { @limitInterval = ScoreboardInterval.new(@project['start'], @project['scheduleGranularity'], @val[1].start, @val[1].end) }) doc('period.limit', <<'EOT' This property is a shortcut for setting the start and end dates of the limit interval. Both dates must be within the project time frame. EOT ) pattern(%w( _resources !resourceLeafList ), lambda { @limitResources = @val[1] }) doc('resources.limit', <<'EOT' When [[limits]] are used in a [[task]] context, the limits can be restricted to a list of resources that are allocated to the task. In that case each listed resource will not be allocated more than the specified upper limit. Lower limits have no impact on the scheduler but do generate a warning when not met. All specified resources must be leaf resources. EOT ) example('Limits-1', '5') pattern(%w( _start !valDate ), lambda { @limitInterval.start = @val[1] }) doc('start.limit', <<'EOT' The start date of the limit interval. It must be within the project time frame. EOT ) end def rule_limitValue pattern([ '!nonZeroWorkingDuration' ], lambda { @limitInterval = ScoreboardInterval.new(@project['start'], @project['scheduleGranularity'], @project['start'], @project['end']) @limitResources = [] @val[0] }) end def rule_limits pattern(%w( !limitsHeader !limitsBody ), lambda { @val[0] }) end def rule_limitsAttributes optional repeatable pattern(%w( _dailymax !limitValue !limitAttributes), lambda { setLimit(@val[0], @val[1], @limitInterval) }) doc('dailymax', <<'EOT' Set a maximum limit for each calendar day. EOT ) example('Limits-1', '1') pattern(%w( _dailymin !limitValue !limitAttributes), lambda { setLimit(@val[0], @val[1], @limitInterval) }) doc('dailymin', <<'EOT' Minimum required effort for any calendar day. This value cannot be guaranteed by the scheduler. It is only checked after the schedule is complete. In case the minimum required amount has not been reached, a warning will be generated. EOT ) example('Limits-1', '4') pattern(%w( _maximum !limitValue !limitAttributes), lambda { setLimit(@val[0], @val[1], @limitInterval) }) doc('maximum', <<'EOT' Set a maximum limit for the specified period. You must ensure that the overall effort can be achieved! EOT ) pattern(%w( _minimum !limitValue !limitAttributes), lambda { setLimit(@val[0], @val[1], @limitInterval) }) doc('minimum', <<'EOT' Set a minim limit for each calendar month. This will only result in a warning if not met. EOT ) pattern(%w( _monthlymax !limitValue !limitAttributes), lambda { setLimit(@val[0], @val[1], @limitInterval) }) doc('monthlymax', <<'EOT' Set a maximum limit for each calendar month. EOT ) pattern(%w( _monthlymin !limitValue !limitAttributes), lambda { setLimit(@val[0], @val[1], @limitInterval) }) doc('monthlymin', <<'EOT' Minimum required effort for any calendar month. This value cannot be guaranteed by the scheduler. It is only checked after the schedule is complete. In case the minimum required amount has not been reached, a warning will be generated. EOT ) pattern(%w( _weeklymax !limitValue !limitAttributes), lambda { setLimit(@val[0], @val[1], @limitInterval) }) doc('weeklymax', <<'EOT' Set a maximum limit for each calendar week. EOT ) pattern(%w( _weeklymin !limitValue !limitAttributes), lambda { setLimit(@val[0], @val[1], @limitInterval) }) doc('weeklymin', <<'EOT' Minimum required effort for any calendar week. This value cannot be guaranteed by the scheduler. It is only checked after the schedule is complete. In case the minimum required amount has not been reached, a warning will be generated. EOT ) end def rule_limitsBody optionsRule('limitsAttributes') end def rule_limitsHeader pattern(%w( _limits ), lambda { @limits = Limits.new @limits.setProject(@project) @limits }) end def rule_listOfDays pattern(%w( !weekDayInterval !moreListOfDays), lambda { weekDays = Array.new(7, false) ([ @val[0] ] + (@val[1] ? @val[1] : [])).each do |dayList| 7.times { |i| weekDays[i] = true if dayList[i] } end weekDays }) end def rule_listOfTimes pattern(%w( _off ), lambda { [ ] }) pattern(%w( !timeInterval !moreTimeIntervals ), lambda { [ @val[0] ] + (@val[1].nil? ? [] : @val[1]) }) end def rule_listType pattern([ '_bullets' ], lambda { :bullets }) descr('List items as bullet list') pattern([ '_comma' ], lambda { :comma }) descr('List items as comma separated list') pattern([ '_numbered' ], lambda { :numbered }) descr('List items as numbered list') end def rule_loadunit pattern(%w( _loadunit !loadunitName ), lambda { @property.set('loadUnit', @val[1]) }) doc('loadunit', <<'EOT' Determines what unit should be used to display all load values in this report. EOT ) end def rule_loadunitName pattern([ '_days' ], lambda { :days }) descr('Display all load and duration values as days.') pattern([ '_hours' ], lambda { :hours }) descr('Display all load and duration values as hours.') pattern([ '_longauto'] , lambda { :longauto }) descr(<<'EOT' Automatically select the unit that produces the shortest and most readable value. The unit name will not be abbreviated. It will not use quarters since it is not common. EOT ) pattern([ '_minutes' ], lambda { :minutes }) descr('Display all load and duration values as minutes.') pattern([ '_months' ], lambda { :months }) descr('Display all load and duration values as months.') pattern([ '_quarters' ], lambda { :quarters }) descr('Display all load and duration values as quarters.') pattern([ '_shortauto' ], lambda { :shortauto }) descr(<<'EOT' Automatically select the unit that produces the shortest and most readable value. The unit name will be abbreviated. It will not use quarters since it is not common. EOT ) pattern([ '_weeks' ], lambda { :weeks }) descr('Display all load and duration values as weeks.') pattern([ '_years' ], lambda { :years }) descr('Display all load and duration values as years.') end def rule_logicalExpression pattern(%w( !operation ), lambda { LogicalExpression.new(@val[0], sourceFileInfo) }) pattern(%w( _@ !allOrNone ), lambda { LogicalExpression.new(LogicalOperation.new(@val[1]), sourceFileInfo) }) doc('logicalexpression', <<'EOT' A logical expression is a combination of operands and mathematical operations. The final result of a logical expression is always true or false. Logical expressions are used the reduce the properties in a report to a certain subset or to select alternatives for the cell content of a table. When the logical expression is used with attributes like [[hidetask]] or [[hideresource]] and evaluates to true for a certain property, this property is hidden or rolled-up in the report. Operands can be previously declared flags, built-in [[functions]], property attributes (specified as scenario.attribute) or another logical expression. When you combine logical operations to a more complex expression, the operators are evaluated from left to right. ''''a | b & c'''' is identical to ''''(a | b) & c''''. It's highly recommended that you always use brackets to control the evaluation sequence. Currently, TaskJuggler does not support the concept of operator precedence or right-left associativity. This may change in the future. An operand can also be just a number. 0 evaluates to false, all other numbers to true. The logical expression can also be the special constants ''''@all'''' or ''''@none''''. The first always evaluates to true, the latter to false. Date attributes needs special attention. Attributes like [[maxend]] can be undefined. To use such an attribute in a comparison, you need to test for the validity first. E. g. to compare the end date of the ''''plan'''' scenario with the ''''maxend'''' value use ''''isvalid(plan.maxend) & (plan.end > plan.maxend)''''. The ''''&'''' and ''''|'''' operators are lazy. If the result is already known after evaluation of the first operand, the second operand will not be evaluated any more. EOT ) also(%w( functions )) example('LogicalExpression', '1') end def rule_macro pattern(%w( _macro $ID $MACRO ), lambda { if @scanner.macroDefined?(@val[1]) warning('marco_redefinition', "Redefining macro #{@val[1]}") end @scanner.addMacro(TextParser::Macro.new(@val[1], @val[2], @sourceFileInfo[0])) }) doc('macro', <<'EOT' Defines a text fragment that can later be inserted by using the specified ID. To insert the text fragment anywhere in the text you need to write ${ID}.The body is not optional. It must be enclosed in square brackets. Macros can be declared like this: macro FOO [ This text ] If later ''''${FOO}'''' is found in the project file, it is expanded to ''''This text''''. Macros may have arguments. Arguments are accessed with special macros with numbers as names. The number specifies the index of the argument. macro FOO [ This ${1} text ] will expand to ''''This stupid text'''' if called as ''''${FOO "stupid"}''''. Macros may call other macros. All macro arguments must be enclosed by double quotes. In case the argument contains a double quote, it must be escaped by a backslash (''''\''''). User defined macro IDs should start with one uppercase letter as all lowercase letter IDs are reserved for built-in macros. To terminate the macro definition, the '''']'''' must be the last character in the line. If there are any other characters trailing it (even spaces or comments) the '''']'''' will not be considered the end of the macro definition. In macro calls the macro names can be prefixed by a question mark. In this case the macro will expand to nothing if the macro is not defined. Otherwise the undefined macro would be flagged with an error message. The macro call ${?foo} will expand to nothing if foo is undefined. EOT ) example('Macro-1') end def rule_moreBangs optional repeatable singlePattern('_!') end def rule_moreAlternatives commaListRule('!resourceId') end def rule_moreArguments commaListRule('!argument') end def rule_moreChargeSetItems commaListRule('!chargeSetItem') end def rule_moreColumnDef commaListRule('!columnDef') end def rule_moreDepTasks commaListRule('!taskDep') end def rule_moreExportFormats commaListRule('!exportFormat') end def rule_moreJournalSortCriteria commaListRule('!journalSortCriterium') end def rule_moreListOfDays commaListRule('!weekDayInterval') end def rule_moreOutputFormats commaListRule('!outputFormat') end def rule_moreProjectIDs commaListRule('$ID') end def rule_morePredTasks commaListRule('!taskPred') end def rule_moreSortCriteria commaListRule('!sortNonTree') end def rule_moreTimeIntervals commaListRule('!timeInterval') end def rule_navigator pattern(%w( !navigatorHeader !navigatorBody ), lambda { @project['navigators'][@navigator.id] = @navigator }) doc('navigator', <<'EOT' Defines a navigator object with the specified ID. This object can be used in reports to include a navigation bar with references to other reports. EOT ) example('navigator') end def rule_navigatorAttributes optional repeatable pattern(%w( _hidereport !logicalExpression ), lambda { @navigator.hideReport = @val[1] }) doc('hidereport', <<'EOT' This attribute can be used to exclude the reports that match the specified [[logicalexpression|logical expression]] from the navigation bar. EOT ) end def rule_navigatorBody optional pattern(%w( _{ !navigatorAttributes _} )) end def rule_navigatorHeader pattern(%w( _navigator $ID ), lambda { if @project['navigators'][@val[1]] error('navigator_exists', "The navigator #{@val[1]} has already been defined.", @sourceFileInfo[0]) end @navigator = Navigator.new(@val[1], @project) }) end def rule_nikuReportAttributes optional repeatable pattern(%w( !formats )) pattern(%w( !headline )) pattern(%w( !hideresource )) pattern(%w( !hidetask )) pattern(%w( !numberFormat ), lambda { @property.set('numberFormat', @val[0]) }) pattern(%w( !reportEnd )) pattern(%w( !reportPeriod )) pattern(%w( !reportStart )) pattern(%w( !reportTitle )) pattern(%w( _timeoff $STRING $STRING ), lambda { @property.set('timeOffId', @val[1]) @property.set('timeOffName', @val[2]) }) doc('timeoff.nikureport', < 1 error('operand_attribute', 'Attributes must be specified as .', @sourceFileInfo[0]) end scenario, attribute = @val[0].split('.') if (scenarioIdx = @project.scenarioIdx(scenario)).nil? error('operand_unkn_scen', "Unknown scenario ID #{scenario}", @sourceFileInfo[0]) end # TODO: Do at least some basic sanity checks of the attribute is valid. LogicalAttribute.new(attribute, @project.scenario(scenarioIdx)) }) pattern(%w( !date ), lambda { LogicalOperation.new(@val[0]) }) pattern(%w( $ID !argumentList ), lambda { if @val[1].nil? unless @project['flags'].include?(@val[0]) error('operand_unkn_flag', "Undeclared flag '#{@val[0]}'", @sourceFileInfo[0]) end LogicalFlag.new(@val[0]) else func = LogicalFunction.new(@val[0]) res = func.setArgumentsAndCheck(@val[1]) unless res.nil? error(*res) end func end }) pattern(%w( $INTEGER ), lambda { LogicalOperation.new(@val[0]) }) pattern(%w( $FLOAT ), lambda { LogicalOperation.new(@val[0]) }) pattern(%w( $STRING ), lambda { LogicalOperation.new(@val[0]) }) end def rule_operation pattern(%w( !operand !operationChain ), lambda { operation = LogicalOperation.new(@val[0]) if @val[1] # Further operators/operands create an operation tree. @val[1].each do |ops| operation = LogicalOperation.new(operation) operation.operator = ops[0] operation.operand2 = ops[1] end end operation }) arg(0, 'operand', <<'EOT' An operand can consist of a date, a text string, a [[functions|function]], a property attribute or a numerical value. It can also be the name of a declared flag. Use the ''''scenario_id.attribute'''' notation to use an attribute of the currently evaluated property. The scenario ID always has to be specified, also for non-scenario specific attributes. This is necessary to distinguish them from flags. See [[columnid]] for a list of available attributes. The use of list attributes is not recommended. User defined attributes are available as well. An operand can be a negated operand by prefixing a ~ character or it can be another logical expression enclosed in braces. EOT ) end def rule_operationChain optional repeatable pattern(%w( !operatorAndOperand), lambda { @val[0] }) end def rule_operatorAndOperand pattern(%w( !operator !operand), lambda{ [ @val[0], @val[1] ] }) arg(1, 'operand', <<'EOT' An operand can consist of a date, a text string or a numerical value. It can also be the name of a declared flag. Finally, an operand can be a negated operand by prefixing a ~ character or it can be another operation enclosed in braces. EOT ) end def rule_operator singlePattern('_|') descr('The \'or\' operator') singlePattern('_&') descr('The \'and\' operator') singlePattern('_>') descr('The \'greater than\' operator') singlePattern('_<') descr('The \'smaller than\' operator') singlePattern('_=') descr('The \'equal\' operator') singlePattern('_>=') descr('The \'greater-or-equal\' operator') singlePattern('_<=') descr('The \'smaller-or-equal\' operator') singlePattern('_!=') descr('The \'not-equal\' operator') end def rule_optionalID optional pattern(%w( $ID ), lambda { @val[0] }) arg(0, 'id', <<"EOT" An optional ID. If you ever want to reference this property, you must specify your own unique ID. If no ID is specified, one will be automatically generated. These IDs may become visible in reports, but may change at any time. You may never rely on automatically generated IDs. EOT ) end def rule_optionalMinus optional pattern(%w( _- ), lambda { true }) end def rule_optionalPercent optional pattern(%w( !number _% ), lambda { @val[0] / 100.0 }) end def rule_optionalScenarioIdCol optional pattern(%w( $ID_WITH_COLON ), lambda { if (@scenarioIdx = @project.scenarioIdx(@val[0])).nil? error('unknown_scenario_id', "Unknown scenario: #{@val[0]}", @sourceFileInfo[0]) end @scenarioIdx }) end def rule_optionalVersion optional pattern(%w( $STRING ), lambda { @val[0] }) arg(0, 'version', <<"EOT" An optional version ID. This can be something simple as "4.2" or an ID tag of a revision control system. If not specified, it defaults to "1.0". EOT ) end def rule_outputFormat pattern(%w( _csv ), lambda { :csv }) descr(<<'EOT' The report lists the resources and their respective values as colon-separated-value (CSV) format. Due to the very simple nature of the CSV format, only a small subset of features will be supported for CSV output. Including tasks or listing multiple scenarios will result in very difficult to read reports. EOT ) pattern(%w( _html ), lambda { :html }) descr('Generate a web page (HTML file)') pattern(%w( _niku ), lambda { :niku }) descr('Generate an XOG XML file to be used with Clarity.') end def rule_outputFormats pattern(%w( !outputFormat !moreOutputFormats ), lambda { [ @val[0] ] + (@val[1].nil? ? [] : @val[1]) }) end def rule_plusOrMinus singlePattern('_+') singlePattern('_-') end def rule_project pattern(%w( !projectProlog !projectDeclaration !properties . ), lambda { @val[1] }) end def rule_projectBody optionsRule('projectBodyAttributes') end def rule_projectBodyAttributes repeatable optional pattern(%w( _alertlevels !alertLevelDefinitions ), lambda { if @val[1].length < 2 error('too_few_alert_levels', 'You must specify at least 2 different alert levels.', @sourceFileInfo[1]) end levels = @project['alertLevels'] levels.clear @val[1].each do |level| if levels.indexById(level[0]) error('alert_level_redef', "Alert level '#{level[0]}' has been defined multiple times.", @sourceFileInfo[1]) end if levels.indexByName(level[1]) error('alert_name_redef', "Alert level name '#{level[1]}' has been defined multiple " + "times.", @sourceFileInfo[1]) end @project['alertLevels'].add(AlertLevelDefinition.new(*level)) end }) level(:beta) doc('alertlevels', <<'EOT' By default TaskJuggler supports the pre-defined alert levels: green, yellow and red. This attribute can be used to replace them with your own set of alert levels. You can define any number of levels, but you need to define at least two and they must be specified in ascending order from the least severity to highest severity. Additionally, you need to provide a 15x15 pixel image file with the name ''''flag-X.png'''' for each level where ''''X'''' matches the ID of the alert level. These files need to be in the ''''icons'''' directory to be found by the browser when showing HTML reports. EOT ) example('AlertLevels') pattern(%w( !currencyFormat ), lambda { @project['currencyFormat'] = @val[0] }) pattern(%w( _currency $STRING ), lambda { @project['currency'] = @val[1] }) doc('currency', 'The default currency unit.') example('Account') arg(1, 'symbol', 'Currency symbol') pattern(%w( _dailyworkinghours !number ), lambda { @project['dailyworkinghours'] = @val[1] }) doc('dailyworkinghours', <<'EOT' Set the average number of working hours per day. This is used as the base to convert working hours into working days. This affects for example the length task attribute. The default value is 8 hours and should work for most Western countries. The value you specify should match the settings you specified as your default [[workinghours.project|working hours]]. EOT ) example('Project') arg(1, 'hours', 'Average number of working hours per working day') pattern(%w( _extend !extendProperty !extendBody ), lambda { updateParserTables }) doc('extend', <<'EOT' Often it is desirable to collect more information in the project file than is necessary for task scheduling and resource allocation. To add such information to tasks, resources or accounts, the user can extend these properties with user-defined attributes. The new attributes can be of various types such as text, date or reference to capture various types of data. Optionally the user can specify if the attribute value should be inherited from the enclosing property. EOT ) example('CustomAttributes') pattern(%w( !projectBodyInclude )) pattern(%w( !journalEntry )) pattern(%w( _now !date ), lambda { @project['now'] = @val[1] @scanner.addMacro(TextParser::Macro.new('now', @val[1].to_s, @sourceFileInfo[0])) @scanner.addMacro(TextParser::Macro.new( 'today', @val[1].to_s(@project['timeFormat']), @sourceFileInfo[0])) }) doc('now', <<'EOT' Specify the date that TaskJuggler uses for calculation as current date. If no value is specified, the current value of the system clock is used. EOT ) arg(1, 'date', 'Alternative date to be used as current date for all ' + 'computations') pattern(%w( _markdate !date ), lambda { @project['markdate'] = @val[1] @scanner.addMacro(TextParser::Macro.new('markdate', @val[1].to_s, @sourceFileInfo[0])) @scanner.addMacro(TextParser::Macro.new( 'today', @val[1].to_s(@project['timeFormat']), @sourceFileInfo[0])) }) doc('markdate', <<'EOT' Specify the reference date that TaskJuggler uses as date that can be specified and set by the user. It can be used as additional point in time to help with tracking tasks. If no value is specified, the current value of the system clock is used. EOT ) arg(1, 'date', 'Alternative date to be used as custom date specified by the user') pattern(%w( !numberFormat ), lambda { @project['numberFormat'] = @val[0] }) pattern(%w( _outputdir $STRING ), lambda { # Directory name must be terminated by a slash. if @val[1].empty? error('outdir_empty', 'Output directory may not be empty.') end if !File.directory?(@val[1]) error('outdir_missing', "Output directory '#{@val[1]}' does not exist or is not " + "a directory!") end @project.outputDir = @val[1] + (@val[1][-1] == ?/ ? '' : '/') }) doc('outputdir', 'Specifies the directory into which the reports should be generated. ' + 'This will not affect reports whose name start with a slash. This ' + 'setting can be overwritten by the command line option -o or --output-dir.') arg(1, 'directory', 'Path to an existing directory') pattern(%w( !scenario )) pattern(%w( _shorttimeformat $STRING ), lambda { @project['shortTimeFormat'] = @val[1] }) doc('shorttimeformat', 'Specifies time format for short time specifications. This is normal ' + 'just hours and minutes.') arg(1, 'format', 'strftime like format string') pattern(%w( !timeformat ), lambda { @project['timeFormat'] = @val[0] }) pattern(%w( !timezone ), lambda { @val[0] }) pattern(%w( _timingresolution $INTEGER _min ), lambda { goodValues = [ 5, 10, 15, 20, 30, 60 ] unless goodValues.include?(@val[1]) error('bad_timing_res', "Timing resolution must be one of #{goodValues.join(', ')} min.", @sourceFileInfo[1]) end if @val[1] > (Project.maxScheduleGranularity / 60) error('too_large_timing_res', 'The maximum allowed timing resolution for the timezone is ' + "#{Project.maxScheduleGranularity / 60} minutes.", @sourceFileInfo[1]) end @project['scheduleGranularity'] = @val[1] * 60 }) doc('timingresolution', <<'EOT' Sets the minimum timing resolution. The smaller the value, the longer the scheduling process lasts and the more memory the application needs. The default and maximum value is 1 hour. The smallest value is 5 min. This value is a pretty fundamental setting of TaskJuggler. It has a severe impact on memory usage and scheduling performance. You should set this value to the minimum required resolution. Make sure that all values that you specify are aligned with the resolution. Changing the timing resolution will reset the [[workinghours.project|working hours]] to the default times. It's recommended that this is the very first option in the project header section. Do not use this option after you've set the time zone! EOT ) pattern(%w( _trackingscenario !scenarioId ), lambda { @project['trackingScenarioIdx'] = @val[1] # The tracking scenario and all child scenarios will always be scheduled # in projection mode. @project.scenario(@val[1]).all.each do |scenario| scenario.set('projection', true) end }) doc('trackingscenario', <<'EOT' Specifies which scenario is used to capture what actually has happened with the project. All sub-scenarios of this scenario inherit the bookings of the tracking scenario and may not have any bookings of their own. The tracking scenario must also be specified to use time and status sheet reports. The tracking scenario must be defined after all scenarios have been defined. The tracking scenario and all scenarios derived from it will be scheduled in projection mode. This means that the scheduler will only add bookings after the current date or the date specified by [[now]]. It is assumed that all allocations prior to this date have been provided as [[booking.task| task bookings]] or [[booking.resource|resource bookings]]. EOT ) example('TimeSheet1', '2') pattern(%w( _weekstartsmonday ), lambda { @project['weekStartsMonday'] = true }) doc('weekstartsmonday', 'Specify that you want to base all week calculation on weeks ' + 'starting on Monday. This is common in many European countries.') pattern(%w( _weekstartssunday ), lambda { @project['weekStartsMonday'] = false }) doc('weekstartssunday', 'Specify that you want to base all week calculation on weeks ' + 'starting on Sunday. This is common in the United States of America.') pattern(%w( !workinghoursProject )) pattern(%w( _yearlyworkingdays !number ), lambda { @project['yearlyworkingdays'] = @val[1] }) doc('yearlyworkingdays', <<'EOT' Specifies the number of average working days per year. This should correlate to the specified workinghours and vacation. It affects the conversion of working hours, working days, working weeks, working months and working years into each other. When public holidays and leaves are disregarded, this value should be equal to the number of working days per week times 52.1428 (the average number of weeks per year). E. g. for a culture with 5 working days it is 260.714 (the default), for 6 working days it is 312.8568 and for 7 working days it is 365. EOT ) arg(1, 'days', 'Number of average working days for a year') end def rule_projectDeclaration pattern(%w( !projectHeader !projectBody ), lambda { # If the user has specified a tracking scenario, we mark all children of # that scenario to disallow own bookings. These scenarios will inherit # their bookings from the tracking scenario. if (idx = @project['trackingScenarioIdx']) @project.scenario(idx).allLeaves(true).each do |scenario| scenario.set('ownbookings', false) end end @val[0] }) doc('project', <<'EOT' The project property is mandatory and should be the first property in a project file. It is used to capture basic attributes such as the project id, name and the expected time frame. Be aware that the dates for the project period default to UTC times. See [[interval2]] for details. EOT ) end def rule_projectHeader pattern(%w( _project !optionalID $STRING !optionalVersion !interval ), lambda { @project = Project.new(@val[1], @val[2], @val[3]) @project['start'] = @val[4].start @project['end'] = @val[4].end @projectId = @val[1] setGlobalMacros @property = nil @reportCounter = 0 @project }) arg(2, 'name', 'The name of the project') end def rule_projectIDs pattern(%w( $ID !moreProjectIDs ), lambda { [ @val[0] ] + (@val[1].nil? ? [] : @val[1]) }) end def rule_projection optionsRule('projectionAttributes') end def rule_projectionAttributes optional repeatable pattern(%w( _sloppy )) level(:deprecated) also('trackingscenario') doc('sloppy.projection', '') pattern(%w( _strict ), lambda { warning('projection_strict', 'The strict mode is now always used.') }) level(:deprecated) also('trackingscenario') doc('strict.projection', '') end def rule_projectProlog optional repeatable pattern(%w( !prologInclude )) pattern(%w( !macro )) end def rule_projectProperties # This rule is not defining actual syntax. It's only used for the # documentation. pattern(%w( !projectPropertiesBody )) doc('properties', <<'EOT' The project properties. Every project must consist of at least one task. The other properties are optional. To save the scheduled data at least one output generating property should be used. EOT ) end def rule_projectPropertiesBody # This rule is not defining actual syntax. It's only used for the # documentation. optionsRule('properties') end def rule_projectBodyInclude pattern(%w( _include !includeFile !projectBodyAttributes . )) lastSyntaxToken(1) doc('include.project', <<'EOT' Includes the specified file name as if its contents would be written instead of the include property. When the included files contains other include statements or report definitions, the filenames are relative to the file where they are defined in. This version of the include directive may only be used inside the [[project]] header section. The included files must only contain content that may be present in a project header section. EOT ) end def rule_prologInclude pattern(%w( _include !includeFile !projectProlog . )) lastSyntaxToken(1) doc('include.macro', <<'EOT' Includes the specified file name as if its contents would be written instead of the include property. The only exception is the include statement itself. When the included files contains other include statements or report definitions, the filenames are relative to the file where they are defined in. The included file may only contain macro definitions. This version of the include directive can only be used before the [[project]] header. EOT ) end def rule_properties pattern(%w( !propertiesBody )) end def rule_propertiesBody repeatable optional pattern(%w( !account )) pattern(%w( _auxdir $STRING ), lambda { auxdir = @val[1] # Ensure that the directory always ends with a '/'. auxdir += '/' unless auxdir[-1] == ?/ @project['auxdir'] = auxdir }) level(:beta) doc('auxdir', <<'EOT' Specifies an alternative directory for the auxiliary report files such as CSS, JavaScript and icon files. This setting will affect all subsequent report definitions unless it gets overridden. If this attribute is not set, the directory and its contents will be generated automatically. If this attribute is provided, the user has to ensure that the directory exists and is filled with the proper data. The specified path can be absolute or relative to the generated report file. EOT ) pattern(%w( _copyright $STRING ), lambda { @project['copyright'] = @val[1] }) doc('copyright', <<'EOT' Set a copyright notice for the project file and its content. This copyright notice will be added to all reports that can support it. EOT ) example('Caption', '2') pattern(%w( !balance ), lambda { @project['costaccount'] = @val[0][0] @project['revenueaccount'] = @val[0][1] }) pattern(%w( _flags !declareFlagList ), lambda { unless @project['flags'].include?(@val[1]) @project['flags'] += @val[1] end }) doc('flags', <<'EOT' Declare one or more flag for later use. Flags can be used to mark tasks, resources or other properties to filter them in reports. EOT ) pattern(%w( !propertiesInclude )) pattern(%w( !leaves ), lambda { @val[0].each do |v| @project['leaves'] << v end }) pattern(%w( !limits ), lambda { @project['limits'] = @val[0] }) doc('limits', <<'EOT' Set per-interval allocation limits for the following resource definitions. The limits can be overwritten in each resource definition and the global limits can be changed later. EOT ) pattern(%w( !macro )) pattern(%w( !navigator )) pattern(%w( _projectid $ID ), lambda { @project['projectids'] << @val[1] @project['projectids'].uniq! @project['projectid'] = @projectId = @val[1] }) doc('projectid', <<'EOT' This declares a new project id and activates it. All subsequent task definitions will inherit this ID. The tasks of a project can have different IDs. This is particularly helpful if the project is merged from several sub projects that each have their own ID. EOT ) pattern(%w( _projectids !projectIDs ), lambda { @project['projectids'] += @val[1] @project['projectids'].uniq! }) doc('projectids', <<'EOT' Declares a list of project IDs. When an include file that was generated from another project brings different project IDs, these need to be declared first. EOT ) pattern(%w( _rate !number ), lambda { @project['rate'] = @val[1].to_f }) doc('rate', <<'EOT' Set the default rate for all subsequently defined resources. The rate describes the daily cost of a resource. EOT ) pattern(%w( !reportProperties )) pattern(%w( !resource )) pattern(%w( !shift )) pattern(%w( !statusSheet )) pattern(%w( _supplement !supplement )) doc('supplement', <<'EOT' The supplement keyword provides a mechanism to add more attributes to already defined accounts, tasks or resources. The additional attributes must obey the same rules as in regular task or resource definitions and must be enclosed by curly braces. This construct is primarily meant for situations where the information about a task or resource is split over several files. E. g. the vacation dates for the resources may be in a separate file that was generated by some other tool. EOT ) example('Supplement') pattern(%w( !task )) pattern(%w( !timeSheet )) pattern(%w( _vacation !vacationName !intervals ), lambda { @val[2].each do |interval| @project['leaves'] << Leave.new(:holiday, interval) end }) doc('vacation', <<'EOT' Specify a global vacation period for all subsequently defined resources. A vacation can also be used to block out the time before a resource joined or after it left. For employees changing their work schedule from full-time to part-time, or vice versa, please refer to the 'Shift' property. EOT ) arg(1, 'name', 'Name or purpose of the vacation') end def rule_propertiesFile pattern(%w( !propertiesBody . )) end def rule_propertiesInclude pattern(%w( _include !includeProperties !properties . ), lambda { }) lastSyntaxToken(1) doc('include.properties', <<'EOT' Includes the specified file name as if its contents would be written instead of the include property. The only exception is the include statement itself. When the included files contains other include statements or report definitions, the filenames are relative to the file where they are defined in. include commands can be used in the project header, at global scope or between property declarations of tasks, resources, and accounts. For technical reasons you have to supply the optional pair of curly brackets if the include is followed immediately by a macro call that is defined within the included file. EOT ) end def rule_purge pattern(%w( _purge !optionalScenarioIdCol $ID ), lambda { attrId = @val[2] if (attributeDefinition = @property.attributeDefinition(attrId)).nil? error('purge_unknown_id', "#{attrId} is not a known attribute for this property", @sourceFileInfo[2]) end if attributeDefinition.scenarioSpecific @scenarioIdx = 0 unless @val[1] else if @val[1] error('purge_non_sc_spec_attr', 'Scenario specified for a non-scenario specific attribute') end end if @property.attributeDefinition(attrId).scenarioSpecific @property.getAttribute(attrId, @scenarioIdx).reset else @property.getAttribute(attrId).reset end }) doc('purge', <<'EOT' Many attributes inherit their values from the enclosing property or the global scope. In certain circumstances, this is not desirable, e. g. for list attributes. A list attribute is any attribute that takes a comma separated list of values as argument. [[allocate]] and [[flags.task]] are good examples of commonly used list attributes. By defining values for such a list attribute in a nested property, the new values will be appended to the list that was inherited from the enclosing property. The purge attribute resets any attribute to its default value. A subsequent definition for the attribute within the property will then add their values to an empty list. The value of the enclosing property is not affected by purge. For scenario specific attributes, an optional scenario ID can be specified before the attribute ID. If it's missing, the default (first) scenario will be used. EOT ) arg(1, 'attribute', 'Any name of a list attribute') end def rule_referenceAttributes optional repeatable pattern(%w( _label $STRING ), lambda { @val[1] }) end def rule_referenceBody optionsRule('referenceAttributes') end def rule_relativeId pattern(%w( _! !moreBangs !idOrAbsoluteId ), lambda { str = '!' if @val[1] @val[1].each { |bang| str += bang } end str += @val[2] str }) end def rule_reports pattern(%w( !accountReport )) pattern(%w( !export )) pattern(%w( !resourceReport )) pattern(%w( !taskReport )) pattern(%w( !textReport )) pattern(%w( !traceReport )) end def rule_reportableAttributes singlePattern('_activetasks') descr(<<'EOT' The number of sub-tasks (including the current task) that are active in the reported time period. Active means that they are ongoing at the current time or [[now]] date. EOT ) singlePattern('_annualleave') descr(<<'EOT' The number of annual leave units within the reported time period. The unit can be adjusted with [[loadunit]]. EOT ) singlePattern('_annualleavebalance') descr(<<'EOT' The balance of the annual leave at the end of the reporting interval. The unit can be adjusted with [[loadunit]]. EOT ) singlePattern('_annualleavelist') descr(<<'EOT' A list with all annual leave intervals. The list can be customized with the [[listtype.column|listtype]] attribute. EOT ) singlePattern('_alert') descr(<<'EOT' The alert level of the property that was reported with the date closest to the end date of the report. Container properties that don't have their own alert level reported with a date equal or newer than the alert levels of all their sub properties will get the highest alert level of their direct sub properties. EOT ) singlePattern('_alertmessages') level(:deprecated) also('journal') descr('Deprecated. Please use ''''journal'''' instead') singlePattern('_alertsummaries') level(:deprecated) also('journal') descr('Deprecated. Please use ''''journal'''' instead') singlePattern('_alerttrend') descr(<<'EOT' Shows how the alert level at the end of the report period compares to the alert level at the beginning of the report period. Possible values are ''''Up'''', ''''Down'''' or ''''Flat''''. EOT ) singlePattern('_balance') descr(<<'EOT' The account balance at the beginning of the reported period. This is the balance before any transactions of the reported period have been credited. EOT ) singlePattern('_bsi') descr('The hierarchical or work breakdown structure index (i. e. 1.2.3)') singlePattern('_chart') descr(<<'EOT' A Gantt chart. This column type requires all lines to have the same fixed height. This does not work well with rich text columns in some browsers. Some show a scrollbar for the compressed table cells, others don't. It is recommended, that you don't use rich text columns in conjuction with the chart column. EOT ) singlePattern('_children') descr(<<'EOT' A list of all direct sub elements. The list can be customized by the [[listitem.column|listitem]] and [[listtype.column|listtype]] attributes. EOT ) singlePattern('_closedtasks') descr(<<'EOT' The number of sub-tasks (including the current task) that have been closed during the reported time period. Closed means that they have an end date before the current time or [[now]] date. EOT ) singlePattern('_competitorcount') descr(<<'EOT' The number of tasks that have successfully competed for the same resources and have potentially delayed the completion of this task. EOT ) singlePattern('_competitors') descr(<<'EOT' A list of tasks that have successfully competed for the same resources and have potentially delayed the completion of this task. EOT ) singlePattern('_complete') descr(<<'EOT' The completion degree of a task. Unless a completion degree is manually provided, this is a computed value relative to the [[now]] date of the project. A task that has ended before the now date is always 100% complete. A task that starts at or after the now date is always 0%. For [[effort]] based tasks the computation degree is the percentage of done effort of the overall effort. For other leaf tasks, the completion degree is the percentage of the already passed duration of the overall task duration. For container tasks, it's always the average of the direct sub tasks. If the sub tasks consist of a mixture of effort and non-effort tasks, the completion value is only of limited value. EOT ) pattern([ '_completed' ], lambda { 'complete' }) level(:deprecated) also('complete') descr('Deprecated alias for complete') singlePattern('_criticalness') descr('A measure for how much effort the resource is allocated for, or ' + 'how strained the allocated resources of a task are.') singlePattern('_cost') descr(<<'EOT' The cost of the task or resource. The use of this column requires that a cost account has been set for the report using the [[balance]] attribute. EOT ) singlePattern('_daily') descr('A group of columns with one column for each day') singlePattern('_directreports') descr(<<'EOT' The resources that have this resource assigned as manager. The list can be customized by the [[listitem.column|listitem]] and [[listtype.column|listtype]] attribute. EOT ) singlePattern('_duration') descr('The duration of a task') singlePattern('_duties') descr(<<'EOT' List of tasks that the resource is allocated to The list can be customized by the [[listitem.column|listitem]] and [[listtype.column|listtype]] attribute. EOT ) singlePattern('_efficiency') descr('Measure for how efficient a resource can perform tasks') singlePattern('_effort') descr('The allocated effort during the reporting period') singlePattern('_effortdone') descr('The already completed effort as of now') singlePattern('_effortleft') descr('The remaining allocated effort as of now') singlePattern('_email') descr('The email address of a resource') singlePattern('_end') descr('The end date of a task') singlePattern('_flags') descr('List of attached flags') singlePattern('_followers') descr(<<'EOT' A list of tasks that depend on the current task. The list contains the names, the IDs, the date and the type of dependency. For the type the following symbols are used for : * ''']->[''': End-to-Start dependency * '''[->[''': Start-to-Start dependency * ''']->]''': End-to-End dependency * '''[->]''': Start-to-End dependency The list can be customized by the [[listitem.column|listitem]] and [[listtype.column]] attributes. The dependency symbol can be generated via the ''''dependency'''' attribute in the query, the target date via the ''''date'''' attribute. EOT ) singlePattern('_freetime') descr(<<'EOT' The amount of unallocated work time of a resource during the reporting period. EOT ) singlePattern('_freework') descr(<<'EOT' The amount of unallocated work capacity of a resource during the reporting period. This is the product of unallocated work time times the efficiency of the resource. EOT ) singlePattern('_fte') descr(<<'EOT' The Full-Time-Equivalent of a resource or group. This is the ratio of the resource working time and the global working time. Working time is defined by working hours and leaves. The FTE value can vary over time and is calculated for the report interval or the user specified interval. EOT ) singlePattern('_gauge') descr(<<'EOT' When [[complete]] values have been provided to capture the actual progress on tasks, the gauge column will list whether the task is ahead of, behind or on schedule. EOT ) singlePattern('_headcount') descr(<<'EOT' For resources this is the headcount number of the resource or resource group. For a single resource this is the [[efficiency]] rounded to the next integer. Resources that are marked as unemployed at the report start time are not counted. For a group it is the sum of the sub resources headcount. For tasks it's the number of different resources allocated to the task during the report interval. Resources are weighted with their rounded efficiencies. EOT ) pattern([ '_hierarchindex' ], lambda { 'bsi' }) level(:deprecated) also('bsi') descr('Deprecated alias for bsi') singlePattern('_hourly') descr('A group of columns with one column for each hour') singlePattern('_id') descr('The id of the item') singlePattern('_index') descr('The index of the item based on the nesting hierachy') singlePattern('_inputs') descr(<<'EOT' A list of milestones that are a prerequiste for the current task. For container tasks it will also include the inputs of the child tasks. Inputs may not have any predecessors. The list can be customized by the [[listitem.column|listitem]] and [[listtype.column|listtype]] attribute. EOT ) singlePattern('_journal') descr(<<'EOT' The journal entries for the task or resource for the reported interval. The generated text can be customized with [[journalmode]], [[journalattributes]], [[hidejournalentry]] and [[sortjournalentries]]. If used in queries without a property context, the journal for the complete project is generated. EOT ) singlePattern('_journal_sub') level(:deprecated) also('journal') descr('Deprecated. Please use ''''journal'''' instead') singlePattern('_journalmessages') level(:deprecated) also('journal') descr('Deprecated. Please use ''''journal'''' instead') singlePattern('_journalsummaries') level(:deprecated) also('journal') descr('Deprecated. Please use ''''journal'''' instead') singlePattern('_line') descr('The line number in the report') singlePattern('_managers') descr(<<'EOT' A list of managers that the resource reports to. The list can be customized by the [[listitem.column|listitem]] and [[listtype.column|listtype]] attributes. EOT ) singlePattern('_maxend') descr('The latest allowed end of a task') singlePattern('_maxstart') descr('The lastest allowed start of a task') singlePattern('_minend') descr('The earliest allowed end of a task') singlePattern('_minstart') descr('The earliest allowed start of a task') singlePattern('_monthly') descr('A group of columns with one column for each month') singlePattern('_no') descr('The object line number in the report (Cannot be used for sorting!)') singlePattern('_name') descr('The name or description of the item') singlePattern('_note') descr('The note attached to a task') singlePattern('_opentasks') descr(<<'EOT' The number of sub-tasks (including the current task) that have not yet been closed during the reported time period. Closed means that they have an end date before the current time or [[now]] date. EOT ) singlePattern('_pathcriticalness') descr('The criticalness of the task with respect to all the paths that ' + 'it is a part of.') singlePattern('_precursors') descr(<<'EOT' A list of tasks the current task depends on. The list contains the names, the IDs, the date and the type of dependency. For the type the following symbols are used * ''']->[''': End-to-Start dependency * '''[->[''': Start-to-Start dependency * ''']->]''': End-to-End dependency * '''[->]''': Start-to-End dependency The list can be customized by the [[listitem.column|listitem]] and [[listtype.column|listtype]] attributes. The dependency symbol can be generated via the ''''dependency'''' attribute in the query, the target date via the ''''date'''' attribute. EOT ) singlePattern('_priority') descr('The priority of a task') singlePattern('_quarterly') descr('A group of columns with one column for each quarter') singlePattern('_rate') descr('The daily cost of a resource.') singlePattern('_reports') descr(<<'EOT' All resources that have this resource assigned as a direct or indirect manager. The list can be customized by the [[listitem.column|listitem]] and [[listtype.column|listtype]] attributes. EOT ) singlePattern('_resources') descr(<<'EOT' A list of resources that are assigned to the task in the report time frame. The list can be customized by the [[listitem.column|listitem]] and [[listtype.column|listtype]] attributes. EOT ) singlePattern('_responsible') descr(<<'EOT' The responsible people for this task. The list can be customized by the [[listitem.column|listitem]] and [[listtype.column|listtype]] attributes. EOT ) singlePattern('_revenue') descr(<<'EOT' The revenue of the task or resource. The use of this column requires that a revenue account has been set for the report using the [[balance]] attribute. EOT ) singlePattern('_scenario') descr('The name of the scenario') singlePattern('_scheduling') descr(<<'EOT' The scheduling mode of the leaf tasks. ASAP tasks are scheduled start to end while ALAP tasks are scheduled end to start. EOT ) singlePattern('_seqno') descr('The index of the item based on the declaration order') singlePattern('_sickleave') descr(<<'EOT' The number of sick leave units within the reported time period. The unit can be adjusted with [[loadunit]]. EOT ) singlePattern('_specialleave') descr(<<'EOT' The number of special leave units within the reported time period. The unit can be adjusted with [[loadunit]]. EOT ) singlePattern('_start') descr('The start date of the task') singlePattern('_status') descr(<<'EOT' The status of a task. It is determined based on the current date or the date specified by [[now]]. EOT ) singlePattern('_targets') descr(<<'EOT' A list of milestones that depend on the current task. For container tasks it will also include the targets of the child tasks. Targets may not have any follower tasks. The list can be customized by the [[listitem.column|listitem]] and [[listtype.column|listtype]] attributes. EOT ) singlePattern('_turnover') descr(<<'EOT' The financial turnover of an account during the reporting interval. EOT ) pattern([ '_wbs' ], lambda { 'bsi' }) level(:deprecated) also('bsi') descr('Deprecated alias for bsi.') singlePattern('_unpaidleave') descr(<<'EOT' The number of unpaid leave units within the reported time period. The unit can be adjusted with [[loadunit]]. EOT ) singlePattern('_weekly') descr('A group of columns with one column for each week') singlePattern('_yearly') descr('A group of columns with one column for each year') end def rule_reportAttributes optional repeatable pattern(%w( _accountroot !accountId), lambda { if @val[1].leaf? error('accountroot_leaf', "#{@val[1].fullId} is not a container account", @sourceFileInfo[1]) end @property.set('accountroot', @val[1]) }) doc('accountroot', <<'EOT' Only accounts below the specified root-level accounts are exported. The exported accounts will have the ID of the root-level account stripped from their ID, so that the sub-accounts of the root-level account become top-level accounts in the report file. EOT ) example('AccountReport') pattern(%w( _auxdir $STRING ), lambda { auxdir = @val[1] # Ensure that the directory always ends with a '/'. auxdir += '/' unless auxdir[-1] == ?/ @property.set('auxdir', auxdir) }) level(:beta) doc('auxdir.report', <<'EOT' Specifies an alternative directory for the auxiliary report files such as CSS, JavaScript and icon files. If this attribute is not set, the directory will be generated automatically. If this attribute is provided, the user has to ensure that the directory exists and is filled with the proper data. The specified path can be absolute or relative to the generated report file. EOT ) pattern(%w( !balance ), lambda { @property.set('costaccount', @val[0][0]) @property.set('revenueaccount', @val[0][1]) }) pattern(%w( _caption $STRING ), lambda { @property.set('caption', newRichText(@val[1], @sourceFileInfo[1])) }) doc('caption', <<'EOT' The caption will be embedded in the footer of the table or data segment. The text will be interpreted as [[Rich_Text_Attributes|Rich Text]]. EOT ) arg(1, 'text', 'The caption text.') example('Caption', '1') pattern(%w( _center $STRING ), lambda { @property.set('center', newRichText(@val[1], @sourceFileInfo[1])) }) doc('center', <<'EOT' This attribute defines the center section of the [[textreport]]. The text will be interpreted as [[Rich_Text_Attributes|Rich Text]]. EOT ) arg(1, 'text', 'The text') example('textreport') pattern(%w( _columns !columnDef !moreColumnDef ), lambda { columns = [ @val[1] ] columns += @val[2] if @val[2] @property.set('columns', columns) }) doc('columns', <<'EOT' Specifies which columns shall be included in a report. Some columns show values that are constant over the course of the project. Other columns show calculated values that depend on the time period that was chosen for the report. EOT ) pattern(%w( !currencyFormat ), lambda { @property.set('currencyFormat', @val[0]) }) pattern(%w( !reportEnd )) pattern(%w( _epilog $STRING ), lambda { @property.set('epilog', newRichText(@val[1], @sourceFileInfo[1])) }) doc('epilog', <<'EOT' Define a text section that is printed right after the actual report data. The text will be interpreted as [[Rich_Text_Attributes|Rich Text]]. EOT ) also(%w( footer header prolog )) pattern(%w( !flags )) doc('flags.report', <<'EOT' Attach a set of flags. The flags can be used in logical expressions to filter properties from the reports. EOT ) pattern(%w( _footer $STRING ), lambda { @property.set('footer', newRichText(@val[1], @sourceFileInfo[1])) }) doc('footer', <<'EOT' Define a text section that is put at the bottom of the report. The text will be interpreted as [[Rich_Text_Attributes|Rich Text]]. EOT ) example('textreport') also(%w( epilog header prolog )) pattern(%w( !formats )) pattern(%w( _header $STRING ), lambda { @property.set('header', newRichText(@val[1], @sourceFileInfo[1])) }) doc('header', <<'EOT' Define a text section that is put at the top of the report. The text will be interpreted as [[Rich_Text_Attributes|Rich Text]]. EOT ) example('textreport') also(%w( epilog footer prolog )) pattern(%w( !headline )) pattern(%w( !hidejournalentry )) pattern(%w( !hideaccount )) pattern(%w( !hideresource )) pattern(%w( !hidetask )) pattern(%w( _height $INTEGER ), lambda { if @val[1] < 200 error('min_report_height', "The report must have a minimum height of 200 pixels.") end @property.set('height', @val[1]) }) doc('height', <<'EOT' Set the height of the report in pixels. This attribute is only used for reports that cannot determine the height based on the content. Such reports can be freely resized to fit in. The vast majority of reports can determine their height based on the provided content. These reports will simply ignore this setting. EOT ) also('width') pattern(%w( !journalReportAttributes )) pattern(%w( _journalmode !journalReportMode ), lambda { @property.set('journalMode', @val[1]) }) doc('journalmode', <<'EOT' This attribute controls what journal entries are aggregated into the report. EOT ) pattern(%w( _left $STRING ), lambda { @property.set('left', newRichText(@val[1], @sourceFileInfo[1])) }) doc('left', <<'EOT' This attribute defines the left margin section of the [[textreport]]. The text will be interpreted as [[Rich_Text_Attributes|Rich Text]]. The margin will not span the [[header]] or [[footer]] sections. EOT ) example('textreport') pattern(%w( !loadunit )) pattern(%w( !numberFormat ), lambda { @property.set('numberFormat', @val[0]) }) pattern(%w( _opennodes !nodeIdList ), lambda { @property.set('openNodes', @val[1]) }) doc('opennodes', 'For internal use only!') pattern(%w( !reportPeriod )) pattern(%w( _prolog $STRING ), lambda { @property.set('prolog', newRichText(@val[1], @sourceFileInfo[1])) }) doc('prolog', <<'EOT' Define a text section that is printed right before the actual report data. The text will be interpreted as [[Rich_Text_Attributes|Rich Text]]. EOT ) also(%w( epilog footer header )) pattern(%w( !purge )) pattern(%w( _rawhtmlhead $STRING ), lambda { @property.set('rawHtmlHead', @val[1]) }) doc('rawhtmlhead', <<'EOT' Define an HTML fragment that will be inserted at the end of the HTML head section. EOT ) pattern(%w( !reports )) pattern(%w( _right $STRING ), lambda { @property.set('right', newRichText(@val[1], @sourceFileInfo[1])) }) doc('right', <<'EOT' This attribute defines the right margin section of the [[textreport]]. The text will be interpreted as [[Rich_Text_Attributes|Rich Text]]. The margin will not span the [[header]] or [[footer]] sections. EOT ) example('textreport') pattern(%w( !rollupaccount )) pattern(%w( !rollupresource )) pattern(%w( !rolluptask )) pattern(%w( _scenarios !scenarioIdList ), lambda { # Don't include disabled scenarios in the report @val[1].delete_if { |sc| !@project.scenario(sc).get('active') } @property.set('scenarios', @val[1]) }) doc('scenarios', <<'EOT' List of scenarios that should be included in the report. By default, only the top-level scenario will be included. You can use this attribute to include data from the defined set of scenarios. Not all reports support reporting data from multiple scenarios. They will only include data from the first one in the list. EOT ) pattern(%w( _selfcontained !yesNo ), lambda { @property.set('selfcontained', @val[1]) }) doc('selfcontained', <<'EOT' Try to generate selfcontained output files when the format supports this. E. g. for HTML reports, the style sheet will be included and no icons will be used. EOT ) pattern(%w( !sortAccounts )) pattern(%w( !sortJournalEntries )) pattern(%w( !sortResources )) pattern(%w( !sortTasks )) pattern(%w( !reportStart )) pattern(%w( _resourceroot !resourceId), lambda { if @val[1].leaf? error('resourceroot_leaf', "#{@val[1].fullId} is not a group resource", @sourceFileInfo[1]) end @property.set('resourceroot', @val[1]) }) doc('resourceroot', <<'EOT' Only resources below the specified root-level resources are exported. The exported resources will have the ID of the root-level resource stripped from their ID, so that the sub-resources of the root-level resource become top-level resources in the report file. EOT ) example('ResourceRoot') pattern(%w( _taskroot !taskId), lambda { if @val[1].leaf? error('taskroot_leaf', "#{@val[1].fullId} is not a container task", @sourceFileInfo[1]) end @property.set('taskroot', @val[1]) }) doc('taskroot', <<'EOT' Only tasks below the specified root-level tasks are exported. The exported tasks will have the ID of the root-level task stripped from their ID, so that the sub-tasks of the root-level task become top-level tasks in the report file. EOT ) example('TaskRoot') pattern(%w( !timeformat ), lambda { @property.set('timeFormat', @val[0]) }) pattern(%w( _timezone !validTimeZone ), lambda { @property.set('timezone', @val[1]) }) doc('timezone.report', <<'EOT' Sets the time zone used for all dates in the report. This setting is ignored if the report is embedded into another report. Embedded in this context means the report is part of another generated report. It does not mean that the report definition is a sub report of another report definition. EOT ) pattern(%w( !reportTitle )) pattern(%w( _width $INTEGER ), lambda { if @val[1] < 400 error('min_report_width', "The report must have a minimum width of 400 pixels.") end @property.set('width', @val[1]) }) doc('width', <<'EOT' Set the width of the report in pixels. This attribute is only used for reports that cannot determine the width based on the content. Such reports can be freely resized to fit in. The vast majority of reports can determine their width based on the provided content. These reports will simply ignore this setting. EOT ) also('height') end def rule_reportEnd pattern(%w( _end !date ), lambda { if @val[1] < @property.get('start') error('report_end', "End date must be before start date #{@property.get('start')}", @sourceFileInfo[1]) end @property.set('end', @val[1]) }) doc('end.report', <<'EOT' Specifies the end date of the report. In task reports only tasks that start before this end date are listed. EOT ) example('Export', '2') end def rule_reportId pattern(%w( !reportIdUnverifd ), lambda { id = @val[0] if @property && @property.is_a?(Report) id = @property.fullId + '.' + id else id = @reportprefix + '.' + id unless @reportprefix.empty? end # In case we have a nested supplement, we need to prepend the parent ID. if (report = @project.report(id)).nil? error('report_id_expected', "#{id} is not a defined report.", @sourceFileInfo[0]) end report }) arg(0, 'report', 'The ID of a defined report') end def rule_reportIdUnverifd singlePattern('$ABSOLUTE_ID') singlePattern('$ID') end def rule_reportName pattern(%w( $STRING ), lambda { @val[0] }) arg(0, 'name', <<'EOT' The name of the report. This will be the base name for generated output files. The suffix will depend on the specified [[formats]]. It will also be used in navigation bars. By default, report definitions do not generate any files. With more complex projects, most report definitions will be used to describe elements of composed reports. If you want to generate a file from this report, you must specify the list of [[formats]] that you want to generate. The report name will then be used as a base name to create the file. The suffix will be appended based on the generated format. Reports have a local name space. All IDs and file names must be unique within the reports that belong to the same enclosing report. To reference a report for inclusion into another report, you need to specify the full report ID. This is composed of the report ID, prefixed by a dot-separated list of all parent report IDs. EOT ) end def rule_reportPeriod pattern(%w( _period !interval ), lambda { @property.set('start', @val[1].start) @property.set('end', @val[1].end) }) doc('period.report', <<'EOT' This property is a shortcut for setting the start and end property at the same time. EOT ) end def rule_reportProperties pattern(%w( !iCalReport )) pattern(%w( !nikuReport )) pattern(%w( !reports )) pattern(%w( !tagfile )) pattern(%w( !statusSheetReport )) pattern(%w( !timeSheetReport )) end def rule_reportPropertiesBody optional repeatable pattern(%w( !macro )) pattern(%w( !reportProperties )) end def rule_reportPropertiesFile pattern(%w( !reportPropertiesBody . )) end def rule_reportStart pattern(%w( _start !date ), lambda { if @val[1] > @property.get('end') error('report_start', "Start date must be before end date #{@property.get('end')}", @sourceFileInfo[1]) end @property.set('start', @val[1]) }) doc('start.report', <<'EOT' Specifies the start date of the report. In task reports only tasks that end after this end date are listed. EOT ) end def rule_reportBody optionsRule('reportAttributes') end def rule_reportTitle pattern(%w( _title $STRING ), lambda { @property.set('title', @val[1]) }) doc('title', <<'EOT' The title of the report will be used in external references to the report. It will not show up in the reports directly. It's used e. g. by [[navigator]]. EOT ) end def rule_resource pattern(%w( !resourceHeader !resourceBody ), lambda { @property = @property.parent }) doc('resource', <<'EOT' Tasks that have an effort specification need to have at least one resource assigned to do the work. Use this property to define resources or groups of resources. Resources have a global name space. All IDs must be unique within the resources of the project. EOT ) end def rule_resourceAttributes repeatable optional pattern(%w( _email $STRING ), lambda { @property.set('email', @val[1]) }) doc('email', 'The email address of the resource.') pattern(%w( !journalEntry )) pattern(%w( !purge )) pattern(%w( !resource )) pattern(%w( !resourceScenarioAttributes )) pattern(%w( !scenarioIdCol !resourceScenarioAttributes ), lambda { @scenarioIdx = 0 }) pattern(%w( _supplement !resourceId !resourceBody ), lambda { @property = @idStack.pop }) doc('supplement.resource', <<'EOT' The supplement keyword provides a mechanism to add more attributes to already defined resources. The additional attributes must obey the same rules as in regular resource definitions and must be enclosed by curly braces. This construct is primarily meant for situations where the information about a resource is split over several files. E. g. the vacation dates for the resources may be in a separate file that was generated by some other tool. EOT ) example('Supplement', 'resource') # Other attributes will be added automatically. end def rule_resourceBody optionsRule('resourceAttributes') end def rule_resourceBooking pattern(%w( !resourceBookingHeader !bookingBody ), lambda { unless @project.scenario(@scenarioIdx).get('ownbookings') error('no_own_resource_booking', "The scenario #{@project.scenario(@scenarioIdx).fullId} " + 'inherits its bookings from the tracking ' + 'scenario. You cannot specificy additional bookings for it.') end @val[0].task.addBooking(@scenarioIdx, @val[0]) }) end def rule_resourceBookingHeader pattern(%w( !taskId !valIntervals ), lambda { checkBooking(@val[0], @property) @booking = Booking.new(@property, @val[0], @val[1]) @booking.sourceFileInfo = @sourceFileInfo[0] @booking }) arg(0, 'id', 'Absolute ID of a defined task') end def rule_resourceId pattern(%w( $ID ), lambda { id = (@resourceprefix.empty? ? '' : @resourceprefix + '.') + @val[0] if (resource = @project.resource(id)).nil? error('resource_id_expected', "#{id} is not a defined resource.", @sourceFileInfo[0]) end resource }) arg(0, 'resource', 'The ID of a defined resource') end def rule_resourceHeader pattern(%w( _resource !optionalID $STRING ), lambda { if @property.nil? && !@resourceprefix.empty? @property = @project.resource(@resourceprefix) end if @val[1] && @project.resource(@val[1]) error('resource_exists', "Resource #{@val[1]} has already been defined.", @sourceFileInfo[1], @property) end @property = Resource.new(@project, @val[1], @val[2], @property) @property.sourceFileInfo = @sourceFileInfo[0] @property.inheritAttributes @scenarioIdx = 0 }) # arg(1, 'id', <<'EOT' #The ID of the resource. Resources have a global name space. The ID must be #unique within the whole project. #EOT # ) arg(2, 'name', 'The name of the resource') end def rule_resourceLeafList listRule('moreResourceLeafList', '!leafResourceId') end def rule_resourceList listRule('moreResources', '!undefResourceId') end def rule_resourceReport pattern(%w( !resourceReportHeader !reportBody ), lambda { @property = @property.parent }) doc('resourcereport', <<'EOT' The report lists resources and their respective values in a table. The task that the resources are allocated to can be listed as well. To reduce the list of included resources, you can use the [[hideresource]], [[rollupresource]] or [[resourceroot]] attributes. The order of the tasks can be controlled with [[sortresources]]. If the first sorting criteria is tree sorting, the parent resources will always be included to form the tree. Tree sorting is the default. You need to change it if you do not want certain parent resources to be included in the report. By default, all the tasks that the resources are allocated to are hidden, but they can be listed as well. Use the [[hidetask]] attribute to select which tasks should be included. EOT ) end def rule_resourceReportHeader pattern(%w( _resourcereport !optionalID !reportName ), lambda { newReport(@val[1], @val[2], :resourcereport, @sourceFileInfo[0]) do unless @property.modified?('columns') # Set the default columns for this report. %w( no name ).each do |col| @property.get('columns') << TableColumnDefinition.new(col, columnTitle(col)) end end # Show all resources, sorted by tree and id-up. unless @property.modified?('hideResource') @property.set('hideResource', LogicalExpression.new(LogicalOperation.new(0))) end unless @property.modified?('sortResources') @property.set('sortResources', [ [ 'tree', true, -1 ], [ 'id', true, -1 ] ]) end # Hide all resources, but set sorting to tree, start-up, seqno-up. unless @property.modified?('hideTask') @property.set('hideTask', LogicalExpression.new(LogicalOperation.new(1))) end unless @property.modified?('sortTasks') @property.set('sortTasks', [ [ 'tree', true, -1 ], [ 'start', true, 0 ], [ 'seqno', true, -1 ] ]) end end }) end def rule_resourceScenarioAttributes pattern(%w( !chargeset )) pattern(%w( _efficiency !number ), lambda { @property['efficiency', @scenarioIdx] = @val[1] }) doc('efficiency', <<'EOT' The efficiency of a resource can be used for two purposes. First you can use it as a crude way to model a team. A team of 5 people should have an efficiency of 5.0. Keep in mind that you cannot track the members of the team individually if you use this feature. They always act as a group. The other use is to model performance variations between your resources. Again, this is a fairly crude mechanism and should be used with care. A resource that isn't very good at some task might be pretty good at another. This can't be taken into account as the resource efficiency can only be set globally for all tasks. All resources that do not contribute effort to the task, should have an efficiency of 0.0. A typical example would be a conference room. It's necessary for a meeting, but it does not contribute any work. EOT ) example('Efficiency') pattern(%w( !flags )) doc('flags.resource', <<'EOT' Attach a set of flags. The flags can be used in logical expressions to filter properties from the reports. EOT ) pattern(%w( _booking !resourceBooking )) doc('booking.resource', <<'EOT' The booking attribute can be used to report actually completed work. A task with bookings must be [[scheduling|scheduled]] in ''''ASAP'''' mode. If the scenario is not the [[trackingscenario|tracking scenario]] or derived from it, the scheduler will not allocate resources prior to the current date or the date specified with [[now]] when a task has at least one booking. Bookings are only valid in the scenario they have been defined in. They will in general not be passed to any other scenario. If you have defined a [[trackingscenario|tracking scenario]], the bookings of this scenario will be passed to all the derived scenarios of the tracking scenario. The sloppy attribute can be used when you want to skip non-working time or other allocations automatically. If it's not given, all bookings must only cover working time for the resource. The booking attributes is designed to capture the exact amount of completed work. This attribute is not really intended to specify completed effort by hand. Usually, booking statements are generated by [[export]] reports. The [[sloppy.booking|sloppy]] and [[overtime.booking|overtime]] attributes are only kludge for users who want to write them manually. Bookings can be used to report already completed work by specifying the exact time intervals a certain resource has worked on this task. Bookings can be defined in the task or resource context. If you move tasks around very often, put your bookings in the task context. EOT ) also(%w( scheduling booking.task )) example('Booking') pattern(%w( !fail )) pattern(%w( !leaveAllowances )) pattern(%w( !leaves ), lambda { @property['leaves', @scenarioIdx] += @val[0] }) pattern(%w( !limits ), lambda { @property['limits', @scenarioIdx] = @val[0] }) doc('limits.resource', <<'EOT' Set per-interval usage limits for the resource. EOT ) example('Limits-1', '6') pattern(%w( _managers !resourceList ), lambda { @property['managers', @scenarioIdx] = @property['managers', @scenarioIdx] + @val[1] }) doc('managers', <<'EOT' Defines one or more resources to be the manager who is responsible for this resource. Managers must be leaf resources. This attribute does not impact the scheduling. It can only be used for documentation purposes. You must only specify direct managers here. Do not list higher level managers here. If necessary, use the [[purge]] attribute to clear inherited managers. For most use cases, there should be only one manager. But TaskJuggler is not limited to just one manager. Dotted reporting lines can be captured as well as long as the managers are not reporting to each other. EOT ) also(%w( statussheet )) example('Manager') pattern(%w( _rate !number ), lambda { @property['rate', @scenarioIdx] = @val[1] }) doc('rate.resource', <<'EOT' The rate specifies the daily cost of the resource. EOT ) pattern(%w( !resourceShiftAssignments !shiftAssignments ), lambda { checkContainer('shifts') # Set same value again to set the 'provided' state for the attribute. begin @property['shifts', @scenarioIdx] = @shiftAssignments rescue AttributeOverwrite # Multiple shift assignments are a common idiom, so don't warn about # them. end @shiftAssignments = nil }) level(:deprecated) also('shifts.resource') doc('shift.resource', <<'EOT' This keyword has been deprecated. Please use [[shifts.resource|shifts (resource)]] instead. EOT ) pattern(%w( !resourceShiftsAssignments !shiftAssignments ), lambda { checkContainer('shifts') # Set same value again to set the 'provided' state for the attribute. begin @property['shifts', @scenarioIdx] = @shiftAssignments rescue AttributeOverwrite # Multiple shift assignments are a common idiom, so don't warn about # them. end @shiftAssignments = nil }) doc('shifts.resource', <<'EOT' Limits the working time of a resource to a defined shift during the specified interval. Multiple shifts can be defined, but shift intervals may not overlap. In case a shift is defined for a certain interval, the shift working hours replace the standard resource working hours for this interval. EOT ) pattern(%w( _vacation !vacationName !intervals ), lambda { @val[2].each do |interval| # We map the old 'vacation' attribute to public holidays. @property['leaves', @scenarioIdx] += [ Leave.new(:holiday, interval) ] end }) doc('vacation.resource', <<'EOT' Specify a vacation period for the resource. It can also be used to block out the time before a resource joined or after it left. For employees changing their work schedule from full-time to part-time, or vice versa, please refer to the 'Shift' property. EOT ) pattern(%w( !warn )) pattern(%w( !workinghoursResource )) # Other attributes will be added automatically. end def rule_resourceShiftAssignments pattern(%w( _shift ), lambda { @shiftAssignments = @property['shifts', @scenarioIdx] }) end def rule_resourceShiftsAssignments pattern(%w( _shifts ), lambda { @shiftAssignments = @property['shifts', @scenarioIdx] }) end def rule_rollupaccount pattern(%w( _rollupaccount !logicalExpression ), lambda { @property.set('rollupAccount', @val[1]) }) doc('rollupaccount', <<'EOT' Do not show sub-accounts of accounts that match the specified [[logicalexpression|logical expression]]. EOT ) end def rule_rollupresource pattern(%w( _rollupresource !logicalExpression ), lambda { @property.set('rollupResource', @val[1]) }) doc('rollupresource', <<'EOT' Do not show sub-resources of resources that match the specified [[logicalexpression|logical expression]]. EOT ) example('RollupResource') end def rule_rolluptask pattern(%w( _rolluptask !logicalExpression ), lambda { @property.set('rollupTask', @val[1]) }) doc('rolluptask', <<'EOT' Do not show sub-tasks of tasks that match the specified [[logicalexpression|logical expression]]. EOT ) end def rule_scenario pattern(%w( !scenarioHeader !scenarioBody ), lambda { @property = @property.parent }) doc('scenario', <<'EOT' Defines a new project scenario. By default, the project has only one scenario called ''''plan''''. To do plan vs. actual comparisons or to do a what-if-analysis, you can define a set of scenarios. There can only be one top-level scenario. Additional scenarios are either derived from this top-level scenario or other scenarios. Each nested scenario is a variation of the enclosing scenario. All scenarios share the same set of properties (task, resources, etc.) but the attributes that are listed as scenario specific may differ between the various scenarios. A nested scenario uses all attributes from the enclosing scenario unless the user has specified a different value for this attribute. By default, the scheduler assigns resources to tasks beginning with the project start date. If the scenario is switched to projection mode, no assignments will be made prior to the current date or the date specified by [[now]]. In this case, TaskJuggler assumes, that all assignments prior to the current date have been provided by [[booking.task]] statements. EOT ) end def rule_scenarioAttributes optional repeatable pattern(%w( _active !yesNo), lambda { @property.set('active', @val[1]) }) doc('active', <<'EOT' Enable the scenario to be scheduled or not. By default, all scenarios will be scheduled. If a scenario is marked as inactive, it cannot be scheduled and will be ignored in the reports. EOT ) pattern(%w( _disabled ), lambda { @property.set('active', false) }) level(:deprecated) also('active') doc('disabled', <<'EOT' This attribute is deprecated. Please use [active] instead. Disable the scenario for scheduling. The default for the top-level scenario is to be enabled. EOT ) example('Scenario') pattern(%w( _enabled ), lambda { @property.set('active', true) }) level(:deprecated) also('active') doc('enabled', <<'EOT' This attribute is deprecated. Please use [active] instead. Enable the scenario for scheduling. This is the default for the top-level scenario. EOT ) pattern(%w( _projection !projection )) level(:deprecated) also('booking.task') doc('projection', <<'EOT' This keyword has been deprecated! Don't use it anymore! Projection mode is now automatically enabled as soon as a scenario has bookings. EOT ) pattern(%w( !scenario )) end def rule_scenarioBody optionsRule('scenarioAttributes') end def rule_scenarioHeader pattern(%w( _scenario $ID $STRING ), lambda { # If this is the top-level scenario, we must delete the default scenario # first. @project.scenarios.each do |scenario| if scenario.get('projection') error('scenario_after_tracking', 'Scenarios must be defined before a tracking scenario is set.') end end @project.scenarios.clearProperties if @property.nil? if @project.scenario(@val[1]) error('scenario_exists', "Scenario #{@val[1]} has already been defined.", @sourceFileInfo[1]) end @property = Scenario.new(@project, @val[1], @val[2], @property) @property.inheritAttributes if @project.scenarios.length > 1 MessageHandlerInstance.instance.hideScenario = false end }) arg(1, 'id', 'The ID of the scenario') arg(2, 'name', 'The name of the scenario') end def rule_scenarioId pattern(%w( $ID ), lambda { if (@scenarioIdx = @project.scenarioIdx(@val[0])).nil? error('unknown_scenario_id', "Unknown scenario: #{@val[0]}", @sourceFileInfo[0]) end @scenarioIdx }) arg(0, 'scenario', 'ID of a defined scenario') end def rule_scenarioIdCol pattern(%w( $ID_WITH_COLON ), lambda { if (@scenarioIdx = @project.scenarioIdx(@val[0])).nil? error('unknown_scenario_id', "Unknown scenario: #{@val[0]}", @sourceFileInfo[0]) end }) end def rule_scenarioIdList listRule('moreScnarioIdList', '!scenarioIdx') end def rule_scenarioIdx pattern(%w( $ID ), lambda { if (scenarioIdx = @project.scenarioIdx(@val[0])).nil? error('unknown_scenario_idx', "Unknown scenario #{@val[0]}", @sourceFileInfo[0]) end scenarioIdx }) end def rule_schedulingDirection singlePattern('_alap') singlePattern('_asap') end def rule_schedulingMode singlePattern('_planning') singlePattern('_projection') end def rule_shift pattern(%w( !shiftHeader !shiftBody ), lambda { @property = @property.parent }) doc('shift', <<'EOT' A shift combines several workhours related settings in a reusable entity. Besides the weekly working hours it can also hold information such as leaves and a time zone. It lets you create a work time calendar that can be used to limit the working time for resources or tasks. Shifts have a global name space. All IDs must be unique within the shifts of the project. EOT ) also(%w( shifts.task shifts.resource )) end def rule_shiftAssignment pattern(%w( !shiftId !intervalOptional ), lambda { # Make sure we have a ShiftAssignment for the property. unless @shiftAssignments @shiftAssignments = ShiftAssignments.new @shiftAssignments.project = @project end if @val[1].nil? interval = TimeInterval.new(@project['start'], @project['end']) else interval = @val[1] end if !@shiftAssignments.addAssignment( ShiftAssignment.new(@val[0].scenario(@scenarioIdx), interval)) error('shift_assignment_overlap', 'Shifts may not overlap each other.', @sourceFileInfo[0], @property) end @shiftAssignments.assignments.last }) end def rule_shiftAssignments listRule('moreShiftAssignments', '!shiftAssignment') end def rule_shiftAttributes optional repeatable pattern(%w( !shift )) pattern(%w( !shiftScenarioAttributes )) pattern(%w( !scenarioIdCol !shiftScenarioAttributes ), lambda { @scenarioIdx = 0 }) end def rule_shiftBody optionsRule('shiftAttributes') end def rule_shiftHeader pattern(%w( _shift !optionalID $STRING ), lambda { if @val[1] && @project.shift(@val[1]) error('shift_exists', "Shift #{@val[1]} has already been defined.", @sourceFileInfo[1]) end @property = Shift.new(@project, @val[1], @val[2], @property) @property.sourceFileInfo = @sourceFileInfo[0] @property.inheritAttributes @scenarioIdx = 0 }) arg(2, 'name', 'The name of the shift') end def rule_shiftId pattern(%w( $ID ), lambda { if (shift = @project.shift(@val[0])).nil? error('shift_id_expected', "#{@val[0]} is not a defined shift.", @sourceFileInfo[0]) end shift }) arg(0, 'shift', 'The ID of a defined shift') end def rule_shiftScenarioAttributes pattern(%w( !leaves ), lambda { @property['leaves', @scenarioIdx] += @val[0] }) pattern(%w( _replace ), lambda { @property['replace', @scenarioIdx] = true }) doc('replace', <<'EOT' This replace mode is only effective for shifts that are assigned to resources directly. When replace mode is activated the leave definitions of the shift will replace all the leave definitions of the resource for the given period. The mode is not effective for shifts that are assigned to tasks or allocations. EOT ) pattern(%w( _timezone !validTimeZone ), lambda { @property['timezone', @scenarioIdx] = @val[1] }) doc('timezone.shift', <<'EOT' Sets the time zone of the shift. The working hours of the shift are assumed to be within the specified time zone. The time zone does not effect the vaction interval. The latter is assumed to be within the project time zone. TaskJuggler stores all dates internally as UTC. Since all events must align with the [[timingresolution|timing resolution]] for time zones you may have to change the timing resolution appropriately. The time zone difference compared to UTC must be a multiple of the used timing resolution. EOT ) arg(1, 'zone', <<'EOT' Time zone to use. E. g. 'Europe/Berlin' or 'America/Denver'. Don't use the 3 letter acronyms. See [http://en.wikipedia.org/wiki/List_of_zoneinfo_time_zones Wikipedia] for possible values. EOT ) pattern(%w( _vacation !vacationName !intervalsOptional ), lambda { @val[2].each do |interval| # We map the old 'vacation' attribute to public holidays. @property['leaves', @scenarioIdx] += [ Leave.new(:holiday, interval) ] end }) doc('vacation.shift', <<'EOT' Specify a vacation period associated with this shift. EOT ) pattern(%w( !workinghoursShift )) end def rule_sortCriteria pattern([ "!sortCriterium", "!moreSortCriteria" ], lambda { [ @val[0] ] + (@val[1].nil? ? [] : @val[1]) }) end def rule_sortCriterium pattern(%w( !sortTree ), lambda { @val[0] }) pattern(%w( !sortNonTree ), lambda { @val[0] }) end def rule_sortNonTree pattern(%w( $ABSOLUTE_ID ), lambda { args = @val[0].split('.') case args.length when 2 # . # We default to the top-level scenario. if args[1] != 'up' && args[1]!= 'down' error('sort_direction', "Sorting direction must be 'up' or 'down'", @sourceFileInfo[0]) end scenario = -1 direction = args[1] == 'up' attribute = args[0] when 3 # .. if (scenario = @project.scenarioIdx(args[0])).nil? error('sort_unknown_scen', "Unknown scenario #{args[0]} in sorting criterium", @sourceFileInfo[0]) end attribute = args[1] if args[2] != 'up' && args[2] != 'down' error('sort_direction', "Sorting direction must be 'up' or 'down'", @sourceFileInfo[0]) end direction = args[2] == 'up' else error('sorting_crit_exptd1', "Sorting criterium expected (e.g. tree, start.up or " + "plan.end.down).", @sourceFileInfo[0]) end if attribute == 'bsi' error('sorting_bsi', "Sorting by bsi is not supported. Please use 'tree' " + '(without appended .up or .down) instead.', @sourceFileInfo[0]) end case @sortProperty when :account ps = @project.accounts when :resource ps = @project.resources when :task ps = @project.tasks end unless ps.knownAttribute?(attribute) || TableReport::calculated?(attribute) error('sorting_unknown_attr', "Sorting criterium '#{attribute} is not a known attribute.") end if scenario > 0 && !(ps.scenarioSpecific?(attribute) || TableReport.scenarioSpecific?(attribute)) error('sorting_attr_scen_spec', "Sorting criterium '#{attribute}' is not scenario specific " + "but a scenario has been specified.") elsif scenario == -1 && (ps.scenarioSpecific?(attribute) || TableReport.scenarioSpecific?(attribute)) # If no scenario was specified but the attribute is scenario specific, # we default to the top-level scenario. scenario = 0 end [ attribute, direction, scenario ] }) arg(0, 'criteria', <<'EOT' The sorting criteria must consist of a property attribute ID. See [[columnid]] for a complete list of available attributes. The ID must be suffixed by '.up' or '.down' to determine the sorting direction. Optionally the ID may be prefixed with a scenario ID and a dot to determine the scenario that should be used for sorting. In case no scenario was specified, the top-level scenario is used. Example values are 'plan.start.up' or 'priority.down'. EOT ) end def rule_sortJournalEntries pattern(%w( _sortjournalentries !journalSortCriteria ), lambda { @property.set('sortJournalEntries', @val[1]) }) doc('sortjournalentries', <<'EOT' Determines how the entries in a journal are sorted. Multiple criteria can be specified as a comma separated list. If one criteria is not sufficient to sort a group of journal entries, the next criteria will be used to sort the entries in this group. The following values are supported: * ''''date.down'''': Sort descending order by the date of the journal entry * ''''date.up'''': Sort ascending order by the date of the journal entry * ''''alert.down'''': Sort in descending order by the alert level of the journal entry * ''''alert.up'''': Sort in ascending order by the alert level of the journal entry ''''property.down'''': Sort in descending order by the task or resource the journal entry is associated with * ''''property.up'''': Sort in ascending order by the task or resource the journal entry is associated with EOT ) end def rule_sortAccountsKeyword pattern(%w( _sortaccounts ), lambda { @sortProperty = :account }) end def rule_sortAccounts pattern(%w( !sortAccountsKeyword !sortCriteria ), lambda { @property.set('sortAccounts', @val[1]) }) doc('sortaccounts', <<'EOT' Determines how the accounts are sorted in the report. Multiple criteria can be specified as a comma separated list. If one criteria is not sufficient to sort a group of accounts, the next criteria will be used to sort the accounts in this group. EOT ) end def rule_sortResourcesKeyword pattern(%w( _sortresources ), lambda { @sortProperty = :resource }) end def rule_sortResources pattern(%w( !sortResourcesKeyword !sortCriteria ), lambda { @property.set('sortResources', @val[1]) }) doc('sortresources', <<'EOT' Determines how the resources are sorted in the report. Multiple criteria can be specified as a comma separated list. If one criteria is not sufficient to sort a group of resources, the next criteria will be used to sort the resources in this group. EOT ) end def rule_sortTasksKeyword pattern(%w( _sorttasks ), lambda { @sortProperty = :task }) end def rule_sortTasks pattern(%w( !sortTasksKeyword !sortCriteria ), lambda { @property.set('sortTasks', @val[1]) }) doc('sorttasks', <<'EOT' Determines how the tasks are sorted in the report. Multiple criteria can be specified as comma separated list. If one criteria is not sufficient to sort a group of tasks, the next criteria will be used to sort the tasks within this group. EOT ) end def rule_sortTree pattern(%w( $ID ), lambda { if @val[0] != 'tree' error('sorting_crit_exptd2', "Sorting criterium expected (e.g. tree, start.up or " + "plan.end.down).", @sourceFileInfo[0]) end [ 'tree', true, -1 ] }) arg(0, 'tree', 'Use \'tree\' as first criteria to keep the breakdown structure.') end def rule_ssReportHeader pattern(%w( _statussheetreport !optionalID $STRING ), lambda { newReport(@val[1], @val[2], :statusSheet, @sourceFileInfo[0]) do @property.set('formats', [ :tjp ]) unless (@project['trackingScenarioIdx']) error('ss_no_tracking_scenario', 'You must have a tracking scenario defined to use status sheets.') end # Show all tasks, sorted by id-up. @property.set('hideTask', LogicalExpression.new(LogicalOperation.new(0))) @property.set('sortTasks', [ [ 'id', true, -1 ] ]) # Show all resources, sorted by seqno-up. @property.set('hideResource', LogicalExpression.new(LogicalOperation.new(0))) @property.set('sortResources', [ [ 'seqno', true, -1 ] ]) @property.set('loadUnit', :hours) @property.set('definitions', []) end }) arg(2, 'file name', <<'EOT' The name of the status sheet report file to generate. It must end with a .tji extension, or use . to use the standard output channel. EOT ) end def rule_ssReportAttributes optional repeatable pattern(%w( !hideresource )) pattern(%w( !hidetask )) pattern(%w( !reportEnd )) pattern(%w( !reportPeriod )) pattern(%w( !reportStart )) pattern(%w( !sortResources )) pattern(%w( !sortTasks )) end def rule_ssReportBody optionsRule('ssReportAttributes') end def rule_ssStatusAttributes optional repeatable pattern(%w( !author )) pattern(%w( !details )) pattern(%w( _flags !flagList ), lambda { @val[1].each do |flag| next if @journalEntry.flags.include?(flag) @journalEntry.flags << flag end }) doc('flags.statussheet', <<'EOT' Status sheet entries can have flags attached to them. These can be used to include only entries in a report that have a certain flag. EOT ) pattern(%w( !summary )) end def rule_ssStatusBody optional pattern(%w( _{ !ssStatusAttributes _} )) end def rule_ssStatusHeader pattern(%w( _status !alertLevel $STRING ), lambda { @journalEntry = JournalEntry.new(@project['journal'], @sheetEnd, @val[2], @property, @sourceFileInfo[0]) @journalEntry.alertLevel = @val[1] @journalEntry.author = @sheetAuthor @journalEntry.moderators << @sheetModerator }) end def rule_ssStatus pattern(%w( !ssStatusHeader !ssStatusBody )) doc('status.statussheet', <<'EOT' The status attribute can be used to describe the current status of the task or resource. The content of the status messages is added to the project journal. EOT ) end def rule_statusSheet pattern(%w( !statusSheetHeader !statusSheetBody ), lambda { [ @sheetAuthor, @sheetStart, @sheetEnd ] }) doc('statussheet', <<'EOT' A status sheet can be used to capture the status of various tasks outside of the regular task tree definition. It is intended for use by managers that don't directly work with the full project plan, but need to report the current status of each task or task-tree that they are responsible for. EOT ) example('StatusSheet') end def rule_statusSheetAttributes optional repeatable pattern(%w( !statusSheetTask )) end def rule_statusSheetBody optionsRule('statusSheetAttributes') end def rule_statusSheetFile pattern(%w( !statusSheet . ), lambda { @val[0] }) lastSyntaxToken(1) end def rule_statusSheetHeader pattern(%w( _statussheet !resourceId !valIntervalOrDate ), lambda { unless @project['trackingScenarioIdx'] error('ss_no_tracking_scenario', 'No trackingscenario defined.') end @sheetAuthor = @val[1] @sheetModerator = @val[1] @sheetStart = @val[2].start @sheetEnd = @val[2].end # Make sure that we don't have any status sheet entries from the same # author for the same report period. There may have been a previous # submission of the same report and this is an update to it. All old # entries must be removed before we process the sheet. @project['journal'].delete_if do |e| # Journal entries from status sheets have the sheet end date as entry # date. e.moderators.include?(@sheetModerator) && e.date == @sheetEnd end }) arg(1, 'reporter', <<'EOT' The ID of a defined resource. This identifies the status reporter. Unless the status entries provide a different author, the sheet author will be used as status entry author. EOT ) end def rule_statusSheetReport pattern(%w( !ssReportHeader !ssReportBody ), lambda { @property = nil }) doc('statussheetreport', <<'EOT' A status sheet report is a template for a status sheet. It collects all the status information of the top-level task that a resource is responsible for. This report is typically used by managers or team leads to review the time sheet status information and destill it down to a summary that can be forwarded to the next person in the reporting chain. The report will be for the specified [trackingscenario]. EOT ) end def rule_statusSheetTask pattern(%w( !statusSheetTaskHeader !statusSheetTaskBody), lambda { @property = @propertyStack.pop }) doc('task.statussheet', <<'EOT' Opens the task with the specified ID to add a status report. Child tasks can be opened inside this context by specifying their relative ID to this parent. EOT ) end def rule_statusSheetTaskAttributes optional repeatable pattern(%w( !ssStatus )) pattern(%w( !statusSheetTask ), lambda { }) end def rule_statusSheetTaskBody optionsRule('statusSheetTaskAttributes') end def rule_statusSheetTaskHeader pattern(%w( _task !taskId ), lambda { if @property @propertyStack.push(@property) else @propertyStack = [] end @property = @val[1] }) end def rule_subNodeId optional pattern(%w( _: !idOrAbsoluteId ), lambda { @val[1] }) end def rule_summary pattern(%w( _summary $STRING ), lambda { return if @val[1].empty? if @val[1].length > 480 error('ts_summary_too_long', "The summary text must be 480 characters long or shorter. " + "This text has #{@val[1].length} characters.", @sourceFileInfo[1]) end if @val[1] == "A summary text\n" error('ts_default_summary', "'A summary text' is not a valid summary", @sourceFileInfo[1]) end rtTokenSetIntro = [ :LINEBREAK, :SPACE, :WORD, :BOLD, :ITALIC, :CODE, :BOLDITALIC, :HREF, :HREFEND ] @journalEntry.summary = newRichText(@val[1], @sourceFileInfo[1], rtTokenSetIntro) }) doc('summary', <<'EOT' This is the introductory part of the journal or status entry. It should only summarize the full entry but should contain more details than the headline. The text including formatting characters must be 240 characters long or less. EOT ) arg(1, 'text', <<'EOT' The text will be interpreted as [[Rich_Text_Attributes|Rich Text]]. Only a small subset of the markup is supported for this attribute. You can use word formatting, hyperlinks and paragraphs. EOT ) end def rule_supplement pattern(%w( !supplementAccount !accountBody ), lambda { @property = @idStack.pop }) pattern(%w( !supplementReport !reportBody ), lambda { @property = @idStack.pop }) pattern(%w( !supplementResource !resourceBody ), lambda { @property = @idStack.pop }) pattern(%w( !supplementTask !taskBody ), lambda { @property = @idStack.pop }) end def rule_supplementAccount pattern(%w( _account !accountId ), lambda { @idStack.push(@property) @property = @val[1] }) arg(1, 'account ID', 'The ID of an already defined account.') end def rule_supplementReport pattern(%w( _report !reportId ), lambda { @idStack.push(@property) @property = @val[1] }) arg(1, 'report ID', 'The absolute ID of an already defined report.') end def rule_supplementResource pattern(%w( _resource !resourceId ), lambda { @idStack.push(@property) @property = @val[1] }) arg(1, 'resource ID', 'The ID of an already defined resource.') end def rule_supplementTask pattern(%w( _task !taskId ), lambda { @idStack.push(@property) @property = @val[1] }) arg(1, 'task ID', 'The absolute ID of an already defined task.') end def rule_tagfile pattern(%w( !tagfileHeader !tagfileBody ), lambda { @property = nil }) doc('tagfile', <<'EOT' The tagfile report generates a file that maps properties to source file locations. This can be used by editors to quickly jump to a certain task or resource definition. Currently only the ctags format is supported that is used by editors like [http://www.vim.org|vim]. EOT ) end def rule_tagfileHeader pattern(%w( _tagfile !optionalID $STRING ), lambda { newReport(@val[1], @val[2], :tagfile, @sourceFileInfo[0]) do @property.set('formats', [ :ctags ]) # Include all tasks. @property.set('hideTask', LogicalExpression.new(LogicalOperation.new(0))) @property.set('sortTasks', [ [ 'seqno', true, -1 ] ]) # Include all resources. @property.set('hideResource', LogicalExpression.new(LogicalOperation.new(0))) @property.set('sortResources', [ [ 'seqno', true, -1 ] ]) end }) arg(2, 'file name', <<'EOT' The name of the tagfile to generate. Use ''''tags'''' if you want vim and other tools to find it automatically. EOT ) end def rule_tagfileAttributes optional repeatable pattern(%w( !hideresource )) pattern(%w( !hidetask )) pattern(%w( !rollupresource )) pattern(%w( !rolluptask )) end def rule_tagfileBody optionsRule('tagfileAttributes') end def rule_task pattern(%w( !taskHeader !taskBody ), lambda { @property = @property.parent }) doc('task', <<'EOT' Tasks are the central elements of a project plan. Use a task to specify the various steps and phases of the project. Depending on the attributes of that task, a task can be a container task, a milestone or a regular leaf task. The latter may have resources assigned. By specifying dependencies the user can force a certain sequence of tasks. Tasks have a local name space. All IDs must be unique within the tasks that belong to the same enclosing task. EOT ) end def rule_taskAttributes repeatable optional pattern(%w( _adopt !taskList ), lambda { @val[1].each do |task| @property.adopt(task) end }) level(:experimental) doc('adopt.task', <<'EOT' Add a previously defined task and its sub-tasks to this task. This can be used to create virtual projects that contain task (sub-)trees that are originally defined in another task context. Adopted tasks don't inherit anything from their step parents. However, the adopting task is scheduled to fit all adopted sub-tasks. A top-level task and all its sub-tasks must never contain the same task more than once. All reports must use appropriate filters by setting [[taskroot]], [[hidetask]] or [[rolluptask]] to ensure that no tasks are contained more than once in the report. EOT ) pattern(%w( !journalEntry )) pattern(%w( _note $STRING ), lambda { @property.set('note', newRichText(@val[1], @sourceFileInfo[1])) }) doc('note.task', <<'EOT' Attach a note to the task. This is usually a more detailed specification of what the task is about. EOT ) pattern(%w( !purge )) pattern(%w( _supplement !supplementTask !taskBody ), lambda { @property = @idStack.pop }) doc('supplement.task', <<'EOT' The supplement keyword provides a mechanism to add more attributes to already defined tasks. The additional attributes must obey the same rules as in regular task definitions and must be enclosed by curly braces. This construct is primarily meant for situations where the information about a task is split over several files. E. g. the vacation dates for the resources may be in a separate file that was generated by some other tool. EOT ) example('Supplement', 'task') pattern(%w( !task )) pattern(%w( !taskScenarioAttributes )) pattern(%w( !scenarioIdCol !taskScenarioAttributes ), lambda { @scenarioIdx = 0 }) # Other attributes will be added automatically. end def rule_taskBody optionsRule('taskAttributes') end def rule_taskBooking pattern(%w( !taskBookingHeader !bookingBody ), lambda { unless @project.scenario(@scenarioIdx).get('ownbookings') error('no_own_task_booking', "The scenario #{@project.scenario(@scenarioIdx).fullId} " + 'inherits its bookings from the tracking ' + 'scenario. You cannot specificy additional bookings for it.') end @val[0].task.addBooking(@scenarioIdx, @val[0]) }) end def rule_taskBookingHeader pattern(%w( !resourceId !valIntervals ), lambda { checkBooking(@property, @val[0]) @booking = Booking.new(@val[0], @property, @val[1]) @booking.sourceFileInfo = @sourceFileInfo[0] @booking }) end def rule_taskDep pattern(%w( !taskDepHeader !taskDepBody ), lambda { @val[0] }) end def rule_taskDepAttributes optional repeatable pattern(%w( _gapduration !intervalDuration ), lambda { @taskDependency.gapDuration = @val[1] }) doc('gapduration', <<'EOT' Specifies the minimum required gap between the start or end of a preceding task and the start of this task, or the start or end of a following task and the end of this task. This is calendar time, not working time. 7d means one week. EOT ) pattern(%w( _gaplength !nonZeroWorkingDuration ), lambda { @taskDependency.gapLength = @val[1] }) doc('gaplength', <<'EOT' Specifies the minimum required gap between the start or end of a preceding task and the start of this task, or the start or end of a following task and the end of this task. This is working time, not calendar time. 7d means 7 working days, not one week. Whether a day is considered a working day or not depends on the defined working hours and global leaves. EOT ) pattern(%w( _onend ), lambda { @taskDependency.onEnd = true }) doc('onend', <<'EOT' The target of the dependency is the end of the task. EOT ) pattern(%w( _onstart ), lambda { @taskDependency.onEnd = false }) doc('onstart', <<'EOT' The target of the dependency is the start of the task. EOT ) end def rule_taskDepBody optionsRule('taskDepAttributes') end def rule_taskDepHeader pattern(%w( !taskDepId ), lambda { @taskDependency = TaskDependency.new(@val[0], true) }) end def rule_taskDepId singlePattern('$ABSOLUTE_ID') arg(0, 'ABSOLUTE ID', <<'EOT' A reference using the full qualified ID of a task. The IDs of all enclosing parent tasks must be prepended to the task ID and separated with a dot, e.g. ''''proj.plan.doc''''. EOT ) singlePattern('$ID') arg(0, 'ID', 'Just the ID of the task without any parent IDs.') pattern(%w( !relativeId ), lambda { task = @property id = @val[0] while task && id[0] == ?! id = id.slice(1, id.length) task = task.parent end error('too_many_bangs', "Too many '!' for relative task in this context.", @sourceFileInfo[0], @property) if id[0] == ?! if task task.fullId + '.' + id else id end }) arg(0, 'RELATIVE ID', <<'EOT' A relative task ID always starts with one or more exclamation marks and is followed by a task ID. Each exclamation mark lifts the scope where the ID is looked for to the enclosing task. The ID may contain some of the parent IDs separated by dots, e. g. ''''!!plan.doc''''. EOT ) end def rule_taskDepList pattern(%w( !taskDep !moreDepTasks ), lambda { [ @val[0] ] + (@val[1].nil? ? [] : @val[1]) }) end def rule_taskHeader pattern(%w( _task !optionalID $STRING ), lambda { if @property.nil? && !@taskprefix.empty? @property = @project.task(@taskprefix) end if @val[1] id = (@property ? @property.fullId + '.' : '') + @val[1] if @project.task(id) error('task_exists', "Task #{id} has already been defined.", @sourceFileInfo[0]) end end @property = Task.new(@project, @val[1], @val[2], @property) @property['projectid', 0] = @projectId @property.sourceFileInfo = @sourceFileInfo[0] @property.inheritAttributes @scenarioIdx = 0 }) arg(2, 'name', 'The name of the task') end def rule_taskId pattern(%w( !taskIdUnverifd ), lambda { id = @val[0] if @property && @property.is_a?(Task) # In case we have a nested supplement, we need to prepend the parent ID. id = @property.fullId + '.' + id else id = @taskprefix + '.' + id unless @taskprefix.empty? end if (task = @project.task(id)).nil? error('unknown_task', "Unknown task #{id}", @sourceFileInfo[0]) end task }) end def rule_taskIdUnverifd singlePattern('$ABSOLUTE_ID') singlePattern('$ID') end def rule_taskList listRule('moreTasks', '!absoluteTaskId') end def rule_taskPeriod pattern(%w( _period !valInterval), lambda { @property['start', @scenarioIdx] = @val[1].start @property['end', @scenarioIdx] = @val[1].end }) doc('period.task', <<'EOT' This property is a shortcut for setting the start and end property at the same time. In contrast to using these, it does not change the scheduling direction. EOT ) end def rule_taskPred pattern(%w( !taskPredHeader !taskDepBody ), lambda { @val[0] }) end def rule_taskPredHeader pattern(%w( !taskDepId ), lambda { @taskDependency = TaskDependency.new(@val[0], false) }) end def rule_taskPredList pattern(%w( !taskPred !morePredTasks ), lambda { [ @val[0] ] + (@val[1].nil? ? [] : @val[1]) }) end def rule_taskReport pattern(%w( !taskReportHeader !reportBody ), lambda { @property = @property.parent }) doc('taskreport', <<'EOT' The report lists tasks and their respective values in a table. To reduce the list of included tasks, you can use the [[hidetask]], [[rolluptask]] or [[taskroot]] attributes. The order of the tasks can be controlled with [[sorttasks]]. If the first sorting criteria is tree sorting, the parent tasks will always be included to form the tree. Tree sorting is the default. You need to change it if you do not want certain parent tasks to be included in the report. By default, all the resources that are allocated to each task are hidden, but they can be listed as well. Use the [[hideresource]] attribute to select which resources should be included. EOT ) example('HtmlTaskReport') end def rule_taskReportHeader pattern(%w( _taskreport !optionalID !reportName ), lambda { newReport(@val[1], @val[2], :taskreport, @sourceFileInfo[0]) do unless @property.modified?('columns') # Set the default columns for this report. %w( bsi name start end effort chart ).each do |col| @property.get('columns') << TableColumnDefinition.new(col, columnTitle(col)) end end # Show all tasks, sorted by tree, start-up, seqno-up. unless @property.modified?('hideTask') @property.set('hideTask', LogicalExpression.new(LogicalOperation.new(0))) end unless @property.modified?('sortTasks') @property.set('sortTasks', [ [ 'tree', true, -1 ], [ 'start', true, 0 ], [ 'seqno', true, -1 ] ]) end # Show no resources, but set sorting to id-up. unless @property.modified?('hideResource') @property.set('hideResource', LogicalExpression.new(LogicalOperation.new(1))) end unless @property.modified?('sortResources') @property.set('sortResources', [ [ 'id', true, -1 ] ]) end end }) end def rule_taskScenarioAttributes pattern(%w( _account $ID )) level(:removed) also('chargeset') doc('account.task', '') pattern(%w( !allocate )) pattern(%w( _booking !taskBooking )) doc('booking.task', <<'EOT' The booking attribute can be used to report actually completed work. A task with bookings must be [[scheduling|scheduled]] in ''''ASAP'''' mode. If the scenario is not the [[trackingscenario|tracking scenario]] or derived from it, the scheduler will not allocate resources prior to the current date or the date specified with [[now]] when a task has at least one booking. Bookings are only valid in the scenario they have been defined in. They will in general not be passed to any other scenario. If you have defined a [[trackingscenario|tracking scenario]], the bookings of this scenario will be passed to all the derived scenarios of the tracking scenario. The sloppy attribute can be used when you want to skip non-working time or other allocations automatically. If it's not given, all bookings must only cover working time for the resource. The booking attributes is designed to capture the exact amount of completed work. This attribute is not really intended to specify completed effort by hand. Usually, booking statements are generated by [[export]] reports. The [[sloppy.booking|sloppy]] and [[overtime.booking|overtime]] attributes are only kludge for users who want to write them manually. Bookings can be used to report already completed work by specifying the exact time intervals a certain resource has worked on this task. Bookings can be defined in the task or resource context. If you move tasks around very often, put your bookings in the task context. EOT ) also(%w( booking.resource )) example('Booking') pattern(%w( _charge !number !chargeMode ), lambda { checkContainer('charge') if @property['chargeset', @scenarioIdx].empty? error('task_without_chargeset', 'The task does not have a chargeset defined.', @sourceFileInfo[0], @property) end case @val[2] when 'onstart' mode = :onStart amount = @val[1] when 'onend' mode = :onEnd amount = @val[1] when 'perhour' mode = :perDiem amount = @val[1] * 24 when 'perday' mode = :perDiem amount = @val[1] when 'perweek' mode = :perDiem amount = @val[1] / 7.0 end @property['charge', @scenarioIdx] += [ Charge.new(amount, mode, @property, @scenarioIdx) ] }) doc('charge', <<'EOT' Specify a one-time or per-period charge to a certain account. The charge can occur at the start of the task, at the end of it, or continuously over the duration of the task. The accounts to be charged are determined by the [[chargeset]] setting of the task. EOT ) arg(1, 'amount', 'The amount to charge') pattern(%w( !chargeset )) pattern(%w( _complete !number), lambda { if @val[1] < 0.0 || @val[1] > 100.0 error('task_complete', "Complete value must be between 0 and 100", @sourceFileInfo[1], @property) end @property['complete', @scenarioIdx] = @val[1] }) doc('complete', <<'EOT' Specifies what percentage of the task is already completed. This can be useful for simple progress tracking like in a TODO list. The provided completion degree is used for the ''''complete'''' and ''''gauge'''' columns in reports. Reports with calendar elements may show the completed part of the task in a different color. The completion percentage has no impact on the scheduler. It's meant for documentation purposes only. EOT ) example('Complete', '1') arg(1, 'percent', 'The percent value. It must be between 0 and 100.') pattern(%w( _depends !taskDepList ), lambda { checkContainer('depends') @property['depends', @scenarioIdx] += @val[1] begin @property['forward', @scenarioIdx] = true rescue AttributeOverwrite end }) doc('depends', <<'EOT' Specifies that the task cannot start before the specified tasks have been finished. By using the 'depends' attribute, the scheduling policy is automatically set to ASAP. If both depends and precedes are used, the last policy counts. EOT ) example('Depends1') pattern(%w( _duration !calendarDuration ), lambda { setDurationAttribute('duration', @val[1]) }) doc('duration', <<'EOT' Specifies the time the task should last. This is calendar time, not working time. 7d means one week. If resources are specified they are allocated when available. Availability of resources has no impact on the duration of the task. It will always be the specified duration. Tasks may not have subtasks if this attribute is used. Setting this attribute will reset the [[effort]] and [[length]] attributes. EOT ) example('Durations') also(%w( effort length )) pattern(%w( _effort !workingDuration ), lambda { if @val[1] <= 0 error('effort_zero', "Effort value must at least as large as the " + "timing resolution " + "(#{@project['scheduleGranularity'] / 60}min).", @sourceFileInfo[1], @property) end setDurationAttribute('effort', @val[1]) }) doc('effort', <<'EOT' Specifies the effort needed to complete the task. An effort of ''''6d'''' (6 resource-days) can be done with 2 full-time resources in 3 working days. The task will not finish before the allocated resources have contributed the specified effort. Hence the duration of the task will depend on the availability of the allocated resources. The specified effort value must be at least as large as the [[timingresolution]]. WARNING: In almost all real world projects effort is not the product of time and resources. This is only true if the task can be partitioned without adding any overhead. For more information about this read ''The Mythical Man-Month'' by Frederick P. Brooks, Jr. Tasks may not have subtasks if this attribute is used. Setting this attribute will reset the [[duration]] and [[length]] attributes. A task with an effort value cannot be a [[milestone]]. EOT ) example('Durations') also(%w( duration length )) pattern(%w( _effortdone !workingDuration ), lambda { @property['effortdone', @scenarioIdx] = @val[1] }) level(:beta) doc('effortdone', <<'EOT' Specifies how much effort of the task has already been completed. This can only be used for [[effort]] based tasks and only if the task is scheduled in [[schedulingmode|projection mode]]. No [[booking.task|bookings]] must be specified for the scenario. TaskJuggler is unable to create exact bookings for the time period before the current date. All effort values prior to the current date will be reported as zero. This attribute forces the task to be scheduled in [[scheduling|ASAP mode]]. The task must have a predetermined [[start]] date. EOT ) also(%w( effort effortleft schedulingmode trackingscenario )) pattern(%w( _effortleft !workingDuration ), lambda { @property['effortleft', @scenarioIdx] = @val[1] }) level(:beta) doc('effortleft', <<'EOT' Specifies how much effort of the task is still not completed. This can only be used for [[effort]] based tasks and only if the task is scheduled in [[schedulingmode|projection mode]]. No [[booking.task|bookings]] must be specified for the scenario. TaskJuggler is unable to create exact bookings for the time period before the current date. All effort values prior to the current date will be reported as zero. This attribute forces the task to be scheduled in [[scheduling|ASAP mode]]. The task must have a predetermined [[start]] date. EOT ) also(%w( effort effortdone schedulingmode trackingscenario )) pattern(%w( _end !valDate ), lambda { @property['end', @scenarioIdx] = @val[1] begin @property['forward', @scenarioIdx] = false rescue AttributeOverwrite end }) doc('end', <<'EOT' The end attribute provides a guideline to the scheduler when the task should end. It will never end later, but it may end earlier when allocated resources are not available that long. When an end date is provided for a container task, it will be passed down to ALAP tasks that don't have a well defined end criteria. Setting an end date will implicitly set the scheduling policy for this task to ALAP. EOT ) example('Export', '1') pattern(%w( _endcredit !number ), lambda { @property['charge', @scenarioIdx] = @property['charge', @scenarioIdx] + [ Charge.new(@val[1], :onEnd, @property, @scenarioIdx) ] }) level(:deprecated) doc('endcredit', <<'EOT' Specifies an amount that is credited to the accounts specified by the [[chargeset]] attributes at the moment the task ends. EOT ) also('charge') example('Account', '1') pattern(%w( !flags )) doc('flags.task', <<'EOT' Attach a set of flags. The flags can be used in logical expressions to filter properties from the reports. EOT ) pattern(%w( !fail )) pattern(%w( _length !nonZeroWorkingDuration ), lambda { setDurationAttribute('length', @val[1]) }) doc('length', <<'EOT' Specifies the duration of this task as working time, not calendar time. 7d means 7 working days, or 7 times 8 hours (assuming default settings), not one week. A task with a length specification may have resource allocations. Resources are allocated when they are available. There is no guarantee that the task will get any resources allocated. The availability of resources has no impact on the duration of the task. A time slot where none of the specified resources is available is still considered working time, if there is no global vacation and global working hours are defined accordingly. For the length calculation, the global working hours and the global leaves matter unless the task has [[shifts.task|shifts]] assigned. In the latter case the working hours and leaves of the shift apply for the specified period to determine if a slot is working time or not. If a resource has additional working hours defined, it's quite possible that a task with a length of 5d will have an allocated effort larger than 40 hours. Resource working hours only have an impact on whether an allocation is made or not for a particular time slot. They don't effect the resulting duration of the task. Tasks may not have subtasks if this attribute is used. Setting this attribute will reset the [[duration]], [[effort]] and [[milestone]] attributes. EOT ) also(%w( duration effort )) pattern(%w( !limits ), lambda { checkContainer('limits') @property['limits', @scenarioIdx] = @val[0] }) doc('limits.task', <<'EOT' Set per-interval allocation limits for the task. This setting affects all allocations for this task. EOT ) example('Limits-1', '2') pattern(%w( _maxend !valDate ), lambda { @property['maxend', @scenarioIdx] = @val[1] }) doc('maxend', <<'EOT' Specifies the maximum wanted end time of the task. The value is not used during scheduling, but is checked after all tasks have been scheduled. If the end of the task is later than the specified value, then an error is reported. EOT ) pattern(%w( _maxstart !valDate ), lambda { @property['maxstart', @scenarioIdx] = @val[1] }) doc('maxstart', <<'EOT' Specifies the maximum wanted start time of the task. The value is not used during scheduling, but is checked after all tasks have been scheduled. If the start of the task is later than the specified value, then an error is reported. EOT ) pattern(%w( _milestone ), lambda { setDurationAttribute('milestone') }) doc('milestone', <<'EOT' Turns the task into a special task that has no duration. You may not specify a duration, length, effort or subtask for a milestone task. A task that only has a start or an end specification and no duration specification, inherited start or end dates, no dependencies or sub tasks, will be recognized as milestone automatically. EOT ) pattern(%w( _minend !valDate ), lambda { @property['minend', @scenarioIdx] = @val[1] }) doc('minend', <<'EOT' Specifies the minimum wanted end time of the task. The value is not used during scheduling, but is checked after all tasks have been scheduled. If the end of the task is earlier than the specified value, then an error is reported. EOT ) pattern(%w( _minstart !valDate ), lambda { @property['minstart', @scenarioIdx] = @val[1] }) doc('minstart', <<'EOT' Specifies the minimum wanted start time of the task. The value is not used during scheduling, but is checked after all tasks have been scheduled. If the start of the task is earlier than the specified value, then an error is reported. EOT ) pattern(%w( _startcredit !number ), lambda { @property['charge', @scenarioIdx] += [ Charge.new(@val[1], :onStart, @property, @scenarioIdx) ] }) level(:deprecated) doc('startcredit', <<'EOT' Specifies an amount that is credited to the account specified by the [[chargeset]] attributes at the moment the task starts. EOT ) also('charge') pattern(%w( !taskPeriod )) pattern(%w( _precedes !taskPredList ), lambda { checkContainer('precedes') @property['precedes', @scenarioIdx] += @val[1] begin @property['forward', @scenarioIdx] = false rescue AttributeOverwrite end }) doc('precedes', <<'EOT' Specifies that the tasks with the specified IDs cannot start before this task has been finished. If multiple IDs are specified, they must be separated by commas. IDs must be either global or relative. A relative ID starts with a number of '!'. Each '!' moves the scope to the parent task. Global IDs do not contain '!', but have IDs separated by dots. By using the 'precedes' attribute, the scheduling policy is automatically set to ALAP. If both depends and precedes are used within a task, the last policy counts. EOT ) pattern(%w( _priority $INTEGER ), lambda { if @val[1] < 0 || @val[1] > 1000 error('task_priority', "Priority must have a value between 0 and 1000", @sourceFileInfo[1], @property) end @property['priority', @scenarioIdx] = @val[1] }) doc('priority', <<'EOT' Specifies the priority of the task. A task with higher priority is more likely to get the requested resources. The default priority value of all tasks is 500. Don't confuse the priority of a tasks with the importance or urgency of a task. It only increases the chances that the task gets the requested resources. It does not mean that the task happens earlier, though that is usually the effect you will see. It also does not have any effect on tasks that don't have any resources assigned (e.g. milestones). For milestones, it will raise or lower the chances that tasks leading up the milestone will get their resources over tasks with equal priority that compete for the same resources. This attribute is inherited by subtasks if specified prior to the definition of the subtask. EOT ) arg(1, 'value', 'Priority value (1 - 1000)') example('Priority') pattern(%w( _projectid $ID ), lambda { unless @project['projectids'].include?(@val[1]) error('unknown_projectid', "Unknown project ID #{@val[1]}", @sourceFileInfo[1]) end begin @property['projectid', @scenarioIdx] = @val[1] rescue AttributeOverwrite # This attribute always overwrites the implicitly provided ID. end }) doc('projectid.task', <<'EOT' In larger projects it may be desirable to work with different project IDs for parts of the project. This attribute assignes a new project ID to this task and all subsequently defined sub tasks. The project ID needs to be declared first using [[projectid]] or [[projectids]]. EOT ) pattern(%w( _responsible !resourceList ), lambda { @property['responsible', @scenarioIdx] += @val[1] @property['responsible', @scenarioIdx].uniq! }) doc('responsible', <<'EOT' The ID of the resource that is responsible for this task. This value is for documentation purposes only. It's not used by the scheduler. EOT ) pattern(%w( _scheduled ), lambda { @property['scheduled', @scenarioIdx] = true }) doc('scheduled', <<'EOT' It specifies that the task can be ignored for scheduling in the scenario. This option only makes sense if you provide all resource [[booking.resource|bookings]] manually. Without booking statements, the task will be reported with 0 effort and no resources assigned. If the task is not a milestone, has no effort, length or duration criteria, the start and end date will be derived from the first and last booking in case those dates are not supplied. EOT ) pattern(%w( _scheduling !schedulingDirection ), lambda { if @val[1] == 'alap' begin @property['forward', @scenarioIdx] = false rescue AttributeOverwrite end elsif @val[1] == 'asap' begin @property['forward', @scenarioIdx] = true rescue AttributeOverwrite end end }) doc('scheduling', <<'EOT' Specifies the scheduling policy for the task. A task can be scheduled from start to end (As Soon As Possible, ASAP) or from end to start (As Late As Possible, ALAP). A task can be scheduled from start to end (ASAP mode) when it has a hard (start) or soft (depends) criteria for the start time. A task can be scheduled from end to start (ALAP mode) when it has a hard (end) or soft (precedes) criteria for the end time. Some task attributes set the scheduling policy implicitly. This attribute can be used to explicitly set the scheduling policy of the task to a certain direction. To avoid it being overwritten again by an implicit attribute, this attribute should always be the last attribute of the task. A random mixture of ASAP and ALAP tasks can have unexpected side effects on the scheduling of the project. It increases significantly the scheduling complexity and results in much longer scheduling times. Especially in projects with many hundreds of tasks, the scheduling time of a project with a mixture of ASAP and ALAP times can be 2 to 10 times longer. When the project contains chains of ALAP and ASAP tasks, the tasks further down the dependency chain will be served much later than other non-chained tasks, even when they have a much higher priority. This can result in situations where high priority tasks do not get their resources, even though the parallel competing tasks have a much lower priority. ALAP tasks may not have [[booking.task|bookings]], since the first booked slot determines the start date of the task and prevents it from being scheduled from end to start. As a general rule, try to avoid ALAP tasks whenever possible. Have a close eye on tasks that have been switched implicitly to ALAP mode because the end attribute comes after the start attribute. EOT ) pattern(%w( _schedulingmode !schedulingMode ), lambda { @property['projectionmode', @scenarioIdx] = (@val[1] == 'projection') }) level(:beta) doc('schedulingmode', <<'EOT' The scheduling mode controls how the scheduler assigns resources to this task. In planning mode, resources are allocated before and after the current date. In projection mode, resources are only allocated after the current date. In this mode, any resource activity prior to the current date must be provided with [[booking.task|bookings]]. Alternatively, the [[effortdone]] or [[effortleft]] attribute can be used. This scheduling mode is automatically set to projection mode when the [[trackingscenario]] is set. However, the setting can be overwritten by using this attribute. EOT ) pattern(%w( !taskShiftAssignments !shiftAssignments ), lambda { checkContainer('shift') # Set same value again to set the 'provided' state for the attribute. begin @property['shifts', @scenarioIdx] = @shiftAssignments rescue AttributeOverwrite # Multiple shift assignments are a common idiom, so don't warn about # them. end @shiftAssignments = nil }) level(:deprecated) doc('shift.task', <<'EOT' This keyword has been deprecated. Please use [[shifts.task|shifts (task)]] instead. EOT ) also('shifts.task') pattern(%w( !taskShiftsAssignments !shiftAssignments ), lambda { checkContainer('shifts') begin @property['shifts', @scenarioIdx] = @shiftAssignments rescue AttributeOverwrite # Multiple shift assignments are a common idiom, so don't warn about # them. end @shiftAssignments = nil }) doc('shifts.task', <<'EOT' Limits the working time for this task during the specified interval to the working hours of the given shift. Multiple shifts can be defined, but shift intervals may not overlap. This is an additional working time restriction on top of the working hours of the allocated resources. It does not replace the resource working hour restrictions. For a resource to be assigned to a time slot, both the respective task shift as well as the resource working hours must declare the time slot as duty slot. EOT ) pattern(%w( _start !valDate), lambda { @property['start', @scenarioIdx] = @val[1] begin @property['forward', @scenarioIdx] = true rescue AttributeOverwrite end }) doc('start', <<'EOT' The start attribute provides a guideline to the scheduler when the task should start. It will never start earlier, but it may start later when allocated resources are not available immediately. When a start date is provided for a container task, it will be passed down to ASAP tasks that don't have a well defined start criteria. Setting a start date will implicitly set the scheduling policy for this task to ASAP. EOT ) also(%w( end period.task maxstart minstart scheduling )) pattern(%w( !warn )) # Other attributes will be added automatically. end def rule_taskShiftAssignments pattern(%w( _shift ), lambda { @shiftAssignments = @property['shifts', @scenarioIdx] }) end def rule_taskShiftsAssignments pattern(%w( _shifts ), lambda { @shiftAssignments = @property['shifts', @scenarioIdx] }) end def rule_textReport pattern(%w( !textReportHeader !reportBody ), lambda { @property = @property.parent }) doc('textreport', <<'EOT' This report consists of 5 [[Rich_Text_Attributes|Rich Text]] sections, a header, a center section with a left and right margin and a footer. The sections may contain the output of other defined reports. EOT ) example('textreport') end def rule_textReportHeader pattern(%w( _textreport !optionalID !reportName ), lambda { newReport(@val[1], @val[2], :textreport, @sourceFileInfo[0]) }) end def rule_timeformat pattern(%w( _timeformat $STRING ), lambda { @val[1] }) doc('timeformat', <<'EOT' Determines how time specifications in reports look like. EOT ) arg(1, 'format', <<'EOT' Ordinary characters placed in the format string are copied without conversion. Conversion specifiers are introduced by a `%' character, and are replaced as follows: * ''''%a'''' The abbreviated weekday name according to the current locale. * ''''%A'''' The full weekday name according to the current locale. * ''''%b'''' The abbreviated month name according to the current locale. * ''''%B'''' The full month name according to the current locale. * ''''%c'''' The preferred date and time representation for the current locale. * ''''%C'''' The century number (year/100) as a 2-digit integer. (SU) * ''''%d'''' The day of the month as a decimal number (range 01 to 31). * ''''%e'''' Like ''''%d'''', the day of the month as a decimal number, but a leading zero is replaced by a space. (SU) * ''''%E'''' Modifier: use alternative format, see below. (SU) * ''''%F'''' Equivalent to ''''%Y-%m-%d'''' (the ISO 8601 date format). (C99) * ''''%G'''' The ISO 8601 year with century as a decimal number. The 4-digit year corresponding to the ISO week number (see %V). This has the same format and value as ''''%y'''', except that if the ISO week number belongs to the previous or next year, that year is used instead. (TZ) * ''''%g'''' Like %G, but without century, i.e., with a 2-digit year (00-99). (TZ) * ''''%h'''' Equivalent to ''''%b''''. (SU) * ''''%H'''' The hour as a decimal number using a 24-hour clock (range 00 to 23). * ''''%I'''' The hour as a decimal number using a 12-hour clock (range 01 to 12). * ''''%j'''' The day of the year as a decimal number (range 001 to 366). * ''''%k'''' The hour (24-hour clock) as a decimal number (range 0 to 23); single digits are preceded by a blank. (See also ''''%H''''.) (TZ) * ''''%l'''' The hour (12-hour clock) as a decimal number (range 1 to 12); single digits are preceded by a blank. (See also ''''%I''''.) (TZ) * ''''%m'''' The month as a decimal number (range 01 to 12). * ''''%M'''' The minute as a decimal number (range 00 to 59). * ''''%n'''' A newline character. (SU) * ''''%O'''' Modifier: use alternative format, see below. (SU) * ''''%p'''' Either 'AM' or 'PM' according to the given time value, or the corresponding strings for the current locale. Noon is treated as `pm' and midnight as 'am'. * ''''%P'''' Like %p but in lowercase: 'am' or 'pm' or ''''%a'''' corresponding string for the current locale. (GNU) * ''''%r'''' The time in a.m. or p.m. notation. In the POSIX locale this is equivalent to ''''%I:%M:%S %p''''. (SU) * ''''%R'''' The time in 24-hour notation (%H:%M). (SU) For a version including the seconds, see ''''%T'''' below. * ''''%s'''' The number of seconds since the Epoch, i.e., since 1970-01-01 00:00:00 UTC. (TZ) * ''''%S'''' The second as a decimal number (range 00 to 61). * ''''%t'''' A tab character. (SU) * ''''%T'''' The time in 24-hour notation (%H:%M:%S). (SU) * ''''%u'''' The day of the week as a decimal, range 1 to 7, Monday being 1. See also ''''%w''''. (SU) * ''''%U'''' The week number of the current year as a decimal number, range 00 to 53, starting with the first Sunday as the first day of week 01. See also ''''%V'''' and ''''%W''''. * ''''%V'''' The ISO 8601:1988 week number of the current year as a decimal number, range 01 to 53, where week 1 is the first week that has at least 4 days in the current year, and with Monday as the first day of the week. See also ''''%U'''' and ''''%W''''. %(SU) * ''''%w'''' The day of the week as a decimal, range 0 to 6, Sunday being 0. See also ''''%u''''. * ''''%W'''' The week number of the current %year as a decimal number, range 00 to 53, starting with the first Monday as the first day of week 01. * ''''%x'''' The preferred date representation for the current locale without the time. * ''''%X'''' The preferred time representation for the current locale without the date. * ''''%y'''' The year as a decimal number without a century (range 00 to 99). * ''''%Y'''' The year as a decimal number including the century. * ''''%z'''' The time zone as hour offset from GMT. Required to emit RFC822-conformant dates (using ''''%a, %d %%b %Y %H:%M:%S %%z''''). (GNU) * ''''%Z'''' The time zone or name or abbreviation. * ''''%+'''' The date and time in date(1) format. (TZ) * ''''%%'''' A literal ''''%'''' character. Some conversion specifiers can be modified by preceding them by the E or O modifier to indicate that an alternative format should be used. If the alternative format or specification does not exist for the current locale, the behavior will be as if the unmodified conversion specification were used. (SU) The Single Unix Specification mentions %Ec, %EC, %Ex, %%EX, %Ry, %EY, %Od, %Oe, %OH, %OI, %Om, %OM, %OS, %Ou, %OU, %OV, %Ow, %OW, %Oy, where the effect of the O modifier is to use alternative numeric symbols (say, Roman numerals), and that of the E modifier is to use a locale-dependent alternative representation. This documentation of the timeformat attribute has been taken from the man page of the GNU strftime function. EOT ) end def rule_timeInterval pattern([ '$TIME', '_-', '$TIME' ], lambda { if @val[0] >= @val[2] error('time_interval', "End time of interval must be larger than start time", @sourceFileInfo[0]) end [ @val[0], @val[2] ] }) end def rule_timeSheet pattern(%w( !timeSheetHeader !timeSheetBody ), lambda { @timeSheet }) doc('timesheet', <<'EOT' A time sheet record can be used to capture the current status of the tasks assigned to a specific resource and the achieved progress for a given period of time. The status is assumed to be for the end of this time period. There must be a separate time sheet record for each resource per period. Different resources can use different reporting periods and reports for the same resource may have different reporting periods as long as they don't overlap. For the time after the last time sheet, TaskJuggler will project the result based on the plan data. For periods without a time sheet record prior to the last record for this resource, TaskJuggler assumes that no work has been done. The work is booked for the scenario specified by [[trackingscenario]]. The intended use for time sheets is to have all resources report a time sheet every day, week or month. All time sheets can be added to the project plan. The status information is always used to determine the current status of the project. The [[work]], [[remaining]] and [[end.timesheet|end]] attributes are ignored if there are also [[booking.task|bookings]] for the resource in the time sheet period. The non-ignored attributes of the time sheets will be converted into [[booking.task|booking]] statements internally. These bookings can then be [[export|exported]] into a file which can then be added to the project again. This way, you can use time sheets to incrementally record progress of your project. There is a possibility that time sheets conflict with other data in the plan. In case TaskJuggler cannot automatically resolve them, these conflicts have to be manually resolved by either changing the plan or the time sheet. The status messages are interpreted as [[journalentry|journal entries]]. The alert level will be evaluated and the current state of the project can be put into a dashboard using the ''''alert'''' and ''''alertmessage'''' [[columnid| columns]]. Currently, the provided effort values and dates are not yet used to automatically update the plan data. This feature will be added in future versions. EOT ) example('TimeSheet1', '1') end def rule_timeSheetAttributes optional repeatable pattern(%w( !tsNewTaskHeader !tsTaskBody ), lambda { @property = nil @timeSheetRecord = nil }) doc('newtask', <<'EOT' The keyword can be used to request a new task to the project. If the task ID requires further parent tasks that don't exist yet, these tasks will be requested as well. If the task exists already, an error will be generated. The newly requested task can be used immediately to report progress and status against it. These tasks will not automatically be added to the project plan. The project manager has to manually create them after reviewing the request during the time sheet reviews. EOT ) example('TimeSheet1', '3') pattern(%w( _shift !shiftId ), lambda { #TODO }) doc('shift.timesheet', <<'EOT' Specifies an alternative [[shift]] for the time sheet period. This shift will override any existing working hour definitions for the resource. It will not override already declared [[leaves]] though. The primary use of this feature is to let the resources report different total work time for the report period. EOT ) pattern(%w( !tsStatus )) pattern(%w( !tsTaskHeader !tsTaskBody ), lambda { @property = nil @timeSheetRecord = nil }) doc('task.timesheet', <<'EOT' Specifies an existing task that progress and status should be reported against. EOT ) example('TimeSheet1', '4') end def rule_timeSheetFile pattern(%w( !timeSheet . ), lambda { @val[0] }) lastSyntaxToken(1) end def rule_timeSheetBody pattern(%w( _{ !timeSheetAttributes _} ), lambda { }) end def rule_timeSheetHeader pattern(%w( _timesheet !resourceId !valIntervalOrDate ), lambda { @sheetAuthor = @val[1] @property = nil unless @sheetAuthor.leaf? error('ts_group_author', 'A resource group cannot file a time sheet', @sourceFileInfo[1]) end unless (scenarioIdx = @project['trackingScenarioIdx']) error('ts_no_tracking_scenario', 'No trackingscenario defined.') end # Currently time sheets are hardcoded for scenario 0. @timeSheet = TimeSheet.new(@sheetAuthor, @val[2], scenarioIdx) @timeSheet.sourceFileInfo = @sourceFileInfo[0] @project.timeSheets << @timeSheet }) end def rule_timeSheetReport pattern(%w( !tsReportHeader !tsReportBody ), lambda { @property = nil }) doc('timesheetreport', <<'EOT' For projects that flow mostly according to plan, TaskJuggler already knows much of the information that should be contained in the time sheets. With this property, you can generate a report that contains drafts of the time sheets for one or more resources. The time sheet drafts will be for the specified report period and the specified [trackingscenario]. EOT ) end def rule_timezone pattern(%w( _timezone !validTimeZone ), lambda{ TjTime.setTimeZone(@val[1]) @project['timezone'] = @val[1] }) doc('timezone', <<'EOT' Sets the default time zone of the project. All dates and times that have no time zones specified will be assumed to be in this time zone. If no time zone is specified for the project, UTC is assumed. The project start and end time are not affected by this setting. They are always considered to be UTC unless specified differently. In case the specified time zone is not hour-aligned with UTC, the [[timingresolution]] will automatically be decreased accordingly. Do not change the timingresolution after you've set the time zone! Changing the time zone will reset the [[workinghours.project|working hours]] to the default times. It's recommended that you declare your working hours after the time zone. EOT ) arg(1, 'zone', <<'EOT' Time zone to use. E. g. 'Europe/Berlin' or 'America/Denver'. Don't use the 3 letter acronyms. See [http://en.wikipedia.org/wiki/List_of_zoneinfo_time_zones Wikipedia] for possible values. EOT ) end def rule_traceReport pattern(%w( !traceReportHeader !reportBody ), lambda { @property = @property.parent }) doc('tracereport', <<'EOT' The trace report works noticeably different than all other TaskJuggler reports. It uses a CSV file to track the values of the selected attributes. Each time ''''tj3'''' is run with the ''''--add-trace'''' option, a new set of values is appended to the CSV file. The first column of the CSV file holds the date when the snapshot was taken. This is either the current date or the ''''now'''' date if provided. There is no need to specify CSV as output format for the report. You can either use these tracked values directly by specifying other report formats or by importing the CSV file into another program. The first column always contains the current date when that table row was added. All subsequent columns can be defined by the user with the [[columns]] attribute. This column set is then repeated for all properties that are not hidden by [[hideaccount]], [[hideresource]] and [[hidetask]]. By default, all properties are excluded. You must provide at least one of the ''''hide...'''' attributes to select the properties you want to have included in the report. Please be aware that the total number of columns is the product of attributes defined with [[columns]] times the number of included properties. Select your values carefully or you will end up with very large reports. The column headers can be customized by using the [[title.column|title]] attribute. When you include multiple properties, these headers are not unique unless you include mini-queries to modify them based on the property the column is representing. You can use the queries ''''<-id->'''', ''''<-name->'''', ''''<-scenario->'''' and ''''<-attribute->''''. ''''<-id->'''' is replaced with the ID of the property, ''''<-name->'''' with the name and so on. You can change the set of tracked values over time. Old values will be preserved and the corresponding columns will be the last ones in the CSV file. When other formats are requested, the CSV file is read in and a report that shows the tracked values over time will be generated. The CSV file may contain all kinds of values that are being tracked. Report formats that don't support a mix of different values will just show the values of the second column. The values in the CSV files are fixed units and cannot be formatted. Effort values are always in resource-days. This allows other software to interpret the file without any need for additional context information. The HTML version generates SVG graphs that are embedded in the HTML page. These graphs are only visible if the web browser supports HTML5. This is true for the latest generation of browsers, but older browsers may not support this format. EOT ) example('TraceReport') end def rule_traceReportHeader pattern(%w( _tracereport !optionalID !reportName ), lambda { newReport(@val[1], @val[2], :tracereport, @sourceFileInfo[0]) do # The top-level always inherits the global timeFormat setting. This is # not desirable in this case, so we ignore this. if (@property.level == 0 && !@property.provided('timeFormat')) || (@property.level > 0 && !@property.modified?('timeFormat')) # CSV readers such of Libre-/OpenOffice can't deal with time zones. We # probably also don't need seconds. @property.set('timeFormat', '%Y-%m-%d-%H:%M') end unless @property.modified?('columns') # Set the default columns for this report. %w( end ).each do |col| @property.get('columns') << TableColumnDefinition.new(col, columnTitle(col)) end end # Hide all accounts. unless @property.modified?('hideAccount') @property.set('hideAccount', LogicalExpression.new(LogicalOperation.new(1))) end unless @property.modified?('sortAccounts') @property.set('sortAccounts', [ [ 'tree', true, -1 ], [ 'seqno', true, -1 ] ]) end # Show all tasks, sorted by tree, start-up, seqno-up. unless @property.modified?('hideTask') @property.set('hideTask', LogicalExpression.new(LogicalOperation.new(0))) end unless @property.modified?('sortTasks') @property.set('sortTasks', [ [ 'tree', true, -1 ], [ 'start', true, 0 ], [ 'seqno', true, -1 ] ]) end # Show no resources, but set sorting to id-up. unless @property.modified?('hideResource') @property.set('hideResource', LogicalExpression.new(LogicalOperation.new(1))) end unless @property.modified?('sortResources') @property.set('sortResources', [ [ 'id', true, -1 ] ]) end end }) end def rule_tsNewTaskHeader pattern(%w( _newtask !taskIdUnverifd $STRING ), lambda { @timeSheetRecord = TimeSheetRecord.new(@timeSheet, @val[1]) @timeSheetRecord.name = @val[2] @timeSheetRecord.sourceFileInfo = @sourceFileInfo[0] }) arg(1, 'task', 'ID of the new task') end def rule_tsReportHeader pattern(%w( _timesheetreport !optionalID $STRING ), lambda { newReport(@val[1], @val[2], :timeSheet, @sourceFileInfo[0]) do @property.set('formats', [ :tjp ]) unless (scenarioIdx = @project['trackingScenarioIdx']) error('ts_no_tracking_scenario', 'You must have a tracking scenario defined to use time sheets.') end @property.set('scenarios', [ scenarioIdx ]) # Show all tasks, sorted by seqno-up. @property.set('hideTask', LogicalExpression.new(LogicalOperation.new(0))) @property.set('sortTasks', [ [ 'seqno', true, -1 ] ]) # Show all resources, sorted by seqno-up. @property.set('hideResource', LogicalExpression.new(LogicalOperation.new(0))) @property.set('sortResources', [ [ 'seqno', true, -1 ] ]) @property.set('loadUnit', :hours) @property.set('definitions', []) end }) arg(2, 'file name', <<'EOT' The name of the time sheet report file to generate. It must end with a .tji extension, or use . to use the standard output channel. EOT ) end def rule_tsReportAttributes optional repeatable pattern(%w( !hideresource )) pattern(%w( !hidetask )) pattern(%w( !reportEnd )) pattern(%w( !reportPeriod )) pattern(%w( !reportStart )) pattern(%w( !sortResources )) pattern(%w( !sortTasks )) end def rule_tsReportBody optionsRule('tsReportAttributes') end def rule_tsStatusAttributes optional repeatable pattern(%w( !details )) pattern(%w( _flags !flagList ), lambda { @val[1].each do |flag| next if @journalEntry.flags.include?(flag) @journalEntry.flags << flag end }) doc('flags.timesheet', <<'EOT' Time sheet entries can have flags attached to them. These can be used to include only entries in a report that have a certain flag. EOT ) pattern(%w( !summary )) end def rule_tsStatusBody optional pattern(%w( _{ !tsStatusAttributes _} )) end def rule_tsStatusHeader pattern(%w( _status !alertLevel $STRING ), lambda { if @val[2].length > 120 error('ts_headline_too_long', "The headline must be 120 or less characters long. This one " + "has #{@val[2].length} characters.", @sourceFileInfo[2]) end if @val[2] == 'Your headline here!' error('ts_no_headline', "'Your headline here!' is not a valid headline", @sourceFileInfo[2]) end @journalEntry = JournalEntry.new(@project['journal'], @timeSheet.interval.end, @val[2], @property || @timeSheet.resource, @sourceFileInfo[0]) @journalEntry.alertLevel = @val[1] @journalEntry.timeSheetRecord = @timeSheetRecord @journalEntry.author = @sheetAuthor @timeSheetRecord.status = @journalEntry if @timeSheetRecord }) end def rule_tsStatus pattern(%w( !tsStatusHeader !tsStatusBody )) doc('status.timesheet', <<'EOT' The status attribute can be used to describe the current status of the task or resource. The content of the status messages is added to the project journal. The status section is optional for tasks that have been worked on less than one day during the report interval. EOT ) arg(2, 'headline', <<'EOT' A short headline for the status. Must be 60 characters or shorter. EOT ) example('TimeSheet1', '4') end def rule_tsTaskAttributes optional repeatable pattern(%w( _end !valDate ), lambda { if @val[1] < @timeSheet.interval.start error('ts_end_too_early', "The expected task end date must be after the start date of " + "this time sheet report.", @sourceFileInfo[1]) end @timeSheetRecord.expectedEnd = @val[1] }) doc('end.timesheet', <<'EOT' The expected end date for the task. This can only be used for duration based tasks. For effort based tasks [[remaining]] has to be used. EOT ) example('TimeSheet1', '5') pattern(%w( _priority $INTEGER ), lambda { priority = @val[1] if priority < 1 || priority > 1000 error('ts_bad_priority', "Priority value #{priority} must be between 1 and 1000.", @sourceFileInfo[1]) end @timeSheetRecord.priority = priority }) doc('priority.timesheet', <<'EOT' The priority is a value between 1 and 1000. It is used to determine the sequence of tasks when converting [[work]] to [[booking.task|bookings]]. Tasks that need to finish earlier in the period should have a high priority, tasks that end later in the period should have a low priority. For tasks that don't get finished in the reported period the priority should be set to 1. EOT ) pattern(%w( _remaining !workingDuration ), lambda { @timeSheetRecord.remaining = @val[1] }) doc('remaining', <<'EOT' The remaining effort for the task. This value is ignored if there are [[booking.task|bookings]] for the resource that overlap with the time sheet period. If there are no bookings, the value is compared with the [[effort]] specification of the task. If there is a mismatch between the accumulated effort specified with bookings, [[work]] and [[remaining]] on one side and the specified [[effort]] on the other, a warning is generated. This attribute can only be used with tasks that are effort based. Duration based tasks need to have an [[end.timesheet|end]] attribute. EOT ) example('TimeSheet1', '6') pattern(%w( !tsStatus )) pattern(%w( _work !workingDurationPercent ), lambda { @timeSheetRecord.work = @val[1] }) doc('work', <<'EOT' The amount of time that the resource has spent with the task during the reported period. This value is ignored when there are [[booking.task|bookings]] for the resource overlapping with the time sheet period. If there are no bookings, TaskJuggler will try to convert the work specification into bookings internally before the actual scheduling is started. Every task listed in the time sheet needs to have a work attribute. The total accumulated work time that is reported must match exactly the total working hours for the resource for that period. If a resource has no vacation during the week that is reported and it has a regular 40 hour work week, exactly 40 hours total or 5 working days have to be reported. EOT ) example('TimeSheet1', '4') end def rule_tsTaskBody pattern(%w( _{ !tsTaskAttributes _} )) end def rule_tsTaskHeader pattern(%w( _task !taskId ), lambda { @property = @val[1] unless @property.leaf? error('ts_task_not_leaf', 'You cannot specify a task that has sub tasks here.', @sourceFileInfo[1], @property) end @timeSheetRecord = TimeSheetRecord.new(@timeSheet, @property) @timeSheetRecord.sourceFileInfo = @sourceFileInfo[0] }) arg(1, 'task', 'ID of an already existing task') end def rule_undefResourceId pattern(%w( $ID ), lambda { (@resourceprefix.empty? ? '' : @resourceprefix + '.') + @val[0] }) arg(0, 'resource', 'The ID of a defined resource') end def rule_vacationName optional pattern(%w( $STRING )) # We just throw the name away arg(0, 'name', 'An optional name or reason for the leave') end def rule_valDate pattern(%w( !date ), lambda { if @val[0] < @project['start'] || @val[0] > @project['end'] error('date_in_range', "Date #{@val[0]} must be within the project time frame " + "#{@project['start']} - #{@project['end']}", @sourceFileInfo[0]) end @val[0] }) end def rule_validTimeZone pattern(%w( $STRING ), lambda { unless TjTime.checkTimeZone(@val[0]) error('bad_time_zone', "#{@val[0]} is not a known time zone", @sourceFileInfo[0]) end @val[0] }) end def rule_valIntervalOrDate pattern(%w( !date !intervalOptionalEnd ), lambda { if @val[1] mode = @val[1][0] endSpec = @val[1][1] if mode == 0 unless @val[0] < endSpec error('start_before_end', "The end date (#{endSpec}) must be " + "after the start date (#{@val[0]}).", @sourceFileInfo[1]) end iv = TimeInterval.new(@val[0], endSpec) else iv = TimeInterval.new(@val[0], @val[0] + endSpec) end else iv = TimeInterval.new(@val[0], @val[0].sameTimeNextDay) end checkInterval(iv) iv }) doc('interval4', <<'EOT' There are three ways to specify a date interval. The first is the most obvious. A date interval consists of a start and end DATE. Watch out for end dates without a time specification! Date specifications are 0 extended. An end date without a time is expanded to midnight that day. So the day of the end date is not included in the interval! The start and end dates must be separated by a hyphen character. In the second form, the end date is omitted. A 24 hour interval is assumed. The third form specifies the start date and an interval duration. The duration must be prefixed by a plus character. The start and end date of the interval must be within the specified project time frame. EOT ) end def rule_valInterval pattern(%w( !date !intervalEnd ), lambda { mode = @val[1][0] endSpec = @val[1][1] if mode == 0 unless @val[0] < endSpec error('start_before_end', "The end date (#{endSpec}) must be after " + "the start date (#{@val[0]}).", @sourceFileInfo[1]) end iv = TimeInterval.new(@val[0], endSpec) else iv = TimeInterval.new(@val[0], @val[0] + endSpec) end checkInterval(iv) iv }) doc('interval1', <<'EOT' There are two ways to specify a date interval. The start and end date must lie within the specified project period. The first is the most obvious. A date interval consists of a start and end DATE. Watch out for end dates without a time specification! Date specifications are 0 extended. An end date without a time is expanded to midnight that day. So the day of the end date is not included in the interval! The start and end dates must be separated by a hyphen character. The second form specifies the start date and an interval duration. The duration must be prefixed by a plus character. EOT ) end def rule_valIntervals listRule('moreValIntervals', '!valIntervalOrDate') end def rule_warn pattern(%w( _warn !logicalExpression ), lambda { begin @property.set('warn', @property.get('warn') + [ @val[1] ]) rescue AttributeOverwrite end }) doc('warn', <<'EOT' The warn attribute adds a [[logicalexpression|logical expression]] to the property. The condition described by the logical expression is checked after the scheduling and a warning is generated if the condition evaluates to true. This attribute is primarily intended for testing purposes. EOT ) end def rule_weekday pattern(%w( _sun ), lambda { 0 }) pattern(%w( _mon ), lambda { 1 }) pattern(%w( _tue ), lambda { 2 }) pattern(%w( _wed ), lambda { 3 }) pattern(%w( _thu ), lambda { 4 }) pattern(%w( _fri ), lambda { 5 }) pattern(%w( _sat ), lambda { 6 }) end def rule_weekDayInterval pattern(%w( !weekday !weekDayIntervalEnd ), lambda { weekdays = Array.new(7, false) if @val[1].nil? weekdays[@val[0]] = true else d = @val[0] loop do weekdays[d] = true break if d == @val[1] d = (d + 1) % 7 end end weekdays }) arg(0, 'weekday', 'Weekday (sun - sat)') end def rule_weekDayIntervalEnd optional pattern([ '_-', '!weekday' ], lambda { @val[1] }) arg(1, 'end weekday', 'Weekday (sun - sat). It is included in the interval.') end def rule_nonZeroWorkingDuration pattern(%w( !workingDuration ), lambda { slots = @val[0] if slots <= 0 error('working_duration_too_small', "Duration values must be at least " + "#{@project['scheduleGranularity'] / 60} minutes " + "(your timingresolution) long.") end slots }) end def rule_workingDuration pattern(%w( !number !durationUnit ), lambda { convFactors = [ 60, # minutes 60 * 60, # hours 60 * 60 * @project['dailyworkinghours'], # days 60 * 60 * @project['dailyworkinghours'] * (@project.weeklyWorkingDays), # weeks 60 * 60 * @project['dailyworkinghours'] * (@project['yearlyworkingdays'] / 12), # months 60 * 60 * @project['dailyworkinghours'] * @project['yearlyworkingdays'] # years ] # The result will always be in number of time slots. (@val[0] * convFactors[@val[1]] / @project['scheduleGranularity']).round.to_i }) arg(0, 'value', 'A floating point or integer number') end def rule_workingDurationPercent pattern(%w( !number !durationUnitOrPercent ), lambda { if @val[1] >= 0 # Absolute value in minutes, hours or days. convFactors = [ 60, # minutes 60 * 60, # hours 60 * 60 * @project['dailyworkinghours'] # days ] # The result will always be in number of time slots. (@val[0] * convFactors[@val[1]] / @project['scheduleGranularity']).round.to_i else # Percentage values are always returned as Float in the rage of 0.0 to # 1.0. if @val[0] < 0.0 || @val[0] > 100.0 error('illegal_percentage', "Percentage values must be between 0 and 100%.", @sourceFileInfo[1]) end @val[0] / 100.0 end }) arg(0, 'value', 'A floating point or integer number') end def rule_workinghours pattern(%w( _workinghours !listOfDays !listOfTimes), lambda { if @property.nil? # We are changing global working hours. wh = @project['workinghours'] else unless (wh = @property['workinghours', @scenarioIdx]) # The property does not have it's own WorkingHours yet. wh = WorkingHours.new(@project['workinghours']) end end wh.timezone = @project['timezone'] begin 7.times { |i| wh.setWorkingHours(i, @val[2]) if @val[1][i] } rescue error('bad_workinghours', $!.message) end if @property # Make sure we actually assign something so the attribute is marked as # set by the user. begin @property['workinghours', @scenarioIdx] = wh rescue AttributeOverwrite # Working hours can be set multiple times. end end }) end def rule_workinghoursProject pattern(%w( !workinghours )) doc('workinghours.project', <<'EOT' Set the default working hours for all subsequent resource definitions. The standard working hours are 9:00am - 12:00am, 1:00pm - 6:00pm, Monday to Friday. The working hours specification limits the availability of resources to certain time slots of week days. These default working hours can be replaced with other working hours for individual resources. EOT ) also(%w( dailyworkinghours workinghours.resource workinghours.shift )) example('Project') end def rule_workinghoursResource pattern(%w( !workinghours )) doc('workinghours.resource', <<'EOT' Set the working hours for a specific resource. The working hours specification limits the availability of resources to certain time slots of week days. EOT ) also(%w( workinghours.project workinghours.shift )) end def rule_workinghoursShift pattern(%w( !workinghours )) doc('workinghours.shift', <<'EOT' Set the working hours for the shift. The working hours specification limits the availability of resources or the activity on a task to certain time slots of week days. The shift working hours will replace the default or resource working hours for the specified time frame when assigning the shift to a resource. In case the shift is used for a task, resources are only assigned during the working hours of this shift and during the working hours of the allocated resource. Allocations only happen when both the task shift and the resource work hours allow work to happen. EOT ) also(%w( workinghours.project workinghours.resource )) end def rule_yesNo pattern(%w( _yes ), lambda { true }) pattern(%w( _no ), lambda { false }) end end end TaskJuggler-3.8.1/lib/taskjuggler/URLParameter.rb000066400000000000000000000012531473026623400216750ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = URLParameter.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'zlib' class TaskJuggler class URLParameter def URLParameter.encode(data) [Zlib::Deflate.deflate(data)].pack('m') end def URLParameter.decode(data) Zlib::Inflate.inflate(data.unpack('m')[0]) end end end TaskJuggler-3.8.1/lib/taskjuggler/UTF8String.rb000066400000000000000000000102171473026623400213070ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = UTF8String.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # KCODE='u' require 'base64' # This is an extension and modification of the standard String class. We do a # lot of UTF-8 character processing in the parser. Ruby 1.8 does not have good # enough UTF-8 support and Ruby 1.9 only handles UTF-8 characters as Strings. # This is very inefficient compared to representing them as Integer objects. # Some of these hacks can be removed once we have switched to 1.9 support # only. class String if RUBY_VERSION < '1.9.0' # Iterate over the String calling the block for each UTF-8 character in # the String. This implementation looks more awkward but is noticeably # faster than the often propagated regexp based implementations. def each_utf8_char c = '' length = 0 each_byte do |b| c << b if length > 0 # subsequent unicode byte if (length -= 1) == 0 # end of unicode character reached yield c c = '' end elsif (b & 0xC0) == 0xC0 # first unicode byte length = -1 while (b & 0x80) != 0 length += 1 b = b << 1 end else # ASCII character yield c c = '' end end end alias old_double_left_angle << # Replacement for the existing << operator that also works for characters # above Integer 255 (UTF-8 characters). def <<(obj) if obj.is_a?(String) || (obj < 256) # In this case we can use the built-in concat. concat(obj) else # UTF-8 characters have a maximum length of 4 byte and no byte is 0. mask = 0xFF000000 pos = 3 while pos >= 0 # Use the built-in concat operator for each byte. concat((obj & mask) >> (8 * pos)) if (obj & mask) != 0 # Move mask and position to the next byte. mask = mask >> 8 pos -= 1 end end end # Return the number of UTF8 characters in the String. We don't override # the built-in length() function here as we don't know who else uses it # for what purpose. def length_utf8 len = 0 each_utf8_char { |c| len += 1 } len end def ljust(len, pad = ' ') return self + pad * (len - length_utf8) if length_utf8 < len self end alias old_reverse reverse # UTF-8 aware version of reverse that replaces the built-in one. def reverse a = [] each_utf8_char { |c| a << c } a.reverse.join end else alias each_utf8_char each_char alias length_utf8 length end def to_quoted_printable [self].pack('M').gsub(/\n/, "\r\n") end def to_base64 Base64.encode64(self) end def unix2dos gsub(/\n/, "\r\n") end # Ensure the String is really UTF-8 encoded and newlines are only \n. If # that's not possible, an Encoding::UndefinedConversionError is raised. def forceUTF8Encoding if RUBY_VERSION < '1.9.0' # Ruby 1.8 really only support 7 bit ASCII well. Only do the line-end # clean-up. gsub(/\r\n/, "\n") else begin # Ensure that the text has LF line ends and is UTF-8 encoded. encode('UTF-8', :universal_newline => true) rescue # The encoding of the String is broken. Find the first broken line and # report it. lineCtr = 1 each_line do |line| begin line.encode('UTF-8') rescue line = line.encode('UTF-8', :invalid => :replace, :undef => :replace, :replace => '') raise Encoding::UndefinedConversionError, "UTF-8 encoding error in line #{lineCtr}: #{line}" end lineCtr += 1 end end end end end TaskJuggler-3.8.1/lib/taskjuggler/UserManual.rb000066400000000000000000000177051473026623400214570ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = UserManual.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'fileutils' require 'taskjuggler/Tj3Config' require 'taskjuggler/RichText/Document' require 'taskjuggler/SyntaxReference' require 'taskjuggler/TjTime' require 'taskjuggler/RichText/FunctionExample' require 'taskjuggler/HTMLElements' class TaskJuggler # This class specializes the RichTextDocument class for the TaskJuggler user # manual. This manual is not only generated from a set of RichTextSnip files, # but also contains the SyntaxReference for the TJP syntax. class UserManual < RichTextDocument include HTMLElements # Create a UserManual object and gather the TJP syntax information. def initialize super # Don't confuse this with RichTextDocument#references @reference = SyntaxReference.new(self) registerFunctionHandler(RichTextFunctionExample.new) @linkTarget = '_top' end def generate(directory) # Directory where to find the manual RichText sources. Must be relative # to lib directory. srcDir = AppConfig.dataDirs('manual')[0] # Directory where to put the generated HTML files. Must be relative to # lib directory. destDir = directory + (directory[-1] == '/' ? '' : '/') # A list of all source files. The order is important. %w( Intro TaskJuggler_2x_Migration Reporting_Bugs Installation How_To_Contribute Getting_Started Tutorial The_TaskJuggler_Syntax Rich_Text_Attributes List_Attributes Software Day_To_Day_Juggling TaskJuggler_Internals fdl ).each do |file| snip = addSnip(File.join(srcDir, file)) snip.cssClass = 'manual' end # Generate the table of contents tableOfContents # Generate the HTML files. generateHTML(destDir) checkInternalReferences FileUtils.cp_r(AppConfig.dataDirs('data/css')[0], destDir) end # Generate the manual in HTML format. _directory_ specifies a directory # where the HTML files should be put. def generateHTML(directory) generateHTMLindex(directory) generateHTMLReference(directory) # The SyntaxReference only generates the reference list when the HTML is # generated. So we have to collect it after the HTML generation. @references.merge!(@reference.internalReferences) super end # Callback function used by the RichTextDocument and KeywordDocumentation # classes to generate the HTML style sheet for the manual pages. def generateStyleSheet XMLElement.new('link', 'rel' => 'stylesheet', 'type' => 'text/css', 'href' => 'css/tjmanual.css') end # Callback function used by the RichTextDocument class to generate the cover # page for the manual. def generateHTMLCover [ DIV.new('align' => 'center', 'style' => 'margin-top:40px; margin-botton:40px') do [ H1.new { "The #{AppConfig.softwareName} User Manual" }, EM.new { 'Project Management beyond Gantt Chart drawing' }, BR.new, B.new do "Copyright (c) #{AppConfig.copyright.join(', ')} " + "by #{AppConfig.authors.join(', ')}" end, BR.new, "Generated on #{TjTime.new.strftime('%Y-%m-%d')}", BR.new, H3.new { "This manual covers #{AppConfig.softwareName} " + "version #{AppConfig.version}." } ] end, BR.new, HR.new, BR.new ] end # Callback function used by the RichTextDocument class to generate the # header for the manual pages. def generateHTMLHeader DIV.new('align' => 'center') do [ H3.new('align' => 'center') do "The #{AppConfig.softwareName} User Manual" end, EM.new('align' => 'center') do 'Project Management beyond Gantt Chart Drawing' end ] end end # Callback function used by the RichTextDocument class to generate the # footer for the manual pages. def generateHTMLFooter DIV.new('align' => 'center', 'style' => 'font-size:10px;') do [ "Copyright (c) #{AppConfig.copyright.join(', ')} by " + "#{AppConfig.authors.join(', ')}.", A.new('href' => AppConfig.contact) do 'TaskJuggler' end, ' is a trademark of Chris Schlaeger.' ] end end # Callback function used by the RichTextDocument and KeywordDocumentation # classes to generate the navigation bars for the manual pages. # _predLabel_: Text for the reference to the previous page. May be nil. # _predURL: URL to the previous page. # _succLabel_: Text for the reference to the next page. May be nil. # _succURL: URL to the next page. def generateHTMLNavigationBar(predLabel, predURL, succLabel, succURL) html = [ BR.new, HR.new ] if predLabel || succLabel # We use a tabel to get the desired layout. html += [ TABLE.new('style' => 'width:90%; margin-left:5%; ' + 'margin-right:5%') do TR.new do [ TD.new('style' => 'text-align:left; width:35%;') do if predLabel # Link to previous page. [ '<< ', A.new('href' => predURL) { predLabel }, ' <<' ] end end, # Link to table of contents TD.new('style' => 'text-align:center; width:30%;') do A.new('href' => 'toc.html') { 'Table Of Contents' } end, TD.new('style' => 'text-align:right; width:35%;') do if succLabel # Link to next page. [ '>> ', A.new('href' => succURL) { succLabel }, ' >>' ] end end ] end end, HR.new ] end html << BR.new html end # Generate the top-level file for the HTML user manual. def generateHTMLindex(directory) html = HTMLDocument.new(:frameset) html.generateHead("The #{AppConfig.softwareName} User Manual", { 'description' => 'A reference and user manual for the ' + 'TaskJuggler project management software.', 'keywords' => 'taskjuggler, manual, reference'}) html.html << FRAMESET.new('cols' => '15%, 85%') do [ FRAMESET.new('rows' => '140,*') do [ FRAME.new('src' => 'alphabet.html', 'name' => 'alphabet'), FRAME.new('src' => 'navbar.html', 'name' => 'navigator') ] end, FRAME.new('src' => 'toc.html', 'name' => 'display') ] end html.write(directory + 'index.html') end private # Create a table of contents that includes both the sections from the # RichText pages as well as the SyntaxReference. def tableOfContents super # Let's call the reference 'Appendix A' @reference.tableOfContents(@toc, 'A') @anchors += @reference.all end # Generate the HTML pages for the syntax reference and a navigation page # with links to all generated pages. def generateHTMLReference(directory) keywords = @reference.all @reference.generateHTMLnavbar(directory, keywords) keywords.each do |keyword| @reference.generateHTMLreference(directory, keyword) end end end end TaskJuggler-3.8.1/lib/taskjuggler/VimSyntax.rb000066400000000000000000000164471473026623400213470ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = VimSyntax.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/SyntaxReference' class TaskJuggler # This class is a generator for vim (http://www.vim.org) TaskJuggler syntax # highlighting files. class VimSyntax # Create a generator object. def initialize # Create a syntax reference for all current keywords. @reference = SyntaxReference.new(nil, true) @properties = [] @attributes = [] @optionBlocks = [] @reference.keywords.each_value do |kw| if kw.isProperty? @properties << kw else @attributes << kw end if !kw.optionalAttributes.empty? @optionBlocks << kw end end @file = nil end # Generate the vim syntax file into _file_. def generate(file) @file = File.open(file, 'w') header setLocal keywords matches regions highlights @file.close end private def header # Generate the header section. Mostly consists of comments and a check # if we have source the syntax file already. @file.write <<"EOT" " Vim syntax file " Language: TaskJuggler " Maintainer: TaskJuggler Developers " Last Change: #{Time.now} " This file was automatically generated by VimSyntax.rb if exists("b:current_syntax") finish endif EOT end def setLocal cinwords = [] @optionBlocks.each { |kw| cinwords += kw.names } cinwords.uniq!.sort! @file.write <<'EOT' setlocal softtabstop=2 setlocal cindent shiftwidth=2 setlocal tabstop=2 setlocal expandtab setlocal cinoptions=g0,t0,+0,(0,c0,C1,n-2 EOT @file.write "setlocal cinwords=#{cinwords.join(',')}\n" @file.write <<'EOT' setlocal cinkeys=0{,0},!^F,o,O setlocal cindent EOT end def regions @optionBlocks.each do |kw| single = kw.names.length == 1 kw.names.each do |name| normalizedName = "#{normalize(kw.keyword)}" + "#{single ? '' : "_#{name}"}" tag = name == 'supplement' ? kw.keyword.gsub(/\./, ' ') : name @file.write "syn region tjpblk_#{normalizedName}" + " start=/^\\s*#{tag}\\s.*{\\s*$/ end=/^\\s*}\\s*$/ transparent" # We allow properties and special attributes to be folded. foldable = %w( task.timesheet project ) @file.write " fold" if @properties.include?(kw) || foldable.include?(kw.keyword) # The header is part of the region. So we must make sure that common # parameters and the property/attribute name are contained as well. @file.write " contains=@tjpcommon,tjp_#{normalizedName}" kw.optionalAttributes.each do |oa| tag = normalize(oa.keyword) @file.write ",tjp_#{tag}" if !oa.optionalAttributes.empty? # Option blocks may be contained as block or non-block. @file.write ",tjpblk_#{tag}" end end if name == 'supplement' @file.write(',tjp_supplement') end if !kw.globalScope? # Every region but a property and 'project' is contained. @file.write " contained" end @file.puts end end @file.write <<'EOT' syn region tjpblk_macro start=/macro\s\+\h\w*\s*\[/ end=/\]$/ transparent fold contains=ALL syn region tjpstring start=/"/ skip=/\\"/ end=/"/ syn region tjpstring start=/'/ skip=/\\'/ end=/'/ syn region tjpstring start=/\s-8<-$/ end=/^\s*->8-/ fold syn region tjpmlcomment start=+/\*+ end=+\*/+ syn sync fromstart set foldmethod=syntax EOT end def keywords %w( macro project supplement ).each do |kw| @file.puts "syn keyword tjp_#{kw} #{kw} contained" end @file.puts # Property keywords @properties.each do |kw| single = kw.names.length == 1 kw.names.each do |name| # Ignore the 'supplement' entries. They are not real properties. next if name == 'supplement' @file.puts "syn keyword tjp_#{normalize(kw.keyword)}" + "#{single ? '' : "_#{name}"} #{name} contained" @file.puts "hi def link tjp_#{normalize(kw.keyword)}" + "#{single ? '' : "_#{name}"} Function" end end @file.puts # Attribute keywords @attributes.each do |kw| next if %w( resourcereport taskreport textreport ).include?(kw.keyword) single = kw.names.length == 1 kw.names.each do |name| break if [ '%', '(', '~', 'include', 'macro', 'project', 'supplement' ].include?(name) @file.puts "syn keyword tjp_#{normalize(kw.keyword)}" + "#{single ? '' : "_#{name}"} #{name}" + "#{kw.globalScope? && !@optionBlocks.include?(kw) ? '' : ' contained'}" @file.puts "hi def link tjp_#{normalize(kw.keyword)}" + "#{single ? '' : "_#{name}"} Type" end end @file.puts end def matches @file.write <<'EOT' syn match tjparg contained /\${.*}/ syn match tjpcomment /#.*$/ syn match tjpcomment "//.*$" syn match tjpinclude /include.*$/ syn match tjpnumber /\s[-+]\?\d\+\(\.\d\+\)\?\([hdwmy]\|min\)\?/ syn match tjpdate /\s\d\{4}-\d\{1,2}-\d\{1,2}\(-\d\{1,2}:\d\{1,2}\(:\d\{1,2}\)\?\(-[-+]\?\d\{4}\)\?\)\?/ syn match tjptime /\s\d\{1,2}:\d\d\(:\d\d\)\?/ syn cluster tjpcommon contains=tjpcomment,tjpdate,tjptime,tjpstring,tjpnumber EOT end def highlights @file.write <<'EOT' hi def link tjp_macro PreProc hi def link tjp_supplement Function hi def link tjp_project Function hi def link tjpproperty Function hi def link tjpattribute Type hi def link tjparg Special hi def link tjpstring String hi def link tjpcomment Comment hi def link tjpmlcomment Comment hi def link tjpinclude Include hi def link tjpdate Constant hi def link tjptime Constant hi def link tjpnumber Number let b:current_syntax = "tjp" " Support running tj3 from within vim. Just type ':make your_project.tjp' to " activate it. setlocal makeprg=tj3\ --silent " Support browsing the man page by typing Shift-k while having the cursor over " any syntax keyword setlocal keywordprg=tj3man " Remap Ctrl-] to show full ID of property defined in the current " line. This requires a current ctags file (generated by 'tagfile' " report') to be present in the directory where vim was started. map :call ShowFullID() function! ShowFullID() let linenumber = line(".") let filename = bufname("%") execute "!grep '".filename."\t".linenumber.";' tags|cut -f 1" endfunction augroup TaskJugglerSource " Remove all trailing white spaces from line ends when saving files " Note: This overwrites the s mark. autocmd BufWritePre *.tj[ip] mark s | %s/\s\+$//e | normal `s augroup END EOT end def normalize(str) str.gsub(/\./, '_') end end end TaskJuggler-3.8.1/lib/taskjuggler/WorkingHours.rb000066400000000000000000000173051473026623400220400ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = WorkingHours.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/Interval' require 'taskjuggler/Scoreboard' class TaskJuggler # Class to store the working hours for each day of the week. The working hours # are stored as Arrays of Integer intervals for each day of the week. A day off # is modelled as empty Array for that week day. The start end end times of # each working period are stored as seconds after midnight. class WorkingHours attr_reader :days, :startDate, :endDate, :slotDuration, :timezone, :scoreboard # Create a new WorkingHours object. The method accepts a reference to an # existing WorkingHours object in +wh+. When it's present, the new object # will be a deep copy of the given object. The Scoreboard object is _not_ # deep copied. It will be copied on write. def initialize(arg1 = nil, startDate = nil, endDate = nil, timeZone = nil) # One entry for every day of the week. Sunday === 0. @days = Array.new(7, []) @scoreboard = nil if arg1.is_a?(WorkingHours) # Create a copy of the passed WorkingHours object. wh = arg1 @timezone = wh.timezone 7.times do |day| hours = [] wh.days[day].each do |hrs| hours << hrs.dup end setWorkingHours(day, hours) end @startDate = wh.startDate @endDate = wh.endDate @slotDuration = wh.slotDuration # Make sure the copied scoreboard has been created, so we can share it # copy-on-write. wh.onShift?(0) @scoreboard = wh.scoreboard else slotDuration = arg1 if arg1.nil? || startDate.nil? || endDate.nil? raise "You must supply values for slotDuration, start and end dates" end @startDate = startDate @endDate = endDate @slotDuration = slotDuration # Create a new object with default working hours. @timezone = timeZone # Set the default working hours. Monday to Friday 9am - 5pm. # Saturday and Sunday are days off. 1.upto(5) do |day| @days[day] = [ [ 9 * 60 * 60, 17 * 60 * 60 ] ] end end end # Since we want to share the scoreboard among instances with identical # working hours, we need to prevent the scoreboard from being deep cloned. # Calling the constructor with self in a re-defined deep_clone method will # do just that. def deep_clone WorkingHours.new(self) end # Return true of the given WorkingHours object +wh+ is identical to this # object. def ==(wh) return false if wh.nil? || @timezone != wh.timezone || @startDate != wh.startDate || @endDate != wh.endDate || @slotDuration != wh.slotDuration 7.times do |d| return false if @days[d].length != wh.days[d].length # Check all working hour intervals @days[d].length.times do |i| return false if @days[d][i][0] != wh.days[d][i][0] || @days[d][i][1] != wh.days[d][i][1] end end true end # Set the working hours for a given week day. +dayOfWeek+ must be 0 for # Sunday, 1 for Monday and so on. +intervals+ must be an Array that # contains an Array with 2 Integers for each working period. Each value # specifies the time of day as minutes after midnight. The first value is # the start time of the interval, the second the end time. def setWorkingHours(dayOfWeek, intervals) # Changing the working hours requires the score board to be regenerated. @scoreboard = nil # Legal values range from 0 Sunday to 6 Saturday. if dayOfWeek < 0 || dayOfWeek > 6 raise "dayOfWeek out of range: #{dayOfWeek}" end intervals.each do |iv| if iv[0] < 0 || iv[0] > 24 * 60 * 60 || iv[1] < 0 || iv[1] > 24 * 60 * 60 raise "Time interval has illegal values: " + "#{time_to_s(iv[0])} - #{time_to_s(iv[1])}" end if iv[0] >= iv[1] raise "Interval end time must be larger than start time" end end @days[dayOfWeek] = intervals end # Set the time zone _zone_ for the working hours. This will reset the # @scoreboard. def timezone=(zone) @scoreboard = nil @timezone = zone end # Return the working hour intervals for a given day of the week. # +dayOfWeek+ must 0 for Sunday, 1 for Monday and so on. The result is an # Array that contains Arrays of 2 Integers. def getWorkingHours(dayOfWeek) @days[dayOfWeek] end # Return true if _arg_ is within the defined working hours. _arg_ can be a # TjTime object or a global scoreboard index. def onShift?(arg) initScoreboard unless @scoreboard if arg.is_a?(TjTime) @scoreboard.get(arg) else @scoreboard[arg] end end # Return true only if all slots in the _interval_ are offhour slots. def timeOff?(interval) initScoreboard unless @scoreboard startIdx = @scoreboard.dateToIdx(interval.start) endIdx = @scoreboard.dateToIdx(interval.end) startIdx.upto(endIdx - 1) do |i| return false if @scoreboard[i] end true end # Return the number of working hours per week. def weeklyWorkingHours seconds = 0 @days.each do |day| day.each do |from, to| seconds += (to - from) end end seconds / (60 * 60) end # Returns the time interval settings for each day in a human readable form. def to_s dayNames = %w( Sun Mon Tue Wed Thu Fri Sat ) str = '' 7.times do |day| str += "#{dayNames[day]}: " if @days[day].empty? str += "off" str += "\n" if day < 6 next end first = true @days[day].each do |iv| if first first = false else str += ', ' end str += "#{time_to_s(iv[0])} - #{time_to_s(iv[0])}" end str += "\n" if day < 6 end str end private def time_to_s(t) "#{t >= 24 * 60 * 60 ? '24:00' : "#{t / 3600}:#{t % 3600}"}" end def initScoreboard # The scoreboard is an Array of True/False values. It spans a certain # time period with one entry per time slot. @scoreboard = Scoreboard.new(@startDate, @endDate, @slotDuration, false) oldTimezone = nil # Active the appropriate time zone for the working hours. if @timezone oldTimezone = TjTime.setTimeZone(@timezone) end date = @startDate @scoreboard.collect! do |slot| # The weekday and seconds of the day needs to be calculated according # to the local timezone. weekday = date.wday secondsOfDay = date.secondsOfDay result = false @days[weekday].each do |iv| # Check the working hours of that day if they overlap with +date+. if iv[0] <= secondsOfDay && secondsOfDay < iv[1] # The time slot is a working slot. result = true break end end # Calculate date of next scoreboard slot date += @slotDuration result end # Restore old time zone setting. if @timezone && oldTimezone TjTime.setTimeZone(oldTimezone) end end end end TaskJuggler-3.8.1/lib/taskjuggler/XMLDocument.rb000066400000000000000000000032521473026623400215320ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = XMLDocument.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/XMLElement' class TaskJuggler # This class provides a rather simple XML document generator. It provides # basic features to create a tree of XMLElements and to generate a XML String # or file. It's much less powerful than REXML but provides a more efficient # API to create XMLDocuments with lots of attributes. class XMLDocument # Create an empty XML document. def initialize(&block) @elements = block ? yield(block) : [] end # Add a top-level XMLElement. def <<(arg) if arg.is_a?(Array) @elements += arg.flatten elsif arg.nil? # do nothing elsif arg.is_a?(XMLElement) @elements << arg else raise ArgumentError, "Unsupported argument of type #{arg.class}: " + "#{arg.inspect}" end end # Produce the XMLDocument as String. def to_s str = '' @elements.each do |element| str << element.to_s(0) end str end # Write the XMLDocument to the specified file. def write(filename) f = filename == '.' ? $stdout : File.new(filename, 'w') @elements.each do |element| f.puts element.to_s(0) end f.close unless f == $stdout end end end TaskJuggler-3.8.1/lib/taskjuggler/XMLElement.rb000066400000000000000000000147151473026623400213530ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = XMLElement.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/UTF8String' class TaskJuggler # This class models an XML node that may contain other XML nodes. XML element # trees can be constructed with the class constructor and converted into XML. class XMLElement # Construct a new XML element and include it in an existing XMLElement tree. def initialize(name, attributes = {}, selfClosing = false, &block) if (name.nil? && attributes.length > 0) || (!name.nil? && !name.is_a?(String)) raise ArgumentError, "Name must be nil or a String " end @name = name attributes.each do |n, v| if n.nil? || v.nil? raise ArgumentError, "Attribute name (#{n}) or value (#{v}) may not be nil" end unless v.is_a?(String) raise ArgumentError, "Attribute value of #{n} must be a String" end end @attributes = attributes # This can be set to true if is legal for this element. @selfClosing = selfClosing @children = block ? yield(block) : [] # Allow blocks with single elements not to be Arrays. They will be # automatically converted into Arrays here. unless @children.is_a?(Array) @children = [ @children ] else @children.flatten! end # Convert all children that are text String objects into XMLText # objects. @children.collect! do |c| c.is_a?(String) ? XMLText.new(c) : c end # Make sure we have no nil objects in the list. @children.delete_if { |c| c.nil? } # Now all children must be XMLElement objects. @children.each do |c| unless c.is_a?(XMLElement) raise ArgumentError, "Element must be of type XMLElement, not #{c.class}: #{c.inspect}" end end end # Add a new child or a set of new childs to the element. def <<(arg) # If the argument is an array, we have to insert each element # individually. if arg.is_a?(XMLElement) @children << arg elsif arg.is_a?(String) @children << XMLText.new(arg) elsif arg.is_a?(Array) # Delete all nil entries arg.delete_if { |i| i.nil? } # Check that the rest are really all XMLElement objects. arg.each do |i| unless i.is_a?(XMLElement) raise ArgumentError, "Element must be of type XMLElement, not #{i.class}: #{i.inspect}" end end @children += arg elsif arg.nil? # Do nothing. Insertions of nil are simply ignored. else raise "Elements must be of type XMLElement not #{arg.class}" end self end # Add or change _attribute_ to _value_. def []=(attribute, value) raise ArgumentError, "Attribute value #{value} is not a String" unless value.is_a?(String) @attributes[attribute] = value end # Return the value of attribute _attribute_. def [](attribute) @attributes[attribute] end # Return the element and all sub elements as properly formatted XML. def to_s(indent = 0) out = '<' + @name @attributes.keys.sort.each do |attrName| out << " #{attrName}=\"#{escape(@attributes[attrName], true)}\"" end if @children.empty? && @selfClosing out << '/>' else out << '>' @children.each do |child| # We only insert newlines for multiple childs and after a tag has been # closed. if @children.size > 1 && !child.is_a?(XMLText) && out[-1] == ?> out << "\n" + indentation(indent + 1) end out << child.to_s(indent + 1) end out << "\n" + indentation(indent) if @children.size > 1 && out[-1] == ?> out << '' end end protected # Escape special characters in input String _str_. def escape(str, quotes = false) out = '' str.each_utf8_char do |c| case c when '&' out << '&' when '"' out << '\"' else out << c end end out end def indentation(indent) ' ' * indent end end # This is a specialized XMLElement to represent a simple text. class XMLText < XMLElement def initialize(text) super(nil, {}) raise 'Text may not be nil' unless text @text = text end def to_s(indent) out = '' @text.each_utf8_char do |c| case c when '<' out << '<' when '>' out << '>' when '&' out << '&' else out << c end end out end end # This is a convenience class that allows the creation of an XMLText nested # into an XMLElement. The _name_ and _attributes_ belong to the XMLElement, # the text to the XMLText. class XMLNamedText < XMLElement def initialize(text, name, attributes = {}) super(name, attributes) self << XMLText.new(text) end end # This is a specialized XMLElement to represent a comment. class XMLComment < XMLElement def initialize(text = '') super(nil, {}) @text = text end def to_s(indent) '\n#{' ' * indent}" end private # It is crucial to canonicalize xml comment text because xml # comment syntax forbids having a -- in the comment body. I # picked emacs's "M-x comment-region" approach of putting a # backslash between the two. def canonicalize_comment(text) new_text = text.gsub("--", "-\\-") new_text end end # This is a specialized XMLElement to represent XML blobs. The content is not # interpreted and must be valid XML in the content it is added. class XMLBlob < XMLElement def initialize(blob = '') super(nil, {}) raise ArgumentError, "blob may not be nil" if blob.nil? @blob = blob end def to_s(indent) out = '' @blob.each_utf8_char do |c| out += (c == "\n" ? "\n" + ' ' * indent : c) end out end end end TaskJuggler-3.8.1/lib/taskjuggler/apps/000077500000000000000000000000001473026623400200075ustar00rootroot00000000000000TaskJuggler-3.8.1/lib/taskjuggler/apps/Tj3.rb000066400000000000000000000201351473026623400207750ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = Tj3.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/Tj3AppBase' require 'taskjuggler/TaskJuggler' # Name of the application suite AppConfig.appName = 'tj3' class TaskJuggler class Tj3 < Tj3AppBase def initialize super # tj3 just requires Ruby 1.8.7. All other apps need 1.9.2. @mininumRubyVersion = '1.8.7' # By default, we're only using 1 CPU core. @maxCpuCores = 1 # Don't generate warnings for differences between time sheet data and # the plan. @warnTsDeltas = false # Don't stop after reading all files. @checkSyntax = false # Don't generate reports when previous errors have been found. @forceReports = false # List of requested report IDs. @reportIDs = [] # List of requested report IDs (as regular expressions). @reportRegExpIDs = [] # Don't generate trace reports by default @generateTraces = false # Should a booking file be generated? @freeze = false # The cut-off date for the freeze @freezeDate = TjTime.new.align(3600) # Should bookings be grouped by Task or by Resource (default). @freezeByTask = false # Generate a list of all defined reports if true. @listReports = false # Regular expression to select the report IDs to be listed. @listReportPattern = '*' # Don't generate any reports. @noReports = false # Treat warnings like errors or not. @abortOnWarning = false # The directory where generated reports should be put in. @outputDir = nil # The file names of the time sheet files to check. @timeSheets = [] # The file names of the status sheet files to check. @statusSheets = [] # Show some progress information by default TaskJuggler::Log.silent = false end def processArguments(argv) super do @opts.banner.prepend(<<'EOT' This is the main application. It reads in your project files, schedules the project and generates the reports. EOT ) @opts.on('--debuglevel N', Integer, format("Verbosity of debug output")) do |arg| TaskJuggler::Log.level = arg end @opts.on('--debugmodules x,y,z', Array, format('Restrict debug output to a list of modules')) do |arg| TaskJuggler::Log.segments = arg end @opts.on('--freeze', format('Generate or update the booking file for ' + 'the project. The file will have the same ' + 'base name as the project file but has a ' + '-bookings.tji extension.')) do @freeze = true end @opts.on('--freezedate ', String, format('Use a different date than the current moment' + 'as cut-off date for the booking file')) do |arg| begin @freezeDate = TjTime.new(arg).align(3600) rescue TjException => msg error('tj3_ivld_freeze_date', "Invalid freeze date: #{msg.message}") end end @opts.on('--freezebytask', format('Group the bookings in the booking file generated ' + 'during a freeze by task instead of by resource.')) do @freezeByTask = true end @opts.on('--check-time-sheet ', String, format("Check the given time sheet")) do |arg| @timeSheets << arg end @opts.on('--check-status-sheet ', String, format("Check the given status sheet")) do |arg| @statusSheets << arg end @opts.on('--warn-ts-deltas', format('Turn on warnings for requested changes in time ' + 'sheets')) do @warnTsDeltas = true end @opts.on('--check-syntax', format('Only parse the input files and check the syntax.')) do @checkSyntax = true end @opts.on('--no-reports', format('Just schedule the project, but don\'t generate any ' + 'reports.')) do @noReports = true end @opts.on('--list-reports ', String, format('List id, formats and file name of all the defined ' + 'reports that have IDs that match the specified ' + 'regular expression.')) do |arg| @listReports = true @listReportPattern = arg end @opts.on('--report ', String, format('Only generate the report with the specified ID. ' + 'This option can be used multiple times.')) do |arg| @reportIDs << arg end @opts.on('--reports ', String, format('Only generate the reports that have IDs that match ' + 'the specified regular expression. This option can ' + 'be used multiple times.')) do |arg| @reportRegExpIDs << arg end @opts.on('-f', '--force-reports', format('Generate reports despite scheduling errors')) do @forceReports = true end @opts.on('--add-trace', format('Append a current data set to all trace reports.')) do @generateTraces = true end @opts.on('--abort-on-warnings', format('Abort program on warnings like we do on errors.')) do @abortOnWarning = true end @opts.on('-o', '--output-dir ', String, format('Directory the reports should go into')) do |arg| @outputDir = arg + (arg[-1] == ?/ ? '' : '/') end @opts.on('-c N', Integer, format('Maximum number of CPU cores to use')) do |arg| @maxCpuCores = arg end end end def appMain(files) if files.empty? error('tj3_tjp_file_missing', 'You must provide at least one .tjp file') end if @outputDir && !File.directory?(@outputDir) error('tj3_outdir_missing', "Output directory '#{@outputDir}' does not exist or is not " + "a directory!") end tj = TaskJuggler.new tj.maxCpuCores = @maxCpuCores tj.warnTsDeltas = @warnTsDeltas tj.generateTraces = @generateTraces MessageHandlerInstance.instance.abortOnWarning = @abortOnWarning keepParser = !@timeSheets.empty? || !@statusSheets.empty? return 1 unless tj.parse(files, keepParser) return 0 if @checkSyntax if !tj.schedule return 1 unless @forceReports end # The checks of time and status sheets is probably only used for # debugging. Normally, this function is provided by tj3client. @timeSheets.each do |ts| return 1 if !tj.checkTimeSheet(ts) || tj.errors > 0 end @statusSheets.each do |ss| return 1 if !tj.checkStatusSheet(ss) || tj.errors > 0 end # Check for freeze mode and generate the booking file if requested. if @freeze return 1 unless tj.freeze(@freezeDate, @freezeByTask) && tj.errors == 0 end # List all the reports that match the requested expression. tj.listReports(@listReportPattern, true) if @listReports return 0 if @noReports if @reportIDs.empty? && @reportRegExpIDs.empty? return 1 if !tj.generateReports(@outputDir) || tj.errors > 0 else @reportIDs.each do |id| return 1 if !tj.generateReport(id, false) end @reportRegExpIDs.each do |id| return 1 if !tj.generateReport(id, true) end end 0 end end end TaskJuggler-3.8.1/lib/taskjuggler/apps/Tj3Client.rb000066400000000000000000000346401473026623400221420ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = Tj3Client.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'drb' require 'drb/acl' require 'taskjuggler/Tj3AppBase' require 'taskjuggler/daemon/DaemonConnector' # Name of the application AppConfig.appName = 'tj3client' class TaskJuggler # The Tj3Client class provides the primary interface to the TaskJuggler # daemon. It exposes a rich commandline interface that supports key # operations like add/removing a project, generating a report or checking a # time or status sheet. All connections are made via DRb and tj3client # requires a properly configured tj3d to work. class Tj3Client < Tj3AppBase include DaemonConnectorMixin def initialize super # For security reasons, this will probably not change. All DRb # operations are limited to localhost only. The client and the sever # must have access to the identical file system. @host = '127.0.0.1' # The default port. 'T' and 'J' in ASCII decimal @port = 8474 # The file with the server URI in case port is 0. @uriFile = File.join(Dir.getwd, '.tj3d.uri') # This must must be changed for the communication to work. @authKey = nil # Determines whether report IDs are fix IDs or regular expressions that # match a set of reports. @regExpMode = false # Prevents usage of protective sandbox if set to true. @unsafeMode = false # List of requested output formats for reports. @formats = nil @mandatoryArgs = ' [arg1 arg2 ...]' # This list describes the supported command line commands and their # parameter. # :label : The command name # :args : A list of parameters. If the first character is a '+' the # parameter must be provided 1 or more times. If the first character is # a '*' the parameter must be provided 0 or more times. Repeatable and # optional paramters must follow the mandatory ones. # :descr : A short description of the command used for the help text. @commands = [ { :label => 'status', :args => [], :descr => 'Display the status of the available projects' }, { :label => 'terminate', :args => [], :descr => 'Terminate the TaskJuggler daemon' }, { :label => 'add', :args => [ 'tjp file', '*tji file'], :descr => 'Add a new project or update and existing one' }, { :label => 'update', :args => [], :descr => 'Reload all projects that have modified files and '+ 'are not being reloaded already' }, { :label => 'remove', :args => [ '+project ID' ], :descr => 'Remove the project with the specified ID from the ' + 'daemon' }, { :label => 'report', :args => [ 'project ID', '+report ID', '!=', '*tji file'], :descr => 'Generate the report with the provided ID for ' + 'the project with the given ID'}, { :label => 'list-reports', :args => [ 'project ID', '!report ID' ], :descr => 'List all available reports of the project or those ' + 'that match the provided report ID' }, { :label => 'check-ts', :args => [ 'project ID', 'time sheet' ], :descr => 'Check the provided time sheet for correctness ' + 'against the project with the given ID'}, { :label => 'check-ss', :args => [ 'project ID', 'status sheet' ], :descr => 'Check the provided status sheet for correctness ' + 'against the project with the given ID'} ] end def processArguments(argv) super do prebanner = <<'EOT' The TaskJuggler client is used to send commands and data to the TaskJuggler daemon. The communication is done via TCP/IP. The following commands are supported: EOT # Convert the command list into a help text. @commands.each do |cmd| tail = '' args = cmd[:args].dup args.map! do |c| if c[0] == '*' "[<#{c[1..-1]}> ...]" elsif c[0] == '+' "<#{c[1..-1]}> [<#{c[1..-1]}> ...]" elsif c[0] == '!' tail += ']' "[#{c[1..-1]} " else "<#{c}>" end end args = args.join(' ') prebanner += " #{cmd[:label] + ' ' + args + tail}" + "\n\n#{' ' * 10 + format(cmd[:descr], 10)}\n" end @opts.banner.prepend(prebanner) @opts.on('-p', '--port ', Integer, format('Use the specified TCP/IP port')) do |arg| @port = arg end @opts.on('--urifile ', String, format('If the port is 0, use this file to get the URI ' + 'of the server.')) do |arg| @uriFile = arg end @opts.on('-r', '--regexp', format('The report IDs are not fixed but regular ' + 'expressions that match a set of reports')) do |arg| @regExpMode = true end @opts.on('--unsafe', format('Run the program without sandbox protection. This ' + 'is not recommended for normal operation! It may ' + 'only be used for debugging or testing ' + 'purposes.')) do |arg| @unsafeMode = true end @opts.on('--format [FORMAT]', [ :csv, :html, :mspxml, :niku, :tjp ], format('Request the report to be generated in the specified' + 'format. Use multiple options to request multiple ' + 'formats. Supported formats are csv, html, niku and ' + 'tjp. By default, the formats specified in the ' + 'report definition are used.')) do |arg| @formats = [] unless @formats @formats << arg end end end def appMain(args) # Run a first check of the non-optional command line arguments. checkCommand(args) # Read some configuration variables. Except for the authKey, they are # all optional. @rc.configure(self, 'global') @broker = connectDaemon retVal = executeCommand(args[0], args[1..-1]) disconnectDaemon @broker = nil retVal end private def checkCommand(args) if args.empty? errorMessage = 'You must specify a command!' else errorMessage = "Unknown command #{args[0]}" @commands.each do |cmd| # The first value of args is the command name. if cmd[:label] == args[0] # Find out how many arguments we need to have and if that's a # lower limit or a fixed value. minArgs = 0 varArgs = false cmd[:args].each do |arg| # Arguments starting with '+' must have 1 or more showings. # Arguments starting with '*' may show up 0 or more times. minArgs += 1 unless '!*'.include?(arg[0]) varArgs = true if '!*+'.include?(arg[0]) end return true if args.length - 1 >= minArgs errorMessage = "Command #{args[0]} must have " + "#{varArgs ? 'at least ' : ''}#{minArgs} " + 'arguments' end end end error('tjc_cmd_error', errorMessage) end def executeCommand(command, args) case command when 'status' $stdout.puts callDaemon(:status, []) when 'terminate' callDaemon(:stop, []) info('tjc_daemon_term', 'Daemon terminated') when 'add' res = callDaemon(:addProject, [ Dir.getwd, args, $stdout, $stderr, $stdin, @silent ]) if res info('tjc_proj_added', "Project(s) #{args.join(', ')} added") return 0 else warning('tjc_proj_adding_failed', "Projects(s) #{args.join(', ')} could not be added") return 1 end when 'remove' args.each do |arg| unless callDaemon(:removeProject, arg) error('tjc_prj_not_found', "Project '#{arg}' not found in list") end end info('tjc_prj_removed', 'Project removed') when 'update' callDaemon(:update, []) info('tjc_reload_req', 'Reload requested') when 'report' # The first value of args is the project ID. The following values # could be either report IDs or TJI file # names ('.' or '*.tji'). projectId = args.shift # Ask the ProjectServer to launch a new ReportServer process and # provide a DRbObject reference to it. connectToReportServer(projectId) reportIds, tjiFiles = splitIdsAndFiles(args) if reportIds.empty? disconnectReportServer error('tjc_no_rep_id', 'You must provide at least one report ID') end # Send the provided .tji files to the ReportServer. failed = !addFiles(tjiFiles) # Ask the ReportServer to generate the reports with the provided IDs. unless failed reportIds.each do |reportId| begin unless @reportServer.generateReport(@rs_authKey, reportId, @regExpMode, @formats, nil) failed = true break end rescue error('tjc_gen_rep_failed', "Could not generate report #{reportId}: #{$!}") end end end # Terminate the ReportServer disconnectReportServer return failed ? 1 : 0 when 'list-reports' # The first value of args is the project ID. The following values # could be either report IDs or TJI file # names ('.' or '*.tji'). projectId = args.shift # Ask the ProjectServer to launch a new ReportServer process and # provide a DRbObject reference to it. connectToReportServer(projectId) reportIds, tjiFiles = splitIdsAndFiles(args) if reportIds.empty? # If the user did not provide a report ID we generate a full list. reportIds = [ '.*' ] @regExpMode = true end # Send the provided .tji files to the ReportServer. failed = !addFiles(tjiFiles) # Ask the ReportServer to generate the reports with the provided IDs. unless failed reportIds.each do |reportId| begin unless @reportServer.listReports(@rs_authKey, reportId, @regExpMode) failed = true break end rescue error('tjc_report_list_failed', "Getting report list failed: #{$!}") end end end # Terminate the ReportServer disconnectReportServer return failed ? 1 : 0 when 'check-ts' connectToReportServer(args[0]) begin res = @reportServer.checkTimeSheet(@rs_authKey, args[1]) rescue error('tjc_tschck_failed', "Time sheet check failed: #{$!}") end disconnectReportServer return res ? 0 : 1 when 'check-ss' connectToReportServer(args[0]) begin res = @reportServer.checkStatusSheet(@rs_authKey, args[1]) rescue error('tjc_sschck_failed', "Status sheet check failed: #{$!}") end disconnectReportServer return res ? 0 : 1 else raise "Unknown command #{command}" end 0 end def connectToReportServer(projectId) @ps_uri, @ps_authKey = callDaemon(:getProject, projectId) if @ps_uri.nil? error('tjc_prj_id_not_loaded', "No project with ID #{projectId} loaded") end begin @projectServer = DRbObject.new(nil, @ps_uri) @rs_uri, @rs_authKey = @projectServer.getReportServer(@ps_authKey) @reportServer = DRbObject.new(nil, @rs_uri) rescue error('tjc_no_rep_srv', "Cannot get report server: #{$!}") end begin @reportServer.connect(@rs_authKey, $stdout, $stderr, $stdin, @silent) rescue error('tjc_no_io_connect', "Can't connect IO: #{$!}") end end def disconnectReportServer begin @reportServer.disconnect(@rs_authKey) rescue error('tjc_no_io_disconnect', "Can't disconnect IO: #{$!}") end begin @reportServer.terminate(@rs_authKey) rescue error('tjc_srv_term_failed', "Report server termination failed: #{$!}") end @reportServer = nil @rs_uri = nil @rs_authKey = nil @projectServer = nil @ps_uri = nil @ps_authKey = nil end # Call the TaskJuggler daemon (ProjectBroker) and execute the provided # command with the provided arguments. def callDaemon(command, args) begin return @broker.command(@authKey, command, args) rescue error('tjc_call_srv_failed', "Call to TaskJuggler server on host '#{@host}' " + "port #{@port} failed: #{$!}") end end # Sort the remaining arguments into a report ID and a TJI file list. # If .tji files are present, they must be separated from the report ID # list by a '='. def splitIdsAndFiles(args) reportIds = [] tjiFiles = [] addToReports = true args.each do |arg| if arg == '=' # Switch to tji file list. addToReports = false elsif addToReports reportIds << arg else tjiFiles << arg end end [ reportIds, tjiFiles ] end # Transfer the _tjiFiles_ to the _reportServer_. def addFiles(tjiFiles) tjiFiles.each do |file| begin unless @reportServer.addFile(@rs_authKey, file) return false end rescue error('tjc_canont_add_file', "Cannot add file #{file} to ReportServer") end end true end end end TaskJuggler-3.8.1/lib/taskjuggler/apps/Tj3Daemon.rb000066400000000000000000000143451473026623400221270ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = Tj3Daemon.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'drb' require 'taskjuggler/Tj3AppBase' require 'taskjuggler/MessageHandler' require 'taskjuggler/daemon/ProjectBroker' # Name of the application AppConfig.appName = 'tj3d' class TaskJuggler class Tj3Daemon < Tj3AppBase def initialize super @mandatoryArgs = '[ [ ...] ...]' @mhi = MessageHandlerInstance.instance @mhi.logFile = File.join(Dir.getwd, "/#{AppConfig.appName}.log") @mhi.appName = AppConfig.appName # By default show only warnings and more serious messages. @mhi.outputLevel = :warning @daemonize = true @uriFile = File.join(Dir.getwd, '.tj3d.uri') @port = nil @webServer = false @webServerPort = 8080 @webdPidFile = File.join(Dir.getwd, ".tj3webd-#{$$}.pid") end def processArguments(argv) super do @opts.banner.prepend(<<'EOT' The TaskJuggler daemon can be used to quickly generate reports for a number of scheduled projects that are resident in memory. Once the daemon has been started tj3client can be used to control it. EOT ) @opts.on('-d', '--dont-daemonize', format("Don't put program into daemon mode. Keep it " + 'connected to the terminal and show debug output.')) do @daemonize = false end @opts.on('-p', '--port ', Integer, format('Use the specified TCP/IP port to serve tj3client ' + 'requests (Default: 8474).')) do |arg| @port = arg end @opts.on('--logfile ', String, format('Log daemon messages to the specified file.')) do |arg| @mhi.logFile = arg end @opts.on('--urifile ', String, format('If the port is 0, use this file to store the URI ' + 'of the server.')) do |arg| @uriFile = arg end @opts.on('-w', '--webserver', format('Start a web server that serves the reports of ' + 'the loaded projects.')) do @webServer = true end @opts.on('--webserver-port ', Integer, format('Use the specified TCP/IP port to serve web browser ' + 'requests (Default: 8080).')) do |arg| @webServerPort = arg end end end def appMain(files) broker = ProjectBroker.new @rc.configure(self, 'global') @rc.configure(@mhi, 'global.log') @rc.configure(broker, 'global') @rc.configure(broker, 'daemon') # Set some config variables if corresponding data was provided via the # command line. broker.port = @port if @port broker.uriFile = @uriFile broker.projectFiles = sortInputFiles(files) unless files.empty? broker.daemonize = @daemonize # Create log files for standard IO for each child process if the daemon # is not disconnected from the terminal. broker.logStdIO = !@daemonize if @webServer webdCommand = "tj3webd --webserver-port #{@webServerPort} " + "--pidfile #{@webdPidFile}" # Also start the web server as a separate process. We keep the PID, so # we can terminate that process again when we exit the daemon. begin `#{webdCommand}` rescue error('tj3webd_start_failed', "Could not start tj3webd: #{$!}") end info('web_server_started', "Web server started as '#{webdCommand}'") end broker.start if @webServer pid = nil begin # Read the PID of the web server from the PID file. File.open(@webdPidFile, 'r') do |f| pid = f.read.to_i end rescue warning('cannot_read_webd_pidfile', "Cannot read tj3webd PID file (#{@webdPidFile}): #{$!}") end # If we have started the web server, we are also trying to terminate # that process again. begin Process.kill("TERM", pid) rescue warning('tj3webd_term_failed', "Could not terminate web server: #{$!}") end info('web_server_terminated', "Web server with PID #{pid} terminated") end 0 end private # Sort the provided input files into groups of projects. Each *.tjp file # starts a new project. A *.tjp file may be followed by any number of # *.tji files. The result is an Array of projects. Each consists of an # Array like this: [ , (, ...) ]. def sortInputFiles(files) projects = [] project = nil files.each do |file| if file[-4..-1] == '.tjp' # The project master file determines the working directory. If it's # an absolute file name, that directory will become the working # directory. If it's a relative file name, the current working # directory will be kept. if file[0] == '/' # Absolute file name workingDir = File.dirname(file) fileName = File.basename(file) else # Relative file name workingDir = Dir.getwd fileName = file end project = [ workingDir, fileName ] projects << project elsif file[-4..-1] == '.tji' # .tji files are optional. But if they are specified, they must # always follow the master file in the list. if project.nil? error('tj3d_tji_before_tjp', "You must specify a '.tjp' file before the '.tji' files") end project << file else error('tj3d_no_file_Ext', "Project files must have a '.tjp' or '.tji' extension") end end projects end end end TaskJuggler-3.8.1/lib/taskjuggler/apps/Tj3Man.rb000066400000000000000000000073351473026623400214400ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = Tj3Man.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/Tj3AppBase' require 'taskjuggler/TernarySearchTree' require 'taskjuggler/SyntaxReference' require 'taskjuggler/UserManual' AppConfig.appName = 'tj3man' class TaskJuggler class Tj3Man < Tj3AppBase def initialize super @man = SyntaxReference.new @keywords = TernarySearchTree.new(@man.all) @manual = false @showHtml = false @browser = ENV['BROWSER'] || 'firefox' @directory = './' @mininumRubyVersion = '1.8.7' end def processArguments(argv) super do @opts.banner.prepend(<<'EOT' This program can be used to generate the user manual in HTML format or to get a textual help for individual keywords. EOT ) @opts.on('-d', '--dir ', String, format('directory to put the manual')) do |dir| @directory = dir end @opts.on('--html', format('Show the user manual in your local web browser. ' + 'By default, Firefox is used or the browser specified ' + 'with the $BROWSER environment variable.')) do @showHtml = true end @opts.on('--browser ', String, format('Specify the command to start your web browser. ' + 'The default is \'firefox\'.')) do |browser| @browser = browser end @opts.on('-m', '--manual', format('Generate the user manual into the current directory ' + 'or the directory specified with the -d option.')) do @manual = true end end end def appMain(requestedKeywords) if @manual UserManual.new.generate(@directory) elsif requestedKeywords.empty? showManual else requestedKeywords.each do |keyword| if (kws = @keywords[keyword, true]).nil? error('tj3man_no_matches', "No matches found for '#{keyword}'") elsif kws.length == 1 || kws.include?(keyword) showManual(keyword) else warning('tj3man_multi_match', "Multiple matches found for '#{keyword}':\n" + "#{kws.join(', ')}") end end end 0 end private def showManual(keyword = nil) if @showHtml # If the user requested HTML format, we start the browser. startBrowser(keyword) else if keyword # Print the documentation for the keyword. puts @man.to_s(keyword) else # Print a list of all documented keywords. puts @man.all.join("\n") end end end # Start the web browser with either the entry page or the page for the # specified keyword. def startBrowser(keyword = nil) # Find the manual relative to this file. manualDir = File.join(File.dirname(__FILE__), '..', '..', '..', 'manual', 'html') file = "#{manualDir}/#{keyword || 'index'}.html" # Make sure the file exists. unless File.exist?(file) $stderr.puts "Cannot open manual file #{file}" exit 1 end # Start the browser. begin `#{@browser} file:#{file}` rescue $stderr.puts "Cannot open browser: #{$!}" exit 1 end end end end TaskJuggler-3.8.1/lib/taskjuggler/apps/Tj3SsReceiver.rb000066400000000000000000000026471473026623400230000ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = Tj3SsReceiver.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/Tj3SheetAppBase' require 'taskjuggler/StatusSheetReceiver' # Name of the application AppConfig.appName = 'tj3ss_receiver' class TaskJuggler class Tj3SsReceiver < Tj3SheetAppBase def initialize super end def processArguments(argv) super do @opts.banner.prepend(<<'EOT' This program can be used to receive filled-out status sheets via email. It reads the emails from STDIN and extracts the status sheet from the attached files. The status sheet is checked for correctness. Good status sheets are filed away. The sender be informed by email that the status sheets was accepted or rejected. EOT ) end end def appMain(argv) ts = TaskJuggler::StatusSheetReceiver.new('tj3ss_receiver') @rc.configure(ts, 'global') @rc.configure(ts, 'statussheets') @rc.configure(ts, 'statussheets.receiver') ts.workingDir = @workingDir if @workingDir ts.dryRun = @dryRun ts.processEmail 0 end end end TaskJuggler-3.8.1/lib/taskjuggler/apps/Tj3SsSender.rb000066400000000000000000000054121473026623400224450ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = Tj3SsSender.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # # This script is used to send out the time sheet templates to the employees. # It should be run from a cron job once a week. require 'taskjuggler/Tj3SheetAppBase' require 'taskjuggler/StatusSheetSender' # Name of the application AppConfig.appName = 'tj3ss_sender' class TaskJuggler class Tj3SsSender < Tj3SheetAppBase def initialize super @optsSummaryWidth = 25 @force = false @intervalDuration = nil @hideResource = nil # The default report period end is next Wednesday 0:00. @date = TjTime.new.nextDayOfWeek(3).to_s('%Y-%m-%d') @resourceList = [] end def processArguments(argv) super do @opts.banner.prepend(<<'EOT' This program can be used to out status sheets templates via email. It will generate status sheet templates for managers of the project. The project data will be accesses via tj3client from a running TaskJuggler server process. EOT ) @opts.on('-r', '--resource ', String, format('Only generate template for given resource')) do |arg| @resourceList << arg end @opts.on('-f', '--force', format('Send out a new template even if one exists ' + 'already')) do |arg| @force = true end @opts.on('--hideresource ', String, format('Filter expression to limit the resource list')) do |arg| @hideResource = arg end @opts.on('-i', '--interval ', String, format('The duration of the interval. This is a number ' + 'directly followed by a unit. 1w means one week ' + '(the default), 5d means 5 days and 72h means 72 ' + 'hours.')) do |arg| @intervalDuration = arg end optsEndDate end end def appMain(argv) ts = StatusSheetSender.new('tj3ss_sender') @rc.configure(ts, 'global') @rc.configure(ts, 'statussheets') @rc.configure(ts, 'statussheets.sender') ts.workingDir = @workingDir if @workingDir ts.dryRun = @dryRun ts.force = @force ts.intervalDuration = @intervalDuration if @intervalDuration ts.date = @date if @date ts.hideResource = @hideResource if @hideResource ts.sendTemplates(@resourceList) 0 end end end TaskJuggler-3.8.1/lib/taskjuggler/apps/Tj3TsReceiver.rb000066400000000000000000000030221473026623400227650ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = Tj3TsReceiver.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # # This script is used to send out the time sheet templates to the employees. # It should be run from a cron job once a week. require 'taskjuggler/Tj3SheetAppBase' require 'taskjuggler/TimeSheetReceiver' # Name of the application suite AppConfig.appName = 'tj3ts_receiver' class TaskJuggler class Tj3TsReceiver < Tj3SheetAppBase def initialize super end def processArguments(argv) super do @opts.banner.prepend(<<'EOT' This program can be used to receive filled-out time sheets via email. It reads the emails from STDIN and extracts the time sheet from the attached files. The time sheet is checked for correctness. Good time sheets are filed away. The sender will be informed by email that the time sheets was accepted or rejected. EOT ) end end def appMain(argv) ts = TimeSheetReceiver.new('tj3ts_receiver') @rc.configure(ts, 'global') @rc.configure(ts, 'timesheets') @rc.configure(ts, 'timesheets.receiver') ts.workingDir = @workingDir if @workingDir ts.dryRun = @dryRun ts.processEmail 0 end end end TaskJuggler-3.8.1/lib/taskjuggler/apps/Tj3TsSender.rb000066400000000000000000000042241473026623400224460ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = Tj3TsSender.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # # This script is used to send out the time sheet templates to the employees. # It should be run from a cron job once a week. require 'taskjuggler/Tj3SheetAppBase' require 'taskjuggler/TimeSheetSender' # Name of the application suite AppConfig.appName = 'tj3ts_sender' class TaskJuggler class Tj3TsSender < Tj3SheetAppBase def initialize super @optsSummaryWidth = 22 @force = false @intervalDuration = nil # The default report period end is next Monday 0:00. @date = TjTime.new.nextDayOfWeek(1).to_s('%Y-%m-%d') @resourceList = [] end def processArguments(argv) super do @opts.banner.prepend(<<'EOT' This program can be used to send out time sheets templates via email. It will generate time sheet templates for all resources of the project. The project data will be accesses via tj3client from a running TaskJuggler server process. EOT ) @opts.on('-r', '--resource ', String, format('Only generate template for given resource')) do |arg| @resourceList << arg end @opts.on('-f', '--force', format('Send out a new template even if one exists ' + 'already')) do |arg| @force = true end optsEndDate end end def appMain(argv) ts = TimeSheetSender.new('tj3ts_sender') @rc.configure(ts, 'global') @rc.configure(ts, 'timesheets') @rc.configure(ts, 'timesheets.sender') ts.workingDir = @workingDir if @workingDir ts.dryRun = @dryRun ts.force = @force ts.intervalDuration = @intervalDuration if @intervalDuration ts.date = @date if @date ts.sendTemplates(@resourceList) 0 end end end TaskJuggler-3.8.1/lib/taskjuggler/apps/Tj3TsSummary.rb000066400000000000000000000051171473026623400226650ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = Tj3TsSummary.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # # This script is used to send out the time sheet templates to the employees. # It should be run from a cron job once a week. require 'taskjuggler/Tj3SheetAppBase' require 'taskjuggler/TimeSheetSummary' # Name of the application AppConfig.appName = 'tj3ts_summary' class TaskJuggler class Tj3TsSummary < Tj3SheetAppBase def initialize super # The default report period end is next Monday 0:00. @date = TjTime.new.nextDayOfWeek(1).to_s('%Y-%m-%d') @resourceList = [] @sheetRecipients = [] @digestRecipients = [] end def processArguments(argv) super do @opts.banner.prepend(<<'EOT' This program can be used to send out individual copies and a summary of all accepted time sheets a list of email addresses. The directory structures for templates and submitted time sheets must be present. The project data will be accesses via tj3client from a running TaskJuggler server process. EOT ) @opts.on('-r', '--resource ', String, format('Only generate summary for given resource')) do |arg| @resourceList << arg end @opts.on('-t', '--to ', String, format('Send all individual reports and a summary report ' + 'to this email address')) do |arg| @sheetRecipients << arg @digestRecipients << arg end @opts.on('--sheet ', String, format('Send all reports to this email address')) do |arg| @sheetRecipients << arg end @opts.on('--digest ', String, format('Send a summary report to this email address')) do |arg| @digestRecipients << arg end optsEndDate end end def appMain(argv) ts = TimeSheetSummary.new @rc.configure(ts, 'global') @rc.configure(ts, 'timesheets') @rc.configure(ts, 'timesheets.summary') ts.workingDir = @workingDir if @workingDir ts.dryRun = @dryRun ts.date = @date if @date ts.sheetRecipients += @sheetRecipients ts.digestRecipients += @digestRecipients ts.sendSummary(@resourceList) 0 end end end TaskJuggler-3.8.1/lib/taskjuggler/apps/Tj3WebD.rb000066400000000000000000000064401473026623400215420ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = Tj3WebD.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'drb' require 'taskjuggler/Tj3AppBase' require 'taskjuggler/MessageHandler' require 'taskjuggler/daemon/WebServer' # Name of the application AppConfig.appName = 'tj3webd' class TaskJuggler class Tj3WebD < Tj3AppBase def initialize super @mhi = MessageHandlerInstance.instance @mhi.logFile = File.join(Dir.getwd, "/#{AppConfig.appName}.log") @mhi.appName = AppConfig.appName # By default show only warnings and more serious messages. @mhi.outputLevel = :warning @daemonize = true @uriFile = File.join(Dir.getwd, '.tj3d.uri') @port = nil @webServerPort = nil @pidFile = nil end def processArguments(argv) super do @opts.banner.prepend(<<'EOT' The TaskJuggler web server can be used to serve the HTTP reports of TaskJuggler projects to be viewed by any HTML5 compliant web browser. It uses the TaskJuggler daemon (tj3d) for data hosting and report generation. EOT ) @opts.on('-d', '--dont-daemonize', format("Don't put program into daemon mode. Keep it " + 'connected to the terminal and show debug output.')) do @daemonize = false end @opts.on('-p', '--port ', Integer, format('Use the specified TCP/IP port to connect to the ' + 'TaskJuggler daemon (Default: 8474).')) do |arg| @port = arg end @opts.on('--pidfile ', String, format('Write the process ID of the daemon to the ' + 'specified file.')) do |arg| @pidFile = arg end @opts.on('--logfile ', String, format('Log daemon messages to the specified file.')) do |arg| @mhi.logFile = arg end @opts.on('--urifile ', String, format('If the port is 0, use this file to read the URI ' + 'of the TaskJuggler daemon.')) do |arg| @uriFile = arg end @opts.on('--webserver-port ', Integer, format('Use the specified TCP/IP port to serve web browser ' + 'requests (Default: 8080).')) do |arg| @webServerPort = arg end end end def appMain(files) @rc.configure(self, 'global') @rc.configure(@mhi, 'global.log') webServer = WebServer.new @rc.configure(webServer, 'global') @rc.configure(webServer, 'webd') # Set some config variables if corresponding data was provided via the # command line. webServer.port = @port if @port webServer.uriFile = @uriFile webServer.webServerPort = @webServerPort if @webServerPort webServer.daemonize = @daemonize webServer.pidFile = @pidFile debug('', "pidFile 1: #{@pidFile}") webServer.start 0 end end end TaskJuggler-3.8.1/lib/taskjuggler/daemon/000077500000000000000000000000001473026623400203075ustar00rootroot00000000000000TaskJuggler-3.8.1/lib/taskjuggler/daemon/Daemon.rb000066400000000000000000000066641473026623400220530ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = Daemon.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/MessageHandler' class TaskJuggler # This class provides the basic functionality to turn the current process # into a background process (daemon). To use it, derive you main class from # this class and call the start() method. class Daemon include MessageHandler attr_accessor :pidFile, :daemonize def initialize # You can set this flag to false to prevent the program from # disconnecting from the current terminal. This is useful for debugging # purposes. @daemonize = true # Save the PID of the running daemon as number into this file. @pidFile = nil end # Call this method to turn the process into a background process. def start return 0 unless @daemonize # Fork and have the parent exit if (pid = fork) == -1 fatal('first_fork_failed', 'First fork failed') elsif !pid.nil? # This is the parent. We can exit now. debug('', "Forked a child process with PID #{pid}") exit! 0 end # Create a new session Process.setsid # Fork again to make sure we lose the controlling terminal if (pid = fork) == -1 fatal('second_fork_failed', 'Second fork failed') elsif !pid.nil? # This is the parent. We can exit now. debug('', "Forked a child process with PID #{pid}") exit! 0 end @pid = Process.pid writePidFile # Change current working directory to the file system root Dir.chdir '/' # Make sure we can create files with any permission File.umask 0 # We no longer have a controlling terminal, so these are useless. $stdin.reopen('/dev/null') $stdout.reopen('/dev/null', 'a') $stderr.reopen($stdout) info('daemon_pid', "The process is running as daemon now with PID #{@pid}") 0 end # This method may provide some cleanup functionality in the future. You # better call it before you exit. def stop if @pidFile begin File.delete(@pidFile) rescue warning('cannot_delete_pidfile', "Cannote delete the PID file (#{@pidFile}): #{$!}") end info('daemon_deleted_pidfile', "PID file #{@pidFile} deleted") end end private def writePidFile if @pidFile # Prepend the current working dir to @pidFile unless it's already an # absolute path. The working dir is changed to '/' later. We need the # absolute name to be able to delete it on exit again. if @pidFile[0] != '/' @pidFile = File.join(Dir.getwd, @pidFile) end # If requested, write the PID of the daemon to the specified file. begin File.open(@pidFile, 'w') do |f| f.puts @pid end rescue warning('cannot_save_pidfile', "Cannot write PID to #{@pidFile}") end info('daemon_wrote_pidfile', "PID file #{@pidFile} written with PID #{@pid}") end end end end TaskJuggler-3.8.1/lib/taskjuggler/daemon/DaemonConnector.rb000066400000000000000000000070631473026623400237200ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = DaemonConnector.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'drb' require 'drb/acl' require 'taskjuggler/MessageHandler' class TaskJuggler module DaemonConnectorMixin include MessageHandler def connectDaemon unless @authKey error('missing_auth_key', <<'EOT' You must set an authentication key in the configuration file. Create a file named .taskjugglerrc or taskjuggler.rc that contains at least the following lines. Replace 'your_secret_key' with some random character sequence. _global: authKey: your_secret_key EOT ) end uri = "druby://#{@host}:#{@port}" if @port == 0 # If the @port is configured to 0, we need to read the URI to connect # to the server from the .tj3d.uri file that has been generated by the # server. begin uri = File.read(@uriFile).chomp rescue error('tjc_port_0', 'The server port is configured to be 0, but no ' + ".tj3d.uri file can be found: #{$!}") end end debug('', "DRb URI determined as #{uri}") # We try to play it safe here. The client also starts a DRb server, so # we need to make sure it's constricted to localhost only. We require # the DRb server for the standard IO redirection to work. $SAFE = 1 unless @unsafeMode DRb.install_acl(ACL.new(%w[ deny all allow 127.0.0.1 ])) DRb.start_service('druby://127.0.0.1:0') debug('', 'DRb service started') broker = nil begin # Get the ProjectBroker object from the tj3d. broker = DRbObject.new_with_uri(uri) # Client and server should always come from the same Gem. Since we # restict communication to localhost, that's probably not a problem. if (check = broker.apiVersion(@authKey, 1)) < 0 error('tjc_too_old', 'This client is too old for the server. Please ' + 'upgrade to a more recent version of the software.') elsif check == 0 error('tjc_auth_fail', 'Authentication failed. Please check your authentication ' + 'key to match the server key.') end debug('', "Connection with report broker on #{uri} established") rescue => e # If we ended up here due to a previously reported TjRuntimeError, we # just pass it through. if e.is_a?(TjRuntimeError) raise TjRuntimeError, $! end error('tjc_srv_not_responding', "TaskJuggler server (tj3d) on URI '#{uri}' is not " + "responding:\n#{$!}") end broker end def disconnectDaemon DRb.stop_service end end class DaemonConnector include DaemonConnectorMixin def initialize(authKey, host, port, uri) @authKey = authKey @host = host @port = port @uri = uri @unsafeMode = true @broker = connectDaemon end def disconnect disconnectDaemon @broker = nil end def getProject(projectId) @broker.getProject(@authKey, projectId) end def getProjectList @broker.getProjectList(@authKey) end end end TaskJuggler-3.8.1/lib/taskjuggler/daemon/ProcessIntercom.rb000066400000000000000000000126141473026623400237570ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = ProcessIntercom.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/AppConfig' require 'taskjuggler/Log' require 'taskjuggler/MessageHandler' class TaskJuggler module ProcessIntercomIface include MessageHandler # This function catches all unhandled exceptions in the passed block. def trap begin MessageHandlerInstance.instance.trapSetup = true res = yield MessageHandlerInstance.instance.trapSetup = false res rescue => e # Any exception here is a fatal error. We try hard to terminate the DRb # thread and then exit the program. begin fatal('pi_crash_trap', "#{e}\n#{e.backtrace.join("\n")}\n\n" + "#{'*' * 79}\nYou have triggered a bug in " + "#{AppConfig.softwareName} version #{AppConfig.version}!\n" + "Please see the user manual on how to get this bug fixed!\n" + "#{'*' * 79}\n") rescue RuntimeError @server.terminate return false end end end def terminate(authKey) return false unless @server.checkKey(authKey, 'terminate') trap { @server.terminate } end def connect(authKey, stdout, stderr, stdin, silent) return false unless @server.checkKey(authKey, 'connect') trap { @server.connect(stdout, stderr, stdin, silent) } end def disconnect(authKey) return false unless @server.checkKey(authKey, 'disconnect') trap { @server.disconnect } end end module ProcessIntercom include MessageHandler def initIntercom # This is the authentication key that clients will need to provide to # execute DRb methods. @authKey = generateAuthKey # This flag will be set to true by DRb method calls to terminate the # process. @terminate = false # This mutex is locked while a client is connected. @clientConnection = Mutex.new # This lock protects the @timerStart @timeLock = Monitor.new # The time stamp of the last client interaction. @timerStart = nil end def terminate debug('', 'Terminating on external request') @terminate = true end def connect(stdout, stderr, stdin, silent) # Set the client lock. @clientConnection.lock debug('', 'Rerouting ProjectServer standard IO to client') # Make sure that all output to STDOUT and STDERR is sent to the client. # Input is read from the client STDIN. We save a copy of the old file # handles so we can restore then later again. @stdout = $stdout @stderr = $stderr @stdin = $stdin $stdout = stdout if stdout $stderr = stderr if stdout $stdin = stdin if stdin Log.silent = silent Term::ANSIColor.coloring = !silent debug('', 'IO is now routed to the client') true end def disconnect debug('', 'Restoring IO') Log.silent = true $stdout = @stdout if @stdout $stderr = @stderr if @stderr $stdin = @stdin if @stdin debug('', 'Standard IO has been restored') # Release the client lock @clientConnection.unlock true end def generateAuthKey rand(1000000000).to_s end def checkKey(authKey, command) if authKey == @authKey debug('', "Accepted authentication key for command '#{command}'") else warning('auth_key_rejected', "Rejected wrong authentication key #{authKey}" + "for command '#{command}'") return false end true end # This function must be called after each client interaction to restart the # client connection timer. def restartTimer @timeLock.synchronize do debug('', 'Reseting client connection timer') @timerStart = Time.new end end # Check if the client interaction timer has already expired. def timerExpired? res = nil @timeLock.synchronize do # We should see client interaction every 2 minutes. res = (Time.new > @timerStart + 2 * 60) end res end # This method starts a new thread and waits for the @terminate variable to # be true. If that happens, it waits for the @clientConnection lock or # forces an exit after the timeout has been reached. It shuts down the DRb # server. def startTerminator Thread.new do loop do if @terminate # We wait for the client to propery disconnect. In case this does # not happen, we'll wait for the timeout and exit anyway. restartTimer while @clientConnection.locked? && !timerExpired? do sleep 1 end if timerExpired? warning('drb_timeout_shutdown', 'Shutting down DRb server due to timeout') else debug('', 'Shutting down the DRb server') end DRb.stop_service break else sleep 1 end end end end end end TaskJuggler-3.8.1/lib/taskjuggler/daemon/ProjectBroker.rb000066400000000000000000000501161473026623400234120ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = ProjectBroker.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'monitor' require 'thread' require 'drb' require 'drb/acl' require 'taskjuggler/daemon/Daemon' require 'taskjuggler/daemon/ProjectServer' require 'taskjuggler/TjTime' require 'taskjuggler/MessageHandler' class TaskJuggler # The ProjectBroker is the central object of the TaskJuggler daemon. It can # manage multiple scheduled projects that it keeps in separate sub # processes. Requests to a specific project will be redirected to the # specific ProjectServer process. Projects can be added or removed. Adding # an already existing one (identified by project ID) will replace the old # one as soon as the new one has been scheduled successfully. # # The daemon uses DRb to communicate with the client and it's sub processes. # The communication is restricted to localhost. All remote commands require # an authentication key. # # Currently only tj3client can be used to communicate with the TaskJuggler # daemon. class ProjectBroker < Daemon include MessageHandler attr_accessor :authKey, :port, :uriFile, :projectFiles, :logStdIO def initialize super # We don't have a default key. The user must provice a key in the config # file. Otherwise the daemon will not start. @authKey = nil # The default TCP/IP port. ASCII code decimals for 'T' and 'J'. @port = 8474 # The name of the URI file. @uriFile = nil # A list of loaded projects as Array of ProjectRecord objects. @projects = [] # We operate with multiple threads so we need a Monitor to synchronize # the access to the list. @projects.extend(MonitorMixin) # A list of the initial projects. Array with Array of files names. @projectFiles = [] # This Queue is used to load new projects. The DRb thread pushes load # requests that the housekeeping thread will then perform. @projectsToLoad = Queue.new # Set this flag to true to have standard IO logged into files. There # will be seperate set of files for each process. @logStdIO = !@daemonize # This flag will be set to true to terminate the daemon. @terminate = false end def start # To ensure a certain level of security, the user must provide an # authentication key to authenticate the client to this server. unless @authKey error('pb_no_auth_key', <<'EOT' You must set an authentication key in the configuration file. Create a file named .taskjugglerrc or taskjuggler.rc that contains at least the following lines. Replace 'your_secret_key' with some random character sequence. _global: authKey: your_secret_key EOT ) end # In daemon mode, we fork twice and only the 2nd child continues here. super() debug('', "Starting project broker") # Setup a DRb server to handle the incomming requests from the clients. brokerIface = ProjectBrokerIface.new(self) begin $SAFE = 1 DRb.install_acl(ACL.new(%w[ deny all allow 127.0.0.1 ])) @uri = DRb.start_service("druby://127.0.0.1:#{@port}", brokerIface).uri info('daemon_uri', "TaskJuggler daemon is listening on #{@uri}") rescue error('port_in_use', "Cannot listen on port #{@port}: #{$!}") end if @port == 0 && @uriFile # If the port is set to 0 (any port) we save the ProjectBroker URI in # the file .tj3d.uri. tj3client will look for it. begin File.open(@uriFile, 'w') { |f| f.write @uri } rescue error('cannot_write_uri', "Cannot write URI file #{@uriFile}: #{$!}") end end # If project files were specified on the command line, we add them here. i = 0 @projectFiles.each do |project| @projectsToLoad.push(project) end # Start a Thread that waits for the @terminate flag to be set and does # some other work asynchronously. startHousekeeping debug('', 'Shutting down ProjectBroker DRb server') DRb.stop_service # If we have created a URI file, we need to delete it again. if @port == 0 && @uriFile begin File.delete(@uriFile) rescue error('cannot_delete_uri', "Cannot delete URI file .tj3d.uri: #{$!}") end end info('daemon_terminated', 'TaskJuggler daemon terminated') end # All remote commands must provide the proper authentication key. Usually # the client and the server get this secret key from the same # configuration file. def checkKey(authKey, command) if authKey == @authKey debug('', "Accepted authentication key for command '#{command}'") else warning('wrong_auth_key', "Rejected wrong authentication key '#{authKey}' " + "for command '#{command}'") return false end true end # This command will initiate the termination of the daemon. def stop debug('', 'Terminating on client request') # Shut down the web server if we've started one. if @webServer @webServer.stop end # Send termination signal to all ProjectServer instances @projects.synchronize do @projects.each { |p| p.terminateServer } end # Setting the @terminate flag to true will case the terminator Thread to # call DRb.stop_service @terminate = true super end # Generate a table with information about the loaded projects. def status if @projects.empty? "No projects registered\n" else format = " %3s | %-25s | %-14s | %s | %-20s\n" out = sprintf(format, 'No.', 'Project ID', 'Status', 'M', 'Loaded since') out += " #{'-' * 4}+#{'-' * 27}+#{'-' * 16}+#{'-' * 3}+#{'-' * 20}\n" @projects.synchronize do i = 0 @projects.each do |project| out += project.to_s(format, i += 1) end end out end end # Adding a new project or replacing an existing one. The command waits # until the project has been loaded or the load has failed. def addProject(cwd, args, stdOut, stdErr, stdIn, silent) # We need some tag to identify the ProjectRecord that this project was # associated to. Just use a large enough random number. tag = rand(9999999999999) debug('', "Pushing #{tag} to load Queue") @projectsToLoad.push(tag) # Now we have to wait until the project shows up in the @projects # list. We use our tag to identify the right entry. pr = nil while pr.nil? @projects.synchronize do @projects.each do |p| if p.tag == tag pr = p break end end end # The wait in this loop should be pretty short and we don't want to # miss IO from the ProjectServer process. sleep 0.1 unless pr end debug('', "Found tag #{tag} in list of loaded projects with URI " + "#{pr.uri}") res = false # Open a DRb connection to the ProjectServer process begin projectServer = DRbObject.new(nil, pr.uri) rescue warning('pb_cannot_get_ps', "Can't get ProjectServer object: #{$!}") return false end begin # Hook up IO from requestor to the ProjectServer process. projectServer.connect(pr.authKey, stdOut, stdErr, stdIn, silent) rescue warning('pb_cannot_connect_io', "Can't connect IO: #{$!}") return false end # Ask the ProjectServer to load the files in _args_ into the # ProjectServer. begin res = projectServer.loadProject(pr.authKey, [ cwd, *args ]) rescue warning('pb_load_failed', "Loading of project failed: #{$!}") return false end # Disconnect the IO from the ProjectServer and close the DRb connection. begin projectServer.disconnect(pr.authKey) rescue warning('pb_cannot_disconnect_io', "Can't disconnect IO: #{$!}") return false end res end def removeProject(indexOrId) @projects.synchronize do # Find all projects with the IDs in indexOrId and mark them as # :obsolete. if /^[0-9]$/.match(indexOrId) index = indexOrId.to_i - 1 if index >= 0 && index < @projects.length # If we have marked the project as obsolete, we return false to # indicate the double remove. return false if p.state == :obsolete @projects[index].state = :obsolete return true end else @projects.each do |p| if indexOrId == p.id # If we have marked the project as obsolete, we return false to # indicate the double remove. return false if p.state == :obsolete p.state = :obsolete return true end end end end false end # Return the ProjectServer URI and authKey for the project with project ID # _projectId_. def getProject(projectId) # Find the project with the ID args[0]. project = nil @projects.synchronize do @projects.each do |p| project = p if p.id == projectId && p.state == :ready end end if project.nil? debug('', "No project with ID #{projectId} found") return [ nil, nil ] end [ project.uri, project.authKey ] end # Reload all projects that have modified files and are not already being # reloaded. def update @projects.synchronize do @projects.each do |project| if project.modified && !project.reloading project.reloading = true @projectsToLoad.push(project.files) end end end end # Return a list of IDs of projects that are in state :ready. def getProjectList list = [] @projects.synchronize do @projects.each do |project| list << project.id if project.state == :ready end end list end def report(projectId, reportId) uri, key = getProject(projectId) end # This is a callback from the ProjectServer process. It's used to update # the current state of the ProjectServer in the ProjectRecord list. # _projectKey_ is the authentication key for that project. It is used to # idenfity the entry in the ProjectRecord list to be updated. def updateState(projectKey, filesOrId, state, modified) result = false if filesOrId.is_a?(Array) files = filesOrId # Use the name of the master files for now. id = files[1] elsif filesOrId.is_a?(String) id = filesOrId files = nil else id = files = nil end @projects.synchronize do @projects.each do |project| # Don't accept updates for already obsolete entries. next if project.state == :obsolete debug('', "Updating state for #{id} to #{state}") # Only update the record that has the matching key if project.authKey == projectKey project.id = id if id # An Array of [ workingDir, tjpFile, ... other tji files ] project.files = files if files # If the state is being changed from something to :ready, this is # now the current project for the project ID. if state == :ready && project.state != :ready # Mark other project records with same project ID as obsolete @projects.each do |p| if p != project && p.id == id p.state = :obsolete debug('', "Marking entry with ID #{id} as obsolete") end end project.readySince = TjTime.new end # Failed ProjectServers are terminated automatically. We can't # reach them any more. project.uri = nil if state == :failed project.state = state project.modified = modified result = true break end end end result end private def startHousekeeping begin cntr = 0 loop do if @terminate # Give the caller a chance to properly terminate the connection. sleep 0.5 break elsif !@projectsToLoad.empty? loadProject(@projectsToLoad.pop) else # Send termination command to all obsolute ProjectServer # objects. To minimize the locking of @projects we collect the # obsolete items first. termList = [] @projects.synchronize do @projects.each do |p| if p.state == :obsolete termList << p elsif p.state == :failed # Start removal of entries that didn't parse. p.state = :obsolete end end end # And then send them a termination command. termList.each { |p| p.terminateServer } # Check every 10 seconds that the ProjectServer processes are # still alive. If not, remove them from the list. if (cntr += 1) > 10 @projects.synchronize do @projects.each do |p| unless p.ping termList << p unless termList.include?(p) end end end cntr = 0 end # The housekeeping thread rarely needs to so something. Make # sure it's sleeping most of the time. sleep 1 # Remove the obsolete records from the @projects list. @projects.synchronize do @projects.delete_if { |p| termList.include?(p) } end end end rescue => exception # TjRuntimeError exceptions are simply passed through. if exception.is_a?(TjRuntimeError) raise TjRuntimeError, $! end fatal('pb_housekeeping_error', "ProjectBroker housekeeping error: #{$!}") end end def loadProject(tagOrProject) if tagOrProject.is_a?(Array) tag = rand(9999999999999) project = tagOrProject # The 2nd element of the Array is the *.tjp file name. debug('', "Loading project #{tagOrProject[1]} with tag #{tag}") else tag = tagOrProject project = nil debug('', "Loading project for tag #{tag}") end pr = ProjectRecord.new(tag) ps = ProjectServer.new(@authKey, project, @logStdIO) # The ProjectServer can be reached via this DRb URI pr.uri = ps.uri # Method calls must be authenticated with this key pr.authKey = ps.authKey # Add the ProjectRecord to the @projects list @projects.synchronize do @projects << pr end end end # This class is the DRb interface for ProjectBroker. We only want to expose # these methods for remote access. class ProjectBrokerIface include MessageHandler def initialize(broker) @broker = broker end # Check the authentication key and the client/server version match. # The following return values can be generated: # 0 : authKey does not match # 1 : client and server versions match # -1 : client and server versions don't match def apiVersion(authKey, version) return 0 unless @broker.checkKey(authKey, 'apiVersion') version == 1 ? 1 : -1 end # This function catches all unhandled exceptions in the passed block. def trap begin yield rescue => e # TjRuntimeError exceptions are simply passed through. if e.is_a?(TjRuntimeError) raise TjRuntimeError, $! end # Any exception here is a fata error. We try hard to terminate the DRb # thread and then exit the program. begin fatal('pb_crash_trap', "#{e}\n#{e.backtrace.join("\n")}\n\n" + "#{'*' * 79}\nYou have triggered a bug in " + "#{AppConfig.softwareName} version #{AppConfig.version}!\n" + "Please see the user manual on how to get this bug fixed!\n" + "#{'*' * 79}\n") rescue RuntimeError @broker.stop end end end def command(authKey, cmd, args) return false unless @broker.checkKey(authKey, cmd) trap do case cmd when :status @broker.status when :stop @broker.stop when :addProject # To pass the DRbObject as separate arguments we need to convert it # into a real Array again. @broker.addProject(*Array.new(args)) when :removeProject @broker.removeProject(args) when :getProject @broker.getProject(args) when :update @broker.update else fatal('unknown_command', 'Unknown command #{cmd} called') end end end def getProjectList(authKey) return false unless @broker.checkKey(authKey, 'getProjectList') trap { @broker.getProjectList } end def getProject(authKey, id) return false unless @broker.checkKey(authKey, 'getProject') debug('', "PID: #{id} Class: #{id.class}") trap { @broker.getProject(id) } end def updateState(authKey, projectKey, id, status, modified) return false unless @broker.checkKey(authKey, 'updateState') trap { @broker.updateState(projectKey, id, status, modified) } end end # The ProjectRecord objects are used to manage the loaded projects. There is # one entry for each project in the @projects list. class ProjectRecord < Monitor include MessageHandler attr_accessor :authKey, :uri, :files, :id, :state, :readySince, :modified, :reloading attr_reader :tag def initialize(tag) # Before we know the project ID we use this tag to uniquely identify the # project. @tag = tag # Array of [ workingDir, tjp file, ... tji files ] @files = nil # The authentication key for the ProjectServer process. @authKey = nil # The DRb URI where the ProjectServer process is listening. @uri = nil # The ID of the project. @id = nil # The state of the project. :new, :loading, :ready, :failed # and :obsolete are supported. @state = :new # A time stamp when the project became ready for service. @readySince = nil # True if any of the input files have been modified after the load. @modified = false # True if the reload has already been triggered. @reloading = false @projectServer = nil end def ping return true unless @uri debug('', "Sending ping to ProjectServer #{@uri}") begin @projectServer = DRbObject.new(nil, @uri) unless @projectServer @projectServer.ping(@authKey) rescue warning('ping_failed', "Ping failed: #{$!}") return false end true end # Call this function to terminate the ProjectServer. def terminateServer return unless @uri begin debug('', "Sending termination request to ProjectServer #{@uri}") @projectServer = DRbObject.new(nil, @uri) unless @projectServer @projectServer.terminate(@authKey) rescue error('proj_serv_term_failed', "Termination of ProjectServer failed: #{$!}") end @uri = nil end # This is used to generate the status table. def to_s(format, index) sprintf(format, index, @id, @state, @modified ? '*' : ' ', @readySince ? @readySince.to_s('%Y-%m-%d %H:%M:%S') : '') end end end TaskJuggler-3.8.1/lib/taskjuggler/daemon/ProjectServer.rb000066400000000000000000000361171473026623400234410ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = ProjectServer.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'drb' require 'drb/acl' require 'monitor' require 'taskjuggler/daemon/ProcessIntercom' require 'taskjuggler/daemon/ReportServer' require 'taskjuggler/MessageHandler' require 'taskjuggler/TaskJuggler' require 'taskjuggler/TjTime' class TaskJuggler # The ProjectServer objects are created from the ProjectBroker to handle the # data of a particular project. Each ProjectServer runs in a separate # process that is forked-off in the constructor. Any action such as adding # more files or generating a report will cause the process to fork again, # creating a ReportServer object. This way the initially loaded project can # be modified but the original version is always preserved for subsequent # calls. Each ProjectServer process has a unique secret authentication key # that only the ProjectBroker knows. It will pass it with the URI of the # ProjectServer to the client to permit direct access to the ProjectServer. class ProjectServer include ProcessIntercom attr_reader :authKey, :uri def initialize(daemonAuthKey, projectData = nil, logConsole = false) @daemonAuthKey = daemonAuthKey @projectData = projectData # Since we are still in the ProjectBroker process, the current DRb # server is still the ProjectBroker DRb server. @daemonURI = DRb.current_server.uri # Used later to store the DRbObject of the ProjectBroker. @daemon = nil initIntercom @logConsole = logConsole @pid = nil @uri = nil # A reference to the TaskJuggler object that holds the project data. @tj = nil # The current state of the project. @state = :new # A time stamp when the last @state update happened. @stateUpdated = TjTime.new # A lock to protect access to @state @stateLock = Monitor.new # A Queue to asynchronously generate new ReportServer objects. @reportServerRequests = Queue.new # A list of active ReportServer objects @reportServers = [] @reportServers.extend(MonitorMixin) @lastPing = TjTime.new # We've started a DRb server before. This will continue to live somewhat # in the child. All attempts to create a DRb connection from the child # to the parent will end up in the child again. So we use a Pipe to # communicate the URI of the child DRb server to the parent. The # communication from the parent to the child is not affected by the # zombie DRb server in the child process. rd, wr = IO.pipe if (@pid = fork) == -1 fatal('ps_fork_failed', 'ProjectServer fork failed') elsif @pid.nil? # This is the child if @logConsole # If the Broker wasn't daemonized, log stdout and stderr to PID # specific files. $stderr.reopen("tj3d.ps.#{$$}.stderr", 'w') $stdout.reopen("tj3d.ps.#{$$}.stdout", 'w') end begin $SAFE = 1 DRb.install_acl(ACL.new(%w[ deny all allow 127.0.0.1 ])) iFace = ProjectServerIface.new(self) begin @uri = DRb.start_service('druby://127.0.0.1:0', iFace).uri debug('', "Project server is listening on #{@uri}") rescue error('ps_cannot_start_drb', "ProjectServer can't start DRb: #{$!}") end # Send the URI of the newly started DRb server to the parent process. rd.close wr.write @uri wr.close # Start a Thread that waits for the @terminate flag to be set and does # other background tasks. startTerminator # Start another Thread that will be used to fork-off ReportServer # processes. startHousekeeping # Cleanup the DRb threads DRb.thread.join debug('', 'Project server terminated') exit 0 rescue => exception # TjRuntimeError exceptions are simply passed through. if exception.is_a?(TjRuntimeError) raise TjRuntimeError, $! end error('ps_cannot_start_drb', "ProjectServer can't start DRb: #{$!}") end else # This is the parent Process.detach(@pid) wr.close @uri = rd.read rd.close end end # Wait until the project load has been finished. The result is true if the # project scheduled without errors. Otherwise the result is false. # _args_ is an Array of Strings. The first element is the working # directory. The second one is the master project file (.tjp file). # Additionally a list of optional .tji files can be provided. def loadProject(args) dirAndFiles = args.dup # The first argument is the working directory Dir.chdir(args.shift) # Save a time stamp of when the project file loading started. @modifiedCheck = TjTime.new updateState(:loading, dirAndFiles, false) begin @tj = TaskJuggler.new # Make sure that trace reports get CSV formats included so there # reports can be generated on request. @tj.generateTraces = true # Parse all project files unless @tj.parse(args, true) warning('parse_failed', "Parsing of #{args.join(' ')} failed") updateState(:failed, nil, false) @terminate = true return false end # Then schedule the project unless @tj.schedule warning('schedule_failed', "Scheduling of project #{@tj.projectId} failed") updateState(:failed, @tj.projectId, false) @terminate = true return false end rescue TjRuntimeError updateState(:failed, nil, false) @terminate = true return false end # Great, everything went fine. We've got a project to work with. updateState(:ready, @tj.projectId, false) debug('', "Project #{@tj.projectId} loaded") restartTimer true end # Return the name of the loaded project or nil. def getProjectName return nil unless @tj restartTimer @tj.projectName end # Return a list of the HTML reports defined for the project. def getReportList return [] unless @tj && (project = @tj.project) list = [] project.reports.each do |report| unless report.get('formats').empty? list << [ report.fullId, report.name ] end end restartTimer list end # This function triggers the creation of a new ReportServer process. It # will return the URI and the authentication key of this new server. def getReportServer # ReportServer objects only make sense for successfully scheduled # projects. return [ nil, nil ] unless @state == :ready # The ReportServer will be created asynchronously in another Thread. To # find it in the @reportServers list, we create a unique tag to identify # it. tag = rand(99999999999999) debug('', "Pushing #{tag} onto report server request queue") @reportServerRequests.push(tag) # Now wait until the new ReportServer shows up in the list. reportServer = nil while reportServer.nil? @reportServers.synchronize do @reportServers.each do |rs| reportServer = rs if rs.tag == tag end end # It should not take that long, so we use a short idle time here. sleep 0.1 if reportServer.nil? end debug('', "Got report server with URI #{reportServer.uri} for " + "tag #{tag}") restartTimer [ reportServer.uri, reportServer.authKey ] end # This function is called regularly by the ProjectBroker process to check # that the ProjectServer is still operating properly. def ping # Store the time stamp. If we don't get the ping for some time, we # assume the ProjectBroker has died. @lastPing = TjTime.new # Now also check our ReportServers if they are still there. If not, we # can remove them from the @reportServers list. @reportServers.synchronize do deadServers = [] @reportServers.each do |rs| unless rs.ping deadServers << rs end end @reportServers.delete_if { |rs| deadServers.include?(rs) } end end private # Update the _state_, _id_ and _modified_ state of the project locally and # remotely. def updateState(state, filesOrId, modified) begin @daemon = DRbObject.new(nil, @daemonURI) unless @daemon @daemon.updateState(@daemonAuthKey, @authKey, filesOrId, state, modified) rescue => exception # TjRuntimeError exceptions are simply passed through. if exception.is_a?(TjRuntimeError) raise TjRuntimeError, $! end error('cannot_update_daemon_state', "Can't update state with daemon: #{$!}") end @stateLock.synchronize do @state = state @stateUpdated = TjTime.new @modified = modified @modifiedCheck = TjTime.new end end def startHousekeeping Thread.new do begin loop do # Exit this thread if the @terminate flag is set. break if @terminate # Was the project data provided during object creation? # Then we load the data here. if @projectData loadProject(@projectData) @projectData = nil end # Check every 60 seconds if the input files have been modified. # Don't check if we already know it has been modified. if @stateLock.synchronize { @state == :ready && !@modified && @modifiedCheck + 60 < TjTime.new } # Reset the timer @stateLock.synchronize { @modifiedCheck = TjTime.new } if @tj.project.inputFiles.modified? debug('', "Project #{@tj.projectId} has been modified") updateState(:ready, @tj.projectId, true) end end # Check for pending requests for new ReportServers. unless @reportServerRequests.empty? tag = @reportServerRequests.pop debug('', "Popped #{tag}") # Create an new entry for the @reportServers list. rsr = ReportServerRecord.new(tag) debug('', "RSR created") # Create a new ReportServer object that runs as a separate # process. The constructor will tell us the URI and authentication # key of the new ReportServer. rs = ReportServer.new(@tj, @logConsole) rsr.uri = rs.uri rsr.authKey = rs.authKey debug('', "Adding ReportServer with URI #{rsr.uri} to list") # Add the new ReportServer to our list. @reportServers.synchronize do @reportServers << rsr end end # Some state changing operations are not atomic. Since the client # can die during the transaction, the server might hang in some # states. Here we define timeout for each state. If the timeout is # not 0 and exceeded, we immediately terminate the process. timeouts = { :new => 30, :loading => 15 * 60, :failed => 60, :ready => 0 } if timeouts[@state] > 0 && TjTime.new - @stateUpdated > timeouts[@state] error('state_timeout', "Reached timeout for state #{@state}. Terminating.") end # If we have not received a ping from the ProjectBroker for 2 # minutes, we assume it has died and terminate as well. if TjTime.new - @lastPing > 180 # Since the abort via error() is not thread safe, we issue a # warning and abort manually. warning('daemon_heartbeat_lost', 'Heartbeat from daemon lost. Terminating.') exit 1 end sleep 1 end rescue => exception # TjRuntimeError exceptions are simply passed through. if exception.is_a?(TjRuntimeError) raise TjRuntimeError, $! end # Make sure we get a backtrace for this thread. fatal('ps_housekeeping_error', "ProjectServer housekeeping error: #{$!}") end end end end # This is the DRb call interface of the ProjectServer class. All functions # must be authenticated with the proper key. class ProjectServerIface include ProcessIntercomIface def initialize(server) @server = server end def loadProject(authKey, args) return false unless @server.checkKey(authKey, 'loadProject') trap { @server.loadProject(args) } end def getProjectName(authKey) return false unless @server.checkKey(authKey, 'getReportServer') trap { @server.getProjectName } end def getReportList(authKey) return false unless @server.checkKey(authKey, 'getReportServer') trap { @server.getReportList } end def getReportServer(authKey) return false unless @server.checkKey(authKey, 'getReportServer') trap { @server.getReportServer } end def ping(authKey) return false unless @server.checkKey(authKey, 'ping') trap { @server.ping } true end end # This class stores the information about a ReportServer that was created by # the ProjectServer. class ReportServerRecord include MessageHandler attr_reader :tag attr_accessor :uri, :authKey def initialize(tag) # A random tag to uniquely identify the entry. @tag = tag # The URI of the ReportServer process. @uri = nil # The authentication key of the ReportServer. @authKey = nil # The DRbObject of the ReportServer. @reportServer = nil end # Send a ping to the ReportServer process to check that it is still # functioning properly. If not, it has probably terminated and we can # remove it from the list of active ReportServers. def ping return true unless @uri debug('', "Sending ping to ReportServer #{@uri}") begin @reportServer = DRbObject.new(nil, @uri) unless @reportServer @reportServer.ping(@authKey) rescue => exception # TjRuntimeError exceptions are simply passed through. if exception.is_a?(TjRuntimeError) raise TjRuntimeError, $! end # ReportServer processes terminate on request of their clients. Not # responding to a ping is a normal event. debug('', "ReportServer (#{@uri}) has terminated") return false end true end end end TaskJuggler-3.8.1/lib/taskjuggler/daemon/ReportServer.rb000066400000000000000000000152651473026623400233070ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = ReportServer.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/daemon/ProcessIntercom' require 'taskjuggler/TjException' require 'taskjuggler/TjTime' class TaskJuggler class ReportServer include ProcessIntercom attr_reader :uri, :authKey def initialize(tj, logConsole = false) initIntercom @pid = nil @uri = nil # A reference to the TaskJuggler object that holds the project data. @tj = tj # This is set to the ID(s) of the reports that should be generated. @reportId = 'unknown' @lastPing = TjTime.new # We've started a DRb server before. This will continue to live somewhat # in the child. All attempts to create a DRb connection from the child # to the parent will end up in the child again. So we use a Pipe to # communicate the URI of the child DRb server to the parent. The # communication from the parent to the child is not affected by the # zombie DRb server in the child process. rd, wr = IO.pipe if (@pid = fork) == -1 fatal('rs_fork_failed', 'ReportServer fork failed') elsif @pid.nil? if logConsole # If the Broker wasn't daemonized, log stdout and stderr to PID # specific files. $stderr.reopen("tj3d.rs.#{$$}.stderr", 'w') $stdout.reopen("tj3d.rs.#{$$}.stdout", 'w') end begin # This is the child $SAFE = 1 DRb.install_acl(ACL.new(%w[ deny all allow 127.0.0.1 ])) iFace = ReportServerIface.new(self) begin uri = DRb.start_service('druby://127.0.0.1:0', iFace).uri debug('', "Report server is listening on #{uri}") rescue error('rs_cannot_start_drb', "ReportServer can't start DRb: #{$!}") end # Send the URI of the newly started DRb server to the parent process. rd.close wr.write uri wr.close # Start a Thread that waits for the @terminate flag to be set and does # other background tasks. startTerminator startWatchDog # Cleanup the DRb threads DRb.thread.join debug('', 'Report server terminated') exit 0 rescue => exception # TjRuntimeError exceptions are simply passed through. if exception.is_a?(TjRuntimeError) raise TjRuntimeError, $! end error('rs_unexp_excp', "ReportServer caught unexpected exception: #{$!}") end else Process.detach(@pid) # This is the parent wr.close @uri = rd.read rd.close end end def ping @lastPing = TjTime.new end def addFile(file) begin @tj.parseFile(file, :reportPropertiesFile) rescue TjRuntimeError return false end restartTimer true end def generateReport(id, regExpMode, formats, dynamicAttributes) info('generating_report', "Generating report #{id}") startTime = Time.now @reportId = id begin if (ok = @tj.generateReport(id, regExpMode, formats, dynamicAttributes)) info('report_id_generated', "Report #{id} generated in #{Time.now - startTime} seconds") else error('report_generation_failed', "Report generation of #{id} failed") end rescue TjRuntimeError return false end restartTimer ok end def listReports(id, regExpMode) info('listing_report_id', "Listing report #{id}") begin if (ok = @tj.listReports(id, regExpMode)) debug('', "Report list for #{id} generated") else error('repor_list_comp_failed', "Report list compilation of #{id} failed") end rescue TjRuntimeError return false end restartTimer ok end def checkTimeSheet(sheet) info('check_time_sheet', "Checking time sheet #{sheet}") @reportId = 'timesheet' begin ok = @tj.checkTimeSheet(sheet) debug('', "Time sheet #{sheet} is #{ok ? '' : 'not '}ok") rescue TjRuntimeError return false end restartTimer ok end def checkStatusSheet(sheet) info('check_status_sheet', "Checking status sheet #{sheet}") @reportId = 'statussheet' begin ok = @tj.checkStatusSheet(sheet) debug('', "Status sheet #{sheet} is #{ok ? '' : 'not '}ok") rescue TjRuntimeError return false end restartTimer ok end private def startWatchDog Thread.new do loop do if TjTime.new - @lastPing > 120 # Since the abort via error() is not thread safe, we issue a # warning and abort manually. warning('ps_heartbeat_lost', "Report server (Project #{@tj.project['projectid']} " + "report #{@reportId}) lost heartbeat " + 'from ProjectServer. Terminating.') exit 1 end sleep 30 end end end end class ReportServerIface include ProcessIntercomIface def initialize(server) @server = server end def ping(authKey) return false unless @server.checkKey(authKey, 'addFile') trap { @server.ping } end def addFile(authKey, file) return false unless @server.checkKey(authKey, 'addFile') trap { @server.addFile(file) } end def generateReport(authKey, reportId, regExpMode, formats, dynamicAttributes) return false unless @server.checkKey(authKey, 'generateReport') trap do @server.generateReport(reportId, regExpMode, formats, dynamicAttributes) end end def listReports(authKey, reportId, regExpMode) return false unless @server.checkKey(authKey, 'generateReport') trap { @server.listReports(reportId, regExpMode) } end def checkTimeSheet(authKey, sheet) return false unless @server.checkKey(authKey, 'checkTimeSheet') trap { @server.checkTimeSheet(sheet) } end def checkStatusSheet(authKey, sheet) return false unless @server.checkKey(authKey, 'checkStatusSheet') trap { @server.checkStatusSheet(sheet) } end end end TaskJuggler-3.8.1/lib/taskjuggler/daemon/ReportServlet.rb000066400000000000000000000166521473026623400234660ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = ReportServlet.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'webrick' require 'taskjuggler/MessageHandler' require 'taskjuggler/RichText' require 'taskjuggler/HTMLDocument' require 'taskjuggler/URLParameter' require 'taskjuggler/daemon/DaemonConnector' class TaskJuggler class ReportServlet < WEBrick::HTTPServlet::AbstractServlet def initialize(config, options) super @authKey = options[0] @host = options[1] @port = options[2] @uri = options[3] end def self.get_instance(config, options) self.new(config, options) end def do_GET(req, res) debug('', "Serving URL #{req}") @req = req @res = res begin # WEBrick is returning the query elements as FormData objects. We must # use to_s to explicitely convert them to String objects. projectId = req.query['project'].to_s debug('', "Project ID: #{projectId}") reportId = req.query['report'].to_s debug('', "Report ID: #{reportId}") if projectId.empty? || reportId.empty? debug('', "Project welcome page requested") generateWelcomePage(projectId) else debug('', "Report #{reportId} of project #{projectId} requested") attributes = req.query['attributes'] || '' unless attributes.empty? attributes = URLParameter.decode(attributes) end debug('', "Attributes: #{attributes}") generateReport(projectId, reportId, attributes) end rescue error('get_req_failed', "Cannot serve GET request: #{req}\n#{$!}") end end private def connectToBroker begin broker = DaemonConnector.new(@authKey, @host, @port, @uri) rescue error('cannot_connect_broker', "Cannot connect to the TaskJuggler daemon: #{$!}\n" + "Please make sure you have tj3d running and listening " + "on port #{@port} or URI '#{@uri}'.") end broker end def generateReport(projectId, reportId, attributes) broker = connectToBroker # Request the Project credentials from the ProbjectBroker. begin @ps_uri, @ps_authKey = broker.getProject(projectId) rescue error('cannot_get_project_server', "Cannot get project server for ID #{projectId}: #{$!}") end if @ps_uri.nil? error('ps_uri_nil', "No project with ID #{projectId} loaded") end # Get the responsible ReportServer that can generate the report. begin @projectServer = DRbObject.new(nil, @ps_uri) @rs_uri, @rs_authKey = @projectServer.getReportServer(@ps_authKey) @reportServer = DRbObject.new(nil, @rs_uri) rescue error('cannot_get_report_server', "Cannot get report server: #{$!}") end # Create two StringIO buffers that will receive the $stdout and $stderr # text from the report server. This buffer will contain the generated # report as HTML encoded text. They will be send via DRb, so we have to # extend them with DRbUndumped. stdOut = StringIO.new('') stdOut.extend(DRbUndumped) stdErr = StringIO.new('') stdErr.extend(DRbUndumped) begin @reportServer.connect(@rs_authKey, stdOut, stdErr, $stdin, true) rescue => exception # TjRuntimeError exceptions are simply passed through. if exception.is_a?(TjRuntimeError) raise TjRuntimeError, $! end error('rs_io_connect_failed', "Can't connect IO: #{$!}") end # Ask the ReportServer to generate the reports with the provided ID. retVal = true begin retVal = @reportServer.generateReport(@rs_authKey, reportId, false, nil, attributes) rescue stdOut.rewind stdErr.rewind error('rs_generate_report_failed', "Report server crashed: #{$!}\n#{stdErr.read}\n#{stdOut.read}") end # Disconnect the ReportServer begin @reportServer.disconnect(@rs_authKey) rescue error('rs_io_disconnect_failed', "Can't disconnect IO: #{$!}") end # And send a termination request. begin @reportServer.terminate(@rs_authKey) rescue error('report_server_term_failed', "Report server termination failed: #{$!}") end @reportServer = nil broker.disconnect @res['content-type'] = 'text/html' if retVal # To read the $stdout of the ReportServer we need to rewind the buffer # and then read the full text. stdOut.rewind @res.body = stdOut.read else stdErr.rewind error('get_req_stderr', "Error while parsing attribute definition:\n-8<-\n" + "#{attributes}\n->8-\n#{stdErr.read}") end end def generateWelcomePage(projectId) broker = connectToBroker begin projects = broker.getProjectList rescue error('cannot_get_project_list', "Cannot get project list from daemon: #{$!}") end text = "== Welcome to the TaskJuggler Project Server ==\n----\n" projects.each do |id| if id == projectId # Show the list of reports for this project. text << "* [/taskjuggler #{getProjectName(id)}]\n" reports = getReportList(id) if reports.empty? text << "** This project has no reports defined.\n" else reports.each do |reportId, reportName| text << "** [/taskjuggler?project=#{id};report=#{reportId} " + "#{reportName}]\n" end end else # Just show a link to open the report list. text << "* [/taskjuggler?project=#{id} #{getProjectName(id)}]\n" end end # We no longer need the broker. broker.disconnect rt = RichText.new(text) rti = rt.generateIntermediateFormat rti.sectionNumbers = false page = HTMLDocument.new page.generateHead("The TaskJuggler Project Server") page.html << rti.to_html @res['content-type'] = 'text/html' @res.body = page.to_s end def getProjectName(id) broker = connectToBroker uri, authKey = broker.getProject(id) return nil unless uri projectServer = DRbObject.new(nil, uri) return nil unless projectServer res = projectServer.getProjectName(authKey) broker.disconnect res end def getReportList(id) broker = connectToBroker uri, authKey = broker.getProject(id) return [] unless uri projectServer = DRbObject.new(nil, uri) return [] unless projectServer res = projectServer.getReportList(authKey) broker.disconnect res end def error(id, message) @res.status = 412 @res.body = "ERROR: #{message}" @res['content-type'] = 'text/plain' MessageHandlerInstance.instance.error(id, message) end def debug(id, message) MessageHandlerInstance.instance.debug(id, message) end end end TaskJuggler-3.8.1/lib/taskjuggler/daemon/WebServer.rb000066400000000000000000000076041473026623400225470ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = WebServer.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'webrick' require 'taskjuggler/AppConfig' require 'taskjuggler/daemon/Daemon' require 'taskjuggler/daemon/WelcomePage' require 'taskjuggler/daemon/ReportServlet' class TaskJuggler # The WebServer class provides a self-contained HTTP server that can serve # HTML versions of Report objects that are generated on the fly. class WebServer < Daemon include DaemonConnectorMixin attr_accessor :authKey, :port, :uriFile, :webServerPort # Create a web server object that runs in a separate thread. def initialize super # For security reasons, this will probably not change. All DRb # operations are limited to localhost only. The client and the sever # must have access to the identical file system. @host = '127.0.0.1' # The default TCP/IP port. ASCII code decimals for 'T' and 'J'. @port = 8474 # The file with the server URI in case port is 0. @uriFile = File.join(Dir.getwd, '.tj3d.uri') # We don't have a default key. The user must provice a key in the config # file. Otherwise the daemon will not start. @authKey = nil # Reference to WEBrick object. @webServer = nil # Port used by the web server @webServerPort = 8080 Kernel.trap('TERM') do debug('webserver_term_signal', 'TERM signal received. Exiting...') # When the OS sends us a TERM signal, we try to exit gracefully. stop end end def start # In daemon mode, we fork twice and only the 2nd child continues here. super() debug('', "Starting web server") config = { :Port => @webServerPort } begin @server = WEBrick::HTTPServer.new(config) info('webserver_port', "Web server is listening on port #{@webServerPort}") rescue fatal('webrick_start_failed', "Cannot start WEBrick: #{$!}") end begin @server.mount('/', WelcomePage, nil) rescue fatal('welcome_page_mount_failed', "Cannot mount WEBrick welcome page: #{$!}") end begin @server.mount('/taskjuggler', ReportServlet, [ @authKey, @host, @port, @uri ]) rescue fatal('broker_page_mount_failed', "Cannot mount WEBrick broker page: #{$!}") end # Serve some directories via the FileHandler servlet. %w( css icons scripts ).each do |dir| unless (fullDir = AppConfig.dataDirs("data/#{dir}")[0]) error('dir_not_found', <<"EOT" Cannot find the #{dir} directory. This is usually the result of an improper TaskJuggler installation. If you know the directory, you can use the TASKJUGGLER_DATA_PATH environment variable to specify the location. The variable should be set to the path without the /data at the end. Multiple directories must be separated by colons. EOT ) end begin @server.mount("/#{dir}", WEBrick::HTTPServlet::FileHandler, fullDir) rescue fatal('dir_mount_failed', "Cannot mount directory #{dir} in WEBrick: #{$!}") end end # Install signal handler to exit gracefully on CTRL-C. intHandler = Kernel.trap('INT') do stop end begin @server.start rescue fatal('web_server_error', "Web server error: #{$!}") end end # Stop the web server. def stop if @server @server.shutdown @server = nil end super end end end TaskJuggler-3.8.1/lib/taskjuggler/daemon/WelcomePage.rb000066400000000000000000000041511473026623400230250ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = WelcomePage.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'webrick' require 'taskjuggler/Tj3Config' require 'taskjuggler/HTMLDocument' class TaskJuggler class WelcomePage < WEBrick::HTTPServlet::AbstractServlet def initialize(config, *options) super end def self.get_instance(config, options) self.new(config, *options) end def do_GET(req, res) @req = req @res = res begin generateWelcomePage #rescue end end private def generateWelcomePage() text = <<"EOT" == Welcome to TaskJuggler == ---- This is the welcome page of your TaskJuggler built-in web server. To access your loaded TaskJuggler projects, click [/taskjuggler here]. If you are seeing this page instead of the site you expected, please contact the administrator of the site involved. Try sending mail to . Although this site is running the TaskJuggler software it almost certainly has no other connection to the TaskJuggler project, so please do not send mail about this site or its contents to the TaskJuggler authors. If you do, your message will be ignored. You can use the following links to learn more about TaskJuggler: * [#{AppConfig.contact} The TaskJuggler web site] * [#{AppConfig.contact+ "/tj3/manual/index.html"} User Manual] ---- #{AppConfig.softwareName} v#{AppConfig.version} - Copyright (c) #{AppConfig.copyright.join(', ')} by #{AppConfig.authors.join(', ')} EOT rt = RichText.new(text) rti = rt.generateIntermediateFormat rti.sectionNumbers = false page = HTMLDocument.new page.generateHead("Welcome to TaskJuggler") page.html << rti.to_html @res['content-type'] = 'text/html' @res.body = page.to_s end end end TaskJuggler-3.8.1/lib/taskjuggler/deep_copy.rb000066400000000000000000000051121473026623400213370ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = deep_copy.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # # This file extends some Ruby core classes to add deep-copying support. I'm # aware of the commonly suggested method using Marshal: # # class Object # def deep_copy # Marshal.load(Marshal.dump(self)) # end # end # # But just because I program in Ruby, I don't have to write bloatware. It's # like taking a trip to the moon and back to shop groceries at the store # around the corner. I'm not sure if I need more special cases than Array and # Hash, but this file works for me. # # In certain cases the full deep copy may not be desired. To preserve # references to objects, you need to overload deep_clone and handle the # special cases. Alternatively, an object can be frozen to prevent deep # copies. class Object # This is a variant of Object#clone that returns a deep copy of an object. def deep_clone # We can't clone frozen objects. So just return a reference to them. # Built-in classed can't be cloned either. The check below is probably # cheaper than the frequent (hiddent) exceptions from those objects. return self if frozen? || nil? || is_a?(Integer) || is_a?(Float) || is_a?(TrueClass) || is_a?(FalseClass) || is_a?(Symbol) # In case we have loops in our graph, we return references, not # deep-copied objects. if RUBY_VERSION < '1.9.0' return @clonedObject if instance_variables.include?('@clonedObject') else return @clonedObject if instance_variables.include?(:@clonedObject) end # Clone the current Object (shallow copy with internal state) begin @clonedObject = clone rescue TypeError return self end # Recursively copy all instance variables. @clonedObject.instance_variables.each do |var| val = instance_variable_get(var).deep_clone @clonedObject.instance_variable_set(var, val) end if kind_of?(Array) @clonedObject.collect! { |x| x.deep_clone } elsif kind_of?(Hash) @clonedObject.each { |key, val| store(key, val.deep_clone) } end # Remove the @clonedObject again. if RUBY_VERSION < '1.9.0' remove_instance_variable('@clonedObject') else remove_instance_variable(:@clonedObject) end end end TaskJuggler-3.8.1/lib/taskjuggler/reports/000077500000000000000000000000001473026623400205425ustar00rootroot00000000000000TaskJuggler-3.8.1/lib/taskjuggler/reports/AccountListRE.rb000066400000000000000000000104611473026623400235500ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = AccountListRE.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/reports/TableReport' require 'taskjuggler/reports/ReportTable' require 'taskjuggler/TableColumnDefinition' require 'taskjuggler/LogicalExpression' class TaskJuggler # This specialization of TableReport implements a task listing. It # generates a list of tasks that can optionally have the allocated resources # nested underneath each task line. class AccountListRE < TableReport # Create a new object and set some default values. def initialize(report) super @table = ReportTable.new @table.selfcontained = report.get('selfcontained') @table.auxDir = report.get('auxdir') end # Generate the table in the intermediate format. def generateIntermediateFormat super # Prepare the account list. accountList = PropertyList.new(@project.accounts) accountList.setSorting(@report.get('sortAccounts')) accountList.query = @report.project.reportContexts.last.query accountList = filterAccountList(accountList, @report.get('hideAccount'), @report.get('rollupAccount'), @report.get('openNodes')) accountList.sort! # Generate the table header. @report.get('columns').each do |columnDescr| adjustColumnPeriod(columnDescr) generateHeaderCell(columnDescr) end if (costAccount = @report.get('costaccount')) && (revenueAccount = @report.get('revenueaccount')) # We are in balance mode. First show the cost and then the revenue # accounts and then the total balance. costAccountList = PropertyList.new(@project.accounts) costAccountList.clear costAccountList.setSorting(@report.get('sortAccounts')) costAccountList.query = @report.project.reportContexts.last.query revenueAccountList = PropertyList.new(@project.accounts) revenueAccountList.clear revenueAccountList.setSorting(@report.get('sortAccounts')) revenueAccountList.query = @report.project.reportContexts.last.query # Split the account list into a cost and a revenue account list. accountList.each do |account| if account.isChildOf?(costAccount) || account == costAccount costAccountList << account elsif account.isChildOf?(revenueAccount) || account == revenueAccount revenueAccountList << account end end # Make sure that the top-level cost and revenue accounts are always # included in the lists. unless costAccountList.include?(costAccount) costAccountList << costAccount end unless revenueAccountList.include?(revenueAccount) revenueAccountList << revenueAccount end generateAccountList(costAccountList, 0, nil) generateAccountList(revenueAccountList, costAccountList.length, nil) # To generate a total line that reports revenue minus cost, we create # a temporary Account object that adopts the cost and revenue # accounts. totalAccount = Account.new(@report.project, '0', "Total", nil) totalAccount.adopt(costAccount) totalAccount.adopt(revenueAccount) totalAccountList = PropertyList.new(@project.accounts) totalAccountList.clear totalAccountList.setSorting(@report.get('sortAccounts')) totalAccountList.query = @report.project.reportContexts.last.query totalAccountList << totalAccount generateAccountList(totalAccountList, costAccountList.length + revenueAccountList.length, nil) @report.project.removeAccount(totalAccount) else # We are not in balance mode. Simply show a list of all reports that # aren't filtered out. generateAccountList(accountList, 0, nil) end end end end TaskJuggler-3.8.1/lib/taskjuggler/reports/CSVFile.rb000066400000000000000000000164711473026623400223330ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = CSVFile.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/UTF8String' class TaskJuggler # This is a very lightweight version of the Ruby library class CSV. That class # changed significantly from 1.8 to 1.9 and is a compatibility nightmare. # Hence we use our own class. class CSVFile attr_reader :data # At construction time you need to specify the +data+ container. This is an # Array of Arrays that holds the table. Optionally, you can specify a # +separator+ and a +quote+ string for the CSV file. def initialize(data = nil, separator = ';', quote = '"') @data = data if !separator.nil? && '."'.include?(separator) raise "Illegal separator: #{separator}" end @separator = separator raise "Illegal quote: #{quote}" if quote == '.' @quote = quote end # Use this function to write the table into a CSV file +fileName+. '.' can # be used to write to $stdout. def write(fileName) if (fileName == '.') file = $stdout else file = File.open(fileName, 'w') end file.write(to_s) file.close unless fileName == '.' end # Read the data as Array of Arrays from a CSV formated file +fileName+. In # case '.' is used for the +fileName+ the data is read from $stdin. def read(fileName) if (fileName == '.') file = $stdin else file = File.open(fileName, 'r') end parse(file.read) file.close unless fileName == '.' @data end # Convert the CSV data into a CSV formatted String. def to_s raise "No seperator defined." if @separator.nil? s = '' @data.each do |line| first = true line.each do |field| # Don't output a separator before the first field of the line. if first first = false else s << @separator end s << marshal(field) end s << "\n" end s end # Read the data as Array of Arrays from a CSV formated String +str+. def parse(str) @data = [] state = :startOfRecord fields = field = quoted = nil # Make sure the input is terminated with a record end. str += "\n" unless str[-1] == ?\n # If the user hasn't defined a separator, we try to detect it. @separator = detectSeparator(str) unless @separator line = 1 str.each_utf8_char do |c| #puts "c: #{c} State: #{state}" case state when :startOfRecord # This will store the fields of a record fields = [] state = :startOfField redo when :startOfField field = '' quoted = false if c == @quote # We've found the start of a quoted field. state = :inQuotedField quoted = true elsif c == @separator || c == "\n" # We've found an empty field field = nil state = :fieldEnd redo else # We've found the first character of an unquoted field field << c state = :inUnquotedField end when :inQuotedField # We are processing the content of a quoted field if c == @quote # This could be then end of the field or a quoted quote. state = :quoteInQuotedField else # We've found a normal character of the quoted field field << c line += 1 if c == "\n" end when :quoteInQuotedField # We are processing a quoted quote or the end of a quoted field if c == @quote # We've found a quoted quote field << c state = :inQuotedField elsif c == @separator || c == "\n" state = :fieldEnd redo else raise "Line #{line}: Unexpected character #{c} in cell: #{field}" end when :inUnquotedField # We are processing an unquoted field if c == @separator || c == "\n" # We've found the end of a unquoted field state = :fieldEnd redo else # A normal character of an unquoted field field << c end when :fieldEnd # We've completed processing a field. Add the field to the list of # fields. Convert Integers and Floats in native types. fields << unMarshal(field, quoted) if c == "\n" # The field end is an end of a record as well. state = :recordEnd redo else # Get the next field. state = :startOfField end when :recordEnd # We've found the end of a record. Add fields to the @data # structure. @data << fields # Look for a new record. state = :startOfRecord line += 1 else raise "Unknown state #{state}" end end unless state == :startOfRecord if state == :inQuotedField raise "Line #{line}: Unterminated quoted cell: #{field}" else raise "Line #{line}: CSV error in state #{state}: #{field}" end end @data end # Utility function that tries to convert a String into a native type that # is supported by the CSVFile generator. If no native type is found, the # input String _str_ will be returned unmodified. nil is returned as nil. def CSVFile.strToNative(str) if str.nil? nil elsif /^[-+]?\d+$/ =~ str # field is an Integer str.to_i elsif /^[-+]?\d*\.?\d+([eE][-+]?\d+)?$/ =~ str # field is a Float str.to_f else # Everything else is kept as String str end end private # This function is used to properly quote @quote and @separation # characters contained in the +field+. def marshal(field) if field.nil? '' elsif field.is_a?(Integer) || field.is_a?(Float) # Numbers don't have to be quoted. field.to_s else # Duplicate quote characters. f = field.gsub(Regexp.new(@quote), "#{@quote * 2}") # Enclose the field in quote characters @quote + f.to_s + @quote end end # Convert the String _field_ into a native Ruby type. If field was # _quoted_, the result is always the String. def unMarshal(field, quoted) # Quoted Strings and nil are returned verbatim. if quoted || field.nil? field else # Unquoted fields are inspected for special types CSVFile.strToNative(field) end end def detectSeparator(str) # Pick the separator that was found the most. best = nil bestCount = 0 "\t;:".each_char do |c| if best.nil? || str.count(c) > bestCount best = c bestCount = str.count(c) end end return best end end end TaskJuggler-3.8.1/lib/taskjuggler/reports/ChartPlotter.rb000066400000000000000000000345601473026623400235120ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = ChartPlotter.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/Painter' class TaskJuggler class ChartPlotterError < RuntimeError end class ChartPlotter def initialize(width, height, data) # +------------------------------------------------ # | ^ # | topMargin | legendGap # | v <-> # | | -x- foo # |<-leftMargin->| -x- bar # | | <-legend # | | Width----> # | +------------ # | ^ <-rightMargin-> # | bottomMargin| # | v # +------------------------------------------------ # <-----------------canvasWidth--------------------> # The width of the canvas area @width = width # The height of the canvas area @height = height # The raw data to plot as loaded from the CSV file. @data = data # The margins between the graph plotting area and the canvas borders. @topMargin = 30 @bottomMargin = 30 @leftMargin = 70 @rightMargin = (@width * 0.382).to_i @legendGap = 20 @markerWidth = 20 @markerX = @width - @rightMargin + @legendGap @markerGap = 5 @labelX = @markerX + @markerWidth + @markerGap @labelHeight = 24 # The location of the 0/0 point of the graph plotter. @x0 = @leftMargin @y0 = @height - @bottomMargin @labels = [] @yData = [] @xData = nil @dataType = nil @xMinDate = nil @xMaxDate = nil @yMinDate = nil @yMaxDate = nil @yMinVal = nil @yMaxVal = nil end # Create the chart as Painter object. def generate analyzeData calcChartGeometry @painter = Painter.new(@width, @height) do |pa| drawGrid(pa) 0.upto(@yData.length - 1) do |ci| # Compute a unique and distinguishable color for each data set. We # primarily use the hue value of the HSV color space for this. It # has 6 main colors each 60 degrees apart from each other. After the # first 360 round, we shift the angle by 60 / round so we get a # different color set than in the previous round. Additionally, the # saturation is decreased with each data set. color = Painter::Color.new( (60 * (ci % 6) + (60 / (1 + ci / 6))) % 360, 255 - (ci / 8), 230, :hsv) drawDataGraph(pa, ci, color) drawLegendEntry(pa, ci, color) end end end def to_svg @painter.to_svg end private def analyzeData # Convert the @data from a line list into a column list. Each element of # the list is an Array for the other dimension. columns = [] ri = 0 @data.each do |row| ci = 0 row.each do |col| columns << [] if ri == 0 if ci >= columns.length error("Row #{ri} contains more elements than the header row") end columns[ci][ri] = col ci += 1 end ri += 1 end header = true line = 1 columns[0].each do |date| if header unless date == "Date" error("First column must have a 'Date' header instead of '#{date}'") end header = false else unless date.is_a?(TjTime) error("First colum (#{date}) of line #{line} must be all dates") end @xMinDate = date if @xMinDate.nil? || date < @xMinDate @xMaxDate = date if @xMaxDate.nil? || date > @xMaxDate end line += 1 end unless @xMinDate && @xMaxDate error("First column does not contain valid dates.") end # Add the xData values. @xData = columns[0][1..-1] # Now eleminate columns that contain invalid data. 1.upto(columns.length - 1) do |colIdx| col = columns[colIdx] badCol = false col[1..-1].each do |cell| if cell if cell.is_a?(TjTime) if @dataType && @dataType != :date error("Column #{colIdx} contains non-date (#{cell}). " + "The columns will be ignored.") badCol = true break else @dataType = :date end @yMinDate = cell if @yMinDate.nil? || cell < @yMinDate @yMaxDate = cell if @yMaxDate.nil? || cell > @yMaxDate elsif cell.is_a?(Integer) || cell.is_a?(Float) if @dataType && @dataType != :number error("Column #{colIdx} contains non-number (#{cell}). " + "The columns will be ignored.") badCol = true break else @dataType = :number end @yMinVal = cell if @yMinVal.nil? || cell < @yMinVal @yMaxVal = cell if @yMaxVal.nil? || cell > @yMaxVal else error("Column #{colIdx} contains invalid data (#{cell}). " + "The columns will be ignored.") badCol = true break end else # Ignore missing values next unless cell end end # Store the header of the column. It will be used as label. @labels << col[0] # Add the column values as another entry into the yData list. @yData << col[1..-1] unless badCol end if @dataType.nil? || @yData.empty? error("Columns don't contain any valid dates.") end end def calcChartGeometry # The size of the X-axis in pixels xAxisPixels = @width - (@rightMargin + @leftMargin) fm = Painter::FontMetrics.new # Width of the date label in pixels @dateLabelWidth = fm.width('LiberationSans', 10.0, '2000-01-01') # Height of the date label in pixels @labelHeight = fm.height('LiberationSans', 10.0) # Distance between 2 labels in pixels labelPadding = 10 # The number of labels that fit underneath the X-axis @noXLabels = (xAxisPixels / (@dateLabelWidth + labelPadding)).floor # The number of labels that fit along the Y-axis yAxisPixels = @height - (@topMargin + @bottomMargin) @noYLabels = (yAxisPixels / (@labelHeight + labelPadding)).floor @noYLabels = 10 if @noYLabels > 10 # Set min X date to midnight time. @xMinDate = @xMinDate.midnight # Ensure that we have at least a @noXLabels days long interval. minInterval = 60 * 60 * 24 * @noXLabels @xMaxDate = @xMinDate + minInterval if @xMaxDate - @xMinDate < minInterval case @dataType when :date # Set min Y date to midnight time. @yMinDate = @yMinDate.midnight # Ensure that we have at least a @noYLabels days long interval. minInterval = 60 * 60 * 24 * @noYLabels if @yMaxDate - @yMinDate < minInterval @yMaxDate = @yMinDate + minInterval end when :number # If all Y values are the same, we ensure that the Y-axis starts at 0 # to provide a sense of scale. @yMinVal = 0 if @yMinVal == @yMaxVal # Ensure that Y-axis has at least a range of @noYLabels if @yMaxVal - @yMinVal < @noYLabels @yMaxVal = @yMinVal + @noYLabels end else raise "Unsupported dataType: #{@dataType}" end end def xLabels(p) # The time difference between two labels. labelInterval = (@xMaxDate - @xMinDate) / @noXLabels # We want the first label to show left-aligned with the Y-axis. Calc the # date for the first label. date = @xMinDate + labelInterval / 2 p.group(:font_family => 'LiberationSans, Arial', :font_size => 10.0, :stroke => p.color(:black), :stroke_width => 1, :fill => p.color(:black)) do |gp| @noXLabels.times do |i| x = xDate2c(date) gp.text(x - @dateLabelWidth / 2, y2c(-5 - @labelHeight), date.to_s('%Y-%m-%d'), :stroke_width => 0) #gp.rect(x - @dateLabelWidth / 2, y2c(-5 - @labelHeight), # @dateLabelWidth, @labelHeight, :fill => gp.color(:white)) gp.line(x, y2c(0), x, y2c(-4)) date += labelInterval end end end def yLabels(p) case @dataType when :date return unless @yMinDate && @yMaxDate yInterval = @yMaxDate - @yMinDate # The time difference between two labels. labelInterval = yInterval / @noYLabels date = @yMinDate + labelInterval / 2 p.group(:font_family => 'LiberationSans, Arial', :font_size => 10.0, :stroke => p.color(:black), :stroke_width => 1, :fill => p.color(:black)) do |gp| @noYLabels.times do |i| y = yDate2c(date) gp.text(0, y + @labelHeight / 2 - 2, date.to_s('%Y-%m-%d'), :stroke_width => 0) gp.line(x2c(-4), y, @width - @rightMargin, y) date += labelInterval end end when :number return unless @yMinVal && @yMaxVal yInterval = (@yMaxVal - @yMinVal).to_f fm = Painter::FontMetrics.new # The value difference between two labels. labelInterval = yInterval / @noYLabels # We'd like to have the labels to only show number starting with # single most significant digit that read 1, 2 or 5. If necessary, we # increase the labelInterval to the next matching number and reduce # the number of y labels accordingly. factor = 10 ** Math.log10(labelInterval).floor msd = (labelInterval / factor).ceil if msd == 3 || msd == 4 msd = 5 elsif msd > 5 msd = 10 end labelInterval = msd * factor @noYLabels = ((@yMaxVal - @yMinVal) / labelInterval).floor val = @yMinVal + labelInterval p.group(:font_family => 'LiberationSans, Arial', :font_size => 10.0, :stroke => p.color(:black), :stroke_width => 1, :fill => p.color(:black)) do |gp| @noYLabels.times do |i| y = yNum2c(val) labelText = val.to_s labelWidth = fm.width('LiberationSans', 10.0, labelText) gp.text(@leftMargin - 7 - labelWidth, y + @labelHeight / 2 - 3, labelText, :stroke_width => 0) gp.line(x2c(-4), y, @width - @rightMargin, y) val += labelInterval end end else raise "Unsupported dataType #{@dataType}" end end # Convert a chart X coordinate to a canvas X coordinate. def x2c(x) @x0 + x end # Convert a chart Y coordinate to a canvas Y coordinate. def y2c(y) @y0 - y end # Convert a date to a chart X coordinate. def xDate2c(date) x2c(((date - @xMinDate) * (@width - (@leftMargin + @rightMargin))) / (@xMaxDate - @xMinDate)) end # Convert a Y data date to a chart Y coordinate. def yDate2c(date) y2c(((date - @yMinDate) * (@height - (@topMargin + @bottomMargin))) / (@yMaxDate - @yMinDate)) end # Convert a Y data value to a chart Y coordinate. def yNum2c(number) y2c(((number - @yMinVal) * (@height - (@topMargin + @bottomMargin))) / (@yMaxVal - @yMinVal)) end def drawGrid(painter) painter.group(:stroke => painter.color(:black), :font_size => 11) do |p| p.line(x2c(0), y2c(0), x2c(@width - (@leftMargin + @rightMargin)), y2c(0)) p.line(x2c(0), y2c(0), x2c(0), y2c(@height - (@topMargin + @bottomMargin))) yLabels(p) xLabels(p) end end def drawDataGraph(painter, ci, color) values = @yData[ci] painter.group(:stroke_width => 3, :stroke => color, :fill => color) do |p| lastX = lastY = nil # Plot markers for each x/y data pair of the set and connect the # dots with lines. If a y value is nil, the line will be # interrupted. values.length.times do |i| if values[i] xc = xDate2c(@xData[i]) if values[i].is_a?(TjTime) yc = yDate2c(values[i]) else yc = yNum2c(values[i]) end p.line(lastX, lastY, xc, yc) if lastY setMarker(p, ci, xc, yc) lastX = xc lastY = yc end end end end def drawLegendEntry(painter, ci, color) painter.group(:stroke_width => 3, :stroke => color, :fill => color, :font_size => 11) do |p| # Add the marker to the legend labelY = @topMargin + @labelHeight / 2 + ci * (@labelHeight + 4) markerY = labelY + (@labelHeight + 4) / 2 setMarker(p, ci, @markerX + @markerWidth / 2, markerY) p.line(@markerX, markerY, @markerX + @markerWidth, markerY) p.text(@labelX, labelY + @labelHeight, @labels[ci], :stroke => p.color(:black), :stroke_width => 0, :fill => p.color(:black)) end end def setMarker(p, type, x, y) r = 4 case (type / 5) % 5 when 0 # Diamond points = [ [ x - r, y ], [ x, y + r ], [ x + r, y ], [ x, y - r ], [ x - r, y ] ] p.polyline(points) when 1 # Square rr = (r / Math.sqrt(2.0)).to_i p.rect(x - rr, y - rr, 2 * rr, 2 * rr) when 2 # Triangle Down points = [ [ x - r, y - r ], [ x, y + r ], [ x + r, y - r ], [ x - r, y - r ] ] p.polyline(points) when 3 # Triangle Up points = [ [ x - r, y + r ], [ x, y - r ], [ x + r, y + r ], [ x - r, y + r ] ] p.polyline(points) else p.circle(x, y, r) end end def error(msg) raise ChartPlotterError, msg end end end TaskJuggler-3.8.1/lib/taskjuggler/reports/CollisionDetector.rb000066400000000000000000000127131473026623400245200ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = CollisionDetector.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/reports/HTMLGraphics' class TaskJuggler class CollisionDetector include HTMLGraphics def initialize(width, height) @width = width @height = height # The zones are stored as Arrays of line segments. Horizontal blocks are # stored separately from vertical blocks. Blocked segments for a # particular x coordinate are stored in @vLines, for y coordinates in # @hLines. Each entry is an Array of [ start, end ] values that describe # the blocked segments of that particular line. Start and end point are # part of the segment. A listed segment will not be overwritten during # routing. @hLines = Array.new(@height) { |i| i = [] } @vLines = Array.new(@width) { |i| i = [] } end # This function registers an area as don't-cross-zone. The rectangular zone # is described by _x_, _y_, _w_ and _h_. If _horiz_ is true, the zone will # be blocked for horizontal lines, if _vert_ is true the zone will be # blocked for vertical lines. def addBlockedZone(x, y, w, h, horiz, vert) # Clip the input rectangle to fit within the handled area of this router. x = clip(x.to_i, @width - 1) y = clip(y.to_i, @height - 1) w = clip(w.to_i, @width - x) h = clip(h.to_i, @height - y) # We can ignore empty zones. return if w == 0 || h == 0 # Break the rectangle into line segments and add them to the appropriate # line Arrays. if horiz y.upto(y + h - 1) do |i| addSegment(@hLines[i], [ x, x + w - 1 ]) end end if vert x.upto(x + w - 1) do |i| addSegment(@vLines[i], [ y, y + h - 1 ]) end end end # Find out if there is a block at line _pos_ for the start/end coordinates # given by _segment_. If _horizontal_ is true, we are looking for a # horizontal block, otherwise a vertical. def collision?(pos, segment, horizontal) line = (horizontal ? @hLines : @vLines)[pos] # For complex charts, the segment lists can be rather long. We use a # binary search to be fairly efficient. l = 0 u = line.length - 1 while l <= u # Look at the element in the middle between l and u. p = l + ((u - l) / 2).to_i return true if overlaps?(line[p], segment) if segment[0] > line[p][1] # The potential target is above p. Adjust lower bound. l = p + 1 else # The potential target is below p. Adjust upper bound. u = p - 1 end end false end def to_html html = [] # Change this to determine what zones you want to see. if true # Show vertical blocks x = 0 @vLines.each do |line| line.each do |segment| html << lineToHTML(x, segment[0], x, segment[1], 'white') end x += 1 end else # Show horizontal blocks y = 0 @hLines.each do |line| line.each do |segment| html << lineToHTML(segment[0], y, segment[1], y, 'white') end y += 1 end end html end private # Simple utility function to limit _v_ between 0 and _max_. def clip(v, max) v = 0 if v < 0 v = max if v > max v end # This function adds a new segment to the line. In case the new segment # overlaps with or directly attaches to existing segments, these segments # are merged into a single segment. def addSegment(line, newSegment) # Search for overlaping or directly attaching segments in the list. i = 0 while (i < line.length) segment = line[i] if mergeable?(newSegment, segment) # Merge exiting segment into new one merge(newSegment, segment) # Remove the old one from the list and restart with the newly created # one at the same position. line.delete_at(i) next elsif segment[0] > newSegment[1] # Segments are stored in ascending order. If the next segment starts # with a larger value, we insert the new segment before the larger # one. line.insert(i, newSegment) return end i += 1 end # Append new segment line << newSegment end # Return true if the two segments described by _s1_ and _s2_ overlap each # other. A segment is a [ start, end ] Array. The two points are part of the # segment. def overlaps?(s1, s2) (s1[0] <= s2[0] && s2[0] <= s1[1]) || (s2[0] <= s1[0] && s1[0] <= s2[1]) end # Return true if the two segments described by _s1_ and _s2_ overlap each # other or are directly attached to each other. def mergeable?(s1, s2) overlaps?(s1, s2) || (s1[1] + 1 == s2[0]) || (s2[1] + 1 == s1[0]) end # Merge the two segments described by _dst_ and _src_ into _dst_. def merge(dst, seg) dst[0] = seg[0] if seg[0] < dst[0] dst[1] = seg[1] if seg[1] > dst[1] end end end TaskJuggler-3.8.1/lib/taskjuggler/reports/ColumnTable.rb000066400000000000000000000050311473026623400232730ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = ColumnTable.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/reports/ReportTable' class TaskJuggler # This class is essentially a wrapper around ReportTable that allows us to # embed a ReportTable object as a column of another ReportTable object. Both # ReportTables must have the same number of lines. class ColumnTable < ReportTable attr_writer :viewWidth # Create a new ColumnTable object. def initialize super # The user requested width of the column (chart) @viewWidth = nil # The header will have 2 lines. So, use a smaller font. This should match # the font size used for the GanttChart header. @headerFontSize = 10 # This is an embedded table. @embedded = true end def to_html height = 2 * @headerLineHeight + 1 @lines.each do |line| # Add line height plus 1 pixel padding height += line.height + 1 end # Since we don't know the resulting width of the column, we need to always # add an extra space for the scrollbar. td = XMLElement.new('td', 'rowspan' => "#{2 + @lines.length + 1}", 'style' => 'padding:0px; vertical-align:top;') # Now we generate a 'div' that will contain the nested table. It has a # height that fits all lines but has a maximum width. In case the embedded # table is larger, a scrollbar will appear. We assume that the scrollbar # has a height of SCROLLBARHEIGHT pixels or less. # If there is a user specified with, use it. Otherwise use the # calculated minimum with. width = @viewWidth ? @viewWidth : minWidth td << (scrollDiv = XMLElement.new('div', 'class' => 'tabback', 'style' => 'position:relative; overflow:auto; ' + "width:#{width}px; " + 'margin-top:-1px; margin-bottom:-1px; ' + "height:#{height + SCROLLBARHEIGHT + 2}px;")) scrollDiv << (contentDiv = XMLElement.new('div', 'style' => 'margin: 0px; padding: 0px; position: absolute; top: 0px;' + "left: 0px; width: #{@viewWidth}px; height: #{height}px; ")) contentDiv << super td end end end TaskJuggler-3.8.1/lib/taskjuggler/reports/ExportRE.rb000066400000000000000000000021541473026623400226010ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = ExportRE.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/reports/ReportBase' require 'taskjuggler/reports/TjpExportRE' require 'taskjuggler/reports/MspXmlRE' class TaskJuggler # This specialization of ReportBase implements an export of the # project data in the TJP syntax format. class ExportRE < ReportBase # Create a new object and set some default values. def initialize(report) super(report) end def generateIntermediateFormat super end # Return the project data in TJP syntax format. def to_tjp TjpExportRE.new(@report).to_tjp end # Return the project data in Microsoft Project XML format. def to_mspxml MspXmlRE.new(@report).to_mspxml end end end TaskJuggler-3.8.1/lib/taskjuggler/reports/GanttChart.rb000066400000000000000000000306101473026623400231260ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = GanttChart.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/reports/GanttHeader' require 'taskjuggler/reports/GanttLine' require 'taskjuggler/reports/GanttRouter' require 'taskjuggler/reports/HTMLGraphics' class TaskJuggler # This class represents an abstract (output format independent) Gantt chart. # It provides generator functions that can transform the abstract form into # formats such as HTML or SVG. # The appearance of the chart depend on 3 variable: the report period, # the geometrical width and the scale. The report period is always provided by # the user. In addition the width _or_ the scale can be provided. The # non-provided value will then be calculated. So after the object has been # created, the user must call generateByWidth or generateByResolution. class GanttChart # The height in pixels of a horizontal scrollbar on an HTML page. This # value should be large enough to work for all browsers. SCROLLBARHEIGHT = 20 include HTMLGraphics attr_reader :start, :end, :now, :weekStartsMonday, :header, :width, :scale, :scales, :table, :markdate attr_writer :viewWidth # Create the GanttChart object, but don't do much right now. We still need # more information about the chart before we can actually generate it. _now_ # is the date that should be used as current date. _weekStartsMonday_ is # true if the weeks should start on Mondays instead of Sundays. _table_ is a # reference to the TableReport that the chart is part of. def initialize(now, weekStartsMonday, columnDef, table = nil, markdate = nil) # The start and end dates of the reported interval. @start = nil @end = nil @now = now @columnDef = columnDef @table = table @markdate = markdate # This defines the possible horizontal scales that the Gantt chart can # have. The scales differ in their resolution and the amount of detail # that is displayed. A scale is defined by its name. The _name_ must be # unique and can be used to select the scale. The _stepSize_ defines the # width of a scale step in pixels. The _stepsToFunc_ is a TjTime method # that determines the number of steps between 2 dates. _minTimeOff_ # defines the minimum required length of an time-off interval that is # displayed in this scale. @@scales = [ { 'name' => 'hour', 'stepSize' => 20, 'stepsToFunc' => :hoursTo, 'minTimeOff' => 5 * 60 }, { 'name' => 'day', 'stepSize' => 20, 'stepsToFunc' => :daysTo, 'minTimeOff' => 6 * 60 * 60 }, { 'name' => 'week', 'stepSize' => 20, 'stepsToFunc' => :weeksTo, 'minTimeOff' => 24 * 60 * 60 }, { 'name' => 'month', 'stepSize' => 35, 'stepsToFunc' => :monthsTo, 'minTimeOff' => 5 * 24 * 60 * 60 }, { 'name' => 'quarter', 'stepSize' => 28, 'stepsToFunc' => :quartersTo, 'minTimeOff' => -1 }, { 'name' => 'year', 'stepSize' => 20, 'stepsToFunc' => :yearsTo, 'minTimeOff' => -1 } ] # This points to one of the scales above and marks the current scale. @scale = nil # The height of the chart (without the header) @height = 0 # The width of the chart in pixels. @width = 0 # The width of the view that the chart is presented in. If it's nil, the # view will be adapted to the width of the chart. @viewWidth = nil # True of the week starts on a Monday. @weekStartsMonday = weekStartsMonday # Reference to the GanttHeader object that models the chart header. @header = nil # The GanttLine objects that model the lines of the chart. @lines = [] # The router for dependency lines. @router = nil # This dictionary stores primary task lines indexed by their task. To # handle multiple scenarios, the dictionary stored the lines in an Array. # This is used to generate dependency arrows. @tasks = {} # This is a list of the dependency lines. Each entry is an Array of [x, y] # coordinate pairs. @depArrows = [] # This is the list of arrow heads used for the dependency arrows. It # contains an Array of [ x, y ] coordinates that mark the tip of the # arrow. @arrowHeads = [] end # Add a primary tasks line to the dictonary. _task_ is a reference to the # Task object and _line_ is the corresponding primary ReportTableLine. def addTask(task, line) if @tasks.include?(task) # Append the line to the existing lines. @tasks[task] << line else # Add a new Array for this tasks and store the first line. @tasks[task] = [ line ] end end def generateByWidth(periodStart, periodEnd, width) @start = periodStart @end = periodEnd @width = width # TODO end # Generate the actual chart data based on the report interval specified by # _periodStart_ and _periodEnd_ as well as the name of the requested scale # to be used. This function (or generateByWidth) must be called before any # GanttLine objects are created for this chart. def generateByScale(periodStart, periodEnd, scaleName) @start = periodStart @end = periodEnd @scale = scaleByName(scaleName) @stepSize = @scale['stepSize'] steps = @start.send(@scale['stepsToFunc'], @end) @width = @stepSize * steps @header = GanttHeader.new(@columnDef, self) end # Convert the chart into an HTML representation. def to_html completeChart # The chart is rendered into a cell that extends over the full height of # the table. No other cells for this column will be generated. In case # there is a scrollbar, the table will have an extra line to hold the # scrollbar. td = XMLElement.new('td', 'rowspan' => "#{2 + @lines.length + (hasScrollbar? ? 1 : 0)}", 'style' => 'padding:0px; vertical-align:top;') # Now we generate two 'div's nested into each other. The first div is the # view. It may contain a scrollbar if the second div is wider than the # first one. In case we need a scrollbar The outer div is # SCROLLBARHEIGHT pixels heigher to hold the scrollbar. Unfortunately # this must be a hardcoded value even though the height of the scrollbar # varies from system to system. This value should be good enough for # most systems. td << (scrollDiv = XMLElement.new('div', 'class' => 'tabback', 'style' => 'position:relative; ' + "overflow:auto; " + "width:#{hasScrollbar? ? @viewWidth : @width}px; " + "height:#{@height + (hasScrollbar? ? SCROLLBARHEIGHT : 0)}px;")) scrollDiv << (div = XMLElement.new('div', 'style' => "margin:0px; padding:0px; " + "position:absolute; overflow:hidden; " + "top:0px; left:0px; " + "width:#{@width}px; " + "height:#{@height}px; " + "font-size:10px;")) # Add the header. div << @header.to_html # These are the lines of the chart. @lines.each do |line| div << line.to_html end # This is used for debugging and testing only. #div << @router.to_html # Render the dependency lines. @depArrows.each do |arrow| xx = yy = nil arrow.each do |x, y| if xx div << lineToHTML(xx, yy, x, y, 'depline') end xx = x yy = y end end # And the corresponsing arrow heads. @arrowHeads.each do |x, y| div << arrowHeadToHTML(x, y) end td end # This is a noop function. def to_csv(csv, startColumn) # Can't put a Gantt chart into a CSV file. 0 end # Utility function that convers a date to the corresponding X-position in # the Gantt chart. def dateToX(date) ((@width / (@end - @start)) * (date - @start)).to_i end # This is not a user callable function. It's only meant for use within the # library. def addLine(line) #:nodoc: if @scale.nil? raise "generateByScale or generateByWidth must be called first" end @lines << line end # Returns true if the chart includes a scrollbar. def hasScrollbar? @viewWidth && (@viewWidth < @width) end private # Find the scale with the name _name_ and return a reference to the scale. # If nothing is round an exception is raised. def scaleByName(name) @@scales.each do |scale| return scale if scale['name'] == name end raise "Unknown scale #{name}" end # Calculate the overall height of the chart and generate dependency arrows. def completeChart @lines.each do |line| @height = line.y + line.height if line.y + line.height > @height end # To layout the dependency lines, we use a GanttRouter. We only provide # the start and end coordinates of each line and it will do the layout # and routing. @router = GanttRouter.new(@width, @height) # We don't want horizontal lines to cross the task bars. So we block # these chart zones. Milestones should not be crossed in any direction. @lines.each do |line| line.addBlockedZones(@router) end # Also protect the current date line from other vertical lines. @router.addZone(@header.nowLineX - 1, 0, 3, @height - 1, false, true) # Protect the date set in custom reference line from other vertical lines. if @header.markdateLineX @router.addZone(@header.markdateLineX - 1, 0, 3, @height - 1, false, true) end # Generate the dependency arrows for all visible tasks. @tasks.each do |task, lines| generateDepLines(task, lines) end # Make sure we have exactly one arrow head for each line end point even # if the point is used by multiple lines. @depArrows.each do |line| endPoint = line.last @arrowHeads << endPoint unless @arrowHeads.include?(endPoint) end end # Generate an output format independent description of the dependency lines # for a specific _task_. _lines_ is a list of GanttLines that the tasks are # displayed on. Reports with multiple scenarios have multiple lines per # task. def generateDepLines(task, lines) # Since we need the line and the index we use an index iterator. lines.length.times do |lineIndex| line = lines[lineIndex] scenarioIdx = line.query.scenarioIdx # Generate the dependencies on the start of the task. generateTaskDepLines('startsuccs', task, scenarioIdx, lineIndex, *line.getTask.startDepLineStart) # Generate the dependencies on the end of the task. generateTaskDepLines('endsuccs', task, scenarioIdx, lineIndex, *line.getTask.endDepLineStart) end end # Generate the dependencies on the start or end of the task depending on # _kind_. Use 'startsuccs' for the start and 'endsuccs' for end. _startX_ # and _startY_ are the graphic coordinates for the begin of the arrow # line. _task_ references the Task in question and _scenarioIdx_ the # scenario. _lineIndex_ specifies the line number in the chart. def generateTaskDepLines(kind, task, scenarioIdx, lineIndex, startX, startY) # This is an Array that holds 4 values for # each entry: The x and y coordinates for start and end points. touples = [] task[kind, scenarioIdx].each do |t, onEnd| # Skip inherited dependencies and tasks that are not included in the # chart. if (t.parent && task.hasDependency?(scenarioIdx, kind, t.parent, onEnd)) || !@tasks.include?(t) next end endX, endY = @tasks[t][lineIndex].getTask.send( onEnd ? :endDepLineEnd : :startDepLineEnd) touples << [ startX, startY, endX, endY ] end @depArrows += @router.routeLines(touples) unless touples.empty?() end end end TaskJuggler-3.8.1/lib/taskjuggler/reports/GanttContainer.rb000066400000000000000000000060521473026623400240120ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = GanttContainer.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/reports/HTMLGraphics' class TaskJuggler # The GanttContainer represents a container task (task with sub-tasks). class GanttContainer include HTMLGraphics # The size of the bars in pixels from center to top/bottom. @@size = 5 # Create a GanttContainer object based on the following information: _line_ # is a reference to the GanttLine. _xStart_ is the left edge of the task in # chart coordinates. _xEnd_ is the right edge. The container extends over # the edges due to the shape of the jags. def initialize(lineHeight, xStart, xEnd, y) @lineHeight = lineHeight @start = xStart @end = xEnd @y = y end # Return the point [ x, y ] where task start dependency lines should start # from. def startDepLineStart [ @start, @y + @lineHeight / 2 ] end # Return the point [ x, y ] where task start dependency lines should end at. def startDepLineEnd [ @start - @@size, @y + @lineHeight / 2 ] end # Return the point [ x, y ] where task end dependency lines should start # from. def endDepLineStart [ @end + @@size, @y + @lineHeight / 2 ] end # Return the point [ x, y ] where task end dependency lines should end at. def endDepLineEnd [ @end, @y + @lineHeight / 2 ] end def addBlockedZones(router) # Horizontal block router.addZone(@start - @@size, @y + (@lineHeight / 2) - @@size - 2, @end - @start + 1 + 2 * @@size, 2 * @@size + 5, true, false) # Block for arrowhead. router.addZone(@start - @@size - 9, @y + (@lineHeight / 2) - 7, 10, 15, true, true) # Vertical block for end cap router.addZone(@start - @@size - 2, @y, 2 * @@size + 5, @lineHeight, false, true) router.addZone(@end - @@size - 2, @y, 2 * @@size + 5, @lineHeight, false, true) end # Convert the abstact representation of the GanttContainer into HTML # elements. def to_html xStart = @start.to_i yCenter = (@lineHeight / 2).to_i width = @end.to_i - @start.to_i + 1 html = [] # Invisible trigger frame for tooltips. html << rectToHTML(xStart - @@size, 0, width + 2 * @@size, @lineHeight, 'tj_gantt_frame') # The bar html << rectToHTML(xStart - @@size, yCenter - @@size, width + 2 * @@size, @@size, 'containerbar') # The left jag html << jagToHTML(xStart, yCenter) # The right jag html << jagToHTML(xStart + width, yCenter) html end end end TaskJuggler-3.8.1/lib/taskjuggler/reports/GanttHeader.rb000066400000000000000000000130431473026623400232560ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = GanttHeader.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/reports/GanttHeaderScaleItem' class TaskJuggler # This class stores output format independent information to describe a # GanttChart header. A Gantt chart header consists of 2 lines. The top line # holds the large scale (e. g. the year or month and year) and the lower line # holds the small scale (e. g. week or day). class GanttHeader attr_reader :gridLines, :nowLineX, :cellStartDates, :markdateLineX attr_accessor :height # Create a GanttHeader object and generate the scales for the header. def initialize(columnDef, chart) @columnDef = columnDef @chart = chart @largeScale = [] @smallScale = [] # Positions where chart should be marked with vertical lines that match # the large scale. @gridLines = [] # X coordinate of the "now" line. nil if "now" is off-chart. @nowLineX = nil # X coordinate of the custom "markdate" line with date specified by user. # nil if "markdate" is off-chart. @markdateLineX = nil # The x coordinates and width of the cells created by the small scale. The # values are stored as [ x, w ]. @cellStartDates = [] # The height of the header in pixels. @height = 39 generate end # Convert the header into an HTML format. def to_html div = XMLElement.new('div', 'class' => 'tabback', 'style' => "margin:0px; padding:0px; " + "position:relative; " + "width:#{@chart.width.to_i}px; " + "height:#{@height.to_i}px; " + "font-size:#{(@height / 4).to_i}px; ") @largeScale.each { |s| div << s.to_html } @smallScale.each { |s| div << s.to_html } div end private # Call genHeaderScale with the right set of parameters (depending on the # selected scale) for the lower and upper header line. def generate # The 2 header lines are separated by a 1 pixel boundary. h = ((@height - 1) / 2).to_i case @chart.scale['name'] when 'hour' genHeaderScale(@largeScale, 0, h, :midnight, :sameTimeNextDay, @columnDef.timeformat1 || '%A %Y-%m-%d') genHeaderScale(@smallScale, h + 1, h, :beginOfHour, :sameTimeNextHour, @columnDef.timeformat2 || '%H') when 'day' genHeaderScale(@largeScale, 0, h, :beginOfMonth, :sameTimeNextMonth, @columnDef.timeformat1 || '%b %Y') genHeaderScale(@smallScale, h + 1, h, :midnight, :sameTimeNextDay, @columnDef.timeformat2 || '%d') when 'week' genHeaderScale(@largeScale, 0, h, :beginOfMonth, :sameTimeNextMonth, @columnDef.timeformat1 || '%b %Y') genHeaderScale(@smallScale, h + 1, h, :beginOfWeek, :sameTimeNextWeek, @columnDef.timeformat2 || '%d') when 'month' genHeaderScale(@largeScale, 0, h, :beginOfYear, :sameTimeNextYear, @columnDef.timeformat1 || '%Y') genHeaderScale(@smallScale, h + 1, h, :beginOfMonth, :sameTimeNextMonth, @columnDef.timeformat2 || '%b') when 'quarter' genHeaderScale(@largeScale, 0, h, :beginOfYear, :sameTimeNextYear, @columnDef.timeformat1 || '%Y') genHeaderScale(@smallScale, h + 1, h, :beginOfQuarter, :sameTimeNextQuarter, @columnDef.timeformat2 || 'Q%Q') when 'year' genHeaderScale(@smallScale, h + 1, h, :beginOfYear, :sameTimeNextYear, @columnDef.timeformat1 || '%Y') else raise "Unknown scale: #{@chart.scale['name']}" end nlx = @chart.dateToX(@chart.now) @nowLineX = nlx if nlx if @chart.markdate flx = @chart.dateToX(@chart.markdate) @markdateLineX = flx if flx end end # Generate the actual scale cells. def genHeaderScale(scale, y, h, beginOfFunc, sameTimeNextFunc, timeformat) # The beginOfWeek function needs a parameter, so we have to handle it as a # special case. if beginOfFunc == :beginOfWeek t = @chart.start.send(beginOfFunc, @chart.weekStartsMonday) else t = @chart.start.send(beginOfFunc) end # Now we iterate of the report period in steps defined by # sameTimeNextFunc. For each time slot we generate GanttHeaderScaleItem # object and append it to the scale. while t < @chart.end nextT = t.send(sameTimeNextFunc) # Determine the end of the cell. We keep 1 pixel for the boundary. w = (xR = @chart.dateToX(nextT).to_i - 1) - (x = @chart.dateToX(t).to_i) # We collect the positions of the large grid scale marks for later use # in the chart. if scale == @largeScale @gridLines << xR else @cellStartDates << t end scale << GanttHeaderScaleItem.new(t.to_s(timeformat), x, y, w, h) t = nextT end # Add the end date of the last cell when generating the small scale. @cellStartDates << t if scale == @smallScale end end end TaskJuggler-3.8.1/lib/taskjuggler/reports/GanttHeaderScaleItem.rb000066400000000000000000000021541473026623400250460ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = GanttHeaderScaleItem.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # class TaskJuggler # This class is a storate container for all data related to a scale step of a # GanttChart header. class GanttHeaderScaleItem attr_reader :label, :pos, :width def initialize(label, x, y, width, height) @label = label @x = x @y = y @width = width @height = height end def to_html div = XMLElement.new('div', 'class' => 'tabhead', 'style' => "font-weight:bold; position:absolute; " + "left:#{@x}px; top:#{@y}px; width:#{@width}px; height:#{@height}px; ") div << (div1 = XMLElement.new('div', 'style' => 'padding:3px; ')) div1 << XMLText.new("#{label}") div end end end TaskJuggler-3.8.1/lib/taskjuggler/reports/GanttLine.rb000066400000000000000000000337211473026623400227620ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = GanttLine.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/reports/GanttTaskBar' require 'taskjuggler/reports/GanttMilestone' require 'taskjuggler/reports/GanttContainer' require 'taskjuggler/reports/GanttLoadStack' require 'taskjuggler/reports/HTMLGraphics' require 'taskjuggler/XMLDocument' class TaskJuggler # This class models the abstract (output independent) form of a line of a # Gantt chart. Each line represents a property. Depending on the type of # property and it's context (for nested properties) the content varies. Tasks # (not nested) are represented as task bars or milestones. When nested into a # resource they are represented as load stacks. class GanttLine include HTMLGraphics attr_reader :y, :height, :query # Create a GanttLine object and generate the abstract representation. def initialize(chart, query, y, height, lineIndex, tooltip) # A reference to the chart that the line belongs to. @chart = chart # Register the line with the chart. @chart.addLine(self) # The query is used to access the presented project data. @query = query # A CellSettingPatternList object to determine the tooltips for the # line's content. @tooltip = tooltip # The category determines the background color of the line. @category = nil # The y coordinate of the topmost pixel of this line. @y = y + chart.header.height + 1 # The height of the line in screen pixels. @height = height # The index of the line in the chart. It starts with 0 and is # incremented for each line by one. @lineIndex = lineIndex # The x coordinates of the time-off zones. It's an Array of [ startX, endX # ] touples. @timeOffZones = [] generate end # Convert the abstract representation of the GanttLine into HTML elements. def to_html # The whole line is put in a 'div' section. All coordinates relative to # the top-left corner of this div. Elements that extend over the # boundaries of this div are cut off. div = XMLElement.new('div', 'class' => @category, 'style' => "margin:0px; padding:0px; " + "position:absolute; " + "left:0px; top:#{@y}px; " + "width:#{@chart.width.to_i}px; " + "height:#{@height}px; " + "font-size:10px;") # Render time-off zones. @timeOffZones.each do |zone| div << rectToHTML(zone[0], 0, zone[1], @height, 'offduty') end # Render grid lines. The grid lines are determined by the large scale. @chart.header.gridLines.each do |line| div << rectToHTML(line, 0, 1, @height, 'tabvline') end # Now render the content as HTML elements. @content.each do |c| html = c.to_html if html && html[0] addHtmlTooltip(@tooltip, @query, html[0], div) div << html end end # Render the 'now' line if @chart.header.nowLineX div << rectToHTML(@chart.header.nowLineX, 0, 1, @height, 'nowline') end # Render the 'markdate' line if @chart.header.markdateLineX div << rectToHTML(@chart.header.markdateLineX, 0, 1, @height, 'markdateline') end div end # This function only works for primary task lines. It returns the generated # intermediate object for that line. def getTask if @content.length == 1 @content[0] else nil end end # Register the areas that dependency lines should not cross. def addBlockedZones(router) @content.each do |c| c.addBlockedZones(router) end end private # Create the data objects that represent the abstract form of this # perticular Gantt chart line. def generate # This Array holds the GanttLineObjects. @content = [] generateTimeOffZones if @query.property.is_a?(Task) generateTask else generateResource end end # Generate abstract form of a task line. The task can be a primary line or # appear in the scope of a resource. def generateTask # Set the background color @category = "taskcell#{(@lineIndex + 1) % 2 + 1}" project = @query.project property = @query.property scopeProperty = @query.scopeProperty taskStart = property['start', @query.scenarioIdx] || project['start'] taskEnd = property['end', @query.scenarioIdx] || project['end'] if scopeProperty # The task is nested into a resource. We show the work the resource is # doing for this task relative to the work the resource is doing for # all tasks. x = nil startDate = endDate = nil categories = [ 'busy', nil ] @chart.header.cellStartDates.each do |date| if x.nil? x = @chart.dateToX(endDate = date).to_i else xNew = @chart.dateToX(date).to_i w = xNew - x startDate = endDate endDate = date # If we have a scope limiting task, we only want to generate load # stacks that overlap with the task interval. next if endDate <= taskStart || taskEnd <= startDate if startDate < taskStart && endDate > taskStart # Make sure the left edge of the first stack aligns with the # start of the scope task. startDate = taskStart x = @chart.dateToX(startDate) w = xNew - x + 1 elsif startDate < taskEnd && endDate > taskEnd # Make sure the right edge of the last stack aligns with the end # of the scope task. endDate = taskEnd w = @chart.dateToX(endDate) - x end startIdx = project.dateToIdx(startDate) endIdx = project.dateToIdx(endDate) overallWork = scopeProperty.getEffectiveWork(@query.scenarioIdx, startIdx, endIdx) + scopeProperty.getEffectiveFreeWork(@query.scenarioIdx, startIdx, endIdx) workThisTask = property.getEffectiveWork(@query.scenarioIdx, startIdx, endIdx, scopeProperty) # If all values are 0 we make sure we show an empty frame. if overallWork == 0 && workThisTask == 0 values = [ 0, 1 ] else values = [ workThisTask, overallWork - workThisTask ] end @content << GanttLoadStack.new(self, x + 1, w - 2, values, categories) x = xNew end end if @chart.table @chart.table.legend.addGanttItem('Resource assigned to task(s)', 'busy') end else # The task is not nested into a resource. We show the classical Gantt # bars for the task. xStart = @chart.dateToX(taskStart) xEnd = @chart.dateToX(taskEnd) @chart.addTask(property, self) @content << if property['milestone', @query.scenarioIdx] GanttMilestone.new(@height, xStart, @y) elsif property.container? && ((rollupExpr = @query.project.reportContexts. last.report.get('rollupTask')).nil? || !rollupExpr.eval(@query)) GanttContainer.new(@height, xStart, xEnd, @y) else GanttTaskBar.new(@query, @height, xStart, xEnd, @y) end # Make sure the legend includes the Gantt symbols. @chart.table.legend.showGanttItems = true if @chart.table @chart.table.legend.addGanttItem('Off-duty period', 'offduty') end end # Generate abstract form of a resource line. The resource can be a primary # line or appear in the scope of a task. def generateResource # Set the alternating background color @category = "resourcecell#{(@lineIndex + 1) % 2 + 1}" # The cellStartDate Array contains the end of the final cell as last # element. We need to use a shift mechanism to start and end # dates/positions properly. x = nil startDate = endDate = nil project = @query.project property = @query.property scopeProperty = @query.scopeProperty # For unnested resource lines we show the assigned work and the # available work. For resources in a task scope we show the work # allocated to this task, the work allocated to other tasks and the free # work. if scopeProperty categories = [ 'assigned', 'busy', 'free' ] taskStart = scopeProperty['start', @query.scenarioIdx] || project['start'] taskEnd = scopeProperty['end', @query.scenarioIdx] || project['end'] if @chart.table @chart.table.legend.addGanttItem('Resource assigned to this task', 'assigned') @chart.table.legend.addGanttItem('Resource assigned to task(s)', 'busy') @chart.table.legend.addGanttItem('Resource available', 'free') @chart.table.legend.addGanttItem('Off-duty period', 'offduty') end else categories = [ 'busy', 'free' ] if @chart.table @chart.table.legend.addGanttItem('Resource assigned to task(s)', 'busy') @chart.table.legend.addGanttItem('Resource available', 'free') @chart.table.legend.addGanttItem('Off-duty period', 'offduty') end end endDate = nil @chart.header.cellStartDates.each do |date| if endDate.nil? endDate = date next end startDate = endDate endDate = date if scopeProperty # If we have a scope limiting task, we only want to generate load # stacks that overlap with the task interval. next if endDate <= taskStart || taskEnd <= startDate if startDate < taskStart # Make sure the left edge of the first stack aligns with the # start of the scope task. startDate = taskStart end if endDate > taskEnd # Make sure the right edge of the last stack aligns with the end # of the scope task. endDate = taskEnd end startIdx = project.dateToIdx(startDate) endIdx = project.dateToIdx(endDate) taskWork = property.getEffectiveWork(@query.scenarioIdx, startIdx, endIdx, scopeProperty) overallWork = property.getEffectiveWork(@query.scenarioIdx, startIdx, endIdx) freeWork = property.getEffectiveFreeWork(@query.scenarioIdx, startIdx, endIdx) values = [ taskWork, overallWork - taskWork, freeWork ] else startIdx = project.dateToIdx(startDate) endIdx = project.dateToIdx(endDate) values = [] values << property.getEffectiveWork(@query.scenarioIdx, startIdx, endIdx) values << property.getEffectiveFreeWork(@query.scenarioIdx, startIdx, endIdx) end x = @chart.dateToX(startDate) w = @chart.dateToX(endDate) - x + 1 @content << GanttLoadStack.new(self, x + 1, w - 2, values, categories) end end # Generate the data structures that mark the time-off periods of a task or # resource int the chart. Depending on the resolution, the only periods with # a duration above the threshold are shown. def generateTimeOffZones iv = TimeInterval.new(@chart.start, @chart.end) # Don't show any zones if the threshold for this scale is 0 or smaller. return if (minTimeOff = @chart.scale['minTimeOff']) <= 0 # Get the time-off intervals. @timeOffZones = @query.property.collectTimeOffIntervals( @query.scenarioIdx, iv, minTimeOff) # Convert the start/end dates to X coordinates of the chart. When # finished, the zones in @timeOffZones are [ startX, endX ] touples. zones = [] @timeOffZones.each do |zone| zones << [ s = @chart.dateToX(zone.start), @chart.dateToX(zone.end) - s ] end @timeOffZones = zones end def addHtmlTooltip(tooltip, query, trigger, hook = nil) return unless tooltip tooltip = tooltip.getPattern(query) return unless tooltip && !tooltip.empty? if tooltip.respond_to?('functionHandler') tooltip.setQuery(query) end if query query.attributeId = 'name' query.process title = query.to_s else title = '' end trigger['onclick'] = "TagToTip('ID#{trigger.object_id}', " + "TITLE, '#{title.gsub(/'/, ''')}')" trigger['style'] += 'cursor:help; ' hook = trigger unless hook hook << (ltDiv = XMLElement.new('div', 'class' => 'tj_tooltip_box', 'id' => "ID#{trigger.object_id}")) ltDiv << (tooltip.respond_to?('to_html') ? tooltip.to_html : XMLText.new(tooltip)) end end end TaskJuggler-3.8.1/lib/taskjuggler/reports/GanttLoadStack.rb000066400000000000000000000071011473026623400237310ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = GanttLoadStack.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/reports/HTMLGraphics' class TaskJuggler # The GanttLoadStack is a simple stack diagram that shows the relative shares # of the values. The stack is always normed to the line height. class GanttLoadStack include HTMLGraphics # Create a GanttLoadStack object based on the following information: _line_ # is a reference to the GanttLine. _x_ is the left edge in chart coordinates # and _w_ is the stack width. _values_ are the values to be displayed and # _categories_ determines the color for each of the values. def initialize(line, x, w, values, categories) @line = line @lineHeight = line.height @x = x @y = @line.y @w = w <= 0 ? 1 : w @drawFrame = false if values.length != categories.length raise "Values and categories must have the same number of entries!" end @categories = categories i = 0 @categories.each do |cat| if cat.nil? && values[i] > 0 @drawFrame = true break end i += 1 end # Convert the values to chart Y coordinates and store them in yLevels. sum = 0 values.each { |v| sum += v } # If the sum is 0, all yLevels values must be 0 as well. if sum == 0 @yLevels = nil @drawFrame = true else @yLevels = [] values.each do |v| # We leave 1 pixel to the top and bottom of the line and need 1 pixel # for the frame. @yLevels << (@lineHeight - 4) * v / sum end end end def addBlockedZones(router) # Horizontal block router.addZone(@x - 2, @y, @w + 4, @lineHeight, true, false) end # Convert the abstact representation of the GanttLoadStack into HTML # elements. def to_html # Draw nothing if all values are 0. return nil unless @yLevels html = [] # Draw a background rectable to create a frame. In case the frame is not # fully filled by the stack, we need to draw a real frame to keep the # background. if @drawFrame # Top frame line html << @line.lineToHTML(@x, 1, @x + @w - 1, 1, 'loadstackframe') # Bottom frame line html << @line.lineToHTML(@x, @lineHeight - 2, @x + @w - 1, @lineHeight - 2, 'loadstackframe') # Left frame line html << @line.lineToHTML(@x, 1, @x, @lineHeight - 2, 'loadstackframe') # Right frame line html << @line.lineToHTML(@x + @w - 1, 1, @x + @w - 1, @lineHeight - 2, 'loadstackframe') else html << @line.rectToHTML(@x, 1, @w, @lineHeight - 2, 'loadstackframe') end yPos = 2 # Than draw the slighly narrower bars as a pile ontop of it. (@yLevels.length - 1).downto(0) do |i| next if @yLevels[i] <= 0 if @categories[i] html << @line.rectToHTML(@x + 1, yPos.to_i, @w - 2, (yPos + @yLevels[i]).to_i - yPos.to_i, @categories[i]) end yPos += @yLevels[i] end html end end end TaskJuggler-3.8.1/lib/taskjuggler/reports/GanttMilestone.rb000066400000000000000000000046261473026623400240340ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = GanttMilestone.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/reports/HTMLGraphics' class TaskJuggler # The GanttMilestone represents a milestone task. class GanttMilestone include HTMLGraphics # The size of the milestone symbol measured from the center to the tips. @@size = 6 # Create a GanttMilestone object based on the following information: _task_ # is a reference to the Task to be displayed. _lineHeight_ is the height of # the line this milestone is shown in. _x_ and _y_ are the coordinates of # the center of the milestone in the GanttChart. def initialize(lineHeight, x, y) @lineHeight = lineHeight @x = x @y = y end # Return the point [ x, y ] where task start dependency lines should start # from. def startDepLineStart [ @x + @@size, @y + @lineHeight / 2 ] end # Return the point [ x, y ] where task start dependency lines should end at. def startDepLineEnd [ @x - @@size, @y + @lineHeight / 2 ] end # Return the point [ x, y ] where task end dependency lines should start # from. def endDepLineStart [ @x + @@size , @y + @lineHeight / 2 ] end # Return the point [ x, y ] where task end dependency lines should end at. def endDepLineEnd [ @x + @@size, @y + @lineHeight / 2 ] end def addBlockedZones(router) router.addZone(@x - @@size - 2, @y + (@lineHeight / 2) - @@size - 2, 2 * @@size + 5, 2 * @@size + 5, true, true) # Block for arrowhead. router.addZone(@x - @@size - 9, @y + (@lineHeight / 2) - 7, 10, 15, true, true) end # Convert the abstact representation of the GanttMilestone into HTML # elements. def to_html html = [] # Invisible trigger frame for tooltips. html << rectToHTML(@x - (@lineHeight / 2), 0, @lineHeight, @lineHeight, 'tj_gantt_frame') # Draw a diamond shape. html += diamondToHTML(@x, @lineHeight / 2) html end end end TaskJuggler-3.8.1/lib/taskjuggler/reports/GanttRouter.rb000066400000000000000000000215541473026623400233540ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = GanttRouter.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/reports/CollisionDetector' class TaskJuggler # The GanttRouter is used by the GanttChart to route the dependency lines from # the start to the end point. The chart is a rectangular area with a certain # width and height. The graphical elements of the Gantt chart can be # registered as don't-cross-zones. These zones block the either horizontal or # vertical lines (or both) from crossing the zone. Zones can be registered by # calling addZone(). The route() method returns routed path from # start to end point. class GanttRouter # Minimum distance between the starting point and the first turning point. MinStartGap = 5 # Minimum distance between the last turning point and the tip of the # arrow. MinEndGap = 10 # Create a GanttRouter object. _width_ and _height_ describe the size of the # rectangular area this router is operating on. def initialize(width, height) @width = width.to_i @height = height.to_i @detector = CollisionDetector.new(@width, @height) end def addZone(x, y, w, h, horiz, vert) @detector.addBlockedZone(x, y, w, h, horiz, vert) end def routeLines(fromToPoints) # We first convert the fromToPoints list into a more readable list of # Hash objects. routes = [] fromToPoints.each do |touple| routes << { :startX => touple[0], :startY => touple[1], :endX => touple[2], :endY => touple[3], :id => touple[4] } end # To make sure that we minimize the crossings of arrows that # originate from the same position, we sort the arrows by the # smallest angle between the vertical line through the task end # and the line between the start and end of the arrow. routes.each do |r| adjLeg = (r[:endX] - MinEndGap) - (r[:startX] + MinStartGap) oppLeg = (r[:startY] - r[:endY]).abs r[:distance] = Math.sqrt(adjLeg ** 2 + oppLeg ** 2) # We can now calculate the sinus values of the angle between the # vertical and a line through the coordinates. sinus = oppLeg.abs / r[:distance] r[:angle] = (adjLeg < 0 ? Math::PI / 2 + Math.asin(Math::PI/2 - sinus) : Math.asin(sinus)) / (Math::PI / (2 * 90)) end # We sort the arrows from small to a large angle. In case the angle is # identical, we use the length of the line as second criteria. routes.sort! { |r1, r2| (r1[:angle] / 5).to_i == (r2[:angle] / 5).to_i ? -(r1[:distance] <=> r2[:distance]) : -(r1[:angle] <=> r2[:angle]) } # Now that the routes are in proper order, we can actually lay the # routes. routePoints = [] routes.each do |r| routePoints << route(r[:startX], r[:startY], r[:endX], r[:endY]) end routePoints end # Find a non-blocked route from the _startPoint_ [ x, y ] to the # _endPoint_ [ x, y ]. The route always starts from the start point towards # the right side of the chart and reaches the end point from the left side # of the chart. All lines are always strictly horizontal or vertical. There # are no diagonal lines. The result is an Array of [ x, y ] points that # include the _startPoint_ as first and _endPoint_ as last element. def route(startX, startY, endX, endY) points = [ [ startX, startY ] ] startGap = MinStartGap endGap = MinEndGap if endX - startX > startGap + endGap + 2 # If the horizontal distance between start and end point is large enough # we can try a direct route. # # xSeg # |startGap| # startX/endX X--------1 # | # | # 2------X endX/endY # |endGap| # xSeg = placeLine([ startY + (startY < endY ? 1 : -1), endY ], false, startX + startGap, 1) if xSeg && xSeg < endX - endGap # The simple version works. Add the lines. addLineTo(points, xSeg, startY) # Point 1 addLineTo(points, xSeg, endY) # Point 2 addLineTo(points, endX, endY) return points end end # If the simple approach above fails, the try a more complex routing # strategy. # # x1 # |startGap| # startX/startY X--------1 yLS # | # 3---------------2 ySeg # | # 4------X endX/endY # |endGap| # x2 # Place horizontal segue. We don't know the width yet, so we have to # assume full width. That's acceptable for horizontal lines. deltaY = startY < endY ? 1 : -1 ySeg = placeLine([ 0, @width - 1 ], true, startY + 2 * deltaY, deltaY) raise "Routing failed" unless ySeg # Place 1st vertical x1 = placeLine([ startY + deltaY, ySeg ], false, startX + startGap, 1) raise "Routing failed" unless x1 # Place 2nd vertical x2 = placeLine([ ySeg + deltaY, endY ], false, endX - endGap, -1) raise "Routing failed" unless x2 # Now add the points 1 - 4 to the list and mark the zones around them. For # vertical lines, we only mark vertical zones and vice versa. addLineTo(points, x1, startY) # Point 1 if x1 != x2 addLineTo(points, x1, ySeg) # Point 2 addLineTo(points, x2, ySeg) # Point 3 end addLineTo(points, x2, endY) # Point 4 addLineTo(points, endX, endY) points end # This function is only intended for debugging purposes. It marks either the # vertical or horizontal zones in the chart. def to_html @detector.to_html end private # This function is at the heart of the routing algorithm. It tries to find a # place for the line described by _segment_ without overlapping with the # defined zones. _horizontal_ determines whether the line is running # horizontally or vertically. _start_ is the first coordinate that is looked # at. In case of collisions, _start_ is moved by _delta_ and the check is # repeated. The function returns the first collision free coordinate or the # outside edge of the routing area. def placeLine(segment, horizontal, start, delta) raise "delta may not be 0" if delta == 0 # Start must be an integer and lie within the routing area. pos = start.to_i pos = 0 if pos < 0 max = (horizontal ? @height: @width) - 1 pos = max if pos > max # Make sure that the segment coordinates are in ascending order. segment.sort! # TODO: Remove this check once the code becomes stable. #checkLines(lines) while @detector.collision?(pos, segment, horizontal) pos += delta # Check if we have exceded the chart area towards top/left. if delta < 0 if pos < 0 break end else # And towards right/bottom. break if pos >= (horizontal ? @height : @width) end end pos end # This function adds another waypoint to an existing line. In addition it # adds a zone that is 2 pixel wide on each side of the line and runs in the # direction of the line. This avoids too closely aligned parallel lines in # the chart. def addLineTo(points, x2, y2) raise "Point list may not be empty" if points.empty? x1, y1 = points[-1] points << [ x2, y2 ] if x1 == x2 # vertical line return if x1 < 0 || x1 >= @width x, y, w, h = justify(x1 - 2, y1, 5, y2 - y1 + 1) addZone(x, y, w, h, false, true) else # horizontal line return if y1 < 0 || x1 >= @height x, y, w, h = justify(x1, y1 - 2, x2 - x1 + 1, 5) addZone(x, y, w, h, true, false) end end # This function makes sure that the rectangle described by _x_, _y_, _w_ # and _h_ is properly justfified. If the width or height are negative, _x_ # and _y_ are adjusted to describe the same rectangle with all positive # coordinates. def justify(x, y, w, h) if w < 0 w = -w x = x - w + 1 end if h < 0 h = -h y = y - h + 1 end # Return the potentially adjusted rectangle coordinates. return x, y, w, h end end end TaskJuggler-3.8.1/lib/taskjuggler/reports/GanttTaskBar.rb000066400000000000000000000063641473026623400234250ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = GanttTaskBar.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/reports/HTMLGraphics' class TaskJuggler # The GanttTaskBar represents a normal task that is part of a GanttChart. class GanttTaskBar include HTMLGraphics # The size of the bar in pixels from center to top/bottom. @@size = 6 # Create a GanttContainer object based on the following information: _line_ # is a reference to the GanttLine. _xStart_ is the left edge of the task in # chart coordinates. _xEnd_ is the right edge. def initialize(query, lineHeight, xStart, xEnd, y) @query = query @lineHeight = lineHeight @start = xStart @end = xEnd @y = y end # Return the point [ x, y ] where task start dependency lines should start # from. def startDepLineStart [ @start + 1, @y + @lineHeight / 2 ] end # Return the point [ x, y ] where task start dependency lines should end at. def startDepLineEnd [ @start - 1, @y + @lineHeight / 2 ] end # Return the point [ x, y ] where task end dependency lines should start # from. def endDepLineStart [ @end + 1, @y + @lineHeight / 2 ] end # Return the point [ x, y ] where task end dependency lines should end at. def endDepLineEnd [ @end - 1, @y + @lineHeight / 2 ] end def addBlockedZones(router) # Horizontal block for whole bar. router.addZone(@start, @y + (@lineHeight / 2) - @@size - 1, @end - @start + 1, 2 * @@size + 3, true, false) # Block for arrowhead. router.addZone(@start - 9, @y + (@lineHeight / 2) - 7, 10, 15, true, true) # Vertical block for end cap router.addZone(@start - 2, @y, 5, @lineHeight, false, true) router.addZone(@end - 2, @y, 5, @lineHeight, false, true) end # Convert the abstact representation of the GanttTaskBar into HTML # elements. def to_html xStart = @start.to_i yCenter = (@lineHeight / 2).to_i width = @end.to_i - @start.to_i + 1 html = [] # Invisible trigger frame for tooltips. html << rectToHTML(xStart, 0, width, @lineHeight, 'tj_gantt_frame') # First we draw the task frame. html << rectToHTML(xStart, yCenter - @@size, width, 2 * @@size, 'taskbarframe') # The we draw the filling. html << rectToHTML(xStart + 1, yCenter - @@size + 1, width - 2, 2 * @@size - 2, 'taskbar') # And then the progress bar. If query is null we assume 50% completion. if @query @query.attributeId = 'complete' @query.process res = @query.result completion = res ? res / 100.0 : 0.0 else completion = 0.5 end html << rectToHTML(xStart + 1, yCenter - @@size / 2, (width - 2) * completion, @@size, 'progressbar') end end end TaskJuggler-3.8.1/lib/taskjuggler/reports/HTMLGraphics.rb000066400000000000000000000055361473026623400233250ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = HTMLGraphics.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # class TaskJuggler # This module provides some functions to render simple graphical objects like # filled rectangles and lines as HTML elements. module HTMLGraphics # Render a line as HTML element. We use 'div's with a single pixel width or # height for this purpose. As a consequence of this, we can only generate # horizontal or vertical lines. Diagonal lines are not supported. _xs_ and # _ys_ are the start coordinates, _xe_ and _ye_ are the end coordinates. # _category_ determines the color. def lineToHTML(xs, ys, xe, ye, category) xs = xs.to_i ys = ys.to_i xe = xe.to_i ye = ye.to_i if ys == ye # Horizontal line xs, xe = xe, xs if xe < xs style = "left:#{xs}px; top:#{ys}px; " + "width:#{xe - xs + 1}px; height:1px;" elsif xs == xe # Vertical line ys, ye = ye, ys if ye < ys style = "left:#{xs}px; top:#{ys}px; " + "width:1px; height:#{ye - ys + 1}px;" else raise "Can't draw diagonal line #{xs}/#{ys} to #{xe}/#{ye}!" end XMLElement.new('div', 'class' => category, 'style' => style) end # Draw a filled rectable at position _x_ and _y_ with the dimension _w_ and # _h_ into another HTML element. The color is determined by the class # _category_. def rectToHTML(x, y, w, h, category) style = "left:#{x.to_i}px; top:#{y.to_i}px; " + "width:#{w.to_i}px; height:#{h.to_i}px;" XMLElement.new('div', 'class' => category, 'style' => style) end def jagToHTML(x, y) XMLElement.new('div', 'class' => 'tj_gantt_jag', 'style' => "left:#{x.to_i - 5}px; " + "top:#{y.to_i}px;") end def diamondToHTML(x, y) html = [] html << XMLElement.new('div', 'class' => 'tj_diamond_top', 'style' => "left:#{x.to_i - 6}px; " + "top:#{y.to_i - 7}px;") html << XMLElement.new('div', 'class' => 'tj_diamond_bottom', 'style' => "left:#{x.to_i - 6}px; " + "top:#{y.to_i}px;") html end def arrowHeadToHTML(x, y) XMLElement.new('div', 'class' => 'tj_arrow_head', 'style' => "left:#{x.to_i - 5}px; " + "top:#{y.to_i - 5}px;") end end end TaskJuggler-3.8.1/lib/taskjuggler/reports/ICalReport.rb000066400000000000000000000124261473026623400231000ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = ICalReport.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/reports/ReportBase' require 'taskjuggler/ICalendar' class TaskJuggler # This Report derivative generates iCalendar files. class ICalReport < ReportBase # Create a new ICalReport with _report_ as description. def initialize(report) super end # Generate an intermediate version of the report data. def generateIntermediateFormat super # Prepare the task list. @taskList = PropertyList.new(@project.tasks) @taskList.setSorting(a('sortTasks')) @taskList = filterTaskList(@taskList, nil, a('hideTask'), a('rollupTask'), a('openNodes')) @taskList.sort! # Prepare the resource list. This is not yet used. @resourceList = PropertyList.new(@project.resources) @resourceList.setSorting(a('sortResources')) @resourceList = filterResourceList(@resourceList, nil, a('hideResource'), a('rollupResource'), a('openNodes')) @resourceList.sort! @query = @report.project.reportContexts.last.query.dup @ical = ICalendar.new("#{AppConfig.packageName}-#{@project['projectid']}") # Use the project start date of the current date (whichever is earlier) # for the calendar creation date. @ical.creationDate = [ @report.project['start'], TjTime.new ].min # Use the project 'now' date a last modification date. @ical.lastModified = @report.project['now'] # We only care about the first requested scenario. scenarioIdx = a('scenarios').first uidMap = {} @taskList.each do |task| todo = ICalendar::Todo.new( @ical, "#{task['projectid', scenarioIdx]}-#{task.fullId}", task.name, task['start', scenarioIdx], task['end', scenarioIdx]) # Save the ical UID of this TODO. uidMap[task] = todo.uid @query.property = task @query.attributeId = 'complete' @query.scenarioIdx = scenarioIdx @query.process todo.percentComplete = @query.to_num.to_i # We must convert the TJ priority range (1 - 1000) to iCalendar range # (0 - 9). todo.priority = (task['priority', scenarioIdx] - 1) / 100 # If there is a parent task and it's known already, we set the # relation in the TODO component. if task.parent && uidMap[task.parent] todo.relatedTo = uidMap[task.parent] end # If we have a task note, use this for the DESCRIPTION property. if (note = task.get('note')) note = note.to_s todo.description = note end # Check if we have a responsible resource with an email address. Since # ICalendar only knows one organizer we ignore all but the first. organizer = nil if !(responsible = task['responsible', scenarioIdx]).empty? && @resourceList.include?(organizer = responsible[0]) && organizer.get('email') todo.setOrganizer(organizer.name, organizer.get('email')) end # Set the assigned resources as attendees. attendees = [] task['assignedresources', scenarioIdx].each do |resource| next unless @resourceList.include?(resource) && resource.get('email') attendees << resource todo.addAttendee(resource.name, resource.get('email')) end # Generate an additional VEVENT entry for all leaf tasks that aren't # milestones. if task.leaf? && !task['milestone', scenarioIdx] && @report.get('novevents') == [false] event = ICalendar::Event.new( @ical, "#{task['projectid', scenarioIdx]}-#{task.fullId}", task.name, task['start', scenarioIdx], task['end', scenarioIdx]) event.description = note if note if organizer event.setOrganizer(organizer.name, organizer.get('email')) end attendees.each do |attendee| event.addAttendee(attendee.name, attendee.get('email')) end end # Generate VJOURNAL entries for all the journal entries of this task. @report.project['journal']. entriesByTask(task, a('start'), a('end'), a('hideJournalEntry')).each do |entry| journal = ICalendar::Journal.new( @ical, "#{task['projectid', scenarioIdx]}-#{task.fullId}", entry.headline, entry.date) journal.relatedTo = uidMap[task] journal.description = entry.summary.to_s + entry.details.to_s # Set the author of the journal entry as organizer. if (author = entry.author) && @resourceList.include?(author) && author.get('email') journal.setOrganizer(author.name, author.get('email')) end end end end # Convert the intermediate format into a DOS formated String. def to_iCal @ical.to_s end end end TaskJuggler-3.8.1/lib/taskjuggler/reports/MspXmlRE.rb000066400000000000000000000406001473026623400225360ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = MspXmlRE.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/reports/ReportBase' class TaskJuggler # This specialization of ReportBase implements an export of the # project data into Microsoft Project XML format. Due to limitations of MS # Project and this implementation, only a subset of core data is being # exported. The exported data is already a scheduled project with full # resource/task assignment data. class MspXmlRE < ReportBase # Create a new object and set some default values. def initialize(report) super(report) # This report type currently only supports a single scenario. Use the # first specified one. @scenarioIdx = a('scenarios').first # Hash to map calendar names to UIDs (numbers). @calendarUIDs = {} @timeformat = "%Y-%m-%dT%H:%M:%S" end def generateIntermediateFormat super end # Return the project data in Microsoft Project XML format. def to_mspxml @query = @project.reportContexts.last.query.dup # Prepare the resource list. @resourceList = PropertyList.new(@project.resources) @resourceList.setSorting(a('sortResources')) @resourceList = filterResourceList(@resourceList, nil, a('hideResource'), a('rollupResource'), a('openNodes')) @resourceList.sort! # Prepare the task list. @taskList = PropertyList.new(@project.tasks) @taskList.includeAdopted # The MSP XML format requires that the tasks are listed in 'tree' order. # We purposely ignore the user provided sorting criteria. @taskList.setSorting([ [ 'tree', true, -1 ], [ 'seqno', true, -1 ] ]) @taskList = filterTaskList(@taskList, nil, a('hideTask'), a('rollupTask'), a('openNodes')) @taskList.sort! @taskList.checkForDuplicates(@report.sourceFileInfo) @file = XMLDocument.new @file << XMLBlob.new('') @file << XMLComment.new(<<"EOT" Generated by #{AppConfig.softwareName} v#{AppConfig.version} on #{TjTime.new} For more information about #{AppConfig.softwareName} see #{AppConfig.contact}. Project: #{@project['name']} Date: #{@project['now']} EOT ) @file << (project = XMLElement.new('Project', 'xmlns' => 'http://schemas.microsoft.com/project')) calendars = generateProjectAttributes(project) generateTasks(project) generateResources(project, calendars) generateAssignments(project) @file.to_s end private def generateProjectAttributes(p) p << XMLNamedText.new('14', 'SaveVersion') p << XMLNamedText.new(@report.name + '.xml', 'Name') p << XMLNamedText.new(TjTime.new.to_s(@timeformat), 'CreationDate') p << XMLNamedText.new('1', 'ScheduleFromStart') p << XMLNamedText.new(@project['start'].to_s(@timeformat), 'StartDate') p << XMLNamedText.new(@project['end'].to_s(@timeformat), 'FinishDate') p << XMLNamedText.new('09:00:00', 'DefaultStartTime') p << XMLNamedText.new('17:00:00', 'DefaultFinishTime') p << XMLNamedText.new('1', 'CalendarUID') p << XMLNamedText.new((@project.dailyWorkingHours * 60).to_i.to_s, 'MinutesPerDay') p << XMLNamedText.new((@project.weeklyWorkingDays * @project.dailyWorkingHours * 60).to_i.to_s, 'MinutesPerWeek') p << XMLNamedText.new((@project.yearlyWorkingDays / 12).to_s, 'DaysPerMonth') p << XMLNamedText.new(@project['now'].to_s(@timeformat), 'CurrentDate') p << XMLNamedText.new(@project['now'].to_s(@timeformat), 'StatusDate') loadUnitsMap = { :minutes => 1, :hours => 2, :days => 3, :weeks => 4, :months => 5, :quarters => 5, :years => 5, :shortAuto => 3, :longAuto => 3 } p << XMLNamedText.new(loadUnitsMap[a('loadUnit')].to_s, 'WorkFormat') p << XMLNamedText.new('1', 'NewTasksAreManual') p << XMLNamedText.new('0', 'SpreadPercentComplete') rate = (@project['rate'] / @project.dailyWorkingHours).to_s p << XMLNamedText.new(rate, 'StandardRate') p << XMLNamedText.new(rate, 'OvertimeRate') p << XMLNamedText.new(@project['currency'], 'CurrencySymbol') p << XMLNamedText.new(@project['currency'], 'CurrencyCode') #p << XMLNamedText.new('0', 'MicrosoftProjectServerURL') p << (calendars = XMLElement.new('Calendars')) generateCalendar(calendars, @project['workinghours'], 'Standard') calendars end def generateTasks(project) project << (tasks = XMLElement.new('Tasks')) @taskList.each do |task| generateTask(tasks, task) end end def generateResources(project, calendars) project << (resources = XMLElement.new('Resources')) @resourceList.each do |resource| generateResource(resources, resource, calendars) end end def generateAssignments(project) project << (assignments = XMLElement.new('Assignments')) i = 0 @taskList.each do |task| rollupTask = a('rollupTask') @query.property = task @query.scopeProperty = nil # We only generate assignments for leaf tasks and rolled-up container # tasks. next if (task.container? && !(rollupTask && rollupTask.eval(@query))) task.assignedResources(@scenarioIdx).each do |resource| generateAssignment(assignments, task, resource, i) i += 1 end end end def generateCalendar(calendars, workinghours, name) calendars << (cal = XMLElement.new('Calendar')) uid = @calendarUIDs.length.to_s @calendarUIDs[name] = uid cal << XMLNamedText.new(uid, 'UID') cal << XMLNamedText.new(name, 'Name') cal << XMLNamedText.new('1', 'IsBaseCalendar') cal << XMLNamedText.new('-1', 'BaseCalendarUID') cal << (weekdays = XMLElement.new('WeekDays')) d = 1 workinghours.days.each do |day| weekdays << (weekday = XMLElement.new('WeekDay')) weekday << XMLNamedText.new(d.to_s, 'DayType') d += 1 if day.empty? weekday << XMLNamedText.new('0', 'DayWorking') else weekday << XMLNamedText.new('1', 'DayWorking') weekday << (workingtimes = XMLElement.new('WorkingTimes')) day.each do |iv| workingtimes << (worktime = XMLElement.new('WorkingTime')) worktime << XMLNamedText.new(daytime_to_s(iv[0]), 'FromTime') worktime << XMLNamedText.new(daytime_to_s(iv[1]), 'ToTime') end end end end def generateTask(tasks, task) @query.property = task task.calcCompletion(@scenarioIdx) percentComplete = task['complete', @scenarioIdx] tasks << (t = XMLElement.new('Task')) t << XMLNamedText.new(task.get('index').to_s, 'UID') t << XMLNamedText.new(task.get('index').to_s, 'ID') t << XMLNamedText.new('1', 'Active') t << XMLNamedText.new('0', 'Type') t << XMLNamedText.new('0', 'IsNull') t << XMLNamedText.new(task.get('name'), 'Name') t << XMLNamedText.new(task.get('bsi'), 'WBS') t << XMLNamedText.new(task.get('bsi'), 'OutlineNumber') t << XMLNamedText.new((task.level - (a('taskroot') ? a('taskroot').level : 0)).to_s, 'OutlineLevel') t << XMLNamedText.new(task['priority', @scenarioIdx].to_s, 'Priority') t << XMLNamedText.new(task['start', @scenarioIdx].to_s(@timeformat), 'Start') t << XMLNamedText.new(task['end', @scenarioIdx].to_s(@timeformat), 'Finish') t << XMLNamedText.new(task['start', @scenarioIdx].to_s(@timeformat), 'ManualStart') t << XMLNamedText.new(task['end', @scenarioIdx].to_s(@timeformat), 'ManualFinish') t << XMLNamedText.new(task['start', @scenarioIdx].to_s(@timeformat), 'ActualStart') t << XMLNamedText.new(task['end', @scenarioIdx].to_s(@timeformat), 'ActualFinish') t << XMLNamedText.new('2', 'ConstraintType') t << XMLNamedText.new(task['start', @scenarioIdx].to_s(@timeformat), 'ConstraintDate') t << XMLNamedText.new('3', 'FixedCostAccrual') if (note = task.get('note')) t << XMLNamedText.new(note.to_s, 'Notes') end responsible = task['responsible', @scenarioIdx]. map { |r| r.name }.join(', ') t << XMLNamedText.new(responsible, 'Contact') unless responsible.empty? if task.container? rollupTask = a('rollupTask') t << XMLNamedText.new(rollupTask ? '1' : '0', 'Manual') t << XMLNamedText.new(rollupTask && rollupTask.eval(@query) ? '0' : '1', 'Summary') else t << XMLNamedText.new('1', 'Manual') t << XMLNamedText.new('0', 'Summary') t << XMLNamedText.new('0', 'Estimated') t << XMLNamedText.new('7', 'DurationFormat') if task['milestone', @scenarioIdx] t << XMLNamedText.new('1', 'Milestone') else duration = task['end', @scenarioIdx] - task['start', @scenarioIdx] t << XMLNamedText.new(durationToMsp(duration), 'Duration') t << XMLNamedText.new(durationToMsp(duration), 'Work') t << XMLNamedText.new('0', 'Milestone') t << XMLNamedText.new('1', 'EffortDriven') t << XMLNamedText.new(percentComplete.to_i.to_s, 'PercentComplete') t << XMLNamedText.new(percentComplete.to_i.to_s, 'PercentWorkComplete') end end task['startpreds', @scenarioIdx].each do |dt, onEnd| next unless @taskList.include?(dt) next if task.parent && task.parent['startpreds', @scenarioIdx].include?([ dt, onEnd ]) t << (pl = XMLElement.new('PredecessorLink')) pl << XMLNamedText.new(@taskList[dt].get('index').to_s, 'PredecessorUID') pl << XMLNamedText.new(onEnd ? '1' : '3', 'Type') end task['endpreds', @scenarioIdx].each do |dt, onEnd| next unless @taskList.include?(dt) next if task.parent && task.parent['endpreds', @scenarioIdx].include?([ dt, onEnd ]) t << (pl = XMLElement.new('PredecessorLink')) pl << XMLNamedText.new(@taskList[dt].get('index').to_s, 'PredecessorUID') pl << XMLNamedText.new(onEnd ? '0' : '2', 'Type') end end def generateResource(resources, resource, calendars) # MS Project can only deal with a flat resource list. We don't export # resource groups. return unless resource.leaf? resources << (r = XMLElement.new('Resource')) r << XMLNamedText.new(resource.get('index').to_s, 'UID') # All TJ resources are people or equipment. r << XMLNamedText.new('1', 'Type') r << XMLNamedText.new(resource.name, 'Name') r << XMLNamedText.new(resource.id, 'Initials') # MS Project seems to use hourly rates, TJ daily rates. rate = (resource['rate', @scenarioIdx] / @project.dailyWorkingHours).to_s r << XMLNamedText.new(rate, 'StandardRate') r << XMLNamedText.new(rate, 'OvertimeRate') r << XMLNamedText.new(resource['efficiency', @scenarioIdx].to_s, 'MaxUnits') if (email = resource.get('email')) r << XMLNamedText.new(email, 'EmailAddress') end r << XMLNamedText.new(resource.parent.name, 'Group') if resource.parent #if (code = resource.get('Code')) # r << XMLNamedText.new(code, 'Code') # r << XMLNamedText.new('1', 'IsEnterprise') #end #if (ntaccount = resource.get('NTAccount')) # r << XMLNamedText.new(ntaccount, 'NTAccount') #end # Generate a calendar for this resource and assign it. generateCalendar(calendars, resource['workinghours', @scenarioIdx], "Calendar #{resource.name}") r << XMLNamedText.new(@calendarUIDs["Calendar #{resource.name}"], 'CalendarUID') end def generateAssignment(assignments, task, resource, uid) assignments << (a = XMLElement.new('Assignment')) a << XMLNamedText.new(uid.to_s, 'UID') a << XMLNamedText.new(@taskList[task].get('index').to_s, 'TaskUID') a << XMLNamedText.new(resource.get('index').to_s, 'ResourceUID') a << XMLNamedText.new(resource['efficiency', @scenarioIdx].to_s, 'Units') a << XMLNamedText.new(task['start', @scenarioIdx].to_s(@timeformat), 'start') a << XMLNamedText.new(task['end', @scenarioIdx].to_s(@timeformat), 'finish') a << XMLNamedText.new('100.0', 'Cost') a << XMLNamedText.new('8', 'WorkContour') # The PercentWorkComplete value must be 0. Otherwise the completed # work of the assignments will be ignored. a << XMLNamedText.new('0', 'PercentWorkComplete') responsible = task['responsible', @scenarioIdx]. map { |r| r.name }.join(', ') a << XMLNamedText.new(responsible, 'AssnOwner') unless responsible.empty? # Setup the query for this task and resource. @query.property = resource @query.scopeProperty = task @query.attributeId = 'effort' @query.scenarioIdx = @scenarioIdx @query.start = task['start', @scenarioIdx] @query.end = task['end', @scenarioIdx] @query.process tStart = task['start', @scenarioIdx] # We provide assignement data on a day-by-day basis. We report the work # that happens each day from task start to task end. tStart = tStart.midnight tEnd = task['end', @scenarioIdx] t = tStart while t < tEnd tn = t.sameTimeNextDay # We need to make sure that the stored intervals are within the task # and report boundaries. tn and tnc are corrected versions of t and tn # that meet this criterium. tc = t < task['start', @scenarioIdx] ? task['start', @scenarioIdx] : t tc = tc < a('start') ? a('start') : tc tnc = tn > task['end', @scenarioIdx] ? task['end', @scenarioIdx] : tn tnc = tnc > a('end') ? a('end') : tnc @query.start = tc @query.end = tnc @query.process workSeconds = @query.to_num * @project.dailyWorkingHours * 3600 a << (td = XMLElement.new('TimephasedData')) td << XMLNamedText.new(uid.to_s, 'UID') # The Type must be 1 or MS Project will take forever to load the file. td << XMLNamedText.new('1', 'Type') td << XMLNamedText.new(tc.to_s(@timeformat), 'Start') td << XMLNamedText.new((tnc - 1).to_s(@timeformat), 'Finish') td << XMLNamedText.new('2', 'Unit') td << XMLNamedText.new(durationToMsp(workSeconds), 'Value') t = tn end end def findRolledUpParent(task) return nil unless (rollupTask = a('rollupTask')) hideTask = a('hideTask') while task @query.property = task # We don't want to include any tasks that are explicitely hidden via # 'hidetask'. return nil if hideTask && hideTask.eval(@query) return task if rollupTask.eval(@query) && @taskList.include?(task) task = task.parent end end def durationToMsp(duration) hours = (duration / (60 * 60)).to_i minutes = ((duration - (hours * 60 * 60)) / 60).to_i seconds = (duration % 60).to_i "PT#{hours}H#{minutes}M#{seconds}S" end def daytime_to_s(t) h = (t / (60 * 60)).to_i m = ((t - (h * 60 * 60)) / 60).to_i s = (t % 60).to_i sprintf('%02d:%02d:%02d', h, m, s) end end end TaskJuggler-3.8.1/lib/taskjuggler/reports/Navigator.rb000066400000000000000000000167231473026623400230320ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = Navigator.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/reports/ReportContext' class TaskJuggler class NavigatorElement attr_reader :parent, :label attr_accessor :url, :elements, :current def initialize(parent, label = nil, url = nil) @parent = parent @label = label @url = url @elements = [] # True if the current report is included in this NavigatorElement or any # of its sub elements. @current = false end def to_html(html = nil) first = true topLevel = html.nil? # If we don't have a container yet, to put all the menus into, create one. html ||= XMLElement.new('div', 'class' => 'navbar_container') html << XMLElement.new('hr', 'class' => 'navbar_topruler') if topLevel # Create a container for this (sub-)menu. html << (div = XMLElement.new('div', 'class' => 'navbar')) @elements.each do |element| # Separate the menu entries by vertical bars. Prepend them for all but # the first entry. if first first = false else div << XMLText.new('|') end if element.current # The navbar entry is referencing this page. Highlight is as the # currently selected page. div << (span = XMLElement.new('span', 'class' => 'navbar_current')) span << XMLText.new(element.label) else # The navbar entry is refencing another page. Show the link to it. div << (span = XMLElement.new('span', 'class' => 'navbar_other')) span << (a = XMLElement.new('a', 'href' => element.url)) a << XMLText.new(element.label) end end # Now see if the current menu entry is actually just holding another sub # menu and generate that menue in another line after an HR. @elements.each do |element| if element.current && !element.elements.empty? html << XMLElement.new('hr', 'class' => 'navbar_midruler') unless first element.to_html(html) break end end html << XMLElement.new('hr', 'class' => 'navbar_bottomruler') if topLevel html end # Return a text version of the tree. Currently used for debugging only. def to_s(indent = 0) @elements.each do |element| puts "#{' ' * indent}#{element.current ? '<' : ''}" + "#{element.label}#{element.current ? '>' : ''}" + " -> #{element.url}" element.to_s(indent + 1) end end end # A Navigator is an automatically generated menu to navigate a list of # reports. The hierarchical structure of the reports will be reused to # group them. The actual structure of the Navigator depends on the output # format. class Navigator attr_reader :id attr_accessor :hideReport def initialize(id, project) @id = id @project = project @hideReport = LogicalExpression.new(LogicalOperation.new(0)) @elements = [] end # Generate an output format independant version of the navigator. This is # a tree of NavigatorElement objects. def generate(allReports, currentReports, reportDef, parentElement) element = nextParentElement = nextParentReport = nil currentReports.each do |report| hasURL = report.get('formats').include?(:html) # Only generate menu entries for container reports or leaf reports # have a HTML output format. next if (report.leaf? && !hasURL) || !allReports.include?(report) # What label should be used for the menu entry? It's either the name # of the report or the user specified title. label = report.get('title') || report.name url = findReportURL(report, allReports, reportDef) # Now we have all data so we can create the actual menu entry. parentElement.elements << (element = NavigatorElement.new(parentElement, label, url)) # Check if 'report' matches the 'reportDef' report or is a child of # it. if reportDef == report || reportDef.isChildOf?(report) nextParentReport = report nextParentElement = element element.current = true end end if nextParentReport && nextParentReport.container? generate(allReports, nextParentReport.kids, reportDef, nextParentElement) end end def to_html # The the Report object that contains this Navigator. reportDef ||= @project.reportContexts.last.report raise "Report context missing" unless reportDef # Compile a list of all reports that the user wants to include in the # menu. reports = filterReports return nil if reports.empty? # Make sure the report is actually in the filtered list. unless reports.include?(reportDef) @project.warning('nav_in_hidden_rep', "Navigator requested for a report that is not " + "included in the navigator list.", reportDef.sourceFileInfo) return nil end # Find the list of reports that become the top-level menu entries. topLevelReports = [ reportDef ] report = reportDef while report.parent report = report.parent topLevelReports = report.kids end generate(reports, topLevelReports, reportDef, content = NavigatorElement.new(nil)) content.to_html end private def filterReports list = PropertyList.new(@project.reports) list.setSorting([[ 'seqno', true, -1 ]]) list.sort! # Remove all reports that the user doesn't want to have include. query = @project.reportContexts.last.query.dup query.scopeProperty = nil query.scenarioIdx = query.scenario = nil list.delete_if do |property| query.property = property @hideReport.eval(query) end list end # Remove the URL or directory path from _url1_ that is identical to # _url2_. def normalizeURL(url1, url2) cut = 0 url1.length.times do |i| return url1[cut, url1.length - cut] if url1[i] != url2[i] cut = i + 1 if url1[i] == ?/ end url1 end # Find the URL to be used for the current Navigator menu entry. def findReportURL(report, allReports, reportDef) return nil unless allReports.include?(report) if report.get('formats').include?(:html) # The element references an HTML report. Point to it. if @project.reportContexts.last.report.interactive? url = "/taskjuggler?project=#{report.project['projectid']};" + "report=#{report.fullId}" else url = report.name + '.html' url = normalizeURL(url, reportDef.name) end return url else # The menu element is just a entry for another sub-menu. The the URL # from the first kid of the report that has a URL. report.kids.each do |r| if (url = findReportURL(r, allReports, reportDef)) return url end end end nil end end end TaskJuggler-3.8.1/lib/taskjuggler/reports/NikuReport.rb000066400000000000000000000425401473026623400231760ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = NikuReport.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/AppConfig' require 'taskjuggler/reports/ReportBase' class TaskJuggler class NikuProject attr_reader :name, :id, :tasks, :resources def initialize(id, name) @id = id @name = name @tasks = [] @resources = {} end end class NikuResource attr_reader :id attr_accessor :sum def initialize(id) @id = id @sum = 0.0 end end # The Niku report can be used to export resource allocation data for certain # task groups in the Niku XOG format. This file can be read by the Clarity # enterprise resource management software from Computer Associates. # Since I don't think this is a use case for many users, the implementation # is somewhat of a hack. The report relies on 3 custom attributes that the # user has to define in the project. # Resources must be tagged with a ClarityRID and Tasks must have a # ClarityPID and a ClarityPName. # This file format works for our Clarity installation. I have no idea if it # is even portable to other Clarity installations. class NikuReport < ReportBase def initialize(report) super(report) # A Hash to store NikuProject objects by id @projects = {} # A Hash to map ClarityRID to Resource @resources = {} # Unallocated and vacation time during the report period for all # resources hashed by ClarityId. Values are in days. @resourcesFreeWork = {} # Resources total effort during the report period hashed by ClarityId @resourcesTotalEffort = {} @scenarioIdx = nil end def generateIntermediateFormat super @scenarioIdx = a('scenarios')[0] computeResourceTotals collectProjects computeProjectAllocations end def to_html tableFrame = generateHtmlTableFrame tableFrame << (tr = XMLElement.new('tr')) tr << (td = XMLElement.new('td')) td << (table = XMLElement.new('table', 'class' => 'tj_table', 'cellspacing' => '1')) # Table Header with two rows. First the project name, then the ID. table << (thead = XMLElement.new('thead')) thead << (tr = XMLElement.new('tr', 'class' => 'tabline')) # First line tr << htmlTabCell('Project', true, 'right') @projects.keys.sort.each do |projectId| # Don't include projects without allocations. next if projectTotal(projectId) <= 0.0 name = @projects[projectId].name # To avoid exploding tables for long project names, we only show the # last 15 characters for those. We expect the last characters to be # more significant in those names than the first. name = '...' + name[-15..-1] if name.length > 15 tr << htmlTabCell(name, true, 'center') end tr << htmlTabCell('', true) # Second line thead << (tr = XMLElement.new('tr', 'class' => 'tabline')) tr << htmlTabCell('Resource', true, 'left') @projects.keys.sort.each do |projectId| # Don't include projects without allocations. next if projectTotal(projectId) <= 0.0 tr << htmlTabCell(projectId, true, 'center') end tr << htmlTabCell('Total', true, 'center') # The actual content. One line per resource. table << (tbody = XMLElement.new('tbody')) numberFormat = a('numberFormat') @resourcesTotalEffort.keys.sort.each do |resourceId| tbody << (tr = XMLElement.new('tr', 'class' => 'tabline')) tr << htmlTabCell("#{@resources[resourceId].name} (#{resourceId})", true, 'left') @projects.keys.sort.each do |projectId| next if projectTotal(projectId) <= 0.0 value = sum(projectId, resourceId) valStr = numberFormat.format(value) valStr = '' if valStr.to_f == 0.0 tr << htmlTabCell(valStr) end tr << htmlTabCell(numberFormat.format(resourceTotal(resourceId)), true) end # Project totals tbody << (tr = XMLElement.new('tr', 'class' => 'tabline')) tr << htmlTabCell('Total', 'true', 'left') @projects.keys.sort.each do |projectId| next if (pTotal = projectTotal(projectId)) <= 0.0 tr << htmlTabCell(numberFormat.format(pTotal), true, 'right') end tr << htmlTabCell(numberFormat.format(total()), true, 'right') tableFrame end def to_niku xml = XMLDocument.new xml << XMLComment.new(<<"EOT" Generated by #{AppConfig.softwareName} v#{AppConfig.version} on #{TjTime.new} For more information about #{AppConfig.softwareName} see #{AppConfig.contact}. Project: #{@project['name']} Date: #{@project['now']} EOT ) xml << (nikuDataBus = XMLElement.new('NikuDataBus', 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance', 'xsi:noNamespaceSchemaLocation' => '../xsd/nikuxog_project.xsd')) nikuDataBus << XMLElement.new('Header', 'action' => 'write', 'externalSource' => 'NIKU', 'objectType' => 'project', 'version' => '7.5.0') nikuDataBus << (projects = XMLElement.new('Projects')) timeFormat = '%Y-%m-%dT%H:%M:%S' numberFormat = a('numberFormat') @projects.keys.sort.each do |projectId| prj = @projects[projectId] projects << (project = XMLElement.new('Project', 'name' => prj.name, 'projectID' => prj.id)) project << (resources = XMLElement.new('Resources')) # We iterate over all resources to ensure that all have an entry in # the Clarity database for all projects. This is done to work around a # limitation of Clarity with respect to filling time sheets with # assigned projects. @resources.keys.sort.each do |clarityRID| resources << (resource = XMLElement.new('Resource', 'resourceID' => clarityRID, 'defaultAllocation' => '0')) resource << (allocCurve = XMLElement.new('AllocCurve')) sum = sum(prj.id, clarityRID) allocCurve << (XMLElement.new('Segment', 'start' => a('start').to_s(timeFormat), 'finish' => (a('end') - 1).to_s(timeFormat), 'sum' => numberFormat.format(sum).to_s)) end # The custom information section usually contains Clarity installation # specific parts. They are identical for each project section, so we # mis-use the title attribute to insert them as an XML blob. project << XMLBlob.new(a('title')) unless a('title').empty? end xml.to_s end def to_csv table = [] # Header line with project names table << (row = []) # First column is the resource name and ID. row << "" projectIds = @projects.keys.sort projectIds.each do |projectId| row << @projects[projectId].name end # Header line with project IDs table << (row = []) row << "Resource" projectIds.each do |projectId| row << projectId end @resourcesTotalEffort.keys.sort.each do |resourceId| # Add one line per resource. table << (row = []) row << "#{@resources[resourceId].name} (#{resourceId})" projectIds.each do |projectId| row << sum(projectId, resourceId) end end table end private def sum(projectId, resourceId) project = @projects[projectId] return 0.0 unless project resource = project.resources[resourceId] return 0.0 unless resource && @resourcesTotalEffort[resourceId] resource.sum / @resourcesTotalEffort[resourceId] end def resourceTotal(resourceId) total = 0.0 @projects.each_key do |projectId| total += sum(projectId, resourceId) end total end def projectTotal(projectId) total = 0.0 @resources.each_key do |resourceId| total += sum(projectId, resourceId) end total end def total total = 0.0 @projects.each_key do |projectId| @resources.each_key do |resourceId| total += sum(projectId, resourceId) end end total end def htmlTabCell(text, headerCell = false, align = 'right') td = XMLElement.new('td', 'class' => headerCell ? 'tabhead' : 'taskcell1') td << XMLNamedText.new(text, 'div', 'class' => headerCell ? 'headercelldiv' : 'celldiv', 'style' => "text-align:#{align}") td end # The report must contain percent values for the allocation of the # resources. A value of 1.0 means 100%. The resource is fully allocated # for the whole report period. To compute the percentage later on, we # first have to compute the maximum possible allocation. def computeResourceTotals # Prepare the resource list. resourceList = PropertyList.new(@project.resources) resourceList.setSorting(@report.get('sortResources')) resourceList = filterResourceList(resourceList, nil, @report.get('hideResource'), @report.get('rollupResource'), @report.get('openNodes')) # Prepare a template for the Query we will use to get all the data. queryAttrs = { 'project' => @project, 'scopeProperty' => nil, 'scenarioIdx' => @scenarioIdx, 'loadUnit' => a('loadUnit'), 'numberFormat' => a('numberFormat'), 'timeFormat' => a('timeFormat'), 'currencyFormat' => a('currencyFormat'), 'start' => a('start'), 'end' => a('end'), 'journalMode' => a('journalMode'), 'journalAttributes' => a('journalAttributes'), 'sortJournalEntries' => a('sortJournalEntries'), 'costAccount' => a('costaccount'), 'revenueAccount' => a('revenueaccount') } query = Query.new(queryAttrs) # Calculate the number of working days in the report interval. workingDays = @project.workingDays(TimeInterval.new(a('start'), a('end'))) resourceList.each do |resource| # We only care about leaf resources that have the custom attribute # 'ClarityRID' set. next if !resource.leaf? || (resourceId = resource.get('ClarityRID')).nil? || resourceId.empty? query.property = resource # First get the allocated effort. query.attributeId = 'effort' query.process # Effort in resource days total = query.to_num # A fully allocated resource should always have a total of 1.0 per # working day. If the total is larger, we assume unpaid overtime. If # it's less, the resource was either not fully allocated or had less # working hours or was on vacation. if total >= workingDays @resourcesFreeWork[resourceId] = 0.0 else @resourcesFreeWork[resourceId] = workingDays - total total = workingDays end @resources[resourceId] = resource # This is the maximum possible work of this resource in the report # period. @resourcesTotalEffort[resourceId] = total end # Make sure that we have at least one Resource with a ClarityRID. if @resourcesTotalEffort.empty? raise TjException.new, 'No resources with the custom attribute ClarityRID were found!' end end # Search the Task list for the various ClarityPIDs and create a new Task # list for each ClarityPID. def collectProjects # Prepare the task list. taskList = PropertyList.new(@project.tasks) taskList.setSorting(@report.get('sortTasks')) taskList = filterTaskList(taskList, nil, @report.get('hideTask'), @report.get('rollupTask'), @report.get('openNodes')) taskList.each do |task| # We only care about tasks that are leaf tasks and have resource # allocations. next unless task.leaf? || task['assignedresources', @scenarioIdx].empty? id = task.get('ClarityPID') # Ignore tasks without a ClarityPID attribute. next if id.nil? if id.empty? raise TjException.new, "ClarityPID of task #{task.fullId} may not be empty" end name = task.get('ClarityPName') if name.nil? raise TjException.new, "ClarityPName of task #{task.fullId} has not been set!" end if name.empty? raise TjException.new, "ClarityPName of task #{task.fullId} may not be empty!" end if (project = @projects[id]).nil? # We don't have a record for the Clarity project yet, so we create a # new NikuProject object. project = NikuProject.new(id, name) # And store it in the project list hashed by the ClarityPID. @projects[id] = project else # Due to a design flaw in the Niku file format, Clarity projects are # identified by a name and an ID. We have to check that those pairs # are always the same. if (fTask = project.tasks.first).get('ClarityPName') != name raise TjException.new, "Task #{task.fullId} and task #{fTask.fullId} " + "have same ClarityPID (#{id}) but different ClarityPName " + "(#{name}/#{fTask.get('ClarityPName')})" end end # Append the Task to the task list of the Clarity project. project.tasks << task end if @projects.empty? raise TjException.new, 'No tasks with the custom attributes ClarityPID and ClarityPName ' + 'were found!' end # If the user did specify a project ID and name to collect the vacation # time, we'll add this as a project as well. if (id = @report.get('timeOffId')) && (name = @report.get('timeOffName')) @projects[id] = project = NikuProject.new(id, name) @resources.each do |resourceId, resource| project.resources[resourceId] = r = NikuResource.new(resourceId) r.sum = @resourcesFreeWork[resourceId] end end end # Compute the total effort each Resource is allocated to the Task objects # that have the same ClarityPID. def computeProjectAllocations # Prepare a template for the Query we will use to get all the data. queryAttrs = { 'project' => @project, 'scenarioIdx' => @scenarioIdx, 'loadUnit' => a('loadUnit'), 'numberFormat' => a('numberFormat'), 'timeFormat' => a('timeFormat'), 'currencyFormat' => a('currencyFormat'), 'start' => a('start'), 'end' => a('end'), 'journalMode' => a('journalMode'), 'journalAttributes' => a('journalAttributes'), 'sortJournalEntries' => a('sortJournalEntries'), 'costAccount' => a('costaccount'), 'revenueAccount' => a('revenueaccount') } query = Query.new(queryAttrs) timeOffId = @report.get('timeOffId') @projects.each_value do |project| next if project.id == timeOffId project.tasks.each do |task| task['assignedresources', @scenarioIdx].each do |resource| # Only consider resources that are in the filtered resource list. next unless @resources[resource.get('ClarityRID')] query.property = task query.scopeProperty = resource query.attributeId = 'effort' query.process work = query.to_num # If the resource was not actually working on this task during the # report period, we don't create a record for it. next if work <= 0.0 resourceId = resource.get('ClarityRID') if (resourceRecord = project.resources[resourceId]).nil? # If we don't already have a NikuResource object for the # Resource, we create a new one. resourceRecord = NikuResource.new(resourceId) # Store the new NikuResource in the resource list of the # NikuProject record. project.resources[resourceId] = resourceRecord end resourceRecord.sum += query.to_num end end end end end end TaskJuggler-3.8.1/lib/taskjuggler/reports/Report.rb000066400000000000000000000377041473026623400223550ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = Report.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'fileutils' require 'taskjuggler/PropertyTreeNode' require 'taskjuggler/reports/AccountListRE' require 'taskjuggler/reports/TextReport' require 'taskjuggler/reports/TaskListRE' require 'taskjuggler/reports/ResourceListRE' require 'taskjuggler/reports/TraceReport' require 'taskjuggler/reports/TagFile' require 'taskjuggler/reports/ExportRE' require 'taskjuggler/reports/StatusSheetReport' require 'taskjuggler/reports/TimeSheetReport' require 'taskjuggler/reports/NikuReport' require 'taskjuggler/reports/ICalReport' require 'taskjuggler/reports/CSVFile' require 'taskjuggler/reports/Navigator' require 'taskjuggler/reports/ReportContext' require 'taskjuggler/HTMLDocument' class TaskJuggler # Just a dummy class to make the 'flags' attribute work. class ReportScenario < ScenarioData end # The Report class holds the fundamental description and functionality to # turn the scheduled project into a user readable form. A report may contain # other reports. class Report < PropertyTreeNode attr_accessor :typeSpec, :content # Create a new report object. def initialize(project, id, name, parent) super(project.reports, id, name, parent) @messageHandler = MessageHandlerInstance.instance checkFileName(name) project.addReport(self) # The type specifier must be set for every report. It tells whether this # is a task, resource, text or other report. @typeSpec = nil # Reports don't really have any scenario specific attributes. But the # flag handling code assumes they are. To use flags, we need them as # well. @data = Array.new(@project.scenarioCount, nil) @project.scenarioCount.times do |i| ReportScenario.new(self, i, @scenarioAttributes[i]) end end # The generate function is where the action happens in this class. The # report defined by all the class attributes and report elements is # generated according the the requested output format(s). # _requestedFormats_ can be a list of formats that should be generated (e. # g. :html, :csv, etc.). def generate(requestedFormats = nil) oldTimeZone = TjTime.setTimeZone(get('timezone')) generateIntermediateFormat # We either generate the requested formats or the list of formats that # was specified in the report definition. (requestedFormats || get('formats')).each do |format| if @name.empty? error('empty_report_file_name', "Report #{@id} has output formats requested, but the " + "file name is empty.", sourceFileInfo) end case format when :iCal generateICal when :html generateHTML copyAuxiliaryFiles when :csv generateCSV when :ctags generateCTags when :niku generateNiku when :tjp generateTJP when :mspxml generateMspXml else raise 'Unknown report output format #{format}.' end end TjTime.setTimeZone(oldTimeZone) 0 end # Generate an output format agnostic version that can later be turned into # the respective output formats. def generateIntermediateFormat if get('scenarios').empty? warning('all_scenarios_disabled', "The report #{fullId} has only disabled scenarios. The " + "report will possibly be empty.") end @content = nil case @typeSpec when :accountreport @content = AccountListRE.new(self) when :export @content = ExportRE.new(self) when :iCal @content = ICalReport.new(self) when :niku @content = NikuReport.new(self) when :resourcereport @content = ResourceListRE.new(self) when :tagfile @content = TagFile.new(self) when :textreport @content = TextReport.new(self) when :taskreport @content = TaskListRE.new(self) when :tracereport @content = TraceReport.new(self) when :statusSheet @content = StatusSheetReport.new(self) when :timeSheet @content = TimeSheetReport.new(self) else raise "Unknown report type" end # Most output format can be generated from a common intermediate # representation of the elements. We generate that IR first. @content.generateIntermediateFormat if @content end # Render the content of the report as HTML (without the framing). def to_html @content ? @content.to_html : nil end # Return true if the report should be rendered in the interactive version, # false if not. The top-level report defines the output format and the # interactive setting. def interactive? @project.reportContexts.first.report.get('interactive') end private # Convenience function to access a report attribute def a(attribute) get(attribute) end # Generate an HTML version of the report. def generateHTML return nil unless @content unless @content.respond_to?('to_html') warning('html_not_supported', "HTML format is not supported for report #{@id} of " + "type #{@typeSpec}.") return nil end html = HTMLDocument.new head = html.generateHead(@project['name'] + " - #{get('title') || @name}", { 'description' => 'TaskJuggler Report', 'keywords' => 'taskjuggler, project, management' }, a('rawHtmlHead')) if a('selfcontained') auxSrcDir = AppConfig.dataDirs('data/css')[0] cssFileName = (auxSrcDir ? auxSrcDir + '/tjreport.css' : '') # Raise an error if we haven't found the data directory if auxSrcDir.nil? || !File.exist?(cssFileName) dataDirError(cssFileName, AppConfig.dataSearchDirs('data/css')) end cssFile = IO.read(cssFileName) if cssFile.empty? error('css_file_error', "Cannot read '#{cssFileName}'. Make sure the file is not " + "empty and you have read access permission.", sourceFileInfo) end head << XMLElement.new('meta', 'http-equiv' => 'Content-Style-Type', 'content' => 'text/css; charset=utf-8') head << (style = XMLElement.new('style', 'type' => 'text/css')) style << XMLBlob.new("\n" + cssFile) else head << XMLElement.new('link', 'rel' => 'stylesheet', 'type' => 'text/css', 'href' => "#{a('auxdir')}css/tjreport.css") end html.html << XMLComment.new("Dynamic Report ID: " + "#{@project.reportContexts.last.dynamicReportId}") html.html << (body = XMLElement.new('body')) unless a('selfcontained') body << XMLElement.new('script', 'type' => 'text/javascript', 'src' => "#{a('auxdir')}scripts/wz_tooltip.js") body << (noscript = XMLElement.new('noscript')) noscript << (nsdiv = XMLElement.new('div', 'style' => 'text-align:center; ' + 'color:#FF0000')) nsdiv << XMLText.new(<<'EOT' This page requires Javascript for full functionality. Please enable it in your browser settings! EOT ) end # Make sure we have some margins around the report. body << (frame = XMLElement.new('div', 'class' => 'tj_page')) frame << @content.to_html # The footer with some administrative information. frame << (div = XMLElement.new('div', 'class' => 'copyright')) div << XMLText.new(@project['copyright'] + " - ") if @project['copyright'] div << XMLText.new("Project: #{@project['name']} " + "Version: #{@project['version']} - " + "Created on #{TjTime.new.to_s("%Y-%m-%d %H:%M:%S")} " + "with ") div << XMLNamedText.new("#{AppConfig.softwareName}", 'a', 'href' => "#{AppConfig.contact}") div << XMLText.new(" v#{AppConfig.version}") fileName = if a('interactive') || @name == '.' # Interactive HTML reports are always sent to stdout. '.' else # Prepend the specified output directory unless the provided file # name is an absolute file name. absoluteFileName(@name) + '.html' end begin html.write(fileName) rescue IOError, SystemCallError error('write_html', "Cannot write to file #{fileName}.\n#{$!}", sourceFileInfo) end end # Generate a CSV version of the report. def generateCSV # The CSV format can only handle the first element of a report. return nil unless @content unless @content.respond_to?('to_csv') warning('csv_not_supported', "CSV format is not supported for report #{@id} of " + "type #{@typeSpec}.") return nil end return nil unless (csv = @content.to_csv) # Use the CSVFile class to write the Array of Arrays to a colon # separated file. Write to $stdout if the filename was set to '.'. begin fileName = (@name == '.' ? '.' : absoluteFileName(@name) + '.csv') CSVFile.new(csv, ';').write(fileName) rescue IOError, SystemCallError error('write_csv', "Cannot write to file #{fileName}.\n#{$!}", sourceFileInfo) end end # Generate the report in TJP format. def generateTJP unless @content.respond_to?('to_tjp') warning('tjp_not_supported', "TJP format is not supported for report #{@id} of " + "type #{@typeSpec}.") return nil end begin fileName = '.' if @name == '.' $stdout.write(@content.to_tjp) else fileName = @name fileName += a('definitions').include?('project') ? '.tjp' : '.tji' File.open(fileName, 'w') { |f| f.write(@content.to_tjp) } end rescue IOError, SystemCallError error('write_tjp', "Cannot write to file #{fileName}.\n#{$!}", sourceFileInfo) end end # Generate the report in Microsoft Project XML format. def generateMspXml unless @content.respond_to?('to_mspxml') warning('mspxml_not_supported', "Microsoft Project XML format is not supported for " + "report #{@id} of type #{@typeSpec}.") return nil end begin fileName = '.' if @name == '.' $stdout.write(@content.to_mspxml) else fileName = absoluteFileName(@name) + '.xml' File.open(fileName, 'w') { |f| f.write(@content.to_mspxml) } end rescue IOError, SystemCallError error('write_mspxml', "Cannot write to file #{fileName}.\n#{$!}", sourceFileInfo) end end # Generate Niku report def generateNiku unless @content.respond_to?('to_niku') warning('niku_not_supported', "niku format is not supported for report #{@id} of " + "type #{@typeSpec}.") return nil end begin f = @name == '.' ? $stdout : File.new(absoluteFileName(@name) + '.xml', 'w') f.puts "#{@content.to_niku}" rescue IOError, SystemCallError error('write_niku', "Cannot write to file #{@name}.\n#{$!}", sourceFileInfo) end end # Generate the report in iCal format. def generateICal unless @content.respond_to?('to_iCal') warning('ical_not_supported', "iCalendar format is not supported for report #{@id} of " + "type #{@typeSpec}.") return nil end begin f = @name == '.' ? $stdout : File.new(absoluteFileName(@name) + '.ics', 'w') f.puts "#{@content.to_iCal}" rescue IOError, SystemCallError error('write_ical', "Cannot write to file #{@name}.\n#{$!}", sourceFileInfo) end end # Generate ctags file def generateCTags unless @content.respond_to?('to_ctags') warning('ctags_not_supported', "ctags format is not supported for report #{@id} of " + "type #{@typeSpec}.") return nil end begin f = @name == '.' ? $stdout : File.new(absoluteFileName(@name), 'w') f.puts "#{@content.to_ctags}" rescue IOError, SystemCallError error('write_ctags', "Cannot write to file #{@name}.\n#{$!}", sourceFileInfo) end end def copyAuxiliaryFiles # Don't copy files if output is stdout, the requested by the web server # or the user has specified a custom aux directory. return if @name == '.' || a('interactive') || !a('auxdir').empty? copyDirectory('css') copyDirectory('icons') copyDirectory('scripts') end def copyDirectory(dirName) # The directory needs to be in the same directory as the HTML report. auxDstDir = File.dirname(absoluteFileName(@name)) + '/' # Find the data directory that came with the TaskJuggler installation. auxSrcDir = AppConfig.dataDirs("data/#{dirName}")[0] # Raise an error if we haven't found the data directory if auxSrcDir.nil? || !File.exist?(auxSrcDir) dataDirError(dirName, AppConfig.dataSearchDirs("data/#{dirName}")) end # Don't copy directory if all files are up-to-date. return if directoryUpToDate?(auxSrcDir, auxDstDir + dirName) begin # Recursively copy the directory and all content. FileUtils.cp_r(auxSrcDir, auxDstDir) rescue IOError, SystemCallError error('copy_dir', "Cannot copy directory #{auxSrcDir} to " + "#{auxDstDir}.\n#{$!}", sourceFileInfo) end end def directoryUpToDate?(auxSrcDir, auxDstDir) return false unless File.exist?(auxDstDir) Dir.entries(auxSrcDir).each do |file| next if file == '.' || file == '..' srcFile = (auxSrcDir + '/' + file) dstFile = (auxDstDir + '/' + file) return false if !File.exist?(dstFile) || File.mtime(srcFile) > File.mtime(dstFile) end true end def dataDirError(dirName, dirs) error('data_dir_error', <<"EOT", Cannot find the #{dirName} directory. This is usually the result of an improper TaskJuggler installation. If you know the directory, you can use the TASKJUGGLER_DATA_PATH environment variable to specify the location. The variable should be set to the path without the /data at the end. Multiple directories must be separated by colons. The following directories have been tried: #{dirs.join("\n")} EOT sourceFileInfo ) end def windowsOS? (/cygwin|mswin|mingw|bccwin|wince|emx/ =~ RUBY_PLATFORM) != nil end def checkFileName(name) if windowsOS? illegalChars = /[\x00\\\*\?\"<>\|]/ else illegalChars = /[\\?%*:|"<>]/ end if name =~ illegalChars error('invalid_file_name', 'File names may not contain any of the following characters: ' + '\?%*:|\"<>', sourceFileInfo) end end def absoluteFileName?(name) if windowsOS? name[0] =~ /[a-zA-Z]/ && name[1] == ?: else name[0] == ?/ end end def absoluteFileName(name) (absoluteFileName?(name) ? '' : @project.outputDir) + name end end end TaskJuggler-3.8.1/lib/taskjuggler/reports/ReportBase.rb000066400000000000000000000147551473026623400231510ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = ReportBase.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # class TaskJuggler # This is the abstract base class for all kinds of reports. The derived # classes must implement the generateIntermediateFormat function as well as # the to_* members. class ReportBase def initialize(report) @report = report @project = report.project end # Convenience function to access a report attribute def a(attribute) @report.get(attribute) end def generateIntermediateFormat query = @report.project.reportContexts.last.query %w( header left center right footer prolog headline caption epilog ).each do |name| next unless (text = a(name)) text.setQuery(query) end end # Take the complete account list and remove all accounts that are matching # the hide expression, the rollup Expression or are not a descendent of # accountroot. def filterAccountList(list_, hideExpr, rollupExpr, openNodes) list = PropertyList.new(list_) if (accountroot = a('accountroot')) # Remove all accounts that are not descendents of the accountroot. list.delete_if { |account| !account.isChildOf?(accountroot) } end standardFilterOps(list, hideExpr, rollupExpr, openNodes, nil, accountroot) end # Take the complete task list and remove all tasks that are matching the # hide expression, the rollup Expression or are not a descendent of # taskroot. In case resource is not nil, a task is only included if # the resource is allocated to it in any of the reported scenarios. def filterTaskList(list_, resource, hideExpr, rollupExpr, openNodes) list = PropertyList.new(list_) if (taskRoot = a('taskroot')) # Remove all tasks that are not descendents of the taskRoot. list.delete_if { |task| !task.isChildOf?(taskRoot) } end if resource # If we have a resource we need to check that the resource is allocated # to the tasks in any of the reported scenarios within the report time # frame. list.delete_if do |task| delete = true a('scenarios').each do |scenarioIdx| iv = TimeInterval.new(a('start'), a('end')) if task.hasResourceAllocated?(scenarioIdx, iv, resource) delete = false break; end end delete end end standardFilterOps(list, hideExpr, rollupExpr, openNodes, resource, taskRoot) end # Take the complete resource list and remove all resources that are matching # the hide expression, the rollup Expression or are not a descendent of # resourceroot. In case task is not nil, a resource is only included if # it is assigned to the task in any of the reported scenarios. def filterResourceList(list_, task, hideExpr, rollupExpr, openNodes) list = PropertyList.new(list_) if (resourceRoot = a('resourceroot')) # Remove all resources that are not descendents of the resourceRoot. list.delete_if { |resource| !resource.isChildOf?(resourceRoot) } end if task # If we have a task we need to check that the resources are assigned # to the task in any of the reported scenarios. iv = TimeInterval.new(a('start'), a('end')) list.delete_if do |resource| delete = true a('scenarios').each do |scenarioIdx| if task.hasResourceAllocated?(scenarioIdx, iv, resource) delete = false break; end end delete end end standardFilterOps(list, hideExpr, rollupExpr, openNodes, task, resourceRoot) end private def generateHtmlTableFrame table = XMLElement.new('table', 'class' => 'tj_table_frame', 'cellspacing' => '1') # Headline box if a('headline') table << generateHtmlTableRow do td = XMLElement.new('td') td << (div = XMLElement.new('div', 'class' => 'tj_table_headline')) div << a('headline').to_html td end end table end def generateHtmlTableRow XMLElement.new('tr') << yield end # Convert the RichText object _name_ into a HTML form. def rt_to_html(name) return unless a(name) a(name).sectionNumbers = false a(name).to_html end # This function implements the generic filtering functionality for all kinds # of lists. def standardFilterOps(list, hideExpr, rollupExpr, openNodes, scopeProperty, root) # Make a copy of the current Query. query = @project.reportContexts.last.query.dup query.scopeProperty = scopeProperty # Remove all properties that the user wants to have hidden. if hideExpr list.delete_if do |property| query.property = property hideExpr.eval(query) end end # Remove all children of properties that the user has rolled-up. if rollupExpr || openNodes list.delete_if do |property| parent = property.parent delete = false while (parent) query.property = parent # If openNodes is not nil, only the listed nodes will be unrolled. # If openNodes is nil, only the nodes that match rollupExpr will # not be unrolled. if (openNodes && !openNodes.include?([ parent, scopeProperty ])) || (!openNodes && rollupExpr.eval(query)) delete = true break end parent = parent.parent end delete end end # Re-add parents in tree mode if list.treeMode? parents = [] list.each do |property| parent = property while (parent = parent.parent) parents << parent unless list.include?(parent) || parents.include?(parent) break if parent == root end end list.append(parents) end list end end end TaskJuggler-3.8.1/lib/taskjuggler/reports/ReportContext.rb000066400000000000000000000061131473026623400237100ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = ReportContext.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # class TaskJuggler # The ReportContext objects provide some settings that are used during the # generation of a report. Reports can be nested, so multiple objects can # exist at a time. But there is only one current ReportContext that is # always accessable via Project.reportContexts.last(). class ReportContext attr_reader :dynamicReportId, :project, :report, :query attr_accessor :childReportCounter, :tasks, :resources, :attributeBackup def initialize(project, report) @project = project @report = report @childReportCounter = 0 @attributeBackup = nil queryAttrs = { 'project' => @project, 'loadUnit' => @report.get('loadUnit'), 'numberFormat' => @report.get('numberFormat'), 'timeFormat' => @report.get('timeFormat'), 'currencyFormat' => @report.get('currencyFormat'), 'start' => @report.get('start'), 'end' => @report.get('end'), 'hideJournalEntry' => @report.get('hideJournalEntry'), 'journalMode' => @report.get('journalMode'), 'journalAttributes' => @report.get('journalAttributes'), 'sortJournalEntries' => @report.get('sortJournalEntries'), 'costAccount' => @report.get('costaccount'), 'revenueAccount' => @report.get('revenueaccount') } @query = Query.new(queryAttrs) if (@parent = @project.reportContexts.last) # For interactive reports we need some ID that uniquely identifies the # report within the composed report. Since a project report can be # included multiple times in the same report, we need to generate # another ID for each instantiated report. We create this report by # using a counter for the number of child reports that each report # has. The unique ID is then the concatenated list of counters from # parent to leaf, separating each value by a '.'. @dynamicReportId = @parent.dynamicReportId + ".#{@parent.childReportCounter}" @parent.childReportCounter += 1 # If the new ReportContext is created from within an existing context, # this is used as parent context and the settings are copied as # default initial values. @tasks = @parent.tasks.dup @resources = @parent.resources.dup else # The ID of the root report is always "0". The first child will then # be "0.0", the seconds "0.1" and so on. @dynamicReportId = "0" # There is no existing ReportContext yet, so we create one based on # the settings of the report. @tasks = @project.tasks.dup @resources = @project.resources.dup end end end end TaskJuggler-3.8.1/lib/taskjuggler/reports/ReportTable.rb000066400000000000000000000131161473026623400233140ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = ReportTable.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/reports/ReportTableColumn' require 'taskjuggler/reports/ReportTableLine' class TaskJuggler # This class models the intermediate format of all report tables. The # generators for all the table reports create the report in this intermediate # format. The to_* member functions can then output the table in the # appropriate format. class ReportTable # The height in pixels of a horizontal scrollbar on an HTML page. This # value should be large enough to work for all browsers. SCROLLBARHEIGHT = 20 attr_reader :maxIndent, :headerLineHeight, :headerFontSize attr_accessor :equiLines, :embedded, :selfcontained, :auxDir # Create a new ReportTable object. def initialize # The height if the header lines in screen pixels. @headerLineHeight = 19 # Size of the font used in the header @headerFontSize = 15 # Array of ReportTableColumn objects. @columns = [] # Array of ReportTableLine objects. @lines = [] @maxIndent = 0 # Whether or not all table lines must have same height. @equiLines = false # True if the table is embedded as a column of another ReportTable. @embedded = false # True if the report does not rely on the data of other files. @selfcontained = false # Path to the auxiliary data directory. @auxDir = '' end # This function should only be called by the ReportTableColumn constructor. def addColumn(col) @columns << col end # This function should only be called by the ReportTableLine constructor. def addLine(line) @lines << line end # Return the number of registered lines for this table. def lines @lines.length end # Return the minimum required width for the table. If we don't have a # mininum with, nil is returned. def minWidth width = 1 @columns.each do |column| cw = column.minWidth width += cw + 1 if cw end width end # Output the table as HTML. def to_html determineMaxIndents attr = { 'class' => 'tj_table', 'cellspacing' => '1' } attr['style'] = 'width:100%; ' if @embedded table = XMLElement.new('table', attr) table << (tbody = XMLElement.new('tbody')) # Generate the 1st table header line. allCellsHave2Rows = true lineHeight = @headerLineHeight @columns.each do |col| if col.cell1.rows != 2 && !col.cell1.special allCellsHave2Rows = false break; end end if allCellsHave2Rows @columns.each { |col| col.cell1.rows = 1 } lineHeight = @headerLineHeight * 2 + 1 end tbody << (tr = XMLElement.new('tr', 'class' => 'tabhead', 'style' => "height:#{lineHeight}px; " + "font-size:#{@headerFontSize}px;")) @columns.each { |col| tr << col.to_html(1) } unless allCellsHave2Rows # Generate the 2nd table header line. tbody << (tr = XMLElement.new('tr', 'class' => 'tabhead', 'style' => "height:#{@headerLineHeight}px; " + "font-size:#{@headerFontSize}px;")) @columns.each { |col| tr << col.to_html(2) } end # Generate the rest of the table. @lines.each { |line| tbody << line.to_html } # In case we have columns with scrollbars, we generate an extra line with # cells for all columns that don't have a scrollbar. The scrollbar must # have a height of SCROLLBARHEIGHT pixels or less. if hasScrollbar? tbody << (tr = XMLElement.new('tr', 'style' => "height:#{SCROLLBARHEIGHT}px")) @columns.each do |column| unless column.scrollbar tr << XMLElement.new('td') end end end table end # Convert the intermediate representation into an Array of Arrays. _csv_ is # the destination Array of Arrays. It may contain columns already. def to_csv(csv = [[ ]], startColumn = 0) # Generate the header line. columnIdx = startColumn @columns.each do |col| columnIdx += col.to_csv(csv, columnIdx) end if @embedded columnIdx - startColumn else # Content of embedded tables is inserted when generating the # respective Line. lineIdx = 1 @lines.each do |line| # Insert a new Array for each line. csv[lineIdx] = [] line.to_csv(csv, startColumn, lineIdx) lineIdx += 1 end csv end end private # Some columns need to be indented when the data is sorted in tree mode. # This function determines the largest needed indentation of all lines. The # result is stored in the _@maxIndent_ variable. def determineMaxIndents @maxIndent = 0 @lines.each do |line| @maxIndent = line.indentation if line.indentation > @maxIndent end end # Returns true if any of the columns has a scrollbar. def hasScrollbar? @columns.each { |col| return true if col.scrollbar } false end end end TaskJuggler-3.8.1/lib/taskjuggler/reports/ReportTableCell.rb000066400000000000000000000327601473026623400241220ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = ReportTableCell.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2024 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # class TaskJuggler # This class models the output format independent version of a cell in a # TableReport. It belongs to a certain ReportTableLine and # ReportTableColumn. Normally a cell contains text on a colored background. # By help of the @special variable it can alternatively contain any object # the provides the necessary output methods such as to_html. class ReportTableCell attr_reader :line attr_accessor :data, :category, :hidden, :alignment, :padding, :force_string, :text, :tooltip, :showTooltipHint, :iconTooltip, :cellColor, :indent, :icon, :fontSize, :fontColor, :bold, :width, :rows, :columns, :special # Create the ReportTableCell object and initialize the attributes to some # default values. _line_ is the ReportTableLine this cell belongs to. _text_ # is the text that should appear in the cell. _headerCell_ is a flag that # must be true only for table header cells. def initialize(line, query, text = nil, headerCell = false) @line = line @line.addCell(self) if line # Specifies whether this is a header cell or not. @headerCell = headerCell # A copy of a Query object that is needed to access project data via the # query function. @query = query ? query.dup : nil # The cell textual content. This may be a String or a # RichTextIntermediate object. self.text = text || '' # A custom text for the tooltip. @tooltip = nil # Determines if the tooltip is triggered by an special hinting icon or # the whole cell. @showTooltipHint = true # The original data of the cell content (optional, nil if not provided) @data = nil # Determines the background color of the cell. @category = nil # True of the cell is hidden (because other cells span multiple rows or # columns) @hidden = false # How to horizontally align the cell @alignment = :center # Horizontal padding between frame and cell content @padding = 3 # Don't convert Strings that look like numbers to String @force_string = false # Whether or not to indent the cell. If not nil, it is an Integer # indicating the indentation level. @indent = nil # The basename of the icon file @icon = nil # A custom tooltip for the cell icon @iconTooltip = nil # Font size of the cell text in pixels @fontSize = nil # The background color of the cell. Overwrite the @category color. @cellColor = nil # The color of the cell text font. @fontColor = nil # True of a bold font is to be used for the cell text. @bold = false # The width of the column in pixels @width = nil # The number of rows the cell spans @rows = 1 # The number of columns the cell spans @columns = 1 # Ignore everything and use this reference to generate the output. @special = nil end # Return true if two cells are similar enough so that they can be merged in # the report to a single, wider cell. _c_ is the cell to compare this cell # with. def ==(c) @text == c.text && @tooltip == c.tooltip && @alignment == c.alignment && @padding == c.padding && @indent == c.indent && @cellColor == c.cellColor && @category == c.category end # Turn the abstract cell representation into an HTML element tree. def to_html return nil if @hidden return @special.to_html if @special # Determine cell attributes attribs = { } attribs['rowspan'] = "#{@rows}" if @rows > 1 attribs['colspan'] = "#{@columns}" if @columns > 1 attribs['class'] = @category ? @category : 'tabcell' style = '' style += "background-color: #{@cellColor}; " if @cellColor attribs['style'] = style unless style.empty? cell = XMLElement.new('td', attribs) cell << (table = XMLElement.new('table', 'class' => @category ? 'tj_table_cell' : 'tj_table_header_cell', 'cellspacing' => '0', 'style' => cellStyle)) table << (row = XMLElement.new('tr')) calculateIndentation # Insert a padding cell for the left side indentation. if @leftIndent && @leftIndent > 0 row << XMLElement.new('td', 'style' => "width:#{@leftIndent}px; ") end row << cellIcon(cell) labelDiv, tooltip = cellLabel row << labelDiv # Overwrite the tooltip if the user has specified a custom tooltip. tooltip = @tooltip if @tooltip if tooltip && !tooltip.empty? && !selfcontained if @showTooltipHint row << (td = XMLElement.new('td')) td << XMLElement.new('img', 'src' => "#{auxDir}icons/details.png", 'class' => 'tj_table_cell_tooltip') addHtmlTooltip(tooltip, td, cell) else addHtmlTooltip(tooltip, cell) end end # Insert a padding cell for the right side indentation. if @rightIndent && @rightIndent > 0 row << XMLElement.new('td', 'style' => "width:#{@rightIndent}px; ") end cell end # Add the text content of the cell to an Array of Arrays form of the table. def to_csv(csv, columnIdx, lineIdx) # We only support left indentation in CSV files as the spaces for right # indentation will be disregarded by most applications. indent = @indent && @alignment == :left ? ' ' * @indent : '' columns = 1 if @special # This is for nested tables. They will be inserted as whole columns # in the existing table. csv[lineIdx][columnIdx] = nil columns = @special.to_csv(csv, columnIdx) else cell = if @data && @data.is_a?(String) @data elsif @text if @text.respond_to?('functionHandler') @text.setQuery(@query) end str = @text.to_s # Remove any trailing line breaks. These don't really make much # sense in CSV files. while str[-1] == ?\n str.chomp! end str end # Try to convert numbers and other types to their native Ruby type if # they are supported by CSVFile. native = @force_string ? cell : CSVFile.strToNative(cell) # Only for String objects, we add the indentation. csv[lineIdx][columnIdx] = (native.is_a?(String) && !@force_string ? indent + native : native) end return columns end private def selfcontained @line && @line.table.selfcontained end def auxDir @line ? @line.table.auxDir : nil end def calculateIndentation # In tree sorting mode, some cells have to be indented to reflect the # tree nesting structure. The indentation is achieved with padding cells # and needs to be applied to the proper side depending on the alignment. @leftIndent = @rightIndent = 0 if @indent && @alignment != :center if @alignment == :left @leftIndent = @indent * 8 elsif @alignment == :right @rightIndent = (@line.table.maxIndent - @indent) * 8 end end end # Determine cell style def cellStyle style = "text-align:#{@alignment.to_s}; " if @line && @line.table.equiLines style += "height:#{@line.height - 7}px; " end style end def cellIcon(cell) if @icon && !selfcontained td = XMLElement.new('td', 'class' => 'tj_table_cell_icon') td << XMLElement.new('img', 'src' => "#{auxDir}icons/#{@icon}.png", 'alt' => "Icon") addHtmlTooltip(@iconTooltip, td, cell) return td end nil end def cellLabel # If we have a RichText content and a width limit, we enable line # wrapping. # Overfl. Wrap. Height Width # Fixed Height: x - x - # Fixed Width: x x - x # Both: x - x x # None: - x - - fixedHeight = @line && @line.table.equiLines fixedWidth = !@width.nil? style = '' style += "overflow:hidden; " if fixedHeight || fixedWidth style += "white-space:#{fixedWidth && !fixedHeight ? 'normal' : 'nowrap'}; " if fixedHeight && !fixedWidth style += "height:#{@line.height - 3}px; " end style += 'font-weight:bold; ' if @bold style += "font-size: #{@fontSize}px; " if fontSize if @fontColor style += "color:#{@fontColor}; " end return nil, nil if @text.nil? || @text.empty? tooltip = nil # @text can be a String or a RichText (with or without embedded # queries). To find out if @text has multiple lines, we need to expand # it and convert it to a plain text again. textAsString = if @text.is_a?(RichTextIntermediate) # @text is a RichText. if @text.respond_to?('functionHandler') @text.setQuery(@query) end @text.to_s else @text end return nil, nil if textAsString.empty? if @width # We have 4 pixels padding on each side of the cell. labelWidth = @width - 8 labelWidth -= @leftIndent if @leftIndent labelWidth -= @rightIndent if @rightIndent if !selfcontained # The icons are 20 pixels width including padding. labelWidth -= 20 if @icon labelWidth -= 20 if tooltip || @tooltip end else labelWidth = nil end shortText, singleLine = shortVersion(textAsString, labelWidth) if (@line && @line.table.equiLines && (!singleLine || @width )) && !@headerCell # The cell is size-limited. We only put a shortened plain-text version # in the cell and provide the full content via a tooltip. # Header cells are never shortened. tooltip = @text if shortText != textAsString tl = XMLText.new(shortText) else tl = (@text.is_a?(RichTextIntermediate) ? @text.to_html : XMLText.new(@text)) end if labelWidth style += "min-width: #{labelWidth}px; max-width: #{labelWidth}px; " end td = XMLElement.new('td', 'class' => 'tj_table_cell_label', 'style' => style) td << tl return td, tooltip end # Convert a RichText String into a small one-line plain text # version that fits the column. def shortVersion(itext, width) text = itext.to_s singleLine = true modified = false if text.include?("\n") text = text[0, text.index("\n")] singleLine = false modified = true end if width widthWithoutIcon = width - 20 # Assuming an average character width of 7 pixels maxChars = widthWithoutIcon / 7 if text.length > maxChars if maxChars > 0 text = text[0, maxChars] else text = '' end modified = true end end # Add three dots to show that there is more info available. text += "..." if modified [ text, singleLine ] end def addHtmlTooltip(tooltip, trigger, hook = nil) return unless tooltip && !tooltip.empty? && !selfcontained hook = trigger if hook.nil? if tooltip.respond_to?('functionHandler') tooltip.setQuery(@query) end if @query @query.attributeId = 'name' @query.process title = @query.to_s else title = '' end trigger['onclick'] = "TagToTip('ID#{trigger.object_id}', " + "TITLE, '#{title.gsub(/'/, ''')}')" trigger['style'] = trigger['style'] ? trigger['style'] : 'cursor:help; ' hook << (ltDiv = XMLElement.new('div', 'class' => 'tj_tooltip_box', 'style' => 'cursor:help', 'id' => "ID#{trigger.object_id}")) ltDiv << (tooltip.respond_to?('to_html') ? tooltip.to_html : XMLText.new(tooltip)) end end # This class is used to model cells that are just placeholders for a line of # an embedded ReportTable. class PlaceHolderCell # Create a new placeholder cell. _line_ is the line that this cell belongs # to. _embeddedLine_ is the ReportTableLine that is embedded in this cell. def initialize(line, embeddedLine) @line = line @line.addCell(self) if line @embeddedLine = embeddedLine end # Add the current cell to the _csv_ CSV Arrays. _columnIdx_ is the start # column in the _csv_. _lineIdx_ is the index of the current line. The # return value is the number of added cells. def to_csv(csv, columnIdx, lineIdx) @embeddedLine.to_csv(csv, columnIdx, lineIdx) end def to_html nil end end end TaskJuggler-3.8.1/lib/taskjuggler/reports/ReportTableColumn.rb000066400000000000000000000056041473026623400244750ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = ReportTableColumn.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # class TaskJuggler # The ReportTableColumn class models the output format independend column of a # ReportTable. It usually just contains the table header description. The # table header comprises of one or two lines per column. So each column header # consists of 2 cells. @cell1 is the top cell and must be present. @cell2 is # the optional bottom cell. If @cell2 is hidden, @cell1 takes all the vertical # space. # # For some columns, the table does not contain the usual grid lines but # another abstract object that responds to the usual generator methods such as # to_html(). In such a case, @cell1 references the embedded object via its # special variable. The embedded object then replaced the complete column # content. class ReportTableColumn attr_reader :definition, :cell1, :cell2 attr_accessor :scrollbar # Create a new column. _table_ is a reference to the ReportTable this column # belongs to. _definition_ is the TableColumnDefinition of the column from # the project definition. _title_ is the text that is used for the column # header. def initialize(table, definition, title) @table = table # Register this new column with the ReportTable. @table.addColumn(self) @definition = definition # Register this new column with the TableColumnDefinition. definition.column = self if definition # Create the 2 cells of the header. @cell1 = ReportTableCell.new(nil, nil, title, true) @cell1.padding = 5 @cell2 = ReportTableCell.new(nil, nil, '', true) # Header text is always bold. @cell1.bold = @cell2.bold = true # This variable is set to true if the column requires a scrollbar later # on. @scrollbar = false end # Return the mininum required width for the column. def minWidth width = @cell1.width width = @cell2.width if width.nil? || @cell2.width > width width end # Convert the abstract representation into HTML elements. def to_html(row) if row == 1 @cell1.to_html else @cell2.to_html end end # Put the abstract representation into an Array. _csv_ is an Array of Arrays # of Strings. We have an Array with Strings for every cell. The outer Array # holds the Arrays representing the lines. def to_csv(csv, startColumn) # For CSV reports we can only include the first header line. @cell1.to_csv(csv, startColumn, 0) end end end TaskJuggler-3.8.1/lib/taskjuggler/reports/ReportTableLegend.rb000066400000000000000000000135621473026623400244400ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = ReportTableLegend.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # class TaskJuggler # The ReportTableLegend models an output format independent legend for the # ReportTable. It lists the graphical symbols used in the table together with # a short textual description. class ReportTableLegend attr_accessor :showGanttItems # Create a new ReportTableLegend object. def initialize @showGanttItems = false @ganttItems = [] @calendarItems = [] end # Add another Gantt item to the legend. Make sure we don't have any # duplicates. def addGanttItem(text, color) @ganttItems << [ text, color ] unless @ganttItems.include?([ text, color ]) end # Add another chart item to the legend. Make sure we don't have any # duplicates. def addCalendarItem(text, color) unless @calendarItems.include?([ text, color ]) @calendarItems << [ text, color ] end end # Convert the abstract description into HTML elements. def to_html return nil if !@showGanttItems && @ganttItems.empty? && @calendarItems.empty? frame = XMLElement.new('div', 'class' => 'tj_table_legend_frame') frame << (legend = XMLElement.new('table', 'class' => 'tj_table_legend', 'cellspacing' => '1')) legend << headlineToHTML('Gantt Chart Symbols:') # Generate the Gantt chart symbols if @showGanttItems legend << (row = XMLElement.new('tr', 'class' => 'tj_legend_row')) row << ganttItemToHTML(GanttContainer.new(15, 10, 35, 0), 'Container Task', 40) row << ganttItemToHTML(GanttTaskBar.new(nil, 15, 5, 35, 0), 'Normal Task', 40) row << ganttItemToHTML(GanttMilestone.new(15, 10, 0), 'Milestone', 20) row << XMLElement.new('td', 'class' => 'tj_legend_spacer') end legend << itemsToHTML(@ganttItems) legend << headlineToHTML('Calendar Symbols:') legend << itemsToHTML(@calendarItems) frame end private # In case we have both the calendar and the Gantt chart in the report # element, we have to add description lines before the symbols. The two # charts use the same colors for different meanings. This function generates # the HTML version of the headlines. def headlineToHTML(text) unless @calendarItems.empty? || @ganttItems.empty? div = XMLElement.new('tr', 'class' => 'tj_legend_headline') div << XMLNamedText.new(text, 'td', 'colspan' => '10') return div end nil end # Turn the Gantt symbold descriptions into HTML elements. def ganttItemToHTML(itemRef, name, width) cells = [] # Empty cell for margin first. cells << (item = XMLElement.new('td', 'class' => 'tj_legend_spacer')) # The symbol cell cells << (item = XMLElement.new('td', 'class' => 'tj_legend_item')) item << (symbol = XMLElement.new('div', 'class' => 'tj_legend_symbol', 'style' => 'top:3px')) symbol << itemRef.to_html # The label cell cells << (item = XMLElement.new('td', 'class' => 'tj_legend_item')) item << (label = XMLElement.new('div', 'class' => 'tj_legend_label')) label << XMLText.new(name) cells end # Turn a single color item into HTML elements. def itemToHTML(itemRef) cells = [] # Empty cell for margin first. cells << XMLElement.new('td', 'class' => 'tj_legend_spacer') # The symbol cell cells << (item = XMLElement.new('td', 'class' => 'tj_legend_item')) item << (symbol = XMLElement.new('div', 'class' => 'tj_legend_symbol')) symbol << (box = XMLElement.new('div', 'style' => 'position:relative; ' + 'top:2px;' + 'width:20px; height:15px')) box << (div = XMLElement.new('div', 'class' => 'loadstackframe', 'style' => 'position:absolute; ' + 'left:5px; width:16px; height:15px;')) div << XMLElement.new('div', 'class' => "#{itemRef[1]}", 'style' => 'position:absolute; ' + 'left:1px; top:1px; ' + 'width:14px; height:13px;') # The label cell cells << (item = XMLElement.new('td', 'class' => 'tj_legend_item')) item << (label = XMLElement.new('div', 'class' => 'tj_legend_label')) label << XMLText.new(itemRef[0]) cells end # Turn the color items into HTML elements. def itemsToHTML(items) rows = [] row = nil gridCells = ((items.length / 3) + (items.length % 3 != 0 ? 1 : 0)) * 3 gridCells.times do |i| # We show no more than 3 items in a row. if i % 3 == 0 rows << (row = XMLElement.new('tr', 'class' => 'tj_legend_row')) end # If we run out of items before the line is filled, we just insert # empty cells to fill the line. if i < items.length row << itemToHTML(items[i]) else row << XMLElement.new('td', 'class' => 'tj_legend_item', 'colspan' => '3') end if (i + 1) % 3 == 0 # Append an empty cell at the end of each row. row << XMLElement.new('td', 'class' => 'tj_legend_spacer') end end rows end end end TaskJuggler-3.8.1/lib/taskjuggler/reports/ReportTableLine.rb000066400000000000000000000062631473026623400241310ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = ReportTableLine.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/reports/ReportTableCell' class TaskJuggler class ReportTableLine attr_reader :table, :property, :scopeLine attr_accessor :height, :indentation, :fontSize, :bold, :no, :lineNo, :subLineNo # Create a ReportTableCell object and initialize the variables with default # values. _table_ is a reference to the ReportTable object this line belongs # to. _property_ is a reference to the Task or Resource that is displayed in # this line. _scopeLine_ is the line that sets the scope for this line. The # value is nil if this is a primary line. def initialize(table, property, scopeLine) @table = table @property = property @scopeLine = scopeLine # Register the new line with the table it belongs to. @table.addLine(self) # The cells of this line. Should be references to ReportTableCell objects. @cells = [] # Heigh of the line in screen pixels @height = 21 # Indentation for hierachiecal columns in screen pixels. @indentation = 0 # The factor used to enlarge or shrink the font size for this line. @fontSize = 12 # Specifies whether the whole line should be in bold type or not. @bold = false # Counter that counts primary and nested lines separately. It restarts # with 0 for each new nested line set. Scenario lines don't count. @no = nil # Counter that counts the primary lines. Scenario lines don't count. @lineNo = nil # Counter that counts all lines. @subLineNo = nil end # Return the last non-hidden cell of the line. Start to look for the cell at # the first cell after _count_ cells. def last(count = 0) (1 + count).upto(@cells.length) do |i| return @cells[-i] unless @cells[-i].hidden end nil end # Add the new cell to the line. _cell_ must reference a ReportTableCell # object. def addCell(cell) @cells << cell end # Return the scope property or nil def scopeProperty @scopeLine ? @scopeLine.property : nil end # Return this line as a set of XMLElement that represent the line in HTML. def to_html style = "" style += "height:#{@height}px; " if @table.equiLines style += "font-size:#{@fontSize}px; " if @fontSize tr = XMLElement.new('tr', 'class' => 'tabline', 'style' => style) @cells.each { |cell| tr << cell.to_html } tr end # Convert the intermediate format into an Array of values. One entry for # every column cell of this line. def to_csv(csv, startColumn, lineIdx) columnIdx = startColumn @cells.each do |cell| columnIdx += cell.to_csv(csv, columnIdx, lineIdx) end columnIdx - startColumn end end end TaskJuggler-3.8.1/lib/taskjuggler/reports/ResourceListRE.rb000066400000000000000000000052441473026623400237460ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = ResourceListRE.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/reports/TableReport' require 'taskjuggler/reports/ReportTable' require 'taskjuggler/TableColumnDefinition' require 'taskjuggler/LogicalExpression' class TaskJuggler # This specialization of TableReport implements a resource listing. It # generates a list of resources that can optionally have the assigned tasks # nested underneath each resource line. class ResourceListRE < TableReport # Create a new object and set some default values. def initialize(report) super @table = ReportTable.new @table.selfcontained = report.get('selfcontained') @table.auxDir = report.get('auxdir') end # Generate the table in the intermediate format. def generateIntermediateFormat super # Prepare the resource list. resourceList = PropertyList.new(@project.resources) resourceList.setSorting(@report.get('sortResources')) resourceList.query = @report.project.reportContexts.last.query resourceList = filterResourceList(resourceList, nil, @report.get('hideResource'), @report.get('rollupResource'), @report.get('openNodes')) resourceList.sort! # Prepare the task list. Don't filter it yet! It would break the # *_() LogicalFunctions. taskList = PropertyList.new(@project.tasks) taskList.setSorting(@report.get('sortTasks')) taskList.query = @report.project.reportContexts.last.query taskList.sort! assignedTaskList = [] resourceList.each do |resource| assignedTaskList += filterTaskList(taskList, resource, @report.get('hideTask'), @report.get('rollupTask'), @report.get('openNodes')) assignedTaskList.uniq! end # Generate the table header. @report.get('columns').each do |columnDescr| adjustColumnPeriod(columnDescr, assignedTaskList, @report.get('scenarios')) generateHeaderCell(columnDescr) end # Generate the list. generateResourceList(resourceList, taskList, nil) end end end TaskJuggler-3.8.1/lib/taskjuggler/reports/StatusSheetReport.rb000066400000000000000000000203241473026623400245400ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = StatusSheetReport.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/reports/ReportBase' class TaskJuggler class ManagerStatusRecord attr_reader :resource, :responsibilities def initialize(resource) # The Resource record of the manager @resource = resource # A list of Task objects with their JournalEntry records. Stored as # Array of ManagerResponsibilities objects. @responsibilities = [] end def sort!(taskList) @responsibilities.sort! do |r1, r2| taskList.itemIndex(r1.task) <=> taskList.itemIndex(r2.task) end @responsibilities.each { |r| r.sort!(taskList) } end end class ManagerResponsibilities attr_reader :task, :journalEntries def initialize(task, journalEntries) @task = task @journalEntries = journalEntries.dup end def sort!(taskList) @journalEntries.sort! do |e1, e2| taskList.itemIndex(e1.property) <=> taskList.itemIndex(e2.property) end end end # This specialization of ReportBase implements a template generator for # status sheets. The status sheet is structured using the TJP file syntax. class StatusSheetReport < ReportBase # Create a new object and set some default values. def initialize(report) super(report) # A list of ManagerStatusRecord objects, one for each manager. @managers = [] end # In the future we might want to generate other output than TJP synatx. So # we generate an abstract version of the status sheet first. def generateIntermediateFormat super # Prepare the resource list. resourceList = PropertyList.new(@project.resources) resourceList.setSorting(@report.get('sortResources')) resourceList = filterResourceList(resourceList, nil, @report.get('hideResource'), @report.get('rollupResource'), @report.get('openNodes')) # Prepare a template for the Query we will use to get all the data. scenarioIdx = a('scenarios')[0] queryAttrs = { 'project' => @project, 'scopeProperty' => nil, 'scenarioIdx' => scenarioIdx, 'loadUnit' => :days, 'numberFormat' => RealFormat.new([ '-', '', '', '.', 1]), 'timeFormat' => "%Y-%m-%d", 'currencyFormat' => a('currencyFormat'), 'start' => a('start'), 'end' => a('end'), 'hideJournalEntry' => a('hideJournalEntry'), 'journalMode' => a('journalMode'), 'journalAttributes' => a('journalAttributes'), 'sortJournalEntries' => a('sortJournalEntries'), 'costAccount' => a('costaccount'), 'revenueAccount' => a('revenueaccount') } resourceList.query = Query.new(queryAttrs) resourceList.sort! # Prepare the task list. taskList = PropertyList.new(@project.tasks) taskList.setSorting(@report.get('sortTasks')) taskList = filterTaskList(taskList, nil, @report.get('hideTask'), @report.get('rollupTask'), @report.get('openNodes')) taskList.sort! resourceList.each do |resource| # Status sheets only make sense for leaf resources. next unless resource.leaf? # Collect a list of tasks that the Resource is responsible for and # don't have a parent task that the Resource is responsible for. topLevelTasks = [] taskList.each do |task| if task['responsible', scenarioIdx].include?(resource) && (task.parent.nil? || !task.parent['responsible', scenarioIdx].include?(resource)) topLevelTasks << task end end next if topLevelTasks.empty? # Store the list of top-level responsibilities. @managers << (manager = ManagerStatusRecord.new(resource)) topLevelTasks.each do |task| # Get a list of all the current Journal entries for this task and # all it's sub tasks. entries = @project['journal']. currentEntriesR(a('end'), task, 0, a('start') + 1, resourceList.query) next if entries.empty? manager.responsibilities << ManagerResponsibilities.new(task, entries) end # Sort the responsibilities list according to the original taskList. manager.sort!(taskList) end end # Generate a time sheet in TJP syntax format. def to_tjp # This String will hold the result. @file = '' # Iterate over all the ManagerStatusRecord objects. @managers.each do |manager| resource = manager.resource @file << "# --------8<--------8<--------\n" # Generate the time sheet header @file << "statussheet #{resource.fullId} " + "#{a('start')} - #{a('end')} {\n\n" if manager.responsibilities.empty? # If there were no assignments, just write a comment. @file << " # This resource is not responsible for any task.\n\n" else manager.responsibilities.each do |responsibility| task = responsibility.task @file << " # Task: #{task.name}\n" responsibility.journalEntries.each do |entry| task = entry.property @file << " task #{task.fullId} {\n" alertLevel = @project['alertLevels'][entry.alertLevel].id @file << " # status #{alertLevel} \"#{entry.headline}\" {\n" @file << " # # Date: #{entry.date}\n" if (tsRecord = entry.timeSheetRecord) @file << " # # " @file << "Work: #{tsRecord.actualWorkPercent.to_i}% " if tsRecord.actualWorkPercent != tsRecord.planWorkPercent @file << "(#{tsRecord.planWorkPercent.to_i}%) " end if tsRecord.remaining @file << " Remaining: #{tsRecord.actualRemaining}d " if tsRecord.actualRemaining != tsRecord.planRemaining @file << "(#{tsRecord.planRemaining}d) " end else @file << " End: " + "#{tsRecord.actualEnd.to_s(a('timeFormat'))} " if tsRecord.actualEnd != tsRecord.planEnd @file << "(#{tsRecord.planEnd.to_s(a('timeFormat'))}) " end end @file << "\n" end @file << " # author #{entry.author.fullId}\n" if entry.author unless entry.flags.empty? @file << " # flags #{entry.flags.join(', ')}\n" end if entry.summary @file << " # summary -8<-\n" + indentBlock(4, entry.summary.richText.inputText) + " # ->8-\n" end if entry.details @file << " # details -8<-\n" + indentBlock(4, entry.details.richText.inputText) + " # ->8-\n" end @file << " # }\n }\n\n" end end end @file << "}\n# -------->8-------->8--------\n\n" end @file end private def indentBlock(indent, text) indentation = ' ' * indent + '# ' buffer = indentation out = '' text.each_utf8_char do |c| unless buffer.empty? out += buffer buffer = '' end out << c buffer = indentation if c == "\n" end # Make sure we always have a trailing line break out += "\n" unless out[-1] == "\n" out end end end TaskJuggler-3.8.1/lib/taskjuggler/reports/TableReport.rb000066400000000000000000001347011473026623400233200ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = TableReport.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2024 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/reports/ReportBase' require 'taskjuggler/reports/GanttChart' require 'taskjuggler/reports/ReportTableLegend' require 'taskjuggler/reports/ColumnTable' require 'taskjuggler/reports/TableReportColumn' require 'taskjuggler/Query' class TaskJuggler # This is base class for all types of tabular reports. All tabular reports # are converted to an abstract (output independent) intermediate form first, # before the are turned into the requested output format. class TableReport < ReportBase attr_reader :legend @@propertiesById = { # ID Header Indent Align Scen Spec. 'activetasks' => [ 'Active Tasks', true, :right, true ], 'annualleave' => [ 'Annual Leave', true, :right, true ], 'annualleavebalance'=> [ 'Annual Leave Balance', false, :right, true ], 'annualleavelist' => [ 'Annual Leave List', false, :left, true ], 'alert' => [ 'Alert', true, :left, false ], 'alertmessages' => [ 'Alert Messages', false, :left, false ], 'alertsummaries' => [ 'Alert Summaries', false, :left, false ], 'alerttrend' => [ 'Alert Trend', false, :left, false ], 'balance' => [ 'Balance', true, :right, true ], 'bsi' => [ 'BSI', false, :left, false ], 'children' => [ 'Children' , false, :left, false ], 'closedtasks' => [ 'Closed Tasks', true, :right, true ], 'competitorcount' => [ 'Competitor count', true, :right, true ], 'competitors' => [ 'Competitors', true, :left, true ], 'complete' => [ 'Completion', false, :right, true ], 'cost' => [ 'Cost', true, :right, true ], 'duration' => [ 'Duration', true, :right, true ], 'effort' => [ 'Effort', true, :right, true ], 'effortdone' => [ 'Effort Done', true, :right, true ], 'effortleft' => [ 'Effort Left', true, :right, true ], 'freetime' => [ 'Free Time', true, :right, true ], 'freework' => [ 'Free Work', true, :right, true ], 'followers' => [ 'Followers', false, :left, true ], 'fte' => [ 'FTE', true, :right, true ], 'headcount' => [ 'Headcount', true, :right, true ], 'id' => [ 'Id', false, :left, false ], 'inputs' => [ 'Inputs', false, :left, true ], 'journal' => [ 'Journal', false, :left, false ], 'journal_sub' => [ 'Journal', false, :left, false ], 'journalmessages' => [ 'Journal Messages', false, :left, false ], 'journalsummaries' => [ 'Journal Summaries', false, :left, false ], 'line' => [ 'Line No.', false, :right, false ], 'name' => [ 'Name', true, :left, false ], 'no' => [ 'No.', false, :right, false ], 'opentasks' => [ 'Open Tasks', true, :right, true ], 'precursors' => [ 'Precursors', false, :left, true ], 'rate' => [ 'Rate', true, :right, true ], 'resources' => [ 'Resources', false, :left, true ], 'responsible' => [ 'Responsible', false, :left, true ], 'revenue' => [ 'Revenue', true, :right, true ], 'scenario' => [ 'Scenario', false, :left, true ], 'scheduling' => [ 'Scheduling Mode', true, :left, true ], 'sickleave' => [ 'Sick Leave', true, :right, true ], 'specialleave' => [ 'Special Leave', true, :right, true ], 'status' => [ 'Status', false, :left, true ], 'targets' => [ 'Targets', false, :left, true ], 'unpaidleave' => [ 'Unpaid Leave', true, :right, true ] } @@propertiesByType = { # Type Indent Align DateAttribute => [ false, :left ], IntegerAttribute => [ false, :right ], FloatAttribute => [ false, :right ], ResourceListAttribute => [ false, :left ], RichTextAttribute => [ false, :left ], StringAttribute => [ false, :left ] } # Generate a new TableReport object. def initialize(report) super @report.content = self # Reference to the intermediate representation. @table = nil # The table is generated row after row. We need to hold some computed # values that are specific to certain columns. For that we use a Hash of # ReportTableColumn objects. @columns = { } @legend = ReportTableLegend.new end def generateIntermediateFormat super end # Turn the TableReport into an equivalent HTML element tree. def to_html html = [] html << XMLComment.new("Dynamic Report ID: " + "#{@report.project.reportContexts.last. dynamicReportId}") html << rt_to_html('header') html << (tableFrame = generateHtmlTableFrame) # Now generate the actual table with the data. tableFrame << generateHtmlTableRow do td = XMLElement.new('td') td << @table.to_html td end # Embedd the caption as RichText into the table footer. if a('caption') tableFrame << generateHtmlTableRow do td = XMLElement.new('td') td << (div = XMLElement.new('div', 'class' => 'tj_table_caption')) a('caption').sectionNumbers = false div << a('caption').to_html td end end # The legend. tableFrame << generateHtmlTableRow do td = XMLElement.new('td') td << @legend.to_html td end html << rt_to_html('footer') html end # Convert the table into an Array of Arrays. It has one Array for each # line. The nested Arrays have one String for each column. def to_csv @table.to_csv end # Returns the default column title for the columns _id_. def TableReport::defaultColumnTitle(id) # Return an empty string for some special columns that don't have a fixed # title. specials = %w( chart hourly daily weekly monthly quarterly yearly) return '' if specials.include?(id) # Return the title for build-in hardwired columns. @@propertiesById.include?(id) ? @@propertiesById[id][0] : nil end # Return if the column values should be indented based on the _colId_ or the # _propertyType_. def TableReport::indent(colId, propertyType) if @@propertiesById.has_key?(colId) return @@propertiesById[colId][1] elsif @@propertiesByType.has_key?(propertyType) return @@propertiesByType[propertyType][0] else false end end # Return the alignment of the column based on the _colId_ or the # _attributeType_. def TableReport::alignment(colId, attributeType) if @@propertiesById.has_key?(colId) return @@propertiesById[colId][2] elsif @@propertiesByType.has_key?(attributeType) return @@propertiesByType[attributeType][1] else :center end end # This function returns true if the values for the _colId_ column need to be # calculated. def TableReport::calculated?(colId) return @@propertiesById.has_key?(colId) end # This functions returns true if the values for the _col_id_ column are # scenario specific. def TableReport::scenarioSpecific?(colId) if @@propertiesById.has_key?(colId) return @@propertiesById[colId][3] end return false end #def TableReport::supportedColumns # @@propertiesById.keys #end protected # In case the user has not specified the report period, we try to fit all # the _tasks_ in and add an extra 5% time at both ends for some specific # type of columns. _scenarios_ is a list of scenario indexes. _columnDef_ # is a reference to the TableColumnDefinition object describing the # current column. def adjustColumnPeriod(columnDef, tasks = [], scenarios = []) # If we have user specified dates for the report period or the column # period, we don't adjust the period. This flag is used to mark if we # have user-provided values. doNotAdjustStart = false doNotAdjustEnd = false # Determine the start date for the column. if columnDef.start # We have a user-specified, column specific start date. rStart = columnDef.start doNotAdjustStart = true else # Use the report start date. rStart = a('start') doNotAdjustStart = true if rStart != @project['start'] end if columnDef.end rEnd = columnDef.end doNotAdjustEnd = true else rEnd = a('end') doNotAdjustEnd = true if rEnd != @project['end'] end origStart = rStart origEnd = rEnd # Save the unadjusted dates to the columns Hash. @columns[columnDef] = TableReportColumn.new(rStart, rEnd) # If the task list is empty or the user has provided a custom start or # end date, we don't touch the report period. return if tasks.empty? || scenarios.empty? || (doNotAdjustStart && doNotAdjustEnd) # Find the start date of the earliest tasks included in the report and # the end date of the last included tasks. rStart = rEnd = nil scenarios.each do |scenarioIdx| tasks.each do |task| date = task['start', scenarioIdx] || @project['start'] rStart = date if rStart.nil? || date < rStart date = task['end', scenarioIdx] || @project['end'] rEnd = date if rEnd.nil? || date > rEnd end end # We want to add at least 5% on both ends. margin = 0 minWidth = rEnd - rStart + 1 case columnDef.id when 'chart' # In case we have a 'chart' column, we enforce certain minimum width # The following table contains an entry for each scale. The entry # consists of the triple 'seconds per unit', 'minimum width units' # and 'margin units'. The minimum with does not include the margins # since they are always added. mwMap = { 'hour' => [ 60 * 60, 18, 2 ], 'day' => [ 60 * 60 * 24, 18, 2 ], 'week' => [ 60 * 60 * 24 * 7, 6, 1 ], 'month' => [ 60 * 60 * 24 * 31, 10, 1 ], 'quarter' => [ 60 * 60 * 24 * 90, 6, 1 ], 'year' => [ 60 * 60 * 24 * 365, 4, 1 ] } entry = mwMap[columnDef.scale] raise "Unknown scale #{columnDef.scale}" unless entry margin = entry[0] * entry[2] # If the with determined by start and end dates of the task is below # the minimum width, we increase the width to the value provided by # the table. minWidth = entry[0] * entry[1] if minWidth < entry[0] * entry[1] when 'hourly', 'daily', 'weekly', 'monthly', 'quarterly', 'yearly' # For the calendar columns we use a similar approach as we use for # the 'chart' column. mwMap = { 'hourly' => [ 60 * 60, 18, 2 ], 'daily' => [ 60 * 60 * 24, 18, 2 ], 'weekly' => [ 60 * 60 * 24 * 7, 6, 1 ], 'monthly' => [ 60 * 60 * 24 * 31, 10, 1 ], 'quarterly' => [ 60 * 60 * 24 * 90, 6, 1 ], 'yearly' => [ 60 * 60 * 24 * 365, 4, 1 ] } entry = mwMap[columnDef.id] raise "Unknown scale #{columnDef.id}" unless entry margin = entry[0] * entry[2] minWidth = entry[0] * entry[1] if minWidth < entry[0] * entry[1] else doNotAdjustStart = doNotAdjustEnd = true end unless doNotAdjustStart && doNotAdjustEnd if minWidth > (rEnd - rStart + 1) margin = (minWidth - (rEnd - rStart + 1)) / 2 end rStart -= margin rEnd += margin # This could cause rStart to be larger than rEnd. rStart = origStart if doNotAdjustStart rEnd = origEnd if doNotAdjustEnd # Ensure that we have a valid interval. If not, go back to the # original interval dates. if rStart >= rEnd rStart = origStart rEnd = origEnd end # Save the adjusted dates to the columns Hash. @columns[columnDef] = TableReportColumn.new(rStart, rEnd) end end # Generates cells for the table header. _columnDef_ is the # TableColumnDefinition object that describes the column. Based on the id of # the column different actions need to be taken to generate the header text. def generateHeaderCell(columnDef) rStart = @columns[columnDef].start rEnd = @columns[columnDef].end case columnDef.id when 'chart' # For the 'chart' column we generate a GanttChart object. The sizes are # set so that the lines of the Gantt chart line up with the lines of the # table gantt = GanttChart.new(a('now'), a('weekStartsMonday'), columnDef, self, a('markdate')) gantt.generateByScale(rStart, rEnd, columnDef.scale) # The header consists of 2 lines separated by a 1 pixel boundary. gantt.header.height = @table.headerLineHeight * 2 + 1 # The maximum width of the chart. In case it needs more space, a # scrollbar is shown or the chart gets truncated depending on the output # format. gantt.viewWidth = columnDef.width ? columnDef.width : 450 column = ReportTableColumn.new(@table, columnDef, '') column.cell1.special = gantt column.cell2.hidden = true column.scrollbar = gantt.hasScrollbar? @table.equiLines = true when 'hourly' genCalChartHeader(columnDef, rStart.midnight, rEnd, :sameTimeNextHour, '%A %Y-%m-%d', '%H') when 'daily' genCalChartHeader(columnDef, rStart.midnight, rEnd, :sameTimeNextDay, '%b %Y', '%d') when 'weekly' genCalChartHeader(columnDef, rStart.beginOfWeek(a('weekStartsMonday')), rEnd, :sameTimeNextWeek, '%b %Y', '%d') when 'monthly' genCalChartHeader(columnDef, rStart.beginOfMonth, rEnd, :sameTimeNextMonth, '%Y', '%b') when 'quarterly' genCalChartHeader(columnDef, rStart.beginOfQuarter, rEnd, :sameTimeNextQuarter, '%Y', 'Q%Q') when 'yearly' genCalChartHeader(columnDef, rStart.beginOfYear, rEnd, :sameTimeNextYear, nil, '%Y') else # This is the most common case. It does not need any special treatment. # We just set the pre-defined or user-defined column title in the first # row of the header. The 2nd row is not visible. column = ReportTableColumn.new(@table, columnDef, columnDef.title) column.cell1.rows = 2 column.cell2.hidden = true column.cell1.width = columnDef.width if columnDef.width end end # Generate a ReportTableLine for each of the accounts in _accountList_. If # _scopeLine_ is defined, the generated account lines will be within the # scope this resource line. def generateAccountList(accountList, lineOffset, mode) # Get the current Query from the report context and create a copy. We # are going to modify it. accountList.query = query = @project.reportContexts.last.query.dup accountList.sort! # The primary line counter. Is not used for enclosed lines. no = lineOffset # The scope line counter. It's reset for each new scope. lineNo = lineOffset # Init the variable to get a larger scope line = nil accountList.each do |account| query.property = account no += 1 Log.activity if lineNo % 10 == 0 lineNo += 1 a('scenarios').each do |scenarioIdx| query.scenarioIdx = scenarioIdx # Generate line for each account. line = ReportTableLine.new(@table, account, nil) line.no = no line.lineNo = lineNo line.subLineNo = @table.lines setIndent(line, a('accountroot'), accountList.treeMode?) # Generate a cell for each column in this line. a('columns').each do |columnDef| next unless generateTableCell(line, columnDef, query) end end end lineNo end # Generate a ReportTableLine for each of the tasks in _taskList_. In case # _resourceList_ is not nil, it also generates the nested resource lines for # each resource that is assigned to the particular task. If _scopeLine_ # is defined, the generated task lines will be within the scope this # resource line. def generateTaskList(taskList, resourceList, scopeLine) # Get the current Query from the report context and create a copy. We # are going to modify it. taskList.query = query = @project.reportContexts.last.query.dup query.scopeProperty = scopeLine ? scopeLine.property : nil taskList.sort! # The primary line counter. Is not used for enclosed lines. no = 0 # The scope line counter. It's reset for each new scope. lineNo = scopeLine ? scopeLine.lineNo : 0 # Init the variable to get a larger scope line = nil taskList.each do |task| # Get the current Query from the report context and create a copy. We # are going to modify it. query.property = task query.scopeProperty = scopeLine ? scopeLine.property : nil no += 1 Log.activity if lineNo % 10 == 0 lineNo += 1 a('scenarios').each do |scenarioIdx| query.scenarioIdx = scenarioIdx # Generate line for each task. line = ReportTableLine.new(@table, task, scopeLine) line.no = no unless scopeLine line.lineNo = lineNo line.subLineNo = @table.lines setIndent(line, a('taskroot'), taskList.treeMode?) # Generate a cell for each column in this line. a('columns').each do |columnDef| next unless generateTableCell(line, columnDef, query) end end if resourceList # If we have a resourceList we generate nested lines for each of the # resources that are assigned to this task and pass the user-defined # filter. resourceList.setSorting(a('sortResources')) assignedResourceList = filterResourceList(resourceList, task, a('hideResource'), a('rollupResource'), a('openNodes')) assignedResourceList.sort! lineNo = generateResourceList(assignedResourceList, nil, line) end end lineNo end # Generate a ReportTableLine for each of the resources in _resourceList_. In # case _taskList_ is not nil, it also generates the nested task lines for # each task that the resource is assigned to. If _scopeLine_ is defined, the # generated resource lines will be within the scope this task line. def generateResourceList(resourceList, taskList, scopeLine) # Get the current Query from the report context and create a copy. We # are going to modify it. resourceList.query = query = @project.reportContexts.last.query.dup query.scopeProperty = scopeLine ? scopeLine.property : nil resourceList.sort! # The primary line counter. Is not used for enclosed lines. no = 0 # The scope line counter. It's reset for each new scope. lineNo = scopeLine ? scopeLine.lineNo : 0 # Init the variable to get a larger scope line = nil resourceList.each do |resource| # Get the current Query from the report context and create a copy. We # are going to modify it. query.property = resource query.scopeProperty = scopeLine ? scopeLine.property : nil no += 1 Log.activity if lineNo % 10 == 0 lineNo += 1 a('scenarios').each do |scenarioIdx| query.scenarioIdx = scenarioIdx # Generate line for each resource. line = ReportTableLine.new(@table, resource, scopeLine) line.no = no unless scopeLine line.lineNo = lineNo line.subLineNo = @table.lines setIndent(line, a('resourceroot'), resourceList.treeMode?) # Generate a cell for each column in this line. a('columns').each do |column| next unless generateTableCell(line, column, query) end end if taskList # If we have a taskList we generate nested lines for each of the # tasks that the resource is assigned to and pass the user-defined # filter. taskList.setSorting(a('sortTasks')) assignedTaskList = filterTaskList(taskList, resource, a('hideTask'), a('rollupTask'), a('openNodes')) assignedTaskList.sort! lineNo = generateTaskList(assignedTaskList, nil, line) end end lineNo end private # Generate the header data for calendar tables. They consists of columns for # each hour, day, week, etc. _columnDef_ is the definition of the columns. # _t_ is the start time for the calendar. _sameTimeNextFunc_ is a function # that is called to advance _t_ to the next table column interval. # _timeformat1_ and _timeformat2_ are strftime format Strings that are used # to generate the upper and lower title of the particular column. def genCalChartHeader(columnDef, t, rEnd, sameTimeNextFunc, timeformat1, timeformat2) tableColumn = ReportTableColumn.new(@table, columnDef, '') # Overwrite the built-in time formats if the user specified a different # one. timeformat1 = columnDef.timeformat1 if columnDef.timeformat1 timeformat2 = columnDef.timeformat2 if columnDef.timeformat2 # Calendar chars only work when all lines have same height. @table.equiLines = true # Embedded tables have unpredictable width. So we always need to make room # for a potential scrollbar. tableColumn.scrollbar = true # Create the table that is embedded in this column. tableColumn.cell1.special = table = ColumnTable.new table.equiLines = true table.selfcontained = a('selfcontained') tableColumn.cell2.hidden = true table.viewWidth = columnDef.width ? columnDef.width : 450 # Iterate over the report interval until we hit the end date. The # iteration is done with 2 nested loops. The outer loops generates the # intervals for the upper (larger) scale. The inner loop generates the # lower (smaller) scale. while t < rEnd cellsInInterval = 0 # Label for upper scale. The yearly calendar only has a lower scale. currentInterval = t.to_s(timeformat1) if timeformat1 firstColumn = nil # The innter loops terminates when the label for the upper scale has # changed to the next scale cell. while t < rEnd && (timeformat1.nil? || t.to_s(timeformat1) == currentInterval) # call TjTime::sameTimeNext... function to get the end of the column. nextT = t.send(sameTimeNextFunc) iv = TimeInterval.new(t, nextT) # Create the new column object. column = ReportTableColumn.new(table, nil, '') # Store the date of the column in the original form. column.cell1.data = t.to_s(a('timeFormat')) # The upper scale cells will be merged into one large cell that spans # all lower scale cells that belong to this upper cell. if firstColumn.nil? firstColumn = column column.cell1.text = currentInterval else column.cell1.hidden = true end column.cell2.text = t.to_s(timeformat2) # We assume an average of 7 pixel per character width = 8 + 7 * column.cell2.text.length # Ensure a minimum with of 28 to have good looking tables even with # small column headers (like day of months numbers). column.cell2.width = width <= 28 ? 28 : width # Off-duty cells will have a different color than working time cells. unless @project.hasWorkingTime(iv) column.cell2.category = 'tabhead_offduty' end cellsInInterval += 1 t = nextT end # The the first upper scale cell how many trailing hidden cells are # following. firstColumn.cell1.columns = cellsInInterval end end # Generate a cell of the table. _line_ is the ReportTableLine that this cell # should belong to. _property_ is the PropertyTreeNode that is reported in # this _line_. _columnDef_ is the TableColumnDefinition of the column this # cell should belong to. _scenarioIdx_ is the index of the scenario that is # reported in this _line_. # # There are 4 kinds of cells. The most simple one is the standard cell. It # literally reports the value of a property attribute. Calculated cells are # more flexible. They contain computed values. The values are computed at # cell generation time. The calendar columns consist of multiple sub # columns. In such a case many cells are generated with a single call of # this method. The last kind of cell is actually not a cell. It just # generates the chart objects that belong to the property in this line. def generateTableCell(line, columnDef, query) # Adjust the Query to use column specific settings. We create a copy of # the Query to avoid spoiling the original query with column specific # settings. query = query.dup query.attributeId = columnDef.id query.start = @columns[columnDef].start query.end = @columns[columnDef].end query.listType = columnDef.listType query.listItem = columnDef.listItem case columnDef.id when 'chart' # Generate a hidden cell. The real meat is in the actual chart object, # not in this cell. cell = ReportTableCell.new(line, query, '') cell.hidden = true cell.text = nil # The GanttChart can be reached via the special variable of the column # header. chart = columnDef.column.cell1.special GanttLine.new(chart, query, (line.subLineNo - 1) * (line.height + 1), line.height, line.subLineNo, a('selfcontained') ? nil : columnDef.tooltip) return true # The calendar cells can be all generated by the same function. But we # need to use different parameters. when 'hourly' start = query.start.midnight sameTimeNextFunc = :sameTimeNextHour when 'daily' start = query.start.midnight sameTimeNextFunc = :sameTimeNextDay when 'weekly' start = query.start.beginOfWeek(a('weekStartsMonday')) sameTimeNextFunc = :sameTimeNextWeek when 'monthly' start = query.start.beginOfMonth sameTimeNextFunc = :sameTimeNextMonth when 'quarterly' start = query.start.beginOfQuarter sameTimeNextFunc = :sameTimeNextQuarter when 'yearly' start = query.start.beginOfYear sameTimeNextFunc = :sameTimeNextYear else if TableReport.calculated?(columnDef.id) return genCalculatedCell(query, line, columnDef) else return genStandardCell(query, line, columnDef) end end # The calendar cells don't live in this ReportTable but in an embedded # ReportTable that can be reached via the column header special variable. # For embedded column tables we need to create a new line. tcLine = ReportTableLine.new(columnDef.column.cell1.special, line.property, line.scopeLine) PlaceHolderCell.new(line, tcLine) tcLine.subLineNo = line.subLineNo # Depending on the property type we use different generator functions. if query.property.is_a?(Task) genCalChartTaskCell(query, tcLine, columnDef, start, sameTimeNextFunc) elsif query.property.is_a?(Resource) genCalChartResourceCell(query, tcLine, columnDef, start, sameTimeNextFunc) elsif query.property.is_a?(Account) genCalChartAccountCell(query, tcLine, columnDef, start, sameTimeNextFunc) else raise "Unknown property type #{query.property.class}" end true end # Generate a ReportTableCell filled the value of an attribute of the # property that line is for. It returns true if the cell exists, false for a # hidden cell. def genStandardCell(query, line, columnDef) # Find out, what type of PropertyTreeNode we are dealing with. property = line.property if property.is_a?(Task) propertyList = @project.tasks elsif property.is_a?(Resource) propertyList = @project.resources elsif property.is_a?(Account) propertyList = @project.accounts else raise "Unknown property type #{property.class}" end # Create a new cell cell = newCell(query, line) unless setScenarioSettings(cell, query.scenarioIdx, propertyList.scenarioSpecific?(columnDef.id)) return false end setStandardCellAttributes(query, cell, columnDef, propertyList.attributeType(columnDef.id), line) # If the user has requested a custom cell text, this will be used # instead of the queried one. if (cdText = columnDef.cellText.getPattern(query)) cell.text = cdText elsif query.process cell.text = (rti = query.to_rti) ? rti : query.to_s end setCustomCellAttributes(cell, columnDef, query) checkCellText(cell) true end # Generate a ReportTableCell filled with a calculted value from the property # or other sources of information. It returns true if the cell exists, false # for a hidden cell. _query_ is the Query to get the cell value. _line_ # is the ReportTableLine of the cell. _columnDef_ is the # TableColumnDefinition of the column. def genCalculatedCell(query, line, columnDef) # Create a new cell cell = newCell(query, line) cell.force_string = true if columnDef.id == 'bsi' unless setScenarioSettings(cell, query.scenarioIdx, TableReport.scenarioSpecific?(columnDef.id)) return false end setStandardCellAttributes(query, cell, columnDef, nil, line) if query.process cell.text = (rti = query.to_rti) ? rti : query.to_s end # Some columns need some extra care. case columnDef.id when 'alert' id = @project['alertLevels'][query.to_sort].id cell.icon = "flag-#{id}" cell.fontColor = @project['alertLevels'][query.to_sort].color when 'alerttrend' icons = %w( up flat down ) cell.icon = "trend-#{icons[query.to_sort]}" when 'line' cell.text = line.lineNo.to_s when 'name' property = query.property cell.icon = if property.is_a?(Task) if property.container? 'taskgroup' else 'task' end elsif property.is_a?(Resource) if property.container? 'resourcegroup' else 'resource' end else nil end cell.iconTooltip = RichText.new("'''ID:''' #{property.fullId}"). generateIntermediateFormat when 'no' cell.text = line.no.to_s when 'bsi' cell.indent = 2 if line.scopeLine when 'scenario' cell.text = @project.scenario(query.scenarioIdx).name end # Replace the cell text if the user has requested a custom cell text. cdText = columnDef.cellText.getPattern(query) cell.text = cdText if cdText setCustomCellAttributes(cell, columnDef, query) checkCellText(cell) true end # Generate the cells for the account lines of a calendar column. These # lines do not directly belong to the @table object but to an embedded # ColumnTable object. Therefor a single @table column usually has many # cells on each single line. _scenarioIdx_ is the index of the scenario # that is reported in this line. _line_ is the @table line. _t_ is the # start date for the calendar. _sameTimeNextFunc_ is the function that # will move the date to the next cell. def genCalChartAccountCell(query, line, columnDef, t, sameTimeNextFunc) # We modify the start and end dates to match the cell boundaries. So # we need to make sure we don't modify the original Query but our own # copies. query = query.dup firstCell = nil endDate = query.end while t < endDate # call TjTime::sameTimeNext... function nextT = t.send(sameTimeNextFunc) query.attributeId = 'balance' query.start = t query.end = nextT query.process # Create a new cell cell = newCell(query, line) cell.text = query.to_s cdText = columnDef.cellText.getPattern(query) cell.text = cdText if cdText cell.showTooltipHint = false setAccountCellBgColor(query, line, cell) setCustomCellAttributes(cell, columnDef, query) tryCellMerging(cell, line, firstCell) t = nextT firstCell = cell unless firstCell end end # Generate the cells for the task lines of a calendar column. These lines do # not directly belong to the @table object but to an embedded ColumnTable # object. Therefor a single @table column usually has many cells on each # single line. _scenarioIdx_ is the index of the scenario that is reported # in this line. _line_ is the @table line. _t_ is the start date for the # calendar. _sameTimeNextFunc_ is the function that will move the date to # the next cell. def genCalChartTaskCell(query, line, columnDef, t, sameTimeNextFunc) task = line.property # Get the interval of the task. In case a date is invalid due to a # scheduling problem, we use the full project interval. taskStart = task['start', query.scenarioIdx] taskEnd = task['end', query.scenarioIdx] taskIv = TimeInterval.new(taskStart.nil? ? @project['start'] : taskStart, taskEnd.nil? ? @project['end'] : taskEnd) # We modify the start and end dates to match the cell boundaries. So # we need to make sure we don't modify the original Query but our own # copies. query = query.dup firstCell = nil endDate = query.end while t < endDate # call TjTime::sameTimeNext... function nextT = t.send(sameTimeNextFunc) cellIv = TimeInterval.new(t, nextT) case columnDef.content when 'empty' # Create a new cell cell = newCell(query, line) # We only generate cells will different background colors. when 'load' query.attributeId = 'effort' query.start = t query.end = nextT query.process # Create a new cell cell = newCell(query, line) # To increase readability show empty cells instead of 0.0 values. cell.text = query.to_s if query.to_num != 0.0 else raise "Unknown column content #{column.content}" end cdText = columnDef.cellText.getPattern(query) cell.text = cdText if cdText cell.showTooltipHint = false # Determine cell category (mostly the background color) if cellIv.overlaps?(taskIv) # The cell is either a container or leaf task cell.category = task.container? ? 'calconttask' : 'caltask' elsif !@project.isWorkingTime(cellIv) # The cell is a vacation cell. cell.category = 'offduty' else # The cell is just filled with the background color. cell.category = 'taskcell' end cell.category += line.subLineNo % 2 == 1 ? '1' : '2' setCustomCellAttributes(cell, columnDef, query) tryCellMerging(cell, line, firstCell) t = nextT firstCell = cell unless firstCell end legend.addCalendarItem('Container Task', 'calconttask1') legend.addCalendarItem('Task', 'caltask1') legend.addCalendarItem('Off duty time', 'offduty') end # Generate the cells for the resource lines of a calendar column. These # lines do not directly belong to the @table object but to an embedded # ColumnTable object. Therefor a single @table column usually has many cells # on each single line. _scenarioIdx_ is the index of the scenario that is # reported in this line. _line_ is the @table line. _t_ is the start date # for the calendar. _sameTimeNextFunc_ is the function that will move the # date to the next cell. def genCalChartResourceCell(query, line, columnDef, t, sameTimeNextFunc) # Find out if we have an enclosing task scope. if line.scopeLine && line.scopeLine.property.is_a?(Task) task = line.scopeLine.property # Get the interval of the task. In case a date is invalid due to a # scheduling problem, we use the full project interval. taskStart = task['start', query.scenarioIdx] taskEnd = task['end', query.scenarioIdx] taskIv = TimeInterval.new(taskStart.nil? ? @project['start'] : taskStart, taskEnd.nil? ? @project['end'] : taskEnd) else task = nil end # We modify the start and end dates to match the cell boundaries. So # we need to make sure we don't modify the original Query but our own # copies. query = query.dup firstCell = nil endDate = query.end while t < endDate # Create a new cell cell = newCell(query, line) # call TjTime::sameTimeNext... function nextT = t.send(sameTimeNextFunc) cellIv = TimeInterval.new(t, nextT) # Get work load for all tasks. query.scopeProperty = nil query.attributeId = 'effort' query.startIdx = @project.dateToIdx(t) query.endIdx = @project.dateToIdx(nextT) query.process workLoad = query.to_num scaledWorkLoad = query.to_s if task # Get work load for the particular task. query.scopeProperty = task query.process workLoadTask = query.to_num scaledWorkLoad = query.to_s else workLoadTask = 0.0 end # Get unassigned work load. query.attributeId = 'freework' query.process freeLoad = query.to_num case columnDef.content when 'empty' # We only generate cells will different background colors. when 'load' # Report the workload of the resource in this time interval. # To increase readability, we don't show 0.0 values. wLoad = task ? workLoadTask : workLoad if wLoad > 0.0 cell.text = scaledWorkLoad end else raise "Unknown column content #{column.content}" end cdText = columnDef.cellText.getPattern(query) cell.text = cdText if cdText # Set the tooltip for the cell. We might delete it again. cell.tooltip = columnDef.tooltip.getPattern(query) || nil cell.showTooltipHint = false # Determine cell category (mostly the background color) cell.category = if task if cellIv.overlaps?(taskIv) if workLoadTask > 0.0 && freeLoad == 0.0 'busy' elsif workLoad == 0.0 && freeLoad == 0.0 cell.tooltip = nil 'offduty' else 'loaded' end else if freeLoad > 0.0 'free' elsif workLoad == 0.0 && freeLoad == 0.0 cell.tooltip = nil 'offduty' else cell.tooltip = nil 'resourcecell' end end else if workLoad > 0.0 && freeLoad == 0.0 'busy' elsif workLoad > 0.0 && freeLoad > 0.0 'loaded' elsif workLoad == 0.0 && freeLoad == 0.0 cell.tooltip = nil 'offduty' else 'free' end end cell.category += line.subLineNo % 2 == 1 ? '1' : '2' setCustomCellAttributes(cell, columnDef, query) tryCellMerging(cell, line, firstCell) t = nextT firstCell = cell unless firstCell end legend.addCalendarItem('Resource is fully loaded', 'busy1') legend.addCalendarItem('Resource is partially loaded', 'loaded1') legend.addCalendarItem('Resource is available', 'free') legend.addCalendarItem('Off duty time', 'offduty') end # This method takes care of often used cell attributes like indentation, # alignment and background color. def setStandardCellAttributes(query, cell, columnDef, attributeType, line) # Determine whether it should be indented if TableReport.indent(columnDef.id, attributeType) cell.indent = line.indentation end # Determine the cell alignment cell.alignment = TableReport.alignment(columnDef.id, attributeType) # Set background color if line.property.is_a?(Task) cell.category = 'taskcell' cell.category += line.subLineNo % 2 == 1 ? '1' : '2' elsif line.property.is_a?(Resource) cell.category = 'resourcecell' cell.category += line.subLineNo % 2 == 1 ? '1' : '2' elsif line.property.is_a?(Account) setAccountCellBgColor(query, line, cell) end # Set column width cell.width = columnDef.width if columnDef.width end def setCustomCellAttributes(cell, columnDef, query) # Replace the cell background color if the user has requested a custom # color. cellColor = columnDef.cellColor.getPattern(query) cell.cellColor = cellColor if cellColor # Replace the font color setting if the user has requested a custom # color. fontColor = columnDef.fontColor.getPattern(query) cell.fontColor = fontColor if fontColor # Replace the default cell alignment if the user has requested a custom # alignment. hAlign = columnDef.hAlign.getPattern(query) cell.alignment = hAlign if hAlign # Register the custom tooltip if the user has requested one. cdTooltip = columnDef.tooltip.getPattern(query) cell.tooltip = cdTooltip if cdTooltip end def setScenarioSettings(cell, scenarioIdx, scenarioSpecific) # Check if we are dealing with multiple scenarios. if a('scenarios').length > 1 # Check if the attribute is not scenario specific unless scenarioSpecific if scenarioIdx == a('scenarios').first # Use a somewhat bigger font. cell.fontSize = 15 else # And hide the cells for all but the first scenario. cell.hidden = true return false end cell.rows = a('scenarios').length end end true end # Create a new ReportTableCell object and initialize some common values. def newCell(query, line) property = line.property cell = ReportTableCell.new(line, query) # Cells for containers should be using bold font face. cell.bold = true if property.container? && line.bold cell end # Determine the indentation for this line. def setIndent(line, propertyRoot, treeMode) property = line.property scopeLine = line.scopeLine level = property.level - (propertyRoot ? propertyRoot.level : 0) # We indent at least as much as the scopeline + 1, if we have a scope. line.indentation = scopeLine.indentation + 1 if scopeLine # In tree mode we indent according to the level. if treeMode line.indentation += level line.bold = true end end def setAccountCellBgColor(query, line, cell) if query.costAccount && (query.property.isChildOf?(query.costAccount) || query.costAccount == query.property) prefix = 'cost' elsif query.revenueAccount && (query.property.isChildOf?(query.revenueAccount) || query.revenueAccount == query.property) prefix = 'revenue' else prefix = '' end cell.category = prefix + 'accountcell' + (line.subLineNo % 2 == 1 ? '1' : '2') end # Make sure we have a valid cell text. If not, this is the result of an # error. This could happen after scheduling errors. def checkCellText(cell) unless cell.text cell.text = '' cell.fontColor = '#FF0000' end end # Try to merge equal cells without text to multi-column cells. def tryCellMerging(cell, line, firstCell) if cell.text == '' && firstCell && (c = line.last(1)) && c == cell cell.hidden = true c.columns += 1 end end end end TaskJuggler-3.8.1/lib/taskjuggler/reports/TableReportColumn.rb000066400000000000000000000013121473026623400244650ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = TableReportColumn.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # class TaskJuggler # This class holds some computed data that is used to render the TableReport # Columns. class TableReportColumn attr_accessor :start, :end def initialize(startDate, endDate) @start = startDate @end = endDate end end end TaskJuggler-3.8.1/lib/taskjuggler/reports/TagFile.rb000066400000000000000000000072101473026623400224020ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = TagFile.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/reports/ReportBase.rb' require 'taskjuggler/Tj3Config' class TaskJuggler # This class specializes ReportBase to generate tag files used by editors # such as vim. class TagFile < ReportBase # The TagFileEntry class is used to store the intermediate representation # of the TagFile. class TagFileEntry attr_reader :tag, :file, :line, :kind # Create a new TagFileEntry object. _tag_ is the property ID. _file_ is # the source file name, _line_ the line number in this file. _kind_ # specifies the property type. The following types should be used: # r : Resource # t : Task # p : Report def initialize(tag, file, line, kind) @tag = tag @file = file @line = line @kind = kind end # Used to sort the tag file entries by tag. def <=>(e) @tag <=> e.tag end # Convert the entry into a ctags compatible line. def to_ctags "#{@tag}\t#{@file}\t#{@line};\"\t#{@kind}\n" end end def initialize(report) super end def generateIntermediateFormat super @tags = [] # Add the resources. @resourceList = PropertyList.new(@project.resources) @resourceList.setSorting(a('sortResources')) @resourceList = filterResourceList(@resourceList, nil, a('hideResource'), a('rollupResource'), a('openNodes')) @resourceList.each do |resource| next unless resource.sourceFileInfo @tags << TagFileEntry.new(resource.fullId, resource.sourceFileInfo.fileName, resource.sourceFileInfo.lineNo, 'r') end # Add the tasks. @taskList = PropertyList.new(@project.tasks) @taskList.setSorting(a('sortTasks')) @taskList = filterTaskList(@taskList, nil, a('hideTask'), a('rollupTask'), a('openNodes')) @taskList.each do |task| next unless task.sourceFileInfo @tags << TagFileEntry.new(task.fullId, task.sourceFileInfo.fileName, task.sourceFileInfo.lineNo, 't') end # Add the reports. @project.reports.each do |report| next unless report.sourceFileInfo @tags << TagFileEntry.new(report.fullId, report.sourceFileInfo.fileName, report.sourceFileInfo.lineNo, 'p') end end # Returns a String that contains the content of the ctags file. # See http://vimdoc.sourceforge.net/htmldoc/tagsrch.html for the spec. def to_ctags # The ctags header. Not used if this is really needed. s = <<"EOT" !_TAG_FILE_FORMAT 2 /extended format; --format=1 will not append ;" to lines/ !_TAG_FILE_SORTED 1 /0=unsorted, 1=sorted, 2=foldcase/ !_TAG_PROGRAM_AUTHOR #{AppConfig.authors.join(';')} // !_TAG_PROGRAM_NAME #{AppConfig.softwareName} // !_TAG_PROGRAM_URL #{AppConfig.contact} /official site/ !_TAG_PROGRAM_VERSION #{AppConfig.version} // EOT # Turn the list of Tags into ctags lines. @tags.sort.each do |tag| s << tag.to_ctags end s end end end TaskJuggler-3.8.1/lib/taskjuggler/reports/TaskListRE.rb000066400000000000000000000043771473026623400230670ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = TaskListRE.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/reports/TableReport' require 'taskjuggler/reports/ReportTable' require 'taskjuggler/TableColumnDefinition' require 'taskjuggler/LogicalExpression' class TaskJuggler # This specialization of TableReport implements a task listing. It # generates a list of tasks that can optionally have the allocated resources # nested underneath each task line. class TaskListRE < TableReport # Create a new object and set some default values. def initialize(report) super @table = ReportTable.new @table.selfcontained = report.get('selfcontained') @table.auxDir = report.get('auxdir') end # Generate the table in the intermediate format. def generateIntermediateFormat super # Prepare the task list. taskList = PropertyList.new(@project.tasks) taskList.includeAdopted taskList.setSorting(@report.get('sortTasks')) taskList.query = @report.project.reportContexts.last.query taskList = filterTaskList(taskList, nil, @report.get('hideTask'), @report.get('rollupTask'), @report.get('openNodes')) taskList.sort! taskList.checkForDuplicates(@report.sourceFileInfo) # Prepare the resource list. Don't filter it yet! It would break the # *_() LogicalFunctions. resourceList = PropertyList.new(@project.resources) resourceList.setSorting(@report.get('sortResources')) resourceList.query = @report.project.reportContexts.last.query resourceList.sort! # Generate the table header. @report.get('columns').each do |columnDescr| adjustColumnPeriod(columnDescr, taskList, @report.get('scenarios')) generateHeaderCell(columnDescr) end # Generate the list. generateTaskList(taskList, resourceList, nil) end end end TaskJuggler-3.8.1/lib/taskjuggler/reports/TextReport.rb000066400000000000000000000053371473026623400232170ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = TextReport.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/reports/ReportBase' class TaskJuggler # This is the most basic type of report. It only contains 5 RichText elements. # It's got a header and footer and a central text with margin elements left # and right. class TextReport < ReportBase attr_accessor :header, :left, :center, :right, :footer def initialize(report) super @lWidth = @cWidth = @rWidth = 0 @lPadding = @cPadding = @rPadding = 0 end def generateIntermediateFormat super # A width of 0 means, the columns flexible. if a('center') if a('left') && a('right') @lWidth = @rWidth = 20 @cWidth = 60 @lPadding = @cPadding = 2 elsif a('left') && !a('right') @lWidth = 25 @cWidth = 75 @lPadding = 2 elsif !a('left') && a('right') @cWidth = 75 @rWidth = 25 @cPadding = 2 else @cWidth = 100 end else if a('left') && a('right') @lWidth = @rWidth = 50 @lPadding = 2 elsif a('left') && !a('right') @lWidth = 100 elsif !a('left') && a('right') @rWidth = 100 end end end def to_html html = [] html << rt_to_html('header') if a('left') || a('center') || a('right') html << (table = XMLElement.new('table', 'class' => 'tj_text_page', 'cellspacing' => '0')) table << (row = XMLElement.new('tr', 'class' => 'tj_text_row')) %w( left center right).each do |i| width = instance_variable_get('@' + i[0].chr + 'Width') padding = instance_variable_get('@' + i[0].chr + 'Padding') if a(i) row << (col = XMLElement.new('td', 'class' => "tj_column_#{i}")) style = '' style += "width:#{width}%; " if width > 0 style += "padding-right:#{padding}%; " if padding > 0 col['style'] = style col << rt_to_html(i) end end end html << rt_to_html('footer') html end def to_csv @report.warning('text_report_no_csv', "textreport '#{@report.fullId}' cannot be converted " + "into CSV format") nil end end end TaskJuggler-3.8.1/lib/taskjuggler/reports/TimeSheetReport.rb000066400000000000000000000255401473026623400241600ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = TimeSheetReport.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/reports/ReportBase' class TaskJuggler # Utility class for the intermediate TimeSheetReport format. class TSResourceRecord attr_reader :resource, :tasks attr_accessor :vacationHours, :vacationPercent def initialize(resource) @resource = resource @vacationHours = 0.0 @vacationPercent = 0.0 @tasks = [] end end # Utility class for the intermediate TimeSheetReport format. class TSTaskRecord attr_reader :task, :workDays, :workPercent, :remaining, :endDate def initialize(task, workDays, workPercent, remaining = nil, endDate = nil) @task = task @workDays = workDays @workPercent = workPercent @remaining = remaining @endDate = endDate end end # This specialization of ReportBase implements a template generator for time # sheets. The time sheet is structured using the TJP file syntax. class TimeSheetReport < ReportBase # Create a new object and set some default values. def initialize(report) super(report) @current = [] @future = [] end # In the future we might want to generate other output than TJP synatx. So # we generate an abstract version of the time sheet first. This abstract # version has a TSResourceRecord for each resource and each of these # records holds a TSTaskRecord for each assigned task. def generateIntermediateFormat super @current = collectRecords(a('start'), a('end')) newEnd = a('end') + (a('end').to_i - a('start').to_i) newEnd = @project['end'] if newEnd > @project['end'] @future = collectRecords(a('end'), a('end') + (a('end') - a('start'))) end # Generate a time sheet in TJP syntax format. def to_tjp # This String will hold the result. @file = <<'EOT' # The status headline should be no more than 60 characters and may # not be empty! The status summary is optional and should be no # longer than one or two sentences of plain text. The details section # is also optional has no length limitation. You can use simple # markup in this section. It is recommended that you provide at # least a summary or a details section. # See http://www.taskjuggler.org/tj3/manual/timesheet.html for details. # # --------8<--------8<-------- EOT # Iterate over all the resources that we have TSResourceRecords for. @current.each do |rr| resource = rr.resource # Generate the time sheet header @file << "timesheet #{resource.fullId} " + "#{a('start')} - #{a('end')} {\n\n" @file << " # Vacation time: #{rr.vacationPercent}%\n\n" if rr.tasks.empty? # If there were no assignments, just write a comment. @file << " # There were no planned tasks assignments for " + "this period!\n\n" else rr.tasks.each do |tr| task = tr.task @file << " # Task: #{task.name}\n" @file << " task #{task.fullId} {\n" #@file << " work #{tr.workDays * # @project['dailyworkinghours']}h\n" @file << " work #{tr.workPercent}%\n" if tr.remaining @file << " remaining #{tr.remaining}d\n" else @file << " end #{tr.endDate}\n" end c = tr.workDays > 1.0 ? '' : '# ' @file << " #{c}status green \"Your headline here!\" {\n" + " # summary -8<-\n" + " # A summary text\n" + " # ->8-\n" + " # details -8<-\n" + " # Some more details\n" + " # ->8-\n" + " # flags ...\n" + " #{c}}\n" @file << " }\n\n" end end @file << <<'EOT' # If you had unplanned tasks, uncomment and fill out the # following lines: # newtask new.task.id "A task title" { # work X% # remaining Y.Yd # status green "Your headline here!" { # summary -8<- # A summary text # ->8- # details -8<- # Some more details # ->8- # flags ... # } # } # You can use the following section to report personal notes. # status green "Your headline here!" { # summary -8<- # A summary text # ->8- # details -8<- # Some more details # ->8- # } EOT future = @future[@future.index { |r| r.resource == resource }] if future && !future.tasks.empty? @file << <<'EOT' # # Your upcoming tasks for the next period # Please check them carefully and discuss any necessary # changes with your manager or project manager! # EOT future.tasks.each do |taskRecord| @file << " # #{taskRecord.task.name}: #{taskRecord.workPercent}%\n" end @file << "\n" else @file << "\n # You have no future assignments for this project!\n" end @file << "}\n# -------->8-------->8--------\n\n" end @file end private def collectRecords(from, to) # Prepare the resource list. resourceList = PropertyList.new(@project.resources) resourceList.setSorting(@report.get('sortResources')) resourceList = filterResourceList(resourceList, nil, @report.get('hideResource'), @report.get('rollupResource'), @report.get('openNodes')) # Prepare a template for the Query we will use to get all the data. scenarioIdx = a('scenarios')[0] queryAttrs = { 'project' => @project, 'scopeProperty' => nil, 'scenarioIdx' => scenarioIdx, 'loadUnit' => :days, 'numberFormat' => RealFormat.new([ '-', '', '', '.', 1]), 'timeFormat' => "%Y-%m-%d", 'currencyFormat' => a('currencyFormat'), 'start' => from, 'end' => to, 'hideJournalEntry' => a('hideJournalEntry'), 'journalMode' => a('journalMode'), 'journalAttributes' => a('journalAttributes'), 'sortJournalEntries' => a('sortJournalEntries'), 'costAccount' => a('costaccount'), 'revenueAccount' => a('revenueaccount') } resourceList.query = Query.new(queryAttrs) resourceList.sort! # Prepare the task list. taskList = PropertyList.new(@project.tasks) taskList.setSorting(@report.get('sortTasks')) taskList = filterTaskList(taskList, nil, @report.get('hideTask'), @report.get('rollupTask'), @report.get('openNodes')) records = [] resourceList.each do |resource| # Time sheets only make sense for leaf resources that actuall do work. next unless resource.leaf? # Create a new TSResourceRecord for the resource. records << (resourceRecord = TSResourceRecord.new(resource)) # Calculate the average working days per week (usually 5) weeklyWorkingDays = @project.weeklyWorkingDays # Calculate the number of weeks in the report weeksToReport = (to - from) / (60 * 60 * 24 * 7) # Get the vacation days for the resource for this period. queryAttrs['property'] = resource query = Query.new(queryAttrs) query.attributeId = 'timeoffdays' query.start = from query.end = to query.process resourceRecord.vacationHours = query.to_s resourceRecord.vacationPercent = (query.to_num / (weeksToReport * weeklyWorkingDays)) * 100.0 # Now we have to find all the task that the resource is allocated to # during the report period. assignedTaskList = filterTaskList(taskList, resource, a('hideTask'), a('rollupTask'), a('openNodes')) queryAttrs['scopeProperty'] = resource assignedTaskList.query = Query.new(queryAttrs) assignedTaskList.sort! assignedTaskList.each do |task| # Time sheet task records only make sense for leaf tasks. reportIv = TimeInterval.new(from, to) taskIv = TimeInterval.new(task['start', scenarioIdx], task['end', scenarioIdx]) next if !task.leaf? || !reportIv.overlaps?(taskIv) queryAttrs['property'] = task query = Query.new(queryAttrs) # Get the allocated effort for the task for this period. query.attributeId = 'effort' query.start = from query.end = to query.process # The Query.to_num of an effort always returns the value in days. workDays = query.to_num workPercent = (workDays / (weeksToReport * weeklyWorkingDays)) * 100.0 remaining = endDate = nil if task['effort', scenarioIdx] > 0 # The task is an effort based task. # Get the remaining effort for this task. query.start = to query.end = task['end', scenarioIdx] query.loadUnit = :days query.process remaining = query.to_s else # The task is a duration task. # Get the planned task end date. endDate = task['end', scenarioIdx] end # Put all data into a TSTaskRecord and push it into the resource # record. resourceRecord.tasks << TSTaskRecord.new(task, workDays, workPercent, remaining, endDate) end end records end # This utility function is used to indent multi-line attributes. All # attributes should be filtered through this function. Attributes that # contain line breaks will be indented properly. In addition to the # indentation specified by _indent_ all but the first line will be indented # after the first word of the first line. The text may not end with a line # break. def indentBlock(text, indent) out = '' firstSpace = 0 text.length.times do |i| if firstSpace == 0 && text[i] == ?\ # There must be a space after ? firstSpace = i end out << text[i] if text[i] == ?\n out += ' ' * (indent + firstSpace) end end out end end end TaskJuggler-3.8.1/lib/taskjuggler/reports/TjpExportRE.rb000066400000000000000000000417061473026623400232650ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = TjpExportRE.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/reports/ReportBase' class TaskJuggler # This specialization of ReportBase implements an export of the # project data in the TJP syntax format. class TjpExportRE < ReportBase # Create a new object and set some default values. def initialize(report) super(report) @supportedTaskAttrs = %w( booking complete depends flags maxend maxstart minend minstart note priority projectid responsible ) @supportedResourceAttrs = %w( booking flags shifts vacation workinghours ) end def generateIntermediateFormat super end # Return the project data in TJP syntax format. def to_tjp # Prepare the resource list. @resourceList = PropertyList.new(@project.resources) @resourceList.setSorting(a('sortResources')) @resourceList = filterResourceList(@resourceList, nil, a('hideResource'), a('rollupResource'), a('openNodes')) @resourceList.sort! # Prepare the task list. @taskList = PropertyList.new(@project.tasks) @taskList.setSorting(a('sortTasks')) @taskList = filterTaskList(@taskList, nil, a('hideTask'), a('rollupTask'), a('openNodes')) @taskList.sort! getBookings @file = '' generateProjectProperty if a('definitions').include?('project') generateFlagDeclaration if a('definitions').include?('flags') generateProjectIDs if a('definitions').include?('projectids') generateShiftList if a('definitions').include?('shifts') generateResourceList if a('definitions').include?('resources') generateTaskList if a('definitions').include?('tasks') generateTaskAttributes unless a('taskAttributes').empty? generateResourceAttributes unless a('resourceAttributes').empty? @file end private def generateProjectProperty @file << "project #{@project['projectid']} \"#{@project['name']}\" " + "\"#{@project['version']}\" #{@project['start']} - " + "#{@project['end']} {\n" # Add timingresolution attribute if it's not the default value. if @project['scheduleGranularity'] != 3600 generateAttributeText("timingresolution " + "#{@project['scheduleGranularity'] / 60}min", 2) end generateAttributeText("timezone \"#{@project['timezone']}\"", 2) if @project['alertLevels'].modified? generateAttributeText(@project['alertLevels'].to_tjp, 2) end generateCustomAttributeDeclarations('resource', @project.resources, a('resourceAttributes')) generateCustomAttributeDeclarations('task', @project.tasks, a('taskAttributes')) generateScenarioDefinition(@project.scenario(0), 2) @file << "}\n\n" end def generateScenarioDefinition(scenario, indent) @file << "#{' ' * indent}scenario #{scenario.id} " + "#{quotedString(scenario.name)} {\n" scenario.children.each do |sc| generateScenarioDefinition(sc, indent + 2) end @file << "#{' ' * (indent + 2)}active " + "#{scenario.get('active') ? 'yes' : 'no'}\n" @file << "#{' ' * indent}}\n" end def generateCustomAttributeDeclarations(tag, propertySet, attributes) # First we search the attribute definitions for any user defined # attributes and count them. customAttributes = 0 propertySet.eachAttributeDefinition do |ad| customAttributes += 1 if ad.userDefined end # Return if there are no user defined attributes. return if customAttributes == 0 # Generate definitions for each user defined attribute that is in the # taskAttributes list. @file << ' extend ' + tag + "{\n" propertySet.eachAttributeDefinition do |ad| next unless ad.userDefined && attributes.include?(ad.id) @file << " #{ad.objClass.tjpId} #{ad.id} " + "#{quotedString(ad.name)}" if ad.scenarioSpecific || ad.inheritedFromParent @file << " { " @file << "scenariospecific " if ad.scenarioSpecific @file << "inherit " if ad.inheritedFromParent @file << "}" end @file << "\n" end @file << " }\n" end def generateFlagDeclaration flags = [] properties = @resourceList + @taskList properties.each do |property| a('scenarios').each do |scenarioIdx| property['flags', scenarioIdx].each do |flag| flags << flag unless flags.include?(flag) end end end flags.sort unless flags.empty? @file << "flags #{flags.join(', ')}\n\n" end end def generateProjectIDs # Compile a list of all projectIDs from the tasks in the taskList. projectIDs = [] a('scenarios').each do |scenarioIdx| @taskList.each do |task| pid = task['projectid', scenarioIdx] projectIDs << pid unless pid.nil? || projectIDs.include?(pid) end end @file << "projectids #{projectIDs.join(', ')}\n\n" unless projectIDs.empty? end def generateShiftList @project.shifts.each do |shift| generateShift(shift, 0) unless shift.parent end end def generateShift(shift, indent) @file << ' ' * indent + "shift #{shift.id} " + "#{quotedString(shift.name)} {\n" a('scenarios').each do |scenarioIdx| generateAttribute(shift, 'workinghours', indent + 2, scenarioIdx) end # Call this method recursively for all children. shift.children.each do |subshift| generateShift(subshift, indent + 2) end @file << ' ' * indent + "}\n" end def generateResourceList # The resource definitions are generated recursively. So we only need to # start it for the top-level resources. @resourceList.each do |resource| if resource.parent.nil? generateResource(resource, 0) end end @file << "\n" end def generateResource(resource, indent) Log.activity if resource.sequenceNo % 100 == 0 @file << ' ' * indent + "resource #{resource.id} " + "#{quotedString(resource.name)}" @file << ' {' unless resource.children.empty? @file << "\n" # Call this function recursively for all children that are included in the # resource list as well. resource.children.each do |subresource| if @resourceList.include?(subresource) generateResource(subresource, indent + 2) end end @file << ' ' * indent + "}\n" unless resource.children.empty? end def generateTaskList # The task definitions are generated recursively. So we only need to start # it for the top-level tasks. @taskList.each do |task| if task.parent.nil? generateTask(task, 0) end end end # Generate a task definition. It only contains a very small set of # attributes that have to be passed on the the nested tasks at creation # time. All other attributes are declared in subsequent supplement # statements. def generateTask(task, indent) Log.activity if task.sequenceNo % 100 == 0 @file << ' ' * indent + "task #{task.subId} " + "#{quotedString(task.name)} {\n" if a('taskAttributes').include?('depends') a('scenarios').each do |scenarioIdx| generateTaskDependency(scenarioIdx, task, 'depends', indent + 2) generateTaskDependency(scenarioIdx, task, 'precedes', indent + 2) end end # Call this function recursively for all children that are included in the # task list as well. task.children.each do |subtask| if @taskList.include?(subtask) generateTask(subtask, indent + 2) end end # Determine whether this task has subtasks that are included in the # report or whether this is a leaf task for the report. isLeafTask = true task.children.each do |subtask| if @taskList.include?(subtask) isLeafTask = false break end end # For leaf tasks we put some attributes right here. if isLeafTask a('scenarios').each do |scenarioIdx| generateAttribute(task, 'start', indent + 2, scenarioIdx) if !task['milestone', scenarioIdx] generateAttribute(task, 'end', indent + 2, scenarioIdx) generateAttributeText('scheduling ' + (task['forward', scenarioIdx] ? 'asap' : 'alap'), indent + 2, scenarioIdx) end if task['scheduled', scenarioIdx] && !inheritable?(task, 'scheduled', scenarioIdx) generateAttributeText('scheduled', indent + 2, scenarioIdx) end end end @file << ' ' * indent + "}\n" end # Generate 'depends' or 'precedes' attributes for a task. def generateTaskDependency(scenarioIdx, task, tag, indent) return unless a('taskAttributes').include?('depends') taskDeps = task[tag, scenarioIdx] unless taskDeps.empty? str = "#{tag} " first = true taskDeps.each do |dep| next if inheritable?(task, tag, scenarioIdx, dep) || (task.parent && task.parent[tag, scenarioIdx].include?(dep)) if first first = false else str << ', ' end str << dep.task.fullId end generateAttributeText(str, indent, scenarioIdx) unless first end end # Generate a list of resource supplement statements that include the rest of # the attributes. def generateResourceAttributes @resourceList.each do |resource| Log.activity if resource.sequenceNo % 100 == 0 @file << "supplement resource #{resource.fullId} {\n" @project.resources.eachAttributeDefinition do |attrDef| id = attrDef.id next if (!@supportedResourceAttrs.include?(id) && !attrDef.userDefined) || !a('resourceAttributes').include?(id) if attrDef.scenarioSpecific a('scenarios').each do |scenarioIdx| next if inheritable?(resource, id, scenarioIdx) generateAttribute(resource, id, 2, scenarioIdx) end else generateAttribute(resource, id, 2) end end # Since 'booking' is a task attribute, we need a special handling if # we want to list them in the resource context. if a('resourceAttributes').include?('booking') && a('resourceAttributes')[0] != '*' a('scenarios').each do |scenarioIdx| generateBookingsByResource(resource, 2, scenarioIdx) end end @file << "}\n" end end # Generate a list of task supplement statements that include the rest of the # attributes. def generateTaskAttributes @taskList.each do |task| Log.activity if task.sequenceNo % 100 == 0 @file << "supplement task #{task.fullId} {\n" # Declare adopted tasks. adoptees = "" task.adoptees.each do |adoptee| next unless @taskList.include?(adoptee) adoptees += ', ' unless adoptees.empty? adoptees += adoptee.fullId end generateAttributeText("adopt #{adoptees}", 2) unless adoptees.empty? @project.tasks.eachAttributeDefinition do |attrDef| id = attrDef.id next if (!@supportedTaskAttrs.include?(id) && !attrDef.userDefined) || !a('taskAttributes').include?(id) if attrDef.scenarioSpecific a('scenarios').each do |scenarioIdx| # Some attributes need special treatment. case id when 'depends' next # already taken care of when 'booking' generateBookingsByTask(task, 2, scenarioIdx) else generateAttribute(task, id, 2, scenarioIdx) end end else generateAttribute(task, id, 2) end end @file << "}\n" end end def generateAttribute(property, attrId, indent, scenarioIdx = nil) val = scenarioIdx ? property[attrId, scenarioIdx] : property.get(attrId) return if val.nil? || (val.is_a?(Array) && val.empty?) || (scenarioIdx && inheritable?(property, attrId, scenarioIdx)) generateAttributeText(property.getAttribute(attrId, scenarioIdx).to_tjp, indent, scenarioIdx) end def generateAttributeText(text, indent, scenarioIdx = nil) @file << ' ' * indent tag = '' if !scenarioIdx.nil? && scenarioIdx != 0 tag = "#{@project.scenario(scenarioIdx).id}:" @file << tag end @file << "#{indentBlock(text, indent + tag.length + 2)}\n" end # Get the booking data for all resources that should be included in the # report. def getBookings @bookings = {} if a('taskAttributes').include?('booking') || a('resourceAttributes').include?('booking') a('scenarios').each do |scenarioIdx| @bookings[scenarioIdx] = {} @resourceList.each do |resource| # Get the bookings for this resource hashed by task. bookings = resource.getBookings( scenarioIdx, TimeInterval.new(a('start'), a('end'))) next if bookings.nil? # Now convert/add them to a tripple-stage hash by scenarioIdx, task # and then resource. bookings.each do |task, booking| next unless @taskList.include?(task) if !@bookings[scenarioIdx].include?(task) @bookings[scenarioIdx][task] = {} end @bookings[scenarioIdx][task][resource] = booking end end end end end def generateBookingsByTask(task, indent, scenarioIdx) return unless @bookings[scenarioIdx].include?(task) # Convert Hash into an [ Resource, Booking ] Array sorted by Resource # ID. This guarantees a reproducible order. resourceBookings = @bookings[scenarioIdx][task].sort do |a, b| a[0].fullId <=> b[0].fullId end resourceBookings.each do |resourceId, booking| generateAttributeText('booking ' + booking.to_tjp(false), indent, scenarioIdx) end end def generateBookingsByResource(resource, indent, scenarioIdx) # Get the bookings for this resource hashed by task. bookings = resource.getBookings(scenarioIdx, TimeInterval.new(a('start'), a('end')), false) bookings.each do |booking| next unless @taskList.include?(booking.task) generateAttributeText('booking ' + booking.to_tjp(true), indent, scenarioIdx) end end # This utility function is used to indent multi-line attributes. All # attributes should be filtered through this function. Attributes that # contain line breaks will be indented properly. In addition to the # indentation specified by _indent_ all but the first line will be indented # after the first word of the first line. The text may not end with a line # break. def indentBlock(text, indent) out = '' firstSpace = 0 text.length.times do |i| if firstSpace == 0 && text[i] == ?\ # There must be a space after ? firstSpace = i end out << text[i] if text[i] == ?\n out += ' ' * (indent + firstSpace - 1) end end out end def quotedString(str) if str.include?("\n") "-8<-\n#{str}\n->8-" else escaped = str.gsub("\"", '\"') "\"#{escaped}\"" end end # Return true if the attribute value for _attrId_ can be inherited from # the parent scenario. def inheritable?(property, attrId, scenarioIdx, listItem = nil) parentScenario = @project.scenario(scenarioIdx).parent return false unless parentScenario parentScenarioIdx = @project.scenarioIdx(parentScenario) parentAttr = property[attrId, parentScenarioIdx] if parentAttr.is_a?(Array) && listItem return parentAttr.include?(listItem) else return property[attrId, scenarioIdx] == parentAttr end end end end TaskJuggler-3.8.1/lib/taskjuggler/reports/TraceReport.rb000066400000000000000000000204221473026623400233210ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = TraceReport.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/reports/ReportBase' require 'taskjuggler/reports/CSVFile' require 'taskjuggler/reports/ChartPlotter' require 'taskjuggler/TableColumnSorter' require 'taskjuggler/MessageHandler' class TaskJuggler # The trace report is used to periodically snapshot a specific list of # property attributes and add them to a CSV file. class TraceReport < ReportBase include MessageHandler # Create a new object and set some default values. def initialize(report) super @table = nil end # Generate the table in the intermediate format. def generateIntermediateFormat super queryAttrs = { 'project' => @project, 'scopeProperty' => nil, 'loadUnit' => a('loadUnit'), 'numberFormat' => a('numberFormat'), # We use a hardcoded %Y-%m-%d format for tracereports. 'timeFormat' => "%Y-%m-%d", 'currencyFormat' => a('currencyFormat'), 'start' => a('start'), 'end' => a('end'), 'hideJournalEntry' => a('hideJournalEntry'), 'journalMode' => a('journalMode'), 'journalAttributes' => a('journalAttributes'), 'sortJournalEntries' => a('sortJournalEntries'), 'costAccount' => a('costaccount'), 'revenueAccount' => a('revenueaccount') } query = Query.new(queryAttrs) # Prepare the account list. accountList = PropertyList.new(@project.accounts) accountList.setSorting(a('sortAccounts')) accountList.query = query accountList = filterAccountList(accountList, a('hideAccount'), a('rollupAccount'), a('openNodes')) accountList.sort! # Prepare the resource list. resourceList = PropertyList.new(@project.resources) resourceList.setSorting(a('sortResources')) resourceList.query = query resourceList = filterTaskList(resourceList, nil, a('hideResource'), a('rollupResource'), a('openNodes')) resourceList.sort! # Prepare the task list. taskList = PropertyList.new(@project.tasks) taskList.includeAdopted taskList.setSorting(a('sortTasks')) taskList.query = query taskList = filterTaskList(taskList, nil, a('hideTask'), a('rollupTask'), a('openNodes')) taskList.sort! @fileName = (@report.name[0] == '/' ? '' : @project.outputDir) + @report.name + '.csv' # Generate the table header. headers = [ 'Date' ] + generatePropertyListHeader(accountList, query) + generatePropertyListHeader(resourceList, query) + generatePropertyListHeader(taskList, query) discontinuedColumns = 0 if File.exist?(@fileName) begin @table = CSVFile.new(nil, nil).read(@fileName) rescue error('tr_cannot_read_csv', "Cannot read CSV file #{@fileName}: #{$!}") end if @table[0] != headers # Some columns have changed. We move all discontinued columns to the # last columns and rearrange the others according to the new # headers. New columns will be filled with nil in previous rows. sorter = TableColumnSorter.new(@table) @table = sorter.sort(headers) discontinuedColumns = sorter.discontinuedColumns end else @table = [ headers ] end # Convert empty strings into nil objects and dates in %Y-%m-%d format # into TjTime objects. @table.each do |line| line.length.times do |i| if line[i] == '' line[i] = nil elsif line[i].is_a?(String) && /\d{4}-\d{2}-\d{2}/ =~ line[i] line[i] = TjTime.new(line[i]) end end end query = @project.reportContexts.last.query.dup dateTag = @project['now'].midnight idx = @table.index { |line| line[0] == dateTag } discColumnValues = discontinuedColumns > 0 ? Array.new(discontinuedColumns, nil) : [] if idx # We already have an entry for the current date. All old values of # this line will be overwritten with the current values. The old # values in the discontinued columns will be kept. if discontinuedColumns > 0 discColumnValues = @table[idx][headers.length..-1] end @table[idx] = [] else # Append a new line of values to the table. @table << [] idx = -1 end # The first entry is always the current date. @table[idx] << dateTag # Now add the new values to the line generatePropertyListValues(idx, accountList, query) generatePropertyListValues(idx, resourceList, query) generatePropertyListValues(idx, taskList, query) # Fill the discontinued columns with old values or nil. @table[idx] += discColumnValues # Sort the table by ascending first column dates. We need to ensure that # the header remains the first line in the table. @table.sort! { |l1, l2| l1[0].is_a?(String) ? -1 : (l2[0].is_a?(String) ? 1 : l1[0] <=> l2[0]) } end def to_html html = [] html << rt_to_html('header') begin plotter = ChartPlotter.new(a('width'), a('height'), @table) plotter.generate html << plotter.to_svg rescue ChartPlotterError => exception warning('chartPlotterError', exception.message, @report.sourceFileInfo) end html << rt_to_html('footer') html end def to_csv # Convert all TjTime values into String with format %Y-%m-%d and nil # objects into empty Strings. @table.each do |line| line.length.times do |i| if line[i].nil? line[i] = '' elsif line[i].is_a?(TjTime) line[i] = line[i].to_s('%Y-%m-%d') end end end @table end private def generatePropertyListHeader(propertyList, query) headers = [] query = query.dup a('columns').each do |columnDescr| query.attributeId = columnDescr.id a('scenarios').each do |scenarioIdx| query.scenarioIdx = scenarioIdx propertyList.each do |property| query.property = property #adjustColumnPeriod(columnDescr, propertyList, a.get('scenarios')) header = SimpleQueryExpander.new(columnDescr.title, query, @report.sourceFileInfo).expand if headers.include?(header) error('trace_columns_not_uniq', "The column title '#{header}' is already used " + "by a previous column. Column titles must be " + "unique!") end headers << header end end end headers end def generatePropertyListValues(idx, propertyList, query) @report.get('columns').each do |columnDescr| query.attributeId = columnDescr.id a('scenarios').each do |scenarioIdx| query.scenarioIdx = scenarioIdx propertyList.each do |property| query.property = property query.process @table[idx] << query.result end end end end def columnTitle(property, scenarioIdx, columnDescr) title = columnDescr.title.dup # The title can be parameterized by including mini-queries for the ID # or the name of the property, the scenario id or the attribute ID. title.gsub!(/<-id->/, property.fullId) title.gsub!(/<-scenario->/, @project.scenario(scenarioIdx).id) title.gsub!(/<-name->/, property.name) title.gsub!(/<-attribute->/, columnDescr.id) title end end end TaskJuggler-3.8.1/lib/taskjuggler/version.rb000066400000000000000000000000221473026623400210500ustar00rootroot00000000000000VERSION = '3.8.1' TaskJuggler-3.8.1/lib/tj3.rb000066400000000000000000000007521473026623400155530ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = tj3.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/apps/Tj3' exit TaskJuggler::Tj3.new.main() TaskJuggler-3.8.1/lib/tj3client.rb000066400000000000000000000010001473026623400167350ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = tj3client.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/apps/Tj3Client' exit TaskJuggler::Tj3Client.new.main(ARGV) TaskJuggler-3.8.1/lib/tj3d.rb000066400000000000000000000007671473026623400157250ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = tj3d.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/apps/Tj3Daemon' exit TaskJuggler::Tj3Daemon.new.main() TaskJuggler-3.8.1/lib/tj3man.rb000066400000000000000000000007621473026623400162500ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = tj3man.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/apps/Tj3Man' exit TaskJuggler::Tj3Man.new.main() TaskJuggler-3.8.1/lib/tj3ss_receiver.rb000066400000000000000000000010111473026623400177720ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = tj3ss_receiver.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/apps/Tj3SsReceiver' exit TaskJuggler::Tj3SsReceiver.new.main() TaskJuggler-3.8.1/lib/tj3ss_sender.rb000066400000000000000000000010031473026623400174470ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = tj3ss_sender.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/apps/Tj3SsSender' exit TaskJuggler::Tj3SsSender.new.main() TaskJuggler-3.8.1/lib/tj3ts_receiver.rb000066400000000000000000000010111473026623400177730ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = tj3ts_receiver.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/apps/Tj3TsReceiver' exit TaskJuggler::Tj3TsReceiver.new.main() TaskJuggler-3.8.1/lib/tj3ts_sender.rb000066400000000000000000000010031473026623400174500ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = tj3ts_sender.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/apps/Tj3TsSender' exit TaskJuggler::Tj3TsSender.new.main() TaskJuggler-3.8.1/lib/tj3ts_summary.rb000066400000000000000000000010061473026623400176700ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = tj3ts_summary.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/apps/Tj3TsSummary' exit TaskJuggler::Tj3TsSummary.new.main() TaskJuggler-3.8.1/lib/tj3webd.rb000066400000000000000000000007661473026623400164220ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = tj3webd.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/apps/Tj3WebD' exit TaskJuggler::Tj3WebD.new.main() TaskJuggler-3.8.1/lib/updateheader.sh000066400000000000000000000010131473026623400175040ustar00rootroot00000000000000files="*.rb taskjuggler/*.rb taskjuggler/apps/*.rb taskjuggler/daemon/*.rb taskjuggler/reports/*.rb taskjuggler/RichText/*.rb taskjuggler/TextParser/*.rb ../test/*.rb ../spec/*.rb ../spec/support/*.rb" for f in $files; do basename=`basename $f` sed "s/\$FILE/$basename/g" header.tmpl > header firstLine=`head -1 $f` if test "$firstLine" == "#!/usr/bin/env ruby -w"; then sed '1,/^$/d' $f > tmpfile mv -f tmpfile $f else echo "$f has no header" fi cat header $f > tmpfile mv -f tmpfile $f done TaskJuggler-3.8.1/manual/000077500000000000000000000000001473026623400152315ustar00rootroot00000000000000TaskJuggler-3.8.1/manual/Day_To_Day_Juggling000066400000000000000000001077341473026623400207720ustar00rootroot00000000000000== Day To Day Juggling == === Working with multiple scenarios === To analyze the impact that a small variation can have on a project, TaskJuggler supports an unlimited amount of scenarios. Each additional scenario is a slight derivation of it's parent. The task tree structure needs to be the same for all scenarios, but most attributes can vary from one scenario to another. Several report types support comparative listing of multiple [[scenarios]]. By default, TaskJuggler knows about one scenario, called ''''plan''''. The name of this scenario can be changed just like you can add more scenarios in the [[project]] section of your project files. <[example file="Scenario" tag="header"]> This header section defines 3 different scenarios. ''''plan'''' is the top-level scenario. It has two derived scenarios, ''''actual'''' and ''''test''''. These two scenarios are identical to the ''''plan'''' scenario except for those attributes that are changed for these scenarios. Normally, all scenarios are scheduled on each ''''tj3'''' run. To temporarily disable the scheduling of a scenario, you can set the [[active]] attribute to ''''no''''. <[example file="Scenario" tag="task"]> If you prefix an attribute with the scenario ID followed immediately by a colon, you can specify a value for a particular attribute. Keep in mind that setting an attribute also sets the same value for ''all derived scenarios'' of this scenario as well! If you would specify ''''actual:start'''' first and then ''''plan:start'''', the latter would overwrite the first value again since actual is a derived scenario of plan. The syntax reference lists for each attribute whether it is scenario specific or not. === Important and fall-back Tasks === By default, the scheduler tries to guess the right priority of tasks. The higher the priority, the more likely it will get the requested resources. To override this mechanism, the [[priority]] attribute can be used. <[example file="Priority" tag="project"]> In the above example, the regular project work needs to be frequently interrupted by the ''Customer Support'' task. It's only 2 hours a day, but it's pretty important that this is done. Since the task has a higher priority than the regular work, the scheduler will try to ensure that a maximum of 2 hours per day is spent on support. There is no guarantee, that the task will always get the resource for 2 hours each day, but it's pretty likely in this setup. There is only one task that is more important, the ''Attend Conference'' task. It has fixed dates and we want to make sure ''Tux'' can attend. So we use ''''priority 1000'''', the highest possible priority. There should be only one such task. If not, you need to ensure that the top priority tasks don't compete for the same resource in the same time frame. In contrast to the support and conference task, the ''Maintenance work'' task is a fall-back task. It has a lower priority than the regular work. Tux only gets assigned to it when there is no other work. We have limited the regular work to 25 hours per week. Since we spend up to 10 hours per week on support, there should be a remainder of 5 hours per week for the maintenance task. Again, no guarantees given. If you want to ensure that a certain minimum or maximum effort is spent on a task, you can use the [[warn]] attribute. This will not affect the decisions of the scheduler, but at least it will trigger a warning if your criteria are not met. === Tracking the Project === Once the initial plan has been made and the project has started, TaskJuggler can be turned from a planning tool into a tracking tool. You don't have to change a lot to do this. After all, as the initial plan is almost always just a first guess, you need to continue planning your project as new details become evident. As the work progresses, you continuously review the state of the project and update the plan accordingly. A weekly review and update cycle seems to be pretty common for most projects. Usually the plan for the past week and the reality are mostly aligned. The future parts of the project often are more affected by necessary changes. While it is generally accepted to invest some amount of time in project planning, it is very common that once the project has been started, project managers tend to avoid a proper tracking of the project. Our bet is that the vast majority of project plans are only made to get management or investor approval. After the approval phase, many project managers only work with their project plan again when the project is getting out of control and they are desperate for any help they can get. Of course, there are projects that are done using strict project management techniques that require detailed status tracking. Both extremes probably have their fans and TaskJuggler offers good support for both extremes as well as various techniques in between. === Recording Progress === As mentioned previously, your initial project plan is only a first estimate of how the project will progress. During the course of the project you will have to make changes to the plan as new information needs to be taken into account and you probably want to track the progress of the project in a formalized form. TaskJuggler will support you during this phase of the project as well, but it needs your help. You have to provide the additional information in the project file. In return you get current status reports and an updated project plan based on the current status of the project. ==== Using completion values ==== The most simple form of capturing the current status of the project is to use the complete attribute. task impl "Implementation" { depends !spec effort 4w allocate dev1, dev2 complete 50 } This tells TaskJuggler that 50% of the task's effort has been completed by the [[now|current date]]. Tasks that have no completion specification will be assumed to be on track and TaskJuggler calculates the expected completion degree based on the current date. Completion specifications only need to be supplied for tasks that are either ahead of schedule or behind schedule. Please be aware that the completion degree does not affect the scheduling and resource allocation. It is only for reporting purposes. It also does not tell TaskJuggler which resource actually worked on the tasks, nor does it update the total or remaining effort. ==== Using bookings ==== When TaskJuggler schedules your plan, it can tell you who should work when on what. Now, that's the plan. But reality might be different. To tell TaskJuggler what really happened, you can use [[booking.task|booking statements]]. When the past is exactly described by providing booking statements, you can enable [[projection|projection mode]]. Entering all the bookings for each resource and task may sound like a daunting task at first. If you do it manually, it certainly is. Fortunately, TaskJuggler can generate them for you by using either the ''''--freeze'''' option of ''''tj3'''' or by generating a manual [[export|export report]]. Before we discuss this in more detail, we need to make sure that the plan is up-to-date. === Tracking status and actuals === Creating a good project plan is one thing. Executing it is a whole new story. Usually, the first plan is never fully correct and the only way to make sure that you are making progress according to plan is to regularly get status updates from all the project contributors. These status updates should be provided by all project contributors on a regular basis, usually once a week. The gathered information should tell project managers who really worked how much on what tasks and how much work the contributors believe is really left now. There are two categories of tasks in a project that need to be treated slightly differently. A task can either be effort based or duration based. In the former case, the contributors must tell how much effort is left. For duration based task, this doesn't make much sense. For these task, the expected end date should be reported. In addition to those numbers, managers in the reporting chain usually want to have a textual status that describes what happened and what kind of issues were encountered. Usually, these textual status reports are combined with alert levels like green, yellow and red. Green means everything is progressing according to plan, yellow means there is some schedule risk and red means the project is in serious trouble. Usually first line managers like to get all the details while people further up in the reporting chain only like to see summaries with varying level of details. All of this creates additional overhead but is usually inevitable to ensure that you complete the project within the given time and budget. As a comprehensive project management solution, TaskJuggler provides full support for all those tracking and reporting steps. It comes with a powerful email and web based communication system that simplifies the tracking process for individual contributors as well as managers. As a side note we would like to mention that the recording of the work time of employees is regulated by labor law in certain countries. You might also require approval from a Worker's Council before you can deploy any time recording tools. Please consult with your corporate counsel or legal expert for all geographic regions of your teams before you deploy a time tracking solution. We also would like to point out that introducing status reporting and time sheets is usually a big change for every staff. Don't underestimate the psychological impact and the training requirements. We also recommend to test the described process with a small group of employees first to get familiar with the process and to adapt it to your needs. Don't rush a deployment! You usually only have one chance to roll-out such a new process. ==== The reporting and tracking cycle ==== In this description, we assume that you are using a weekly reporting cycle. TaskJuggler does support arbitrary cycles, but we highly recommend the described weekly cycle. # '''Time sheets''': Every project contributor needs to fill out a [[timesheet|time sheet]] once a week. To simplify this task as much as possible, a template will be send out by email. The template already lists all tasks that were planned for this week to work on with the respective effort values and end dates. It also provides sections for textual status reports. The contributor needs to review and complete the time sheet and has to send it back via email. TaskJuggler validates the submission and returns an email with either an error message or a nicely formatted version of the time sheet. # All time sheets must be submitted by a certain deadline, e. g. midnight on Sunday. TaskJuggler will then compile a summary report and sent it out to a list of interested parties. It will also detect missing time sheets and will send out a reminder to those contributors that have not submitted their report. # On Monday the project managers need to review the time sheets and update the plan accordingly. TaskJuggler can compile a list of changes compared to the plan. This makes it easy to update the plan according to the actual progress that was made. The closer the actuals match the plan the less work this is. The project managers now generate bookings for the last week and add them to the database with previous bookings. Doing so will prevent changes to the plan to affect the past. Only the future will be modified. # Once the plan has been updated, managers will receive their status sheet templates per email. Each manager will get the information for the tasks that they are [[responsible]] for. To consolidate the information for the next manager in the reporting chain they can moderate the reports in three ways. Consolidated manager reports are called dashboard reports. ## A status report for a task can be removed from the dashboard. ## A status report for a task can be corrected or updated. ## All reports for sub tasks of a task can be summarized by creating a new status for that task. This will remove all reports for sub tasks of that particular tasks from the dashboard. # Managers than need to send back the edited status report via email. Like with time sheets, TaskJuggler will check them and return either an error message or a plain text version of the dashboard report of the manager. In addition to the plain text versions of the time sheet summaries and the dashboards, TaskJuggler provides support for publishing them as HTML pages from a web server. === Implementing the status tracking system === ==== Prerequisites ==== The .tjp and .tji files of your project plan should be managed by a revision control system. TaskJuggler does not require a particular software, but for this manual we illustrate the implementation with [http://subversion.apache.org Subversion]. It should be obvious how to do this with other software though. All communication of time sheets and status sheets is done via email. TaskJuggler has built-in support for sending emails. To receive emails and to feed them to the correct program, TaskJuggler needs support from a mail transfer agent (MTA) and a mail processor. In this documentation we describe the setup with [http://www.postfix.org/ postfix] as MTA and [http://www.procmail.org/ procmail] as mail processor. These are standard parts of any Linux distribution and should be easy to setup. It's certainly possible to use other MTAs and mail processors, but this is not the scope of this manual. Finally, you need a web server to publish your reports. This can really be any web server. The generated reports are static HTML pages that can simply be put into a directory that the web server is serving. For the email based communication you need to provide email addresses for all project contributors. This is done in the project plan in the resource definition by using the [[email]] attribute. resource joe "Joe Average" { email "joe@your_company.com } In this manual, we assume you have a dedicated Linux machine with a local user called ''''taskjuggler''''. Your project files (*.tjp and *.tji) is under Subversion control and the taskjuggler user has a checked-out version in ''''/home/taskjuggler/projects/prj''''. You can use another user name, another source code management system and even another operating system like Windows or MacOS. This is all possible, but not the scope of this manual. To use the tracking system, you need to setup the [[Software#tj3d|taskjuggler server]] to serve your project. ==== The Time Sheet Template Sender ==== Each project contributor needs to fill out a time sheet each week. To simplify the process each contributor will receive a template that already contains a lot of the information they need to provide. To send out the time sheets, the command ''''tj3ts_sender'''' must be used. It will call ''''tj3client'''' with appropriate parameters. To use it, you need to have a properly configured daemon running and the appropriate project loaded. Then you need to add the configuration data for ''''tj3ts_sender'''' to your TaskJuggler configuration file. The time sheet related settings have their own top-level section: _global: emailDeliveryMethod: smtp smtpServer: smtp.your_company.com authKey: topsecret scmCommand: "svn add %f ; svn commit -m '%m' %f" projectId: prj _timesheets: senderEmail: 'TaskJuggler ' _sender: hideResource: '~isleaf()' _summary: sheetRecipients: - team@your_company.com digestRecipients: - managers@your_company.com The ''''emailDeliveryMethod'''' defines how emails should be sent. Use ''''smtp'''' to directly send the emails to an SMTP server. The ''''smtpServer'''' defines which host will handle your emails. Replace the host name with your local SMTP server. Alternatively, you can use the method ''''sendmail'''' on UNIX-like systems to pass the email to the sendmail tool. In this case, the ''''smtpServer'''' line can be omitted. The 'scmCommand' setting contains the command to add and commit new and old files to the source code management system. The command in this example works for Subversion. The TaskJuggler server may serve multiple projects. With the ''''projectId'''' option you have to specify which project you would like to work with. ''''senderEmail'''' is the email address the time sheet infrastructure will use. Outgoing emails will have this address as sender so that replies will come back to this email address. We'll cover later how these are processed. The hideResource option works similarly to the [[hideresource]] attribute in the report definitions of the project plan. It allows you to restrict the sending of time sheet templates to a subset of your defined resources. In this example, we only want to send templates to individual resources and not the teams you might have defined. By default the time sheets will cover the week from Monday morning 0:00 to Sunday night 24:00. When called without the ''''-e'''' option, ''''tj3ts_sender'''' will send out templates for the current week. To call the ''''tj3ts_sender'''' command you either need to be in the ''''/home/taskjuggler/projects/prj'''' directory or use the ''''-c'''' command line option to point it to the configuration file to use. In the latter case you also need to call it with the ''''-d'''' option to change the output directory to your project directory. To test the command without sending out actual emails you can use the ''''--dryrun'''' option on the command line. To do its job, ''''tj3ts_sender'''' needs to generate a number of files and directories. A copy of the generated templates will be stored in ''''TimeSheetTemplates//'''' under ''''-date.tji''''. '''''''' is replaced with the end date of the reporting interval and '''''''' is the ID of the resource. If you re-run the command existing templates will not be regenerated nor will they be sent out again. You can use the ''''-f'''' command line option to force them to be generated and sent out again. The ''''tj3ts_sender'''' command will also add the reporting interval to a file called ''''TimeSheetTemplates/acceptable_intervals''''. We'll cover this file later on when we deal with the time sheet receiver. ==== The Time Sheet Receiver ==== To receive the filled-out time sheets and to process them automatically you need to create a special user. TaskJuggler requires a number of email addresses to be setup to receive emails. We recommend to use the following setup. Create a special user called ''''taskjuggler'''' on a dedicated Linux machine. Then create the following email aliases for this user. timesheets timesheet-request statussheets statussheet-request Your MTA must be configured to use procmail for email delivery. See the manual of your MTA for details on how to configure aliases and for using procmail for delivery. If you have a resident MTA expert you should ask him or her for support. The next step is to configure procmail to forward the incoming emails to the appropriate TaskJuggler components. Create a file called ''''.procmailrc'''' in the home directory of the taskjuggler user and put in the following content: For debugging and testing purposes, all incoming emails are archived in a directory called ''''Mail''''. If there is no such directory in the taskjuggler home directory, you need to create it now. PATH=$HOME/bin:/usr/bin:/bin:/usr/local/bin MAILDIR=$HOME/Mail/ DEFAULT=$HOME/Mail/all LOGFILE=$MAILDIR/procmail.log SHELL=/bin/sh PROJECTDIR=/home/taskjuggler/projects/prj LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 # Archive all incoming emails in a file called all :0 c all :0 * ^Subject:.*Out of Office.* /dev/null :0 * ^To:.*timesheets@taskjuggler\.your_company\.com { :0 c: timesheets :0 w: tj3ts_receiver.lock | tj3ts_receiver --silent -c $PROJECTDIR/.taskjugglerrc -d $PROJECTDIR :0 failed_sheets } :0 * ^To:.*timesheet-request@taskjuggler\.your_company\.com { ID=`formail -xSubject:` :0 c: timesheet-request :0 w: tj3ts_sender.lock | tj3ts_sender -r $ID -f --silent -c $PROJECTDIR/.taskjugglerrc -d $PROJECTDIR } :0 * ^To:.*statussheets@taskjuggler\.your_company\.com { :0 c: statussheets :0 w: tj3ss_receiver.lock | tj3ss_receiver --silent -c $PROJECTDIR/.taskjugglerrc -d $PROJECTDIR :0 failed_sheets } :0 * ^To:.*statussheet-request@taskjuggler\.your_company\.com { ID=`formail -xSubject:` :0 c: statussheet-request :0 w: tj3ss_sender.lock | tj3ss_sender -r $ID -f --silent -c $PROJECTDIR/.taskjugglerrc -d $PROJECTDIR } # Forward a copy to project admins :0 c ! taskjuggler-admin@your_company.com # Since we have archived a copy we can discard all mails here. :0 /dev/null This procmail configuration will cause incoming emails that are addressed to timesheets@taskjuggler.your_company.com to be forwarded to the ''''tj3ts_receiver'''' program. Of course you need to replace ''your_company.com'' with whatever domain you are using. The received emails are then checked for syntactical and logical errors. If such are found, an email is sent back with an appropriate error message. The time sheet contains the resource ID of the reporting resource. As soon as this has been detected, all email communication will be sent to the email address in the project plan. Only when the resource ID could not be identified, the sender of the email will get the answer. This was implemented as a security measure so other users cannot easily retrieve project related information from other users. Correct time sheets are archived in the ''''TimeSheets//'''' directory where '''''''' is the end date of the reporting period. If the directory does not exist yet, it will be created. The file will be called ''''-.tji''''. If a SCM command was specified, the file will be automatically put under revision control. Subsequent submission of the same time sheet will simply overwrite the earlier submissions. The file name will also be added to a file called ''''all.tji'''' which consists of include statements of all time sheet files in the directory. There also is an automatically maintained file ''''all.tji'''' in the ''''TimeSheets'''' directory that includes all the ''''/all.tji'''' files. To add all the submitted time sheets to your project plan, simply include the top-level ''''all.tji''''. ''''tj3ts_receiver'''' will only accept time sheets for the time periods listed in ''''TimeSheetTemplates/acceptable_intervals''''. ''''tj3ts_sender'''' will automatically enable the current period when it sends out the templates. If you want to stop receiving time sheet updates for a certain period, simply remove the period from the ''''acceptable_intervals'''' file. ==== Time Sheet Template Requests ==== Normally, the time sheets are sent out once a week automatically. In case a project contributor leaves earlier for vacation or has lost the template, they can request the template for the current week again. By sending an email to ''''timesheet-request@taskjuggler.your_company.com'''' and putting their resource ID in the subject of the email, they will receive an email with the time sheet template. The email will be sent to the email address in the project plan, not the sender of the request email. ==== Time Sheet Summaries ==== All time sheets should be successfully submitted by Sunday 24:00. After this deadline, your can send out a summary of all submitted time sheets. This summary will also contain a list of those project contributors that have not submitted their time sheet. These individuals will also get a reminder to submit their time sheets immediately. To send out the summary report, the program ''''tj3ts_summary'''' is used. Before you can use it, you need to add a few settings to the TaskJuggler configuration file. _global: emailDeliveryMethod: smtp smtpServer: smtp.your_company.com authKey: topsecret projectId: prj _timesheets: senderEmail: 'TaskJuggler ' _summary: sheetRecipients: - team@your_company.com digestRecipients: - managers@your_company.com ''''sheetRecipients'''' is a list of email addresses that should receive a copy of the submitted time sheet. Each email address must be put on a separate, properly indented line that starts with a dash followed by a space. The emails will have the email of the original time sheet author as sender address. ==== Updating the Project Plan ==== The time sheets contain two kind of information that are intended for two sets of audiences. Project managers will be interested primarily in the scheduling related information but surely like to look at the task status as well. Managers responsible for certain parts of the project will be primarily interested in the status reports for the ongoing tasks. We'll cover the processing of the status information in the next sections. This section deals with the processing of the scheduling related information. Project contributors can specify several deviations of the current project plan. * Task may need more effort or time than was originally planned for. * They may have not worked the planned amounts on the tasks. * They may have started to work on new tasks that are not even in the project plan. The is usually a sign of project discipline and should be avoided. But in reality this will happen and TaskJuggler is able to handle it. TaskJuggler can print a summary of all the deltas between the plan and the actual reports in the time sheets. tj3 --warn-ts-deltas YourProject.tjp TimeSheets/all.tji In this example call ''''YourProject.tjp'''' is your main project file and all submitted time sheets are included by TimeSheets/all.tji. This file and all subsequent include files are automatically generated and updated by ''''tj3ts_receiver''''. Project managers should use the printed output of this command to update the project plan accordingly. The specified deltas of existing tasks must be updated in the main project plan. For new tasks in the time sheets, the task has to be created in the project plan. Then the newtask statement in the time sheet needs to be converted into a normal task report. newtask some.task.id "My new task" { ... } Needs to be converted into task some.task.id { ... } The task ID in the status sheet must match the newly created task in the project plan. To check that all deltas were properly processed, re-run the check command. tj3 --warn-ts-deltas YourProject.tjp TimeSheets/all.tji You may also want to remove the interval from the ''''TimeSheetTemplates/acceptable_intervals'''' file to prevent further submissions of time sheets for this time period. === Recording actual Resource Usage === To ensure that future changes won't change the past of the project, we need to freeze the history of the project. History in this context means which resource worked on what task from when to when. Since TaskJuggler cannot know what level of detail you want to include in the reports, this information has to be recorded with the highest possible accuracy. This means that we have to capture the exact start and end dates for every period that a resource worked on a task. Unless you use some external time tracking system to capture this information and export it to TaskJuggler, you probably want TaskJuggler to generate this data for you based on the plan information. Before you can freeze that past part of your project, you need to tell TaskJuggler which scenario should be used for tracking the actual progress. See the [[trackingscenario]] documentation for more details on this. Before you freeze your project for the first time, you should make sure that the current date is still before the project start. If that is not the case, use the [[now]] attribute to set the current date to the project start: now ${projectstart} Once you have frozen the project for the first time, you should remove the [[now]] attribute again. It will be automatically updated. To freeze your project up to a certain date, you can use the following command: tj3 --freeze yourproject.tjp --freezedate YYYY-MM-DD This will generate two files, ''''yourproject-header.tji'''' and ''''yourproject-bookings.tji''''. The header files contains the date of the freeze as a [[now]] attribute. You must [[include.project| include]] this file at the end of your project header section. The bookings file contains the resource assignment data. It usually contains many [[booking.task|booking]] entries that look similar to this: supplement task t { booking r 2010-02-19-09:00-+0000 + 3.0h, 2010-02-19-13:00-+0000 + 5.0h, 2010-02-22-09:00-+0000 + 3.0h, 2010-02-22-13:00-+0000 + 5.0h, } The booking file must be [[include.properties|included]] at the end of your main project file. In case there are still some discrepancies between the booking data and the actual assignments of the resources, you can edit the booking file to correct the data. The next time you run ''''tj3'''' with your project, all assignments prior to the date in the project header file will be taken only from the bookings file. All assignments after this date will be determined by the scheduler according to your provided constraints. When you run ''''tj3 --freeze'''' again, it will update the header and booking files. Since you have included your booking file, any modifications you have made will be preserved. That is, the actual data will be preserved, not the formatting since the file will be completely re-generated again. ==== Status Sheets ==== For larger projects with many contributors the flood of time sheets can become hard to manage. Higher level managers are usually not interested in all the details as long as the project executes according to plan. To keep the managers on each level informed with the proper amount and content TaskJuggler provides the concept of status sheets. To use status sheets, the reporting chains must be reflected in the task hierarchy of the project. The [[responsible]] attribute must be used to assign tasks to managers. Leaf tasks or whole sub trees must be assigned to the lowest level of management. The responsibility for one or more level of parent tasks must be assigned to the next level of managers and so on. When all time sheets have been submitted, the reports for all tasks are sent to the responsible managers for these tasks. The information is generated by the ''''tj3ss_sender'''' program and is called a status report template. Each manager will get one template that includes the status reports for the tasks they are responsible for. It's the managers task to prepare the report for the next level of management. To do this, the manager has 3 options: * Forward the status report of a task directly to the next level. The original authorship can be kept or removed. The content can also be edited if needed. * Similar reports for a task or a whole task sub-tree can be combined into just one report. To achieve this a new task report must be created for the parent tasks of these lower-level tasks. This will replace all reports for sub tasks with this newly created report. * A task report can simply be removed from the status report. The status sheet template is designed to perform all three actions in a simple manner. The original reports are commented out. To remove a report, it needs to be uncommented and the headline must be set to an empty string. To change a report, the text must be edited after the comment marks have been removed. To create a summary report for a group of tasks, a new report for the common parent task must be created. ==== The Status Sheet Template Sender ==== To send out the time sheets, the command ''''tj3ts_sender'''' must be used. It will use the ''''tj3client'''' program to retrieve the necessary data from the TaskJuggler server. Before the program can be used, a new section must be added to the TaskJuggler configuration file. _statussheets: projectId: prj _sender: senderEmail: 'TaskJuggler ' hideResource: '~(isleaf() & manager)' If you are using status sheets for only one level of management you can hardcode that like in the example above. For multiple level of management you need to specify which group of managers should the report templates be generated for and pass that information on the command line. Use the ''''--hideresource'''' option to specify a logical expression to filter away the resources you don't want templates to be generated for. The easiest way to achieve this is by using unique flags for each management level. In the example above we assume you have assigned the flag ''''manager'''' to each first-level manager. For the override mechanism to work, the manager reports must always have a newer date than the original report. So, the end date of the first-level manager status sheets must be after the time sheet interval. The second-level mangers must use a later date than the first-level managers and so on. By default ''''tj3ss_sender'''' will use the next Wednesday as end date. If you need a different date, you must use the ''''-e'''' option to specify that date. Let's say you have two levels of managers that use status sheets. The time sheets are due midnight on Sunday. The project managers can work in the deltas and new tasks on Monday. After that you generate the reports for the first level managers with and end date of Wednesday. This implies a submission deadline of midnight on Tuesday. The second level manager templates will be sent out right after this deadline with an end date of Thursday. That would be the deadline for the second-level managers. The final report can than be generated by TaskJuggler automatically right after that deadline. ==== Requesting Status Sheet Templates ==== Usually the status sheets templates should be send out automatically. But sometimes a manager needs them earlier or needs an updated version due to a late incoming downstream report. The above provided procmail configuration supports the generation of status sheets templates on request by email. By sending an email to statussheet-request@taskjuggler.your_company.com and putting their resource ID in the subject of the email, managers will receive an email with the status sheet template. The email will be sent to the email address in the project plan, not the sender of the request email. The setup described here only works for first-level managers. By adding more email addresses, this can easily be extended for more levels of management. You just need to make sure that ''''tj3ss_sender'''' is called with the proper parameters to change the resource selection and end date. ==== The Status Sheet Receiver ==== Similarly to the time sheets, the completed status sheets must be send back by email. We already described how the necessary email aliases should be configured. For status sheets the address ''''statussheets@taskjuggler.your_company.com'''' can be used. The incoming emails will then be forwarded to the ''''tj3ss_receiver'''' program that will process them. To use it, you first need to add the following settings to the ''''statussheets'''' section of your TaskJuggler configuration file: _statussheets: projectId: prj _receiver: senderEmail: 'TaskJuggler ' This will set the sender email of outgoing emails. Every incoming status sheet will be checked and either an error message will be returned or a consolidated status report for all tasks that the resource is responsible for. This report can either be directly forwarded to the next level manager or interested groups, or an HTML report can be generated and shared. This is especially useful in case the next level management is not getting status sheet templates. Usually status reports only contain task reports for the current reporting period. But if there were tasks with an elevated status, these will be carried forward until they were removed by providing an empty headline or replaced with a new report for the same task or a parent task. TaskJuggler-3.8.1/manual/Getting_Started000066400000000000000000000051431473026623400202460ustar00rootroot00000000000000== Getting Started == === Basics === TaskJuggler uses one or more text files to describe a project. The main project should be placed in a file with the .tjp extension. This main project may include other files. Such included files must have file names with a ''''.tji'''' extension. The graphical user interface from the 2.x version has not been ported to TaskJuggler 3.x yet. So all work with TaskJuggler needs to be done in your favorite text editor and in a command shell. The commandline version of TaskJuggler works like a compiler. You provide the source files, it computes the contents and creates the output files. Let's say you have a project file called ''''AcSo.tjp''''. It contains the tasks of your project and their dependencies. To schedule the project and create report files you have to ask TaskJuggler to process it. tj3 AcSo.tjp TaskJuggler will try to schedule all tasks with the specified conditions and generate the reports that were requested with the [[taskreport]], [[resourcereport]] or other report properties in the input file. The report files will be generated in the current directory or relative to it. If you specify file names in a project file, you need to use the ''''/'''' as directory separator. This way, projects are portable across all operating systems. Do not use the ''''\'''' or '''':'''' that are used on some operating systems. === Structure of a TJP File === Each TaskJuggler project consists of one or more text files. There is always a main project file that may [[include.properties|include]] other files. The main file name should have a ''''.tjp'''' suffix, the included files must have a ''''.tji'''' suffix. Every project must start with a [[project|project header]]. The project header must be in the main project file. All other elements may be put into include files. The project header must then be followed by any number of project properties such as [[account| accounts]], [[resource|resources]], [[task|tasks]] and reports. Each project must have at least one task defined and should have at least one report. Properties don't have to be listed in a particular order, but may have interdependencies that require such an order. If you want to assign a resource to work on a task, this resource needs to be defined first. It is therefore recommended to define them in the following sequence: * [[macro|macros]] * [[flags]] * [[account|accounts]] * [[shift|shifts]] * [[vacation|vacations]] * [[resource|resources]] * [[task|tasks]] * [[accountreport|accountreports]] * [[resourcereport|resourcereports]] * [[taskreport|taskreports]] * [[textreport|textreports]] * [[export|exports]] TaskJuggler-3.8.1/manual/How_To_Contribute000066400000000000000000000160461473026623400205600ustar00rootroot00000000000000=== How to Contribute === ==== Why contribute? ==== TaskJuggler is an Open Source Project. It was developed by volunteers mostly in their spare time. Made available under the GNU General Public license and similar licenses, TaskJuggler can be shared and used free of charge by anybody who respects the license conditions. Does that mean you can use it without worrying about anything? Clearly not! Though users have no legal obligation to contribute, you should feel a moral obligation to support Open Source in whatever way you can. This can range from helping out other users with their first Linux installation to actively contributing to the TaskJuggler Project, not just as a programmer. The following section describes, how you can contribute to any of the components that are part of the TaskJuggler software releases. ==== Preparing a contribution ==== All TaskJuggler development is coordinated using the [https://github.com/taskjuggler/TaskJuggler/issues github source code management platform]. All changes must be submitted using Git github so that we can track the authorship of each submission. There is [http://help.github.com/ excellent documentation] available on how to use github. Make sure you have followed the steps described in the [Installation.html#Installation_Steps_for_Developers Installation Steps for Developers] chapter. If you have never used Git before, you need to configure it first. You need to set your name and email address. This information will be present in all patches that you submit. git config --global user.name "Your Name" git config --global user.email "firstname.lastname@domain.org" Do not use the development snapshots or send your patches as plain diff files. We'd like to ensure that we know who contributed to TaskJuggler. Therefor we are only accepting signed-off git patches with full user names and valid email addresses. Next you need to find the files where you want to make your modifications. Sometimes files will be generated from other files. Do not change those generated files. Your changes will be overwritten the next time you call the make utility. ==== Creating a Patch ==== When you are done with your changes, it's a good idea to test them. In the taskjuggler directory run the following commands. cd taskjuggler3 rake test rake spec rake manual rake gem If there are no errors, you can check or test the result. If everything works fine, you can lock at your changes again. git diff The git-diff utility performs a line-by-line comparison of the files against the latest version in you local repository. Try to only make changes that have an impact on the generated files. Do not change indentation or line wrapping of paragraphs unless absolutely necessary. These kinds of changes increase the size of diff files and make it much harder to evaluate the patches. When making changes to the program code, please use exactly the same coding style as the rest of the code. If your contribution is large enough to justify a copyright claim, please indicate what copyright you claim in the patch. For modifications to existing files, we will assume that your contribution falls under the same license as the modified file. All new files will need to contain a license declaration, preferably GPL version 2. In any case, the license must be [http://www.opensource.org/licenses an OSI accepted license] and be compatible with the GPL version 2 used by the rest of the project. Review all changes carefully. In case you have created new source files, you need to register them with your repository. git add FILENAME If you think you are done, you can commit your changes to your local repository. git commit -a -s The -s parameter is very important. We will only accept signed-off patches. By signing off on your patches you confirm that you wrote the code and have the right to pass it on as a patch. See [http://gerrit.googlecode.com/svn/documentation/2.0/user-signedoffby.html this document] for more information! Please include a meaningful commit message. The first line (header line) should be prefixed by ''''Fix: '''' for bug fixes or ''''New: '''' for new features. This is used to automatically generate the change log from one release to another. So a bug that has been introduced after the last release and is fixed before the next release does not need to be included in the changelog. For those cases, don't use any prefix. After the header line leave a blank line and include one or more paragraphs with more detailed information about the patch. This information will also be included in the the change log if the header line has a prefix. If you fix a bug that was reported by somebody else, please also include a reported-by line: Reported-by: whoever-reported-the-bug Then push it into your forked github repository. git push The final step is to send us a [http://help.github.com/pull-requests/ pull request] for your changes. ==== Contributing to the User Manual ==== The user manual is currently a rough port of the 2.x manual. It contains many inaccuracies and does not provide much more than a tutorial and a syntax reference. Any help to turn this into a real user manual is greatly appreciated. The manual is composed from 3 different sources. # The sources for normal pages are in MediaWiki format and can be found in the ''''manual'''' directory of the source distribution. # The information in the syntax reference is extracted from the TJP parser source code. It can be found in the file ''''lib/TjpSyntaxRules.rb''''. You can ignore all but the ''''doc(...)'''', ''''arg(...)'''' and ''''example(...)'''' sections. # The TJP syntax examples are in the ''''test/TestSuite/Syntax/Correct'''' directory. The following command build the HTML files for the manual in the ''''manual/html'''' directory. rake manual ==== Contributing to the Test Suite ==== The test suite can be found in the ''''test'''' and ''''spec'''' directories. It contains unit and system tests but is very rudimentary at the moment. Adding more system tests to the test/CSV-Report directory is probably the best place to start. Originally, TaskJuggler used classic Ruby unit tests, but a migration to [http://rspec.info/|RSpec-2] is in the works now. New tests should be written as RSpec tests unless they require infrastructure only available in the ''''test'''' directory. ==== Contributing to the Ruby code ==== For the first stable TaskJuggler release we have most 2.x features supported. The few things that break backwards compatibility are documented in the [[TaskJuggler_2x_Migration]] section. In general, patches are very welcome. Please follow the coding style and naming conventions used in the existing code. Larger changes should be preceded by a discussion in the [http://www.taskjuggler.org/contact.html TaskJuggler Developer Forum]. ==== Some final words to Contributors ==== We do welcome all contributions, but please understand that we reserve the right to reject any contribution that does not follow the above guidelines or otherwise conflicts with the goals of the TaskJuggler team. It is a good idea to contact the team prior to making any larger efforts. TaskJuggler-3.8.1/manual/Installation000066400000000000000000000364601473026623400176260ustar00rootroot00000000000000== Installation == TaskJuggler 3.x is written in [http://www.ruby-lang.org Ruby]. It should run on any platform that Ruby is available on. It uses the standard Ruby mechanism for distribution, a package format called [http://docs.rubygems.org RubyGems]. === Requirements === Ruby applications are platform independent. There is no need to compile anything. But TaskJuggler has a very small set of dependencies that you have to take care of first. Please make sure you have the minimum required version installed. ==== Supported Operating Systems ==== * '''Linux''': Linux is the primary development platform for TaskJuggler. Releases are tested on recent openSUSE versions. * '''Other Unix OSes''': Should work as well, but releases are not tested on these OSes. * '''Windows''': Windows7 and some older version of Windows should work. There is no maintainer for this platform, so all releases are not tested on this platform. * '''MacOSX''': Will probably work as well. Releases are not tested on this OS. Older MacOS versions will likely not work. If you are interested in becoming the maintainer for any of the currently unmaintained (and untested) OSes, please contact us via the developer mailing list. ==== Other required Software ==== * '''Ruby:''' TaskJuggler 3.x is written in Ruby. You need a Ruby runtime environment to run it. This can be downloaded from [http://www.ruby-lang.org/en/downloads/ here]. Most Linux distributions usually have Ruby already included. So does MacOS X Leopard. For Windows, there is a one-click installer available. The recommended Ruby version to make full use of TaskJuggler is Ruby 2.0. Ruby 1.9.1 contains some bugs that prevent the multi-core support to work. For users that are not interested in multi-core support, the web server, the time sheet infrastructure and daemon Ruby 1.8.7 is still OK to use. On Windows you need at least Ruby 1.9.2. If you want to use non-ASCII characters, Ruby 1.9.2 or later is required as well. You must have configured your system locale to be UTF-8 to work properly with non-ASCII characters. See below for instructions on how to use the latest and greatest Ruby version in parallel with your distribution Ruby. * '''RubyGems:''' If it did not come with your OS or the Ruby installation, see [http://docs.rubygems.org here] how to get and install it. RubyGems is a cross-platform package manager. It will download and install all other required software packages automatically when you install TaskJuggler. These packages are called Ruby gems. Other versions of Ruby (Rubinius, JRuby, etc.) may work but have not been tested. === Installation Steps for Users === ==== The easy way ==== ===== System Wide Installation ===== TaskJuggler is a command line tool. It does not (yet) have a graphical user interface. To use it, you need to know how to open a command or terminal window. In this manual, we refer to it as your shell. The following paragraphs describe the commands you need to type into your [http://en.wikipedia.org/wiki/Shell_(computing) shell]. On systems that already have Ruby and the gem package manager installed you can simply type the following command as root or admin user into your shell or command window: gem install taskjuggler This will download and install the latest version from the [http://rubygems.org/gems/taskjuggler RubyGems.org] site. ===== Installation into a local Directory ===== If you don't want to install TaskJuggler for all users on the system, you can also install it into your home or data directory. This does not require root or admin permissions. The following steps describe the installation on a Linux system with the bash shell. You may have to use slightly different commands on a different operating system. Create a new directory ''''taskjuggler'''' in your $HOME directory for the installation to go into. mkdir taskjuggler Install the gem and all dependencies. gem install --install-dir taskjuggler taskjuggler-X.X.X.gem If you must use a proxy to access the Internet, you need to set a shell environment variable so ''''gem'''' can find and use it. The setting below needs to be adapted to your local environment. Check with your admin or IT department if needed. export HTTP_PROXY=http://%USER%:%PASSWORD%@%SERVER%:%PORT% Configure your ''''PATH'''' variable to find the taskjuggler programs. export PATH="${PATH}:${HOME}/taskjuggler/bin" Configure gem to find the installed files. export GEM_HOME=${HOME}/taskjuggler The last two settings should also be added to your .profile file to make them permanent. That's it. You now should run TaskJuggler. tj3 --version ==== The manual way ==== If the easy way doesn't work for you, you need to download and install the packages manually. Download TaskJuggler gem file from the [http://rubygems.org/gems/taskjuggler RubyGems.org] site. A gem package is an operating system and architecture independent archive file for Ruby programs. You can install it on any system that has Ruby and RubyGems installed. Normally, you should be logged-in as root or administrator to run the following installation command. Replace the X.X.X with the actual version that you have downloaded. gem install pkg/taskjuggler-X.X.X.gem It will install all components of the Gem in the appropriate place. On user friendly Linux distributions, the start scripts will be installed in a standard directory like ''''/usr/bin''''. On Debian based distributions, the start scripts end up in a place like ''''/var/lib/gems/1.8/bin/'''' that is not listed in the ''''PATH'''' variable. You either have to create a symbolic link for each start script or add the directory to your PATH variable. If you use the standard [http://en.wikipedia.org/wiki/Bash bash shell], put the following line in your ''''${HOME}/.profile'''' file. PATH=${PATH}:/var/lib/gems/1.8/bin/ Windows and MacOS platforms may require similar steps. === Update from older TaskJuggler 3.x versions === Updates work just like the installation. gem update taskjuggler For downloaded or self-built packages use the following command: gem update pkg/taskjuggler-X.X.X.gem === Installing TaskJuggler from the Git Repository === The following description is for developers and users that want to learn more about TaskJuggler or want to make improvements. TaskJuggler is [http://en.wikipedia.org/wiki/Open_source Open Source] software and you are encouraged to read and modify the source code. Before you download the source code, make sure you have all the necessary dependencies installed. You should have Ruby 1.9.2 or later and you need to have the following gems installed gem install rake mail rspec term-ansicolor rcov rcov is optional, but you must have the other gems and their dependencies installed. To get the source code, the recommended way it to check out the latest code from the developer repository. To do this, you need to have [http://www.kernel.org/pub/software/scm/git/docs/ git] installed. Then checkout the source code with the following command git clone git@github.com/taskjuggler/TaskJuggler.git Make sure you have removed all previously installed instances of TaskJuggler from your system before doing so. It is a common mistake to have an old version of the TaskJuggler installed and then use parts of the old and new version together. If your Ruby installation does not come with the [http://rake.rubyforge.org Rake] build tool, you need to install it now. If you are interested in a code coverage analysis, you need to also install the [http://eigenclass.org/hiki.rb?rcov rcov] code coverage analysis tool. This tool is not needed for most developers. You can safely ignore the warning during rake builds if you don't have it installed. The following command will create a gem package from the source code. cd taskjuggler3; rake gem If you plan to modify the TaskJuggler files, creating and installing the gem file for every test run is not very comfortable. To run tj3 from source put the following code in your ''''.profile'''' file. This is for users of the bash shell. Adapt it accordingly if you use another shell. # Make sure the shell finds the TaskJuggler programs export PATH=${PATH}:${TASKJUGGLER_DIR}/bin === Quickly switching between various TaskJuggler 3.x versions === One of the benefits of using TaskJuggler from the Git repository is the ability to get the latest bug fixes. If a bug was reported, it is usually fixed fairly quickly, but it can take several weeks before the next official release happens. The following commands must all be executed from within the checked-out Git directory. git pull gets you the latest changes. We usually try to keep the head branch stable. Using it should not be much more risky than using a regular release. Nevertheless, problems can occur and a fixed version might take a few days. git checkout -f XXXXXXXX will switch your current working copy to the version with commit ID XXXXXXXX. Alternatively, you can also use tag names. git checkout -f release-0.0.10 This will switch to the released version 0.0.10. git tag provides you with a list of all tags. TaskJuggler 3.x is written in Ruby. There is no make or build process needed. Every code change is effective immediately. The tutorial, the manual and some other parts do require a build step. rake release will do it all and even create installable gem files again. === Installing a newer Ruby version === New Ruby versions are released usually about once or twice a year. Unfortunately, it takes some time before Linux distributions pick up the new release. Depending on your distribution, this can take anything from a few weeks to several years. Many distributions still have not yet made the switch to Ruby 2.0. The core part of TaskJuggler can be used with Ruby 1.8.9, but it is at least 3 times slower. Therefor it is recommended, that you install the latest stable release of Ruby to use TaskJuggler. This can easily and safely being done in parallel to your distribution Ruby. Both versions can be used in parallel without interfering each other. This section only covers Linux. For other operating system, please search the web for instructions. If you want to contribute the description for another OS, please see [[How_To_Contribute]]. First, you need to download the source code of the latest stable release from [http://www.ruby-lang.org/en/downloads/ www.ruby-lang.org]. The source code is distributed as zipped tarfile. You can extract it like this. Change the file name to the actual version you have downloaded. tar -Zxvf ruby-X.X.X-*.tar.gz This will create a directory with the same name as the archive, but without the ''''.tar.gz'''' extension. Before you continue, make sure you have all the necessary packages installed to compile ruby. That would be everything you need to compile C programs. That includes gcc, make, zlib and libyaml. If something is missing, you will run into problems in the next 2 steps. It's sometimes not obvious which package to install to fix the issue. Now change into this directory and configure the source code for your specific OS and compile it. We configure Ruby to append ''''19'''' to all executable names. This way, you can easily choose if you want to run the old or the new Ruby. ''''ruby'''' runs your distribution Ruby, ''''ruby19'''' runs your new ruby. cd ruby-X.X.X-* ./configure --program-suffix=19 make If all goes well, you can install it now. This requires root permission, so you need to enter the root password. All executables will be installed into ''''/usr/local/bin''''. sudo make install The TaskJuggler front-end scripts always use the ''''ruby'''' interpreter that's the first in the PATH. You need to set a link in your local ''''bin'''' directory to point to your ''''ruby19'''' executable as ''''ruby''''. ln -s /usr/local/bin/ruby19 ${HOME}/bin/ruby Make sure your ''''${HOME}/bin'''' directory is the first directory in the ''''PATH''''. This step varies a lot depending on the login shell. E. g. for ''''bash'''' put the following at the end in your ''''.profile'''' shell config file. Please make sure that ''''/usr/local/bin'''' is also in the PATH so that the ruby executables (all having a ''''19'''' suffix) will be found as well. export PATH=${HOME}/bin:${PATH} Log out and back in again. Now which ruby should return the path to the link to your ''''${HOME}/bin/ruby''''. You now have the latest Ruby installed and are ready to use TaskJuggler. As a final step, you need to install the ''''mail'''' and ''''term-ansicolor'''' gems. sudo gem19 install mail term-ansicolor If you don't want to use TaskJuggler from the git repository, you can install the TaskJuggler gem as well. sudo gem19 install taskjuggler === Installing the Vim Support === TaskJuggler can be used with any text editor that supports UTF-8 text file editing. If you don't have a preference yet, we recommend to try the [http://www.vim.org Vim] text editor. It's a very powerful editor and it has been customized for better integration with TaskJuggler. This section describes how to activate and use the Vim integration. Vim is provided by pretty much any Linux distribution and also works well on MacOX and Windows. See the web page for how to install it if you don't have it yet. This section describes the integration on Linux. Please see the [[How_To_Contribute]] section if you want to contribute the description for another OS. If you have never customized Vim, you need to create a few directories first. cd ${HOME} mkdir .vim mkdir .vim/syntax Then copy the syntax file ''''tjp.vim'''' into the vim syntax directory. The following command works if you have installed TaskJuggler as a gem with the system provided Ruby. For other cases, you may have to modify it accordingly. cp `gem contents taskjuggler | fgrep tjp.vim` .vim/syntax Now we have to make sure Vim detects the file. Edit the ''''.vim/filetype.vim'''' file to contain the following section. augroup filetypedetect au BufNewFile,BufRead *.tjp,*.tji setf tjp augroup END And edit the ''''.vim/syntax.vim'''' file to contain the following line. au! Syntax tjp so ~/.vim/syntax/tjp.vim When you now open a ''''.tjp'''' or ''''.tji'''' file in Vim, you should have the following features available: * Syntax highlighting. TJP keywords should be colored in different colors. * Syntax folding. The optional parts of properties within the curly braces can be collapsed. For this to work, the opening brace needs to be on the same line as the property keyword. The closing brace must be the first non-blank character of the last line of the block. See the '''':help fold'''' Vim help command for details how to open and close folds. * Tag navigation. If you include a [[tagfile]] report in your project, Vim will know all property IDs and can jump to them. If you have a task with the ID ''''foo.bar'''', the command '''':ta foo.bar'''' will put the cursor right where task ''''foo.bar'''' was declared. * ID completion. If you include a [[tagfile]] report in your project, Vim can tell you the full hierarchical ID of a property. Just move the cursor to the first line of the property definition and press ''''Ctrl-]''''. * Run tj3 from within vim. Just type '''':make your_project.tjp'''' to start the scheduling process. In case of errors or warnings, you will be able to navigate the errors with '''':cn'''' and '''':cp''''. * Move the cursor over any TaskJuggler syntax keyword and press ''''shift-k'''' to get the manual page for this keyword. TaskJuggler-3.8.1/manual/Intro000066400000000000000000000143201473026623400162470ustar00rootroot00000000000000== Introduction == === About TaskJuggler === TaskJuggler is a modern and powerful project management tool. Its new approach to project planning and tracking is far superior to the commonly used Gantt chart editing tools. It has already been successfully used in many projects and scales to projects with hundreds of resources and thousands of tasks. TaskJuggler is an Open Source tool for serious project managers. It covers the complete spectrum of project management tasks from the first idea to the completion of the project without enforcing certain work flows or methodologies. It assists you during project scoping, resource assignment, cost and revenue planning, risk and communication management, status tracking and reporting. TaskJuggler provides an optimizing scheduler that computes your project time lines and resource assignments based on the project outline and the constrains that you have provided. The built-in resource balancer and constrains checker offload you from having to worry about irrelevant details and ring the alarm if the project gets out of hand. The flexible "as many details as necessary"-approach allows you to still plan your project as you go, making it also ideal for new management strategies such as Extreme Programming and Agile Project Management. If you are about to build a skyscraper or just want to put together your colleague's shift plan for the next month, TaskJuggler is the right tool for you. If you just want to draw nice looking Gantt charts to impress your boss or your investors, TaskJuggler might not be right for you. It can certainly produce nice looking Gantt charts and other reports, but it takes some effort to master its power. For those that are willing to invest a few hours to get started with the software it will become a companion you don't want to miss anymore. TaskJuggler is a command line tool that you use from a [http://en.wikipedia.org/wiki/Shell_(computing) shell]. This means that to enter your project data you will use one of the most versatile and powerful tools there is: your favorite [http://en.wikipedia.org/wiki/Text_editor text editor]. To get a first impression, you can look at this [http://www.taskjuggler.org/tj3/examples/Tutorial/tutorial.tjp project file]. The project description is fairly intuitive, but very powerful as well. The [[Tutorial]] will explain this file line by line. Please look at the [http://www.taskjuggler.org/tj3/examples/Overview.html resulting reports] that visualize the project. === License and Copyright === This program is free software; you can redistribute it and/or modify it under the terms of [http://www.gnu.org/licenses/old-licenses/gpl-2.0.html version 2 of the GNU General Public License] as published by the Free Software Foundation. You accept the terms of this license by distributing or using this software. This manual is Copyright (c) 2006, 2007, 2008, 2009, 2010 Chris Schlaeger. Permission is granted to copy, distribute and/or modify this document under the terms of the GNU Free Documentation License, Version 1.3 or any later version published by the Free Software Foundation; with no Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts. A copy of the license is included in the section entitled "GNU Free Documentation License". The HTML reports use icons from the [http://www.kde.org/people/credits/ KDE Icon Team]. The icons are licensed under the [http://www.fsf.org/licenses/lgpl.html GNU Lesser General Public License]. The HTML reports use Java Script code from [http://www.walterzorn.de/en/tooltip/tooltip_e.htm Walter Zorn]. The code is licensed under the [http://www.fsf.org/licenses/lgpl.html GNU Lesser General Public License]. TaskJuggler does require other software components to operate. These components include the Ruby runtime system, operating system libraries and other components installed as Ruby gems. We have used great care to ensure that all dependencies are compatible with the TaskJuggler license and are being used as required by those licenses. But use cases may vary and you should check those licenses yourself to ensure that you use those components in accordance with their licenses. === Features and Highlights === ==== Basic Properties ==== * Manages tasks, resources and accounts of your project * Powerful to-do list management * Detailed reference manual * Simple installation * Runs on all Linux, Unix, Windows, MacOS and several other operating systems * Full integration with Vim text editor ==== Advanced Scheduling ==== * Automatic resource leveling and tasks conflict resolution * Unlimited number of scenarios (baselines) of the same project for what-if analysis * Flexible working hours and leave management * Support for shift working * Multiple time zone support ==== Accounting ==== * Tasks may have initial costs, finishing costs * Resources may have usage based costs * Task and/or resource base cost models * Support for profit/loss analysis ==== Reporting ==== * Comprehensive and flexible reports so you can find the information you need when you need it * Powerful filtering functions to provide the right amount of detail to the right audience * Time and status sheet reporting infrastructure * Project tracking and status reporting with dashboard support ==== Scaling and Enterprise Features ==== * Projects can be combined to larger projects * Support for central resource allocation database * Manages roles and complex reporting lines * Powerful project description language with macro support * Scales well on multi-core or multi-CPU systems * Support for project management teams and revision control systems * Data export to Microsoft Project and Computer Associates Clarity ==== Web Publishing and Groupware Functions ==== * HTML reports for web publishing * CSV data export for exchange with popular office software * iCalendar export for data exchange with calendar and productivity applications * Built-in web server for dynamic and interactive reports * Server based time sheet system for status and actual work reporting === TaskJuggler on the Web === The official TaskJuggler web site can be found at [http://www.taskjuggler.org]. Since the developers are mostly busy project managers themselves, we have created a [http://www.taskjuggler.org/contact.html forum] for users to help each other. TaskJuggler-3.8.1/manual/List_Attributes000066400000000000000000000034331473026623400203000ustar00rootroot00000000000000=== List Attributes === All properties have some optional attributes. Which attributes can be used depends on the type of the property. Attributes can either be single value attributes or they can have multiple values. In the latter case, we call them list attributes. All list attributes are marked as such in the syntax reference. When using an attribute inside a property context, it is important to understand whether it is a list attribute or not. In many cases, the attribute name already indicates that the attribute may have a list of values. E. g. [[flags.task|flags]] or [[limits.task|limits]]. The multiple values of the list attributes can either be specified as a comma separated list or by using multiple attribute statements in the same context. In this example, the task has the flags ''''f1'''', ''''f2'''' and ''''f3'''' assigned. The second ''''flags'''' attribute does not override the first value. It will append the new ones to the old list. <[example file="ListAttributes" tag="define"]> Special care needs to be taken when list attributes are inherited by the enclosing property or by the parent scenario. <[example file="ListAttributes" tag="inherit"]> In this example, task ''''T3'''' has the flags ''''f1'''' and ''''f2'''' assigned. The same works for scenarios as well. Even though the syntax may not look like inheritance is at play, the scenario ''''s2'''' inherits all values from ''''s1''''. <[example file="ListAttributes" tag="scenario"]> In scenario ''''s2'''' the task ''''T4'''' has the flags ''''f1'''' and ''''f2'''' assigned. Sometimes this inheritance is not desired. In these cases, you can explicitly purge the attribute list before assigning new values. Here, task ''''T6'''' only has the flag ''''f2'''' assigned. <[example file="ListAttributes" tag="purge"]> TaskJuggler-3.8.1/manual/Reporting_Bugs000066400000000000000000000040121473026623400201020ustar00rootroot00000000000000=== Reporting Bugs and Feature Requests === All official releases of TaskJuggler are meant to be stable releases unless explicitly noted differently. But our test suite is still very small and some features are not tested at all. So it's very likely that your current version of TaskJuggler contains some bugs. If you find a bug, please follow this procedure: * Read this manual to make sure that it is actually a bug and not a feature. * Check the [http://www.taskjuggler.org TaskJuggler web page] and the [http://www.taskjuggler.org/contact.html discussion groups]. You should also search the [https://github.com/taskjuggler/TaskJuggler/issues issue tracker] if the problem has been reported before. If so, it's quite likely that there is already a workaround or fix available. * Try to create a test project that is as small as possible but still reproduces the bug. * If TaskJuggler has crashed you will usually get some debug output. This may not make any sense to you but it is vital information to analyze the bug. Please include it completely in your bug report. Use the following command to store the messages into a file. tj3 yourproject.tjp 2> error_message * Enter a detailed description of the problem into the [https://github.com/taskjuggler/TaskJuggler/issues?state=open issue tracker] and don't forget to attach your test project and the error log. You can paste that into the input field. * Not attaching a test project will severely limit our abilities to help you. In 95% of the reported bugs we need a test case to find and fix the problem. Please make sure you provide meaningful descriptions and a small but complete test project. Not providing this information with the first bug report means we have to follow up with you or the bug report is being ignored. * Please open a new issue for each bug. Don't include multiple problems in one bug report. This usually leads to one bug being fixed and the rest of the problems to be ignored. * If you have a feature request, please open a new issue and set the "Feature" label. TaskJuggler-3.8.1/manual/Rich_Text_Attributes000066400000000000000000000217361473026623400212640ustar00rootroot00000000000000=== Rich Text Attributes === TaskJuggler supports Rich Text data for some STRING attributes that are marked accordingly in the syntax reference. Rich Text means, that you can use certain markup symbols to structure the text into sections, include headlines, bullet lists and the like. The following sections describe the supported markup elements and how to use them. The markup syntax is mostly compatible to the syntax used by the popular [http://www.mediawiki.org MediaWiki]. ==== Block Markups ==== All block markups are delimited by an empty line. The markup must always start at the beginning of the first line of the block. Block markups cannot be nested. The simplest form of a block is a paragraph. It's a block of text that is separated by empty lines from other blocks. There is no markup needed to start a text block. Headlines can be inserted by using ''''='''' characters to start a line. There are 3 level of headlines. == Headline Level 1 == === Headline Level 2 === ==== Headline Level 3 ==== A line that starts with four dashes creates a horizontal line. ---- Items of a bullet list start with a star. The number of stars determines the bullet list level of the item. Three levels are supported. Bullet items may span multiple lines but cannot contain paragraphs. * Bullet 1 ** Bullet 2 *** Bullet 3 Enumerated lists are formed by using a ''''#'''' instead of ''''*''''. # Enumeration Level 1 ## Enumeration Level 2 ### Enumeration Level 3 Sections of lines that start with a space character are interpreted as pre-formatted text. The formatting will be preserved by using a fixed-width font and by not interpreting any markup characters within the text. Pre-formatted text start with a single space at the start of each line. ==== In-Line Markups ==== In-line markups may occur within a text block. They don't have to start at the start of the line. This is an ''italic'' word. This is a '''bold''' word. This is a ''''monospaced'''' word. This is a '''''italic and bold''''' word. The monospaced format is not part of the original MediaWiki markup, but we found it useful to have for this manual. Text can be colored when enclosed in ''''fcol'''' tags. This is a green word. The following colors are supported: black, maroon, green, olive, navy, purple, teal, silver, gray, red, lime, yellow, blue, fuchsia, aqua and white. Alternatively, a hash sign followed by a 3 or 6 digit hexadecimal number can be used as well. The hexadecimal number specifies the values for the red, green and blue component of the color (i. e., #FFF for white). The above listed in-line markups cannot be nested. Links to external documents are possible as well. In the first form, the URL will appear in the readable text as well. In the second form, the text after the URL will be visible but the link will be available if the output format supports it. [http://www.taskjuggler.org] [http://www.taskjuggler.org The TaskJuggler Web Site] For local references, the second form is available as well. In this form, ''''.html'''' is appended to the first word in the reference to create the URL. [[item]] [[item|An item]] Images can be added with a similar syntax. [[File:image.jpg]] [[File:image.jpg|alt=An image]] This first version will be replaced with the file ''''image.jpg'''' when the output format supports this. Otherwise a blank space will be inserted. The second version inserts the text ''''An image'''' if the output format does not support images. The following image types are supported and detected by their file name extensions: ''''.jpg'''', ''''.gif'''', ''''.png'''' and ''''.svg''''. The vertical positioning of the embedded file can be controlled with additional attributes. [[File:image.svg|text-bottom]] The following attributes are supported: ''''top, middle, bottom, baseline, sub, super, text-top, text-bottom''''. In some situations, it is desirable to not interpret certain markup sequences and reproduce the text verbatim. Such text must be enclosed in nowiki tags. This is not '''bold''' text. You can also insert raw HTML code by enclosing it in '''...''' tags. For all other output formats, this content will be ignored. There is also no error checking whether the code is valid! Use this feature very carefully. ==== Block and Inline Generators ==== Block and inline generators are a very powerful extension that allow you to insert arbitrarily complex content. Block generators create a text block whereas inline generators generate an element that fits inside a text paragraph. Block generators use the following syntax: <[generator_name parameter1="value1" ... ]> Inline generators have a very similar syntax: <-generator_name parameter1="value1" ... -> Each generator is identified by a name. See the following list for supported generators and their functionality. Generators can have one or more optional parameters. Some parameters are mandatory, other are optional. The value of a parameter must be enclosed in single or double quotes. Since your rich text content must already be enclosed by double or single quotes, make sure you don't use the same quoting marks for the parameter value. Alternatively you can put a backslash in front of the quote mark to escape it. ---- '''Block Generator''' ''''navigator'''' Parameters: * ''''id'''' : ID of a defined [[navigator]] The navigator generator inserts the referenced navigator. ---- '''Block Generator''' ''''report'''' Parameters: * ''''id'''' : ID of a defined report The report generator inserts the referenced report as a new block of this text. The referenced report inherits some context such as the report period and the property set from the referencing report. ---- '''Inline Generator''' ''''reportlink'''' Parameters: * ''''id'''' : ID of a defined report * ''''attributes'''': A set of attributes that override the original attributes of the referenced report. All report attributes are supported. Since the values of attributes already must be enclosed by single or double quotes, all single or double quotes contained in the string must be escaped with backslashes. This feature enables reports with content that is customized based on where they have been referenced from. It requires the reports to be dynamically generated and is only available when used with the ''''tj3d'''' web server ''''tj3webd''''. The ''''tj3'''' application will ignore the attributes setting. taskreport "All" { formats html columns name { celltext 1 -8<- <-query attribute="name"-> <-reportlink id="taskRep" attributes="hidetask plan.id != \"<-id->\""-> ->8- }, start, end } taskreport taskRep "Task" { formats html } The report link generator inserts a link to the referenced report. ---- '''Inline Generator''' ''''query'''' Parameters: * ''''family'''' : Specifies whether a ''''task'''' or a ''''resource'''' should be queried. * ''''property'''' : The ID of the task or resource to be queried. If no property is specified and no property is provided by the scope, the query will return a global project attribute. When the query is used with a scope that already provides a property, a sequence of ! can be used to move the property to the parent property. That way you can access attributes of any of the parents. * ''''scopeproperty'''' : The ID of the scope property. If the property is a task this must be a resource ID and vice versa. * ''''attribute'''' : The ID of the attribute which value should be returned by the query. If a property ID is provided, this must be one of the names that can be used as [[columnid]] values. Without a property, global attributes of the project can be requested. The following attributes are supported: ''''copyright'''', ''''currency'''', ''''end'''', ''''name'''', ''''now'''', ''''projectid'''', ''''start'''' and ''''version''''. * ''''scenario'''' : The ID of a scenario. This must be provided whenever the requested attribute is scenario specific. * ''''start'''' : The start date of the report period of the current report. * ''''end'''' : The end date of the report period of the current report. * ''''loadunit'''' : The [[loadunit]] that should be used in case the requested attribute is an effort or duration value. * ''''timeformat'''' : The [[timeformat]] used to format date attributes. * ''''numberformat'''' : The [[numberformat]] used to format arithmetic attributes. * ''''currencyformat'''' : The [[currencyformat]] used to format currency values. The query generator inserts any requested value from the project, a task or a resource. Queries are context aware. Depending on the context where the query is used, certain or all of the above parameters have already predefined values. When used in the header section of a report, the context does not provide a property or scope property. Start and end dates as well the formatting options are taken from the report context. But when used e. g. in [[celltext.column]], the cell provides that property and the attribute and possibly even the scope property. TaskJuggler-3.8.1/manual/Software000066400000000000000000000211761473026623400167550ustar00rootroot00000000000000== The TaskJuggler Software == After the installation of a software package the first question that most users have is ''"How do I run it?"''. Many users expect to find an icon on their desktop or an entry in the start menu hierarchy. Don't bother looking for them, you won't find any for TaskJuggler. As we have mentioned before, TaskJuggler is a command line program. All the components are started from a shell by typing in the command name. This chapter describes the most important TaskJuggler commands and how to use them. If you haven't used command line programs before, don't worry - you will quickly get used to it. === ''''tj3'''' === ''''tj3'''' is the main program that you will probably use the most. It reads in your project files and schedules them. In case there are no errors, it will also generate all the reports that you have defined in your project files. It requires at least one parameter, the name of your main project file. In case your main project file is called ''''tutorial.tjp'''' you can process it by typing tj3 tutorial.tjp You will see an output similar to the one below. > tj3 tutorial.tjp TaskJuggler v3.0.0 - A Project Management Software Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011 by Chris Schlaeger This program is free software; you can redistribute it and/or modify it under the terms of version 2 of the GNU General Public License as published by the Free Software Foundation. Reading file tutorial.tjp [ Done ] Preparing scenario Plan [ Done ] Scheduling scenario Plan [ Done ] Checking scenario Plan [ Done ] Preparing scenario Delayed [ Done ] Scheduling scenario Delayed [ Done ] Checking scenario Delayed [ Done ] Report Overview [ Done ] Report Status [ Done ] Report Development [ Done ] Report Deliveries [ Done ] Report ContactList [ Done ] Report ResourceGraph [ Done ] ''''tj3'''' supports a number of runtime options that control the behavior. Please use the ''''--help'''' option to get more details. tj3 --help === ''''tj3man'''' === ''''tj3man'''' provides you with a quick access to the syntax reference. If you run it without further parameters it will print a list of all syntax keywords. In some cases, keywords can be used in different contexts with different meanings. These keywords are listed multiple times with the context identifier appended separated by a dot. E. g. the ''''shift'''' keyword can be used in the global context, in the [[resource]], [[task]] and [[timesheet]] context. It will be listed as shift shift.resource shift.task shift.timesheet To get more information about a specific keyword you need to provide it as parameter to ''''tj3man''''. tj3man shift.task The same information that is available in your shell is also available in your web browser. tj3man --html The latter defaults to using the [http://www.mozilla.com/en-US/firefox/new/ Mozilla Firefox] web browser. Please see tj3man --help how to use a different browser. If you omit the keyword, you will get this user manual (browser). === ''''tj3d'''' === ''''tj3d'''' is the TaskJuggler daemon. It is a program that runs in the background, disconnected from your shell to provide certain services. It can generate reports on demand and processes incoming time sheets or status reports. Depending on the size of your project the scheduling time can take several minutes or more. Since all operations need to be done on the data of a scheduled project, it makes sense to have this data readily available. This is the job of the TaskJuggler server or daemon in Linux lingo. The program is called ''''tj3d''''. When started, it automatically disconnects from the terminal and runs in the background. All interactions with the server are done via the TCP/IP protocol. For security reasons, only connections from the same machine (localhost) are accepted. To get access, all clients must provide an authentication key. A TaskJuggler server can serve any number of projects. Once a project has been loaded successfully, clients can retrieve the data in form of reports. Projects are identified by their project ID. If a newly added project has the same ID as an already loaded project, the new project will replace the old project once it was scheduled successfully. Before you start the server, you need to provide a configuration file with some basic settings. All taskjuggler components can use the same TaskJuggler configuration file. The format is a simple plain text format that follows the [http://www.yaml.org/ YAML specification]. The file should be called ''''.taskjugglerrc'''' or ''''taskjuggler.rc''''. The settings are structured by sections. Section names always start with an underscore. _global: authKey: topsecret _log: logLevel: 3 outputLevel: 3 This file sets the authentication key for all TaskJuggler components. You must replace ''topsecret'' with your own random character sequence. For the purpose of this documentation we assume you have a local user called ''taskjuggler'' and your project data in ''''/home/taskjuggler/project/prj''''. Your TaskJuggler configuration should then be put into ''''/home/taskjuggler/project/prj/.taskjugglerrc''''. The log section controls the content of the log file. Since the daemon does not have a terminal attached to it, all messages are stored in a file called ''''tj3d.log''''. For debugging purposes, you can use the ''''-d'''' option to prevent the daemon from disconnecting from the terminal. In this case the ''''outputLevel'''' configuration option controls the amount of details to be printed out. * 0: No output * 1: Only fatal errors * 2: Fatal and normal errors * 3: Like 2, but additionally with information messages * 4: Like 3, but additionally with debug messages The configuration file will be searched for in the current directory, the current user's home directory or ''''/etc''''. You can also explicitly tell the server where to find the configuration file with the ''''-c'''' option. See tj3d --help for details. So far, the daemon has not received any kind of security review. We strongly advise you to only use the daemon in a trusted environment with only trusted users! === ''''tj3client'''' === To control the TaskJuggler server, you need to use the TaskJuggler client. You can use the client to add or remove projects from the server, inquire the status of loaded projects. It can also be used to show the available reports for each project and to generate report or check time or status sheets. The client must provide the correct authentication key to the server. You need to ensure that it can find the proper configuration file with the authentication key. tj3client --help will provide a full list of supported commands. To load a project simply type tj3client add yourproject.tjp In case there were no errors tj3client status should now list your project. tj3client list-reports shows a list of available reports for the project with the provided ID. To generate a report, you can type tj3client report A server that is running can be terminated with the following command. tj3client terminate === ''''tj3webd'''' === This is a web server to serve the HTML reports of a project to any web browser. The HTML reports are generated on the fly when accessed. ''''tj3webd'''' requires that ''''tj3d'''' is already running on the same machine. By default, the web server is listening on port 8080. This can be changed in the ''''_global'''' section of the config file. _global: authKey: topsecret webServerPort: 8080 To access the HTML reports, point your web browser to ''''http://localhost:8080/taskjuggler''''. This assumes that the server is running on your local machine. You will then see a list of all loaded projects. Click on the project name to get a list of all reports for this project. ''WARNING: Please be aware that the web server when you have started it can be accessed by anybody on your local machine and by anybody that can reach your machine over the network! It will serve all reports of all projects that are hosted by the TaskJuggler daemon.'' TaskJuggler-3.8.1/manual/TaskJuggler_2x_Migration000066400000000000000000000121411473026623400220170ustar00rootroot00000000000000=== TaskJuggler 2.x Migration === This section will cover changes between TaskJuggler 2.x and 3.x. * The syntax for macros has changed slightly. The terminating '''']'''' must be the last character before the line break. No spaces or comments are allowed here. Parameters of macro calls must always be enclosed by double quotes. In contrast to regular strings, single quotes are not allowed here. The parameter may not span multiple lines. * The ''''projection'''' attribute has been removed. The is now provided by [[trackingscenario]]. * The default working hours have been changed to 9:00 - 17:00. * IDs for properties such as tasks, resources and reports are now optional. If you don't need to reference a property, you can omit the ID. TaskJuggler will automatically assign an ID then. * Top-level accounts no longer need a ''''cost'''' or ''''revenue'''' attribute. Any two top level accounts can now be balanced against each other using the [[balance]] attribute in the report. * The ''''shift'''' attribute for tasks and resources has been renamed to ''''shifts'''' to allow support for multiple shifts. * The global ''''limits'''' attribute has been removed. Since both tasks and resources have a ''''limits'''' attribute, a global attribute was inconsistent as only resources inherited this attribute. Use a parent resource to emulate the old behavior. * Shifts and limits for allocations have been deprecated. The concept was bogus and not compatible with bookings. The functionality is now provided by [[shifts.task|shifts]] and [[limits.task|limits]] on the task level. Limits for a task can be [[resources.limit|selectively applied]] to certain resources. * The ''''startbuffer'''' and ''''endbuffer'''' attributes have been deprecated. They have not been widely used and had no impact on scheduling. * The project attribute ''''allowredifinitions'''' has been dropped. It was an ugly workaround for a rare corner case. Using [[supplement]] is the clean way to do this. * Camel case names for function names in logical expressions have been deprecated. Function names need to be all lower case now. Some functions have been removed as all attributes can now be accessed by scenario.attribute_id notation. * The format for report has been changed considerably. The old format was not very flexible and had some design flaws. TaskJuggler 3.x now supports report nesting and composition. A report definition can be used to generated multiple output [[formats]]. The name of a report must now be specified without the file name extension. It will be automatically added depending on the output format. * The sorting modes have been extended to include the scenario. Also, the sorting direction is no longer mangled with the attribute name. What used to be ''''startup'''' is now ''''plan.start.up''''. See [[sorttasks]] or [[sortresources]] for details. * The attribute ''''properties'''' for ''''export'''' reports is no longer supported. The naming was inconsistent with TaskJuggler lingo and did not handle tasks and resources separately. It has been replaced with [[taskattributes]] and [[resourceattributes]]. * The ''''barlabels'''' attribute for reports is no longer needed. HTML reports have always empty Gantt-chart bars and the calendar reports always have values. * Support for reading and writing XML files is no longer available. The content was redundant with the TJP file format and it was not widely used. Keeping it in sync was too much of an effort to be worth it. There is nothing in the TJ3 design that would prevent this feature from being added again, but there are no plans for this right now. === Using TaskJuggler 2.x and TaskJuggler 3.x in parallel === While TaskJuggler 3.x has many new features over TaskJuggler 2.x like the much improved HTML reports, many 2.x users will miss the graphical user interface. To ease the migration, you can continue to use the TaskJuggler 2.x front-end while using TaskJuggler 3.x for report generation. This is possible because TaskJuggler 3.x can read-in the TaskJuggler 2.x export files. Export files are fully scheduled projects that include start and end dates for all tasks and bookings for resource allocations. To export all tasks and resources into a TJP file that can be read by TaskJuggler 3.x include the following export report definition in your TaskJuggler 2.x project plan. The necessary patches to support this only made it into TaskJuggler 2.x after the 2.4.3 release. So be sure to use a recent version from the Git repository to try this. export "FullProject.tjp" { taskattributes all resourceattributes all hideresource 0 } The resulting ''''FullProject.tjp'''' file is a valid self-contained project file that can be read with TaskJuggler 2.x or TaskJuggler 3.x. The file does not contain any report definitions. To generate reports with TaskJuggler 3.x you need to create an additional file that contains the TaskJuggler 3.x report definitions. Let's assume the file is called ''''tj3reports.tji''''. Start TaskJuggler 3.x with the following command: tj3 FullProject.tjp tj3reports.tji Now you have generated TaskJuggler 3.x reports from you TaskJuggler 2.x project. TaskJuggler-3.8.1/manual/TaskJuggler_Internals000066400000000000000000000167041473026623400214250ustar00rootroot00000000000000== TaskJuggler Internals == This chapter contains information that you don't need to know to use TaskJuggler. It describes internal algorithms that are provided for the curious. === How the Scheduler works === It's important to understand that the scheduler implementation is not an optimization algorithm. It does not search a solution space and evaluates various alternative results against each other. This has been tried, but for any real-world project, the solution space becomes unmanageable and scheduling runs took hours to complete. Instead, we use a heuristic to decide when each task gets its resources assigned. This heuristic is certainly not perfect but has shown good results with fairly moderate computation costs. The following sections contain an overview of the scheduling algorithm. Users are also encouraged to read the actual source code. It can be found in ''''Project.rb'''', ''''TaskScenario.rb'''', ''''ResourceScenario.rb'''' and ''''Allocation.rb''''. All these files can be found in the ''''lib/taskjuggler'''' directory. You can also browse the sources on [https://github.com/taskjuggler/TaskJuggler github]. The scheduler needs to determine the start and end date for all tasks that don't have such dates yet. To deal with multiple concurrent time zones, all time related events are stored internally as UTC time. Additionally, it allocates resources to tasks. All events such as start or end of a task, or allocation of a resource can only happen aligned with the [[timingresolution|timing resolution]]. This determines the smallest possible allocation period that we call a time slot. The duration of the slot can be set by the user. Possible values are 5, 10, 15, 30 and 60 minutes. TaskJuggler keeps a scoreboard for each time slot for each leaf resource. Each scoreboard entry specifies whether the resource is unassigned, assigned to a specific task or on leave. This explains why the project duration and number of allocated resources determines the memory usage of the scheduler. For the scheduling of the project, the scheduler only looks at leave tasks that are not milestones. Container tasks and milestones are scheduled once all necessary information is available. During the scheduling process, leave tasks can have 3 different states. # ''''Not ready for scheduling'''': The task is missing a start or end date that depends on another task's date that hasn't been determined yet. # ''''Ready for scheduling'''': The task has at least a start or end date but one of them is still missing or resources have not yet been assigned for all time slots. The scheduling direction (ASAP or ALAP) determines whether the start or end date is needed. ASAP tasks are scheduled from start to end, so they require a start date. ALAP tasks are scheduled from end to start. # ''''Scheduling completed'''': The task has a start and end date and resources have been assigned for all time slots. The goal of the scheduler is to transfer all tasks in the completed state. Until this goal has been reached, at least one task needs to be in the ready state. If that's not the case, the project schedule cannot be determined and an error is raised. In case there are more than one task in the ready state, we need to have a well defined priority of the tasks. This is necessary since those ready tasks may compete for the same resource in the same time slot. The priority can be directly influenced by the user with the [[priority]] attribute. This user-defined priority always trumps the other internal criteria described below. In case two tasks have the same priority, an additional measure is used. This measure is called path criticalness. The path criticalness is calculated for each leaf task. The path criticalness is a measure for how important the task is to keep the overall project duration (start of first task to end of last task) to a minimum. To determine the path criticalness, we first need to determine the resource criticalness first. This is a measure for how likely the tasks that have this resource in their allocation list will actually get the resource. A resource criticalness larger than 1.0 means that statistically, at least one tasks will not get enough of this resource. This is just a statistical measure based on the total requested allocations and the number of available work time. Once we have determined the criticalness of all allocated resources, we can calculate the criticalness of each individual task. This really only matters for [[effort]] based tasks. These really need their allocations to be finished within a limited amount of time. For [[length]] and [[duration]] tasks, the allocations are by definition optional. The user can still influence the allocation to length and duration tasks by adjusting the priority appropriately. However, there is no guarantee that such tasks will ever get any resources assigned. The criticalness of an effort based task is defined as the average of the criticalness of the resources allocated to this task. We also assign a criticalness to milestones. Based on their priority a criticalness between 0 and 2.0 is assigned. This is done to reflect the user perception that milestones are usually some important goal of the project. The final step is now the computation of the path criticalness for each effort-based leaf task. For each possible chain of task (path) that is going through a task, the sum of the criticalness values of the tasks of the path is computed. The largest sum is the path criticalness of that task. This heuristic will favor allocations to tasks with critical resources and long dependency chains. As a result, the critical paths of the project are tried to be kept short. The user can use the '''criticalness''' and '''pathcriticalness''' [[columnid|columns]] to review the respective values for he project's tasks and resources. When the criticalness and pathcriticalness for all leaf resources and tasks has been determined, the leaf tasks are sorted by priority (high to low), then by pathcricialness (high to low) and then by the index (low to high). In a loop that is terminated when all tasks have been scheduled or an error condition has been detected, the first task that is ready for scheduling is completely scheduled. This means that resources are allocated for all time slots and missing dates are being computed. The newly determined end (for ASAP tasks) or start (for ALAP tasks) date is then propagated to dependent tasks, milestones or parent tasks if needed. This can result in other tasks becoming ready for scheduling and the list is searched again for the first task that is ready for scheduling to be scheduled. A task can only be scheduled when it is ready for scheduling. This means that at least the start date for the scheduling process must be known. For ASAP (As Soon As Possible) tasks, the scheduler start date is the start date of the task. For ALAP (As Late As Possible) tasks the scheduler start date is the end date of the task. ASAP task will be scheduled from start to end, ALAP tasks from end to start. This is important to understand as resource assignments for each time slot will be determined in this order. Mixing ASAP and ALAP tasks in the same project is supported but should be used very carefully. It can lead to situations where a lower priority tasks that is earlier in the scheduling process ready for scheduling takes away resources from a higher prioritized task that competes for the same resources. This condition is known is priority inversion. If the scheduler detects such a situation, a warning is generated. TaskJuggler-3.8.1/manual/The_TaskJuggler_Syntax000066400000000000000000000120641473026623400215470ustar00rootroot00000000000000== The TaskJuggler Syntax == === Understanding the Syntax Reference === This manual provides a comprehensive reference of the TaskJuggler syntax. It is automatically generated from the same data that the parser uses to read your project files. This ensures that the software and the manual always match. The syntax reference is organized based on the keywords of the TaskJuggler syntax. There is an entry for every keyword. In some cases, a keyword can appear in different contexts with potentially different meanings. In this case, the context is provided in brackets after the keyword for at least one of them. That way, every keyword is uniquely referenced. The syntax for each keyword is described in the syntax section of the table that is provided for each keyword. The syntax always starts with the keyword. Keywords are then followed by arguments. In some cases, an argument can be automatically generated. In such cases, the argument is enclosed in square brackets. keyword [] Arguments can be variable or picked from a predefined list of options. Variable arguments are listed with their names enclosed in angle brackets. keyword List of predefined options are enclosed in brackets, the options separated by a vertical bar. keyword ( foo | bar | foobar ) Some keywords take one or more arguments. These are known as list attributes. The arguments are comma separated. The three dots in the syntax description mean that the sequence before the dots can be repeated if needed. Inheritable list attributes will append the new list values to the inherited list. Use can use the [[purge]] attribute to clear the list before assigning new values to the list attribute. keyword arg1 [, arg2 ... ] Variable arguments are further described in the ''Arguments'' section of the keyword syntax table. The name is listed immediately followed by the type of the variable argument. The supported types and their meaning is described in the following sections. ==== ABSOLUTE_ID ==== An absolute identifier is composed of identifiers that are concatenated by dots, e. g. ''''foo.bar''''. It is used to reference a TaskJuggler property that lives in a hierarchical name space. Accounts, Tasks, Reports are examples for such hierarchical name spaces. To reference the sub-task ''''bar'''' of task ''''foo'''' the absolute ID ''''foo.bar'''' is used. ==== ID ==== An identifier is composed of the letters ''''a'''' to ''''z'''', ''''A'''' to ''''Z'''', the underscore and the digits ''''0'''' to ''''9''''. There are no limits for the number of characters, but it may not begin with a digit. ==== INTEGER ==== An integer is any natural number, e. g. ''''0'''', ''''1'''', ''''2'''' and so on. ==== STRING ==== Strings are character sequences that are enclosed by special character marks. There are three different marks supported. For short strings that fit on one line, you can either use single or double quotes. 'This is a single quoted string.' "This is a double quoted string." Single quoted strings may contain double quotes and vice versa. Alternatively, you can prefix the quote mark with a backslash to use it within a string. 'It\'s a string with a quote included.' If you want to use a backslash right before the included quote you need to escape the backslash with another backslash. Backslashes that aren't followed by a quote mark don't need to be escaped. 'A backslash \\\' followed by a quote.' For multi-line strings or strings with many included quotes cut mark strings are recommended. A cut-mark-string starts with ''''-8<-'''' (scissor on a dotted line) and ends with ''''->8-''''(scissors looking the other way). The start mark must be immediately followed by a line break. The indentation of the first line after the opening scissors must be repeated for all following lines of the string and is not included in the resulting strings. The terminating cut mark must only be preceded by white spaces in that line. When considering the indentation, tabs are not identical with some number of spaces. Each indented line must be prefixed by the exact same combination of tabs and spaces. -8<- This is a multi-line string. ->8- === Predefined Macros === TaskJuggler supports a few predefined macros. These are available after the project header. Their values correspond to the values provided in the project header. * ''''projectstart'''' The start date of the project. * ''''projectend'''' The end date of the project. * ''''now'''' The current date. If the user does not provide a date with the [[now]] keyword, the moment of the processing of the file will be inserted. Keep in mind that this will change every time you process your project. The current setting of [[timeformat]] has no impact on the expansion of the macro. * ''''today'''' Identical to ''''now'''' but formatted according to the [[timeformat]] setting of the current context. === Environment Variable Expansions === By using the $(VAR) syntax, you can insert the value of the environment variable name VAR. The name of the variable must consist only of uppercase ASCII letters, underscores or decimal digits. TaskJuggler-3.8.1/manual/Tutorial000066400000000000000000001017311473026623400167620ustar00rootroot00000000000000== The Tutorial: Your first Project == We have mentioned already that TaskJuggler uses plain text files that capture the known parts of the project. As you will see now, the syntax of these files is easy to understand and very intuitive. This chapter will walk you step by step through your first project. You create the project plan for a made-up accounting software project. This project demonstrates most of the commonly used features of TaskJuggler. It also includes some of the more advanced concepts that you may or may not need for your projects. Don't get scared by them. You can use them once you are more familiar with TaskJuggler and your projects grow larger. The complete tutorial example comes with your TaskJuggler software installation. You can use the following command to find the base directory of the example projects. ruby -e "puts Gem::Specification.find_by_name('taskjuggler').gem_dir" The file for the tutorial project is called ''''examples/Tutorial/tutorial.tjp''''. You can use any plain text editor to view and modify it. === Starting the project === Every TaskJuggler project file must start with the [[project]] property. It tells TaskJuggler the name of your project and a start and end date. The start and end dates don't need to be exact, but must fit all tasks of the project. It is the time interval the TaskJuggler scheduler will use to fit the tasks in. So, make it large enough for all your tasks to fit in. But don't make it too large, because this will result in longer scheduling times and higher memory consumption. <[example file="tutorial" tag="header1"]> All TaskJuggler properties have a unique ID, a name, and a set of optional attributes. The name must always be specified. The ID can be omitted if you never have to reference the property from another context. If you omit the ID, TaskJuggler will automatically generate a unique ID. The optional attributes are always enclosed in curly braces. If no optional attributes are specified, the braces can be omitted as well. In this example we will introduce a number of the attributes that may or may not matter for your specific projects. If you don't see an immediate need for a specific attribute, feel free to ignore it for now. You can always come back to them later. A full list of the supported project attributes can be found in the ''attributes'' section of the [[project]] property documentation. Attributes always start with a keyword that identifies them. The meaning and parameters of attributes depends on the property context that they are used in. A context is delimited by a set of curly braces that enclose optional attributes of a property. The area outside of any property is called the global scope. Usually, attributes have one or more arguments. These arguments can be dates, character strings, numbers or symbols. Strings must be enclosed in single or double quotes. The argument types and meaning is explained for each keyword in the syntax reference section of this manual. TaskJuggler manages all events with an accuracy of up to 15 minutes. In many cases, you don't care about this level of accuracy. Nevertheless, it's good to have it when you need it. All dates can optionally be extended by a time. By default, TaskJuggler assumes that all times are UTC (world time) times. If you prefer a different time zone, you need to use the [[timezone]] attribute. <[example file="tutorial" tag="timezone"]> Be aware that the project start and end dates in the project header are specified before you specify the time zone. The project header dates are always assumed to be UTC unless you specify differently. See [[interval2]] for details. project acso "Accounting Software" 2002-01-16-0:00-+0100 The [[currency]] attribute specifies the unit of all currency values. <[example file="tutorial" tag="currency"]> Because each culture has its own way of specifying dates and numbers, the format for these is configurable. Use the [[timeformat]] attribute to specify the default format for dates. This format is used for reports, it does not affect the way you specify dates in the project files. Here you always need to use the [[date|TaskJuggler date notation]]. <[example file="tutorial" tag="formats"]> We also can specify the way numbers or currency values are shown in the reports. Use the [[numberformat]] and [[currencyformat]] attributes for this. The attribute [[now]] is used to set the current day for the scheduler to another value than to the moment your invoke TaskJuggler. If this attribute is not present, TaskJuggler will use the current moment of time to determine where you are with your tasks. To get a defined result for the reports in this example we've picked a specific date that fits our purpose here. In your projects, you would use [[now]] to generate status reports for the date you specify. <[example file="tutorial" tag="now"]> In this tutorial we would like to compare two scenarios of the project. The first scenario is the one that we have planned. The second scenario is how it really happened. The two scenarios have the same task structure, but the start and end dates and other attributes of the task that are scenario specific may vary. In this example we assume that the project got delayed and use a second scenario that we name "Delayed" to describe the actual project. The scenario property is used to specify the scenarios. The delayed scenario is nested into the plan scenario. This tells TaskJuggler to use all values from the plan scenario also for the second scenario unless the second scenario has it's own values. This is a very easy but also powerful way to analyze the impact of certain changes to the plan of record. We'll see further below, how to specify values for a scenario and how to compare the results. <[example file="tutorial" tag="scenario"]> To summarize the above, let's look at the complete header again. Don't get scared by the wealth of attributes here. They are all optional and mostly used to illustrate the flexibility of TaskJuggler. <[example file="tutorial" tag="header2"]> === Global Attributes === For this tutorial, we also like to do a simple profit and loss analysis of the project. We will track labor cost versus customer payments. To calculate the labor costs we have to specify the default daily costs of an employee. This can be changed for certain employees later, but it illustrates an important concept of TaskJuggler – inheritance of attributes. In order to reduce the size of the TaskJuggler project file to a readable minimum, properties inherit many attributes from their enclosing scopes. We'll see further below, what this actually means. Right after the project property we are at top-level scope, so this is the default for all following properties. <[example file="tutorial" tag="rate"]> The [[rate]] attribute can be used to specify the daily costs of resources. All subsequently declared resources will get this rate. But it can certainly be changed to a different rate at group or individual resource level. You may also want to tell TaskJuggler about holidays that affect all resources. Global holidays are time periods where TaskJuggler does not do any resource assignments to tasks. <[example file="tutorial" tag="vacation"]> Use the [[leaves]] attribute to define a global holiday. Global holidays may have a name and must have a date or date range. Other leaves for individual resources or groups of resources can be defined similarly. === Macros === Macros are another TaskJuggler feature to save you typing work and to keep project files small and maintainable. Macros are text patterns that can be defined once and inserted multiple times in the project file. A [[macro]] always has a name and the text pattern is enclosed by square brackets. <[example file="tutorial" tag="macro"]> To use the macro you simply have to write ''''${allocate_developers}'''' and TaskJuggler will replace the term ''''${allocate_developers}'''' with the pattern. We will use this macro further below in the example and then explain the meaning of the pattern. === Declaring Flags === A TaskJuggler feature that you will probably make heavy use of is flags. Once declared you can attach them to any property. When you generate reports of the TaskJuggler results, you can use the flags to filter out unwanted properties and limit the report to exactly those details that you want to have included. <[example file="tutorial" tag="flags"]> This is a [[flags]] declaration. All flags need to be declared before they can be used to avoid hard to find errors due to misspelled flag names. The flags should be declared before any property at global scope. We will see further down, how we can make use of these flags. === Declaring Accounts === The use of our resources will generate costs. For a profit and loss analysis, we need to balance the cost against the customer payments. In order not to get lost with all the various amounts, we declare 3 [[account|accounts]] to credit the amounts to. We create one account for the development costs, one for the documentation costs, and one for the customer payments. Actually, there is a fourth account consisting of two accounts nested into it. <[example file="tutorial" tag="accounts"]> The account needs an ID and a name. IDs may only consist of the characters a to z, A to Z and the underscore. All but the first character may also be digits 0 to 9. The ID is necessary so that we can reference the property again later without having to write the potentially much longer name. The name may contain space characters and therefore has to be enclosed with single or double quotes. Accounts can be grouped by nesting them. You can use this feature to create sets of accounts. Such sets can then be balanced against each other to create a profit and loss analysis. When you have specified accounts in your project, you must at least define one default [[balance]]. <[example file="tutorial" tag="balance"]> === Declaring Resources === While the above introduced account property is only needed if you want to do a P&L analysis, resources are usually found in almost any project. <[example file="tutorial" tag="resources"]> This snippet of the example shows the use of the [[resource| resource property]]. Just like accounts, resources should have an ID and must have a name. Resource IDs, like account IDs must also be unique within their property class. As you can see, resource properties can be nested: ''''dev'''' is a group or container resource, a team that consists of three other resources. ''''dev1'''', alias Paul Smith, costs more than the normal employee. So the declaration of ''''dev1'''' overwrites the inherited default rate with a higher value. The default value has been inherited from the enclosing scope, resource ''''dev'''', which in turn has inherited it from the global scope. The declaration of the resource Klaus Müller uses another optional attribute. Attributes are only inherited from the parent property if the attribute was declared in the parent property before the child property declaration was started. The syntax reference lists for each property whether an attribute is inherited from the parent or the attribute in the global scope. With [[leaves]] you can specify certain time intervals where the resource is not available. Leaves are list attributes. They accumulate the declarations. If you want to get rid of inherited or previously assigned values, you can use the [[purge]] attribute to clear the list. ''''leaves'''' requires a time interval. It is important to understand how TaskJuggler handles time intervals. Internally, TaskJuggler uses the number of seconds after January 1st, 1970 to store any date. So all dates are actually stored with an accuracy of 1 second in UTC time. ''''2002-02-01'''' specifies midnight February 1st, 2002. Following the TaskJuggler concept of requiring as little information as necessary and extending the rest with sensible defaults, TaskJuggler adds the time 0:00:00 if nothing else has been specified. So the vacation ends on midnight February 5th, 2002. Well, almost. Every time you specify a time interval, the end date is not included in the interval. So Klaus Müller's vacation ends exactly at 0:00:00 on February 5th, 2002. February 5 is not part of the leave! Peter Murphy only works 6.4 hours a day. So we use the [[limits.resource|limits]] attribute to limit his daily working hours. We could also define exact working hours using the [[shift|shift property]], but we ignore this for now. Note that we have attached the flag ''''team'''' after the declaration of the sub-resources to the team resources. This way, these flags don't get passed down to the sub-resources. If we would have declared the flags before the sub-resources, then they would have the flags attached as well. === Specifying the Tasks === Let's focus on the real work now. The project should solve a problem: the creation of an accounting software. Because the job is quite complicated, we break it down into several subtasks. We need to do a specification, develop the software, test the software, and write a manual. Using the [[task|task property]], this would look as follows: <[example file="tutorial" tag="task1"]> Similar to resources, tasks are declared by using the task keyword followed by an ID and a name string. All TaskJuggler properties have their own namespaces. This means, that it is quite OK to have a resource and a task with the same ID. Tasks may have optional attributes which can be tasks again, so tasks can be nested. In contrast to all other TaskJuggler properties, task IDs inherit the ID of the enclosing task as a prefix to the ID. The full ID of the spec task is AcSo.spec. You need to use this absolute ID when you want to reference the task later on. This hierarchical name space for tasks was chosen to support large projects where multiple project managers may use the same ID in different sub tasks. To track important milestones of the project, we also added a task called Milestones. This task, like most of the other tasks will get some subtasks later on. We consider the specification task simple enough, so we don't have to break it into further subtasks. So let's add some more details to it. <[example file="tutorial" tag="spec"]> The [[effort]] to complete the task is specified with 20 man-days. Alternatively we could have used the [[length]] attribute or the [[duration]] attribute. ''''length'''' specifies the duration of the task in working days while ''''duration'''' specifies the duration in calendar days. Contrary to ''''effort'''', these two don't have to have a specification of the involved resources. Since ''''effort'''' specifies the duration in man-days, we need to say who should be allocated to the task. The task won't finish before the resources could be allocated long enough to reach the specified effort. Tasks with ''''length'''' or ''''duration'''' criteria and allocated resources will last exactly as long as requested. Resources will be allocated only if available. It's possible that such a task ends up with no allocations at all if the resources are always assigned to other tasks for that period. Each task can only have one of the three duration criteria. Container tasks may never have a duration specification. They are automatically adjusted to fit all sub tasks. Here we use the allocate_developers macro mentioned above. The expression ''''${allocate_developers}'''' is simply expanded to <[example file="tutorial" tag="expandedmacro"]> If you need to [[allocate]] the same bunch of people to several tasks, the macro saves you some typing. You could have written the allocate attributes directly instead of using the macro. Since the allocation of multiple resources to a task is a good place for macro usage, we found it a good idea to use it in this example as well. For TaskJuggler to schedule a task, it needs to know either the start and end criteria of a task, or one of them and a duration specification. The start and end criteria can either be fixed dates or relative dates. Relative dates are specifications of the type ''task B starts after task A has finished''. Or in other words, task B depends on task A. In this example the spec task depends on a subtask of the deliveries task. We have not specified it yet, but it has the local ID ''''start''''. To specify the dependency between the two tasks, we use the [[depends]] attribute. This attribute must be followed by one or more task IDs. If more than one ID is specified, each ID has to be separated with a comma from the previous one. Task IDs can be either absolute IDs or relative IDs. An absolute ID of a task is the ID of this task prepended by the IDs of all enclosing tasks. The task IDs are separated by a dot from each other. The absolute ID of the specification task would be ''''AcSo.spec''''. Relative IDs always start with one or more exclamation marks. Each exclamation mark moves the scope to the next enclosing task. So ''''!deliveries.start'''' is expanded to ''''AcSo.deliveries.start'''' since ''''AcSo'''' is the enclosing task of ''''deliveries''''. Relative task IDs are a little bit confusing at first, but have a real advantage over absolute IDs. Sooner or later you want to move tasks around in your project and then it's a lot less likely that you have to fix dependency specifications of relative IDs. The software development task is still too complex to specify it directly. So we split it further into subtasks. <[example file="tutorial" tag="software"]> We use the [[priority]] attribute to mark the importance of the tasks. 500 is the default priority of top-level tasks. Setting the priority to 1000 marks the task as most important task, since the possible range is 1 (not important at all) to 1000 (ultimately important). ''''priority'''' is an attribute that is passed down to subtasks if specified before the subtasks' declaration. So all subtasks of software have a priority of 1000 as well, unless they have their own priority definition. <[example file="tutorial" tag="database"]> The work on the database coupling should not start before the specification has been finished. So we again use the [[depends]] attribute to let TaskJuggler know about this. This time we use two exclamation marks for the relative ID. The first one puts us in the scope of the enclosing software task. The second one is to get into the AcSo scope that contains the spec tasks. For a change, we [[allocate]] resources directly without using a macro. <[example file="tutorial" tag="gui"]> One more interesting thing to note is the fact that we like the resource ''''dev2'''' only to work 6 hours each day on this task, so we use the optional attribute [[limits.resource]] to specify this. TaskJuggler can schedule your project for two different [[scenario| scenarios]]. We have called the first scenario ''''plan'''' scenario and the second ''''delayed'''' scenario. Many of the reports allow you to put the values of both scenarios side by side to each other, so you can compare the scenarios. All scenario-specific values that are not explicitly stated for the ''''delayed'''' scenario are taken from the ''''plan'''' scenario. So the user only has to specify the values that differ in the delayed scenario. The two scenarios must have the same task structure and the same dependencies. But the start and end dates of tasks as well as the duration may vary. In the example we have planned the work on the graphical user interface to be 35 man-days. It turned out that we actually needed 40 man-days. By prefixing the [[effort]] attribute with ''''delayed:'''', the effort value for the ''''delayed'''' scenario can be specified. <[example file="tutorial" tag="backend"]> By default, TaskJuggler assumes that all tasks are on schedule. Sometimes you want to generate reports that show how much of a task actually has been completed. TaskJuggler uses the current date for this, unless you have specified another date using the now attribute. If a task is ahead of schedule or late, this can be specified using the [[complete]] attribute. This specifies how many percent of the task have been completed up to the current date. In our case the back-end implementation is slightly ahead of schedule as we will see from the report. <[example file="tutorial" tag="test"]> The software testing task has been split up into an alpha and a beta test task. The interesting thing here is, that efforts can not only be specified as man-days, but also man-weeks, man-hours, etc. By default, TaskJuggler assumes a man-day is 8 hours, man-week is 40 man-hours or 5 man-days. The conversion factor can be changed using the [[dailyworkinghours]] attribute. Let's go back to the outermost task again. At the beginning of the example we stated that we want to credit all development work to one account with ID dev and all documentation work to the account doc. To achieve this, we use the attribute [[chargeset]] to credit all tasks to the ''''dev'''' account. For the duration of the ''''AcSo'''' task we also have running costs for the lease on the building and the equipment. To compensate this, we charge a daily rate of USD 170 per day using the [[charge]] attribute. <[example file="tutorial" tag="charge"]> Since we specify the attribute for the top-level task before we declare any subtasks, this attribute will be inherited by all subtasks and their subtasks and so on. The only exception is the writing of the manual. We need to change the chargeset for this task again, as it is also a subtask of AcSo and we want to use a different account for it. <[example file="tutorial" tag="manual"]> === Specifying Milestones === All tasks that have been discussed so far, had a certain duration. We did not always specify the duration explicitly, but we expect them to last for a certain period of time. Sometimes you just want to capture a certain moment in your project plan. These moments are usually called milestones, since they have some level of importance for the progress of the project. TaskJuggler has support for milestones as well. Milestones are leaf tasks that don't have a duration specification. <[example file="tutorial" tag="deliveries"]> We have put all important milestones of the project as subtasks of the deliveries task. This way they show up nicely grouped in the reports. All milestones either have a dependency or a fixed start date. For the first milestone we have used the attribute [[start]] to set a fixed start date. All other tasks have direct or indirect dependencies on this task. Moving back the start date will slip the whole project. This has actually happened, so we use the ''''delayed:'''' prefix again to specify the start date for the delayed scenario. Every milestone is linked to a customer payment. By using the [[charge]] attribute we can credit the specified amount to the account associated with this task. Since we have assigned the ''''rev'''' account to the enclosing task, all milestones will use this account as well. This time, we use the keyword ''''onstart'''' to indicate that this is not a continuous charge but a one-time charge that is credited at the begin of the task. Did you notice the line in the task done that starts with a hash? This line is commented out. If TaskJuggler finds a hash, it ignores the rest of the line. This way you can include comments in your project. The [[maxend]] attribute specifies that the task should end no later than the specified date. This information is not used for scheduling, but only for checking the schedule afterwards. Since the task will end later than the specified date, commenting out the line would trigger a warning. Now the project has been completely specified. Stopping here would result in a valid TaskJuggler file that could be processed and scheduled. But no reports would be generated to visualize the results. === Visualizing the Project === To see and share the project data, reports can be generated. You can generate any number of reports and you can select from a variety of report types and output formats. To have a report generated after the project scheduling has been completed, you need include a report definition into the project description. Report definitions are properties that are very similar to the task and resource properties that you are already familiar with. Just like these, report definitions can be nested to take advantage of the attribute inheritance mechanism. Every report definition starts with the type of the report. Each type of report has a particular focus. A [[taskreport]] lists the project data in the form of a task list. A [[resourcereport]] does the same in form of a resource list. For a more generic report, you can use the [[textreport]]. A ''''textreport'''' does not directly present the data in form of a task or resource list. It just consists of text building blocks that are described by [[Rich_Text_Attributes|Rich Text]]. There can be a building block at the top and bottom, as well as three columns in the center. The column are called ''''left'''', ''''center'''' and ''''right''''. For our first report, we'll just use the center column for now. Like every property, you need to specify a name. This name will be the base name of the generated report file. Depending on the output format, the proper suffix is appended. For this report, we only chose to generate a web page in HTML format. There is no default format defined for reports. If the [[formats]] attribute is not specified, no output file will be generated for the report specification. This may seem odd at first glance since TaskJuggler syntax always tries to use the most compact and readable syntax for the common case. As you will see in a minute, reports may be composed of several report specifications. One report specification can include the output of another report specification as well. In this case, the included report does not need to generate it's own file. The output will be included within the output of another report specification. In case of such composed reports, the output format specification of the top-level format will be used for all included reports as well. <[example file="tutorial" tag="overview_report1"]> For the main report, we choose the file name ''''Overview'''' and the format ''''html''''. So, the generated file will be called ''''Overview.html''''. As we've mentioned before, the sections of a ''''textreport'''' are defined in Rich Text format. Here we use a so called block generator to include the HTML output of another report definition. The ''''report'''' block generator allows us to compose reports by combining their output into a single report. You must provide the ''''id'''' parameter to specify which report definition you would like to use. In this case, it is a report definition with the ID ''''overview''''. Note that generator parameters need to be enclosed in single or double quotes. We are essentially marking a string within a string. This can only work out, if we don't use the same parameter for both. Let's define this report first. <[example file="tutorial" tag="overview1"]> Instead of another [[textreport]] definition we are now using a [[taskreport]]. A task report contains a list of tasks in a table structure. By default, it contains all tasks of the project. As we will see later on, we can use filter expressions to limit the content to a well defined subset of tasks. The table contains a line for each task and comes by default with a few columns like the name of the task, and the start and end dates. For this project overview report, we like to have also the effort for each task, the duration, the effort, the cost and revenue numbers included. To top it off, we also include a column with a Gantt chart. By including the cost and revenue column, we are able to do a simple profit and loss analysis on the project. This P&L is computed from the accounts that we have provided above. For this to work, we need to tell TaskJuggler which accounts are cost accounts and which are revenue accounts. We have already conveniently grouped the accounts and the [[balance]] attribute specifies which accounts are used for the P&L in this report. <[example file="tutorial" tag="overview2"]> The columns of the report can be customized. You can overwrite the default title or the cell content. See [[columns]] for a full list of available attributes. For the chart column, we'd like to have a tool tip that displays additional details when the mouse pointer is placed over a task bar. Since we use this tool tip in several reports, we have defined the ''''TaskTip'''' macro for it. <[example file="tutorial" tag="tasktip"]> The [[tooltip.column|tooltip]] attribute describes the content of the tool tip. The first parameter is a logical expression that determines when the tool tip is active. You can specify multiple tool tips. The first matching one is being displayed. The condition is evaluated for each report line. The ''''istask()'''' function only evaluates to true for task lines. See [[functions]] for a complete list of functions that can be used in [[logicalexpression|logical expressions]]. The content of the tool tip is a template that uses [[Rich_Text_Attributes#Block_and_Inline_Generators|query generators]] to include task attributes such as the start and end date. We have chosen to include the start and end date of each task in the report. By default, TaskJuggler lists dates as day, month and year. We like the format to be similar to the format that the project syntax uses, but also like to include the weekday. To change the date format, the [[timeformat]] attribute can be used. The project will last a few weeks. The most convenient unit to list efforts in is man or resource days. The [[loadunit]] attribute tells TaskJuggler to list the load of each task or resource in man days. Since this will just be a number without a unit, it is advisable to include a small hint for the reader that these values are indeed man or resource days. The caption of the table is a convenient place to put this information by using the [[caption]] attribute. <[example file="tutorial" tag="overview3"]> The ''''taskreport'''' can contain more than just the table. It is not as flexible as the ''''textreport'''', but still has support for a header and footer. Let's look at the header first. We not only like to put a headline here, but several paragraphs of text. The [[header]] attribute is a [[Rich_Text_Attributes|Rich Text]] attribute just like [[center]]. We could enclose it in single or double quotes again. But for Strings that span multiple lines and potentially include single or double quotes as well, scissor-marks or cut-here-marks are recommended. These marks look like a pair of scissors that cut along a dashed line. Use ''''-8<-'''' to begin a string and ''''->8-'''' to terminate it. The opening cut mark must be immediately followed by a line break. The indentation of the following line defines the indentation that will be ignored for all lines of the string. The following lines must have at least the same indentation. The indentation that exceeds the indentation of the first line will be kept in the resulting string. With this feature, you can define multi-line Rich Text strings without disturbing the indentation structure of your project file. <[example file="tutorial" tag="overview4"]> Section headers are surrounded by ''''==''''. The number of equal signs, define the section level. You need to start with two equal characters for the first level. Text that is surrounded by blank lines will create a paragraph. Bullet lists can be made by starting a line with a ''''#'''' character. Remember that the indentation of cut-mark strings will be ignored. Your ''''#'''' character must not be the first character in the line as long it is only preceded by the exact same number of blanks as the first line of the cut-mark string. If you want to reference other reports from this report, you can include the file name of this report by ''''[['''' and '''']]''''. Don't include the extension of the file name, it will be automatically appended. The actual representation of the reference depends on the chosen output format. For HTML output, the reference is a click-able link to the referenced report file. For the [[footer]] we can proceed accordingly. We just add a few more paragraphs of text to describe certain aspects of the project. By putting it all together, we end up with the following report definition. <[example file="tutorial" tag="overview"]> The generated report can be found [http://www.taskjuggler.org/tj3/examples/Tutorial/Overview.html here]. It serves as an entry page for the other reports. While it already contains some references, a navigator bar would be handy as well. Fortunately, there is a block generator called 'navigator' to take care of this. But before we can include the navigator in the report, we need to define it first. <[example file="tutorial" tag="navigator"]> [[hidereport]] is a filter attribute. The logical expression determines which reports will be included in the navigator bar. A logical expression of 0 means hide no reports, so all are included. The best place to put a navigator bar in the report is right at the top. We use two horizontal lines to separate the navigator from the main headline and the rest of the report. ''''----'''' at the begin of the line create such a horizontal separation line. <[example file="tutorial" tag="overview_report2"]> TaskJuggler-3.8.1/manual/fdl000066400000000000000000000555731473026623400157400ustar00rootroot00000000000000== GNU Free Documentation License == Version 1.3, 3 November 2008 Copyright (C) 2000, 2001, 2002, 2007, 2008 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. 0. PREAMBLE The purpose of this License is to make a manual, textbook, or other functional and useful document "free" in the sense of freedom: to assure everyone the effective freedom to copy and redistribute it, with or without modifying it, either commercially or noncommercially. Secondarily, this License preserves for the author and publisher a way to get credit for their work, while not being considered responsible for modifications made by others. This License is a kind of "copyleft", which means that derivative works of the document must themselves be free in the same sense. It complements the GNU General Public License, which is a copyleft license designed for free software. We have designed this License in order to use it for manuals for free software, because free software needs free documentation: a free program should come with manuals providing the same freedoms that the software does. But this License is not limited to software manuals; it can be used for any textual work, regardless of subject matter or whether it is published as a printed book. We recommend this License principally for works whose purpose is instruction or reference. 1. APPLICABILITY AND DEFINITIONS This License applies to any manual or other work, in any medium, that contains a notice placed by the copyright holder saying it can be distributed under the terms of this License. Such a notice grants a world-wide, royalty-free license, unlimited in duration, to use that work under the conditions stated herein. The "Document", below, refers to any such manual or work. Any member of the public is a licensee, and is addressed as "you". You accept the license if you copy, modify or distribute the work in a way requiring permission under copyright law. A "Modified Version" of the Document means any work containing the Document or a portion of it, either copied verbatim, or with modifications and/or translated into another language. A "Secondary Section" is a named appendix or a front-matter section of the Document that deals exclusively with the relationship of the publishers or authors of the Document to the Document's overall subject (or to related matters) and contains nothing that could fall directly within that overall subject. (Thus, if the Document is in part a textbook of mathematics, a Secondary Section may not explain any mathematics.) The relationship could be a matter of historical connection with the subject or with related matters, or of legal, commercial, philosophical, ethical or political position regarding them. The "Invariant Sections" are certain Secondary Sections whose titles are designated, as being those of Invariant Sections, in the notice that says that the Document is released under this License. If a section does not fit the above definition of Secondary then it is not allowed to be designated as Invariant. The Document may contain zero Invariant Sections. If the Document does not identify any Invariant Sections then there are none. The "Cover Texts" are certain short passages of text that are listed, as Front-Cover Texts or Back-Cover Texts, in the notice that says that the Document is released under this License. A Front-Cover Text may be at most 5 words, and a Back-Cover Text may be at most 25 words. A "Transparent" copy of the Document means a machine-readable copy, represented in a format whose specification is available to the general public, that is suitable for revising the document straightforwardly with generic text editors or (for images composed of pixels) generic paint programs or (for drawings) some widely available drawing editor, and that is suitable for input to text formatters or for automatic translation to a variety of formats suitable for input to text formatters. A copy made in an otherwise Transparent file format whose markup, or absence of markup, has been arranged to thwart or discourage subsequent modification by readers is not Transparent. An image format is not Transparent if used for any substantial amount of text. A copy that is not "Transparent" is called "Opaque". Examples of suitable formats for Transparent copies include plain ASCII without markup, Texinfo input format, LaTeX input format, SGML or XML using a publicly available DTD, and standard-conforming simple HTML, PostScript or PDF designed for human modification. Examples of transparent image formats include PNG, XCF and JPG. Opaque formats include proprietary formats that can be read and edited only by proprietary word processors, SGML or XML for which the DTD and/or processing tools are not generally available, and the machine-generated HTML, PostScript or PDF produced by some word processors for output purposes only. The "Title Page" means, for a printed book, the title page itself, plus such following pages as are needed to hold, legibly, the material this License requires to appear in the title page. For works in formats which do not have any title page as such, "Title Page" means the text near the most prominent appearance of the work's title, preceding the beginning of the body of the text. The "publisher" means any person or entity that distributes copies of the Document to the public. A section "Entitled XYZ" means a named subunit of the Document whose title either is precisely XYZ or contains XYZ in parentheses following text that translates XYZ in another language. (Here XYZ stands for a specific section name mentioned below, such as "Acknowledgements", "Dedications", "Endorsements", or "History".) To "Preserve the Title" of such a section when you modify the Document means that it remains a section "Entitled XYZ" according to this definition. The Document may include Warranty Disclaimers next to the notice which states that this License applies to the Document. These Warranty Disclaimers are considered to be included by reference in this License, but only as regards disclaiming warranties: any other implication that these Warranty Disclaimers may have is void and has no effect on the meaning of this License. 2. VERBATIM COPYING You may copy and distribute the Document in any medium, either commercially or noncommercially, provided that this License, the copyright notices, and the license notice saying this License applies to the Document are reproduced in all copies, and that you add no other conditions whatsoever to those of this License. You may not use technical measures to obstruct or control the reading or further copying of the copies you make or distribute. However, you may accept compensation in exchange for copies. If you distribute a large enough number of copies you must also follow the conditions in section 3. You may also lend copies, under the same conditions stated above, and you may publicly display copies. 3. COPYING IN QUANTITY If you publish printed copies (or copies in media that commonly have printed covers) of the Document, numbering more than 100, and the Document's license notice requires Cover Texts, you must enclose the copies in covers that carry, clearly and legibly, all these Cover Texts: Front-Cover Texts on the front cover, and Back-Cover Texts on the back cover. Both covers must also clearly and legibly identify you as the publisher of these copies. The front cover must present the full title with all words of the title equally prominent and visible. You may add other material on the covers in addition. Copying with changes limited to the covers, as long as they preserve the title of the Document and satisfy these conditions, can be treated as verbatim copying in other respects. If the required texts for either cover are too voluminous to fit legibly, you should put the first ones listed (as many as fit reasonably) on the actual cover, and continue the rest onto adjacent pages. If you publish or distribute Opaque copies of the Document numbering more than 100, you must either include a machine-readable Transparent copy along with each Opaque copy, or state in or with each Opaque copy a computer-network location from which the general network-using public has access to download using public-standard network protocols a complete Transparent copy of the Document, free of added material. If you use the latter option, you must take reasonably prudent steps, when you begin distribution of Opaque copies in quantity, to ensure that this Transparent copy will remain thus accessible at the stated location until at least one year after the last time you distribute an Opaque copy (directly or through your agents or retailers) of that edition to the public. It is requested, but not required, that you contact the authors of the Document well before redistributing any large number of copies, to give them a chance to provide you with an updated version of the Document. 4. MODIFICATIONS You may copy and distribute a Modified Version of the Document under the conditions of sections 2 and 3 above, provided that you release the Modified Version under precisely this License, with the Modified Version filling the role of the Document, thus licensing distribution and modification of the Modified Version to whoever possesses a copy of it. In addition, you must do these things in the Modified Version: A. Use in the Title Page (and on the covers, if any) a title distinct from that of the Document, and from those of previous versions (which should, if there were any, be listed in the History section of the Document). You may use the same title as a previous version if the original publisher of that version gives permission. B. List on the Title Page, as authors, one or more persons or entities responsible for authorship of the modifications in the Modified Version, together with at least five of the principal authors of the Document (all of its principal authors, if it has fewer than five), unless they release you from this requirement. C. State on the Title page the name of the publisher of the Modified Version, as the publisher. D. Preserve all the copyright notices of the Document. E. Add an appropriate copyright notice for your modifications adjacent to the other copyright notices. F. Include, immediately after the copyright notices, a license notice giving the public permission to use the Modified Version under the terms of this License, in the form shown in the Addendum below. G. Preserve in that license notice the full lists of Invariant Sections and required Cover Texts given in the Document's license notice. H. Include an unaltered copy of this License. I. Preserve the section Entitled "History", Preserve its Title, and add to it an item stating at least the title, year, new authors, and publisher of the Modified Version as given on the Title Page. If there is no section Entitled "History" in the Document, create one stating the title, year, authors, and publisher of the Document as given on its Title Page, then add an item describing the Modified Version as stated in the previous sentence. J. Preserve the network location, if any, given in the Document for public access to a Transparent copy of the Document, and likewise the network locations given in the Document for previous versions it was based on. These may be placed in the "History" section. You may omit a network location for a work that was published at least four years before the Document itself, or if the original publisher of the version it refers to gives permission. K. For any section Entitled "Acknowledgements" or "Dedications", Preserve the Title of the section, and preserve in the section all the substance and tone of each of the contributor acknowledgements and/or dedications given therein. L. Preserve all the Invariant Sections of the Document, unaltered in their text and in their titles. Section numbers or the equivalent are not considered part of the section titles. M. Delete any section Entitled "Endorsements". Such a section may not be included in the Modified Version. N. Do not retitle any existing section to be Entitled "Endorsements" or to conflict in title with any Invariant Section. O. Preserve any Warranty Disclaimers. If the Modified Version includes new front-matter sections or appendices that qualify as Secondary Sections and contain no material copied from the Document, you may at your option designate some or all of these sections as invariant. To do this, add their titles to the list of Invariant Sections in the Modified Version's license notice. These titles must be distinct from any other section titles. You may add a section Entitled "Endorsements", provided it contains nothing but endorsements of your Modified Version by various parties--for example, statements of peer review or that the text has been approved by an organization as the authoritative definition of a standard. You may add a passage of up to five words as a Front-Cover Text, and a passage of up to 25 words as a Back-Cover Text, to the end of the list of Cover Texts in the Modified Version. Only one passage of Front-Cover Text and one of Back-Cover Text may be added by (or through arrangements made by) any one entity. If the Document already includes a cover text for the same cover, previously added by you or by arrangement made by the same entity you are acting on behalf of, you may not add another; but you may replace the old one, on explicit permission from the previous publisher that added the old one. The author(s) and publisher(s) of the Document do not by this License give permission to use their names for publicity for or to assert or imply endorsement of any Modified Version. 5. COMBINING DOCUMENTS You may combine the Document with other documents released under this License, under the terms defined in section 4 above for modified versions, provided that you include in the combination all of the Invariant Sections of all of the original documents, unmodified, and list them all as Invariant Sections of your combined work in its license notice, and that you preserve all their Warranty Disclaimers. The combined work need only contain one copy of this License, and multiple identical Invariant Sections may be replaced with a single copy. If there are multiple Invariant Sections with the same name but different contents, make the title of each such section unique by adding at the end of it, in parentheses, the name of the original author or publisher of that section if known, or else a unique number. Make the same adjustment to the section titles in the list of Invariant Sections in the license notice of the combined work. In the combination, you must combine any sections Entitled "History" in the various original documents, forming one section Entitled "History"; likewise combine any sections Entitled "Acknowledgements", and any sections Entitled "Dedications". You must delete all sections Entitled "Endorsements". 6. COLLECTIONS OF DOCUMENTS You may make a collection consisting of the Document and other documents released under this License, and replace the individual copies of this License in the various documents with a single copy that is included in the collection, provided that you follow the rules of this License for verbatim copying of each of the documents in all other respects. You may extract a single document from such a collection, and distribute it individually under this License, provided you insert a copy of this License into the extracted document, and follow this License in all other respects regarding verbatim copying of that document. 7. AGGREGATION WITH INDEPENDENT WORKS A compilation of the Document or its derivatives with other separate and independent documents or works, in or on a volume of a storage or distribution medium, is called an "aggregate" if the copyright resulting from the compilation is not used to limit the legal rights of the compilation's users beyond what the individual works permit. When the Document is included in an aggregate, this License does not apply to the other works in the aggregate which are not themselves derivative works of the Document. If the Cover Text requirement of section 3 is applicable to these copies of the Document, then if the Document is less than one half of the entire aggregate, the Document's Cover Texts may be placed on covers that bracket the Document within the aggregate, or the electronic equivalent of covers if the Document is in electronic form. Otherwise they must appear on printed covers that bracket the whole aggregate. 8. TRANSLATION Translation is considered a kind of modification, so you may distribute translations of the Document under the terms of section 4. Replacing Invariant Sections with translations requires special permission from their copyright holders, but you may include translations of some or all Invariant Sections in addition to the original versions of these Invariant Sections. You may include a translation of this License, and all the license notices in the Document, and any Warranty Disclaimers, provided that you also include the original English version of this License and the original versions of those notices and disclaimers. In case of a disagreement between the translation and the original version of this License or a notice or disclaimer, the original version will prevail. If a section in the Document is Entitled "Acknowledgements", "Dedications", or "History", the requirement (section 4) to Preserve its Title (section 1) will typically require changing the actual title. 9. TERMINATION You may not copy, modify, sublicense, or distribute the Document except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense, or distribute it is void, and will automatically terminate your rights under this License. However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, receipt of a copy of some or all of the same material does not give you any rights to use it. 10. FUTURE REVISIONS OF THIS LICENSE The Free Software Foundation may publish new, revised versions of the GNU Free Documentation License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. See http://www.gnu.org/copyleft/. Each version of the License is given a distinguishing version number. If the Document specifies that a particular numbered version of this License "or any later version" applies to it, you have the option of following the terms and conditions either of that specified version or of any later version that has been published (not as a draft) by the Free Software Foundation. If the Document does not specify a version number of this License, you may choose any version ever published (not as a draft) by the Free Software Foundation. If the Document specifies that a proxy can decide which future versions of this License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Document. 11. RELICENSING "Massive Multiauthor Collaboration Site" (or "MMC Site") means any World Wide Web server that publishes copyrightable works and also provides prominent facilities for anybody to edit those works. A public wiki that anybody can edit is an example of such a server. A "Massive Multiauthor Collaboration" (or "MMC") contained in the site means any set of copyrightable works thus published on the MMC site. "CC-BY-SA" means the Creative Commons Attribution-Share Alike 3.0 license published by Creative Commons Corporation, a not-for-profit corporation with a principal place of business in San Francisco, California, as well as future copyleft versions of that license published by that same organization. "Incorporate" means to publish or republish a Document, in whole or in part, as part of another Document. An MMC is "eligible for relicensing" if it is licensed under this License, and if all works that were first published under this License somewhere other than this MMC, and subsequently incorporated in whole or in part into the MMC, (1) had no cover texts or invariant sections, and (2) were thus incorporated prior to November 1, 2008. The operator of an MMC Site may republish an MMC contained in the site under CC-BY-SA on the same site at any time before August 1, 2009, provided the MMC is eligible for relicensing. ADDENDUM: How to use this License for your documents To use this License in a document you have written, include a copy of the License in the document and put the following copyright and license notices just after the title page: Copyright (c) YEAR YOUR NAME. Permission is granted to copy, distribute and/or modify this document under the terms of the GNU Free Documentation License, Version 1.3 or any later version published by the Free Software Foundation; with no Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts. A copy of the license is included in the section entitled "GNU Free Documentation License". If you have Invariant Sections, Front-Cover Texts and Back-Cover Texts, replace the "with...Texts." line with this: with the Invariant Sections being LIST THEIR TITLES, with the Front-Cover Texts being LIST, and with the Back-Cover Texts being LIST. If you have Invariant Sections without Cover Texts, or some other combination of the three, merge those two alternatives to suit the situation. If your document contains nontrivial examples of program code, we recommend releasing these examples in parallel under your choice of free software license, such as the GNU General Public License, to permit their use in free software. TaskJuggler-3.8.1/spec/000077500000000000000000000000001473026623400147065ustar00rootroot00000000000000TaskJuggler-3.8.1/spec/Color_spec.rb000066400000000000000000000036621473026623400173320ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = Color_spec.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/Painter/Color' RSpec.configure do |config| config.expect_with(:rspec) { |c| c.syntax = :should } end class TaskJuggler class Painter describe Color do it 'should Convert RGB to HSV' do Color.new(0, 0, 0).to_hsv.should == [ 0, 0, 0 ] Color.new(255, 0, 0).to_hsv.should == [ 0, 255, 255 ] Color.new(255, 0, 4).to_hsv.should == [ 359, 255, 255 ] Color.new(255, 255, 255).to_hsv.should == [ 0, 0, 255 ] Color.new(60, 125, 116).to_hsv.should == [ 171, 132, 125 ] end it 'should convert HSV to RGB' do Color.new(0, 0, 0, :hsv).to_rgb.should == [ 0, 0, 0 ] Color.new(0, 0, 255, :hsv).to_rgb.should == [ 255, 255, 255 ] Color.new(150, 0, 255, :hsv).to_rgb.should == [ 255, 255, 255 ] Color.new(93, 156, 121, :hsv).to_rgb.should == [ 80, 121, 46 ] Color.new(275, 87, 94, :hsv).to_rgb.should == [ 80, 61, 94 ] Color.new(335, 47, 223, :hsv).to_rgb.should == [ 223, 181, 199 ] end it 'should Convert to HSV and back' do 0.step(255, 8) do |r| 0.step(255, 8) do |g| 0.step(255, 8) do |b| rgbRef = [r, g, b] hsv = Color.new(r, g, b).to_hsv rgb = Color.new(*hsv, :hsv).to_rgb 3.times do |i| # Due to rounding errors, we tolerate a difference of up to 5. (rgb[i] - rgbRef[i]).abs.should <= 5 end end end end end end end end TaskJuggler-3.8.1/spec/ICalendar_spec.rb000066400000000000000000000021501473026623400200650ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = ICalendar_spec.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'rubygems' require 'taskjuggler/ICalendar' require 'support/spec_helper.rb' RSpec.configure do |config| config.expect_with(:rspec) { |c| c.syntax = :should } end class TaskJuggler describe ICalendar do describe ICalendar::Component do it 'should quote properly' do c = ICalendar::Component.new(nil, '', nil, nil) [ [ '', '' ], [ 'foo', 'foo' ], [ '"', '\"' ], [ ';', '\;' ], [ ',', '\,' ], [ "\n", '\n' ], [ "foo\nbar", 'foo\nbar' ], [ 'a"b"c', 'a\"b\"c' ] ].each do |i, o| c.send('quoted', i).should == o end end end end end TaskJuggler-3.8.1/spec/IntervalList_spec.rb000066400000000000000000000077061473026623400206770ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = IntervalList_spec.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'rubygems' require 'taskjuggler/IntervalList' RSpec.configure do |config| config.expect_with(:rspec) { |c| c.syntax = :should } end class TaskJuggler describe IntervalList do before(:all) do @t0 = TjTime.new('2011-01-01').freeze @t1 = TjTime.new('2011-01-02').freeze @t2 = TjTime.new('2011-01-03').freeze @t3 = TjTime.new('2011-01-04').freeze @t4 = TjTime.new('2011-01-05').freeze @t5 = TjTime.new('2011-01-06').freeze @t6 = TjTime.new('2011-01-07').freeze @i0_1 = TimeInterval.new(@t0, @t1).freeze @i0_2 = TimeInterval.new(@t0, @t2).freeze @i0_3 = TimeInterval.new(@t0, @t3).freeze @i1_2 = TimeInterval.new(@t1, @t2).freeze @i1_3 = TimeInterval.new(@t1, @t3).freeze @i2_3 = TimeInterval.new(@t2, @t3).freeze @i3_4 = TimeInterval.new(@t3, @t4).freeze @i3_6 = TimeInterval.new(@t3, @t6).freeze @i4_5 = TimeInterval.new(@t4, @t5).freeze @i4_6 = TimeInterval.new(@t4, @t6).freeze @i5_6 = TimeInterval.new(@t5, @t6).freeze end describe "#<<" do before do @il = IntervalList.new end it 'should add a new interval' do @il << @i0_1 @il.should == [ @i0_1 ] end it 'Intervals should be added in ascending order' do @il << @i1_2 lambda { @il << @i0_1 }.should raise_error RuntimeError end it 'should merge adjecent intervals on add' do @il << @i0_1 @il << @i1_2 @il.should == [ @i0_2 ] end it 'should not merge non-adjecent intervals on add' do @il << @i0_1 @il << @i2_3 @il.should == [ @i0_1, @i2_3 ] end it 'operator concatenation should work' do @il << @i_01 << @i2_3 << @i4_5 @il.length.should == 3 end end describe '#&' do it 'without overlap should be empty' do il1 = IntervalList.new([ @i0_1 ]) il2 = IntervalList.new([ @i1_2 ]) (il1 & il2).should be_empty (il2 & il1).should be_empty end it 'with empty list should be empty' do il1 = IntervalList.new([ @i0_1 ]) il2 = IntervalList.new([ ]) (il1 & il2).should be_empty (il2 & il1).should be_empty end it 'with self should be self' do il = IntervalList.new([ @i0_1 ]) (il & il).should == il end it 'with partial overlap should be overlap' do il1 = IntervalList.new([ @i0_2 ]) il2 = IntervalList.new([ @i1_3 ]) il3 = IntervalList.new([ @i1_2 ]) (il1 & il2).should == il3 (il2 & il1).should == il3 end it 'with center inclusion should be inclusion' do il1 = IntervalList.new([ @i0_3, @i3_6 ]) il2 = IntervalList.new([ @i1_2, @i4_5 ]) (il1 & il2).should == il2 (il2 & il1).should == il2 end it 'with left inclusion should be inclusion' do il1 = IntervalList.new([ @i1_3, @i4_6 ]) il2 = IntervalList.new([ @i1_2, @i4_5 ]) (il1 & il2).should == il2 (il2 & il1).should == il2 end it 'with right inclusion should be inclusion' do il1 = IntervalList.new([ @i1_3, @i4_6 ]) il2 = IntervalList.new([ @i2_3, @i5_6 ]) (il1 & il2).should == il2 (il2 & il1).should == il2 end it 'with adjecent intervals should be empty' do il1 = IntervalList.new([ @i0_1, @i2_3 ]) il2 = IntervalList.new([ @i1_2, @i3_4 ]) (il1 & il2).should be_empty (il2 & il1).should be_empty end end end end TaskJuggler-3.8.1/spec/ProjectBroker_spec.rb000066400000000000000000000067351473026623400210330ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = ProjectBroker_spec.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/daemon/ProjectBroker' require 'support/spec_helper' RSpec.configure do |config| config.expect_with(:rspec) { |c| c.syntax = :should } end class TaskJuggler def TaskJuggler::runBroker(pb, key) pb.authKey = key pb.daemonize = false pb.logStdIO = false pb.port = 0 # Don't generate any debug or info messages mh = MessageHandlerInstance.instance mh.outputLevel = 1 mh.logLevel = 1 t = Thread.new { pb.start } yield pb.stop t.join end describe ProjectBroker, :ruby => 1.9 do it "can be started and stopped" do @pb = ProjectBroker.new @authKey = 'secret' TaskJuggler::runBroker(@pb, @authKey) do true end end end describe ProjectBrokerIface, :ruby => 1.9 do before do @pb = ProjectBroker.new @pbi = ProjectBrokerIface.new(@pb) @authKey = 'secret' end describe "apiVersion" do it "should fail with bad authentication key" do TaskJuggler::runBroker(@pb, @authKey) do @pbi.apiVersion('bad key', 1).should == 0 end end it "should pass with correct authentication key" do TaskJuggler::runBroker(@pb, @authKey) do @pbi.apiVersion(@authKey, 1).should == 1 end end it "should fail with wrong API version", :ruby => 1.9 do TaskJuggler::runBroker(@pb, @authKey) do @pbi.apiVersion(@authKey, 0).should == -1 end end end describe "command" do it "should fail with bad authentication key" do TaskJuggler::runBroker(@pb, @authKey) do @pbi.command('bad key', :status, []).should be false end end it "should support 'status'" do TaskJuggler::runBroker(@pb, @authKey) do @pbi.command(@authKey, :status, []).should match \ /.*No projects registered.*/ end end it "should support 'terminate'" do TaskJuggler::runBroker(@pb, @authKey) do @pbi.command(@authKey, :stop, []).should be_nil end end it "should support 'add' and 'remove'" do TaskJuggler::runBroker(@pb, @authKey) do stdIn = StringIO.new("project foo 'foo' 2011-01-04 +1w task 'foo'") stdOut = StringIO.new stdErr = StringIO.new args = [ Dir.getwd, [ '.' ], stdOut, stdErr, stdIn, true ] @pbi.command(@authKey, :addProject, args).should be true stdErr.string.should be_empty # Can't remove non-existing project bar @pbi.command(@authKey, :removeProject, 'bar').should be false @pbi.command(@authKey, :removeProject, 'foo').should be true # Can't remove foo twice @pbi.command(@authKey, :removeProject, 'foo').should be false end end end describe "updateState" do it "should fail with bad authentication key" do TaskJuggler::runBroker(@pb, @authKey) do @pbi.updateState('bad key', 'foo', 'foo', :status, true).should \ be false end end end end end TaskJuggler-3.8.1/spec/StatusSheets_spec.rb000066400000000000000000000161131473026623400207060ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = StatusSheets_spec.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'rubygems' require 'support/DaemonControl' require 'taskjuggler/apps/Tj3SsSender' require 'taskjuggler/apps/Tj3SsReceiver' RSpec.configure do |config| config.expect_with(:rspec) { |c| c.syntax = :should } end class TaskJuggler class StatusSheetTest end describe StatusSheetTest do include DaemonControl before(:all) do # Make sure we run in the same directory as the spec file. @pwd = pwd cd(File.dirname(__FILE__)) ENV['TASKJUGGLER_DATA_PATH'] = "../" cleanup startDaemon(<<'EOT' smtpServer: example.com _statussheets: projectId: sstest senderEmail: foo@example.com _sender: hideResource: '~(isleaf() & manager)' EOT ) prj = <<'EOT' project sstest "Time Sheet Test" 2011-03-14 +2m { trackingscenario plan now ${projectstart} } flags manager, important, late resource "Team" { resource boss "Boss" { email "boss@example.com" flags manager } resource r1 "R1" { email "r1@example.com" } resource r2 "R2" { email "r2@example.com" } } task t1 "T1" { effort 2.5d allocate r1 responsible boss } task t2 "T2" { depends !t1 effort 2.5d allocate r1 responsible boss } task t3 "T3" { effort 10d allocate r2 responsible boss } timesheet r1 2011-03-14-00:00-+0000 - 2011-03-21-00:00-+0000 { task t1 { work 30.0% remaining 2.0d status red "More work" { flags important, late details -8<- This is more work than expected. ->8- } } task t2 { work 50.0% remaining 0.0d status green "All work done!" } newtask t4 "A new job" { work 20% remaining 1.0d status green "May be a good idea" { summary -8<- I thought this might be useful work. ->8- } } } timesheet r2 2011-03-14-00:00-+0000 - 2011-03-21-00:00-+0000 { # Task: T3 task t3 { work 100.0% remaining 5.0d status green "What a job!" } status yellow "I'm not feeling good!" { flags important summary -8<- We all live on a yellow submarine! ->8- } } EOT res = stdIoWrapper(prj) do Tj3Client.new.main(%w( --unsafe add . )) end unless res.stdErr.include?("Info: Project(s) . added") raise "Project not loaded: #{res.stdErr}" end raise "Can't load project: #{res.stdErr}" unless res.returnValue == 0 res = stdIoWrapper do Tj3SsSender.new.main(%w( --dryrun --silent -e 2011-03-23 )) end unless res.returnValue == 0 raise " Status sheet template generation failed: #{res.stdErr}" end @sss_mails = collectMails(res.stdOut) @sheet = <<'EOT' # --------8<--------8<-------- statussheet boss 2011-03-16-00:00-+0000 - 2011-03-23-00:00-+0000 { # Task: T1 task t1 { status green "No More work" { # Date: 2011-03-21-00:00-+0000 # Work: 30% (50%) Remaining: 2.0d (0.0d) author r1 flags late details -8<- This is job is a breeze. ->8- } } # Task: T2 task t2 { # status green "All work done!" { # # Date: 2011-03-21-00:00-+0000 # # Work: 50% Remaining: 0.0d # author r1 # } } # Task: T3 task t3 { status green "What a nice job!" { # Date: 2011-03-21-00:00-+0000 # Work: 100% Remaining: 5.0d (0.0d) author r2 } } } # -------->8-------->8-------- EOT mailBody = @sheet.unix2dos.to_base64 mail = Mail.new do subject "Status sheet" content_type [ 'text', 'plain', { 'charset' => 'UTF-8' } ] content_transfer_encoding 'base64' body mailBody end mail.to = 'taskjuggler@example.com' mail.from 'boss@example.com' res = stdIoWrapper(mail.to_s) do Tj3SsReceiver.new.main(%w( --dryrun --silent . )) end unless res.returnValue == 0 raise " Status sheet reception failed: #{res.stdErr}" end @ssr_mails = collectMails(res.stdOut) end after(:all) do stopDaemon cleanup cd(@pwd) end it 'is just a dummy' do end describe StatusSheetSender do it 'should have generated 1 mail' do @sss_mails.length.should == 1 end it 'should have email sender foo@example.com' do @sss_mails.each do |mail| mail.from[0].should == 'foo@example.com' end end it 'should have proper email receivers' do @sss_mails[0].to[0].should == 'boss@example.com' end it 'should generate properly dated headers' do countLines(@sss_mails[0].parts[0].decoded, 'statussheet boss 2011-03-16-00:00-+0000 - ' + '2011-03-23-00:00-+0000').should == 1 end it 'should have matching status sheets in body and attachment' do @sss_mails.each do |mail| bodySheet = extractStatusSheet(mail.parts[0].decoded) attachedSheet = extractStatusSheet(mail.part[1].decoded).tr("\r", '') bodySheet.should == attachedSheet end end end describe StatusSheetReceiver do it 'should have generated 1 mails' do @ssr_mails.length.should == 1 end it 'should have email sender foo@example.com' do @ssr_mails.each do |mail| mail.from[0].should == 'foo@example.com' end end it 'should have email receivers boss@example.com' do @ssr_mails[0].to[0].should == 'boss@example.com' end it 'should have stored status sheet' do @sheet.should == File.read('StatusSheets/2011-03-23/boss_2011-03-23.tji') end end private def countLines(text, pattern) c = 0 if pattern.is_a?(Regexp) text.each_line do |line| c += 1 if line =~ pattern end else text.each_line do |line| c += 1 if line.include?(pattern) end end c end def extractStatusSheet(lines) sheet = nil lines.each_line do |line| if line =~ /^# --------8<--------8<--------/ sheet = "" elsif line =~ /^# -------->8-------->8--------/ raise 'Found end marker, but no start marker' unless sheet return sheet elsif sheet sheet += line end end raise "No end marker found" end def collectMails(lines) mails = [] mailLines = nil lines.each_line do |line| if line =~ /^-- Email Start ---/ mailLines = "" elsif line =~ /^-- Email End ---/ raise 'Found end marker, but no start marker' unless mailLines mails << Mail.read_from_string(mailLines) mailLines = nil elsif mailLines mailLines += line end end mails end end end TaskJuggler-3.8.1/spec/TableColumnSorter_spec.rb000066400000000000000000000045031473026623400216530ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = TableColumnSorter_spec.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'rubygems' require 'taskjuggler/TableColumnSorter' RSpec.configure do |config| config.expect_with(:rspec) { |c| c.syntax = :should } end class TaskJuggler describe TableColumnSorter do before do @table = [ %w( One Two Three ), [ 1, 2, 3 ] ] @sorter = TableColumnSorter.new(@table) end it "should not change for same header" do t = @sorter.sort(%w( One Two Three )) t.should == @table @sorter.discontinuedColumns.should == 0 end it "should not change for all remove" do t = @sorter.sort(%w( )) t.should == @table @sorter.discontinuedColumns.should == 3 end it "should move Two to back" do t = @sorter.sort(%w( One Three )) t.should == [ %w( One Three Two ), [ 1, 3, 2 ] ] @sorter.discontinuedColumns.should == 1 end it "should not change when last columns is missing" do t = @sorter.sort(%w( One Two )) t.should == @table @sorter.discontinuedColumns.should == 1 end it "should insert Four in front" do t = @sorter.sort(%w( Four One Two Three )) t.should == [ %w( Four One Two Three ), [ nil, 1, 2, 3 ] ] @sorter.discontinuedColumns.should == 0 end it "should insert Four and Five at end" do t = @sorter.sort(%w( One Two Three Four Five )) t.should == [ %w( One Two Three Four Five ), [ 1, 2, 3, nil, nil ] ] @sorter.discontinuedColumns.should == 0 end it "should insert Four at end and move Three to back" do t = @sorter.sort(%w( One Two Four )) t.should == [ %w( One Two Four Three ), [ 1, 2, nil, 3 ] ] @sorter.discontinuedColumns.should == 1 end it "should keep first columns and insert new directly after" do t = @sorter.sort(%w( One Four Five )) t.should == [ %w( One Four Five Two Three), [ 1, nil, nil, 2, 3 ] ] @sorter.discontinuedColumns.should == 2 end end end TaskJuggler-3.8.1/spec/TernarySearchTree_spec.rb000066400000000000000000000070251473026623400216430ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = TernarySearchTree_spec.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'rubygems' require 'taskjuggler/TernarySearchTree' RSpec.configure do |config| config.expect_with(:rspec) { |c| c.syntax = :should } end class TaskJuggler describe TernarySearchTree do before do @tst = TernarySearchTree.new end it 'should not contain anything yet' do @tst.length.should be_equal 0 @tst[''].should be_nil end it 'should accept single element on creation' do @tst = TernarySearchTree.new('foo') @tst.length.should == 1 end it 'should accept an Array on creation' do @tst = TernarySearchTree.new(%w( foo bar )) @tst.length.should == 2 end it 'should not accept an empty String' do lambda { @tst.insert('') }.should raise_error ArgumentError end it 'should not accept nil' do lambda { @tst.insert(nil) }.should raise_error ArgumentError end it 'should store inserted values' do v = %w( foo bar foobar barfoo fo ba foo1 bar1 zzz ) @tst.insertList(v) @tst.length.should be_equal v.length rv = @tst.to_a.sort rv.should == v.sort end it 'should find exact matches' do v = %w( foo bar foobar barfoo fo ba foo1 bar1 zzz ) v.each { |val| @tst << val } v.each do |val| @tst[val].should == val end end it 'should not find non-existing elements' do %w( foo bar foobar barfoo fo ba foo1 bar1 zzz ).each { |v| @tst << v } @tst['foos'].should be_nil @tst['bax'].should be_nil @tst[''].should be_nil end it 'should find partial matches' do %w( foo bar foobar barfoo ba foo1 bar1 zzz ).each { |v| @tst << v } @tst['foo', true].sort.should == %w( foo foobar foo1 ).sort @tst['fo', true].sort.should == %w( foo foobar foo1 ).sort @tst['b', true].sort.should == %w( bar barfoo ba bar1 ).sort @tst['zzz', true].should == [ 'zzz' ] end it 'should not find non-existing elements' do %w( foo bar foobar barfoo fo ba foo1 bar1 zzz ).each { |v| @tst << v } @tst['foos', true].should be_nil @tst['', true].should be_nil end it 'should store duplicate entries only once' do v = %w( foo bar foobar bar foo fo ba foo1 ba foobar bar1 zzz ) @tst.insertList(v) @tst.length.should == v.uniq.length end it 'maxDepth should work' do v = %w( a b c d e f) v.each { |val| @tst << val } @tst.maxDepth.should == v.length end it 'should be able to balance a tree' do %w( aa ab ac ba bb bc ca cb cc ).each { |v| @tst << v } tst = @tst.balanced @tst.balance! @tst.to_a.should == tst.to_a # The tree is not perfectly balanced. @tst.maxDepth.should == 5 end #it 'should store integer lists' do # @tst.insert([ 0, 1, 2 ]) # @tst.length.should == 1 # @tst.to_a.should be == [ 0, 1, 2 ] #end #it 'should work with integer lists as well' do # v = [ [ 0, 1, 2], [ 0, 3, 2], [ 1, 3 ], [ 0, 2, 1], [ 1, 0, 3] ] # @tst.insertList(v) # @tst.length.should be_equal v.length # rv = @tst.to_a.sort # rv.should == v.sort #end end end TaskJuggler-3.8.1/spec/TimeSheets_spec.rb000066400000000000000000000216101473026623400203170ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = TimeSheets_spec.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'rubygems' require 'support/DaemonControl' require 'taskjuggler/apps/Tj3TsSender' require 'taskjuggler/apps/Tj3TsReceiver' require 'taskjuggler/apps/Tj3TsSummary' RSpec.configure do |config| config.expect_with(:rspec) { |c| c.syntax = :should } end class TaskJuggler describe TimeSheets do include DaemonControl before(:all) do # Make sure we run in the same directory as the spec file. @pwd = pwd cd(File.dirname(__FILE__)) ENV['TASKJUGGLER_DATA_PATH'] = "../" cleanup startDaemon(<<'EOT' emailDeliveryMethod: smtp smtpServer: foobar.com _timesheets: projectId: tstest senderEmail: foo@example.com _sender: hideResource: '~isleaf()' _summary: sheetRecipients: - archive@example.com digestRecipients: - archive@example.com - crew@example.com EOT ) prj = <<'EOT' project tstest "Time Sheet Test" 2011-03-14 +2m { trackingscenario plan now ${projectstart} } flags important, late resource "Team" { resource r1 "R1" { email "r1@example.com" } resource r2 "R2" { email "r2@example.com" } } task t1 "T1" { effort 2.5d allocate r1 } task t2 "T2" { depends !t1 effort 2.5d allocate r1 } task t3 "T3" { effort 10d allocate r2 } EOT res = stdIoWrapper(prj) do Tj3Client.new.main(%w( --unsafe add . )) end unless res.stdErr.include?("Info: Project(s) . added") raise "Project not loaded: #{res.stdErr}" end raise "Can't load project" unless res.returnValue == 0 res = stdIoWrapper do Tj3TsSender.new.main(%w( --dryrun --silent -e 2011-03-21 )) end if res.stdErr != '' raise "Tj3TsSender failed: #{res.stdErr}" end @tss_mails = collectMails(res.stdOut) raise "Timesheet generation failed" unless res.returnValue == 0 @sheet1 = <<'EOT' # --------8<--------8<-------- timesheet r1 2011-03-14-00:00-+0000 - 2011-03-21-00:00-+0000 { task t1 { work 30.0% remaining 2.0d status red "More work" { flags important, late details -8<- This is more work than expected. ->8- } } task t2 { work 50.0% remaining 0.0d status green "All work done!" } newtask t4 "A new job" { work 20% remaining 1.0d status green "May be a good idea" { summary -8<- I thought this might be useful work. ->8- } } } # -------->8-------->8-------- EOT @sheet2 = <<'EOT' # --------8<--------8<-------- timesheet r2 2011-03-14-00:00-+0000 - 2011-03-21-00:00-+0000 { # Task: T3 task t3 { work 100.0% remaining 5.0d status green "What a job!" } status yellow "I'm not feeling good!" { summary -8<- We all live on a yellow submarine! ->8- } } # -------->8-------->8-------- EOT @tsr_mails = [] [ @sheet1, @sheet2 ].each do |sheet| mail = Mail.new do subject "Timesheet" content_type [ 'text', 'plain', { 'charset' => 'UTF-8' } ] content_transfer_encoding 'base64' body sheet.to_base64 end mail.to = 'taskjuggler@example.com' mail.from 'r@example.com' res = stdIoWrapper(mail.to_s) do Tj3TsReceiver.new.main(%w( --dryrun --silent )) end @tsr_mails += collectMails(res.stdOut) unless res.returnValue == 0 raise "Timesheet processing failed: #{res.stdErr}" end end res = stdIoWrapper(prj) do Tj3Client.new.main(%w( --unsafe --silent add . TimeSheets/2011-03-21/all.tji )) end unless res.returnValue == 0 raise "Project reloading failed: #{res.stdErr}" end res = stdIoWrapper do Tj3TsSummary.new.main(%w( --dryrun --silent -e 2011-03-21 )) end @sum_mails = collectMails(res.stdOut) unless res.returnValue == 0 raise "Summary generation failed: #{res.stdErr}" end end after(:all) do stopDaemon cleanup cd(@pwd) end describe TimeSheetSender do it 'should have generated 2 mails' do @tss_mails.length.should == 2 end it 'should have email sender foo@example.com' do @tss_mails.each do |mail| mail.from[0].should == 'foo@example.com' end end it 'should have proper email receivers' do @tss_mails[0].to[0].should == 'r1@example.com' @tss_mails[1].to[0].should == 'r2@example.com' end it 'should generate properly dated headers' do countLines(@tss_mails[0].parts[0].decoded, 'timesheet r1 2011-03-14-00:00-+0000 - ' + '2011-03-21-00:00-+0000').should == 1 countLines(@tss_mails[1].parts[0].decoded, 'timesheet r2 2011-03-14-00:00-+0000 - ' + '2011-03-21-00:00-+0000').should == 1 end it 'should have matching timesheets in body and attachment' do @tss_mails.each do |mail| bodySheet = extractTimeSheet(mail.parts[0].decoded) attachedSheet = extractTimeSheet(mail.part[1].decoded).tr("\r", '') bodySheet.should == attachedSheet end end end describe TimeSheetReceiver do it 'should have generated 2 mails' do @tsr_mails.length.should == 2 end it 'should have email sender foo@example.com' do @tsr_mails.each do |mail| mail.from[0].should == 'foo@example.com' end end it 'should have proper email receivers' do @tsr_mails[0].to[0].should == 'r1@example.com' @tsr_mails[1].to[0].should == 'r2@example.com' end it 'should have stored timesheets' do @sheet1.should == File.read('TimeSheets/2011-03-21/r1_2011-03-21.tji') @sheet2.should == File.read('TimeSheets/2011-03-21/r2_2011-03-21.tji') end it 'should report an error on bad keyword' do sheet = <<'EOT' # --------8<--------8<-------- timesheet r2 2011-03-14-00:00-+0000 - 2011-03-21-00:00-+0000 { task t3 { wirk 100.0% remaining 5.0d status green "All green!" } } # -------->8-------->8-------- EOT mail = Mail.new do subject "Timesheet" content_type [ 'text', 'plain', { 'charset' => 'UTF-8' } ] content_transfer_encoding 'base64' body sheet.unix2dos.to_base64 end mail.to = 'taskjuggler@example.com' mail.from 'r@example.com' res = stdIoWrapper(mail.to_s) do Tj3TsReceiver.new.main(%w( --dryrun --silent )) end countLines(res.stdErr, /\.\:5\: Error\: Unexpected token 'wirk' found\./).should == 1 res.returnValue.should == 1 end end describe TimeSheetSummary do it 'should have generated 4 mails' do @sum_mails.length.should == 4 end it 'should have proper email receivers' do @sum_mails[0].to[0].should == 'archive@example.com' @sum_mails[1].to[0].should == 'archive@example.com' @sum_mails[2].to[0].should == 'archive@example.com' @sum_mails[3].to[0].should == 'crew@example.com' end it 'should have proper email senders' do @sum_mails[0].from[0].should == 'r1@example.com' @sum_mails[1].from[0].should == 'r2@example.com' @sum_mails[2].from[0].should == 'foo@example.com' @sum_mails[3].from[0].should == 'foo@example.com' end end private def countLines(text, pattern) c = 0 if pattern.is_a?(Regexp) text.each_line do |line| c += 1 if line =~ pattern end else text.each_line do |line| c += 1 if line.include?(pattern) end end c end def extractTimeSheet(lines) sheet = nil lines.each_line do |line| if line =~ /^# --------8<--------8<--------/ sheet = "" elsif line =~ /^# -------->8-------->8--------/ raise 'Found end marker, but no start marker' unless sheet return sheet elsif sheet sheet += line end end raise "No end marker found" end def collectMails(lines) mails = [] mailLines = nil lines.each_line do |line| if line =~ /^-- Email Start ---/ mailLines = "" elsif line =~ /^-- Email End ---/ raise 'Found end marker, but no start marker' unless mailLines mails << Mail.read_from_string(mailLines) mailLines = nil elsif mailLines mailLines += line end end mails end end end TaskJuggler-3.8.1/spec/Tj3Daemon_spec.rb000066400000000000000000000024761473026623400200420ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = Tj3Daemon_spec.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'rubygems' require 'support/DaemonControl' RSpec.configure do |config| config.expect_with(:rspec) { |c| c.syntax = :should } end class TaskJuggler describe Tj3Daemon do include DaemonControl before(:each) do cleanup startDaemon end after(:each) do stopDaemon cleanup end it 'should be startable and stopable' do res = stdIoWrapper do Tj3Client.new.main(%w( --unsafe --silent status )) end res.returnValue.should == 0 res.stdErr.should == '' res.stdOut.should match /No projects registered/ end it 'should be able to load a project' do prj = 'project foo "Foo" 2011-03-14 +1d task "Foo"' res = stdIoWrapper(prj) do Tj3Client.new.main(%w( --unsafe add . )) end res.returnValue.should == 0 res.stdErr.should match /Project\(s\) \. added/ end end end TaskJuggler-3.8.1/spec/Tj3_spec.rb000066400000000000000000000016541473026623400167130ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = Tj3_spec.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'rubygems' require 'taskjuggler/StdIoWrapper' require 'taskjuggler/apps/Tj3' RSpec.configure do |config| config.expect_with(:rspec) { |c| c.syntax = :should } end class TaskJuggler describe Tj3 do include StdIoWrapper it 'should schedule a project' do prj = 'project "Foo" 2011-03-14 +1d task "Foo"' res = stdIoWrapper(prj) do Tj3.new.main(%w( --silent --no-reports . )) end res.stdErr.should == '' res.returnValue.should == 0 end end end TaskJuggler-3.8.1/spec/TraceReport_spec.rb000066400000000000000000000044101473026623400204760ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = TraceReport_spec.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/TaskJuggler' require 'taskjuggler/StdIoWrapper' require 'taskjuggler/apps/Tj3' class TaskJuggler describe TraceReport do include StdIoWrapper before(:all) do tf = 'tracereport' @tf = tf + '.csv' @prj = <<"EOT" project "Test" 2012-03-01 +2m { now 2012-03-11 } resource r "R" task t1 "T1" { effort 10d allocate r } task t2 "T2" { depends t1 duration 5d } task t3 "T3" { depends t2 } tracereport '#{tf}' { columns complete } EOT end after(:all) do File.delete(@tf) end it 'should generate a trace report' do File.delete(@tf) if File.exist?(@tf) tj3(@prj) ref = <<'EOT' "Date";"t1:plan.complete";"t2:plan.complete";"t3:plan.complete" "2012-03-11";70.0;0.0;0.0 EOT checkCSV(@tf, ref) end it 'should replace the existing line' do before = File.read(@tf) tj3(@prj) after = File.read(@tf) before.should == after end it 'should add a new line for another day' do prj = @prj.gsub(/now 2012-03-11/, 'now 2012-03-18') tj3(prj) ref = <<'EOT' "Date";"t1:plan.complete";"t2:plan.complete";"t3:plan.complete" "2012-03-11";70.0;0.0;0.0 "2012-03-18";100.0;65.83333333333333;0.0 EOT checkCSV(@tf, ref) end it 'should add to a file without data columns' do File.write(@tf, <<'EOT' "Date" "2012-03-11" EOT ) tj3(@prj) ref = <<'EOT' "Date";"t1:plan.complete";"t2:plan.complete";"t3:plan.complete" "2012-03-11";70.0;0.0;0.0 EOT checkCSV(@tf, ref) end private def tj3(prj) res = stdIoWrapper(prj) do Tj3.new.main(%w( --silent --add-trace . )) end res.stdOut.should == '' res.stdErr.should == '' res.returnValue.should == 0 end def checkCSV(file, ref) File.read(file).should == ref end end end TaskJuggler-3.8.1/spec/support/000077500000000000000000000000001473026623400164225ustar00rootroot00000000000000TaskJuggler-3.8.1/spec/support/DaemonControl.rb000066400000000000000000000044351473026623400215210ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = DaemonControl.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/StdIoWrapper' require 'taskjuggler/apps/Tj3Daemon' require 'taskjuggler/apps/Tj3Client' require 'fileutils' class TaskJuggler module DaemonControl include StdIoWrapper include FileUtils def startDaemon(config = '') (f = File.new('taskjuggler.rc', 'w')).write(<<"EOT" _global: authKey: 'secret_key' port: 0 _log: outputLevel: 3 logLevel: 3 #{config} EOT ) f.close if (pid = fork).nil? at_exit { exit! } $stdout.reopen('stdout.log', 'w') $stderr.reopen('stderr.log', 'w') res = stdIoWrapper do Tj3Daemon.new.main(%w( --silent )) end raise "Failed to start tj3d: #{res.stdErr}" if res.returnValue != 0 exit! else # Wait for the daemon to get online. i = 0 while !File.exist?('.tj3d.uri') && i < 10 sleep 0.5 i += 1 end raise 'Daemon did not start properly' if i == 10 end 0 end def stopDaemon res = stdIoWrapper do Tj3Client.new.main(%w( --silent --unsafe terminate )) end raise "tj3d termination failed: #{res.stdErr}" if res.returnValue != 0 i = 0 while File.exist?('.tj3d.uri') && i < 10 sleep 0.5 i += 1 end raise "Daemon did not terminate properly" if i == 10 # Cleanup file system again. %w( taskjuggler.rc stdout.log stderr.log ).each do |file| File.delete(file) end end def cleanup rm_rf %w( TimeSheetTemplates TimeSheets timesheets.log StatusSheetTemplates StatusSheets statussheets.log tj3d.log tj3client.log tj3.log tj3ss_sender.log tj3ss_receiver.log tj3ss_summary.log tj3ts_sender.log tj3ts_receiver.log tj3ts_summary.log ) end end end TaskJuggler-3.8.1/spec/support/spec_helper.rb000066400000000000000000000010631473026623400212400ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = spec_helper.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # RSpec.configure do |c| c.filter_run_excluding :ruby => lambda {|version| !(RUBY_VERSION.to_s =~ /^#{version.to_s}/) } end TaskJuggler-3.8.1/taskjuggler.gemspec000066400000000000000000000053171473026623400176510ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # = taskjuggler.gemspec -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # # This gemspec file will be used to package the taskjuggler gem. Before you # use it, the manual and other generated files must have been created! lib = File.expand_path('../lib', __FILE__) $:.unshift lib unless $:.include?(lib) # Get software version number from Tj3Config class. begin $: << 'lib' require 'taskjuggler/Tj3Config' PROJECT_VERSION = AppConfig.version PROJECT_NAME = AppConfig.softwareName rescue LoadError raise "Error: Cannot determine software settings: #{$!}" end GEM_SPEC = Gem::Specification.new { |s| s.name = 'taskjuggler' s.version = PROJECT_VERSION s.homepage = 'http://www.taskjuggler.org' s.author = 'Chris Schlaeger' s.email = 'chris@linux.com' s.summary = 'A Project Management Software' s.description = <<'EOT' TaskJuggler is a modern and powerful, Free and Open Source Software project management tool. It's new approach to project planning and tracking is more flexible and superior to the commonly used Gantt chart editing tools. TaskJuggler is project management software for serious project managers. It covers the complete spectrum of project management tasks from the first idea to the completion of the project. It assists you during project scoping, resource assignment, cost and revenue planning, risk and communication management. EOT s.license = 'GPL-2.0' s.require_path = 'lib' s.files = (`git ls-files -- lib`).split("\n") + (`git ls-files -- data`).split("\n") + (`git ls-files -- manual`).split("\n") + (`git ls-files -- examples`).split("\n") + (`git ls-files -- tasks`).split("\n") + %w( .gemtest taskjuggler.gemspec Rakefile ) + # Generated files, not contained in Git repository. %w( data/tjp.vim ) + Dir.glob('manual/html/**/*') + Dir.glob('man/*.1') s.bindir = 'bin' s.executables = (`git ls-files -- bin`).split("\n"). map { |fn| File.basename(fn) } s.test_files = (`git ls-files -- test`).split("\n") + (`git ls-files -- spec`).split("\n") s.extra_rdoc_files = %w( README.rdoc COPYING CHANGELOG ) s.add_dependency('mail', '~> 2.7', '>= 2.7.1') s.add_runtime_dependency('term-ansicolor', '~> 1.7', '>= 1.7.1') s.add_development_dependency('rspec', '~> 2.5', '>= 2.5.0') s.platform = Gem::Platform::RUBY s.required_ruby_version = '>= 2.0.0' } TaskJuggler-3.8.1/tasks/000077500000000000000000000000001473026623400151015ustar00rootroot00000000000000TaskJuggler-3.8.1/tasks/changelog.rake000066400000000000000000000077371473026623400177120ustar00rootroot00000000000000require 'time' CLOBBER.include "CHANGELOG" desc 'Generate the CHANGELOG file' task :changelog do class Entry attr_reader :type def initialize(ref, author, time, message) @ref = ref @author = author @time = time @message = message if (m = /New: (.*)/.match(@message)) @type = :feature @message = m[1] elsif (m = /Fix: (.*)/.match(@message)) @type = :bugfix @message = m[1] else @type = :other end end def to_s " * #{@message}\n" end end class Release attr_reader :date, :version, :tag def initialize(tag, predecessor) @tag = tag # We only support release tags in the form X.X.X @version = /\d+\.\d+\.\d+/.match(tag) # Construct a Git range. interval = predecessor ? "#{predecessor.tag}..#{@tag}" : @tag # Get the date of the release date = Time.parse(/Date: (.*)/.match(`git show #{tag}`)[1]).utc @date = date.strftime("%Y-%m-%d") @entries = [] # Use -z option for git-log to get 0 bytes as separators. `git log -z #{interval}`.split("\0").each do |commit| # We ignore merges. next if commit =~ /^Merge: \d*/ ref, author, time, _, message = commit.split("\n", 5) ref = ref[/commit ([0-9a-f]+)/, 1] author = author[/Author: (.*)/, 1].strip time = Time.parse(time[/Date: (.*)/, 1]).utc # Eleminate git-svn-id: lines message.gsub!(/git-svn-id: .*\n/, '') # Eliminate Signed-off-by: lines message.gsub!(/Signed-off-by: .*\n/, '') message.strip! @entries << Entry.new(ref, author, time, message) end end def empty? @entries.empty? end def to_s s = '' if hasFeatures? || hasFixes? if hasFeatures? s << "== New Features\n\n" @entries.each do |entry| s << entry.to_s if entry.type == :feature end s << "\n" end if hasFixes? s << "== Bug Fixes\n\n" @entries.each do |entry| s << entry.to_s if entry.type == :bugfix end s << "\n" end else @entries.each do |entry| s << entry.to_s end end s end private def hasFeatures? @entries.each do |entry| return true if entry.type == :feature end false end def hasFixes? @entries.each do |entry| return true if entry.type == :bugfix end false end end class ChangeLog def initialize @releases = [] predecessor = nil getReleaseVersions.each do |version| @releases << (predecessor = Release.new(version, predecessor)) end end def to_s s = '' @releases.reverse.each do |release| next if release.empty? # We use RDOC markup syntax to generate a title if release.version s << "= Release #{release.version} (#{release.date})\n\n" else s << "= Next Release (Some Day)\n\n" end s << release.to_s + "\n" end s end private # 'git tag' is not sorted numerically. This function implements a # numerical comparison for tag versions of the format 'release-X.X.X'. X # can be a multi-digit number. def compareTags(a, b) def versionToComparable(v) /\d+\.\d+\.\d+/.match(v)[0].split('.').map{ |l| sprintf("%03d", l.to_i)}. join('.') end versionToComparable(a) <=> versionToComparable(b) end def getReleaseVersions # Get list of release tags from Git repository releaseVersions = `git tag`.split("\n").map { |r| r.chomp }. delete_if { |r| ! (/release-\d+\.\d+\.\d+/ =~ r) }. sort{ |a, b| compareTags(a, b) } releaseVersions << 'HEAD' end end File.open('CHANGELOG', 'w+') do |changelog| changelog.puts ChangeLog.new.to_s end end TaskJuggler-3.8.1/tasks/gem.rake000066400000000000000000000036371473026623400165260ustar00rootroot00000000000000# GEM TASK require 'find' require 'rubygems' require 'rubygems/package' CLOBBER.include "pkg/" # Unfortunately Rake::GemPackageTest cannot deal with files that are generated # by Rake targets. So we have to write our own packaging task. desc 'Build the gem package' task :gem => [:clobber] do Rake::Task[:vim].invoke Rake::Task[:manual].invoke Rake::Task[:changelog].invoke Rake::Task[:permissions].invoke Rake::Task[:help2man].invoke load 'taskjuggler.gemspec'; # Build the gem file according to the loaded spec. if RUBY_VERSION >= "2.0.0" Gem::Package.build(GEM_SPEC) else Gem::Builder.new(GEM_SPEC).build end pkgBase = "#{GEM_SPEC.name}-#{GEM_SPEC.version}" # Create a pkg directory if it doesn't exist already. FileUtils.mkdir_p('pkg') # Move the gem file into the pkg directory. verbose(true) { FileUtils.mv("#{pkgBase}.gem", "pkg/#{pkgBase}.gem")} # Create a tar file with all files that are in the gem. FileUtils.rm_f("pkg/#{pkgBase}.tar") FileUtils.rm_f("pkg/#{pkgBase}.tar.gz") verbose(false) {GEM_SPEC.files.each { |f| `tar rf pkg/#{pkgBase}.tar "#{f}"` } } # And gzip the file. `gzip pkg/#{pkgBase}.tar` end desc 'Make sure all files and directories are readable' task :permissions do # Find the bin and test directories relative to this file. baseDir = File.expand_path('..', File.dirname(__FILE__)) execs = Dir.glob("#{baseDir}/bin/*") + Dir.glob("#{baseDir}/test/**/genrefs") Find.find(baseDir) do |f| # Ignore the whoke pkg directory as it may contain links to the other # directories. next if Regexp.new("#{baseDir}/pkg/*").match(f) FileUtils.chmod_R((FileTest.directory?(f) || execs.include?(f) ? 0755 : 0644), f) end end desc 'Run all tests and build scripts and create the gem package' task :release do Rake::Task[:test].invoke Rake::Task[:spec].invoke Rake::Task[:rdoc].invoke Rake::Task[:gem].invoke end TaskJuggler-3.8.1/tasks/help2man.rake000066400000000000000000000007461473026623400174620ustar00rootroot00000000000000# TASK MAN GENERATE CLOBBER.include "man" directory "man" desc 'Generate man pages from help' task :help2man => 'man' do help2man = %x{which help2man} help2man.chomp! Dir.foreach('bin') do |prog| next if prog == '.' or prog == '..' system help2man,"--output=man/#{prog}.1","--no-info","--manual=TaskJuggler",*("--include=h2m/#{prog}.h2m" unless !File.exist?("h2m/#{prog}.h2m")),"bin/#{prog}" FileUtils.chmod(0644, "man/#{prog}.1") end FileUtils.chmod(0755, 'man') end TaskJuggler-3.8.1/tasks/kate.rake000066400000000000000000000003271473026623400166730ustar00rootroot00000000000000# TASK Kate SYNTAX require 'taskjuggler/KateSyntax' CLOBBER.include "data/kate-tjp.xml" desc 'Generate kate-tjp.xml Kate syntax file' task :kate do TaskJuggler::KateSyntax.new.generate('data/kate-tjp.xml') end TaskJuggler-3.8.1/tasks/manual.rake000066400000000000000000000006561473026623400172310ustar00rootroot00000000000000# TASK MANUAL require 'taskjuggler/apps/Tj3Man' CLOBBER.include "manual/html/" desc 'Generate User Manual' task :manual do htmldir = 'manual/html' rm_rf htmldir if File.exist? htmldir mkdir_p htmldir # Make sure we can run 'rake manual' from all subdirs. ENV['TASKJUGGLER_DATA_PATH'] = Array.new(4) { |i| Dir.getwd + '/..' * i }.join(':') TaskJuggler::Tj3Man.new.main([ '-d', htmldir, '-m', '--silent' ]) end TaskJuggler-3.8.1/tasks/rdoc.rake000066400000000000000000000005111473026623400166710ustar00rootroot00000000000000if RUBY_VERSION < '1.9.3' require 'rake/rdoctask' else require 'rdoc/task' end # RDOC TASK Rake::RDocTask.new(:rdoc) do |t| t.rdoc_files = %w( README.rdoc COPYING CHANGELOG ) + `git ls-files -- lib`.split("\n") t.title = "TaskJuggler API documentation" t.main = 'README.rdoc' t.rdoc_dir = 'doc' end TaskJuggler-3.8.1/tasks/spec.rake000066400000000000000000000002531473026623400166770ustar00rootroot00000000000000require 'rake' require 'rspec/core/rake_task' desc 'Run all RSpec tests in the spec directory' RSpec::Core::RakeTask.new(:spec) do |t| t.pattern = 'spec/*_spec.rb' end TaskJuggler-3.8.1/tasks/test.rake000066400000000000000000000010461473026623400167250ustar00rootroot00000000000000$:.unshift File.join(File.dirname(__FILE__), '..', 'test') require 'rake/testtask' CLEAN.include "test/TestSuite/Export-Reports/refs/Leave.tjp" CLEAN.include "test/TestSuite/Export-Reports/refs/ListAttributes.tjp" CLEAN.include "test/TestSuite/Export-Reports/refs/Macro-4.tjp" CLEAN.include "test/TestSuite/Export-Reports/refs/TraceReport.tjp" # TEST TASK desc 'Run all unit tests in the test directory' Rake::TestTask.new(:unittest) do |t| t.libs << 'test' t.test_files = FileList['test/test_*.rb'] t.verbose = false t.warning = true end TaskJuggler-3.8.1/tasks/vim.rake000066400000000000000000000003041473026623400165350ustar00rootroot00000000000000# TASK VIM SYNTAX require 'taskjuggler/VimSyntax' CLOBBER.include "data/tjp.vim" desc 'Generate vim.tjp Vim syntax file' task :vim do TaskJuggler::VimSyntax.new.generate('data/tjp.vim') end TaskJuggler-3.8.1/test/000077500000000000000000000000001473026623400147335ustar00rootroot00000000000000TaskJuggler-3.8.1/test/MessageChecker.rb000066400000000000000000000040331473026623400201310ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = MessageChecker.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'taskjuggler/MessageHandler' module MessageChecker # Check that all messages that were generated during the TaskJuggler run # match the references specified in the test file. def checkMessages(tj, file) refMessages = collectMessages(file) TaskJuggler::MessageHandlerInstance.instance.messages.each do |message| assert(ref = refMessages.pop, "Unexpected #{message.type.to_s} #{message.id}: #{message}") assert_equal(ref[0], message.type.to_s, "Error in #{file}: Got #{message.type.to_s} instead of #{ref[0]}") assert_equal(ref[2], message.id, "Error in #{file}: Got #{message.id} instead of #{ref[2]}") if message.sourceFileInfo assert_equal(ref[1], message.sourceFileInfo.lineNo, "Error in #{file}: Got line #{message.sourceFileInfo.lineNo} " + "instead of #{ref[1]}") end end # Make sure that all reference messages have been generated. assert(refMessages.empty?, "Error in #{file}: missing #{refMessages.length} errors") end # All files that generate messages have comments in them that specify the # expected messages. The comments have the following form: # MARK: # We collect all these reference messages to compare them with the # generated messages after the test has been run. def collectMessages(file) refMessages = [] File.open(file) do |f| f.each_line do |line| if line =~ /^# MARK: ([a-z]+) ([0-9]+) ([a-z0-9_]*)/ refMessages << [ $1, $2.to_i, $3 ] end end end refMessages.reverse! end end TaskJuggler-3.8.1/test/ReferenceGenerator.rb000066400000000000000000000050351473026623400210300ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = ReferenceGenerator.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # # This script can be used to (re-)generate all reference reports in the # TaskJuggler test suite. These reports will be put in the /refs directories # in the TestSuite sub-directories. Usually, reference reports are generated # by hand and then manually checked for correctness before they are added to # the test suite. But sometimes changes in the syntax will require all # reference files to be regenerated. # Reference reports must use the following naming scheme: # -[0-9]+.(csv|html) $:.unshift File.join(File.dirname(__FILE__), '..', 'lib') if __FILE__ == $0 $:.unshift File.dirname(__FILE__) require 'fileutils' require 'taskjuggler/Tj3Config' require 'taskjuggler/TaskJuggler' class TaskJuggler class ReferenceGenerator def initialize AppConfig.appName = 'taskjuggler3' ENV['TASKJUGGLER_DATA_PATH'] = './:../' ENV['TZ'] = 'Europe/Berlin' end def generate processDirectory('ReportGenerator/Correct') end private def processProject(tjpFile, outputDir) deleteOldReports(tjpFile[0..-5]) puts "Generating references for #{tjpFile}" tj = TaskJuggler.new tj.parse([ tjpFile ]) || error("Parser failed for ${tjpFile}") tj.schedule || error("Scheduler failed for #{tjpFile}") tj.generateReports(outputDir) || error("Report generator failed for #{tjpFile}") unless tj.messageHandler.messages.empty? error("Unexpected error in #{tjpFile}") end end def processDirectory(dir) puts "Generating references in #{dir}" path = File.dirname(__FILE__) + '/' projectDir = path + "TestSuite/#{dir}/" outputDir = path + "TestSuite/#{dir}/refs/" Dir.glob(projectDir + '*.tjp').each do |f| processProject(f, outputDir) end end def deleteOldReports(basename) %w( .csv .html ).each do |ext| Dir.glob(basename + "-[0-9]*" + ext).each do |f| puts "Removing old report #{f}" File.delete(f) end end end def error(text) $stderr.puts text exit 1 end end ReferenceGenerator.new.generate end TaskJuggler-3.8.1/test/TestSuite/000077500000000000000000000000001473026623400166645ustar00rootroot00000000000000TaskJuggler-3.8.1/test/TestSuite/CSV-Reports/000077500000000000000000000000001473026623400207535ustar00rootroot00000000000000TaskJuggler-3.8.1/test/TestSuite/CSV-Reports/Leave.tjp000066400000000000000000000014021473026623400225230ustar00rootroot00000000000000project "Annual Leave" 2011-12-19 +1y { now 2012-07-01 } leaves holiday "Christmas" 2011-12-24 +3d, holiday "New Year" 2011-12-31 +3d shift s1 "Shift 1" { leaves annual 2011-12-19 +3w, special 2012-01-12 +1d } resource team "Team" { leaveallowances annual 2011-12-19 20d leaves holiday 2012-01-06 resource r1 "R1" { leaves annual 2011-12-19 +3w, special 2012-01-12 +1d } resource r2 "R2" { leaveallowances annual 2012-06-01 -10d leaves sick 2012-01-04 +2d, unpaid 2012-01-10 +3d } } resource r3 "R3" { shifts s1 2011-12-19 +3w leaves sick 2012-01-04 +2d } task "foo" resourcereport "." { formats csv columns name, annualleave, annualleavebalance, sickleave, specialleave, unpaidleave } TaskJuggler-3.8.1/test/TestSuite/CSV-Reports/alert.tjp000066400000000000000000000001611473026623400225770ustar00rootroot00000000000000project "Alert" 2011-03-07 +2m include "project-1.tji" taskreport '.' { formats csv columns name, alert } TaskJuggler-3.8.1/test/TestSuite/CSV-Reports/celltext.tjp000066400000000000000000000003201473026623400233110ustar00rootroot00000000000000project test "Test" "1.0" 2007-12-16 +3m include "project-1.tji" taskreport csv "." { formats csv timezone "Europe/Amsterdam" columns bsi, name { celltext 1 "<-query-> (<-query attribute='id'->)" } } TaskJuggler-3.8.1/test/TestSuite/CSV-Reports/efficiency.tjp000066400000000000000000000004161473026623400235770ustar00rootroot00000000000000project "Efficiency Test" 2011-12-12 +2m resource "R1" { efficiency 0.7 } resource "T1" { resource "R2" { efficiency 0.0 } resource "R3" { efficiency 2.0 } } task "Foo" resourcereport "." { formats csv columns name, efficiency, fte, headcount } TaskJuggler-3.8.1/test/TestSuite/CSV-Reports/headcount.tjp000066400000000000000000000007751473026623400234550ustar00rootroot00000000000000project "Headcount Test" 2011-12-12 +2m resource r1 "R1" { efficiency 0.7 } resource "T1" { resource r2 "R2" { efficiency 0.0 } resource r3 "R3" { efficiency 2.0 vacation 2012-01-01 +10d } } task "Project" { task "Foo" { effort 40d allocate r1, r2, r3 } task "Bar" { effort 40d allocate r3, r1 } } taskreport "." { formats csv columns name, efficiency, headcount, weekly { celltext 1 "<-query attribute='headcount'->" } sorttasks tree, id.up } TaskJuggler-3.8.1/test/TestSuite/CSV-Reports/inputs.tjp000066400000000000000000000003311473026623400230110ustar00rootroot00000000000000project "inputs" "1.0" 2007-12-16 +3m task t1 "T1" task t2 "T2" { depends !t1 } task t3 "T3" task t4 "T4" { depends !t3 } task t5 "T5" { depends !t4 } taskreport '.' { formats csv columns name, inputs } TaskJuggler-3.8.1/test/TestSuite/CSV-Reports/niku.tjp000066400000000000000000000027621473026623400224470ustar00rootroot00000000000000project "Niku Test" 2010-02-01 +3m { extend task { text ClarityPID "Clarity PID" text ClarityPName "Clarity Project Name" } extend resource { text ClarityRID "Clarity Resource ID" } } # The ClarityPID and ClarityPName must be always kept in sync. The # easiest way to achieve this, is by using such macros. macro PID_p1 [ ClarityPID "p1" ClarityPName "Project 1" ] macro PID_p2 [ ClarityPID "p2" ClarityPName "Project 2" ] macro PID_p3 [ ClarityPID "p3" ClarityPName "Project 3" ] macro Resource [ resource ${1} "${1}" { ClarityRID "${1}" } ] ${Resource "r1"} ${Resource "r2"} supplement resource r2 { vacation 2010-02-15 +1w } ${Resource "r3"} task "T1" { allocate r1 effort 5w ${PID_p1} } task t2 "T2" { allocate r2 effort 10d ${PID_p1} } task "T3" { depends !t2 allocate r2 effort 10d ${PID_p2} } task "T4" { allocate r3 effort 3w ${PID_p2} } nikureport "." { formats csv headline "This is a test report" period 2010-02-01-8:00 - %{2010-03-01 -6h} timeoff "vacations" "Vacation time" # Depending on your Clarity configuration, you may need to add this # CustomInformation section in the report. It's raw XML code and # will be embedded into each section of the resulting # report. This is just an example and it must be customized to work! title -8<- foo_active foo_eng ->8- } TaskJuggler-3.8.1/test/TestSuite/CSV-Reports/project-1.tji000066400000000000000000000017261473026623400232750ustar00rootroot00000000000000resource jill "Jill" resource joe "Joe" resource jack "Jack" task plan "Plan Work" { start ${projectstart} task plan_a "Plan A" { effort 2w allocate joe } task plan_b "Plan B" { effort 1.5w allocate jill } task plan_c "Plan C" { depends !plan_a allocate jill effort 1w } } task execute "Execute Work" { depends !plan task ex1 "Step 1" { allocate jill effort 4w } task ex2 "Step 2" { task ex2_1 "Step 2.1" { allocate joe effort 2d } task ex2_2 "Step 2.2" { depends !!ex1 effort 2w allocate jack, jill journalentry %{${projectstart} +2d} "Red" { alert red } } } task ms1 "Milestone 1" { depends !ex2 } task ex3 "Step 3" { depends execute.ex2.ex2_1 effort 12d allocate jack } task ex4 "Step 4" { depends !ex1 effort 2d allocate joe, jack } } task check "Check Work" { depends !execute allocate jill effort 1w } TaskJuggler-3.8.1/test/TestSuite/CSV-Reports/quotes.tjp000066400000000000000000000003621473026623400230130ustar00rootroot00000000000000project "targets" "1.0" 2007-12-16 +3m { timezone 'UTC' } task t1 "T1" task t2 "T2" { depends !t1 } task t3 "T3" { depends !t1 } task t4 "T4 with \"quotes\"" { depends !t3 } taskreport '.' { formats csv columns name, targets } TaskJuggler-3.8.1/test/TestSuite/CSV-Reports/refs/000077500000000000000000000000001473026623400217125ustar00rootroot00000000000000TaskJuggler-3.8.1/test/TestSuite/CSV-Reports/refs/Leave.csv000066400000000000000000000003101473026623400234550ustar00rootroot00000000000000"Name";"Annual Leave";"Annual Leave Balance";"Sick Leave";"Special Leave";"Unpaid Leave" "R3";11.0;-11.0;2.0;0.0;0.0 "Team";12.0;"";2.0;1.0;3.0 " R1";12.0;8.0;0.0;1.0;0.0 " R2";0.0;10.0;2.0;0.0;3.0 TaskJuggler-3.8.1/test/TestSuite/CSV-Reports/refs/alert.csv000066400000000000000000000004541473026623400235410ustar00rootroot00000000000000"Name";"Alert" "Plan Work";"Green" " Plan A";" Green" " Plan B";" Green" " Plan C";" Green" "Execute Work";"Red" " Step 1";" Green" " Step 2";" Red" " Step 2.1";" Green" " Step 2.2";" Red" " Step 3";" Green" " Step 4";" Green" " Milestone 1";" Green" "Check Work";"Green" TaskJuggler-3.8.1/test/TestSuite/CSV-Reports/refs/celltext.csv000066400000000000000000000006551473026623400242610ustar00rootroot00000000000000"BSI";"Name" "1";"Plan Work (plan)" "1.1";" Plan A (plan.plan_a)" "1.2";" Plan B (plan.plan_b)" "1.3";" Plan C (plan.plan_c)" "2";"Execute Work (execute)" "2.1";" Step 1 (execute.ex1)" "2.2";" Step 2 (execute.ex2)" "2.2.1";" Step 2.1 (execute.ex2.ex2_1)" "2.2.2";" Step 2.2 (execute.ex2.ex2_2)" "2.4";" Step 3 (execute.ex3)" "2.5";" Step 4 (execute.ex4)" "2.3";" Milestone 1 (execute.ms1)" "3";"Check Work (check)" TaskJuggler-3.8.1/test/TestSuite/CSV-Reports/refs/efficiency.csv000066400000000000000000000001561473026623400245350ustar00rootroot00000000000000"Name";"Efficiency";"FTE";"Headcount" "R1";0.7;0.7;1.0 "T1";1.0;2.0;2.0 " R2";0.0;0.0;0.0 " R3";2.0;2.0;2.0 TaskJuggler-3.8.1/test/TestSuite/CSV-Reports/refs/headcount.csv000066400000000000000000000004601473026623400244010ustar00rootroot00000000000000"Name";"Efficiency";"Headcount";"2011-12-05";"2011-12-12";"2011-12-19";"2011-12-26";"2012-01-02";"2012-01-09";"2012-01-16";"2012-01-23";"2012-01-30" "Project";"";3.0;0.0;3.0;3.0;3.0;1.0;3.0;3.0;3.0;0.0 " Foo";"";3.0;0.0;0.0;0.0;3.0;1.0;3.0;3.0;3.0;0.0 " Bar";"";3.0;0.0;3.0;3.0;3.0;0.0;0.0;0.0;0.0;0.0 TaskJuggler-3.8.1/test/TestSuite/CSV-Reports/refs/inputs.csv000066400000000000000000000001561473026623400237530ustar00rootroot00000000000000"Name";"Inputs" "T1";"" "T2";"T1 (t1) 2007-12-16" "T3";"" "T4";"T3 (t3) 2007-12-16" "T5";"T3 (t3) 2007-12-16" TaskJuggler-3.8.1/test/TestSuite/CSV-Reports/refs/niku.csv000066400000000000000000000002221473026623400233710ustar00rootroot00000000000000"";"Project 1";"Project 2";"Vacation time" "Resource";"p1";"p2";"vacations" "r1 (r1)";1.0;0.0;0.0 "r2 (r2)";0.5;0.25;0.25 "r3 (r3)";0.0;0.75;0.25 TaskJuggler-3.8.1/test/TestSuite/CSV-Reports/refs/quotes.csv000066400000000000000000000002311473026623400237430ustar00rootroot00000000000000"Name";"Targets" "T1";"T2 (t2) 2007-12-16, T4 with ""quotes"" (t4) 2007-12-16" "T2";"" "T3";"T4 with ""quotes"" (t4) 2007-12-16" "T4 with ""quotes""";"" TaskJuggler-3.8.1/test/TestSuite/CSV-Reports/refs/resourcereport.csv000066400000000000000000000007511473026623400255150ustar00rootroot00000000000000"BSI";"Name";"Effort";"2007-12-17-00:00:00";"2007-12-24-00:00:00";"2007-12-31-00:00:00";"2008-01-07-00:00:00";"2008-01-14-00:00:00";"2008-01-21-00:00:00";"2008-01-28-00:00:00";"2008-02-04-00:00:00";"2008-02-11-00:00:00";"2008-02-18-00:00:00";"2008-02-25-00:00:00";"2008-03-03-00:00:00";"2008-03-10-00:00:00" "3";"Jack";17.0;"";"";"";3.0;5.0;4.0;"";5.0;"";"";"";"";"" "1";"Jill";42.5;5.0;2.5;5.0;5.0;5.0;5.0;5.0;5.0;5.0;"";"";"";"" "2";"Joe";14.0;5.0;5.0;"";2.0;"";"";"";2.0;"";"";"";"";"" TaskJuggler-3.8.1/test/TestSuite/CSV-Reports/refs/resourcereport_with_tasks.csv000066400000000000000000000027341473026623400277600ustar00rootroot00000000000000"BSI";"Name";"Effort";"2007-12-10-00:00:00";"2007-12-17-00:00:00";"2007-12-24-00:00:00";"2007-12-31-00:00:00";"2008-01-07-00:00:00";"2008-01-14-00:00:00";"2008-01-21-00:00:00";"2008-01-28-00:00:00";"2008-02-04-00:00:00";"2008-02-11-00:00:00";"2008-02-18-00:00:00" "3";"Jack";17.0;"";"";"";"";3.0;5.0;4.0;"";5.0;"";"" "2";" Execute Work";17.0;"";"";"";"";3.0;5.0;4.0;"";5.0;"";"" "2.2";" Step 2";5.0;"";"";"";"";"";"";"";"";5.0;"";"" "2.2.2";" Step 2.2";5.0;"";"";"";"";"";"";"";"";5.0;"";"" "2.4";" Step 3";12.0;"";"";"";"";3.0;5.0;4.0;"";"";"";"" "1";"Jill";42.5;"";5.0;2.5;5.0;5.0;5.0;5.0;5.0;5.0;5.0;"" "1";" Plan Work";12.5;"";5.0;2.5;5.0;"";"";"";"";"";"";"" "1.2";" Plan B";7.5;"";5.0;2.5;"";"";"";"";"";"";"";"" "1.3";" Plan C";5.0;"";"";"";5.0;"";"";"";"";"";"";"" "2";" Execute Work";25.0;"";"";"";"";5.0;5.0;5.0;5.0;5.0;"";"" "2.1";" Step 1";20.0;"";"";"";"";5.0;5.0;5.0;5.0;"";"";"" "2.2";" Step 2";5.0;"";"";"";"";"";"";"";"";5.0;"";"" "2.2.2";" Step 2.2";5.0;"";"";"";"";"";"";"";"";5.0;"";"" "3";" Check Work";5.0;"";"";"";"";"";"";"";"";"";5.0;"" "2";"Joe";14.0;"";5.0;5.0;"";2.0;"";"";"";2.0;"";"" "1";" Plan Work";10.0;"";5.0;5.0;"";"";"";"";"";"";"";"" "1.1";" Plan A";10.0;"";5.0;5.0;"";"";"";"";"";"";"";"" "2";" Execute Work";4.0;"";"";"";"";2.0;"";"";"";2.0;"";"" "2.2";" Step 2";2.0;"";"";"";"";2.0;"";"";"";"";"";"" "2.2.1";" Step 2.1";2.0;"";"";"";"";2.0;"";"";"";"";"";"" "2.5";" Step 4";2.0;"";"";"";"";"";"";"";"";2.0;"";"" TaskJuggler-3.8.1/test/TestSuite/CSV-Reports/refs/sortByTree.csv000066400000000000000000000007671473026623400245430ustar00rootroot00000000000000"BSI";"Name";"Id";"Duration" "1";"Plan Work";"plan";19.8 "1.1";" Plan A";"plan.plan_a";11.4 "1.2";" Plan B";"plan.plan_b";9.2 "1.3";" Plan C";"plan.plan_c";4.4 "2";"Execute Work";"execute";32.4 "2.1";" Step 1";"execute.ex1";25.4 "2.2";" Step 2";"execute.ex2";32.4 "2.2.1";" Step 2.1";"execute.ex2.ex2_1";1.4 "2.2.2";" Step 2.2";"execute.ex2.ex2_2";4.4 "2.3";" Milestone 1";"execute.ms1";0.0 "2.4";" Step 3";"execute.ex3";15.4 "2.5";" Step 4";"execute.ex4";1.4 "3";"Check Work";"check";4.4 TaskJuggler-3.8.1/test/TestSuite/CSV-Reports/refs/sortBy_duration.down.csv000066400000000000000000000003731473026623400265670ustar00rootroot00000000000000"Duration";"Id" 32.4;"execute" 32.4;"execute.ex2" 25.4;"execute.ex1" 19.8;"plan" 15.4;"execute.ex3" 11.4;"plan.plan_a" 9.2;"plan.plan_b" 4.4;"check" 4.4;"execute.ex2.ex2_2" 4.4;"plan.plan_c" 1.4;"execute.ex2.ex2_1" 1.4;"execute.ex4" 0.0;"execute.ms1" TaskJuggler-3.8.1/test/TestSuite/CSV-Reports/refs/sortBy_effort.up.csv000066400000000000000000000005141473026623400257010ustar00rootroot00000000000000"Effort";"BSI";"Id" 0.0;"2.3";"execute.ms1" 2.0;"2.2.1";"execute.ex2.ex2_1" 2.0;"2.5";"execute.ex4" 5.0;"3";"check" 5.0;"1.3";"plan.plan_c" 7.5;"1.2";"plan.plan_b" 10.0;"2.2.2";"execute.ex2.ex2_2" 10.0;"1.1";"plan.plan_a" 12.0;"2.2";"execute.ex2" 12.0;"2.4";"execute.ex3" 20.0;"2.1";"execute.ex1" 22.5;"1";"plan" 46.0;"2";"execute" TaskJuggler-3.8.1/test/TestSuite/CSV-Reports/refs/sortBy_plan.start.down.csv000066400000000000000000000011741473026623400270300ustar00rootroot00000000000000"Start";"BSI";"Id" "2008-02-11-10:00:00-+0100";"3";"check" "2008-02-08-19:00:00-+0100";"2.3";"execute.ms1" "2008-02-04-10:00:00-+0100";"2.2.2";"execute.ex2.ex2_2" "2008-02-04-10:00:00-+0100";"2.5";"execute.ex4" "2008-01-09-10:00:00-+0100";"2.4";"execute.ex3" "2008-01-07-10:00:00-+0100";"2";"execute" "2008-01-07-10:00:00-+0100";"2.1";"execute.ex1" "2008-01-07-10:00:00-+0100";"2.2";"execute.ex2" "2008-01-07-10:00:00-+0100";"2.2.1";"execute.ex2.ex2_1" "2007-12-31-10:00:00-+0100";"1.3";"plan.plan_c" "2007-12-17-10:00:00-+0100";"1.1";"plan.plan_a" "2007-12-17-10:00:00-+0100";"1.2";"plan.plan_b" "2007-12-16-01:00:00-+0100";"1";"plan" TaskJuggler-3.8.1/test/TestSuite/CSV-Reports/refs/targets.csv000066400000000000000000000001511473026623400240750ustar00rootroot00000000000000"Name";"Targets" "T1";"T2 (t2) 2007-12-16, T4 (t4) 2007-12-16" "T2";"" "T3";"T4 (t4) 2007-12-16" "T4";"" TaskJuggler-3.8.1/test/TestSuite/CSV-Reports/refs/taskreport.csv000066400000000000000000000035541473026623400246340ustar00rootroot00000000000000"BSI";"Name";"Start";"End";"Duration";"Effort";"2007-12-10-00:00:00-+0100";"2007-12-17-00:00:00-+0100";"2007-12-24-00:00:00-+0100";"2007-12-31-00:00:00-+0100";"2008-01-07-00:00:00-+0100";"2008-01-14-00:00:00-+0100";"2008-01-21-00:00:00-+0100";"2008-01-28-00:00:00-+0100";"2008-02-04-00:00:00-+0100";"2008-02-11-00:00:00-+0100";"2008-02-18-00:00:00-+0100" "1";"Plan Work";"2007-12-16-01:00:00-+0100";"2008-01-04-19:00:00-+0100";19.8;22.5;"";10.0;7.5;5.0;"";"";"";"";"";"";"" "1.1";" Plan A";"2007-12-17-10:00:00-+0100";"2007-12-28-19:00:00-+0100";11.4;10.0;"";5.0;5.0;"";"";"";"";"";"";"";"" "1.2";" Plan B";"2007-12-17-10:00:00-+0100";"2007-12-26-15:00:00-+0100";9.2;7.5;"";5.0;2.5;"";"";"";"";"";"";"";"" "1.3";" Plan C";"2007-12-31-10:00:00-+0100";"2008-01-04-19:00:00-+0100";4.4;5.0;"";"";"";5.0;"";"";"";"";"";"";"" "2";"Execute Work";"2008-01-07-10:00:00-+0100";"2008-02-08-19:00:00-+0100";32.4;46.0;"";"";"";"";10.0;10.0;9.0;5.0;12.0;"";"" "2.1";" Step 1";"2008-01-07-10:00:00-+0100";"2008-02-01-19:00:00-+0100";25.4;20.0;"";"";"";"";5.0;5.0;5.0;5.0;"";"";"" "2.2";" Step 2";"2008-01-07-10:00:00-+0100";"2008-02-08-19:00:00-+0100";32.4;12.0;"";"";"";"";2.0;"";"";"";10.0;"";"" "2.2.1";" Step 2.1";"2008-01-07-10:00:00-+0100";"2008-01-08-19:00:00-+0100";1.4;2.0;"";"";"";"";2.0;"";"";"";"";"";"" "2.2.2";" Step 2.2";"2008-02-04-10:00:00-+0100";"2008-02-08-19:00:00-+0100";4.4;10.0;"";"";"";"";"";"";"";"";10.0;"";"" "2.4";" Step 3";"2008-01-09-10:00:00-+0100";"2008-01-24-19:00:00-+0100";15.4;12.0;"";"";"";"";3.0;5.0;4.0;"";"";"";"" "2.5";" Step 4";"2008-02-04-10:00:00-+0100";"2008-02-05-19:00:00-+0100";1.4;2.0;"";"";"";"";"";"";"";"";2.0;"";"" "2.3";" Milestone 1";"2008-02-08-19:00:00-+0100";"2008-02-08-19:00:00-+0100";0.0;0.0;"";"";"";"";"";"";"";"";"";"";"" "3";"Check Work";"2008-02-11-10:00:00-+0100";"2008-02-15-19:00:00-+0100";4.4;5.0;"";"";"";"";"";"";"";"";"";5.0;"" TaskJuggler-3.8.1/test/TestSuite/CSV-Reports/refs/taskreport_with_resources.csv000066400000000000000000000057251473026623400277630ustar00rootroot00000000000000"BSI";"Name";"Start";"End";"Duration";"Effort";"2007-12-10-00:00:00-+0100";"2007-12-17-00:00:00-+0100";"2007-12-24-00:00:00-+0100";"2007-12-31-00:00:00-+0100";"2008-01-07-00:00:00-+0100";"2008-01-14-00:00:00-+0100";"2008-01-21-00:00:00-+0100";"2008-01-28-00:00:00-+0100";"2008-02-04-00:00:00-+0100";"2008-02-11-00:00:00-+0100";"2008-02-18-00:00:00-+0100" "1";"Plan Work";"2007-12-16-01:00:00-+0100";"2008-01-04-19:00:00-+0100";19.8;22.5;"";10.0;7.5;5.0;"";"";"";"";"";"";"" "1";" Jill";"";"";"";12.5;"";5.0;2.5;5.0;"";"";"";"";"";"";"" "2";" Joe";"";"";"";10.0;"";5.0;5.0;"";"";"";"";"";"";"";"" "1.1";" Plan A";"2007-12-17-10:00:00-+0100";"2007-12-28-19:00:00-+0100";11.4;10.0;"";5.0;5.0;"";"";"";"";"";"";"";"" "2";" Joe";"";"";"";10.0;"";5.0;5.0;"";"";"";"";"";"";"";"" "1.2";" Plan B";"2007-12-17-10:00:00-+0100";"2007-12-26-15:00:00-+0100";9.2;7.5;"";5.0;2.5;"";"";"";"";"";"";"";"" "1";" Jill";"";"";"";7.5;"";5.0;2.5;"";"";"";"";"";"";"";"" "1.3";" Plan C";"2007-12-31-10:00:00-+0100";"2008-01-04-19:00:00-+0100";4.4;5.0;"";"";"";5.0;"";"";"";"";"";"";"" "1";" Jill";"";"";"";5.0;"";"";"";5.0;"";"";"";"";"";"";"" "2";"Execute Work";"2008-01-07-10:00:00-+0100";"2008-02-08-19:00:00-+0100";32.4;46.0;"";"";"";"";10.0;10.0;9.0;5.0;12.0;"";"" "3";" Jack";"";"";"";17.0;"";"";"";"";3.0;5.0;4.0;"";5.0;"";"" "1";" Jill";"";"";"";25.0;"";"";"";"";5.0;5.0;5.0;5.0;5.0;"";"" "2";" Joe";"";"";"";4.0;"";"";"";"";2.0;"";"";"";2.0;"";"" "2.1";" Step 1";"2008-01-07-10:00:00-+0100";"2008-02-01-19:00:00-+0100";25.4;20.0;"";"";"";"";5.0;5.0;5.0;5.0;"";"";"" "1";" Jill";"";"";"";20.0;"";"";"";"";5.0;5.0;5.0;5.0;"";"";"" "2.2";" Step 2";"2008-01-07-10:00:00-+0100";"2008-02-08-19:00:00-+0100";32.4;12.0;"";"";"";"";2.0;"";"";"";10.0;"";"" "3";" Jack";"";"";"";5.0;"";"";"";"";"";"";"";"";5.0;"";"" "1";" Jill";"";"";"";5.0;"";"";"";"";"";"";"";"";5.0;"";"" "2";" Joe";"";"";"";2.0;"";"";"";"";2.0;"";"";"";"";"";"" "2.2.1";" Step 2.1";"2008-01-07-10:00:00-+0100";"2008-01-08-19:00:00-+0100";1.4;2.0;"";"";"";"";2.0;"";"";"";"";"";"" "2";" Joe";"";"";"";2.0;"";"";"";"";2.0;"";"";"";"";"";"" "2.2.2";" Step 2.2";"2008-02-04-10:00:00-+0100";"2008-02-08-19:00:00-+0100";4.4;10.0;"";"";"";"";"";"";"";"";10.0;"";"" "3";" Jack";"";"";"";5.0;"";"";"";"";"";"";"";"";5.0;"";"" "1";" Jill";"";"";"";5.0;"";"";"";"";"";"";"";"";5.0;"";"" "2.4";" Step 3";"2008-01-09-10:00:00-+0100";"2008-01-24-19:00:00-+0100";15.4;12.0;"";"";"";"";3.0;5.0;4.0;"";"";"";"" "3";" Jack";"";"";"";12.0;"";"";"";"";3.0;5.0;4.0;"";"";"";"" "2.5";" Step 4";"2008-02-04-10:00:00-+0100";"2008-02-05-19:00:00-+0100";1.4;2.0;"";"";"";"";"";"";"";"";2.0;"";"" "2";" Joe";"";"";"";2.0;"";"";"";"";"";"";"";"";2.0;"";"" "2.3";" Milestone 1";"2008-02-08-19:00:00-+0100";"2008-02-08-19:00:00-+0100";0.0;0.0;"";"";"";"";"";"";"";"";"";"";"" "3";"Check Work";"2008-02-11-10:00:00-+0100";"2008-02-15-19:00:00-+0100";4.4;5.0;"";"";"";"";"";"";"";"";"";5.0;"" "1";" Jill";"";"";"";5.0;"";"";"";"";"";"";"";"";"";5.0;"" TaskJuggler-3.8.1/test/TestSuite/CSV-Reports/refs/weekly.csv000066400000000000000000000011501473026623400237240ustar00rootroot00000000000000"Name";"2007-12-31";"2008-01-07";"2008-01-14";"Duration";"2007-12-31";"2008-01-07";"2008-01-14";"Effort" "Jack";"";3.0;5.0;"";"";3.0;5.0;8.0 " Execute Work";"";3.0;5.0;32.4;"";3.0;5.0;8.0 " Step 3";"";3.0;5.0;15.4;"";3.0;5.0;8.0 "Jill";5.0;5.0;5.0;"";5.0;5.0;5.0;15.0 " Plan Work";5.0;"";"";19.8;5.0;"";"";5.0 " Plan C";5.0;"";"";4.4;5.0;"";"";5.0 " Execute Work";"";5.0;5.0;32.4;"";5.0;5.0;10.0 " Step 1";"";5.0;5.0;25.4;"";5.0;5.0;10.0 "Joe";"";2.0;"";"";"";2.0;"";2.0 " Execute Work";"";2.0;"";32.4;"";2.0;"";2.0 " Step 2";"";2.0;"";32.4;"";2.0;"";2.0 " Step 2.1";"";2.0;"";1.4;"";2.0;"";2.0 TaskJuggler-3.8.1/test/TestSuite/CSV-Reports/resourcereport.tjp000066400000000000000000000003401473026623400245520ustar00rootroot00000000000000project test "Test" "1.0" 2007-12-16 +3m include "project-1.tji" resourcereport csv "." { formats csv timezone "Europe/Amsterdam" columns bsi, name, effort, weekly timeformat "%Y-%m-%d-%H:%M:%S" loadunit days } TaskJuggler-3.8.1/test/TestSuite/CSV-Reports/resourcereport_with_tasks.tjp000066400000000000000000000003551473026623400270200ustar00rootroot00000000000000project test "Test" "1.0" 2007-12-16 +3m include "project-1.tji" resourcereport csv "." { formats csv timezone "Europe/Amsterdam" columns bsi, name, effort, weekly timeformat "%Y-%m-%d-%H:%M:%S" loadunit days hidetask 0 } TaskJuggler-3.8.1/test/TestSuite/CSV-Reports/sortByTree.tjp000066400000000000000000000003621473026623400235750ustar00rootroot00000000000000project test "Test" "1.0" 2007-12-16 +3m { workinghours mon - fri 9:00 - 12:00, 13:00 - 18:00 } include "project-1.tji" taskreport cvs "." { formats csv timezone "Europe/Amsterdam" columns bsi, name, id, duration sorttasks tree } TaskJuggler-3.8.1/test/TestSuite/CSV-Reports/sortBy_duration.down.tjp000066400000000000000000000004371473026623400256330ustar00rootroot00000000000000project test "Test" "1.0" 2007-12-16 +3m { workinghours mon - fri 9:00 - 12:00, 13:00 - 18:00 } include "project-1.tji" taskreport csv "." { formats csv timezone "Europe/Amsterdam" columns duration, id timeformat "%Y-%m-%d-%H:%M:%S" sorttasks plan.duration.down, id.up } TaskJuggler-3.8.1/test/TestSuite/CSV-Reports/sortBy_effort.up.tjp000066400000000000000000000003441473026623400247450ustar00rootroot00000000000000project test "Test" "1.0" 2007-12-16 +3m include "project-1.tji" taskreport csv "." { formats csv timezone "Europe/Amsterdam" columns effort, bsi, id timeformat "%Y-%m-%d-%H:%M:%S" sorttasks plan.effort.up, id.up } TaskJuggler-3.8.1/test/TestSuite/CSV-Reports/sortBy_plan.start.down.tjp000066400000000000000000000004411473026623400260670ustar00rootroot00000000000000project test "Test" "1.0" 2007-12-16 +3m { workinghours mon - fri 9:00 - 12:00, 13:00 - 18:00 } include "project-1.tji" taskreport csv "." { formats csv timezone "Europe/Amsterdam" columns start, bsi, id timeformat "%Y-%m-%d-%H:%M:%S-%z" sorttasks plan.start.down, id.up } TaskJuggler-3.8.1/test/TestSuite/CSV-Reports/targets.tjp000066400000000000000000000003421473026623400231420ustar00rootroot00000000000000project "targets" "1.0" 2007-12-16 +3m { timezone 'UTC' } task t1 "T1" task t2 "T2" { depends !t1 } task t3 "T3" { depends !t1 } task t4 "T4" { depends !t3 } taskreport '.' { formats csv columns name, targets } TaskJuggler-3.8.1/test/TestSuite/CSV-Reports/taskreport.tjp000066400000000000000000000004561473026623400236750ustar00rootroot00000000000000project test "Test" "1.0" 2007-12-16 +3m { workinghours mon - fri 9:00 - 12:00, 13:00 - 18:00 } include "project-1.tji" taskreport csv "." { formats csv timezone 'Europe/Amsterdam' columns bsi, name, start, end, duration, effort, weekly timeformat "%Y-%m-%d-%H:%M:%S-%z" loadunit days } TaskJuggler-3.8.1/test/TestSuite/CSV-Reports/taskreport_with_resources.tjp000066400000000000000000000004771473026623400270250ustar00rootroot00000000000000project test "Test" "1.0" 2007-12-16 +3m { workinghours mon - fri 9:00 - 12:00, 13:00 - 18:00 } include "project-1.tji" taskreport csv "." { formats csv timezone "Europe/Amsterdam" columns bsi, name, start, end, duration, effort, weekly timeformat "%Y-%m-%d-%H:%M:%S-%z" loadunit days hideresource 0 } TaskJuggler-3.8.1/test/TestSuite/CSV-Reports/weekly.tjp000066400000000000000000000004561473026623400227770ustar00rootroot00000000000000project "Weekly" "1.0" 2007-12-16 +3m { timezone 'UTC' workinghours mon - fri 9:00 - 12:00, 13:00 - 18:00 } include "project-1.tji" resourcereport '.' { formats csv timezone "Europe/Amsterdam" columns name, weekly, duration, weekly, effort period 2007-12-31-0:00-+0100 + 3w hidetask 0 } TaskJuggler-3.8.1/test/TestSuite/Export-Reports/000077500000000000000000000000001473026623400216015ustar00rootroot00000000000000TaskJuggler-3.8.1/test/TestSuite/Export-Reports/export.tji000066400000000000000000000001321473026623400236260ustar00rootroot00000000000000export "." { timezone "UTC" definitions * taskattributes * resourceattributes * } TaskJuggler-3.8.1/test/TestSuite/Export-Reports/refs/000077500000000000000000000000001473026623400225405ustar00rootroot00000000000000TaskJuggler-3.8.1/test/TestSuite/Export-Reports/refs/Account.tjp000066400000000000000000000044001473026623400246510ustar00rootroot00000000000000project simple "Simple Project" "1.0" 2007-01-01-00:00-+0000 - 2007-01-30-00:00-+0000 { timezone "America/Denver" scenario plan "Plan Scenario" { active yes } } projectids simple resource tux "Tux" resource konqui "Konqui" task items "Room decoration" { task plan "Plan work and buy material" { start 2007-01-06-07:00-+0000 end 2007-01-10-00:00-+0000 scheduling asap scheduled } task remove "Remove old inventory" { depends items.plan start 2007-01-10-16:00-+0000 end 2007-01-10-20:00-+0000 scheduling asap scheduled } task implement "Arrange new decoration" { depends items.remove start 2007-01-10-20:00-+0000 end 2007-01-13-00:00-+0000 scheduling asap scheduled } task acceptance "Presentation and customer acceptance" { depends items.implement start 2007-01-13-00:00-+0000 end 2007-01-18-00:00-+0000 scheduling asap scheduled } } supplement task items { priority 500 projectid simple } supplement task items.plan { priority 500 projectid simple } supplement task items.remove { booking konqui 2007-01-10-16:00-+0000 + 4.0h { overtime 2 } booking tux 2007-01-10-16:00-+0000 + 4.0h { overtime 2 } priority 500 projectid simple } supplement task items.implement { booking konqui 2007-01-10-20:00-+0000 + 4.0h, 2007-01-11-16:00-+0000 + 8.0h, 2007-01-12-16:00-+0000 + 8.0h { overtime 2 } booking tux 2007-01-10-20:00-+0000 + 4.0h, 2007-01-11-16:00-+0000 + 8.0h, 2007-01-12-16:00-+0000 + 8.0h { overtime 2 } priority 500 projectid simple } supplement task items.acceptance { priority 500 projectid simple } supplement resource tux { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } supplement resource konqui { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } TaskJuggler-3.8.1/test/TestSuite/Export-Reports/refs/AccountReport.tjp000066400000000000000000001236021473026623400260530ustar00rootroot00000000000000project prj "AccountReport" "1.0" 2011-11-09-00:00-+0000 - 2012-11-08-00:00-+0000 { timezone "UTC" scenario plan "Plan Scenario" { active yes } } projectids prj resource teamA "Team A" { resource _Resource_2 "R1" resource _Resource_3 "R2" } resource teamB "Team B" { resource _Resource_5 "R3" resource _Resource_6 "R4" } task _Task_1 "Products" { task p1 "Product 1" { start 2012-02-08-10:00-+0000 end 2012-10-08-17:00-+0000 scheduling asap scheduled } task p2 "Product 2" { start 2011-11-09-09:00-+0000 end 2012-08-08-14:00-+0000 scheduling asap scheduled } task p3 "Product 3" { start 2011-11-09-09:00-+0000 end 2012-02-08-11:00-+0000 scheduling asap scheduled } task mf "Manufacturing" { depends _Task_1.p1, _Task_1.p2, _Task_1.p3 start 2012-10-08-17:00-+0000 end 2012-10-22-17:00-+0000 scheduling asap scheduled } task _Task_6 "Final Payment" { depends _Task_1.mf start 2012-10-22-17:00-+0000 scheduled } } supplement task _Task_1 { priority 500 projectid prj } supplement task _Task_1.p1 { booking _Resource_2 2012-08-08-14:00-+0000 + 3.0h, 2012-08-09-09:00-+0000 + 8.0h, 2012-08-10-09:00-+0000 + 8.0h, 2012-08-13-09:00-+0000 + 8.0h, 2012-08-14-09:00-+0000 + 8.0h, 2012-08-15-09:00-+0000 + 8.0h, 2012-08-16-09:00-+0000 + 8.0h, 2012-08-17-09:00-+0000 + 8.0h, 2012-08-20-09:00-+0000 + 8.0h, 2012-08-21-09:00-+0000 + 8.0h, 2012-08-22-09:00-+0000 + 8.0h, 2012-08-23-09:00-+0000 + 8.0h, 2012-08-24-09:00-+0000 + 8.0h, 2012-08-27-09:00-+0000 + 8.0h, 2012-08-28-09:00-+0000 + 8.0h, 2012-08-29-09:00-+0000 + 8.0h, 2012-08-30-09:00-+0000 + 8.0h, 2012-08-31-09:00-+0000 + 8.0h, 2012-09-03-09:00-+0000 + 8.0h, 2012-09-04-09:00-+0000 + 8.0h, 2012-09-05-09:00-+0000 + 8.0h, 2012-09-06-09:00-+0000 + 8.0h, 2012-09-07-09:00-+0000 + 8.0h, 2012-09-10-09:00-+0000 + 8.0h, 2012-09-11-09:00-+0000 + 8.0h, 2012-09-12-09:00-+0000 + 8.0h, 2012-09-13-09:00-+0000 + 8.0h, 2012-09-14-09:00-+0000 + 8.0h, 2012-09-17-09:00-+0000 + 8.0h, 2012-09-18-09:00-+0000 + 8.0h, 2012-09-19-09:00-+0000 + 8.0h, 2012-09-20-09:00-+0000 + 8.0h, 2012-09-21-09:00-+0000 + 8.0h, 2012-09-24-09:00-+0000 + 8.0h, 2012-09-25-09:00-+0000 + 8.0h, 2012-09-26-09:00-+0000 + 8.0h, 2012-09-27-09:00-+0000 + 8.0h, 2012-09-28-09:00-+0000 + 8.0h, 2012-10-01-09:00-+0000 + 8.0h, 2012-10-02-09:00-+0000 + 8.0h, 2012-10-03-09:00-+0000 + 8.0h, 2012-10-04-09:00-+0000 + 8.0h, 2012-10-05-09:00-+0000 + 8.0h, 2012-10-08-09:00-+0000 + 8.0h { overtime 2 } booking _Resource_3 2012-08-08-13:00-+0000 + 4.0h, 2012-08-09-09:00-+0000 + 8.0h, 2012-08-10-09:00-+0000 + 8.0h, 2012-08-13-09:00-+0000 + 8.0h, 2012-08-14-09:00-+0000 + 8.0h, 2012-08-15-09:00-+0000 + 8.0h, 2012-08-16-09:00-+0000 + 8.0h, 2012-08-17-09:00-+0000 + 8.0h, 2012-08-20-09:00-+0000 + 8.0h, 2012-08-21-09:00-+0000 + 8.0h, 2012-08-22-09:00-+0000 + 8.0h, 2012-08-23-09:00-+0000 + 8.0h, 2012-08-24-09:00-+0000 + 8.0h, 2012-08-27-09:00-+0000 + 8.0h, 2012-08-28-09:00-+0000 + 8.0h, 2012-08-29-09:00-+0000 + 8.0h, 2012-08-30-09:00-+0000 + 8.0h, 2012-08-31-09:00-+0000 + 8.0h, 2012-09-03-09:00-+0000 + 8.0h, 2012-09-04-09:00-+0000 + 8.0h, 2012-09-05-09:00-+0000 + 8.0h, 2012-09-06-09:00-+0000 + 8.0h, 2012-09-07-09:00-+0000 + 8.0h, 2012-09-10-09:00-+0000 + 8.0h, 2012-09-11-09:00-+0000 + 8.0h, 2012-09-12-09:00-+0000 + 8.0h, 2012-09-13-09:00-+0000 + 8.0h, 2012-09-14-09:00-+0000 + 8.0h, 2012-09-17-09:00-+0000 + 8.0h, 2012-09-18-09:00-+0000 + 8.0h, 2012-09-19-09:00-+0000 + 8.0h, 2012-09-20-09:00-+0000 + 8.0h, 2012-09-21-09:00-+0000 + 8.0h, 2012-09-24-09:00-+0000 + 8.0h, 2012-09-25-09:00-+0000 + 8.0h, 2012-09-26-09:00-+0000 + 8.0h, 2012-09-27-09:00-+0000 + 8.0h, 2012-09-28-09:00-+0000 + 8.0h, 2012-10-01-09:00-+0000 + 8.0h, 2012-10-02-09:00-+0000 + 8.0h, 2012-10-03-09:00-+0000 + 8.0h, 2012-10-04-09:00-+0000 + 8.0h, 2012-10-05-09:00-+0000 + 8.0h, 2012-10-08-09:00-+0000 + 8.0h { overtime 2 } booking _Resource_5 2012-02-08-11:00-+0000 + 6.0h, 2012-02-09-09:00-+0000 + 8.0h, 2012-02-10-09:00-+0000 + 8.0h, 2012-02-13-09:00-+0000 + 8.0h, 2012-02-14-09:00-+0000 + 8.0h, 2012-02-15-09:00-+0000 + 8.0h, 2012-02-16-09:00-+0000 + 8.0h, 2012-02-17-09:00-+0000 + 8.0h, 2012-02-20-09:00-+0000 + 8.0h, 2012-02-21-09:00-+0000 + 8.0h, 2012-02-22-09:00-+0000 + 8.0h, 2012-02-23-09:00-+0000 + 8.0h, 2012-02-24-09:00-+0000 + 8.0h, 2012-02-27-09:00-+0000 + 8.0h, 2012-02-28-09:00-+0000 + 8.0h, 2012-02-29-09:00-+0000 + 8.0h, 2012-03-01-09:00-+0000 + 8.0h, 2012-03-02-09:00-+0000 + 8.0h, 2012-03-05-09:00-+0000 + 8.0h, 2012-03-06-09:00-+0000 + 8.0h, 2012-03-07-09:00-+0000 + 8.0h, 2012-03-08-09:00-+0000 + 8.0h, 2012-03-09-09:00-+0000 + 8.0h, 2012-03-12-09:00-+0000 + 8.0h, 2012-03-13-09:00-+0000 + 8.0h, 2012-03-14-09:00-+0000 + 8.0h, 2012-03-15-09:00-+0000 + 8.0h, 2012-03-16-09:00-+0000 + 8.0h, 2012-03-19-09:00-+0000 + 8.0h, 2012-03-20-09:00-+0000 + 8.0h, 2012-03-21-09:00-+0000 + 8.0h, 2012-03-22-09:00-+0000 + 8.0h, 2012-03-23-09:00-+0000 + 8.0h, 2012-03-26-09:00-+0000 + 8.0h, 2012-03-27-09:00-+0000 + 8.0h, 2012-03-28-09:00-+0000 + 8.0h, 2012-03-29-09:00-+0000 + 8.0h, 2012-03-30-09:00-+0000 + 8.0h, 2012-04-02-09:00-+0000 + 8.0h, 2012-04-03-09:00-+0000 + 8.0h, 2012-04-04-09:00-+0000 + 8.0h, 2012-04-05-09:00-+0000 + 8.0h, 2012-04-06-09:00-+0000 + 8.0h, 2012-04-09-09:00-+0000 + 8.0h, 2012-04-10-09:00-+0000 + 8.0h, 2012-04-11-09:00-+0000 + 8.0h, 2012-04-12-09:00-+0000 + 8.0h, 2012-04-13-09:00-+0000 + 8.0h, 2012-04-16-09:00-+0000 + 8.0h, 2012-04-17-09:00-+0000 + 8.0h, 2012-04-18-09:00-+0000 + 8.0h, 2012-04-19-09:00-+0000 + 8.0h, 2012-04-20-09:00-+0000 + 8.0h, 2012-04-23-09:00-+0000 + 8.0h, 2012-04-24-09:00-+0000 + 8.0h, 2012-04-25-09:00-+0000 + 8.0h, 2012-04-26-09:00-+0000 + 8.0h, 2012-04-27-09:00-+0000 + 8.0h, 2012-04-30-09:00-+0000 + 8.0h, 2012-05-01-09:00-+0000 + 8.0h, 2012-05-02-09:00-+0000 + 8.0h, 2012-05-03-09:00-+0000 + 8.0h, 2012-05-04-09:00-+0000 + 8.0h, 2012-05-07-09:00-+0000 + 8.0h, 2012-05-08-09:00-+0000 + 8.0h, 2012-05-09-09:00-+0000 + 8.0h, 2012-05-10-09:00-+0000 + 8.0h, 2012-05-11-09:00-+0000 + 8.0h, 2012-05-14-09:00-+0000 + 8.0h, 2012-05-15-09:00-+0000 + 8.0h, 2012-05-16-09:00-+0000 + 8.0h, 2012-05-17-09:00-+0000 + 8.0h, 2012-05-18-09:00-+0000 + 8.0h, 2012-05-21-09:00-+0000 + 8.0h, 2012-05-22-09:00-+0000 + 8.0h, 2012-05-23-09:00-+0000 + 8.0h, 2012-05-24-09:00-+0000 + 8.0h, 2012-05-25-09:00-+0000 + 8.0h, 2012-05-28-09:00-+0000 + 8.0h, 2012-05-29-09:00-+0000 + 8.0h, 2012-05-30-09:00-+0000 + 8.0h, 2012-05-31-09:00-+0000 + 8.0h, 2012-06-01-09:00-+0000 + 8.0h, 2012-06-04-09:00-+0000 + 8.0h, 2012-06-05-09:00-+0000 + 8.0h, 2012-06-06-09:00-+0000 + 8.0h, 2012-06-07-09:00-+0000 + 8.0h, 2012-06-08-09:00-+0000 + 8.0h, 2012-06-11-09:00-+0000 + 8.0h, 2012-06-12-09:00-+0000 + 8.0h, 2012-06-13-09:00-+0000 + 8.0h, 2012-06-14-09:00-+0000 + 8.0h, 2012-06-15-09:00-+0000 + 8.0h, 2012-06-18-09:00-+0000 + 8.0h, 2012-06-19-09:00-+0000 + 8.0h, 2012-06-20-09:00-+0000 + 8.0h, 2012-06-21-09:00-+0000 + 8.0h, 2012-06-22-09:00-+0000 + 8.0h, 2012-06-25-09:00-+0000 + 8.0h, 2012-06-26-09:00-+0000 + 8.0h, 2012-06-27-09:00-+0000 + 8.0h, 2012-06-28-09:00-+0000 + 8.0h, 2012-06-29-09:00-+0000 + 8.0h, 2012-07-02-09:00-+0000 + 8.0h, 2012-07-03-09:00-+0000 + 8.0h, 2012-07-04-09:00-+0000 + 8.0h, 2012-07-05-09:00-+0000 + 8.0h, 2012-07-06-09:00-+0000 + 8.0h, 2012-07-09-09:00-+0000 + 8.0h, 2012-07-10-09:00-+0000 + 8.0h, 2012-07-11-09:00-+0000 + 8.0h, 2012-07-12-09:00-+0000 + 8.0h, 2012-07-13-09:00-+0000 + 8.0h, 2012-07-16-09:00-+0000 + 8.0h, 2012-07-17-09:00-+0000 + 8.0h, 2012-07-18-09:00-+0000 + 8.0h, 2012-07-19-09:00-+0000 + 8.0h, 2012-07-20-09:00-+0000 + 8.0h, 2012-07-23-09:00-+0000 + 8.0h, 2012-07-24-09:00-+0000 + 8.0h, 2012-07-25-09:00-+0000 + 8.0h, 2012-07-26-09:00-+0000 + 8.0h, 2012-07-27-09:00-+0000 + 8.0h, 2012-07-30-09:00-+0000 + 8.0h, 2012-07-31-09:00-+0000 + 8.0h, 2012-08-01-09:00-+0000 + 8.0h, 2012-08-02-09:00-+0000 + 8.0h, 2012-08-03-09:00-+0000 + 8.0h, 2012-08-06-09:00-+0000 + 8.0h, 2012-08-07-09:00-+0000 + 8.0h, 2012-08-08-09:00-+0000 + 8.0h, 2012-08-09-09:00-+0000 + 8.0h, 2012-08-10-09:00-+0000 + 8.0h, 2012-08-13-09:00-+0000 + 8.0h, 2012-08-14-09:00-+0000 + 8.0h, 2012-08-15-09:00-+0000 + 8.0h, 2012-08-16-09:00-+0000 + 8.0h, 2012-08-17-09:00-+0000 + 8.0h, 2012-08-20-09:00-+0000 + 8.0h, 2012-08-21-09:00-+0000 + 8.0h, 2012-08-22-09:00-+0000 + 8.0h, 2012-08-23-09:00-+0000 + 8.0h, 2012-08-24-09:00-+0000 + 8.0h, 2012-08-27-09:00-+0000 + 8.0h, 2012-08-28-09:00-+0000 + 8.0h, 2012-08-29-09:00-+0000 + 8.0h, 2012-08-30-09:00-+0000 + 8.0h, 2012-08-31-09:00-+0000 + 8.0h, 2012-09-03-09:00-+0000 + 8.0h, 2012-09-04-09:00-+0000 + 8.0h, 2012-09-05-09:00-+0000 + 8.0h, 2012-09-06-09:00-+0000 + 8.0h, 2012-09-07-09:00-+0000 + 8.0h, 2012-09-10-09:00-+0000 + 8.0h, 2012-09-11-09:00-+0000 + 8.0h, 2012-09-12-09:00-+0000 + 8.0h, 2012-09-13-09:00-+0000 + 8.0h, 2012-09-14-09:00-+0000 + 8.0h, 2012-09-17-09:00-+0000 + 8.0h, 2012-09-18-09:00-+0000 + 8.0h, 2012-09-19-09:00-+0000 + 8.0h, 2012-09-20-09:00-+0000 + 8.0h, 2012-09-21-09:00-+0000 + 8.0h, 2012-09-24-09:00-+0000 + 8.0h, 2012-09-25-09:00-+0000 + 8.0h, 2012-09-26-09:00-+0000 + 8.0h, 2012-09-27-09:00-+0000 + 8.0h, 2012-09-28-09:00-+0000 + 8.0h, 2012-10-01-09:00-+0000 + 8.0h, 2012-10-02-09:00-+0000 + 8.0h, 2012-10-03-09:00-+0000 + 8.0h, 2012-10-04-09:00-+0000 + 8.0h, 2012-10-05-09:00-+0000 + 8.0h, 2012-10-08-09:00-+0000 + 8.0h { overtime 2 } booking _Resource_6 2012-02-08-10:00-+0000 + 7.0h, 2012-02-09-09:00-+0000 + 8.0h, 2012-02-10-09:00-+0000 + 8.0h, 2012-02-13-09:00-+0000 + 8.0h, 2012-02-14-09:00-+0000 + 8.0h, 2012-02-15-09:00-+0000 + 8.0h, 2012-02-16-09:00-+0000 + 8.0h, 2012-02-17-09:00-+0000 + 8.0h, 2012-02-20-09:00-+0000 + 8.0h, 2012-02-21-09:00-+0000 + 8.0h, 2012-02-22-09:00-+0000 + 8.0h, 2012-02-23-09:00-+0000 + 8.0h, 2012-02-24-09:00-+0000 + 8.0h, 2012-02-27-09:00-+0000 + 8.0h, 2012-02-28-09:00-+0000 + 8.0h, 2012-02-29-09:00-+0000 + 8.0h, 2012-03-01-09:00-+0000 + 8.0h, 2012-03-02-09:00-+0000 + 8.0h, 2012-03-05-09:00-+0000 + 8.0h, 2012-03-06-09:00-+0000 + 8.0h, 2012-03-07-09:00-+0000 + 8.0h, 2012-03-08-09:00-+0000 + 8.0h, 2012-03-09-09:00-+0000 + 8.0h, 2012-03-12-09:00-+0000 + 8.0h, 2012-03-13-09:00-+0000 + 8.0h, 2012-03-14-09:00-+0000 + 8.0h, 2012-03-15-09:00-+0000 + 8.0h, 2012-03-16-09:00-+0000 + 8.0h, 2012-03-19-09:00-+0000 + 8.0h, 2012-03-20-09:00-+0000 + 8.0h, 2012-03-21-09:00-+0000 + 8.0h, 2012-03-22-09:00-+0000 + 8.0h, 2012-03-23-09:00-+0000 + 8.0h, 2012-03-26-09:00-+0000 + 8.0h, 2012-03-27-09:00-+0000 + 8.0h, 2012-03-28-09:00-+0000 + 8.0h, 2012-03-29-09:00-+0000 + 8.0h, 2012-03-30-09:00-+0000 + 8.0h, 2012-04-02-09:00-+0000 + 8.0h, 2012-04-03-09:00-+0000 + 8.0h, 2012-04-04-09:00-+0000 + 8.0h, 2012-04-05-09:00-+0000 + 8.0h, 2012-04-06-09:00-+0000 + 8.0h, 2012-04-09-09:00-+0000 + 8.0h, 2012-04-10-09:00-+0000 + 8.0h, 2012-04-11-09:00-+0000 + 8.0h, 2012-04-12-09:00-+0000 + 8.0h, 2012-04-13-09:00-+0000 + 8.0h, 2012-04-16-09:00-+0000 + 8.0h, 2012-04-17-09:00-+0000 + 8.0h, 2012-04-18-09:00-+0000 + 8.0h, 2012-04-19-09:00-+0000 + 8.0h, 2012-04-20-09:00-+0000 + 8.0h, 2012-04-23-09:00-+0000 + 8.0h, 2012-04-24-09:00-+0000 + 8.0h, 2012-04-25-09:00-+0000 + 8.0h, 2012-04-26-09:00-+0000 + 8.0h, 2012-04-27-09:00-+0000 + 8.0h, 2012-04-30-09:00-+0000 + 8.0h, 2012-05-01-09:00-+0000 + 8.0h, 2012-05-02-09:00-+0000 + 8.0h, 2012-05-03-09:00-+0000 + 8.0h, 2012-05-04-09:00-+0000 + 8.0h, 2012-05-07-09:00-+0000 + 8.0h, 2012-05-08-09:00-+0000 + 8.0h, 2012-05-09-09:00-+0000 + 8.0h, 2012-05-10-09:00-+0000 + 8.0h, 2012-05-11-09:00-+0000 + 8.0h, 2012-05-14-09:00-+0000 + 8.0h, 2012-05-15-09:00-+0000 + 8.0h, 2012-05-16-09:00-+0000 + 8.0h, 2012-05-17-09:00-+0000 + 8.0h, 2012-05-18-09:00-+0000 + 8.0h, 2012-05-21-09:00-+0000 + 8.0h, 2012-05-22-09:00-+0000 + 8.0h, 2012-05-23-09:00-+0000 + 8.0h, 2012-05-24-09:00-+0000 + 8.0h, 2012-05-25-09:00-+0000 + 8.0h, 2012-05-28-09:00-+0000 + 8.0h, 2012-05-29-09:00-+0000 + 8.0h, 2012-05-30-09:00-+0000 + 8.0h, 2012-05-31-09:00-+0000 + 8.0h, 2012-06-01-09:00-+0000 + 8.0h, 2012-06-04-09:00-+0000 + 8.0h, 2012-06-05-09:00-+0000 + 8.0h, 2012-06-06-09:00-+0000 + 8.0h, 2012-06-07-09:00-+0000 + 8.0h, 2012-06-08-09:00-+0000 + 8.0h, 2012-06-11-09:00-+0000 + 8.0h, 2012-06-12-09:00-+0000 + 8.0h, 2012-06-13-09:00-+0000 + 8.0h, 2012-06-14-09:00-+0000 + 8.0h, 2012-06-15-09:00-+0000 + 8.0h, 2012-06-18-09:00-+0000 + 8.0h, 2012-06-19-09:00-+0000 + 8.0h, 2012-06-20-09:00-+0000 + 8.0h, 2012-06-21-09:00-+0000 + 8.0h, 2012-06-22-09:00-+0000 + 8.0h, 2012-06-25-09:00-+0000 + 8.0h, 2012-06-26-09:00-+0000 + 8.0h, 2012-06-27-09:00-+0000 + 8.0h, 2012-06-28-09:00-+0000 + 8.0h, 2012-06-29-09:00-+0000 + 8.0h, 2012-07-02-09:00-+0000 + 8.0h, 2012-07-03-09:00-+0000 + 8.0h, 2012-07-04-09:00-+0000 + 8.0h, 2012-07-05-09:00-+0000 + 8.0h, 2012-07-06-09:00-+0000 + 8.0h, 2012-07-09-09:00-+0000 + 8.0h, 2012-07-10-09:00-+0000 + 8.0h, 2012-07-11-09:00-+0000 + 8.0h, 2012-07-12-09:00-+0000 + 8.0h, 2012-07-13-09:00-+0000 + 8.0h, 2012-07-16-09:00-+0000 + 8.0h, 2012-07-17-09:00-+0000 + 8.0h, 2012-07-18-09:00-+0000 + 8.0h, 2012-07-19-09:00-+0000 + 8.0h, 2012-07-20-09:00-+0000 + 8.0h, 2012-07-23-09:00-+0000 + 8.0h, 2012-07-24-09:00-+0000 + 8.0h, 2012-07-25-09:00-+0000 + 8.0h, 2012-07-26-09:00-+0000 + 8.0h, 2012-07-27-09:00-+0000 + 8.0h, 2012-07-30-09:00-+0000 + 8.0h, 2012-07-31-09:00-+0000 + 8.0h, 2012-08-01-09:00-+0000 + 8.0h, 2012-08-02-09:00-+0000 + 8.0h, 2012-08-03-09:00-+0000 + 8.0h, 2012-08-06-09:00-+0000 + 8.0h, 2012-08-07-09:00-+0000 + 8.0h, 2012-08-08-09:00-+0000 + 8.0h, 2012-08-09-09:00-+0000 + 8.0h, 2012-08-10-09:00-+0000 + 8.0h, 2012-08-13-09:00-+0000 + 8.0h, 2012-08-14-09:00-+0000 + 8.0h, 2012-08-15-09:00-+0000 + 8.0h, 2012-08-16-09:00-+0000 + 8.0h, 2012-08-17-09:00-+0000 + 8.0h, 2012-08-20-09:00-+0000 + 8.0h, 2012-08-21-09:00-+0000 + 8.0h, 2012-08-22-09:00-+0000 + 8.0h, 2012-08-23-09:00-+0000 + 8.0h, 2012-08-24-09:00-+0000 + 8.0h, 2012-08-27-09:00-+0000 + 8.0h, 2012-08-28-09:00-+0000 + 8.0h, 2012-08-29-09:00-+0000 + 8.0h, 2012-08-30-09:00-+0000 + 8.0h, 2012-08-31-09:00-+0000 + 8.0h, 2012-09-03-09:00-+0000 + 8.0h, 2012-09-04-09:00-+0000 + 8.0h, 2012-09-05-09:00-+0000 + 8.0h, 2012-09-06-09:00-+0000 + 8.0h, 2012-09-07-09:00-+0000 + 8.0h, 2012-09-10-09:00-+0000 + 8.0h, 2012-09-11-09:00-+0000 + 8.0h, 2012-09-12-09:00-+0000 + 8.0h, 2012-09-13-09:00-+0000 + 8.0h, 2012-09-14-09:00-+0000 + 8.0h, 2012-09-17-09:00-+0000 + 8.0h, 2012-09-18-09:00-+0000 + 8.0h, 2012-09-19-09:00-+0000 + 8.0h, 2012-09-20-09:00-+0000 + 8.0h, 2012-09-21-09:00-+0000 + 8.0h, 2012-09-24-09:00-+0000 + 8.0h, 2012-09-25-09:00-+0000 + 8.0h, 2012-09-26-09:00-+0000 + 8.0h, 2012-09-27-09:00-+0000 + 8.0h, 2012-09-28-09:00-+0000 + 8.0h, 2012-10-01-09:00-+0000 + 8.0h, 2012-10-02-09:00-+0000 + 8.0h, 2012-10-03-09:00-+0000 + 8.0h, 2012-10-04-09:00-+0000 + 8.0h, 2012-10-05-09:00-+0000 + 8.0h, 2012-10-08-09:00-+0000 + 8.0h { overtime 2 } priority 500 projectid prj } supplement task _Task_1.p2 { booking _Resource_2 2011-11-09-09:00-+0000 + 8.0h, 2011-11-10-09:00-+0000 + 8.0h, 2011-11-11-09:00-+0000 + 8.0h, 2011-11-14-09:00-+0000 + 8.0h, 2011-11-15-09:00-+0000 + 8.0h, 2011-11-16-09:00-+0000 + 8.0h, 2011-11-17-09:00-+0000 + 8.0h, 2011-11-18-09:00-+0000 + 8.0h, 2011-11-21-09:00-+0000 + 8.0h, 2011-11-22-09:00-+0000 + 8.0h, 2011-11-23-09:00-+0000 + 8.0h, 2011-11-24-09:00-+0000 + 8.0h, 2011-11-25-09:00-+0000 + 8.0h, 2011-11-28-09:00-+0000 + 8.0h, 2011-11-29-09:00-+0000 + 8.0h, 2011-11-30-09:00-+0000 + 8.0h, 2011-12-01-09:00-+0000 + 8.0h, 2011-12-02-09:00-+0000 + 8.0h, 2011-12-05-09:00-+0000 + 8.0h, 2011-12-06-09:00-+0000 + 8.0h, 2011-12-07-09:00-+0000 + 8.0h, 2011-12-08-09:00-+0000 + 8.0h, 2011-12-09-09:00-+0000 + 8.0h, 2011-12-12-09:00-+0000 + 8.0h, 2011-12-13-09:00-+0000 + 8.0h, 2011-12-14-09:00-+0000 + 8.0h, 2011-12-15-09:00-+0000 + 8.0h, 2011-12-16-09:00-+0000 + 8.0h, 2011-12-19-09:00-+0000 + 8.0h, 2011-12-20-09:00-+0000 + 8.0h, 2011-12-21-09:00-+0000 + 8.0h, 2011-12-22-09:00-+0000 + 8.0h, 2011-12-23-09:00-+0000 + 8.0h, 2011-12-26-09:00-+0000 + 8.0h, 2011-12-27-09:00-+0000 + 8.0h, 2011-12-28-09:00-+0000 + 8.0h, 2011-12-29-09:00-+0000 + 8.0h, 2011-12-30-09:00-+0000 + 8.0h, 2012-01-02-09:00-+0000 + 8.0h, 2012-01-03-09:00-+0000 + 8.0h, 2012-01-04-09:00-+0000 + 8.0h, 2012-01-05-09:00-+0000 + 8.0h, 2012-01-06-09:00-+0000 + 8.0h, 2012-01-09-09:00-+0000 + 8.0h, 2012-01-10-09:00-+0000 + 8.0h, 2012-01-11-09:00-+0000 + 8.0h, 2012-01-12-09:00-+0000 + 8.0h, 2012-01-13-09:00-+0000 + 8.0h, 2012-01-16-09:00-+0000 + 8.0h, 2012-01-17-09:00-+0000 + 8.0h, 2012-01-18-09:00-+0000 + 8.0h, 2012-01-19-09:00-+0000 + 8.0h, 2012-01-20-09:00-+0000 + 8.0h, 2012-01-23-09:00-+0000 + 8.0h, 2012-01-24-09:00-+0000 + 8.0h, 2012-01-25-09:00-+0000 + 8.0h, 2012-01-26-09:00-+0000 + 8.0h, 2012-01-27-09:00-+0000 + 8.0h, 2012-01-30-09:00-+0000 + 8.0h, 2012-01-31-09:00-+0000 + 8.0h, 2012-02-01-09:00-+0000 + 8.0h, 2012-02-02-09:00-+0000 + 8.0h, 2012-02-03-09:00-+0000 + 8.0h, 2012-02-06-09:00-+0000 + 8.0h, 2012-02-07-09:00-+0000 + 8.0h, 2012-02-08-09:00-+0000 + 8.0h, 2012-02-09-09:00-+0000 + 8.0h, 2012-02-10-09:00-+0000 + 8.0h, 2012-02-13-09:00-+0000 + 8.0h, 2012-02-14-09:00-+0000 + 8.0h, 2012-02-15-09:00-+0000 + 8.0h, 2012-02-16-09:00-+0000 + 8.0h, 2012-02-17-09:00-+0000 + 8.0h, 2012-02-20-09:00-+0000 + 8.0h, 2012-02-21-09:00-+0000 + 8.0h, 2012-02-22-09:00-+0000 + 8.0h, 2012-02-23-09:00-+0000 + 8.0h, 2012-02-24-09:00-+0000 + 8.0h, 2012-02-27-09:00-+0000 + 8.0h, 2012-02-28-09:00-+0000 + 8.0h, 2012-02-29-09:00-+0000 + 8.0h, 2012-03-01-09:00-+0000 + 8.0h, 2012-03-02-09:00-+0000 + 8.0h, 2012-03-05-09:00-+0000 + 8.0h, 2012-03-06-09:00-+0000 + 8.0h, 2012-03-07-09:00-+0000 + 8.0h, 2012-03-08-09:00-+0000 + 8.0h, 2012-03-09-09:00-+0000 + 8.0h, 2012-03-12-09:00-+0000 + 8.0h, 2012-03-13-09:00-+0000 + 8.0h, 2012-03-14-09:00-+0000 + 8.0h, 2012-03-15-09:00-+0000 + 8.0h, 2012-03-16-09:00-+0000 + 8.0h, 2012-03-19-09:00-+0000 + 8.0h, 2012-03-20-09:00-+0000 + 8.0h, 2012-03-21-09:00-+0000 + 8.0h, 2012-03-22-09:00-+0000 + 8.0h, 2012-03-23-09:00-+0000 + 8.0h, 2012-03-26-09:00-+0000 + 8.0h, 2012-03-27-09:00-+0000 + 8.0h, 2012-03-28-09:00-+0000 + 8.0h, 2012-03-29-09:00-+0000 + 8.0h, 2012-03-30-09:00-+0000 + 8.0h, 2012-04-02-09:00-+0000 + 8.0h, 2012-04-03-09:00-+0000 + 8.0h, 2012-04-04-09:00-+0000 + 8.0h, 2012-04-05-09:00-+0000 + 8.0h, 2012-04-06-09:00-+0000 + 8.0h, 2012-04-09-09:00-+0000 + 8.0h, 2012-04-10-09:00-+0000 + 8.0h, 2012-04-11-09:00-+0000 + 8.0h, 2012-04-12-09:00-+0000 + 8.0h, 2012-04-13-09:00-+0000 + 8.0h, 2012-04-16-09:00-+0000 + 8.0h, 2012-04-17-09:00-+0000 + 8.0h, 2012-04-18-09:00-+0000 + 8.0h, 2012-04-19-09:00-+0000 + 8.0h, 2012-04-20-09:00-+0000 + 8.0h, 2012-04-23-09:00-+0000 + 8.0h, 2012-04-24-09:00-+0000 + 8.0h, 2012-04-25-09:00-+0000 + 8.0h, 2012-04-26-09:00-+0000 + 8.0h, 2012-04-27-09:00-+0000 + 8.0h, 2012-04-30-09:00-+0000 + 8.0h, 2012-05-01-09:00-+0000 + 8.0h, 2012-05-02-09:00-+0000 + 8.0h, 2012-05-03-09:00-+0000 + 8.0h, 2012-05-04-09:00-+0000 + 8.0h, 2012-05-07-09:00-+0000 + 8.0h, 2012-05-08-09:00-+0000 + 8.0h, 2012-05-09-09:00-+0000 + 8.0h, 2012-05-10-09:00-+0000 + 8.0h, 2012-05-11-09:00-+0000 + 8.0h, 2012-05-14-09:00-+0000 + 8.0h, 2012-05-15-09:00-+0000 + 8.0h, 2012-05-16-09:00-+0000 + 8.0h, 2012-05-17-09:00-+0000 + 8.0h, 2012-05-18-09:00-+0000 + 8.0h, 2012-05-21-09:00-+0000 + 8.0h, 2012-05-22-09:00-+0000 + 8.0h, 2012-05-23-09:00-+0000 + 8.0h, 2012-05-24-09:00-+0000 + 8.0h, 2012-05-25-09:00-+0000 + 8.0h, 2012-05-28-09:00-+0000 + 8.0h, 2012-05-29-09:00-+0000 + 8.0h, 2012-05-30-09:00-+0000 + 8.0h, 2012-05-31-09:00-+0000 + 8.0h, 2012-06-01-09:00-+0000 + 8.0h, 2012-06-04-09:00-+0000 + 8.0h, 2012-06-05-09:00-+0000 + 8.0h, 2012-06-06-09:00-+0000 + 8.0h, 2012-06-07-09:00-+0000 + 8.0h, 2012-06-08-09:00-+0000 + 8.0h, 2012-06-11-09:00-+0000 + 8.0h, 2012-06-12-09:00-+0000 + 8.0h, 2012-06-13-09:00-+0000 + 8.0h, 2012-06-14-09:00-+0000 + 8.0h, 2012-06-15-09:00-+0000 + 8.0h, 2012-06-18-09:00-+0000 + 8.0h, 2012-06-19-09:00-+0000 + 8.0h, 2012-06-20-09:00-+0000 + 8.0h, 2012-06-21-09:00-+0000 + 8.0h, 2012-06-22-09:00-+0000 + 8.0h, 2012-06-25-09:00-+0000 + 8.0h, 2012-06-26-09:00-+0000 + 8.0h, 2012-06-27-09:00-+0000 + 8.0h, 2012-06-28-09:00-+0000 + 8.0h, 2012-06-29-09:00-+0000 + 8.0h, 2012-07-02-09:00-+0000 + 8.0h, 2012-07-03-09:00-+0000 + 8.0h, 2012-07-04-09:00-+0000 + 8.0h, 2012-07-05-09:00-+0000 + 8.0h, 2012-07-06-09:00-+0000 + 8.0h, 2012-07-09-09:00-+0000 + 8.0h, 2012-07-10-09:00-+0000 + 8.0h, 2012-07-11-09:00-+0000 + 8.0h, 2012-07-12-09:00-+0000 + 8.0h, 2012-07-13-09:00-+0000 + 8.0h, 2012-07-16-09:00-+0000 + 8.0h, 2012-07-17-09:00-+0000 + 8.0h, 2012-07-18-09:00-+0000 + 8.0h, 2012-07-19-09:00-+0000 + 8.0h, 2012-07-20-09:00-+0000 + 8.0h, 2012-07-23-09:00-+0000 + 8.0h, 2012-07-24-09:00-+0000 + 8.0h, 2012-07-25-09:00-+0000 + 8.0h, 2012-07-26-09:00-+0000 + 8.0h, 2012-07-27-09:00-+0000 + 8.0h, 2012-07-30-09:00-+0000 + 8.0h, 2012-07-31-09:00-+0000 + 8.0h, 2012-08-01-09:00-+0000 + 8.0h, 2012-08-02-09:00-+0000 + 8.0h, 2012-08-03-09:00-+0000 + 8.0h, 2012-08-06-09:00-+0000 + 8.0h, 2012-08-07-09:00-+0000 + 8.0h, 2012-08-08-09:00-+0000 + 5.0h { overtime 2 } booking _Resource_3 2011-11-09-09:00-+0000 + 8.0h, 2011-11-10-09:00-+0000 + 8.0h, 2011-11-11-09:00-+0000 + 8.0h, 2011-11-14-09:00-+0000 + 8.0h, 2011-11-15-09:00-+0000 + 8.0h, 2011-11-16-09:00-+0000 + 8.0h, 2011-11-17-09:00-+0000 + 8.0h, 2011-11-18-09:00-+0000 + 8.0h, 2011-11-21-09:00-+0000 + 8.0h, 2011-11-22-09:00-+0000 + 8.0h, 2011-11-23-09:00-+0000 + 8.0h, 2011-11-24-09:00-+0000 + 8.0h, 2011-11-25-09:00-+0000 + 8.0h, 2011-11-28-09:00-+0000 + 8.0h, 2011-11-29-09:00-+0000 + 8.0h, 2011-11-30-09:00-+0000 + 8.0h, 2011-12-01-09:00-+0000 + 8.0h, 2011-12-02-09:00-+0000 + 8.0h, 2011-12-05-09:00-+0000 + 8.0h, 2011-12-06-09:00-+0000 + 8.0h, 2011-12-07-09:00-+0000 + 8.0h, 2011-12-08-09:00-+0000 + 8.0h, 2011-12-09-09:00-+0000 + 8.0h, 2011-12-12-09:00-+0000 + 8.0h, 2011-12-13-09:00-+0000 + 8.0h, 2011-12-14-09:00-+0000 + 8.0h, 2011-12-15-09:00-+0000 + 8.0h, 2011-12-16-09:00-+0000 + 8.0h, 2011-12-19-09:00-+0000 + 8.0h, 2011-12-20-09:00-+0000 + 8.0h, 2011-12-21-09:00-+0000 + 8.0h, 2011-12-22-09:00-+0000 + 8.0h, 2011-12-23-09:00-+0000 + 8.0h, 2011-12-26-09:00-+0000 + 8.0h, 2011-12-27-09:00-+0000 + 8.0h, 2011-12-28-09:00-+0000 + 8.0h, 2011-12-29-09:00-+0000 + 8.0h, 2011-12-30-09:00-+0000 + 8.0h, 2012-01-02-09:00-+0000 + 8.0h, 2012-01-03-09:00-+0000 + 8.0h, 2012-01-04-09:00-+0000 + 8.0h, 2012-01-05-09:00-+0000 + 8.0h, 2012-01-06-09:00-+0000 + 8.0h, 2012-01-09-09:00-+0000 + 8.0h, 2012-01-10-09:00-+0000 + 8.0h, 2012-01-11-09:00-+0000 + 8.0h, 2012-01-12-09:00-+0000 + 8.0h, 2012-01-13-09:00-+0000 + 8.0h, 2012-01-16-09:00-+0000 + 8.0h, 2012-01-17-09:00-+0000 + 8.0h, 2012-01-18-09:00-+0000 + 8.0h, 2012-01-19-09:00-+0000 + 8.0h, 2012-01-20-09:00-+0000 + 8.0h, 2012-01-23-09:00-+0000 + 8.0h, 2012-01-24-09:00-+0000 + 8.0h, 2012-01-25-09:00-+0000 + 8.0h, 2012-01-26-09:00-+0000 + 8.0h, 2012-01-27-09:00-+0000 + 8.0h, 2012-01-30-09:00-+0000 + 8.0h, 2012-01-31-09:00-+0000 + 8.0h, 2012-02-01-09:00-+0000 + 8.0h, 2012-02-02-09:00-+0000 + 8.0h, 2012-02-03-09:00-+0000 + 8.0h, 2012-02-06-09:00-+0000 + 8.0h, 2012-02-07-09:00-+0000 + 8.0h, 2012-02-08-09:00-+0000 + 8.0h, 2012-02-09-09:00-+0000 + 8.0h, 2012-02-10-09:00-+0000 + 8.0h, 2012-02-13-09:00-+0000 + 8.0h, 2012-02-14-09:00-+0000 + 8.0h, 2012-02-15-09:00-+0000 + 8.0h, 2012-02-16-09:00-+0000 + 8.0h, 2012-02-17-09:00-+0000 + 8.0h, 2012-02-20-09:00-+0000 + 8.0h, 2012-02-21-09:00-+0000 + 8.0h, 2012-02-22-09:00-+0000 + 8.0h, 2012-02-23-09:00-+0000 + 8.0h, 2012-02-24-09:00-+0000 + 8.0h, 2012-02-27-09:00-+0000 + 8.0h, 2012-02-28-09:00-+0000 + 8.0h, 2012-02-29-09:00-+0000 + 8.0h, 2012-03-01-09:00-+0000 + 8.0h, 2012-03-02-09:00-+0000 + 8.0h, 2012-03-05-09:00-+0000 + 8.0h, 2012-03-06-09:00-+0000 + 8.0h, 2012-03-07-09:00-+0000 + 8.0h, 2012-03-08-09:00-+0000 + 8.0h, 2012-03-09-09:00-+0000 + 8.0h, 2012-03-12-09:00-+0000 + 8.0h, 2012-03-13-09:00-+0000 + 8.0h, 2012-03-14-09:00-+0000 + 8.0h, 2012-03-15-09:00-+0000 + 8.0h, 2012-03-16-09:00-+0000 + 8.0h, 2012-03-19-09:00-+0000 + 8.0h, 2012-03-20-09:00-+0000 + 8.0h, 2012-03-21-09:00-+0000 + 8.0h, 2012-03-22-09:00-+0000 + 8.0h, 2012-03-23-09:00-+0000 + 8.0h, 2012-03-26-09:00-+0000 + 8.0h, 2012-03-27-09:00-+0000 + 8.0h, 2012-03-28-09:00-+0000 + 8.0h, 2012-03-29-09:00-+0000 + 8.0h, 2012-03-30-09:00-+0000 + 8.0h, 2012-04-02-09:00-+0000 + 8.0h, 2012-04-03-09:00-+0000 + 8.0h, 2012-04-04-09:00-+0000 + 8.0h, 2012-04-05-09:00-+0000 + 8.0h, 2012-04-06-09:00-+0000 + 8.0h, 2012-04-09-09:00-+0000 + 8.0h, 2012-04-10-09:00-+0000 + 8.0h, 2012-04-11-09:00-+0000 + 8.0h, 2012-04-12-09:00-+0000 + 8.0h, 2012-04-13-09:00-+0000 + 8.0h, 2012-04-16-09:00-+0000 + 8.0h, 2012-04-17-09:00-+0000 + 8.0h, 2012-04-18-09:00-+0000 + 8.0h, 2012-04-19-09:00-+0000 + 8.0h, 2012-04-20-09:00-+0000 + 8.0h, 2012-04-23-09:00-+0000 + 8.0h, 2012-04-24-09:00-+0000 + 8.0h, 2012-04-25-09:00-+0000 + 8.0h, 2012-04-26-09:00-+0000 + 8.0h, 2012-04-27-09:00-+0000 + 8.0h, 2012-04-30-09:00-+0000 + 8.0h, 2012-05-01-09:00-+0000 + 8.0h, 2012-05-02-09:00-+0000 + 8.0h, 2012-05-03-09:00-+0000 + 8.0h, 2012-05-04-09:00-+0000 + 8.0h, 2012-05-07-09:00-+0000 + 8.0h, 2012-05-08-09:00-+0000 + 8.0h, 2012-05-09-09:00-+0000 + 8.0h, 2012-05-10-09:00-+0000 + 8.0h, 2012-05-11-09:00-+0000 + 8.0h, 2012-05-14-09:00-+0000 + 8.0h, 2012-05-15-09:00-+0000 + 8.0h, 2012-05-16-09:00-+0000 + 8.0h, 2012-05-17-09:00-+0000 + 8.0h, 2012-05-18-09:00-+0000 + 8.0h, 2012-05-21-09:00-+0000 + 8.0h, 2012-05-22-09:00-+0000 + 8.0h, 2012-05-23-09:00-+0000 + 8.0h, 2012-05-24-09:00-+0000 + 8.0h, 2012-05-25-09:00-+0000 + 8.0h, 2012-05-28-09:00-+0000 + 8.0h, 2012-05-29-09:00-+0000 + 8.0h, 2012-05-30-09:00-+0000 + 8.0h, 2012-05-31-09:00-+0000 + 8.0h, 2012-06-01-09:00-+0000 + 8.0h, 2012-06-04-09:00-+0000 + 8.0h, 2012-06-05-09:00-+0000 + 8.0h, 2012-06-06-09:00-+0000 + 8.0h, 2012-06-07-09:00-+0000 + 8.0h, 2012-06-08-09:00-+0000 + 8.0h, 2012-06-11-09:00-+0000 + 8.0h, 2012-06-12-09:00-+0000 + 8.0h, 2012-06-13-09:00-+0000 + 8.0h, 2012-06-14-09:00-+0000 + 8.0h, 2012-06-15-09:00-+0000 + 8.0h, 2012-06-18-09:00-+0000 + 8.0h, 2012-06-19-09:00-+0000 + 8.0h, 2012-06-20-09:00-+0000 + 8.0h, 2012-06-21-09:00-+0000 + 8.0h, 2012-06-22-09:00-+0000 + 8.0h, 2012-06-25-09:00-+0000 + 8.0h, 2012-06-26-09:00-+0000 + 8.0h, 2012-06-27-09:00-+0000 + 8.0h, 2012-06-28-09:00-+0000 + 8.0h, 2012-06-29-09:00-+0000 + 8.0h, 2012-07-02-09:00-+0000 + 8.0h, 2012-07-03-09:00-+0000 + 8.0h, 2012-07-04-09:00-+0000 + 8.0h, 2012-07-05-09:00-+0000 + 8.0h, 2012-07-06-09:00-+0000 + 8.0h, 2012-07-09-09:00-+0000 + 8.0h, 2012-07-10-09:00-+0000 + 8.0h, 2012-07-11-09:00-+0000 + 8.0h, 2012-07-12-09:00-+0000 + 8.0h, 2012-07-13-09:00-+0000 + 8.0h, 2012-07-16-09:00-+0000 + 8.0h, 2012-07-17-09:00-+0000 + 8.0h, 2012-07-18-09:00-+0000 + 8.0h, 2012-07-19-09:00-+0000 + 8.0h, 2012-07-20-09:00-+0000 + 8.0h, 2012-07-23-09:00-+0000 + 8.0h, 2012-07-24-09:00-+0000 + 8.0h, 2012-07-25-09:00-+0000 + 8.0h, 2012-07-26-09:00-+0000 + 8.0h, 2012-07-27-09:00-+0000 + 8.0h, 2012-07-30-09:00-+0000 + 8.0h, 2012-07-31-09:00-+0000 + 8.0h, 2012-08-01-09:00-+0000 + 8.0h, 2012-08-02-09:00-+0000 + 8.0h, 2012-08-03-09:00-+0000 + 8.0h, 2012-08-06-09:00-+0000 + 8.0h, 2012-08-07-09:00-+0000 + 8.0h, 2012-08-08-09:00-+0000 + 4.0h { overtime 2 } priority 600 projectid prj } supplement task _Task_1.p3 { booking _Resource_5 2011-11-09-09:00-+0000 + 8.0h, 2011-11-10-09:00-+0000 + 8.0h, 2011-11-11-09:00-+0000 + 8.0h, 2011-11-14-09:00-+0000 + 8.0h, 2011-11-15-09:00-+0000 + 8.0h, 2011-11-16-09:00-+0000 + 8.0h, 2011-11-17-09:00-+0000 + 8.0h, 2011-11-18-09:00-+0000 + 8.0h, 2011-11-21-09:00-+0000 + 8.0h, 2011-11-22-09:00-+0000 + 8.0h, 2011-11-23-09:00-+0000 + 8.0h, 2011-11-24-09:00-+0000 + 8.0h, 2011-11-25-09:00-+0000 + 8.0h, 2011-11-28-09:00-+0000 + 8.0h, 2011-11-29-09:00-+0000 + 8.0h, 2011-11-30-09:00-+0000 + 8.0h, 2011-12-01-09:00-+0000 + 8.0h, 2011-12-02-09:00-+0000 + 8.0h, 2011-12-05-09:00-+0000 + 8.0h, 2011-12-06-09:00-+0000 + 8.0h, 2011-12-07-09:00-+0000 + 8.0h, 2011-12-08-09:00-+0000 + 8.0h, 2011-12-09-09:00-+0000 + 8.0h, 2011-12-12-09:00-+0000 + 8.0h, 2011-12-13-09:00-+0000 + 8.0h, 2011-12-14-09:00-+0000 + 8.0h, 2011-12-15-09:00-+0000 + 8.0h, 2011-12-16-09:00-+0000 + 8.0h, 2011-12-19-09:00-+0000 + 8.0h, 2011-12-20-09:00-+0000 + 8.0h, 2011-12-21-09:00-+0000 + 8.0h, 2011-12-22-09:00-+0000 + 8.0h, 2011-12-23-09:00-+0000 + 8.0h, 2011-12-26-09:00-+0000 + 8.0h, 2011-12-27-09:00-+0000 + 8.0h, 2011-12-28-09:00-+0000 + 8.0h, 2011-12-29-09:00-+0000 + 8.0h, 2011-12-30-09:00-+0000 + 8.0h, 2012-01-02-09:00-+0000 + 8.0h, 2012-01-03-09:00-+0000 + 8.0h, 2012-01-04-09:00-+0000 + 8.0h, 2012-01-05-09:00-+0000 + 8.0h, 2012-01-06-09:00-+0000 + 8.0h, 2012-01-09-09:00-+0000 + 8.0h, 2012-01-10-09:00-+0000 + 8.0h, 2012-01-11-09:00-+0000 + 8.0h, 2012-01-12-09:00-+0000 + 8.0h, 2012-01-13-09:00-+0000 + 8.0h, 2012-01-16-09:00-+0000 + 8.0h, 2012-01-17-09:00-+0000 + 8.0h, 2012-01-18-09:00-+0000 + 8.0h, 2012-01-19-09:00-+0000 + 8.0h, 2012-01-20-09:00-+0000 + 8.0h, 2012-01-23-09:00-+0000 + 8.0h, 2012-01-24-09:00-+0000 + 8.0h, 2012-01-25-09:00-+0000 + 8.0h, 2012-01-26-09:00-+0000 + 8.0h, 2012-01-27-09:00-+0000 + 8.0h, 2012-01-30-09:00-+0000 + 8.0h, 2012-01-31-09:00-+0000 + 8.0h, 2012-02-01-09:00-+0000 + 8.0h, 2012-02-02-09:00-+0000 + 8.0h, 2012-02-03-09:00-+0000 + 8.0h, 2012-02-06-09:00-+0000 + 8.0h, 2012-02-07-09:00-+0000 + 8.0h, 2012-02-08-09:00-+0000 + 2.0h { overtime 2 } booking _Resource_6 2011-11-09-09:00-+0000 + 8.0h, 2011-11-10-09:00-+0000 + 8.0h, 2011-11-11-09:00-+0000 + 8.0h, 2011-11-14-09:00-+0000 + 8.0h, 2011-11-15-09:00-+0000 + 8.0h, 2011-11-16-09:00-+0000 + 8.0h, 2011-11-17-09:00-+0000 + 8.0h, 2011-11-18-09:00-+0000 + 8.0h, 2011-11-21-09:00-+0000 + 8.0h, 2011-11-22-09:00-+0000 + 8.0h, 2011-11-23-09:00-+0000 + 8.0h, 2011-11-24-09:00-+0000 + 8.0h, 2011-11-25-09:00-+0000 + 8.0h, 2011-11-28-09:00-+0000 + 8.0h, 2011-11-29-09:00-+0000 + 8.0h, 2011-11-30-09:00-+0000 + 8.0h, 2011-12-01-09:00-+0000 + 8.0h, 2011-12-02-09:00-+0000 + 8.0h, 2011-12-05-09:00-+0000 + 8.0h, 2011-12-06-09:00-+0000 + 8.0h, 2011-12-07-09:00-+0000 + 8.0h, 2011-12-08-09:00-+0000 + 8.0h, 2011-12-09-09:00-+0000 + 8.0h, 2011-12-12-09:00-+0000 + 8.0h, 2011-12-13-09:00-+0000 + 8.0h, 2011-12-14-09:00-+0000 + 8.0h, 2011-12-15-09:00-+0000 + 8.0h, 2011-12-16-09:00-+0000 + 8.0h, 2011-12-19-09:00-+0000 + 8.0h, 2011-12-20-09:00-+0000 + 8.0h, 2011-12-21-09:00-+0000 + 8.0h, 2011-12-22-09:00-+0000 + 8.0h, 2011-12-23-09:00-+0000 + 8.0h, 2011-12-26-09:00-+0000 + 8.0h, 2011-12-27-09:00-+0000 + 8.0h, 2011-12-28-09:00-+0000 + 8.0h, 2011-12-29-09:00-+0000 + 8.0h, 2011-12-30-09:00-+0000 + 8.0h, 2012-01-02-09:00-+0000 + 8.0h, 2012-01-03-09:00-+0000 + 8.0h, 2012-01-04-09:00-+0000 + 8.0h, 2012-01-05-09:00-+0000 + 8.0h, 2012-01-06-09:00-+0000 + 8.0h, 2012-01-09-09:00-+0000 + 8.0h, 2012-01-10-09:00-+0000 + 8.0h, 2012-01-11-09:00-+0000 + 8.0h, 2012-01-12-09:00-+0000 + 8.0h, 2012-01-13-09:00-+0000 + 8.0h, 2012-01-16-09:00-+0000 + 8.0h, 2012-01-17-09:00-+0000 + 8.0h, 2012-01-18-09:00-+0000 + 8.0h, 2012-01-19-09:00-+0000 + 8.0h, 2012-01-20-09:00-+0000 + 8.0h, 2012-01-23-09:00-+0000 + 8.0h, 2012-01-24-09:00-+0000 + 8.0h, 2012-01-25-09:00-+0000 + 8.0h, 2012-01-26-09:00-+0000 + 8.0h, 2012-01-27-09:00-+0000 + 8.0h, 2012-01-30-09:00-+0000 + 8.0h, 2012-01-31-09:00-+0000 + 8.0h, 2012-02-01-09:00-+0000 + 8.0h, 2012-02-02-09:00-+0000 + 8.0h, 2012-02-03-09:00-+0000 + 8.0h, 2012-02-06-09:00-+0000 + 8.0h, 2012-02-07-09:00-+0000 + 8.0h, 2012-02-08-09:00-+0000 + 1.0h { overtime 2 } priority 600 projectid prj } supplement task _Task_1.mf { priority 500 projectid prj } supplement task _Task_1._Task_6 { priority 500 projectid prj } supplement resource teamA { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } supplement resource _Resource_2 { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } supplement resource _Resource_3 { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } supplement resource teamB { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } supplement resource _Resource_5 { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } supplement resource _Resource_6 { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } TaskJuggler-3.8.1/test/TestSuite/Export-Reports/refs/AdoptedTasks.tjp000066400000000000000000000011021473026623400256370ustar00rootroot00000000000000project prj "Adopted Tasks" "1.0" 2011-03-05-00:00-+0000 - 2011-04-04-10:00-+0000 { timezone "UTC" scenario plan "Plan Scenario" { active yes } } projectids prj task t1 "T1" { start 2011-03-05-00:00-+0000 scheduled } task t2 "T2" { start 2011-03-05-00:00-+0000 scheduled } task t3 "T3" { start 2011-03-05-00:00-+0000 end 2011-03-05-00:00-+0000 scheduling asap scheduled } supplement task t1 { priority 500 projectid prj } supplement task t2 { priority 500 projectid prj } supplement task t3 { adopt t1, t2 priority 500 projectid prj } TaskJuggler-3.8.1/test/TestSuite/Export-Reports/refs/AlertLevels.tjp000066400000000000000000000007711473026623400255060ustar00rootroot00000000000000project prj "Alert Levels" "1.0" 2011-11-24-00:00-+0000 - 2012-01-23-20:00-+0000 { timezone "UTC" alertlevels green 'Low' '#2AA46C', blue 'Guarded' '#457CC4', yellow 'Elevated' '#F1D821', orange 'High' '#F99836', red 'Severe' '#E43745' scenario plan "Plan Scenario" { active yes } } projectids prj task _Task_1 "Holiday Season" { start 2011-11-24-00:00-+0000 scheduled } supplement task _Task_1 { priority 500 projectid prj } TaskJuggler-3.8.1/test/TestSuite/Export-Reports/refs/Allocate-1.tjp000066400000000000000000000052541473026623400251470ustar00rootroot00000000000000project allocate "allocate" "1.0" 2003-06-05-00:00-+0000 - 2003-07-05-00:00-+0000 { timezone "America/Denver" scenario plan "Plan Scenario" { active yes } } projectids allocate resource r1 "Resource 1" resource r2 "Resource 2" task t1 "Task 1" { task t2 "Task 2" { start 2003-06-19-15:00-+0000 end 2003-07-02-23:00-+0000 scheduling asap scheduled } task t3 "Task 3" { start 2003-06-05-15:00-+0000 end 2003-06-18-23:00-+0000 scheduling asap scheduled } task m1 "Milestone 1" { start 2003-06-05-06:00-+0000 scheduled } } supplement task t1 { priority 500 projectid allocate } supplement task t1.t2 { booking r1 2003-06-19-15:00-+0000 + 8.0h, 2003-06-20-15:00-+0000 + 8.0h, 2003-06-23-15:00-+0000 + 8.0h, 2003-06-24-15:00-+0000 + 8.0h, 2003-06-25-15:00-+0000 + 8.0h, 2003-06-26-15:00-+0000 + 8.0h, 2003-06-27-15:00-+0000 + 8.0h, 2003-06-30-15:00-+0000 + 8.0h, 2003-07-01-15:00-+0000 + 8.0h, 2003-07-02-15:00-+0000 + 8.0h { overtime 2 } priority 500 projectid allocate } supplement task t1.t3 { booking r1 2003-06-05-15:00-+0000 + 8.0h, 2003-06-06-15:00-+0000 + 8.0h, 2003-06-09-15:00-+0000 + 8.0h, 2003-06-10-15:00-+0000 + 8.0h, 2003-06-11-15:00-+0000 + 8.0h, 2003-06-12-15:00-+0000 + 8.0h, 2003-06-13-15:00-+0000 + 8.0h, 2003-06-16-15:00-+0000 + 8.0h, 2003-06-17-15:00-+0000 + 8.0h, 2003-06-18-15:00-+0000 + 8.0h { overtime 2 } booking r2 2003-06-05-15:00-+0000 + 8.0h, 2003-06-06-15:00-+0000 + 8.0h, 2003-06-09-15:00-+0000 + 8.0h, 2003-06-10-15:00-+0000 + 8.0h, 2003-06-11-15:00-+0000 + 8.0h, 2003-06-12-15:00-+0000 + 8.0h, 2003-06-13-15:00-+0000 + 8.0h, 2003-06-16-15:00-+0000 + 8.0h, 2003-06-17-15:00-+0000 + 8.0h, 2003-06-18-15:00-+0000 + 8.0h { overtime 2 } priority 500 projectid allocate } supplement task t1.m1 { priority 500 projectid allocate } supplement resource r1 { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } supplement resource r2 { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } TaskJuggler-3.8.1/test/TestSuite/Export-Reports/refs/Alternative.tjp000066400000000000000000000023271473026623400255410ustar00rootroot00000000000000project prj "Project" "1.0" 2000-01-01-00:00-+0000 - 2000-03-01-00:00-+0000 { timezone "America/Denver" scenario plan "Plan Scenario" { active yes } } projectids prj resource tuxus "Tuxus" resource tuxia "Tuxia" task t "Task" { start 2000-01-03-16:00-+0000 end 2000-01-08-00:00-+0000 scheduling asap scheduled } supplement task t { booking tuxus 2000-01-03-16:00-+0000 + 8.0h, 2000-01-04-16:00-+0000 + 8.0h, 2000-01-05-16:00-+0000 + 8.0h, 2000-01-06-16:00-+0000 + 8.0h, 2000-01-07-16:00-+0000 + 8.0h { overtime 2 } priority 500 projectid prj } supplement resource tuxus { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } supplement resource tuxia { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } TaskJuggler-3.8.1/test/TestSuite/Export-Reports/refs/AutoID.tjp000066400000000000000000000033211473026623400244030ustar00rootroot00000000000000project prj "Simple Project" "1.0" 2009-10-04-00:00-+0000 - 2009-11-03-10:00-+0000 { timezone "America/Denver" scenario plan "Plan Scenario" { active yes } } projectids prj shift _Shift_1 "Foo" { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off shift _Shift_2 "bar" { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } } resource _Resource_1 "Foo" { resource _Resource_2 "Bar" } task _Task_1 "Foo" { task _Task_2 "Bar" { start 2009-10-04-00:00-+0000 scheduled } } supplement task _Task_1 { priority 500 projectid prj } supplement task _Task_1._Task_2 { priority 500 projectid prj } supplement resource _Resource_1 { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } supplement resource _Resource_2 { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } TaskJuggler-3.8.1/test/TestSuite/Export-Reports/refs/AutoMacros.tjp000066400000000000000000000004671473026623400253430ustar00rootroot00000000000000project prj "Auto Macro Test" "1.0" 2006-09-22-00:00-+0000 - 2006-10-22-10:00-+0000 { timezone "UTC" scenario plan "Plan Scenario" { active yes } } projectids prj task items "Project breakdown" { start 2006-09-22-00:00-+0000 scheduled } supplement task items { priority 500 projectid prj } TaskJuggler-3.8.1/test/TestSuite/Export-Reports/refs/Booking.tjp000066400000000000000000000022211473026623400246440ustar00rootroot00000000000000project project "Simple Project" "1.0" 2007-01-05-00:00-+0000 - 2007-02-04-10:00-+0000 { timezone "America/Denver" scenario plan "Plan Scenario" { active yes } } projectids project resource tux "Tux" task test "Testing" { start 2007-01-08-20:00-+0000 end 2007-01-24-22:00-+0000 scheduling asap scheduled } supplement task test { booking tux 2007-01-08-20:00-+0000 + 4.0h, 2007-01-09-20:00-+0000 + 4.0h, 2007-01-11-15:00-+0000 + 10.0h, 2007-01-15-16:00-+0000 + 8.0h, 2007-01-16-16:00-+0000 + 8.0h, 2007-01-17-16:00-+0000 + 8.0h, 2007-01-18-16:00-+0000 + 8.0h, 2007-01-19-16:00-+0000 + 8.0h, 2007-01-22-16:00-+0000 + 8.0h, 2007-01-23-16:00-+0000 + 8.0h, 2007-01-24-16:00-+0000 + 6.0h { overtime 2 } priority 500 projectid project } supplement resource tux { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } TaskJuggler-3.8.1/test/TestSuite/Export-Reports/refs/Caption.tjp000066400000000000000000000027171473026623400246630ustar00rootroot00000000000000project simple "Simple Project" "1.0" 2007-01-01-00:00-+0000 - 2007-02-01-00:00-+0000 { timezone "America/Denver" scenario plan "Plan Scenario" { active yes } } projectids simple resource r1 "Resource 1" task plant "How to plant a tree" { task plan "Choose the planting site" { start 2007-01-01-16:00-+0000 end 2007-01-03-00:00-+0000 scheduling asap scheduled } task buy "Get a tree" { depends plant.plan start 2007-01-03-16:00-+0000 end 2007-01-04-00:00-+0000 scheduling asap scheduled } task action "Plant the tree" { depends plant.buy start 2007-01-04-16:00-+0000 end 2007-01-04-20:00-+0000 scheduling asap scheduled } } supplement task plant { priority 500 projectid simple } supplement task plant.plan { booking r1 2007-01-01-16:00-+0000 + 8.0h, 2007-01-02-16:00-+0000 + 8.0h { overtime 2 } priority 500 projectid simple } supplement task plant.buy { booking r1 2007-01-03-16:00-+0000 + 8.0h { overtime 2 } priority 500 projectid simple } supplement task plant.action { booking r1 2007-01-04-16:00-+0000 + 4.0h { overtime 2 } priority 500 projectid simple } supplement resource r1 { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } TaskJuggler-3.8.1/test/TestSuite/Export-Reports/refs/Celltext.tjp000066400000000000000000000017531473026623400250510ustar00rootroot00000000000000project celltext "celltext" "1.0" 2007-01-01-00:00-+0000 - 2007-03-01-00:00-+0000 { timezone "America/Denver" scenario plan "Plan Scenario" { active yes } } projectids celltext resource tux "Tux" task t "Task" { task s "SubTask" { start 2007-01-01-16:00-+0000 end 2007-01-06-00:00-+0000 scheduling asap scheduled } } supplement task t { priority 500 projectid celltext } supplement task t.s { booking tux 2007-01-01-16:00-+0000 + 8.0h, 2007-01-02-16:00-+0000 + 8.0h, 2007-01-03-16:00-+0000 + 8.0h, 2007-01-04-16:00-+0000 + 8.0h, 2007-01-05-16:00-+0000 + 8.0h { overtime 2 } priority 500 projectid celltext } supplement resource tux { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } TaskJuggler-3.8.1/test/TestSuite/Export-Reports/refs/Comments.tjp000066400000000000000000000027631473026623400250540ustar00rootroot00000000000000project simple "Simple Project" "1.0" 2005-06-06-00:00-+0000 - 2005-06-26-00:00-+0000 { timezone "America/Denver" scenario plan "Plan Scenario" { active yes } } projectids simple resource tux "Tux" task items "Project breakdown" { task plan "Plan work" { start 2005-06-06-06:00-+0000 end 2005-06-08-23:00-+0000 scheduling asap scheduled } task implementation "Implement work" { depends items.plan start 2005-06-09-15:00-+0000 end 2005-06-15-23:00-+0000 scheduling asap scheduled } task acceptance "Customer acceptance" { depends items.implementation start 2005-06-15-23:00-+0000 end 2005-06-20-23:00-+0000 scheduling asap scheduled } } supplement task items { priority 500 projectid simple } supplement task items.plan { priority 500 projectid simple } supplement task items.implementation { booking tux 2005-06-09-15:00-+0000 + 8.0h, 2005-06-10-15:00-+0000 + 8.0h, 2005-06-13-15:00-+0000 + 8.0h, 2005-06-14-15:00-+0000 + 8.0h, 2005-06-15-15:00-+0000 + 8.0h { overtime 2 } priority 500 projectid simple } supplement task items.acceptance { priority 500 projectid simple } supplement resource tux { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } TaskJuggler-3.8.1/test/TestSuite/Export-Reports/refs/Complete.tjp000066400000000000000000000006051473026623400250300ustar00rootroot00000000000000project simple "Simple Project" "1.0" 2007-01-01-00:00-+0000 - 2007-03-02-20:00-+0000 { timezone "America/Denver" scenario plan "Plan Scenario" { active yes } } projectids simple task _Task_1 "Build house" { start 2007-01-06-07:00-+0000 end 2007-02-05-07:00-+0000 scheduling asap scheduled } supplement task _Task_1 { complete 20 priority 500 projectid simple } TaskJuggler-3.8.1/test/TestSuite/Export-Reports/refs/Currencyformat.tjp000066400000000000000000000004441473026623400262640ustar00rootroot00000000000000project prj "Project" "1.0" 2007-01-01-00:00-+0000 - 2007-03-01-00:00-+0000 { timezone "Europe/Berlin" scenario plan "Plan Scenario" { active yes } } projectids prj task t "Task" { start 2007-01-01-00:00-+0000 scheduled } supplement task t { priority 500 projectid prj } TaskJuggler-3.8.1/test/TestSuite/Export-Reports/refs/CustomAttributes.tjp000066400000000000000000000043011473026623400265760ustar00rootroot00000000000000project prj "Extend Test" "1.0" 2013-04-24-00:00-+0000 - 2013-05-24-10:00-+0000 { timezone "UTC" extend resource{ date Birthday "Birthday" date BirthdayS "Birthday" { scenariospecific inherit } richtext Claim "Claim" richtext ClaimS "Claim" { scenariospecific inherit } number Count "Count" number CountS "Count" { scenariospecific } text Intro "Intro" text IntroS "Intro" { scenariospecific } reference URL "URL" reference URLS "URL" { scenariospecific } } extend task{ richtext Claim "Claim" richtext ClaimS "Claim" { scenariospecific inherit } number Count "Count" number CountS "Count" { scenariospecific } date DueDate "Due Date" date DueDateS "Due Date" { scenariospecific inherit } text Intro "Intro" text IntroS "Intro" { scenariospecific } reference URL "URL" reference URLS "URL" { scenariospecific } } scenario one "One" { scenario two "Two" { active yes } active yes } } projectids prj resource _Resource_1 "R" task _Task_1 "T" { start 2013-04-24-00:00-+0000 scheduled } supplement task _Task_1 { Claim "A '''big''' statement." two:ClaimS "A '''big''' statement." Count 42 two:CountS 42 DueDate 2013-05-01-00:00-+0000 two:DueDateS 2013-05-01-00:00-+0000 Intro "Let's think about this..." two:IntroS "Let's think about this..." URL "http://www.taskjuggler.org" { label "http://www.taskjuggler.org" } two:URLS "http://www.taskjuggler.org" { label "TJ Web" } priority 500 projectid prj } supplement resource _Resource_1 { Birthday 2000-05-01-00:00-+0000 two:BirthdayS 2000-05-01-00:00-+0000 Claim "A '''big''' statement." two:ClaimS "A '''big''' statement." Count 42 two:CountS 42 Intro "Let's think about this..." two:IntroS "Let's think about this..." URL "http://www.taskjuggler.org" { label "http://www.taskjuggler.org" } two:URLS "http://www.taskjuggler.org" { label "TJ Web" } workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } TaskJuggler-3.8.1/test/TestSuite/Export-Reports/refs/Depends1.tjp000066400000000000000000000017001473026623400247200ustar00rootroot00000000000000project prj "P" "1.0" 2007-11-09-00:00-+0000 - 2007-12-24-00:00-+0000 { timezone "America/Denver" scenario plan "Plan Scenario" { active yes } } projectids prj task foo1 "foo1" { task foo2 "foo2" { start 2007-12-04-07:00-+0000 scheduled } task foo3 "foo3" { depends foo1.foo2 start 2007-12-04-07:00-+0000 end 2007-12-05-00:00-+0000 scheduling asap scheduled } } task bar "bar" { depends foo1.foo2 start 2007-12-04-07:00-+0000 end 2007-12-06-00:00-+0000 scheduling asap scheduled } task bar1 "bar1" { depends foo1, bar start 2007-12-07-00:00-+0000 end 2007-12-09-00:00-+0000 scheduling asap scheduled } supplement task foo1 { priority 500 projectid prj } supplement task foo1.foo2 { priority 500 projectid prj } supplement task foo1.foo3 { priority 500 projectid prj } supplement task bar { priority 500 projectid prj } supplement task bar1 { priority 500 projectid prj } TaskJuggler-3.8.1/test/TestSuite/Export-Reports/refs/Durations.tjp000066400000000000000000000033701473026623400252320ustar00rootroot00000000000000project prj "Duration Example" "1.0" 2007-06-06-00:00-+0000 - 2007-06-26-00:00-+0000 { timezone "America/Denver" scenario plan "Plan Scenario" { active yes } } projectids prj resource tux "Tux" task t "Enclosing" { task durationTask "Duration Task" { start 2007-06-06-06:00-+0000 end 2007-06-16-06:00-+0000 scheduling asap scheduled } task intervalTask "Interval Task" { start 2007-06-17-06:00-+0000 scheduled } task lengthTask "Length Task" { start 2007-06-06-06:00-+0000 end 2007-06-19-23:00-+0000 scheduling asap scheduled } task effortTask "Effort Task" { start 2007-06-06-15:00-+0000 end 2007-06-19-23:00-+0000 scheduling asap scheduled } } supplement task t { priority 500 projectid prj } supplement task t.durationTask { priority 500 projectid prj } supplement task t.intervalTask { priority 500 projectid prj } supplement task t.lengthTask { priority 500 projectid prj } supplement task t.effortTask { booking tux 2007-06-06-15:00-+0000 + 8.0h, 2007-06-07-15:00-+0000 + 8.0h, 2007-06-08-15:00-+0000 + 8.0h, 2007-06-11-15:00-+0000 + 8.0h, 2007-06-12-15:00-+0000 + 8.0h, 2007-06-13-15:00-+0000 + 8.0h, 2007-06-14-15:00-+0000 + 8.0h, 2007-06-15-15:00-+0000 + 8.0h, 2007-06-18-15:00-+0000 + 8.0h, 2007-06-19-15:00-+0000 + 8.0h { overtime 2 } priority 500 projectid prj } supplement resource tux { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } TaskJuggler-3.8.1/test/TestSuite/Export-Reports/refs/Efficiency.tjp000066400000000000000000000032311473026623400253220ustar00rootroot00000000000000project prj "Resource Efficiency Example" "1.0" 2007-07-21-00:00-+0000 - 2007-07-22-00:00-+0000 { timezone "America/Denver" scenario plan "Plan Scenario" { active yes } } projectids prj resource tuxies "Tuxies" resource tux1 "Tux 1" resource tux2 "Tux 2" resource confRoom "Conference Room" task t "An important date" { start 2007-07-21-06:00-+0000 scheduled } supplement task t { priority 500 projectid prj } supplement resource tuxies { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } supplement resource tux1 { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } supplement resource tux2 { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } supplement resource confRoom { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } TaskJuggler-3.8.1/test/TestSuite/Export-Reports/refs/EnvVar.tjp000066400000000000000000000005511473026623400244610ustar00rootroot00000000000000project prj "Test" "1.0" 2011-05-05-00:00-+0000 - 2011-11-03-12:00-+0000 { timezone "UTC" scenario plan "Plan Scenario" { active yes } } projectids prj task t "A t_e_s_t_1 task" { start 2011-05-05-00:00-+0000 end 2011-09-05-00:00-+0000 scheduling asap scheduled } supplement task t { note "A test String" priority 500 projectid prj } TaskJuggler-3.8.1/test/TestSuite/Export-Reports/refs/Flags.tjp000066400000000000000000000017531473026623400243210ustar00rootroot00000000000000project prj "Flags Example" "1.0" 2005-07-21-00:00-+0000 - 2005-08-26-00:00-+0000 { timezone "America/Denver" scenario plan "Plan Scenario" { active yes } } flags important projectids prj task items "Project breakdown" { task plan "Plan work" { start 2005-07-22-06:00-+0000 end 2005-07-26-23:00-+0000 scheduling asap scheduled } task implementation "Implement work" { depends items.plan start 2005-07-26-23:00-+0000 end 2005-08-02-23:00-+0000 scheduling asap scheduled } task acceptance "Customer acceptance" { depends items.implementation start 2005-08-02-23:00-+0000 end 2005-08-07-23:00-+0000 scheduling asap scheduled } } supplement task items { priority 500 projectid prj } supplement task items.plan { flags important priority 500 projectid prj } supplement task items.implementation { priority 500 projectid prj } supplement task items.acceptance { flags important priority 500 projectid prj } TaskJuggler-3.8.1/test/TestSuite/Export-Reports/refs/Gap.tjp000066400000000000000000000010671473026623400237720ustar00rootroot00000000000000project prj "Example Project" "1.0" 2005-05-29-00:00-+0000 - 2005-07-01-00:00-+0000 { timezone "America/Denver" scenario plan "Plan Scenario" { active yes } } projectids prj task t1 "Task 1" { start 2005-05-29-06:00-+0000 scheduled } task t2 "Task 2" { depends t1 start 2005-06-03-06:00-+0000 scheduled } task t3 "Task 3" { depends t1 start 2005-06-03-23:00-+0000 scheduled } supplement task t1 { priority 500 projectid prj } supplement task t2 { priority 500 projectid prj } supplement task t3 { priority 500 projectid prj } TaskJuggler-3.8.1/test/TestSuite/Export-Reports/refs/HtmlTaskReport.tjp000066400000000000000000000027411473026623400262060ustar00rootroot00000000000000project prj "Simple Project" "1.0" 2005-06-06-00:00-+0000 - 2005-06-26-00:00-+0000 { timezone "America/Denver" scenario plan "Plan Scenario" { active yes } } projectids prj resource tux "Tux" task items "Project breakdown" { task plan "Plan work" { start 2005-06-06-06:00-+0000 end 2005-06-08-23:00-+0000 scheduling asap scheduled } task implementation "Implement work" { depends items.plan start 2005-06-09-15:00-+0000 end 2005-06-15-23:00-+0000 scheduling asap scheduled } task acceptance "Customer acceptance" { depends items.implementation start 2005-06-15-23:00-+0000 end 2005-06-20-23:00-+0000 scheduling asap scheduled } } supplement task items { priority 500 projectid prj } supplement task items.plan { priority 500 projectid prj } supplement task items.implementation { booking tux 2005-06-09-15:00-+0000 + 8.0h, 2005-06-10-15:00-+0000 + 8.0h, 2005-06-13-15:00-+0000 + 8.0h, 2005-06-14-15:00-+0000 + 8.0h, 2005-06-15-15:00-+0000 + 8.0h { overtime 2 } priority 500 projectid prj } supplement task items.acceptance { priority 500 projectid prj } supplement resource tux { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } TaskJuggler-3.8.1/test/TestSuite/Export-Reports/refs/Include.tjp000066400000000000000000000032361473026623400246460ustar00rootroot00000000000000project prj "Include Test" "1.0" 2010-02-26-00:00-+0000 - 2010-03-05-00:00-+0000 { timezone "America/Denver" scenario plan "Plan Scenario" { active yes } } projectids prj resource _Resource_1 "RF1" resource _Resource_2 "RF2" resource _Resource_3 "R" resource _Resource_4 "R2" task _Task_1 "Foo" { start 2010-02-26-00:00-+0000 scheduled } supplement task _Task_1 { priority 500 projectid prj } supplement resource _Resource_1 { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } supplement resource _Resource_2 { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } supplement resource _Resource_3 { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } supplement resource _Resource_4 { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } TaskJuggler-3.8.1/test/TestSuite/Export-Reports/refs/Journal.tjp000066400000000000000000000012001473026623400246620ustar00rootroot00000000000000project prj "Project with Journal" "1.0" 2009-05-04-00:00-+0000 - 2009-06-03-10:00-+0000 { timezone "America/Denver" scenario plan "Plan Scenario" { active yes } } projectids prj resource tux "Tux" task t1 "Task1" { start 2009-05-05-06:00-+0000 scheduled } supplement task t1 { priority 500 projectid prj } supplement resource tux { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } TaskJuggler-3.8.1/test/TestSuite/Export-Reports/refs/Limits-1.tjp000066400000000000000000000170111473026623400246560ustar00rootroot00000000000000project limits "Limits" "1.0" 2007-03-01-00:00-+0000 - 2008-02-29-00:00-+0000 { timezone "Europe/Amsterdam" scenario plan "Plan Scenario" { active yes } } projectids limits resource r1 "R1" resource r2 "R2" task t1 "Task 1" { start 2007-03-30-07:00-+0000 end 2007-05-07-09:00-+0000 scheduling asap scheduled } task t2 "Task 2" { start 2007-03-30-10:00-+0000 end 2007-05-01-12:00-+0000 scheduling asap scheduled } task t3 "Task 3" { start 2007-05-01-12:00-+0000 end 2007-06-04-13:00-+0000 scheduling asap scheduled } task t4 "Task 4" { start 2007-05-09-09:00-+0000 end 2007-06-07-13:00-+0000 scheduling asap scheduled } task t5 "Task 5" { start 2007-03-01-00:00-+0000 end 2007-04-30-00:00-+0000 scheduling asap scheduled } task t6 "Task 6" { start 2007-03-01-00:00-+0000 end 2007-04-30-00:00-+0000 scheduling asap scheduled } task t7 "Task 7" { start 2007-06-19-22:00-+0000 end 2007-07-09-22:00-+0000 scheduling asap scheduled } supplement task t1 { booking r2 2007-03-30-07:00-+0000 + 3.0h, 2007-04-02-07:00-+0000 + 3.0h, 2007-04-03-07:00-+0000 + 3.0h, 2007-04-04-07:00-+0000 + 3.0h, 2007-04-05-07:00-+0000 + 3.0h, 2007-04-06-07:00-+0000 + 3.0h, 2007-04-09-07:00-+0000 + 3.0h, 2007-04-10-07:00-+0000 + 3.0h, 2007-04-11-07:00-+0000 + 3.0h, 2007-04-12-07:00-+0000 + 3.0h, 2007-04-13-07:00-+0000 + 3.0h, 2007-04-16-07:00-+0000 + 3.0h, 2007-04-17-07:00-+0000 + 3.0h, 2007-04-18-07:00-+0000 + 3.0h, 2007-04-19-07:00-+0000 + 3.0h, 2007-04-20-07:00-+0000 + 3.0h, 2007-04-23-07:00-+0000 + 3.0h, 2007-04-24-07:00-+0000 + 3.0h, 2007-04-25-07:00-+0000 + 3.0h, 2007-04-26-07:00-+0000 + 3.0h, 2007-04-27-07:00-+0000 + 3.0h, 2007-04-30-07:00-+0000 + 3.0h, 2007-05-01-07:00-+0000 + 3.0h, 2007-05-02-07:00-+0000 + 3.0h, 2007-05-03-07:00-+0000 + 3.0h, 2007-05-04-07:00-+0000 + 3.0h, 2007-05-07-07:00-+0000 + 2.0h { overtime 2 } priority 500 projectid limits } supplement task t2 { booking r2 2007-03-30-10:00-+0000 + 5.0h, 2007-04-02-10:00-+0000 + 5.0h, 2007-04-03-10:00-+0000 + 5.0h, 2007-04-04-10:00-+0000 + 5.0h, 2007-04-05-10:00-+0000 + 2.0h, 2007-04-09-10:00-+0000 + 5.0h, 2007-04-10-10:00-+0000 + 5.0h, 2007-04-11-10:00-+0000 + 5.0h, 2007-04-12-10:00-+0000 + 2.0h, 2007-04-16-10:00-+0000 + 5.0h, 2007-04-17-10:00-+0000 + 5.0h, 2007-04-18-10:00-+0000 + 5.0h, 2007-04-19-10:00-+0000 + 2.0h, 2007-04-23-10:00-+0000 + 5.0h, 2007-04-24-10:00-+0000 + 5.0h, 2007-04-25-10:00-+0000 + 5.0h, 2007-04-26-10:00-+0000 + 2.0h, 2007-04-30-10:00-+0000 + 5.0h, 2007-05-01-10:00-+0000 + 2.0h { overtime 2 } priority 500 projectid limits } supplement task t3 { booking r2 2007-05-01-12:00-+0000 + 3.0h, 2007-05-02-10:00-+0000 + 5.0h, 2007-05-03-10:00-+0000 + 2.0h, 2007-05-07-09:00-+0000 + 6.0h, 2007-05-08-07:00-+0000 + 8.0h, 2007-05-09-07:00-+0000 + 2.0h, 2007-05-14-07:00-+0000 + 8.0h, 2007-05-15-07:00-+0000 + 8.0h, 2007-05-21-07:00-+0000 + 8.0h, 2007-05-22-07:00-+0000 + 8.0h, 2007-05-28-07:00-+0000 + 8.0h, 2007-05-29-07:00-+0000 + 8.0h, 2007-06-04-07:00-+0000 + 6.0h { overtime 2 } priority 500 projectid limits } supplement task t4 { booking r2 2007-05-09-09:00-+0000 + 6.0h, 2007-05-10-07:00-+0000 + 8.0h, 2007-05-16-07:00-+0000 + 8.0h, 2007-05-17-07:00-+0000 + 8.0h, 2007-05-23-07:00-+0000 + 8.0h, 2007-05-24-07:00-+0000 + 2.0h, 2007-05-30-07:00-+0000 + 8.0h, 2007-05-31-07:00-+0000 + 8.0h, 2007-06-04-13:00-+0000 + 2.0h, 2007-06-05-07:00-+0000 + 8.0h, 2007-06-06-07:00-+0000 + 8.0h, 2007-06-07-07:00-+0000 + 6.0h { overtime 2 } priority 500 projectid limits } supplement task t5 { booking r1 2007-03-01-08:00-+0000 + 2.0h, 2007-03-02-08:00-+0000 + 2.0h, 2007-03-05-08:00-+0000 + 2.0h, 2007-03-06-08:00-+0000 + 2.0h, 2007-03-07-08:00-+0000 + 2.0h, 2007-03-12-08:00-+0000 + 2.0h, 2007-03-13-08:00-+0000 + 2.0h, 2007-03-14-08:00-+0000 + 2.0h, 2007-03-19-08:00-+0000 + 2.0h, 2007-03-20-08:00-+0000 + 2.0h, 2007-04-02-07:00-+0000 + 2.0h, 2007-04-03-07:00-+0000 + 2.0h, 2007-04-04-07:00-+0000 + 2.0h, 2007-04-09-07:00-+0000 + 2.0h, 2007-04-10-07:00-+0000 + 2.0h, 2007-04-11-07:00-+0000 + 2.0h, 2007-04-16-07:00-+0000 + 2.0h, 2007-04-17-07:00-+0000 + 2.0h, 2007-04-18-07:00-+0000 + 2.0h, 2007-04-23-07:00-+0000 + 2.0h { overtime 2 } priority 500 projectid limits } supplement task t6 { booking r2 2007-03-01-08:00-+0000 + 4.0h, 2007-03-02-08:00-+0000 + 4.0h, 2007-03-05-08:00-+0000 + 4.0h, 2007-03-06-08:00-+0000 + 4.0h, 2007-03-07-08:00-+0000 + 4.0h, 2007-03-08-08:00-+0000 + 4.0h, 2007-03-09-08:00-+0000 + 4.0h, 2007-03-12-08:00-+0000 + 4.0h, 2007-03-13-08:00-+0000 + 4.0h, 2007-03-14-08:00-+0000 + 4.0h, 2007-03-15-08:00-+0000 + 4.0h, 2007-03-16-08:00-+0000 + 4.0h, 2007-03-19-08:00-+0000 + 4.0h, 2007-03-20-08:00-+0000 + 4.0h, 2007-03-21-08:00-+0000 + 4.0h, 2007-03-22-08:00-+0000 + 4.0h, 2007-03-23-08:00-+0000 + 4.0h, 2007-03-26-07:00-+0000 + 4.0h, 2007-03-27-07:00-+0000 + 4.0h, 2007-03-28-07:00-+0000 + 4.0h { overtime 2 } priority 500 projectid limits } supplement task t7 { booking r1 2007-06-20-07:00-+0000 + 2.0h, 2007-06-21-07:00-+0000 + 2.0h, 2007-06-22-07:00-+0000 + 2.0h, 2007-06-25-07:00-+0000 + 2.0h, 2007-06-26-07:00-+0000 + 2.0h, 2007-06-27-07:00-+0000 + 2.0h, 2007-07-02-07:00-+0000 + 2.0h, 2007-07-03-07:00-+0000 + 2.0h, 2007-07-04-07:00-+0000 + 2.0h, 2007-07-09-07:00-+0000 + 2.0h { overtime 2 } booking r2 2007-06-20-07:00-+0000 + 6.0h, 2007-06-21-07:00-+0000 + 6.0h, 2007-06-22-07:00-+0000 + 6.0h, 2007-06-25-07:00-+0000 + 6.0h, 2007-06-26-07:00-+0000 + 6.0h, 2007-06-27-07:00-+0000 + 6.0h, 2007-06-28-07:00-+0000 + 6.0h, 2007-06-29-07:00-+0000 + 6.0h, 2007-07-02-07:00-+0000 + 6.0h, 2007-07-03-07:00-+0000 + 6.0h, 2007-07-04-07:00-+0000 + 6.0h, 2007-07-05-07:00-+0000 + 6.0h, 2007-07-06-07:00-+0000 + 6.0h, 2007-07-09-07:00-+0000 + 6.0h { overtime 2 } priority 500 projectid limits } supplement resource r1 { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } supplement resource r2 { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } TaskJuggler-3.8.1/test/TestSuite/Export-Reports/refs/LoadUnits.tjp000066400000000000000000000312721473026623400251660ustar00rootroot00000000000000project simple "Simple Project" "$Id" 2000-01-01-00:00-+0000 - 2001-02-01-00:00-+0000 { timezone "America/Denver" scenario plan "Plan Scenario" { active yes } } projectids simple resource tux1 "Tux1" resource tux2 "Tux2" resource tux3 "Tux3" task t1 "Task1" { start 2000-01-03-16:00-+0000 end 2000-01-29-00:00-+0000 scheduling asap scheduled } task t2 "Task2" { start 2000-01-01-07:00-+0000 end 2000-12-20-00:00-+0000 scheduling asap scheduled } task t3 "Task3" { start 2000-01-03-16:00-+0000 end 2000-01-05-00:00-+0000 scheduling asap scheduled } supplement task t1 { booking tux1 2000-01-03-16:00-+0000 + 8.0h, 2000-01-04-16:00-+0000 + 8.0h, 2000-01-05-16:00-+0000 + 8.0h, 2000-01-06-16:00-+0000 + 8.0h, 2000-01-07-16:00-+0000 + 8.0h, 2000-01-10-16:00-+0000 + 8.0h, 2000-01-11-16:00-+0000 + 8.0h, 2000-01-12-16:00-+0000 + 8.0h, 2000-01-13-16:00-+0000 + 8.0h, 2000-01-14-16:00-+0000 + 8.0h, 2000-01-17-16:00-+0000 + 8.0h, 2000-01-18-16:00-+0000 + 8.0h, 2000-01-19-16:00-+0000 + 8.0h, 2000-01-20-16:00-+0000 + 8.0h, 2000-01-21-16:00-+0000 + 8.0h, 2000-01-24-16:00-+0000 + 8.0h, 2000-01-25-16:00-+0000 + 8.0h, 2000-01-26-16:00-+0000 + 8.0h, 2000-01-27-16:00-+0000 + 8.0h, 2000-01-28-16:00-+0000 + 8.0h { overtime 2 } priority 500 projectid simple } supplement task t2 { booking tux2 2000-01-03-16:00-+0000 + 8.0h, 2000-01-04-16:00-+0000 + 8.0h, 2000-01-05-16:00-+0000 + 8.0h, 2000-01-06-16:00-+0000 + 8.0h, 2000-01-07-16:00-+0000 + 8.0h, 2000-01-10-16:00-+0000 + 8.0h, 2000-01-11-16:00-+0000 + 8.0h, 2000-01-12-16:00-+0000 + 8.0h, 2000-01-13-16:00-+0000 + 8.0h, 2000-01-14-16:00-+0000 + 8.0h, 2000-01-17-16:00-+0000 + 8.0h, 2000-01-18-16:00-+0000 + 8.0h, 2000-01-19-16:00-+0000 + 8.0h, 2000-01-20-16:00-+0000 + 8.0h, 2000-01-21-16:00-+0000 + 8.0h, 2000-01-24-16:00-+0000 + 8.0h, 2000-01-25-16:00-+0000 + 8.0h, 2000-01-26-16:00-+0000 + 8.0h, 2000-01-27-16:00-+0000 + 8.0h, 2000-01-28-16:00-+0000 + 8.0h, 2000-01-31-16:00-+0000 + 8.0h, 2000-02-01-16:00-+0000 + 8.0h, 2000-02-02-16:00-+0000 + 8.0h, 2000-02-03-16:00-+0000 + 8.0h, 2000-02-04-16:00-+0000 + 8.0h, 2000-02-07-16:00-+0000 + 8.0h, 2000-02-08-16:00-+0000 + 8.0h, 2000-02-09-16:00-+0000 + 8.0h, 2000-02-10-16:00-+0000 + 8.0h, 2000-02-11-16:00-+0000 + 8.0h, 2000-02-14-16:00-+0000 + 8.0h, 2000-02-15-16:00-+0000 + 8.0h, 2000-02-16-16:00-+0000 + 8.0h, 2000-02-17-16:00-+0000 + 8.0h, 2000-02-18-16:00-+0000 + 8.0h, 2000-02-21-16:00-+0000 + 8.0h, 2000-02-22-16:00-+0000 + 8.0h, 2000-02-23-16:00-+0000 + 8.0h, 2000-02-24-16:00-+0000 + 8.0h, 2000-02-25-16:00-+0000 + 8.0h, 2000-02-28-16:00-+0000 + 8.0h, 2000-02-29-16:00-+0000 + 8.0h, 2000-03-01-16:00-+0000 + 8.0h, 2000-03-02-16:00-+0000 + 8.0h, 2000-03-03-16:00-+0000 + 8.0h, 2000-03-06-16:00-+0000 + 8.0h, 2000-03-07-16:00-+0000 + 8.0h, 2000-03-08-16:00-+0000 + 8.0h, 2000-03-09-16:00-+0000 + 8.0h, 2000-03-10-16:00-+0000 + 8.0h, 2000-03-13-16:00-+0000 + 8.0h, 2000-03-14-16:00-+0000 + 8.0h, 2000-03-15-16:00-+0000 + 8.0h, 2000-03-16-16:00-+0000 + 8.0h, 2000-03-17-16:00-+0000 + 8.0h, 2000-03-20-16:00-+0000 + 8.0h, 2000-03-21-16:00-+0000 + 8.0h, 2000-03-22-16:00-+0000 + 8.0h, 2000-03-23-16:00-+0000 + 8.0h, 2000-03-24-16:00-+0000 + 8.0h, 2000-03-27-16:00-+0000 + 8.0h, 2000-03-28-16:00-+0000 + 8.0h, 2000-03-29-16:00-+0000 + 8.0h, 2000-03-30-16:00-+0000 + 8.0h, 2000-03-31-16:00-+0000 + 8.0h, 2000-04-03-15:00-+0000 + 8.0h, 2000-04-04-15:00-+0000 + 8.0h, 2000-04-05-15:00-+0000 + 8.0h, 2000-04-06-15:00-+0000 + 8.0h, 2000-04-07-15:00-+0000 + 8.0h, 2000-04-10-15:00-+0000 + 8.0h, 2000-04-11-15:00-+0000 + 8.0h, 2000-04-12-15:00-+0000 + 8.0h, 2000-04-13-15:00-+0000 + 8.0h, 2000-04-14-15:00-+0000 + 8.0h, 2000-04-17-15:00-+0000 + 8.0h, 2000-04-18-15:00-+0000 + 8.0h, 2000-04-19-15:00-+0000 + 8.0h, 2000-04-20-15:00-+0000 + 8.0h, 2000-04-21-15:00-+0000 + 8.0h, 2000-04-24-15:00-+0000 + 8.0h, 2000-04-25-15:00-+0000 + 8.0h, 2000-04-26-15:00-+0000 + 8.0h, 2000-04-27-15:00-+0000 + 8.0h, 2000-04-28-15:00-+0000 + 8.0h, 2000-05-01-15:00-+0000 + 8.0h, 2000-05-02-15:00-+0000 + 8.0h, 2000-05-03-15:00-+0000 + 8.0h, 2000-05-04-15:00-+0000 + 8.0h, 2000-05-05-15:00-+0000 + 8.0h, 2000-05-08-15:00-+0000 + 8.0h, 2000-05-09-15:00-+0000 + 8.0h, 2000-05-10-15:00-+0000 + 8.0h, 2000-05-11-15:00-+0000 + 8.0h, 2000-05-12-15:00-+0000 + 8.0h, 2000-05-15-15:00-+0000 + 8.0h, 2000-05-16-15:00-+0000 + 8.0h, 2000-05-17-15:00-+0000 + 8.0h, 2000-05-18-15:00-+0000 + 8.0h, 2000-05-19-15:00-+0000 + 8.0h, 2000-05-22-15:00-+0000 + 8.0h, 2000-05-23-15:00-+0000 + 8.0h, 2000-05-24-15:00-+0000 + 8.0h, 2000-05-25-15:00-+0000 + 8.0h, 2000-05-26-15:00-+0000 + 8.0h, 2000-05-29-15:00-+0000 + 8.0h, 2000-05-30-15:00-+0000 + 8.0h, 2000-05-31-15:00-+0000 + 8.0h, 2000-06-01-15:00-+0000 + 8.0h, 2000-06-02-15:00-+0000 + 8.0h, 2000-06-05-15:00-+0000 + 8.0h, 2000-06-06-15:00-+0000 + 8.0h, 2000-06-07-15:00-+0000 + 8.0h, 2000-06-08-15:00-+0000 + 8.0h, 2000-06-09-15:00-+0000 + 8.0h, 2000-06-12-15:00-+0000 + 8.0h, 2000-06-13-15:00-+0000 + 8.0h, 2000-06-14-15:00-+0000 + 8.0h, 2000-06-15-15:00-+0000 + 8.0h, 2000-06-16-15:00-+0000 + 8.0h, 2000-06-19-15:00-+0000 + 8.0h, 2000-06-20-15:00-+0000 + 8.0h, 2000-06-21-15:00-+0000 + 8.0h, 2000-06-22-15:00-+0000 + 8.0h, 2000-06-23-15:00-+0000 + 8.0h, 2000-06-26-15:00-+0000 + 8.0h, 2000-06-27-15:00-+0000 + 8.0h, 2000-06-28-15:00-+0000 + 8.0h, 2000-06-29-15:00-+0000 + 8.0h, 2000-06-30-15:00-+0000 + 8.0h, 2000-07-03-15:00-+0000 + 8.0h, 2000-07-04-15:00-+0000 + 8.0h, 2000-07-05-15:00-+0000 + 8.0h, 2000-07-06-15:00-+0000 + 8.0h, 2000-07-07-15:00-+0000 + 8.0h, 2000-07-10-15:00-+0000 + 8.0h, 2000-07-11-15:00-+0000 + 8.0h, 2000-07-12-15:00-+0000 + 8.0h, 2000-07-13-15:00-+0000 + 8.0h, 2000-07-14-15:00-+0000 + 8.0h, 2000-07-17-15:00-+0000 + 8.0h, 2000-07-18-15:00-+0000 + 8.0h, 2000-07-19-15:00-+0000 + 8.0h, 2000-07-20-15:00-+0000 + 8.0h, 2000-07-21-15:00-+0000 + 8.0h, 2000-07-24-15:00-+0000 + 8.0h, 2000-07-25-15:00-+0000 + 8.0h, 2000-07-26-15:00-+0000 + 8.0h, 2000-07-27-15:00-+0000 + 8.0h, 2000-07-28-15:00-+0000 + 8.0h, 2000-07-31-15:00-+0000 + 8.0h, 2000-08-01-15:00-+0000 + 8.0h, 2000-08-02-15:00-+0000 + 8.0h, 2000-08-03-15:00-+0000 + 8.0h, 2000-08-04-15:00-+0000 + 8.0h, 2000-08-07-15:00-+0000 + 8.0h, 2000-08-08-15:00-+0000 + 8.0h, 2000-08-09-15:00-+0000 + 8.0h, 2000-08-10-15:00-+0000 + 8.0h, 2000-08-11-15:00-+0000 + 8.0h, 2000-08-14-15:00-+0000 + 8.0h, 2000-08-15-15:00-+0000 + 8.0h, 2000-08-16-15:00-+0000 + 8.0h, 2000-08-17-15:00-+0000 + 8.0h, 2000-08-18-15:00-+0000 + 8.0h, 2000-08-21-15:00-+0000 + 8.0h, 2000-08-22-15:00-+0000 + 8.0h, 2000-08-23-15:00-+0000 + 8.0h, 2000-08-24-15:00-+0000 + 8.0h, 2000-08-25-15:00-+0000 + 8.0h, 2000-08-28-15:00-+0000 + 8.0h, 2000-08-29-15:00-+0000 + 8.0h, 2000-08-30-15:00-+0000 + 8.0h, 2000-08-31-15:00-+0000 + 8.0h, 2000-09-01-15:00-+0000 + 8.0h, 2000-09-04-15:00-+0000 + 8.0h, 2000-09-05-15:00-+0000 + 8.0h, 2000-09-06-15:00-+0000 + 8.0h, 2000-09-07-15:00-+0000 + 8.0h, 2000-09-08-15:00-+0000 + 8.0h, 2000-09-11-15:00-+0000 + 8.0h, 2000-09-12-15:00-+0000 + 8.0h, 2000-09-13-15:00-+0000 + 8.0h, 2000-09-14-15:00-+0000 + 8.0h, 2000-09-15-15:00-+0000 + 8.0h, 2000-09-18-15:00-+0000 + 8.0h, 2000-09-19-15:00-+0000 + 8.0h, 2000-09-20-15:00-+0000 + 8.0h, 2000-09-21-15:00-+0000 + 8.0h, 2000-09-22-15:00-+0000 + 8.0h, 2000-09-25-15:00-+0000 + 8.0h, 2000-09-26-15:00-+0000 + 8.0h, 2000-09-27-15:00-+0000 + 8.0h, 2000-09-28-15:00-+0000 + 8.0h, 2000-09-29-15:00-+0000 + 8.0h, 2000-10-02-15:00-+0000 + 8.0h, 2000-10-03-15:00-+0000 + 8.0h, 2000-10-04-15:00-+0000 + 8.0h, 2000-10-05-15:00-+0000 + 8.0h, 2000-10-06-15:00-+0000 + 8.0h, 2000-10-09-15:00-+0000 + 8.0h, 2000-10-10-15:00-+0000 + 8.0h, 2000-10-11-15:00-+0000 + 8.0h, 2000-10-12-15:00-+0000 + 8.0h, 2000-10-13-15:00-+0000 + 8.0h, 2000-10-16-15:00-+0000 + 8.0h, 2000-10-17-15:00-+0000 + 8.0h, 2000-10-18-15:00-+0000 + 8.0h, 2000-10-19-15:00-+0000 + 8.0h, 2000-10-20-15:00-+0000 + 8.0h, 2000-10-23-15:00-+0000 + 8.0h, 2000-10-24-15:00-+0000 + 8.0h, 2000-10-25-15:00-+0000 + 8.0h, 2000-10-26-15:00-+0000 + 8.0h, 2000-10-27-15:00-+0000 + 8.0h, 2000-10-30-16:00-+0000 + 8.0h, 2000-10-31-16:00-+0000 + 8.0h, 2000-11-01-16:00-+0000 + 8.0h, 2000-11-02-16:00-+0000 + 8.0h, 2000-11-03-16:00-+0000 + 8.0h, 2000-11-06-16:00-+0000 + 8.0h, 2000-11-07-16:00-+0000 + 8.0h, 2000-11-08-16:00-+0000 + 8.0h, 2000-11-09-16:00-+0000 + 8.0h, 2000-11-10-16:00-+0000 + 8.0h, 2000-11-13-16:00-+0000 + 8.0h, 2000-11-14-16:00-+0000 + 8.0h, 2000-11-15-16:00-+0000 + 8.0h, 2000-11-16-16:00-+0000 + 8.0h, 2000-11-17-16:00-+0000 + 8.0h, 2000-11-20-16:00-+0000 + 8.0h, 2000-11-21-16:00-+0000 + 8.0h, 2000-11-22-16:00-+0000 + 8.0h, 2000-11-23-16:00-+0000 + 8.0h, 2000-11-24-16:00-+0000 + 8.0h, 2000-11-27-16:00-+0000 + 8.0h, 2000-11-28-16:00-+0000 + 8.0h, 2000-11-29-16:00-+0000 + 8.0h, 2000-11-30-16:00-+0000 + 8.0h, 2000-12-01-16:00-+0000 + 8.0h, 2000-12-04-16:00-+0000 + 8.0h, 2000-12-05-16:00-+0000 + 8.0h, 2000-12-06-16:00-+0000 + 8.0h, 2000-12-07-16:00-+0000 + 8.0h, 2000-12-08-16:00-+0000 + 8.0h, 2000-12-11-16:00-+0000 + 8.0h, 2000-12-12-16:00-+0000 + 8.0h, 2000-12-13-16:00-+0000 + 8.0h, 2000-12-14-16:00-+0000 + 8.0h, 2000-12-15-16:00-+0000 + 8.0h, 2000-12-18-16:00-+0000 + 8.0h, 2000-12-19-16:00-+0000 + 8.0h { overtime 2 } priority 500 projectid simple } supplement task t3 { booking tux3 2000-01-03-16:00-+0000 + 8.0h, 2000-01-04-16:00-+0000 + 8.0h { overtime 2 } priority 500 projectid simple } supplement resource tux1 { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } supplement resource tux2 { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } supplement resource tux3 { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } TaskJuggler-3.8.1/test/TestSuite/Export-Reports/refs/LogicalExpression.tjp000066400000000000000000000013321473026623400267100ustar00rootroot00000000000000project prj "LogExp" "1.0" 2009-10-19-00:00-+0000 - 2009-12-18-20:00-+0000 { timezone "America/Denver" scenario plan "Plan Scenario" { active yes } } projectids prj resource r "R" task _Task_1 "T" { start 2009-10-19-15:00-+0000 end 2009-10-19-23:00-+0000 scheduling asap scheduled } supplement task _Task_1 { booking r 2009-10-19-15:00-+0000 + 8.0h { overtime 2 } priority 500 projectid prj } supplement resource r { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } TaskJuggler-3.8.1/test/TestSuite/Export-Reports/refs/LogicalFunction.tjp000066400000000000000000000025421473026623400263420ustar00rootroot00000000000000project prj "Logical Function Demo" "1.0" 2009-11-21-00:00-+0000 - 2009-12-05-00:00-+0000 { timezone "America/Denver" scenario plan "Plan Scenario" { active yes } } projectids prj resource _Resource_1 "Team" { resource joe "Joe" } task _Task_1 "Parent" { task _Task_2 "Sub" { start 2009-11-23-16:00-+0000 end 2009-11-28-00:00-+0000 scheduling asap scheduled } } supplement task _Task_1 { priority 500 projectid prj } supplement task _Task_1._Task_2 { booking joe 2009-11-23-16:00-+0000 + 8.0h, 2009-11-24-16:00-+0000 + 8.0h, 2009-11-25-16:00-+0000 + 8.0h, 2009-11-26-16:00-+0000 + 8.0h, 2009-11-27-16:00-+0000 + 8.0h { overtime 2 } priority 500 projectid prj } supplement resource _Resource_1 { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } supplement resource joe { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } TaskJuggler-3.8.1/test/TestSuite/Export-Reports/refs/Macro-1.tjp000066400000000000000000000037271473026623400244670ustar00rootroot00000000000000project prj "Example Project" "1.0" 2008-01-18-00:00-+0000 - 2008-03-18-20:00-+0000 { timezone "America/Denver" scenario plan "Plan Scenario" { active yes } } projectids prj resource tux1 "Tux1" resource tux2 "Tux2" task t1 "Task1" { start 2008-01-18-00:00-+0000 scheduled } task t2 "Task2" { depends t1 start 2008-01-18-16:00-+0000 end 2008-02-01-00:00-+0000 scheduling asap scheduled } supplement task t1 { priority 500 projectid prj } supplement task t2 { booking tux1 2008-01-18-16:00-+0000 + 8.0h, 2008-01-21-16:00-+0000 + 8.0h, 2008-01-22-16:00-+0000 + 8.0h, 2008-01-23-16:00-+0000 + 8.0h, 2008-01-24-16:00-+0000 + 8.0h, 2008-01-25-16:00-+0000 + 8.0h, 2008-01-28-16:00-+0000 + 8.0h, 2008-01-29-16:00-+0000 + 8.0h, 2008-01-30-16:00-+0000 + 8.0h, 2008-01-31-16:00-+0000 + 8.0h { overtime 2 } booking tux2 2008-01-18-16:00-+0000 + 8.0h, 2008-01-21-16:00-+0000 + 8.0h, 2008-01-22-16:00-+0000 + 8.0h, 2008-01-23-16:00-+0000 + 8.0h, 2008-01-24-16:00-+0000 + 8.0h, 2008-01-25-16:00-+0000 + 8.0h, 2008-01-28-16:00-+0000 + 8.0h, 2008-01-29-16:00-+0000 + 8.0h, 2008-01-30-16:00-+0000 + 8.0h, 2008-01-31-16:00-+0000 + 8.0h { overtime 2 } priority 500 projectid prj } supplement resource tux1 { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } supplement resource tux2 { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } TaskJuggler-3.8.1/test/TestSuite/Export-Reports/refs/Macro-2.tjp000066400000000000000000000007031473026623400244570ustar00rootroot00000000000000project prj "Test" "1.0" 2010-04-28-00:00-+0000 - 2010-05-05-00:00-+0000 { timezone "America/Denver" scenario plan "Plan Scenario" { active yes } } projectids prj task _Task_1 " Crème brûlée Prepare " { start 2010-04-28-00:00-+0000 scheduled } task _Task_2 "task" { start 2010-04-28-00:00-+0000 scheduled } supplement task _Task_1 { priority 500 projectid prj } supplement task _Task_2 { priority 500 projectid prj } TaskJuggler-3.8.1/test/TestSuite/Export-Reports/refs/Macro-3.tjp000066400000000000000000000004671473026623400244670ustar00rootroot00000000000000project foo600 "Project" "6" 2009-12-01-00:00-+0000 - 2012-09-30-00:00-+0000 { timezone "America/Denver" scenario plan "Plan Scenario" { active yes } } projectids foo600 task _Task_1 "foo" { start 2009-12-01-00:00-+0000 scheduled } supplement task _Task_1 { priority 500 projectid foo600 } TaskJuggler-3.8.1/test/TestSuite/Export-Reports/refs/Manager.tjp000066400000000000000000000074101473026623400246330ustar00rootroot00000000000000project prj "test" "1.0" 2010-04-03-00:00-+0000 - 2010-04-10-00:00-+0000 { timezone "America/Denver" scenario plan "Plan Scenario" { active yes } } projectids prj resource _Resource_1 "The Company" { resource ceo "Big Boss" resource _Resource_3 "R&D Team" { resource vpe "VP Engineering" resource _Resource_5 "The Hacker" resource _Resource_6 "Doc Writer" } resource _Resource_7 "F&A Team" { resource coo "Chief Operating Officer" resource _Resource_9 "HR Lady" resource _Resource_10 "Accountant" } } task _Task_1 "T" { start 2010-04-03-00:00-+0000 scheduled } supplement task _Task_1 { priority 500 projectid prj } supplement resource _Resource_1 { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } supplement resource ceo { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } supplement resource _Resource_3 { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } supplement resource vpe { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } supplement resource _Resource_5 { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } supplement resource _Resource_6 { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } supplement resource _Resource_7 { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } supplement resource coo { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } supplement resource _Resource_9 { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } supplement resource _Resource_10 { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } TaskJuggler-3.8.1/test/TestSuite/Export-Reports/refs/Mandatory.tjp000066400000000000000000000027451473026623400252250ustar00rootroot00000000000000project prj "Project" "1.0" 2000-01-01-00:00-+0000 - 2000-03-01-00:00-+0000 { timingresolution 15min timezone "America/Denver" scenario plan "Plan Scenario" { active yes } } projectids prj resource tuxus "Tuxus" resource truck "Truck" task t "Ship stones to customers" { start 2000-01-03-16:00-+0000 end 2000-01-08-00:00-+0000 scheduling asap scheduled } supplement task t { booking truck 2000-01-03-16:00-+0000 + 8.0h, 2000-01-04-16:00-+0000 + 8.0h, 2000-01-05-16:00-+0000 + 8.0h, 2000-01-06-16:00-+0000 + 8.0h, 2000-01-07-16:00-+0000 + 8.0h { overtime 2 } booking tuxus 2000-01-03-16:00-+0000 + 8.0h, 2000-01-04-16:00-+0000 + 8.0h, 2000-01-05-16:00-+0000 + 8.0h, 2000-01-06-16:00-+0000 + 8.0h, 2000-01-07-16:00-+0000 + 8.0h { overtime 2 } priority 500 projectid prj } supplement resource tuxus { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } supplement resource truck { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } TaskJuggler-3.8.1/test/TestSuite/Export-Reports/refs/Milestone.tjp000066400000000000000000000007331473026623400252210ustar00rootroot00000000000000project prj "Milestone demo" "1.0" 2005-07-15-00:00-+0000 - 2005-08-01-00:00-+0000 { timezone "America/Denver" scenario plan "Plan Scenario" { active yes } } projectids prj task project_start "Project Start" { start 2005-07-15-06:00-+0000 scheduled } task deadline "Important Deadline" { start 2005-07-20-06:00-+0000 scheduled } supplement task project_start { priority 500 projectid prj } supplement task deadline { priority 500 projectid prj } TaskJuggler-3.8.1/test/TestSuite/Export-Reports/refs/MinMax.tjp000066400000000000000000000012111473026623400244430ustar00rootroot00000000000000project prj "Min Max Example" "1.0" 2005-06-06-00:00-+0000 - 2005-06-26-00:00-+0000 { timezone "America/Denver" scenario plan "Plan Scenario" { active yes } } projectids prj task items "Project breakdown" { task plan "Plan work" { start 2005-06-07-06:00-+0000 end 2005-06-09-23:00-+0000 scheduling asap scheduled } } supplement task items { priority 500 projectid prj } supplement task items.plan { maxend 2005-06-11-06:00-+0000 maxstart 2005-06-08-06:00-+0000 minend 2005-06-09-06:00-+0000 minstart 2005-06-06-06:00-+0000 note "Some more information about this task." priority 500 projectid prj } TaskJuggler-3.8.1/test/TestSuite/Export-Reports/refs/Niku.tjp000066400000000000000000000112511473026623400241650ustar00rootroot00000000000000project prj "Niku Test" "1.0" 2010-02-01-00:00-+0000 - 2010-05-03-06:00-+0000 { timezone "America/Denver" extend resource{ text ClarityRID "Clarity Resource ID" } extend task{ text ClarityPID "Clarity PID" text ClarityPName "Clarity Project Name" } scenario plan "Plan Scenario" { active yes } } projectids prj resource r1 "r1" resource r2 "r2" resource r3 "r3" task _Task_1 "T1" { start 2010-02-01-16:00-+0000 end 2010-03-06-00:00-+0000 scheduling asap scheduled } task t2 "T2" { start 2010-02-01-16:00-+0000 end 2010-02-13-00:00-+0000 scheduling asap scheduled } task _Task_3 "T3" { depends t2 start 2010-02-22-16:00-+0000 end 2010-03-06-00:00-+0000 scheduling asap scheduled } task _Task_4 "T4" { start 2010-02-01-16:00-+0000 end 2010-02-20-00:00-+0000 scheduling asap scheduled } supplement task _Task_1 { ClarityPID "p1" ClarityPName "Project 1" booking r1 2010-02-01-16:00-+0000 + 8.0h, 2010-02-02-16:00-+0000 + 8.0h, 2010-02-03-16:00-+0000 + 8.0h, 2010-02-04-16:00-+0000 + 8.0h, 2010-02-05-16:00-+0000 + 8.0h, 2010-02-08-16:00-+0000 + 8.0h, 2010-02-09-16:00-+0000 + 8.0h, 2010-02-10-16:00-+0000 + 8.0h, 2010-02-11-16:00-+0000 + 8.0h, 2010-02-12-16:00-+0000 + 8.0h, 2010-02-15-16:00-+0000 + 8.0h, 2010-02-16-16:00-+0000 + 8.0h, 2010-02-17-16:00-+0000 + 8.0h, 2010-02-18-16:00-+0000 + 8.0h, 2010-02-19-16:00-+0000 + 8.0h, 2010-02-22-16:00-+0000 + 8.0h, 2010-02-23-16:00-+0000 + 8.0h, 2010-02-24-16:00-+0000 + 8.0h, 2010-02-25-16:00-+0000 + 8.0h, 2010-02-26-16:00-+0000 + 8.0h, 2010-03-01-16:00-+0000 + 8.0h, 2010-03-02-16:00-+0000 + 8.0h, 2010-03-03-16:00-+0000 + 8.0h, 2010-03-04-16:00-+0000 + 8.0h, 2010-03-05-16:00-+0000 + 8.0h { overtime 2 } priority 500 projectid prj } supplement task t2 { ClarityPID "p1" ClarityPName "Project 1" booking r2 2010-02-01-16:00-+0000 + 8.0h, 2010-02-02-16:00-+0000 + 8.0h, 2010-02-03-16:00-+0000 + 8.0h, 2010-02-04-16:00-+0000 + 8.0h, 2010-02-05-16:00-+0000 + 8.0h, 2010-02-08-16:00-+0000 + 8.0h, 2010-02-09-16:00-+0000 + 8.0h, 2010-02-10-16:00-+0000 + 8.0h, 2010-02-11-16:00-+0000 + 8.0h, 2010-02-12-16:00-+0000 + 8.0h { overtime 2 } priority 500 projectid prj } supplement task _Task_3 { ClarityPID "p2" ClarityPName "Project 2" booking r2 2010-02-22-16:00-+0000 + 8.0h, 2010-02-23-16:00-+0000 + 8.0h, 2010-02-24-16:00-+0000 + 8.0h, 2010-02-25-16:00-+0000 + 8.0h, 2010-02-26-16:00-+0000 + 8.0h, 2010-03-01-16:00-+0000 + 8.0h, 2010-03-02-16:00-+0000 + 8.0h, 2010-03-03-16:00-+0000 + 8.0h, 2010-03-04-16:00-+0000 + 8.0h, 2010-03-05-16:00-+0000 + 8.0h { overtime 2 } priority 500 projectid prj } supplement task _Task_4 { ClarityPID "p2" ClarityPName "Project 2" booking r3 2010-02-01-16:00-+0000 + 8.0h, 2010-02-02-16:00-+0000 + 8.0h, 2010-02-03-16:00-+0000 + 8.0h, 2010-02-04-16:00-+0000 + 8.0h, 2010-02-05-16:00-+0000 + 8.0h, 2010-02-08-16:00-+0000 + 8.0h, 2010-02-09-16:00-+0000 + 8.0h, 2010-02-10-16:00-+0000 + 8.0h, 2010-02-11-16:00-+0000 + 8.0h, 2010-02-12-16:00-+0000 + 8.0h, 2010-02-15-16:00-+0000 + 8.0h, 2010-02-16-16:00-+0000 + 8.0h, 2010-02-17-16:00-+0000 + 8.0h, 2010-02-18-16:00-+0000 + 8.0h, 2010-02-19-16:00-+0000 + 8.0h { overtime 2 } priority 500 projectid prj } supplement resource r1 { ClarityRID "r1" workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } supplement resource r2 { ClarityRID "r2" workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } supplement resource r3 { ClarityRID "r3" workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } TaskJuggler-3.8.1/test/TestSuite/Export-Reports/refs/Numberformat.tjp000066400000000000000000000004441473026623400257220ustar00rootroot00000000000000project prj "Project" "1.0" 2000-01-01-00:00-+0000 - 2000-03-01-00:00-+0000 { timezone "Europe/Berlin" scenario plan "Plan Scenario" { active yes } } projectids prj task t "Task" { start 2000-01-01-00:00-+0000 scheduled } supplement task t { priority 500 projectid prj } TaskJuggler-3.8.1/test/TestSuite/Export-Reports/refs/Period.tjp000066400000000000000000000007241473026623400245040ustar00rootroot00000000000000project prj "Period Project" "1.0" 2006-09-24-00:00-+0000 - 2006-12-24-06:00-+0000 { timezone "America/Denver" scenario plan "Plan Scenario" { active yes } } projectids prj task items "Project breakdown" { task plan "Plan work" { start 2006-10-01-06:00-+0000 end 2006-10-15-06:00-+0000 scheduling asap scheduled } } supplement task items { priority 500 projectid prj } supplement task items.plan { priority 500 projectid prj } TaskJuggler-3.8.1/test/TestSuite/Export-Reports/refs/Persistent.tjp000066400000000000000000000023261473026623400254220ustar00rootroot00000000000000project prj "Project" "1.0" 2003-06-05-00:00-+0000 - 2003-07-05-00:00-+0000 { timezone "America/Denver" scenario plan "Plan Scenario" { active yes } } projectids prj resource r1 "Resource 1" resource r2 "Resource 2" task t1 "Task 1" { start 2003-06-05-15:00-+0000 end 2003-06-11-23:00-+0000 scheduling asap scheduled } supplement task t1 { booking r1 2003-06-05-15:00-+0000 + 8.0h, 2003-06-06-15:00-+0000 + 8.0h, 2003-06-09-15:00-+0000 + 8.0h, 2003-06-10-15:00-+0000 + 8.0h, 2003-06-11-15:00-+0000 + 8.0h { overtime 2 } priority 500 projectid prj } supplement resource r1 { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } supplement resource r2 { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } TaskJuggler-3.8.1/test/TestSuite/Export-Reports/refs/Precedes1.tjp000066400000000000000000000014071473026623400250740ustar00rootroot00000000000000project prj "P" "1.0" 2003-11-09-00:00-+0000 - 2003-12-24-00:00-+0000 { timezone "America/Denver" scenario plan "Plan Scenario" { active yes } } projectids prj task foo1 "foo1" { task foo2 "foo2" { start 2003-12-04-07:00-+0000 scheduled } task foo3 "foo3" { precedes foo1.foo2 start 2003-12-03-16:00-+0000 end 2003-12-04-07:00-+0000 scheduling alap scheduled } } task bar "bar" { precedes foo1.foo2 start 2003-12-02-16:00-+0000 end 2003-12-04-07:00-+0000 scheduling alap scheduled } supplement task foo1 { priority 500 projectid prj } supplement task foo1.foo2 { priority 500 projectid prj } supplement task foo1.foo3 { priority 500 projectid prj } supplement task bar { priority 500 projectid prj } TaskJuggler-3.8.1/test/TestSuite/Export-Reports/refs/Priority.tjp000066400000000000000000000120311473026623400250750ustar00rootroot00000000000000project prj "Priority Demo" "1.0" 2011-04-17-07:00-+0000 - 2011-06-17-03:00-+0000 { timezone "America/Denver" scenario plan "Plan Scenario" { active yes } } projectids prj resource tux "Tux" task jobs "Project breakdown" { task work "The regular work" { start 2011-04-18-15:00-+0000 end 2011-06-01-20:00-+0000 scheduling asap scheduled } task support "Customer Support" { start 2011-04-17-07:00-+0000 end 2011-06-17-03:00-+0000 scheduling alap scheduled } task conference "Attend Conference" { start 2011-04-25-06:00-+0000 end 2011-04-27-06:00-+0000 scheduling asap scheduled } task maintenance "Maintenance work" { start 2011-04-17-07:00-+0000 end 2011-06-17-03:00-+0000 scheduling alap scheduled } } supplement task jobs { priority 500 projectid prj } supplement task jobs.work { booking tux 2011-04-18-15:00-+0000 + 6.0h, 2011-04-19-15:00-+0000 + 6.0h, 2011-04-20-15:00-+0000 + 6.0h, 2011-04-21-15:00-+0000 + 6.0h, 2011-04-22-15:00-+0000 + 1.0h, 2011-04-27-15:00-+0000 + 6.0h, 2011-04-28-15:00-+0000 + 6.0h, 2011-04-29-15:00-+0000 + 6.0h, 2011-05-02-15:00-+0000 + 6.0h, 2011-05-03-15:00-+0000 + 6.0h, 2011-05-04-15:00-+0000 + 6.0h, 2011-05-05-15:00-+0000 + 6.0h, 2011-05-06-15:00-+0000 + 1.0h, 2011-05-09-15:00-+0000 + 6.0h, 2011-05-10-15:00-+0000 + 6.0h, 2011-05-11-15:00-+0000 + 6.0h, 2011-05-12-15:00-+0000 + 6.0h, 2011-05-13-15:00-+0000 + 1.0h, 2011-05-16-15:00-+0000 + 6.0h, 2011-05-17-15:00-+0000 + 6.0h, 2011-05-18-15:00-+0000 + 6.0h, 2011-05-19-15:00-+0000 + 6.0h, 2011-05-20-15:00-+0000 + 1.0h, 2011-05-23-15:00-+0000 + 6.0h, 2011-05-24-15:00-+0000 + 6.0h, 2011-05-25-15:00-+0000 + 6.0h, 2011-05-26-15:00-+0000 + 6.0h, 2011-05-27-15:00-+0000 + 1.0h, 2011-05-30-15:00-+0000 + 6.0h, 2011-05-31-15:00-+0000 + 6.0h, 2011-06-01-15:00-+0000 + 5.0h { overtime 2 } priority 500 projectid prj } supplement task jobs.support { booking tux 2011-04-18-21:00-+0000 + 2.0h, 2011-04-19-21:00-+0000 + 2.0h, 2011-04-20-21:00-+0000 + 2.0h, 2011-04-21-21:00-+0000 + 2.0h, 2011-04-22-21:00-+0000 + 2.0h, 2011-04-27-21:00-+0000 + 2.0h, 2011-04-28-21:00-+0000 + 2.0h, 2011-04-29-21:00-+0000 + 2.0h, 2011-05-02-21:00-+0000 + 2.0h, 2011-05-03-21:00-+0000 + 2.0h, 2011-05-04-21:00-+0000 + 2.0h, 2011-05-05-21:00-+0000 + 2.0h, 2011-05-06-21:00-+0000 + 2.0h, 2011-05-09-21:00-+0000 + 2.0h, 2011-05-10-21:00-+0000 + 2.0h, 2011-05-11-21:00-+0000 + 2.0h, 2011-05-12-21:00-+0000 + 2.0h, 2011-05-13-21:00-+0000 + 2.0h, 2011-05-16-21:00-+0000 + 2.0h, 2011-05-17-21:00-+0000 + 2.0h, 2011-05-18-21:00-+0000 + 2.0h, 2011-05-19-21:00-+0000 + 2.0h, 2011-05-20-21:00-+0000 + 2.0h, 2011-05-23-21:00-+0000 + 2.0h, 2011-05-24-21:00-+0000 + 2.0h, 2011-05-25-21:00-+0000 + 2.0h, 2011-05-26-21:00-+0000 + 2.0h, 2011-05-27-21:00-+0000 + 2.0h, 2011-05-30-21:00-+0000 + 2.0h, 2011-05-31-21:00-+0000 + 2.0h, 2011-06-01-21:00-+0000 + 2.0h, 2011-06-02-21:00-+0000 + 2.0h, 2011-06-03-21:00-+0000 + 2.0h, 2011-06-06-21:00-+0000 + 2.0h, 2011-06-07-21:00-+0000 + 2.0h, 2011-06-08-21:00-+0000 + 2.0h, 2011-06-09-21:00-+0000 + 2.0h, 2011-06-10-21:00-+0000 + 2.0h, 2011-06-13-21:00-+0000 + 2.0h, 2011-06-14-21:00-+0000 + 2.0h, 2011-06-15-21:00-+0000 + 2.0h, 2011-06-16-15:00-+0000 + 8.0h { overtime 2 } priority 800 projectid prj } supplement task jobs.conference { booking tux 2011-04-25-15:00-+0000 + 8.0h, 2011-04-26-15:00-+0000 + 8.0h { overtime 2 } priority 1000 projectid prj } supplement task jobs.maintenance { booking tux 2011-04-22-16:00-+0000 + 5.0h, 2011-05-06-16:00-+0000 + 5.0h, 2011-05-13-16:00-+0000 + 5.0h, 2011-05-20-16:00-+0000 + 5.0h, 2011-05-27-16:00-+0000 + 5.0h, 2011-06-01-20:00-+0000 + 1.0h, 2011-06-02-15:00-+0000 + 6.0h, 2011-06-03-15:00-+0000 + 6.0h, 2011-06-08-17:00-+0000 + 4.0h, 2011-06-09-15:00-+0000 + 6.0h, 2011-06-10-15:00-+0000 + 6.0h, 2011-06-13-15:00-+0000 + 6.0h, 2011-06-14-15:00-+0000 + 6.0h, 2011-06-15-15:00-+0000 + 6.0h { overtime 2 } priority 300 projectid prj } supplement resource tux { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } TaskJuggler-3.8.1/test/TestSuite/Export-Reports/refs/Project.tjp000066400000000000000000000004441473026623400246670ustar00rootroot00000000000000project prj "Example Project" "1.0" 2007-01-01-00:00-+0000 - 2007-03-09-00:00-+0000 { timezone "America/Denver" scenario plan "Plan" { active yes } } projectids prj task t "Task" { start 2007-01-01-07:00-+0000 scheduled } supplement task t { priority 500 projectid prj } TaskJuggler-3.8.1/test/TestSuite/Export-Reports/refs/ProjectIDs.tjp000066400000000000000000000010571473026623400252700ustar00rootroot00000000000000project prj1 "ProjectIDs example" "1.0" 2006-08-22-00:00-+0000 - 2006-09-21-10:00-+0000 { timezone "America/Denver" scenario plan "Plan Scenario" { active yes } } projectids prj, prj1, prj2 task t1 "Task 1" { start 2006-08-22-06:00-+0000 scheduled } task t2 "Task 2" { start 2006-08-22-06:00-+0000 scheduled } task t3 "Task 3" { start 2006-08-22-06:00-+0000 scheduled } supplement task t1 { priority 500 projectid prj } supplement task t2 { priority 500 projectid prj1 } supplement task t3 { priority 500 projectid prj2 } TaskJuggler-3.8.1/test/TestSuite/Export-Reports/refs/Query.tjp000066400000000000000000000021311473026623400243610ustar00rootroot00000000000000project prj "Query Demo" "1.0" 2009-11-22-00:00-+0000 - 2009-12-22-10:00-+0000 { timezone "America/Denver" scenario plan "Plan Scenario" { active yes } } projectids prj resource joe "Joe" task _Task_1 "Job" { start 2009-11-23-16:00-+0000 end 2009-12-05-00:00-+0000 scheduling asap scheduled } supplement task _Task_1 { booking joe 2009-11-23-16:00-+0000 + 8.0h, 2009-11-24-16:00-+0000 + 8.0h, 2009-11-25-16:00-+0000 + 8.0h, 2009-11-26-16:00-+0000 + 8.0h, 2009-11-27-16:00-+0000 + 8.0h, 2009-11-30-16:00-+0000 + 8.0h, 2009-12-01-16:00-+0000 + 8.0h, 2009-12-02-16:00-+0000 + 8.0h, 2009-12-03-16:00-+0000 + 8.0h, 2009-12-04-16:00-+0000 + 8.0h { overtime 2 } priority 500 projectid prj } supplement resource joe { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } TaskJuggler-3.8.1/test/TestSuite/Export-Reports/refs/Reports.tjp000066400000000000000000000057341473026623400247260ustar00rootroot00000000000000project prj "Test Project" "1.0" 2000-01-01-00:00-+0000 - 2000-03-01-00:00-+0000 { timezone "America/Denver" scenario plan "Plan Scenario" { active yes } } flags flag3, flag2, flag1, flag4 projectids prj resource r1 "FooResource 1" resource r2 "FooResource 2" resource r3 "FooResource 3" resource r4 "FooResource 4" task t1 "FooTask1" { task t1_1 "FooTask1_1" { start 2000-01-03-16:00-+0000 end 2000-01-15-00:00-+0000 scheduling asap scheduled } } task t2 "FooTask2" { start 2000-01-01-07:00-+0000 end 2000-01-02-07:00-+0000 scheduling asap scheduled } task t3 "FooTask3" { start 2000-01-01-07:00-+0000 scheduled } supplement task t1 { flags flag3 priority 500 projectid prj } supplement task t1.t1_1 { booking r1 2000-01-03-16:00-+0000 + 8.0h, 2000-01-04-16:00-+0000 + 8.0h, 2000-01-05-16:00-+0000 + 8.0h, 2000-01-06-16:00-+0000 + 8.0h, 2000-01-07-16:00-+0000 + 8.0h, 2000-01-10-16:00-+0000 + 8.0h, 2000-01-11-16:00-+0000 + 8.0h, 2000-01-12-16:00-+0000 + 8.0h, 2000-01-13-16:00-+0000 + 8.0h, 2000-01-14-16:00-+0000 + 8.0h { overtime 2 } booking r2 2000-01-03-16:00-+0000 + 8.0h, 2000-01-04-16:00-+0000 + 8.0h, 2000-01-05-16:00-+0000 + 8.0h, 2000-01-06-16:00-+0000 + 8.0h, 2000-01-07-16:00-+0000 + 8.0h, 2000-01-10-16:00-+0000 + 8.0h, 2000-01-11-16:00-+0000 + 8.0h, 2000-01-12-16:00-+0000 + 8.0h, 2000-01-13-16:00-+0000 + 8.0h, 2000-01-14-16:00-+0000 + 8.0h { overtime 2 } flags flag2 priority 500 projectid prj } supplement task t2 { flags flag1 priority 500 projectid prj } supplement task t3 { flags flag4 priority 500 projectid prj } supplement resource r1 { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } supplement resource r2 { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } supplement resource r3 { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } supplement resource r4 { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } TaskJuggler-3.8.1/test/TestSuite/Export-Reports/refs/Resource.tjp000066400000000000000000000031761473026623400250550ustar00rootroot00000000000000project prj "Resource Examples" "1.0" 2005-06-06-00:00-+0000 - 2005-06-26-00:00-+0000 { timezone "America/Denver" scenario plan "Plan Scenario" { active yes } } projectids prj resource tux1 "Tux1" resource team "A team" { resource tux2 "Tux2" resource tux3 "Tux3" } task t "An important date" { start 2005-06-10-06:00-+0000 scheduled } supplement task t { priority 500 projectid prj } supplement resource tux1 { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } supplement resource team { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } supplement resource tux2 { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } supplement resource tux3 { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } TaskJuggler-3.8.1/test/TestSuite/Export-Reports/refs/ResourcePrefix.tjp000066400000000000000000000035401473026623400262260ustar00rootroot00000000000000project prj "Resource Prefix Example" "1.0" 2009-09-13-00:00-+0000 - 2009-10-13-10:00-+0000 { timezone "America/Denver" scenario plan "Plan Scenario" { active yes } } projectids prj resource team "Team" { resource foo "Foo" resource bar "Bar" } task t "Task" { start 2009-09-14-14:00-+0000 end 2009-09-21-17:00-+0000 scheduling asap scheduled } supplement task t { booking bar 2009-09-14-14:00-+0000 + 7.0h, 2009-09-15-14:00-+0000 + 7.0h, 2009-09-16-14:00-+0000 + 7.0h, 2009-09-17-14:00-+0000 + 7.0h, 2009-09-18-14:00-+0000 + 7.0h, 2009-09-21-14:00-+0000 + 3.0h { overtime 2 } booking foo 2009-09-14-15:00-+0000 + 8.0h, 2009-09-15-15:00-+0000 + 8.0h, 2009-09-16-15:00-+0000 + 8.0h, 2009-09-17-15:00-+0000 + 8.0h, 2009-09-18-15:00-+0000 + 8.0h, 2009-09-21-15:00-+0000 + 2.0h { overtime 2 } priority 500 projectid prj } supplement resource team { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } supplement resource foo { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } supplement resource bar { workinghours sun off workinghours mon 8:00 - 15:00 workinghours tue 8:00 - 15:00 workinghours wed 8:00 - 15:00 workinghours thu 8:00 - 15:00 workinghours fri 8:00 - 15:00 workinghours sat off } TaskJuggler-3.8.1/test/TestSuite/Export-Reports/refs/ResourceRoot.tjp000066400000000000000000000051111473026623400257100ustar00rootroot00000000000000project prj "Test" "1.0" 2010-11-10-00:00-+0000 - 2011-01-09-20:00-+0000 { timezone "America/Denver" scenario plan "Plan Scenario" { active yes } } projectids prj resource org "Org" { resource team1 "Team1" { resource r1 "R1" resource r2 "R2" resource r3 "R3" } resource r4 "R4" } resource r5 "R5" task _Task_1 "T" { start 2010-11-10-00:00-+0000 scheduled } supplement task _Task_1 { priority 500 projectid prj } supplement resource org { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } supplement resource team1 { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } supplement resource r1 { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } supplement resource r2 { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } supplement resource r3 { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } supplement resource r4 { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } supplement resource r5 { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } TaskJuggler-3.8.1/test/TestSuite/Export-Reports/refs/Responsible.tjp000066400000000000000000000024051473026623400255450ustar00rootroot00000000000000project prj "Responsible Demo" "1.0" 2005-07-15-00:00-+0000 - 2005-08-01-00:00-+0000 { timezone "America/Denver" scenario plan "Plan Scenario" { active yes } } projectids prj resource tux "Tux" resource ubertux "Uber Tux" task someJob "Some Job" { start 2005-07-15-15:00-+0000 end 2005-07-21-23:00-+0000 scheduling asap scheduled } supplement task someJob { booking tux 2005-07-15-15:00-+0000 + 8.0h, 2005-07-18-15:00-+0000 + 8.0h, 2005-07-19-15:00-+0000 + 8.0h, 2005-07-20-15:00-+0000 + 8.0h, 2005-07-21-15:00-+0000 + 8.0h { overtime 2 } priority 500 projectid prj responsible ubertux } supplement resource tux { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } supplement resource ubertux { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } TaskJuggler-3.8.1/test/TestSuite/Export-Reports/refs/RollupResource.tjp000066400000000000000000000051111473026623400262420ustar00rootroot00000000000000project prj "Test" "1.0" 2010-11-10-00:00-+0000 - 2011-01-09-20:00-+0000 { timezone "America/Denver" scenario plan "Plan Scenario" { active yes } } projectids prj resource org "Org" { resource team1 "Team1" { resource r1 "R1" resource r2 "R2" resource r3 "R3" } resource r4 "R4" } resource r5 "R5" task _Task_1 "T" { start 2010-11-10-00:00-+0000 scheduled } supplement task _Task_1 { priority 500 projectid prj } supplement resource org { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } supplement resource team1 { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } supplement resource r1 { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } supplement resource r2 { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } supplement resource r3 { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } supplement resource r4 { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } supplement resource r5 { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } TaskJuggler-3.8.1/test/TestSuite/Export-Reports/refs/Scenario.tjp000066400000000000000000000007071473026623400250260ustar00rootroot00000000000000project prj "Example" "1.0" 2007-05-29-00:00-+0000 - 2007-07-01-00:00-+0000 { timezone "America/Denver" scenario plan "Planned Scenario" { scenario actual "Actual Scenario" { active yes } scenario test "Test Scenario" { active no } active yes } } projectids prj task t "Task" { start 2007-05-29-06:00-+0000 scheduled actual:start 2007-06-03-06:00-+0000 } supplement task t { priority 500 projectid prj } TaskJuggler-3.8.1/test/TestSuite/Export-Reports/refs/Scheduling.tjp000066400000000000000000000017661473026623400253560ustar00rootroot00000000000000project prj "Scheduling Example" "1.0" 2005-07-23-00:00-+0000 - 2005-09-01-00:00-+0000 { timezone "America/Denver" scenario plan "Plan Scenario" { active yes } } projectids prj task items "Project breakdown" { task t1 "Task 1" { start 2005-07-25-06:00-+0000 end 2005-08-01-06:00-+0000 scheduling alap scheduled } task t2 "Task 2" { start 2005-07-25-06:00-+0000 end 2005-08-01-06:00-+0000 scheduling asap scheduled } task t3 "Task 3" { start 2005-07-25-06:00-+0000 end 2005-08-01-06:00-+0000 scheduling asap scheduled } task t4 "Task 4" { start 2005-07-25-06:00-+0000 end 2005-08-01-06:00-+0000 scheduling alap scheduled } } supplement task items { priority 500 projectid prj } supplement task items.t1 { priority 500 projectid prj } supplement task items.t2 { priority 500 projectid prj } supplement task items.t3 { priority 500 projectid prj } supplement task items.t4 { priority 500 projectid prj } TaskJuggler-3.8.1/test/TestSuite/Export-Reports/refs/Select.tjp000066400000000000000000000037471473026623400245110ustar00rootroot00000000000000project prj "Project" "1.0" 2000-01-01-00:00-+0000 - 2000-03-01-00:00-+0000 { timezone "America/Denver" scenario plan "Plan Scenario" { active yes } } projectids prj resource tuxus "Tuxus" resource tuxia "Tuxia" task t1 "Task 1" { start 2000-01-03-16:00-+0000 end 2000-01-08-00:00-+0000 scheduling asap scheduled } task t2 "Task 2" { start 2000-01-03-16:00-+0000 end 2000-01-08-00:00-+0000 scheduling asap scheduled } task t3 "Task 3" { start 2000-01-10-16:00-+0000 end 2000-01-15-00:00-+0000 scheduling asap scheduled } supplement task t1 { booking tuxus 2000-01-03-16:00-+0000 + 8.0h, 2000-01-04-16:00-+0000 + 8.0h, 2000-01-05-16:00-+0000 + 8.0h, 2000-01-06-16:00-+0000 + 8.0h, 2000-01-07-16:00-+0000 + 8.0h { overtime 2 } priority 500 projectid prj } supplement task t2 { booking tuxia 2000-01-03-16:00-+0000 + 8.0h, 2000-01-04-16:00-+0000 + 8.0h, 2000-01-05-16:00-+0000 + 8.0h, 2000-01-06-16:00-+0000 + 8.0h, 2000-01-07-16:00-+0000 + 8.0h { overtime 2 } priority 500 projectid prj } supplement task t3 { booking tuxus 2000-01-10-16:00-+0000 + 8.0h, 2000-01-11-16:00-+0000 + 8.0h, 2000-01-12-16:00-+0000 + 8.0h, 2000-01-13-16:00-+0000 + 8.0h, 2000-01-14-16:00-+0000 + 8.0h { overtime 2 } priority 500 projectid prj } supplement resource tuxus { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } supplement resource tuxia { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } TaskJuggler-3.8.1/test/TestSuite/Export-Reports/refs/Shift.tjp000066400000000000000000000055001473026623400243340ustar00rootroot00000000000000project prj "Example" "1.0" 2000-01-01-00:00-+0000 - 2000-03-31-00:00-+0000 { timezone "America/Denver" scenario plan "Plan Scenario" { active yes } } projectids prj shift s1 "Shift1" { workinghours sun off workinghours mon 10:00 - 12:00, 13:00 - 15:00 workinghours tue 9:00 - 14:00 workinghours wed off workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off shift s2 "Shift2" { workinghours sun off workinghours mon 10:00 - 17:00 workinghours tue 9:00 - 14:00 workinghours wed off workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } } shift s3 "Part-time schedule 1" { workinghours sun off workinghours mon 9:00 - 12:00, 13:00 - 18:00 workinghours tue off workinghours wed 9:00 - 12:00 workinghours thu off workinghours fri 9:00 - 12:00, 13:00 - 18:00 workinghours sat off } shift s4 "Part-time schedule 2" { workinghours sun off workinghours mon off workinghours tue 9:00 - 12:00, 13:00 - 18:00 workinghours wed off workinghours thu 9:00 - 12:00, 13:00 - 18:00 workinghours fri off workinghours sat off } shift s5 "All-day, all-week shift" { workinghours sun 0:00 - 24:00 workinghours mon 0:00 - 24:00 workinghours tue 0:00 - 24:00 workinghours wed 0:00 - 24:00 workinghours thu 0:00 - 24:00 workinghours fri 0:00 - 24:00 workinghours sat 0:00 - 24:00 } resource r1 "Resource1" resource r2 "Resource2" task t1 "Task1" { start 2000-01-01-07:00-+0000 end 2000-02-08-20:00-+0000 scheduling asap scheduled } supplement task t1 { priority 500 projectid prj } supplement resource r1 { shifts s1 2000-01-01-07:00-+0000 - 2000-01-10-07:00-+0000, s2 2000-01-11-07:00-+0000 - 2000-01-20-07:00-+0000 workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } supplement resource r2 { shifts s3 2005-01-01-07:00-+0000 - 2005-01-15-07:00-+0000, s4 2005-01-15-07:00-+0000 - 2006-01-01-07:00-+0000 workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } TaskJuggler-3.8.1/test/TestSuite/Export-Reports/refs/Simple.tjp000066400000000000000000000027411473026623400245140ustar00rootroot00000000000000project prj "Simple Project" "1.0" 2005-06-06-00:00-+0000 - 2005-06-26-00:00-+0000 { timezone "America/Denver" scenario plan "Plan Scenario" { active yes } } projectids prj resource tux "Tux" task items "Project breakdown" { task plan "Plan work" { start 2005-06-06-06:00-+0000 end 2005-06-08-23:00-+0000 scheduling asap scheduled } task implementation "Implement work" { depends items.plan start 2005-06-09-15:00-+0000 end 2005-06-15-23:00-+0000 scheduling asap scheduled } task acceptance "Customer acceptance" { depends items.implementation start 2005-06-15-23:00-+0000 end 2005-06-20-23:00-+0000 scheduling asap scheduled } } supplement task items { priority 500 projectid prj } supplement task items.plan { priority 500 projectid prj } supplement task items.implementation { booking tux 2005-06-09-15:00-+0000 + 8.0h, 2005-06-10-15:00-+0000 + 8.0h, 2005-06-13-15:00-+0000 + 8.0h, 2005-06-14-15:00-+0000 + 8.0h, 2005-06-15-15:00-+0000 + 8.0h { overtime 2 } priority 500 projectid prj } supplement task items.acceptance { priority 500 projectid prj } supplement resource tux { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } TaskJuggler-3.8.1/test/TestSuite/Export-Reports/refs/StatusSheet.tjp000066400000000000000000000042651473026623400255420ustar00rootroot00000000000000project prj "test" "1.0" 2009-11-30-00:00-+0000 - 2010-01-29-20:00-+0000 { timezone "America/Denver" scenario plan "Plan Scenario" { active yes } } projectids prj resource r1 "R1" resource r2 "R2" resource r3 "R3" task t1 "Task 1" { start 2009-11-30-16:00-+0000 end 2009-12-05-00:00-+0000 scheduling asap scheduled } task t2 "Task 2" { task t3 "Task 3" { start 2009-11-30-16:00-+0000 end 2009-12-12-00:00-+0000 scheduling asap scheduled } } supplement task t1 { booking r1 2009-11-30-16:00-+0000 + 8.0h, 2009-12-01-16:00-+0000 + 8.0h, 2009-12-02-16:00-+0000 + 8.0h, 2009-12-03-16:00-+0000 + 8.0h, 2009-12-04-16:00-+0000 + 8.0h { overtime 2 } priority 500 projectid prj } supplement task t2 { priority 500 projectid prj } supplement task t2.t3 { booking r2 2009-11-30-16:00-+0000 + 8.0h, 2009-12-01-16:00-+0000 + 8.0h, 2009-12-02-16:00-+0000 + 8.0h, 2009-12-03-16:00-+0000 + 8.0h, 2009-12-04-16:00-+0000 + 8.0h, 2009-12-07-16:00-+0000 + 8.0h, 2009-12-08-16:00-+0000 + 8.0h, 2009-12-09-16:00-+0000 + 8.0h, 2009-12-10-16:00-+0000 + 8.0h, 2009-12-11-16:00-+0000 + 8.0h { overtime 2 } priority 500 projectid prj } supplement resource r1 { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } supplement resource r2 { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } supplement resource r3 { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } TaskJuggler-3.8.1/test/TestSuite/Export-Reports/refs/String.tjp000066400000000000000000000014741473026623400245330ustar00rootroot00000000000000project prj "String Tests" "1.0" 2005-06-06-00:00-+0000 - 2005-06-26-00:00-+0000 { timezone "America/Denver" scenario plan "Plan Scenario" { active yes } } projectids prj resource tux "Tux \"The Penguing\" Tuxus" task items "Project Plan\\\\Breakdown" { start 2005-06-06-15:00-+0000 end 2005-06-07-23:00-+0000 scheduling asap scheduled } supplement task items { booking tux 2005-06-06-15:00-+0000 + 8.0h, 2005-06-07-15:00-+0000 + 8.0h { overtime 2 } priority 500 projectid prj } supplement resource tux { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } TaskJuggler-3.8.1/test/TestSuite/Export-Reports/refs/Supplement.tjp000066400000000000000000000015511473026623400254150ustar00rootroot00000000000000project prj "Test Project" "1.0" 2000-01-01-00:00-+0000 - 2000-01-04-00:00-+0000 { timezone "America/Denver" scenario plan "Plan Scenario" { active yes } } flags important projectids prj resource joe "Joe" task top "Top Task" { task sub "Sub Task" { start 2000-01-01-00:00-+0000 end 2000-01-04-00:00-+0000 scheduling asap scheduled } } supplement task top { flags important priority 500 projectid prj } supplement task top.sub { booking joe 2000-01-03-16:00-+0000 + 8.0h { overtime 2 } priority 500 projectid prj } supplement resource joe { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } TaskJuggler-3.8.1/test/TestSuite/Export-Reports/refs/TaskPrefix.tjp000066400000000000000000000022051473026623400253360ustar00rootroot00000000000000project project "Include task prefix test" "1.0" 2010-12-01-00:00-+0000 - 2010-12-31-10:00-+0000 { timezone "Europe/Amsterdam" scenario plan "Plan Scenario" { active yes } } projectids project resource tux "Tux" task parent_task "Parent task" { task sub_task1 "Sub task 1" { start 2010-12-01-08:00-+0000 end 2010-12-01-16:00-+0000 scheduling asap scheduled } } task other_task "Other task" { start 2010-12-02-08:00-+0000 end 2010-12-02-16:00-+0000 scheduling asap scheduled } supplement task parent_task { priority 500 projectid project } supplement task parent_task.sub_task1 { booking tux 2010-12-01-08:00-+0000 + 8.0h { overtime 2 } priority 500 projectid project } supplement task other_task { booking tux 2010-12-02-08:00-+0000 + 8.0h { overtime 2 } priority 500 projectid project } supplement resource tux { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } TaskJuggler-3.8.1/test/TestSuite/Export-Reports/refs/TaskRoot.tjp000066400000000000000000000030761473026623400250330ustar00rootroot00000000000000project prj "Taskroot Example" "1.0" 2005-07-22-00:00-+0000 - 2005-08-26-00:00-+0000 { timezone "America/Denver" scenario plan "Plan Scenario" { active yes } } projectids prj task items "Project breakdown" { task plan "Plan work" { start 2005-07-22-06:00-+0000 end 2005-07-26-23:00-+0000 scheduling asap scheduled } task implementation "Implement work" { task phase1 "Phase 1" { depends items.plan start 2005-07-26-23:00-+0000 end 2005-08-02-23:00-+0000 scheduling asap scheduled } task phase2 "Phase 2" { depends items.implementation.phase1 start 2005-08-02-23:00-+0000 end 2005-08-05-23:00-+0000 scheduling asap scheduled } task phase3 "Phase 3" { depends items.implementation.phase2 start 2005-08-05-23:00-+0000 end 2005-08-11-23:00-+0000 scheduling asap scheduled } } task acceptance "Customer acceptance" { depends items.implementation start 2005-08-11-23:00-+0000 end 2005-08-16-23:00-+0000 scheduling asap scheduled } } supplement task items { priority 500 projectid prj } supplement task items.plan { priority 500 projectid prj } supplement task items.implementation { priority 500 projectid prj } supplement task items.implementation.phase1 { priority 500 projectid prj } supplement task items.implementation.phase2 { priority 500 projectid prj } supplement task items.implementation.phase3 { priority 500 projectid prj } supplement task items.acceptance { priority 500 projectid prj } TaskJuggler-3.8.1/test/TestSuite/Export-Reports/refs/TimeFrame.tjp000066400000000000000000000020541473026623400251310ustar00rootroot00000000000000project prj "Simple Project" "1.0" 2000-01-01-12:00-+0000 - 2000-01-04-18:00-+0000 { timezone "America/Denver" scenario plan "Plan Scenario" { active yes } } projectids prj resource tux "Tux" task t1 "Task1" { start 2000-01-01-19:00-+0000 end 2000-01-04-00:00-+0000 scheduling asap scheduled } task t2 "Task2" { start 2000-01-02-07:00-+0000 end 2000-01-03-07:00-+0000 scheduling asap scheduled } task t3 "Task3" { start 2000-01-03-16:00-+0000 end 2000-01-04-00:00-+0000 scheduling asap scheduled } supplement task t1 { priority 500 projectid prj } supplement task t2 { priority 500 projectid prj } supplement task t3 { booking tux 2000-01-03-16:00-+0000 + 8.0h { overtime 2 } priority 500 projectid prj } supplement resource tux { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } TaskJuggler-3.8.1/test/TestSuite/Export-Reports/refs/TimeSheet1.tjp000066400000000000000000000034331473026623400252320ustar00rootroot00000000000000project prj "test" "1.0" 2009-11-30-00:00-+0000 - 2010-01-29-20:00-+0000 { timezone "America/Denver" scenario plan "Plan Scenario" { active yes } } projectids prj resource r1 "R1" resource r2 "R2" task t1 "Task 1" { start 2009-11-30-16:00-+0000 end 2009-12-05-00:00-+0000 scheduling asap scheduled } task t2 "Task 2" { task t3 "Task 3" { start 2009-11-30-00:00-+0000 end 2009-12-10-00:00-+0000 scheduling asap scheduled } } supplement task t1 { booking r1 2009-11-30-16:00-+0000 + 8.0h, 2009-12-01-16:00-+0000 + 8.0h, 2009-12-02-16:00-+0000 + 8.0h, 2009-12-03-16:00-+0000 + 8.0h, 2009-12-04-16:00-+0000 + 8.0h { overtime 2 } priority 500 projectid prj } supplement task t2 { priority 500 projectid prj } supplement task t2.t3 { booking r2 2009-11-30-16:00-+0000 + 8.0h, 2009-12-01-16:00-+0000 + 8.0h, 2009-12-02-16:00-+0000 + 8.0h, 2009-12-03-16:00-+0000 + 8.0h, 2009-12-04-16:00-+0000 + 8.0h, 2009-12-07-16:00-+0000 + 8.0h, 2009-12-08-16:00-+0000 + 8.0h, 2009-12-09-16:00-+0000 + 8.0h { overtime 2 } priority 500 projectid prj } supplement resource r1 { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } supplement resource r2 { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } TaskJuggler-3.8.1/test/TestSuite/Export-Reports/refs/Timezone.tjp000066400000000000000000000004531473026623400250530ustar00rootroot00000000000000project tz "Timezone" "1.0" 2005-06-06-00:00-+0000 - 2005-06-07-00:00-+0000 { timezone "Europe/Athens" scenario plan "Plan Scenario" { active yes } } projectids tz task item "Project" { start 2005-06-06-09:00-+0000 scheduled } supplement task item { priority 500 projectid tz } TaskJuggler-3.8.1/test/TestSuite/Export-Reports/refs/Vacation.tjp000066400000000000000000000037221473026623400250270ustar00rootroot00000000000000project prj "Vacation Examples" "1.0" 2005-07-22-00:00-+0000 - 2006-01-01-00:00-+0000 { timezone "America/Denver" scenario plan "Plan Scenario" { active yes } } projectids prj resource team "A team" { resource tux2 "Tux2" resource tux3 "Tux3" } resource tuxia "Tuxia" resource tuxus "Tuxus" task t "An important date" { start 2005-07-22-06:00-+0000 scheduled } supplement task t { priority 500 projectid prj } supplement resource team { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } supplement resource tux2 { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } supplement resource tux3 { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } supplement resource tuxia { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } supplement resource tuxus { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } TaskJuggler-3.8.1/test/TestSuite/Export-Reports/refs/navigator.tjp000066400000000000000000000015621473026623400252550ustar00rootroot00000000000000project prj "Navigator Example" "1.0" 2011-12-12-00:00-+0000 - 2012-01-11-10:00-+0000 { timezone "UTC" scenario plan "Plan Scenario" { active yes } } projectids prj task foo "Foo" { task _Task_2 "Foo 1" { start 2011-12-12-00:00-+0000 scheduled } task _Task_3 "Foo 2" { start 2011-12-12-00:00-+0000 scheduled } } task bar "Bar" { task _Task_5 "Bar 1" { start 2011-12-12-00:00-+0000 scheduled } task _Task_6 "Bar 2" { start 2011-12-12-00:00-+0000 scheduled } } supplement task foo { priority 500 projectid prj } supplement task foo._Task_2 { priority 500 projectid prj } supplement task foo._Task_3 { priority 500 projectid prj } supplement task bar { priority 500 projectid prj } supplement task bar._Task_5 { priority 500 projectid prj } supplement task bar._Task_6 { priority 500 projectid prj } TaskJuggler-3.8.1/test/TestSuite/Export-Reports/refs/template.tjp000066400000000000000000000065001473026623400250730ustar00rootroot00000000000000project your_project_id "Your Project Title" "1.0" 2011-11-11-05:00-+0000 - 2012-03-11-21:00-+0000 { timezone "America/New_York" scenario plan "Plan Scenario" { active yes } } projectids your_project_id resource r1 "Resource 1" resource t1 "Team 1" { resource r2 "Resource 2" resource r3 "Resource 3" } resource s1 "System 1" task project "Project" { task wp1 "Workpackage 1" { task t1 "Task 1" { start 2011-11-11-05:00-+0000 scheduled } task t2 "Task 2" { start 2011-11-11-05:00-+0000 scheduled } } task wp2 "Work package 2" { depends project.wp1 task t1 "Task 1" { start 2011-11-11-05:00-+0000 scheduled } task t2 "Task 2" { start 2011-11-11-05:00-+0000 scheduled } } task deliveries "Deliveries" { task _Task_9 "Item 1" { depends project.wp1 start 2011-11-11-05:00-+0000 scheduled } task _Task_10 "Item 2" { depends project.wp2 start 2011-11-11-05:00-+0000 scheduled } } } supplement task project { priority 500 projectid your_project_id } supplement task project.wp1 { priority 500 projectid your_project_id } supplement task project.wp1.t1 { priority 500 projectid your_project_id } supplement task project.wp1.t2 { priority 500 projectid your_project_id } supplement task project.wp2 { priority 500 projectid your_project_id } supplement task project.wp2.t1 { priority 500 projectid your_project_id } supplement task project.wp2.t2 { priority 500 projectid your_project_id } supplement task project.deliveries { priority 500 projectid your_project_id } supplement task project.deliveries._Task_9 { priority 500 projectid your_project_id } supplement task project.deliveries._Task_10 { priority 500 projectid your_project_id } supplement resource r1 { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } supplement resource t1 { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } supplement resource r2 { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } supplement resource r3 { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } supplement resource s1 { workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } TaskJuggler-3.8.1/test/TestSuite/Export-Reports/refs/textreport.tjp000066400000000000000000000004421473026623400254770ustar00rootroot00000000000000project prj "Test" "1.0" 2011-12-11-00:00-+0000 - 2012-01-10-10:00-+0000 { timezone "UTC" scenario plan "Plan Scenario" { active yes } } projectids prj task _Task_1 "Foo" { start 2011-12-11-00:00-+0000 scheduled } supplement task _Task_1 { priority 500 projectid prj } TaskJuggler-3.8.1/test/TestSuite/Export-Reports/refs/tutorial.tjp000066400000000000000000000636161473026623400251360ustar00rootroot00000000000000project acso "Accounting Software" "1.0" 2002-01-16-00:00-+0000 - 2002-05-17-16:00-+0000 { timezone "Europe/Paris" extend resource{ text Phone "Phone" } scenario plan "Plan" { scenario delayed "Delayed" { active yes } active yes } } flags team projectids acso resource boss "Paul Henry Bullock" resource dev "Developers" { resource dev1 "Paul Smith" resource dev2 "Sébastien Bono" resource dev3 "Klaus Müller" } resource misc "The Others" { resource test "Peter Murphy" resource doc "Dim Sung" } task AcSo "Accounting Software" { task spec "Specification" { depends AcSo.deliveries.start start 2002-01-16-08:00-+0000 end 2002-01-24-14:00-+0000 scheduling asap scheduled delayed:start 2002-01-21-08:00-+0000 delayed:end 2002-01-29-14:00-+0000 delayed:scheduling asap } task software "Software Development" { depends AcSo.spec task database "Database coupling" { start 2002-01-24-14:00-+0000 end 2002-02-07-14:00-+0000 scheduling asap scheduled delayed:start 2002-01-29-14:00-+0000 delayed:end 2002-02-12-14:00-+0000 delayed:scheduling asap } task gui "Graphical User Interface" { depends AcSo.software.database, AcSo.software.backend start 2002-02-28-14:00-+0000 end 2002-03-28-13:00-+0000 scheduling asap scheduled delayed:start 2002-03-05-14:00-+0000 delayed:end 2002-04-08-11:00-+0000 delayed:scheduling asap } task backend "Back-End Functions" { depends AcSo.software.database start 2002-02-07-14:00-+0000 end 2002-02-28-14:00-+0000 scheduling asap scheduled delayed:start 2002-02-12-14:00-+0000 delayed:end 2002-03-05-14:00-+0000 delayed:scheduling asap } } task test "Software testing" { task alpha "Alpha Test" { depends AcSo.software start 2002-03-28-13:00-+0000 end 2002-04-03-10:00-+0000 scheduling asap scheduled delayed:start 2002-04-08-11:00-+0000 delayed:end 2002-04-11-09:00-+0000 delayed:scheduling asap } task beta "Beta Test" { depends AcSo.test.alpha start 2002-04-03-10:00-+0000 end 2002-04-18-13:00-+0000 scheduling asap scheduled delayed:start 2002-04-11-09:00-+0000 delayed:end 2002-04-26-12:00-+0000 delayed:scheduling asap } } task manual "Manual" { depends AcSo.deliveries.start start 2002-01-16-08:00-+0000 end 2002-02-26-11:00-+0000 scheduling asap scheduled delayed:start 2002-01-21-08:00-+0000 delayed:end 2002-03-01-11:00-+0000 delayed:scheduling asap } task deliveries "Milestones" { task start "Project start" { start 2002-01-16-00:00-+0000 scheduled delayed:start 2002-01-19-23:00-+0000 } task prev "Technology Preview" { depends AcSo.software.backend start 2002-02-28-14:00-+0000 scheduled delayed:start 2002-03-05-14:00-+0000 } task beta "Beta version" { depends AcSo.test.alpha start 2002-04-03-10:00-+0000 scheduled delayed:start 2002-04-11-09:00-+0000 } task done "Ship Product to Customer" { depends AcSo.test.beta, AcSo.manual start 2002-04-18-13:00-+0000 scheduled delayed:start 2002-04-26-12:00-+0000 } } } supplement task AcSo { priority 500 projectid acso responsible boss } supplement task AcSo.spec { booking dev1 2002-01-16-08:00-+0000 + 8.0h, 2002-01-17-08:00-+0000 + 8.0h, 2002-01-18-08:00-+0000 + 8.0h, 2002-01-21-08:00-+0000 + 8.0h, 2002-01-22-08:00-+0000 + 8.0h, 2002-01-23-08:00-+0000 + 8.0h, 2002-01-24-08:00-+0000 + 6.0h { overtime 2 } booking dev2 2002-01-16-08:00-+0000 + 8.0h, 2002-01-17-08:00-+0000 + 8.0h, 2002-01-18-08:00-+0000 + 8.0h, 2002-01-21-08:00-+0000 + 8.0h, 2002-01-22-08:00-+0000 + 8.0h, 2002-01-23-08:00-+0000 + 8.0h, 2002-01-24-08:00-+0000 + 5.0h { overtime 2 } booking dev3 2002-01-16-08:00-+0000 + 8.0h, 2002-01-17-08:00-+0000 + 8.0h, 2002-01-18-08:00-+0000 + 8.0h, 2002-01-21-08:00-+0000 + 8.0h, 2002-01-22-08:00-+0000 + 8.0h, 2002-01-23-08:00-+0000 + 8.0h, 2002-01-24-08:00-+0000 + 5.0h { overtime 2 } delayed:booking dev1 2002-01-21-08:00-+0000 + 8.0h, 2002-01-22-08:00-+0000 + 8.0h, 2002-01-23-08:00-+0000 + 8.0h, 2002-01-24-08:00-+0000 + 8.0h, 2002-01-25-08:00-+0000 + 8.0h, 2002-01-28-08:00-+0000 + 8.0h, 2002-01-29-08:00-+0000 + 6.0h { overtime 2 } delayed:booking dev2 2002-01-21-08:00-+0000 + 8.0h, 2002-01-22-08:00-+0000 + 8.0h, 2002-01-23-08:00-+0000 + 8.0h, 2002-01-24-08:00-+0000 + 8.0h, 2002-01-25-08:00-+0000 + 8.0h, 2002-01-28-08:00-+0000 + 8.0h, 2002-01-29-08:00-+0000 + 5.0h { overtime 2 } delayed:booking dev3 2002-01-21-08:00-+0000 + 8.0h, 2002-01-22-08:00-+0000 + 8.0h, 2002-01-23-08:00-+0000 + 8.0h, 2002-01-24-08:00-+0000 + 8.0h, 2002-01-25-08:00-+0000 + 8.0h, 2002-01-28-08:00-+0000 + 8.0h, 2002-01-29-08:00-+0000 + 5.0h { overtime 2 } priority 500 projectid acso responsible boss } supplement task AcSo.software { priority 1000 projectid acso responsible boss, dev1 } supplement task AcSo.software.database { booking dev1 2002-01-24-14:00-+0000 + 2.0h, 2002-01-25-08:00-+0000 + 8.0h, 2002-01-28-08:00-+0000 + 8.0h, 2002-01-29-08:00-+0000 + 8.0h, 2002-01-30-08:00-+0000 + 8.0h, 2002-01-31-08:00-+0000 + 8.0h, 2002-02-01-08:00-+0000 + 8.0h, 2002-02-04-08:00-+0000 + 8.0h, 2002-02-05-08:00-+0000 + 8.0h, 2002-02-06-08:00-+0000 + 8.0h, 2002-02-07-08:00-+0000 + 6.0h { overtime 2 } booking dev2 2002-01-24-14:00-+0000 + 2.0h, 2002-01-25-08:00-+0000 + 8.0h, 2002-01-28-08:00-+0000 + 8.0h, 2002-01-29-08:00-+0000 + 8.0h, 2002-01-30-08:00-+0000 + 8.0h, 2002-01-31-08:00-+0000 + 8.0h, 2002-02-01-08:00-+0000 + 8.0h, 2002-02-04-08:00-+0000 + 8.0h, 2002-02-05-08:00-+0000 + 8.0h, 2002-02-06-08:00-+0000 + 8.0h, 2002-02-07-08:00-+0000 + 6.0h { overtime 2 } delayed:booking dev1 2002-01-29-14:00-+0000 + 2.0h, 2002-01-30-08:00-+0000 + 8.0h, 2002-01-31-08:00-+0000 + 8.0h, 2002-02-01-08:00-+0000 + 8.0h, 2002-02-04-08:00-+0000 + 8.0h, 2002-02-05-08:00-+0000 + 8.0h, 2002-02-06-08:00-+0000 + 8.0h, 2002-02-07-08:00-+0000 + 8.0h, 2002-02-08-08:00-+0000 + 8.0h, 2002-02-11-08:00-+0000 + 8.0h, 2002-02-12-08:00-+0000 + 6.0h { overtime 2 } delayed:booking dev2 2002-01-29-14:00-+0000 + 2.0h, 2002-01-30-08:00-+0000 + 8.0h, 2002-01-31-08:00-+0000 + 8.0h, 2002-02-01-08:00-+0000 + 8.0h, 2002-02-04-08:00-+0000 + 8.0h, 2002-02-05-08:00-+0000 + 8.0h, 2002-02-06-08:00-+0000 + 8.0h, 2002-02-07-08:00-+0000 + 8.0h, 2002-02-08-08:00-+0000 + 8.0h, 2002-02-11-08:00-+0000 + 8.0h, 2002-02-12-08:00-+0000 + 6.0h { overtime 2 } priority 1000 projectid acso responsible boss, dev1 } supplement task AcSo.software.gui { booking dev2 2002-02-28-14:00-+0000 + 2.0h, 2002-03-01-08:00-+0000 + 6.0h, 2002-03-04-08:00-+0000 + 6.0h, 2002-03-05-08:00-+0000 + 6.0h, 2002-03-06-08:00-+0000 + 6.0h, 2002-03-07-08:00-+0000 + 6.0h, 2002-03-08-08:00-+0000 + 6.0h, 2002-03-11-08:00-+0000 + 6.0h, 2002-03-12-08:00-+0000 + 6.0h, 2002-03-13-08:00-+0000 + 6.0h, 2002-03-14-08:00-+0000 + 6.0h, 2002-03-15-08:00-+0000 + 6.0h, 2002-03-18-08:00-+0000 + 6.0h, 2002-03-19-08:00-+0000 + 6.0h, 2002-03-20-08:00-+0000 + 6.0h, 2002-03-21-08:00-+0000 + 6.0h, 2002-03-22-08:00-+0000 + 6.0h, 2002-03-25-08:00-+0000 + 6.0h, 2002-03-26-08:00-+0000 + 6.0h, 2002-03-27-08:00-+0000 + 6.0h, 2002-03-28-08:00-+0000 + 5.0h { overtime 2 } booking dev3 2002-02-28-14:00-+0000 + 2.0h, 2002-03-01-08:00-+0000 + 8.0h, 2002-03-04-08:00-+0000 + 8.0h, 2002-03-05-08:00-+0000 + 8.0h, 2002-03-06-08:00-+0000 + 8.0h, 2002-03-07-08:00-+0000 + 8.0h, 2002-03-08-08:00-+0000 + 8.0h, 2002-03-11-08:00-+0000 + 8.0h, 2002-03-12-08:00-+0000 + 8.0h, 2002-03-13-08:00-+0000 + 8.0h, 2002-03-14-08:00-+0000 + 8.0h, 2002-03-15-08:00-+0000 + 8.0h, 2002-03-18-08:00-+0000 + 8.0h, 2002-03-19-08:00-+0000 + 8.0h, 2002-03-20-08:00-+0000 + 8.0h, 2002-03-21-08:00-+0000 + 8.0h, 2002-03-22-08:00-+0000 + 8.0h, 2002-03-25-08:00-+0000 + 8.0h, 2002-03-26-08:00-+0000 + 8.0h, 2002-03-27-08:00-+0000 + 8.0h, 2002-03-28-08:00-+0000 + 5.0h { overtime 2 } delayed:booking dev2 2002-03-05-14:00-+0000 + 2.0h, 2002-03-06-08:00-+0000 + 6.0h, 2002-03-07-08:00-+0000 + 6.0h, 2002-03-08-08:00-+0000 + 6.0h, 2002-03-11-08:00-+0000 + 6.0h, 2002-03-12-08:00-+0000 + 6.0h, 2002-03-13-08:00-+0000 + 6.0h, 2002-03-14-08:00-+0000 + 6.0h, 2002-03-15-08:00-+0000 + 6.0h, 2002-03-18-08:00-+0000 + 6.0h, 2002-03-19-08:00-+0000 + 6.0h, 2002-03-20-08:00-+0000 + 6.0h, 2002-03-21-08:00-+0000 + 6.0h, 2002-03-22-08:00-+0000 + 6.0h, 2002-03-25-08:00-+0000 + 6.0h, 2002-03-26-08:00-+0000 + 6.0h, 2002-03-27-08:00-+0000 + 6.0h, 2002-03-28-08:00-+0000 + 6.0h, 2002-04-01-07:00-+0000 + 6.0h, 2002-04-02-07:00-+0000 + 6.0h, 2002-04-03-07:00-+0000 + 6.0h, 2002-04-04-07:00-+0000 + 6.0h, 2002-04-05-07:00-+0000 + 6.0h, 2002-04-08-07:00-+0000 + 4.0h { overtime 2 } delayed:booking dev3 2002-03-05-14:00-+0000 + 2.0h, 2002-03-06-08:00-+0000 + 8.0h, 2002-03-07-08:00-+0000 + 8.0h, 2002-03-08-08:00-+0000 + 8.0h, 2002-03-11-08:00-+0000 + 8.0h, 2002-03-12-08:00-+0000 + 8.0h, 2002-03-13-08:00-+0000 + 8.0h, 2002-03-14-08:00-+0000 + 8.0h, 2002-03-15-08:00-+0000 + 8.0h, 2002-03-18-08:00-+0000 + 8.0h, 2002-03-19-08:00-+0000 + 8.0h, 2002-03-20-08:00-+0000 + 8.0h, 2002-03-21-08:00-+0000 + 8.0h, 2002-03-22-08:00-+0000 + 8.0h, 2002-03-25-08:00-+0000 + 8.0h, 2002-03-26-08:00-+0000 + 8.0h, 2002-03-27-08:00-+0000 + 8.0h, 2002-03-28-08:00-+0000 + 8.0h, 2002-04-01-07:00-+0000 + 8.0h, 2002-04-02-07:00-+0000 + 8.0h, 2002-04-03-07:00-+0000 + 8.0h, 2002-04-04-07:00-+0000 + 8.0h, 2002-04-05-07:00-+0000 + 8.0h, 2002-04-08-07:00-+0000 + 4.0h { overtime 2 } priority 1000 projectid acso responsible boss, dev1 } supplement task AcSo.software.backend { booking dev1 2002-02-07-14:00-+0000 + 2.0h, 2002-02-08-08:00-+0000 + 8.0h, 2002-02-11-08:00-+0000 + 8.0h, 2002-02-12-08:00-+0000 + 8.0h, 2002-02-13-08:00-+0000 + 8.0h, 2002-02-14-08:00-+0000 + 8.0h, 2002-02-15-08:00-+0000 + 8.0h, 2002-02-18-08:00-+0000 + 8.0h, 2002-02-19-08:00-+0000 + 8.0h, 2002-02-20-08:00-+0000 + 8.0h, 2002-02-21-08:00-+0000 + 8.0h, 2002-02-22-08:00-+0000 + 8.0h, 2002-02-25-08:00-+0000 + 8.0h, 2002-02-26-08:00-+0000 + 8.0h, 2002-02-27-08:00-+0000 + 8.0h, 2002-02-28-08:00-+0000 + 6.0h { overtime 2 } booking dev2 2002-02-07-14:00-+0000 + 2.0h, 2002-02-08-08:00-+0000 + 8.0h, 2002-02-11-08:00-+0000 + 8.0h, 2002-02-12-08:00-+0000 + 8.0h, 2002-02-13-08:00-+0000 + 8.0h, 2002-02-14-08:00-+0000 + 8.0h, 2002-02-15-08:00-+0000 + 8.0h, 2002-02-18-08:00-+0000 + 8.0h, 2002-02-19-08:00-+0000 + 8.0h, 2002-02-20-08:00-+0000 + 8.0h, 2002-02-21-08:00-+0000 + 8.0h, 2002-02-22-08:00-+0000 + 8.0h, 2002-02-25-08:00-+0000 + 8.0h, 2002-02-26-08:00-+0000 + 8.0h, 2002-02-27-08:00-+0000 + 8.0h, 2002-02-28-08:00-+0000 + 6.0h { overtime 2 } delayed:booking dev1 2002-02-12-14:00-+0000 + 2.0h, 2002-02-13-08:00-+0000 + 8.0h, 2002-02-14-08:00-+0000 + 8.0h, 2002-02-15-08:00-+0000 + 8.0h, 2002-02-18-08:00-+0000 + 8.0h, 2002-02-19-08:00-+0000 + 8.0h, 2002-02-20-08:00-+0000 + 8.0h, 2002-02-21-08:00-+0000 + 8.0h, 2002-02-22-08:00-+0000 + 8.0h, 2002-02-25-08:00-+0000 + 8.0h, 2002-02-26-08:00-+0000 + 8.0h, 2002-02-27-08:00-+0000 + 8.0h, 2002-02-28-08:00-+0000 + 8.0h, 2002-03-01-08:00-+0000 + 8.0h, 2002-03-04-08:00-+0000 + 8.0h, 2002-03-05-08:00-+0000 + 6.0h { overtime 2 } delayed:booking dev2 2002-02-12-14:00-+0000 + 2.0h, 2002-02-13-08:00-+0000 + 8.0h, 2002-02-14-08:00-+0000 + 8.0h, 2002-02-15-08:00-+0000 + 8.0h, 2002-02-18-08:00-+0000 + 8.0h, 2002-02-19-08:00-+0000 + 8.0h, 2002-02-20-08:00-+0000 + 8.0h, 2002-02-21-08:00-+0000 + 8.0h, 2002-02-22-08:00-+0000 + 8.0h, 2002-02-25-08:00-+0000 + 8.0h, 2002-02-26-08:00-+0000 + 8.0h, 2002-02-27-08:00-+0000 + 8.0h, 2002-02-28-08:00-+0000 + 8.0h, 2002-03-01-08:00-+0000 + 8.0h, 2002-03-04-08:00-+0000 + 8.0h, 2002-03-05-08:00-+0000 + 6.0h { overtime 2 } complete 95 priority 1000 projectid acso responsible boss, dev1 } supplement task AcSo.test { priority 500 projectid acso responsible boss } supplement task AcSo.test.alpha { booking dev2 2002-03-28-13:00-+0000 + 3.0h, 2002-04-01-07:00-+0000 + 8.0h, 2002-04-02-07:00-+0000 + 8.0h, 2002-04-03-07:00-+0000 + 3.0h { overtime 2 } booking test 2002-03-28-13:00-+0000 + 3.0h, 2002-04-01-07:00-+0000 + 6.0h, 2002-04-02-07:00-+0000 + 6.0h, 2002-04-03-07:00-+0000 + 3.0h { overtime 2 } delayed:booking dev2 2002-04-08-11:00-+0000 + 4.0h, 2002-04-09-07:00-+0000 + 8.0h, 2002-04-10-07:00-+0000 + 8.0h, 2002-04-11-07:00-+0000 + 2.0h { overtime 2 } delayed:booking test 2002-04-08-11:00-+0000 + 4.0h, 2002-04-09-07:00-+0000 + 6.0h, 2002-04-10-07:00-+0000 + 6.0h, 2002-04-11-07:00-+0000 + 2.0h { overtime 2 } note "Hopefully most bugs will be found and fixed here." priority 500 projectid acso responsible boss } supplement task AcSo.test.beta { booking dev1 2002-04-03-10:00-+0000 + 5.0h, 2002-04-04-07:00-+0000 + 8.0h, 2002-04-05-07:00-+0000 + 8.0h, 2002-04-08-07:00-+0000 + 8.0h, 2002-04-09-07:00-+0000 + 8.0h, 2002-04-10-07:00-+0000 + 8.0h, 2002-04-11-07:00-+0000 + 8.0h, 2002-04-12-07:00-+0000 + 8.0h, 2002-04-15-07:00-+0000 + 8.0h, 2002-04-16-07:00-+0000 + 8.0h, 2002-04-17-07:00-+0000 + 8.0h, 2002-04-18-07:00-+0000 + 6.0h { overtime 2 } booking test 2002-04-03-10:00-+0000 + 3.0h, 2002-04-04-07:00-+0000 + 6.0h, 2002-04-05-07:00-+0000 + 6.0h, 2002-04-08-07:00-+0000 + 6.0h, 2002-04-09-07:00-+0000 + 6.0h, 2002-04-10-07:00-+0000 + 6.0h, 2002-04-11-07:00-+0000 + 6.0h, 2002-04-12-07:00-+0000 + 6.0h, 2002-04-15-07:00-+0000 + 6.0h, 2002-04-16-07:00-+0000 + 6.0h, 2002-04-17-07:00-+0000 + 6.0h, 2002-04-18-07:00-+0000 + 6.0h { overtime 2 } delayed:booking dev1 2002-04-11-09:00-+0000 + 6.0h, 2002-04-12-07:00-+0000 + 8.0h, 2002-04-15-07:00-+0000 + 8.0h, 2002-04-16-07:00-+0000 + 8.0h, 2002-04-17-07:00-+0000 + 8.0h, 2002-04-18-07:00-+0000 + 8.0h, 2002-04-19-07:00-+0000 + 8.0h, 2002-04-22-07:00-+0000 + 8.0h, 2002-04-23-07:00-+0000 + 8.0h, 2002-04-24-07:00-+0000 + 8.0h, 2002-04-25-07:00-+0000 + 8.0h, 2002-04-26-07:00-+0000 + 5.0h { overtime 2 } delayed:booking test 2002-04-11-09:00-+0000 + 4.0h, 2002-04-12-07:00-+0000 + 6.0h, 2002-04-15-07:00-+0000 + 6.0h, 2002-04-16-07:00-+0000 + 6.0h, 2002-04-17-07:00-+0000 + 6.0h, 2002-04-18-07:00-+0000 + 6.0h, 2002-04-19-07:00-+0000 + 6.0h, 2002-04-22-07:00-+0000 + 6.0h, 2002-04-23-07:00-+0000 + 6.0h, 2002-04-24-07:00-+0000 + 6.0h, 2002-04-25-07:00-+0000 + 6.0h, 2002-04-26-07:00-+0000 + 5.0h { overtime 2 } priority 500 projectid acso responsible boss } supplement task AcSo.manual { booking dev3 2002-01-24-13:00-+0000 + 3.0h, 2002-01-25-08:00-+0000 + 8.0h, 2002-01-28-08:00-+0000 + 8.0h, 2002-01-29-08:00-+0000 + 8.0h, 2002-01-30-08:00-+0000 + 8.0h, 2002-01-31-08:00-+0000 + 8.0h, 2002-02-05-08:00-+0000 + 8.0h, 2002-02-06-08:00-+0000 + 8.0h, 2002-02-07-08:00-+0000 + 8.0h, 2002-02-08-08:00-+0000 + 8.0h, 2002-02-11-08:00-+0000 + 8.0h, 2002-02-12-08:00-+0000 + 8.0h, 2002-02-13-08:00-+0000 + 8.0h, 2002-02-14-08:00-+0000 + 8.0h, 2002-02-15-08:00-+0000 + 8.0h, 2002-02-18-08:00-+0000 + 8.0h, 2002-02-19-08:00-+0000 + 8.0h, 2002-02-20-08:00-+0000 + 8.0h, 2002-02-21-08:00-+0000 + 8.0h, 2002-02-22-08:00-+0000 + 8.0h, 2002-02-25-08:00-+0000 + 8.0h, 2002-02-26-08:00-+0000 + 2.0h { overtime 2 } booking doc 2002-01-16-08:00-+0000 + 8.0h, 2002-01-17-08:00-+0000 + 8.0h, 2002-01-18-08:00-+0000 + 8.0h, 2002-01-21-08:00-+0000 + 8.0h, 2002-01-22-08:00-+0000 + 8.0h, 2002-01-23-08:00-+0000 + 8.0h, 2002-01-24-08:00-+0000 + 8.0h, 2002-01-25-08:00-+0000 + 8.0h, 2002-01-28-08:00-+0000 + 8.0h, 2002-01-29-08:00-+0000 + 8.0h, 2002-01-30-08:00-+0000 + 8.0h, 2002-01-31-08:00-+0000 + 8.0h, 2002-02-01-08:00-+0000 + 8.0h, 2002-02-04-08:00-+0000 + 8.0h, 2002-02-05-08:00-+0000 + 8.0h, 2002-02-06-08:00-+0000 + 8.0h, 2002-02-07-08:00-+0000 + 8.0h, 2002-02-08-08:00-+0000 + 8.0h, 2002-02-11-08:00-+0000 + 8.0h, 2002-02-12-08:00-+0000 + 8.0h, 2002-02-13-08:00-+0000 + 8.0h, 2002-02-14-08:00-+0000 + 8.0h, 2002-02-15-08:00-+0000 + 8.0h, 2002-02-18-08:00-+0000 + 8.0h, 2002-02-19-08:00-+0000 + 8.0h, 2002-02-20-08:00-+0000 + 8.0h, 2002-02-21-08:00-+0000 + 8.0h, 2002-02-22-08:00-+0000 + 8.0h, 2002-02-25-08:00-+0000 + 8.0h, 2002-02-26-08:00-+0000 + 3.0h { overtime 2 } delayed:booking dev3 2002-01-29-13:00-+0000 + 3.0h, 2002-01-30-08:00-+0000 + 8.0h, 2002-01-31-08:00-+0000 + 8.0h, 2002-02-05-08:00-+0000 + 8.0h, 2002-02-06-08:00-+0000 + 8.0h, 2002-02-07-08:00-+0000 + 8.0h, 2002-02-08-08:00-+0000 + 8.0h, 2002-02-11-08:00-+0000 + 8.0h, 2002-02-12-08:00-+0000 + 8.0h, 2002-02-13-08:00-+0000 + 8.0h, 2002-02-14-08:00-+0000 + 8.0h, 2002-02-15-08:00-+0000 + 8.0h, 2002-02-18-08:00-+0000 + 8.0h, 2002-02-19-08:00-+0000 + 8.0h, 2002-02-20-08:00-+0000 + 8.0h, 2002-02-21-08:00-+0000 + 8.0h, 2002-02-22-08:00-+0000 + 8.0h, 2002-02-25-08:00-+0000 + 8.0h, 2002-02-26-08:00-+0000 + 8.0h, 2002-02-27-08:00-+0000 + 8.0h, 2002-02-28-08:00-+0000 + 8.0h, 2002-03-01-08:00-+0000 + 2.0h { overtime 2 } delayed:booking doc 2002-01-21-08:00-+0000 + 8.0h, 2002-01-22-08:00-+0000 + 8.0h, 2002-01-23-08:00-+0000 + 8.0h, 2002-01-24-08:00-+0000 + 8.0h, 2002-01-25-08:00-+0000 + 8.0h, 2002-01-28-08:00-+0000 + 8.0h, 2002-01-29-08:00-+0000 + 8.0h, 2002-01-30-08:00-+0000 + 8.0h, 2002-01-31-08:00-+0000 + 8.0h, 2002-02-01-08:00-+0000 + 8.0h, 2002-02-04-08:00-+0000 + 8.0h, 2002-02-05-08:00-+0000 + 8.0h, 2002-02-06-08:00-+0000 + 8.0h, 2002-02-07-08:00-+0000 + 8.0h, 2002-02-08-08:00-+0000 + 8.0h, 2002-02-11-08:00-+0000 + 8.0h, 2002-02-12-08:00-+0000 + 8.0h, 2002-02-13-08:00-+0000 + 8.0h, 2002-02-14-08:00-+0000 + 8.0h, 2002-02-15-08:00-+0000 + 8.0h, 2002-02-18-08:00-+0000 + 8.0h, 2002-02-19-08:00-+0000 + 8.0h, 2002-02-20-08:00-+0000 + 8.0h, 2002-02-21-08:00-+0000 + 8.0h, 2002-02-22-08:00-+0000 + 8.0h, 2002-02-25-08:00-+0000 + 8.0h, 2002-02-26-08:00-+0000 + 8.0h, 2002-02-27-08:00-+0000 + 8.0h, 2002-02-28-08:00-+0000 + 8.0h, 2002-03-01-08:00-+0000 + 3.0h { overtime 2 } priority 500 projectid acso responsible boss } supplement task AcSo.deliveries { priority 500 projectid acso responsible boss } supplement task AcSo.deliveries.start { priority 500 projectid acso responsible boss } supplement task AcSo.deliveries.prev { note "All '''major''' features should be usable." priority 500 projectid acso responsible boss } supplement task AcSo.deliveries.beta { note "Fully functional, may contain bugs." priority 500 projectid acso responsible boss } supplement task AcSo.deliveries.done { note "All priority 1 and 2 bugs must be fixed." priority 500 projectid acso responsible boss } supplement resource boss { Phone "x100" workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } supplement resource dev { flags team workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } supplement resource dev1 { Phone "x362" workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } supplement resource dev2 { Phone "x234" workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } supplement resource dev3 { Phone "x490" workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } supplement resource misc { flags team workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } supplement resource test { Phone "x666" workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } supplement resource doc { Phone "x482" workinghours sun off workinghours mon 9:00 - 17:00 workinghours tue 9:00 - 17:00 workinghours wed 9:00 - 17:00 workinghours thu 9:00 - 17:00 workinghours fri 9:00 - 17:00 workinghours sat off } TaskJuggler-3.8.1/test/TestSuite/HTML-Reports/000077500000000000000000000000001473026623400210645ustar00rootroot00000000000000TaskJuggler-3.8.1/test/TestSuite/HTML-Reports/Alerts-2.tjp000066400000000000000000000010641473026623400231750ustar00rootroot00000000000000project "Transitive Alert Test" 2012-05-04 +2m task t1 "T1" { task t1 "T1" { task t1 "T1" { journalentry 2012-05-06 "Red" { alert red } } } task t2 "T2" { depends !t1 } } task t2 "T2" { task t2 "T2" { depends t1.t2 } } task t3 "T3" { adopt t2 } task "L2" { task t1 "T1" { depends t2.t2 } } task "L3" { task t1 "T1" { depends t1 } } task l4 "L4" { depends t1.t2 task t1 "T1" } task "L5" { adopt l4 } taskreport "Alerts-2" { formats html journalmode alerts_dep columns name, journal } TaskJuggler-3.8.1/test/TestSuite/HTML-Reports/Alerts.tjp000066400000000000000000000032721473026623400230410ustar00rootroot00000000000000project "Alerts" 2009-11-18 +2m task "Green" { journalentry 2009-11-20 "Green" } task "Yellow" { journalentry 2009-11-20 "Yellow" { alert yellow } } task "Red" { journalentry 2009-11-20 "Red" { alert red } } task "Inherit Yellow" { task "Green" { } task "Yellow" { journalentry 2009-11-22 "Yellow" { alert yellow } } } task "Inherit Red" { task "Inherit Yellow" { task "Green" { } task "Yellow" { journalentry 2009-11-23 "Yellow" { alert yellow } } } task "Inherit Green" { task "Green" { } } task "Inherit Red" { task "Green" { } task "Red" { journalentry 2009-11-20 "Yellow" { alert yellow } journalentry 2009-11-21 "Green" { alert green } journalentry 2009-11-22 "Red" { alert red } } } } task "Overwrite Yellow" { journalentry 2009-11-23 "Yellow" { alert yellow } task "Inherit Yellow" { task "Green" { } task "Yellow" { journalentry 2009-11-23 "Yellow" { alert yellow } } } task "Overwrite Red" { journalentry 2009-11-22 "Red" { alert red } task "Green" { } } task "Inherit Red" { task "Green" { } task "Red" { journalentry 2009-11-20 "Yellow" { alert yellow } journalentry 2009-11-21 "Green" { alert green } journalentry 2009-11-22 "Red" { alert red } } } } task "Overwrite Green" { task "Overwrite Green" { task "Red" { journalentry 2009-11-20 "Red" { alert red } } task "Green" { journalentry 2009-11-23 "Green" { alert green } } journalentry 2009-11-22 "Green" { alert green } } } taskreport "Alerts" { formats html columns name, alert sorttasks alert.down #hidetask plan.alert < 2 } TaskJuggler-3.8.1/test/TestSuite/HTML-Reports/Calendars.tjp000066400000000000000000000003521473026623400234770ustar00rootroot00000000000000project "Test" 2012-09-05 +2y task "Foo" taskreport "Calendars" { formats html columns name, quarterly { timeformat1 "TF1: %Y" timeformat2 "TF2: %Q" }, chart { timeformat1 "TF1: %Y" timeformat2 "TF2: %Q" } } TaskJuggler-3.8.1/test/TestSuite/HTML-Reports/CellText.tjp000066400000000000000000000032301473026623400233250ustar00rootroot00000000000000project "Cell Text Test" 2009-11-23 +2m resource moe "Moe" resource curly "Curly Joe" resource larry "Larry" resource shemp "Shemp" resource ted "Ted" resource joe "Joe" resource emil "Emil" task ted "With Ted" { task joke1 "Joke 1" { effort 3w allocate ted, moe, shemp } task joke2 "Joke 2" { depends !joke1 effort 4w allocate ted, moe, larry, shemp } task joke3 "Joke 3" { depends !joke2 effort 4w allocate ted, moe, larry, curly } } task "With Moe" { task joke4 "Joke 4" { depends ted.joke3 effort 3w allocate moe, larry, curly } task joke5 "Joke 5" { depends !joke4 effort 3w allocate moe, larry, shemp } task joke6 "Joke 6" { depends !joke5 effort 3w allocate moe, larry, joe } task joke7 "Joke 7" { depends !joke6 effort 3w allocate moe, larry, curly } task "Joke 8" { depends !joke7 effort 3w allocate moe, emil, curly } } resourcereport "CellText" { formats html columns name, id { celltext isleaf() & isresource() "Person" celltext ~isleaf() & isresource() "Group" celltext isleaf() & istask() "Show" celltext ~isleaf() & istask() "Tour" tooltip isleaf() & isresource() "<-query attribute='name'-> is a person" tooltip ~isleaf() & isresource() "<-query attribute='name'-> is a group" tooltip isleaf() & istask() "<-query attribute='name'-> is a show" tooltip ~isleaf() & istask() "<-query attribute='name'-> is a tour" }, weekly hidetask 0 } TaskJuggler-3.8.1/test/TestSuite/HTML-Reports/ColumnPeriods.tjp000066400000000000000000000017031473026623400243670ustar00rootroot00000000000000project 'test' 2009-12-05 +6m shift part30 "30 hours part time" { workinghours thu 8:00 - 14:00 workinghours fri off } shift part20 "20 hours part time" { workinghours mon, tue off workinghours wed 12:00 - 16:00 } shift part10 "10 hours part time" { workinghours mon - fri 10:00 - 12:00 } resource r1 "R1" resource r2 "R2" { shift part20 } resource r3 "R3" task "T1" { effort 20d allocate r1, r2 shift part10 } task "T2" { effort 30d priority 600 allocate r1, r2 } task "T3" { effort 40d allocate r1, r2, r3 shift part30 } taskreport "ColumnPeriods" { formats html columns name, effort { title "Nov" start 2009-12-01 end 2009-12-01 }, effort { title "Dec" period 2009-12-01 - 2010-01-01 }, effort { title "Jan" start 2010-01-01 end 2010-02-01 }, effort { title "Feb" period 2010-02-01 - 2010-03-01 }, effort { title "Mar" start 2010-03-01 end 2010-04-01 }, monthly } TaskJuggler-3.8.1/test/TestSuite/HTML-Reports/IsOngoing.tjp000066400000000000000000000010751473026623400235020ustar00rootroot00000000000000project "test" 2009-12-05 +1y task "Hide" { period 2010-01-01 - 2010-02-01 } task "Show" { period 2010-02-01 - 2010-03-01 } task "Show" { period 2010-02-01 - 2010-05-01 } task "Show" { period 2010-03-01 - 2010-04-01 } task "Show" { period 2010-04-01 - 2010-05-01 } task "Hide" { period 2010-05-01 - 2010-06-01 } task "Show" { start 2010-02-15 } task "Hide" { end 2010-04-18 } taskreport "IsOngoing" { formats html columns no, name, start, end, chart hidetask ~isongoing(planx) period 2010-02-15 - 2010-04-18 headline "5 Tasks should be shown" } TaskJuggler-3.8.1/test/TestSuite/HTML-Reports/Navigator.tjp000066400000000000000000000010251473026623400235330ustar00rootroot00000000000000project navigator "1.0" 2011-10-10 +1m task "foo" navigator menu textreport "top" { header '<[navigator id="menu"]>' textreport "Navigator-1" { title "One" center "one" formats html } textreport "Navigator-2" { title "Two" center "two" formats html } textreport "Navigator-3" { title "Three" textreport "Navigator-4" { title "Four" center "four" formats html } textreport "Navigator-5" { title "Five" center "five" formats html } } } TaskJuggler-3.8.1/test/TestSuite/HTML-Reports/Query.tjp000066400000000000000000000012701473026623400227100ustar00rootroot00000000000000project query "Query Test" 2009-11-08 +1m copyright "The Gang" resource foo "Foo" task bar "Bar" { effort 1w allocate foo } textreport "Query" { formats html center -8<- * Copyright: <-query attribute="copyright"-> * Currency: <-query attribute="currency"-> * End: <-query attribute="end"-> * Name: <-query attribute="name"-> * Now: <-query attribute="now"-> * Project ID: <-query attribute="projectid"-> * Start: <-query attribute="start"-> * Version: <-query attribute="version"-> The task <-query family="task" property="bar" attribute="name"-> has an effort of <-query family="task" property="bar" attribute="effort"->. ->8- } TaskJuggler-3.8.1/test/TestSuite/HTML-Reports/Sorting.tjp000066400000000000000000000017471473026623400232410ustar00rootroot00000000000000project "Task Sorting" 2009-11-10 +2m task "Root" { task "Group 1" { task "M1" { start 2009-11-11 } task "M2" { start 2009-11-20 } task "M3" { start 2009-11-14 } } task "Group 2" { task "Group 3" { task "M1" { start 2009-12-30 } task "M2" { start 2009-12-20 } task "M3" { start 2009-12-10 } } task "Group 4" { task "M1" { start 2009-11-20 } task "M2" { start 2009-12-10 } task "M3" { start 2009-11-30 } } task "Group 5" { task "M1" { start 2009-12-01 } } task "M1" { start 2009-11-20 } task "M2" { start 2009-11-30 } task "M3" { start 2009-11-11 } } } task "M1" { start 2009-12-05 } task "Group6" { task "M1" { start 2009-11-20 } } taskreport "TaskSorting" { formats html columns name, chart sorttasks tree, plan.start.up } TaskJuggler-3.8.1/test/TestSuite/HTML-Reports/TimeSheet.tjp000066400000000000000000000022111473026623400234660ustar00rootroot00000000000000project "test" 2009-11-30 +2m trackingscenario plan resource r1 "R1" resource r2 "R2" resource r3 "R3" task t1 "Task 1" { effort 5d allocate r1 } task t2 "Task 2" { task t3 "Task 3" { effort 10d allocate r2 } task t4 "Task 4" { effort 5d allocate r3 } } timesheet r1 2009-11-30 +1w { task t1 { work 5d remaining 0d status green "All work done" { summary "I had good fun!" details -8<- This task went smoothly and I got three things done: * Have fun * Be on time * Get things done ->8- } } } timesheet r2 2009-11-30 +1w { task t2.t3 { work 5d remaining 8d status red "I need more time" { summary "This takes longer than expected" details -8<- To finish on time, I need help. Get this r1 guy to help me out here. * I want to have fun too! ->8- } } } timesheet r3 2009-11-30 +1w { task t2.t4 { work 5d remaining 0d status green "All things fine" } } taskreport "TimeSheet" { formats html columns name, id, alert, alertmessage hidetask ~hasalert(0) sorttasks alert.down } TaskJuggler-3.8.1/test/TestSuite/HTML-Reports/UDAQuery.tjp000066400000000000000000000010441473026623400232410ustar00rootroot00000000000000project "test" 2009-12-01 +1m { extend resource { reference LinkR "Link R" } extend task { reference LinkT "Link T" } } resource r1 "R1" { LinkR "http://www.taskjuggler.org" { label "TaskJuggler" } } task "T1" { effort 10d allocate r1 LinkT "http://www.taskjuggler.org" { label "TaskJuggler" } } taskreport "UDAQuery" { formats html columns no, name { celltext istask() "[<-LinkT-> <-name->]" celltext isresource() "[<-LinkR-> <-name->]" }, LinkR, LinkT hidetask 0 hideresource 0 } TaskJuggler-3.8.1/test/TestSuite/HTML-Reports/depArrows.tjp000066400000000000000000000026721473026623400235600ustar00rootroot00000000000000project "Dependency Arrows" 2009-10-10 +2m macro endSuccTask [ task "${1}" { depends ${2} { gapduration ${3}d } } ] macro startSuccTask [ task "${1}" { depends bt { onstart gapduration ${2}d } } ] task at "atask" { duration 2d } ${endSuccTask "as3" "at" "2"} ${endSuccTask "as1" "at" "0.5"} ${endSuccTask "as2" "at" "1"} ${endSuccTask "au2" "at" "1"} ${endSuccTask "au1" "at" "2"} ${endSuccTask "au3" "at" "0.5"} task bt "btask" { start ${projectstart} duration 3d } ${startSuccTask "bs3" "2"} ${startSuccTask "bu2" "1"} ${startSuccTask "bu3" "0.5"} ${startSuccTask "bu1" "2"} ${startSuccTask "bs1" "0.5"} ${startSuccTask "bs2" "1"} task ct "ctask" { start ${projectstart} duration 3d } ${endSuccTask "cs1" "ct" "0.5"} ${endSuccTask "cu2" "ct" "4"} ${endSuccTask "cs2" "ct" "1"} ${endSuccTask "cu1" "ct" "3"} ${endSuccTask "cu3" "ct" "5"} ${endSuccTask "cs3" "ct" "2"} task dt "dtask" { start ${projectstart} duration 1d } ${endSuccTask "du1" "dt" "0.5"} ${endSuccTask "du2" "dt" "1"} ${endSuccTask "du3" "dt" "2"} ${endSuccTask "du4" "dt" "3"} ${endSuccTask "du5" "dt" "4"} ${endSuccTask "du6" "dt" "5"} task et "etask" { start ${projectstart} duration 1d } ${endSuccTask "es1" "et" "1"} ${endSuccTask "es2" "et" "1"} ${endSuccTask "es3" "et" "1"} ${endSuccTask "eu4" "et" "1"} ${endSuccTask "eu5" "et" "1"} ${endSuccTask "eu6" "et" "1"} taskreport "depArrows" { formats html columns name, chart { scale day } sorttasks name.up } TaskJuggler-3.8.1/test/TestSuite/HTML-Reports/reference.tjp000066400000000000000000000005241473026623400235420ustar00rootroot00000000000000project "Test" 2008-02-28 +1m { extend task { reference URL "URL" } } task a "A A A" { task b "B B B" { URL "http://www.taskjuggler.org" { label "foo" } } } task c "C C C" { URL "http://www.kde.org" } taskreport "reference" { formats html columns name { celltext plan.URL != '' "[<-URL-> <-name->]" }, URL, start } TaskJuggler-3.8.1/test/TestSuite/ReportGenerator/000077500000000000000000000000001473026623400220065ustar00rootroot00000000000000TaskJuggler-3.8.1/test/TestSuite/ReportGenerator/Correct/000077500000000000000000000000001473026623400234075ustar00rootroot00000000000000TaskJuggler-3.8.1/test/TestSuite/ReportGenerator/Correct/Alerts.tjp000066400000000000000000000036571473026623400253730ustar00rootroot00000000000000project "Alert Tests" 2010-08-03 +6m task "T1" { task "T1.1" { task "T1.1.1" { journalentry 2010-08-03 "T1.1.1 Not in report" journalentry 2010-08-05 "T1.1.1" journalentry 2010-08-15 "T1.1.1 Not in report" } journalentry 2010-08-05 "T1.1" } } task "T2" { task "T2.1" { task "T2.1.1" { journalentry 2010-08-03 "T2.1.1 Red Not in report" { alert red } journalentry 2010-08-05 "T2.1.1" journalentry 2010-08-15 "T2.1.1 Green Not in report" { alert green } } journalentry 2010-08-05 "T2.1 Yellow" { alert yellow } } } task "T3" { task "T3.1" { task "T3.1.1" { journalentry 2010-08-03 "T3.1.1 Not in report" journalentry 2010-08-06 "T3.1.1 Red" { alert red } journalentry 2010-08-15 "T3.1.1 Green Not in report" { alert green } } journalentry 2010-08-06 "T3.1 Yellow" { alert yellow } } } taskreport "Alerts-1" { formats html, csv columns name, alert, journal { celltext 1 "<-query attribute='journal' journalmode='journal_sub'->" title "Journal_sub (full period)" period ${projectstart} - ${projectend} }, journal { title "journal (full period)" period ${projectstart} - ${projectend} }, journal { title "journal" }, journal { title "journal_sub" celltext 1 "<-query attribute='journal' journalmode='journal_sub'->" }, journal { title "alerts_down" celltext 1 "<-query attribute='journal' journalmode='alerts_down'->" }, journal { title "status_down" celltext 1 "<-query attribute='journal' journalmode='status_down'->" }, journal { title "status_up" celltext 1 "<-query attribute='journal' journalmode='status_up'->" } period 2010-08-05 - 2010-08-15 } TaskJuggler-3.8.1/test/TestSuite/ReportGenerator/Correct/FTE.tjp000066400000000000000000000003061473026623400245430ustar00rootroot00000000000000project "FTE" 2011-10-29 +1y resource "Team" { resource "A" { workinghours mon - fri 8:00 - 12:00 } resource "B" } task "T" resourcereport "FTE-1" { formats csv columns name, fte } TaskJuggler-3.8.1/test/TestSuite/ReportGenerator/Correct/Journal.tjp000066400000000000000000000020521473026623400255370ustar00rootroot00000000000000project "Journal" 2010-06-07 +1m flags foo, bar resource a1 "A1" resource a2 "A2" resource a3 "A3" task "T" { duration 3w journalentry 2010-06-07-8:00 "Entry 1" { flags foo, bar author a1 summary "Summary 1" details "Deails 1" } journalentry 2010-06-14-8:00 "Entry 2" { flags foo author a2 summary "Summary 1" details "Deails 1" } journalentry 2010-06-21-8:00 "Entry 3" { flags bar author a3 summary "Summary 1" details "Deails 1" } } textreport "Journal-0" { formats html journalmode journal journalattributes * sortjournalentries date.up center "<-query attribute='journal'->" } taskreport "Journal-1" { formats csv journalmode alerts_down # Only "Entry 2" should be included columns name, journal { period 2010-06-14 +1w } } taskreport "Journal-2" { formats csv # Only "Entry 1" should be included columns name, journal hidejournalentry ~(foo & bar) } textreport "Journal-3" { formats html hidejournalentry foo left -8<- <-query attribute='journal'-> ->8- } TaskJuggler-3.8.1/test/TestSuite/ReportGenerator/Correct/JournalMode.tjp000066400000000000000000000045071473026623400263530ustar00rootroot00000000000000project "Test" 2011-08-01 +1m { now 2011-08-15 trackingscenario plan } flags foo resource r1 "R1" resource r2 "R2" resource r3 "R3" resource r4 "R4" task t1 "T1" { task t2 "T2" { effort 1w allocate r1 } task t3 "T3" { effort 1w allocate r2 journalentry 2011-08-08 "r1 had to help out" { author r2 } } } # Timesheets 1st week timesheet r1 2011-08-01 +1w { task t1.t2 { work 3d remaining 6d status green "All good" { summary "No problems found" details "Work is progressing well" } } task t1.t3 { work 1d remaining 2d status yellow "Helped out" { summary "r2 asked for d2" } } newtask t4 "T4" { work 1d remaining 3d status red "Big Problem" { flags foo summary "Had to help out" details "Unplanned distraction" } } } timesheet r3 2011-08-01 +1w { status yellow "Not feeling good" { summary "Will see the doctor again" } newtask sick "Out sick" { work 5d remaining 2d status green "Hope to feel better soon" } } # Timesheet 2nd week timesheet r1 2011-08-08 +1w { task t1.t2 { work 5d remaining 1d status green "All ok" } task t1.t3 { work 0d remaining 2d status red "Fire burning" { details "It's really hot" } } } statussheet r4 2011-08-09 { task t1 { status green "Don't panik!" } } macro columns [ columns name, journal { title "journal" celltext 1 "<-query attribute='journal' journalmode='journal'->" }, journal { title "journal_sub" celltext 1 "<-query attribute='journal' journalmode='journal_sub'->" }, journal { title "status_up" celltext 1 "<-query attribute='journal' journalmode='status_up'->" }, journal { title "status_down" celltext 1 "<-query attribute='journal' journalmode='status_down'->" }, journal { title "alerts_down" celltext 1 "<-query attribute='journal' journalmode='alerts_down'->" } ] resourcereport "JournalMode-1" { formats html, csv journalmode journal journalattributes * ${columns} } taskreport "JournalMode-2" { formats html, csv journalmode journal journalattributes * ${columns} } TaskJuggler-3.8.1/test/TestSuite/ReportGenerator/Correct/LogicalFunctions1.tjp000066400000000000000000000007331473026623400274550ustar00rootroot00000000000000project "Logical Functions" 2009-11-25 +2m task "Task 1" task "Task 2" task "Task 3" task "Task 4" task "Task 5" task "" taskreport "LogicalFunctions1-1" { formats csv columns index, name, id { celltext plan.index < 3 "Index < 3" celltext 1 "Index >= 3" }, id { celltext plan.name = "Task 3" "This is task 3" celltext 1 "" }, id { celltext plan.name = "" "Task with no name" celltext 1 "" } } TaskJuggler-3.8.1/test/TestSuite/ReportGenerator/Correct/LogicalFunctions2.tjp000066400000000000000000000004221473026623400274510ustar00rootroot00000000000000project "LogFunc2" 2010-06-07 +1m resource r1 "R1" resource r2 "R2" { vacation 2010-06-14 +7d } task "Task" { effort 25d allocate r1, r2 } resourcereport "LogicalFunctions2-1" { formats csv period 2010-06-14 +7d hideresource ~isactive(plan) columns name } TaskJuggler-3.8.1/test/TestSuite/ReportGenerator/Correct/LogicalFunctions3.tjp000066400000000000000000000004631473026623400274570ustar00rootroot00000000000000project "LogFunc3" 2011-05-27 +1m task t1 "Task 1" { task t2 "Task 2" { task t3 "Task 3" { task t6 "Task 6" } task t7 "Task 7" } task t4 "Task 4" } task t5 "Task 5" taskreport "LogicalFunctions3-1" { formats csv sorttasks id.up hidetask ~(ischildof(t1.t2)) columns name } TaskJuggler-3.8.1/test/TestSuite/ReportGenerator/Correct/LogicalFunctions4.tjp000066400000000000000000000003261473026623400274560ustar00rootroot00000000000000project "LogFunc4" 2011-06-02 +1m task "Task 1" { duration 1w } task "Milestone" task "Task 2" { duration 1d } taskreport "LogicalFunctions4-1" { formats csv hidetask ~ismilestone(plan) columns name } TaskJuggler-3.8.1/test/TestSuite/ReportGenerator/Correct/Macros.tjp000066400000000000000000000011061473026623400253500ustar00rootroot00000000000000project "Macros" 2010-07-21 +1m { } macro m1 ["t1"] macro n1 [note "This is a note"] task t1 ${m1} { ${n1} } macro m2 ["t] macro m3 [2"] macro n2 [note] task t2 ${m2}${m3} { ${n2} -8<- This is a note ->8- } macro m4 [t] macro m5 [3] macro n3 [is a] task t3 "${m4}${m5}" { note "This ${n3} note" } macro m6 [4] macro n4 [is a] task t4 "t${m6}" { note -8<- This ${n4} note ->8- } macro m7 [t] macro n5 [This is a note ->8-] task t5 "${m7}5" { note -8<- ${n5} } taskreport "Macros-1" { formats csv columns id, name, note } TaskJuggler-3.8.1/test/TestSuite/ReportGenerator/Correct/refs/000077500000000000000000000000001473026623400243465ustar00rootroot00000000000000TaskJuggler-3.8.1/test/TestSuite/ReportGenerator/Correct/refs/Alerts-1.csv000066400000000000000000000162221473026623400264560ustar00rootroot00000000000000"Name";"Alert";"Journal_sub (full period)";"journal (full period)";"journal";"journal_sub";"alerts_down";"status_down";"status_up" "T1";"Green";"[Green]T1.1.1 (ID: _Task_1._Task_2._Task_3) T1.1.1 Not in report Reported on 2010-08-03 [Green]T1.1 (ID: _Task_1._Task_2) T1.1 Reported on 2010-08-05 [Green]T1.1.1 (ID: _Task_1._Task_2._Task_3) T1.1.1 Reported on 2010-08-05 [Green]T1.1.1 (ID: _Task_1._Task_2._Task_3) T1.1.1 Not in report Reported on 2010-08-15";"";"";"[Green]T1.1 (ID: _Task_1._Task_2) T1.1 Reported on 2010-08-05 [Green]T1.1.1 (ID: _Task_1._Task_2._Task_3) T1.1.1 Reported on 2010-08-05";"";"[Green]T1.1.1 (ID: _Task_1._Task_2._Task_3) T1.1.1 Not in report Reported on 2010-08-15";"" " T1.1";" Green";"[Green]T1.1.1 (ID: _Task_1._Task_2._Task_3) T1.1.1 Not in report Reported on 2010-08-03 [Green]T1.1 (ID: _Task_1._Task_2) T1.1 Reported on 2010-08-05 [Green]T1.1.1 (ID: _Task_1._Task_2._Task_3) T1.1.1 Reported on 2010-08-05 [Green]T1.1.1 (ID: _Task_1._Task_2._Task_3) T1.1.1 Not in report Reported on 2010-08-15";"[Green]T1.1 (ID: _Task_1._Task_2) T1.1 Reported on 2010-08-05";"[Green]T1.1 (ID: _Task_1._Task_2) T1.1 Reported on 2010-08-05";"[Green]T1.1 (ID: _Task_1._Task_2) T1.1 Reported on 2010-08-05 [Green]T1.1.1 (ID: _Task_1._Task_2._Task_3) T1.1.1 Reported on 2010-08-05";"";"[Green]T1.1.1 (ID: _Task_1._Task_2._Task_3) T1.1.1 Not in report Reported on 2010-08-15";"[Green]T1.1 (ID: _Task_1._Task_2) T1.1 Reported on 2010-08-05" " T1.1.1";" Green";"[Green]T1.1.1 (ID: _Task_1._Task_2._Task_3) T1.1.1 Not in report Reported on 2010-08-03 [Green]T1.1.1 (ID: _Task_1._Task_2._Task_3) T1.1.1 Reported on 2010-08-05 [Green]T1.1.1 (ID: _Task_1._Task_2._Task_3) T1.1.1 Not in report Reported on 2010-08-15";"[Green]T1.1.1 (ID: _Task_1._Task_2._Task_3) T1.1.1 Not in report Reported on 2010-08-03 [Green]T1.1.1 (ID: _Task_1._Task_2._Task_3) T1.1.1 Reported on 2010-08-05 [Green]T1.1.1 (ID: _Task_1._Task_2._Task_3) T1.1.1 Not in report Reported on 2010-08-15";"[Green]T1.1.1 (ID: _Task_1._Task_2._Task_3) T1.1.1 Reported on 2010-08-05";"[Green]T1.1.1 (ID: _Task_1._Task_2._Task_3) T1.1.1 Reported on 2010-08-05";"";"[Green]T1.1.1 (ID: _Task_1._Task_2._Task_3) T1.1.1 Not in report Reported on 2010-08-15";"[Green]T1.1.1 (ID: _Task_1._Task_2._Task_3) T1.1.1 Not in report Reported on 2010-08-15" "T2";"Yellow";"[Green]T2.1.1 (ID: _Task_4._Task_5._Task_6) T2.1.1 Reported on 2010-08-05 [Green]T2.1.1 (ID: _Task_4._Task_5._Task_6) T2.1.1 Green Not in report Reported on 2010-08-15 [Yellow]T2.1 (ID: _Task_4._Task_5) T2.1 Yellow Reported on 2010-08-05 [Red]T2.1.1 (ID: _Task_4._Task_5._Task_6) T2.1.1 Red Not in report Reported on 2010-08-03";"";"";"[Green]T2.1.1 (ID: _Task_4._Task_5._Task_6) T2.1.1 Reported on 2010-08-05 [Yellow]T2.1 (ID: _Task_4._Task_5) T2.1 Yellow Reported on 2010-08-05";"[Yellow]T2.1 (ID: _Task_4._Task_5) T2.1 Yellow Reported on 2010-08-05";"[Yellow]T2.1 (ID: _Task_4._Task_5) T2.1 Yellow Reported on 2010-08-05";"" " T2.1";" Yellow";"[Green]T2.1.1 (ID: _Task_4._Task_5._Task_6) T2.1.1 Reported on 2010-08-05 [Green]T2.1.1 (ID: _Task_4._Task_5._Task_6) T2.1.1 Green Not in report Reported on 2010-08-15 [Yellow]T2.1 (ID: _Task_4._Task_5) T2.1 Yellow Reported on 2010-08-05 [Red]T2.1.1 (ID: _Task_4._Task_5._Task_6) T2.1.1 Red Not in report Reported on 2010-08-03";"[Yellow]T2.1 (ID: _Task_4._Task_5) T2.1 Yellow Reported on 2010-08-05";"[Yellow]T2.1 (ID: _Task_4._Task_5) T2.1 Yellow Reported on 2010-08-05";"[Green]T2.1.1 (ID: _Task_4._Task_5._Task_6) T2.1.1 Reported on 2010-08-05 [Yellow]T2.1 (ID: _Task_4._Task_5) T2.1 Yellow Reported on 2010-08-05";"[Yellow]T2.1 (ID: _Task_4._Task_5) T2.1 Yellow Reported on 2010-08-05";"[Yellow]T2.1 (ID: _Task_4._Task_5) T2.1 Yellow Reported on 2010-08-05";"[Yellow]T2.1 (ID: _Task_4._Task_5) T2.1 Yellow Reported on 2010-08-05" " T2.1.1";" Green";"[Green]T2.1.1 (ID: _Task_4._Task_5._Task_6) T2.1.1 Reported on 2010-08-05 [Green]T2.1.1 (ID: _Task_4._Task_5._Task_6) T2.1.1 Green Not in report Reported on 2010-08-15 [Red]T2.1.1 (ID: _Task_4._Task_5._Task_6) T2.1.1 Red Not in report Reported on 2010-08-03";"[Green]T2.1.1 (ID: _Task_4._Task_5._Task_6) T2.1.1 Reported on 2010-08-05 [Green]T2.1.1 (ID: _Task_4._Task_5._Task_6) T2.1.1 Green Not in report Reported on 2010-08-15 [Red]T2.1.1 (ID: _Task_4._Task_5._Task_6) T2.1.1 Red Not in report Reported on 2010-08-03";"[Green]T2.1.1 (ID: _Task_4._Task_5._Task_6) T2.1.1 Reported on 2010-08-05";"[Green]T2.1.1 (ID: _Task_4._Task_5._Task_6) T2.1.1 Reported on 2010-08-05";"";"[Green]T2.1.1 (ID: _Task_4._Task_5._Task_6) T2.1.1 Green Not in report Reported on 2010-08-15";"[Green]T2.1.1 (ID: _Task_4._Task_5._Task_6) T2.1.1 Green Not in report Reported on 2010-08-15" "T3";"Yellow";"[Green]T3.1.1 (ID: _Task_7._Task_8._Task_9) T3.1.1 Not in report Reported on 2010-08-03 [Green]T3.1.1 (ID: _Task_7._Task_8._Task_9) T3.1.1 Green Not in report Reported on 2010-08-15 [Yellow]T3.1 (ID: _Task_7._Task_8) T3.1 Yellow Reported on 2010-08-06 [Red]T3.1.1 (ID: _Task_7._Task_8._Task_9) T3.1.1 Red Reported on 2010-08-06";"";"";"[Yellow]T3.1 (ID: _Task_7._Task_8) T3.1 Yellow Reported on 2010-08-06 [Red]T3.1.1 (ID: _Task_7._Task_8._Task_9) T3.1.1 Red Reported on 2010-08-06";"[Yellow]T3.1 (ID: _Task_7._Task_8) T3.1 Yellow Reported on 2010-08-06";"[Yellow]T3.1 (ID: _Task_7._Task_8) T3.1 Yellow Reported on 2010-08-06";"" " T3.1";" Yellow";"[Green]T3.1.1 (ID: _Task_7._Task_8._Task_9) T3.1.1 Not in report Reported on 2010-08-03 [Green]T3.1.1 (ID: _Task_7._Task_8._Task_9) T3.1.1 Green Not in report Reported on 2010-08-15 [Yellow]T3.1 (ID: _Task_7._Task_8) T3.1 Yellow Reported on 2010-08-06 [Red]T3.1.1 (ID: _Task_7._Task_8._Task_9) T3.1.1 Red Reported on 2010-08-06";"[Yellow]T3.1 (ID: _Task_7._Task_8) T3.1 Yellow Reported on 2010-08-06";"[Yellow]T3.1 (ID: _Task_7._Task_8) T3.1 Yellow Reported on 2010-08-06";"[Yellow]T3.1 (ID: _Task_7._Task_8) T3.1 Yellow Reported on 2010-08-06 [Red]T3.1.1 (ID: _Task_7._Task_8._Task_9) T3.1.1 Red Reported on 2010-08-06";"[Yellow]T3.1 (ID: _Task_7._Task_8) T3.1 Yellow Reported on 2010-08-06";"[Yellow]T3.1 (ID: _Task_7._Task_8) T3.1 Yellow Reported on 2010-08-06";"[Yellow]T3.1 (ID: _Task_7._Task_8) T3.1 Yellow Reported on 2010-08-06" " T3.1.1";" Green";"[Green]T3.1.1 (ID: _Task_7._Task_8._Task_9) T3.1.1 Not in report Reported on 2010-08-03 [Green]T3.1.1 (ID: _Task_7._Task_8._Task_9) T3.1.1 Green Not in report Reported on 2010-08-15 [Red]T3.1.1 (ID: _Task_7._Task_8._Task_9) T3.1.1 Red Reported on 2010-08-06";"[Green]T3.1.1 (ID: _Task_7._Task_8._Task_9) T3.1.1 Not in report Reported on 2010-08-03 [Green]T3.1.1 (ID: _Task_7._Task_8._Task_9) T3.1.1 Green Not in report Reported on 2010-08-15 [Red]T3.1.1 (ID: _Task_7._Task_8._Task_9) T3.1.1 Red Reported on 2010-08-06";"[Red]T3.1.1 (ID: _Task_7._Task_8._Task_9) T3.1.1 Red Reported on 2010-08-06";"[Red]T3.1.1 (ID: _Task_7._Task_8._Task_9) T3.1.1 Red Reported on 2010-08-06";"";"[Green]T3.1.1 (ID: _Task_7._Task_8._Task_9) T3.1.1 Green Not in report Reported on 2010-08-15";"[Green]T3.1.1 (ID: _Task_7._Task_8._Task_9) T3.1.1 Green Not in report Reported on 2010-08-15" TaskJuggler-3.8.1/test/TestSuite/ReportGenerator/Correct/refs/DependencyList-1.csv000066400000000000000000000001461473026623400301340ustar00rootroot00000000000000"Name";"Followers" "A";"B ]->[ (2011-05-14)), C ]->[ (2011-05-21))" "B";"C ]->[ (2011-05-21))" "C";"" TaskJuggler-3.8.1/test/TestSuite/ReportGenerator/Correct/refs/FTE-1.csv000066400000000000000000000000541473026623400256360ustar00rootroot00000000000000"Name";"FTE" "Team";1.5 " A";0.5 " B";1.0 TaskJuggler-3.8.1/test/TestSuite/ReportGenerator/Correct/refs/Journal-1.csv000066400000000000000000000000301473026623400266240ustar00rootroot00000000000000"Name";"Journal" "T";"" TaskJuggler-3.8.1/test/TestSuite/ReportGenerator/Correct/refs/Journal-2.csv000066400000000000000000000001731473026623400266350ustar00rootroot00000000000000"Name";"Journal" "T";"[Green]T (ID: _Task_1) Entry 1 Reported on 2010-06-07 by A1 Flags: foo, bar Summary 1 Deails 1" TaskJuggler-3.8.1/test/TestSuite/ReportGenerator/Correct/refs/JournalMode-1.csv000066400000000000000000000021661473026623400274450ustar00rootroot00000000000000"Name";"journal";"journal_sub";"status_up";"status_down";"alerts_down" "R1";"[Green]T2 (ID: t1.t2) Work: 60% (0%) Remaining: 6.0d (5.0d) All good Reported on 2011-08-08 by R1 No problems found Work is progressing well [Green]T2 (ID: t1.t2) Work: 100% (0%) Remaining: 1.0d (5.0d) All ok Reported on 2011-08-15 by R1 [Yellow]T3 (ID: t1.t3) Work: 20% (0%) Remaining: 2.0d (0.0d) Helped out Reported on 2011-08-08 by R1 r2 asked for d2 [Red][New Task] T4 (ID: t4) Work: 20.0% Remaining: 3.0d Big Problem Reported on 2011-08-08 by R1 Flags: foo Had to help out Unplanned distraction [Red]T3 (ID: t1.t3) Work: 0% Remaining: 2.0d (0.0d) Fire burning Reported on 2011-08-15 by R1 It's really hot";"";"";"";"" "R2";"[Green]T3 (ID: t1.t3) r1 had to help out Reported on 2011-08-08 by R2";"";"";"";"" "R3";"[Green][New Task] Out sick (ID: sick) Work: 100.0% Remaining: 2.0d Hope to feel better soon Reported on 2011-08-08 by R3 [Yellow]Personal Notes Not feeling good Reported on 2011-08-08 by R3 Will see the doctor again";"";"";"";"" "R4";"[Green]T1 (ID: t1) Don't panik! Reported on 2011-08-10 by R4";"";"";"";"" TaskJuggler-3.8.1/test/TestSuite/ReportGenerator/Correct/refs/JournalMode-2.csv000066400000000000000000000054431473026623400274470ustar00rootroot00000000000000"Name";"journal";"journal_sub";"status_up";"status_down";"alerts_down" "T1";"[Green]T1 (ID: t1) Don't panik! Reported on 2011-08-10 by R4";"[Green]T2 (ID: t1.t2) Work: 60% (0%) Remaining: 6.0d (5.0d) All good Reported on 2011-08-08 by R1 No problems found Work is progressing well [Green]T3 (ID: t1.t3) r1 had to help out Reported on 2011-08-08 by R2 [Green]T1 (ID: t1) Don't panik! Reported on 2011-08-10 by R4 [Green]T2 (ID: t1.t2) Work: 100% (0%) Remaining: 1.0d (5.0d) All ok Reported on 2011-08-15 by R1 [Yellow]T3 (ID: t1.t3) Work: 20% (0%) Remaining: 2.0d (0.0d) Helped out Reported on 2011-08-08 by R1 r2 asked for d2 [Red]T3 (ID: t1.t3) Work: 0% Remaining: 2.0d (0.0d) Fire burning Reported on 2011-08-15 by R1 It's really hot";"[Green]T1 (ID: t1) Don't panik! Reported on 2011-08-10 by R4";"[Green]T2 (ID: t1.t2) Work: 100% (0%) Remaining: 1.0d (5.0d) All ok Reported on 2011-08-15 by R1 [Red]T3 (ID: t1.t3) Work: 0% Remaining: 2.0d (0.0d) Fire burning Reported on 2011-08-15 by R1 It's really hot";"[Red]T3 (ID: t1.t3) Work: 0% Remaining: 2.0d (0.0d) Fire burning Reported on 2011-08-15 by R1 It's really hot" " T2";"[Green]T2 (ID: t1.t2) Work: 60% (0%) Remaining: 6.0d (5.0d) All good Reported on 2011-08-08 by R1 No problems found Work is progressing well [Green]T2 (ID: t1.t2) Work: 100% (0%) Remaining: 1.0d (5.0d) All ok Reported on 2011-08-15 by R1";"[Green]T2 (ID: t1.t2) Work: 60% (0%) Remaining: 6.0d (5.0d) All good Reported on 2011-08-08 by R1 No problems found Work is progressing well [Green]T2 (ID: t1.t2) Work: 100% (0%) Remaining: 1.0d (5.0d) All ok Reported on 2011-08-15 by R1";"[Green]T2 (ID: t1.t2) Work: 100% (0%) Remaining: 1.0d (5.0d) All ok Reported on 2011-08-15 by R1";"[Green]T2 (ID: t1.t2) Work: 100% (0%) Remaining: 1.0d (5.0d) All ok Reported on 2011-08-15 by R1";"" " T3";"[Green]T3 (ID: t1.t3) r1 had to help out Reported on 2011-08-08 by R2 [Yellow]T3 (ID: t1.t3) Work: 20% (0%) Remaining: 2.0d (0.0d) Helped out Reported on 2011-08-08 by R1 r2 asked for d2 [Red]T3 (ID: t1.t3) Work: 0% Remaining: 2.0d (0.0d) Fire burning Reported on 2011-08-15 by R1 It's really hot";"[Green]T3 (ID: t1.t3) r1 had to help out Reported on 2011-08-08 by R2 [Yellow]T3 (ID: t1.t3) Work: 20% (0%) Remaining: 2.0d (0.0d) Helped out Reported on 2011-08-08 by R1 r2 asked for d2 [Red]T3 (ID: t1.t3) Work: 0% Remaining: 2.0d (0.0d) Fire burning Reported on 2011-08-15 by R1 It's really hot";"[Red]T3 (ID: t1.t3) Work: 0% Remaining: 2.0d (0.0d) Fire burning Reported on 2011-08-15 by R1 It's really hot";"[Red]T3 (ID: t1.t3) Work: 0% Remaining: 2.0d (0.0d) Fire burning Reported on 2011-08-15 by R1 It's really hot";"[Red]T3 (ID: t1.t3) Work: 0% Remaining: 2.0d (0.0d) Fire burning Reported on 2011-08-15 by R1 It's really hot" TaskJuggler-3.8.1/test/TestSuite/ReportGenerator/Correct/refs/LogicalFunctions1-1.csv000066400000000000000000000003511473026623400305440ustar00rootroot00000000000000"Index";"Name";"Id";"Id";"Id" 1;"Task 1";"Index < 3";"";"" 2;"Task 2";"Index < 3";"";"" 3;"Task 3";"Index >= 3";"This is task 3";"" 4;"Task 4";"Index >= 3";"";"" 5;"Task 5";"Index >= 3";"";"" 6;"";"Index >= 3";"";"Task with no name" TaskJuggler-3.8.1/test/TestSuite/ReportGenerator/Correct/refs/LogicalFunctions2-1.csv000066400000000000000000000000141473026623400305410ustar00rootroot00000000000000"Name" "R1" TaskJuggler-3.8.1/test/TestSuite/ReportGenerator/Correct/refs/LogicalFunctions3-1.csv000066400000000000000000000000421473026623400305430ustar00rootroot00000000000000"Name" "Task 3" "Task 6" "Task 7" TaskJuggler-3.8.1/test/TestSuite/ReportGenerator/Correct/refs/LogicalFunctions4-1.csv000066400000000000000000000000231473026623400305430ustar00rootroot00000000000000"Name" "Milestone" TaskJuggler-3.8.1/test/TestSuite/ReportGenerator/Correct/refs/Macros-1.csv000066400000000000000000000002321473026623400264420ustar00rootroot00000000000000"Id";"Name";"Note" "t1";"t1";"This is a note" "t2";"t2";"This is a note" "t3";"t3";"This is a note" "t4";"t4";"This is a note" "t5";"t5";"This is a note" TaskJuggler-3.8.1/test/TestSuite/ReportGenerator/Errors/000077500000000000000000000000001473026623400232625ustar00rootroot00000000000000TaskJuggler-3.8.1/test/TestSuite/ReportGenerator/Errors/no_report_defined.tjp000066400000000000000000000001641473026623400274670ustar00rootroot00000000000000project "test" 2010-05-23 +1w task "foo" # MARK: warning 0 no_report_defined # MARK: warning 0 all_formats_empty TaskJuggler-3.8.1/test/TestSuite/ReportGenerator/Errors/rtp_report_recursion.tjp000066400000000000000000000005331473026623400302730ustar00rootroot00000000000000project "test" 2010-05-23 +1w task "foo" textreport "rtp_report_recursion" { formats html center "<[report id='r1']>" } textreport r1 "R1" { center "<[report id='r2']>" } textreport r2 "R2" { center "<[report id='r3']>" } # MARK: error 19 rtp_report_recursion textreport r3 "R3" { left "Hello, world!" center "<[report id='r1']>" } TaskJuggler-3.8.1/test/TestSuite/Scheduler/000077500000000000000000000000001473026623400206025ustar00rootroot00000000000000TaskJuggler-3.8.1/test/TestSuite/Scheduler/Correct/000077500000000000000000000000001473026623400222035ustar00rootroot00000000000000TaskJuggler-3.8.1/test/TestSuite/Scheduler/Correct/Allocate.tjp000066400000000000000000000044731473026623400244560ustar00rootroot00000000000000project test "Test" "1.0" 2007-08-31 +3w { workinghours mon-fri 9:00 - 12:00, 13:00 - 18:00 } macro Fixend [ minend ${1} maxend ${1} ] shift mowefr "Mo, We, Fr shift" { workinghours mon, wed, fri 9:00 - 12:00, 13:00 - 18:00 workinghours tue, thu, sat off } shift tuthsa "Tu, Th, Sa shift" { workinghours tue, thu, sat 9:00 - 12:00, 13:00 - 18:00 workinghours mon, wed, fri off } resource tue_off "Tuesday off" { workinghours mon, wed - fri 10:00 - 18:00 workinghours tue, sat, sun off } resource all_days "All days" resource all_days2 "All days2" resource only_wed "Only Wednesday" { workinghours sun - sat off resource ow1 "Monday off" { workinghours sun, mon, sat off workinghours tue - fri 10:00 - 18:00 } resource ow2 "Tuesday off" { workinghours sun, tue, sat off workinghours mon, wed - fri 10:00 - 18:00 } resource ow3 "Thu Fri off" { workinghours sun, thu - sat off workinghours mon - wed 10:00 - 18:00 } } resource all_week_group "All week group" { workinghours mon - fri 10:00 - 18:00 workinghours sat, sun off resource aw1 "All week 1" resource aw2 "All week 2" resource aw3 "All week 3" resource aw4 "All week 4" } task mandatory "Mandatory Tests" { task t1 "Task1" { start ${projectstart} effort 13d allocate tue_off { mandatory }, all_days ${Fixend "2007-09-12-18:00"} } task t2 "Task2" { start ${projectstart} effort 14d allocate only_wed { mandatory }, all_week_group ${Fixend "2007-09-12-18:00"} } } resource r1 "Resource 1" resource r2 "Resource 2" resource r3 "Resource 3" resource r4 "Resource 4" task inheritedAllocs "Inherited Allocations" { allocate r1 task t1 "Task 1" { effort 3d ${Fixend "2007-09-04-18:00"} } task t2 "Task 2" { allocate r2 task t2_1 "Task 2.1" { effort 2d ${Fixend "2007-09-03-18:00"} } task t2_2 "Task 2.2" { effort 4d depends !!t1 ${Fixend "2007-09-06-18:00"} } } task t3 "Task 3" { effort 2d depends !t2 ${Fixend "2007-09-10-18:00"} } } task allocShift "Allocation Shift" { effort 10d allocate r3 { shifts mowefr 2007-09-01 +1w }, r4 { shifts tuthsa 2007-09-01 +1w } ${Fixend "2007-09-11-14:00"} } taskreport allocate "Allocate" { formats html columns no, name, end, daily hideresource 0 } TaskJuggler-3.8.1/test/TestSuite/Scheduler/Correct/AutomaticMilestones.tjp000066400000000000000000000021211473026623400267070ustar00rootroot00000000000000project test "Test" "1.0" 2009-02-12 +4m include "checks.tji" task msStart "Start Milestone" { start ${projectstart} milestone } task msEnd "End Milestone" { end ${projectend} milestone } task ms1 "Milestone 1" { start ${projectstart} ${FixStart "${projectstart}"} ${FixEnd "${projectstart}"} } task ms2 "Milestone 2" { start ${projectstart} depends msStart ${FixStart "${projectstart}"} ${FixEnd "${projectstart}"} } task ms3 "Milestone 3" { depends !msStart ${FixStart "${projectstart}"} ${FixEnd "${projectstart}"} } task ms4 "Milestone 4" { end ${projectend} ${FixStart "${projectend}"} ${FixEnd "${projectend}"} } task ms5 "Milestone 5" { end ${projectend} precedes msEnd ${FixStart "${projectend}"} ${FixEnd "${projectend}"} } task ms6 "Milestone 6" { precedes msEnd ${FixStart "${projectend}"} ${FixEnd "${projectend}"} } task t1 "Task 1" { period ${projectstart} - ${projectend} ${FixStart "${projectstart}"} ${FixEnd "${projectend}"} } taskreport ms "AutomaticMilestones" { formats html columns name, id, start, end, chart } TaskJuggler-3.8.1/test/TestSuite/Scheduler/Correct/Booking.tjp000066400000000000000000000061151473026623400243150ustar00rootroot00000000000000project test "Test" "1.0" 2007-04-22 +5w { now 2007-04-30 scenario plan "Plan" workinghours mon - fri 9:00 - 12:00, 13:00 - 18:00 } include "checks.tji" resource tux1 "Tux 1" resource tux2 "Tux 2" { vacation 2007-04-24 +2d } resource tux3 "Tux 3" { vacation 2007-04-24 +2d } resource tux4 "Tux 4" { vacation 2007-04-24 +2d } task t1 "Task 1" { effort 7d allocate tux1 # We got 2d from bookings. See supplement below! booking tux1 2007-04-24 +10h, 2007-04-25 +10h { sloppy 1 } booking tux1 2007-04-27 +10h { overtime 2 } ${FixStart "2007-04-24-9:00"} ${FixEnd "2007-05-04-18:00"} } task t2 "Task 2" { effort 10d allocate tux2 booking tux2 2007-04-23 +5d { sloppy 2 } ${FixEnd "2007-05-08-18:00"} } task t3 "Task 3" { effort 11d allocate tux3 booking tux3 2007-04-23 +5d { sloppy 2 overtime 1 } ${FixEnd "2007-05-01-18:00"} } task t4 "Task 3" { effort 16d allocate tux4 booking tux4 2007-04-23 +5d { sloppy 2 overtime 2 } ${FixEnd "2007-04-30-18:00"} } supplement resource tux1 { booking t1 2007-04-26-4:00 +10h { sloppy 1 } } resource r10 "R10" resource r11 "R11" resource r12 "R12" resource r13 "R13" resource r14 "R14" resource r15 "R15" resource r16 "R16" resource r17 "R17" resource r18 "R18" resource r19 "R19" resource r20 "R20" task t10 "Task 10" { booking r10 2007-04-23-9:00 +1h end 2007-04-24 scheduling asap ${FixStart "2007-04-23-9:00"} ${FixEnd "2007-04-24"} } task t11 "Task 11" { start 2007-04-22-12:00 booking r11 2007-04-23-9:00 +1h end 2007-04-24 scheduling asap ${FixStart "2007-04-22-12:00"} ${FixEnd "2007-04-24"} } task t12 "Task 12" { effort 1d allocate r12 booking r12 2007-04-23-9:00 +1h ${FixStart "2007-04-23-9:00"} ${FixEnd "2007-04-30-17:00"} } task t13 "Task 13" { effort 4h allocate r13 booking r13 2007-04-23-9:00 +5h { sloppy 2 } ${FixStart "2007-04-23-9:00"} ${FixEnd "2007-04-23-14:00"} } task t14 "Task 14" { effort 4h allocate r14 booking r14 2007-04-23-9:00 +5h { sloppy 2 } ${FixStart "2007-04-23-9:00"} ${FixEnd "2007-04-23-14:00"} } task t15 "Task 15" { length 4h allocate r15 booking r15 2007-04-23-9:00 +5h { sloppy 2 } ${FixStart "2007-04-23-9:00"} ${FixEnd "2007-04-23-14:00"} } task t16 "Task 16" { length 5h allocate r16 booking r16 2007-04-23-9:00 +5h { sloppy 2 } ${FixStart "2007-04-23-9:00"} ${FixEnd "2007-04-23-15:00"} } task t17 "Task 17" { length 6h allocate r17 booking r17 2007-04-23-9:00 +5h { sloppy 2 } ${FixStart "2007-04-23-9:00"} ${FixEnd "2007-04-23-16:00"} } task t18 "Task 18" { duration 5h allocate r18 booking r18 2007-04-23-9:00 +5h { sloppy 2 } ${FixStart "2007-04-23-9:00"} ${FixEnd "2007-04-23-14:00"} } task t19 "Task 19" { duration 5h allocate r19 booking r19 2007-04-23-9:00 +5h { sloppy 2 } ${FixStart "2007-04-23-9:00"} ${FixEnd "2007-04-23-14:00"} } task t20 "Task 20" { duration 6h allocate r20 booking r20 2007-04-23-9:00 +5h { sloppy 2 } ${FixStart "2007-04-23-9:00"} ${FixEnd "2007-04-23-15:00"} } taskreport booking "Booking" { formats html columns no, name, effort, hourly hideresource 0 } TaskJuggler-3.8.1/test/TestSuite/Scheduler/Correct/Booking2.tjp000066400000000000000000000017451473026623400244030ustar00rootroot00000000000000project "Booking2" 2010-02-08 +1m { now 2010-02-15 trackingscenario plan } include "checks.tji" resource r1 "R1" task "T1" { effort 40h allocate r1 ${FixStart "2010-02-15-9:00"} ${FixEnd "2010-02-19-17:00"} } resource r2 "R2" task "T2" { effort 40h allocate r2 booking r2 2010-02-08-9:00 +5d { sloppy 1 } ${FixStart "2010-02-08-9:00"} ${FixEnd "2010-02-12-17:00"} } resource r3 "r3" { vacation 2010-02-09 } task "T3" { effort 32h allocate r3 booking r3 2010-02-08 +2d { sloppy 2 } ${FixStart "2010-02-08-9:00"} ${FixEnd "2010-02-17-17:00"} } resource r4 "R4" task "T4" { effort 56h allocate r4 booking r4 2010-02-08 +2d { overtime 1 } ${FixStart "2010-02-08"} ${FixEnd "2010-02-15-17:00"} } resource r5 "r5" { vacation 2010-02-09 } task "T5" { effort 56h allocate r5 booking r5 2010-02-08 +2d { overtime 2 } ${FixEnd "2010-02-15-17:00"} } taskreport "Booking2" { formats html hideresource 0 columns no, name, effort, end, hourly } TaskJuggler-3.8.1/test/TestSuite/Scheduler/Correct/Booking3.tjp000066400000000000000000000010151473026623400243720ustar00rootroot00000000000000project "Test" 2011-05-15 +1w { scenario s1 "S1" { scenario s2 "S2" } trackingscenario s2 now 2011-05-17 workinghours mon-fri 9:00 - 12:00, 13:00 - 18:00 } include 'checks.tji' resource r1 "R1" resource r2 "R2" task t1 "T1" { effort 2d allocate r1 booking r1 2011-05-16-9:00 +3h, 2011-05-16-13:00 +5h ${FixEndSc "2011-05-17-18:00" "s1"} ${FixEndSc "2011-05-18-18:00" "s2"} } taskreport "Booking3" { formats html columns no, name, scenario, start, end, hourly scenarios s1, s2 } TaskJuggler-3.8.1/test/TestSuite/Scheduler/Correct/Container-2.tjp000066400000000000000000000011651473026623400250060ustar00rootroot00000000000000project prj "My Project" "1.0" 2009-01-01-0:00-+0100 - 2009-01-31-0:00-+0100 { timezone 'Europe/Amsterdam' } include "checks.tji" task C0 "C0" { task T0 "T0" { duration 6d ${FixTask "2009-01-01" "2009-01-07"} } ${FixTask "2009-01-01" "2009-01-07"} } task T1 "T1" { depends C0 duration 1d ${FixTask "2009-01-07" "2009-01-08"} } task M1 "M1" { milestone start 2009-01-10 depends T1, C0 ${FixMS "2009-01-10"} } task M2 "M2" { milestone depends C0 ${FixMS "2009-01-07"} } taskreport tr "Container-2" { formats html columns name,start,end,chart { scale day } sorttasks tree, seqno.up } TaskJuggler-3.8.1/test/TestSuite/Scheduler/Correct/Container.tjp000066400000000000000000000013031473026623400246410ustar00rootroot00000000000000project prj "Project" "1.0" 2009-01-01 +1m include "checks.tji" task S "start" { milestone start 2009-01-01 } task E "end" { milestone end 2009-01-30 } task C0 "C0" { depends S precedes E ${FixTask "2009-01-10-0:00" "2009-01-20-0:00"} task C1 "C1" { ${FixEnd "2009-01-16-17:00"} task T1 "T1" { ${FixTask "2009-01-10-0:00" "2009-01-16-17:00"} start 2009-1-10 length 1w } } task C2 "C2" { ${FixStart "2009-01-13-9:00"} scheduling alap task T2 "T2" { ${FixTask "2009-01-13-9:00" "2009-01-20-0:00"} end 2009-1-20 length 1w } } } taskreport t "Container" { formats html columns name, start, end, chart { scale day } } TaskJuggler-3.8.1/test/TestSuite/Scheduler/Correct/DateAndDep.tjp000066400000000000000000000006051473026623400246540ustar00rootroot00000000000000project "Date and dependency" 2013-06-22 +2m include "checks.tji" resource r "R" task "Forward" { task t1 "T1" { start 2013-06-24 effort 2d allocate r } task t2 "T2" { depends !t1 { onstart } start 2013-06-27 effort 2d allocate r ${FixStart "2013-06-27-9:00"} } } taskreport "DateAndDep" { formats html columns no, name, start, end, chart } TaskJuggler-3.8.1/test/TestSuite/Scheduler/Correct/Depends.tjp000066400000000000000000000017531473026623400243120ustar00rootroot00000000000000project test "Test" "1.0" 2007-04-02 +3m include "checks.tji" resource r1 "R1" resource r2 "R2" task t1 "T1" { start ${projectstart} duration 1w ${FixEnd "2007-04-09"} } task t2 "T2" { depends t1 duration 1w ${FixTask "2007-04-09" "2007-04-16"} } task t3 "T3" { depends t2 { gapduration 1w } duration 1w ${FixTask "2007-04-23" "2007-04-30"} } task t4 "T4" { depends t3 { onend gaplength 1w } duration 1w ${FixTask "2007-05-04-17:00" "2007-05-11-17:00"} } task t5 "T5" { depends t4 { onstart } duration 1w ${FixTask "2007-05-04-17:00" "2007-05-11-17:00"} } task t6 "T6" { depends t4 { onstart gaplength 1w } duration 1w ${FixTask "2007-05-11-17:00" "2007-05-18-17:00"} } task t7 "T7" { start ${projectstart} effort 1w allocate r1 ${FixEnd "2007-04-06-17:00"} } task t8 "T8" { depends t7 { onstart gaplength 1w } effort 1w allocate r2 ${FixTask "2007-04-9-9:00" "2007-04-13-17:00"} } taskreport depends "Depends" { formats html columns name, start, end, daily } TaskJuggler-3.8.1/test/TestSuite/Scheduler/Correct/Duration.tjp000066400000000000000000000015441473026623400245130ustar00rootroot00000000000000project test "Test" "1.0" 2007-03-25 +6m resource r1 "R1" resource r2 "R2" resource r3 "R3" resource r4 "R4" task duration "Duration Tasks" { start 2007-03-27 # MARK: warning 11 allocate_no_assigned task t1 "Duration 5h" { duration 5h allocate r1 minend 2007-03-27-5:00 maxend 2007-03-27-5:00 fail plan.effort != 0 } task t2 "Duration 5d" { duration 5d allocate r2 minend 2007-04-01 maxend 2007-04-01 fail plan.effort != 4 } task t3 "Duration 5w" { duration 5w allocate r3 minend 2007-05-01 maxend 2007-05-01 fail plan.effort != 25 } task t4 "Duration 5m" { duration 5m allocate r4 minend 2007-08-26-2:00 maxend 2007-08-26-2:00 fail plan.effort != 109 } } taskreport duration "Duration" { formats html loadunit days columns name, start, effort, end, daily } TaskJuggler-3.8.1/test/TestSuite/Scheduler/Correct/InheritStartEnd.tjp000066400000000000000000000045011473026623400257710ustar00rootroot00000000000000project "Test" 2009-02-04 - 2009-06-01 { timezone "Europe/Amsterdam" } include "checks.tji" resource r1 "Resource 1" resource r2 "Resource 2" resource r3 "Resource 3" resource r4 "Resource 4" task ms1 "Milestone 1" { ${FixStart "${projectstart}"} milestone } task ms2 "Milestone 2" { milestone scheduling alap ${FixEnd "${projectend}"} } task p1 "Parent Tasks" { task only_s_e "Only start or end dates" { task t1 "Task 1" { ${FixMS "${projectstart}"} } task t2 "Task 2" { start 2009-02-06 # This is an implicit milestone ${FixTask "2009-02-06" "2009-02-06"} } task t3 "Task 3" { end 2009-03-01 # This is an implicit milestone ${FixMS "2009-03-01"} } } task ms "Milestones" { task t1 "Task 1" { milestone ${FixMS "${projectstart}"} } task t2 "Task 2" { milestone scheduling alap ${FixMS "${projectend}"} } task t3 "Task 3" { start 2009-02-06 milestone ${FixMS "2009-02-06"} } task t4 "Task 4" { end 2009-03-01 milestone ${FixMS "2009-03-01"} } task t5 "Task 5" { start 2009-02-06 depends ms1 ${FixMS "2009-02-06"} } task t6 "Task 6" { end 2009-03-01 precedes ms2 ${FixMS "2009-03-01"} } } task w_effort "With effort" { task t1 "Task 1" { allocate r1 effort 20d scheduling asap ${FixTask "2009-02-04-09:00" "2009-03-03-17:00"} } task t2 "Task 2" { start 2009-02-06 effort 20d scheduling asap allocate r2 ${FixTask "2009-02-06-09:00" "2009-03-05-17:00"} } task t3 "Task 3" { effort 20d allocate r3 scheduling alap ${FixTask "2009-05-04-09:00" "2009-05-29-17:00"} } task t4 "Task 4" { end 2009-05-01 scheduling alap effort 20d allocate r4 ${FixTask "2009-04-03-09:00" "2009-04-30-17:00"} } } } task p2 "Parent Tasks 2" { task t1 "Task1" { ${FixMS "${projectstart}"} } task t2 "Task2" { depends !t1 ${FixMS "${projectstart}"} } task t3 "Task3" { scheduling alap ${FixMS "${projectend}"} } task t4 "Task4" { precedes !t3 ${FixMS "${projectend}"} } } taskreport inherit "InheritStartEnd" { formats html columns name, id, start, end, chart } TaskJuggler-3.8.1/test/TestSuite/Scheduler/Correct/InheritedAttributes.tjp000066400000000000000000000026351473026623400267120ustar00rootroot00000000000000project "Inherited Attributes" 2011-01-04 +1m { scenario s1 "S1" { scenario s2 "S2" { scenario s3 "S3" } } } # Task/Scenario 1 2 3 # T1 3! 2! 2 # T2 3 2 2 # T3 3 2 2 task "T1" { priority 3 s2:priority 2 fail s1.priority != 3 fail s2.priority != 2 fail s3.priority != 2 task "T2" { fail s1.priority != 3 fail s2.priority != 2 fail s3.priority != 2 task "T3" { fail s1.priority != 3 fail s2.priority != 2 fail s3.priority != 2 } } } # Task/Scenario 1 2 3 # T1 3! 2! 2 # T2 3 2 2 # T3 3 1 1 task "T1" { priority 3 s2:priority 2 fail s1.priority != 3 fail s2.priority != 2 fail s3.priority != 2 task "T2" { fail s1.priority != 3 fail s2.priority != 2 fail s3.priority != 2 task "T3" { s2:priority 1 fail s1.priority != 3 fail s2.priority != 1 fail s3.priority != 1 } } } # Task/Scenario 1 2 3 # T1 3! 2! 2 # T2 3 2 1 # T3 3 2 1 task "T1" { priority 3 s2:priority 2 fail s1.priority != 3 fail s2.priority != 2 fail s3.priority != 2 task "T2" { s3:priority 1 fail s1.priority != 3 fail s2.priority != 2 fail s3.priority != 1 task "T3" { s2:priority 1 fail s1.priority != 3 fail s2.priority != 1 fail s3.priority != 1 } } } TaskJuggler-3.8.1/test/TestSuite/Scheduler/Correct/Leaves.tjp000066400000000000000000000006061473026623400241430ustar00rootroot00000000000000project "Leaves" 2012-03-12 +6m { scenario s1 "S1" { scenario s2 "S2" } } vacation "Goof-out day" 2012-03-14 resource r1 "R1" { vacation 2012-03-19 +1w } task "T1" { effort 20d allocate r1 fail s1.end != 2012-04-16-17:00 fail s2.end != 2012-04-16-17:00 } taskreport "Leaves" { formats html columns no, name, start, end, daily scenarios s1, s2 hideresource 0 } TaskJuggler-3.8.1/test/TestSuite/Scheduler/Correct/Limits.tjp000066400000000000000000000033121473026623400241620ustar00rootroot00000000000000project test "Test" "1.0" 2007-08-29 +6m { workinghours mon-fri 9:00 - 12:00, 13:00 - 18:00 } include "checks.tji" resource a "A" resource b "B" resource c "C" resource d "D" resource e "E" resource f "F" resource g "G" resource h "H" resource i "I" resource j "J" resource k "K" task tasks "Tasks" { task t1 "40h with 4h daily limit" { effort 40h allocate a limits { dailymax 4h } ${FixEnd "2007-09-11-14:00"} } task t2 "30d with 3d weekly limit" { effort 30d allocate b limits { weeklymax 3d } ${FixEnd "2007-10-31-18:00"} } task t3 "45d with 15d monthly limit" { effort 45d allocate c limits { monthlymax 15d } ${FixEnd "2007-11-15-18:00"} } task t4 "7h/day 4d/week 15d/months" { effort 50d allocate d limits { dailymax 7h weeklymax 4d monthlymax 15d } ${FixEnd "2007-12-04-15:00"} } } task nested "Nested Tasks (20h/week limit)" { limits { weeklymax 20h } task a "Task A" { allocate e effort 30d ${FixEnd "2008-02-06-14:00"} } task b "Task B (7h/day limit)" { effort 30d allocate f priority 600 limits { dailymax 7h } ${FixEnd "2007-11-14-16:00"} } } task interval "Interval Limit" { effort 20d allocate g limits { weeklymax 8h { period 2007-09-05 +8d }} ${FixEnd "2007-10-01-18:00"} } task resource "Resource Limit" { effort 10d allocate h limits { weeklymax 16h { resources h }} ${FixEnd "2007-09-25-18:00"} } task resources "Multiple Resources" { effort 20d allocate i, j, k limits { dailymax 2h { resources i} dailymax 6h { resources j} } ${FixEnd "2007-09-11-18:00"} } taskreport limits "Limits" { formats html columns no, name, start, end, effort, daily } TaskJuggler-3.8.1/test/TestSuite/Scheduler/Correct/Manager.tjp000066400000000000000000000001621473026623400242730ustar00rootroot00000000000000project "test" 2010-04-03 +1w resource boss "Big Boss" resource joe "Joe Average" { managers boss } task "T" TaskJuggler-3.8.1/test/TestSuite/Scheduler/Correct/Mandatory.tjp000066400000000000000000000011311473026623400246540ustar00rootroot00000000000000project "Mandatory" 2009-10-31 +2m include "checks.tji" resource a "A" { workinghours mon, wed, fri off } resource b "B" resource c "C" { workinghours tue, thu off } resource d "D" { workinghours wed off } resource e "E" task "T1" { effort 16d allocate a { mandatory}, b ${FixTask "2009-11-03-9:00" "2009-11-26-17:00"} } task "T2" { effort 24d allocate c { mandatory}, d { mandatory}, e ${FixTask "2009-11-02-9:00" "2009-11-27-17:00"} } taskreport "Mandatory" { formats html timeformat "%Y-%m-%d %H:%M" columns no, name, start, end, chart { scale day } hideresource 0 } TaskJuggler-3.8.1/test/TestSuite/Scheduler/Correct/MultipleMandatories.tjp000066400000000000000000000014251473026623400267060ustar00rootroot00000000000000project test "Test" "1.0" 2007-11-22 +6d resource computenodes "Compute Nodes" { resource node01 "node01" resource node02 "node02" resource node03 "node03" resource node04 "node04" resource node05 "node05" } macro allocNodes [ allocate node01 { mandatory alternative node02, node03, node04, node05 } ] task ProductionPlan "Production Plan" { task A "A" { start ${projectstart} effort 4d ${allocNodes} ${allocNodes} ${allocNodes} ${allocNodes} } task B1 "B1" { depends !A effort 4d ${allocNodes} ${allocNodes} ${allocNodes} ${allocNodes} } task B2 "B2" { depends !A effort 4d ${allocNodes} ${allocNodes} ${allocNodes} ${allocNodes} minend 2007-11-26-17:00 maxend 2007-11-26-17:00 } } TaskJuggler-3.8.1/test/TestSuite/Scheduler/Correct/Optimize-1.tjp000066400000000000000000000007521473026623400246640ustar00rootroot00000000000000project test "Test" "1.0" 2000-01-01-0:00 +1m { timezone "UTC" } include "checks.tji" resource tux1 "Tux1" resource tux2 "Tux2" task t1 "Task1" { start 2000-01-01 ${FixEnd "2000-01-04-17:00"} effort 2d allocate tux1 { alternative tux2 persistent } } task t2 "Task2" { start 2000-01-01 ${FixEnd "2000-01-06-17:00"} effort 4d allocate tux1 } taskreport optimize1 "Optimize-1" { formats html columns name, start, end, criticalness, pathcriticalness, daily hideresource 0 } TaskJuggler-3.8.1/test/TestSuite/Scheduler/Correct/Optimize-2.tjp000066400000000000000000000011421473026623400246570ustar00rootroot00000000000000project test "Test" "1.0" 2000-01-01-0:00 +1m { timezone "UTC" } include "checks.tji" resource tux1 "Tux1" resource tux2 "Tux2" task t1 "Task1" { start 2000-01-01 ${FixEnd "2000-01-14-17:00"} effort 10d allocate tux2 { alternative tux1 persistent } } task t2 "Task2" { start 2000-01-01 ${FixEnd "2000-01-06-17:00"} effort 4d allocate tux2 { persistent } } task t3 "Task3" { depends !t2 ${FixEnd "2000-01-20-17:00"} effort 10d allocate tux2 { persistent } } taskreport optimize2 "Optimize-2" { formats html timeformat "%Y-%m-%d %H:%M" columns name, start, end, daily hideresource 0 } TaskJuggler-3.8.1/test/TestSuite/Scheduler/Correct/Optimize-3.tjp000066400000000000000000000010511473026623400246570ustar00rootroot00000000000000project test "Test" "1.0" 2000-01-01-0:00 +1m { timezone "UTC" } include "checks.tji" resource tux1 "Tux1" resource tux2 "Tux2" task t1 "Task1" { start 2000-01-04 ${FixEnd "2000-01-10-17:00"} effort 5d allocate tux1 } task t2 "Task2" { start 2000-01-10 ${FixEnd "2000-01-17-17:00"} effort 5d allocate tux1 } task t3 "Task3" { start 2000-01-02 ${FixEnd "2000-01-06-17:00"} effort 4d allocate tux1 { alternative tux2 persistent } } taskreport optimize3 "Optimize-3" { formats html columns name, start, end, daily hideresource 0 } TaskJuggler-3.8.1/test/TestSuite/Scheduler/Correct/Optimize-4.tjp000066400000000000000000000011411473026623400246600ustar00rootroot00000000000000project test "Test" "Test" 2000-01-01-0:00 +1m { timezone "UTC" } include "checks.tji" # This example does not get optimized properly yet. resource tux1 "Tux1" resource tux2 "Tux2" task t1 "Task1" { start 2000-01-01 ${FixEnd "2000-01-07-17:00"} effort 5d allocate tux1 } task t2 "Task2" { start 2000-01-01 ${FixEnd "2000-01-24-17:00"} effort 6d allocate tux1 } task t3 "Task3" { depends !t1 ${FixEnd "2000-01-14-17:00"} effort 10d allocate tux1, tux2 } taskreport optimize4 "Optimize-4" { formats html columns name, start, end, criticalness, pathcriticalness, daily hideresource 0 } TaskJuggler-3.8.1/test/TestSuite/Scheduler/Correct/Optimize-5.tjp000066400000000000000000000017141473026623400246670ustar00rootroot00000000000000project test "Test" "1.0" 2000-01-01-0:00 +2m { timezone "UTC" } include "checks.tji" resource tux1 "Tux1" resource tux2 "Tux2" resource tux3 "Tux3" task t0 "Task0" { start 2000-01-01 ${FixEnd "2000-01-07-17:00"} effort 5d allocate tux1 { alternative tux3 } } task t1 "Task1" { start 2000-01-01 ${FixEnd "2000-01-07-17:00"} effort 5d allocate tux1 } task t2 "Task2" { start 2000-01-01 ${FixEnd "2000-01-07-17:00"} effort 5d allocate tux2 } task t3 "Task3" { depends !t1, !t2 ${FixEnd "2000-01-11-17:00"} effort 4d allocate tux1, tux2 } task t4 "Task4" { depends !t3 ${FixEnd "2000-01-18-17:00"} effort 5d allocate tux1 } task t5 "Task5" { depends !t3 ${FixEnd "2000-01-25-17:00"} effort 10d allocate tux2 } task t6 "Task6" { start 2000-01-01 effort 5d allocate tux1 ${FixEnd "2000-01-25-17:00"} } taskreport optimize5 "Optimize-5" { formats html columns name, start, end, criticalness, pathcriticalness, daily hideresource 0 } TaskJuggler-3.8.1/test/TestSuite/Scheduler/Correct/PersistentResources-2.tjp000066400000000000000000000010451473026623400271140ustar00rootroot00000000000000project "Test" 2012-08-10 +3m include "checks.tji" resource a "A" { leaves project "" 2012-08-14 - ${projectend} } resource b "B" macro Alloc [ allocate a { alternative b persistent } ] # MARK: warning 15 broken_persistence task "T1" { start 2012-08-13 effort 5d ${Alloc} ${FixEnd "2012-08-27-17:00"} } task "T2" { start 2012-08-14 effort 6d ${Alloc} ${FixEnd "2012-08-21-17:00"} } taskreport "PersistentResources" { formats html columns name, criticalness, pathcriticalness, chart { scale day } hideresource @none } TaskJuggler-3.8.1/test/TestSuite/Scheduler/Correct/PersistentResources.tjp000066400000000000000000000007141473026623400267570ustar00rootroot00000000000000project "Test" 2012-08-10 +3m include "checks.tji" resource a "A" resource b "B" macro Alloc [ allocate a { alternative b persistent } ] task "T1" { start 2012-08-13 effort 5d ${Alloc} ${FixEnd "2012-08-17-17:00"} } task "T2" { start 2012-08-14 effort 6d ${Alloc} ${FixEnd "2012-08-21-17:00"} } taskreport "PersistentResources" { formats html columns name, criticalness, pathcriticalness, chart { scale day } hideresource @none } TaskJuggler-3.8.1/test/TestSuite/Scheduler/Correct/Precedes.tjp000066400000000000000000000020061473026623400244520ustar00rootroot00000000000000project test "Test" "1.0" 2007-04-02 - 2007-07-01 include "checks.tji" resource r1 "R1" resource r2 "R2" task t1 "T1" { end ${projectend} duration 1w ${FixStart "2007-06-24"} } task t2 "T2" { precedes t1 duration 1w ${FixTask "2007-06-17" "2007-06-24"} } task t3 "T3" { precedes t2 { gapduration 1w } duration 1w ${FixTask "2007-06-03" "2007-06-10"} } task t4 "T4" { precedes t3 { onstart gaplength 1w } duration 1w ${FixTask "2007-05-21-9:00" "2007-05-28-9:00"} } task t5 "T5" { precedes t4 { onend } duration 1w ${FixTask "2007-05-21-9:00" "2007-05-28-9:00"} } task t6 "T6" { precedes t4 { onend gaplength 1w } duration 1w ${FixTask "2007-05-14-9:00" "2007-05-21-9:00"} } task t7 "T7" { end ${projectend} effort 1w allocate r1 ${FixStart "2007-06-25-9:00"} } task t8 "T8" { precedes t7 { onend gaplength 1w } effort 1w allocate r2 ${FixTask "2007-06-18-9:00" "2007-06-22-17:00"} } taskreport preceds "Precedes" { formats html columns name, start, end, daily } TaskJuggler-3.8.1/test/TestSuite/Scheduler/Correct/PriorityInversion.tjp000066400000000000000000000007501473026623400264420ustar00rootroot00000000000000project pi "Priority Inversion" 2011-02-01 +1m resource r1 "R1" resource r2 "R2" task t1 "T1" { start 2011-02-08 effort 1w allocate r1 priority 400 } # MARK: warning 15 priority_inversion # MARK: info 23 priority_inversion_info task "T2" { precedes t1 { onend } effort 1w allocate r2 priority 600 } task "T3" { start 2011-02-08 effort 1w allocate r2 priority 500 } taskreport "PriorityInversion" { formats html columns no, name, daily hideresource 0 } TaskJuggler-3.8.1/test/TestSuite/Scheduler/Correct/Scenarios.tjp000066400000000000000000000005121473026623400246460ustar00rootroot00000000000000project "Multiple Scenarios" "1.0" 2011-02-27 +2m { scenario s1 "S1" { scenario s2 "S2" } } include "checks.tji" resource r1 "R1" resource r2 "R2" task t1 "T1" { allocate r1 allocate r2 effort 2w ${FixTaskSc "2011-02-28-9:00" "2011-03-04-17:00" "s1"} ${FixTaskSc "2011-02-28-9:00" "2011-03-04-17:00" "s2"} } TaskJuggler-3.8.1/test/TestSuite/Scheduler/Correct/Scheduled.tjp000066400000000000000000000002621473026623400246220ustar00rootroot00000000000000project "Test" 2013-08-18 +1m include "checks.tji" resource r "R" task "Foo" { booking r 2013-08-19-9:00 +8h scheduled ${FixTask "2013-08-19-9:00" "2013-08-19-17:00"} } TaskJuggler-3.8.1/test/TestSuite/Scheduler/Correct/SchedulingMode.tjp000066400000000000000000000017771473026623400256300ustar00rootroot00000000000000project "Scheduling Mode" 2014-03-16 +2m { scenario s1 "S1" { scenario s2 "S2" scenario s3 "S3" } trackingscenario s2 now 2014-03-23 timeformat "%Y-%m-%d-%H:%M" } include "checks.tji" resource r1 "R1" resource r2 "R2" task t1 "T1" { effort 10d allocate r1 ${FixStartSc "2014-03-17-09:00" "s1"} ${FixStartSc "2014-03-24-09:00" "s2"} ${FixStartSc "2014-03-17-09:00" "s3"} ${FixEndSc "2014-03-28-17:00" "s1"} ${FixEndSc "2014-04-04-17:00" "s2"} ${FixEndSc "2014-03-28-17:00" "s3"} } task t2 "T2" { task t3 "T3" { effort 10d allocate r2 s2:start 2014-03-17-9:00 s2:effortdone 5d ${FixStartSc "2014-03-17-09:00" "s1"} ${FixStartSc "2014-03-17-09:00" "s2"} ${FixStartSc "2014-03-17-09:00" "s3"} ${FixEndSc "2014-03-28-17:00" "s1"} ${FixEndSc "2014-03-28-17:00" "s2"} ${FixEndSc "2014-03-28-17:00" "s3"} } } taskreport "SchedulingMode" { formats html columns no, name, start, end, effort, effortdone, effortleft, chart scenarios s1, s2, s3 } TaskJuggler-3.8.1/test/TestSuite/Scheduler/Correct/Shift.tjp000066400000000000000000000035421473026623400240030ustar00rootroot00000000000000project test "Test" "1.0" 2007-08-22 +2m { workinghours mon-fri 9:00 - 12:00, 13:00 - 18:00 } include "checks.tji" shift odd_days "Odd Days" { workinghours mon, wed, fri 10:00 - 16:00 workinghours tue, thu, sat, sun off vacation 2007-09-03 } shift even_days "Even Days" { workinghours mon, wed, fri off workinghours tue, thu, sat, sun 10:00 - 16:00 vacation 2007-09-04 shift even_no_we "Even Days, no weekend" { workinghours sat, sun off } } shift morning "Morning" { workinghours mon - sun 8:00 - 12:00 } shift thu_vac "Vacation on Thursday" { vacation 2007-09-06 replace } resource team "Team" { vacation 2007-08-22 +3d shifts morning 2007-08-23 +5d resource mdf "MDF Worker" { shifts odd_days 2007-09-01 +2w } resource ttss "TTSS Worker" { shifts even_days 2007-09-01 +2w } resource tt "TT Worker" { shifts even_no_we 2007-09-01 +2w } resource wed_vac "Vacation on Wednesday" { vacation 2007-09-05 shifts thu_vac 2007-09-01 +7d } resource work1 "Worker 1" } resource default "Default Worker" task prj "Project" { start 2007-08-22 task mdf "MDF Task" { effort 4w allocate mdf ${FixEnd "2007-10-01-16:00"} } task ttss "TTSS Task" { effort 5w allocate ttss ${FixEnd "2007-10-05-11:00"} } task tt "TT Task" { effort 4w allocate tt ${FixEnd "2007-10-03-11:00"} } task default "Default Task" { effort 7w allocate default ${FixEnd "2007-10-09-18:00"} } task vac_test "Vacation on Thursday" { start 2007-09-01 effort 4d allocate wed_vac ${FixEnd "2007-09-07-18:00"} } task work1 "Task with shift morning" { start ${projectstart} allocate work1 effort 3w shifts morning 2007-09-01 +4w ${FixEnd "2007-10-02-18:00"} } } taskreport shift "Shift" { formats html columns name, start, end, daily } TaskJuggler-3.8.1/test/TestSuite/Scheduler/Correct/Shift2.tjp000066400000000000000000000015601473026623400240630ustar00rootroot00000000000000project "Shift2" 2010-02-06 +1w include "checks.tji" shift s2 "s2" { replace workinghours mon - fri 8:00 - 16:00 } shift s3 "s3" { vacation 2010-02-09 workinghours mon - fri 8:00 - 16:00 } shift s4 "s4" { replace vacation 2010-02-09 workinghours mon - fri 8:00 - 16:00 } vacation 2010-02-10 resource r1 "r1" task "T1" { effort 32h allocate r1 ${FixEnd "2010-02-12-17:00"} } resource r2 "r2" { shifts s2 2010-02-08 +1w } task "T2" { effort 40h allocate r2 ${FixEnd "2010-02-12-16:00"} } resource r3 "r3" { shifts s3 2010-02-08 +1w } task "T3" { effort 24h allocate r3 ${FixEnd "2010-02-12-16:00"} } resource r4 "r4" { shifts s4 2010-02-08 +1w } task "T4" { effort 32h allocate r4 ${FixEnd "2010-02-12-16:00"} } taskreport "Shift2" { formats html columns name, start, end, daily sorttasks index.up loadunit hours } TaskJuggler-3.8.1/test/TestSuite/Scheduler/Correct/TimeSheet2.tjp000066400000000000000000000026121473026623400246740ustar00rootroot00000000000000project "test" 2010-02-21 +2m { trackingscenario plan now ${projectstart} } vacation 2010-02-24 resource r1 "R1" resource r2 "R2" resource r3 "R3" { efficiency 0.0 } task t1 "Task 1" { effort 5d allocate r1 } task t2 "Task 2" { task t3 "Task 3" { effort 10d allocate r2 } } timesheet r1 2010-02-21 +1w { task t1 { work 80% remaining 1.0d status green "Lots of work done" { summary "I had good fun!" details -8<- This task went smoothly and I got three things done: * Have fun * Be on time * Get things done ->8- } } status yellow "About me" { summary "I'm not feeling good." } } timesheet r1 2010-02-28 +1w { newtask t4 "Something great" { work 100% remaining 10d status green "I had a great idea!" } } timesheet r2 2010-02-21 +1w { task t1 { work 20% remaining 1d status green "I helped r1" } task t2.t3 { work 60% remaining 8d status red "I need more time" { summary "This takes longer than expected" details -8<- To finish on time, I need help. Get this r1 guy to help me out here. * I want to have fun too! ->8- } } } timesheet r3 2010-02-21 +1w { task t1 { work 0% remaining 1.0d status green "I see nothing but roses." } } resourcereport "TimeSheet2" { formats html columns name, journal } TaskJuggler-3.8.1/test/TestSuite/Scheduler/Correct/WeakDeps.tjp000066400000000000000000000010741473026623400244270ustar00rootroot00000000000000project "Weak Deps Test" 2011-01-06 +2m resource r1 "R1" resource r2 "R2" task t1 "Task 1" { task st1 "SubTask 1" { effort 1d allocate r1 } task st2 "SubTask 2" { effort 1d allocate r2 depends !st1 { onstart } } } task t2 "Task 2" { task st1 "SubTask 1" { effort 1d allocate r1 } task st2 "SubTask 2" { depends !st1 { onstart } } } task t3 "Task 3" { task st1 "SubTask 1" { effort 1d scheduling alap allocate r1 } task st2 "SubTask 2" { effort 1d allocate r2 precedes !st1 { onend } } } TaskJuggler-3.8.1/test/TestSuite/Scheduler/Correct/checks.tji000066400000000000000000000006471473026623400241620ustar00rootroot00000000000000macro FixStart [ minstart ${1} maxstart ${1} ] macro FixEnd [ minend ${1} maxend ${1} ] macro FixMS [ ${FixStart "${1}"} ${FixEnd "${1}"}] macro FixTask [ ${FixStart "${1}"} ${FixEnd "${2}"}] macro FixStartSc [ fail ${2}.start != ${1} ] macro FixEndSc [ fail ${2}.end != ${1} ] macro FixMSSc [ ${FixStartSc "${1}" "${2}"} ${FixEndSc "${1}" "${2}"}] macro FixTaskSc [ ${FixStartSc "${1}" "${3}"} ${FixEndSc "${2}" "${3}"}] TaskJuggler-3.8.1/test/TestSuite/Scheduler/Correct/hammock.tjp000066400000000000000000000006531473026623400243450ustar00rootroot00000000000000project "Hammock tasks" 2013-06-22 +2m include 'checks.tji' task m1 "Milestone 1" { start 2013-07-01 } task m2 "Milestone 2" { start 2013-08-01 } task p1 "phase 1" { precedes m2 depends m1 task f "Foo" { ${FixStart "2013-07-01"} ${FixEnd "2013-08-01"} } task b "Bar" { ${FixStart "2013-07-01"} ${FixEnd "2013-08-01"} } } taskreport "test" { formats html columns name, start, end, chart } TaskJuggler-3.8.1/test/TestSuite/Scheduler/Correct/purge.tjp000066400000000000000000000007361473026623400240520ustar00rootroot00000000000000project "Test" 2012-05-31 +1m { scenario s1 "S1" { scenario s2 "S2" } } include "checks.tji" resource r1 "R1" { workinghours mon - fri 8:00 - 12:00 } resource r2 "R2" task "T1" { allocate r1 task "T2" { purge s2:allocate s2:allocate r2 effort 2w ${FixEndSc "2012-06-27-12:00" "s1"} ${FixEndSc "2012-06-13-17:00" "s2"} } } taskreport "purge" { formats html columns name, scenario, end, weekly hideresource @none scenarios s1, s2 } TaskJuggler-3.8.1/test/TestSuite/Scheduler/Errors/000077500000000000000000000000001473026623400220565ustar00rootroot00000000000000TaskJuggler-3.8.1/test/TestSuite/Scheduler/Errors/account_no_leaf.tjp000066400000000000000000000002701473026623400257130ustar00rootroot00000000000000project test "Test" "1.0" 2007-10-31 +1m account foo "Foo" # MARK: error 6 account_no_leaf task t "T" { effort 1d chargeset foo } supplement account foo { account bar "Bar" } TaskJuggler-3.8.1/test/TestSuite/Scheduler/Errors/allocate_no_assigned.tjp000066400000000000000000000002651473026623400267350ustar00rootroot00000000000000project "Test" 2011-07-06 +1m resource r "R" task "T1" { effort 5d allocate r priority 600 } # MARK: warning 11 allocate_no_assigned task "T2" { length 5d allocate r } TaskJuggler-3.8.1/test/TestSuite/Scheduler/Errors/booking_conflict.tjp000066400000000000000000000002701473026623400261050ustar00rootroot00000000000000project test "Test" "1.0" 2007-04-24 +2m resource tux "Tux" # MARK: error 9 booking_conflict task foo "Foo" { booking tux 2007-04-25-10:00 +2h booking tux 2007-04-25-11:00 +1h } TaskJuggler-3.8.1/test/TestSuite/Scheduler/Errors/booking_no_duty.tjp000066400000000000000000000002241473026623400257640ustar00rootroot00000000000000project test "Test" "1.0" 2007-04-26 +2m resource tux "Tux" # MARK: error 8 booking_no_duty task foo "Foo" { booking tux 2007-04-26-16:00 +3h } TaskJuggler-3.8.1/test/TestSuite/Scheduler/Errors/booking_on_vacation.tjp000066400000000000000000000002651473026623400266100ustar00rootroot00000000000000project test "Test" "1.0" 2007-04-26 +2m resource tux "Tux" { vacation 2007-04-27 +2d } # MARK: error 8 booking_on_vacation task foo "Foo" { booking tux 2007-04-27-10:00 +1h } TaskJuggler-3.8.1/test/TestSuite/Scheduler/Errors/container_booking.tjp000066400000000000000000000003231473026623400262650ustar00rootroot00000000000000project test "Test" "1.0" 2007-10-31 +1m resource foo "Foo" # MARK: error 6 container_booking task t "T" { start ${projectstart} booking foo 2007-11-01-10:00 +1h } supplement task t { task bar "Bar" } TaskJuggler-3.8.1/test/TestSuite/Scheduler/Errors/container_duration.tjp000066400000000000000000000002311473026623400264600ustar00rootroot00000000000000project test "Test" "1.0" 2007-10-31 +1m resource tux "Tux" # MARK: error 6 container_duration task t "T" { effort 1d allocate tux task s "S" } TaskJuggler-3.8.1/test/TestSuite/Scheduler/Errors/container_milestone.tjp000066400000000000000000000002601473026623400266340ustar00rootroot00000000000000project test "Test" "1.0" 2013-01-22 +1m resource tux "Tux" # MARK: error 6 container_milestone task t "T" { milestone task s "S" { effort 1d allocate tux } } TaskJuggler-3.8.1/test/TestSuite/Scheduler/Errors/effort_no_allocations.tjp000066400000000000000000000002031473026623400271410ustar00rootroot00000000000000project test "Test" "1.0" 2008-02-02 +1m # MARK: error 4 effort_no_allocations task t "T" { start ${projectstart} effort 2d } TaskJuggler-3.8.1/test/TestSuite/Scheduler/Errors/impossible_end_dep.tjp000066400000000000000000000003411473026623400264170ustar00rootroot00000000000000project prj "Project" "1.0" 2009-10-04 +6m task T1 "T1" { end 2009-12-31 duration 2w } # MARK: error 8 impossible_end_dep task T2 "T2" { precedes !T1 start 2009-12-24 } task T3 "T3" { precedes !T2 duration 1w } TaskJuggler-3.8.1/test/TestSuite/Scheduler/Errors/impossible_start_dep.tjp000066400000000000000000000003411473026623400270060ustar00rootroot00000000000000project prj "Project" "1.0" 2009-08-16 +6m task T1 "T1" { start 2009-09-23 duration 2w } # MARK: error 8 impossible_start_dep task T2 "T2" { depends !T1 end 2009-09-30 } task T3 "T3" { depends !T2 duration 1w } TaskJuggler-3.8.1/test/TestSuite/Scheduler/Errors/loop_detected_1.tjp000066400000000000000000000004351473026623400256310ustar00rootroot00000000000000project test "Test" "1.0" 2000-01-01 - 2000-01-04 task t1 "Task1" { length 1d depends !t2 } task t2 "Task2" { length 1d depends !t1 } # MARK: warning 3 loop_detected # MARK: info 3 loop_at_end # MARK: info 8 loop_at_start # MARK: info 8 loop_at_end # MARK: error 3 loop_end TaskJuggler-3.8.1/test/TestSuite/Scheduler/Errors/loop_detected_10.tjp000066400000000000000000000011011473026623400257000ustar00rootroot00000000000000project test "Test" "1.0" 2000-01-01 - 2000-01-08 task a "Task A" { start 2000-01-01 task b1 "Task B1" { task c1 "Task C1" { length 1d depends !!b2 } task c2 "Task C2" { length 1d depends !c1 } } task b2 "Task B2" { task c1 "Task C1" { length 1d } task c2 "Task C2" { depends !!b1.c2 length 1d } } } # MARK: warning 7 loop_detected # MARK: info 7 loop_at_end # MARK: info 11 loop_at_start # MARK: info 11 loop_at_end # MARK: info 21 loop_at_start # MARK: info 21 loop_at_end # MARK: info 17 loop_at_end # MARK: error 7 loop_end TaskJuggler-3.8.1/test/TestSuite/Scheduler/Errors/loop_detected_11.tjp000066400000000000000000000006611473026623400257130ustar00rootroot00000000000000project prj "Loop Detector Test" "$Id" 2000-01-01 - 2000-01-10 task t1 "Task1" { task t2 "Task2" { start 2000-01-01 length 1d } task t3 "Task3" { length 1d depends !t2, !t4 } task t4 "Task4" { length 1d depends !t3 } task t5 "Task5" { length 1d depends !t4 } } # MARK: warning 8 loop_detected # MARK: info 8 loop_at_end # MARK: info 12 loop_at_start # MARK: info 12 loop_at_end # MARK: error 8 loop_end TaskJuggler-3.8.1/test/TestSuite/Scheduler/Errors/loop_detected_12.tjp000066400000000000000000000005151473026623400257120ustar00rootroot00000000000000project test "Test" "1.0" 2000-01-01 - 2000-01-10 task t1 "Task1" { task t2 "Task2" { end 2000-01-04 length 1d precedes !t3 } task t3 "Task3" { length 1d precedes !t2 } } # MARK: warning 4 loop_detected # MARK: info 4 loop_at_start # MARK: info 9 loop_at_end # MARK: info 9 loop_at_start # MARK: error 4 loop_end TaskJuggler-3.8.1/test/TestSuite/Scheduler/Errors/loop_detected_13.tjp000066400000000000000000000006011473026623400257070ustar00rootroot00000000000000project test "Test" "1.0" 2005-10-25 - 2005-12-01 task a "Task A" { start 2005-10-25 task b1 "Task B1" { depends m2 task c1 "Task C1" { precedes m2 length 1d } } } task m2 "Milestone2" { start 2005-11-20 milestone } # MARK: warning 9 loop_detected # MARK: info 9 loop_at_start # MARK: info 16 loop_at_end # MARK: info 16 loop_at_start # MARK: error 9 loop_end TaskJuggler-3.8.1/test/TestSuite/Scheduler/Errors/loop_detected_14.tjp000066400000000000000000000006141473026623400257140ustar00rootroot00000000000000project prj "Loop Detector Test" "$Id" 2005-10-25 - 2005-12-01 task a "Task A" { start 2005-10-25 task b1 "Task B1" { precedes m2 task c1 "Task C1" { depends m2 length 1d } } } task m2 "Milestone2" { start 2005-11-20 milestone } # MARK: warning 9 loop_detected # MARK: info 9 loop_at_end # MARK: info 16 loop_at_start # MARK: info 16 loop_at_end # MARK: error 9 loop_end TaskJuggler-3.8.1/test/TestSuite/Scheduler/Errors/loop_detected_2.tjp000066400000000000000000000006021473026623400256260ustar00rootroot00000000000000project test "Test" "1.0" 2000-01-01 - 2000-01-04 task t1 "Task1" { length 1d depends !t3 } task t2 "Task2" { length 1d depends !t1 } task t3 "Task3" { length 1d depends !t2 } # MARK: warning 3 loop_detected # MARK: info 3 loop_at_end # MARK: info 8 loop_at_start # MARK: info 8 loop_at_end # MARK: info 13 loop_at_start # MARK: info 13 loop_at_end # MARK: error 3 loop_end TaskJuggler-3.8.1/test/TestSuite/Scheduler/Errors/loop_detected_3.tjp000066400000000000000000000004401473026623400256270ustar00rootroot00000000000000project test "Test" "1.0" 2000-01-01 - 2000-01-04 task t1 "Task1" { length 1d precedes !t2 } task t2 "Task2" { length 1d precedes !t1 } # MARK: warning 3 loop_detected # MARK: info 3 loop_at_start # MARK: info 8 loop_at_end # MARK: info 8 loop_at_start # MARK: error 3 loop_end TaskJuggler-3.8.1/test/TestSuite/Scheduler/Errors/loop_detected_4.tjp000066400000000000000000000010651473026623400256340ustar00rootroot00000000000000project test "Test" "1.0" 2000-01-01 - 2000-01-04 task t1 "Task1" { start 2000-01-01 task s1 "Sub1" { length 1d depends !!t3.s1 } } task t2 "Task2" { depends !t1 task s1 "Sub1" { length 1d } } task t3 "Task3" { task s1 "Sub1" { length 1d depends !!t2 } } # MARK: warning 6 loop_detected # MARK: info 6 loop_at_end # MARK: info 3 loop_at_end # MARK: info 12 loop_at_start # MARK: info 14 loop_at_start # MARK: info 14 loop_at_end # MARK: info 12 loop_at_end # MARK: info 20 loop_at_start # MARK: info 20 loop_at_end # MARK: error 6 loop_end TaskJuggler-3.8.1/test/TestSuite/Scheduler/Errors/loop_detected_5.tjp000066400000000000000000000011021473026623400256250ustar00rootroot00000000000000project test "Test" "1.0" 2000-01-01 - 2000-01-08 task a "Task A" { start 2000-01-01 task b1 "Task B1" { task c1 "Task C1" { length 1d depends !!b2 } task c2 "Task C2" { length 1d depends !c1 } } task b2 "Task B2" { task c1 "Task C1" { length 1d } task c2 "Task C2" { depends !!b1.c2 length 1d } } } # MARK: warning 7 loop_detected # MARK: info 7 loop_at_end # MARK: info 11 loop_at_start # MARK: info 11 loop_at_end # MARK: info 21 loop_at_start # MARK: info 21 loop_at_end # MARK: info 17 loop_at_end # MARK: error 7 loop_end TaskJuggler-3.8.1/test/TestSuite/Scheduler/Errors/loop_detected_6.tjp000066400000000000000000000010721473026623400256340ustar00rootroot00000000000000project test "Test" "1.0" 2000-01-01 - 2000-01-04 task t1 "Task1" { end 2000-01-04 task s1 "Sub1" { length 1d precedes !!t3.s1 } } task t2 "Task2" { precedes !t1 task s1 "Sub1" { length 1d } } task t3 "Task3" { task s1 "Sub1" { length 1d precedes !!t2 } } # MARK: warning 6 loop_detected # MARK: info 6 loop_at_start # MARK: info 3 loop_at_start # MARK: info 12 loop_at_end # MARK: info 14 loop_at_end # MARK: info 14 loop_at_start # MARK: info 12 loop_at_start # MARK: info 20 loop_at_end # MARK: info 20 loop_at_start # MARK: error 6 loop_end TaskJuggler-3.8.1/test/TestSuite/Scheduler/Errors/loop_detected_9.tjp000066400000000000000000000005131473026623400256360ustar00rootroot00000000000000project test "Test" "1.0" 2000-01-01 - 2000-01-10 task t1 "Task1" { task t2 "Task2" { start 2000-01-01 length 1d depends !t3 } task t3 "Task3" { length 1d depends !t2 } } # MARK: warning 4 loop_detected # MARK: info 4 loop_at_end # MARK: info 9 loop_at_start # MARK: info 9 loop_at_end # MARK: error 4 loop_end TaskJuggler-3.8.1/test/TestSuite/Scheduler/Errors/manager_is_group.tjp000066400000000000000000000003271473026623400261200ustar00rootroot00000000000000project "test" 2010-04-03 +1w resource group "Group" { resource "Stan" resource "Oli" } # MARK: error 8 manager_is_group resource joe "Joe Average" { managers group } task "T" { effort 1d allocate joe } TaskJuggler-3.8.1/test/TestSuite/Scheduler/Errors/manager_is_self.tjp000066400000000000000000000002261473026623400257130ustar00rootroot00000000000000project "test" 2010-04-03 +1w # MARK: error 4 manager_is_self resource joe "Joe Average" { managers joe } task "T" { effort 1d allocate joe } TaskJuggler-3.8.1/test/TestSuite/Scheduler/Errors/maxend.tjp000066400000000000000000000001711473026623400240500ustar00rootroot00000000000000project test "Test" "1.0" 2007-03-28 +2m task t "Task" { end 2007-03-29 # MARK: warning 3 maxend maxend 2007-03-28 } TaskJuggler-3.8.1/test/TestSuite/Scheduler/Errors/maxstart.tjp000066400000000000000000000001771473026623400244450ustar00rootroot00000000000000project test "Test" "1.0" 2007-03-28 +2m task t "Task" { start 2007-03-29 # MARK: warning 3 maxstart maxstart 2007-03-28 } TaskJuggler-3.8.1/test/TestSuite/Scheduler/Errors/milestone_booking.tjp000066400000000000000000000002671473026623400263110ustar00rootroot00000000000000project test "Test" "1.0" 2007-10-31 +1m resource foo "Foo" # MARK: error 6 milestone_booking task t "T" { start ${projectstart} booking foo 2007-11-01-10:00 +1h milestone } TaskJuggler-3.8.1/test/TestSuite/Scheduler/Errors/milestone_start_end.tjp000066400000000000000000000002151473026623400266350ustar00rootroot00000000000000project test "Test" "1.0" 2008-02-02 +1m # MARK: error 4 milestone_start_end task t "T" { start 2008-02-10 end 2008-02-11 milestone } TaskJuggler-3.8.1/test/TestSuite/Scheduler/Errors/minend.tjp000066400000000000000000000001711473026623400240460ustar00rootroot00000000000000project test "Test" "1.0" 2007-03-28 +2m task t "Task" { end 2007-03-29 # MARK: warning 3 minend minend 2007-03-30 } TaskJuggler-3.8.1/test/TestSuite/Scheduler/Errors/minstart.tjp000066400000000000000000000001771473026623400244430ustar00rootroot00000000000000project test "Test" "1.0" 2007-03-28 +2m task t "Task" { start 2007-03-29 # MARK: warning 3 minstart minstart 2007-03-30 } TaskJuggler-3.8.1/test/TestSuite/Scheduler/Errors/no_tasks.tjp000066400000000000000000000001241473026623400244130ustar00rootroot00000000000000project test "Test" "1.0" 2007-03-28 +2m resource r "R" # MARK: error 0 no_tasks TaskJuggler-3.8.1/test/TestSuite/Scheduler/Errors/not_scheduled.tjp000066400000000000000000000003031473026623400254110ustar00rootroot00000000000000project test "Test" "1.0" 2010-04-28 +1w resource r "R" task "Foo" { # MARK: error 7 not_scheduled task bar "Bar" { start ${projectstart} effort 1d allocate r scheduled } } TaskJuggler-3.8.1/test/TestSuite/Scheduler/Errors/onend_wrong_direction.tjp000066400000000000000000000004531473026623400271560ustar00rootroot00000000000000project "Test" 2011-01-08 +1m resource r1 "R1" resource r2 "R2" task "Task 1" { task t2 "Task 2" { effort 1w allocate r1 scheduling alap } # MARK: error 13 onend_wrong_direction task "Task 3" { precedes !t2 { onend } effort 1w allocate r2 scheduling asap } } TaskJuggler-3.8.1/test/TestSuite/Scheduler/Errors/onstart_wrong_direction.tjp000066400000000000000000000004321473026623400275420ustar00rootroot00000000000000project "Test" 2011-01-08 +1m resource r1 "R1" resource r2 "R2" task "Task 1" { task t2 "Task 2" { effort 1w allocate r1 } # MARK: error 12 onstart_wrong_direction task "Task 3" { depends !t2 { onstart } effort 1w allocate r2 scheduling alap } } TaskJuggler-3.8.1/test/TestSuite/Scheduler/Errors/overbooked_duration.tjp000066400000000000000000000002761473026623400266460ustar00rootroot00000000000000project "Test" 2011-06-24 +1m resource r "R" # MARK: warning 6 overbooked_duration task "T" { allocate r duration 1d booking r 2011-06-24-9:00 +8h, 2011-06-27-9:00 +1h } TaskJuggler-3.8.1/test/TestSuite/Scheduler/Errors/overbooked_effort.tjp000066400000000000000000000002721473026623400263020ustar00rootroot00000000000000project "Test" 2011-06-24 +1m resource r "R" # MARK: warning 6 overbooked_effort task "T" { allocate r effort 1d booking r 2011-06-24-9:00 +8h, 2011-06-27-9:00 +1h } TaskJuggler-3.8.1/test/TestSuite/Scheduler/Errors/overbooked_length.tjp000066400000000000000000000003171473026623400262760ustar00rootroot00000000000000project "Test" 2011-06-24 +1m { now 2011-07-01 } resource r "R" # MARK: warning 8 overbooked_length task "T" { allocate r length 1d booking r 2011-06-24-9:00 +8h, 2011-06-27-9:00 +1h } TaskJuggler-3.8.1/test/TestSuite/Scheduler/Errors/resource_fail_check.tjp000066400000000000000000000002271473026623400265550ustar00rootroot00000000000000project "test" 2010-04-02 +1m # MARK: error 4 resource_fail_check resource r "R" { fail plan.effort != 6 } task "T" { allocate r effort 5d } TaskJuggler-3.8.1/test/TestSuite/Scheduler/Errors/resource_warn_check.tjp000066400000000000000000000002311473026623400266040ustar00rootroot00000000000000project "test" 2010-04-02 +1m # MARK: warning 4 resource_warn_check resource r "R" { warn plan.effort != 6 } task "T" { allocate r effort 5d } TaskJuggler-3.8.1/test/TestSuite/Scheduler/Errors/sched_runaway.tjp000066400000000000000000000005331473026623400254320ustar00rootroot00000000000000project "Runaway" 2013-06-23 +3w resource r "R" # MARK: warning 11 runaway # MARK: info 11 runaway_tasks # MARK: info 17 runaway_competitor # MARK: warning 11 unscheduled_tasks # MARK: warning 11 unscheduled_task # MARK: error 11 sched_runaway task "T low" { effort 15d priority 1 allocate r } task "T high" { effort 10d allocate r } TaskJuggler-3.8.1/test/TestSuite/Scheduler/Errors/task_depend_child.tjp000066400000000000000000000002331473026623400262170ustar00rootroot00000000000000project test "Test" "1.0" 2007-04-01 +2m task t1 "T1" { # MARK: error 3 task_depend_child depends t1.t2 task t2 "T2" { start ${projectstart} } } TaskJuggler-3.8.1/test/TestSuite/Scheduler/Errors/task_depend_multi.tjp000066400000000000000000000002461473026623400262720ustar00rootroot00000000000000project test "Test" "1.0" 2007-04-01 +2m task t1 "T1" { start ${projectstart} duration 1d } task t2 "T2" { # MARK: error 8 task_depend_multi depends t1, t1 } TaskJuggler-3.8.1/test/TestSuite/Scheduler/Errors/task_depend_parent.tjp000066400000000000000000000002321473026623400264240ustar00rootroot00000000000000project test "Test" "1.0" 2007-04-01 +2m task t1 "T1" { start ${projectstart} task t2 "T2" { depends t1 # MARK: error 5 task_depend_parent } } TaskJuggler-3.8.1/test/TestSuite/Scheduler/Errors/task_depend_self.tjp000066400000000000000000000002311473026623400260630ustar00rootroot00000000000000project test "Test" "1.0" 2007-04-01 +2m task t1 "T1" { start ${projectstart} task t2 "T2" { # MARK: error 5 task_depend_self depends t1.t2 } } TaskJuggler-3.8.1/test/TestSuite/Scheduler/Errors/task_depend_unknown.tjp000066400000000000000000000002331473026623400266330ustar00rootroot00000000000000project test "Test" "1.0" 2007-04-01 +2m task t1 "T1" { start ${projectstart} task t2 "T2" { depends foo # MARK: error 5 task_depend_unknown } } TaskJuggler-3.8.1/test/TestSuite/Scheduler/Errors/task_fail_check.tjp000066400000000000000000000001621473026623400256660ustar00rootroot00000000000000project "test" 2010-04-02 +1m # MARK: error 4 task_fail_check task "T" { length 2d fail plan.length != 3 } TaskJuggler-3.8.1/test/TestSuite/Scheduler/Errors/task_overspecified_1.tjp000066400000000000000000000002151473026623400266640ustar00rootroot00000000000000project test "Test" "1.0" 2008-02-03 +1m # MARK: error 4 task_overspecified task t "T" { start 2008-02-04 length 2d end 2008-02-06 } TaskJuggler-3.8.1/test/TestSuite/Scheduler/Errors/task_overspecified_2.tjp000066400000000000000000000003011473026623400266610ustar00rootroot00000000000000project test "Test" "1.0" 2008-02-03 +1m task m "M" { start ${projectstart} } # MARK: error 8 task_overspecified task t "T" { depends !m length 2d end 2008-02-06 scheduling asap } TaskJuggler-3.8.1/test/TestSuite/Scheduler/Errors/task_overspecified_3.tjp000066400000000000000000000003021473026623400266630ustar00rootroot00000000000000project test "Test" "1.0" 2008-02-03 +1m task m "M" { end ${projectstart} } # MARK: error 8 task_overspecified task t "T" { precedes !m length 2d start 2008-02-03 scheduling alap } TaskJuggler-3.8.1/test/TestSuite/Scheduler/Errors/task_pred_before.tjp000066400000000000000000000002351473026623400260730ustar00rootroot00000000000000project "Test" 2010-07-12 +1m task t1 "T1" { duration 1w } # MARK: error 7 task_pred_before task t2 "T2" { depends !t1 duration 8d end 2010-07-26 } TaskJuggler-3.8.1/test/TestSuite/Scheduler/Errors/task_pred_before_1.tjp000066400000000000000000000002561473026623400263160ustar00rootroot00000000000000project "Test" 2010-07-12 +1m task t1 "T1" { duration 1w } # MARK: error 7 task_pred_before task t2 "T2" { depends !t1 { gaplength 3d } duration 6d end 2010-07-26 } TaskJuggler-3.8.1/test/TestSuite/Scheduler/Errors/task_pred_before_2.tjp000066400000000000000000000002601473026623400263120ustar00rootroot00000000000000project "Test" 2010-07-12 +1m task t1 "T1" { duration 1w } # MARK: error 7 task_pred_before task t2 "T2" { depends !t1 { gapduration 3d } duration 6d end 2010-07-26 } TaskJuggler-3.8.1/test/TestSuite/Scheduler/Errors/task_succ_after.tjp000066400000000000000000000002651473026623400257400ustar00rootroot00000000000000project "Test" 2010-07-12 +1m task t1 "T1" { duration 1w end 2010-07-26 } # MARK: error 8 task_succ_after task t2 "T2" { precedes !t1 duration 8d start ${projectstart} } TaskJuggler-3.8.1/test/TestSuite/Scheduler/Errors/task_succ_after_1.tjp000066400000000000000000000003061473026623400261540ustar00rootroot00000000000000project "Test" 2010-07-12 +1m task t1 "T1" { duration 1w end 2010-07-26 } # MARK: error 8 task_succ_after task t2 "T2" { precedes !t1 { gaplength 3d } duration 6d start ${projectstart} } TaskJuggler-3.8.1/test/TestSuite/Scheduler/Errors/task_succ_after_2.tjp000066400000000000000000000003101473026623400261500ustar00rootroot00000000000000project "Test" 2010-07-12 +1m task t1 "T1" { duration 1w end 2010-07-26 } # MARK: error 8 task_succ_after task t2 "T2" { precedes !t1 { gapduration 3d } duration 6d start ${projectstart} } TaskJuggler-3.8.1/test/TestSuite/Scheduler/Errors/task_underspecified_1.tjp000066400000000000000000000002211473026623400270230ustar00rootroot00000000000000project test "Test" "1.0" 2009-02-13 +1m resource r "R" # MARK: error 6 task_underspecified task t "T" { booking r 2009-02-16 { sloppy 2 } } TaskJuggler-3.8.1/test/TestSuite/Scheduler/Errors/task_underspecified_3.tjp000066400000000000000000000002141473026623400270270ustar00rootroot00000000000000project test "Test" "1.0" 2009-02-13 +1m resource r "R" # MARK: error 6 task_underspecified task t "T" { scheduling alap allocate r } TaskJuggler-3.8.1/test/TestSuite/Scheduler/Errors/task_warn_check.tjp000066400000000000000000000001641473026623400257240ustar00rootroot00000000000000project "test" 2010-04-02 +1m # MARK: warning 4 task_warn_check task "T" { length 2d warn plan.length != 3 } TaskJuggler-3.8.1/test/TestSuite/Scheduler/Errors/ts_alert1_more_details.tjp000066400000000000000000000004441473026623400272240ustar00rootroot00000000000000project "test" 2009-11-30 +2m { trackingscenario plan } resource r1 "R1" task t1 "Task 1" { effort 5d allocate r1 } timesheet r1 2009-11-30 +1w { # MARK: error 14 ts_alert1_more_details task t1 { work 2d remaining 0d status yellow "Some headline here!" { } } } TaskJuggler-3.8.1/test/TestSuite/Scheduler/Errors/ts_alert2_more_details.tjp000066400000000000000000000005011473026623400272170ustar00rootroot00000000000000project "test" 2009-11-30 +2m { trackingscenario plan } resource r1 "R1" task t1 "Task 1" { effort 5d allocate r1 } timesheet r1 2009-11-30 +1w { # MARK: error 14 ts_alert2_more_details task t1 { work 2d remaining 0d status red "Some headline here!" { summary "I had good fun!" } } } TaskJuggler-3.8.1/test/TestSuite/Scheduler/Errors/ts_no_expected_end.tjp000066400000000000000000000007071473026623400264320ustar00rootroot00000000000000project "test" 2009-11-30 +2m { trackingscenario plan } resource r1 "R1" task t1 "Task 1" { duration 5d allocate r1 } timesheet r1 2009-11-30 +1w { # MARK: error 14 ts_no_expected_end task t1 { work 0d status green "All work done" { summary "I had good fun!" details -8<- This task went smoothly and I got three things done: * Have fun * Be on time * Get things done ->8- } } } TaskJuggler-3.8.1/test/TestSuite/Scheduler/Errors/ts_no_headline1.tjp000066400000000000000000000007051473026623400256330ustar00rootroot00000000000000project "test" 2009-11-30 +2m { trackingscenario plan } resource r1 "R1" task t1 "Task 1" { effort 5d allocate r1 } timesheet r1 2009-11-30 +1w { # MARK: error 14 ts_no_headline task t1 { work 2d remaining 0d status green "" { summary "I had good fun!" details -8<- This task went smoothly and I got three things done: * Have fun * Be on time * Get things done ->8- } } } TaskJuggler-3.8.1/test/TestSuite/Scheduler/Errors/ts_no_rem_or_end.tjp000066400000000000000000000007251473026623400261140ustar00rootroot00000000000000project "test" 2009-11-30 +2m { trackingscenario plan } resource r1 "R1" task t1 "Task 1" { duration 5d allocate r1 } timesheet r1 2009-11-30 +1w { # MARK: error 14 ts_no_rem_or_end newtask t2 "A new task" { work 5d status green "All work done" { summary "I had good fun!" details -8<- This task went smoothly and I got three things done: * Have fun * Be on time * Get things done ->8- } } } TaskJuggler-3.8.1/test/TestSuite/Scheduler/Errors/ts_no_remaining.tjp000066400000000000000000000007021473026623400257470ustar00rootroot00000000000000project "test" 2009-11-30 +2m { trackingscenario plan } resource r1 "R1" task t1 "Task 1" { effort 5d allocate r1 } timesheet r1 2009-11-30 +1w { # MARK: error 14 ts_no_remaining task t1 { work 0d status green "All work done" { summary "I had good fun!" details -8<- This task went smoothly and I got three things done: * Have fun * Be on time * Get things done ->8- } } } TaskJuggler-3.8.1/test/TestSuite/Scheduler/Errors/ts_no_status_work.tjp000066400000000000000000000003571473026623400263710ustar00rootroot00000000000000project "test" 2009-11-30 +2m { trackingscenario plan } resource r1 "R1" task t1 "Task 1" { effort 5d allocate r1 } timesheet r1 2009-11-30 +1w { # MARK: error 14 ts_no_status_work task t1 { work 2d remaining 0d } } TaskJuggler-3.8.1/test/TestSuite/Scheduler/Errors/ts_no_work.tjp000066400000000000000000000007021473026623400247600ustar00rootroot00000000000000project "test" 2009-11-30 +2m { trackingscenario plan } resource r1 "R1" task t1 "Task 1" { effort 5d allocate r1 } timesheet r1 2009-11-30 +1w { # MARK: error 14 ts_no_work task t1 { remaining 0d status green "All work done" { summary "I had good fun!" details -8<- This task went smoothly and I got three things done: * Have fun * Be on time * Get things done ->8- } } } TaskJuggler-3.8.1/test/TestSuite/Scheduler/Errors/ts_res_new_task.tjp000066400000000000000000000005201473026623400257640ustar00rootroot00000000000000project "test" 2009-11-30 +2m { trackingscenario plan now ${projectstart} } resource r1 "R1" task t1 "Task 1" { effort 5d allocate r1 } timesheet r1 2009-11-30 +1w { # MARK: warning 15 ts_res_new_task newtask t1 "T1" { work 5d remaining 0d status yellow "Big problem" { summary "Big problem" } } } TaskJuggler-3.8.1/test/TestSuite/Scheduler/Errors/ts_work_too_high.tjp000066400000000000000000000007241473026623400261500ustar00rootroot00000000000000project "test" 2009-11-30 +2m { trackingscenario plan } resource r1 "R1" task t1 "Task 1" { effort 5d allocate r1 } # MARK: error 13 ts_work_too_high timesheet r1 2009-11-30 +1w { task t1 { work 8d remaining 4d status green "All work done" { summary "I had good fun!" details -8<- This task went smoothly and I got three things done: * Have fun * Be on time * Get things done ->8- } } } TaskJuggler-3.8.1/test/TestSuite/Scheduler/Errors/ts_work_too_low.tjp000066400000000000000000000007231473026623400260310ustar00rootroot00000000000000project "test" 2009-11-30 +2m { trackingscenario plan } resource r1 "R1" task t1 "Task 1" { effort 5d allocate r1 } # MARK: error 13 ts_work_too_low timesheet r1 2009-11-30 +1w { task t1 { work 3d remaining 4d status green "All work done" { summary "I had good fun!" details -8<- This task went smoothly and I got three things done: * Have fun * Be on time * Get things done ->8- } } } TaskJuggler-3.8.1/test/TestSuite/Syntax/000077500000000000000000000000001473026623400201525ustar00rootroot00000000000000TaskJuggler-3.8.1/test/TestSuite/Syntax/Correct/000077500000000000000000000000001473026623400215535ustar00rootroot00000000000000TaskJuggler-3.8.1/test/TestSuite/Syntax/Correct/Account.tjp000066400000000000000000000025131473026623400236670ustar00rootroot00000000000000project simple "Simple Project" "1.0" 2007-01-01 - 2007-01-30 { timezone "America/Denver" currency "USD" } # *** EXAMPLE: 1 + account project_cost "Project Costs" account payments "Customer Payments"{ credits 2007-01-01 "Customer down payment" 500.0, 2007-01-14 "1st rate" 2000.0 } balance project_cost payments resource tux "Tux" { rate 300 } resource konqui "Konqui" { rate 200 } task items "Room decoration" { start 2007-01-06 # The default account for all tasks chargeset project_cost task plan "Plan work and buy material" { # Upfront material cost charge 500.0 onstart length 2d } task remove "Remove old inventory" { allocate tux allocate konqui effort 1d depends !plan } task implement "Arrange new decoration" { effort 5d allocate tux, konqui depends !remove } task acceptance "Presentation and customer acceptance" { duration 5d depends !implement chargeset payments # Customer pays at end of acceptance charge 2000.0 onend } } # *** EXAMPLE: 1 - accountreport "Account-1" { formats html timeformat "%d-%M-%y" columns index, name, weekly } accountreport "Account-2" { formats html timeformat "%d-%M-%y" columns index, name, weekly { celltext 1 "<-query attribute='turnover'->" } } TaskJuggler-3.8.1/test/TestSuite/Syntax/Correct/AccountReport.tjp000066400000000000000000000025011473026623400250600ustar00rootroot00000000000000project "AccountReport" 2011-11-09 +1y { currencyformat "(" ")" "," "." 0 } account resourceCost "Resource Cost" { aggregate resources account teamA "Team A" account teamB "Team B" } account productCost "Product Cost" { aggregate tasks } account customerPayments "Customer Payments" { credits 2011-12-01 "First downpayment" 80000, 2012-06-01 "Second downpayment" 200000 } balance productCost customerPayments resource teamA "Team A" { rate 420 chargeset teamA resource "R1" resource "R2" } resource teamB "Team B" { rate 380 chargeset teamB resource "R3" resource "R4" } task "Products" { chargeset productCost task p1 "Product 1" { effort 20m allocate teamA, teamB } task p2 "Product 2" { effort 18m allocate teamA priority 600 } task p3 "Product 3" { effort 6m allocate teamB priority 600 } task mf "Manufacturing" { depends !p1, !p2, !p3 duration 2w charge 12000 onend } task "Final Payment" { depends !mf purge chargeset chargeset customerPayments charge 170000 onstart } } accountreport "TeamBudget" { formats html accountroot resourceCost balance - columns no, name, quarterly { celltext 1 "<-query attribute='turnover'->" } } accountreport "ProfiAndLoss" { formats html columns no, name, monthly } TaskJuggler-3.8.1/test/TestSuite/Syntax/Correct/AdoptedTasks.tjp000066400000000000000000000002601473026623400246560ustar00rootroot00000000000000project "Adopted Tasks" 2011-03-05 +1m task t1 "T1" task t2 "T2" task t3 "T3" { adopt t1, t2 } taskreport "AdoptedTasks" { formats html columns no, bsi, index, name } TaskJuggler-3.8.1/test/TestSuite/Syntax/Correct/AlertLevels.tjp000066400000000000000000000012431473026623400245140ustar00rootroot00000000000000project "Alert Levels" 2011-11-24 +2m { alertlevels green "Low" "#2AA46C", blue "Guarded" "#457CC4", yellow "Elevated" "#F1D821", orange "High" "#F99836", red "Severe" "#E43745" } task "Holiday Season" { journalentry 2011-11-24 "All safe unless you are a turkey" { alert green } journalentry 2011-11-27 "Strange lights appearing everywhere" { alert blue } journalentry 2011-12-01 "Alarm bells heared" { alert yellow } journalentry 2011-12-20 "Everybody has strange packages" { alert orange } journalentry 2011-12-25 "Guy with red coat entered through chimney" { alert red } } TaskJuggler-3.8.1/test/TestSuite/Syntax/Correct/Allocate-1.tjp000066400000000000000000000007411473026623400241560ustar00rootroot00000000000000project allocate "allocate" "1.0" 2003-06-05 - 2003-07-05 { timezone "America/Denver" } # *** EXAMPLE: 1 + resource r1 "Resource 1" resource r2 "Resource 2" task t1 "Task 1" { start 2003-06-05 # All sub-tasks inherit this allocation of r1 allocate r1 task t2 "Task 2" { effort 10d } task t3 "Task 3" { effort 20d # This task has r1 and r2 allocated allocate r2 } # *** EXAMPLE: 1 - task m1 "Milestone 1" { milestone } # *** EXAMPLE: 1 + } TaskJuggler-3.8.1/test/TestSuite/Syntax/Correct/Alternative.tjp000066400000000000000000000004311473026623400245460ustar00rootroot00000000000000project "Project" 2000-01-01 - 2000-03-01 { timezone "America/Denver" } # *** EXAMPLE: 1 + resource tuxus "Tuxus" resource tuxia "Tuxia" task t "Task" { start ${projectstart} effort 5d # Use tuxus or tuxia, whoever is available. allocate tuxus { alternative tuxia } } TaskJuggler-3.8.1/test/TestSuite/Syntax/Correct/AutoID.tjp000066400000000000000000000003651473026623400234230ustar00rootroot00000000000000project "Simple Project" 2009-10-04 +1m { timezone "America/Denver" } account "Foo" { account "Bar" } shift "Foo" { shift "bar" } resource "Foo" { resource "Bar" } task "Foo" { task "Bar" } taskreport "Foo" { taskreport "Bar" } TaskJuggler-3.8.1/test/TestSuite/Syntax/Correct/AutoMacros.tjp000066400000000000000000000003541473026623400243510ustar00rootroot00000000000000project prj "Auto Macro Test" "1.0" 2006-09-22-0:00 +1m { timezone "UTC" now 2006-10-15 } task items "Project breakdown" { start ${projectstart} } taskreport tasks "My Tasks" { formats html start ${now} end ${projectend} } TaskJuggler-3.8.1/test/TestSuite/Syntax/Correct/Booking.tjp000066400000000000000000000017141473026623400236650ustar00rootroot00000000000000project project "Simple Project" "1.0" 2007-01-05 +1m { timezone "America/Denver" # The baseline date for the projection. now 2007-01-15 } resource tux "Tux" task test "Testing" { start 2007-01-05 effort 10d allocate tux } supplement resource tux { # Book a whole day (8 hours). The 1 hour lunch break is skipped. booking test 2007-01-06-9:00 +9h { sloppy 1 } # Book 2 days in the afternoon, 4 hours each. booking test 2007-01-08-13:00 +4h, 2007-01-09-13:00 +4h # This is a common mistake. With standard working hours, this will # yield a zero time booking! The interval is midnight to 8am. So # it's outside of the working hours and 'sloppy 2' suppresses the # warning. booking test 2007-01-11 +8h { sloppy 2 } # Use 'overtime' to book off-hour slots. This booking will book the # full 10 hours, ignoring the lunch break and adding an extra hour # in the morning. booking test 2007-01-11-8:00 +10h { overtime 1 } } TaskJuggler-3.8.1/test/TestSuite/Syntax/Correct/Caption.tjp000066400000000000000000000013261473026623400236710ustar00rootroot00000000000000project simple "Simple Project" "1.0" 2007-01-01 - 2007-02-01 { timezone "America/Denver" } # *** EXAMPLE: 2 + copyright "Tux Inc." # *** EXAMPLE: 1 + resource r1 "Resource 1" task plant "How to plant a tree" { start 2007-01-01 # All sub-tasks inherit this allocation of r1 allocate r1 task plan "Choose the planting site" { effort 2d } task buy "Get a tree" { effort 1d depends !plan } task action "Plant the tree" { effort 0.5d depends !buy } } taskreport planttree "PlantTree.html" { formats html caption "This project shows how to plant a tree easily" headline "How to plant a tree" columns name, start, end, daily # Don't hide any resource, thus show them all. hideresource 0 } TaskJuggler-3.8.1/test/TestSuite/Syntax/Correct/Celltext.tjp000066400000000000000000000013731473026623400240620ustar00rootroot00000000000000project celltext "celltext" "1.0" 2007-01-01 - 2007-03-01 { timezone "America/Denver" } # *** EXAMPLE: 1 + resource tux "Tux" task t "Task" { task s "SubTask" { start 2007-01-01 effort 5d allocate tux } } # *** EXAMPLE: 1 - # Just a very basic report with some standard columns taskreport simple "SimpleReport" { formats html columns bsi, name, start, end, weekly } # Report with custom colum title taskreport custom "CustomTitle" { formats html columns bsi, name { title "Work Item" }, effort } # *** EXAMPLE: 1 + # Report with index and task name combined in one single column taskreport combined "CombinedColumn" { formats html columns name { celltext 1 "<-query attribute='bsi'-> <-query->"}, start, end, weekly } TaskJuggler-3.8.1/test/TestSuite/Syntax/Correct/Comments.tjp000066400000000000000000000010641473026623400240600ustar00rootroot00000000000000/* This is a multi-line comment * that spans multiple rows. */ project simple "Simple Project" "1.0" 2005-06-06 - 2005-06-26 { timezone "America/Denver" } // A resource definition resource tux "Tux" task items "Project breakdown" { start 2005-06-06 task plan "Plan work" { length 3d } task implementation "Implement work" { effort 5d // Some effort allocate tux # A resource depends !plan } task acceptance "Customer acceptance" { duration 5d depends !implementation } } taskreport tasks "My Tasks" # End of project TaskJuggler-3.8.1/test/TestSuite/Syntax/Correct/Complete.tjp000066400000000000000000000006331473026623400240440ustar00rootroot00000000000000project simple "Simple Project" "1.0" 2007-01-01 +2m { timezone "America/Denver" now 2007-01-15 } # *** EXAMPLE: 1 + task "Build house" { start 2007-01-06 duration 30d # This task should have been completed on Jan 15, but only # 20% of the task have been completed so far. complete 20 } taskreport "Complete" { formats html columns name, end { title "Due date" }, complete, gauge, chart } TaskJuggler-3.8.1/test/TestSuite/Syntax/Correct/CompletedWork.tji000066400000000000000000000007431473026623400250460ustar00rootroot00000000000000supplement task t { plan:booking r 2000-01-03-09:00-+0100 + 3.0h, 2000-01-03-13:00-+0100 + 5.0h, 2000-01-04-09:00-+0100 + 3.0h, 2000-01-04-13:00-+0100 + 5.0h, 2000-01-05-09:00-+0100 + 3.0h, 2000-01-05-13:00-+0100 + 5.0h, 2000-01-06-09:00-+0100 + 3.0h, 2000-01-06-13:00-+0100 + 5.0h, 2000-01-07-09:00-+0100 + 3.0h, 2000-01-07-13:00-+0100 + 5.0h } TaskJuggler-3.8.1/test/TestSuite/Syntax/Correct/Currencyformat.tjp000066400000000000000000000004721473026623400253000ustar00rootroot00000000000000project prj "Project" "1.0" 2007-01-01 - 2007-03-01 { timezone "Europe/Berlin" # German currency format: e. g. -10.000,20 5.014,11 currencyformat "-" "" "." "," 2 # US currency format: e. g. (10,000.20) 5,014.11 currencyformat "(" ")" "," "." 2 } task t "Task" { start ${projectstart} milestone } TaskJuggler-3.8.1/test/TestSuite/Syntax/Correct/CustomAttributes.tjp000066400000000000000000000030371473026623400256160ustar00rootroot00000000000000project "Extend Test" 2013-04-24 +1m { extend task { date DueDate "Due Date" number Count "Count" reference URL "URL" richtext Claim "Claim" text Intro "Intro" date DueDateS "Due Date" { scenariospecific inherit } number CountS "Count" { scenariospecific } reference URLS "URL" { scenariospecific } richtext ClaimS "Claim" { scenariospecific inherit } text IntroS "Intro" { scenariospecific } } extend resource { date Birthday "Birthday" number Count "Count" reference URL "URL" richtext Claim "Claim" text Intro "Intro" date BirthdayS "Birthday" { scenariospecific inherit } number CountS "Count" { scenariospecific } reference URLS "URL" { scenariospecific } richtext ClaimS "Claim" { scenariospecific inherit } text IntroS "Intro" { scenariospecific } } scenario one "One" { scenario two "Two" } } resource "R" { Birthday 2000-05-01 Count 42 URL "http://www.taskjuggler.org" Claim "A '''big''' statement." Intro "Let's think about this..." two:BirthdayS 2000-05-01 two:CountS 42 two:URLS "http://www.taskjuggler.org" { label "TJ Web" } two:ClaimS "A '''big''' statement." two:IntroS "Let's think about this..." } task "T" { DueDate 2013-05-01 Count 42 URL "http://www.taskjuggler.org" Claim "A '''big''' statement." Intro "Let's think about this..." two:DueDateS 2013-05-01 two:CountS 42 two:URLS "http://www.taskjuggler.org" { label "TJ Web" } two:ClaimS "A '''big''' statement." two:IntroS "Let's think about this..." } TaskJuggler-3.8.1/test/TestSuite/Syntax/Correct/Depends1.tjp000066400000000000000000000005371473026623400237420ustar00rootroot00000000000000project "P" 2007-11-09 - 2007-12-24 { timezone "America/Denver" } task foo1 "foo1" { task foo2 "foo2" { start 2007-12-04 milestone } task foo3 "foo3" { depends !foo2 length 1d } } task bar "bar" { depends foo1.foo2 length 2d } task bar1 "bar1" { depends foo1 { gapduration 2d }, bar { gaplength 1d } duration 2d } TaskJuggler-3.8.1/test/TestSuite/Syntax/Correct/Durations.tjp000066400000000000000000000012471473026623400242460ustar00rootroot00000000000000project "Duration Example" 2007-06-06 - 2007-06-26 { timezone "America/Denver" } resource tux "Tux" task t "Enclosing" { start 2007-06-06 task durationTask "Duration Task" { # This task is 10 calendar days long. duration 10d } task intervalTask "Interval Task" { # This task is similar to the durationTask. Instead of a start # date and a duration it has a fixed start and end date. end 2007-06-17 } task lengthTask "Length Task" { # This task 10 working days long. So about 12 calendar days. length 10d } task effortTask "Effort Task" { # Tux will need 10 days to complete this task. effort 10d allocate tux } } TaskJuggler-3.8.1/test/TestSuite/Syntax/Correct/Efficiency.tjp000066400000000000000000000007551473026623400243450ustar00rootroot00000000000000project "Resource Efficiency Example" 2007-07-21 - 2007-07-22 { timezone "America/Denver" } # A team of 5 people. They can only be assigned en block. Either all # or nobody works. resource tuxies "Tuxies" { efficiency 5.0 } # A hard-working guy resource tux1 "Tux 1" { efficiency 1.2 } # And a lazy one resource tux2 "Tux 2" { efficiency 0.9 } # And a thing that cannot do any work resource confRoom "Conference Room" { efficiency 0 } task t "An important date" { start 2007-07-21 } TaskJuggler-3.8.1/test/TestSuite/Syntax/Correct/EnvVar.tjp000066400000000000000000000002531473026623400234730ustar00rootroot00000000000000project "Test" 2011-05-05 +6m task t "A $(TEST1) task" { note $(TEST2) duration 12$(TEST3)d } taskreport "EnvVar" { columns name, duration, note formats html } TaskJuggler-3.8.1/test/TestSuite/Syntax/Correct/Export.tjp000066400000000000000000000016251473026623400235570ustar00rootroot00000000000000# *** EXAMPLE: 1 + # *** EXAMPLE: 2 + project export "Project" "1.0" 2007-01-01 - 2008-01-01 { timezone "America/Denver" } resource tux "Tux" resource bob "Bob" # *** EXAMPLE: 1 - task t1 "Task 1" { start 2007-01-01 effort 20d allocate tux allocate bob limits { dailymax 6h } } # *** EXAMPLE: 1 + # *** EXAMPLE: 2 - task t2 "Task 2" { start 2007-01-01 end 2007-06-30 allocate tux allocate bob limits { weeklymax 3d } } # *** EXAMPLE: 1 - # *** EXAMPLE: 2 + # Export the project as fully scheduled project. export "FullProject" { definitions * taskattributes * hideresource 0 } # Export only bookings for 1st week as resource supplements export "Week1Bookings" { definitions - start 2007-01-01 end 2007-01-08 taskattributes booking hideresource 0 } # Export the scheduled project as Microsoft Project XML format. export "MS-Project" { formats mspxml loadunit quarters } TaskJuggler-3.8.1/test/TestSuite/Syntax/Correct/Flags.tjp000066400000000000000000000012161473026623400233260ustar00rootroot00000000000000project prj "Flags Example" "1.0" 2005-07-21 - 2005-08-26 { timezone "America/Denver" } # Declare the flag to mark important tasks flags important task items "Project breakdown" { start 2005-07-22 task plan "Plan work" { length 3d flags important } task implementation "Implement work" { length 5d depends !plan } task acceptance "Customer acceptance" { duration 5d depends !implementation flags important } } taskreport tasks "My Tasks" { formats html # Show only the important tasks hidetask ~important # Turn treemode off so parent tasks are not automatically included. sorttasks name.up } TaskJuggler-3.8.1/test/TestSuite/Syntax/Correct/Freeze.tjp000066400000000000000000000011051473026623400235070ustar00rootroot00000000000000project "Project" 2000-01-01-0:00 - 2000-03-01-0:00 { timezone "Europe/Berlin" now 2000-01-08 trackingscenario plan } resource r "Resource" task t "Task" { start ${projectstart} effort 10d allocate r } # Include the data from previous scheduling run. # We assume that the exported data has been frozen. # By importing it, we make sure they don't get changed any more. include "CompletedWork.tji" # Export only bookings for 1st week as resource supplements export "CompletedWork.tji" { start 2000-01-01 end 2000-01-08 taskattributes booking hideresource 0 } TaskJuggler-3.8.1/test/TestSuite/Syntax/Correct/Gap.tjp000066400000000000000000000004701473026623400230020ustar00rootroot00000000000000project prj "Example Project" "1.0" 2005-05-29 - 2005-07-01 { timezone "America/Denver" } task t1 "Task 1" { start 2005-05-29 } task t2 "Task 2" { # starts 5 calendar days after t1 depends !t1 { gapduration 5d } } task t3 "Task 3" { # starts 5 working days after t1 depends !t1 { gaplength 5d } } TaskJuggler-3.8.1/test/TestSuite/Syntax/Correct/HtmlTaskReport.tjp000066400000000000000000000012241473026623400252140ustar00rootroot00000000000000project "Simple Project" 2005-06-06 - 2005-06-26 { timezone "America/Denver" } copyright "Bucks Beavis Inc." resource tux "Tux" task items "Project breakdown" { start 2005-06-06 task plan "Plan work" { length 3d } task implementation "Implement work" { effort 5d allocate tux depends !plan } task acceptance "Customer acceptance" { duration 5d depends !implementation } } taskreport breakdown "ProjectBreakdown.html" { formats html caption "This is the project breakdown" headline "Project Breakdown" columns name, start, end, daily # Don't hide any resource, meaning show them all. hideresource 0 } TaskJuggler-3.8.1/test/TestSuite/Syntax/Correct/Include.tjp000066400000000000000000000002021473026623400236470ustar00rootroot00000000000000project "Include Test" 2010-02-26 +1w { timezone "America/Denver" } include "include/dir3/all.tji" include "include/file1.tji" TaskJuggler-3.8.1/test/TestSuite/Syntax/Correct/Journal.tjp000066400000000000000000000010101473026623400236740ustar00rootroot00000000000000project "Project with Journal" 2009-05-04 +1m { timezone "America/Denver" journalentry 2009-05-05 "The project started." journalentry 2009-05-10 "We made some progress." { summary "All jobs have been assigned. The crew is working away." details "Let's hope, we can keep the good spirit." } } resource tux "Tux" { journalentry 2009-05-07 "This guy is a bummer." } task t1 "Task1" { journalentry 2009-05-08 "Probably will be done sooner." journalentry 2009-05-12 "Maybe not." start 2009-05-05 } TaskJuggler-3.8.1/test/TestSuite/Syntax/Correct/Leave.tjp000066400000000000000000000014031473026623400233240ustar00rootroot00000000000000project "Annual Leave" 2011-12-19 +1y { now 2012-07-01 } leaves holiday "Christmas" 2011-12-24 +3d, holiday "New Year" 2011-12-31 +3d shift s1 "Shift 1" { leaves annual 2011-12-19 +3w, special 2012-01-12 +1d } resource team "Team" { leaveallowances annual 2011-12-19 20d leaves holiday 2012-01-06 resource r1 "R1" { leaves annual 2011-12-19 +3w, special 2012-01-12 +1d } resource r2 "R2" { leaveallowances annual 2012-06-01 -10d leaves sick 2012-01-04 +2d, unpaid 2012-01-10 +3d } } resource r3 "R3" { shifts s1 2011-12-19 +3w leaves sick 2012-01-04 +2d } task "foo" resourcereport "." { formats html columns name, annualleave, annualleavebalance, sickleave, specialleave, unpaidleave } TaskJuggler-3.8.1/test/TestSuite/Syntax/Correct/Limits-1.tjp000066400000000000000000000033341473026623400236740ustar00rootroot00000000000000project limits "Limits" "1.0" 2007-03-01 +1y { timezone "Europe/Amsterdam" } # Default limit that affects all subsequently defined resources limits { weeklymax 4d } # *** EXAMPLE: 1 + # *** EXAMPLE: 2 + # *** EXAMPLE: 3 + # *** EXAMPLE: 4 + # *** EXAMPLE: 6 + resource r1 "R1" { # Limit the usage of this resource to a maximum of 2 hours per day, # 6 hours per week and 2.5 days per month. limits { dailymax 2h weeklymax 6h monthlymax 2.5d } } # *** EXAMPLE: 6 - resource r2 "R2" # *** EXAMPLE: 2 - # *** EXAMPLE: 3 - # *** EXAMPLE: 4 - task t1 "Task 1" { start 2007-03-30 effort 10d allocate r2 limits { dailymax 3h } } # *** EXAMPLE: 4 + task t2 "Task 2" { start 2007-03-30 effort 10d allocate r2 limits { dailymin 5h } } # *** EXAMPLE: 1 - # *** EXAMPLE: 2 + # *** EXAMPLE: 4 - task t3 "Task 3" { start 2007-03-30 effort 10d allocate r2 limits { weeklymax 2d } } # *** EXAMPLE: 2 - # *** EXAMPLE: 3 + task t4 "Task 4" { start 2007-03-30 effort 10d allocate r2 limits { monthlymax 1w } } # *** EXAMPLE: 1 + # *** EXAMPLE: 2 + # *** EXAMPLE: 4 + task t5 "Task 5" { start ${projectstart} duration 60d # allocation is subject to resource limits allocate r1 } # *** EXAMPLE: 4 - task t6 "Task 6" { start ${projectstart} duration 60d allocate r2 limits { dailymax 4h weeklymax 3d monthlymax 2w } } # *** EXAMPLE: 3 - # *** EXAMPLE: 5 + # *** EXAMPLE: 4 + task t7 "Task 7" { start 2007-06-20 duration 20d allocate r1, r2 # limits can also be specified per resource limits { # Limit r1 to half days only dailymax 4h { resources r1 } # Limit r2 to 6 hours per day dailymax 6h { resources r2 } } } # *** EXAMPLE: 1 - # *** EXAMPLE: 2 - # *** EXAMPLE: 4 - # *** EXAMPLE: 5 - TaskJuggler-3.8.1/test/TestSuite/Syntax/Correct/ListAttributes.tjp000066400000000000000000000012361473026623400252560ustar00rootroot00000000000000# *** EXAMPLE: scenario + project "List attributes" 2014-04-06 +1m { scenario s1 "S1" { scenario s2 "S2" } } # *** EXAMPLE: scenario - flags f1, f2, f3 # *** EXAMPLE: define + task "T1" { flags f1 flags f2, f3 } # *** EXAMPLE: define - # *** EXAMPLE: inherit + task "T2" { flags f1 task "T3" { flags f2 } } # *** EXAMPLE: inherit - # *** EXAMPLE: scenario + task "T4" { s1:flags f1 s2:flags f2 } # *** EXAMPLE: scenario - # *** EXAMPLE: purge + task "T5" { flags f1 task "T6" { purge flags flags f2 } } # *** EXAMPLE: purge - taskreport "ListAttributes" { formats html columns name, scenario, flags scenarios s1, s2 } TaskJuggler-3.8.1/test/TestSuite/Syntax/Correct/LoadUnits.tjp000066400000000000000000000007571473026623400242050ustar00rootroot00000000000000project simple "Simple Project" "$Id" 2000-01-01 - 2001-02-01 { timezone "America/Denver" yearlyworkingdays 252 } resource tux1 "Tux1" resource tux2 "Tux2" resource tux3 "Tux3" task t1 "Task1" { start 2000-01-01 effort 20d allocate tux1 } task t2 "Task2" { start 2000-01-01 length 1y allocate tux2 } task t3 "Task3" { start 2000-01-01 effort 2d allocate tux3 } taskreport loadunits "LoadUnits" { formats html columns no, name, effort, monthly loadunit months } TaskJuggler-3.8.1/test/TestSuite/Syntax/Correct/LogicalExpression.tjp000066400000000000000000000010731473026623400257250ustar00rootroot00000000000000project "LogExp" 2009-10-19 +2m { timezone "America/Denver" } resource r "R" task "T" { allocate r effort 1d } macro rep [ taskreport "LogExp${1}" { hidetask 1 ${2} 0 } ] ${rep "1" ">"} ${rep "2" "<"} ${rep "3" "<="} ${rep "4" ">="} ${rep "5" "!="} ${rep "6" "="} taskreport "LogExp7" { hidetask @none hideresource @all } # *** EXAMPLE: 1 + taskreport "LeaveTasks" { hidetask isleaf() sorttasks id.up # not 'tree' to really hide parent tasks } taskreport "Overruns" { hidetask isvalid(plan.maxend) & (plan.end > plan.maxend) } # *** EXAMPLE: 1 - TaskJuggler-3.8.1/test/TestSuite/Syntax/Correct/LogicalFunction.tjp000066400000000000000000000004751473026623400253600ustar00rootroot00000000000000project "Logical Function Demo" 2009-11-21 +2w { timezone "America/Denver" } resource "Team" { resource joe "Joe" } task "Parent" { task "Sub" { effort 1w allocate joe } } # *** EXAMPLE: 1 + taskreport "LogicalFunction" { formats html hideresource ~(isleaf() & isleaf_()) } # *** EXAMPLE: 1 - TaskJuggler-3.8.1/test/TestSuite/Syntax/Correct/Macro-1.tjp000066400000000000000000000004461473026623400234750ustar00rootroot00000000000000project "Example Project" 2008-01-18 +2m { timezone "America/Denver" } macro allocateGroup [ allocate tux1, tux2 ] resource tux1 "Tux1" resource tux2 "Tux2" task t1 "Task1" { start ${projectstart} # built-in macro } task t2 "Task2" { depends !t1 effort 20d ${allocateGroup} } TaskJuggler-3.8.1/test/TestSuite/Syntax/Correct/Macro-2.tjp000066400000000000000000000003071473026623400234720ustar00rootroot00000000000000project "Test" 2010-04-28 +1w { timezone "America/Denver" } macro task [ task "${0}" ] macro tname [ Prepare ] macro meal [ Crème brûlée ] task "${meal} ${tname}" ${task "${tname} ${meal} "} TaskJuggler-3.8.1/test/TestSuite/Syntax/Correct/Macro-3.tjp000066400000000000000000000005211473026623400234710ustar00rootroot00000000000000macro major [6] macro minor [0] macro maint [0] macro content [foo] macro content_title [Project] macro start_date [2009-12-01] macro end_date [2012-09-30] project ${content}${major}${minor}${maint} "${content_title}" "${major}" 2009-12-01 - 2012-09-30 { timezone "America/Denver" } task "foo" TaskJuggler-3.8.1/test/TestSuite/Syntax/Correct/Macro-4.tjp000066400000000000000000000000651473026623400234750ustar00rootroot00000000000000project "Test" 2012-08-17 +1m task t ${?undef} "T" TaskJuggler-3.8.1/test/TestSuite/Syntax/Correct/Manager.tjp000066400000000000000000000010271473026623400236440ustar00rootroot00000000000000project "test" 2010-04-03 +1w { timezone "America/Denver" } resource "The Company" { resource ceo "Big Boss" managers ceo resource "R&D Team" { resource vpe "VP Engineering" purge managers managers vpe resource "The Hacker" resource "Doc Writer" } resource "F&A Team" { resource coo "Chief Operating Officer" purge managers managers coo resource "HR Lady" resource "Accountant" } } task "T" resourcereport "Managers" { formats html columns name, directreports, reports } TaskJuggler-3.8.1/test/TestSuite/Syntax/Correct/Mandatory.tjp000066400000000000000000000006551473026623400242360ustar00rootroot00000000000000project prj "Project" "1.0" 2000-01-01 - 2000-03-01 { timingresolution 15min timezone "America/Denver" } resource tuxus "Tuxus" resource truck "Truck" { # Truck does not do any work! efficiency 0.0 } task t "Ship stones to customers" { start 2000-01-01 effort 5d # We need the truck to deliver the stones, so only allocate # tuxus when the truck is available. allocate tuxus allocate truck { mandatory } } TaskJuggler-3.8.1/test/TestSuite/Syntax/Correct/Milestone.tjp000066400000000000000000000005241473026623400242320ustar00rootroot00000000000000project prj "Milestone demo" "1.0" 2005-07-15 - 2005-08-01 { timezone "America/Denver" } task project_start "Project Start" { start 2005-07-15 milestone } task deadline "Important Deadline" { start 2005-07-20 # A task with only a start or end date and no duration specification # is automatically assumed to be a milestone. } TaskJuggler-3.8.1/test/TestSuite/Syntax/Correct/MinMax.tjp000066400000000000000000000006331473026623400234650ustar00rootroot00000000000000project "Min Max Example" 2005-06-06 - 2005-06-26 { timezone "America/Denver" } task items "Project breakdown" { start 2005-06-07 task plan "Plan work" { note "Some more information about this task." # Set acceptable interval for task start minstart 2005-06-06 maxstart 2005-06-08 length 3d # Set acceptable interval for task end minend 2005-06-09 maxend 2005-06-11 } } TaskJuggler-3.8.1/test/TestSuite/Syntax/Correct/Niku.tjp000066400000000000000000000030361473026623400232020ustar00rootroot00000000000000project "Niku Test" 2010-02-01 +3m { timezone "America/Denver" extend task { text ClarityPID "Clarity PID" text ClarityPName "Clarity Project Name" } extend resource { text ClarityRID "Clarity Resource ID" } } # The ClarityPID and ClarityPName must be always kept in sync. The # easiest way to achieve this, is by using such macros. macro PID_p1 [ ClarityPID "p1" ClarityPName "Project 1" ] macro PID_p2 [ ClarityPID "p2" ClarityPName "Project 2" ] macro PID_p3 [ ClarityPID "p3" ClarityPName "Project 3" ] macro Resource [ resource ${1} "${1}" { ClarityRID "${1}" } ] ${Resource "r1"} ${Resource "r2"} ${Resource "r3"} supplement resource r2 { vacation 2010-02-15 +1w } task "T1" { allocate r1 effort 5w ${PID_p1} } task t2 "T2" { allocate r2 effort 10d ${PID_p1} } task "T3" { depends !t2 allocate r2 effort 10d ${PID_p2} } task "T4" { allocate r3 effort 3w ${PID_p2} } nikureport "niku" { formats niku, html, csv headline "This is a test report" period 2010-02-01-8:00 - %{2010-03-01 -6h} timeoff "vacations" "Vacation time" # Depending on your Clarity configuration, you may need to add this # CustomInformation section in the report. It's raw XML code and # will be embedded into each section of the resulting # report. This is just an example and it must be customized to work! title -8<- foo_active foo_eng ->8- } TaskJuggler-3.8.1/test/TestSuite/Syntax/Correct/Numberformat.tjp000066400000000000000000000004631473026623400247360ustar00rootroot00000000000000project prj "Project" "1.0" 2000-01-01 - 2000-03-01 { timezone "Europe/Berlin" # German number format: e. g. -10000,20 5014,11 numberformat "-" "" "" "," 2 # US currency format: e. g. (10,000.20) 5,014.11 currencyformat "(" ")" "," "." 2 } task t "Task" { start ${projectstart} milestone } TaskJuggler-3.8.1/test/TestSuite/Syntax/Correct/Period.tjp000066400000000000000000000004331473026623400235140ustar00rootroot00000000000000project prj "Period Project" "1.0" 2006-09-24 +3m { timezone "America/Denver" now 2006-10-02 } task items "Project breakdown" { start ${projectstart} task plan "Plan work" { period 2006-10-01 +2w } } taskreport tasks "My Tasks" { formats html period ${now} +1w } TaskJuggler-3.8.1/test/TestSuite/Syntax/Correct/Persistent.tjp000066400000000000000000000004221473026623400244300ustar00rootroot00000000000000project "Project" 2003-06-05 - 2003-07-05 { timezone "America/Denver" } resource r1 "Resource 1" resource r2 "Resource 2" task t1 "Task 1" { start 2003-06-05 effort 5d # Pick one of them and use it for the entire task allocate r1 { alternative r2 persistent } } TaskJuggler-3.8.1/test/TestSuite/Syntax/Correct/Precedes1.tjp000066400000000000000000000004051473026623400241040ustar00rootroot00000000000000project "P" 2003-11-09 - 2003-12-24 { timezone "America/Denver" } task foo1 "foo1" { task foo2 "foo2" { start 2003-12-04 milestone } task foo3 "foo3" { precedes !foo2 length 1d } } task bar "bar" { precedes foo1.foo2 length 2d } TaskJuggler-3.8.1/test/TestSuite/Syntax/Correct/Priority.tjp000066400000000000000000000016201473026623400241120ustar00rootroot00000000000000project "Priority Demo" 2011-04-17-0:00--0700 +2m { timezone "America/Denver" } resource tux "Tux" # *** EXAMPLE: project + task jobs "Project breakdown" { start ${projectstart} task work "The regular work" { effort 20d priority 500 allocate tux limits { weeklymax 25h } } task support "Customer Support" { # This is a high priority task. Due to the high priority tux is # spending the required daily maximum on it. end ${projectend} priority 800 allocate tux limits { dailymax 2h } } task conference "Attend Conference" { period 2011-04-25 +2d allocate tux priority 1000 } task maintenance "Maintenance work" { # This is a fallback task. Whenever tux is not doing something # else he is allocated to this task. end ${projectend} priority 300 allocate tux limits { weeklymax 2d } } } # *** EXAMPLE: project - TaskJuggler-3.8.1/test/TestSuite/Syntax/Correct/Project.tjp000066400000000000000000000011471473026623400237030ustar00rootroot00000000000000project prj "Example Project" "1.0" 2007-01-01 - 2007-03-09 { # The following attributes are all optional. They illustrate the # default values. These attributes are only needed if you want to # specify different values than those listed below. timingresolution 60min timezone "America/Denver" dailyworkinghours 8 yearlyworkingdays 260.714 timeformat "%Y-%m-%d %H:%M" shorttimeformat "%H:%M" currencyformat "(" ")" "," "." 0 weekstartsmonday workinghours mon - fri 9:00 - 12:00, 13:00 - 18:00 workinghours sat, sun off scenario plan "Plan" { } } task t "Task" { start 2007-01-01 } TaskJuggler-3.8.1/test/TestSuite/Syntax/Correct/ProjectIDs.tjp000066400000000000000000000005551473026623400243050ustar00rootroot00000000000000project "ProjectIDs example" 2006-08-22 +1m { timezone "America/Denver" } task t1 "Task 1" { start 2006-08-22 # This task has project ID "mainID" } projectid prj1 projectids prj2 task t2 "Task 2" { start 2006-08-22 # This task has now project ID "prj1" } task t3 "Task 3" { start 2006-08-22 projectid prj2 # This task has now project ID "prj2" } TaskJuggler-3.8.1/test/TestSuite/Syntax/Correct/Query.tjp000066400000000000000000000005371473026623400234040ustar00rootroot00000000000000project "Query Demo" 2009-11-22 +1m { timezone "America/Denver" now 2009-12-06 } resource joe "Joe" task "Job" { effort 2w allocate joe } taskreport "QueryDemo" { formats html header "Project data as of <-query attribute='now'->" columns name { celltext 1 "<-query-> : <-query attribute='id'->" }, effortdone, effortleft } TaskJuggler-3.8.1/test/TestSuite/Syntax/Correct/Reports.tjp000066400000000000000000000016501473026623400237320ustar00rootroot00000000000000project "Test Project" 2000-01-01 - 2000-03-01 { timezone "America/Denver" } flags flag1, flag2, flag3, flag4 rate 100.0 resource r1 "FooResource 1" resource r2 "FooResource 2" resource r3 "FooResource 3" resource r4 "FooResource 4" account a1 "FooAccount 1" { account a3 "FooAccount 3" } account a2 "FooAccount 2" { account a4 "FooAccount 4" } task t1 "FooTask1" { chargeset a4 task t1_1 "FooTask1_1" { flags flag2 start 2000-01-01 effort 20d allocate r1 allocate r2 } flags flag3 } task t2 "FooTask2" { flags flag1 start 2000-01-01 duration 1d chargeset a4 charge 10000.0 onstart } task t3 "FooTask3" { flags flag4 milestone start 2000-01-01 } taskreport task "Report_task" { formats html balance a1 a2 columns bsi, name { title "Task Name" }, daily, effort sorttasks tree, plan.start.up, name.up } resourcereport resource "Report_resource.html" { formats html } TaskJuggler-3.8.1/test/TestSuite/Syntax/Correct/Resource.tjp000066400000000000000000000005741473026623400240670ustar00rootroot00000000000000project "Resource Examples" 2005-06-06 - 2005-06-26 { timezone "America/Denver" } # A simple resource resource tux1 "Tux1" # A team resource team "A team" { # A 2 days of team vacation vacation 2005-06-07 - 2006-05-09 resource tux2 "Tux2" resource tux3 "Tux3" { # And one extra day vacation 2005-06-10 } } task t "An important date" { start 2005-06-10 } TaskJuggler-3.8.1/test/TestSuite/Syntax/Correct/ResourcePrefix.tji000066400000000000000000000000241473026623400252240ustar00rootroot00000000000000resource bar "Bar" TaskJuggler-3.8.1/test/TestSuite/Syntax/Correct/ResourcePrefix.tjp000066400000000000000000000005761473026623400252470ustar00rootroot00000000000000project "Resource Prefix Example" 2009-09-13 +1m { timezone "America/Denver" } resource team "Team" { resource foo "Foo" } include "ResourcePrefix.tji" { resourceprefix team } supplement resource bar { workinghours mon-fri 8:00-15:00 workinghours sat, sun off } task t "Task" { effort 10d allocate foo, bar } resourcereport rp "ResourcePrefix" { formats html } TaskJuggler-3.8.1/test/TestSuite/Syntax/Correct/ResourceRoot.tjp000066400000000000000000000006031473026623400247240ustar00rootroot00000000000000project "Test" "1.0" 2010-11-10 +2m { timezone "America/Denver" } resource org "Org" { resource team1 "Team1" { resource r1 "R1" resource r2 "R2" resource r3 "R3" } resource r4 "R4" } resource r5 "R5" task "T" resourcereport "ResourceRoot" { formats html columns name, id # Only list the Team1 as if it would be a top-level resource. resourceroot team1 } TaskJuggler-3.8.1/test/TestSuite/Syntax/Correct/Responsible.tjp000066400000000000000000000004461473026623400245630ustar00rootroot00000000000000project "Responsible Demo" 2005-07-15 - 2005-08-01 { timezone "America/Denver" } resource tux "Tux" resource ubertux "Uber Tux" task someJob "Some Job" { start 2005-07-15 effort 1w allocate tux responsible ubertux } taskreport joblist "Job List" { columns effort, responsible } TaskJuggler-3.8.1/test/TestSuite/Syntax/Correct/RollupResource.tjp000066400000000000000000000005671473026623400252670ustar00rootroot00000000000000project "Test" "1.0" 2010-11-10 +2m { timezone "America/Denver" } resource org "Org" { resource team1 "Team1" { resource r1 "R1" resource r2 "R2" resource r3 "R3" } resource r4 "R4" } resource r5 "R5" task "T" resourcereport "RollupResource" { formats html columns name, id # Don't list the Team1 resources. rollupresource plan.id = 'team1' } TaskJuggler-3.8.1/test/TestSuite/Syntax/Correct/Scenario.tjp000066400000000000000000000006071473026623400240400ustar00rootroot00000000000000# *** EXAMPLE: header + project "Example" 2007-05-29 - 2007-07-01 { timezone "America/Denver" scenario plan "Planned Scenario" { scenario actual "Actual Scenario" scenario test "Test Scenario" { active no } } } # *** EXAMPLE: header - # *** EXAMPLE: task + task t "Task" { start 2007-05-29 actual:start 2007-06-03 test:start 2007-06-07 } # *** EXAMPLE: task - TaskJuggler-3.8.1/test/TestSuite/Syntax/Correct/Scheduling.tjp000066400000000000000000000010151473026623400243540ustar00rootroot00000000000000project "Scheduling Example" 2005-07-23 - 2005-09-01 { timezone "America/Denver" } task items "Project breakdown" { task t1 "Task 1" { start 2005-07-25 end 2005-08-01 # Implicite ALAP task } task t2 "Task 2" { end 2005-08-01 start 2005-07-25 # Implicite ASAP task } task t3 "Task 3" { start 2005-07-25 end 2005-08-01 scheduling asap # Explicite ASAP task } task t4 "Task 4" { end 2005-08-01 start 2005-07-25 scheduling alap # Explicite ALAP task } } TaskJuggler-3.8.1/test/TestSuite/Syntax/Correct/Select.tjp000066400000000000000000000012621473026623400235120ustar00rootroot00000000000000project "Project" 2000-01-01 - 2000-03-01 { timezone "America/Denver" } resource tuxus "Tuxus" resource tuxia "Tuxia" task t1 "Task 1" { start 2000-01-01 effort 5d # First try to allocate Tuxus. When he is not available try Tuxia. allocate tuxus { alternative tuxia select order } } task t2 "Task 2" { start 2000-01-01 effort 5d # Use tuxux or tuxia, whoever is available and try to balance # the allocated load. allocate tuxus { alternative tuxia select minloaded} } task t3 "Task 3" { start 2000-01-01 effort 5d # For slave drivers: Always pick the resource that has been loaded # the most already. allocate tuxus { alternative tuxia select maxloaded} } TaskJuggler-3.8.1/test/TestSuite/Syntax/Correct/Shift.tjp000066400000000000000000000032371473026623400233540ustar00rootroot00000000000000project "Example" 2000-01-01 - 2000-03-31 { timezone "America/Denver" } shift s1 "Shift1" { # Special working hours Monday to Wednesday. Use program defaults # for other days. workinghours mon 10:00 - 12:00, 13:00 - 15:00 workinghours tue 9:00 - 14:00 workinghours wed off shift s2 "Shift2" { # Like s1 but with different times on Monday workinghours mon 10:00 - 17:00 } } resource r1 "Resource1" { shifts s1 2000-01-01 - 2000-01-10, s2 2000-01-11 - 2000-01-20 } shift s3 "Part-time schedule 1" { # The resource works on mondays, wednesdays and fridays. # The days that the resource doesn't work must be mentioned # explicitely otherwise the defaults values are used (usually # full-time employment). workinghours mon,fri 9:00 - 12:00, 13:00 - 18:00 workinghours wed 9:00 - 12:00 workinghours tue, thu off } shift s4 "Part-time schedule 2" { # The resource changes his schedule to work on tuesday and # thursdays. The days that the resource doesn't work must be # mentioned explicitely, otherwise the default values are used # (usually full-time employment). workinghours tue, thu 9:00 - 12:00, 13:00 - 18:00 workinghours mon, wed, fri off } shift s5 "All-day, all-week shift" { workinghours mon-sun 0:00 - 24:00 } # Now determine when these schedules are applicable resource r2 "Resource2" { # r2 works three days a week from January to June shifts s3 2005-01-01 - 2005-01-15, # r2 switches to two days a week s4 2005-01-15 - 2006-01-01 } task t1 "Task1" { start 2000-01-01 length 200h # During the specified interval only work at the shift s2 working # hours. shifts s2 2000-01-09 - 2000-01-17 } TaskJuggler-3.8.1/test/TestSuite/Syntax/Correct/Simple.tjp000066400000000000000000000006411473026623400235240ustar00rootroot00000000000000project "Simple Project" 2005-06-06 - 2005-06-26 { timezone "America/Denver" } resource tux "Tux" task items "Project breakdown" { start 2005-06-06 task plan "Plan work" { length 3d } task implementation "Implement work" { effort 5d allocate tux depends !plan } task acceptance "Customer acceptance" { duration 5d depends !implementation } } taskreport tasks "My Tasks" TaskJuggler-3.8.1/test/TestSuite/Syntax/Correct/StatusSheet.tjp000066400000000000000000000015621473026623400245520ustar00rootroot00000000000000project "test" 2009-11-30 +2m { timezone "America/Denver" trackingscenario plan now ${projectstart} } resource r1 "R1" resource r2 "R2" resource r3 "R3" task t1 "Task 1" { effort 5d allocate r1 } task t2 "Task 2" { task t3 "Task 3" { effort 10d allocate r2 } } statussheet r3 2009-12-04 { task t1 { status green "All work done" { author r1 summary "I had good fun!" details -8<- This task went smoothly and I got three things done: * Have fun * Be on time * Get things done ->8- } } task t2 { task t3 { status red "I need more time" { author r2 summary "This takes longer than expected" details -8<- To finish on time, I need help. Get this r1 guy to help me out here. * I want to have fun too! ->8- } } } } TaskJuggler-3.8.1/test/TestSuite/Syntax/Correct/String.tjp000066400000000000000000000003621473026623400235410ustar00rootroot00000000000000project "String Tests" 2005-06-06 - 2005-06-26 { timezone "America/Denver" } resource tux "Tux \"The Penguing\" Tuxus" task items "Project Plan\\\\Breakdown" { start 2005-06-06 effort 2d allocate tux } taskreport tasks "My Tasks" TaskJuggler-3.8.1/test/TestSuite/Syntax/Correct/Supplement.tjp000066400000000000000000000010451473026623400244260ustar00rootroot00000000000000project "Test Project" 2000-01-01 - 2000-01-04 { timezone "America/Denver" } flags important # *** EXAMPLE: resource + resource joe "Joe" # *** EXAMPLE: resource - # *** EXAMPLE: task + task top "Top Task" { task sub "Sub Task" supplement task sub { length 1d } } # *** EXAMPLE: task - # *** EXAMPLE: resource + supplement resource joe { vacation 2000-02-10 - 2000-02-20 } # *** EXAMPLE: resource - # *** EXAMPLE: task + supplement task top { flags important supplement task sub { allocate joe } } # *** EXAMPLE: task - TaskJuggler-3.8.1/test/TestSuite/Syntax/Correct/TaskPrefix.tji000066400000000000000000000000541473026623400243420ustar00rootroot00000000000000task sub_task1 "Sub task 1" { effort 1d } TaskJuggler-3.8.1/test/TestSuite/Syntax/Correct/TaskPrefix.tjp000066400000000000000000000007611473026623400243560ustar00rootroot00000000000000project project "Include task prefix test" "1.0" 2010-12-01 +1m { timezone "Europe/Amsterdam" now 2010-12-01 } resource tux "Tux" task parent_task "Parent task" { start ${projectstart} allocate tux } include "TaskPrefix.tji" { taskprefix parent_task } task other_task "Other task" { start ${projectstart} effort 1d allocate tux } supplement resource tux { booking parent_task.sub_task1 2010-12-01-9:00 +9h { sloppy 2 } booking other_task 2010-12-02-9:00 +9h { sloppy 2 } } TaskJuggler-3.8.1/test/TestSuite/Syntax/Correct/TaskRoot.tjp000066400000000000000000000011501473026623400240350ustar00rootroot00000000000000project "Taskroot Example" 2005-07-22 - 2005-08-26 { timezone "America/Denver" } task items "Project breakdown" { start 2005-07-22 task plan "Plan work" { length 3d } task implementation "Implement work" { task phase1 "Phase 1" { length 5d depends !!plan } task phase2 "Phase 2" { length 3d depends !phase1 } task phase3 "Phase 3" { length 4d depends !phase2 } } task acceptance "Customer acceptance" { duration 5d depends !implementation } } taskreport tasks "My Tasks" { formats html taskroot items.implementation } TaskJuggler-3.8.1/test/TestSuite/Syntax/Correct/TimeFrame.tjp000066400000000000000000000004441473026623400241450ustar00rootroot00000000000000project "Simple Project" 2000-01-01-12:00 - 2000-01-04-18:00 { timezone "America/Denver" } resource tux "Tux" task t1 "Task1" { start 2000-01-01-12:00 length 1d } task t2 "Task2" { start 2000-01-02 duration 1d } task t3 "Task3" { start 2000-01-02 effort 1d allocate tux } TaskJuggler-3.8.1/test/TestSuite/Syntax/Correct/TimeSheet1.tjp000066400000000000000000000031361473026623400242450ustar00rootroot00000000000000# *** EXAMPLE: 2 + # *** EXAMPLE: 1 + project "test" 2009-11-30 +2m { timezone "America/Denver" trackingscenario plan now ${projectstart} } # *** EXAMPLE: 1 - resource r1 "R1" # *** EXAMPLE: 2 - resource r2 "R2" # *** EXAMPLE: 6 + task t1 "Task 1" { effort 5d allocate r1 } # *** EXAMPLE: 6 - # *** EXAMPLE: 5 + task t2 "Task 2" { task t3 "Task 3" { duration 10d allocate r2 } } # *** EXAMPLE: 6 + # *** EXAMPLE: 5 - # *** EXAMPLE: 1 + # *** EXAMPLE: 3 + # *** EXAMPLE: 4 + timesheet r1 2009-11-30 +1w { # *** EXAMPLE: 3 - task t1 { work 3d remaining 0d status green "All work done" { summary "I had good fun!" details -8<- This task went smoothly and I got three things done: * Have fun * Be on time * Get things done ->8- } } # *** EXAMPLE: 6 - # *** EXAMPLE: 4 - # *** EXAMPLE: 3 + newtask t4 "Another fun job" { work 2d remaining 4d status yellow "Had a cool idea" { summary "Will have a schedule impact though." } } # *** EXAMPLE: 4 + # *** EXAMPLE: 6 + } # *** EXAMPLE: 6 - # *** EXAMPLE: 4 - # *** EXAMPLE: 3 - # *** EXAMPLE: 5 + timesheet r2 2009-11-30 +1w { task t2.t3 { work 5d end 2009-12-10 status red "I need more time" { summary "This takes longer than expected" details -8<- To finish on time, I need help. Get this r1 guy to help me out here. * I want to have fun too! ->8- } } status yellow "My wife got ill" { summary "I might have to work from home for a few days next week." } } # *** EXAMPLE: 5 - # *** EXAMPLE: 1 - TaskJuggler-3.8.1/test/TestSuite/Syntax/Correct/Timezone.tjp000066400000000000000000000002351473026623400240640ustar00rootroot00000000000000project tz "Timezone" "1.0" 2005-06-06-00:00-+0000 - 2005-06-07-0:00-+0000 { timezone "Europe/Athens" } task item "Project" { start 2005-06-06-12:00 } TaskJuggler-3.8.1/test/TestSuite/Syntax/Correct/Timezone2.tjp000066400000000000000000000002401473026623400241420ustar00rootroot00000000000000project tz "Timezone" "1.0" 2005-06-06-00:00-+1300 - 2005-06-07-0:00-+1300 { timezone "Pacific/Auckland" } task item "Project" { start 2005-06-06-12:00 } TaskJuggler-3.8.1/test/TestSuite/Syntax/Correct/TraceReport.tjp000066400000000000000000000002331473026623400245220ustar00rootroot00000000000000project "Trace Reports" 2012-01-14 +2m task "Foo" task "Bar" task "FooBar" tracereport "TraceReport" { columns end { title "<-name->" } hidetask 0 } TaskJuggler-3.8.1/test/TestSuite/Syntax/Correct/Vacation.tjp000066400000000000000000000014371473026623400240430ustar00rootroot00000000000000project "Vacation Examples" 2005-07-22 - 2006-01-01 { timezone "America/Denver" } # Labor Day vacation "Labor Day" 2005-09-05 # 2 days Christmas break (27th not included!) vacation "Christmas" 2005-12-25 - 2005-12-27 resource team "A team" { # 2 days of team vacation vacation 2005-10-07 +2d resource tux2 "Tux2" resource tux3 "Tux3" { # And one extra day vacation 2005-08-10 } } # The vacation property is also usefull when new employees start # working in the course of a project or if someone quits. resource tuxia "Tuxia" { # Tuxia is a new employee as of August 1st 2005 vacation 1971-01-01 - 2005-08-01 } resource tuxus "Tuxus" { # Tuxus quits his job on September 1st 2005 vacation 2005-09-01 - 2030-01-01 } task t "An important date" { start 2005-07-22 } TaskJuggler-3.8.1/test/TestSuite/Syntax/Correct/include/000077500000000000000000000000001473026623400231765ustar00rootroot00000000000000TaskJuggler-3.8.1/test/TestSuite/Syntax/Correct/include/dir1/000077500000000000000000000000001473026623400240355ustar00rootroot00000000000000TaskJuggler-3.8.1/test/TestSuite/Syntax/Correct/include/dir1/file2.tji000066400000000000000000000001061473026623400255430ustar00rootroot00000000000000resource "R" include "../dir2/file3.tji" { } include "file5.tji" { } TaskJuggler-3.8.1/test/TestSuite/Syntax/Correct/include/dir1/file5.tji000066400000000000000000000000171473026623400255470ustar00rootroot00000000000000resource "R2" TaskJuggler-3.8.1/test/TestSuite/Syntax/Correct/include/dir2/000077500000000000000000000000001473026623400240365ustar00rootroot00000000000000TaskJuggler-3.8.1/test/TestSuite/Syntax/Correct/include/dir2/file3.tji000066400000000000000000000000141473026623400255430ustar00rootroot00000000000000task "Foo" TaskJuggler-3.8.1/test/TestSuite/Syntax/Correct/include/dir3/000077500000000000000000000000001473026623400240375ustar00rootroot00000000000000TaskJuggler-3.8.1/test/TestSuite/Syntax/Correct/include/dir3/all.tji000066400000000000000000000000611473026623400253140ustar00rootroot00000000000000include "file1.tji" { } include "file2.tji" { } TaskJuggler-3.8.1/test/TestSuite/Syntax/Correct/include/dir3/file1.tji000066400000000000000000000000231473026623400255420ustar00rootroot00000000000000resource "RF1" { } TaskJuggler-3.8.1/test/TestSuite/Syntax/Correct/include/dir3/file2.tji000066400000000000000000000000171473026623400255460ustar00rootroot00000000000000resource "RF2" TaskJuggler-3.8.1/test/TestSuite/Syntax/Correct/include/file1.tji000066400000000000000000000000361473026623400247050ustar00rootroot00000000000000include "dir1/file2.tji" { } TaskJuggler-3.8.1/test/TestSuite/Syntax/Correct/manual2example.rb000066400000000000000000000013321473026623400250120ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = manual2example.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009 by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # def removeTags(fileName, destDir) oFile = File.open("../../../../examples/#{destDir}/#{fileName}", 'w') File.open(fileName, 'r') do |iFile| while line = iFile.gets oFile.puts line unless line =~ /^# \*\*\* EXAMPLE:/ end end oFile.close end removeTags('tutorial.tjp', 'Tutorial') removeTags('template.tjp', 'ProjectTemplate') TaskJuggler-3.8.1/test/TestSuite/Syntax/Correct/navigator.tjp000066400000000000000000000006321473026623400242650ustar00rootroot00000000000000project "Navigator Example" 2011-12-12 +1m task foo "Foo" { task "Foo 1" task "Foo 2" } task bar "Bar" { task "Bar 1" task "Bar 2" } navigator navbar textreport frame "" { header -8<- == My Reports == <[navigator id="navbar"]> ->8- footer "----" taskreport "Foo Reports" { formats html taskroot foo } taskreport "Bar Reports" { formats html taskroot bar } } TaskJuggler-3.8.1/test/TestSuite/Syntax/Correct/template.tjp000066400000000000000000000220461473026623400241110ustar00rootroot00000000000000/* * This file contains a project skeletton. It is part of the * TaskJuggler project management tool. You can use this as a basis to * start you own project file. */ project your_project_id "Your Project Title" 2011-11-11-0:00--0500 +4m { # Set the default time zone for the project. If not specified, UTC # is used. timezone "America/New_York" # Hide the clock time. Only show the date. timeformat "%Y-%m-%d" # Use US format for numbers numberformat "-" "" "," "." 1 # Use US financial format for currency values. Don't show cents. currencyformat "(" ")" "," "." 0 # Pick a day during the project that will be reported as 'today' in # the project reports. If not specified, the current day will be # used, but this will likely be outside of the project range, so it # can't be seen in the reports. now 2011-12-24 # The currency for all money values is the Euro. currency "USD" # You can define multiple scenarios here if you need them. #scenario plan "Plan" { # scenario actual "Actual" #} # You can define your own attributes for tasks and resources. This # is handy to capture additonal information about the project that # is not directly impacting the project schedule but you like to # keep in one place. #extend task { # reference spec "Link to Wiki page" #} #extend resource { # text Phone "Phone" #} } copyright "Claim your rights here" # If you have any text block that you need multiple times to describe # your project, you should define a macro for it. Macros can even have # variable segments that you can set upon calling the macro. # # macro Task [ # task "A ${1} task" { # } # ] # # Can be called as # ${Task "big"} # to generate # task "A big task" { # } # You can attach flags to accounts, resources and tasks. These can be # used to filter out subsets of them during reporting. flags important, hidden # If you want to do budget planning for you project, you need to # define some accounts. account cost "Project Cost" { account dev "Development" account doc "Documentation" } account rev "Customer Payments" # The Profit&Loss analysis should be rev - cost accounts. balance cost rev # Define you public holidays here. vacation "New Year's Day" 2012-01-02 vacation "Birthday of Martin Luther King, Jr." 2012-01-16 vacation "Washington's Birthday" 2012-02-20 vacation "Memorial Day" 2012-05-28 vacation "Independence Day" 2012-07-04 vacation "Labor Day" 2012-09-03 vacation "Columbus Day" 2012-10-08 vacation "Veterans Day" 2012-11-12 vacation "Thanksgiving Day" 2012-11-22 vacation "Christmas Day" 2012-12-25 # The daily default rate of all resources. This can be overridden for each # resource. We specify this, so that we can do a good calculation of # the costs of the project. rate 400.0 # This is a set of example resources. resource r1 "Resource 1" resource t1 "Team 1" { managers r1 resource r2 "Resource 2" resource r3 "Resource 3" } # This is a resource that does not do any work. resource s1 "System 1" { efficiency 0.0 rate 600.0 } task project "Project" { task wp1 "Workpackage 1" { task t1 "Task 1" task t2 "Task 2" } task wp2 "Work package 2" { depends !wp1 task t1 "Task 1" task t2 "Task 2" } task deliveries "Deliveries" { task "Item 1" { depends !!wp1 } task "Item 2" { depends !!wp2 } } } # Now the project has been specified completely. Stopping here would # result in a valid TaskJuggler file that could be processed and # scheduled. But no reports would be generated to visualize the # results. navigator navbar { hidereport 0 } macro TaskTip [ tooltip istask() -8<- '''Start: ''' <-query attribute='start'-> '''End: ''' <-query attribute='end'-> ---- '''Resources:''' <-query attribute='resources'-> ---- '''Precursors: ''' <-query attribute='precursors'-> ---- '''Followers: ''' <-query attribute='followers'-> ->8- ] textreport frame "" { header -8<- == TaskJuggler Project Template == <[navigator id="navbar"]> ->8- footer "----" textreport index "Overview" { formats html center '<[report id="overview"]>' } textreport "Status" { formats html center -8<- <[report id="status.dashboard"]> ---- <[report id="status.completed"]> ---- <[report id="status.ongoing"]> ---- <[report id="status.future"]> ->8- } textreport wps "Work packages" { textreport wp1 "Work package 1" { formats html center '<[report id="wp1"]>' } textreport wp2 "Work package 2" { formats html center '<[report id="wp2"]>' } } textreport "Deliveries" { formats html center '<[report id="deliveries"]>' } textreport "ContactList" { formats html title "Contact List" center '<[report id="contactList"]>' } textreport "ResourceGraph" { formats html title "Resource Graph" center '<[report id="resourceGraph"]>' } } # A traditional Gantt chart with a project overview. taskreport overview "" { header -8<- === Project Overview === The project is structured into 2 work packages. # Specification # <-reportlink id='frame.wps.wp1'-> # <-reportlink id='frame.wps.wp2'-> # Testing === Original Project Plan === ->8- columns bsi { title 'WBS' }, name, start, end, effort, cost, revenue, chart { ${TaskTip} } # For this report we like to have the abbreviated weekday in front # of the date. %a is the tag for this. timeformat "%a %Y-%m-%d" loadunit days hideresource 1 balance cost rev caption 'All effort values are in man days.' footer -8<- === Staffing === All project phases are properly staffed. See [[ResourceGraph]] for detailed resource allocations. === Current Status === Some blurb about the current situation. ->8- } # Macro to set the background color of a cell according to the alert # level of the task. macro AlertColor [ cellcolor plan.alert = 0 "#90FF90" # green cellcolor plan.alert = 1 "#FFFF90" # yellow cellcolor plan.alert = 2 "#FF9090" # red ] taskreport status "" { columns bsi { width 50 title 'WBS' }, name { width 150 }, start { width 100 }, end { width 100 }, effort { width 100 }, alert { tooltip plan.journal != '' "<-query attribute='journal'->" width 150 }, status { width 150 } taskreport dashboard "" { headline "Project Dashboard (<-query attribute='now'->)" columns name { title "Task" ${AlertColor} width 200}, resources { width 200 ${AlertColor} listtype bullets listitem "<-query attribute='name'->" start ${projectstart} end ${projectend} }, alerttrend { title "Trend" ${AlertColor} width 50 }, journal { width 350 ${AlertColor} } journalmode status_up journalattributes headline, author, date, summary, details hidetask ~hasalert(0) sorttasks alert.down, plan.end.up period %{${now} - 1w} +1w } taskreport completed "" { headline "Already completed tasks" hidetask ~(plan.end <= ${now}) } taskreport ongoing "" { headline "Ongoing tasks" hidetask ~((plan.start <= ${now}) & (plan.end > ${now})) } taskreport future "" { headline "Future tasks" hidetask ~(plan.start > ${now}) } } # A list of tasks showing the resources assigned to each task. taskreport wp1 "" { headline "Work package 1 - Resource Allocation Report" columns bsi { title 'WBS' }, name, start, end, effort { title "Work" }, duration, chart { ${TaskTip} scale day width 500 } timeformat "%Y-%m-%d" hideresource ~(isleaf() & isleaf_()) sortresources name.up taskroot project.wp1 } # A list of tasks showing the resources assigned to each task. taskreport wp2 "" { headline "Work package 2 - Resource Allocation Report" columns bsi { title 'WBS' }, name, start, end, effort { title "Work" }, duration, chart { ${TaskTip} scale day width 500 } timeformat "%Y-%m-%d" hideresource ~(isleaf() & isleaf_()) sortresources name.up taskroot project.wp2 } # A list of all tasks with the percentage completed for each task taskreport deliveries "" { headline "Project Deliverables" columns bsi { title 'WBS' }, name, start, end, note { width 150 }, complete, chart { ${TaskTip} } taskroot project.deliveries hideresource 1 } # A list of all employees with their contact details. resourcereport contactList "" { headline "Contact list and duty plan" columns name, email { celltext 1 "[mailto:<-email-> <-email->]" }, managers { title "Manager" }, chart { scale day } hideresource ~isleaf() sortresources name.up hidetask 1 } # A graph showing resource allocation. It identifies whether each # resource is under- or over-allocated for. resourcereport resourceGraph "" { headline "Resource Allocation Graph" columns no, name, effort, rate, weekly { ${TaskTip} } loadunit shortauto # We only like to show leaf tasks for leaf resources. hidetask ~(isleaf() & isleaf_()) sorttasks plan.start.up } TaskJuggler-3.8.1/test/TestSuite/Syntax/Correct/textreport.tjp000066400000000000000000000004741473026623400245170ustar00rootroot00000000000000project "Test" 2011-12-11 +1m task "Foo" textreport frame "textreport" { taskreport r1 "" taskreport r2 "" formats html header -8<- This is the header ---- ->8- left "<[report id='frame.r1']>" center "Center" right "<[report id='frame.r2']>" footer -8<- ---- This is the footer ->8- } TaskJuggler-3.8.1/test/TestSuite/Syntax/Correct/tutorial.tjp000066400000000000000000000452411473026623400241430ustar00rootroot00000000000000/* * This file contains an example project. It is part of the * TaskJuggler project management tool. It uses a made up software * development project to demonstrate some of the basic features of * TaskJuggler. Please see the TaskJuggler manual for a more detailed * description of the various syntax elements. */ # *** EXAMPLE: header1 + # *** EXAMPLE: header2 + project acso "Accounting Software" 2002-01-16 +4m { # *** EXAMPLE: header1 - # Set the default time zone for the project. If not specified, UTC # is used. # *** EXAMPLE: timezone + timezone "Europe/Paris" # *** EXAMPLE: timezone - # Hide the clock time. Only show the date. # *** EXAMPLE: formats + timeformat "%Y-%m-%d" # *** EXAMPLE: formats - # Use US format for numbers # *** EXAMPLE: formats + numberformat "-" "" "," "." 1 # *** EXAMPLE: formats - # Use US financial format for currency values. Don't show cents. # *** EXAMPLE: formats + currencyformat "(" ")" "," "." 0 # *** EXAMPLE: formats - # Pick a day during the project that will be reported as 'today' in # the project reports. If not specified, the current day will be # used, but this will likely be outside of the project range, so it # can't be seen in the reports. # *** EXAMPLE: now + now 2002-03-05-13:00 # *** EXAMPLE: now - # The date that is used to show additional line on a Gannt chart # and can be specified by the user. # *** EXAMPLE: markdate + markdate 2002-08-07-03:00 # *** EXAMPLE: markdate - # The currency for all money values is the Euro. # *** EXAMPLE: currency + currency "USD" # *** EXAMPLE: currency - # We want to compare the baseline scenario to one with a slightly # delayed start. # *** EXAMPLE: scenario + scenario plan "Plan" { scenario delayed "Delayed" } # *** EXAMPLE: scenario - # *** EXAMPLE: extend + extend resource { text Phone "Phone" } # *** EXAMPLE: extend - # *** EXAMPLE: header1 + } # *** EXAMPLE: header1 - # *** EXAMPLE: header2 - # This is not a real copyright for this file. It's just used as an example. # *** EXAMPLE: copyright + copyright "© 2002 Crappy Software, Inc." # *** EXAMPLE: copyright - # The daily default rate of all resources. This can be overridden for each # resource. We specify this, so that we can do a good calculation of # the costs of the project. # *** EXAMPLE: rate + rate 390.0 # *** EXAMPLE: rate - # Register Good Friday as a global holiday for all resources. # *** EXAMPLE: vacation + leaves holiday "Good Friday" 2002-03-29 # *** EXAMPLE: vacation - # *** EXAMPLE: flags + flags team # *** EXAMPLE: flags - # This is one way to form teams # *** EXAMPLE: macro + macro allocate_developers [ # *** EXAMPLE: expandedmacro + allocate dev1 allocate dev2 allocate dev3 # *** EXAMPLE: expandedmacro - ] # *** EXAMPLE: macro - # In order to do a simple profit and loss analysis of the project we # specify accounts. One for the development costs, one for the # documentation costs, and one account to credit the customer payments # to. # *** EXAMPLE: accounts + account cost "Project Cost" { account dev "Development" account doc "Documentation" } account rev "Payments" # *** EXAMPLE: accounts - # The Profit&Loss analysis should be rev - cost accounts. # *** EXAMPLE: balance + balance cost rev # *** EXAMPLE: balance - # *** EXAMPLE: resources + resource boss "Paul Henry Bullock" { email "phb@crappysoftware.com" Phone "x100" rate 480 } resource dev "Developers" { managers boss resource dev1 "Paul Smith" { email "paul@crappysoftware.com" Phone "x362" rate 350.0 } resource dev2 "Sébastien Bono" { email "SBono@crappysoftware.com" Phone "x234" } resource dev3 "Klaus Müller" { email "Klaus.Mueller@crappysoftware.com" Phone "x490" leaves annual 2002-02-01 - 2002-02-05 } flags team } resource misc "The Others" { managers boss resource test "Peter Murphy" { email "murphy@crappysoftware.com" Phone "x666" limits { dailymax 6.4h } rate 310.0 } resource doc "Dim Sung" { email "sung@crappysoftware.com" Phone "x482" rate 300.0 leaves annual 2002-03-11 - 2002-03-16 } flags team } # *** EXAMPLE: resources - # Now we specify the work packages. The whole project is described as # a task that contains subtasks. These subtasks are then broken down # into smaller tasks and so on. The innermost tasks describe the real # work and have resources allocated to them. Many attributes of tasks # are inherited from the enclosing task. This saves you a lot of typing. # *** EXAMPLE: task1 + # *** EXAMPLE: charge + task AcSo "Accounting Software" { # *** EXAMPLE: task1 - # *** EXAMPLE: charge - # All work-related costs will be booked to this account unless the # subtasks specify something different. # *** EXAMPLE: charge + chargeset dev # *** EXAMPLE: charge - # For the duration of the project we have running cost that are not # included in the labor cost. # *** EXAMPLE: charge + charge 170 perday # *** EXAMPLE: charge - responsible boss # *** EXAMPLE: task1 + # *** EXAMPLE: spec + # *** EXAMPLE: charge + task spec "Specification" { # *** EXAMPLE: charge - # *** EXAMPLE: task1 - # *** EXAMPLE: spec - # The effort to finish this task is 20 man-days. # *** EXAMPLE: spec + effort 20d # *** EXAMPLE: spec - # Now we use the macro declared above to allocate the resources # for this task. Because they can work in parallel, they may finish this # task earlier than in 20 working-days. # *** EXAMPLE: spec + ${allocate_developers} # *** EXAMPLE: spec - # Each task without subtasks must have a start or an end # criterion and a duration. For this task we use a reference to a # milestone defined further below as the start criterion. So this task # can not start before the specified milestone has been reached. # References to other tasks may be relative. Each exclamation mark (!) # means 'in the scope of the enclosing task'. To descent into a task, the # fullstop (.) together with the id of the tasks have to be specified. # *** EXAMPLE: spec + depends !deliveries.start # *** EXAMPLE: task1 + } # *** EXAMPLE: task1 - # *** EXAMPLE: spec - # *** EXAMPLE: task1 + # *** EXAMPLE: software + task software "Software Development" { # *** EXAMPLE: task1 - # *** EXAMPLE: software - # The software is the most critical task of the project. So we set # the priority of this task (and all its subtasks) to 1000, the top # priority. The higher the priority, the more likely the task will # get the requested resources. # *** EXAMPLE: software + priority 1000 # *** EXAMPLE: software - # All subtasks depend on the specification task. depends !spec responsible dev1 # *** EXAMPLE: software + # *** EXAMPLE: database + task database "Database coupling" { # *** EXAMPLE: software - effort 20d allocate dev1, dev2 # *** EXAMPLE: software + journalentry 2002-02-03 "Problems with the SQL Libary" { author dev1 alert yellow summary -8<- We ran into some compatibility problems with the SQL Library. ->8- details -8<- We have already contacted the vendor and are now waiting for their advise. ->8- } } # *** EXAMPLE: database - # *** EXAMPLE: software - # *** EXAMPLE: software + # *** EXAMPLE: gui + task gui "Graphical User Interface" { # *** EXAMPLE: software - effort 35d # *** EXAMPLE: gui - # This task has taken 5 man-days more than originally planned. # We record this as well, so that we can generate reports that # compare the delayed schedule of the project to the original plan. # *** EXAMPLE: gui + delayed:effort 40d depends !database, !backend allocate dev2, dev3 # Resource dev2 should only work 6 hours per day on this task. limits { dailymax 6h { resources dev2 } } # *** EXAMPLE: software + } # *** EXAMPLE: gui - # *** EXAMPLE: software - # *** EXAMPLE: software + # *** EXAMPLE: backend + task backend "Back-End Functions" { # *** EXAMPLE: software - effort 30d # *** EXAMPLE: backend - # This task is behind schedule, because it should have been # finished already. To document this, we specify that the task # is 95% completed. If nothing is specified, TaskJuggler assumes # that the task is on schedule and computes the completion rate # according to the current day and the plan data. # *** EXAMPLE: backend + complete 95 depends !database allocate dev1, dev2 # *** EXAMPLE: software + } # *** EXAMPLE: backend - # *** EXAMPLE: task1 + } # *** EXAMPLE: task1 - # *** EXAMPLE: software - # *** EXAMPLE: task1 + # *** EXAMPLE: test + task test "Software testing" { # *** EXAMPLE: task1 - task alpha "Alpha Test" { # *** EXAMPLE: test - # Efforts can not only be specified as man-days, but also as # man-weeks, man-hours, etc. By default, TaskJuggler assumes # that a man-week is 5 man-days or 40 man-hours. These values # can be changed, of course. # *** EXAMPLE: test + effort 1w # *** EXAMPLE: test - # This task depends on a task in the scope of the enclosing # task's enclosing task. So we need two exclamation marks (!!) # to get there. # *** EXAMPLE: test + depends !!software allocate test, dev2 note "Hopefully most bugs will be found and fixed here." journalentry 2002-03-01 "Contract with Peter not yet signed" { author boss alert red summary -8<- The paperwork is stuck with HR and I can't hunt it down. ->8- details -8<- If we don't get the contract closed within the next week, the start of the testing is at risk. ->8- } } task beta "Beta Test" { effort 4w depends !alpha allocate test, dev1 } # *** EXAMPLE: task1 + } # *** EXAMPLE: test - # *** EXAMPLE: task1 - # *** EXAMPLE: task1 + # *** EXAMPLE: manual + task manual "Manual" { # *** EXAMPLE: task1 - effort 10w depends !deliveries.start allocate doc, dev3 purge chargeset chargeset doc # *** EXAMPLE: task1 + journalentry 2002-02-28 "User manual completed" { author boss summary "The doc writers did a really great job to finish on time." } } # *** EXAMPLE: manual - # *** EXAMPLE: task1 - # *** EXAMPLE: task1 + # *** EXAMPLE: deliveries + task deliveries "Milestones" { # *** EXAMPLE: deliveries - # *** EXAMPLE: task1 - # Some milestones have customer payments associated with them. We # credit these payments to the 'rev' account. # *** EXAMPLE: deliveries + purge chargeset chargeset rev task start "Project start" { # *** EXAMPLE: deliveries - # A task that has no duration is a milestone. It only needs a # start or end criterion. All other tasks depend on this task. # Here we use the built-in macro ${projectstart} to align the # start of the task with the above specified project time frame. # *** EXAMPLE: deliveries + start ${projectstart} # *** EXAMPLE: deliveries - # For some reason the actual start of the project got delayed. # We record this, so that we can compare the planned run to the # delayed run of the project. # *** EXAMPLE: deliveries + delayed:start 2002-01-20 # *** EXAMPLE: deliveries - # At the beginning of this task we receive a payment from the # customer. This is credited to the account associated with this # task when the task starts. # *** EXAMPLE: deliveries + charge 21000.0 onstart } task prev "Technology Preview" { depends !!software.backend charge 31000.0 onstart note "All '''major''' features should be usable." } task beta "Beta version" { depends !!test.alpha charge 13000.0 onstart note "Fully functional, may contain bugs." } task done "Ship Product to Customer" { # *** EXAMPLE: deliveries - # The next line can be uncommented to trigger a warning about # the project being late. For all tasks, limits for the start and # end values can be specified. Those limits are checked after the # project has been scheduled. For all violated limits a warning # is issued. # *** EXAMPLE: deliveries + # maxend 2002-04-17 depends !!test.beta, !!manual charge 33000.0 onstart note "All priority 1 and 2 bugs must be fixed." } # *** EXAMPLE: task1 + } } # *** EXAMPLE: deliveries - # *** EXAMPLE: task1 - # Now the project has been specified completely. Stopping here would # result in a valid TaskJuggler file that could be processed and # scheduled. But no reports would be generated to visualize the # results. # *** EXAMPLE: navigator + navigator navbar { hidereport @none } # *** EXAMPLE: navigator - # *** EXAMPLE: tasktip + macro TaskTip [ tooltip istask() -8<- '''Start: ''' <-query attribute='start'-> '''End: ''' <-query attribute='end'-> ---- '''Resources:''' <-query attribute='resources'-> ---- '''Precursors: ''' <-query attribute='precursors'-> ---- '''Followers: ''' <-query attribute='followers'-> ->8- ] # *** EXAMPLE: tasktip - # *** EXAMPLE: overview_report1 + # *** EXAMPLE: overview_report2 + textreport frame "" { # *** EXAMPLE: overview_report1 - header -8<- == Accounting Software Project == <[navigator id="navbar"]> ->8- footer "----" # *** EXAMPLE: overview_report1 + textreport index "Overview" { formats html center '<[report id="overview"]>' } # *** EXAMPLE: overview_report1 - # *** EXAMPLE: overview_report2 - textreport "Status" { formats html center -8<- <[report id="status.dashboard"]> ---- <[report id="status.completed"]> ---- <[report id="status.ongoing"]> ---- <[report id="status.future"]> ->8- } textreport development "Development" { formats html center '<[report id="development"]>' } textreport "Deliveries" { formats html center '<[report id="deliveries"]>' } textreport "ContactList" { formats html title "Contact List" center '<[report id="contactList"]>' } textreport "ResourceGraph" { formats html title "Resource Graph" center '<[report id="resourceGraph"]>' } # *** EXAMPLE: overview_report1 + # *** EXAMPLE: overview_report2 + } # *** EXAMPLE: overview_report1 - # *** EXAMPLE: overview_report2 - # A traditional Gantt chart with a project overview. # *** EXAMPLE: overview + # *** EXAMPLE: overview1 + taskreport overview "" { # *** EXAMPLE: overview1 - # *** EXAMPLE: overview4 + header -8<- === Project Overview === The project is structured into 3 phases. # Specification # <-reportlink id='frame.development'-> # Testing === Original Project Plan === ->8- # *** EXAMPLE: overview4 - # *** EXAMPLE: overview1 + columns bsi { title 'WBS' }, name, start, end, effort, cost, revenue, chart { ${TaskTip} } # *** EXAMPLE: overview1 - # For this report we like to have the abbreviated weekday in front # of the date. %a is the tag for this. # *** EXAMPLE: overview3 + timeformat "%a %Y-%m-%d" loadunit days # *** EXAMPLE: overview3 - hideresource @all # *** EXAMPLE: overview2 + balance cost rev # *** EXAMPLE: overview2 - # *** EXAMPLE: overview3 + caption 'All effort values are in man days.' # *** EXAMPLE: overview3 - footer -8<- === Staffing === All project phases are properly staffed. See [[ResourceGraph]] for detailed resource allocations. === Current Status === The project started off with a delay of 4 days. This slightly affected the original schedule. See [[Deliveries]] for the impact on the delivery dates. ->8- # *** EXAMPLE: overview1 + } # *** EXAMPLE: overview1 - # *** EXAMPLE: overview - # Macro to set the background color of a cell according to the alert # level of the task. macro AlertColor [ cellcolor plan.alert = 0 "#90FF90" # green cellcolor plan.alert = 1 "#FFFF90" # yellow cellcolor plan.alert = 2 "#FF9090" # red ] taskreport status "" { columns bsi { width 50 title 'WBS' }, name { width 150 }, start { width 100 }, end { width 100 }, effort { width 100 }, alert { tooltip plan.journal != '' "<-query attribute='journal'->" width 150 }, status { width 150 } scenarios delayed taskreport dashboard "" { headline "Project Dashboard (<-query attribute='now'->)" columns name { title "Task" ${AlertColor} width 200}, resources { width 200 ${AlertColor} listtype bullets listitem "<-query attribute='name'->" start ${projectstart} end ${projectend} }, alerttrend { title "Trend" ${AlertColor} width 50 }, journal { width 350 ${AlertColor} } journalmode status_up journalattributes headline, author, date, summary, details hidetask ~hasalert(0) sorttasks alert.down, delayed.end.up period %{${now} - 1w} +1w } taskreport completed "" { headline "Already completed tasks" hidetask ~(delayed.end <= ${now}) } taskreport ongoing "" { headline "Ongoing tasks" hidetask ~((delayed.start <= ${now}) & (delayed.end > ${now})) } taskreport future "" { headline "Future tasks" hidetask ~(delayed.start > ${now}) } } # A list of tasks showing the resources assigned to each task. taskreport development "" { scenarios delayed headline "Development - Resource Allocation Report" columns bsi { title 'WBS' }, name, start, end, effort { title "Work" }, duration, chart { ${TaskTip} scale day width 500 } timeformat "%Y-%m-%d" hideresource ~(isleaf() & isleaf_()) sortresources name.up } # A list of all tasks with the percentage completed for each task taskreport deliveries "" { headline "Project Deliverables" columns bsi { title 'WBS' }, name, start, end, note { width 150 }, complete, chart { ${TaskTip} } taskroot AcSo.deliveries hideresource @all scenarios plan, delayed } # A list of all employees with their contact details. resourcereport contactList "" { scenarios delayed headline "Contact list and duty plan" columns name, email { celltext 1 "[mailto:<-email-> <-email->]" }, Phone, managers { title "Manager" }, chart { scale day } hideresource ~isleaf() sortresources name.up hidetask @all } # A graph showing resource allocation. It identifies whether each # resource is under- or over-allocated for. resourcereport resourceGraph "" { scenarios delayed headline "Resource Allocation Graph" columns no, name, effort, rate, weekly { ${TaskTip} } loadunit shortauto # We only like to show leaf tasks for leaf resources. hidetask ~(isleaf() & isleaf_()) sorttasks plan.start.up } TaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/000077500000000000000000000000001473026623400214265ustar00rootroot00000000000000TaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/adopt_duplicate_child-1.tjp000066400000000000000000000002221473026623400266030ustar00rootroot00000000000000project "Adopted Tasks" 2011-03-05 +1m task t1 "T1" { task t2 "T2" } # MARK: error 7 adopt_duplicate_child task t3 "T3" { adopt t1.t2, t1 } TaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/adopt_duplicate_child-2.tjp000066400000000000000000000002241473026623400266060ustar00rootroot00000000000000project "Adopted Tasks" 2011-03-05 +1m task t1 "T1" { task t2 "T2" # MARK: error 6 adopt_duplicate_child task t3 "T3" { adopt t1.t2 } } TaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/adopt_duplicate_child-3.tjp000066400000000000000000000002221473026623400266050ustar00rootroot00000000000000project "Adopted Tasks" 2011-03-05 +1m task t1 "T1" { task t2 "T2" } # MARK: error 7 adopt_duplicate_child task t3 "T3" { adopt t1, t1.t2 } TaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/adopt_self.tjp000066400000000000000000000001401473026623400242600ustar00rootroot00000000000000project "Adopted Tasks" 2011-11-17 +1m # MARK: error 4 adopt_self task t1 "T1" { adopt t1 } TaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/alert_level_redef.tjp000066400000000000000000000003071473026623400256100ustar00rootroot00000000000000project "Test" 2011-12-08 +1m { # MARK: error 3 alert_level_redef alertlevels foo "Foo" "#000", bar "Bar" "#111", foo "Foo2" "#222", bar1 "Bar1" "#333" } TaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/alert_name_redef.tjp000066400000000000000000000003061473026623400254200ustar00rootroot00000000000000project "Test" 2011-12-08 +1m { # MARK: error 3 alert_name_redef alertlevels foo "Foo" "#000", bar "Bar" "#111", foo2 "Foo" "#222", bar1 "Bar1" "#333" } TaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/bad_include.tjp000066400000000000000000000003511473026623400243750ustar00rootroot00000000000000project test "Test" "1.0" 2007-08-19 +2m # Unfortunately, include file errors are reported at the first token # after the include statement. # MARK: error 6 bad_include include "foo.tji" task bar "Bar" { start ${projectstart} } TaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/bad_time_zone.tjp000066400000000000000000000001601473026623400247410ustar00rootroot00000000000000project test "Test" "1.0" 2007-08-26 +1w { # MARK: error 3 bad_time_zone timezone "foo/bar" } task foo "Foo" TaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/bad_timing_res.tjp000066400000000000000000000001471473026623400251150ustar00rootroot00000000000000project test "Test" "1.0" 2007-08-26 +1w { # MARK: error 3 bad_timing_res timingresolution 21 min } TaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/booking_group.tjp000066400000000000000000000003371473026623400250140ustar00rootroot00000000000000project test "Test" "1.0" 2007-08-19 +2m resource tuxies "Tuxies" { resource tux "Tux" } task t "T" { start 2007-08-19 } supplement resource tuxies { # MARK: error 13 booking_group booking t 2007-08-25-10:00 +2h } TaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/booking_milestone.tjp000066400000000000000000000003151473026623400256530ustar00rootroot00000000000000project test "Test" "1.0" 2007-08-19 +2m resource tux "Tux" task t "T" { start 2007-08-19 milestone } supplement resource tux { # MARK: error 12 booking_milestone booking t 2007-08-25-10:00 +2h } TaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/booking_no_leaf.tjp000066400000000000000000000003141473026623400252560ustar00rootroot00000000000000project test "Test" "1.0" 2007-08-19 +2m resource tux "Tux" task t "T" { start 2007-08-19 task s "S" } supplement resource tux { # MARK: error 12 booking_no_leaf booking t 2007-08-25-10:00 +2h } TaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/chargeset.tjp000066400000000000000000000003431473026623400241120ustar00rootroot00000000000000project test "Test" "1.0" 2007-11-16 +2m account group1 "Group1" { account g1 "G1" account g2 "G2" account g3 "G3" } task t "T" { start ${projectstart} # MARK: error 12 chargeset chargeset g1 30%, g2 30%, g3 30% } TaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/chargeset_master.tjp000066400000000000000000000003511473026623400254640ustar00rootroot00000000000000project test "Test" "1.0" 2007-11-16 +2m account group1 "Group1" { account g1 "G1" account g2 "G2" account g3 "G3" } task t "T" { start ${projectstart} chargeset g1, g2 # MARK: error 13 chargeset_master chargeset g3 } TaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/container_attribute.tjp000066400000000000000000000002731473026623400262140ustar00rootroot00000000000000project test "Test" "1.0" 2008-02-03 +2m task m "M" { start ${projectstart} } task t "T" { start ${projectstart} task s "S" # MARK: error 11 container_attribute depends !m } TaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/cost_acct_no_top.tjp000066400000000000000000000005631473026623400254710ustar00rootroot00000000000000project test "Test" "1.0" 2007-11-16 +2m account group1 "Group1" { account g1 "G1" account g2 "G2" account g3 "G3" } account group2 "Group2" { account g4 "G1" account g5 "G2" account g6 "G3" } task t "T" { start ${projectstart} chargeset g1, g2 } taskreport tasks "Tasks.html" { formats html # MARK: error 23 cost_acct_no_top balance g1 group2 } TaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/cost_rev_same.tjp000066400000000000000000000005451473026623400250020ustar00rootroot00000000000000project test "Test" "1.0" 2007-11-16 +2m account group1 "Group1" { account g1 "G1" account g2 "G2" account g3 "G3" } account group2 "Group2" { account g4 "G1" account g5 "G2" account g6 "G3" } task t "T" { start ${projectstart} chargeset g1, g2 } taskreport tasks "Tasks.html" { # MARK: error 22 cost_rev_same balance group1 group1 } TaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/date_in_range.tjp000066400000000000000000000001541473026623400247240ustar00rootroot00000000000000project test "Test" "1.0" 2007-08-19 +2m task t "T" { # MARK: error 5 date_in_range start 2007-08-18 } TaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/effort_zero.tjp000066400000000000000000000001651473026623400244730ustar00rootroot00000000000000project test "Test" "1.0" 2007-08-19 +2m task t "T" { start 2007-08-19 # MARK: error 6 effort_zero effort 0d } TaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/empty.tjp000066400000000000000000000000371473026623400233030ustar00rootroot00000000000000# MARK: error 2 unexpctd_token TaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/extend_id_cap.tjp000066400000000000000000000001511473026623400247300ustar00rootroot00000000000000project test "Test" "1.0" 2007-03-25 +2m { extend task { # MARK: error 4 extend_id_cap text foo } } TaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/include_before_project.tjp000066400000000000000000000000711473026623400266360ustar00rootroot00000000000000# MARK: error 2 include_before_project include "foo.tji" TaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/include_recursion.tji000066400000000000000000000000401473026623400256440ustar00rootroot00000000000000include "include_recursion.tji" TaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/include_recursion.tjp000066400000000000000000000003441473026623400256620ustar00rootroot00000000000000project "Test" 2009-10-06 +2m # The following error really happens in the included file, but the # testing scripts look for the MARK in this file. # MARK: error 1 include_recursion include "include_recursion.tji" task "Foo" TaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/interval_end_in_range.tjp000066400000000000000000000001711473026623400264600ustar00rootroot00000000000000project test "Test" "1.0" 2007-08-19 +2m task t "T" { # MARK: error 5 interval_end_in_range period 2007-09-14 +2m } TaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/interval_start_in_range.tjp000066400000000000000000000001731473026623400270510ustar00rootroot00000000000000project test "Test" "1.0" 2007-08-19 +2m task t "T" { # MARK: error 5 interval_start_in_range period 2007-08-18 +2d } TaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/invalid_file_name.tjp000066400000000000000000000001641473026623400255730ustar00rootroot00000000000000project "test" 2011-10-29 +1m task "T" # MARK: error 6 invalid_file_name taskreport "foo\bar" { formats html } TaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/junk_after_cut.tjp000066400000000000000000000001621473026623400251470ustar00rootroot00000000000000project test "Test" "1.0" 2007-08-19 +2m # MARK: error 4 junk_after_cut task t -8<- ->8- { start 2007-08-19 } TaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/leaf_resource_id_expected.tjp000066400000000000000000000003251473026623400273200ustar00rootroot00000000000000project test "Test" "1.0" 2009-01-22 +2m resource team "T" { resource foo "Foo" } task t "T" { start ${projectstart} # MARK: error 10 leaf_resource_id_expected limits { dailymax 4h { resources team }} } TaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/macro_stack_overflow.tjp000066400000000000000000000013241473026623400263560ustar00rootroot00000000000000project "Macro Stack Overflow" 2009-11-01 +2m macro foo [ task "bar" { ${foo} } ] # MARK: info 3 macro_stack # MARK: info 3 macro_stack # MARK: info 3 macro_stack # MARK: info 3 macro_stack # MARK: info 3 macro_stack # MARK: info 3 macro_stack # MARK: info 3 macro_stack # MARK: info 3 macro_stack # MARK: info 3 macro_stack # MARK: info 3 macro_stack # MARK: info 3 macro_stack # MARK: info 3 macro_stack # MARK: info 3 macro_stack # MARK: info 3 macro_stack # MARK: info 3 macro_stack # MARK: info 3 macro_stack # MARK: info 3 macro_stack # MARK: info 3 macro_stack # MARK: info 3 macro_stack # MARK: info 3 macro_stack # MARK: info 3 macro_stack # MARK: info 3 macro_stack # MARK: error 28 macro_stack_overflow ${foo} TaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/misaligned_date.tjp000066400000000000000000000002151473026623400252540ustar00rootroot00000000000000project test "Test" "1.0" 2007-08-26 +1w { timingresolution 20 min } # MARK: error 6 misaligned_date vacation "Day off" 2007-08-27-18:15 TaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/multiple_durations.tjp000066400000000000000000000002141473026623400260650ustar00rootroot00000000000000project test "Test" "1.0" 2008-02-02 +1m task t "T" { start ${projectstart} milestone # MARK: error 7 multiple_durations length 2d } TaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/navigator_exists.tjp000066400000000000000000000002401473026623400255320ustar00rootroot00000000000000project test "Test" "1.0" 2007-11-16 +2m task t "T" { start ${projectstart} } navigator foo navigator bar # MARK: error 10 navigator_exists navigator foo TaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/no_own_resource_booking.tjp000066400000000000000000000003721473026623400270650ustar00rootroot00000000000000project "Test" 2011-05-15 +1w { scenario s1 "S1" { scenario s2 "S2" } trackingscenario s1 } resource r "R" task t "T" { duration 2d } supplement resource r { # MARK: error 15 no_own_resource_booking s2:booking t 2011-05-16-9:00 +2h } TaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/no_own_task_booking.tjp000066400000000000000000000003621473026623400261770ustar00rootroot00000000000000project "Test" 2011-05-15 +1w { scenario s1 "S1" { scenario s2 "S2" } trackingscenario s1 } resource r "R" task t "T" { duration 2d } supplement task t { # MARK: error 15 no_own_task_booking s2:booking r 2011-05-16-9:00 +2h } TaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/no_reduce.tjp000066400000000000000000000001401473026623400241030ustar00rootroot00000000000000project test "Test" "1.0" 2010-11-06 +1m task t "T" { # MARK: error 5 no_reduce effort 1dd } TaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/no_token_match1.tjp000066400000000000000000000001321473026623400252120ustar00rootroot00000000000000project test "Test" "1.0" 2009-08-25 +2m # MARK: error 5 no_token_match task t -8<- TaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/no_token_match2.tjp000066400000000000000000000001331473026623400252140ustar00rootroot00000000000000project test "Test" "1.0" 2009-08-25 +2m # MARK: error 5 no_token_match task t -8<- fooTaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/no_token_match3.tjp000066400000000000000000000001371473026623400252210ustar00rootroot00000000000000project test "Test" "1.0" 2009-08-25 +2m # MARK: error 6 no_token_match task t -8<- foo -TaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/no_token_match4.tjp000066400000000000000000000001401473026623400252140ustar00rootroot00000000000000project test "Test" "1.0" 2009-08-25 +2m # MARK: error 6 no_token_match task t -8<- foo ->TaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/no_token_match5.tjp000066400000000000000000000001411473026623400252160ustar00rootroot00000000000000project test "Test" "1.0" 2009-08-25 +2m # MARK: error 6 no_token_match task t -8<- foo ->8TaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/operand_attribute.tjp000066400000000000000000000002361473026623400256610ustar00rootroot00000000000000project test "Test" "1.0" 2007-08-19 +2m task t "T" { start 2007-08-19 } export "report.tji" { # MARK: error 9 operand_attribute hidetask t.foo.bar } TaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/operand_unkn_flag.tjp000066400000000000000000000002271473026623400256220ustar00rootroot00000000000000project test "Test" "1.0" 2007-08-19 +2m task t "T" { start 2007-08-19 } export "report.tji" { # MARK: error 9 operand_unkn_flag hidetask foo } TaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/operand_unkn_scen.tjp000066400000000000000000000003001473026623400256310ustar00rootroot00000000000000project test "Test" "1.0" 2007-08-19 +2m { timezone "Europe/Amsterdam" } task t "T" { start ${projectstart} } export "report.tji" { # MARK: error 11 operand_unkn_scen hidetask foo.t } TaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/overtime_range.tjp000066400000000000000000000003271473026623400251550ustar00rootroot00000000000000project test "Test" "1.0" 2007-04-22 +2m resource tux "Tux" task foo "Foo" { start ${projectstart} } supplement resource tux { # MARK: error 11 overtime_range booking foo ${projectstart} +1h { overtime 3 } } TaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/purge_unknown_id.tjp000066400000000000000000000002001473026623400255120ustar00rootroot00000000000000project test "Test" "1.0" 2007-11-16 +2m task t "T" { start ${projectstart} # MARK: error 6 purge_unknown_id purge foo } TaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/report_end.tjp000066400000000000000000000002431473026623400243050ustar00rootroot00000000000000project test "Test" "1.0" 2007-11-16 +2m task t "T" { start ${projectstart} } taskreport report "report.html" { # MARK: error 9 report_end end 2007-10-01 } TaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/report_exists.tjp000066400000000000000000000002531473026623400250570ustar00rootroot00000000000000project test "Test" "1.0" 2007-11-16 +2m task t "T" { start ${projectstart} } taskreport report "report1" # MARK: error 9 report_exists taskreport report "report2" TaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/report_start.tjp000066400000000000000000000002471473026623400247000ustar00rootroot00000000000000project test "Test" "1.0" 2007-11-16 +2m task t "T" { start ${projectstart} } taskreport report "report.html" { # MARK: error 9 report_start start 2008-02-01 } TaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/resource_exists.tjp000066400000000000000000000002121473026623400253660ustar00rootroot00000000000000project test "Test" "1.0" 2009-02-11 +1m resource r "First resource r" # MARK: error 6 resource_exists resource r "Second resource r" TaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/resource_id_expected.tjp000066400000000000000000000002251473026623400263300ustar00rootroot00000000000000project test "Test" "1.0" 2007-08-19 +2m task t "T" { start 2007-08-19 # MARK: error 6 resource_id_expected booking foo 2007-08-25-10:00 +2h } TaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/resourceroot_leaf.tjp000066400000000000000000000002201473026623400256610ustar00rootroot00000000000000project test "Test" "1.0" 2011-03-27 +2m resource r "R" task t "T" resourcereport "R" { # MARK: error 9 resourceroot_leaf resourceroot r } TaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/rev_acct_no_top.tjp000066400000000000000000000005431473026623400253130ustar00rootroot00000000000000project test "Test" "1.0" 2007-11-16 +2m account group1 "Group1" { account g1 "G1" account g2 "G2" account g3 "G3" } account group2 "Group2" { account g4 "G1" account g5 "G2" account g6 "G3" } task t "T" { start ${projectstart} chargeset g1, g2 } taskreport tasks "Tasks.html" { # MARK: error 22 rev_acct_no_top balance group1 g5 } TaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/runaway_token.tjp000066400000000000000000000001671473026623400250370ustar00rootroot00000000000000project "Test" 2011-06-06 +1m task "Foo" { # MARK: error 6 runaway_token note -8<- This note never ends... TaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/scenario_after_tracking.tjp000066400000000000000000000002041473026623400270070ustar00rootroot00000000000000project "Test" 2011-06-27 +1m { trackingscenario plan # MARK: error 4 scenario_after_tracking scenario foo "Bar" } task "Foo" TaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/scenario_exists.tjp000066400000000000000000000002511473026623400253450ustar00rootroot00000000000000project test "Test" "1.0" 2009-02-11 +1m { scenario plan "Plan" { scenario s "First scenario s" # MARK: error 6 scenario_exists scenario s "Second scenario s" } TaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/shift_assignment_overlap.tjp000066400000000000000000000003771473026623400272510ustar00rootroot00000000000000project test "Test" "1.0" 2007-08-20 +2m shift a "A" { workinghours mon - fri off } shift b "B" { workinghours mon - fri 10:00 - 18:00 } resource r "R" { shifts a 2007-08-20 +2w # MARK: error 13 shift_assignment_overlap shifts b 2007-09-01 +2w } TaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/shift_exists.tjp000066400000000000000000000001731473026623400246620ustar00rootroot00000000000000project test "Test" "1.0" 2009-02-11 +1m shift s "First shift s" # MARK: error 6 shift_exists shift s "Second shift s" TaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/shift_id_expected.tjp000066400000000000000000000001751473026623400256220ustar00rootroot00000000000000project test "Test" "1.0" 2007-08-20 +2m resource r "R" { # MARK: error 5 shift_id_expected shifts foo 2007-08-20 +2d } TaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/sloppy_range.tjp000066400000000000000000000003231473026623400246450ustar00rootroot00000000000000project test "Test" "1.0" 2007-04-22 +2m resource tux "Tux" task foo "Foo" { start ${projectstart} } supplement resource tux { # MARK: error 11 sloppy_range booking foo ${projectstart} +1h { sloppy 4 } } TaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/sort_direction.tjp000066400000000000000000000002541473026623400251750ustar00rootroot00000000000000project test "Test" "1.0" 2007-08-19 +2m task t "T" { start 2007-08-19 } taskreport report "report.html" { # MARK: error 9 sort_direction sorttasks plan.start.foo } TaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/sort_unknown_scen.tjp000066400000000000000000000002551473026623400257250ustar00rootroot00000000000000project test "Test" "1.0" 2007-08-19 +2m task t "T" { start 2007-08-19 } taskreport report "report.html" { # MARK: error 9 sort_unknown_scen sorttasks foo.start.up } TaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/sorting_bsi.tjp000066400000000000000000000002521473026623400244660ustar00rootroot00000000000000project test "Test" "1.0" 2007-11-07 +2m task t "T" { start ${projectstart} } taskreport report "report.html" { # MARK: error 9 sorting_bsi sorttasks plan.bsi.up } TaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/sorting_crit_exptd1.tjp000066400000000000000000000003341473026623400261400ustar00rootroot00000000000000project test "Test" "1.0" 2007-08-19 +2m { timezone "Europe/Amsterdam" } task t "T" { start ${projectstart} } taskreport report "report.html" { # MARK: error 11 sorting_crit_exptd1 sorttasks plan.start.foo.bar } TaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/sorting_crit_exptd2.tjp000066400000000000000000000002461473026623400261430ustar00rootroot00000000000000project test "Test" "1.0" 2007-08-19 +2m task t "T" { start 2007-08-19 } taskreport report "report.html" { # MARK: error 9 sorting_crit_exptd2 sorttasks foo } TaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/ss_no_tracking_scenario.tjp000066400000000000000000000003511473026623400270320ustar00rootroot00000000000000project "Test" 2011-05-15 +1w resource r "R" task t "T" # MARK: error 7 ss_no_tracking_scenario statussheet r 2011-05-15 +1w { task t { work 80% priority 1001 end 2011-05-18 status green "Lots of work done" } } TaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/start_before_end1.tjp000066400000000000000000000002071473026623400255320ustar00rootroot00000000000000project test "Test" "1.0" 2009-02-22 +3m # MARK: error 4 start_before_end vacation "Day off" 2009-02-24 - 2008-02-23 task foo "Foo" TaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/start_before_end2.tjp000066400000000000000000000001771473026623400255410ustar00rootroot00000000000000project test "Test" "1.0" 2009-02-22 +3m task foo "Foo" { # MARK: error 5 start_before_end period 2009-02-24 - 2008-02-23 } TaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/task_complete.tjp000066400000000000000000000001721473026623400247770ustar00rootroot00000000000000project test "Test" "1.0" 2007-08-19 +2m task t "T" { start 2007-08-19 # MARK: error 6 task_complete complete 101 } TaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/task_exists.tjp000066400000000000000000000001661473026623400245110ustar00rootroot00000000000000project test "Test" "1.0" 2009-02-11 +1m task t "First task t" # MARK: error 6 task_exists task t "Second task t" TaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/task_priority.tjp000066400000000000000000000001761473026623400250540ustar00rootroot00000000000000project test "Test" "1.0" 2007-08-19 +2m task t "T" { start 2007-08-19 # MARK: error 6 task_priority priority 1001 } TaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/task_without_chargeset.tjp000066400000000000000000000002451473026623400267200ustar00rootroot00000000000000project test "Test" "1.0" 2007-11-18 +2m account bar "Bar" task foo "Foo" { start ${projectstart} # MARK: error 8 task_without_chargeset charge 1000 onstart } TaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/taskroot_leaf.tjp000066400000000000000000000001641473026623400250030ustar00rootroot00000000000000project test "Test" "1.0" 2011-03-27 +2m task t "T" taskreport "R" { # MARK: error 7 taskroot_leaf taskroot t } TaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/time_error.tjp000066400000000000000000000002701473026623400243130ustar00rootroot00000000000000# MARK: error 2 time_error project tz "Timezone" "1.0" 2005-06-06-00:00-+0000 - 2005-06-07-0:00-+1401 { timezone "Europe/Athens" } task item "Project" { start 2005-06-06-12:00 } TaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/time_interval.tjp000066400000000000000000000002451473026623400250100ustar00rootroot00000000000000project test "Test" "1.0" 2007-08-19 +2m resource tux "Tux" { # MARK: error 5 time_interval workinghours mon 10:00 - 9:00 } task t "T" { start 2007-08-19 } TaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/too_few_alert_levels.tjp000066400000000000000000000001471473026623400263520ustar00rootroot00000000000000project "Test" 2011-12-08 +1m { # MARK: error 3 too_few_alert_levels alertlevels foo "Foo" "#000" } TaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/too_large_timing_res.tjp000066400000000000000000000002331473026623400263360ustar00rootroot00000000000000project test "Test" "1.0" 2007-08-26 +1w { timezone "Pacific/Marquesas" # MARK: error 4 too_large_timing_res timingresolution 60 min } task foo "Foo" TaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/too_many_bangs.tjp000066400000000000000000000002201473026623400251360ustar00rootroot00000000000000project test "Test" "1.0" 2007-08-19 +2m task t "T" { start 2007-08-19 task s "S" { # MARK: error 7 too_many_bangs depends !!!foo } } TaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/ts_bad_priority.tjp000066400000000000000000000004501473026623400253410ustar00rootroot00000000000000project "test" 2010-02-19 +2m { trackingscenario plan } resource r1 "R1" task t1 "Task 1" { duration 5d allocate r1 } timesheet r1 2010-02-21 +1w { task t1 { work 80% # MARK: error 16 ts_bad_priority priority 1001 end 2010-02-28 status green "Lots of work done" } } TaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/ts_default_details.tjp000066400000000000000000000005101473026623400260000ustar00rootroot00000000000000project "test" 2010-02-21 +2m { trackingscenario plan } resource r1 "R1" task t1 "Task 1" { effort 5d allocate r1 } timesheet r1 2010-02-21 +1w { task t1 { work 100% remaining 0d status green "Green" { # MARK: error 19 ts_default_details details -8<- Some more details ->8- } } } TaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/ts_default_summary.tjp000066400000000000000000000005051473026623400260540ustar00rootroot00000000000000project "test" 2010-02-21 +2m { trackingscenario plan } resource r1 "R1" task t1 "Task 1" { effort 5d allocate r1 } timesheet r1 2010-02-21 +1w { task t1 { work 100% remaining 0d status green "Green" { # MARK: error 19 ts_default_summary summary -8<- A summary text ->8- } } } TaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/ts_duplicate_task.tjp000066400000000000000000000005471473026623400256550ustar00rootroot00000000000000project "test" 2010-02-21 +2m { trackingscenario plan } resource r1 "R1" task t1 "Task 1" { effort 5d allocate r1 } # MARK: error 13 ts_duplicate_task timesheet r1 2010-02-21 +1w { task t1 { work 80% remaining 0d status green "Lots of work done" } task t1 { work 80% remaining 0d status green "Lots of work done" } } TaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/ts_end_too_early.tjp000066400000000000000000000004271473026623400255010ustar00rootroot00000000000000project "test" 2010-02-19 +2m { trackingscenario plan } resource r1 "R1" task t1 "Task 1" { duration 5d allocate r1 } timesheet r1 2010-02-21 +1w { task t1 { work 80% # MARK: error 16 ts_end_too_early end 2010-02-20 status green "Lots of work done" } } TaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/ts_headline_too_long.tjp000066400000000000000000000006041473026623400263240ustar00rootroot00000000000000project "test" 2010-02-21 +2m { trackingscenario plan } resource r1 "R1" task t1 "Task 1" { effort 5d allocate r1 } timesheet r1 2010-02-21 +1w { task t1 { work 100% remaining 0d # MARK: error 18 ts_headline_too_long status green "0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567891" } } TaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/ts_no_headline2.tjp000066400000000000000000000007301473026623400252020ustar00rootroot00000000000000project "test" 2009-11-30 +2m { trackingscenario plan } resource r1 "R1" task t1 "Task 1" { effort 5d allocate r1 } timesheet r1 2009-11-30 +1w { task t1 { work 2d remaining 0d # MARK: error 17 ts_no_headline status green "Your headline here!" { summary "I had good fun!" details -8<- This task went smoothly and I got three things done: * Have fun * Be on time * Get things done ->8- } } } TaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/ts_no_tracking_scenario.tjp000066400000000000000000000003471473026623400270400ustar00rootroot00000000000000project "Test" 2011-05-15 +1w resource r "R" task t "T" # MARK: error 7 ts_no_tracking_scenario timesheet r 2011-05-15 +1w { task t { work 80% priority 1001 end 2011-05-18 status green "Lots of work done" } } TaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/ts_summary_too_long.tjp000066400000000000000000000014261473026623400262530ustar00rootroot00000000000000project "test" 2010-02-21 +2m { trackingscenario plan } resource r1 "R1" task t1 "Task 1" { effort 5d allocate r1 } timesheet r1 2010-02-21 +1w { task t1 { work 100% remaining 0d # MARK: error 19 ts_summary_too_long status green "headline" { summary -8<- 012345678901234567890123456789012345678901234567890123456789 012345678901234567890123456789012345678901234567890123456789 012345678901234567890123456789012345678901234567890123456789 012345678901234567890123456789012345678901234567890123456789 012345678901234567890123456789012345678901234567890123456789 012345678901234567890123456789012345678901234567890123456789 012345678901234567890123456789012345678901234567890123456789 012345678901234567890123456789012345678901234567890123456789 ->8- } } } TaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/undecl_flag.tjp000066400000000000000000000001411473026623400244040ustar00rootroot00000000000000project test "Test" "1.0" 2007-03-25 +2m task t "T" { # MARK: error 5 undecl_flag flags foo } TaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/unknown_env_var.tjp000066400000000000000000000001461473026623400253650ustar00rootroot00000000000000project "Test" "1.0" 2011-05-05 +2m task "T" { # MARK: error 5 unknown_env_var note $(UNSET_VAR) } TaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/unknown_projectid.tjp000066400000000000000000000001771473026623400257140ustar00rootroot00000000000000project test "Test" "1.0" 2007-11-15 +2m task t "T" { # MARK: error 5 unknown_projectid projectid foo start 2007-11-15 } TaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/unknown_scenario_id.tjp000066400000000000000000000002061473026623400262010ustar00rootroot00000000000000project test "Test" "1.0" 2007-03-25 +2m resource r "R" { # MARK: error 5 unknown_scenario_id foo:workinghours mon 8:00 - 10:00 } TaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/unknown_scenario_idx.tjp000066400000000000000000000002561473026623400263760ustar00rootroot00000000000000project test "Test" "1.0" 2007-08-19 +2m task t "T" { start 2007-08-19 } taskreport report "report.html" { # MARK: error 9 unknown_scenario_idx scenarios plan, foo } TaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/unknown_task.tjp000066400000000000000000000002351473026623400246660ustar00rootroot00000000000000project test "Test" "1.0" 2007-08-19 +2m task t "T" { start 2007-08-19 } taskreport report "report.html" { # MARK: error 9 unknown_task taskroot foo } TaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/unsupported_token.tjp000066400000000000000000000003071473026623400257350ustar00rootroot00000000000000project "Test" 2009-11-28 +2m task t "T" { journalentry 2009-11-28 "Just testing" { # MARK: error 8 unsupported_token summary -8<- This is all good. * This is bad. ->8- } } TaskJuggler-3.8.1/test/TestSuite/Syntax/Errors/working_duration_too_small.tjp000066400000000000000000000001501473026623400275770ustar00rootroot00000000000000project "Test" 2014-03-10 +1m task "Foo" { # MARK: error 5 working_duration_too_small length 20min } TaskJuggler-3.8.1/test/TjpGen.rb000066400000000000000000000076051473026623400164570ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = TjpGen.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # $:.unshift File.join(File.dirname(__FILE__), '..', 'lib') require 'taskjuggler/TjTime' class TjpGen def initialize(fileName = 'test.tjp', seed = 12345, tasks = 100, resources = 15) @fileName = fileName srand(seed) @tasks = tasks @resources = resources @start = TjTime.local(2000, 1, 1) + rand(365 * 5) * (60 * 60 * 24) @resourceList = [] @taskList = [] @depTargets = [] end def generate File.open(@fileName, "w") do |f| f.puts "project test \"Test\" \"1.0\" #{@start} +270d" begin genResource(f, 0) end while @resourceList.length < @resources begin genTask(f, 0, '', []) end while @taskList.length < @tasks #genReports(f) end end private def genResource(f, level) id = "r#{@resourceList.length}" indent = ' ' * 2 * level f.puts "#{indent}resource #{id} \"Resource #{@resourceList.length}\" {" @resourceList << id if rand(10) < 2 genResource(f, level + 1) end f.puts "#{indent}}" end def genTask(f, level, parent, brothers) id = "t#{@taskList.length}" fullId = parent + id indent = ' ' * 2 * level f.puts "#{indent}task #{id} \"Task #{@taskList.length}\" {" @taskList << fullId if rand(10) < 1 f.puts "#{indent} priority #{(rand(9) + 1) * 100}" end if level == 0 f.puts "#{indent} start #{@start + (60 * 60 * rand(@taskList.length))}" end children = [] if (level <= (Math.log10(@tasks) * 2) && rand(10) < 6) 0.upto(rand(1 + level)) do |i| children << genTask(f, level + 1, fullId + '.', children) end end if children.empty? wof = rand(100) milestone = false if wof < 10 f.puts "#{indent} milestone" milestone = true elsif wof < 70 f.puts "#{indent} effort #{1 + rand(60)}h" genAllocate(f, indent) elsif wof < 80 f.puts "#{indent} length #{1 + rand(80)}h" genAllocate(f, indent) if rand(5) < 2 else f.puts "#{indent} duration #{1 + rand(200)}h" genAllocate(f, indent) if rand(5) < 1 end if @depTargets.empty? || rand(10) < 1 || level == 0 f.puts "#{indent} start #{@start + rand(20) * (60 * 60 * 24)}" else deps = [] if rand(5) < 1 depList = @depTargets else depList = brothers end while !depList.empty? && rand(100) < (milestone ? 60 : 30) dep = depList[rand(depList.length)] deps << dep unless deps.include?(dep) end f.puts "#{indent} depends #{deps.join(', ')}" unless deps.empty? end end if level > 0 && rand(10) < 3 @depTargets << fullId end f.puts "#{indent}}" fullId end def genAllocate(f, indent) res = [] res << @resourceList[rand(@resourceList.length)] while rand(10) < 2 r = @resourceList[rand(@resourceList.length)] res << r unless res.include?(r) end f.puts "#{indent} allocate #{res.join(', ')}" end def genReports(f) f.puts "taskreport \"Tasks\" {" f.puts " columns no, name, start, end, chart" f.puts "}" f.puts "resourcereport \"Resources\" {" f.puts " columns no, name, effort, utilization, chart" f.puts "}" end end fileName = ARGV[0] ? ARGV[0] : 'test.tjp' seed = ARGV[1] ? ARGV[1].to_i : 12345 tasks = ARGV[2] ? ARGV[2].to_i : 100 resources = 1 + (tasks / 21.0).to_i gtor = TjpGen.new(fileName, seed, tasks, resources) gtor.generate TaskJuggler-3.8.1/test/all.rb000066400000000000000000000007571473026623400160410ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = all.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # Dir.glob(File.dirname(__FILE__) + '/test_*.rb').each { |f| require f } TaskJuggler-3.8.1/test/test_AlgorithmDiff.rb000066400000000000000000000075141473026623400210450ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = test_AlgorithmDiff.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # $:.unshift File.join(File.dirname(__FILE__), '..', 'lib') if __FILE__ == $0 require 'test/unit' require 'taskjuggler/AlgorithmDiff' class AlgorithmDiff < Test::Unit::TestCase class TestData < Struct.new(:name, :a, :b, :a_b, :b_a) end def test_editScript_and_patch data = [ TestData.new("identical inputs", [ 1, 2, 3 ], [ 1, 2, 3 ], [ ], [ ] ), TestData.new("delete 1 element in the middle", [ 1, 2, 3 ], [ 1, 3 ], [ '2d1' ], [ '2i2' ] ), TestData.new("delete 2 elements in the middle", [ 1, 2, 3, 4 ], [ 1, 4 ], [ '2d2' ], [ '2i2,3' ] ), TestData.new("delete 2 elements at 2 different locations", [ 1, 2, 3, 4, 5 ], [ 1, 3, 5 ], [ '2d1', '4d1' ], [ '2i2', '4i4' ] ), TestData.new("delete 2 and insert 1 elements at 2 different locations", [ 1, 2, 3, 5, 6, 7 ], [ 1, 3, 4, 5, 7 ], [ '2d1', '3i4', '5d1' ], [ '2i2', '3d1', '5i6' ] ), TestData.new("delete at start", [ 1, 2 ], [ 2 ], [ '1d1' ], [ '1i1' ] ), TestData.new("delete at end", [ 1, 2 ], [ 1 ], [ '2d1' ], [ '2i2' ] ), TestData.new("delete all", [ 1 ], [ ], [ '1d1' ], [ '1i1' ] ), TestData.new("replace 1 in the middle", [ 1, 0, 3 ], [ 1, 2, 3 ], [ '2d1', '2i2' ], [ '2d1', '2i0' ] ), TestData.new("replace 2 in the middle", [ 1, 0, 0, 4 ], [ 1, 2, 3, 4 ], [ '2d2', '2i2,3' ], [ '2d2', '2i0,0' ] ), TestData.new("many similar values, some changes", [ 1, 0, 0, 1, 0, 1, 1, 0, 1, 1, 1, 1, 0, 0, 1 ], [ 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 0, 1 ], [ '3i1,1', '7d1', '11d1', '12i0' ], [ '3d2', '7i1', '12d1', '11i1' ] ) ] data.each do |set| set.a.extend(Diffable) set.b.extend(Diffable) res = (diff = set.a.diff(set.b)).editScript assert_equal(set.a_b, res, "A->B edit script #{set.name} failed") assert_equal(set.b, set.a.patch(diff), "A->B patch #{set.name} failed") res = (diff = set.b.diff(set.a)).editScript assert_equal(set.b_a, res, "B->A edit script #{set.name} failed") assert_equal(set.a, set.b.patch(diff), "B->A patch #{set.name} failed") end end def test_StringDiff data = [ TestData.new("Some insertions, some changes, some deletions", "0\n1\n2\n4\n5\n6\n7\n", "0\n2\nA\nB\n6\n5\n7\n \n", "2d1\n< 1\n4,5c3,4\n< 4\n< 5\n---\n> A\n> B\n6a6\n> 5\n7a8\n> \n", "1a2\n> 1\n3,5c4\n< A\n< B\n< 6\n---\n> 4\n6a6\n> 6\n8d7\n< \n" ) ] data.each do |set| set.a.extend(DiffableString) set.b.extend(DiffableString) res = (diff = set.a.diff(set.b)).to_s assert_equal(set.a_b, res, "A->B text diff #{set.name} failed") assert_equal(set.b, set.a.patch(diff), "A->B text patch #{set.name} failed") res = (diff = set.b.diff(set.a)).to_s assert_equal(set.b_a, res, "B->A text diff #{set.name} failed") assert_equal(set.a, set.b.patch(diff), "B->A text patch #{set.name} failed") end end end TaskJuggler-3.8.1/test/test_BatchProcessor.rb000066400000000000000000000046501473026623400212450ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = test_BatchProcessor.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # $:.unshift File.join(File.dirname(__FILE__), '..', 'lib') if __FILE__ == $0 require 'test/unit' require 'taskjuggler/BatchProcessor' class TestProject < Test::Unit::TestCase def setup @t = Thread.new do # Timout for the watchdog timer. Even slow hardware should finish each # test in under 30s. sleep(120) assert(false, 'Test timed out') end end def teardown @t.kill end def test_basic doRun(1, 1) { sleep 0.1 } end def test_simple doRun(1, 1) { sleep 0.1 } doRun(1, 2) { sleep 0.1 } doRun(1, 7) { sleep 0.1 } doRun(2, 1) { sleep 0.1 } doRun(2, 2) { sleep 0.1 } doRun(2, 33) { sleep 0.1 } doRun(3, 1) { sleep 0.1 } doRun(3, 3) { sleep 0.1 } doRun(3, 67) { sleep 0.1 } end # This test case triggers a Ruby 1.9.x mutex bug def test_fileIO doRun(3, 80) do fname = "test#{$$}.txt" f = File.new(fname, 'w') 0.upto(10000) { |i| f.puts "#{i} Hello, world!" } f.close File.delete(fname) end end def doRun(maxCPUs, jobs, &block) bp = TaskJuggler::BatchProcessor.new(maxCPUs) jobs.times do |i| bp.queue("job #{i}") { runJob(i, &block) } end @cnt = 0 lock = Monitor.new bp.wait do |j| postprocess(j) lock.synchronize { @cnt += 1 } end assert_equal(jobs, @cnt, "Not all threads terminated propertly (#{@cnt})") end def runJob(n, &block) puts "Job #{n} started" yield $stderr.puts "Error #{n}" if n % 2 == 0 puts "Job #{n} finished" # Return the job ID as return value n end def postprocess(job) assert_equal(job.retVal, job.jobId, 'PID mismatch') assert_equal("job #{job.jobId}", job.tag) text = <<"EOT" Job #{job.jobId} started Job #{job.jobId} finished EOT assert_equal(text, job.stdout, "STDOUT mismatch #{job.stdout}") if job.jobId % 2 == 0 text = "Error #{job.jobId}\n" else text = '' end assert_equal(text, job.stderr, "STDERR mismatch #{job.stderr}") end end TaskJuggler-3.8.1/test/test_CSV-Reports.rb000066400000000000000000000074671473026623400204240ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = test_CSV-Reports.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # $:.unshift File.join(File.dirname(__FILE__), '..', 'lib') if __FILE__ == $0 $:.unshift File.dirname(__FILE__) require 'stringio' require 'test/unit' require 'MessageChecker' require 'taskjuggler/TaskJuggler' require 'taskjuggler/reports/CSVFile' require 'taskjuggler/AlgorithmDiff' class TestScheduler < Test::Unit::TestCase include MessageChecker # This function captures the $stdout output of the passed block to a String # and returns it. def captureStdout oldStdOut = $stdout $stdout = (out = StringIO.new) begin yield ensure $stdout = oldStdOut end out.string end # This functions redirects all output of the passed block to a new file with # the name fileName. def stdoutToFile(fileName) oldStdOut = $stdout $stdout = File.open(fileName, 'w') begin yield ensure $stdout = oldStdOut end end # Compare the output CSV (passed as String) with the content of the CSV # reference files _refFile_. def compareCSVs(outStr, refFile) refStr = File.new(refFile, 'r').read diff = refStr.extend(DiffableString).diff(outStr).to_s if diff != '' puts diff File.new('failed.csv', 'w').write(outStr) ref = TaskJuggler::CSVFile.new.parse(refStr) out = TaskJuggler::CSVFile.new.parse(outStr) assert(ref.length == out.length, "Line number mismatch (#{out.length} instead of #{ref.length}) " + "in #{refFile}") 0.upto(ref.length - 1) do |line| refLine = ref[line] outLine = out[line] assert(refLine.length == outLine.length, "Line #{line} size mismatch (#{outLine.length} instead of " + "#{refLine.length}) in #{refFile}") 0.upto(refLine.length - 1) do |cell| assert(refLine[cell] == outLine[cell], "Cell #{cell} of line #{line} mismatch: " + "'#{outLine[cell]}' instead of '#{refLine[cell]}' " + "in #{refFile}") end end end end def checkCSVReport(projectFile) baseDir = File.dirname(projectFile) baseName = File.basename(projectFile, '.tjp') # The reference files must have the same base name as the project file but # they need to be in the ./refs/ directory relative to the project file. refFile = baseDir + "/refs/#{baseName}.csv" (mh = TaskJuggler::MessageHandlerInstance.instance).reset tj = TaskJuggler.new assert(tj.parse([ projectFile ]), "Parser failed for #{projectFile}") assert(tj.schedule, "Scheduler failed for #{projectFile}") if File.file?(refFile) # If there is a reference CSV file for this test case, compare the # output against it. out = captureStdout do assert(tj.generateReports(baseDir), "Report generation failed for #{projectFile}") end compareCSVs(out, refFile) else # If not, we generate the reference file. puts "refFile: #{refFile}" stdoutToFile(refFile) do assert(tj.generateReports, "Reference file generation failed for #{projectFile}") end end assert(mh.messages.empty?, "Unexpected error in #{projectFile}") end def test_CSV_Reports path = File.dirname(__FILE__) testDir = path + '/TestSuite/CSV-Reports/' Dir.glob(testDir + '*.tjp').each do |f| TaskJuggler::TjTime.setTimeZone('Europe/Berlin') checkCSVReport(f) end end end TaskJuggler-3.8.1/test/test_CSVFile.rb000066400000000000000000000033441473026623400175560ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = test_CSVFile.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # $:.unshift File.join(File.dirname(__FILE__), '..', 'lib') if __FILE__ == $0 require 'test/unit' require 'taskjuggler/reports/CSVFile' class TestCSVFile < Test::Unit::TestCase def test_to_s csv = TaskJuggler::CSVFile.new([ [ "foo", "bar" ], [ "rab", "oof" ] ]) ref = < # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # $:.unshift File.join(File.dirname(__FILE__), '..', 'lib') if __FILE__ == $0 require 'test/unit' require 'taskjuggler/reports/GanttRouter' class TaskJuggler class TestCollisionDetector < Test::Unit::TestCase def test_collisions # To test the collion?() method we use a fix block area and then try # various lines that either collide or not. # # 2,2 # +--+ # | | # +--+ # 4,4 # # We use the same set of lines for horizontal and vertical tests. The # first value is the x or y coordinate of the line, the tuple is the start # and end coordinate in the other dimension. The third value is the # expected result. lines = [ [ [ 1, [0, 1] ], false ], [ [ 2, [0, 1] ], false ], [ [ 3, [0, 1] ], false ], [ [ 4, [0, 1] ], false ], [ [ 5, [0, 1] ], false ], [ [ 1, [0, 2] ], false ], [ [ 2, [0, 2] ], true ], [ [ 3, [0, 2] ], true ], [ [ 4, [0, 2] ], true ], [ [ 5, [0, 2] ], false ], [ [ 1, [0, 4] ], false ], [ [ 2, [0, 4] ], true ], [ [ 3, [0, 4] ], true ], [ [ 4, [0, 4] ], true ], [ [ 5, [0, 4] ], false ], [ [ 1, [0, 5] ], false ], [ [ 2, [0, 5] ], true ], [ [ 3, [0, 5] ], true ], [ [ 4, [0, 5] ], true ], [ [ 5, [0, 6] ], false ], [ [ 1, [4, 6] ], false ], [ [ 2, [4, 6] ], true ], [ [ 3, [4, 6] ], true ], [ [ 4, [4, 6] ], true ], [ [ 5, [4, 6] ], false ], [ [ 1, [5, 6] ], false ], [ [ 2, [5, 6] ], false], [ [ 3, [5, 6] ], false ], [ [ 4, [5, 6] ], false ], [ [ 5, [5, 6] ], false ] ] # Try horizontal lines first. cd = CollisionDetector.new(10, 10) cd.addBlockedZone(2, 2, 3, 3, true, true) lines.each do |line| assert_equal(line[1], cd.collision?(*(line[0] + [ true ])), "Horizontal #{line[0]} is not #{line[1]}") end lines.each do |line| assert_equal(line[1], cd.collision?(*(line[0] + [ false ])), "Vertical #{line[0]} is not #{line[1]}") end end end end TaskJuggler-3.8.1/test/test_Export-Reports.rb000066400000000000000000000077571473026623400212540ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = test_Export-Reports.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # $:.unshift File.join(File.dirname(__FILE__), '..', 'lib') if __FILE__ == $0 $:.unshift File.dirname(__FILE__) require 'stringio' require 'test/unit' require 'MessageChecker' require 'taskjuggler/TaskJuggler' require 'taskjuggler/AlgorithmDiff' class TaskJuggler class TestExportReport < Test::Unit::TestCase include MessageChecker # This function captures the $stdout output of the passed block to a String # and returns it. def captureStdout oldStdOut = $stdout $stdout = (out = StringIO.new) begin yield ensure $stdout = oldStdOut end out.string end # This functions redirects all output of the passed block to a new file with # the name fileName. def stdoutToFile(fileName) oldStdOut = $stdout $stdout = File.open(fileName, 'w') begin yield $stdout.close ensure $stdout = oldStdOut end end # Compare the output Export (passed as String in _out_) with the content of # the Export reference files _refFile_. def compareExports(out, refFile, testCase) ref = File.new(refFile, 'r').read diff = ref.extend(DiffableString).diff(out).to_s if diff != '' File.new('failed.tjp', 'w').write(out) end assert_equal('', diff, "output for #{testCase} does not match " + "#{refFile}:\n#{diff}") end def checkExportReport(projectFile, repFile, refFile) tj = TaskJuggler.new assert(tj.parse([ projectFile, repFile ]), "Parser failed for #{projectFile}") # Schedule the project. assert(tj.schedule, "Scheduler failed for #{projectFile}") tj.project.reports.each do |report| next unless report.get('formats').include?(:tjp) if File.file?(refFile) # If there is a reference Export file for this test case, compare the # output against it. out = captureStdout do assert(tj.generateReport(report.fullId, false), "Report generation failed for #{projectFile}") end compareExports(out, refFile, projectFile) else # If not, we generate the reference file. stdoutToFile(refFile) do assert(tj.generateReport(report.fullId, false), "Reference file generation failed for #{projectFile}") end end end assert(MessageHandlerInstance.instance.messages.empty?, "Unexpected error in #{projectFile}") end def test_Export_Reports path = File.dirname(__FILE__) ENV['TEST1'] = 't_e_s_t_1' ENV['TEST2'] = '"A test String"' ENV['TEST3'] = '3' testDir = path + '/TestSuite/Syntax/Correct/' Dir.glob(testDir + '*.tjp').each do |f| # We ignore some test cases that cannot work in this setup. next if %w( Freeze.tjp Export.tjp ).include?(f[testDir.length..-1]) # Take the project, schedule it, check it against the reference and # export it. Then check the export against the reference file. refFile = refFileName(f) repFile = reportDefFileName(f) checkExportReport(f, repFile, refFile) checkExportReport(refFile, repFile, refFile) end end private def refFileName(originalFile) baseDir = File.dirname(originalFile) baseName = File.basename(originalFile, '.tjp') baseDir + "/../../Export-Reports/refs/#{baseName}.tjp" end def reportDefFileName(originalFile) File.dirname(originalFile) + "/../../Export-Reports/export.tji" end end end TaskJuggler-3.8.1/test/test_Journal.rb000066400000000000000000000136471473026623400177440ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = test_Journal.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # $:.unshift File.join(File.dirname(__FILE__), '..', 'lib') if __FILE__ == $0 require 'test/unit' require 'taskjuggler/Project' class TaskJuggler class TestJournal < Test::Unit::TestCase class PTNMockup attr_reader :index def initialize(index) @index = index end def ptn self end end def setup @p = TaskJuggler::Project.new('hello', 'Hello World', '1.0') @p['start'] = tm('2009-11-01') @p['end'] = tm('2009-12-31') @j = @p['journal'] end def teardown end def test_add # First some simple add tests. @j.addEntry(e = JournalEntry.new(@j, tm('2009-11-29'), "E1", PTNMockup.new(1))) assert_equal(1, @j.entries.count) # Make sure we don't add the same entry twice. @j.addEntry(e) assert_equal(1, @j.entries.count) @j.addEntry(JournalEntry.new(@j, tm('2009-11-30'), "E2", PTNMockup.new(2))) @j.addEntry(JournalEntry.new(@j, tm('2009-12-01'), "E3", PTNMockup.new(3))) assert_equal(3, @j.entries.count) end def test_sort # Add a bunch of entries and see if the sorting by date works properly. @j.addEntry(JournalEntry.new(@j, tm('2009-12-10'), "E4", PTNMockup.new(4))) @j.addEntry(JournalEntry.new(@j, tm('2009-12-03'), "E2", PTNMockup.new(2))) @j.addEntry(JournalEntry.new(@j, tm('2009-12-06'), "E3", PTNMockup.new(3))) @j.addEntry(JournalEntry.new(@j, tm('2009-11-29'), "E0", PTNMockup.new(0))) @j.addEntry(JournalEntry.new(@j, tm('2009-12-01'), "E1", PTNMockup.new(1))) @j.addEntry(JournalEntry.new(@j, tm('2009-12-24'), "E5", PTNMockup.new(5))) pList = [] @j.entries.each { |e| pList << e.property } pList.each do |i| assert_equal(pList.index(i), i.index) end end def test_sortSameDate # Add a bunch of entries and see if the sorting by date works properly. @j.addEntry(e = JournalEntry.new(@j, tm('2009-12-10'), "A2", PTNMockup.new(0))) e.alertLevel = 2 @j.addEntry(e = JournalEntry.new(@j, tm('2009-12-10'), "A0", PTNMockup.new(0))) e.alertLevel = 0 @j.addEntry(e = JournalEntry.new(@j, tm('2009-12-10'), "A1", PTNMockup.new(0))) e.alertLevel = 1 @j.addEntry(e = JournalEntry.new(@j, tm('2009-12-10'), "A3", PTNMockup.new(0))) e.alertLevel = 3 i = 0 @j.entries.each do |entry| assert_equal(i, entry.alertLevel) i += 1 end end def test_currentEntries createTaskTree q = Query.new q.scenarioIdx = 0 # Set a 0 alert for a task a1 = addAlert('2009-11-29', 0, t = task('p1.m1.l1')) ce = @j.currentEntriesR(tm('2009-12-05'), t, 0, nil, q) assert_equal(1, ce.count) assert_equal(a1, ce[0]) # Add a later alert for the same task a2 = addAlert('2009-11-30', 0, t = task('p1.m1.l1')) ce = @j.currentEntriesR(tm('2009-12-05'), t, 0, nil, q) assert_equal(1, ce.count) assert_equal(a2, ce[0]) # Add another alert to the sister task and check parent a3 = addAlert('2009-11-30', 0, t = task('p1.m1.l2')) ce = @j.currentEntriesR(tm('2009-12-05'), task('p1.m1'), 0, nil, q) assert_equal(2, ce.count) assert_equal(a2, ce[0]) assert_equal(a3, ce[1]) # Check root task ce = @j.currentEntriesR(tm('2009-12-05'), task('p1'), 0, nil, q) assert_equal(2, ce.count) assert_equal(a2, ce[0]) assert_equal(a3, ce[1]) # Add old override alert to p1.m1 addAlert('2009-11-29', 0, t = task('p1.m1')) ce = @j.currentEntriesR(tm('2009-12-05'), task('p1'), 0, nil, q) assert_equal(2, ce.count) assert_equal(a2, ce[0]) assert_equal(a3, ce[1]) # Add new override alert to p1.m1 a4 = addAlert('2009-12-01', 0, t = task('p1.m1')) ce = @j.currentEntriesR(tm('2009-12-05'), task('p1'), 0, nil, q) assert_equal(1, ce.count) assert_equal(a4, ce[0]) end def test_alertSimple createTaskTree q = Query.new q.scenarioIdx = 0 # Set a 0 alert for a task addAlert('2009-11-29', 0, t = task('p1.m1.l1')) assert_equal(0, @j.alertLevel(tm('2009-12-01'), t, q)) # Now add a later 1 alert addAlert('2009-12-01', 1, t) assert_equal(1, @j.alertLevel(tm('2009-12-02'), t, q)) # Set a 2 alert for p1.m1.l2 addAlert('2009-11-29', 2, task('p1.m1.l2')) assert_equal(2, @j.alertLevel(tm('2009-12-01'), task('p1'), q)) # Overide p1.m1 with 0 alert addAlert('2009-12-01', 0, task('p1.m1')) assert_equal(0, @j.alertLevel(tm('2009-12-01'), task('p1'), q)) end private def tm(s) TjTime.new(s) end def addAlert(date, level, property) raise "No property" unless property @j.addEntry(e = JournalEntry.new(@j, tm(date), "Set #{property.fullId} " + "to level #{level}i at #{date}", property)) e.alertLevel = level e end def createTaskTree p1 = newTask(nil, 'p1') m1 = newTask(p1, 'm1') newTask(p1, 'm2') newTask(m1, 'l1') newTask(m1, 'l2') newTask(nil, 'p2') end def newTask(parent, id) Task.new(@p, id, 'Task #{id}', parent) end def task(id) @p.task(id) || ( raise "Unknown task id #{id}" ) end end end TaskJuggler-3.8.1/test/test_Limits.rb000066400000000000000000000106541473026623400175660ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = test_Limits.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # $:.unshift File.join(File.dirname(__FILE__), '..', 'lib') if __FILE__ == $0 require 'test/unit' require 'taskjuggler/Limits' require 'taskjuggler/Project' require 'taskjuggler/TjTime' class TaskJuggler class TestLimits < Test::Unit::TestCase def setup @p = Project.new('p1', 'p 1', '1.0') @p['start'] = TjTime.new('2009-01-21') @p['end'] = TjTime.new('2009-03-01') end def teardown @p = nil end def test_new l1 = Limits.new l1.setProject(@p) l2 = Limits.new(l1) assert_equal(l1.project, l2.project, "Copy constructor failed") end def test_setLimit l = Limits.new l.setProject(@p) l.setLimit('dailymax', 4) assert_equal(l.limits.length, 1, 'setLimits() failed') l.setLimit('dailymax', 6) assert_equal(l.limits.length, 1, 'setLimits() replace failed') l.setLimit('weeklymax', 20) assert_equal(l.limits.length, 2, 'setLimits() failed') end def test_inc l = Limits.new l.setProject(@p) l.setLimit('weeklymax', 2, ScoreboardInterval.new(@p['start'], @p['scheduleGranularity'], TjTime.new('2009-02-10'), TjTime.new('2009-02-15'))) # Outside of limit interval, should be ignored l.inc(-1) l.inc(100000) assert(l.ok?) # Inside the calendar week interval l.inc(dateToIdx('2009-02-09-10:00')) assert(l.ok?) # The inc will exceed the weekly limit l.inc(dateToIdx('2009-02-09-11:00')) assert(!l.ok?) end def test_ok l = Limits.new l.setProject(@p) l.setLimit('dailymax', 4) assert_equal(l.limits.length, 1, 'setLimits() failed') l.inc(dateToIdx('2009-02-01-10:00')) assert(l.ok?) l.inc(dateToIdx('2009-02-01-11:00')) assert(l.ok?) l.inc(dateToIdx('2009-02-01-12:00')) assert(l.ok?) l.inc(dateToIdx('2009-02-01-13:00')) assert(!l.ok?) assert(l.ok?(dateToIdx('2009-01-31'))) assert(!l.ok?(dateToIdx('2009-02-01'))) assert(l.ok?(dateToIdx('2009-02-01'), false)) end def test_with_resource_1 l = Limits.new l.setProject(@p) l.setLimit('dailymax', 4) r = Resource.new(@p, 'r', 'R', nil) l.setLimit('dailymax', 5, nil, r) l.inc(dateToIdx('2009-02-01-10:00')) assert(l.ok?) l.inc(dateToIdx('2009-02-01-11:00')) assert(l.ok?) l.inc(dateToIdx('2009-02-01-12:00')) assert(l.ok?) l.inc(dateToIdx('2009-02-01-13:00')) assert(!l.ok?) assert(!l.ok?(nil, true, r)) end def test_with_resource_2 l = Limits.new l.setProject(@p) l.setLimit('dailymax', 5) r = Resource.new(@p, 'r', 'R', nil) l.setLimit('dailymax', 1, nil, r) l.inc(dateToIdx('2009-02-01-10:00')) assert(l.ok?) l.inc(dateToIdx('2009-02-01-11:00')) assert(l.ok?) l.inc(dateToIdx('2009-02-01-12:00')) assert(l.ok?) l.inc(dateToIdx('2009-02-01-13:00')) assert(l.ok?) assert(l.ok?(nil, true, r)) l.inc(dateToIdx('2009-02-01-14:00'), r) assert(!l.ok?) assert(!l.ok?(nil, true, r)) end def test_with_resource_3 l = Limits.new l.setProject(@p) l.setLimit('dailymax', 5) r = Resource.new(@p, 'r', 'R', nil) l.setLimit('dailymax', 3, nil, r) l.inc(dateToIdx('2009-02-01-10:00'), r) assert(l.ok?) l.inc(dateToIdx('2009-02-01-11:00')) assert(l.ok?) l.inc(dateToIdx('2009-02-01-12:00'), r) assert(l.ok?) l.inc(dateToIdx('2009-02-01-13:00'), r) assert(l.ok?) assert(!l.ok?(nil, true, r)) end def test_with_resource_4 l = Limits.new l.setProject(@p) l.setLimit('dailymax', 2) r = Resource.new(@p, 'r', 'R', nil) l.setLimit('dailymax', 3, nil, r) l.inc(dateToIdx('2009-02-01-10:00'), r) assert(l.ok?) l.inc(dateToIdx('2009-02-01-11:00')) assert(!l.ok?) l.inc(dateToIdx('2009-02-01-12:00'), r) assert(!l.ok?) assert(!l.ok?(nil, true, r)) l.inc(dateToIdx('2009-02-01-13:00'), r) assert(!l.ok?) assert(!l.ok?(nil, true, r)) end private def dateToIdx(date) @p.dateToIdx(TjTime.new(date)) end end end TaskJuggler-3.8.1/test/test_LogicalExpression.rb000066400000000000000000000060311473026623400217510ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = test_LogicalExpression.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # $:.unshift File.join(File.dirname(__FILE__), '..', 'lib') if __FILE__ == $0 require 'test/unit' require 'taskjuggler/LogicalExpression' class TaskJuggler class TestLogicalExpression < Test::Unit::TestCase def setup end def teardown end def test_unaryOperations parameter = [ [ true, '~', false ], [ false, '~', true ] ] parameter.each do |op, operator, result| exp = LogicalExpression.new(unaryOp(op, operator)) assert_equal(result, exp.eval(nil), "Operation #{operator} #{op} -> #{result} failed") end end def test_binaryOperations parameter = [ [ 2, '<', 3, true ], [ 3, '<', 2, false ], [ 2, '<', 2, false ], [ 4, '>', 5, false ], [ 5, '>', 4, true ], [ 5, '>', 5, false ], [ 'a', '>', 'b', false ], [ 'b', '>', 'a', true ], [ 2, '<=', 3, true ], [ 3, '<=', 2, false ], [ 2, '<=', 2, true], [ 4, '>=', 5, false ], [ 5, '>=', 4, true ], [ 5, '>=', 5, true], [ 'A', '>=', 'B', false ], [ 'A', '>=', 'A', true ], [ 'B', '>=', 'B', true ], [ 6, '=', 5, false ], [ 6, '=', 6, true], [ 'c', '=', 'c', true ], [ 'x', '=', 'y', false ], [ '', '=', '', true ], [ '', '=', 'a', false ], [ true, '&', true, true ], [ true, '&', false, false ], [ false, '&', true, false ], [ false, '&', false, false ], [ 1, '&', 1, true ], [ 1, '&', 0, false ], [ 0, '&', 1, false ], [ 0, '&', 0, false ], [ true, '|', true, true ], [ true, '|', false, true ], [ false, '|', true, true ], [ false, '|', false, false ] ] parameter.each do |op1, operator, op2, result| exp = LogicalExpression.new(binaryOp(op1, operator, op2)) assert_equal(result, exp.eval(nil), "Operation #{op1} #{operator} #{op2} -> #{result} failed") end end def test_operationTrees op1 = binaryOp(2, '<', 4) op2 = binaryOp(3, '>', 6) exp = LogicalExpression.new(binaryOp(op1, '|', op2)) assert_equal(true, exp.eval(nil), "Operation #{exp} -> true failed") end def test_exceptions begin exp = LogicalExpression.new(binaryOp(false, '<', true)) assert_raise TjException do exp.eval(nil) end rescue TjException end end private def binaryOp(op1, operator, op2) LogicalOperation.new(LogicalOperation.new(op1), operator, LogicalOperation.new(op2)) end def unaryOp(op, operator) LogicalOperation.new(LogicalOperation.new(op), operator) end end end TaskJuggler-3.8.1/test/test_MacroTable.rb000066400000000000000000000032321473026623400203300ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = test_MacroTable.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # $:.unshift File.join(File.dirname(__FILE__), '..', 'lib') if __FILE__ == $0 require 'test/unit' class TaskJuggler require 'taskjuggler/TextParser/MacroTable' class TestMacroTable < Test::Unit::TestCase def setup @mt = TaskJuggler::TextParser::MacroTable.new @t = Thread.new do sleep(1) assert('Test timed out') end end def teardown @mt.clear @t.kill end def test_addAndClear @mt.add(TaskJuggler::TextParser::Macro.new('macro1', 'This is macro 1', nil)) @mt.add(TaskJuggler::TextParser::Macro.new('macro2', 'This is macro 2', nil)) @mt.clear end def test_resolve @mt.add(TaskJuggler::TextParser::Macro.new('macro1', 'This is macro 1', nil)) @mt.add(TaskJuggler::TextParser::Macro.new( 'macro2', 'This is macro 2 with ${1} and ${2}', nil)) assert_equal('This is macro 1', @mt.resolve(%w( macro1 ), nil)[1]) assert_equal('This is macro 2 with arg1 and arg2', @mt.resolve(%w( macro2 arg1 arg2), nil)[1]) assert_equal('This is macro 2 with arg1 and arg2', @mt.resolve(%w( macro2 arg1 arg2 arg3), nil)[1]) assert_equal('This is macro 2 with arg1 and ${2}', @mt.resolve(%w( macro2 arg1), nil)[1]) end end end TaskJuggler-3.8.1/test/test_Project.rb000066400000000000000000000026111473026623400177250ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = test_Project.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # $:.unshift File.join(File.dirname(__FILE__), '..', 'lib') if __FILE__ == $0 require 'test/unit' require 'taskjuggler/Project' class TaskJuggler class TestProject < Test::Unit::TestCase def setup end def teardown end def test_helloWorld p = TaskJuggler::Project.new('hello', 'Hello World', '1.0') p['start'] = TjTime.new('2008-07-24') p['end'] = TjTime.new('2008-08-31') assert_equal(p['projectid'], 'hello') assert_equal(p['name'], 'Hello World') assert_equal(p['version'], '1.0') assert_equal(p.scenarioCount, 1) assert_equal(p.scenarioIdx('plan'), 0) assert_equal(p.scenario(0), p.scenario('plan')) p['rate'] = 100.0 assert_equal(p['rate'], 100.0) t = Task.new(p, 'foo', 'Foo', nil) t['start', 0] = TjTime.new('2008-07-25-9:00') t['duration', 0] = 10 assert_equal(p.task('foo'), t) p.schedule assert_equal(TjTime.new('2008-07-25-19:00'), t['end', 0]) p.generateReports(1) end end end TaskJuggler-3.8.1/test/test_ProjectFileScanner.rb000066400000000000000000000076261473026623400220520ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = test_ProjectFileScanner.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # $:.unshift File.join(File.dirname(__FILE__), '..', 'lib') require 'test/unit' require 'taskjuggler/ProjectFileScanner' require 'taskjuggler/MessageHandler' class TaskJuggler class TestProjectFileScanner < Test::Unit::TestCase def setup end def teardown end def test_basic text = <<'EOT' Hello world 1 2.0 # Comment 2008-12-14 // Another comment foo: a.b.c - $ [A Macro] 15:23 "A string" 'It\'s a string' "A mult\"i line string" /* a comment */ EOT ref = [ [:ID, 'Hello', 1], [:ID, 'world', 1], [:INTEGER, 1, 1], [:FLOAT, 2.0, 2], [:DATE, TjTime.new('2008-12-14'), 3], [:ID_WITH_COLON, 'foo', 5], [:ABSOLUTE_ID, 'a.b.c', 6], [:LITERAL, '-', 6], [:LITERAL, '$', 6], [:MACRO, 'A Macro', 6], [:TIME, mktime(15, 23), 7], [:STRING, 'A string', 7], [:STRING, "It's a string", 8], [:STRING, "A\nmult\"i line\nstring", 9], [:eof, '', 14 ] ] check(text, ref) end def test_macro text = <<'EOT' This ${adj} software ${m1 "arg1"} EOT macros = [ [ 'adj', 'great' ], [ 'm1', 'macro with ${1} argument' ] ] ref = [ [:ID, 'This', 1], [:ID, 'great', 1], [:ID, 'software', 1], [:ID, 'macro', 2], [:ID, 'with', 2], [:ID, 'arg1', 2], [:ID, 'argument', 2], [:eof, '', 3] ] check(text, ref, macros) end def test_time text = <<'EOT' 0:00 00:00 1:00 11:59 12:01 24:00 EOT ref = [ [:TIME, mktime(0, 0), 1], [:TIME, mktime(0, 0), 2], [:TIME, mktime(1, 0), 3], [:TIME, mktime(11, 59), 4], [:TIME, mktime(12, 1), 5], [:TIME, mktime(24, 0), 6], [:eof, '', 7] ] check(text, ref) end def test_date text = <<'EOT' 1970-01-01 2035-12-31-23:59:59 2010-08-11-23:10 EOT ref = [ [:DATE, TjTime.new('1970-01-01'), 1], [:DATE, TjTime.new('2035-12-31-23:59:59'), 2], [:DATE, TjTime.new('2010-08-11-23:10'), 3], [:eof, '', 4] ] check(text, ref) end def test_macroDef text = <<'EOT' [ foo ] [ bar ] [ foo ] [ bar ] [] [ ] EOT ref = [ [ :MACRO, ' foo ', 1 ], [ :MACRO, "\n bar ", 2 ], [ :MACRO, " foo\n ", 4 ], [ :MACRO, "\n bar\n ", 6 ], [ :MACRO, '', 9 ], [ :MACRO, "\n ", 10 ], [ :eof, '', 13 ] ] check(text, ref) end def test_macroCall text = '${foo}' macros = [ [ 'foo', 'hello' ] ] ref = [ [ :ID, 'hello', 1 ] ] check(text, ref, macros) end private def mktime(h, m) (h * 60 + m) * 60 end def check(text, ref, macros = []) s = TaskJuggler::ProjectFileScanner.new(text) s.open(true) macros.each do |macro| s.addMacro(TaskJuggler::TextParser::Macro.new(macro[0], macro[1], nil)) end ref.each do |type, val, line| token = s.nextToken assert_equal([ type, val ], token[0..1], "1: Bad token #{token[1]} instead of #{val}") assert_equal(line, token[2].lineNo, "1: Bad line number #{token[2].lineNo} instead of #{line} for #{val}") s.returnToken(token) token = s.nextToken assert_equal([ type, val ], token[0..1], "2: Bad token #{token[1]} instead of #{val}") assert_equal(line, token[2].lineNo, "2: Bad line number #{token[2].lineNo} instead of #{line} for #{val}") end s.close end end end TaskJuggler-3.8.1/test/test_PropertySet.rb000066400000000000000000000035161473026623400206240ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = test_PropertySet.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # $:.unshift File.join(File.dirname(__FILE__), '..', 'lib') if __FILE__ == $0 require 'test/unit' require 'taskjuggler/Project' class TaskJuggler class TestPropertySet < Test::Unit::TestCase def setup end def teardown end def test_index p = TaskJuggler::Project.new('p', 'Project', '1.0') p['start'] = TjTime.new('2008-07-29') p['end'] = TjTime.new('2008-08-31') # This set of Arrays describes the tree structure that we want to test. # Each Array element is an tuple of breakdown structure idex and parent node. nodes = [ [ '1', nil ], [ '1.1', '1' ], [ '1.1.1', '1.1' ], [ '1.1.2', '1.1' ], [ '1.2', '1' ], [ '1.1.3', '1.1'], [ '2', nil ], [ '2.1', '2' ] ] # Now we create the nodes according to the above list. i = 0 nodes.each do |id, parent| # For the node id we use the expected bsi result. Task.new(p, id, "Node #{id}", parent ? p.task(parent) : nil) Resource.new(p, id, "Node #{id}", parent ? p.resource(parent) : nil) i += 1 end p.tasks.index p.resources.index p.tasks.each do |t| assert_equal(t.fullId, t.get('bsi')) end p.tasks.removeProperty('1.1') p.tasks.index assert_equal('1.1', p.task('1.2').get('bsi')) p.resources.each do |r| assert_equal(r.fullId, r.get('bsi')) end end end end TaskJuggler-3.8.1/test/test_Query.rb000066400000000000000000000053401473026623400174260ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = test_Query.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # $:.unshift File.join(File.dirname(__FILE__), '..', 'lib') if __FILE__ == $0 require 'test/unit' require 'taskjuggler/Project' require 'taskjuggler/Query' class TaskJuggler class TestQuery < Test::Unit::TestCase def setup @p = TaskJuggler::Project.new('id', 'name', 'ver') @p['start'] = TjTime.new('2010-09-25') @p['end'] = TjTime.new('2010-09-25') end def teardown end def test_scaleDuration q = Query.new('project' => @p, 'numberFormat' => @p['numberFormat']) units = [ :minutes, :hours, :days, :weeks, :months, :shortauto ] vals = [ # Inp mins hours days weeks months shortauto [ 0.0, '0.0', '0.0', '0.0', '0.0', '0.0', '0.0d'], [ 1.0, '1440.0', '24.0', '1.0', '0.1', '0.0', '1.0d'], [ 2.0, '2880.0', '48.0', '2.0', '0.3', '0.1', '2.0d'], [ 3.0, '4320.0', '72.0', '3.0', '0.4', '0.1', '3.0d'], [ 4.0, '5760.0', '96.0', '4.0', '0.6', '0.1', '4.0d'], [ 7.0, '10080.0', '168.0', '7.0', '1.0', '0.2', '7.0d'], [ 14.0, '20160.0', '336.0', '14.0', '2.0', '0.5', '2.0w'], [ 28.0, '40320.0', '672.0', '28.0', '4.0', '0.9', '4.0w'] ] vals.each do |inp, *out| 0.upto(5) do |i| q.loadUnit = units[i] assert_equal(out[i], q.scaleDuration(inp), "Input: #{inp}, Unit #{units[i]}") end end end def test_scaleLoad q = Query.new('project' => @p, 'numberFormat' => @p['numberFormat']) units = [ :minutes, :hours, :days, :weeks, :months, :shortauto ] vals = [ # Inp mins hours days weeks months shortauto [ 0.0, '0.0', '0.0', '0.0', '0.0', '0.0', '0.0d'], [ 0.25, '120.0', '2.0', '0.3', '0.1', '0.0', '2.0h'], [ 0.1, '48.0', '0.8', '0.1', '0.0', '0.0', '0.1d'], [ 0.5, '240.0', '4.0', '0.5', '0.1', '0.0', '0.5d'], [ 1.0, '480.0', '8.0', '1.0', '0.2', '0.0', '1.0d'], [ 1.5, '720.0', '12.0', '1.5', '0.3', '0.1', '1.5d'], [ 2.0, '960.0', '16.0', '2.0', '0.4', '0.1', '2.0d'], [ 5.0, '2400.0', '40.0', '5.0', '1.0', '0.2', '5.0d'], [ 10.0, '4800.0', '80.0', '10.0', '2.0', '0.5', '2.0w'] ] vals.each do |inp, *out| 0.upto(5) do |i| q.loadUnit = units[i] assert_equal(out[i], q.scaleLoad(inp), "Input: #{inp}, Unit #{units[i]}") end end end end end TaskJuggler-3.8.1/test/test_RealFormat.rb000066400000000000000000000047051473026623400203610ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = test_RealFormat.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # $:.unshift File.join(File.dirname(__FILE__), '..', 'lib') if __FILE__ == $0 require 'test/unit' require 'taskjuggler/RealFormat' class TaskJuggler class TestRealFormat < Test::Unit::TestCase def setup end def teardown end def test_frac values = [ # Input 0 1 2 3 fraction digits [ 0.01, '0', '0.0', '0.01', '0.010' ], [ 0.04, '0', '0.0', '0.04', '0.040' ], [ 0.05, '0', '0.1', '0.05', '0.050' ], [ 0.09, '0', '0.1', '0.09', '0.090' ], [ 0.099, '0', '0.1', '0.10', '0.099' ], [ 0.0999, '0', '0.1', '0.10', '0.100' ], [ 0.1, '0', '0.1', '0.10', '0.100' ], [ 0.4, '0', '0.4', '0.40', '0.400' ], [ 0.5, '1', '0.5', '0.50', '0.500' ], [ 0.9, '1', '0.9', '0.90', '0.900' ], [ 0.99, '1', '1.0', '0.99', '0.990' ], [ 0.999, '1', '1.0', '1.00', '0.999' ], [ 0.9999, '1', '1.0', '1.00', '1.000' ], [ 1.0, '1', '1.0', '1.00', '1.000' ], [ 4.0, '4', '4.0', '4.00', '4.000' ], [ 5.0, '5', '5.0', '5.00', '5.000' ], [ 9.0, '9', '9.0', '9.00', '9.000' ], [ 9.9, '10', '9.9', '9.90', '9.900' ], [ 9.999, '10', '10.0', '10.00', '9.999' ], [ 9.9999, '10', '10.0', '10.00', '10.000' ] ] values.each do |inp, *out| 0.upto(3) do |i| f = RealFormat.new(['(', ')', ',', '.', i]) assert_equal(out[i], res = f.format(inp), "Value: #{inp} Digits: #{i} Result: #{res}") end end end def test_negative f = RealFormat.new(['(', ')', ',', '.', 3]) assert_equal(f.format(-Math::PI), '(3.142)') f = RealFormat.new(['-', '', ',', '.', 3]) assert_equal(f.format(-Math::PI), '-3.142') end def test_thousand f = RealFormat.new(['(', ')', ',', '.', 3]) assert_equal(f.format(1234567.8901234), '1,234,567.890') f = RealFormat.new(['(', ')', ',', '.', 3]) assert_equal(f.format(123456.78901234), '123,456.789') f = RealFormat.new(['(', ')', ',', '.', 0]) assert_equal(f.format(-1234.5678901234), '(1,235)') end end end TaskJuggler-3.8.1/test/test_ReportGenerator.rb000066400000000000000000000052621473026623400214460ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = test_ReportGenerator.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # $:.unshift File.join(File.dirname(__FILE__), '..', 'lib') if __FILE__ == $0 $:.unshift File.dirname(__FILE__) require 'test/unit' require 'fileutils' require 'MessageChecker' require 'taskjuggler/Tj3Config' require 'taskjuggler/TaskJuggler' class TestReportGenerator < Test::Unit::TestCase include MessageChecker def setup @tmpDir = 'tmp-test_ReportGenerator' Dir.delete(@tmpDir) if File.directory?(@tmpDir) Dir.mkdir(@tmpDir) AppConfig.appName = 'taskjuggler3' ENV['TASKJUGGLER_DATA_PATH'] = './:../' end def teardown FileUtils::rm_rf(@tmpDir) end def test_ReportGeneratorErrors path = File.dirname(__FILE__) + '/' Dir.glob(path + 'TestSuite/ReportGenerator/Errors/*.tjp').each do |f| ENV['TZ'] = 'Europe/Berlin' (mh = TaskJuggler::MessageHandlerInstance.instance).reset mh.outputLevel = :none mh.trapSetup = true begin tj = TaskJuggler.new assert(tj.parse([ f ]), "Parser failed for #{f}") assert(tj.schedule, "Scheduler failed for #{f}") tj.warnTsDeltas = true tj.generateReports(@tmpDir) rescue TaskJuggler::TjRuntimeError end checkMessages(tj, f) end end def test_ReportGeneratorCorrect path = File.dirname(__FILE__) + '/' Dir.glob(path + 'TestSuite/ReportGenerator/Correct/*.tjp').each do |f| ENV['TZ'] = 'Europe/Berlin' (mh = TaskJuggler::MessageHandlerInstance.instance).reset mh.outputLevel = :none tj = TaskJuggler.new assert(tj.parse([ f ]), "Parser failed for #{f}") assert(tj.schedule, "Scheduler failed for #{f}") assert(tj.generateReports(@tmpDir), "Report generator failed for #{f}") assert(mh.messages.empty?, "Unexpected error in #{f}") checkReports(f) end end private def checkReports(tjpFile) baseName = File.basename(tjpFile)[0..-5] dirName = File.dirname(tjpFile) counter = 0 Dir.glob(dirName + "/refs/#{baseName}-[0-9]*").each do |ref| reportName = File.basename(ref) assert(FileUtils.compare_file(ref, "#{@tmpDir}/#{reportName}"), "Comparison of report #{reportName} of test case #{tjpFile} failed") counter += 1 end assert(counter > 0, "Project #{tjpFile} has no reference report") end end TaskJuggler-3.8.1/test/test_RichText.rb000066400000000000000000000545741473026623400200700ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = test_RichText.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # $:.unshift File.join(File.dirname(__FILE__), '..', 'lib') if __FILE__ == $0 require 'test/unit' require 'taskjuggler/RichText' require 'taskjuggler/RichText/FunctionHandler' require 'taskjuggler/MessageHandler' class RTFDummy < TaskJuggler::RichTextFunctionHandler def initialize() super('dummy') @blockFunction = true end # Return a XMLElement tree that represents the blockfunc in HTML code. def to_html(args) TaskJuggler::XMLElement.new('blockfunc:dummy', args, true) end end class TestRichText < Test::Unit::TestCase def setup end def teardown end def test_empty inp = '' tagged = "
    \n" str = "\n" html = "
    \n" assert_outputs(inp, tagged, str, html) assert_equal(true, newRichText(inp).empty?, 'Empty string') assert_equal(true, newRichText("\n").empty?, '\n') assert_equal(true, newRichText("\n \n").empty?, '\n \n') assert_equal(false, newRichText("foo").empty?, 'foo') end def test_one_word inp = "foo" tagged = "
    [foo]
    \n" str = "foo\n" html= "
    foo
    \n" assert_outputs(inp, tagged, str, html) end def test_two_words inp = "foo bar" tagged = "
    [foo] [bar]
    \n" str = "foo bar\n" html = "
    foo bar
    \n" assert_outputs(inp, tagged, str, html) end def test_paragraph inp = <<'EOT' A paragraph may span multiple lines of text. Single line breaks are ignored. Only 2 successive newlines end the paragraph. I hope this example is clear now. EOT tagged = <<'EOT'

    [A] [paragraph] [may] [span] [multiple] [lines] [of] [text.] [Single] [line] [breaks] [are] [ignored.]

    [Only] [2] [successive] [newlines] [end] [the] [paragraph.]

    [I] [hope] [this] [example] [is] [clear] [now.]

    EOT str = <<'EOT' A paragraph may span multiple lines of text. Single line breaks are ignored. Only 2 successive newlines end the paragraph. I hope this example is clear now. EOT html = <<'EOT'

    A paragraph may span multiple lines of text. Single line breaks are ignored.

    Only 2 successive newlines end the paragraph.

    I hope this example is clear now.

    EOT assert_outputs(inp, tagged, str, html) end def test_hline inp = <<'EOT' ---- Line above and below ---- == A heading == ---- ---- ---- Another bit of text. ---- EOT tagged = <<'EOT'

    ----

    [Line] [above] [and] [below]


    ----

    1 [A] [heading]


    ----
    ----
    ----

    [Another] [bit] [of] [text.]


    ----
    EOT str = <<'EOT' ------------------------------------------------------------ Line above and below ------------------------------------------------------------ 1) A heading ------------------------------------------------------------ ------------------------------------------------------------ ------------------------------------------------------------ Another bit of text. ------------------------------------------------------------ EOT html = <<'EOT'

    Line above and below


    1 A heading




    Another bit of text.


    EOT assert_outputs(inp, tagged, str, html, 60) end def test_italic inp = "This is a text with ''italic words '' in it." tagged = <<'EOT'
    [This] [is] [a] [text] [with] [italic] [words] [in] [it.]
    EOT str = <<'EOT' This is a text with italic words in it. EOT html = <<'EOT'
    This is a text with italic words in it.
    EOT assert_outputs(inp, tagged, str, html) end def test_bold inp = "This is a text with ''' bold words''' in it." tagged = <<'EOT'
    [This] [is] [a] [text] [with] [bold] [words] [in] [it.]
    EOT str = <<'EOT' This is a text with bold words in it. EOT html = <<'EOT'
    This is a text with bold words in it.
    EOT assert_outputs(inp, tagged, str, html) end def test_code inp = "This is a text with ''''monospaced words'''' in it." tagged = <<'EOT'
    [This] [is] [a] [text] [with] [monospaced] [words] [in] [it.]
    EOT str = <<'EOT' This is a text with monospaced words in it. EOT html = <<'EOT'
    This is a text with monospaced words in it.
    EOT assert_outputs(inp, tagged, str, html) end def test_boldAndItalic inp = <<'EOT' This is a text with some '''bold words''', some ''italic'' words and some '''''bold and italic''''' words in it. EOT tagged = <<'EOT'
    [This] [is] [a] [text] [with] [some] [bold] [words][,] [some] [italic] [words] [and] [some] [bold] [and] [italic] [words] [in] [it.]
    EOT str = <<'EOT' This is a text with some bold words, some italic words and some bold and italic words in it. EOT html = <<'EOT'
    This is a text with some bold words, some italic words and some bold and italic words in it.
    EOT assert_outputs(inp, tagged, str, html) end def test_ref inp = <<'EOT' This is a reference [[item]]. For more info see [[manual|the user manual]]. EOT tagged = <<'EOT'
    [This] [is] [a] [reference] [item][.] [For] [more] [info] [see] [the user manual][.]
    EOT str = <<'EOT' This is a reference item. For more info see the user manual. EOT html = <<'EOT'
    This is a reference item. For more info see the user manual.
    EOT assert_outputs(inp, tagged, str, html) end def test_img inp = <<'EOT' This is an [[File:image.jpg]]. For more info see [[File:icon.png|alt=this image]]. EOT tagged = <<'EOT'
    [This] [is] [an] [.] [For] [more] [info] [see] [.]
    EOT str = <<'EOT' This is an . For more info see this image. EOT html = <<'EOT'
    This is an . For more info see .
    EOT assert_outputs(inp, tagged, str, html) end def test_href inp = <<'EOT' This is a reference [http://www.taskjuggler.org]. For more info see [http://www.taskjuggler.org the TaskJuggler site]. EOT tagged = <<'EOT'
    [This] [is] [a] [reference] [http://www.taskjuggler.org][.] [For] [more] [info] [see] [the] [TaskJuggler] [site][.]
    EOT str = <<'EOT' This is a reference http://www.taskjuggler.org. For more info see the TaskJuggler site. EOT html = <<'EOT'
    This is a reference http://www.taskjuggler.org. For more info see the TaskJuggler site.
    EOT assert_outputs(inp, tagged, str, html) end def test_hrefWithWrappedLines inp = <<'EOT' A [http://www.taskjuggler.org multi line] reference. EOT tagged = <<'EOT'
    [A] [multi] [line] [reference.]
    EOT str = <<'EOT' A multi line reference. EOT html = <<'EOT'
    A multi line reference.
    EOT assert_outputs(inp, tagged, str, html) end def test_headline inp = <<'EOT' = This is not a headline == This is level 1 == === This is level 2 === ==== This is level 3 ==== ===== This is level 4 ===== EOT tagged = <<'EOT'

    [=] [This] [is] [not] [a] [headline]

    1 [This] [is] [level] [1]

    1.1 [This] [is] [level] [2]

    1.1.1 [This] [is] [level] [3]

    1.1.1.1 [This] [is] [level] [4]

    EOT str = <<'EOT' = This is not a headline 1) This is level 1 1.1) This is level 2 1.1.1) This is level 3 1.1.1.1) This is level 4 EOT html = <<'EOT'

    = This is not a headline

    1 This is level 1

    1.1 This is level 2

    1.1.1 This is level 3

    1.1.1.1 This is level 4

    EOT assert_outputs(inp, tagged, str, html) end def test_bullet inp = <<'EOT' * This is a bullet item ** This is a level 2 bullet item *** This is a level 3 bullet item **** This is a level 4 bullet item EOT tagged = <<'EOT'
    • * [This] [is] [a] [bullet] [item]
      • * [This] [is] [a] [level] [2] [bullet] [item]
        • * [This] [is] [a] [level] [3] [bullet] [item]
          • * [This] [is] [a] [level] [4] [bullet] [item]
    EOT str = <<'EOT' * This is a bullet item * This is a level 2 bullet item * This is a level 3 bullet item * This is a level 4 bullet item EOT html = <<'EOT'
    • This is a bullet item
      • This is a level 2 bullet item
        • This is a level 3 bullet item
          • This is a level 4 bullet item
    EOT assert_outputs(inp, tagged, str, html) end def test_number inp = <<'EOT' # This is item 1 # This is item 2 # This is item 3 Normal text. # This is item 1 ## This is item 1.1 ## This is item 1.2 ## This is item 1.3 # This is item 2 ## This is item 2.1 ## This is item 2.2 ### This is item 2.2.1 ### This is item 2.2.2 #### This is item 2.2.2.1 # This is item 3 ## This is item 3.1 ### This is item 3.1.1 # This is item 4 ### This is item 4.0.1 Normal text. # This is item 1 EOT tagged = <<'EOT'
    1. 1 [This] [is] [item] [1]
    2. 2 [This] [is] [item] [2]
    3. 3 [This] [is] [item] [3]

    [Normal] [text.]

    1. 1 [This] [is] [item] [1]
      1. 1.1 [This] [is] [item] [1.1]
      2. 1.2 [This] [is] [item] [1.2]
      3. 1.3 [This] [is] [item] [1.3]
    2. 2 [This] [is] [item] [2]
      1. 2.1 [This] [is] [item] [2.1]
      2. 2.2 [This] [is] [item] [2.2]
        1. 2.2.1 [This] [is] [item] [2.2.1]
        2. 2.2.2 [This] [is] [item] [2.2.2]
          1. 2.2.2.1 [This] [is] [item] [2.2.2.1]
    3. 3 [This] [is] [item] [3]
      1. 3.1 [This] [is] [item] [3.1]
        1. 3.1.1 [This] [is] [item] [3.1.1]
    4. 4 [This] [is] [item] [4]
        1. 4.0.1 [This] [is] [item] [4.0.1]

    [Normal] [text.]

    1. 1 [This] [is] [item] [1]
    EOT str = <<'EOT' 1. This is item 1 2. This is item 2 3. This is item 3 Normal text. 1. This is item 1 1.1 This is item 1.1 1.2 This is item 1.2 1.3 This is item 1.3 2. This is item 2 2.1 This is item 2.1 2.2 This is item 2.2 2.2.1 This is item 2.2.1 2.2.2 This is item 2.2.2 2.2.2.1 This is item 2.2.2.1 3. This is item 3 3.1 This is item 3.1 3.1.1 This is item 3.1.1 4. This is item 4 4.0.1 This is item 4.0.1 Normal text. 1. This is item 1 EOT html = <<'EOT'
    1. This is item 1
    2. This is item 2
    3. This is item 3

    Normal text.

    1. This is item 1
      1. This is item 1.1
      2. This is item 1.2
      3. This is item 1.3
    2. This is item 2
      1. This is item 2.1
      2. This is item 2.2
        1. This is item 2.2.1
        2. This is item 2.2.2
          1. This is item 2.2.2.1
    3. This is item 3
      1. This is item 3.1
        1. This is item 3.1.1
    4. This is item 4
        1. This is item 4.0.1

    Normal text.

    1. This is item 1
    EOT assert_outputs(inp, tagged, str, html) end def test_pre inp = <<'EOT' #include main() { printf("Hello, world!\n") } Some normal text. * A bullet item Some code More text. EOT tagged = <<'EOT'
    #include 
    main() {
      printf("Hello, world!\n")
    }
    

    [Some] [normal] [text.]

    • * [A] [bullet] [item]
    Some code
    

    [More] [text.]

    EOT str = <<'EOT' #include main() { printf("Hello, world!\n") } Some normal text. * A bullet item Some code More text. EOT html = <<'EOT'
    #include <stdin.h>
    main() {
      printf("Hello, world!\n")
    }
    

    Some normal text.

    • A bullet item
    Some code
    

    More text.

    EOT assert_outputs(inp, tagged, str, html) end def test_mix inp = <<'EOT' == This the first section == === This is the section 1.1 === Not sure what to put here. Maybe just some silly text. * A bullet ** Another bullet # A number item * A bullet ## Number 0.1, I guess == Section 2 == * Starts with bullets * ... Some more text. And we're done. EOT tagged = <<'EOT'

    1 [This] [the] [first] [section]

    1.1 [This] [is] [the] [section] [1.1]

    [Not] [sure] [what] [to] [put] [here.] [Maybe] [just] [some] [silly] [text.]

    • * [A] [bullet]
      • * [Another] [bullet]
    1. 1 [A] [number] [item]
    • * [A] [bullet]
      1. 0.1 [Number] [0.1,] [I] [guess]

    2 [Section] [2]

    • * [Starts] [with] [bullets]
    • * [...]

    [Some] [more] [text.] [And] [we]['re] [done.]

    EOT str = <<'EOT' 1) This the first section 1.1) This is the section 1.1 Not sure what to put here. Maybe just some silly text. * A bullet * Another bullet 1. A number item * A bullet 0.1 Number 0.1, I guess 2) Section 2 * Starts with bullets * ... Some more text. And we're done. EOT html = <<'EOT'

    1 This the first section

    1.1 This is the section 1.1

    Not sure what to put here. Maybe just some silly text.

    • A bullet
      • Another bullet
    1. A number item
    • A bullet
      1. Number 0.1, I guess

    2 Section 2

    • Starts with bullets
    • ...

    Some more text. And we're done.

    EOT assert_outputs(inp, tagged, str, html) end def test_htmlblob inp = <<'EOT' A raw foo bar blob. EOT tagged = <<'EOT'
    [A] raw foo bar [blob.]
    EOT str = <<'EOT' A blob. EOT html = <<'EOT'
    A raw foo bar blob.
    EOT assert_outputs(inp, tagged, str, html) end def test_nowiki inp = <<'EOT' == This the first section == === This is the section 1.1 === Not sure ''what'' to put here. Maybe just some silly text. * A bullet ** Another bullet # A number item * A bullet ## Number 0.1, I guess == Section 2 == * Starts with bullets * ... Some more text. And we're done. EOT tagged = <<'EOT'

    1 [This] [the] [first] [section]

    1.1 [This] [is] [the] [section] [1.1]

    [Not] [sure] [''what''] [to] [put] [here.] [Maybe] [just] [some] [silly] [text.]

    • * [A] [bullet]
      • * [Another] [bullet]
    1. 1 [A] [number] [item]
    • * [A] [bullet]

    [##] [Number] [0.1,] [I] [guess]

    [==] [Section] [2] [==]

    • * [Starts] [with] [bullets]
    • * [...]

    [Some] [more] [text.] [And] [we]['re] [done.]

    EOT str = <<'EOT' 1) This the first section 1.1) This is the section 1.1 Not sure ''what'' to put here. Maybe just some silly text. * A bullet * Another bullet 1. A number item * A bullet ## Number 0.1, I guess == Section 2 == * Starts with bullets * ... Some more text. And we're done. EOT html = <<'EOT'

    1 This the first section

    1.1 This is the section 1.1

    Not sure ''what'' to put here. Maybe just some silly text.

    • A bullet
      • Another bullet
    1. A number item
    • A bullet

    ## Number 0.1, I guess

    == Section 2 ==

    • Starts with bullets
    • ...

    Some more text. And we're done.

    EOT assert_outputs(inp, tagged, str, html) end def test_hline_and_link inp = <
    ----

    [bar]

    EOT str = <bar

    \n\n" assert_outputs(inp, tagged, str, html, 60) end def test_blockFunction inp = <<'EOT' <[dummy id="foo" arg1="bar"]> === Header === <[dummy arg1="A \"good\" day"]> some text <[dummy]> EOT tagged = <<'EOT'

    0.1 [Header]

    [some] [text]

    EOT str = <

    some text

    EOT assert_outputs(inp, tagged, str, html, 60) end def test_stringLineWrapping inp = <blue word.\n" assert_outputs(inp, tagged, str, html) end def newRichText(text) mh = TaskJuggler::MessageHandlerInstance.instance mh.outputLevel = :none rText = TaskJuggler::RichText.new(text, [ RTFDummy.new ]) assert(rti = rText.generateIntermediateFormat, mh.to_s) rti.linkTarget = '_blank' rti end def assert_outputs(inp, tagged, str, html, width = 80) # Check tagged output. assert_tagged(inp, tagged) # Check ASCII output. assert_str(inp, str, width) # Check HTML output. assert_html(inp, html, width) end def assert_tagged(inp, ref) out = newRichText(inp).to_tagged + "\n" match(ref, out) end def assert_str(inp, ref, width) rt = newRichText(inp) rt.lineWidth = width out = rt.to_s + "\n" match(ref, out) end def assert_html(inp, ref, width) rt = newRichText(inp) rt.lineWidth = width out = rt.to_html.to_s + "\n" match(ref, out) end def match(ref, out) if ref != out common = '' refDiff = '' outDiff = '' diffI = nil len = ref.length < out.length ? ref.length : out.length len.times do |i| if ref[i] == out[i] common << ref[i] else diffI = i break end end refDiff = ref[diffI,20] + '...' if diffI && ref.length > diffI outDiff = out[diffI,20] + '...' if diffI && out.length > diffI end assert_equal(ref, out, "=== Maching part: #{'=' * 40}\n" + "#{common}\n" + "=== ref diff #{'=' * 44}\n" + "#{refDiff}\n" + "=== out diff #{'=' * 44}\n" + "#{outDiff}\n") end end TaskJuggler-3.8.1/test/test_Scheduler.rb000066400000000000000000000033531473026623400202410ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = test_Scheduler.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # $:.unshift File.join(File.dirname(__FILE__), '..', 'lib') if __FILE__ == $0 $:.unshift File.dirname(__FILE__) require 'test/unit' require 'MessageChecker' require 'taskjuggler/TaskJuggler' class TestScheduler < Test::Unit::TestCase include MessageChecker def test_SchedulerErrors path = File.dirname(__FILE__) + '/' Dir.glob(path + 'TestSuite/Scheduler/Errors/*.tjp').each do |f| ENV['TZ'] = 'Europe/Berlin' (mh = TaskJuggler::MessageHandlerInstance.instance).reset mh.outputLevel = :none mh.trapSetup = true begin tj = TaskJuggler.new assert(tj.parse([ f ]), "Parser failed for #{f}") tj.warnTsDeltas = true tj.schedule rescue TaskJuggler::TjRuntimeError end checkMessages(tj, f) end end def test_SchedulerCorrect path = File.dirname(__FILE__) + '/' Dir.glob(path + 'TestSuite/Scheduler/Correct/*.tjp').each do |f| ENV['TZ'] = 'Europe/Berlin' (mh = TaskJuggler::MessageHandlerInstance.instance).reset mh.outputLevel = :none mh.trapSetup = true begin tj = TaskJuggler.new assert(tj.parse([ f ]), "Parser failed for #{f}") assert(tj.schedule, "Scheduler failed for #{f}") rescue TaskJuggler::TjRuntimeError end checkMessages(tj, f) end end end TaskJuggler-3.8.1/test/test_ShiftAssignments.rb000066400000000000000000000055631473026623400216210ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = test_ShiftAssignments.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # $:.unshift File.join(File.dirname(__FILE__), '..', 'lib') if __FILE__ == $0 require 'test/unit' require 'taskjuggler/Project' class TestPropertySet < Test::Unit::TestCase def setup TaskJuggler::ShiftAssignments.sbClear TaskJuggler::MessageHandlerInstance.instance.reset @p = TaskJuggler::Project.new('p', 'Project', '1.0') @p['start'] = TaskJuggler::TjTime.new('2008-07-29') @p['end'] = TaskJuggler::TjTime.new('2008-08-31') @s1 = TaskJuggler::Shift.new(@p, 's1', "Shift2", nil).scenario(0) @s2 = TaskJuggler::Shift.new(@p, 's2', "Shift1", nil).scenario(0) end def teardown @p = @s1 = @s2 = nil TaskJuggler::ShiftAssignments.sbClear end def test_finalizer sas1 = TaskJuggler::ShiftAssignments.new sas1.project = @p assert_equal(0, TaskJuggler::ShiftAssignments.scoreboards.length) sas1.addAssignment(TaskJuggler::ShiftAssignment.new(@s1, TaskJuggler::TimeInterval.new(TaskJuggler::TjTime.new('2008-08-01'), TaskJuggler::TjTime.new('2008-08-05')))) assert_equal(1, TaskJuggler::ShiftAssignments.scoreboards.length) # Call finalizer directly to check for runtime errors that would otherwise # go unnoticed. TaskJuggler::ShiftAssignments.deleteScoreboard(sas1.object_id) assert_equal(0, TaskJuggler::ShiftAssignments.scoreboards.length) end def test_SBsharing sas1 = TaskJuggler::ShiftAssignments.new sas1.project = @p assert_equal(0, TaskJuggler::ShiftAssignments.scoreboards.length) sas1.addAssignment(TaskJuggler::ShiftAssignment.new(@s1, TaskJuggler::TimeInterval.new(TaskJuggler::TjTime.new('2008-08-01'), TaskJuggler::TjTime.new('2008-08-05')))) sas2 = TaskJuggler::ShiftAssignments.new sas2.project = @p sas2.addAssignment(TaskJuggler::ShiftAssignment.new(@s1, TaskJuggler::TimeInterval.new(TaskJuggler::TjTime.new('2008-08-01'), TaskJuggler::TjTime.new('2008-08-05')))) assert_equal(1, TaskJuggler::ShiftAssignments.scoreboards.length) sas3 = TaskJuggler::ShiftAssignments.new sas3.project = @p sas3.addAssignment(TaskJuggler::ShiftAssignment.new(@s2, TaskJuggler::TimeInterval.new(TaskJuggler::TjTime.new('2008-08-01'), TaskJuggler::TjTime.new('2008-08-05')))) assert_equal(2, TaskJuggler::ShiftAssignments.scoreboards.length) sas1 = sas2 = sas3 = nil end end TaskJuggler-3.8.1/test/test_SimpleQueryExpander.rb000066400000000000000000000024721473026623400222720ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = test_SimpleQueryExpander.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # $:.unshift File.join(File.dirname(__FILE__), '..', 'lib') if __FILE__ == $0 require 'test/unit' require 'taskjuggler/SimpleQueryExpander' require 'taskjuggler/MessageHandler' class TestSimpleQueryExpander < Test::Unit::TestCase class Scenario def id 'scId' end end class Project def initialize end def scenario(foo) Scenario.new end end class Query def initialize end def process end def project Project.new end def scenarioIdx 0 end def attributeId=(value) end def ok true end def to_s 'XXX' end end def setup end def teardown end def test_expand exp = TaskJuggler::SimpleQueryExpander.new('foo <-bar-> foo', Query.new, nil) assert_equal('foo XXX foo', exp.expand) end end TaskJuggler-3.8.1/test/test_Syntax.rb000066400000000000000000000032541473026623400176110ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = test_Syntax.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # $:.unshift File.join(File.dirname(__FILE__), '..', 'lib') if __FILE__ == $0 $:.unshift File.dirname(__FILE__) require 'test/unit' require 'taskjuggler/TaskJuggler' require 'MessageChecker' class TestSyntax < Test::Unit::TestCase include MessageChecker def test_syntaxCorrect ENV['TEST1'] = 't_e_s_t_1' ENV['TEST2'] = '"A test String"' ENV['TEST3'] = '3' path = File.dirname(__FILE__) + '/' Dir.glob(path + 'TestSuite/Syntax/Correct/*.tjp').each do |f| ENV['TZ'] = 'Europe/Berlin' (mh = TaskJuggler::MessageHandlerInstance.instance).reset mh.outputLevel = :none mh.trapSetup = true tj = TaskJuggler.new assert(tj.parse([ f ]), "Parser failed for #{f}") assert(mh.messages.empty?, "Unexpected error in #{f}") end end def test_syntaxErrors path = File.dirname(__FILE__) + '/' Dir.glob(path + 'TestSuite/Syntax/Errors/*.tjp').each do |f| ENV['TZ'] = 'Europe/Berlin' (mh = TaskJuggler::MessageHandlerInstance.instance).reset mh.outputLevel = :none mh.trapSetup = true begin tj = TaskJuggler.new assert(!tj.parse([ f ]), "Parser succedded for #{f}") rescue TaskJuggler::TjRuntimeError end checkMessages(tj, f) end end end TaskJuggler-3.8.1/test/test_TextFormatter.rb000066400000000000000000000054511473026623400211340ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = test_TextFormatter.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # $:.unshift File.join(File.dirname(__FILE__), '..', 'lib') if __FILE__ == $0 require 'test/unit' require 'taskjuggler/TextFormatter' class TestTextFormatter < Test::Unit::TestCase def setup end def teardown end def test_format_empty ftr = TaskJuggler::TextFormatter.new(20, 2, 4) inp = '' ref = "\n" out = ftr.format(inp) assert_equal(ref, out) end def test_format_singleWord ftr = TaskJuggler::TextFormatter.new(20, 2, 4) inp = 'foo' ref = " foo\n" out = ftr.format(inp) assert_equal(ref, out) end def test_format_multipleWords ftr = TaskJuggler::TextFormatter.new(20, 2, 4) inp = "foo bar \n foobar" ref = " foo bar foobar\n" out = ftr.format(inp) assert_equal(ref, out) end def test_format_multipleLines ftr = TaskJuggler::TextFormatter.new(23, 2, 4) inp = < # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # $:.unshift File.join(File.dirname(__FILE__), '..', 'lib') require 'test/unit' require 'taskjuggler/TjTime' class TestTjTime < Test::Unit::TestCase def setup @endTime = TaskJuggler::TjTime.new("2030-01-01") @startTimes = [ TaskJuggler::TjTime.new([ 1972, 3, 15, 19, 27 ]), TaskJuggler::TjTime.new([ 1972, 2, 12, 10 ]), TaskJuggler::TjTime.new([ 1984, 11, 1, 12 ]), TaskJuggler::TjTime.new([ 1992, 1, 1 ]), ] end def teardown end def test_sameTimeNextDay @startTimes.each do |st| t1 = t2 = st t1_a = old_t2_a = t1.to_a begin t2 = t2.sameTimeNextDay t2_a = t2.to_a assert_equal(t1_a[0, 3], t2_a[0, 3]) assert(t2_a[3] == old_t2_a[3] + 1 || t2_a[3] == 1, t2_a.join(', ')) assert(t2_a[7] == old_t2_a[7] + 1 || t2_a[7] == 1, t2_a.join(', ')) old_t2_a = t2_a end while t2 < @endTime end end def test_sameTimeNextWeek @startTimes.each do |st| t1 = t2 = st t1_a = old_t2_a = t1.to_a begin t2 = t2.sameTimeNextWeek t2_a = t2.to_a # Check that hour, minutes and seconds are the same. assert_equal(t1_a[0, 3], t2_a[0, 3]) # Check that weekday is the same assert(t2_a[6] == old_t2_a[6], "old_t2: #{old_t2_a.join(', ')}\nt2: #{t2_a.join(', ')}") # Check that day of year has increased by 7 or has wrapped at end of # the year. assert(t2_a[7] == old_t2_a[7] + 7 || t2_a[7] <= 7, "old_t2: #{old_t2_a.join(', ')}\nt2: #{t2_a.join(', ')}") old_t2_a = t2_a end while t2 < @endTime end end def test_sameTimeNextMonth @startTimes.each do |st| t1 = t2 = st t1_a = old_t2_a = t1.to_a begin t2 = t2.sameTimeNextMonth t2_a = t2.to_a assert_equal(t1_a[0, 3], t2_a[0, 3]) assert(t2_a[3] == t2_a[3] || t2_a[3] > 28, "old_t2: #{old_t2_a.join(', ')}\nt2: #{t2_a.join(', ')}") assert(t2_a[4] == old_t2_a[4] + 1 || t2_a[4] == 1, "old_t2: #{old_t2_a.join(', ')}\nt2: #{t2_a.join(', ')}") old_t2_a = t2_a end while t2 < @endTime end end def test_sameTimeNextQuarter @startTimes.each do |st| t1 = t2 = st t1_a = old_t2_a = t1.to_a begin t2 = t2.sameTimeNextQuarter t2_a = t2.to_a assert_equal(t1_a[0, 3], t2_a[0, 3], "old_t2: #{old_t2_a.join(', ')}\nt2: #{t2_a.join(', ')}") assert((t2_a[4] == old_t2_a[4] + 3 && t2_a[5] == old_t2_a[5]) || (t2_a[4] == old_t2_a[4] - 9 && t2_a[5] == old_t2_a[5] + 1), "old_t2: #{old_t2_a.join(', ')}\nt2: #{t2_a.join(', ')}") old_t2_a = t2_a end while t2 < @endTime end end def test_nextDayOfWeek probes = [ [ '2010-03-17', 0, '2010-03-21' ], [ '2010-03-17', 1, '2010-03-22' ], [ '2010-03-17', 2, '2010-03-23' ], [ '2010-03-17', 3, '2010-03-24' ], [ '2010-03-17', 4, '2010-03-18' ], [ '2010-03-17', 5, '2010-03-19' ], [ '2010-03-17', 6, '2010-03-20' ], ] probes.each do |p| assert_equal(TaskJuggler::TjTime.new(p[2]), TaskJuggler::TjTime.new(p[0]).nextDayOfWeek(p[1])) end end end TaskJuggler-3.8.1/test/test_TjpExample.rb000066400000000000000000000061071473026623400203740ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = test_TjpExample.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # $:.unshift File.join(File.dirname(__FILE__), '..', 'lib') require 'test/unit' require 'taskjuggler/TjpExample' class TestScheduler < Test::Unit::TestCase def setup end def teardown end def test_1 text = <<'EOT' # *** EXAMPLE: 1 + This line is in. # *** EXAMPLE: 1 - This line is out. EOT ex = TaskJuggler::TjpExample.new ex.parse(text) out = ex.to_s ref = <<'EOT' This line is in. This line is out. EOT assert_equal(ref, out) out = ex.to_s('1') ref = <<'EOT' This line is in. EOT assert_equal(ref, out) end def test_2 text = <<'EOT' This line is in no snip. # *** EXAMPLE: 1 + This line is in snip 1. # *** EXAMPLE: 2 + This line is in snip 1 and 2. This line is as well in 1 and 2. # *** EXAMPLE: 1 - This line is in snip 2. # *** EXAMPLE: 3 + This line is in snip 2 and 3. # *** EXAMPLE: 2 - This line is in snip 3. EOT ex = TaskJuggler::TjpExample.new ex.parse(text) out = ex.to_s ref = <<'EOT' This line is in no snip. This line is in snip 1. This line is in snip 1 and 2. This line is as well in 1 and 2. This line is in snip 2. This line is in snip 2 and 3. This line is in snip 3. EOT assert_equal(ref, out) out = ex.to_s('1') ref = <<'EOT' This line is in snip 1. This line is in snip 1 and 2. This line is as well in 1 and 2. EOT assert_equal(ref, out) out = ex.to_s('2') ref = <<'EOT' This line is in snip 1 and 2. This line is as well in 1 and 2. This line is in snip 2. This line is in snip 2 and 3. EOT assert_equal(ref, out) out = ex.to_s('3') ref = <<'EOT' This line is in snip 2 and 3. This line is in snip 3. EOT assert_equal(ref, out) end def test_3 text = <<'EOT' # *** EXAMPLE: 1 + This line is in. # *** EXAMPLE: 1 - This line is out. # *** EXAMPLE: 1 + This line is in as well. EOT ex = TaskJuggler::TjpExample.new ex.parse(text) out = ex.to_s ref = <<'EOT' This line is in. This line is out. This line is in as well. EOT assert_equal(ref, out) out = ex.to_s('1') ref = <<'EOT' This line is in. This line is in as well. EOT assert_equal(ref, out) end def test_error_1 text = <<'EOT' # *** EXAMPLE: 1 - This line is in. EOT ex = TaskJuggler::TjpExample.new assert_raise(RuntimeError) { ex.parse(text) } end def test_error_2 text = <<'EOT' # *** EXAMPLE: 1 + This line is in. # *** EXAMPLE: 1 + This line is in. EOT ex = TaskJuggler::TjpExample.new assert_raise(RuntimeError) { ex.parse(text) } end def test_error_3 text = <<'EOT' # *** EXAMPLE: foo ! This line is in. EOT ex = TaskJuggler::TjpExample.new assert_raise(RuntimeError) { ex.parse(text) } end end TaskJuggler-3.8.1/test/test_URLParameter.rb000066400000000000000000000013741473026623400206270ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = test_URLParameter.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # $:.unshift File.join(File.dirname(__FILE__), '..', 'lib') if __FILE__ == $0 require 'test/unit' require 'taskjuggler/URLParameter' class TaskJuggler class TestURLParameter < Test::Unit::TestCase def test_simple s = "Hello, world!\n" assert_equal(s, URLParameter.decode(URLParameter.encode(s))) end end end TaskJuggler-3.8.1/test/test_UTF8String.rb000066400000000000000000000032301473026623400202320ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = test_UTF8String.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # $:.unshift File.join(File.dirname(__FILE__), '..', 'lib') require 'test/unit' require 'taskjuggler/UTF8String' class TestUTF8String < Test::Unit::TestCase def setup end def teardown end def test_each_utf8_char patterns = [ [ '1', [ '1' ] ], [ 'abc', [ 'a', 'b', 'c' ] ], [ 'àcA绋féà', [ 'à', 'c', 'A', '绋', 'f', 'é', 'à' ] ] ] patterns.each do |inp, out| i = 0 inp.each_utf8_char do |c| assert_equal(out[i], c) i += 1 end end end def test_concat patterns = [ [ '', 'a', 'a' ], [ 'a', 'b', 'ab' ], [ 'abc', 'à', 'abcà' ], [ 'abá', 'b', 'abáb' ] ] patterns.each do |left, right, combined| left << right assert_equal(combined, left) end end def test_length patterns = [ [ '', 0 ], [ 'a', 1 ], [ 'ábc', 3 ], [ 'abç', 3 ], [ 'àcA绋féà', 7] ] patterns.each do |str, len| assert_equal(len, str.length_utf8) end end def test_reverse patterns = [ [ '', '' ], [ 'a', 'a' ], [ 'ábc', 'cbá' ], [ 'abç', 'çba' ] ] patterns.each do |str, rts| assert_equal(rts, str.reverse) end end end TaskJuggler-3.8.1/test/test_WorkingHours.rb000066400000000000000000000121071473026623400207610ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = test_WorkingHours.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # $:.unshift File.join(File.dirname(__FILE__), '..', 'lib') if __FILE__ == $0 require 'test/unit' require 'taskjuggler/TjTime' require 'taskjuggler/WorkingHours' class TaskJuggler class TestLimits < Test::Unit::TestCase def test_equal wh1 = WorkingHours.new(3600, TjTime.new('2010-01-01'), TjTime.new('2010-12-31')) wh2 = WorkingHours.new(3600, TjTime.new('2010-01-01'), TjTime.new('2010-12-31')) assert(wh1 == wh2, "working hours must be equal") wh2.setWorkingHours(3, [[ 10 * 60 * 60, 11 * 60 * 60 ]]) assert(wh1 != wh2, "working hours must not be equal") 0.upto(6) do |d| wh1.setWorkingHours(d, []) wh2.setWorkingHours(d, []) end assert(wh1 == wh2, "working hours must be equal") end def test_onShift wh = WorkingHours.new(3600, TjTime.new('2010-01-01'), TjTime.new('2010-12-31')) timeZones = [ [ 'Europe/Berlin', '+0100' ], [ 'America/Los_Angeles', '-0800' ] ] # 2010-02-08 is a Monday workTimes = %w( 2010-02-08-9:04 2010-02-08-10:00 2010-02-08-11:00 2010-02-08-12:00 2010-02-08-13:00 2010-02-08-14:00 2010-02-08-15:00 2010-02-08-16:00 2010-02-09-9:00 2010-02-09-10:00 2010-02-09-11:00 2010-02-09-12:00 2010-02-09-13:00 2010-02-09-14:00 2010-02-09-15:00 2010-02-09-16:00 2010-02-10-9:00 2010-02-10-10:00 2010-02-10-11:00 2010-02-10-12:00 2010-02-10-13:00 2010-02-10-14:00 2010-02-10-15:00 2010-02-10-16:00 2010-02-11-9:00 2010-02-11-10:00 2010-02-11-11:00 2010-02-11-12:00 2010-02-11-13:00 2010-02-11-14:00 2010-02-11-15:00 2010-02-11-16:00 2010-02-12-9:00 2010-02-12-10:00 2010-02-12-11:00 2010-02-12-12:00 2010-02-12-13:00 2010-02-12-14:00 2010-02-12-15:00 2010-02-12-16:00 ) timeZones.each do |name, offset| wh.timezone = name workTimes.each do |wt| assert(wh.onShift?(TjTime.new(wt + ":00-#{offset}")), "Work time #{wt} (TZ #{name}) failed") end end offTimes = %w( 2010-02-06-9:00 2010-02-06-10:00 2010-02-06-11:00 2010-02-06-12:00 2010-02-06-13:00 2010-02-06-14:00 2010-02-06-15:00 2010-02-06-16:00 2010-02-07-9:00 2010-02-07-10:00 2010-02-07-11:00 2010-02-07-12:00 2010-02-07-13:00 2010-02-07-14:00 2010-02-07-15:00 2010-02-07-16:00 2010-02-08-0:00 2010-02-08-8:00 2010-02-08-19:00 2010-02-08-23:00 2010-02-09-0:00 2010-02-09-8:00 2010-02-09-19:00 2010-02-09-23:00 2010-02-10-0:00 2010-02-10-8:00 2010-02-10-19:00 2010-02-10-23:00 2010-02-10-0:00 2010-02-10-8:00 2010-02-10-19:00 2010-02-10-23:00 2010-02-11-0:00 2010-02-11-8:00 2010-02-11-19:00 2010-02-11-23:00 2010-02-12-0:00 2010-02-12-8:00 2010-02-12-19:00 2010-02-12-23:00 ) timeZones.each do |name, offset| wh.timezone = name offTimes.each do |wt| assert(!wh.onShift?(TjTime.new(wt + ":00-#{offset}")), "Off time #{wt} (TZ #{name}) failed") end end end def test_timeOff # Testing with default working hours. wh = WorkingHours.new(3600, TjTime.new('2010-01-01'), TjTime.new('2010-12-31')) # These intervals must have at least one working time slot in them. workTimes = [ # 2010-09-20 was a Monday [ '2010-09-20-9:00', '2010-09-20-12:00' ], [ '2010-09-20-8:00', '2010-09-20-10:00' ], [ '2010-09-20-11:00', '2010-09-20-13:00' ], [ '2010-09-20-11:00', '2010-09-20-14:00' ], [ '2010-09-20-16:00', '2010-09-20-18:00' ], [ '2010-09-20-16:00', '2010-09-20-19:00' ], [ '2010-09-20-16:00', '2010-09-21-9:00' ], [ '2010-09-20-17:00', '2010-09-21-10:00' ] ] workTimes.each do |iv| assert(!wh.timeOff?(TimeInterval.new(TjTime.new(iv[0]), TjTime.new(iv[1]))), "Work time interval #{iv[0]} - #{iv[1]} failed") end # These intervals must have no working time slot in them. offTimes = [ # 2010-09-17 was a Friday [ '2010-09-17-18:00', '2010-09-19-9:00' ], [ '2010-09-20-18:00', '2010-09-21-9:00' ] ] offTimes.each do |iv| assert(wh.timeOff?(TimeInterval.new(TjTime.new(iv[0]), TjTime.new(iv[1]))), "Off time interval #{iv[0]} - #{iv[1]} failed") end end end end TaskJuggler-3.8.1/test/test_deep_copy.rb000066400000000000000000000027601473026623400202730ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: UTF-8 # # = test_deep_copy.rb -- The TaskJuggler III Project Management Software # # Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # by Chris Schlaeger # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # $:.unshift File.join(File.dirname(__FILE__), '..', 'lib') if __FILE__ == $0 require 'test/unit' require 'taskjuggler/deep_copy' class A def initialize @a = [ 10, 11, 12 ] end def mute @a[1] = 'z' end def muted @a[1] end end class B def initialize @a = 1 @b = 'abc' @c = [ 1, 2, 3 ] @d = [ [ 1, 2], [ 3, 4 ], A.new ] @e = { '0' => 49, '1' => 50, '2' => 51 } end def mute @b[1] = '-' @d[1][1] = 'x' @d[2].mute @e['1'] = 111 end def muted [ @b[1, 1], @d[1][1], @d[2].muted, @e['1'] ] end end class Test_deep_copy < Test::Unit::TestCase def test_clone a = B.new b = a.deep_clone a.mute out = a.muted refA = [ '-', 'x', 'z', 111 ] refA.length.times do |i| assert_equal(refA[i], out[i]) end out = b.muted refB = [ 'b', 4, 11, 50 ] refB.length.times do |i| assert_equal(refB[i], out[i]) end end def test_network a = [ 0, '1', 'abc' ] b = { 'a' => 0, 'b' => '123', 'c' => a } a << b a.deep_clone b.deep_clone end end