stuff_to_do_plugin 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
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