diff --git a/app/controllers/timelog_controller.rb b/app/controllers/timelog_controller.rb index da323fbf1..bed6b1c07 100644 --- a/app/controllers/timelog_controller.rb +++ b/app/controllers/timelog_controller.rb @@ -1,13 +1,106 @@ +# redMine - project management software +# Copyright (C) 2006-2007 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + class TimelogController < ApplicationController layout 'base' before_filter :find_project before_filter :authorize, :only => :edit - before_filter :check_project_privacy, :only => :details + before_filter :check_project_privacy, :except => :edit helper :sort include SortHelper + def report + @available_criterias = { 'version' => {:sql => "#{Issue.table_name}.fixed_version_id", + :values => @project.versions, + :label => :label_version}, + 'category' => {:sql => "#{Issue.table_name}.category_id", + :values => @project.issue_categories, + :label => :field_category}, + 'member' => {:sql => "#{TimeEntry.table_name}.user_id", + :values => @project.users, + :label => :label_member}, + 'tracker' => {:sql => "#{Issue.table_name}.tracker_id", + :values => Tracker.find(:all), + :label => :label_tracker}, + 'activity' => {:sql => "#{TimeEntry.table_name}.activity_id", + :values => Enumeration::get_values('ACTI'), + :label => :label_activity} + } + + @criterias = params[:criterias] || [] + @criterias = @criterias.select{|criteria| @available_criterias.has_key? criteria} + @criterias.uniq! + + @columns = (params[:period] && %w(year month week).include?(params[:period])) ? params[:period] : 'month' + + if params[:date_from] + begin; @date_from = params[:date_from].to_date; rescue; end + end + if params[:date_to] + begin; @date_to = params[:date_to].to_date; rescue; end + end + @date_from ||= Date.civil(Date.today.year, 1, 1) + @date_to ||= Date.civil(Date.today.year, Date.today.month+1, 1) - 1 + + unless @criterias.empty? + sql_select = @criterias.collect{|criteria| @available_criterias[criteria][:sql] + " AS " + criteria}.join(', ') + sql_group_by = @criterias.collect{|criteria| @available_criterias[criteria][:sql]}.join(', ') + + sql = "SELECT #{sql_select}, tyear, tmonth, tweek, SUM(hours) AS hours" + sql << " FROM #{TimeEntry.table_name} LEFT JOIN #{Issue.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id" + sql << " WHERE spent_on BETWEEN '%s' AND '%s'" % [ActiveRecord::Base.connection.quoted_date(@date_from.to_time), ActiveRecord::Base.connection.quoted_date(@date_to.to_time)] + sql << " GROUP BY #{sql_group_by}, tyear, tmonth, tweek" + + @hours = ActiveRecord::Base.connection.select_all(sql) + + @hours.each do |row| + case @columns + when 'year' + row['year'] = row['tyear'] + when 'month' + row['month'] = "#{row['tyear']}-#{row['tmonth']}" + when 'week' + row['week'] = "#{row['tyear']}-#{row['tweek']}" + end + end + end + + @periods = [] + date_from = @date_from + # 100 columns max + while date_from < @date_to && @periods.length < 100 + case @columns + when 'year' + @periods << "#{date_from.year}" + date_from = date_from >> 12 + when 'month' + @periods << "#{date_from.year}-#{date_from.month}" + date_from = date_from >> 1 + when 'week' + @periods << "#{date_from.year}-#{date_from.cweek}" + date_from = date_from + 7 + end + end + + render :layout => false if request.xhr? + end + def details sort_init 'spent_on', 'desc' sort_update diff --git a/app/helpers/timelog_helper.rb b/app/helpers/timelog_helper.rb index 9054ccd18..22e4eba0b 100644 --- a/app/helpers/timelog_helper.rb +++ b/app/helpers/timelog_helper.rb @@ -1,2 +1,30 @@ +# redMine - project management software +# Copyright (C) 2006 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + module TimelogHelper + def select_hours(data, criteria, value) + data.select {|row| row[criteria] == value.to_s} + end + + def sum_hours(data) + sum = 0 + data.each do |row| + sum += row['hours'].to_f + end + sum + end end diff --git a/app/models/time_entry.rb b/app/models/time_entry.rb index c37f5dc86..905857073 100644 --- a/app/models/time_entry.rb +++ b/app/models/time_entry.rb @@ -1,3 +1,20 @@ +# redMine - project management software +# Copyright (C) 2006-2007 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + 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 diff --git a/app/views/reports/issue_report.rhtml b/app/views/reports/issue_report.rhtml index 3af936a5f..bf40e79ae 100644 --- a/app/views/reports/issue_report.rhtml +++ b/app/views/reports/issue_report.rhtml @@ -1,11 +1,13 @@ -

<%=l(:label_report_plural)%>

- <% if @total_hours %> -

<%= l(:label_spent_time) %>: -<%= link_to(lwr(:label_f_hour, @total_hours), {:controller => 'timelog', :action => 'details', :project_id => @project}, :class => 'icon icon-time') %> -

+
+<%= l(:label_spent_time) %>: <%= lwr(:label_f_hour, @total_hours) %>
+<%= link_to(l(:label_details), {:controller => 'timelog', :action => 'details', :project_id => @project}) %> | +<%= link_to(l(:label_report), {:controller => 'timelog', :action => 'report', :project_id => @project}) %> +
<% end %> +

<%=l(:label_report_plural)%>

+

<%=l(:field_tracker)%>  <%= link_to image_tag('zoom_in.png'), :detail => 'tracker' %>

<%= render :partial => 'simple', :locals => { :data => @issues_by_tracker, :field_name => "tracker_id", :rows => @trackers } %> diff --git a/app/views/timelog/_report_criteria.rhtml b/app/views/timelog/_report_criteria.rhtml new file mode 100644 index 000000000..e4f5fa39a --- /dev/null +++ b/app/views/timelog/_report_criteria.rhtml @@ -0,0 +1,17 @@ +<% @available_criterias[criterias[level]][:values].each do |value| %> + +<%= '' * level %> +<%= value.name %> +<%= '' * (criterias.length - level - 1) %> +<% hours_for_value = select_hours(hours, criterias[level], value.id) %> + <% @periods.each do |period| %> + <% sum = sum_hours(select_hours(hours_for_value, @columns, period.to_s)) %> + <%= sum > 0 ? sum : "-" %> + <% end %> + +<% if criterias.length > level+1 %> + <%= render(:partial => 'report_criteria', :locals => {:criterias => criterias, :hours => hours_for_value, :level => (level + 1)}) %> +<% end %> + +<% end %> +<% reset_cycle %> diff --git a/app/views/timelog/report.rhtml b/app/views/timelog/report.rhtml new file mode 100644 index 000000000..4fabfe3dd --- /dev/null +++ b/app/views/timelog/report.rhtml @@ -0,0 +1,52 @@ +

<%= l(:label_spent_time) %>

+ +<% form_remote_tag(:url => {:project_id => @project}, :update => 'content') do %> + <% @criterias.each do |criteria| %> + <%= hidden_field_tag 'criterias[]', criteria %> + <% end %> +

+ <%= l(:label_date_from) %>: <%= text_field_tag 'date_from', @date_from, :size => 10 %><%= calendar_for('date_from') %> +   + <%= l(:label_date_to) %>: <%= text_field_tag 'date_to', @date_to, :size => 10 %><%= calendar_for('date_to') %> +   + <%= l(:label_details) %>: + <%= select_tag 'period', options_for_select([[l(:label_year), 'year'], + [l(:label_month), 'month'], + [l(:label_week), 'week']], @columns) %> +   + <%= submit_tag l(:button_apply) %> + <%= link_to_remote l(:button_clear), {:url => {:project_id => @project}, :update => 'content'}, :class => 'icon icon-reload' %> +

+ + <% if @criterias.length < 3 %> +

<%= l(:button_add) %>: <%= select_tag('criterias[]', options_for_select([[]] + (@available_criterias.keys - @criterias).collect{|k| [l(@available_criterias[k][:label]), k]}), :onchange => "this.form.onsubmit();") %>

+ <% end %> + +
+ +<% unless @criterias.empty? %> + + + +<% @criterias.each do |criteria| %> + +<% end %> +<% @periods.each do |period| %> + +<% end %> + + + + +<%= render :partial => 'report_criteria', :locals => {:criterias => @criterias, :hours => @hours, :level => 0} %> + +
<%= l(@available_criterias[criteria][:label]) %><%= period %>
+<% end %> +<% 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 %> diff --git a/lang/bg.yml b/lang/bg.yml index a91059f79..3d632273e 100644 --- a/lang/bg.yml +++ b/lang/bg.yml @@ -286,7 +286,7 @@ label_none: никакви label_next: Следващ label_previous: Предишен label_used_by: Използва се от -label_details: Детайли... +label_details: Детайли label_add_note: Добавяне на бележка label_per_page: На страница label_calendar: Календар @@ -397,6 +397,11 @@ label_message_last: Last message label_message_new: New message label_reply_plural: Replies label_send_information: Send account information to the user +label_year: Year +label_month: Month +label_week: Week +label_date_from: From +label_date_to: To button_login: Вход button_submit: Изпращане diff --git a/lang/de.yml b/lang/de.yml index 973e4fd91..03e7002f6 100644 --- a/lang/de.yml +++ b/lang/de.yml @@ -286,7 +286,7 @@ label_none: kein label_next: Weiter label_previous: Zurück label_used_by: Benutzt von -label_details: Details... +label_details: Details label_add_note: Kommentar hinzufügen label_per_page: Pro Seite label_calendar: Kalender @@ -397,6 +397,11 @@ label_message_last: Last message label_message_new: New message label_reply_plural: Replies label_send_information: Send account information to the user +label_year: Year +label_month: Month +label_week: Week +label_date_from: From +label_date_to: To button_login: Einloggen button_submit: OK diff --git a/lang/en.yml b/lang/en.yml index 6c3c38f78..e24a2f53f 100644 --- a/lang/en.yml +++ b/lang/en.yml @@ -286,7 +286,7 @@ label_none: none label_next: Next label_previous: Previous label_used_by: Used by -label_details: Details... +label_details: Details label_add_note: Add a note label_per_page: Per page label_calendar: Calendar @@ -397,6 +397,11 @@ label_message_last: Last message label_message_new: New message label_reply_plural: Replies label_send_information: Send account information to the user +label_year: Year +label_month: Month +label_week: Week +label_date_from: From +label_date_to: To button_login: Login button_submit: Submit diff --git a/lang/es.yml b/lang/es.yml index 717c1bf6b..8c4a0ba90 100644 --- a/lang/es.yml +++ b/lang/es.yml @@ -286,7 +286,7 @@ label_none: ninguno label_next: Próximo label_previous: Precedente label_used_by: Utilizado por -label_details: Detalles... +label_details: Detalles label_add_note: Agregar una nota label_per_page: Por la página label_calendar: Calendario @@ -397,6 +397,11 @@ label_message_last: Last message label_message_new: New message label_reply_plural: Replies label_send_information: Send account information to the user +label_year: Year +label_month: Month +label_week: Week +label_date_from: From +label_date_to: To button_login: Conexión button_submit: Someter diff --git a/lang/fr.yml b/lang/fr.yml index bab251425..34449c2ce 100644 --- a/lang/fr.yml +++ b/lang/fr.yml @@ -286,7 +286,7 @@ label_none: aucun label_next: Suivant label_previous: Précédent label_used_by: Utilisé par -label_details: Détails... +label_details: Détails label_add_note: Ajouter une note label_per_page: Par page label_calendar: Calendrier @@ -397,6 +397,11 @@ label_message_last: Dernier message label_message_new: Nouveau message label_reply_plural: Réponses label_send_information: Envoyer les informations à l'utilisateur +label_year: Année +label_month: Mois +label_week: Semaine +label_date_from: Du +label_date_to: Au button_login: Connexion button_submit: Soumettre diff --git a/lang/it.yml b/lang/it.yml index 28d860af0..583c3b7c6 100644 --- a/lang/it.yml +++ b/lang/it.yml @@ -286,7 +286,7 @@ label_none: nessuno label_next: Successivo label_previous: Precedente label_used_by: Usato da -label_details: Dettagli... +label_details: Dettagli label_add_note: Aggiungi una nota label_per_page: Per pagina label_calendar: Calendario @@ -397,6 +397,11 @@ label_message_last: Last message label_message_new: New message label_reply_plural: Replies label_send_information: Send account information to the user +label_year: Year +label_month: Month +label_week: Week +label_date_from: From +label_date_to: To button_login: Login button_submit: Invia diff --git a/lang/ja.yml b/lang/ja.yml index 67f48d5ac..a7f4b54c3 100644 --- a/lang/ja.yml +++ b/lang/ja.yml @@ -287,7 +287,7 @@ label_none: なし label_next: 次 label_previous: 前 label_used_by: 使用中 -label_details: 詳細... +label_details: 詳細 label_add_note: 注記を追加 label_per_page: ページ毎 label_calendar: カレンダー @@ -398,6 +398,11 @@ label_message_last: 最新のメッセージ label_message_new: 新しいメッセージ label_reply_plural: 返答 label_send_information: アカウント情報をユーザに送信 +label_year: Year +label_month: Month +label_week: Week +label_date_from: From +label_date_to: To button_login: ログイン button_submit: 変更 diff --git a/lang/nl.yml b/lang/nl.yml index 1c115369d..34b6897ca 100644 --- a/lang/nl.yml +++ b/lang/nl.yml @@ -286,7 +286,7 @@ label_none: geen label_next: Volgende label_previous: Vorige label_used_by: Gebruikt door -label_details: Details... +label_details: Details label_add_note: Voeg een notitie toe label_per_page: Per pagina label_calendar: Kalender @@ -397,6 +397,11 @@ label_message_last: Laatste bericht label_message_new: Nieuw bericht label_reply_plural: Antwoorden label_send_information: Send account information to the user +label_year: Year +label_month: Month +label_week: Week +label_date_from: From +label_date_to: To button_login: Inloggen button_submit: Toevoegen diff --git a/lang/pt-br.yml b/lang/pt-br.yml index 1090c6907..c37027883 100644 --- a/lang/pt-br.yml +++ b/lang/pt-br.yml @@ -286,7 +286,7 @@ label_none: nenhum label_next: Proximo label_previous: Anterior label_used_by: Usado por -label_details: Detalhes... +label_details: Detalhes label_add_note: Adicionar nota label_per_page: Por pagina label_calendar: Calendario @@ -397,6 +397,11 @@ label_message_last: Last message label_message_new: New message label_reply_plural: Replies label_send_information: Send account information to the user +label_year: Year +label_month: Month +label_week: Week +label_date_from: From +label_date_to: To button_login: Login button_submit: Enviar diff --git a/lang/pt.yml b/lang/pt.yml index b1c02bc95..09236a347 100644 --- a/lang/pt.yml +++ b/lang/pt.yml @@ -286,7 +286,7 @@ label_none: nenhum label_next: Próximo label_previous: Anterior label_used_by: Usado por -label_details: Detalhes... +label_details: Detalhes label_add_note: Adicionar nota label_per_page: Por página label_calendar: Calendário @@ -397,6 +397,11 @@ label_message_last: Last message label_message_new: New message label_reply_plural: Replies label_send_information: Send account information to the user +label_year: Year +label_month: Month +label_week: Week +label_date_from: From +label_date_to: To button_login: Login button_submit: Enviar diff --git a/lang/sv.yml b/lang/sv.yml index f781db9cc..f56ef2ef2 100644 --- a/lang/sv.yml +++ b/lang/sv.yml @@ -286,7 +286,7 @@ label_none: inga label_next: Nästa label_previous: Föregående label_used_by: Använd av -label_details: Detaljer... +label_details: Detaljer label_add_note: Lägg till anteckning label_per_page: Per sida label_calendar: Kalender @@ -397,6 +397,11 @@ label_message_last: Last message label_message_new: New message label_reply_plural: Replies label_send_information: Send account information to the user +label_year: Year +label_month: Month +label_week: Week +label_date_from: From +label_date_to: To button_login: Logga in button_submit: Skicka diff --git a/lang/zh.yml b/lang/zh.yml index e317afa20..850c9b56e 100644 --- a/lang/zh.yml +++ b/lang/zh.yml @@ -289,7 +289,7 @@ label_none: 无 label_next: 下一个 label_previous: 上一个 label_used_by: 使用中 -label_details: 详情... +label_details: 详情 label_add_note: 添加说明 label_per_page: 每面 label_calendar: 日历 @@ -399,6 +399,11 @@ label_message_last: Last message label_message_new: New message label_reply_plural: Replies label_send_information: Send account information to the user +label_year: Year +label_month: Month +label_week: Week +label_date_from: From +label_date_to: To button_login: 登录 button_submit: 提交 diff --git a/test/fixtures/time_entries.yml b/test/fixtures/time_entries.yml new file mode 100644 index 000000000..4e4ff6896 --- /dev/null +++ b/test/fixtures/time_entries.yml @@ -0,0 +1,43 @@ +--- +time_entries_001: + created_on: 2007-03-23 12:54:18 +01:00 + tweek: 12 + tmonth: 3 + project_id: 1 + comments: My hours + updated_on: 2007-03-23 12:54:18 +01:00 + activity_id: 8 + spent_on: 2007-03-23 + issue_id: 1 + id: 1 + hours: 4.25 + user_id: 2 + tyear: 2007 +time_entries_002: + created_on: 2007-03-23 14:11:04 +01:00 + tweek: 12 + tmonth: 3 + project_id: 1 + comments: "" + updated_on: 2007-03-23 14:11:04 +01:00 + activity_id: 8 + spent_on: 2007-03-23 + issue_id: 1 + id: 2 + hours: 150.0 + user_id: 1 + tyear: 2007 +time_entries_003: + created_on: 2007-04-21 12:20:48 +02:00 + tweek: 16 + tmonth: 4 + project_id: 1 + comments: "" + updated_on: 2007-04-21 12:20:48 +02:00 + activity_id: 8 + spent_on: 2007-04-21 + issue_id: 2 + id: 3 + hours: 1.0 + user_id: 1 + tyear: 2007 diff --git a/test/functional/timelog_controller_test.rb b/test/functional/timelog_controller_test.rb new file mode 100644 index 000000000..62f1a2e7f --- /dev/null +++ b/test/functional/timelog_controller_test.rb @@ -0,0 +1,52 @@ +# redMine - project management software +# Copyright (C) 2006-2007 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.dirname(__FILE__) + '/../test_helper' +require 'timelog_controller' + +# Re-raise errors caught by the controller. +class TimelogController; def rescue_action(e) raise e end; end + +class TimelogControllerTest < Test::Unit::TestCase + fixtures :time_entries, :issues + + def setup + @controller = TimelogController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + end + + def test_report_no_criteria + get :report, :project_id => 1 + assert_response :success + assert_template 'report' + end + + def test_report_one_criteria + get :report, :project_id => 1, :period => "month", :date_from => "2007-01-01", :date_to => "2007-12-31", :criterias => ["member"] + assert_response :success + assert_template 'report' + assert_not_nil assigns(:hours) + end + + def test_report_two_criterias + get :report, :project_id => 1, :period => "week", :date_from => "2007-01-01", :date_to => "2007-12-31", :criterias => ["member", "activity"] + assert_response :success + assert_template 'report' + assert_not_nil assigns(:hours) + end +end