Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

This is TrackRecord v2.0, for Rails v3.2.

  • Loading branch information...
commit e9f824ac36fce0cd8ab72126477713015b50be82 1 parent bee793c
@pond authored
Showing with 20,493 additions and 2,046 deletions.
  1. +84 −234 CHANGELOG
  2. +67 −0 Gemfile
  3. +1 −1  LICENSE
  4. BIN  PetitaBold.ttf
  5. +4 −1 README.rdoc
  6. +3 −6 Rakefile
  7. +123 −15 app/controllers/application_controller.rb
  8. +13 −14 app/controllers/audits_controller.rb
  9. +2 −2 app/controllers/charts_controller.rb
  10. +1 −1  app/controllers/control_panels_controller.rb
  11. +11 −12 app/controllers/customers_controller.rb
  12. +30 −0 app/controllers/help_controller.rb
  13. +11 −12 app/controllers/projects_controller.rb
  14. +75 −29 app/controllers/reports_controller.rb
  15. +14 −0 app/controllers/saved_report_auto_titles_controller.rb
  16. +32 −0 app/controllers/saved_reports_base_controller.rb
  17. +40 −0 app/controllers/saved_reports_by_customer_controller.rb
  18. +40 −0 app/controllers/saved_reports_by_project_controller.rb
  19. +39 −0 app/controllers/saved_reports_by_task_controller.rb
  20. +38 −0 app/controllers/saved_reports_by_user_controller.rb
  21. +137 −0 app/controllers/saved_reports_controller.rb
  22. +4 −5 app/controllers/sessions_controller.rb
  23. +11 −10 app/controllers/task_imports_controller.rb
  24. +11 −12 app/controllers/tasks_controller.rb
  25. +1 −1  app/controllers/timesheet_rows_controller.rb
  26. +21 −34 app/controllers/timesheets_controller.rb
  27. +2 −2 app/controllers/trees_controller.rb
  28. +37 −22 app/controllers/users_controller.rb
  29. +1 −1  app/controllers/work_packets_controller.rb
  30. +186 −117 app/helpers/application_helper.rb
  31. +17 −16 app/helpers/audits_helper.rb
  32. +1 −1  app/helpers/charts_helper.rb
  33. +1 −1  app/helpers/customers_helper.rb
  34. +48 −0 app/helpers/help_helper.rb
  35. +3 −3 app/helpers/projects_helper.rb
  36. +81 −91 app/helpers/reports_helper.rb
  37. +2 −0  app/helpers/saved_reports_helper.rb
  38. +3 −3 app/helpers/task_imports_helper.rb
  39. +41 −27 app/helpers/tasks_helper.rb
  40. +1 −1  app/helpers/timesheet_rows_helper.rb
  41. +31 −22 app/helpers/timesheets_helper.rb
  42. +17 −17 app/helpers/users_helper.rb
  43. +1 −1  app/helpers/work_packets_helper.rb
  44. 0  app/models/.gitkeep
  45. +82 −1 app/models/control_panel.rb
  46. +21 −13 app/models/customer.rb
  47. +24 −16 app/models/email_notifier.rb
  48. +21 −14 app/models/project.rb
  49. +39 −0 app/models/rangeable.rb
  50. +102 −0 app/models/saved_report.rb
  51. +38 −31 app/models/task.rb
  52. +4 −4 app/models/task_group.rb
  53. +2 −2 app/models/task_import.rb
  54. +26 −9 app/models/timesheet.rb
  55. +3 −3 app/models/timesheet_row.rb
  56. +41 −39 app/models/user.rb
  57. +2 −2 app/models/work_packet.rb
  58. +14 −15 app/views/audits/index.html.erb
  59. +6 −6 app/views/customers/_edit.html.erb
  60. +12 −8 app/views/customers/delete.html.erb
  61. +11 −13 app/views/customers/index.html.erb
  62. +30 −23 app/views/customers/show.html.erb
  63. +34 −0 app/views/email_notifier/admin_update_notification.html.erb
  64. 0  app/views/email_notifier/{admin_update_notification.erb → admin_update_notification.text.erb}
  65. +37 −0 app/views/email_notifier/signup_notification.html.erb
  66. 0  app/views/email_notifier/{signup_notification.erb → signup_notification.text.erb}
  67. +11 −0 app/views/help/_report_share.erb
  68. +11 −0 app/views/help/_report_title.erb
  69. +4 −0 app/views/help/show.html.erb
  70. +23 −27 app/views/layouts/application.html.erb
  71. +17 −8 app/views/projects/_edit.html.erb
  72. +8 −6 app/views/projects/delete.html.erb
  73. +11 −13 app/views/projects/index.html.erb
  74. +23 −16 app/views/projects/show.html.erb
  75. +4 −5 app/views/reports/_export_csv.html.erb
  76. +0 −239 app/views/reports/new.html.erb
  77. +62 −55 app/views/reports/show.html.erb
  78. +347 −0 app/views/saved_reports/_edit.html.erb
  79. +1 −0  app/views/saved_reports/edit.html.erb
  80. +58 −0 app/views/saved_reports/index.html.erb
  81. +1 −0  app/views/saved_reports/new.html.erb
  82. +2 −2 app/views/sessions/new.html.erb
  83. +4 −2 app/views/shared/_active_inactive_list.html.erb
  84. +15 −15 app/views/shared/_hours.html.erb
  85. +25 −22 app/views/shared/_list.html.erb
  86. +19 −0 app/views/shared/_report_button.html.erb
  87. +68 −0 app/views/shared/_search.html.erb
  88. +38 −33 app/views/shared/_task_info.html.erb
  89. +37 −29 app/views/task_imports/edit.html.erb
  90. +1 −1  app/views/task_imports/new.html.erb
  91. +18 −10 app/views/tasks/_edit.html.erb
  92. +7 −10 app/views/tasks/delete.html.erb
  93. +1 −1  app/views/tasks/edit.html.erb
  94. +11 −13 app/views/tasks/index.html.erb
  95. +1 −1  app/views/tasks/new.html.erb
  96. +15 −9 app/views/tasks/show.html.erb
  97. +45 −13 app/views/timesheet_rows/_edit.html.erb
  98. +3 −3 app/views/timesheet_rows/_show.html.erb
  99. +5 −5 app/views/timesheets/delete.html.erb
  100. +96 −79 app/views/timesheets/edit.html.erb
  101. +19 −17 app/views/timesheets/index.html.erb
  102. +39 −40 app/views/timesheets/new.html.erb
  103. +42 −40 app/views/timesheets/show.html.erb
  104. +21 −21 app/views/users/_admin_edit.html.erb
  105. +15 −15 app/views/users/_manager_edit.html.erb
  106. +11 −11 app/views/users/_normal_edit.html.erb
  107. +4 −4 app/views/users/delete.html.erb
  108. +10 −10 app/views/users/edit.html.erb
  109. +12 −11 app/views/users/home.html.erb
  110. +11 −13 app/views/users/index.html.erb
  111. +4 −4 app/views/users/new.html.erb
  112. +38 −14 app/views/users/show.html.erb
  113. +17 −8 app/views/work_packets/_edit.html.erb
  114. +4 −0 config.ru
  115. +63 −0 config/application.rb
  116. +4 −108 config/boot.rb
  117. +1 −1  config/database_blank.yml
  118. +4 −76 config/environment.rb
  119. +25 −17 config/environments/development.rb
  120. +55 −14 config/environments/production.rb
  121. +37 −17 config/environments/test.rb
  122. +1 −1  config/initializers/add_uk_tax_year_methods_to_date.rb
  123. +7 −0 config/initializers/backtrace_silencers.rb
  124. +1 −1  config/initializers/configure_openid_logging.rb
  125. +1 −1  config/initializers/email_config.rb
  126. +35 −6 config/initializers/extend_acts_as_audited_model.rb
  127. +1 −1  config/initializers/extend_numeric_class_with_precision_method.rb
  128. +1 −1  config/initializers/general_config.rb
  129. +2 −2 config/initializers/inflections.rb
  130. 0  vendor/plugins/quiet_leightbox/rails/init.rb → config/initializers/rails_2_plugin_quiet_leightbox.rb
  131. 0  vendor/plugins/quiet_prototype/rails/init.rb → config/initializers/rails_2_plugin_quiet_prototype.rb
  132. +1 −1  ...or/plugins/safe_in_place_editing/init.rb → config/initializers/rails_2_plugin_safe_in_place_editing.rb
  133. 0  vendor/plugins/yui_tree/rails/init.rb → config/initializers/rails_2_plugin_yui_tree.rb
  134. +11 −0 config/initializers/secret_token.rb
  135. +8 −0 config/initializers/session_store.rb
  136. +14 −0 config/initializers/wrap_parameters.rb
  137. +171 −0 config/locales/en.yml
  138. +30 −0 config/locales/will_paginate.en.yml
  139. +48 −38 config/routes.rb
  140. +9 −0 db/migrate/20111007101114_add_comment_to_audits.rb
  141. +9 −0 db/migrate/20111007101115_rename_changes_to_audited_changes.rb
  142. +10 −0 db/migrate/20111007101116_add_remote_address_to_audits.rb
  143. +11 −0 db/migrate/20111007101117_add_association_to_audits.rb
  144. +96 −0 db/migrate/20111013142252_add_saved_reports_support.rb
  145. +9 −0 db/migrate/20111014140248_add_preferences_to_control_panels.rb
  146. +32 −0 db/migrate/20111017131344_add_date_cache_column_to_timesheets.rb
  147. +23 −0 db/migrate/20130320235805_rename_association_to_associated.rb
  148. +76 −14 db/schema.rb
  149. +7 −0 db/seeds.rb
  150. +17 −9 doc/README_FOR_APP
  151. +952 −0 doc/app/ApplicationController.html
  152. +1,524 −0 doc/app/ApplicationHelper.html
  153. +376 −0 doc/app/AuditsController.html
  154. +485 −0 doc/app/AuditsHelper.html
  155. +412 −0 doc/app/ChartsController.html
  156. +403 −0 doc/app/ChartsHelper.html
  157. +469 −0 doc/app/ControlPanel.html
  158. +289 −0 doc/app/ControlPanelsController.html
  159. +493 −0 doc/app/Customer.html
  160. +659 −0 doc/app/CustomersController.html
  161. +336 −0 doc/app/CustomersHelper.html
  162. +389 −0 doc/app/EmailNotifier.html
  163. +375 −0 doc/app/HelpController.html
  164. +385 −0 doc/app/HelpHelper.html
  165. +459 −0 doc/app/Project.html
  166. +662 −0 doc/app/ProjectsController.html
  167. +370 −0 doc/app/ProjectsHelper.html
  168. +330 −0 doc/app/QuietLeightbox.html
  169. +323 −0 doc/app/QuietLeightbox/ClassMethods.html
  170. +367 −0 doc/app/QuietLeightbox/QuietLeightboxHelper.html
  171. +331 −0 doc/app/QuietPrototype.html
  172. +323 −0 doc/app/QuietPrototype/ClassMethods.html
  173. +363 −0 doc/app/QuietPrototype/QuietPrototypeHelper.html
  174. +353 −0 doc/app/Rangeable.html
  175. +426 −0 doc/app/ReportsController.html
  176. +964 −0 doc/app/ReportsHelper.html
  177. +372 −0 doc/app/ReportsHelper/ReportMonth.html
  178. +372 −0 doc/app/ReportsHelper/ReportWeek.html
  179. +428 −0 doc/app/ReportsHelper/ReportYear.html
  180. +282 −0 doc/app/SafeInPlaceEditing.html
  181. +335 −0 doc/app/SafeInPlaceEditing/ClassMethods.html
  182. +551 −0 doc/app/SafeInPlaceEditingHelper.html
  183. +461 −0 doc/app/SavedReport.html
  184. +334 −0 doc/app/SavedReportAutoTitlesController.html
  185. +289 −0 doc/app/SavedReportsBaseController.html
  186. +357 −0 doc/app/SavedReportsByCustomerController.html
Sorry, we could not display the entire diff because too many files (625) changed.
View
318 CHANGELOG
@@ -1,252 +1,102 @@
-Version 1.54, 2013-03-28
+Version 2.00, 2013-03-27
========================
-- Requires Rails 2.3.18, for Rails security patch reasons.
-- Public release of changes previously used only by Endurance.
-- This is the last release of the v1.x series for Rails 2, except possibly
- for future security-only changes; development now continues on the v2.x
- v2.x series for Rails 3.
+Important
+---------
+TrackRecord now requires Rails 3.2. It does not run on Rails 2. As a result,
+the Ruby version requirements are increased too; you need version 1.9.3 at
+patch 392 or later. The upgrade instructions below assume that this is
+already available.
-Version 1.53, 2013-03-17
-========================
+On the client side, TrackRecord's views now assume a competent HTML 5, CSS 2
+capable browser is used. It looks best on modern CSS 3 aware browsers such as
+recent Safari, Chrome, Opera and Firefox builds. JavaScript is very strongly
+recommended but not required, so if you have it turned off in your browser
+for any reason or your browser doesn't support it, TrackRecord can still be
+used. Internet Explorer is untested and not officially supported at any
+version, though it may work in practice.
-- Requires Rails 2.3.15, for Rails security patch reasons.
-- Daily report type enabled, but with a hard-coded 32 day maximum date range
- throttle applied.
-- Endurance release only.
+Upgrading
+---------
-Version 1.52, 2012-04-11
-========================
+To upgrade, first do a full database backup in case anything goes wrong!
+Next, unpack TrackRecord somewhere new - don't copy it on top of an existing
+installation. Make any configuration changes you made to your old copy:
-- Requires Rails 2.3.14.
-- A few very small fixes related to the CSRF protection in this newer
- version of Rails are included.
-- Endurance release only.
+- You can safely just copy over the old 'config/database.yml'
+- ...and 'config/initializers/email_config.rb'
+- ...and 'config/initializers/general_config.rb' files
+- Possibly port over your e-mail sending configuration for ActionMailer
+ in 'config/environments/production.rb' or '...development.rb' - see:
+ http://guides.rubyonrails.org/action_mailer_basics.html#action-mailer-configuration
-Version 1.51, 2009-12-10
-========================
+You must also set a secret key in 'config/initializers/secret_token.rb';
+this is the equivalent of the key that in previous versions lived inside
+'config/environment.rb'. Comments in the file give detailed instructions.
-- Bug fix update to second public release.
-- Requires Rails 2.3.5.
-- For upgrade instructions, please see version 1.50 information below.
-- Fixes first-time user account setup problems.
-- Correctly propagates changes of project active/inactive state to tasks if
- asked to do so by the user via the relevant form checkbox.
+The gem dependencies for TrackRecord are all listed in 'Gemfile'. To get
+these installed, from inside the 'trackrecord' directory (the same directory
+as this change log file and 'Gemfile'), run:
+ bundle install
-Version 1.50, 2009-12-07
-========================
+...to make sure all the relevant gems required by the updated TrackRecord
+are present. Now make sure your web server is offline and bring your database
+up to date (changing 'production' to 'development' if being rightly cautious
+and testing on a development copy first):
-- Second public release.
-- Requires Rails 2.3.5.
+ rake db:migrate RAILS_ENV=production
-TrackRecord is supplied in two versions - one with and one without all required
-gems embedded (or "frozen") inside. If you downloaded the version without the
-frozen gems, make sure you are running Rails 2.3.5 centrally ("gem update
-rails"). You will also need updated versions of gems 'fastercsv', 'ruby-openid'
-and 'will_paginate' - see the change details list later on for more details.
+Then you should be able to start your web server again. There may be some
+server changes needed for Rails 3; consult your web server documentation if
+necessary. I recommend using Phusion Passenger, if possible, to serve Rails
+application such as TrackRecord.
+If you have trouble running 'bundle' or the migration, in the first
+instance check Google for the error message you see - upgrading Rails and
+using bundles is very rarely a pain-free process and the issues will usually
+be to do with your computing environment rather than TrackRecord's own code.
-Upgrading
----------
-To upgrade from an earlier version, shut down your web server and move the
-current TrackRecord application directory out of the way. Unpack the 1.5
-directory in its place. Copy the database configuration file from the old
-installation ('config/database.yml') and re-apply any customisations you
-made to the old installation. In particular check the following files:
-
- config/initializers/email_config.rb
- config/initializers/general_config.rb
-
-Note that the PATH_PREFIX mechanism for running the application in a non-root
-location has been removed. Most Rails deployment mechanisms provide a better
-way to do this (e.g. see Phusion Passenger). Contact me if you still need this
-feature - e-mail "ahodgkin@rowing.org.uk".
-
-You are now ready to migrate your data for the new version. This adds a
-'billable' flag to tasks. By default all your tasks will be marked billable.
-If more of your tasks are to be marked as not billable instead, change the
-migration so it sets the billable flag to 'false'. See the following file:
-
- db/migrations/017_add_billable_task_flag.rb
-
-Perform a database backup before running the migration so that you can recover
-your data if anything goes wrong. Consult your database documentation for
-instructions. When ready, start the migration by issuing this command:
-
- RAILS_ENV=production rake db:migrate
-
-...from the command line with the current working directory set to somewhere
-inside the TrackRecord 1,5 application directory (change "production" to read
-"development" or another environment name to run the migration for other
-environments, if you need that). You should now be able to restart your web
-server and use the upgraded application.
-
-
-Browsers
---------
-
-This release of TrackRecord has been tested on Firefox 3.5 and Safari 4. For
-very best results please use these browsers or a later version of either. Opera
-10 has been tested and works well, except for some minor visual degradation due
-to use of some non-standard CSS extensions which Opera does not support.
-
-The application should work correctly on older Firefox versions back to version
-2.0, the Opera 9 series and WebKit-based browsers such as recent versions of
-Konqueror or Google Chrome, though these have not been used for testing.
-
-Although TrackRecord will function under Microsoft Internet Explorer 8, some
-functions will not be available and performance will be degraded, with greater
-load on the server for some operations. Earlier versions of this browser are
-not supported at all.
-
-Browsers without JavaScript support such as NetSurf v2.x should work but, as in
-Internet Explorer 8, some functions will not be available and greater load will
-be placed on the web server for some operations.
-
-
-Changes from version 1.0
-------------------------
-
-- The following new gems are now required:
- + Gem: calendar_date_select 1.15 (replaces plugin used in TrackRecord v1.0)
- ("gem install calendar_date_select" or "rake gems:install").
- http://code.google.com/p/calendardateselect/
- http://github.com/timcharper/calendar_date_select
-
-- The following new gem versions are needed:
- + fastercsv 1.5.0
- http://fastercsv.rubyforge.org/
- + ruby-openid 2.1.17
- http://openidenabled.com/ruby-openid/
- http://github.com/pelle/ruby-openid
- + will_paginate (formerly called "mislav-will_paginate") 2.3.11
- http://wiki.github.com/mislav/will_paginate
-
-- The following plugins have been updated:
- + acts_as_audited 2009-01-27
- http://github.com/collectiveidea/acts_as_audited
- + open_id_authentication 2009-10-21
- http://github.com/rails/open_id_authentication
- + safe_in_place_editing 0.02 23-Oct-2009 - custom plugin; provides in-place
- editing features without XSS vulnerabilities present in earlier Rails
- releases and with some extra facilities not available in the current
- Rails release at the time of writing. Currently only available as part of
- TrackRecord.
-
-- The following plugins have been removed:
- + calendar_date_select (is now a gem, see above)
- + scope-out (application now uses Rails' named_scope)
- http://code.google.com/p/scope-out-rails/
-
-- The following new plugins have been added:
- + quiet_leightbox - custom plugin; only include Leightbox components if the
- view requires it. Currently only available as part of TrackRecord.
- + quiet_prototype - custom plugin; only include Prototype and Scriptalicious
- components if the view requires it. Currently only available as part of
- TrackRecord.
- + yui_tree - custom plugin; support the YUI tree component. Currently only
- available as part of TrackRecord.
-
-- The following third party JavaScript components have been updated:
- + Prototype 1.6.1
- http://www.prototypejs.org/
- + Scriptalicious 1.8.3
- http://script.aculo.us/
-
-- The following new third party scripts have been added:
- + builder.js from Scriptalicious 1.8.3
- http://script.aculo.us/
- + leightbox.js (21/2/2006)
- http://www.eight.nl/static/files/leightbox/
- + YUI tree resources are in 'public/yui' and come from the YUI library 2.7.0
- http://developer.yahoo.com/yui/treeview/
-
-- The following scripts have moved:
- + safe_in_place_editing.js is now installed in an eponymous subdirectory.
- + TrackRecord custom scripts are now also in an eponymous subdirectory (see
- 'check_box_toggler.js', 'section_revealer.js' and 'timesheet_editor.js').
-
-- The following scripts are removed:
- + dynamic_task_list.js (replaced by YUI tree)
-
-- Files 'config/initializers/acquire_fastercsv.rb' and 'acquire_paginator.rb'
- removed as the list of gem versions in 'config/environment.rb' does their
- job; application controller file 'application.rb' now called
- 'application_controller.rb'.
-
-- Streamlined sign-in process - users are taken to the log in page straight
- away, without a pointless click-through Home page. Help text on the sign
- in page is hidden behind a show/hide panel to keep the page simple.
-
-- Added notion of billable and non-billable tasks. Tasks are considered
- billable by default.
-
-- Numerous improvements to the report generator:
- + Report on active, inactive, billable and non-billable tasks.
- + Custom task sorting and grouping in the final report.
- + Hide rows or columns which add up to zero hours.
- + Improved date menu selectors which reflect the precise range of actual
- work packets in the database, rather than rounding to a whole year.
- + Extra columns in CSV output to aid further processing (e.g. task and
- project code information).
-
-- Improvements to the bulk task importer:
- + JS toggle switches to select all, deselect all or invert the selection
- of tasks for import.
- + Grouped display of tasks and control over collapsing tasks into their
- parent groups, for cases where an XML project file describes a project
- in greater detail than required by the corresponding timesheets.
- + Important bug fix - sometimes task durations could be rounded to
- integer hours when imported.
-
-- Other minor improvements such as:
- + Uses Rails default scoping and named scopes to improve code legibility
- and efficiency throughout.
- + Detects presence of JavaScript when the user logs in, then shows JS or
- non-JS versions of certain pages to lessen database load where possible.
- For example, YUI tree task selectors only fetch the tasks needed to show
- branches opened by the user, while the non-JS task selector menu must
- fetch all tasks from the database to show one huge selection list; this
- is considerably less efficient at the server side.
- + Only includes scripts in the main layout template when they're needed
- via things like the custom "quiet_prototype" plugin.
- + Minor visual tweaks to the navigation bar (now taller) and various other
- CSS modifications, including a minimum page width to stop undesirable
- wrapping of navigation bar items. A print-specific stylesheet should give
- much better results for printed pages if your browser supports it.
- + Timesheet "show" views show section totals as in the "edit" views (needs
- robust JavaScript support in the browser).
- + Administrator user Home pages are now more clear and compact.
-
-- Various bug fixes, most notable of which are:
- + Custom JavaScript task selectors were a little clumsy and had various
- niggles. Replaced the whole thing with a Leightbox popup which shows a
- heavily customised YUI tree for task selection controlled by a custom
- plugin (see 'vendor/plugins/yui_tree').
- + OpenID servers which return via HTTPS should now work (tested with
- openid.org).
- + The name of a user responsible for a change in the audit trail is now
- shown correctly (due to fixes in the updated acts_as_audited plugin).
- + Section revealer script for hidden text in project views, customer views
- and the sign-in page now works with Opera 10.
- + Migration 014 had a flaw; "rake db:migrate" for a clean installation,
- rather than "rake db:schema:load", would have failed. This is now fixed
- and either approach should work fine.
- + Some redundant ActiveScaffold resources from pre-1.0 versions were
- present in the v1.0 release. These were harmless but have all be removed
- now to keep things tidy.
-
-- Unlisted fixes are very minor, such as the correction of typing errors
- for help text in some views.
-
-
-Version 1.00, 2008-10-14 (roughly)
-==================================
-
-- First public release.
-- Requires Rails 2.1.x (tested on 2.1.0 and 2.1.2).
+Changes from version 1.54 (see the v1.x branch for more v1.x changes)
+-------------------------
+
+- New major version number reflects bump to Rails 3.2 / Ruby 1.9.
+ + Functionality is an evolution and refinement over TrackRecord v1.x,
+ rather than a major overhaul.
+
+- Improved navigation flow throughout.
+ + More consistent and logical navigation options in some places.
+ + Updated visual theme makes things easier to read.
+
+- Reports can be saved for recollection and modification later.
+ + Can be kept private or shared to other TrackRecord users.
+ + Sharing URLs are very simple, since they just refer to a saved.
+ database object rather than recording all report parameters.
+ + Old-style long report URLs from TrackRecord v1.5 are supported and
+ create a temporary user report.
+ + Quick report generation buttons available when managing or viewing
+ customers, projects, tasks and users.
+
+- All list search forms extended to at long last support date ranges.
+
+- Improved labels for customers, projects and tasks everywhere - link back
+ to the original item, with tooltips giving code and description details.
+
+- The beginnings of proper internationalisation are in place, though a lot
+ of messages are not yet run through the language files.
+
+- The 'gruff' gem is commented out inside 'Gemfile' to prevent annoying
+ dependencies. The graph controller mechanism is still present but will
+ not work unless you uncomment the gruff line in 'Gemfile' and re-run
+ "bundle install". It isn't used by default anywhere in TrackRecord.
+
+- Bugs related to tasks with no project or projects with on customer fixed.
+
+- Old-style report URLs are now reliable, rather than leading to 404 errors
+ on older TrackRecord versions if active tasks had become inactive or vice
+ versa. Now the active-vs-inactive lists are reassessed at generation time
+ (and the signed in user's task permissions taken into account as usual).
View
67 Gemfile
@@ -0,0 +1,67 @@
+source 'http://rubygems.org'
+
+# The following line requires Bundler v1.2 or later; see:
+# http://gembundler.com/v1.2/whats_new.html
+ruby '1.9.3'
+
+gem 'rails', '3.2.13'
+
+# Bundle edge Rails instead:
+# gem 'rails', :git => 'git://github.com/rails/rails.git'
+
+gem 'pg'
+gem 'json'
+
+# Gems used only for assets and not required
+# in production environments by default.
+# group :assets do
+# gem 'sass-rails', " ~> 3.1.0"
+# gem 'coffee-rails', "~> 3.1.0"
+# gem 'uglifier'
+# end
+
+gem 'prototype-rails'
+
+# Uncomment to use thin as the 'rails server' web server.
+# gem 'thin'
+
+# Deploy with Capistrano
+# gem 'capistrano'
+
+# To use debugger
+# gem 'ruby-debug'
+
+# 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/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 'will_paginate', '~> 3.0'
+gem 'audited-activerecord', '~> 3.0'
+gem 'acts_as_list'
+
+# If you want the charting stuff for some reason... Note that
+# this brings in awkward dependencies such as ImageMagick via
+# rmagick.
+#
+# https://github.com/topfunky/gruff
+#
+# gem 'gruff'
+# gem 'rmagick', :require => false
+
+# https://github.com/tenderlove/rails_autolink
+# https://github.com/joelmoss/dynamic_form
+
+gem 'rails_autolink'
+gem 'dynamic_form'
+
+# Visualisation aid - for more information, see:
+# http://railroady.prestonlee.com/
+
+gem 'railroady'
View
2  LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2008, 2009 Andrew Hodgkinson
+Copyright (c) 2007-2013 Andrew Hodgkinson
All rights reserved.
Redistribution and use in source and binary forms, with or without
View
BIN  PetitaBold.ttf
Binary file not shown
View
5 README.rdoc
@@ -1,4 +1,4 @@
-== Welcome to TrackRecord v1.54
+== Welcome to TrackRecord v2.0
TrackRecord is a timesheet system written for the Ruby On Rails web
development framework. More information can be found at:
@@ -12,3 +12,6 @@ for details along with:
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.
View
9 Rakefile
@@ -1,10 +1,7 @@
+#!/usr/bin/env rake
# Add your own tasks in files placed in lib/tasks ending in .rake,
# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
-require(File.join(File.dirname(__FILE__), 'config', 'boot'))
+require File.expand_path('../config/application', __FILE__)
-require 'rake'
-require 'rake/testtask'
-require 'rake/rdoctask'
-
-require 'tasks/rails'
+Trackrecord::Application.load_tasks
View
138 app/controllers/application_controller.rb
@@ -1,6 +1,6 @@
########################################################################
# File:: application_controller.rb
-# (C):: Hipposoft 2008, 2009
+# (C):: Hipposoft 2007
#
# Purpose:: Standard Rails application controller.
# ----------------------------------------------------------------------
@@ -71,9 +71,7 @@ def appctrl_show( model )
def appctrl_new( model )
return appctrl_not_permitted() if ( @current_user.restricted? )
-
- @record = model.constantize.new
- @record.assign_defaults( @current_user )
+ @record = model.constantize.new( nil, @current_user )
end
# Create a new object following submission of a 'create' view form.
@@ -81,8 +79,7 @@ def appctrl_new( model )
def appctrl_create( model )
return appctrl_not_permitted() if ( @current_user.restricted? )
-
- @record = model.constantize.new( params[ model.downcase ] )
+ @record = model.constantize.new( params[ model.downcase ], @current_user )
if ( @record.save )
flash[ :notice ] = "New #{ model.downcase } added"
@@ -156,19 +153,48 @@ def appctrl_delete_confirm( model )
# method has been given, for sorting purposes.
def appctrl_index_assist( model )
+
+ # Set up some default sort and pagination data.
+
+ default_sort = -1 # "-1" => "unknown"
default_direction = model::DEFAULT_SORT_DIRECTION.downcase
default_entries = 10
default_page = 1
- params[ :sort ] = "#{ -1 }" if ( params[ :sort ].nil? )
- params[ :page ] = "#{ default_page }" if ( params[ :page ].nil? )
- params[ :entries ] = "#{ default_entries }" if ( params[ :entries ].nil? )
- params[ :direction ] = "#{ default_direction }" if ( params[ :direction ].nil? )
+ # Attempt to read user preferences for sorting and pagination in index
+ # views for the given model. Note the heavy use of "try()" to tolerate
+ # 'nil' values propagated through, e.g. due to no logged in user, or
+ # a user with no control panel (not that this ought to ever happen).
+
+ prefs_prefix = "sorting.#{ model.name.downcase }."
+ cp = @current_user.try( :control_panel )
+ cp_sort = cp.try( :get_preference, "#{ prefs_prefix }sort" )
+ cp_direction = cp.try( :get_preference, "#{ prefs_prefix }direction" )
+ cp_entries = cp.try( :get_preference, "#{ prefs_prefix }entries" )
- sort = params[ :sort ].to_i
- page = params[ :page ].to_i
- entries = params[ :entries ].to_i
+ # For each one, try to read from the parameters; or fall back to the user
+ # settings; or fall back to the defaults. If the value so determined is
+ # different from the user's current setting, then update that setting.
+
+ sort = params[ :sort ].try( :to_i ) || cp_sort || default_sort
+ cp.try( :set_preference, "#{ prefs_prefix }sort", sort ) unless ( cp_sort == sort )
+
+ direction = params[ :direction ] || cp_direction || default_direction
+ cp.try( :set_preference, "#{ prefs_prefix }direction", direction ) unless ( cp_direction == direction )
+
+ entries = params[ :entries ].try( :to_i ) || cp_entries || default_entries
entries = default_entries if ( entries <= 0 or entries > 500 )
+ cp.try( :set_preference, "#{ prefs_prefix }entries", entries ) unless ( cp_entries == entries )
+
+ # Establish a page number, then write the final determined values back into
+ # the parameters hash as views or plugins may refer to these directly.
+
+ page = params[ :page ].try( :to_i ) || default_page
+
+ params[ :sort ] = sort.to_s
+ params[ :direction ] = direction
+ params[ :entries ] = entries.to_s
+ params[ :page ] = page.to_s
if ( 0..@columns.length ).include?( sort )
@@ -197,7 +223,7 @@ def appctrl_index_assist( model )
end
end
- if ( params[ :direction ] == 'desc' )
+ if ( direction == 'desc' )
order << ' DESC'
else
order << ' ASC'
@@ -206,6 +232,88 @@ def appctrl_index_assist( model )
return { :page => page, :per_page => entries, :order => order }
end
+ # Given a key for the params hash, construct a date from the value
+ # associated with the key. If the key is not present or has an emtpy
+ # value, or if any exception occurs trying to parse the date, the
+ # function returns 'nil'; else it returns a Date object instance.
+ #
+ def appctrl_date_from_params( key )
+ unless ( params[ key ].blank? )
+ begin
+ return Date.parse( params[ key ] )
+ rescue
+ # Do nothing - drop through to have-no-date case
+ end
+ end
+
+ nil
+ end
+
+ # Return an array giving a start date and end date based on search
+ # form submission data in the params hash. Pass a default start and
+ # end date for the case where none has been provided in the params,
+ # or the provided value is invalid.
+ #
+ def appctrl_dates_from_search( default_start, default_end )
+ a = appctrl_date_from_params( :search_range_start ) || default_start
+ b = appctrl_date_from_params( :search_range_end ) || default_end
+
+ a, b = b, a if ( a > b )
+
+ return [ a, b ]
+ end
+
+ # Return an SQL fragment of the form "date-field >= :range_start AND
+ # date-field <= bar :range_end" where the ranges are dates obtained
+ # from "appctrl_dates_from_search" and 'date-field' is given as an
+ # optional second input parameter. The return value includes the
+ # ranges which you pass in using named parameter substitution when
+ # the SQL fragment is included in a wider query.
+ #
+ # Pass the model being searched; it must support a 'used_range'
+ # class method that returns a Range of years for all existant
+ # instances in the database at the time of calling, for the field
+ # that is to be searched. The second parameter is that field, or
+ # if omitted, the model's USED_RANGE_COLUMN by default.
+ #
+ # Returns an array (anticipating parallel assignment by the caller)
+ # with the SQl data, the start Date and the end Date of the range,
+ # or an array of 'nil' if there is no usable search data in the
+ # parameters hash (or it is being explicitly cleared).
+ #
+ # Intended side-effect: Makes sure that search parameters are cleared
+ # out if an explicit 'search_cancel' params key has a value.
+ #
+ def appctrl_search_range_sql( model, field = nil )
+ sql = range_start = range_end = nil
+
+ field = model::USED_RANGE_COLUMN if ( field.nil? )
+
+ unless ( params[ :search ].nil? )
+ if ( ( params[ :search ].blank? and params[ :search_range_start ].blank? and params[ :search_range_end ].blank? ) or params[ :search_cancel ] )
+ params.delete( :search )
+ params.delete( :search_range_start )
+ params.delete( :search_range_end )
+ else
+ range = model.used_range()
+ range_start, range_end = appctrl_dates_from_search(
+ Date.new( range.first ), # I.e. start year
+ Date.new( range.last + 1 ) - 1 # I.e. start of year after end year, minus one day; that is, the last day of end year
+ )
+
+ # Since SQL date-only queries work on a 'start of the day' basis,
+ # we do a ">=" start and a "<" end comparison, setting the end to
+ # the beginning of the *next* day.
+
+ range_end += 1
+
+ sql = "#{ model.table_name }.#{ field } >= :range_start AND #{ model.table_name }.#{ field } < :range_end AND"
+ end
+ end
+
+ [ sql, range_start, range_end ]
+ end
+
# YUI tree form submission will present selected task IDs as a single string
# in a comma separated list; the non-JS code does it properly as an array of
# IDs. Sort this out by patching the params hash. Pass the item to patch
@@ -250,7 +358,7 @@ def appctrl_confirm_user
# redirect back to that form.
def appctrl_ensure_user_name
- if ( ( not @current_user.nil? ) and @current_user.name.empty? )
+ if ( ( not @current_user.nil? ) and @current_user.name.blank? )
redirect_to( edit_user_path( @current_user) )
end
end
View
27 app/controllers/audits_controller.rb
@@ -1,6 +1,6 @@
########################################################################
# File:: audits_controller.rb
-# (C):: Hipposoft 2008, 2009
+# (C):: Hipposoft 2008
#
# Purpose:: Manage Acts As Audited tables.
# ----------------------------------------------------------------------
@@ -9,6 +9,8 @@
class AuditsController < ApplicationController
+ uses_prototype( :only => :index )
+
# Security.
before_filter( :permitted? )
@@ -32,24 +34,21 @@ def index
# Get the basic options hash from ApplicationController, then handle
# search forms.
- options = appctrl_index_assist( Audit )
+ options = appctrl_index_assist( Audited::Adapters::ActiveRecord::Audit )
conditions_vars = {}
- unless ( params[ :search ].nil? )
- if ( params[ :search ].empty? or params[ :search_cancel ] )
- params.delete( :search )
- else
- search_num = params[ :search ].to_i
- search_str = "%#{ params[ :search ] }%" # SQL wildcards either side of the search string
- conditions_sql = 'WHERE ( action ILIKE :search_str OR auditable_type ILIKE :search_str OR users.name ILIKE :search_str OR version ILIKE :search_num )'
- conditions_vars = { :search_num => search_num, :search_str => search_str }
- end
- end
+ search_num = params[ :search ].to_i
+ search_str = "%#{ params[ :search ] }%" # SQL wildcards either side of the search string
+
+ range_sql, range_start, range_end = appctrl_search_range_sql( Audited::Adapters::ActiveRecord::Audit )
+
+ conditions_sql = "WHERE #{ range_sql } ( action ILIKE :search_str OR auditable_type ILIKE :search_str OR users.name ILIKE :search_str OR version = :search_num )"
+ conditions_vars = { :search_num => search_num, :search_str => search_str, :range_start => range_start, :range_end => range_end }
# Sort order is already partially compiled in 'options' from the earlier
# call to 'appctrl_index_assist'.
- order_sql = "ORDER BY #{ options[ :order ] }"
+ order_sql = "ORDER BY #{ options[ :order ] }, created_at DESC"
options.delete( :order )
# Construct the query.
@@ -61,7 +60,7 @@ def index
# Now paginate using this SQL query.
- @audits = Audit.paginate_by_sql( [ finder_sql, conditions_vars ], options )
+ @audits = Audited::Adapters::ActiveRecord::Audit.paginate_by_sql( [ finder_sql, conditions_vars ], options )
end
private
View
4 app/controllers/charts_controller.rb
@@ -1,6 +1,6 @@
########################################################################
# File:: charts_controller.rb
-# (C):: Hipposoft 2008, 2009
+# (C):: Hipposoft 2008
#
# Purpose:: Use the Gruff library to generate charts in PNG format for
# inclusion in HTML reports.
@@ -40,7 +40,7 @@ def show
g = Gruff::Mini::Pie.new( "#{ width }x#{ width * 0.75 }" )
g.title = 'Chart'
- g.font = "#{ RAILS_ROOT }/#{ GRAPH_FONT }"
+ g.font = "#{ Rails.root }/#{ GRAPH_FONT }"
g.hide_legend = true
g.hide_title = true
g.marker_font_size = 70
View
2  app/controllers/control_panels_controller.rb
@@ -1,6 +1,6 @@
########################################################################
# File:: control_panels_controller.rb
-# (C):: Hipposoft 2008, 2009
+# (C):: Hipposoft 2008
#
# Purpose:: Manage User settings through a ControlPanel object.
# ----------------------------------------------------------------------
View
23 app/controllers/customers_controller.rb
@@ -1,6 +1,6 @@
########################################################################
# File:: customers_controller.rb
-# (C):: Hipposoft 2008, 2009
+# (C):: Hipposoft 2008
#
# Purpose:: Manage Customer objects. See models/customer.rb for more.
# ----------------------------------------------------------------------
@@ -60,22 +60,21 @@ def index
# If asked to search for something, build extra conditions to do so.
- unless ( params[ :search ].nil? )
- if ( params[ :search ].empty? or params[ :search_cancel ] )
- params.delete( :search )
- else
- search = "%#{ params[ :search ] }%" # SQL wildcards either side of the search string
- conditions_sql << "AND ( customers.title ILIKE :search OR customers.code ILIKE :search )\n"
- vars = { :search => search }
- active_vars.merge!( vars )
- inactive_vars.merge!( vars )
- end
+ range_sql, range_start, range_end = appctrl_search_range_sql( Customer )
+
+ unless ( range_sql.nil? )
+ search = "%#{ params[ :search ] }%" # SQL wildcards either side of the search string
+ conditions_sql << "AND #{ range_sql } ( customers.title ILIKE :search OR customers.code ILIKE :search )\n"
+
+ vars = { :search => search, :range_start => range_start, :range_end => range_end }
+ active_vars.merge!( vars )
+ inactive_vars.merge!( vars )
end
# Sort order is already partially compiled in 'options' from the earlier
# call to 'ApplicationController.appctrl_index_assist'.
- order_sql = "ORDER BY #{ options[ :order ] }"
+ order_sql = "ORDER BY #{ options[ :order ] }, title ASC, code ASC"
options.delete( :order )
# Compile the main SQL statement. Select all columns of the project, fetching
View
30 app/controllers/help_controller.rb
@@ -0,0 +1,30 @@
+########################################################################
+# File:: help_controller.rb
+# (C):: Hipposoft 2010
+#
+# Purpose:: Display help pages.
+# ----------------------------------------------------------------------
+# 15-Jan-2010 (ADH): Created by consolidating an increasing
+# number of duplicated help controllers.
+# 18-Oct-2011 (ADH): Imported into TrackRecord.
+########################################################################
+
+class HelpController < ApplicationController
+
+ # Hide the main heading; it is output by the view rather than letting the
+ # layout do it so that both heading and body text can be wrapped in a single
+ # DIV for CSS styling of the whole text block, if required.
+ #
+ def skip_main_heading?
+ action_name == 'show'
+ end
+
+ def show
+ # The ":id" value ends up being used as the name of a Partial to be
+ # rendered by the 'help' view. To stop hackers putting dots, slashes
+ # etc. into a URL in an attempt to get the renderer to load a different
+ # Partial, only let "a-z" and underscores through.
+
+ @partial = params[ :id ].gsub( /[^a-z_]/, '' )
+ end
+end
View
23 app/controllers/projects_controller.rb
@@ -1,6 +1,6 @@
########################################################################
# File:: projects_controller.rb
-# (C):: Hipposoft 2008, 2009
+# (C):: Hipposoft 2008
#
# Purpose:: Manage Project objects. See models/project.rb for more.
# ----------------------------------------------------------------------
@@ -58,22 +58,21 @@ def index
# If asked to search for something, build extra conditions to do so.
- unless ( params[ :search ].nil? )
- if ( params[ :search ].empty? or params[ :search_cancel ] )
- params.delete( :search )
- else
- search = "%#{ params[ :search ] }%" # SQL wildcards either side of the search string
- conditions_sql << "AND ( projects.title ILIKE :search OR projects.code ILIKE :search OR customers.title ILIKE :search )\n"
- vars = { :search => search }
- active_vars.merge!( vars )
- inactive_vars.merge!( vars )
- end
+ range_sql, range_start, range_end = appctrl_search_range_sql( Project )
+
+ unless ( range_sql.nil? )
+ search = "%#{ params[ :search ] }%" # SQL wildcards either side of the search string
+ conditions_sql << "AND #{ range_sql } ( projects.title ILIKE :search OR projects.code ILIKE :search OR customers.title ILIKE :search )\n"
+
+ vars = { :search => search, :range_start => range_start, :range_end => range_end }
+ active_vars.merge!( vars )
+ inactive_vars.merge!( vars )
end
# Sort order is already partially compiled in 'options' from the earlier
# call to 'ApplicationController.appctrl_index_assist'.
- order_sql = "ORDER BY #{ options[ :order ] }"
+ order_sql = "ORDER BY #{ options[ :order ] }, title ASC, code ASC"
options.delete( :order )
# Compile the main SQL statement. Select all columns of the project, fetching
View
104 app/controllers/reports_controller.rb
@@ -1,15 +1,23 @@
########################################################################
# File:: reports_controller.rb
-# (C):: Hipposoft 2008, 2009
+# (C):: Hipposoft 2008
#
# Purpose:: Generate reports describing timesheet entries in various
-# different ways.
+# 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.
# ----------------------------------------------------------------------
# 09-Feb-2008 (ADH): Created.
+# 19-Oct-2011 (ADH): Functionality split up into this file and
+# SavedReportsController.
########################################################################
class ReportsController < ApplicationController
+ require 'csv'
+
include TrackRecordReport
include TrackRecordSections
@@ -18,41 +26,79 @@ class ReportsController < ApplicationController
@@application_helper = Object.new.extend( ApplicationHelper )
- dynamic_actions = { :only => [ :new, :create ] }
+ # Retrieve saved report parameters from the database and generate a report
+ # using those parameters, restricted by whatever prevailing permissions may
+ # apply to the current user.
+ #
+ def show
+ @saved_report = SavedReport.find_by_id( params[ :id ] )
+
+ # Legacy report link, or missing / unauthorized report?
+
+ if ( @saved_report.nil? && params.has_key?( :report ) )
+
+ # This is pretty weird as active tasks at the time the link to the
+ # legacy report was generated may have become inactive or vice versa.
+ # Indeed even with a conventionally generated report, another user of
+ # the system may have changed an included task's active flag.
+ #
+ # This means that the report's active_tasks and inactive_tasks lists
+ # may contain a mixture of active and inactive items. Fortunately,
+ # this is all resolved by the TrackRecordReport code when the IDs are
+ # given to the Report instance. It re-finds and re-builds the list of
+ # tasks, assigning them to the correct active/inactive lists as it
+ # does so.
+ #
+ # Really the active-vs-inactive list stuff inside a SavedReport is a
+ # legacy throwback to the old directly generated non-model report
+ # code in TrackRecord v1.x - nothing more.
+
+ params[ :report ][ :reportable_user_ids ] = params[ :report ].delete( :user_ids )
+
+ @saved_report = SavedReport.new( params[ :report ] )
+ @saved_report.user = @current_user
+ @saved_report.title = ''
+
+ begin
+ @saved_report.save!()
+ redirect_to( report_path( @saved_report ) )
+
+ rescue
+ flash[ :error ] = "The legacy report could not be generated. An unknown error occurred."
+ redirect_to( home_path() )
+ end
- uses_leightbox( dynamic_actions )
- uses_yui_tree(
- { :xhr_url_method => :trees_path },
- dynamic_actions
- )
+ # NOTE EARLY EXIT!
- # Prepare for the 'new report' view.
- #
- def new
- read_options()
+ return
- # [TODO] Make a dummy report object. In future, perhaps reports could
- # be saved into the database, owned by users.
+ elsif ( @saved_report.nil? || ( @saved_report.user_id != @current_user.id && ! @saved_report.shared ) )
- @report = Report.new( @current_user, params[ :report ] )
- @user_array = @current_user.restricted? ? [ @current_user ] : User.active
- end
+ flash[ :error ] = "The requested report was not found; the owner may have deleted it."
+ redirect_to( home_path() ) and return
- # Generate a report based on a 'new report' form submission.
- #
- def create
- read_options()
+ end
- appctrl_patch_params_from_js( :report, :active_task_ids )
- appctrl_patch_params_from_js( :report, :inactive_task_ids )
+ # Read parameters related to the 'show' action, which itself contains a
+ # form that submits back to here fincluding details about CSV export
+ # parameters. For plain old "show report <id>" uses, there are no such
+ # additional parameters.
+
+ read_options()
- @report = Report.new( @current_user, params[ :report ] )
+ @report = @saved_report.generate_report()
@report.compile()
+ if ( @saved_report.title.empty? )
+ 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() }
end
+
+ flash.delete( :warning ) # Else it shows on the *next* fetched page too
end
private
@@ -118,7 +164,7 @@ def csv_stream_report()
# First compile the file.
- whole_csv_file = FasterCSV.generate do | csv |
+ whole_csv_file = CSV.generate do | csv |
csv << title unless ( @exclude_title )
if ( @is_task_type )
@@ -181,8 +227,8 @@ def csv_report_by_task( csv, headings )
# New section? Write out the section title and totals if so.
if ( sections_new_section?( task ) )
- file_row << sections_section_title()
- file_row << task.project.code << '' << ''
+ file_row << sections_section_title( true )
+ file_row << ( task.project.try( :code ) || '-' ) << '' << ''
row.cells.each_index do | col_index |
file_row << hours( @report.sections[ sections_section_index() ].cells[ col_index ] )
@@ -275,8 +321,8 @@ def csv_report_by_user( csv, headings )
# New section? Write out the section title and totals if so.
if ( sections_new_section?( task ) )
- file_row << sections_section_title()
- file_row << task.project.code << '' << ''
+ file_row << sections_section_title( true )
+ file_row << ( task.project.try( :code ) || '-' ) << '' << ''
@report.filtered_users.each_index do | user_index |
file_row << hours( @report.sections[ sections_section_index() ].user_row_totals[ user_index ] )
View
14 app/controllers/saved_report_auto_titles_controller.rb
@@ -0,0 +1,14 @@
+########################################################################
+# File:: saved_report_auto_titles_controller.rb
+# (C):: Hipposoft 2011
+#
+# Purpose:: Respond to AJAX requests for report titles.
+# ----------------------------------------------------------------------
+# 18-Oct-2011 (ADH): Created.
+########################################################################
+
+class SavedReportAutoTitlesController < ApplicationController
+ def show
+ render :text => "#{ Time.now }"
+ end
+end
View
32 app/controllers/saved_reports_base_controller.rb
@@ -0,0 +1,32 @@
+########################################################################
+# File:: saved_reports_base_controller.rb
+# (C):: Hipposoft 2013
+#
+# Purpose:: Underlying controller used for shared code common to other
+# saved-report-related activities.
+# ----------------------------------------------------------------------
+# 23-Mar-2013 (ADH): Created.
+########################################################################
+
+class SavedReportsBaseController < ApplicationController
+
+ before_filter :confirm_permission
+ 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.
+
+ def confirm_permission
+ @user = User.find_by_id( params[ :user_id ] )
+ return appctrl_not_permitted() if ( @user.nil? || ! ( @user.admin? || @user == @current_user ) )
+ end
+
+ # Get rid of any unnamed reports for the current user. Usually invoked via
+ # "before_filter(...)".
+ #
+ def delete_unnamed_reports_for
+ SavedReport.where( :user_id => @current_user, :title => "" ).delete_all()
+ end
+end
View
40 app/controllers/saved_reports_by_customer_controller.rb
@@ -0,0 +1,40 @@
+########################################################################
+# File:: saved_reports_by_customer_controller.rb
+# (C):: Hipposoft 2013
+#
+# Purpose:: Generate 'canned' reports for a specific customer.
+# ----------------------------------------------------------------------
+# 23-Mar-2013 (ADH): Created.
+########################################################################
+
+class SavedReportsByCustomerController < SavedReportsBaseController
+
+ # Generate a report based on a 'new report' form submission.
+ #
+ def create
+ customer = Customer.find( params[ :item ] )
+
+ saved_report = SavedReport.new()
+ saved_report.user = @user
+ saved_report.title = ""
+ saved_report.shared = false
+
+ saved_report.frequency = "5" # Weekly
+
+ saved_report.include_committed = true
+ saved_report.include_not_committed = false
+
+ saved_report.active_task_ids = customer.tasks.where( :active => true ).map( & :id )
+ saved_report.inactive_task_ids = customer.tasks.where( :active => false ).map( & :id )
+ saved_report.task_grouping = "both"
+
+ if ( saved_report.active_task_ids.count.zero? && saved_report.inactive_task_ids.count.zero? )
+ flash[ :error ] = 'The customer has no associated tasks to show in a report.'
+ redirect_to( home_path() )
+ elsif ( saved_report.save )
+ redirect_to( report_path( saved_report ) )
+ else
+ redirect_to( home_path() )
+ end
+ end
+end
View
40 app/controllers/saved_reports_by_project_controller.rb
@@ -0,0 +1,40 @@
+########################################################################
+# File:: saved_reports_by_project_controller.rb
+# (C):: Hipposoft 2013
+#
+# Purpose:: Generate 'canned' reports for a specific project.
+# ----------------------------------------------------------------------
+# 23-Mar-2013 (ADH): Created.
+########################################################################
+
+class SavedReportsByProjectController < SavedReportsBaseController
+
+ # Generate a report based on a 'new report' form submission.
+ #
+ def create
+ project = Project.find( params[ :item ] )
+
+ saved_report = SavedReport.new()
+ saved_report.user = @user
+ saved_report.title = ""
+ saved_report.shared = false
+
+ saved_report.frequency = "5" # Weekly
+
+ saved_report.include_committed = true
+ saved_report.include_not_committed = false
+
+ saved_report.active_task_ids = project.tasks.where( :active => true ).map( & :id )
+ saved_report.inactive_task_ids = project.tasks.where( :active => false ).map( & :id )
+ saved_report.task_grouping = "both"
+
+ if ( saved_report.active_task_ids.count.zero? && saved_report.inactive_task_ids.count.zero? )
+ flash[ :error ] = 'The project has no associated tasks to show in a report.'
+ redirect_to( home_path() )
+ elsif ( saved_report.save )
+ redirect_to( report_path( saved_report ) )
+ else
+ redirect_to( home_path() )
+ end
+ end
+end
View
39 app/controllers/saved_reports_by_task_controller.rb
@@ -0,0 +1,39 @@
+########################################################################
+# File:: saved_reports_by_task_controller.rb
+# (C):: Hipposoft 2013
+#
+# Purpose:: Generate 'canned' reports for a specific task.
+# ----------------------------------------------------------------------
+# 23-Mar-2013 (ADH): Created.
+########################################################################
+
+class SavedReportsByTaskController < SavedReportsBaseController
+
+ # Generate a report based on a 'new report' form submission.
+ #
+ def create
+ task = Task.find( params[ :item ] )
+
+ saved_report = SavedReport.new()
+ saved_report.user = @user
+ saved_report.title = ""
+ saved_report.shared = false
+
+ saved_report.frequency = "5" # Weekly
+
+ saved_report.include_committed = true
+ saved_report.include_not_committed = true
+
+ if ( task.active? )
+ saved_report.active_task_ids = [ task.id ]
+ else
+ saved_report.inactive_task_ids = [ task.id ]
+ end
+
+ if ( saved_report.save )
+ redirect_to( report_path( saved_report ) )
+ else
+ redirect_to( home_path() )
+ end
+ end
+end
View
38 app/controllers/saved_reports_by_user_controller.rb
@@ -0,0 +1,38 @@
+########################################################################
+# File:: saved_reports_by_user_controller.rb
+# (C):: Hipposoft 2013
+#
+# Purpose:: Generate 'canned' reports for a specific user.
+# ----------------------------------------------------------------------
+# 23-Mar-2013 (ADH): Created.
+########################################################################
+
+class SavedReportsByUserController < SavedReportsBaseController
+
+ # Generate a report based on a 'new report' form submission.
+ #
+ def create
+ of_user = User.find( params[ :item ] )
+
+ saved_report = SavedReport.new()
+ saved_report.user = @user
+ saved_report.title = ""
+ saved_report.shared = false
+ saved_report.frequency = "5" # Weekly
+
+ saved_report.include_committed = true
+ saved_report.include_not_committed = true
+ saved_report.exclude_zero_rows = true
+ saved_report.exclude_zero_cols = true
+
+ saved_report.reportable_users = [ of_user ]
+
+ saved_report.task_grouping = "both"
+
+ if ( saved_report.save )
+ redirect_to( report_path( saved_report ) )
+ else
+ redirect_to( home_path() )
+ end
+ end
+end
View
137 app/controllers/saved_reports_controller.rb
@@ -0,0 +1,137 @@
+########################################################################
+# File:: saved_reports_controller.rb
+# (C):: Hipposoft 2011
+#
+# Purpose:: Managed saved collections of parameters used to generate
+# reports.
+# ----------------------------------------------------------------------
+# 19-Oct-2011 (ADH): Created.
+########################################################################
+
+class SavedReportsController < SavedReportsBaseController
+
+ uses_prototype( :only => :index )
+ uses_leightbox()
+ uses_yui_tree(
+ { :xhr_url_method => :trees_path },
+ { :only => [ :new, :edit ] }
+ )
+
+ # List reports.
+ #
+ def index
+
+ # Set up the column data; see the index helper functions in
+ # 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' }
+ ]
+
+ options = appctrl_index_assist( SavedReport )
+ vars = { :user_id => @current_user.id }
+
+ user_sql = "WHERE ( users.id = :user_id )\n"
+ other_sql = "WHERE ( users.id != :user_id )\n"
+
+ range_sql, range_start, range_end = appctrl_search_range_sql( SavedReport )
+
+ unless ( range_sql.nil? )
+ search_num = params[ :search ].to_i
+ search_str = "%#{ params[ :search ] }%" # SQL wildcards either side of the search string
+
+ conditions_sql = "AND #{ range_sql } ( saved_reports.name ILIKE :search_str OR users.name ILIKE :search_str )"
+
+ user_sql << conditions_sql
+ other_sql << conditions_sql
+
+ vars.merge!( { :search_str => search_str, :search_num => search_num, :range_start => range_start, :range_end => range_end } )
+ end
+
+ # Sort order is already partially compiled in 'options' from the earlier
+ # call to 'appctrl_index_assist'.
+
+ order_sql = "ORDER BY #{ options[ :order ] }"
+ options.delete( :order )
+
+ # Compile the main SQL statement. Select all columns of the project, fetching
+ # customers where the project's customer ID matches those customer IDs, with
+ # only projects containing tasks in the user's permitted task list (if any)
+ # are included, returned in the required order.
+ #
+ # Due to restrictions in the way that DISTINCT works, I just cannot figure out
+ # ANY way in SQL to only return unique projects while still matching the task
+ # permitted ID requirement for restricted users. So, fetch duplicates, then
+ # strip them out in Ruby afterwards (ouch).
+
+ basic_sql = "SELECT saved_reports.* FROM saved_reports\n" <<
+ "INNER JOIN users ON ( saved_reports.user_id = users.id )\n"
+
+ user_sql = "#{ basic_sql }\n#{ user_sql }\n#{ order_sql }"
+ other_sql = "#{ basic_sql }\n#{ other_sql }\n#{ order_sql }"
+
+ # Now paginate using this SQL query.
+
+ @user_reports = SavedReport.paginate_by_sql( [ user_sql, vars ], options );
+ @other_reports = SavedReport.paginate_by_sql( [ other_sql, vars ], options );
+ end
+
+ # 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
+ end
+
+ # Generate a report based on a 'new report' form submission.
+ #
+ def create
+ appctrl_patch_params_from_js( :saved_report, :active_task_ids )
+ appctrl_patch_params_from_js( :saved_report, :inactive_task_ids )
+
+ saved_report = SavedReport.new( params[ :saved_report ] )
+ saved_report.user = @user
+
+ if ( saved_report.save )
+ redirect_to( report_path( saved_report ) )
+ else
+ render( :action => :new )
+ end
+ end
+
+ # Edit an existing report.
+ #
+ def edit
+ @saved_report = SavedReport.find( params[ :id ] )
+ @user_array = @current_user.restricted? ? [ @current_user ] : User.active
+ end
+
+ # Commit changes to an existing report.
+ #
+ 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 ] ) )
+ flash[ :notice ] = "Report details updated"
+ redirect_to( report_path( saved_report ) )
+ else
+ render( :action => :edit )
+ end
+ end
+
+ # 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 ) )
+ end
+end
View
9 app/controllers/sessions_controller.rb
@@ -1,6 +1,6 @@
########################################################################
# File:: sessions_controller.rb
-# (C):: Hipposoft 2008, 2009
+# (C):: Hipposoft 2008
#
# Purpose:: Manage OpenID logins. Originally created from examples in
# the open_id_authentication plugin.
@@ -110,10 +110,9 @@ def open_id_authentication()
# would require an exposed URL that could be used to try
# and create users without OpenID authentication.
- user_type = 'Admin'
-
- @current_user = User.new
- @current_user.assign_defaults( nil, identity_url, user_type )
+ @current_user = User.new
+ @current_user.identity_url = identity_url
+ @current_user.user_type = User::USER_TYPE_ADMIN
@current_user.save!
new_login()
View
21 app/controllers/task_imports_controller.rb
@@ -1,6 +1,6 @@
########################################################################
# File:: task_imports_controller.rb
-# (C):: Hipposoft 2008, 2009
+# (C):: Hipposoft 2008
#
# Purpose:: Import task definition data from external sources.
# ----------------------------------------------------------------------
@@ -47,6 +47,7 @@ def create
# if that's missing then it's GZip compressed. That's true in the
# limited case of project files.
+ xmlfile = xmlfile.tempfile
byte = xmlfile.getc()
xmlfile.rewind()
@@ -58,7 +59,7 @@ def create
parent_uids = task_data[ :parent_uids ]
parent_titles = task_data[ :parent_titles ]
- if ( tasks.nil? or tasks.empty? )
+ if ( tasks.blank? )
flash[ :error ] = 'No usable tasks were found in that file'
else
@@ -129,17 +130,17 @@ def update
project = Project.find( project_id )
to_import.each do | source_task |
- destination_task = Task.new
- destination_task.title = source_task.title
- destination_task.code = source_task.code
- destination_task.duration = source_task.duration
- destination_task.billable = source_task.billable
- destination_task.project = project
- destination_task.save!
+ Task.new do | destination_task |
+ destination_task.title = source_task.title
+ destination_task.code = source_task.code
+ destination_task.duration = source_task.duration
+ destination_task.billable = source_task.billable
+ destination_task.project = project
+ end.save!
end
flash[ :notice ] = "#{ to_import.length } #{ to_import.length == 1 ? 'task' : 'tasks' } imported successfully."
- redirect_to( home_path() ) and return
+ redirect_to( tasks_path() ) and return
end
rescue => error
View
23 app/controllers/tasks_controller.rb
@@ -1,6 +1,6 @@
########################################################################
# File:: tasks_controller.rb
-# (C):: Hipposoft 2008, 2009
+# (C):: Hipposoft 2008
#
# Purpose:: Manage Task objects. See models/task.rb for more.
# ----------------------------------------------------------------------
@@ -75,22 +75,21 @@ def index
# If asked to search for something, build extra conditions to do so.
- unless ( params[ :search ].nil? )
- if ( params[ :search ].empty? or params[ :search_cancel ] )
- params.delete( :search )
- else
- search = "%#{ params[ :search ] }%" # SQL wildcards either side of the search string
- conditions_sql << "AND ( tasks.title ILIKE :search OR tasks.code ILIKE :search OR projects.title ILIKE :search OR customers.title ILIKE :search )\n"
- vars = { :search => search }
- active_vars.merge!( vars )
- inactive_vars.merge!( vars )
- end
+ range_sql, range_start, range_end = appctrl_search_range_sql( Task )
+
+ unless ( range_sql.nil? )
+ search = "%#{ params[ :search ] }%" # SQL wildcards either side of the search string
+ conditions_sql << "AND #{ range_sql } ( tasks.title ILIKE :search OR tasks.code ILIKE :search OR projects.title ILIKE :search OR customers.title ILIKE :search )\n"
+
+ vars = { :search => search, :range_start => range_start, :range_end => range_end }
+ active_vars.merge!( vars )
+ inactive_vars.merge!( vars )
end
# Sort order is already partially compiled in 'options' from the earlier
# call to 'appctrl_index_assist'.
- order_sql = "ORDER BY #{ options[ :order ] }"
+ order_sql = "ORDER BY #{ options[ :order ] }, title ASC, code ASC"
options.delete( :order )
# Compile the main SQL statement. We want to select all columns of any
View
2  app/controllers/timesheet_rows_controller.rb
@@ -1,6 +1,6 @@
########################################################################
# File:: timesheet_rows_controller.rb
-# (C):: Hipposoft 2008, 2009
+# (C):: Hipposoft 2008
#
# Purpose:: Manage TimesheetRow objects. See models/timesheet_row.rb
# for more.
View
55 app/controllers/timesheets_controller.rb
@@ -1,6 +1,6 @@
########################################################################
# File:: timesheets_controller.rb
-# (C):: Hipposoft 2008, 2009
+# (C):: Hipposoft 2008
#
# Purpose:: Manage Timesheet objects. See models/timesheet.rb for more.
# ----------------------------------------------------------------------
@@ -13,6 +13,7 @@ class TimesheetsController < ApplicationController
dynamic_actions = { :only => [ :edit, :update ] }
+ uses_prototype( :only => :index )
uses_leightbox( dynamic_actions )
uses_yui_tree(
{ :xhr_url_method => :trees_path },
@@ -27,16 +28,12 @@ def index
# application_helper.rb for details.
@columns = [
- { :header_text => 'Year', :value_method => 'year',
- :header_align => 'center', :value_align => 'center' },
- { :header_text => 'Week', :value_method => 'week_number',
- :header_align => 'center', :value_align => 'center' },
- { :header_text => 'Start day', :value_method => 'start_day', :sort_by => 'year, week_number' },
- { :header_text => 'Owner', :value_helper => :timesheethelp_owner, :sort_by => 'users.name' },
- { :header_text => 'Last edited', :value_helper => :timesheethelp_updated_at, :sort_by => 'updated_at' },
- { :header_text => 'Committed', :value_helper => :timesheethelp_committed_at, :sort_by => 'committed_at' },
+ { :header_text => 'Start day', :value_method => 'start_day', :sort_by => 'start_day_cache' },
+ { :header_text => 'Owner', :value_helper => :timesheethelp_owner, :sort_by => 'users.name, start_day_cache' },
+ { :header_text => 'Last edited', :value_helper => :timesheethelp_updated_at, :sort_by => 'updated_at' },
+ { :header_text => 'Committed', :value_helper => :timesheethelp_committed_at, :sort_by => 'committed_at' },
{ :header_text => 'Hours', :value_helper => :timesheethelp_hours,
- :header_align => 'center', :value_align => 'center' },
+ :header_align => 'center', :value_align => 'center' },
]
# Get the basic options hash from ApplicationController, then work out
@@ -49,23 +46,22 @@ def index
user_conditions_sql = "WHERE ( timesheets.committed = :committed ) AND ( users.id = :user_id )\n"
other_conditions_sql = "WHERE ( timesheets.committed = :committed ) AND ( users.id != :user_id )\n"
+ range_sql, search_start, search_end = appctrl_search_range_sql( Timesheet, :start_day_cache )
+
# If asked to search for something, build extra conditions to do so.
- unless ( params[ :search ].nil? )
- if ( params[ :search ].empty? or params[ :search_cancel ] )
- params.delete( :search )
- else
- search_num = params[ :search ].to_i
- search_str = "%#{ params[ :search ] }%" # SQL wildcards either side of the search string
- conditions_sql = "AND ( timesheets.year = :search_num OR timesheets.week_number = :search_num OR users.name ILIKE :search_str )"
- vars = { :search_num => search_num, :search_str => search_str }
+ unless ( range_sql.nil? )
+ search_num = params[ :search ].to_i
+ search_str = "%#{ params[ :search ] }%" # SQL wildcards either side of the search string
- user_conditions_sql << conditions_sql
- other_conditions_sql << conditions_sql
+ conditions_sql = "AND #{ range_sql } ( timesheets.year = :search_num OR timesheets.week_number = :search_num OR users.name ILIKE :search_str )"
+ vars = { :search_num => search_num, :search_str => search_str, :search_start => search_start, :search_end => search_end }
- committed_vars.merge!( vars )
- not_committed_vars.merge!( vars )
- end
+ user_conditions_sql << conditions_sql
+ other_conditions_sql << conditions_sql
+
+ committed_vars.merge!( vars )
+ not_committed_vars.merge!( vars )
end
# Sort order is already partially compiled in 'options' from the earlier
@@ -74,16 +70,6 @@ def index
order_sql = "ORDER BY #{ options[ :order ] }"
options.delete( :order )
- # Compile the main SQL statement. Select all columns of the project, fetching
- # customers where the project's customer ID matches those customer IDs, with
- # only projects containing tasks in the user's permitted task list (if any)
- # are included, returned in the required order.
- #
- # Due to restrictions in the way that DISTINCT works, I just cannot figure out
- # ANY way in SQL to only return unique projects while still matching the task
- # permitted ID requirement for restricted users. So, fetch duplicates, then
- # strip them out in Ruby afterwards (ouch).
-
basic_sql = "SELECT timesheets.* FROM timesheets\n" <<
"INNER JOIN users ON ( timesheets.user_id = users.id )"
@@ -155,6 +141,7 @@ def edit
return appctrl_not_permitted() unless @timesheet.can_be_modified_by?( @current_user )
set_next_prev_week_variables( @timesheet )
+ @selected_rows = []
end
# Update a timesheet, its rows and work packets.
@@ -434,7 +421,7 @@ def update_backend( timesheet )
appctrl_patch_params_from_js( :timesheet )
ids = params[ :timesheet ][ :task_ids ]
- if ( ids.nil? or ids.empty? )
+ if ( ids.blank? )
errors.push( "No tasks selected - first choose one or more tasks from the list, then use the 'Add' button" )
else
ids.each do | id |
View
4 app/controllers/trees_controller.rb
@@ -1,6 +1,6 @@
########################################################################
# File:: trees_controller.rb
-# (C):: Hipposoft 2008, 2009
+# (C):: Hipposoft 2008
#
# Purpose:: Collate data for and send data to the YUI tree plugin.
# ----------------------------------------------------------------------
@@ -94,7 +94,7 @@ def index
# and the project's task IDs is empty.
children.reject! do | project |
- project.tasks.send( active).count.zero? || (
+ project.tasks.send( active ).count.zero? || (
( ! restrict_user.nil? ) && ( project.tasks & restrict_user.tasks ).empty?
) || (
( ! include_only.nil? ) && ( project.task_ids & include_only ).empty?
View
59 app/controllers/users_controller.rb
@@ -1,6 +1,6 @@
########################################################################
# File:: users_controller.rb
-# (C):: Hipposoft 2008, 2009
+# (C):: Hipposoft 2008
#
# Purpose:: Manage User objects. See models/user.rb for more.
# ----------------------------------------------------------------------
@@ -55,25 +55,42 @@ def index
# the conditions on objects being fetched, including handling the search
# form data.
- options = appctrl_index_assist( User )
- active_conditions = { :active => true }
- inactive_conditions = { :active => false }
+ options = appctrl_index_assist( User )
+ active_vars = { :active => true }
+ inactive_vars = { :active => false }
+ conditions_sql = "WHERE ( active = :active )\n"
- unless ( params[ :search ].nil? )
- if ( params[ :search ].empty? or params[ :search_cancel ] )
- params.delete( :search )
- else
- search = "%#{ params[ :search ] }%" # SQL wildcards either side of the search string
- str = '( name ILIKE :search OR email ILIKE :search OR identity_url ILIKE :search ) AND active = :active'
- active_conditions = [ str, { :search => search, :active => true } ]
- inactive_conditions = [ str, { :search => search, :active => false } ]
- end
+ # If asked to search for something, build extra conditions to do so.
+
+ range_sql, range_start, range_end = appctrl_search_range_sql( User )
+
+ unless ( range_sql.nil? )
+ search = "%#{ params[ :search ] }%" # SQL wildcards either side of the search string
+ conditions_sql << "AND #{ range_sql } ( name ILIKE :search OR email ILIKE :search OR identity_url ILIKE :search )\n"
+
+ vars = { :search => search, :range_start => range_start, :range_end => range_end }
+ active_vars.merge!( vars )
+ inactive_vars.merge!( vars )
end
- # Finally, compile the collections for the view.
+ # Sort order is already partially compiled in 'options' from the earlier
+ # call to 'appctrl_index_assist'.
+
+ order_sql = "ORDER BY #{ options[ :order ] }, name ASC, code ASC"
+ options.delete( :order )
- @active_users = User.paginate( options.merge( { :conditions => active_conditions } ) )
- @inactive_users = User.paginate( options.merge( { :conditions => inactive_conditions } ) )
+ # Compile the main SQL statement.
+
+ finder_sql = "SELECT * FROM users\n" <<
+ "#{ conditions_sql }\n" <<
+ "#{ order_sql }"
+
+ # Now paginate using this SQL. The only difference between the active and
+ # inactive cases is the value of the variables passed to Active Record for
+ # substitution into the final SQL query going to the database.
+
+ @active_users = User.paginate_by_sql( [ finder_sql, active_vars ], options )
+ @inactive_users = User.paginate_by_sql( [ finder_sql, inactive_vars ], options )
end
# Show user details.
@@ -87,12 +104,10 @@ def show
# session management. The only exception is for administrators, who
# may (carefully!) choose to create user accounts up-front after
# adding an ID to the permitted list.
-
- # Administrators can (carefully) create User accounts up-front.
#
def new
return appctrl_not_permitted() unless @current_user.admin?
- return appctrl_new( 'User' )
+ @record = @user = User.new
end
# Create a new User account.
@@ -133,7 +148,7 @@ def update
@user = User.find( id )
if ( @current_user.admin? and params[ :notify_user ] )
- EmailNotifier.deliver_admin_update_notification( @user )
+ EmailNotifier.admin_update_notification( @user ).deliver()
end
# New user just set up a previously uninitialised account (no
@@ -265,7 +280,7 @@ def update_and_save_user( success_message, failure_action, send_email = false)
end
elsif ( @current_user.manager? )
user_type = params[ :user ][ :user_type ]
- @user.user_type = user_type if ( user_type != 'Admin' )
+ @user.user_type = user_type if ( user_type != User::USER_TYPE_ADMIN )
end
end
@@ -302,7 +317,7 @@ def update_and_save_user( success_message, failure_action, send_email = false)
@user.control_panel.save!
begin
- EmailNotifier.deliver_signup_notification( @user ) if ( send_email )
+ EmailNotifier.signup_notification( @user ).deliver() if ( send_email )
flash[ :notice ] = success_message
rescue => error
flash[ :notice ] = success_message + " Please note, though, that the notification e-mail message could not be sent: #{ error.message }"
View
2  app/controllers/work_packets_controller.rb
@@ -1,6 +1,6 @@
########################################################################
# File:: work_packets_controller.rb
-# (C):: Hipposoft 2008, 2009
+# (C):: Hipposoft 2008
#
# Purpose:: Manage WorkPacket objects. See models/work_packet.rb for
# more.
View
303 app/helpers/application_helper.rb
@@ -1,6 +1,6 @@
########################################################################
# File:: application_helper.rb
-# (C):: Hipposoft 2008, 2009
+# (C):: Hipposoft 2007
#
# Purpose:: Standard Rails application helper.
# ----------------------------------------------------------------------
@@ -18,39 +18,70 @@ module ApplicationHelper
# Equivalent of 'h()', but returns '-' for nil or empty strings.
def apphelp_h( value )
- return '-' if ( value.nil? or value.empty? )
+ return '-'.html_safe() if ( value.blank? )
return h( value )
end
- # Return a dynamic title based on the current request action and
- # controller.
+ # Return an internationalised version of the web site's name.
+ #
+ def apphelp_site_name
+ t( :'uk.org.pond.trackrecord.site_name' )
+ end
- def apphelp_title
- action = h( action_name )
- ctname = h( controller.controller_name )
+ # Return an internationalised version of the given action name. If 'true'
+ # is passed in the second parameter, a default fallback of the humanized
+ # version of the non-internationalised action name will be chosen. If this
+ # parameter is omitted or 'false' is given, the I18n engine's "missing token"
+ # message is returned instead (no default string is used).
+ #
+ def apphelp_action_name( action, use_default = false )
+ options = use_default ? { :default => action.to_s.humanize } : {}
+ t( "uk.org.pond.trackrecord.action_names.#{ action }", options )
+ end
- if ( [ '', 'index', 'list' ].include?( action ) )
- title = ctname.capitalize
- else
- if ( action == 'home' )
- title = 'Home page'
- else
- ctname = ctname.singularize()
+ # Return an internationalised heading appropriate for a page handling the
+ # current action for the current controller, or the given controller and
+ # optional given action name. If you want to use a default string, pass it
+ # in the optional third parameter. Headings like this can be (and are) also
+ # used as descriptive action link text.
+ #
+ def apphelp_heading( ctrl = controller, action = nil, default = nil )
+ action ||= ctrl.action_name
- if ( ctname == 'user' )
- title = "#{ action.capitalize } account"
- elsif ( ctname == 'task_import' )
- title = 'Bulk task import'
- else
- title = "#{ action.capitalize } #{ ctname }"
- end
- end
- end
+ t(
+ "uk.org.pond.trackrecord.controllers.#{ ctrl.controller_name }.action_title_#{ action }",
+ :default => default
+ )
+ end
- # Awkward special case
- title = "Enter timesheets" if ( title == "New timesheet" )
+ # Return an internationalised title appropriate for a page handling the
+ # current action for the current controller, or the given controller.
+ #
+ def apphelp_title( ctrl = controller )
+ "#{ apphelp_site_name }: #{ apphelp_heading( ctrl ) }"
+ end
- return title
+ # Shortcut for long references to "uk.org.pond.trackrecord.generic_messages"
+ # when reading generic messages from the locale file. Pass the message token
+ # part (e.g. "yes", "no", "confirmation"). Only useful for basic messages
+ # which require no parameter substitution or default lookup values.
+ #
+ def apphelp_generic( message_name )
+ I18n::t( "uk.org.pond.trackrecord.generic_messages.#{ message_name }" )
+ end
+
+ # Return a controller view hint, based on looking up "view_<foo>" in the
+ # locale file for the given value of "<foo>" (as a string or symbol). The
+ # controller handling the current request is consulted by default, else
+ # pass a reference to the controller of interest in the optional second
+ # parameter. If the hint includes subsitution tokens, pass them in an
+ # optional third parameter as a hash.
+ #
+ def apphelp_view_hint( hint_name, ctrl = controller, substitutions = {} )
+ t(
+ "uk.org.pond.trackrecord.controllers.#{ ctrl.controller_name }.view_#{ hint_name }",
+ substitutions
+ ).html_safe()
end
# Return data for the navigation bar ("slug").
@@ -58,37 +89,39 @@ def apphelp_title
def apphelp_slug
action = h( action_name )
ctname = h( controller.controller_name )
- sep = '&nbsp;&raquo;&nbsp;'
- slug = link_to( 'Home page', home_path() ) << sep
+ sep = '&nbsp;&raquo;&nbsp;'.html_safe()
+ slug = link_to( 'Home', home_path() ) << sep
if ( ctname == 'users' and action == 'home' )
- slug = 'Home page'
+ slug = 'Home'
elsif ( ctname == 'sessions' and action == 'new' )
slug << 'Sign in'
elsif ( action == 'index' or action == 'list' )
- slug << apphelp_title()
+ slug << apphelp_heading()
elsif ( ctname == 'reports' )
- if ( action == 'create' )
- slug << link_to( 'Reports', new_report_path() ) <<
- sep <<
- 'Show report'
- else
- slug << 'Reports'
- end
+ slug << link_to( 'Reports', new_user_saved_report_path( @current_user ) ) <<
+ sep <<
+ 'Show report'
+ elsif ( ctname == 'saved_reports' )
+ slug << link_to( 'Reports', new_user_saved_report_path( @current_user ) ) <<
+ sep <<
+ apphelp_heading()
else
slug << link_to( ctname.capitalize(), send( "#{ ctname }_path" ) ) <<
sep <<
- apphelp_title()
+ apphelp_heading()
end
return slug
end
- # Return strings 'Yes' or 'No' depending on the value of the given
- # boolean quantity.
+ # Return 'yes' or 'no', internationalised, according to the given value,
+ # which is evaluated as (or should already be) a boolean. Remember that in
+ # Ruby the boolean evaluation of certain types can be unexpected - e.g.
+ # integer zero is not "nil", so it evaluates to 'true' in a boolean context.
#
- def apphelp_boolean( boolean )
- boolean ? 'Yes' : 'No'
+ def apphelp_boolean( bool )
+ apphelp_generic( bool ? :yes : :no )
end