Module TasksHelper
In: app/helpers/tasks_helper.rb
File:tasks_helper.rb
(C):Hipposoft 2008, 2009
Purpose:Support functions for views related to Task objects. See controllers/tasks_controller.rb for more.

          04-Jan-2008 (ADH): Created.

Methods

Public Instance methods

Return list actions appropriate for the given task.

[Source]

    # File app/helpers/tasks_helper.rb, line 73
73:   def taskhelp_actions( task )
74:     if ( @current_user.admin? or ( task.active and @current_user.manager? ) )
75:       actions = [ 'edit' ]
76:       actions.push( 'delete' ) if ( @current_user.admin? )
77:     else
78:       actions = []
79:     end
80: 
81:     actions.push( 'show' )
82:     return actions
83:   end

As taskhelp_billable_class, but for active/inactive tasks.

[Source]

     # File app/helpers/tasks_helper.rb, line 107
107:   def taskhelp_active_class( task_or_boolean )
108:     task_or_boolean = task_or_boolean.active if ( task_or_boolean.is_a?( Task ) )
109:     return ( task_or_boolean ? "task_active" : "task_inactive" )
110:   end

As taskhelp_billable_help, but for active/inactive tasks.

[Source]

     # File app/helpers/tasks_helper.rb, line 114
114:   def taskhelp_active_help
115:     "Names of active or inactive tasks are shown as follows: " <<
116:     "'<span class=\"#{ taskhelp_active_class( true  ) }\">active</span>', " <<
117:     "'<span class=\"#{ taskhelp_active_class( false ) }\">inactive</span>'."
118:   end

Return an HTML class name appropriate for a billable or not billable task according to the given task or, if given a boolean, the value of that boolean where ‘true’ means ‘billable‘

Note that an additional class name may be included for active/inactive tasks (but not when a boolean is given, obviously!).

[Source]

    # File app/helpers/tasks_helper.rb, line 92
92:   def taskhelp_billable_class( task_or_boolean )
93:     task_or_boolean = task_or_boolean.billable if ( task_or_boolean.is_a?( Task ) )
94:     return ( task_or_boolean ? "task_billable" : "task_not_billable" )
95:   end

Return an HTML fragment giving help on billable versus non-billable tasks.

[Source]

     # File app/helpers/tasks_helper.rb, line 99
 99:   def taskhelp_billable_help
100:     "Names of billable or non-billable tasks are shown as follows: " <<
101:     "'<span class=\"#{ taskhelp_billable_class( true  ) }\">billable</span>', " <<
102:     "'<span class=\"#{ taskhelp_billable_class( false ) }\">not billable</span>'."
103:   end

Return a name of a given task‘s associated customer, for use in list views.

[Source]

    # File app/helpers/tasks_helper.rb, line 60
60:   def taskhelp_customer( task )
61:     return '-' if ( task.project.nil? or task.project.customer.nil? )
62:     return link_to( task.project.customer.title, customer_path( task.project.customer ) )
63:   end

Create a degrading task selector using either a YUI tree or a SELECT list, but not both. The latter has high database load. The former has greater client requirements.

The use cases for task selectors in Track Record are so varied that very specific cases are handled with special-case code and HTML output may include extra text to help the user for certain edge conditions, such as a lack of any available tasks (in a timesheet editor, this may be because all tasks are already added to the timesheet; for a user‘s choice of the default list of tasks to show in timesheets, this may be because the user has no permission to view any tasks; when configuring the tasks which a restricted user is able to see, this may be because no active tasks exist).

As a result, pass the reason for calling in the first parameter and an options list in the second.

Supported reasons are symbols and listsed in the ‘case’ statement in the code below. Each is preceeded by comprehensive comments describing the mandatory and (if any) optional key/value pairs which should go into the options hash. Please consult these comments for more information.

The following global options are also supported (none are mandatory):

  Key           Value
  =====================================================================
  :line_prefix  A string to insert at the start of each line of output
                - usually spaces, used if worried about the indentation
                of the overall view HTML.

[Source]

     # File app/helpers/tasks_helper.rb, line 338
338:   def taskhelp_degrading_selector( reason, options )
339:     output = ''
340:     form   = options.delete( :form )
341:     user   = options.delete( :user )
342: 
343:     case reason
344: 
345:       # Generate a selector used to add tasks to a given timesheet. Includes a
346:       # task selector and "add" button which submits to the given form with
347:       # name "add_row". The timesheet and form are specified in the options:
348:       #
349:       #   Key         Value
350:       #   =====================================================================
351:       #   :form       Prevailing outer form, e.g. the value of "f" in a view
352:       #               which has enclosed the call in "form_for :foo do | f |".
353:       #
354:       #               Leads to "task_ids" being invoked on the model associated
355:       #               with the form to determine which items, if any, must be
356:       #               initially selected for lists in the non-JS page version.
357:       #
358:       #   :timesheet  Instance of the timesheet being edited - used to find out
359:       #               which tasks are already included in the timesheet and
360:       #               thus which, if any, should be offered in the selector.
361:       #
362:       # NOTE: An empty string is returned if all tasks are already included in
363:       # the timesheet.
364:       #
365:       when :timesheet_editor
366:         tasks = timesheethelp_tasks_for_addition( options[ :timesheet ] )
367: 
368:         unless ( tasks.empty? )
369: 
370:           if ( session[ :javascript ].nil? )
371:             Task.sort_by_augmented_title( tasks )
372:             output << apphelp_collection_select( form, :task_ids, tasks, :id, :augmented_title )
373:             output << '<br />'
374:             output << form.submit( 'Add', { :name => 'add_row', :id => nil } )
375:           else
376:             output << taskhelp_tree_selector(
377:               form,
378:               {
379:                 :included_tasks => tasks,
380:                 :change_text    => 'Choose tasks...',
381:                 :suffix_html    => " then #{ form.submit( 'add them', { :name => 'add_row', :id => nil } ) }\n"
382:               }
383:             )
384:           end
385: 
386:         end
387: 
388:       # Generate a selector used to add tasks to a given report. The report and
389:       # form are specified in the following options:
390:       #
391:       #   Key        Value
392:       #   =====================================================================
393:       #   :form      Prevailing outer form, e.g. the value of "f" in a view
394:       #              which has enclosed the call in "form_for :foo do | f |".
395:       #
396:       #              Leads to "task_ids" being invoked on the model associated
397:       #              with the form to determine which items, if any, must be
398:       #              initially selected for lists in the non-JS page version.
399:       #
400:       #   :report    Instance of the report being created - used to find out
401:       #              which tasks are already included in the report (for form
402:       #              resubmissions, e.g. from validation failure).
403:       #
404:       #   :inactive  If 'true', the selector is generated for inactive tasks
405:       #              only. If omitted or 'false', only active tasks are shown.
406:       #
407:       #   :name      A name to use instead of "task_ids" in the form submission
408:       #              - optional, required if you want multiple selectors in the
409:       #              same form.
410:       #
411:       # NOTE: All edge case conditions (no tasks, etc.) are handled internally
412:       # with relevant messages included in the HTML output for individual
413:       # selectors, but the caller ought to check that at least *some* tasks can
414:       # be chosen before presenting the user with a report generator form.
415:       #
416:       when :report_generator
417:         report =   options[ :report   ]
418:         active = ( options[ :inactive ] != true )
419:         field  = active ? :active_task_ids : :inactive_task_ids
420: 
421:         if ( session[ :javascript ].nil? )
422:           tasks = active ? Task.active() : Task.inactive()
423:           count = tasks.length
424:         else
425:           tasks = active ? report.active_tasks : report.inactive_tasks
426:           count = @current_user.all_permitted_tasks.count
427:         end
428: 
429:         if ( count.zero? )
430: 
431:           hint = active ? :active : :inactive
432:           output << "No #{ hint } tasks are available."
433: 
434:         else
435: 
436:           if ( session[ :javascript ].nil? )
437:             Task.sort_by_augmented_title( tasks )
438:             output << apphelp_collection_select(
439:               form,
440:               field,
441:               tasks,
442:               :id,
443:               :augmented_title
444:             )
445:           else
446:             output << taskhelp_tree_selector(
447:               form,
448:               {
449:                 :selected_tasks => tasks,
450:                 :params_name    => field,
451:                 :inactive       => ! active
452:               }
453:             )
454:           end
455:         end
456: 
457:       # Generate a selector which controls the default task list shown in
458:       # new timesheets.
459:       #
460:       #   Key    Value
461:       #   =====================================================================
462:       #   :user  Instance of User model for the user for whom default timesheet
463:       #          options are being changed.
464:       #
465:       #   :form  Prevailing outer form, e.g. the value of "f" in a view
466:       #          which has enclosed the call in "form_for :foo do | f |".
467:       #          Typically this is used by a User configuration view, though,
468:       #          where a nested set of fields are being built via something
469:       #          like "fields_for :control_panel do | cp |". In such a case,
470:       #          use "cp" for the ":form" option's value.
471:       #
472:       #          This leads to "task_ids" being invoked on the model associated
473:       #          with the form to determine which items in the selection list,
474:       #          if any, must be initially selected in the non-JS page version.
475:       #
476:       # NOTE: All edge case conditions (no tasks, etc.) are handled internally
477:       # with relevant messages included in the HTML output.
478:       #
479:       when :user_default_task_list
480: 
481:         if ( user.active_permitted_tasks.count.zero? )
482: 
483:           # Warn that the user has no permission to see any tasks at all.
484: 
485:           output << "This account does not have permission to view\n"
486:           output << "any active tasks.\n"
487:           output << "\n\n"
488:           output << "<p>\n"
489: 
490:           # If the currently logged in user is unrestricted, tell them how to
491:           # rectify the above problem. Otherwise, tell them to talk to their
492:           # system administrator.
493: 
494:           if ( @current_user.restricted? )
495:             output << "  Please contact your system administrator for help.\n"
496:           else
497:             output << "  To enable this section, please assign tasks to\n"
498:             output << "  the user account with the security settings above\n"
499:             output << "  and save your changes. Then edit the user account\n"
500:             output << "  again to see the new permitted task list.\n"
501:           end
502: 
503:           output << "</p>"
504: 
505:         else
506: 
507:           if ( session[ :javascript ].nil? )
508:             tasks = user.active_permitted_tasks
509:             Task.sort_by_augmented_title( tasks )
510:             output << apphelp_collection_select( form, :task_ids, tasks, :id, :augmented_title )
511:           else
512:             output << taskhelp_tree_selector(
513:               form,
514:               {
515:                 :restricted_by  => ( user.restricted? ) ? user : nil,
516:                 :selected_tasks => user.control_panel.tasks
517:               }
518:             )
519:           end
520: 
521:         end
522: 
523:       # Generate a selector which controls the list of tasks the user is
524:       # permitted to see. Mandatory options:
525:       #
526:       #   Key    Value
527:       #   =====================================================================
528:       #   :user  Instance of User model for the user to whom task viewing
529:       #          permission is being granted or revoked.
530:       #
531:       #   :form  Prevailing outer form, e.g. the value of "f" in a view
532:       #          which has enclosed the call in "form_for :foo do | f |".
533:       #
534:       #          This leads to "task_ids" being invoked on the model associated
535:       #          with the form to determine which items in the selection list,
536:       #          if any, must be initially selected in the non-JS page version.
537:       #
538:       # NOTE: All edge case conditions (no tasks, etc.) are handled internally
539:       # with relevant messages included in the HTML output.
540:       #
541:       when :user_permitted_task_list
542:         return '' if ( @current_user.restricted? ) # Privileged users only!
543: 
544:         if ( Task.active.count.zero? )
545: 
546:           output << "There are no tasks currently defined. Please\n"
547:           output << "#{ link_to( 'create at least one', new_task_path() ) }."
548: 
549:         else
550: 
551:           if ( session[ :javascript ].nil? )
552:             tasks = Task.active()
553:             Task.sort_by_augmented_title( tasks )
554:             output << apphelp_collection_select( form, :task_ids, tasks, :id, :augmented_title )
555:           else
556: 
557:             # Don't use "user.[foo_]permitted_tasks" here as we *want* an empty
558:             # list for privileged accounts where no tasks have been set up.
559: 
560:             output << taskhelp_tree_selector(
561:               form,
562:               { :selected_tasks => user.tasks }
563:             )
564:           end
565: 
566:           unless ( user.restricted? )
567:             output << "\n\n"
568:             output << "<p>\n"
569:             output << "  This list is only enforced for users with a\n"
570:             output << "  'Normal' account type. It is included here\n"
571:             output << "  in case you are intending to change the account\n"
572:             output << "  type and want to assign tasks at the same time.\n"
573:             output << "</p>"
574:           end
575: 
576:         end
577:     end
578: 
579:     # All done. Indent or otherwise add a prefix to each line of output if
580:     # so required by the options and return the overall result.
581: 
582:     line_prefix = options.delete( :line_prefix )
583:     output.gsub!( /^/, line_prefix ) unless ( output.empty? || line_prefix.nil? )
584: 
585:     return output
586:   end

Return a formatted duration for the given task, for use in list views.

[Source]

    # File app/helpers/tasks_helper.rb, line 67
67:   def taskhelp_duration( task )
68:     return apphelp_string_hours( task.duration.to_s, '0', '?' )
69:   end

Return HTML showing the number of hours overrun on a task, or a label indicating no overrun. Pass the number of hours and the expected task duration.

[Source]

    # File app/helpers/tasks_helper.rb, line 43
43:   def taskhelp_overrun( hours, duration )
44:     if ( hours > duration and duration != 0 )
45:       return apphelp_hours( hours - duration )
46:     else
47:       return '<span class="no_overrun">None</span>'
48:     end
49:   end

Return a name of a given task‘s associated project, for use in list views.

[Source]

    # File app/helpers/tasks_helper.rb, line 53
53:   def taskhelp_project( task )
54:     reutnr '-' if ( task.project.nil? )
55:     return link_to( task.project.title, project_path( task.project ) )
56:   end

Return HTML suitable for an edit form, providing a grouped list of projects that can be assigned to the task. The list is grouped by customer in default customer sort order, along with a ‘None’ entry for unassigned projects. Pass the task object being edited.

[Source]

    # File app/helpers/tasks_helper.rb, line 18
18:   def taskhelp_project_selector( task )
19:     return apphelp_project_selector(
20:     'task_project_id',
21:     'task[project_id]',
22:     task.project_id
23:     )
24:   end

Return HTML showing the remaining hours on a task, or a label indicating overrun. Pass the number of hours and the expected task duration.

[Source]

    # File app/helpers/tasks_helper.rb, line 30
30:   def taskhelp_remaining( hours, duration )
31:     if ( hours > duration and duration != 0 )
32:       return '<span class="overrun">Overrunning</span>'
33:     else
34:       return apphelp_hours( 0 ) if ( duration == 0 )
35:       return apphelp_hours( duration - hours )
36:     end
37:   end

Generate a YUI tree task selector. Pass a form builder object in the first parameter (e.g. "bar" in "form_for @foo do | bar |"). The "object_name" field is used to generate a unique ID and name for a hidden element which carries IDs of selected YUI tree nodes, in the form "name[task_ids][]" - as used by non-JS SELECT lists elsewhere. In the form submission processing code, you must handle the use of special IDs in the YUI tree ("P" prefix for Projects, "C" prefix for Customers, no prefix for tasks).

In the next parameter optionally pass an options hash with keys and values as shown below; any omitted key/value pair results in the described default value being used instead.

  Key              Meaning
  =========================================================================
  :inactive        If 'true', only inactive tasks, customers and projects
                   are shown in the selector. By default, only active items
                   will be shown.

  :restricted_by   The task/project/customer list for currently logged in
                   users who are restricted is always restricted by that
                   user's permitted task list no matter what you set here.
                   If the current user is privileged, though, then passing
                   in a User object results in restriction by that user's
                   permitted task list. By default there is no restriction
                   so for privileged currently logged in users, all tasks
                   would be shown.

  :included_tasks  If you know up-front a full list of tasks to show, then
                   pass them in here. Only items in the included list will
                   be shown. IDs get passed to the XHR handler, among other
                   things. This is NOT a security feature - see
                   ":restricted_by" for that.

  :selected_tasks  An array of tasks to be initially selected in the tree.
                   May be empty. By default no tasks are selected. Ideally
                   the list should include no tasks that the current user
                   or 'restricted_by' key would hide, but if it does, they
                   simply won't be shown or selected and only the task IDs
                   will appear in HTML output.

  :suffix_html     Beneath the text area gadget listing selected tasks is
                   a "Change..." link which pops up the Leightbox overlay
                   containing the YUI tree. If you want any extra HTML
                   inserted directly after the "</a>" of this link but
                   before the (hidden) DIV enclosing the tree, use this
                   option to include it. To keep things tidy, ensure that
                   the string is terminated by a newline ("\n") character.

  :change_text     Speaking of the "Change..." link - alter its text with
                   this option, or omit for the default "Change..." string.

  :params_name     Name to use in params instead of "task_ids", so that
                   instead of reading "params[form.object_name][:task_ids]"
                   in the controller handling the form submission, you read
                   the params entry corresponding to the given name.

See also "taskhelp_degrading_selector" for JS/non-JS degrading code.

[Source]

     # File app/helpers/tasks_helper.rb, line 178
178:   def taskhelp_tree_selector( form, options = {} )
179: 
180:     inactive       = options.delete( :inactive       )
181:     restricted_by  = options.delete( :restricted_by  )
182:     included_tasks = options.delete( :included_tasks )
183:     selected_tasks = options.delete( :selected_tasks ) || []
184:     suffix_html    = options.delete( :suffix_html    ) || ''
185:     change_text    = options.delete( :change_text    ) || 'Change...'
186:     params_name    = options.delete( :params_name    ) || :task_ids
187: 
188:     # Callers may generate trees restricted by the current user, but if that
189:     # user is themselves privileged their restricted task list will be empty
190:     # (because they can see anything). The simplest way to deal with this is
191:     # to clear the restricted user field in such cases.
192: 
193:     restricted_by = nil unless ( restricted_by.nil? || restricted_by.restricted? )
194: 
195:     # Based on the restricted task list - or otherwise - try to get at the root
196:     # customer array as easily as possible, trying to avoid pulling all tasks
197:     # out of the database. This is a bit painful either way, but usually a
198:     # restricted user will have a relatively small set of tasks assigned to
199:     # them so doing the array processing in Ruby isn't too big a deal.
200: 
201:     if ( @current_user.restricted? )
202:       permitted_tasks = @current_user.active_permitted_tasks()
203:     else
204:       permitted_tasks = restricted_by.active_permitted_tasks() unless ( restricted_by.nil? )
205:     end
206: 
207:     unless ( included_tasks.nil? )
208:       if ( permitted_tasks.nil? )
209:         permitted_tasks  = included_tasks
210:       else
211:         permitted_tasks &= included_tasks
212:       end
213:     end
214: 
215:     if ( permitted_tasks.nil? )
216:       root_customers = Customer.all
217:     else
218:       root_projects  = permitted_tasks.map { | task    | task.project     }.uniq
219:       root_customers = root_projects.map   { | project | project.customer }.uniq
220:     end
221: 
222:     # Reject items with no active / inactive tasks.
223: 
224:     method = inactive ? :inactive : :active
225: 
226:     root_customers.reject! do | customer |
227:       customer.tasks.send( method ).count.zero?
228:     end
229: 
230:     # Sort the root list by default order. Related associations are sorted
231:     # according to declarations made in the model code.
232: 
233:     Customer.apply_default_sort_order( root_customers )
234: 
235:     # Now take the selected task list and do something similar to get at the
236:     # selected project and customer IDs so we can build a complete list of the
237:     # node IDs to be initially expanded and checked in the YUI tree. The
238:     # customer list is sorted so that when the YUI tree starts expanding nodes,
239:     # it does it in display order from top to bottom - this looks better than
240:     # an arbitrary expansion order.
241: 
242:     selected_projects     = selected_tasks.map    { | task    | task.project     }.uniq
243:     selected_customers    = selected_projects.map { | project | project.customer }.uniq
244: 
245:     Customer.apply_default_sort_order( selected_customers )
246: 
247:     selected_task_ids     = selected_tasks.map     { | item | item.id         }
248:     selected_project_ids  = selected_projects.map  { | item | "P#{ item.id }" }
249:     selected_customer_ids = selected_customers.map { | item | "C#{ item.id }" }
250: 
251:     selected_ids = selected_customer_ids + selected_project_ids + selected_task_ids
252: 
253:     # Turn an included task list into IDs too, if present
254: 
255:     included_ids = ( included_tasks || [] ).map { | item | item.id }
256: 
257:     # Generate the root node data and extra XHR parameters to pass to the tree
258:     # controller in 'tree_controller.rb'.
259: 
260:     roots = root_customers.map do | customer |
261:       {
262:         :label  => customer.title,
263:         :isLeaf => false,
264:         :id     => "C#{ customer.id }"
265:       }
266:     end
267: 
268:     data_for_xhr_call  = []
269:     data_for_xhr_call << 'inactive' if ( inactive )
270:     data_for_xhr_call << "restrict,#{ restricted_by.id }" unless ( restricted_by.nil? )
271:     data_for_xhr_call << "include,#{ included_ids.join( '_' ) }" unless ( included_ids.empty? )
272: 
273:     # Create and (implicitly) return the HTML.
274: 
275:     id   = "#{ form.object_name }_#{ params_name }"
276:     name = "#{ form.object_name }[#{ params_name }][]"
277:     tree = yui_tree(
278:       :multiple             => true,
279:       :target_form_field_id => id,
280:       :target_name_field_id => "#{ id }_text",
281:       :name_field_separator => "\n", # Yes, a literal newline character
282:       :name_include_parents => ' &raquo; ',
283:       :name_leaf_nodes_only => true,
284:       :form_leaf_nodes_only => true,
285:       :expand               => selected_ids,
286:       :highlight            => selected_ids,
287:       :propagate_up         => true,
288:       :propagate_down       => true,
289:       :root_collection      => roots,
290:       :data_for_xhr_call    => data_for_xhr_call.join( ',' ),
291:       :div_id               => 'yui_tree_container_' << id
292:     ).gsub( /^/, '  ' )
293:     html = "<textarea disabled=\"disabled\" rows=\"5\" cols=\"60\" class=\"tree_selector_text\" id=\"\#{ id }_text\">Task data loading...</textarea>\n<br />\n<a href=\"#leightbox_tree_\#{ id }\" rel=\"leightbox_tree_\#{ id }\" class=\"lbOn\">\#{ change_text }</a>\n\#{ suffix_html }<div id=\"leightbox_tree_\#{ id }\" class=\"leightbox\">\n  <a href=\"#\" class=\"lbAction\" rel=\"deactivate\">Close</a>\n  \#{ taskhelp_billable_help }\n  <p />\n  \#{ hidden_field_tag( id, selected_task_ids.join( ',' ), { :name => name } ) }\n\#{ tree }\n  <a href=\"#\" class=\"lbAction\" rel=\"deactivate\">Close</a>\n</div>\n"
294:   end

[Validate]