Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

This is TrackRecord version 2.24, for Rails 3.2.

Fixed issues #21, #22 and #24. Added many tests to work towards satisfying
issue #6 and allow third parties to better verify their database operation.
A lot of test data is included which bulks the distribution substantially.

Properly implemented access control and in place editing for saved reports;
also included reports in audit table data. Reports include optimistic locks
now, so that if two users were to edit the same thing simultaneously, the
second user to submit the changes would be notified. This is unlikely but a
worthwhile and easy addition.

Database changes: OpenID URLs are now stored canonicalised in the database
and the front-end is much more relaxed in what it accepts. Work packets had
a description field but it was never used so has been removed, reducing the
database storage requirements. Their commit dates are now simply Dates,
not Date/Time objects, which solves issues with database compatibility and
simplifies code slightly in a few places. SQLite blank template for
"database.yml" included in addition to PostgreSQL. "README.rdoc" mentions
this database and provides configuration advice.

Misc changes: Fixed timesheet bulk commit bug that stopped it presenting
the commit form when certain date conditions arose. Improved audit list
model name display. Small efficiency improvement in Control Panel. Modern
scope syntax used in Project, with additional validation revealed as
necessary by new tests. New useful scopes added to Task for billable and
not-billable hours and updated to modern syntax; much more efficient work
packet processing therein. Timesheet access permission model tweaked in
response to test results; more logical/consistent now (see "README.rdoc").
Test-driven changes to TimesheetRow for robustness in hypothetical future
code use cases. Modern scope syntax in User model and some new scopes to
aid tests; "has_many" relationship to saved reports, previously overlooked,
now added. Dead code stripped from WorkPacket and TrackRecordSections.

Documentation updated, especially the main "README.rdoc" file, which should
make life easier for new users.
  • Loading branch information...
commit 0db40158f2e6a3888aa83c63e515483bdeceaa93 1 parent cf1cb1e
@pond authored
Showing with 486,252 additions and 1,373 deletions.
  1. +33 −0 CHANGELOG
  2. +3 −3 Gemfile
  3. +18 −19 Gemfile.lock
  4. +312 −5 README.rdoc
  5. +13 −1 app/assets/stylesheets/trackrecord_all.css.erb
  6. +9 −3 app/controllers/reports_controller.rb
  7. +19 −4 app/controllers/saved_reports_base_controller.rb
  8. +60 −20 app/controllers/saved_reports_controller.rb
  9. +5 −2 app/controllers/sessions_controller.rb
  10. +2 −1  app/controllers/timesheet_force_commits_controller.rb
  11. +16 −8 app/helpers/application_helper.rb
  12. +6 −1 app/helpers/audits_helper.rb
  13. +30 −22 app/helpers/reports_helper.rb
  14. +1 −1  app/models/control_panel.rb
  15. +6 −4 app/models/customer.rb
  16. +13 −1 app/models/project.rb
  17. +34 −2 app/models/saved_report.rb
  18. +22 −32 app/models/task.rb
  19. +17 −12 app/models/task_group.rb
  20. +70 −78 app/models/timesheet.rb
  21. +2 −2 app/models/timesheet_row.rb
  22. +12 −5 app/models/user.rb
  23. +2 −112 app/models/work_packet.rb
  24. +1 −1  app/views/layouts/application.html.erb
  25. +10 −0 app/views/reports/_comprehensive.html.erb
  26. +4 −0 app/views/reports/_tasks.html.erb
  27. +8 −2 app/views/reports/_users.html.erb
  28. +1 −1  app/views/reports/show.html.erb
  29. +3 −3 app/views/saved_reports/_edit.html.erb
  30. +1 −2  app/views/saved_reports/index.html.erb
  31. +9 −9 config/database_blank.yml
  32. +23 −0 config/database_sqlite.yml
  33. +7 −3 config/initializers/extend_acts_as_audited_model.rb
  34. +1 −1  config/locales/en.yml
  35. +5 −0 db/migrate/003_create_work_packets.rb
  36. +10 −0 db/migrate/20130925073310_re_rationalise_identity_urls.rb
  37. +30 −0 db/migrate/20131010053312_add_optimistic_locking_for_reports.rb
  38. +21 −0 db/migrate/20131010060427_remove_description_from_work_packet.rb
  39. +19 −0 db/migrate/20131011010129_change_work_packet_date_column_type.rb
  40. +19 −19 db/schema.rb
  41. +4 −4 doc/README_FOR_APP
  42. +17 −9 doc/app/ApplicationHelper.html
  43. +8 −3 doc/app/AuditsHelper.html
  44. +1 −1  doc/app/ControlPanel.html
  45. +7 −4 doc/app/Customer.html
  46. +3 −3 doc/app/Project.html
  47. +18 −5 doc/app/ReportsController.html
  48. +31 −23 doc/app/ReportsHelper.html
  49. +27 −6 doc/app/SafeInPlaceEditingHelper.html
  50. +81 −6 doc/app/SavedReport.html
  51. +39 −29 doc/app/SavedReportsController.html
  52. +7 −4 doc/app/SessionsController.html
  53. +29 −41 doc/app/Task.html
  54. +78 −14 doc/app/TaskGroup.html
  55. +39 −193 doc/app/Timesheet.html
  56. +11 −9 doc/app/TrackRecordReport/Report.html
  57. +7 −4 doc/app/TrackRecordReportGenerator.html
  58. +6 −6 doc/app/TrackRecordReportGenerator/UkOrgPondCSV.html
  59. +1 −1  doc/app/TrackRecordSections/Sections.html
  60. +10 −37 doc/app/TrackRecordSections/SectionsMixin.html
  61. +12 −10 doc/app/User.html
  62. +3 −225 doc/app/WorkPacket.html
  63. +63 −63 doc/app/created.rid
  64. +8 −9 doc/app/doc/README_FOR_APP.html
  65. +8 −9 doc/app/index.html
  66. +1 −1  doc/app/js/search_index.js
  67. +79 −87 doc/app/table_of_contents.html
  68. +1 −1  lib/report_generators/track_record_report_generator.rb
  69. +32 −7 lib/safe_in_place_editing/README
  70. +15 −13 lib/safe_in_place_editing/safe_in_place_editing.rb
  71. +27 −6 lib/safe_in_place_editing/safe_in_place_editing_helper.rb
  72. +95 −0 lib/tasks/db_dump_reports_for_tests.rake
  73. +100 −0 lib/tasks/db_extract_fixtures.rake
  74. +8 −6 lib/track_record_report.rb
  75. +357 −0 lib/track_record_report_generator_uk_org_pond_csv.rb
  76. +0 −8 lib/track_record_sections.rb
  77. BIN  test/comparison_data/saved_reports/1.yaml.gz
  78. BIN  test/comparison_data/saved_reports/10.yaml.gz
  79. BIN  test/comparison_data/saved_reports/11.yaml.gz
  80. BIN  test/comparison_data/saved_reports/12.yaml.gz
  81. BIN  test/comparison_data/saved_reports/13.yaml.gz
  82. BIN  test/comparison_data/saved_reports/14.yaml.gz
  83. BIN  test/comparison_data/saved_reports/15.yaml.gz
  84. BIN  test/comparison_data/saved_reports/16.yaml.gz
  85. BIN  test/comparison_data/saved_reports/17.yaml.gz
  86. BIN  test/comparison_data/saved_reports/18.yaml.gz
  87. BIN  test/comparison_data/saved_reports/19.yaml.gz
  88. BIN  test/comparison_data/saved_reports/2.yaml.gz
  89. BIN  test/comparison_data/saved_reports/20.yaml.gz
  90. BIN  test/comparison_data/saved_reports/21.yaml.gz
  91. BIN  test/comparison_data/saved_reports/22.yaml.gz
  92. BIN  test/comparison_data/saved_reports/23.yaml.gz
  93. BIN  test/comparison_data/saved_reports/24.yaml.gz
  94. BIN  test/comparison_data/saved_reports/25.yaml.gz
  95. BIN  test/comparison_data/saved_reports/26.yaml.gz
  96. BIN  test/comparison_data/saved_reports/27.yaml.gz
  97. BIN  test/comparison_data/saved_reports/28.yaml.gz
  98. BIN  test/comparison_data/saved_reports/29.yaml.gz
  99. BIN  test/comparison_data/saved_reports/3.yaml.gz
  100. BIN  test/comparison_data/saved_reports/30.yaml.gz
  101. BIN  test/comparison_data/saved_reports/31.yaml.gz
  102. BIN  test/comparison_data/saved_reports/32.yaml.gz
  103. BIN  test/comparison_data/saved_reports/33.yaml.gz
  104. BIN  test/comparison_data/saved_reports/34.yaml.gz
  105. BIN  test/comparison_data/saved_reports/35.yaml.gz
  106. BIN  test/comparison_data/saved_reports/36.yaml.gz
  107. BIN  test/comparison_data/saved_reports/37.yaml.gz
  108. BIN  test/comparison_data/saved_reports/38.yaml.gz
  109. BIN  test/comparison_data/saved_reports/39.yaml.gz
  110. BIN  test/comparison_data/saved_reports/4.yaml.gz
  111. BIN  test/comparison_data/saved_reports/40.yaml.gz
  112. BIN  test/comparison_data/saved_reports/41.yaml.gz
  113. BIN  test/comparison_data/saved_reports/42.yaml.gz
  114. BIN  test/comparison_data/saved_reports/43.yaml.gz
  115. BIN  test/comparison_data/saved_reports/44.yaml.gz
  116. BIN  test/comparison_data/saved_reports/45.yaml.gz
  117. BIN  test/comparison_data/saved_reports/46.yaml.gz
  118. BIN  test/comparison_data/saved_reports/47.yaml.gz
  119. BIN  test/comparison_data/saved_reports/48.yaml.gz
  120. BIN  test/comparison_data/saved_reports/49.yaml.gz
  121. BIN  test/comparison_data/saved_reports/5.yaml.gz
  122. BIN  test/comparison_data/saved_reports/50.yaml.gz
  123. BIN  test/comparison_data/saved_reports/51.yaml.gz
  124. BIN  test/comparison_data/saved_reports/52.yaml.gz
  125. BIN  test/comparison_data/saved_reports/53.yaml.gz
  126. BIN  test/comparison_data/saved_reports/54.yaml.gz
  127. BIN  test/comparison_data/saved_reports/6.yaml.gz
  128. BIN  test/comparison_data/saved_reports/7.yaml.gz
  129. BIN  test/comparison_data/saved_reports/8.yaml.gz
  130. BIN  test/comparison_data/saved_reports/9.yaml.gz
  131. +37,783 −0 test/fixtures/audits.yml
  132. +115 −7 test/fixtures/control_panels.yml
  133. +10 −0 test/fixtures/control_panels_tasks.yml
  134. +253 −11 test/fixtures/customers.yml
  135. +1,251 −17 test/fixtures/projects.yml
  136. +1,621 −0 test/fixtures/saved_reports.yml
  137. +274 −0 test/fixtures/saved_reports_active_tasks.yml
  138. +289 −0 test/fixtures/saved_reports_inactive_tasks.yml
  139. +421 −0 test/fixtures/saved_reports_reportable_users.yml
  140. +4,453 −19 test/fixtures/tasks.yml
  141. +76 −0 test/fixtures/tasks_users.yml
  142. +46,369 −9 test/fixtures/timesheet_rows.yml
  143. +17,629 −11 test/fixtures/timesheets.yml
  144. +169 −19 test/fixtures/users.yml
  145. +370,945 −15 test/fixtures/work_packets.yml
  146. +14 −15 test/functional/reports_controller_test.rb
  147. +66 −3 test/unit/control_panel_test.rb
  148. +348 −3 test/unit/customer_test.rb
  149. +313 −3 test/unit/project_test.rb
  150. +573 −0 test/unit/saved_report_test.rb
  151. +309 −3 test/unit/task_test.rb
  152. +76 −3 test/unit/timesheet_row_test.rb
  153. +457 −3 test/unit/timesheet_test.rb
  154. +227 −3 test/unit/user_test.rb
  155. +101 −3 test/unit/work_packet_test.rb
View
33 CHANGELOG
@@ -1,3 +1,36 @@
+Version 2.24, 2013-10-16
+========================
+
+Please see "README.rdoc" for vital information on installation, upgrading
+from an earlier version, database requirements and how to run the built in
+test suite that helps verify your database's suitability for TrackRecord.
+
+Version 2.24 introduces the following new features:
+
+- Easier to access report modification as requested by Issue #24,
+ requested by sarev.
+
+Version 2.24 fixes the following bugs that were found in v2.23:
+
+- The bulk timesheet commit form would raise an error under
+ certain date conditions.
+
+- An in-place title editing issue arising from an incompatibility
+ between Rails and Prototype.js 1.7's more strict adherence to
+ HTTP rules on data encoding has been worked around within the
+ Safe In Place Editor plugin. This fixes Issue #21, reported by
+ sarev (with thanks).
+
+- Saved report shared flags and copying didn't work as expected
+ for various user types; essentially the feature was only half
+ implemented. Now done properly!
+
+- In-place editors for saved report titles and share flags are
+ now supported in the index view.
+
+There are also various efficiency improvements and areas of code tidying.
+
+
Version 2.23, 2013-09-09
========================
View
6 Gemfile
@@ -35,14 +35,14 @@ gem 'prototype-rails'
# https://github.com/timcharper/calendar_date_select (original, but not Rails 3 compatible)
# http://github.com/paneq/calendar_date_select (Rails 3 fork)
# https://github.com/openid/ruby-openid
-# https://github.com/rails/open_id_authentication
+# https://github.com/grosser/open_id_authentication
# https://github.com/mislav/will_paginate/
# https://github.com/collectiveidea/audited
# https://github.com/swanandp/acts_as_list
gem 'calendar_date_select', '~> 1.6', :git => 'http://github.com/paneq/calendar_date_select'
-gem 'ruby-openid', '~> 2.2'
-gem 'open_id_authentication'
+gem 'ruby-openid', '~> 2.3'
+gem 'open_id_authentication', '~> 1.2'
gem 'will_paginate', '~> 3.0'
gem 'audited-activerecord', '~> 3.0'
gem 'acts_as_list'
View
37 Gemfile.lock
@@ -51,28 +51,27 @@ GEM
coffee-script-source (1.6.3)
dynamic_form (1.1.4)
erubis (2.7.0)
- execjs (1.4.0)
- multi_json (~> 1.0)
+ execjs (2.0.2)
hike (1.2.3)
i18n (0.6.5)
journey (1.0.4)
json (1.8.0)
- libv8 (3.11.8.17)
+ libv8 (3.16.14.3)
mail (2.5.4)
mime-types (~> 1.16)
treetop (~> 1.4.8)
- mime-types (1.24)
- multi_json (1.7.9)
- open_id_authentication (1.1.0)
+ mime-types (1.25)
+ multi_json (1.8.2)
+ open_id_authentication (1.2.0)
rack-openid (~> 1.3)
- pg (0.16.0)
+ pg (0.17.0)
polyglot (0.3.3)
prototype-rails (3.2.1)
rails (~> 3.2)
rack (1.4.5)
rack-cache (1.2)
rack (>= 0.4)
- rack-openid (1.3.1)
+ rack-openid (1.4.0)
rack (>= 1.1.0)
ruby-openid (>= 2.1.8)
rack-ssl (1.3.3)
@@ -87,7 +86,7 @@ GEM
activesupport (= 3.2.14)
bundler (~> 1.0)
railties (= 3.2.14)
- rails_autolink (1.1.0)
+ rails_autolink (1.1.4)
rails (> 3.1)
railties (3.2.14)
actionpack (= 3.2.14)
@@ -100,8 +99,8 @@ GEM
rdoc (3.12.2)
json (~> 1.4)
ref (1.0.5)
- ruby-openid (2.2.3)
- sass (3.2.10)
+ ruby-openid (2.3.0)
+ sass (3.2.12)
sass-rails (3.2.6)
railties (~> 3.2.0)
sass (>= 3.1.10)
@@ -111,19 +110,19 @@ GEM
multi_json (~> 1.0)
rack (~> 1.0)
tilt (~> 1.1, != 1.3.0)
- therubyracer (0.11.4)
- libv8 (~> 3.11.8.12)
+ therubyracer (0.12.0)
+ libv8 (~> 3.16.14.0)
ref
thor (0.18.1)
tilt (1.4.1)
- treetop (1.4.14)
+ treetop (1.4.15)
polyglot
polyglot (>= 0.3.1)
- tzinfo (0.3.37)
- uglifier (2.1.2)
+ tzinfo (0.3.38)
+ uglifier (2.2.1)
execjs (>= 0.3.0)
multi_json (~> 1.0, >= 1.0.2)
- will_paginate (3.0.4)
+ will_paginate (3.0.5)
PLATFORMS
ruby
@@ -135,12 +134,12 @@ DEPENDENCIES
coffee-rails
dynamic_form
json
- open_id_authentication
+ open_id_authentication (~> 1.2)
pg (>= 0.16)
prototype-rails
rails (= 3.2.14)
rails_autolink
- ruby-openid (~> 2.2)
+ ruby-openid (~> 2.3)
sass-rails
therubyracer
uglifier
View
317 README.rdoc
@@ -1,7 +1,8 @@
-== Welcome to TrackRecord v2.23
+= Welcome to TrackRecord v2.24
TrackRecord is a timesheet system written for the Ruby On Rails web
-development framework. More information can be found at:
+development framework. More information, including a link to the most recent
+live source repository, is available at:
* http://trackrecord.pond.org.uk
@@ -10,8 +11,314 @@ for details along with:
* http://www.opensource.org/licenses/bsd-license.php
-See the CHANGELOG file for information on changes and how to upgrade
-from an earlier version of the application.
-
Technical documentation can be found by opening 'doc/app/index.html'
in your preferred web browser.
+
+
+== Requirements, new installations and upgrades
+
+=== Requirements for all users
+
+TrackRecord was developed upon and is optimised for the PostgreSQL database.
+In particular its report generator will run fastest on this platform. Other
+Rails-supported databases _should_ work, but only PostgreSQL has been tested
+extensively under development. PostgreSQL 8.4 or later is required if using
+that database engine.
+
+* http://www.postgresql.org
+
+The installation and upgrade guides below assume you are familiar with the
+general procedure for installing Rails applications, with an environment set
+up already for Ruby On Rails. If not, then I strong recommend you begin with
+"RVM", the Ruby Version Manager:
+
+* http://rvm.io
+
+...and use this to establish a Ruby 1.9 installation. TrackRecord assumes
+Ruby 1.9 patch level 392 or later. It is not tested on Ruby 2.0 and will not
+work on Ruby 1.8.
+
+
+=== Installation for new users
+
+Download either a source archive from the Pond's Place web site (see above),
+from GitHub directly, or glone the repository as per GitHub instructions.
+Then:
+
+[Make a secret key]
+
+ Edit file +config/initializers/secret_token.rb+ as per the instructions in
+ the file; comment out the +raise+ statement, uncomment the +secret_token+
+ assignment underneath and type in or otherwise randomly generate a long
+ token string. Running command +rake+ +secret+ is a good way to do this.
+
+ <em><b>Never make your modified file public anywhere!</b></em> Keep it a
+ secret always.
+
+[Configure TrackRecord for your database]
+
+ Copy a template database configuration file:
+
+ [PostgreSQL 8.4 or later]
+ +config/database_blank.yml+
+
+ [SQLite 3]
+ +config/database_sqlite.yml+
+
+ [Other databases]
+ Roll your own +config/database.yml+ file
+
+ ...as file +config/database.yml+ and modify to suit your database
+ configuration. You then need to say what database access gem you want to
+ use. Edit file +Gemfile+ at the top level of the TrackRecord distribution:
+
+ [PostgreSQL 8.4 or later]
+ No changes are necessary
+
+ [SQLite 3]
+ Comment out the line "<tt>gem 'pg', '>=0.16'</tt>" and add underneath it
+ the line "<tt>gem 'sqlite3'</tt>"
+
+ [Other databases]
+ Comment out the line "<tt>gem 'pg', '>=0.16'</tt>" and add underneath it
+ "+gem+" line(s) specifying whatever is needed for your chosen database;
+ you may need to do a web search to determine the required gem(s)
+
+[Make sure you have all installed gems available]
+
+ Issue the command +bundle+ +install+. This can be a bit fraught as Ruby
+ and Rails gems can be finniky things, especially database adapter gems;
+ these often need native components compiling which in turn require a
+ working native compiler and supporting files on your system. If you get
+ an error, try doing a web search for it as often the answer can be found
+ there. If all else fails see http://trackrecord.pond.org.uk for contact
+ details.
+
+[Finally, set up your database]
+
+ Issue the following commands to set up all database structures. These
+ assume a Unix-like environment. Windows users will need to adapt them.
+
+ rake db:create:all
+ RAILS_ENV=production rake db:migrate
+ RAILS_ENV=development rake db:migrate
+
+ If you get an error, it's likely that your database configuration isn't
+ correct - check the steps above are OK.
+
+[Check it works in development mode]
+
+ Depending on where you're deploying, you may have wider web server issues
+ to consider. For a local machine, you can test with the Rails built in web
+ server:
+
+ * <tt>rails s</tt>
+ * Visit http://localhost:3000 in your web browser
+
+ This will test the server in development mode, so changes made here will
+ not affect your production environment database unless you configured them
+ to be the same in +config/database.yml+.
+
+[Possibly 'precompile assets' for production mode]
+
+ Asset precompilation is rather convoluted Rails-ism aimed at performance
+ improvements in production mode. If you run a production server you may
+ already be familiar with this and have made your own environment settings
+ inside +config/environments/production.rb+, but in any case it is likely
+ that you will have to issue this command:
+
+ RAILS_ENV=production bundle exec rake assets:precompile
+
+ For more, please see http://guides.rubyonrails.org/asset_pipeline.html
+ and section "Notes on asset precompilation" below.
+
+[Running formal tests]
+
+ To make sure your database is suitable for TrackRecord, run the tests with
+ the command "<tt>rake test</tt>". This will take a long time to complete.
+ No test failures are expected.
+
+[Optional configuration]
+
+ You may want to look at files +config/initializers/email_config.rb+ and
+ +config/initializers/general_config.rb+ once you've verified that the
+ software is running, to see what other things can be changed. In general,
+ the other files in +config/initializers+ should not be modified.
+
+
+=== Upgrading for existing users
+
+==== Upgrading from version 1.x
+
+If you are updating from a version earlier than v2.0, please see the
++CHANGELOG+ file's information about v2.0 for details as this is a major
+update from Rails 2 to Rails 3.
+
+
+==== Upgrading from version 2.00 to 2.11 inclusive
+
+You will need to update +config/production.rb+ with a path prefix if you
+deploy in a subdirectory rather than you server's document root. This step
+is required because of a very long standing Rails bug.
+
+See the comments above the "config.relative_url_root" line for details.
+This is vital for anyone running TrackRecord in a non-root location,
+<em>even if you use something like Phusion Passenger and normally expect to
+need no such configuration changes.</em>
+
+* http://www.phusionpassenger.com
+
+You must then also follow the steps shown in the next section.
+
+
+==== Upgrading for all version 2.00 or later users
+
+Otherwise, whenever you update a version 2.x installation, please do the
+following things:
+
+[Make sure gems are up to date]
+
+ (Re-)issue command +bundle+ +install+ (then optionally, +bundle+
+ +update+).
+
+[Make sure your databases are up to date]
+
+ Issue the following commands to update all database structures. These
+ assume a Unix-like environment. Windows users will need to adapt them.
+
+ RAILS_ENV=production rake db:migrate
+ RAILS_ENV=development rake db:migrate
+
+[Possibly recompile assets for production mode]
+
+ TrackRecord as a Rails 3 application requires asset precompilation for
+ production mode, unless you changed +config/environments/production.rb+ so
+ that this wasn't needed. A new release of TrackRecord means updated assets
+ so you need to issue:
+
+ RAILS_ENV=production rake assets:clean
+ RAILS_ENV=production bundle exec rake assets:precompile
+
+ For more, please see http://guides.rubyonrails.org/asset_pipeline.html
+ and section "Notes on asset precompilation" below.
+
+[Re-run the formal tests]
+
+ To make sure your database is still suitable for TrackRecord, it is
+ advisable (though not strictly necessary) to re-run the formal tests with
+ the command "<tt>rake test</tt>". This will take a long time to complete.
+ No test failures are expected.
+
+
+=== Notes on asset precompilation
+
+==== Deploying in a subdirectory
+
+Rails asset precompilation is somewhat broken in that it doesn't understand
+applications deployed into subdirectories. If you are not using Heroku, you
+can fix this by adding the following into config/environments/production.rb:
+
+ config.assets.initialize_on_precompile = true
+ config.relative_url_root = '/<your-app-subdir>'
+
+...then precompile the assets. If already compiled, delete the folder
+'public/assets' then precompile again (see earlier for the command).
+
+
+==== Deploying on Heroku
+
+On Heroku, the 'initialize' flag must be 'false'. The following link may
+provide some insight about what to do:
+
+ https://github.com/rails/rails/issues/8941
+
+I apologise for the inconvenience, but I can't do much about a bug like
+this that's been in Rails for at least two years, especially now that (at
+the time of writing) Rails 4 is out so a Rails 3 fix is even more unlikely.
+
+
+== Running and usage
+
+=== First time setup
+
+The first time you use TrackRecord you'll be asked to provide an Open ID
+for the person that'll become the first system administrator. More details
+about OpenID are provided on the welcome page.
+
+=== Adding other users
+
+To add new users to the system:
+
+* The new user tells the admin what their preferred Open ID is
+* The admin uses the "Manage users" link on their Home page control panel
+ to add a new User entry with the required permissions and Open ID set
+* The admin tells the new user that they're set up
+* The new user can now log in
+
+Only permitted Open IDs added by an administrator in this way will be able
+to sign into the system.
+
+=== Customers, projects and tasks
+
+A customer has many projects and a project has many tasks. Only admins are
+able to create any of these entities.
+
+==== Adding one at a time
+
+When adding new customers etc. to the system, start with the customer,
+then add the projects, then add the tasks. Always take care to assign the
+correct task->project->customer ownership; you don't _have_ to assign a
+task to a project, for example, but users can't add that task to a row in
+their timesheet unti you do.
+
+==== Bulk task import
+
+It is possible to import tasks en masse from XML files exported by software
+such as OpenProj or Microsoft Project. Use the "Bulk task import" link from
+the Home page control panel and follow the instructions presented.
+
+If your workflow is based around creating project plans before setting up
+the timesheet system, this can be really useful as the project plan may be
+used as the import data source.
+
+=== Permissions
+
+Administrator users can do just about anything. Manager users can read
+almost all data on the system, but can only edit their own data or the
+timesheets of other users (so they can make corrections or adjustments if
+necessary). Restricted users can only read data that they have created, or
+that they are given explicit permission to see. An administrator must edit
+the user's account and add a task list to it, so that the user is able to
+do things with that task list - in particular, they can't add task rows
+to their timesheets unless the admin has given them permission to "see"
+those tasks.
+
+Users can make temporary, or permanently saved reports. These can be
+marked as "shared", in which case other users can see them. For managers
+and admins it makes little difference as they can read any report anyway,
+but for restricted users, a report made by someone else is only visible
+if it is marked as shared.
+
+=== Timesheet committing
+
+Timesheets are editable until committed, then they become frozen. This
+leads to the notion of comitted versus non-committed hours in reports and
+so forth. The idea is that at the end of a week, a user finishes editing
+their timesheet and commits it using the relevant pop-up menu in the
+timesheet editor. Thereafter, "history cannot be rewritten" - important if
+you've used timesheet data in reports sent to clients.
+
+For some workflows, you might want to commit less often, or you might find
+that some users don't remember to commit timesheets when you expect them
+to. Consequently, it's possible for admins to bulk commit timesheets that
+lie between two given dates. Use the "Bulk timesheet commit" link on the
+Home page control panel and follow the instructions provided.
+
+=== Audit trail
+
+Changes to "important" objects like customers or timesheets are recorded
+in detail in the audit trail. Admins can examine this if they need to see
+who modified something and when. It's unlikely that you will need it, but
+should anything unexpectedly change in a way that might be a problem for a
+report or client, you do at least have this fallback to help work out what
+happened. Follow the "Raw audit data" link on the Home Page control panel.
View
14 app/assets/stylesheets/trackrecord_all.css.erb
@@ -60,7 +60,7 @@ tr.top {
width: 1px;
}
-/* Heading font (and navigation bar feature area) */
+/* Heading font (and navigation bar feature area), with floated links */
h1, h2, h3, h4, h5, h6,
div#header * {
@@ -68,6 +68,18 @@ div#header * {
font-weight: bold;
}
+h1 div.floated_link,
+h2 div.floated_link,
+h3 div.floated_link,
+h4 div.floated_link,
+h5 div.floated_link,
+h6 div.floated_link {
+ float: right;
+ font-size: 11pt;
+ font-weight: normal;
+ line-height: 170%;
+}
+
/* Header with navigation bar */
div#header {
View
12 app/controllers/reports_controller.rb
@@ -72,7 +72,7 @@ def show
return
- elsif ( @saved_report.nil? || ( @saved_report.user_id != @current_user.id && ! @saved_report.shared && ! @current_user.admin? ) )
+ elsif ( @saved_report.nil? || ! @saved_report.is_permitted_for?( @current_user ) )
flash[ :error ] = @@application_helper.apphelp_view_hint(
:not_found_error,
@@ -85,8 +85,14 @@ def show
# Generate a compilable report from the saved parameters and compile
# the report data into an easily understood form for report generators.
-
- @report = @saved_report.generate_report()
+ #
+ # By this point security checks have verified that the current user is
+ # allowed to see the report, but they may be restricted and this might
+ # not be their report in the first place. Thus we pass in the current
+ # user to let the report mechanism below that filter tasks etc. as
+ # required by the user's restrictions, if any.
+
+ @report = @saved_report.generate_report( true, @current_user )
@report.compile()
if ( @saved_report.title.empty? )
View
23 app/controllers/saved_reports_base_controller.rb
@@ -10,16 +10,31 @@
class SavedReportsBaseController < ApplicationController
- before_filter :confirm_permission
- before_filter :delete_unnamed_reports_for, :only => [ :index, :new, :create ]
+ # The exceptions on 'confirm_permission' are matched by equal exceptions
+ # in SavedReportsController, which defines those methods for in place
+ # editing and verifies permissions before continuing. This is necessary
+ # to avoid a great deal of mucking around trying to get Resourceful URLs
+ # down into the in-place editor so it quotes the user ID in AJAX calls.
+ # "Current user" is instead assumed here.
+
+ before_filter :assign_user
+ before_filter :confirm_permission, :except => [ :set_saved_report_title, :set_saved_report_shared ]
+ before_filter :delete_unnamed_reports_for, :only => [ :index, :new, :create ]
private
# All of this controller's actions are routed within the User resource, so
- # all requests must have a user ID.
+ # all requests must have a user ID.
+ #
+ def assign_user
+ @user = User.find_by_id( params[ :user_id ] )
+ end
+ # If no user ID is present, or the requested user doesn't match the logged
+ # in user, except for admins, then refuse access. Users should only ever be
+ # doing stuff under their own user ID unless they are administrators.
+ #
def confirm_permission
- @user = User.find_by_id( params[ :user_id ] )
return appctrl_not_permitted() if ( @user.nil? || ! ( @current_user.admin? || @user == @current_user ) )
end
View
80 app/controllers/saved_reports_controller.rb
@@ -10,6 +10,25 @@
class SavedReportsController < SavedReportsBaseController
+ # In place editing and security - note also filters present in the
+ # SavedReportsBaseController superclass.
+
+ safe_in_place_edit_for( :saved_report, :title )
+ safe_in_place_edit_for( :saved_report, :shared )
+
+ before_filter(
+ :can_be_modified?,
+ :only =>
+ [
+ :edit,
+ :update,
+ :delete,
+ :destroy,
+ :set_saved_report_title,
+ :set_saved_report_shared
+ ]
+ )
+
uses_prototype( :only => :index )
uses_leightbox()
uses_yui_tree(
@@ -39,6 +58,11 @@ def index
user_sql = "WHERE ( users.id = :user_id )\n"
other_sql = "WHERE ( users.id != :user_id )\n"
+ if ( @current_user.restricted? )
+ other_sql << "AND ( shared = :shared_flag )\n"
+ vars[ :shared_flag ] = true
+ end
+
range_sql, range_start, range_end = appctrl_search_range_sql( SavedReport )
unless ( range_sql.nil? )
@@ -92,24 +116,24 @@ def new
# the concept of "current controller" etc. in the view (we end up
# wanting to render the SavedReportsController edit view anyway).
- @saved_report = nil
+ @record = nil
if ( params.has_key?( :saved_report_id ) )
found_report = SavedReport.find_by_id( params[ :saved_report_id ] ) # No exception raised if record is not found
unless ( found_report.nil? )
- @saved_report = found_report.dup
- @saved_report.active_tasks = found_report.active_tasks
- @saved_report.inactive_tasks = found_report.inactive_tasks
- @saved_report.reportable_users = found_report.reportable_users
+ @record = found_report.dup
+ @record.active_tasks = found_report.active_tasks
+ @record.inactive_tasks = found_report.inactive_tasks
+ @record.reportable_users = found_report.reportable_users
- @saved_report.title << " (copy)"
+ @record.title << " (copy)"
end
end
- if ( @saved_report.nil? )
- @saved_report = SavedReport.new
- @saved_report.user = @user
+ if ( @record.nil? )
+ @record = SavedReport.new
+ @record.user = @user
end
@user_array = @current_user.restricted? ? [ @current_user ] : User.active
@@ -134,21 +158,21 @@ def create
# Edit an existing report.
#
def edit
- @saved_report = SavedReport.find( params[ :id ] )
- @user_array = @current_user.restricted? ? [ @current_user ] : User.active
+ @user_array = @current_user.restricted? ? [ @current_user ] : User.active
end
- # Commit changes to an existing report.
+ # Commit changes to an existing report. Note that some security actions,
+ # e.g. making sure the list of allowed reportable users is valid or the
+ # list of tasks is only that the current user can see, are enforced down
+ # in the report generator. If someone hacks a view, it won't help them.
#
def update
- saved_report = SavedReport.find( params[ :id ] )
-
appctrl_patch_params_from_js( :saved_report, :active_task_ids )
appctrl_patch_params_from_js( :saved_report, :inactive_task_ids )
- if ( saved_report.update_attributes( params[ :saved_report ] ) )
+ if ( @record.update_attributes( params[ :saved_report ] ) )
flash[ :notice ] = "Report details updated."
- redirect_to( report_path( saved_report ) )
+ redirect_to( report_path( @record ) )
else
render( :action => :edit )
end
@@ -157,18 +181,34 @@ def update
# For showing reports, just redirect to the dedicated controller for that.
#
def show
- saved_report = SavedReport.find( params[ :id ] )
- redirect_to( report_path( saved_report ) )
+ saved_report = SavedReport.find_by_id( params[ :id ] )
+
+ if ( saved_report.is_permitted_for?( @current_user ) )
+ redirect_to( report_path( saved_report ) )
+ else
+ appctrl_not_permitted()
+ end
end
- # Confirm deletion of and actually delete a saved report.
+ # Confirm deletion of a saved report.
#
def delete
- appctrl_delete( 'SavedReport' )
+ # All work done via before_filters.
end
+ # Actually delete a saved report.
+ #
def destroy
appctrl_destroy( SavedReport, user_saved_reports_path( @user ) )
end
+private
+
+ # before_filter action - can the item in the params hash be modified by
+ # the current user?
+ #
+ def can_be_modified?
+ @record = SavedReport.find_by_id( params[ :id ] )
+ return appctrl_not_permitted() unless @record.can_be_modified_by?( @current_user )
+ end
end
View
7 app/controllers/sessions_controller.rb
@@ -53,8 +53,11 @@ def create
if ( not identity_url.nil? and identity_url.empty? )
failed_login( 'You must provide an ID.')
else
- identity_url = User.rationalise_id( identity_url )
- session[ :javascript ] = params[ :javascript ] unless ( identity_url.nil? )
+ unless ( identity_url.nil? )
+ identity_url = User.rationalise_id( identity_url )
+ session[ :javascript ] = params[ :javascript ]
+ end
+
open_id_authentication()
end
View
3  app/controllers/timesheet_force_commits_controller.rb
@@ -124,7 +124,8 @@ def assign
earliest = Date.today.beginning_of_month
compare = Date.today.beginning_of_week
- @earliest = earliest - 1.month if ( compare < earliest )
+
+ @earliest = ( compare < earliest ) ? earliest - 1.month : earliest
# Remember, we want the timesheet's *last day* to fall *before*
# the start-of-month date now stored in "earliest". Or to put it
View
24 app/helpers/application_helper.rb
@@ -589,6 +589,15 @@ def apphelp_list_header_link( index_method, text, index )
def apphelp_list_row( structure, item, actions_method, with_reports = false )
output = " <tr class=\"#{ cycle( 'even', 'odd' ) }\">\n"
+ # It's assumed that admins can always modify things, but otherwise,
+ # we will only allow in-place editors if the model instance can tell
+ # us that editing is allowed by the current user.
+
+ can_edit = @current_user.admin? || (
+ item.respond_to?( :can_be_modified_by? ) &&
+ item.can_be_modified_by?( @current_user )
+ )
+
# Handle the item columns first
structure.each_index do | index |
@@ -610,18 +619,17 @@ def apphelp_list_row( structure, item, actions_method, with_reports = false )
output << send( helper, item )
else
- # Restricted users can only edit their own account. Since they are not
- # allowed to list other users on the system, the list view is disabled
- # for them, so there can never be in-place editors in that case. For
- # any other object type, restricted users have no edit permission. The
- # result? Disable all in-place editors for restricted users.
-
- in_place = entry[ :value_in_place ] && @current_user.privileged?
+ in_place = entry[ :value_in_place ] && can_edit
if ( in_place )
output << safe_in_place_editor_field( item, method )
else
- output << h( item.send( method ) )
+ value = item.send( method )
+ safestr = ( value === true or value === false ) ?
+ apphelp_boolean( value ) :
+ h( value )
+
+ output << safestr
end
end
View
7 app/helpers/audits_helper.rb
@@ -19,7 +19,12 @@ module AuditsHelper
# caught to cope with deleted items etc. not being found.
def audithelp_type_of_change( record )
- type = record.auditable_type.downcase
+ type = begin
+ record.auditable_type.constantize.model_name.human
+ rescue
+ record.auditable_type.downcase
+ end
+
type = 'permitted OpenID' if type == 'permittedopenid'
output = "#{ record.action.capitalize() } #{ h( type ) }"
View
52 app/helpers/reports_helper.rb
@@ -405,28 +405,36 @@ def reporthelp_end_date( report )
# Return appropriate list view actions for the given report
def reporthelp_actions( report )
- if ( @current_user.admin? || report.user_id == @current_user.id )
- return [
- {
- :title => :delete,
- :url => delete_user_saved_report_path( :user_id => report.user_id, :id => "%s" )
- },
- {
- :title => :edit,
- :url => edit_user_saved_report_path( :user_id => report.user_id, :id => "%s" )
- },
- {
- :title => :copy,
- :url => user_saved_report_copy_path( :user_id => report.user_id, :saved_report_id => "%s" )
- },
- {
- :title => :show,
- :url => user_saved_report_path( :user_id => report.user_id, :id => "%s" )
- },
- ]
- else
- return []
- end
+ actions = []
+
+ # All links are generated with the *current* user's ID, because the
+ # security mechanism is based around "can <this> user do the action
+ # to <this other> report", where "<this>" comes from the user ID in
+ # the URL. See SavedReportsBaseController.
+
+ actions += [
+ {
+ :title => :delete,
+ :url => delete_user_saved_report_path( :user_id => @current_user.id, :id => "%s" )
+ },
+ {
+ :title => :edit,
+ :url => edit_user_saved_report_path( :user_id => @current_user.id, :id => "%s" )
+ }
+ ] if report.can_be_modified_by?( @current_user )
+
+ actions += [
+ {
+ :title => :copy,
+ :url => user_saved_report_copy_path( :user_id => @current_user.id, :saved_report_id => "%s" )
+ },
+ {
+ :title => :show,
+ :url => user_saved_report_path( :user_id => @current_user.id, :id => "%s" )
+ }
+ ] if report.is_permitted_for?( @current_user )
+
+ return actions
end
# Return an input element and label as part of a form used to export
View
2  app/models/control_panel.rb
@@ -40,7 +40,7 @@ class ControlPanel < ActiveRecord::Base
def remove_inactive_tasks
# See the User model's remove_inactive_tasks method for details.
- self.tasks = Task.active & self.tasks
+ self.tasks = self.tasks.where( :active => true )
end
# Get a value from the instance's preferences hash. The hash is nested in a
View
10 app/models/customer.rb
@@ -69,8 +69,10 @@ def self.apply_default_sort_order( array )
# USE A TRANSACTION around a call to this method. There is no
# need to call here unless the 'active' flag state is changing.
# Pass in 'true' to update associated projects, else 'false' and
- # 'true' to update associated tasks via those projects, else
- # 'false'. Booleans default to 'true' if omitted.
+ # 'true' to update associated tasks via those projects (only if
+ # updating projects too), else 'false'.
+ #
+ # Booleans default to 'true' if omitted.
#
def update_with_side_effects!( attrs, update_projects = true, update_tasks = true )
active = self.active
@@ -88,8 +90,8 @@ def update_with_side_effects!( attrs, update_projects = true, update_tasks = tru
# As update_with_side_effects!, but destroys things rather than
# updating them. Pass 'true' to destroy associated projects, else
# 'false'. If omitted, defaults to 'true'; pass also 'true' to
- # destroy tasks associated with those projects, else 'false',
- # with, again, the default being 'true'.
+ # destroy tasks associated with those projects (only if destroying
+ # projects too), else 'false'. Again, the default is 'true'.
#
def destroy_with_side_effects( destroy_projects = true, destroy_tasks = true )
if ( destroy_projects )
View
14 app/models/project.rb
@@ -29,12 +29,14 @@ class Project < TaskGroup
has_many( :tasks )
has_many( :control_panels )
- scope( :unassigned, { :conditions => { :customer_id => nil } } )
+ scope :unassigned, -> { where( :customer_id => nil ) }
attr_protected() # Necessary for unknown reasons, Rails issues?
USED_RANGE_COLUMN = 'created_at' # For the Rangeable base class of TaskGroup
+ validate( :customer_is_active )
+
# Some default properties are dynamic, so assign these here rather than
# as defaults in a migration.
#
@@ -96,4 +98,14 @@ def destroy_with_side_effects( destroy_tasks = true )
self.destroy()
end
+
+private
+
+ # Run via "validate".
+ #
+ def customer_is_active()
+ unless ( ( not self.active ) or self.customer.nil? or self.customer.active )
+ errors.add( :base, 'Active projects can only be associated with active customers' )
+ end
+ end
end
View
36 app/models/saved_report.rb
@@ -10,6 +10,13 @@
class SavedReport < Rangeable
+ audited( :except => [
+ :lock_version,
+ :updated_at,
+ :created_at,
+ :id
+ ] )
+
DEFAULT_SORT_COLUMN = 'updated_at'
DEFAULT_SORT_DIRECTION = 'DESC'
DEFAULT_SORT_ORDER = "#{ DEFAULT_SORT_COLUMN } #{ DEFAULT_SORT_DIRECTION }"
@@ -71,6 +78,8 @@ class SavedReport < Rangeable
# Validations
+ validates_presence_of :user_id
+
validates_inclusion_of :frequency, :in => 0...TrackRecordReport::Report::FREQUENCY.length
validates_inclusion_of :task_filter, :in => TASK_FILTER_VALUES
@@ -85,13 +94,21 @@ class SavedReport < Rangeable
# pass 'true' on entry to force a refresh of the cache and update the
# TrackRecordReport::Report instance.
#
- def generate_report( flush_cache = false )
+ # Optionally, pass in a user. Without this, the saved report's own user
+ # details will be used for task filtering and so-on. If you allow another
+ # user to view someone else's report, then you will want to pass in that
+ # other user's details, since that user may be subject to different
+ # restrictions (in particular, differing permitted task lists).
+ #
+ def generate_report( flush_cache = false, viewing_user = user )
+
if ( @report.nil? || flush_cache )
+
# The TrackRecord internal Report object can be created from this
# instance's attributes directly, except for many-to-many relationships,
# which are not exposed in that hash and must be assigned manually.
- @report = TrackRecordReport::Report.new( user, attributes() )
+ @report = TrackRecordReport::Report.new( viewing_user, attributes() )
@report.title = title
@report.active_task_ids = active_task_ids
@@ -126,6 +143,21 @@ def range_end_cache
CACHED_REVERSE_RANGE_MAP[ raw_attr_val ] || raw_attr_val
end
+ # Is the given user permitted to do anything with this report?
+ # A shared report can be viewed by anyone, privileged users can
+ # view any report and of course the report's owner can view it.
+ #
+ def is_permitted_for?( comparison_user )
+ shared? or comparison_user == user or comparison_user.privileged?
+ end
+
+ # Is the given user permitted to update this report? Only report
+ # owners or administrators can do so.
+
+ def can_be_modified_by?( comparison_user )
+ comparison_user == user or comparison_user.admin?
+ end
+
private # =====================================================================
# Range maps and "update_cached_ranges": An effective hack to map a Report's
View
54 app/models/task.rb
@@ -28,9 +28,11 @@ class Task < Rangeable
default_scope( { :order => DEFAULT_SORT_ORDER } )
- scope( :active, :conditions => { :active => true } )
- scope( :inactive, :conditions => { :active => false } )
- scope( :unassigned, :conditions => { :project_id => nil } )
+ scope :active, -> { where( :active => true ) }
+ scope :inactive, -> { where( :active => false ) }
+ scope :unassigned, -> { where( :project_id => nil ) }
+ scope :billable, -> { where( :billable => true ) }
+ scope :not_billable, -> { where( :billable => false ) }
# Tasks are the fundamental building blocks of a Project. They define
# specific pieces of work of expected duration, against which work
@@ -212,31 +214,25 @@ def update_with_side_effects!( attrs )
# Number of hours worked on this task, committed or otherwise
#
def total_worked
- return self.work_packets.sum( :worked_hours ) || 0.0
+ return self.work_packets.sum( :worked_hours )
end
# Number of committed hours worked on this task.
#
def committed_worked
- sum = 0.0
+ joins = { :timesheet_row => :timesheet }
+ conditions = { :timesheets => { :committed => true } }
- self.work_packets.all.each do | work_packet |
- sum += work_packet.worked_hours if ( work_packet.timesheet_row.timesheet.committed )
- end
-
- return sum
+ return self.work_packets.joins( joins ).where( conditions ).sum( :worked_hours )
end
# Number of not committed hours worked on this task.
#
def not_committed_worked
- sum = 0.0
-
- self.work_packets.all.each do | work_packet |
- sum += work_packet.worked_hours unless ( work_packet.timesheet_row.timesheet.committed )
- end
+ joins = { :timesheet_row => :timesheet }
+ conditions = { :timesheets => { :committed => false } }
- return sum
+ return self.work_packets.joins( joins ).where( conditions ).sum( :worked_hours )
end
# Number of hours worked on the task between the given start
@@ -248,23 +244,17 @@ def not_committed_worked
# giving sums for those types of hours.
#
def sum_hours_over_range( date_range, user = nil )
- committed_sum = 0.0
- not_committed_sum = 0.0
- work_packets = self.work_packets.all( :conditions => { :date => date_range } )
-
- work_packets.each do | work_packet |
- timesheet = work_packet.timesheet_row.timesheet
-
- if ( user.nil? or timesheet.user == user )
- if ( timesheet.committed )
- committed_sum += work_packet.worked_hours
- else
- not_committed_sum += work_packet.worked_hours
- end
- end
- end
+ joins = { :timesheet_row => { :timesheet => :user } }
+ committed = { :timesheets => { :committed => true } }
+ not_committed = { :timesheets => { :committed => false } }
+ conditions = { :date => date_range }
+
+ conditions[ :users ] = { :id => user.id } unless user.nil?
- return { :committed => committed_sum, :not_committed => not_committed_sum }
+ return {
+ :committed => self.work_packets.joins( joins ).where( committed ).where( conditions ).sum( :worked_hours ),
+ :not_committed => self.work_packets.joins( joins ).where( not_committed ).where( conditions ).sum( :worked_hours )
+ }
end
private
View
29 app/models/task_group.rb
@@ -20,8 +20,19 @@ class TaskGroup < Rangeable
default_scope( { :order => DEFAULT_SORT_ORDER } )
- scope( :active, :conditions => { :active => true } )
- scope( :inactive, :conditions => { :active => false } )
+ # Want to do this, for future compatibility with Rails 4:
+ #
+ # scope :active, -> { where( :active => true ) }
+ # scope :inactive, -> { where( :active => false ) }
+ #
+ # ...however it breaks:
+ #
+ # https://github.com/rails/rails/issues/10658
+ #
+ # Thus, bypass the syntactic sugar and just write the methods.
+
+ def self.active; where( :active => true ); end
+ def self.inactive; where( :active => false ); end
# Derived classes must state their associations. None are set up
# in the base class. They should also restrict mass assignment to
@@ -46,7 +57,7 @@ def augmented_title
return self.title
end
- # Find all projects which the given user is allowed to see.
+ # Find all objects which the given user is allowed to see.
# A conditions hash may be passed to further restrict the search
# (that is, the "{...}" in "find( :all, :conditions => {...})").
#
@@ -79,23 +90,17 @@ def can_be_modified_by?( user )
return self.active
end
- # Is permission granted for the given user to see this project?
+ # Is permission granted for the given user to see this object?
# See also find_permitted. Returns 'true' if permitted, else 'false'.
#
def is_permitted_for?( user )
return true if user.privileged?
- # User is restricted. User can only see this project if it
+ # User is restricted. User can only see this object if it
# has at least one task associated with it and at least one
# of those associated tasks appears in the user's permitted
# task list, so check the intersection of the two arrays.
- return false if ( self.tasks.empty? )
- return true if ( self.tasks & user.tasks ).length > 0
-
- # None of the project's tasks are in the user's permitted
- # list, so the user is not permitted to see this project.
-
- return false
+ return ( self.tasks & user.tasks ).length > 0
end
end
View
148 app/models/timesheet.rb
@@ -73,7 +73,7 @@ def self.used_range( accurate = false )
last = WorkPacket.find_latest_by_tasks()
if accurate
- ( first.date.to_date )..( last.date.to_date )
+ ( first.date )..( last.date )
else
( first.date.year )..( last.date.year )
end
@@ -114,56 +114,48 @@ def self.used_range( accurate = false )
# to 'true'. If so, update the associated user's last committed
# date.
- before_update :check_committed_state
+ before_save :check_committed_state
# Update the start date cache when records are saved or updated.
- before_update :update_start_day_cache
+ before_save :update_start_day_cache
# Is the given user permitted to do anything with this timesheet?
+ # Admins and managers can view anything. Normal users can only view
+ # their own timesheets.
#
def is_permitted_for?( user )
- return ( user.privileged? or user.id == self.user.id )
+ ( user.id == self.user.id ) or ( user.privileged? )
end
- # Is the given user permitted to update this timesheet?
+ # Is the given user permitted to update this timesheet? Admins can
+ # modify anything. Managers can modify their own timesheets whether
+ # committed or not, or any other timesheet provided it is not
+ # committed. Normal users can only modify their own timesheets when
+ # not committed.
#
- def can_be_modified_by?( user )
- return true if ( user.admin? )
- return false unless ( self.is_permitted_for?( user ) )
- return ( not self.committed )
- end
-
- # Instance method that returns an array of all timesheets owned
- # by this user. Pass an optional conditions hash (will be sent
- # in as ":conditions => <given value>").
- #
- def find_mine( conditions = {} )
- Timesheet.where( :user_id => self.user_id ).where( conditions )
- end
-
- # Instance method which returns an array of all committed
- # timesheets owned by this user.
+ # Note there is no special status awarded to admin-owned timesheets;
+ # a manager can modify any not committed timesheet. This keeps the
+ # model simple. Managers are trusted to only modify timesheets they
+ # don't own when really necessary, but they can't revise history by
+ # changing committed data.
#
- def find_mine_committed( conditions = {} )
- conditions.merge!( :committed => true )
- return find_mine( conditions )
- end
-
- # Instance method which returns an array of all uncommitted
- # timesheets owned by this user.
- #
- def find_mine_uncommitted( conditions = {} )
- conditions.merge!( :committed => false )
- return find_mine( conditions )
+ def can_be_modified_by?( user )
+ if ( user.admin? )
+ true
+ elsif ( user.manager? )
+ ( user.id == self.user.id ) or ( not self.committed )
+ else
+ ( user.id == self.user.id ) and ( not self.committed )
+ end
end
- # Return an array of week numbers which can be assigned to the
- # timesheet. Includes the current timesheet's already allocated
+ # Return a sorted array of week numbers which can be assigned to
+ # the timesheet. Includes the current timesheet's already allocated
# week.
#
def unused_weeks()
- timesheets = find_mine( :year => self.year )
+ timesheets = Timesheet.where( :user_id => self.user_id, :year => self.year )
used_weeks = timesheets.select( :week_number ).map( &:week_number )
range = 1..Timesheet.get_last_week_number( self.year )
@@ -198,44 +190,6 @@ def showable_week( nextweek )
end
end
- # Back-end to editable_week and showable_week. See those functions for
- # details. Call with the next/previous week boolean and pass a block;
- # this is given a timesheet or nil; evaluate 'true' to return details
- # on the item or 'false' to move on to the next week.
- #
- def discover_week( nextweek )
- year = self.year
- owner = self.user_id
-
- if ( nextweek )
- inc = 1
- week = self.week_number + 1
- limit = Timesheet.get_last_week_number( year ) + 1
-
- return if ( week >= limit )
- else
- inc = -1
- week = self.week_number - 1
- limit = 0
-
- return if ( week <= limit )
- end
-
- while ( week != limit )
- timesheet = Timesheet.find_by_user_id_and_year_and_week_number(
- owner, year, week
- )
-
- if ( yield( timesheet ) )
- return { :week_number => week, :timesheet => timesheet }
- end
-
- week += inc
- end
-
- return nil
- end
-
# Add a row to the timesheet using the given task object. Does
# nothing if a row containing that task is already present.
# The updated timesheet is not saved - the caller must do this.
@@ -421,7 +375,7 @@ def self.date_for( year, week_number, day_number, as_date = false )
if ( as_date )
return date
else
- return date.strftime( '%d-%b-%Y') # Or ISO: '%Y-%m-%d'
+ return date.strftime( '%d-%b-%Y' ) # Or ISO: '%Y-%m-%d'
end
end
@@ -453,7 +407,7 @@ def tasks_are_active_and_permitted
self.tasks.all.each do | task |
errors.add( :base, "Task '#{ task.augmented_title }' is no longer active and cannot be included" ) unless task.active
- if ( self.user.restricted? )
+ if ( self.user.try( :restricted? ) )
errors.add( :base, "Inclusion of task '#{ task.augmented_title }' is no longer permitted" ) unless self.user.task_ids.include?( task.id )
end
end
@@ -467,16 +421,16 @@ def add_default_rows
end
end
- # Run via "before_update".
+ # Run via "before_safe".
#
def check_committed_state
- if ( self.committed )
+ if ( self.committed && self.user )
self.committed_at = self.user.last_committed = Time.new
self.user.save!
end
end
- # Run via "before_update".
+ # Run via "before_safe".
#
def update_start_day_cache
self.start_day_cache = self.date_for(
@@ -485,4 +439,42 @@ def update_start_day_cache
).to_datetime.in_time_zone( 'UTC' ) # Rails 3 gotcha/bug; auto-conversion to TimeWithZone uses *server's local time zone* rather than UTC+0, contrary to Rails defaults elsewhere; typical result is the cache column ends up in the 'wrong day' unless server is also at UTC +0.
end
+
+ # Back-end to editable_week and showable_week. See those functions for
+ # details. Call with the next/previous week boolean and pass a block;
+ # this is given a timesheet or nil; evaluate 'true' to return details
+ # on the item or 'false' to move on to the next week.
+ #
+ def discover_week( nextweek )
+ year = self.year
+ owner = self.user_id
+
+ if ( nextweek )
+ inc = 1
+ week = self.week_number + 1
+ limit = Timesheet.get_last_week_number( year ) + 1
+
+ return if ( week >= limit )
+ else
+ inc = -1
+ week = self.week_number - 1
+ limit = 0
+
+ return if ( week <= limit )
+ end
+
+ while ( week != limit )
+ timesheet = Timesheet.find_by_user_id_and_year_and_week_number(
+ owner, year, week
+ )
+
+ if ( yield( timesheet ) )
+ return { :week_number => week, :timesheet => timesheet }
+ end
+
+ week += inc
+ end
+
+ return nil
+ end
end
View
4 app/models/timesheet_row.rb
@@ -59,9 +59,9 @@ def row_sum()
# Run via "validate".
#
def task_is_active_and_permitted
- errors.add( :base, 'Only active tasks may be included' ) unless self.task.active
+ errors.add( :base, 'Only active tasks may be included' ) unless self.task.try( :active )
- if ( self.timesheet.user.restricted? )
+ if ( self.timesheet.try( :user ).try( :restricted? ) )
errors.add( :base, 'Inclusion of this task is not permitted' ) unless self.timesheet.user.task_ids.include?( self.task.id )
end
end
View
17 app/models/user.rb
@@ -30,9 +30,11 @@ class User < Rangeable
USER_TYPE_MANAGER = 'Manager'
USER_TYPE_NORMAL = 'Normal'
- scope( :active, :conditions => { :active => true } )
- scope( :inactive, :conditions => { :active => false } )
- scope( :restricted, :conditions => { :user_type => User::USER_TYPE_NORMAL } )
+ scope :active, -> { where( :active => true ) }
+ scope :inactive, -> { where( :active => false ) }
+ scope :restricted, -> { where( :user_type => User::USER_TYPE_NORMAL ) }
+ scope :managers, -> { where( :user_type => User::USER_TYPE_MANAGER ) }
+ scope :admins, -> { where( :user_type => User::USER_TYPE_ADMIN ) }
# A User object stores information describing a timesheet system
# user (obviously), including things like name and e-mail address
@@ -40,8 +42,11 @@ class User < Rangeable
# to see.
has_one( :control_panel, :dependent => :destroy )
+
+ has_many( :timesheets, :dependent => :destroy )
+ has_many( :saved_reports, :dependent => :destroy )
+
has_and_belongs_to_many( :tasks )
- has_many( :timesheets, :dependent => :destroy )
attr_protected(
:user_type,
@@ -203,13 +208,15 @@ def self.rationalise_id( uri )
# Did the user omit the 'http' prefix? If so, the URI parser will
# be a bit confused. Try adding in 'http' instead.
- orignal = URI.parse( "http://#{uri}" ) if ( original.scheme.nil? )
+ original = URI.parse( "http://#{uri}" ) if ( original.scheme.nil? )
# We must by now have at least a scheme and host. If not, something
# very odd is going on - bail out.
return uri if ( original.scheme.nil? or original.host.nil? )
+ original.path.chomp!( '/' )
+
# Looks good - assemble a clean equivalent.
if ( original.scheme.downcase == 'https' )
View
114 app/models/work_packet.rb
@@ -46,115 +46,6 @@ class WorkPacket < ActiveRecord::Base
before_save( :set_date )
- # Find work packets in rows related to the given task ID, held in timesheets
- # owned by the given user ID, between the Dates in the given range. The range
- # MUST be inclusive, for reasons discussed below. The results are sorted by
- # work packet date, descending.
- #
- # The task and user IDs are optional. All tasks and/or users will be
- # included in the count if the given task and/or user ID is nil. The date
- # range is mandatory.
- #
- # Returns an ActiveRecord::Relation instance.
- #
- # IMPORTANT - at the time of writing, Rails 2.1 (and earlier versions) will
- # build a BETWEEN statement in SQL with the given range. Although SQL says
- # that the values on either side of BETWEEN should be treated as inclusive,
- # i.e. a Ruby "a..b" kind of range, some databases may treat the right side
- # as exclusive; PostgreSQL is fine, but if in doubt you need to go to the
- # Rails console and run a test. For example, issue something like this:
- #
- # User.all.collect { |x| x.id }.sort
- #
- # Note any two consecutive IDs listed - e.g. "[1, 2, ...]" - 1 and 2 will do.
- # Use these as part of range conditions for a find:
- #
- # User.find(:all, :conditions => { :id => 1..2 } )
- #
- # Assuming you actually *have* users with IDs 1 and 2, then both should be
- # returned. If you only get one, BETWEEN isn't working and you need to use
- # another database or change the function below to do something else (e.g.
- # hard-code a condition using ">=" and "<=" if your database supports those
- # operators).
- #
- # A final twist is that Rails' "to_s( :db )" operator assumes all ranges are
- # inclusive and generates SQL accordingly. There's a ticket for this in the
- # case of dates:
- #
- # http://dev.rubyonrails.org/ticket/8549
- #
- # ...but actually Rails seems to do this for any kind of range - e.g. change
- # the "1..2" to "1...2" in the User find above and note that the generated
- # SQL is the same. We'd expect it to only look for a user with id '1' (or
- # between 1 and 1) in this case.
- #
- # As a result, ensure you only ever pass inclusive ranges to this function.
- #
- def self.find_by_task_user_and_range( range, task_id = nil, user_id = nil )
- return WorkPacket.find_by_task_user_range_and_committed(
- range,
- nil,
- task_id,
- user_id
- )
- end
-
- # As find_by_task_user_and_range, but only counts work packets belonging to
- # committed timesheets.
- #
- # Returns an ActiveRecord::Relation instance.
- #
- def self.find_committed_by_task_user_and_range( range, task_id = nil, user_id = nil )
- return WorkPacket.find_by_task_user_range_and_committed(
- range,
- true,
- task_id,
- user_id
- )
- end
-
- # As find_by_task_user_and_range, but only counts work packets belonging to
- # timesheets which are not committed.
- #
- # Returns an ActiveRecord::Relation instance.
- #
- def self.find_not_committed_by_task_user_and_range( range, task_id = nil, user_id = nil )
- return WorkPacket.find_by_task_user_range_and_committed(
- range,
- false,
- task_id,
- user_id
- )
- end
-
- # Support find_by_task_user_and_range, find_committed_by_task_user_and_range
- # and find_not_committed_by_task_user_and_range. An extra mandatory second
- # parameter must be set to 'true' to only include work packets from committed
- # timesheets, 'false' for not committed timesheets and 'nil' for either.
- #
- # Returns an ActiveRecord::Relation instance.
- #
- def self.find_by_task_user_range_and_committed( range, committed, task_id = nil, user_id = nil )
-
- # With Rails when joins are specified by a hash, each key's value is the
- # next level of association. We first include the timesheet rows, a second
- # order association, because the rows lead to tasks and timesheets. This
- # key points to an array giving two third order things; :task and, itself
- # a hash key, :timesheet; since it is a hash key, :timesheet's value is
- # the second-order association of timesheets, or the fourth-order
- # association of the work packets - :user.
-
- joins = { :timesheet_row => [ :task, { :timesheet => :user } ] }
- order = 'date DESC'
-
- conditions = { :date => range }
- conditions[ :tasks ] = { :id => task_id } unless task_id.nil?
- conditions[ :users ] = { :id => user_id } unless user_id.nil?
- conditions[ :timesheets ] = { :committed => committed } unless committed.nil?
-
- return WorkPacket.joins( joins ).where( conditions ).order( order )
- end
-
# Return the earliest (first by date) work packet, either across all tasks
# (pass nothing) or for the given tasks specified as an array of task IDs.
# The work packet may be in either a not committed or committed timesheet.
@@ -184,7 +75,6 @@ def self.find_first_by_tasks_and_order( task_ids, order )
return WorkPacket.significant.joins( joins ).where( conditions ).order( order ).first
end
-
end
private
@@ -196,9 +86,9 @@ def set_date
self.date = self.timesheet_row.timesheet.date_for(
self.day_number,
true # Return as a Date rather than a String
- ).to_datetime.in_time_zone( 'UTC' ) # Rails 3 gotcha/bug; auto-conversion to TimeWithZone uses *server's local time zone* rather than UTC+0, contrary to Rails defaults elsewhere; typical result is the cache column ends up in the 'wrong day'
+ )
else
- self.date = Time.current
+ self.date = Date.today
end
end
View
2  app/views/layouts/application.html.erb
@@ -39,7 +39,7 @@
<div id="header">
<div id="title">
- <h1>TrackRecord <span class="version">v2.23</span></h1>
+ <h1>TrackRecord <span class="version">v2.24</span></h1>
<span class="slug"><%= raw( apphelp_slug() ) %></span>
</div>
<div id="navbar">
View
10 app/views/reports/_comprehensive.html.erb
@@ -2,10 +2,16 @@
# Based on "@report", render a comprehensive per-task, per-user report.
# Only render if the report's "user_details" flag is set.
-%>
+
<% if ( @report.filtered_tasks.empty? or @report.filtered_users.empty? ) -%>
<h2 class="report_subtitle">
Comprehensive report for <%= @report.display_range %>
+
+ <div class="floated_link">
+ <%= link_to( 'Alter report', edit_user_saved_report_path( :id => @saved_report.id, :user_id => @saved_report.user_id ) ) %>
+ </div>
</h2>
+
<% if ( @report.filtered_users.empty? and not @report.users.empty? ) -%>
<p>
None of the selected users booked any hours against the reportable tasks
@@ -22,6 +28,10 @@
<h2 class="report_subtitle">
Comprehensive report for <%= @report.display_range %>
<%= ( @report.filtered_users.empty? ) ? '(all users)' : '(selected users only)' %>
+
+ <div class="floated_link">
+ <%= link_to( 'Alter report', edit_user_saved_report_path( :id => @saved_report.id, :user_id => @saved_report.user_id ) ) %>
+ </div>
</h2>
<% if ( @report.filtered_tasks.empty? ) -%>
View
4 app/views/reports/_tasks.html.erb
@@ -4,6 +4,10 @@
<h2 class="report_subtitle">
Task data for <%= @report.display_range %>
<%= ( @report.filtered_users.empty? ) ? '(all users)' : '(selected users only)' %>
+
+ <div class="floated_link">
+ <%= link_to( 'Alter report', edit_user_saved_report_path( :id => @saved_report.id, :user_id => @saved_report.user_id ) ) %>
+ </div>
</h2>
<% if ( @report.filtered_tasks.empty? ) -%>
View
10 app/views/reports/_users.html.erb
@@ -18,8 +18,14 @@
<% else %>
<!-- Per-user summaries -->
- <h2 class="report_subtitle">User summaries for <%= @report.display_range %></h2>
-
+ <h2 class="report_subtitle">
+ User summaries for <%= @report.display_range %>
+ <div class="floated_link">
+ <%= link_to( 'Alter report', edit_user_saved_report_path( :id => @saved_report.id, :user_id => @saved_report.user_id ) ) %>
+ </div>
+ </h2>
+
+ <p></p>
<table class="report display_table ts_show_table">
<!-- Heading - show user names -->
View
2  app/views/reports/show.html.erb
@@ -24,7 +24,7 @@
<br />
<br />
<div class="centred">
- <%= link_to( 'Change report parameters', edit_user_saved_report_path( :id => @saved_report.id, :user_id => @saved_report.user_id ) ) %>,
+ <%= link_to( 'Alter report', edit_user_saved_report_path( :id => @saved_report.id, :user_id => @saved_report.user_id ) ) %>,
<%= link_to( 'create a new report', new_user_saved_report_path( :user_id => @saved_report.user_id ) ) %> or
<%= link_to( 'cancel', home_path() ) %>.
View
6 app/views/saved_reports/_edit.html.erb
@@ -30,7 +30,7 @@
</p>
<% end -%>
<% else -%>
- <%= form_for( [ @saved_report.user, @saved_report ] ) do | f | -%><%= f.error_messages %>
+ <%= form_for( [ @record.user, @record ] ) do | f | -%><%= f.error_messages %>
<p>
<%= f.label :title %>:
@@ -246,7 +246,7 @@
:report_generator,
{
:form => f,
- :report => @saved_report,
+ :report => @record,
:line_prefix => ' '
}
)
@@ -261,7 +261,7 @@
:report_generator,
{
:form => f,
- :report => @saved_report,
+ :report => @record,
:inactive => true,
:line_prefix => ' '
}
View
3  app/views/saved_reports/index.html.erb
@@ -23,7 +23,6 @@
)
%>
-<% if @current_user.privileged? -%>
<h2>All other reports</h2>
<%=
@@ -42,7 +41,7 @@
}
)
%>
-<% end -%>
+
<h2>Find reports</h2>
<%=
View
18 config/database_blank.yml
@@ -1,4 +1,4 @@
-# PostgreSQL database configuration. Syntax taken from
+# TrackRecord PostgreSQL template database configuration. Syntax taken from
# "http://blog.bleything.net/" (Ben Bleything, June 2006).
dbinfo: &dbinfo
@@ -7,18 +7,18 @@ dbinfo: &dbinfo
username: username-goes-here
password: password-goes-here
-# Warning: The database defined as 'test' will be erased and
-# re-generated from your development database when you run 'rake'.
-# Do not set this db to the same as development or production.
-
development:
<<: *dbinfo
database: trackrecord-devel
-test:
- <<: *dbinfo
- database: trackrecord-test
-
production:
<<: *dbinfo
database: trackrecord
+
+# Warning: The database defined as 'test' will be erased and re-generated
+# from your development database when you run 'rake'. Do not set this to
+# the same as development or production.
+
+test:
+ <<: *dbinfo
+ database: trackrecord-test
View
23 config/database_sqlite.yml
@@ -0,0 +1,23 @@
+# TrackRecord SQLite 3 template database configuration. Syntax taken from
+# "http://blog.bleything.net/" (Ben Bleything, June 2006).
+
+dbinfo: &dbinfo
+ adapter: sqlite3
+ pool: 5
+ timeout: 5000
+
+development:
+ <<: *dbinfo
+ database: db/trackrecord-devel.sqlite3
+
+production:
+ <<: *dbinfo
+ database: db/trackrecord.sqlite3
+
+# Warning: The database defined as 'test' will be erased and re-generated
+# from your development database when you run 'rake'. Do not set this to
+# the same as development or production.
+
+test:
+ <<: *dbinfo
+ database: db/trackrecord-test.sqlite3
View
10 config/initializers/extend_acts_as_audited_model.rb
@@ -33,12 +33,16 @@ class Audit
def self.used_range( accurate = false )
first = self.unscoped.order( "#{ USED_RANGE_COLUMN } ASC" ).first
- last = self.unscoped.order( "#{ USED_RANGE_COLUMN } DESC" ).first
+ last = self.unscoped.order( "#{ USED_RANGE_COLUMN } DESC" ).first
+
+ today = Date.today
+ first = first.nil? ? today : first.created_at
+ last = last.nil? ? today : last.created_at
if accurate
- ( first.created_at.to_date )..( last.created_at.to_date )
+ ( first.to_date )..( last.to_date )
else
- ( first.created_at.year )..( last.created_at.year )
+ ( first.year )..( last.year )
end
end
View
2  config/locales/en.yml
@@ -145,7 +145,7 @@ en:
action_title_show: "Report details"
view_not_found_error: "The requested report was not found; the owner may have deleted it."
- view_unnamed_warning: "This report is unnamed. It will be deleted automatically. To save it permanently, use the 'Change report parameters' link underneath the report and give it a name."
+ view_unnamed_warning: "This report is unnamed. It will be deleted automatically. To save it permanently, use the 'Alter report' link underneath the report and give it a name."
view_throttle_warning: "The requested start date was changed from %{original} to %{actual} to prevent generation of an excessively large report."
saved_reports:
View
5 db/migrate/003_create_work_packets.rb
@@ -1,3 +1,8 @@
+# See also "20131010060427_remove_description_from_work_packet.rb".
+#
+# The description field is never used and removed by the later migration, but
+# is kept here for historical consistency and existing user data migrations.
+
class CreateWorkPackets < ActiveRecord::Migration
def self.up
create_table :work_packets do | t |
View
10 db/migrate/20130925073310_re_rationalise_identity_urls.rb
@@ -0,0 +1,10 @@
+# This is unavoidably one-way.
+
+class ReRationaliseIdentityUrls < ActiveRecord::Migration
+ def up
+ User.find_each do | user |
+ user.identity_url = User.rationalise_id( user.identity_url )
+ user.save!
+ end
+ end
+end
View
30 db/migrate/20131010053312_add_optimistic_locking_for_reports.rb
@@ -0,0 +1,30 @@
+# Originally didn't think I'd want optimistic locking on saved
+# report objects. The intention was to only let users modify their
+# own items, but in the end admins were allowed to edit everything
+# as usual for general consistency. There's a small but non-zero
+# chance of a user and admin concurrently editing the same report,
+# so a versioning column is required.
+#
+# This is based on "015_add_optimistic_locking.rb".
+
+class AddOptimisticLockingForReports < ActiveRecord::Migration
+ def self.up
+ add_column :saved_reports, :lock_version, :integer, :default => 0
+
+ # Update existing data
+
+ ActiveRecord::Base.lock_optimistically = false
+
+ SavedReport.reset_column_information
+ SavedReport.all.each do | obj |
+ obj.lock_version = 0
+ obj.save!
+ end
+
+ ActiveRecord::Base.lock_optimistically = true
+ end
+
+ def self.down
+ remove_column :saved_reports, :lock_version
+ end
+end
View
21 db/migrate/20131010060427_remove_description_from_work_packet.rb
@@ -0,0 +1,21 @@
+# See also "003_create_work_packets.rb".
+#
+# Note the migration is not truly reversible since it removes a column,
+# thus discarding data. Since TrackRecord never puts anything in there,
+# though, this is OK. If third parties are using patched variants with
+# some use for individual work packet descriptions, they'll need to
+# comment out the add/remove calls in the code below.
+#
+# It's conceivable of course that this field may need to be re-added in
+# future but it hasn't been required in years and right now just bloats
+# the database.
+
+class RemoveDescriptionFromWorkPacket < ActiveRecord::Migration
+ def up
+ remove_column :work_packets, :description
+ end
+
+ def down
+ add_column :work_packets, :description, :text
+ end
+end
View
19 db/migrate/20131011010129_change_work_packet_date_column_type.rb
@@ -0,0 +1,19 @@
+# Work Packets have dates but these were originally created as DateTime
+# columns. This causes edge case database issues for SQLite when using
+# ranges to try and select work packets based on date. A date such as
+# "2012-02-04" does not match "2012-02-04 00:00:00.000000 UTC" for some
+# queries.
+#
+# The Work Packet date cache column can only ever have a date value as
+# a work packet describes a full day of work on a given task, so the
+# time component is redundant. Removing it solves edge case issues.
+
+class ChangeWorkPacketDateColumnType < ActiveRecord::Migration
+ def up
+ change_column :work_packets, :date, :date
+ end
+
+ def down
+ change_column :work_packets, :date, :datetime
+ end
+end
View
38 db/schema.rb
@@ -11,7 +11,7 @@
#
# It's strongly recommended to check this file into your version control system.
-ActiveRecord::Schema.define(:version => 20130718235640) do
+ActiveRecord::Schema.define(:version => 20131011010129) do
create_table "audits", :force => true do |t|
t.integer "auditable_id"
@@ -38,8 +38,8 @@
t.integer "user_id"
t.integer "project_id"
t.integer "customer_id"
- t.datetime "created_at"
- t.datetime "updated_at"
+ t.datetime "created_at", :null => false
+ t.datetime "updated_at", :null => false
t.text "preferences"
end
@@ -56,8 +56,8 @@
t.string "title", :null => false
t.string "code"
t.text "description"
- t.datetime "created_at"
- t.datetime "updated_at"
+ t.datetime "created_at", :null => false
+ t.datetime "updated_at", :null => false
t.integer "lock_version", :default => 0
end
@@ -82,8 +82,8 @@
t.string "title", :null => false
t.string "code"
t.text "description"
- t.datetime "created_at"
- t.datetime "updated_at"
+ t.datetime "created_at", :null => false
+ t.datetime "updated_at", :null => false
t.integer "lock_version", :default => 0
end
@@ -115,6 +115,7 @@
t.string "range_one_month", :limit => 7
t.string "range_one_week", :limit => 7
t.boolean "user_details"
+ t.integer "lock_version", :default => 0
end
add_index "saved_reports", ["user_id"], :name => "index_saved_reports_on_user_id"
@@ -150,8 +151,8 @@
t.string "code"
t.text "description"
t.decimal "duration", :null => false
- t.datetime "created_at"
- t.datetime "updated_at"
+ t.datetime "created_at", :null => false
+ t.datetime "updated_at", :null => false
t.integer "lock_version", :default => 0
t.boolean "billable", :default => true
end
@@ -167,8 +168,8 @@
create_table "timesheet_rows", :force => true do |t|
t.integer "timesheet_id", :null => false
t.integer "task_id", :null => false
- t.datetime "created_at"
- t.datetime "updated_at"
+ t.datetime "created_at", :null => false
+ t.datetime "updated_at", :null => false
t.integer "position"
end
@@ -179,8 +180,8 @@
t.text "description"
t.boolean "committed", :default => false, :null => false
t.datetime "committed_at"
- t.datetime "created_at"
- t.datetime "updated_at"
+ t.datetime "created_at", :null => false
+ t.datetime "updated_at", :null => false
t.integer "lock_version", :default => 0
t.datetime "start_day_cache"
t.string "auto_sort", :limit => 16
@@ -194,8 +195,8 @@
t.string "user_type", :null => false
t.boolean "active", :default => true, :null => false
t.datetime "last_committed"
- t.datetime "created_at"
- t.datetime "updated_at"
+ t.datetime "created_at", :null => false
+ t.datetime "updated_at", :null => false
t.integer "lock_version", :default => 0
end
@@ -203,10 +204,9 @@
t.integer "timesheet_row_id", :null => false
t.integer "day_number", :null => false
t.decimal "worked_hours", :null => false
- t.text "description"
- t.datetime "date", :null => false
- t.datetime "created_at"
- t.datetime "updated_at"
+ t.date "date", :null => false
+ t.datetime "created_at", :null => false
+ t.datetime "updated_at", :null => false
end
end
View
8 doc/README_FOR_APP
@@ -15,14 +15,14 @@ so you become familiar with the application's underlying data structures.
TrackRecord uses reasonably complex database queries from time to time but
does so through Rails as far as possible, so ActiveRecord should take care
of it - just choose your preferred adapter gem instead of e.g. "pg" in the
-Gemfile and run "bundle install", then set up "config/database.yml".
+Gemfile, run "bundle install" and set up "config/database.yml".
-<em><strong>However, the strong exception is for reports.</em></strong>
+<em><b>However, the strong exception is for reports.</em></b>
See the documentation on TrackRecordReport::FREQUENCY for details.
-<em><strong>This is essential reading</strong></em> if changing databases,
+<em><b>This is essential reading</b></em> if changing databases,
as by default on non-PostgreSQL databases, TrackRecord automatically
enters a 'database safe mode' for reports and report generation is likely
-to run <em>extremely</em> slowly!
+to run very slowly!
== Extensions