backlog 0.23.1 → 0.24.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. data/History.txt +17 -4
  2. data/app/controllers/absences_controller.rb +64 -0
  3. data/app/controllers/periods_controller.rb +1 -1
  4. data/app/controllers/tasks_controller.rb +6 -4
  5. data/app/controllers/user_controller.rb +7 -0
  6. data/app/controllers/works_controller.rb +21 -16
  7. data/app/helpers/absences_helper.rb +2 -0
  8. data/app/models/absence.rb +14 -0
  9. data/app/models/task.rb +3 -2
  10. data/app/models/work.rb +33 -21
  11. data/app/views/absences/_form.rhtml +10 -0
  12. data/app/views/absences/edit.rhtml +9 -0
  13. data/app/views/absences/list.rhtml +27 -0
  14. data/app/views/absences/new.rhtml +8 -0
  15. data/app/views/absences/show.rhtml +8 -0
  16. data/app/views/layouts/absences.rhtml +17 -0
  17. data/app/views/tasks/_task.rhtml +1 -1
  18. data/app/views/tasks/edit.rhtml +1 -1
  19. data/app/views/user/edit.rhtml +36 -1
  20. data/app/views/works/_row.rhtml +19 -5
  21. data/app/views/works/_row_field.rhtml +14 -3
  22. data/app/views/works/daily_work_sheet.rhtml +81 -12
  23. data/app/views/works/list_excel.rhtml +1 -1
  24. data/app/views/works/timeliste.rhtml +3 -3
  25. data/app/views/works/update_row.rjs +3 -1
  26. data/app/views/works/update_time.rjs +2 -2
  27. data/app/views/works/weekly_work_sheet.rhtml +3 -3
  28. data/db/migrate/028_create_absences.rb +34 -0
  29. data/db/schema.rb +11 -2
  30. data/lang/en.yaml +4 -0
  31. data/lang/no.yaml +4 -0
  32. data/lib/change_column_null_migration_fix.rb +2 -0
  33. data/test/fixtures/absences.yml +11 -0
  34. data/test/fixtures/works.yml +10 -5
  35. data/test/functional/absences_controller_test.rb +93 -0
  36. data/test/functional/tasks_controller_test.rb +3 -2
  37. data/test/functional/works_controller_test.rb +33 -6
  38. data/test/test_helper.rb +3 -2
  39. data/test/unit/absence_test.rb +31 -0
  40. data/test/unit/work_test.rb +7 -0
  41. data/vendor/plugins/rails_time/MIT-LICENSE +20 -0
  42. data/vendor/plugins/rails_time/README +28 -0
  43. data/vendor/plugins/rails_time/init.rb +2 -0
  44. data/vendor/plugins/rails_time/lib/activerecord_time_extension.rb +28 -0
  45. data/vendor/plugins/rails_time/lib/time_of_day.rb +81 -0
  46. data/vendor/plugins/rails_time/test/database.yml +18 -0
  47. data/vendor/plugins/rails_time/test/rails_time_test.rb +122 -0
  48. data/vendor/plugins/rails_time/test/schema.rb +10 -0
  49. data/vendor/plugins/rails_time/test/test_helper.rb +17 -0
  50. metadata +28 -3
  51. data/app/views/works/daily_work_sheet_old.rhtml +0 -93
data/History.txt CHANGED
@@ -1,3 +1,16 @@
1
+ == 0.24.0 2008-02-28
2
+
3
+ === Features
4
+
5
+ * Allowed for marking dates as holiday or sick days in the daily work sheet.
6
+ * Show a summary of holidays and sick days in the user view.
7
+ * Added navigation with UP/DOWN keys in the daily work sheet.
8
+
9
+ === Fixes
10
+
11
+ * Made all work related views group and sort by start date and time.
12
+ * Do not try to register new work record in the daily work sheet unless work account name has been filled in.
13
+
1
14
  == 0.23.1 2008-02-26
2
15
 
3
16
  === Features
@@ -18,15 +31,15 @@
18
31
  * Changed sort order in daily work sheet from completion time to start time.
19
32
  * Changed to allow time format without separator: 0800 and 800 equal 08:00.
20
33
  * Minor cosmetic tweaks in the daily work sheet.
21
- * Fixed style sheet to remove the flickers in the sprint view.
34
+ * Fixed style sheet to remove the flicker in the sprint view.
22
35
 
23
36
  == 0.23.0 2008-02-25
24
37
 
25
38
  === Features
26
39
 
27
40
  * Improved input for daily_work_sheet
28
- * Allowed override for development and test database with new config file
29
- * Listed details for works without backlog in "Show Work Acoount" view.
41
+ * Allowed override for development and test database with new configuration file
42
+ * Listed details for works without backlog in "Show Work Account" view.
30
43
  * Added "List Works" view for Work Account.
31
44
  * Added filtering of tasks grabbed by other users in the sprint view.
32
45
 
@@ -43,7 +56,7 @@
43
56
  * Display blank field for hours in daily work sheet if it has not been filled in yet.
44
57
  * Always display row for new work record entry in daily work sheet.
45
58
  Earlier, a row for new input was only shown if the previous row was ended.
46
- * Removed message about missing database settings in main config file on startup since it is optional and deprecated.
59
+ * Removed message about missing database settings in main configuration file on startup since it is optional and deprecated.
47
60
  * Fixed error in class table inheritance library. Validations for the subclass were not called.
48
61
  * Added some Firewatir tests. EXPERIMENTAL!
49
62
  * Added explenation on how to set database connection parameters.
@@ -0,0 +1,64 @@
1
+ class AbsencesController < ApplicationController
2
+ def index
3
+ list
4
+ render :action => 'list'
5
+ end
6
+
7
+ # GETs should be safe (see http://www.w3.org/2001/tag/doc/whenToUseGet.html)
8
+ verify :method => :post, :only => [ :destroy, :create, :update ],
9
+ :redirect_to => { :action => :list }
10
+
11
+ def list
12
+ @absence_pages, @absences = paginate :absences, :per_page => 10
13
+ end
14
+
15
+ def show
16
+ @absence = Absence.find(params[:id])
17
+ end
18
+
19
+ def new
20
+ @absence = Absence.new
21
+ end
22
+
23
+ def create
24
+ @absence = Absence.find(:first, :conditions => {:on => params[:absence][:on], :user_id => current_user.id})
25
+ if @absence
26
+ if params[:absence][:reason] == ''
27
+ @absence.destroy
28
+ flash[:notice] = 'Absence was successfully destroyed.'
29
+ else
30
+ @absence.update_attributes(params[:absence])
31
+ flash[:notice] = 'Absence was successfully updated.'
32
+ end
33
+ back_or_redirect_to :action => 'list'
34
+ else
35
+ @absence = Absence.new(params[:absence])
36
+ @absence.user_id = current_user.id
37
+ if @absence.save
38
+ flash[:notice] = 'Absence was successfully created.'
39
+ back_or_redirect_to :action => 'list'
40
+ else
41
+ render :action => 'new'
42
+ end
43
+ end
44
+ end
45
+
46
+ def edit
47
+ @absence = Absence.find(params[:id])
48
+ end
49
+
50
+ def update
51
+ @absence = Absence.find(params[:id])
52
+ if @absence.update_attributes(params[:absence])
53
+ flash[:notice] = 'Absence was successfully updated.'
54
+ redirect_to :action => 'show', :id => @absence
55
+ else
56
+ render :action => 'edit'
57
+ end
58
+ end
59
+
60
+ def destroy
61
+ Absence.find(params[:id]).destroy
62
+ redirect_to :action => 'list'
63
+ end
64
+ end
@@ -107,7 +107,7 @@ class PeriodsController < ApplicationController
107
107
 
108
108
  def list_work
109
109
  if @period = Period.find_by_id(params[:id])
110
- @works = @period.tasks.map {|t| t.works}.flatten.sort_by {|w| [w.completed_at || Time.now.midnight, w.started_at]}
110
+ @works = @period.tasks.map {|t| t.works}.flatten.sort_by {|w| [w.completed_at || Time.now.midnight, w.started_on, w.start_time]}
111
111
  def @works.page_count; size / 25; end
112
112
  render :template => '/works/list'
113
113
  else
@@ -228,9 +228,10 @@ class TasksController < ApplicationController
228
228
  :action => :edit,
229
229
  :id => started_work,
230
230
  'work[task_id]' => @task.id,
231
- 'work[started_at]' => started_work.started_at.iso8601,
232
- 'work[completed_at]' => [next_quarter, started_work.started_at].max.iso8601,
233
- 'work[hours]' => "%0.2f" % ((next_quarter - started_work.started_at).to_f / 3600),
231
+ 'work[started_on]' => started_work.started_on.to_s,
232
+ 'work[start_time]' => started_work.start_time.to_s,
233
+ 'work[completed_at]' => [next_quarter, started_work.started_on.at(started_work.start_time)].max.iso8601,
234
+ 'work[hours]' => "%0.2f" % ((next_quarter - started_work.started_on.at(started_work.start_time)).to_f / 3600),
234
235
  'estimate[todo]' => @task.todo
235
236
  })
236
237
  else
@@ -239,7 +240,8 @@ class TasksController < ApplicationController
239
240
  :controller => 'works',
240
241
  :action => :new,
241
242
  'work[task_id]' => @task.id,
242
- 'work[started_at]' => Time.previous_quarter.iso8601,
243
+ 'work[started_on]' => Time.previous_quarter.to_date.to_s,
244
+ 'work[start_time]' => Time.previous_quarter.time_of_day.to_s,
243
245
  'work[completed_at]' => next_quarter.iso8601,
244
246
  'work[hours]' => "0.25",
245
247
  'estimate[todo]' => @task.todo
@@ -217,6 +217,13 @@ class UserController < ApplicationController
217
217
  @groups = Group.find(:all, :order => 'name')
218
218
  @periods = @user.periods
219
219
  @associates = User.find(:all) - @user.work_lock_subscribers - [current_user]
220
+ absences = Absence.find(:all, :conditions => ['user_id = ? AND "on" BETWEEN ? AND ?', current_user.id, Date.new(Date.today.year, 1, 1), Date.new(Date.today.year, 12, 31)])
221
+ @holidays = absences.select {|a| a.reason == 'HOLIDAY'}
222
+ @holidays.map! {|a| a.on}
223
+ @sick_days = absences.select {|a| a.reason == 'SICK'}
224
+ @sick_days.map! {|a| a.on}
225
+ @sick_days_with_doctors_leave = absences.select {|a| a.reason == 'SICK_WITH_DOCTOR'}
226
+ @sick_days_with_doctors_leave.map! {|a| a.on}
220
227
  case request.method
221
228
  when :get
222
229
  render
@@ -1,11 +1,11 @@
1
1
  class WorksController < ApplicationController
2
2
  in_place_edit_for :work, :hours
3
3
  in_place_edit_for :work, :invoice
4
- in_place_edit_for :work, :started_at_time
4
+ in_place_edit_for :work, :start_time
5
5
  in_place_edit_for :work, :completed_at_time
6
6
  skip_before_filter :populate_layout, :except => [:create, :destroy, :edit, :index, :list, :new, :show, :update]
7
7
  auto_complete_for :work, :description
8
-
8
+
9
9
  def index
10
10
  list
11
11
  render :action => 'list'
@@ -57,12 +57,13 @@ class WorksController < ApplicationController
57
57
  end
58
58
  params[:work][:customer_id] = customer.id
59
59
  end
60
+ convert_hours_param
61
+ @work = Work.new(params[:work])
62
+ @work.started_on ||= Date.today
63
+ @work.completed_at = Time.now unless @work.start_time || @work.completed_at
64
+ @work.user_id = current_user.id
60
65
  end
61
- convert_hours_param
62
- @work = Work.new(params[:work])
63
- @work.completed_at = Time.now unless @work.started_at || @work.completed_at
64
- @work.user_id = current_user.id
65
- if @work.save
66
+ if @work && @work.save
66
67
  flash[:notice] = 'Work was successfully created.'
67
68
  else
68
69
  new
@@ -76,7 +77,7 @@ class WorksController < ApplicationController
76
77
  end
77
78
 
78
79
  def edit
79
- @work = Work.find(params[:id])
80
+ @work ||= Work.find(params[:id])
80
81
  @work.attributes = params[:work]
81
82
  @estimate = Estimate.new(params[:estimate])
82
83
  @work_accounts = WorkAccount.find(:all)
@@ -100,26 +101,29 @@ class WorksController < ApplicationController
100
101
  end
101
102
 
102
103
  def update_row
103
- update_work
104
- flash.discard
105
- @next_field = params[:next_field] || 'work_account_name'
104
+ if update_work
105
+ #flash.discard
106
+ @next_field = params[:next_field] || 'work_account_name'
107
+ works = Work.find_work_for_day((@work.started_at || @work.completed_at).to_date)
108
+ @day_total = works.inject(BigDecimal('0')){|total,work|total+=work.hours}
109
+ else
110
+ @next_field = params[:field] || 'work_account_name'
111
+ end
106
112
  @work_accounts = WorkAccount.find(:all, :order => :name)
107
113
  @customers = Customer.find(:all, :order => :name)
108
- works = Work.find_work_for_day((@work.started_at || @work.completed_at).to_date)
109
- @day_total = works.inject(BigDecimal('0')){|total,work|total+=work.hours}
110
114
  end
111
115
 
112
116
  def update_time
113
117
  update_work
114
118
  flash.discard
115
- @field = params[:field] || 'started_at_time'
119
+ @field = params[:field] || 'start_time'
116
120
  end
117
121
 
118
122
  def update_work
119
123
  @work = Work.find(params[:id])
120
124
  convert_hours_param
121
125
  if @work.update_attributes(params[:work])
122
- if params[:work] && (params[:work][:started_at_time] || params[:work][:completed_at_time])
126
+ if params[:work] && (params[:work][:start_time] || params[:work][:completed_at_time])
123
127
  @work.calculate_hours!
124
128
  @work.save!
125
129
  end
@@ -154,6 +158,7 @@ class WorksController < ApplicationController
154
158
  @started_works = Task.find_started
155
159
  @work = Work.new(:started_at => Time.now, :completed_at => Time.now)
156
160
  @work_accounts = WorkAccount.find(:all, :order => :name)
161
+ @absence = Absence.find(:first, :conditions => {:on => @date})
157
162
  render :layout => 'wide'
158
163
  end
159
164
 
@@ -198,7 +203,7 @@ class WorksController < ApplicationController
198
203
 
199
204
  def auto_complete_for_work_customer_name
200
205
  @customers = Customer.find(:all,
201
- :conditions => [ 'LOWER(name) LIKE ?',
206
+ :conditions => [ 'LOWER(name) LIKE ?',
202
207
  '%' + params[:work][:customer_name].downcase + '%' ],
203
208
  :order => 'name ASC',
204
209
  :limit => 16)
@@ -0,0 +1,2 @@
1
+ module AbsencesHelper
2
+ end
@@ -0,0 +1,14 @@
1
+ class Absence < ActiveRecord::Base
2
+ validates_presence_of :user_id
3
+ validates_presence_of :on
4
+ validates_presence_of :reason
5
+
6
+ validates_uniqueness_of :on
7
+
8
+ def validate
9
+ if Work.exists? ['user_id = ? AND (started_on = ? OR (started_on <= ? AND completed_at IS NOT NULL AND completed_at >= ?))', user_id, on, on, on]
10
+ errors.add :on, "You have already registered work on this date."
11
+ end
12
+ end
13
+
14
+ end
data/app/models/task.rb CHANGED
@@ -350,13 +350,14 @@ class Task < ActiveRecord::Base
350
350
  # TODO (uwe): Only needed for rails 1.2.x branch. Remove when moving to 1.3 or 2.0.
351
351
  new_work.task = self
352
352
 
353
- new_work.started_at = Time.previous_quarter
353
+ new_work.started_on = Time.previous_quarter.to_date
354
+ new_work.start_time = Time.previous_quarter.time_of_day
354
355
  if current_user
355
356
  last_work = current_user.works.select {|w| w.completed_at}.last
356
357
  else
357
358
  last_work = Work.find(:first, :conditions => 'completed_at IS NOT NULL', :order => 'completed_at DESC')
358
359
  end
359
- if last_work && last_work.completed_at > new_work.started_at
360
+ if last_work && last_work.completed_at > new_work.started_on.at(new_work.start_time)
360
361
  new_work.started_at = last_work.completed_at
361
362
  end
362
363
 
data/app/models/work.rb CHANGED
@@ -9,12 +9,19 @@ class Work < ActiveRecord::Base
9
9
 
10
10
  validates_associated :task
11
11
  validates_presence_of :work_account
12
- validates_presence_of :started_at, :if => :track_times?
12
+ validates_presence_of :started_on
13
+ validates_presence_of :start_time, :if => :track_times?
13
14
 
14
15
  def validate
15
16
  errors.add(:work, "Work account is missing") unless work_account
17
+ if completed_at
18
+ absence_exists = Absence.exists? ['user_id = ? AND "on" BETWEEN ? AND ?', user_id, started_on, completed_at.to_date]
19
+ else
20
+ absence_exists = Absence.exists? :user_id => user_id, :on => started_on
21
+ end
22
+ errors.add :started_on, "You have already marked this date with an absence." if absence_exists
16
23
  end
17
-
24
+
18
25
  alias_method :old_work_account, :work_account
19
26
  def work_account
20
27
  self.old_work_account || (task && task.work_account)
@@ -33,11 +40,11 @@ class Work < ActiveRecord::Base
33
40
  # ]
34
41
  def self.works_for_week(year, week_no, user = current_user)
35
42
  first = Date.commercial(year, week_no, 1)
36
- last = first + 7
37
- works = find(:all, :conditions => "completed_at IS NOT NULL AND started_at BETWEEN '#{first.to_time.iso8601}' AND '#{last.to_time.iso8601}'", :order => 'started_at')
43
+ last = first + 6
44
+ works = find(:all, :conditions => "completed_at IS NOT NULL AND started_on BETWEEN '#{first}' AND '#{last}'", :order => 'started_on, start_time')
38
45
  length = 0
39
46
  works_per_day = (0..6).map do |day|
40
- works_for_day = works.select {|work| work.started_at.to_date == (first + day) && (work.user_id.nil? || (user && work.user_id == user.id)) }
47
+ works_for_day = works.select {|work| work.started_on == (first + day) && (work.user_id.nil? || (user && work.user_id == user.id)) }
41
48
  length = [length, works_for_day.length].max
42
49
  works_for_day
43
50
  end
@@ -54,10 +61,10 @@ class Work < ActiveRecord::Base
54
61
  def self.works_for_week_by_work_account(year, week_no, user = current_user)
55
62
  first_date = Date.commercial(year, week_no, 1)
56
63
  last_date = first_date + 6
57
- works = find(:all, :conditions => "user_id #{user ? " = #{user.id}" : "IS NULL"} AND completed_at IS NOT NULL AND completed_at BETWEEN '#{first_date.to_time.iso8601}' AND '#{(last_date+1).to_time.iso8601}'", :order => 'completed_at, started_at')
64
+ works = find(:all, :conditions => "user_id #{user ? " = #{user.id}" : "IS NULL"} AND started_on BETWEEN '#{first_date}' AND '#{last_date}'", :order => 'completed_at, started_on, start_time')
58
65
  result = {}
59
66
  works.each do |work|
60
- day_of_week = work.completed_at.to_date.cwday - 1
67
+ day_of_week = work.started_on.cwday - 1
61
68
  result[work.work_account] ||= []
62
69
  result[work.work_account][day_of_week] ||= BigDecimal('0')
63
70
  result[work.work_account][day_of_week] += work.hours
@@ -74,13 +81,13 @@ class Work < ActiveRecord::Base
74
81
  # }
75
82
  def self.work_totals_for_week(year, week_no, user = current_user)
76
83
  first = Date.commercial(year, week_no, 1)
77
- last = first + 7
78
- works = find(:all, :conditions => "completed_at IS NOT NULL AND completed_at BETWEEN '#{first.to_time.iso8601}' AND '#{last.to_time.iso8601}'", :order => 'completed_at, started_at')
84
+ last = first + 6
85
+ works = find(:all, :conditions => "completed_at IS NOT NULL AND started_on BETWEEN '#{first}' AND '#{last}'", :order => 'started_on, start_time, completed_at')
79
86
  totals_per_work_account = {}
80
87
  works.map{|w| w.work_account}.uniq.each do |work_account|
81
88
  totals_per_work_account[work_account.id] = [[], []]
82
89
  (0..6).each do |day|
83
- works_for_day = works.select {|work| (work.work_account == work_account) && (work.completed_at.to_date == (first + day)) && ((user.nil? && work.user_id.nil?) || (user && work.user_id == user.id)) }
90
+ works_for_day = works.select {|work| (work.work_account == work_account) && (work.started_on == (first + day)) && ((user.nil? && work.user_id.nil?) || (user && work.user_id == user.id)) }
84
91
  invoice_works_for_day = works_for_day.select {|work| work.invoice? }
85
92
  internal_works_for_day = works_for_day.select {|work| !work.invoice? }
86
93
 
@@ -97,28 +104,33 @@ class Work < ActiveRecord::Base
97
104
  end
98
105
 
99
106
  def self.find_work_for_day date
100
- Work.find(:all, :conditions => "(completed_at IS NULL OR completed_at BETWEEN '#{date}' AND '#{date+1}') AND user_id = #{current_user.id}",
101
- :order => 'started_at, completed_at')
107
+ Work.find(:all, :conditions => "started_on = '#{date}' AND user_id = #{current_user.id}",
108
+ :order => 'start_time, completed_at')
102
109
  end
103
110
 
104
111
  def started?
105
112
  completed_at.nil?
106
113
  end
107
114
 
108
- def started_at_time
109
- started_at && started_at.strftime('%H:%M')
115
+ def started_at
116
+ started_on && start_time && started_on.at(start_time)
110
117
  end
111
118
 
112
- def started_at_time=(new_value)
113
- new_value = '0:00' if new_value == ''
114
- raise "invalid time format: #{new_value}" unless new_value =~ /(\d{0,2}):?(\d{2})/
115
- new_hour, new_minutes = $1.to_i, $2.to_i
116
- t = started_at || Time.now
117
- self.started_at = Time.local(t.year, t.month, t.day, new_hour, new_minutes)
119
+ def started_at=(new_value)
120
+ case new_value
121
+ when String:
122
+ t = Time.parse(new_value)
123
+ when Time:
124
+ t = new_value
125
+ else
126
+ raise "Illegal argument: #{new_value.inspect}"
127
+ end
128
+ self.started_on = Date.new(t.year, t.month, t.day)
129
+ self.start_time = TimeOfDay.new(t.hour, t.min)
118
130
  end
119
131
 
120
132
  def completed_at_time
121
- completed_at && completed_at.strftime('%H:%M')
133
+ completed_at && completed_at.time_of_day
122
134
  end
123
135
 
124
136
  def completed_at_time=(new_value)
@@ -0,0 +1,10 @@
1
+ <%= error_messages_for 'absence' %>
2
+
3
+ <!--[form:absence]-->
4
+ <p><label for="absence_on">On</label><br/>
5
+ <%= date_select 'absence', 'on' %></p>
6
+
7
+ <p><label for="absence_reason">Reason</label><br/>
8
+ <%= text_field 'absence', 'reason' %></p>
9
+ <!--[eoform:absence]-->
10
+
@@ -0,0 +1,9 @@
1
+ <h1>Editing absence</h1>
2
+
3
+ <% form_tag :action => 'update', :id => @absence do %>
4
+ <%= render :partial => 'form' %>
5
+ <%= submit_tag 'Edit' %>
6
+ <% end %>
7
+
8
+ <%= link_to 'Show', :action => 'show', :id => @absence %> |
9
+ <%= link_to 'Back', :action => 'list' %>
@@ -0,0 +1,27 @@
1
+ <h1>Listing absences</h1>
2
+
3
+ <table>
4
+ <tr>
5
+ <% for column in Absence.content_columns %>
6
+ <th><%= column.human_name %></th>
7
+ <% end %>
8
+ </tr>
9
+
10
+ <% for absence in @absences %>
11
+ <tr>
12
+ <% for column in Absence.content_columns %>
13
+ <td><%=h absence.send(column.name) %></td>
14
+ <% end %>
15
+ <td><%= link_to 'Show', :action => 'show', :id => absence %></td>
16
+ <td><%= link_to 'Edit', :action => 'edit', :id => absence %></td>
17
+ <td><%= link_to 'Destroy', { :action => 'destroy', :id => absence }, :confirm => 'Are you sure?', :method => :post %></td>
18
+ </tr>
19
+ <% end %>
20
+ </table>
21
+
22
+ <%= link_to 'Previous page', { :page => @absence_pages.current.previous } if @absence_pages.current.previous %>
23
+ <%= link_to 'Next page', { :page => @absence_pages.current.next } if @absence_pages.current.next %>
24
+
25
+ <br />
26
+
27
+ <%= link_to 'New absence', :action => 'new' %>