Permalink
Browse files

Implement / fix all issues for GitHub Milestone 2.1. See the

CHANGELOG file under version 2.10 for details.
  • Loading branch information...
1 parent f2b1650 commit d38c27c265ff798263458302445b9b810c3336b6 @pond committed Aug 2, 2013
Showing with 10,593 additions and 2,918 deletions.
  1. +62 −0 CHANGELOG
  2. +1 −1 README.rdoc
  3. 0 {public → app/assets}/images/calendar_date_select/calendar.gif
  4. 0 {public → app/assets}/images/rails.png
  5. 0 {public → app/assets}/images/trackrecord/blank.png
  6. 0 {public → app/assets}/images/trackrecord/down.png
  7. 0 {public → app/assets}/images/trackrecord/help_sprite.png
  8. 0 {public → app/assets}/images/trackrecord/masters/arrows.svg
  9. 0 {public → app/assets}/images/trackrecord/masters/defaulticon/License.txt
  10. 0 {public → app/assets}/images/trackrecord/masters/defaulticon/ReadMe.txt
  11. 0 {public → app/assets}/images/trackrecord/masters/defaulticon/overview_icons.png
  12. 0 {public → app/assets}/images/trackrecord/masters/defaulticon/png/128x128/about.png
  13. 0 {public → app/assets}/images/trackrecord/masters/defaulticon/png/128x128/help.png
  14. 0 {public → app/assets}/images/trackrecord/masters/defaulticon/png/128x128/info.png
  15. 0 {public → app/assets}/images/trackrecord/masters/defaulticon/png/16x16/about-inv.png
  16. 0 {public → app/assets}/images/trackrecord/masters/defaulticon/png/16x16/about.png
  17. 0 {public → app/assets}/images/trackrecord/masters/defaulticon/png/16x16/help.png
  18. 0 {public → app/assets}/images/trackrecord/masters/defaulticon/png/16x16/info.png
  19. 0 {public → app/assets}/images/trackrecord/masters/defaulticon/small_preview.png
  20. 0 {public → app/assets}/images/trackrecord/masters/warning.svg
  21. 0 {public → app/assets}/images/trackrecord/openid_med_logo_text.png
  22. 0 {public → app/assets}/images/trackrecord/openid_small_logo.png
  23. 0 {public → app/assets}/images/trackrecord/up.png
  24. 0 {public → app/assets}/images/trackrecord/warning.png
  25. +14 −0 app/assets/javascripts/application.js
  26. +1 −0 app/assets/javascripts/saved_reports.js
  27. +6 −0 app/assets/javascripts/sessions.js
  28. +1 −0 app/assets/javascripts/task_imports.js
  29. 0 {public → app/assets}/javascripts/trackrecord/check_box_toggler.js
  30. 0 {public → app/assets}/javascripts/trackrecord/check_for_javascript.js
  31. +4 −18 public/javascripts/application.js → app/assets/javascripts/trackrecord/global.js
  32. +247 −0 app/assets/javascripts/trackrecord/saved_report_editor.js
  33. 0 {public → app/assets}/javascripts/trackrecord/section_revealer.js
  34. 0 {public → app/assets}/javascripts/trackrecord/timesheet_editor.js
  35. 0 {public → app/assets}/javascripts/trackrecord/timesheet_viewer.js
  36. 0 {public → app/assets}/stylesheets/application.css
  37. 0 {public → app/assets}/stylesheets/calendar_date_select/blue.css
  38. 0 {public → app/assets}/stylesheets/calendar_date_select/default.css
  39. 0 {public → app/assets}/stylesheets/calendar_date_select/green.css
  40. 0 {public → app/assets}/stylesheets/calendar_date_select/plain.css
  41. 0 {public → app/assets}/stylesheets/calendar_date_select/red.css
  42. 0 {public → app/assets}/stylesheets/calendar_date_select/silver.css
  43. 0 {public → app/assets}/stylesheets/leightbox/leightbox.css
  44. 0 {public → app/assets}/stylesheets/scaffold.css
  45. +121 −19 public/stylesheets/trackrecord_all.css → app/assets/stylesheets/trackrecord_all.css.erb
  46. 0 {public → app/assets}/stylesheets/trackrecord_print.css
  47. +91 −21 app/controllers/reports_controller.rb
  48. +31 −10 app/controllers/saved_reports_controller.rb
  49. +17 −0 app/controllers/saved_reports_copy_controller.rb
  50. +17 −5 app/controllers/task_imports_controller.rb
  51. +158 −0 app/controllers/timesheet_force_commits_controller.rb
  52. +17 −5 app/controllers/timesheets_controller.rb
  53. +10 −5 app/helpers/application_helper.rb
  54. +2 −3 app/helpers/charts_helper.rb
  55. +0 −1 app/helpers/help_helper.rb
  56. +109 −7 app/helpers/reports_helper.rb
  57. +96 −3 app/models/saved_report.rb
  58. +2 −0 app/models/task_import.rb
  59. +6 −8 app/models/timesheet.rb
  60. +83 −0 app/models/timesheet_force_commit.rb
  61. +5 −1 app/models/timesheet_row.rb
  62. +34 −41 app/models/work_packet.rb
  63. +11 −0 app/views/help/_xml_import.html.erb
  64. +8 −14 app/views/layouts/application.html.erb
  65. +2 −0 app/views/projects/_edit.html.erb
  66. +0 −1 app/views/projects/edit.html.erb
  67. +0 −1 app/views/projects/new.html.erb
  68. +140 −0 app/views/reports/_comprehensive.html.erb
  69. +0 −15 app/views/reports/_export_csv.html.erb
  70. +45 −0 app/views/reports/_export_elements.html.erb
  71. +129 −0 app/views/reports/_tasks.html.erb
  72. +93 −0 app/views/reports/_users.html.erb
  73. +7 −215 app/views/reports/show.html.erb
  74. +259 −222 app/views/saved_reports/_edit.html.erb
  75. +17 −26 app/views/sessions/new.html.erb
  76. +36 −1 app/views/task_imports/edit.html.erb
  77. +11 −25 app/views/task_imports/new.html.erb
  78. +87 −0 app/views/timesheet_force_commits/new.html.erb
  79. +11 −7 app/views/timesheets/edit.html.erb
  80. +6 −5 app/views/users/home.html.erb
  81. +11 −4 config/application.rb
  82. +18 −0 config/initializers/add_submodules_method_to_module.rb
  83. +14 −0 config/initializers/eager_load_report_generators.rb
  84. +39 −0 config/locales/en.yml
  85. +9 −0 config/routes.rb
  86. +3 −0 db/migrate/20111013142252_add_saved_reports_support.rb
  87. +8 −0 db/migrate/20130712004910_add_new_report_date_range_types.rb
  88. +5 −0 db/migrate/20130716010807_add_user_details_flag_to_saved_reports.rb
  89. +15 −0 db/migrate/20130718235640_populate_saved_report_range_caches.rb
  90. +4 −1 db/schema.rb
  91. +17 −4 doc/README_FOR_APP
  92. +68 −23 doc/app/ApplicationController.html
  93. +38 −23 doc/app/ApplicationHelper.html
  94. +11 −1 doc/app/AuditsController.html
  95. +11 −1 doc/app/AuditsHelper.html
  96. +11 −1 doc/app/ChartsController.html
  97. +13 −4 doc/app/ChartsHelper.html
  98. +11 −1 doc/app/ControlPanel.html
  99. +11 −1 doc/app/ControlPanelsController.html
  100. +21 −9 doc/app/Customer.html
  101. +11 −1 doc/app/CustomersController.html
  102. +11 −1 doc/app/CustomersHelper.html
  103. +11 −1 doc/app/EmailNotifier.html
  104. +11 −1 doc/app/HelpController.html
  105. +11 −2 doc/app/HelpHelper.html
  106. +23 −11 doc/app/Project.html
  107. +11 −1 doc/app/ProjectsController.html
  108. +11 −1 doc/app/ProjectsHelper.html
  109. +11 −1 doc/app/QuietLeightbox.html
  110. +11 −1 doc/app/QuietLeightbox/ClassMethods.html
  111. +31 −4 doc/app/QuietLeightbox/QuietLeightboxHelper.html
  112. +16 −4 doc/app/QuietPrototype.html
  113. +12 −2 doc/app/QuietPrototype/ClassMethods.html
  114. +26 −4 doc/app/QuietPrototype/QuietPrototypeHelper.html
  115. +11 −1 doc/app/Rangeable.html
  116. +70 −8 doc/app/ReportsController.html
  117. +182 −20 doc/app/ReportsHelper.html
  118. +26 −2 doc/app/ReportsHelper/ReportMonth.html
  119. +25 −1 doc/app/ReportsHelper/ReportWeek.html
  120. +13 −3 doc/app/ReportsHelper/ReportYear.html
  121. +11 −1 doc/app/SafeInPlaceEditing.html
  122. +11 −1 doc/app/SafeInPlaceEditing/ClassMethods.html
  123. +11 −1 doc/app/SafeInPlaceEditingHelper.html
  124. +122 −4 doc/app/SavedReport.html
  125. +11 −1 doc/app/SavedReportAutoTitlesController.html
  126. +11 −1 doc/app/SavedReportsBaseController.html
  127. +11 −1 doc/app/SavedReportsByCustomerController.html
  128. +11 −1 doc/app/SavedReportsByProjectController.html
  129. +11 −1 doc/app/SavedReportsByTaskController.html
  130. +11 −1 doc/app/SavedReportsByUserController.html
  131. +110 −15 doc/app/SavedReportsController.html
  132. +347 −0 doc/app/SavedReportsCopyController.html
  133. +11 −1 doc/app/SavedReportsHelper.html
  134. +11 −1 doc/app/SessionsController.html
  135. +34 −22 doc/app/Task.html
  136. +11 −1 doc/app/TaskGroup.html
  137. +49 −13 doc/app/TaskImport.html
  138. +27 −5 doc/app/TaskImportsController.html
  139. +11 −1 doc/app/TaskImportsHelper.html
  140. +21 −12 doc/app/TasksController.html
  141. +11 −1 doc/app/TasksHelper.html
  142. +51 −31 doc/app/Timesheet.html
  143. +599 −0 doc/app/TimesheetForceCommit.html
  144. +446 −0 doc/app/TimesheetForceCommitsController.html
  145. +16 −3 doc/app/TimesheetRow.html
  146. +11 −1 doc/app/TimesheetRowsController.html
  147. +11 −1 doc/app/TimesheetRowsHelper.html
  148. +21 −11 doc/app/TimesheetsController.html
  149. +13 −3 doc/app/TimesheetsHelper.html
  150. +44 −15 doc/app/TrackRecordReport.html
  151. +171 −27 doc/app/TrackRecordReport/Report.html
  152. +17 −7 doc/app/TrackRecordReport/ReportCell.html
  153. +12 −2 doc/app/TrackRecordReport/ReportColumnTotal.html
  154. +17 −7 doc/app/TrackRecordReport/ReportElementaryCalculator.html
  155. +16 −6 doc/app/TrackRecordReport/ReportRow.html
  156. +14 −4 doc/app/TrackRecordReport/ReportSection.html
  157. +12 −2 doc/app/TrackRecordReport/ReportUserColumnTotal.html
  158. +37 −23 doc/app/TrackRecordReport/ReportUserData.html
  159. +12 −2 doc/app/TrackRecordReport/ReportUserRowTotal.html
  160. +694 −0 doc/app/TrackRecordReportGenerator.html
  161. +807 −0 doc/app/TrackRecordReportGenerator/UkOrgPondCSV.html
  162. +11 −1 doc/app/TrackRecordSections.html
  163. +11 −1 doc/app/TreesController.html
  164. +29 −17 doc/app/User.html
  165. +11 −1 doc/app/UsersController.html
  166. +11 −1 doc/app/UsersHelper.html
  167. +49 −45 doc/app/WorkPacket.html
  168. +11 −1 doc/app/WorkPacketsController.html
  169. +11 −1 doc/app/WorkPacketsHelper.html
  170. +11 −1 doc/app/YuiTree.html
  171. +11 −1 doc/app/YuiTree/ClassMethods.html
  172. +11 −1 doc/app/YuiTree/YuiTreeHelper.html
  173. +32 −27 doc/app/created.rid
  174. +30 −7 doc/app/doc/README_FOR_APP.html
  175. +63 −2 doc/app/index.html
  176. +1 −1 doc/app/js/search_index.js
  177. +165 −82 doc/app/table_of_contents.html
  178. +19 −2 lib/quiet_leightbox/quiet_leightbox.rb
  179. +16 −2 lib/quiet_prototype/quiet_prototype.rb
  180. +272 −0 lib/report_generators/track_record_report_generator.rb
  181. +364 −0 lib/report_generators/track_record_report_generator_uk_org_pond_csv.rb
  182. +19 −0 lib/tasks/documentation.rake
  183. +175 −40 lib/track_record_report.rb
  184. +0 −1 public/500.html
  185. +0 −33 public/blank.html
  186. +0 −2 public/blank_iframe.html
  187. +0 −10 public/dispatch.cgi
  188. +0 −24 public/dispatch.fcgi
  189. +0 −10 public/dispatch.rb
  190. 0 {public → vendor/assets}/javascripts/calendar_date_select/calendar_date_select.js
  191. 0 {public → vendor/assets}/javascripts/calendar_date_select/format_american.js
  192. 0 {public → vendor/assets}/javascripts/calendar_date_select/format_danish.js
  193. 0 {public → vendor/assets}/javascripts/calendar_date_select/format_db.js
  194. 0 {public → vendor/assets}/javascripts/calendar_date_select/format_euro_24hr.js
  195. 0 {public → vendor/assets}/javascripts/calendar_date_select/format_euro_24hr_ymd.js
  196. 0 {public → vendor/assets}/javascripts/calendar_date_select/format_finnish.js
  197. 0 {public → vendor/assets}/javascripts/calendar_date_select/format_hyphen_ampm.js
  198. 0 {public → vendor/assets}/javascripts/calendar_date_select/format_iso_date.js
  199. 0 {public → vendor/assets}/javascripts/calendar_date_select/format_italian.js
  200. 0 {public → vendor/assets}/javascripts/calendar_date_select/locale/ar.js
  201. 0 {public → vendor/assets}/javascripts/calendar_date_select/locale/cs.js
  202. 0 {public → vendor/assets}/javascripts/calendar_date_select/locale/da.js
  203. 0 {public → vendor/assets}/javascripts/calendar_date_select/locale/de.js
  204. 0 {public → vendor/assets}/javascripts/calendar_date_select/locale/es.js
  205. 0 {public → vendor/assets}/javascripts/calendar_date_select/locale/fi.js
  206. 0 {public → vendor/assets}/javascripts/calendar_date_select/locale/fr.js
  207. 0 {public → vendor/assets}/javascripts/calendar_date_select/locale/it.js
  208. 0 {public → vendor/assets}/javascripts/calendar_date_select/locale/ja.js
  209. 0 {public → vendor/assets}/javascripts/calendar_date_select/locale/nl.js
  210. 0 {public → vendor/assets}/javascripts/calendar_date_select/locale/pl.js
  211. 0 {public → vendor/assets}/javascripts/calendar_date_select/locale/pt.js
  212. 0 {public → vendor/assets}/javascripts/calendar_date_select/locale/ru.js
  213. 0 {public → vendor/assets}/javascripts/calendar_date_select/locale/sl.js
  214. 0 {public → vendor/assets}/javascripts/calendar_date_select/locale/sv.js
  215. 0 {public → vendor/assets}/javascripts/leightbox/leightbox.js
  216. +2,506 −1,551 {public/javascripts → vendor/assets/javascripts/prototype}/prototype.js
  217. 0 {public/javascripts → vendor/assets/javascripts/prototype_ujs}/rails.js
  218. 0 {public → vendor/assets}/javascripts/safe_in_place_editing/safe_in_place_editing.js
  219. 0 {public/javascripts → vendor/assets/javascripts/scriptaculous}/builder.js
  220. 0 {public/javascripts → vendor/assets/javascripts/scriptaculous}/controls.js
  221. 0 {public/javascripts → vendor/assets/javascripts/scriptaculous}/dragdrop.js
  222. 0 {public/javascripts → vendor/assets/javascripts/scriptaculous}/effects.js
  223. 0 {public/javascripts → vendor/assets/javascripts/scriptaculous}/scriptaculous.js
  224. 0 {public/javascripts → vendor/assets/javascripts/scriptaculous}/slider.js
  225. 0 {public/javascripts → vendor/assets/javascripts/scriptaculous}/sound.js
  226. 0 {public → vendor/assets}/javascripts/yui_tree/yui_tree_support.js
  227. 0 vendor/plugins/.gitkeep
View
62 CHANGELOG
@@ -1,3 +1,64 @@
+Version 2.10, 2013-08-02
+========================
+
+Please see the version 2.00 notes later for information about updating
+from version 1. You will need to migrate your data to upgrade from any
+previous v2.x release; for example, to migrate the production database,
+run this command:
+
+ RAILS_ENV=production rake db:migrate
+
+Version 2.10 fixes the following bugs that were found in v2.04:
+
+- Errors made in a timesheet editor grid (e.g. non-numerical hours) would
+ cause validation to fail correctly, but changes made elsewhere would be
+ reset when the form was presented back with errors listed. Changes to
+ the timesheet's week, description and/or commit flag are now maintained.
+
+- Sign-in page updated with new text, fixing outdated links.
+
+- A few minor comment typing errors fixed, some HTML validation fixes,
+ further improvements to internationalisation.
+
+- The prototype.js library version is updated to version 1.7.1 (with a
+ very large number of alterations despite the minor version change).
+
+- Moved to using the Rails 3 asset pipeline and fixed up issue related to
+ this in various plugins. This is a relatively significant change to the
+ internal organisation of the software and is the main reason for the
+ TrackRecord version number jump.
+
+There are the following enhancements:
+
+- Added "relative week" and "relative month" report ranges. You can
+ now for example set a report to work for "last month" and whenever
+ it is generated, an appropriate time range is chosen. To support
+ this and additional new options, the report editor page has been
+ redesigned, hopefully making it easier and faster to use.
+
+- Added a comprehensive report type. This is similar to the by-task
+ standard report, but adds user breakdowns showing the contribution of
+ selected users to the total hours for each task at each column of the
+ report. This is in addition to the by-task and user summary reports.
+
+- Saved reports can now be copied, thus acting as report templates.
+
+- Extensible report generator mechanism. CSV export now runs as a plugin,
+ allowing other report manipulation and export mechanisms to be added
+ easily by third parties.
+
+- Inline project creation for bulk task import - users reported often
+ going through the import process only to realise that they hadn't added
+ a project to contain the new tasks. Now both can be done simultaneously.
+
+- Administrators can now bulk-commit timesheets over a given date range.
+
+- Efficiency improvements by using ActiveRecord::Relation better, avoiding
+ loading objects into RAM where possible, doing more in the database and
+ less inside Ruby code.
+
+
+
Version 2.04, 2013-07-11
========================
@@ -25,6 +86,7 @@ There are the following enhancements:
identification.
+
Version 2.02 and 2.03, 2013-04-25
=================================
View
2 README.rdoc
@@ -1,4 +1,4 @@
-== Welcome to TrackRecord v2.04
+== Welcome to TrackRecord v2.10
TrackRecord is a timesheet system written for the Ruby On Rails web
development framework. More information can be found at:
View
0 .../images/calendar_date_select/calendar.gif → .../images/calendar_date_select/calendar.gif
File renamed without changes
View
0 public/images/rails.png → app/assets/images/rails.png
File renamed without changes
View
0 public/images/trackrecord/blank.png → app/assets/images/trackrecord/blank.png
File renamed without changes
View
0 public/images/trackrecord/down.png → app/assets/images/trackrecord/down.png
File renamed without changes
View
0 public/images/trackrecord/help_sprite.png → ...assets/images/trackrecord/help_sprite.png
File renamed without changes
View
0 public/images/trackrecord/masters/arrows.svg → ...ets/images/trackrecord/masters/arrows.svg
File renamed without changes.
View
0 ...ackrecord/masters/defaulticon/License.txt → ...ackrecord/masters/defaulticon/License.txt
File renamed without changes.
View
0 ...rackrecord/masters/defaulticon/ReadMe.txt → ...rackrecord/masters/defaulticon/ReadMe.txt
File renamed without changes.
View
0 ...rd/masters/defaulticon/overview_icons.png → ...rd/masters/defaulticon/overview_icons.png
File renamed without changes
View
0 ...masters/defaulticon/png/128x128/about.png → ...masters/defaulticon/png/128x128/about.png
File renamed without changes
View
0 .../masters/defaulticon/png/128x128/help.png → .../masters/defaulticon/png/128x128/help.png
File renamed without changes
View
0 .../masters/defaulticon/png/128x128/info.png → .../masters/defaulticon/png/128x128/info.png
File renamed without changes
View
0 ...sters/defaulticon/png/16x16/about-inv.png → ...sters/defaulticon/png/16x16/about-inv.png
File renamed without changes
View
0 ...d/masters/defaulticon/png/16x16/about.png → ...d/masters/defaulticon/png/16x16/about.png
File renamed without changes
View
0 ...rd/masters/defaulticon/png/16x16/help.png → ...rd/masters/defaulticon/png/16x16/help.png
File renamed without changes
View
0 ...rd/masters/defaulticon/png/16x16/info.png → ...rd/masters/defaulticon/png/16x16/info.png
File renamed without changes
View
0 ...ord/masters/defaulticon/small_preview.png → ...ord/masters/defaulticon/small_preview.png
File renamed without changes
View
0 ...ic/images/trackrecord/masters/warning.svg → ...ts/images/trackrecord/masters/warning.svg
File renamed without changes.
View
0 ...ages/trackrecord/openid_med_logo_text.png → ...ages/trackrecord/openid_med_logo_text.png
File renamed without changes
View
0 .../images/trackrecord/openid_small_logo.png → .../images/trackrecord/openid_small_logo.png
File renamed without changes
View
0 public/images/trackrecord/up.png → app/assets/images/trackrecord/up.png
File renamed without changes
View
0 public/images/trackrecord/warning.png → app/assets/images/trackrecord/warning.png
File renamed without changes
View
14 app/assets/javascripts/application.js
@@ -0,0 +1,14 @@
+/************************************************************************
+ * File: application.js *
+ * Hipposoft 2013 *
+ * *
+ * Purpose: JavaScript assets manifest for all controllers. See also *
+ * complex conditionals in the application view template and *
+ * per-controller scripts, used to avoid loading all possible *
+ * scripts on all possible controllers, bogging down cients. *
+ * *
+ * History: 09-Mar-2008 (ADH): Created. *
+ * 18-Jul-2013 (ADH): Rewritten for asset pipeline. *
+ ************************************************************************/
+
+//= require trackrecord/global
View
1 app/assets/javascripts/saved_reports.js
@@ -0,0 +1 @@
+//= require trackrecord/saved_report_editor
View
6 app/assets/javascripts/sessions.js
@@ -0,0 +1,6 @@
+//= require prototype/prototype
+//= require prototype_ujs/rails
+//= require scriptaculous/scriptaculous
+//= require scriptaculous/builder
+//= require trackrecord/check_for_javascript
+//= require trackrecord/section_revealer
View
1 app/assets/javascripts/task_imports.js
@@ -0,0 +1 @@
+//= require trackrecord/check_box_toggler
View
0 ...ascripts/trackrecord/check_box_toggler.js → ...ascripts/trackrecord/check_box_toggler.js
File renamed without changes.
View
0 ...ripts/trackrecord/check_for_javascript.js → ...ripts/trackrecord/check_for_javascript.js
File renamed without changes.
View
22 public/javascripts/application.js → app/assets/javascripts/trackrecord/global.js
@@ -1,6 +1,6 @@
/************************************************************************
- * File: application.js *
- * Hipposoft 2008 *
+ * File: global.js *
+ * Hipposoft 2008-2013 *
* *
* Purpose: Various JavaScript patches. *
* *
@@ -13,7 +13,8 @@
* state when the 'back' button is used. Attempt to patch *
* around the problem. *
* *
- * History: 09-Mar-2008 (ADH): Created. *
+ * History: 09-Mar-2008 (ADH): Created as "application.js". *
+ * 18-Jul-2013 (ADH): Moved to "trackrecord/global.js". *
************************************************************************/
/****************************************************************************\
@@ -60,21 +61,6 @@ function pageLoaded()
/* The page is being removed. Check through the buttons remembered in the
* 'load' handler and restore previous state if there has been a change.
- *
- * - On Firefox 3, you briefly get to see the button(s) change before the new
- * page loads and if you're very quick, for buttons that are re-enabled, can
- * click on the button again. Doh. But you have to try pretty hard for that.
- *
- * - On Opera 9, again you briefly see the change but can't click on the
- * button before the new page loads. Opera resets the button state when the
- * 'back' button is used anyway, but at least the script is harmless.
- *
- * - On Safari 3, the page transition is instantaneous so you don't get to see
- * the button state being reset. Top marks to WebKit.
- *
- * - On MSIE, this script almost certainly fails because it uses the DOM 2
- * event model. MSIE doesn't support it. The standard was published in late
- * 2000. That's truly pathetic, Microsoft. MSIE users - upgrade to Firefox.
*/
function pageUnloading()
View
247 app/assets/javascripts/trackrecord/saved_report_editor.js
@@ -0,0 +1,247 @@
+/************************************************************************
+ * File: saved_report_editor.js *
+ * Hipposoft 2013 *
+ * *
+ * Purpose: Navigation and calculation assistance for use during *
+ * report editing. *
+ * *
+ * History: 16-Jul-2013 (ADH): Created to get rid of as much inline *
+ * JavaScript in the edit view as possible. *
+ ************************************************************************/
+
+/* (Complex name chosen in an attempt to avoid namespace collisions)
+ *
+ * Once the saved report's name and "is shared" checkbox option fields
+ * have been rendered, call here with inline script to set up an observer
+ * that looks for changes in the "shared" flag and auto-generates a name,
+ * should one not have already been set by the user.
+ */
+
+function ukOrgPondTrackRecordSavedReportEditorObserveShareFlag()
+{
+ var inputFieldAutoValue;
+
+ var inputField = $( 'saved_report_title' );
+ var checkBox = $( 'saved_report_shared' );
+
+ checkBox.observe
+ (
+ 'click',
+ function()
+ {
+ var inputFieldCurrentValue = inputField.getValue();
+
+ if ( checkBox.getValue() )
+ {
+ /* Check box set. If the input field is empty, set it
+ * to the automatically determined value, which will
+ * need to be fetched from the server the first time.
+ */
+
+ if ( inputFieldCurrentValue == '' )
+ {
+ if ( inputFieldAutoValue )
+ {
+ inputField.setValue( inputFieldAutoValue );
+ }
+ else
+ {
+ inputField.disable();
+ inputField.setValue( "" );
+
+ new Ajax.Request
+ (
+ '<%= j( saved_report_auto_title_path() ) =%>',
+ {
+ method: 'get',
+ onSuccess: function( response )
+ {
+ inputFieldAutoValue = response.responseText;
+ inputField.enable();
+ inputField.setValue( inputFieldAutoValue );
+ }
+ }
+ );
+ }
+ }
+ }
+ else if ( inputFieldCurrentValue == inputFieldAutoValue )
+ {
+ /* Check box unset. If the input field value seems to
+ * have been set automatically, clear the value.
+ */
+
+ inputField.setValue( '' );
+ }
+ }
+ );
+};
+
+/* (Complex name chosen in an attempt to avoid namespace collisions)
+ *
+ * Once the saved report's date range related fields have been rendered,
+ * including any radio buttons of name "when_radio" with an inline style
+ * that sets 'display: none' so that non-JavaScript browsers do not show
+ * them, call here to associate event listeners that show/hide related
+ * table rows associated with those radios. The radio must have a CSS
+ * class name that matches a CSS class name on the table rows of
+ * interest; these are shown/hidden and values of 'input' and 'select'
+ * fields within those rows are restored/cleared as radios are chosen.
+ */
+
+function ukOrgPondTrackRecordSavedReportEditorObserveSectionRadios()
+{
+ /* Since this code only refers to form elements higher up in the
+ * page, it can only run when things it wants are already present
+ * in the DOM. Thus it can run immediately, not needing to wait
+ * until the 'load' event.
+ */
+
+ var radios = $$( 'input[name~=when_radio]' );
+
+ /* As radios are selected, hidden ones need related field values
+ * cleared so the user doesn't unwittingly submit form fields
+ * they can't see on JS-capable browsers; yet for the "edit saved
+ * report" case, we do need to remember related field values and
+ * restore them for the selected radio button. Use the following
+ * as a data store. Name chosen to avoid namespace collision.
+ */
+
+ this.ukOrgPondTrackRecordTimesheetEditorRadioStore = {};
+
+ /* This is called to establish initial state and also whenever a
+ * radio changes state. It runs through all radios, restoring the
+ * values for the fields related to the selected radio, while
+ * hiding other fields and clearing their values.
+ */
+
+ function showOrHide()
+ {
+ radios.each
+ (
+ function( radio, index )
+ {
+ var rows = $$( "tr." + radio.className );
+ var display;
+
+ if ( radio.getValue() ) display = null;
+ else display = "none";
+
+ rows.each
+ (
+ function( row, index )
+ {
+ row.style.display = display;
+ row.select( 'input, select' ).each
+ (
+ function( field, index )
+ {
+ if ( display == null )
+ {
+ field.setValue( this.ukOrgPondTrackRecordTimesheetEditorRadioStore[ field.id ] || "" );
+ }
+ else
+ {
+ field.setValue( "" );
+ }
+ }
+ );
+ }
+ );
+ }
+ );
+ }
+
+ /* Now establish initial state, selecting an appropriate radio and
+ * add an appropriate event listener to them all.
+ */
+
+ var fieldsHadValues = false;
+
+ radios.each
+ (
+ function( radio, index )
+ {
+ radio.style.display = "inline";
+ radio.observe
+ (
+ 'change',
+ function() { showOrHide(); }
+ );
+
+ var rows = $$( "tr." + radio.className );
+
+ rows.each
+ (
+ function( row, index )
+ {
+ row.select( 'input, select' ).each
+ (
+ function( field, index )
+ {
+ this.ukOrgPondTrackRecordTimesheetEditorRadioStore[ field.id ] = field.getValue();
+
+ if ( field.getValue() )
+ {
+ fieldsHadValues = true;
+ radio.setValue( true )
+ }
+ }
+ );
+ }
+ );
+ }
+ );
+
+ if ( ! fieldsHadValues ) radios[ radios.length - 1 ].setValue( true );
+
+ showOrHide();
+};
+
+/* (Complex name chosen in an attempt to avoid namespace collisions)
+ *
+ * Once the saved report's "calculate for these users only" multiple
+ * selection list and "Per-user breakdown" checkbox flags have been
+ * rendered, call here to associate event listeners that enable or
+ * disable the "breakdown" checkbox according to whether or not any
+ * users are selected in the selection list.
+ *
+ * This is because if no users are selected the report engine saves
+ * (a lot of) time by not doing user calculations, but leaving the
+ * "per-user breakdown" flag enabled could cause confusion.
+ *
+ * (I did consider having the "breakdown" flag implicitly enable
+ * analysis across all users, but I don't want to accidentally have
+ * the report engine and thus the server heavily loaded in this way
+ * unless the user *really* means it! Someone must explicitly
+ * select some or all users. This also means that if user accounts
+ * are added to TrackRecord later, a user-based report's output
+ * won't change unless it is edited and those new users are added -
+ * the desirable outcome in the majority of use cases).
+ */
+
+function ukOrgPondTrackRecordSavedReportEditorObserveUserList()
+{
+ var selectionList = $( 'saved_report_reportable_user_ids' );
+
+ function setCheckboxState()
+ {
+ var someSelected = ( selectionList.getValue().length ) !== 0;
+ var checkbox = $( 'saved_report_user_details' );
+ var label = $( 'saved_report_user_details_label' );
+
+ if ( someSelected )
+ {
+ checkbox.enable();
+ label.style.opacity = null;
+ }
+ else
+ {
+ checkbox.disable();
+ label.style.opacity = 0.3;
+ }
+ }
+
+ selectionList.observe( 'change', function() { setCheckboxState(); } );
+ setCheckboxState();
+}
View
0 ...vascripts/trackrecord/section_revealer.js → ...vascripts/trackrecord/section_revealer.js
File renamed without changes.
View
0 ...vascripts/trackrecord/timesheet_editor.js → ...vascripts/trackrecord/timesheet_editor.js
File renamed without changes.
View
0 ...vascripts/trackrecord/timesheet_viewer.js → ...vascripts/trackrecord/timesheet_viewer.js
File renamed without changes.
View
0 public/stylesheets/application.css → app/assets/stylesheets/application.css
File renamed without changes.
View
0 ...stylesheets/calendar_date_select/blue.css → ...stylesheets/calendar_date_select/blue.css
File renamed without changes.
View
0 ...lesheets/calendar_date_select/default.css → ...lesheets/calendar_date_select/default.css
File renamed without changes.
View
0 ...tylesheets/calendar_date_select/green.css → ...tylesheets/calendar_date_select/green.css
File renamed without changes.
View
0 ...tylesheets/calendar_date_select/plain.css → ...tylesheets/calendar_date_select/plain.css
File renamed without changes.
View
0 .../stylesheets/calendar_date_select/red.css → .../stylesheets/calendar_date_select/red.css
File renamed without changes.
View
0 ...ylesheets/calendar_date_select/silver.css → ...ylesheets/calendar_date_select/silver.css
File renamed without changes.
View
0 public/stylesheets/leightbox/leightbox.css → ...ssets/stylesheets/leightbox/leightbox.css
File renamed without changes.
View
0 public/stylesheets/scaffold.css → app/assets/stylesheets/scaffold.css
File renamed without changes.
View
140 public/stylesheets/trackrecord_all.css → ...ssets/stylesheets/trackrecord_all.css.erb
@@ -51,9 +51,19 @@ tr.top {
vertical-align: top;
}
-/* Heading font */
+.wide {
+ width: 100%;
+}
+
+.narrow {
+ white-space: nowrap;
+ width: 1px;
+}
+
+/* Heading font (and navigation bar feature area) */
-h1, h2, h3, h4, h5, h6 {
+h1, h2, h3, h4, h5, h6,
+div#header * {
font-family: "Trebuchet MS", sans-serif;
font-weight: bold;
}
@@ -72,7 +82,6 @@ div#header {
background: -o-linear-gradient(top, rgba(96,108,136,1) 0%,rgba(63,76,107,1) 100%); /* Opera 11.10+ */
background: -ms-linear-gradient(top, rgba(96,108,136,1) 0%,rgba(63,76,107,1) 100%); /* IE10+ */
background: linear-gradient(to bottom, rgba(96,108,136,1) 0%,rgba(63,76,107,1) 100%); /* W3C */
- filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#606c88', endColorstr='#3f4c6b',GradientType=0 ); /* IE6-9 */
-webkit-box-shadow: 0px 5px 5px rgba(0, 0, 0, 0.3);
-moz-box-shadow: 0px 5px 5px rgba(0, 0, 0, 0.3);
@@ -103,8 +112,6 @@ div#header span.version {
}
div#header h1, div#navbar a {
- font-family: "Trebuchet MS", sans-serif;
- font-weight: bold;
margin: 0;
}
@@ -155,7 +162,6 @@ div#navbar li.current {
background: -o-linear-gradient(top, #1d6721 0%,#81ff81 100%); /* Opera 11.10+ */
background: -ms-linear-gradient(top, #1d6721 0%,#81ff81 100%); /* IE10+ */
background: linear-gradient(to bottom, #1d6721 0%,#81ff81 100%); /* W3C */
- filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#1d6721', endColorstr='#81ff81',GradientType=0 ); /* IE6-9 */
-webkit-box-shadow: 0px 5px 5px rgba(0, 0, 0, 0.3);
-moz-box-shadow: 0px 5px 5px rgba(0, 0, 0, 0.3);
@@ -169,7 +175,7 @@ div#navbar li.current a {
/* Float management */
-div.float_clear {
+.float_clear {
clear: both;
}
@@ -203,7 +209,6 @@ div#footer {
background: -o-linear-gradient(top, rgba(96,108,136,1) 0%,rgba(63,76,107,1) 100%); /* Opera 11.10+ */
background: -ms-linear-gradient(top, rgba(96,108,136,1) 0%,rgba(63,76,107,1) 100%); /* IE10+ */
background: linear-gradient(to bottom, rgba(96,108,136,1) 0%,rgba(63,76,107,1) 100%); /* W3C */
- filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#606c88', endColorstr='#3f4c6b',GradientType=0 ); /* IE6-9 */
-webkit-box-shadow: 0px -5px 5px rgba(0, 0, 0, 0.3);
-moz-box-shadow: 0px -5px 5px rgba(0, 0, 0, 0.3);
@@ -266,7 +271,7 @@ table.openid form {
table.openid input#openid_url {
font-size: 100%;
- background: url( ../images/trackrecord/openid_small_logo.png ) 2px 50% no-repeat;
+ background: url( <%= asset_path( "trackrecord/openid_small_logo.png" ) %> ) 2px 50% no-repeat;
padding-left: 22px;
display: inline;
}
@@ -280,7 +285,7 @@ table.openid input#commit {
div#home_general,
div#home_management,
div#home_admin {
- float:left;
+ float: left;
width: 32%;
}
@@ -367,7 +372,6 @@ table.list th.sorted_column_asc {
background: -o-linear-gradient(top, #a90329 0%,#8f0222 44%,#6d0019 100%); /* Opera 11.10+ */
background: -ms-linear-gradient(top, #a90329 0%,#8f0222 44%,#6d0019 100%); /* IE10+ */
background: linear-gradient(to bottom, #a90329 0%,#8f0222 44%,#6d0019 100%); /* W3C */
- filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#a90329', endColorstr='#6d0019',GradientType=0 ); /* IE6-9 */
}
table.list th.sorted_column_desc {
@@ -378,15 +382,14 @@ table.list th.sorted_column_desc {
background: -o-linear-gradient(top, #03a929 0%,#028f22 44%,#006d19 100%); /* Opera 11.10+ */
background: -ms-linear-gradient(top, #03a929 0%,#028f22 44%,#006d19 100%); /* IE10+ */
background: linear-gradient(to bottom, #03a929 0%,#028f22 44%,#006d19 100%); /* W3C */
- filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#03a929', endColorstr='#006d19',GradientType=0 ); /* IE6-9 */
}
table.list th.sorted_column_asc a:before {
- content: url( ../images/trackrecord/up.png );
+ content: url( <%= asset_path( "trackrecord/up.png" ) %> );
}
table.list th.sorted_column_desc a:before {
- content: url( ../images/trackrecord/down.png );
+ content: url( <%= asset_path( "trackrecord/down.png" ) %> );
}
/* Navigation links within list tables (overriding some styles above) */
@@ -462,7 +465,6 @@ tr.info {
background: -o-linear-gradient(top, rgba(96,108,136,1) 0%,rgba(63,76,107,1) 100%); /* Opera 11.10+ */
background: -ms-linear-gradient(top, rgba(96,108,136,1) 0%,rgba(63,76,107,1) 100%); /* IE10+ */
background: linear-gradient(to bottom, rgba(96,108,136,1) 0%,rgba(63,76,107,1) 100%); /* W3C */
- filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#606c88', endColorstr='#3f4c6b',GradientType=0 ); /* IE6-9 */
color: #fff;
white-space: nowrap;
@@ -620,7 +622,6 @@ table.timesheet_chart th {
background: -o-linear-gradient(top, rgba(96,108,136,1) 0%,rgba(63,76,107,1) 100%); /* Opera 11.10+ */
background: -ms-linear-gradient(top, rgba(96,108,136,1) 0%,rgba(63,76,107,1) 100%); /* IE10+ */
background: linear-gradient(to bottom, rgba(96,108,136,1) 0%,rgba(63,76,107,1) 100%); /* W3C */
- filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#606c88', endColorstr='#3f4c6b',GradientType=0 ); /* IE6-9 */
font-weight: normal;
color: #fff;
@@ -663,6 +664,60 @@ table.report {
white-space: nowrap;
}
+table.report_edit_wrapper {
+ border: none;
+ border-collapse: collapse;
+ width: 100%;
+ padding: 0;
+ margin: 0;
+}
+
+table.report_edit_wrapper > tbody > tr {
+ vertical-align: top;
+ padding: 0;
+ margin: 0;
+}
+
+table.report_edit_wrapper > tbody > tr > td {
+ width: 50%;
+ padding: 0;
+ margin: 0;
+}
+
+table.report_edit_wrapper > tbody > tr > td.odd {
+ background: #f8f8f8;
+}
+
+table.report_edit_wrapper > tbody > tr > td.even {
+ background: #f0f0f0;
+}
+
+div.report_edit_section {
+ padding: 1px 15px; /* 1px to work around accidental/buggy collapse on WebKit with 0 of *all* padding at top/bottom edges, including elements that have their own padding set, such as headings */
+ margin: 0;
+ height: 100%;
+}
+
+div.report_edit_section textarea,
+div.report_edit_section select[ multiple~=multiple ] {
+ width: 100%;
+}
+
+h2.report_subtitle {
+ margin-top: 3em;
+}
+
+table.display_table.report_actions {
+ border: none;
+ margin: none;
+ border-collapse: collapse;
+}
+
+table.display_table.report_actions td {
+ padding: 5px;
+ vertical-align: baseline;
+}
+
/* Timesheet editor and display grids; reports also use some of
* these styles.
*/
@@ -693,10 +748,35 @@ table.ts_edit_table td.ts_edit_task {
padding: 0 2px;
}
+table.ts_edit_table td.ts_edit_task table,
+table.ts_edit_table td.ts_edit_task tr,
+table.ts_edit_table td.ts_edit_task td {
+ margin: 0;
+}
+
table.ts_show_table td.ts_show_task {
padding: 2px 2px 2px 6px;
}
+td.ts_task_user_detail {
+ font-size: 85%;
+ color: #888;
+}
+
+tr.odd td.ts_task_user_detail {
+ border-top: 1px solid #d4d4d4;
+}
+
+tr.even td.ts_task_user_detail {
+ border-top: 1px solid #c3c3c3;
+}
+
+td.ts_work_user_detail,
+th.ts_total_user_detail {
+ font-size: 85%;
+ opacity: 0.5;
+}
+
table.ts_edit_table td a,
table.ts_show_table td a {
color: inherit;
@@ -730,7 +810,7 @@ td.ts_edit_work input {
table.ts_edit_table th.ts_edit_heading,
table.ts_show_table th.ts_show_heading {
padding: 4px 2px 4px 6px;
- font-size: 90%;
+ font-size: 100%;
text-align: left;
background: #c8c8c8;
}
@@ -744,7 +824,7 @@ table.ts_show_table .total,
table.ts_edit_table .overall_total,
table.ts_show_table .overall_total {
text-align: center;
- padding: 0 2px;
+ padding: 2px;
}
table.ts_edit_table .total:first-child,
@@ -772,6 +852,14 @@ table.bulk_task_import {
border-collapse: collapse;
}
+table.bulk_task_import tr {
+ vertical-align: baseline;
+}
+
+table.bulk_task_import tr td {
+ padding-left: 5px;
+}
+
table.bulk_task_import td.task_title {
width: 100%;
}
@@ -791,6 +879,15 @@ table.bulk_task_import tr.breadcrumb td {
border-top: 7px solid white;
}
+table.bulk_task_import tr td table.bulk_task_import {
+ margin: 0;
+ width: auto;
+}
+
+table.bulk_task_import tr td table.bulk_task_import tr td {
+ padding-left: 0;
+}
+
/* Committed / not committed timesheet labels; active / not active
* items; overrunning / not overrunning labels.
*/
@@ -846,10 +943,11 @@ dt {
img.help {
position: relative;
+ vertical-align: top;
padding: 2px 1px 1px 1px;
top: 0px;
- background: url( ../images/trackrecord/help_sprite.png ) 1px 2px no-repeat;
+ background: url( <%= asset_path( "trackrecord/help_sprite.png" ) %> ) 1px 2px no-repeat;
}
a:hover img.help {
@@ -883,6 +981,10 @@ textarea.tree_selector_text {
padding-left: 17px; /* Check box image is 16px wide with some built-in padding and text labels rarely precisely abut their left edge due to the shapes of characters and anti-aliasing; default '1em' leaves check box too close to text for my liking */
}
+a.ygtvspacer:hover {
+ background: none; /* Override default a:hover rule in scaffold.css */
+}
+
/* Leightbox links */
a.lbOn {
View
0 public/stylesheets/trackrecord_print.css → app/assets/stylesheets/trackrecord_print.css
File renamed without changes.
View
112 app/controllers/reports_controller.rb
@@ -4,10 +4,10 @@
#
# Purpose:: Generate reports describing timesheet entries in various
# different ways. The ReportsController deals with finding
-# SavedReport model instances and, using TrackRecordReport's
-# Report class with its attributes configured from the
-# SavedReport instance's contents, generating and showing
-# reports to users.
+# SavedReport model instances and, using the
+# TrackRecordReport::Report class with its attributes
+# configured from the SavedReport instance's contents,
+# generating and showing reports to users.
# ----------------------------------------------------------------------
# 09-Feb-2008 (ADH): Created.
# 19-Oct-2011 (ADH): Functionality split up into this file and
@@ -93,12 +93,52 @@ def show
flash[ :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."
end
- respond_to do | format |
- format.html { render( { :template => 'reports/show' } ) }
- format.csv { csv_stream_report() }
+ # Everything's ready to render the report in @report, but should we
+ # actually be running a generator instead?
+
+ if ( params.has_key?( :generator ) )
+
+ # Generate a report via a plugin generator. Need to figure out which
+ # one to use and get its parameters sent over.
+
+ supported_types = [ :task, :user, :comprehensive ]
+
+ TrackRecordReportGenerator.submodules.each do | submodule |
+
+ self.extend( submodule )
+
+ params_key = submodule.name.underscore
+
+ if ( params.has_key?( params_key ) )
+ generator_params = params[ params_key ]
+ report_type = supported_types.select do | supported_type |
+ generator_params.has_key?( supported_type )
+ end.first
+
+ if ( understands?( report_type ) )
+ generator_params.delete( report_type )
+ result = generate( report_type, @report, generator_params )
+
+ unless ( result.nil? )
+ flash[ :error ] = result.to_s
+ render( { :template => 'reports/show' } )
+ end
+
+ break
+ end
+ end
+ end
+
+ else
+
+ # Simple render.
+
+ render( { :template => 'reports/show' } )
+
end
flash.delete( :warning ) # Else it shows on the *next* fetched page too
+ flash.delete( :error )
end
private
@@ -108,18 +148,25 @@ def show
#
# Name Meaning
# =========================================================================
- # @is_task_type If 'true' a CSV format report should provide a task-based
- # breakdown of time, else a user-based breakdown. Undefined
- # for non-CSV reports.
+ # @report_type :task - task-based report, no per-user details;
+ # :user - user-summary report, no per-task details;
+ # :comprehensive - per-task, per-user full report.
#
# @exclude_title If 'true' a title row should be excluded in a CSV format
# report. Undefined for non-CSV reports.
#
def read_options
if ( request.format.csv? )
- @is_task_type = params[ :user_report ].nil?
- type = @is_task_type ? 'task' : 'user'
- @exclude_title = ! ( params[ "include_title_#{ type }" ] == '1' )
+
+ if ( params.has_key?( :user_report ) )
+ @report_type = :user
+ elsif ( params.has_key?( :comprehensive_report ) )
+ @report_type = :comprehensive
+ else
+ @report_type = :task
+ end
+
+ @exclude_title = ! ( params[ "include_title_#{ @report_type }" ] == '1' )
end
end
@@ -134,8 +181,6 @@ def csv_stream_report()
headings << ' (com.)' if ( @report.include_committed )
headings << ' (not com.)' if ( @report.include_not_committed )
- @is_task_type = params[ :user_report ].nil?
-
# Old-style streaming has becomes unreliable lately; the v1.0 approach as
# per "http://oldwiki.rubyonrails.org/rails/pages/HowtoExportDataAsCSV"
# often failed with Rails 2.3 and/or certain FasterCSV versions (I never
@@ -156,7 +201,7 @@ def csv_stream_report()
fend_at = @report.range.last.strftime( fformat )
filename = "report_#{ label }_on_#{ stoday }_for_#{ sstart_at }_to_#{ send_at }.csv"
title = [
- "#{ @is_task_type ? 'Task' : 'User' } report on #{ ftoday }",
+ "#{ @report_type.to_s.capitalize } report on #{ ftoday }",
"From #{ fstart_at }",
"To #{ fend_at }",
'(inclusive)'
@@ -170,10 +215,13 @@ def csv_stream_report()
csv << title
end
- if ( @is_task_type )
- csv_report_by_task( csv, headings )
- else
- csv_report_by_user( csv, headings )
+ case @report_type
+ when :user
+ csv_report_by_user( csv, headings )
+ when :comprehensive
+ csv_report_by_task( csv, headings, true )
+ else
+ csv_report_by_task( csv, headings, false )
end
end
@@ -194,7 +242,11 @@ def csv_stream_report()
# of numbers that "hours" will output based on prevailing instance variables
# (see that function for details).
#
- def csv_report_by_task( csv, headings )
+ # The third parameter is a "comprehensive report" flag, forcing per-user
+ # breakdown on the assumption that a report is appropriate compiled. Otherwise
+ # omit the parameter to obey the report's own flags.
+ #
+ def csv_report_by_task( csv, headings, comprehensive = @report.user_details )
# Assemble the heading row.
file_row = [ @report.column_title, 'Code', 'Billable?', 'Active?' ]
@@ -271,6 +323,24 @@ def csv_report_by_task( csv, headings )
end
csv << file_row.flatten
+
+ if ( comprehensive )
+ @report.filtered_users.each_index do | index |
+ user = @report.filtered_users[ index ]
+ user_total = TrackRecordReport::ReportColumnTotal.new
+
+ file_row = [ " ---- #{ user.name }", '', '', '' ]
+
+ row.cells.each do | cell |
+ user_total.add_cell( cell )
+ file_row << hours( cell.user_data[ index ] )
+ end
+
+ file_row << hours( user_total )
+ end
+
+ csv << file_row.flatten
+ end
end
# Column totals.
View
41 app/controllers/saved_reports_controller.rb
@@ -2,7 +2,7 @@
# File:: saved_reports_controller.rb
# (C):: Hipposoft 2011
#
-# Purpose:: Managed saved collections of parameters used to generate
+# Purpose:: Manage saved collections of parameters used to generate
# reports.
# ----------------------------------------------------------------------
# 19-Oct-2011 (ADH): Created.
@@ -25,12 +25,12 @@ def index
# application_helper.rb for details.
@columns = [
- { :header_text => 'Name', :value_method => 'title', :value_in_place => true, :sort_by => 'title' },
- { :header_text => 'Shared', :value_method => 'shared', :value_in_place => true, :sort_by => 'shared' },
- { :header_text => 'Last edited', :value_helper => 'reporthelp_updated_at', :sort_by => 'updated_at' },
- { :header_text => 'Start date', :value_helper => 'reporthelp_start_date' },
- { :header_text => 'End date', :value_helper => 'reporthelp_end_date' },
- { :header_text => 'Owner', :value_helper => 'reporthelp_owner', :sort_by => 'users.name' }
+ { :header_text => 'Name', :value_method => 'title', :value_in_place => true, :sort_by => 'title' },
+ { :header_text => 'Shared', :value_method => 'shared', :value_in_place => true, :sort_by => 'shared' },
+ { :header_text => 'Last edited', :value_helper => 'reporthelp_updated_at', :sort_by => 'updated_at' },
+ { :header_text => 'Start date', :value_helper => 'reporthelp_start_date', :sort_by => 'range_start_cache' },
+ { :header_text => 'End date', :value_helper => 'reporthelp_end_date', :sort_by => 'range_end_cache' },
+ { :header_text => 'Owner', :value_helper => 'reporthelp_owner', :sort_by => 'users.name' }
]
options = appctrl_index_assist( SavedReport )
@@ -84,9 +84,30 @@ def index
# Prepare for the 'new report' view.
#
def new
- @saved_report = SavedReport.new
- @saved_report.user = @user
- @user_array = @current_user.restricted? ? [ @current_user ] : User.active
+
+ # Implement copying; this could've been done in a new controller
+ # but no matter how you look at it, "new-with-an-ID" does not map
+ # cleanly to Rails REST and doing such an action in a separate
+ # controller just leads to code duplication and difficulties with
+ # the concept of "current controller" etc. in the view (we end up
+ # wanting to render the SavedReportsController edit view anyway).
+
+ @saved_report = nil
+
+ if ( params.has_key?( :saved_report_id ) )
+ @saved_report = SavedReport.find_by_id( params[ :saved_report_id ] ) # No exception raised if record is not found
+
+ unless ( @saved_report.nil? )
+ @saved_report.title << " (copy)"
+ end
+ end
+
+ if ( @saved_report.nil? )
+ @saved_report = SavedReport.new
+ @saved_report.user = @user
+ end
+
+ @user_array = @current_user.restricted? ? [ @current_user ] : User.active
end
# Generate a report based on a 'new report' form submission.
View
17 app/controllers/saved_reports_copy_controller.rb
@@ -0,0 +1,17 @@
+########################################################################
+# File:: saved_reports_copy_controller.rb
+# (C):: Hipposoft 2013
+#
+# Purpose:: Provide a RESTful mechanism for copying a saved report,
+# presenting the copy as an editable new unsaved item.
+# ----------------------------------------------------------------------
+# 31-Jul-2013 (ADH): Created.
+########################################################################
+
+class SavedReportsCopyController < SavedReportsBaseController
+ def new
+ @saved_report = SavedReport.new
+ @saved_report.user = @user
+ @user_array = @current_user.restricted? ? [ @current_user ] : User.active
+ end
+end
View
22 app/controllers/task_imports_controller.rb
@@ -122,12 +122,24 @@ def update
# Right, good to go! Do the import.
- project_id = @import.project_id
- project = nil
-
begin
Task.transaction do
- project = Project.find( project_id )
+
+ # Create a new project in passing?
+
+ if params.has_key?( :do_import_new_task )
+ project = Project.new
+ project.title = @import.new_project_title
+ project.customer_id = @import.new_project_customer_id
+ project.description = "Created for XML bulk task import on #{ Time.now }"
+ begin
+ project.save!
+ rescue => inner_error
+ raise "Cannot create project: #{ inner_error }"
+ end
+ else
+ project = Project.find( @import.project_id )
+ end
to_import.each do | source_task |
Task.new do | destination_task |
@@ -159,6 +171,6 @@ def update
# Is the current action permitted?
#
def permitted?
- appctrl_not_permitted() if( @current_user.restricted? )
+ appctrl_not_permitted() if ( @current_user.restricted? )
end
end
View
158 app/controllers/timesheet_force_commits_controller.rb
@@ -0,0 +1,158 @@
+########################################################################
+# File:: timesheet_force_commits_controller.rb
+# (C):: Hipposoft 2013
+#
+# Purpose:: Allow administrators to bulk-commit timesheets forcibly, to
+# help deal with users who never manually commit them.
+# ----------------------------------------------------------------------
+# 31-Jul-2013 (ADH): Created.
+########################################################################
+
+class TimesheetForceCommitsController < ApplicationController
+
+ uses_prototype()
+
+ # Security and prerequisites
+
+ before_filter( :permitted? )
+ before_filter( :assign )
+
+ def new
+ unless ( @timesheets.count.zero? )
+ @object.earliest = @object.earliest_limit
+ @object.latest = @object.latest_limit
+ end
+ end
+
+ def create
+
+ # Unless we get here by someone hacking around with a web page,
+ # the only explanation for a submitted form but a re-counted
+ # timesheet count of zero would be that users submitted things
+ # in other browser sessions while the admin was thinking about
+ # filling in the submitted form.
+
+ if ( @timesheets.count.zero? )
+ flash[ :error ] = "No action taken - while you were using the form, all timesheets were committed by their users anyway."
+ redirect_to( home_path() ) and return
+ end
+
+ # Get a valid, parsed set of Dates into @earliest/@latest
+ # - or nil.
+
+ form_data = params[ :timesheet_force_commit ]
+
+ # The "to_date" call may cause date parsing exceptions.
+
+ begin
+ @object.earliest = @object.to_date( form_data[ :earliest ] )
+ @object.latest = @object.to_date( form_data[ :latest ] )
+
+ rescue => error
+ complain( error ) and return
+
+ end
+
+ # Pick earliest/latest instead of 'nil' and bounds check dates.
+
+ @object.earliest = @object.earliest_limit if ( @object.earliest.nil? || @object.earliest < @object.earliest_limit )
+ @object.latest = @object.latest_limit if ( @object.latest.nil? || @object.latest > @object.latest_limit )
+
+ # Update timesheets.
+
+ begin
+
+ Timesheet.transaction do
+
+ # Construct an ActiveRecord::Relation instance with the
+ # appropriate constraints, then iterate over it in batches
+ # using "find_each".
+ #
+ # Remember that "@object.latest" is an inclusive timesheet
+ # end date and everything runs in UTC.
+ #
+ # We can't use "update_all", even though it'd be fast, as
+ # it doesn't trigger callbacks or validations and both are
+ # important (particularly callbacks).
+
+ timesheets = Timesheet.where( :committed => false )
+ timesheets = timesheets.where( 'start_day_cache >= ?', @object.earliest )
+ timesheets = timesheets.where( 'start_day_cache <= ?', @object.latest - 6.days )
+
+ timesheets.find_each do | timesheet |
+ timesheet.committed = true
+ timesheet.save!
+ end
+ end
+
+ rescue => error
+ complain( error ) and return
+
+ end
+
+ flash[ :notice ] = "Timesheets committed successfully."
+ redirect_to( home_path() )
+ end
+
+private
+
+ # Is the current action permitted?
+ #
+ def permitted?
+ appctrl_not_permitted() unless ( @current_user.privileged? )
+ end
+
+ # Assign useful instance variables.
+ #
+ def assign
+
+ # Start by getting all uncommitted timesheets.
+
+ @timesheets = Timesheet.where( :committed => false ).order( 'start_day_cache ASC' )
+
+ # Further restrict this to timesheets ending before the start of
+ # the current month. If the current week falls into the previous
+ # month - i.e. the current week starts sooner than the month -
+ # then go back one month from there. The idea is to be able to
+ # "close off" a historical month, without accidentally commiting
+ # timesheets in "this" month (or close to it) that a user may be
+ # legitimately editing.
+ #
+ # Under Rails, subtracting "1.month" from a Date does do the right
+ # thing - it doesn't just subtract a fixed number of days. Whatever
+ # month it is now, we end up with the start of the previous month.
+
+ earliest = Date.today.beginning_of_month
+ compare = Date.today.beginning_of_week
+ @earliest = earliest - 1.month if ( compare < 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
+ # another way, any timesheet that starts at or earlier than 7
+ # days prior to "earliest" must finish just before "earliest".
+
+ @timesheets = @timesheets.where( 'start_day_cache <= ?', @earliest - 7.days )
+
+ unless ( @timesheets.count.zero? )
+ @object = TimesheetForceCommit.new()
+ @object.earliest_limit = @timesheets.first.start_day_cache
+ @object.latest_limit = @timesheets.last.start_day_cache + 6.days
+ end
+ end
+
+ # Complain about the given exception, showing the 'new' form again.
+ #
+ def complain( error )
+
+ # Restore raw, user-set data so the user sees exactly what they
+ # typed into form fields, rather than a parsed version.
+
+ form_data = params[ :timesheet_force_commit ]
+
+ @object.earliest = form_data[ :earliest ]
+ @object.latest = form_data[ :latest ]
+
+ flash[ :error ] = "Could not commit timesheets: #{ error }"
+ render( { :action => :new } )
+ end
+end
View
22 app/controllers/timesheets_controller.rb
@@ -160,16 +160,16 @@ def update
end
@timesheet.reload()
+ set_next_prev_week_variables( @timesheet )
if ( @errors.empty? )
flash[ :notice ] = "Week #{ @timesheet.week_number } changes saved."
else
+ restore_errant_form_data()
render( :action => :edit )
return
end
- set_next_prev_week_variables( @timesheet )
-
# Save and edit next or previous editable week, creating a clone
# timesheet for a previously unused week if need be?
@@ -469,19 +469,19 @@ def update_backend( timesheet )
begin
timesheet.save!
rescue => error
- errors.push( "Timesheet: #{ error.message }" )
+ errors.push( "Timesheet: #{ error }" )
end
# If there were any errors handled internally that did not go via
# an exception, the transaction won't roll back. Force it to do so.
- raise( "Rollback" ) unless( errors.empty? )
+ raise( "Changes cannot be saved" ) unless( errors.empty? )
end
return errors
rescue => error # Don't use 'ensure', because early exits elsewhere may fail then.
- errors.push( "Timesheet: #{ error.message }" )
+ errors.push( "Timesheet: #{ error }" )
return errors
end
@@ -533,4 +533,16 @@ def clone_timesheet( original, week_number )
end
+ # In the event of an error, a timesheet is rolled back. Individual
+ # work packets are maintained if not errant, but other data is not.
+ # Restore any timesheet non-associated data - errant or otherwise -
+ # so the user sees what they tried to submit back in the editor form.
+ #
+ def restore_errant_form_data
+ timesheet_hash = params[ :timesheet ]
+
+ @timesheet.week_number = timesheet_hash[ :week_number ]
+ @timesheet.description = timesheet_hash[ :description ]
+ @timesheet.committed = timesheet_hash[ :committed ]
+ end
end
View
15 app/helpers/application_helper.rb
@@ -96,7 +96,7 @@ def apphelp_slug
slug = 'Home'
elsif ( ctname == 'sessions' and action == 'new' )
slug << 'Sign in'
- elsif ( action == 'index' or action == 'list' )
+ elsif ( action == 'index' or action == 'list' or ctname == 'timesheet_force_commits' )
slug << apphelp_heading()
elsif ( ctname == 'reports' )
slug << link_to( 'Reports', new_user_saved_report_path( @current_user ) ) <<
@@ -129,7 +129,8 @@ def apphelp_boolean( bool )
# are wrapped by a DIV with class 'messages'. If there are no messages
# to show, an empty string is returned. Optionally pass an indent string
# to add at the front of any non-empty line of output. If a non-empty
- # result is returned, note that it will be terminated by "\n\n".
+ # result is returned, note that it will be terminated by "\n\n". Result
+ # contains HTML but is safe and marked as such.
#
def apphelp_flash_messages( indent = '' )
output = ''
@@ -210,9 +211,9 @@ def apphelp_string_hours( hours, alt_str, empty_str = nil )
#
def apphelp_date( date )
if ( date.is_a?( Date ) )
- return date.strftime( '<span class="nowrap">%Y-%m-%d</span>' )
+ return date.strftime( '<span class="nowrap">%Y-%m-%d</span>' ).html_safe()
else
- return date.strftime( '<span class="nowrap">%Y-%m-%d</span> <span class="nowrap">%H:%M:%S</span>' )
+ return date.strftime( '<span class="nowrap">%Y-%m-%d</span> <span class="nowrap">%H:%M:%S</span>' ).html_safe()
end
end
@@ -558,7 +559,11 @@ def apphelp_list_row( structure, item, actions_method, with_reports = false )
output << " <td class=\"list_actions\">\n"
actions.each do | action |
output << " "
- output << link_to( action.humanize, { :action => action, :id => item.id } )
+ if ( action.class == Hash )
+ output << link_to( action[ :title ], action[ :url ] % item.id.to_s )
+ else
+ output << link_to( action.humanize, { :action => action, :id => item.id } )
+ end
output << "\n"
end
View
5 app/helpers/charts_helper.rb
@@ -47,9 +47,8 @@ def charthelp_image( duration, committed, not_committed, width, height )
return image_tag(
charthelp_image_url( duration, committed, not_committed, width ),
{
- :size => "#{ width }x#{ height }",
- :alt => "Overview",
- :align => "left"
+ :size => "#{ width }x#{ height }",
+ :alt => "Overview"
}
)
end
View
1 app/helpers/help_helper.rb
@@ -37,7 +37,6 @@ def help_url( url )
'trackrecord/blank.png',
:size => '16x16',
:alt => '?',
- :align => 'top',
:class => 'help'
),
url,
View
116 app/helpers/reports_helper.rb
@@ -19,9 +19,10 @@ def initialize( year, week )
@id = "#{ year }_#{ week }"
@start = "#{ week } (starts Mon #{ Timesheet.date_for( year, week, TimesheetRow::FIRST_DAY ) })"
@end = "#{ week } (ends Sun #{ Timesheet.date_for( year, week, TimesheetRow::LAST_DAY ) })"
+ @all = "#{ week }, Mon #{ Timesheet.date_for( year, week, TimesheetRow::FIRST_DAY ) } - Sun #{ Timesheet.date_for( year, week, TimesheetRow::LAST_DAY ) }"
end
- attr_reader( :start, :end, :id )
+ attr_reader( :start, :end, :all, :id )
end
# Helper class for ReportYear and reporthelp_month_selection.
@@ -31,9 +32,10 @@ def initialize( year, month )
@id = "#{ year }_#{ month }"
@start = "#{ month } (start of #{ Date::MONTHNAMES[ month ] } #{ year })"
@end = "#{ month } (end of #{ Date::MONTHNAMES[ month ] } #{ year })"
+ @all = "#{ month }, #{ Date::MONTHNAMES[ month ] } #{ year }"
end
- attr_reader( :start, :end, :id )
+ attr_reader( :start, :end, :all, :id )
end
# Helper class for reporthelp_week_selection and reporthelp_month_selection.
@@ -84,7 +86,7 @@ def initialize( year, date_range = nil )
@weeks.unshift( ReportWeek.new( year, week ) )
end
- # Work out hte months, again limited as above.
+ # Work out the months, again limited as above.
( 1..12 ).each do | month |
@@ -172,11 +174,68 @@ def reporthelp_week_selection( form, method )
)
end
+ # Return HTML suitable for inclusion in a form which provides
+ # options such as "this month" or "one week ago" in a selection
+ # list. Pass the form being constructed and ":range_one_month"
+ # or ":range_one_week".
+ #
+ def reporthelp_one_selection( form, method )
+ now = DateTime.now.utc
+
+ if ( method == :range_one_week )
+ strings = [
+ apphelp_view_hint( :last_week, SavedReportsController ),
+ apphelp_view_hint( :this_week, SavedReportsController ),
+ apphelp_view_hint( :two_weeks_ago, SavedReportsController ),
+ ]
+ dates = [
+ now - 1.week,
+ now,
+ now - 2.weeks
+ ]
+ objects = dates.map do | date |
+ ReportWeek.new( date.year, date.cweek )
+ end
+ else
+ strings = [
+ apphelp_view_hint( :last_month, SavedReportsController ),
+ apphelp_view_hint( :this_month, SavedReportsController ),
+ apphelp_view_hint( :two_months_ago, SavedReportsController ),
+ ]
+ dates = [
+ now - 1.month,
+ now,
+ now - 2.months
+ ]
+ objects = dates.map do | date |
+ ReportMonth.new( date.year, date.month )
+ end
+ end
+
+ keys = [ "last", "this", "two" ]
+ array = strings.map.with_index do | string, index |
+ o = OpenStruct.new
+ o.value = keys[ index ]
+ o.text = string % objects[ index ].all # The "all" method on a ReportWeek or ReportMonth gives its name/date in a suitable format
+ o
+ end
+
+ form.collection_select(
+ method,
+ array,
+ :value,
+ :text,
+ {
+ :include_blank => '-'
+ }
+ )
+ end
+
# Return HTML suitable for inclusion in the form passed in the
# first parameter (i.e. the 'f' in "form for ... do |f|" ), which
# provides a selection list allowing the user to choose a report
# frequency (weekly, monthly etc.).
-
+ #
def reporthelp_frequency_selection( form )
collection = []
@@ -327,25 +386,68 @@ def reporthelp_updated_at( report )
# List helper - formatted start date for the given report
def reporthelp_start_date( report )
- apphelp_date( report.generate_report().range.min )
+ if ( report.range_start_cache.class != Date )
+ apphelp_view_hint( "date_start_#{ report.range_start_cache }" )
+ else
+ apphelp_date( report.range_start_cache )
+ end
end
# List helper - formatted end date for the given report
def reporthelp_end_date( report )
- apphelp_date( report.generate_report().range.max )
+ if ( report.range_end_cache.class != Date )
+ apphelp_view_hint( "date_end_#{ report.range_end_cache }" )
+ else
+ apphelp_date( report.range_end_cache )
+ end
end
# Return appropriate list view actions for the given report
def reporthelp_actions( report )
if ( @current_user.admin? || report.user_id == @current_user.id )
- return [ 'edit', 'delete', 'show' ]
+ return [
+ 'edit',
+ 'delete',
+ 'show',
+ {
+ :title => 'Copy',
+ :url => user_saved_report_copy_path( :user_id => @current_user.id, :saved_report_id => "%s" )
+ }
+ ]
else
return []
end
end
+ # Return an input element and label as part of a form used to export
+ # a report. Pass the submodule of TrackRecordReportGenerator for
+ # which options are being generated and the array entry from the
+ # option data the submodule provides in "invocation_options_for".
+ #
+ def reporthelp_export_option( submodule, option )
+ prefix = submodule.name.underscore
+ kind = option.keys.first
+ data = option.values.first
+
+ case kind
+ when :checkbox
+ id = "#{ prefix }[#{ data[ :id ] }]"
+ output = check_box_tag( id, "1", data[ :checked ] )
+ output << label_tag( id, data[ :label ] )
+
+ when :radio
+ id = data[ :id ]
+ name = "#{ prefix }[#{ data[ :name ] }]"
+ output = radio_button_tag( name, id, data[ :checked ] )
+ output << label_tag( "#{ name }_#{ id }", data[ :label ] )
+
+ else
+ ""
+ end
+ end
+
private # Meaninless in a module but put here as a marker
# Return an array of integers representing years, from most recent to least
View
99 app/models/saved_report.rb
@@ -81,9 +81,9 @@ class SavedReport < Rangeable
# Return (and cache) a TrackRecordReport::Report instance based on the
# attributes of this SavedReport model instance. The result is cached for
- # later access. If you alter attribute values, pass 'true' on entry to
- # force a refresh of the cache and update the TrackRecordReport::Report
- # instance.
+ # later access within the current request. If you alter attribute values,
+ # pass 'true' on entry to force a refresh of the cache and update the
+ # TrackRecordReport::Report instance.
#
def generate_report( flush_cache = false )
if ( @report.nil? || flush_cache )
@@ -98,6 +98,99 @@ def generate_report( flush_cache = false )
@report.reportable_user_ids = reportable_user_ids
end
+ # Sneakily in passing attempt to update our cached start and end ranges
+
+ update_cached_ranges( @report )
+
@report
end
+
+ # Overload default attribute accessors to provide more meaningful values for
+ # start and end ranges (see private method "update_cached_ranges" for the
+ # other half of this from an implementation standpoint, if interested).
+ #
+ # Each returns either a Date for a fixed report start or end date, or a
+ # symbol - ":all" indicates start-or-end-of-all-time, while symbols starting
+ # with 'last', 'this' or 'two' and ending in '_month' or '_week' refer to
+ # last month/week, this month/week or two months/weeks ago, respectively
+ # (e.g. ":two_week", ":last_month").
+
+ def range_start_cache
+ raw_attr_val = self[ :range_start_cache ]
+ CACHED_REVERSE_RANGE_MAP[ raw_attr_val ] || raw_attr_val
+ end
+
+ def range_end_cache
+ raw_attr_val = self[ :range_end_cache ]
+ CACHED_REVERSE_RANGE_MAP[ raw_attr_val ] || raw_attr_val
+ end
+
+private # =====================================================================
+
+ # Range maps and "update_cached_ranges": An effective hack to map a Report's
+ # indication of date range to a Date value which can be stored in the
+ # database. Store very early dates, so that people can ask the database to
+ # sort on the cache values and get relative-style date reports collected
+ # together. Yes, this does mean that attempts to report time in early January
+ # of the year 4000 would result in confusion, but only confusion for views
+ # which use the cached range values for sorting. They're not used for any
+ # report mathematics calculations. In practice only the 'Saved Reports' index
+ # view would do the wrong thing; and it's something of an edge use case :-)
+ #
+ # A high future date is used rather than a low past date (e.g. the 1900s) so
+ # that sorting makes sense. If you wanted to see your most recent reports,
+ # for example, you'd sort descending by start date. But chances are any of
+ # the relative dates are likely to be the most recent, or a good enough
+ # approximation, so they should show up first in a descending sort.
+ #
+ # It would theoretically be possible to sweep a user's saved reports at an
+ # "appropriate moment" (e.g. rendering of the index view) and update all of
+ # the cached dates to *actual* values and give a true accurate sort, but
+ # that's an unbounded performance problem and not really necessary.
+
+ CACHED_RANGE_MAP = {
+ :all => Date.new( 4000, 1, 1 ),
+ :two_month => Date.new( 4000, 1, 2 ),
+ :last_month => Date.new( 4000, 1, 3 ),
+ :this_month => Date.new( 4000, 1, 4 ),
+ :two_week => Date.new( 4000, 1, 5 ),
+ :last_week => Date.new( 4000, 1, 6 ),
+ :this_week => Date.new( 4000, 1, 7 )
+ }
+
+ CACHED_REVERSE_RANGE_MAP = Hash[ CACHED_RANGE_MAP.map( &:reverse ) ]
+
+ # Based on the *given* report (the possibly-set internal copy of '@report'
+ # is intentionally not consulted so the caller is responsible for thinking
+ # about whether or not the report is up to date or needs regenerating),
+ # update start and end cache values, quietly saving this object if need be.
+ # Fixed reporting dates are stored as such. Relative dates such as "all
+ # time" or "last week" are stored using the CACHED_RANGE_MAP dates defined
+ # above, under the rationale described above. Custom public accessors
+ # written earlier in this file map the values back again, using the
+ # CACHED_REVERSE_RANGE_MAP.
+ #
+ # Returns 'true' for success (report updated and saved, or report cache
+ # was already up to date) or 'false' for failure (report save attempt
+ # failed for any reason - validation, database exception, whatever).
+ #
+ def update_cached_ranges( report )
+ start_cache = CACHED_RANGE_MAP[ report.cacheable_start_indicator ] || report.cacheable_start_indicator
+ end_cache = CACHED_RANGE_MAP[ report.cacheable_end_indicator ] || report.cacheable_end_indicator
+
+ if ( start_cache != self.range_start_cache || end_cache != self.range_end_cache )
+ self.range_start_cache = start_cache
+ self.range_end_cache = end_cache
+
+ begin
+ self.save # Return true/false directly, no validation exceptions...
+ rescue
+ false # ...but might still get DB exceptions, so catch those
+ end
+
+ else
+ true # No change => no need to re-save => return as if saving succeeded
+
+ end