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.
- data/COPYRIGHT.txt +18 -0
- data/CREDITS.txt +6 -0
- data/GPL.txt +339 -0
- data/README.rdoc +61 -0
- data/Rakefile +38 -0
- data/VERSION +1 -0
- data/app/controllers/stuff_to_do_controller.rb +161 -0
- data/app/helpers/stuff_to_do_helper.rb +88 -0
- data/app/models/stuff_to_do.rb +208 -0
- data/app/models/stuff_to_do_filter.rb +32 -0
- data/app/models/stuff_to_do_mailer.rb +16 -0
- data/app/views/settings/_stuff_to_do_settings.html.erb +27 -0
- data/app/views/stuff_to_do/_issue.html.erb +16 -0
- data/app/views/stuff_to_do/_item.html.erb +5 -0
- data/app/views/stuff_to_do/_left_panes.html.erb +51 -0
- data/app/views/stuff_to_do/_panes.html.erb +11 -0
- data/app/views/stuff_to_do/_project.html.erb +6 -0
- data/app/views/stuff_to_do/_right_panes.html.erb +25 -0
- data/app/views/stuff_to_do/_time_grid.html.erb +113 -0
- data/app/views/stuff_to_do/_time_grid_form.html.erb +32 -0
- data/app/views/stuff_to_do/index.html.erb +44 -0
- data/app/views/stuff_to_do_mailer/recommended_below_threshold.erb +3 -0
- data/app/views/stuff_to_do_mailer/recommended_below_threshold.text.html.rhtml +1 -0
- data/assets/images/b.png +0 -0
- data/assets/images/bl.png +0 -0
- data/assets/images/br.png +0 -0
- data/assets/images/closelabel.gif +0 -0
- data/assets/images/loading.gif +0 -0
- data/assets/images/tl.png +0 -0
- data/assets/images/tr.png +0 -0
- data/assets/javascripts/facebox.js +319 -0
- data/assets/javascripts/jquery-1.2.6.min.js +32 -0
- data/assets/javascripts/jquery-ui.js +2839 -0
- data/assets/javascripts/jquery.contextMenu.js +212 -0
- data/assets/javascripts/semantic.cache +15 -0
- data/assets/javascripts/stuff-to-do.js +270 -0
- data/assets/javascripts/ui/build.xml +24 -0
- data/assets/javascripts/ui/effects.blind.js +49 -0
- data/assets/javascripts/ui/effects.bounce.js +78 -0
- data/assets/javascripts/ui/effects.clip.js +54 -0
- data/assets/javascripts/ui/effects.core.js +510 -0
- data/assets/javascripts/ui/effects.drop.js +50 -0
- data/assets/javascripts/ui/effects.explode.js +79 -0
- data/assets/javascripts/ui/effects.fold.js +55 -0
- data/assets/javascripts/ui/effects.highlight.js +48 -0
- data/assets/javascripts/ui/effects.pulsate.js +55 -0
- data/assets/javascripts/ui/effects.scale.js +180 -0
- data/assets/javascripts/ui/effects.shake.js +57 -0
- data/assets/javascripts/ui/effects.slide.js +50 -0
- data/assets/javascripts/ui/effects.transfer.js +59 -0
- data/assets/javascripts/ui/i18n/ui.datepicker-ar.js +26 -0
- data/assets/javascripts/ui/i18n/ui.datepicker-bg.js +25 -0
- data/assets/javascripts/ui/i18n/ui.datepicker-ca.js +25 -0
- data/assets/javascripts/ui/i18n/ui.datepicker-cs.js +25 -0
- data/assets/javascripts/ui/i18n/ui.datepicker-da.js +25 -0
- data/assets/javascripts/ui/i18n/ui.datepicker-de.js +25 -0
- data/assets/javascripts/ui/i18n/ui.datepicker-eo.js +25 -0
- data/assets/javascripts/ui/i18n/ui.datepicker-es.js +25 -0
- data/assets/javascripts/ui/i18n/ui.datepicker-fa.js +25 -0
- data/assets/javascripts/ui/i18n/ui.datepicker-fi.js +25 -0
- data/assets/javascripts/ui/i18n/ui.datepicker-fr.js +25 -0
- data/assets/javascripts/ui/i18n/ui.datepicker-he.js +25 -0
- data/assets/javascripts/ui/i18n/ui.datepicker-hr.js +25 -0
- data/assets/javascripts/ui/i18n/ui.datepicker-hu.js +25 -0
- data/assets/javascripts/ui/i18n/ui.datepicker-hy.js +25 -0
- data/assets/javascripts/ui/i18n/ui.datepicker-id.js +25 -0
- data/assets/javascripts/ui/i18n/ui.datepicker-is.js +25 -0
- data/assets/javascripts/ui/i18n/ui.datepicker-it.js +25 -0
- data/assets/javascripts/ui/i18n/ui.datepicker-ja.js +26 -0
- data/assets/javascripts/ui/i18n/ui.datepicker-ko.js +25 -0
- data/assets/javascripts/ui/i18n/ui.datepicker-lt.js +25 -0
- data/assets/javascripts/ui/i18n/ui.datepicker-lv.js +25 -0
- data/assets/javascripts/ui/i18n/ui.datepicker-nl.js +25 -0
- data/assets/javascripts/ui/i18n/ui.datepicker-no.js +25 -0
- data/assets/javascripts/ui/i18n/ui.datepicker-pl.js +25 -0
- data/assets/javascripts/ui/i18n/ui.datepicker-pt-BR.js +25 -0
- data/assets/javascripts/ui/i18n/ui.datepicker-ro.js +25 -0
- data/assets/javascripts/ui/i18n/ui.datepicker-ru.js +25 -0
- data/assets/javascripts/ui/i18n/ui.datepicker-sk.js +25 -0
- data/assets/javascripts/ui/i18n/ui.datepicker-sl.js +26 -0
- data/assets/javascripts/ui/i18n/ui.datepicker-sq.js +25 -0
- data/assets/javascripts/ui/i18n/ui.datepicker-sv.js +25 -0
- data/assets/javascripts/ui/i18n/ui.datepicker-th.js +25 -0
- data/assets/javascripts/ui/i18n/ui.datepicker-tr.js +25 -0
- data/assets/javascripts/ui/i18n/ui.datepicker-uk.js +25 -0
- data/assets/javascripts/ui/i18n/ui.datepicker-zh-CN.js +25 -0
- data/assets/javascripts/ui/i18n/ui.datepicker-zh-TW.js +25 -0
- data/assets/javascripts/ui/svn.log +11 -0
- data/assets/javascripts/ui/ui.accordion.js +400 -0
- data/assets/javascripts/ui/ui.core.js +533 -0
- data/assets/javascripts/ui/ui.datepicker.js +1754 -0
- data/assets/javascripts/ui/ui.dialog.js +630 -0
- data/assets/javascripts/ui/ui.draggable.js +696 -0
- data/assets/javascripts/ui/ui.droppable.js +314 -0
- data/assets/javascripts/ui/ui.progressbar.js +114 -0
- data/assets/javascripts/ui/ui.resizable.js +805 -0
- data/assets/javascripts/ui/ui.selectable.js +266 -0
- data/assets/javascripts/ui/ui.slider.js +552 -0
- data/assets/javascripts/ui/ui.sortable.js +1012 -0
- data/assets/javascripts/ui/ui.tabs.js +572 -0
- data/assets/stylesheets/stuff_to_do.css +216 -0
- data/config/locales/bg.yml +18 -0
- data/config/locales/ca-fr.yml +18 -0
- data/config/locales/cs.yml +16 -0
- data/config/locales/da.yml +16 -0
- data/config/locales/de.yml +18 -0
- data/config/locales/en.yml +24 -0
- data/config/locales/es.yml +19 -0
- data/config/locales/fr.yml +17 -0
- data/config/locales/hu.yml +16 -0
- data/config/locales/it.yml +16 -0
- data/config/locales/ja.yml +18 -0
- data/config/locales/ko.yml +18 -0
- data/config/locales/lt.yml +18 -0
- data/config/locales/nl.yml +20 -0
- data/config/locales/pt-BR.yml +18 -0
- data/config/locales/ru.yml +19 -0
- data/config/locales/sv.yml +19 -0
- data/config/locales/tr.yml +18 -0
- data/config/routes.rb +3 -0
- data/init.rb +54 -0
- data/lang/bg.yml +17 -0
- data/lang/ca-fr.yml +17 -0
- data/lang/cs.yml +15 -0
- data/lang/da.yml +15 -0
- data/lang/de.yml +17 -0
- data/lang/en.yml +21 -0
- data/lang/es.yml +18 -0
- data/lang/fr.yml +16 -0
- data/lang/hu.yml +15 -0
- data/lang/it.yml +15 -0
- data/lang/ja.yml +17 -0
- data/lang/ko.yml +17 -0
- data/lang/lt.yml +17 -0
- data/lang/pt-br.yml +17 -0
- data/lang/ru.yml +15 -0
- data/lang/sv.yml +18 -0
- data/lang/tr.yml +17 -0
- data/lib/redmine_stuff_to_do/stuff_to_do_compatibility.rb +15 -0
- data/lib/stuff_to_do_array_patch.rb +8 -0
- data/lib/stuff_to_do_issue_patch.rb +57 -0
- data/lib/stuff_to_do_project_patch.rb +31 -0
- data/lib/stuff_to_do_user_patch.rb +10 -0
- data/rails/init.rb +1 -0
- data/spec/controllers/stuff_to_do_controller_add_to_time_grid_spec.rb +58 -0
- data/spec/controllers/stuff_to_do_controller_index_spec.rb +155 -0
- data/spec/controllers/stuff_to_do_controller_remove_from_time_grid_spec.rb +56 -0
- data/spec/controllers/stuff_to_do_controller_reorder_spec.rb +179 -0
- data/spec/controllers/stuff_to_do_controller_save_time_entries_spec.rb +56 -0
- data/spec/controllers/stuff_to_do_private_methods_spec.rb +82 -0
- data/spec/lib/stuff_to_do_issue_patch_spec.rb +60 -0
- data/spec/lib/stuff_to_do_project_patch_spec.rb +50 -0
- data/spec/lib/stuff_to_do_user_patch_spec.rb +8 -0
- data/spec/models/stuff_to_do_filter_spec.rb +3 -0
- data/spec/models/stuff_to_do_mailer_spec.rb +42 -0
- data/spec/models/stuff_to_do_spec.rb +426 -0
- data/spec/sanity_spec.rb +7 -0
- data/spec/spec_helper.rb +130 -0
- 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
|