summaryrefslogtreecommitdiffstats
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/controllers/reports_controller.rb1
-rw-r--r--app/controllers/timelog_controller.rb80
-rw-r--r--app/helpers/sort_helper.rb6
-rw-r--r--app/helpers/timelog_helper.rb2
-rw-r--r--app/models/enumeration.rb5
-rw-r--r--app/models/issue.rb6
-rw-r--r--app/models/permission.rb3
-rw-r--r--app/models/project.rb1
-rw-r--r--app/models/time_entry.rb33
-rw-r--r--app/views/issues/show.rhtml4
-rw-r--r--app/views/reports/issue_report.rhtml11
-rw-r--r--app/views/timelog/details.rhtml51
-rw-r--r--app/views/timelog/edit.rhtml23
13 files changed, 219 insertions, 7 deletions
diff --git a/app/controllers/reports_controller.rb b/app/controllers/reports_controller.rb
index fa00f7c30..ab648460b 100644
--- a/app/controllers/reports_controller.rb
+++ b/app/controllers/reports_controller.rb
@@ -57,6 +57,7 @@ class ReportsController < ApplicationController
issues_by_priority
issues_by_category
issues_by_author
+ @total_hours = @project.time_entries.sum(:hours)
render :template => "reports/issue_report"
end
end
diff --git a/app/controllers/timelog_controller.rb b/app/controllers/timelog_controller.rb
new file mode 100644
index 000000000..5902390d4
--- /dev/null
+++ b/app/controllers/timelog_controller.rb
@@ -0,0 +1,80 @@
+class TimelogController < ApplicationController
+ layout 'base'
+
+ before_filter :find_project
+ before_filter :authorize, :only => :edit
+ before_filter :check_project_privacy, :only => :details
+
+ helper :sort
+ include SortHelper
+
+ def details
+ sort_init 'spent_on', 'desc'
+ sort_update
+
+ @entries = (@issue ? @issue : @project).time_entries.find(:all, :include => [:activity, :user, {:issue => [:tracker, :assigned_to, :priority]}], :order => sort_clause)
+
+ @total_hours = @entries.inject(0) { |sum,entry| sum + entry.hours }
+ @owner_id = logged_in_user ? logged_in_user.id : 0
+
+ send_csv and return if 'csv' == params[:export]
+ render :action => 'details', :layout => false if request.xhr?
+ end
+
+ def edit
+ render_404 and return if @time_entry && @time_entry.user != logged_in_user
+ @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => logged_in_user, :spent_on => Date.today)
+ @time_entry.attributes = params[:time_entry]
+ if request.post? and @time_entry.save
+ flash[:notice] = l(:notice_successful_update)
+ redirect_to :action => 'details', :project_id => @time_entry.project, :issue_id => @time_entry.issue
+ return
+ end
+ @activities = Enumeration::get_values('ACTI')
+ end
+
+private
+ def find_project
+ if params[:id]
+ @time_entry = TimeEntry.find(params[:id])
+ @project = @time_entry.project
+ elsif params[:issue_id]
+ @issue = Issue.find(params[:issue_id])
+ @project = @issue.project
+ elsif params[:project_id]
+ @project = Project.find(params[:project_id])
+ else
+ render_404
+ return false
+ end
+ end
+
+ def send_csv
+ ic = Iconv.new(l(:general_csv_encoding), 'UTF-8')
+ export = StringIO.new
+ CSV::Writer.generate(export, l(:general_csv_separator)) do |csv|
+ # csv header fields
+ headers = [l(:field_spent_on),
+ l(:field_user),
+ l(:field_activity),
+ l(:field_issue),
+ l(:field_hours),
+ l(:field_comment)
+ ]
+ csv << headers.collect {|c| ic.iconv(c) }
+ # csv lines
+ @entries.each do |entry|
+ fields = [l_date(entry.spent_on),
+ entry.user.name,
+ entry.activity.name,
+ (entry.issue ? entry.issue.id : nil),
+ entry.hours,
+ entry.comment
+ ]
+ csv << fields.collect {|c| ic.iconv(c.to_s) }
+ end
+ end
+ export.rewind
+ send_data(export.read, :type => 'text/csv; header=present', :filename => 'export.csv')
+ end
+end
diff --git a/app/helpers/sort_helper.rb b/app/helpers/sort_helper.rb
index 300fbfe54..eac0d4d3f 100644
--- a/app/helpers/sort_helper.rb
+++ b/app/helpers/sort_helper.rb
@@ -107,10 +107,10 @@ module SortHelper
order = 'desc' # changed for desc order by default
end
caption = titleize(Inflector::humanize(column)) unless caption
- params = {:params => {:sort_key => column, :sort_order => order}}
+ #params = {:params => {:sort_key => column, :sort_order => order}}
link_to_remote(caption,
- {:update => "content", :url => { :sort_key => column, :sort_order => order}},
- {:href => url_for(:params => { :sort_key => column, :sort_order => order})}) +
+ {:update => "content", :url => params.update( :sort_key => column, :sort_order => order)},
+ {:href => url_for(:params => params.update(:sort_key => column, :sort_order => order))}) +
(icon ? nbsp(2) + image_tag(icon) : '')
end
diff --git a/app/helpers/timelog_helper.rb b/app/helpers/timelog_helper.rb
new file mode 100644
index 000000000..9054ccd18
--- /dev/null
+++ b/app/helpers/timelog_helper.rb
@@ -0,0 +1,2 @@
+module TimelogHelper
+end
diff --git a/app/models/enumeration.rb b/app/models/enumeration.rb
index 251f00fbe..de8526067 100644
--- a/app/models/enumeration.rb
+++ b/app/models/enumeration.rb
@@ -24,7 +24,8 @@ class Enumeration < ActiveRecord::Base
OPTIONS = {
"IPRI" => :enumeration_issue_priorities,
- "DCAT" => :enumeration_doc_categories
+ "DCAT" => :enumeration_doc_categories,
+ "ACTI" => :enumeration_activities
}.freeze
def self.get_values(option)
@@ -42,6 +43,8 @@ private
raise "Can't delete enumeration" if Issue.find(:first, :conditions => ["priority_id=?", self.id])
when "DCAT"
raise "Can't delete enumeration" if Document.find(:first, :conditions => ["category_id=?", self.id])
+ when "ACTI"
+ raise "Can't delete enumeration" if TimeEntry.find(:first, :conditions => ["activity_id=?", self.id])
end
end
end
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 140071872..dd512017f 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -28,7 +28,7 @@ class Issue < ActiveRecord::Base
has_many :journals, :as => :journalized, :dependent => :destroy
has_many :attachments, :as => :container, :dependent => :destroy
-
+ has_many :time_entries
has_many :custom_values, :dependent => :delete_all, :as => :customized
has_many :custom_fields, :through => :custom_values
@@ -91,6 +91,10 @@ class Issue < ActiveRecord::Base
self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
@current_journal
end
+
+ def spent_hours
+ @spent_hours ||= time_entries.sum(:hours) || 0
+ end
private
# Creates an history for the issue
diff --git a/app/models/permission.rb b/app/models/permission.rb
index 3ce40d116..23f8a5e91 100644
--- a/app/models/permission.rb
+++ b/app/models/permission.rb
@@ -30,7 +30,8 @@ class Permission < ActiveRecord::Base
1100 => :label_news_plural,
1200 => :label_document_plural,
1300 => :label_attachment_plural,
- 1400 => :label_repository
+ 1400 => :label_repository,
+ 1500 => :label_time_tracking
}.freeze
@@cached_perms_for_public = nil
diff --git a/app/models/project.rb b/app/models/project.rb
index 3579921b7..10730ed1e 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -21,6 +21,7 @@ class Project < ActiveRecord::Base
has_many :users, :through => :members
has_many :custom_values, :dependent => :delete_all, :as => :customized
has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker]
+ has_many :time_entries, :dependent => :delete_all
has_many :queries, :dependent => :delete_all
has_many :documents, :dependent => :destroy
has_many :news, :dependent => :delete_all, :include => :author
diff --git a/app/models/time_entry.rb b/app/models/time_entry.rb
new file mode 100644
index 000000000..4f2c561f5
--- /dev/null
+++ b/app/models/time_entry.rb
@@ -0,0 +1,33 @@
+class TimeEntry < ActiveRecord::Base
+ # could have used polymorphic association
+ # project association here allows easy loading of time entries at project level with one database trip
+ belongs_to :project
+ belongs_to :issue
+ belongs_to :user
+ belongs_to :activity, :class_name => 'Enumeration', :foreign_key => :activity_id
+
+ attr_protected :project_id, :user_id, :tyear, :tmonth, :tweek
+
+ validates_presence_of :user_id, :activity_id, :project_id, :hours, :spent_on
+ validates_numericality_of :hours, :allow_nil => true
+ validates_length_of :comment, :maximum => 255
+
+ def before_validation
+ self.project = issue.project if issue && project.nil?
+ end
+
+ def validate
+ errors.add :hours, :activerecord_error_invalid if hours && hours < 0
+ errors.add :project_id, :activerecord_error_invalid if project.nil?
+ errors.add :issue_id, :activerecord_error_invalid if (issue_id && !issue) || (issue && project!=issue.project)
+ end
+
+ # tyear, tmonth, tweek assigned where setting spent_on attributes
+ # these attributes make time aggregations easier
+ def spent_on=(date)
+ super
+ self.tyear = spent_on ? spent_on.year : nil
+ self.tmonth = spent_on ? spent_on.month : nil
+ self.tweek = spent_on ? spent_on.cweek : nil
+ end
+end
diff --git a/app/views/issues/show.rhtml b/app/views/issues/show.rhtml
index 43e959c1b..98e88671c 100644
--- a/app/views/issues/show.rhtml
+++ b/app/views/issues/show.rhtml
@@ -28,7 +28,8 @@
</tr>
<tr>
<td><b><%=l(:field_fixed_version)%> :</b></td><td><%= @issue.fixed_version ? @issue.fixed_version.name : "-" %></td>
- <td></td><td></td>
+ <td><b><%=l(:label_spent_time)%> :</b></td>
+ <td><%= @issue.spent_hours > 0 ? (link_to lwr(:label_f_hour, @issue.spent_hours), {:controller => 'timelog', :action => 'details', :issue_id => @issue}, :class => 'icon icon-time') : "-" %></td>
</tr>
<tr>
<% n = 0
@@ -51,6 +52,7 @@ end %>
<div class="contextual">
<%= link_to_if_authorized l(:button_edit), {:controller => 'issues', :action => 'edit', :id => @issue}, :class => 'icon icon-edit' %>
+<%= link_to_if_authorized l(:button_log_time), {:controller => 'timelog', :action => 'edit', :issue_id => @issue}, :class => 'icon icon-time' %>
<%= link_to_if_authorized l(:button_move), {:controller => 'projects', :action => 'move_issues', :id => @project, "issue_ids[]" => @issue.id }, :class => 'icon icon-move' %>
<%= link_to_if_authorized l(:button_delete), {:controller => 'issues', :action => 'destroy', :id => @issue}, :confirm => l(:text_are_you_sure), :method => :post, :class => 'icon icon-del' %>
</div>
diff --git a/app/views/reports/issue_report.rhtml b/app/views/reports/issue_report.rhtml
index 8ad45afcf..8f832f87e 100644
--- a/app/views/reports/issue_report.rhtml
+++ b/app/views/reports/issue_report.rhtml
@@ -1,5 +1,6 @@
<h2><%=l(:label_report_plural)%></h2>
+<div class="splitcontentleft">
<div class="contextual">
<%= link_to_if_authorized l(:label_query_new), {:controller => 'projects', :action => 'add_query', :id => @project}, :class => 'icon icon-add' %>
</div>
@@ -11,6 +12,16 @@
<li><%= link_to query.name, :controller => 'projects', :action => 'list_issues', :id => @project, :query_id => query %></li>
<% end %>
</ul>
+</div>
+<div class="splitcontentright">
+<% if @total_hours %>
+<h3 class="textright"><%= l(:label_spent_time) %>:
+<%= link_to(lwr(:label_f_hour, @total_hours), {:controller => 'timelog', :action => 'details', :project_id => @project}, :class => 'icon icon-time') %>
+</h3>
+<% end %>
+</div>
+
+<div class="clear"></div>
<div class="splitcontentleft">
<h3><%=l(:field_tracker)%>&nbsp;&nbsp;<%= link_to image_tag('zoom_in.png'), :detail => 'tracker' %></h3>
diff --git a/app/views/timelog/details.rhtml b/app/views/timelog/details.rhtml
new file mode 100644
index 000000000..f85eb0f5a
--- /dev/null
+++ b/app/views/timelog/details.rhtml
@@ -0,0 +1,51 @@
+<div class="contextual">
+<%= link_to_if_authorized l(:button_log_time), {:controller => 'timelog', :action => 'edit', :project_id => @project, :issue_id => @issue}, :class => 'icon icon-time' %>
+</div>
+
+<h2><%= l(:label_spent_time) %></h2>
+
+<h3><%= link_to(@project.name, {:action => 'details', :project_id => @project}) if @project %>
+<%= "/ " + link_to("#{@issue.tracker.name} ##{@issue.id}", {:action => 'details', :issue_id => @issue }) + ": #{h(@issue.subject)}" if @issue %></h3>
+
+<h3 class="textright"><%= l(:label_total) %>: <%= lwr(:label_f_hour, @total_hours) %></h3>
+
+<% unless @entries.empty? %>
+<table class="list">
+<thead>
+<%= sort_header_tag('spent_on', :caption => l(:label_date)) %>
+<%= sort_header_tag('user_id', :caption => l(:label_member)) %>
+<%= sort_header_tag('activity_id', :caption => l(:label_activity)) %>
+<%= sort_header_tag('issue_id', :caption => l(:label_issue)) %>
+<th><%= l(:label_comment) %></th>
+<%= sort_header_tag('hours', :caption => l(:field_hours)) %>
+<th></th>
+</thead>
+<tbody>
+<% @entries.each do |entry| %>
+<tr class="<%= cycle("odd", "even") %>">
+<td align="center"><%= format_date(entry.spent_on) %></td>
+<td align="center"><%= entry.user.name %></td>
+<td align="center"><%= entry.activity.name %></td>
+<td align="center">
+ <% if entry.issue %>
+ <div class="tooltip">
+ <%= link_to "#{entry.issue.tracker.name} ##{entry.issue.id}", {:action => 'details', :issue_id => entry.issue } %>
+ <span class="tip">
+ <%= render :partial => "issues/tooltip", :locals => { :issue => entry.issue }%>
+ </span>
+ </div>
+ <% end %>
+</td>
+<td><%=h entry.comment %></td>
+<td align="center"><strong><%= entry.hours %></strong></td>
+<td align="center"><%= link_to_if_authorized(l(:button_edit), {:controller => 'timelog', :action => 'edit', :id => entry}, :class => "icon icon-edit") if entry.user_id == @owner_id %></td>
+</tr>
+<% end %>
+</tbdoy>
+</table>
+
+<div class="contextual">
+<%= l(:label_export_to) %>
+<%= link_to 'CSV', params.update(:export => 'csv'), :class => 'icon icon-csv' %>
+</div>
+<% end %> \ No newline at end of file
diff --git a/app/views/timelog/edit.rhtml b/app/views/timelog/edit.rhtml
new file mode 100644
index 000000000..b826f7be7
--- /dev/null
+++ b/app/views/timelog/edit.rhtml
@@ -0,0 +1,23 @@
+<h2><%= l(:label_spent_time) %></h2>
+
+<% labelled_tabular_form_for :time_entry, @time_entry, :url => {:action => 'edit', :project_id => @time_entry.project} do |f| %>
+<%= error_messages_for 'time_entry' %>
+
+<div class="box">
+<p><%= f.text_field :issue_id, :size => 6 %> <em><%= h("#{@time_entry.issue.tracker.name} ##{@time_entry.issue.id}: #{@time_entry.issue.subject}") if @time_entry.issue %></em></p>
+<p><%= f.text_field :spent_on, :size => 10, :required => true %><%= calendar_for('time_entry_spent_on') %></p>
+<p><%= f.text_field :hours, :size => 6, :required => true %></p>
+<p><%= f.text_field :comment, :size => 100 %></p>
+<p><%= f.select :activity_id, (@activities.collect {|p| [p.name, p.id]}), :required => true %></p>
+</div>
+
+<%= submit_tag l(:button_save) %>
+
+<% end %>
+
+<% content_for :header_tags do %>
+<%= javascript_include_tag 'calendar/calendar' %>
+<%= javascript_include_tag "calendar/lang/calendar-#{current_language}.js" %>
+<%= javascript_include_tag 'calendar/calendar-setup' %>
+<%= stylesheet_link_tag 'calendar' %>
+<% end %> \ No newline at end of file