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