1
0
mirror of https://github.com/meineerde/redmine.git synced 2026-03-11 19:53:07 +00:00

time tracking initial commit (unstable)

git-svn-id: http://redmine.rubyforge.org/svn/branches/work@364 e93f8b46-1217-0410-a6f0-8f06a7374b81
This commit is contained in:
Jean-Philippe Lang 2007-03-23 10:38:09 +00:00
parent b23cb49756
commit 4a1bf9fe1b
14 changed files with 214 additions and 6 deletions

View File

@ -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

View File

@ -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.find :all
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

View File

@ -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

View File

@ -0,0 +1,2 @@
module TimelogHelper
end

View File

@ -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

View File

@ -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 => "issues.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

View File

@ -0,0 +1,29 @@
class TimeEntry < ActiveRecord::Base
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 && project!=issue.project
end
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

View File

@ -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 ? (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>

View File

@ -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>

View File

@ -0,0 +1,47 @@
<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 %>

View File

@ -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 %>

View File

@ -143,6 +143,9 @@ field_hide_mail: Cacher mon adresse mail
field_comment: Commentaire
field_url: URL
field_start_page: Page de démarrage
field_hours: Heures
field_activity: Activité
field_spent_on: Date
setting_app_title: Titre de l'application
setting_app_subtitle: Sous-titre de l'application
@ -328,6 +331,9 @@ label_wiki: Wiki
label_page_index: Index
label_current_version: Version actuelle
label_preview: Prévisualisation
label_spent_time: Temps passé
label_f_hour: %.2f heure
label_f_hour_plural: %.2f heures
button_login: Connexion
button_submit: Soumettre
@ -352,6 +358,7 @@ button_back: Retour
button_cancel: Annuler
button_activate: Activer
button_sort: Trier
button_log_time: Saisir temps
text_select_mail_notifications: Sélectionner les actions pour lesquelles la notification par mail doit être activée.
text_regexp_info: ex. ^[A-Z0-9]+$

BIN
time/public/images/time.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 993 B

View File

@ -154,6 +154,7 @@ vertical-align: middle;
.icon-attachment { background-image: url(../images/attachment.png); }
.icon-index { background-image: url(../images/index.png); }
.icon-history { background-image: url(../images/history.png); }
.icon-time { background-image: url(../images/time.png); }
.icon22-projects { background-image: url(../images/22x22/projects.png); }
.icon22-users { background-image: url(../images/22x22/users.png); }
@ -540,7 +541,7 @@ font-size: 1em;
/***** Tooltips ******/
.tooltip{position:relative;z-index:24;}
.tooltip:hover{z-index:25;color:#000;}
.tooltip span.tip{display: none}
.tooltip span.tip{display: none; text-align:left;}
div.tooltip:hover span.tip{
display:block;