stuff_to_do_plugin 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. data/COPYRIGHT.txt +18 -0
  2. data/CREDITS.txt +6 -0
  3. data/GPL.txt +339 -0
  4. data/README.rdoc +61 -0
  5. data/Rakefile +38 -0
  6. data/VERSION +1 -0
  7. data/app/controllers/stuff_to_do_controller.rb +161 -0
  8. data/app/helpers/stuff_to_do_helper.rb +88 -0
  9. data/app/models/stuff_to_do.rb +208 -0
  10. data/app/models/stuff_to_do_filter.rb +32 -0
  11. data/app/models/stuff_to_do_mailer.rb +16 -0
  12. data/app/views/settings/_stuff_to_do_settings.html.erb +27 -0
  13. data/app/views/stuff_to_do/_issue.html.erb +16 -0
  14. data/app/views/stuff_to_do/_item.html.erb +5 -0
  15. data/app/views/stuff_to_do/_left_panes.html.erb +51 -0
  16. data/app/views/stuff_to_do/_panes.html.erb +11 -0
  17. data/app/views/stuff_to_do/_project.html.erb +6 -0
  18. data/app/views/stuff_to_do/_right_panes.html.erb +25 -0
  19. data/app/views/stuff_to_do/_time_grid.html.erb +113 -0
  20. data/app/views/stuff_to_do/_time_grid_form.html.erb +32 -0
  21. data/app/views/stuff_to_do/index.html.erb +44 -0
  22. data/app/views/stuff_to_do_mailer/recommended_below_threshold.erb +3 -0
  23. data/app/views/stuff_to_do_mailer/recommended_below_threshold.text.html.rhtml +1 -0
  24. data/assets/images/b.png +0 -0
  25. data/assets/images/bl.png +0 -0
  26. data/assets/images/br.png +0 -0
  27. data/assets/images/closelabel.gif +0 -0
  28. data/assets/images/loading.gif +0 -0
  29. data/assets/images/tl.png +0 -0
  30. data/assets/images/tr.png +0 -0
  31. data/assets/javascripts/facebox.js +319 -0
  32. data/assets/javascripts/jquery-1.2.6.min.js +32 -0
  33. data/assets/javascripts/jquery-ui.js +2839 -0
  34. data/assets/javascripts/jquery.contextMenu.js +212 -0
  35. data/assets/javascripts/semantic.cache +15 -0
  36. data/assets/javascripts/stuff-to-do.js +270 -0
  37. data/assets/javascripts/ui/build.xml +24 -0
  38. data/assets/javascripts/ui/effects.blind.js +49 -0
  39. data/assets/javascripts/ui/effects.bounce.js +78 -0
  40. data/assets/javascripts/ui/effects.clip.js +54 -0
  41. data/assets/javascripts/ui/effects.core.js +510 -0
  42. data/assets/javascripts/ui/effects.drop.js +50 -0
  43. data/assets/javascripts/ui/effects.explode.js +79 -0
  44. data/assets/javascripts/ui/effects.fold.js +55 -0
  45. data/assets/javascripts/ui/effects.highlight.js +48 -0
  46. data/assets/javascripts/ui/effects.pulsate.js +55 -0
  47. data/assets/javascripts/ui/effects.scale.js +180 -0
  48. data/assets/javascripts/ui/effects.shake.js +57 -0
  49. data/assets/javascripts/ui/effects.slide.js +50 -0
  50. data/assets/javascripts/ui/effects.transfer.js +59 -0
  51. data/assets/javascripts/ui/i18n/ui.datepicker-ar.js +26 -0
  52. data/assets/javascripts/ui/i18n/ui.datepicker-bg.js +25 -0
  53. data/assets/javascripts/ui/i18n/ui.datepicker-ca.js +25 -0
  54. data/assets/javascripts/ui/i18n/ui.datepicker-cs.js +25 -0
  55. data/assets/javascripts/ui/i18n/ui.datepicker-da.js +25 -0
  56. data/assets/javascripts/ui/i18n/ui.datepicker-de.js +25 -0
  57. data/assets/javascripts/ui/i18n/ui.datepicker-eo.js +25 -0
  58. data/assets/javascripts/ui/i18n/ui.datepicker-es.js +25 -0
  59. data/assets/javascripts/ui/i18n/ui.datepicker-fa.js +25 -0
  60. data/assets/javascripts/ui/i18n/ui.datepicker-fi.js +25 -0
  61. data/assets/javascripts/ui/i18n/ui.datepicker-fr.js +25 -0
  62. data/assets/javascripts/ui/i18n/ui.datepicker-he.js +25 -0
  63. data/assets/javascripts/ui/i18n/ui.datepicker-hr.js +25 -0
  64. data/assets/javascripts/ui/i18n/ui.datepicker-hu.js +25 -0
  65. data/assets/javascripts/ui/i18n/ui.datepicker-hy.js +25 -0
  66. data/assets/javascripts/ui/i18n/ui.datepicker-id.js +25 -0
  67. data/assets/javascripts/ui/i18n/ui.datepicker-is.js +25 -0
  68. data/assets/javascripts/ui/i18n/ui.datepicker-it.js +25 -0
  69. data/assets/javascripts/ui/i18n/ui.datepicker-ja.js +26 -0
  70. data/assets/javascripts/ui/i18n/ui.datepicker-ko.js +25 -0
  71. data/assets/javascripts/ui/i18n/ui.datepicker-lt.js +25 -0
  72. data/assets/javascripts/ui/i18n/ui.datepicker-lv.js +25 -0
  73. data/assets/javascripts/ui/i18n/ui.datepicker-nl.js +25 -0
  74. data/assets/javascripts/ui/i18n/ui.datepicker-no.js +25 -0
  75. data/assets/javascripts/ui/i18n/ui.datepicker-pl.js +25 -0
  76. data/assets/javascripts/ui/i18n/ui.datepicker-pt-BR.js +25 -0
  77. data/assets/javascripts/ui/i18n/ui.datepicker-ro.js +25 -0
  78. data/assets/javascripts/ui/i18n/ui.datepicker-ru.js +25 -0
  79. data/assets/javascripts/ui/i18n/ui.datepicker-sk.js +25 -0
  80. data/assets/javascripts/ui/i18n/ui.datepicker-sl.js +26 -0
  81. data/assets/javascripts/ui/i18n/ui.datepicker-sq.js +25 -0
  82. data/assets/javascripts/ui/i18n/ui.datepicker-sv.js +25 -0
  83. data/assets/javascripts/ui/i18n/ui.datepicker-th.js +25 -0
  84. data/assets/javascripts/ui/i18n/ui.datepicker-tr.js +25 -0
  85. data/assets/javascripts/ui/i18n/ui.datepicker-uk.js +25 -0
  86. data/assets/javascripts/ui/i18n/ui.datepicker-zh-CN.js +25 -0
  87. data/assets/javascripts/ui/i18n/ui.datepicker-zh-TW.js +25 -0
  88. data/assets/javascripts/ui/svn.log +11 -0
  89. data/assets/javascripts/ui/ui.accordion.js +400 -0
  90. data/assets/javascripts/ui/ui.core.js +533 -0
  91. data/assets/javascripts/ui/ui.datepicker.js +1754 -0
  92. data/assets/javascripts/ui/ui.dialog.js +630 -0
  93. data/assets/javascripts/ui/ui.draggable.js +696 -0
  94. data/assets/javascripts/ui/ui.droppable.js +314 -0
  95. data/assets/javascripts/ui/ui.progressbar.js +114 -0
  96. data/assets/javascripts/ui/ui.resizable.js +805 -0
  97. data/assets/javascripts/ui/ui.selectable.js +266 -0
  98. data/assets/javascripts/ui/ui.slider.js +552 -0
  99. data/assets/javascripts/ui/ui.sortable.js +1012 -0
  100. data/assets/javascripts/ui/ui.tabs.js +572 -0
  101. data/assets/stylesheets/stuff_to_do.css +216 -0
  102. data/config/locales/bg.yml +18 -0
  103. data/config/locales/ca-fr.yml +18 -0
  104. data/config/locales/cs.yml +16 -0
  105. data/config/locales/da.yml +16 -0
  106. data/config/locales/de.yml +18 -0
  107. data/config/locales/en.yml +24 -0
  108. data/config/locales/es.yml +19 -0
  109. data/config/locales/fr.yml +17 -0
  110. data/config/locales/hu.yml +16 -0
  111. data/config/locales/it.yml +16 -0
  112. data/config/locales/ja.yml +18 -0
  113. data/config/locales/ko.yml +18 -0
  114. data/config/locales/lt.yml +18 -0
  115. data/config/locales/nl.yml +20 -0
  116. data/config/locales/pt-BR.yml +18 -0
  117. data/config/locales/ru.yml +19 -0
  118. data/config/locales/sv.yml +19 -0
  119. data/config/locales/tr.yml +18 -0
  120. data/config/routes.rb +3 -0
  121. data/init.rb +54 -0
  122. data/lang/bg.yml +17 -0
  123. data/lang/ca-fr.yml +17 -0
  124. data/lang/cs.yml +15 -0
  125. data/lang/da.yml +15 -0
  126. data/lang/de.yml +17 -0
  127. data/lang/en.yml +21 -0
  128. data/lang/es.yml +18 -0
  129. data/lang/fr.yml +16 -0
  130. data/lang/hu.yml +15 -0
  131. data/lang/it.yml +15 -0
  132. data/lang/ja.yml +17 -0
  133. data/lang/ko.yml +17 -0
  134. data/lang/lt.yml +17 -0
  135. data/lang/pt-br.yml +17 -0
  136. data/lang/ru.yml +15 -0
  137. data/lang/sv.yml +18 -0
  138. data/lang/tr.yml +17 -0
  139. data/lib/redmine_stuff_to_do/stuff_to_do_compatibility.rb +15 -0
  140. data/lib/stuff_to_do_array_patch.rb +8 -0
  141. data/lib/stuff_to_do_issue_patch.rb +57 -0
  142. data/lib/stuff_to_do_project_patch.rb +31 -0
  143. data/lib/stuff_to_do_user_patch.rb +10 -0
  144. data/rails/init.rb +1 -0
  145. data/spec/controllers/stuff_to_do_controller_add_to_time_grid_spec.rb +58 -0
  146. data/spec/controllers/stuff_to_do_controller_index_spec.rb +155 -0
  147. data/spec/controllers/stuff_to_do_controller_remove_from_time_grid_spec.rb +56 -0
  148. data/spec/controllers/stuff_to_do_controller_reorder_spec.rb +179 -0
  149. data/spec/controllers/stuff_to_do_controller_save_time_entries_spec.rb +56 -0
  150. data/spec/controllers/stuff_to_do_private_methods_spec.rb +82 -0
  151. data/spec/lib/stuff_to_do_issue_patch_spec.rb +60 -0
  152. data/spec/lib/stuff_to_do_project_patch_spec.rb +50 -0
  153. data/spec/lib/stuff_to_do_user_patch_spec.rb +8 -0
  154. data/spec/models/stuff_to_do_filter_spec.rb +3 -0
  155. data/spec/models/stuff_to_do_mailer_spec.rb +42 -0
  156. data/spec/models/stuff_to_do_spec.rb +426 -0
  157. data/spec/sanity_spec.rb +7 -0
  158. data/spec/spec_helper.rb +130 -0
  159. metadata +211 -0
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.4.0
@@ -0,0 +1,161 @@
1
+ class StuffToDoController < ApplicationController
2
+ unloadable
3
+
4
+ before_filter :get_user
5
+ before_filter :get_time_grid, :only => [:index, :time_grid]
6
+ before_filter :require_admin, :only => :available_issues
7
+ helper :stuff_to_do
8
+ helper :timelog
9
+
10
+ def index
11
+ @doing_now = StuffToDo.doing_now(@user)
12
+ @recommended = StuffToDo.recommended(@user)
13
+ @available = StuffToDo.available(@user, default_filters )
14
+
15
+ @users = User.active
16
+ @filters = filters_for_view
17
+ end
18
+
19
+ def reorder
20
+ StuffToDo.reorder_list(@user, params[:stuff])
21
+ @doing_now = StuffToDo.doing_now(@user)
22
+ @recommended = StuffToDo.recommended(@user)
23
+ @available = StuffToDo.available(@user, get_filters )
24
+
25
+ respond_to do |format|
26
+ format.html { redirect_to :action => 'index'}
27
+ format.js { render :partial => 'panes', :layout => false}
28
+ end
29
+ end
30
+
31
+ def available_issues
32
+ @available = StuffToDo.available(@user, get_filters)
33
+
34
+ respond_to do |format|
35
+ format.html { redirect_to :action => 'index'}
36
+ format.js { render :partial => 'right_panes', :layout => false}
37
+ end
38
+ end
39
+
40
+ def time_grid
41
+ respond_to do |format|
42
+ format.html { redirect_to :action => 'index'}
43
+ format.js { render :partial => 'time_grid', :layout => false}
44
+ end
45
+ end
46
+
47
+ def add_to_time_grid
48
+ issue = Issue.visible.find_by_id(params[:issue_id])
49
+ # Issue exists and isn't already in user's list
50
+ if issue && !User.current.time_grid_issues.exists?(issue)
51
+ User.current.time_grid_issues << issue
52
+ end
53
+ get_time_grid
54
+ time_grid
55
+ end
56
+
57
+ def remove_from_time_grid
58
+ issue = User.current.time_grid_issues.visible.find_by_id(params[:issue_id])
59
+ User.current.time_grid_issues.delete(issue) if issue
60
+ get_time_grid
61
+ time_grid
62
+ end
63
+
64
+ def save_time_entry
65
+ @time_entry = TimeEntry.new
66
+ @time_entry.user = User.current
67
+ if params[:time_entry] && params[:time_entry].first
68
+ @time_entry.attributes = params[:time_entry].first
69
+ end
70
+ @time_entry.project = @time_entry.issue.project if @time_entry.issue
71
+ respond_to do |format|
72
+ if save_time_entry_from_time_grid(@time_entry)
73
+ flash.now[:time_grid_notice] = l(:notice_successful_update)
74
+ get_time_grid # after saving in order to get the updated data
75
+
76
+ format.js { time_grid }
77
+ else
78
+ format.js { render :text => @time_entry.errors.full_messages.join(', '), :status => 403, :layout => false }
79
+ end
80
+ end
81
+ end
82
+
83
+ private
84
+
85
+ def get_user
86
+ render_403 unless User.current.logged?
87
+
88
+ if params[:user_id] && params[:user_id] != User.current.id.to_s
89
+ if User.current.admin?
90
+ @user = User.find(params[:user_id])
91
+ else
92
+ render_403
93
+ end
94
+ else
95
+ @user = User.current
96
+ end
97
+ end
98
+
99
+ def filters_for_view
100
+ StuffToDoFilter.new
101
+ end
102
+
103
+ def get_filters
104
+ return default_filters unless params[:filter]
105
+
106
+ id = params[:filter].split('-')[-1]
107
+
108
+ if params[:filter].match(/users/)
109
+ return User.find_by_id(id)
110
+ elsif params[:filter].match(/priorities/)
111
+ return Enumeration.find_by_id(id)
112
+ elsif params[:filter].match(/statuses/)
113
+ return IssueStatus.find_by_id(id)
114
+ elsif params[:filter].match(/projects/)
115
+ return Project.new
116
+ else
117
+ return nil
118
+ end
119
+ end
120
+
121
+ def default_filters
122
+ if StuffToDo.using_issues_as_items?
123
+ return @user
124
+ elsif StuffToDo.using_projects_as_items?
125
+ return Project.new
126
+ else
127
+ # Edge case
128
+ return { }
129
+ end
130
+ end
131
+
132
+ def get_time_grid
133
+ @date = parse_date_from_params
134
+ @calendar = Redmine::Helpers::Calendar.new(@date, current_language, :week)
135
+ @issues = User.current.time_grid_issues.visible.all(:order => "#{Issue.table_name}.id ASC")
136
+ @time_entry = TimeEntry.new
137
+ end
138
+
139
+ # Wrap saving the TimeEntry because TimeEntries from the time grid should
140
+ # require comments.
141
+ def save_time_entry_from_time_grid(time_entry)
142
+ time_entry.valid? # Run normal validations
143
+
144
+ # Additional validations
145
+ if time_entry.comments.blank?
146
+ time_entry.errors.add(:comments, :empty)
147
+ end
148
+
149
+ if time_entry.errors.empty? && User.current.allowed_to?(:log_time, time_entry.project)
150
+ return time_entry.save
151
+ else
152
+ return false
153
+ end
154
+ end
155
+
156
+ def parse_date_from_params
157
+ date = Date.parse(params[:date]) if params[:date]
158
+ date ||= Date.civil(params[:year].to_i, params[:month].to_i, params[:day].to_i) if params[:year] && params[:month] && params[:day]
159
+ date ||= Date.today
160
+ end
161
+ end
@@ -0,0 +1,88 @@
1
+ module StuffToDoHelper
2
+ def progress_bar_sum(collection, field, opts)
3
+ issues = remove_non_issues(collection)
4
+
5
+ total = issues.inject(0) {|sum, n| sum + n.read_attribute(field) }
6
+ divisor = issues.length
7
+ return if divisor.nil? || divisor == 0
8
+
9
+ progress_bar(total / divisor, opts)
10
+ end
11
+
12
+ def total_estimates(issues)
13
+ remove_non_issues(issues).collect(&:estimated_hours).compact.sum
14
+ end
15
+
16
+ def filter_options(filters, selected = nil)
17
+ html = options_for_select([[l(:stuff_to_do_label_filter_by), '']]) # Blank
18
+
19
+ filters.each do |filter_group, options|
20
+ next unless [:users, :priorities, :statuses, :projects].include?(filter_group)
21
+ if filter_group == :projects
22
+ # Projects only needs a single item
23
+ html << content_tag(:option,
24
+ filter_group.to_s.capitalize,
25
+ :value => 'projects',
26
+ :style => 'font-weight: bold')
27
+ else
28
+ html << content_tag(:optgroup,
29
+ options_for_select(options.collect { |item| [item.to_s, filter_group.to_s + '-' + item.id.to_s]}, selected),
30
+ :label => filter_group.to_s.capitalize )
31
+ end
32
+ end
33
+
34
+ return html
35
+ end
36
+
37
+ # Returns the stuff for a collection of StuffToDo items, removing anything
38
+ # that have been deleted.
39
+ def stuff_for(stuff_to_do_items)
40
+ return stuff_to_do_items.collect(&:stuff).compact
41
+ end
42
+
43
+ # Returns the issues for a collection of StuffToDo items, removing anything
44
+ # that have been deleted or isn't an Issue
45
+ def issues_for(stuff_to_do_items)
46
+ return remove_non_issues(stuff_to_do_items.collect(&:stuff).compact)
47
+ end
48
+
49
+ def remove_non_issues(stuff_to_do_items)
50
+ stuff_to_do_items.reject {|item| item.class != Issue }
51
+ end
52
+
53
+ def total_hours_for_user_on_day(issue, user, date)
54
+ total = issue.time_entries.inject(0.0) {|sum, time_entry|
55
+ if time_entry.user_id == user.id && time_entry.spent_on == date
56
+ sum += time_entry.hours
57
+ end
58
+ sum
59
+ }
60
+
61
+ total != 0.0 ? total : nil
62
+ end
63
+
64
+ def total_hours_for_issue_for_user(issue, user)
65
+ total = issue.time_entries.inject(0.0) {|sum, time_entry|
66
+ if time_entry.user_id == user.id
67
+ sum += time_entry.hours
68
+ end
69
+ sum
70
+ }
71
+ total
72
+ end
73
+
74
+ def total_hours_for_date(issues, user, date)
75
+ issues.collect {|issue| total_hours_for_user_on_day(issue, user, date)}.compact.sum
76
+ end
77
+
78
+ def total_hours_for_user(issues, user)
79
+ issues.collect {|issue| total_hours_for_issue_for_user(issue, user)}.compact.sum
80
+ end
81
+
82
+ # Redmine 0.8.x compatibility
83
+ def l_hours(hours)
84
+ hours = hours.to_f
85
+ l((hours < 2.0 ? :label_f_hour : :label_f_hour_plural), ("%.2f" % hours.to_f))
86
+ end unless Object.method_defined?('l_hours')
87
+
88
+ end
@@ -0,0 +1,208 @@
1
+ # StuffToDo relates a user to another object at a specific postition
2
+ # in a list.
3
+ #
4
+ # Supported objects:
5
+ # * Issue
6
+ # * Project
7
+ class StuffToDo < ActiveRecord::Base
8
+ USE = {
9
+ 'All' => '0',
10
+ 'Only Issues' => '1',
11
+ 'Only Projects' => '2'
12
+ }
13
+
14
+ belongs_to :stuff, :polymorphic => true
15
+ belongs_to :user
16
+ acts_as_list :scope => :user
17
+
18
+ named_scope :doing_now, lambda { |user|
19
+ {
20
+ :conditions => { :user_id => user.id },
21
+ :limit => 5,
22
+ :order => 'position ASC'
23
+ }
24
+ }
25
+
26
+ # TODO: Rails bug
27
+ #
28
+ # ActiveRecord ignores :offset if :limit isn't added also. But since we
29
+ # want all the records, we need to provide a limit that will include everything
30
+ #
31
+ # http://dev.rubyonrails.org/ticket/7257
32
+ #
33
+ named_scope :recommended, lambda { |user|
34
+ {
35
+ :conditions => { :user_id => user.id },
36
+ :limit => self.count,
37
+ :offset => 5,
38
+ :order => 'position ASC'
39
+ }
40
+ }
41
+
42
+ # Filters the issues that are available to be added for a user.
43
+ #
44
+ # A filter can be a record:
45
+ #
46
+ # * User - issues are assigned to this user
47
+ # * IssueStatus - issues with this status
48
+ # * IssuePriority - issues with this priority
49
+ #
50
+ def self.available(user, filter=nil)
51
+ return [] if filter.blank?
52
+
53
+ if filter.is_a?(Project)
54
+ potential_stuff_to_do = active_and_visible_projects.sort
55
+ else
56
+ potential_stuff_to_do = Issue.find(:all,
57
+ :include => [:status, :priority, :project],
58
+ :conditions => conditions_for_available(filter),
59
+ :order => "#{Issue.table_name}.created_on DESC")
60
+ end
61
+
62
+ stuff_to_do = StuffToDo.find(:all, :conditions => { :user_id => user.id }).collect(&:stuff)
63
+
64
+ return potential_stuff_to_do - stuff_to_do
65
+ end
66
+
67
+ def self.using_projects_as_items?
68
+ ['All', 'Only Projects'].include?(use_setting)
69
+ end
70
+
71
+ def self.using_issues_as_items?
72
+ ['All', 'Only Issues'].include?(use_setting)
73
+ end
74
+
75
+ # Callback used to destroy all StuffToDos when an object is removed and
76
+ # send an email if a user is below the What's Recommend threshold
77
+ def self.remove_associations_to(associated_object)
78
+ user_ids = []
79
+ associated_object.stuff_to_dos.each do |stuff_to_do|
80
+ user_ids << stuff_to_do.user_id if stuff_to_do.user_id
81
+ stuff_to_do.destroy
82
+ end
83
+
84
+ # Deliver an email for each user who is below the threshold
85
+ user_ids.uniq.each do |user_id|
86
+ count = self.count(:conditions => { :user_id => user_id})
87
+ threshold = Setting.plugin_stuff_to_do_plugin['threshold']
88
+
89
+ if threshold && threshold.to_i >= count
90
+ user = User.find_by_id(user_id)
91
+ StuffToDoMailer.deliver_recommended_below_threshold(user, count)
92
+ end
93
+ end
94
+
95
+ return true
96
+ end
97
+
98
+ # Destroys all +NextIssues+ on an +issue+ that are not the assigned to user
99
+ def self.remove_stale_assignments(issue)
100
+ if issue.assigned_to_id.nil?
101
+ self.destroy_all(['stuff_id = (?)', issue.id])
102
+ else
103
+ self.destroy_all(['stuff_id = (?) AND user_id NOT IN (?)',
104
+ issue.id,
105
+ issue.assigned_to_id])
106
+ end
107
+ end
108
+
109
+ # Reorders the list of StuffToDo items for +user+ to be in the order of
110
+ # +ids+. New StuffToDos will be created if needed and old
111
+ # StuffToDos will be removed if they are unassigned.
112
+ #
113
+ # Project based ids need to be prefixed with +project+
114
+ def self.reorder_list(user, ids)
115
+ ids ||= []
116
+ id_position_mapping = ids.to_hash
117
+
118
+ issue_ids = {}
119
+ project_ids = {}
120
+
121
+ id_position_mapping.each do |key,value|
122
+ if value.match(/project/i)
123
+ project_ids[key] = value.sub(/project/i,'').to_i
124
+ else
125
+ issue_ids[key] = value.to_i
126
+ end
127
+ end
128
+
129
+ reorder_issues(user, issue_ids)
130
+ reorder_projects(user, project_ids)
131
+ end
132
+
133
+ private
134
+
135
+ def self.reorder_issues(user, issue_ids)
136
+ reorder_items('Issue', user, issue_ids)
137
+ end
138
+
139
+ def self.reorder_projects(user, project_ids)
140
+ reorder_items('Project', user, project_ids)
141
+ end
142
+
143
+ def self.reorder_items(type, user, ids)
144
+ list = self.find_all_by_user_id_and_stuff_type(user.id, type)
145
+ stuff_to_dos_found = list.collect { |std| std.stuff_id.to_i }
146
+
147
+ remove_missing_records(user, stuff_to_dos_found, ids.values)
148
+
149
+ ids.each do |position, id|
150
+ if existing_list_position = stuff_to_dos_found.index(id.to_i)
151
+ position = position + 1 # acts_as_list is 1 based
152
+ stuff_to_do = list[existing_list_position]
153
+ stuff_to_do.insert_at(position)
154
+ else
155
+ # Not found in list, so create a new StuffToDo item
156
+ stuff_to_do = self.new
157
+ stuff_to_do.stuff_id = id
158
+ stuff_to_do.stuff_type = type
159
+ stuff_to_do.user_id = user.id
160
+
161
+ stuff_to_do.save # TODO: Check return
162
+
163
+ # Have to resave next_issue since acts_as_list automatically moves it
164
+ # to the bottom on create
165
+ stuff_to_do.insert_at(position + 1) # acts_as_list is 1 based
166
+ end
167
+ end
168
+
169
+ end
170
+
171
+ # Destroys saved records that are +ids_found_in_database+ but are
172
+ # not in +ids_to_use+
173
+ def self.remove_missing_records(user, ids_found_in_database, ids_to_use)
174
+ removed = ids_found_in_database - ids_to_use
175
+ removed.each do |id|
176
+ removed_stuff_to_do = self.find_by_user_id_and_stuff_id(user.id, id)
177
+ removed_stuff_to_do.destroy
178
+ end
179
+ end
180
+
181
+ # Redmine 0.8.x compatibility method.
182
+ def self.active_and_visible_projects
183
+ if ::Project.respond_to?(:active) && ::Project.respond_to?(:visible)
184
+ return ::Project.active.visible
185
+ else
186
+ return ::Project.find(:all, :conditions => Project.visible_by)
187
+ end
188
+ end
189
+
190
+ def self.use_setting
191
+ USE.index(Setting.plugin_stuff_to_do_plugin['use_as_stuff_to_do'])
192
+ end
193
+
194
+ def self.conditions_for_available(filter_by)
195
+ conditions_builder = ARCondition.new(["#{IssueStatus.table_name}.is_closed = ?", false ])
196
+ conditions_builder.add(["#{Project.table_name}.status = ?", Project::STATUS_ACTIVE])
197
+
198
+ case
199
+ when filter_by.is_a?(User)
200
+ conditions_builder.add(["assigned_to_id = ?", filter_by.id])
201
+ when filter_by.is_a?(IssueStatus), filter_by.is_a?(Enumeration)
202
+ table_name = filter_by.class.table_name
203
+ conditions_builder.add(["#{table_name}.id = (?)", filter_by.id])
204
+ end
205
+
206
+ conditions_builder.conditions
207
+ end
208
+ end