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 @@
<%= l(:label_comment) %> | +<%= sort_header_tag('hours', :caption => l(:field_hours)) %> ++ + +<% @entries.each do |entry| %> + | |||||
---|---|---|---|---|---|---|
<%= format_date(entry.spent_on) %> | +<%= entry.user.name %> | +<%= entry.activity.name %> | +
+ <% if entry.issue %>
+
+ <%= link_to "#{entry.issue.tracker.name} ##{entry.issue.id}", {:action => 'details', :issue_id => entry.issue } %>
+
+ <%= render :partial => "issues/tooltip", :locals => { :issue => entry.issue }%>
+
+
+ <% end %>
+ |
+<%=h entry.comment %> | +<%= entry.hours %> | +<%= link_to_if_authorized(l(:button_edit), {:controller => 'timelog', :action => 'edit', :id => entry}, :class => "icon icon-edit") if entry.user_id == @owner_id %> | +
<%= f.text_field :issue_id, :size => 6 %> <%= h("#{@time_entry.issue.tracker.name} ##{@time_entry.issue.id}: #{@time_entry.issue.subject}") if @time_entry.issue %>
+<%= f.text_field :spent_on, :size => 10, :required => true %><%= calendar_for('time_entry_spent_on') %>
+<%= f.text_field :hours, :size => 6, :required => true %>
+<%= f.text_field :comment, :size => 100 %>
+<%= f.select :activity_id, (@activities.collect {|p| [p.name, p.id]}), :required => true %>
+