Index: db/migrate/095_add_start_and_end_time_to_time_entries.rb =================================================================== --- db/migrate/095_add_start_and_end_time_to_time_entries.rb (revision 0) +++ db/migrate/095_add_start_and_end_time_to_time_entries.rb (revision 0) @@ -0,0 +1,11 @@ +class AddStartAndEndTimeToTimeEntries < ActiveRecord::Migration + def self.up + add_column :time_entries, :start_time, :datetime + add_column :time_entries, :end_time, :datetime + end + + def self.down + remove_column :time_entries, :start_time + remove_column :time_entries, :end_time + end +end Index: db/migrate/096_allow_null_time_entry_hours.rb =================================================================== --- db/migrate/096_allow_null_time_entry_hours.rb (revision 0) +++ db/migrate/096_allow_null_time_entry_hours.rb (revision 0) @@ -0,0 +1,9 @@ +class AllowNullTimeEntryHours < ActiveRecord::Migration + def self.up + change_column :time_entries, :hours, :float, :null => true + end + + def self.down + # not needed + end +end Index: app/views/issues/_edit.rhtml =================================================================== --- app/views/issues/_edit.rhtml (revision 1669) +++ app/views/issues/_edit.rhtml (working copy) @@ -1,3 +1,6 @@ +<% content_for :header_tags do %> + <%= javascript_include_tag 'time' %> +<% end %> <% labelled_tabular_form_for :issue, @issue, :url => {:action => 'edit', :id => @issue}, :html => {:id => 'issue-form', @@ -18,11 +21,14 @@ <% if authorize_for('timelog', 'edit') %>
<%= l(:button_log_time) %> <% fields_for :time_entry, @time_entry, { :builder => TabularFormBuilder, :lang => current_language} do |time_entry| %> + <%= time_entry.hidden_field :id %>

<%= time_entry.text_field :hours, :size => 6, :label => :label_spent_time %> <%= l(:field_hours) %>

+

<%= time_entry.time_field :start_time %>

<%= time_entry.select :activity_id, activity_collection_for_select_options %>

+

<%= time_entry.time_field :end_time %>

<%= time_entry.text_field :comments, :size => 60 %>

<% end %> Index: app/views/issues/show.rhtml =================================================================== --- app/views/issues/show.rhtml (revision 1669) +++ app/views/issues/show.rhtml (working copy) @@ -33,7 +33,7 @@ <%=l(:field_category)%>:<%=h @issue.category ? @issue.category.name : "-" %> <% if User.current.allowed_to?(:view_time_entries, @project) %> <%=l(:label_spent_time)%>: - <%= @issue.spent_hours > 0 ? (link_to lwr(:label_f_hour, @issue.spent_hours), {:controller => 'timelog', :action => 'details', :project_id => @project, :issue_id => @issue}, :class => 'icon icon-time') : "-" %> + <%= spent_hours(@issue) %> <% end %> Index: app/views/timelog/_list.rhtml =================================================================== --- app/views/timelog/_list.rhtml (revision 1669) +++ app/views/timelog/_list.rhtml (working copy) @@ -2,12 +2,14 @@ <%= sort_header_tag('spent_on', :caption => l(:label_date), :default_order => 'desc') %> +<%= sort_header_tag('start_time', :caption => l(:field_start_time), :default_order => 'desc') %> +<%= sort_header_tag('end_time', :caption => l(:field_end_time), :default_order => 'desc') %> +<%= sort_header_tag('hours', :caption => l(:field_hours)) %> <%= sort_header_tag('user_id', :caption => l(:label_member)) %> <%= sort_header_tag('activity_id', :caption => l(:label_activity)) %> <%= sort_header_tag("#{Project.table_name}.name", :caption => l(:label_project)) %> <%= sort_header_tag('issue_id', :caption => l(:label_issue), :default_order => 'desc') %> <%= l(:field_comments) %> -<%= sort_header_tag('hours', :caption => l(:field_hours)) %> @@ -15,6 +17,9 @@ <% entries.each do |entry| -%> "> <%= format_date(entry.spent_on) %> +<%= format_time(entry.start_time) %> +<%= format_time(entry.end_time) %> +<%= hours(entry) %> <%=h entry.user %> <%=h entry.activity %> <%=h entry.project %> @@ -24,7 +29,6 @@ <% end -%> <%=h entry.comments %> -<%= html_hours("%.2f" % entry.hours) %> <% if entry.editable_by?(User.current) -%> <%= link_to image_tag('edit.png'), {:controller => 'timelog', :action => 'edit', :id => entry}, Index: app/views/timelog/edit.rhtml =================================================================== --- app/views/timelog/edit.rhtml (revision 1669) +++ app/views/timelog/edit.rhtml (working copy) @@ -3,11 +3,24 @@ <% labelled_tabular_form_for :time_entry, @time_entry, :url => {:action => 'edit', :project_id => @time_entry.project} do |f| %> <%= error_messages_for 'time_entry' %> <%= back_url_hidden_field_tag %> +<% content_for :header_tags do %> + <%= javascript_include_tag 'time' %> +<% end %> +<% content_for :header_tags do %> + +<% end %> +

<%= 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.time_field :start_time %>

+

<%= f.time_field :end_time %>

+

<%= f.text_field :hours, :size => 6 %> <%= l(:text_clear_to_recalculate_time_by_range) %>

<%= f.text_field :comments, :size => 100 %>

<%= f.select :activity_id, activity_collection_for_select_options, :required => true %>

Index: lib/tabular_form_builder.rb =================================================================== --- lib/tabular_form_builder.rb (revision 1669) +++ lib/tabular_form_builder.rb (working copy) @@ -40,6 +40,17 @@ END_SRC class_eval src, __FILE__, __LINE__ end + + def time_field(field) + value = nil + value = @object.send(field) if @object + value = value.strftime('%Y-%m-%d %H:%M') if value + result = text_field field, :size => 15, :value => value + result += (' ') % "#{@object_name}_#{field}" + result + end def select(field, choices, options = {}, html_options = {}) label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "") Index: app/controllers/application.rb =================================================================== --- app/controllers/application.rb (revision 1669) +++ app/controllers/application.rb (working copy) @@ -219,4 +219,9 @@ def filename_for_content_disposition(name) request.env['HTTP_USER_AGENT'] =~ %r{MSIE} ? ERB::Util.url_encode(name) : name end + + def url_path(url_parameters) + rs = ::ActionController::Routing::Routes + rs.generate url_parameters + end end Index: app/controllers/issues_controller.rb =================================================================== --- app/controllers/issues_controller.rb (revision 1669) +++ app/controllers/issues_controller.rb (working copy) @@ -101,7 +101,7 @@ @allowed_statuses = @issue.new_statuses_allowed_to(User.current) @edit_allowed = User.current.allowed_to?(:edit_issues, @project) @priorities = Enumeration::get_values('IPRI') - @time_entry = TimeEntry.new + @time_entry = @issue.time_entry_in_progress(User.current) || TimeEntry.new respond_to do |format| format.html { render :template => 'issues/show.rhtml' } format.atom { render :action => 'changes', :layout => false, :content_type => 'application/atom+xml' } @@ -172,7 +172,10 @@ end if request.post? - @time_entry = TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today) + @time_entry = TimeEntry.find_by_id(params[:time_entry][:id]) if params[:time_entry] + @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, + :user => User.current, :spent_on => Date.today) + @time_entry.attributes = params[:time_entry] attachments = attach_files(@issue, params[:attachments]) attachments.each {|a| journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)} Index: app/controllers/timelog_controller.rb =================================================================== --- app/controllers/timelog_controller.rb (revision 1669) +++ app/controllers/timelog_controller.rb (working copy) @@ -175,11 +175,39 @@ end def edit + return if redirect_if_in_progress + render_403 and return if @time_entry && !@time_entry.editable_by?(User.current) @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today) @time_entry.attributes = params[:time_entry] + if !@time_entry.hours + if !@time_entry.start_time + @activate_field = 'time_entry_start_time' + else + @activate_field = 'time_entry_end_time' + end + end + + url_writer = lambda do |entry| + " :timelog, :action => :edit, + :id => entry.id)}\">##{entry.issue_id}-#{entry.id}" + end + if request.post? and @time_entry.save - flash[:notice] = l(:notice_successful_update) + intersecting = @time_entry.find_intersecting_entries + logger.debug "intersecting = #{intersecting.inspect}" + msg = l(:notice_successful_update) + if !intersecting.empty? + + list = lwr(:text_time_entry_intersecting_notice_entry, + intersecting.size) + ' ' + intersecting. + map { |entry| url_writer.call(entry) }. + to_sentence(:skip_last_comma => true, :connector => l(:text_and)) + + msg += ' ' + l(:text_time_entry_intersecting_notice, + url_writer.call(@time_entry), list) + end + flash[:notice] = msg redirect_to(params[:back_url].blank? ? {:action => 'details', :project_id => @time_entry.project} : params[:back_url]) return end @@ -258,4 +286,17 @@ @from ||= (TimeEntry.minimum(:spent_on, :include => :project, :conditions => @project.project_condition(Setting.display_subprojects_issues?)) || Date.today) - 1 @to ||= (TimeEntry.maximum(:spent_on, :include => :project, :conditions => @project.project_condition(Setting.display_subprojects_issues?)) || Date.today) end + + def redirect_if_in_progress + if !@time_entry && @issue + in_progress_entry = @issue.time_entry_in_progress(User.current) + if in_progress_entry + #in order to avoid :id form parameter and not complicate :find_project filter + redirect_to(:controller => 'timelog', :action => 'edit', + :id => in_progress_entry) + return true + end + end + false + end end Index: app/helpers/application_helper.rb =================================================================== --- app/helpers/application_helper.rb (revision 1669) +++ app/helpers/application_helper.rb (working copy) @@ -70,7 +70,7 @@ @date_format ||= (Setting.date_format.blank? || Setting.date_format.size < 2 ? l(:general_fmt_date) : Setting.date_format) date.strftime(@date_format) end - + def format_time(time, include_date = true) return nil unless time time = time.to_time if time.is_a?(String) @@ -78,7 +78,8 @@ local = zone ? time.in_time_zone(zone) : (time.utc? ? time.utc_to_local : time) @date_format ||= (Setting.date_format.blank? || Setting.date_format.size < 2 ? l(:general_fmt_date) : Setting.date_format) @time_format ||= (Setting.time_format.blank? ? l(:general_fmt_time) : Setting.time_format) - include_date ? local.strftime("#{@date_format} #{@time_format}") : local.strftime(@time_format) + format = include_date ? "#{@date_format} #{@time_format}" : @time_format + local.strftime(format) end # Truncates and returns the string as a single line Index: app/helpers/issues_helper.rb =================================================================== --- app/helpers/issues_helper.rb (revision 1669) +++ app/helpers/issues_helper.rb (working copy) @@ -181,4 +181,11 @@ export.rewind export end + + def spent_hours(issue) + return '-' if issue.time_entries.empty? + spent_hours = lwr(:label_f_hour, issue.spent_hours) + spent_hours += " (#{l(:text_in_progress)})" if issue.time_entry_in_progress + link_to spent_hours, {:controller => 'timelog', :action => 'details', :project_id => issue.project, :issue_id => issue}, :class => 'icon icon-time' + end end Index: app/helpers/timelog_helper.rb =================================================================== --- app/helpers/timelog_helper.rb (revision 1669) +++ app/helpers/timelog_helper.rb (working copy) @@ -36,6 +36,11 @@ sum end + def hours(entry) + return '[%s]' % l(:text_in_progress) if !entry.hours + html_hours("%.2f" % entry.hours) + end + def options_for_period_select(value) options_for_select([[l(:label_all_time), 'all'], [l(:label_today), 'today'], Index: app/models/issue.rb =================================================================== --- app/models/issue.rb (revision 1669) +++ app/models/issue.rb (working copy) @@ -28,6 +28,7 @@ has_many :journals, :as => :journalized, :dependent => :destroy has_many :attachments, :as => :container, :dependent => :destroy has_many :time_entries, :dependent => :delete_all + has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC" has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all @@ -251,4 +252,9 @@ def to_s "#{tracker} ##{id}: #{subject}" end + + def time_entry_in_progress(user = nil) + TimeEntry.find_by_issue_id(self.id, + :conditions => 'start_time IS NOT NULL and hours IS NULL' + (user ? " and user_id = #{user.id}" : '')) + end end Index: app/models/time_entry.rb =================================================================== --- app/models/time_entry.rb (revision 1669) +++ app/models/time_entry.rb (working copy) @@ -25,12 +25,17 @@ attr_protected :project_id, :user_id, :tyear, :tmonth, :tweek - acts_as_event :title => Proc.new {|o| "#{o.user}: #{lwr(:label_f_hour, o.hours)} (#{(o.issue || o.project).event_title})"}, - :url => Proc.new {|o| {:controller => 'timelog', :action => 'details', :project_id => o.project}}, + title_proc = Proc.new do |entry| + hours = entry.hours ? lwr(:label_f_hour, entry.hours) : "[#{l(:text_in_progress)}]" + "#{entry.user}: #{hours} (#{(entry.issue || entry.project).event_title})" + end + + acts_as_event :title => title_proc, + :url => Proc.new {|entry| {:controller => 'timelog', :action => 'details', :project_id => entry.project}}, :author => :user, :description => :comments - validates_presence_of :user_id, :activity_id, :project_id, :hours, :spent_on + validates_presence_of :user_id, :activity_id, :project_id, :spent_on validates_numericality_of :hours, :allow_nil => true validates_length_of :comments, :maximum => 255, :allow_nil => true @@ -48,6 +53,18 @@ def validate errors.add :hours, :activerecord_error_invalid if hours && (hours < 0 || hours >= 1000) + + if !start_time && !hours + #rather verbose, but l() always translate to English here for some reason + errors.add :hours, ll(User.current.language, + :activerecord_error_field_must_be_set_if_other_is_not, + ll(User.current.language, :field_start_time)) + + errors.add :start_time, ll(User.current.language, + :activerecord_error_field_must_be_set_if_other_is_not, + ll(User.current.language, :field_hours)) + end + 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 @@ -75,4 +92,25 @@ yield end end + + def before_save + if !hours && start_time && end_time + self.hours = (end_time - start_time) / 3600 + end + end + + def find_intersecting_entries + params = {:start_time => start_time, :end_time => end_time, :id => id} + + self.class.find_all_by_user_id(user_id, :conditions => + ["(" + + #this entry's start time or end time is between other's start_time and end_time + "start_time < :start_time and :start_time < end_time OR " + + "start_time < :end_time and :end_time < end_time OR " + + #other's entry's start time or end time is between this entry's start_time and end_time + "start_time > :start_time and start_time < :end_time OR " + + "end_time > :start_time and end_time < :end_time" + + ")" + + "and id <> :id", params]) + end end Index: lang/bg.yml =================================================================== --- lang/bg.yml (revision 1669) +++ lang/bg.yml (working copy) @@ -632,3 +632,13 @@ setting_mail_handler_api_enabled: Enable WS for incoming emails setting_mail_handler_api_key: API key text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them." +field_start_time: Start Time +field_end_time: End Time +text_in_progress: in progress +activerecord_error_field_must_be_set_if_other_is_not: must be set if %s is not +text_clear_to_recalculate_time_by_range: Clear to recalculate by start/end time +text_and: and +text_time_entry_intersecting_notice: Notice that this timelog entry (%s) intersects with %s. +text_time_entry_intersecting_notice_entry: entry +text_time_entry_intersecting_notice_entry_plural: entries +button_now: Now Index: lang/cs.yml =================================================================== --- lang/cs.yml (revision 1669) +++ lang/cs.yml (working copy) @@ -637,3 +637,13 @@ setting_mail_handler_api_enabled: Enable WS for incoming emails setting_mail_handler_api_key: API key text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them." +field_start_time: Start Time +field_end_time: End Time +text_in_progress: in progress +activerecord_error_field_must_be_set_if_other_is_not: must be set if %s is not +text_clear_to_recalculate_time_by_range: Clear to recalculate by start/end time +text_and: and +text_time_entry_intersecting_notice: Notice that this timelog entry (%s) intersects with %s. +text_time_entry_intersecting_notice_entry: entry +text_time_entry_intersecting_notice_entry_plural: entries +button_now: Now Index: lang/da.yml =================================================================== --- lang/da.yml (revision 1669) +++ lang/da.yml (working copy) @@ -634,3 +634,13 @@ setting_mail_handler_api_enabled: Enable WS for incoming emails setting_mail_handler_api_key: API key text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them." +field_start_time: Start Time +field_end_time: End Time +text_in_progress: in progress +activerecord_error_field_must_be_set_if_other_is_not: must be set if %s is not +text_clear_to_recalculate_time_by_range: Clear to recalculate by start/end time +text_and: and +text_time_entry_intersecting_notice: Notice that this timelog entry (%s) intersects with %s. +text_time_entry_intersecting_notice_entry: entry +text_time_entry_intersecting_notice_entry_plural: entries +button_now: Now Index: lang/de.yml =================================================================== --- lang/de.yml (revision 1669) +++ lang/de.yml (working copy) @@ -633,3 +633,13 @@ setting_mail_handler_api_enabled: Enable WS for incoming emails setting_mail_handler_api_key: API key text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them." +field_start_time: Start Time +field_end_time: End Time +text_in_progress: in progress +activerecord_error_field_must_be_set_if_other_is_not: must be set if %s is not +text_clear_to_recalculate_time_by_range: Clear to recalculate by start/end time +text_and: and +text_time_entry_intersecting_notice: Notice that this timelog entry (%s) intersects with %s. +text_time_entry_intersecting_notice_entry: entry +text_time_entry_intersecting_notice_entry_plural: entries +button_now: Now Index: lang/en.yml =================================================================== --- lang/en.yml (revision 1669) +++ lang/en.yml (working copy) @@ -170,6 +170,8 @@ field_hours: Hours field_activity: Activity field_spent_on: Date +field_start_time: Start Time +field_end_time: End Time field_identifier: Identifier field_is_filter: Used as a filter field_issue_to_id: Related issue @@ -558,6 +560,7 @@ button_annotate: Annotate button_update: Update button_configure: Configure +button_now: Now status_active: active status_registered: registered @@ -633,3 +636,10 @@ enumeration_issue_priorities: Issue priorities enumeration_doc_categories: Document categories enumeration_activities: Activities (time tracking) +text_in_progress: in progress +activerecord_error_field_must_be_set_if_other_is_not: must be set if %s is not +text_clear_to_recalculate_time_by_range: Clear to recalculate by start/end time +text_and: and +text_time_entry_intersecting_notice: Notice that this timelog entry (%s) intersects with %s. +text_time_entry_intersecting_notice_entry: entry +text_time_entry_intersecting_notice_entry_plural: entries Index: lang/es.yml =================================================================== --- lang/es.yml (revision 1669) +++ lang/es.yml (working copy) @@ -635,3 +635,13 @@ setting_mail_handler_api_enabled: Enable WS for incoming emails setting_mail_handler_api_key: API key text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them." +field_start_time: Start Time +field_end_time: End Time +text_in_progress: in progress +activerecord_error_field_must_be_set_if_other_is_not: must be set if %s is not +text_clear_to_recalculate_time_by_range: Clear to recalculate by start/end time +text_and: and +text_time_entry_intersecting_notice: Notice that this timelog entry (%s) intersects with %s. +text_time_entry_intersecting_notice_entry: entry +text_time_entry_intersecting_notice_entry_plural: entries +button_now: Now Index: lang/fi.yml =================================================================== --- lang/fi.yml (revision 1669) +++ lang/fi.yml (working copy) @@ -632,3 +632,13 @@ setting_mail_handler_api_enabled: Enable WS for incoming emails setting_mail_handler_api_key: API key text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them." +field_start_time: Start Time +field_end_time: End Time +text_in_progress: in progress +activerecord_error_field_must_be_set_if_other_is_not: must be set if %s is not +text_clear_to_recalculate_time_by_range: Clear to recalculate by start/end time +text_and: and +text_time_entry_intersecting_notice: Notice that this timelog entry (%s) intersects with %s. +text_time_entry_intersecting_notice_entry: entry +text_time_entry_intersecting_notice_entry_plural: entries +button_now: Now Index: lang/fr.yml =================================================================== --- lang/fr.yml (revision 1669) +++ lang/fr.yml (working copy) @@ -633,3 +633,13 @@ enumeration_issue_priorities: Priorités des demandes enumeration_doc_categories: Catégories des documents enumeration_activities: Activités (suivi du temps) +field_start_time: Start Time +field_end_time: End Time +text_in_progress: in progress +activerecord_error_field_must_be_set_if_other_is_not: must be set if %s is not +text_clear_to_recalculate_time_by_range: Clear to recalculate by start/end time +text_and: and +text_time_entry_intersecting_notice: Notice that this timelog entry (%s) intersects with %s. +text_time_entry_intersecting_notice_entry: entry +text_time_entry_intersecting_notice_entry_plural: entries +button_now: Now Index: lang/he.yml =================================================================== --- lang/he.yml (revision 1669) +++ lang/he.yml (working copy) @@ -632,3 +632,13 @@ setting_mail_handler_api_enabled: Enable WS for incoming emails setting_mail_handler_api_key: API key text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them." +field_start_time: Start Time +field_end_time: End Time +text_in_progress: in progress +activerecord_error_field_must_be_set_if_other_is_not: must be set if %s is not +text_clear_to_recalculate_time_by_range: Clear to recalculate by start/end time +text_and: and +text_time_entry_intersecting_notice: Notice that this timelog entry (%s) intersects with %s. +text_time_entry_intersecting_notice_entry: entry +text_time_entry_intersecting_notice_entry_plural: entries +button_now: Now Index: lang/hu.yml =================================================================== --- lang/hu.yml (revision 1669) +++ lang/hu.yml (working copy) @@ -633,3 +633,13 @@ setting_mail_handler_api_enabled: Web Service engedélyezése a beérkezett levelekhez setting_mail_handler_api_key: API kulcs text_email_delivery_not_configured: "Az E-mail küldés nincs konfigurálva, és az értesítések ki vannak kapcsolva.\nÁllítsd be az SMTP szervert a config/email.yml fájlban és indítsd újra az alkalmazást, hogy érvénybe lépjen." +field_start_time: Start Time +field_end_time: End Time +text_in_progress: in progress +activerecord_error_field_must_be_set_if_other_is_not: must be set if %s is not +text_clear_to_recalculate_time_by_range: Clear to recalculate by start/end time +text_and: and +text_time_entry_intersecting_notice: Notice that this timelog entry (%s) intersects with %s. +text_time_entry_intersecting_notice_entry: entry +text_time_entry_intersecting_notice_entry_plural: entries +button_now: Now Index: lang/it.yml =================================================================== --- lang/it.yml (revision 1669) +++ lang/it.yml (working copy) @@ -632,3 +632,13 @@ setting_mail_handler_api_enabled: Abilita WS per le e-mail in arrivo setting_mail_handler_api_key: chiave API text_email_delivery_not_configured: "La consegna via e-mail non è configurata e le notifiche sono disabilitate.\nConfigura il tuo server SMTP in config/email.yml e riavvia l'applicazione per abilitarle." +field_start_time: Start Time +field_end_time: End Time +text_in_progress: in progress +activerecord_error_field_must_be_set_if_other_is_not: must be set if %s is not +text_clear_to_recalculate_time_by_range: Clear to recalculate by start/end time +text_and: and +text_time_entry_intersecting_notice: Notice that this timelog entry (%s) intersects with %s. +text_time_entry_intersecting_notice_entry: entry +text_time_entry_intersecting_notice_entry_plural: entries +button_now: Now Index: lang/ja.yml =================================================================== --- lang/ja.yml (revision 1669) +++ lang/ja.yml (working copy) @@ -633,3 +633,13 @@ setting_mail_handler_api_enabled: Enable WS for incoming emails setting_mail_handler_api_key: API key text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them." +field_start_time: Start Time +field_end_time: End Time +text_in_progress: in progress +activerecord_error_field_must_be_set_if_other_is_not: must be set if %s is not +text_clear_to_recalculate_time_by_range: Clear to recalculate by start/end time +text_and: and +text_time_entry_intersecting_notice: Notice that this timelog entry (%s) intersects with %s. +text_time_entry_intersecting_notice_entry: entry +text_time_entry_intersecting_notice_entry_plural: entries +button_now: Now Index: lang/ko.yml =================================================================== --- lang/ko.yml (revision 1669) +++ lang/ko.yml (working copy) @@ -632,3 +632,13 @@ setting_mail_handler_api_enabled: Enable WS for incoming emails setting_mail_handler_api_key: API key text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them." +field_start_time: Start Time +field_end_time: End Time +text_in_progress: in progress +activerecord_error_field_must_be_set_if_other_is_not: must be set if %s is not +text_clear_to_recalculate_time_by_range: Clear to recalculate by start/end time +text_and: and +text_time_entry_intersecting_notice: Notice that this timelog entry (%s) intersects with %s. +text_time_entry_intersecting_notice_entry: entry +text_time_entry_intersecting_notice_entry_plural: entries +button_now: Now Index: lang/lt.yml =================================================================== --- lang/lt.yml (revision 1669) +++ lang/lt.yml (working copy) @@ -635,3 +635,13 @@ setting_mail_handler_api_key: API raktas text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them." +field_start_time: Start Time +field_end_time: End Time +text_in_progress: in progress +activerecord_error_field_must_be_set_if_other_is_not: must be set if %s is not +text_clear_to_recalculate_time_by_range: Clear to recalculate by start/end time +text_and: and +text_time_entry_intersecting_notice: Notice that this timelog entry (%s) intersects with %s. +text_time_entry_intersecting_notice_entry: entry +text_time_entry_intersecting_notice_entry_plural: entries +button_now: Now Index: lang/nl.yml =================================================================== --- lang/nl.yml (revision 1669) +++ lang/nl.yml (working copy) @@ -633,3 +633,13 @@ setting_mail_handler_api_enabled: Enable WS for incoming emails setting_mail_handler_api_key: API key text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them." +field_start_time: Start Time +field_end_time: End Time +text_in_progress: in progress +activerecord_error_field_must_be_set_if_other_is_not: must be set if %s is not +text_clear_to_recalculate_time_by_range: Clear to recalculate by start/end time +text_and: and +text_time_entry_intersecting_notice: Notice that this timelog entry (%s) intersects with %s. +text_time_entry_intersecting_notice_entry: entry +text_time_entry_intersecting_notice_entry_plural: entries +button_now: Now Index: lang/no.yml =================================================================== --- lang/no.yml (revision 1669) +++ lang/no.yml (working copy) @@ -633,3 +633,13 @@ setting_mail_handler_api_enabled: Enable WS for incoming emails setting_mail_handler_api_key: API key text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them." +field_start_time: Start Time +field_end_time: End Time +text_in_progress: in progress +activerecord_error_field_must_be_set_if_other_is_not: must be set if %s is not +text_clear_to_recalculate_time_by_range: Clear to recalculate by start/end time +text_and: and +text_time_entry_intersecting_notice: Notice that this timelog entry (%s) intersects with %s. +text_time_entry_intersecting_notice_entry: entry +text_time_entry_intersecting_notice_entry_plural: entries +button_now: Now Index: lang/pl.yml =================================================================== --- lang/pl.yml (revision 1669) +++ lang/pl.yml (working copy) @@ -632,3 +632,13 @@ setting_mail_handler_api_enabled: Enable WS for incoming emails setting_mail_handler_api_key: API key text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them." +field_start_time: Start Time +field_end_time: End Time +text_in_progress: in progress +activerecord_error_field_must_be_set_if_other_is_not: must be set if %s is not +text_clear_to_recalculate_time_by_range: Clear to recalculate by start/end time +text_and: and +text_time_entry_intersecting_notice: Notice that this timelog entry (%s) intersects with %s. +text_time_entry_intersecting_notice_entry: entry +text_time_entry_intersecting_notice_entry_plural: entries +button_now: Now Index: lang/pt-br.yml =================================================================== --- lang/pt-br.yml (revision 1669) +++ lang/pt-br.yml (working copy) @@ -632,3 +632,13 @@ setting_mail_handler_api_enabled: Enable WS for incoming emails setting_mail_handler_api_key: API key text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them." +field_start_time: Start Time +field_end_time: End Time +text_in_progress: in progress +activerecord_error_field_must_be_set_if_other_is_not: must be set if %s is not +text_clear_to_recalculate_time_by_range: Clear to recalculate by start/end time +text_and: and +text_time_entry_intersecting_notice: Notice that this timelog entry (%s) intersects with %s. +text_time_entry_intersecting_notice_entry: entry +text_time_entry_intersecting_notice_entry_plural: entries +button_now: Now Index: lang/pt.yml =================================================================== --- lang/pt.yml (revision 1669) +++ lang/pt.yml (working copy) @@ -632,3 +632,13 @@ setting_mail_handler_api_enabled: Enable WS for incoming emails setting_mail_handler_api_key: API key text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them." +field_start_time: Start Time +field_end_time: End Time +text_in_progress: in progress +activerecord_error_field_must_be_set_if_other_is_not: must be set if %s is not +text_clear_to_recalculate_time_by_range: Clear to recalculate by start/end time +text_and: and +text_time_entry_intersecting_notice: Notice that this timelog entry (%s) intersects with %s. +text_time_entry_intersecting_notice_entry: entry +text_time_entry_intersecting_notice_entry_plural: entries +button_now: Now Index: lang/ro.yml =================================================================== --- lang/ro.yml (revision 1669) +++ lang/ro.yml (working copy) @@ -632,3 +632,13 @@ setting_mail_handler_api_enabled: Enable WS for incoming emails setting_mail_handler_api_key: API key text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them." +field_start_time: Start Time +field_end_time: End Time +text_in_progress: in progress +activerecord_error_field_must_be_set_if_other_is_not: must be set if %s is not +text_clear_to_recalculate_time_by_range: Clear to recalculate by start/end time +text_and: and +text_time_entry_intersecting_notice: Notice that this timelog entry (%s) intersects with %s. +text_time_entry_intersecting_notice_entry: entry +text_time_entry_intersecting_notice_entry_plural: entries +button_now: Now Index: lang/ru.yml =================================================================== --- lang/ru.yml (revision 1669) +++ lang/ru.yml (working copy) @@ -164,9 +164,11 @@ field_url: URL field_start_page: Стартовая страница field_subproject: Подпроект -field_hours: Час(а,ов) +field_hours: Часов field_activity: Деятельность field_spent_on: Дата +field_start_time: Время начала +field_end_time: Время окончания field_identifier: Ун. идентификатор field_is_filter: Используется в качестве фильтра field_issue_to_id: Связанные задачи @@ -395,7 +397,7 @@ label_issue_tracking: Ситуация по задачам label_spent_time: Затраченное время label_f_hour: %.2f час -label_f_hour_plural: %.2f часов(а) +label_f_hour_plural: %.2f часов label_time_tracking: Учет времени label_change_plural: Правки label_statistics: Статистика @@ -508,6 +510,7 @@ button_copy: Копировать button_annotate: Авторство button_update: Обновить +button_now: Сейчас status_active: Активен status_registered: Зарегистрирован @@ -636,3 +639,10 @@ setting_mail_handler_api_enabled: Включить веб-сервис для входящих сообщений setting_mail_handler_api_key: API ключ text_email_delivery_not_configured: "Параметры работы с почтовым сервером не настроены и функция уведомления по email не активна.\nНастроить параметры для вашего SMTP сервера вы можете в файле config/email.yml. Для применения изменений перезапустите приложение." +text_in_progress: в процессе +activerecord_error_field_must_be_set_if_other_is_not: необходимо задать, если значение поля '%s' не задано +text_clear_to_recalculate_time_by_range: Очистите, чтобы пересчитать по времени окончания +text_and: и +text_time_entry_intersecting_notice: Обратите внимание, что эта запись о потраченном времени (%s) пересекается с %s. +text_time_entry_intersecting_notice_entry: записью +text_time_entry_intersecting_notice_entry_plural: записями Index: lang/sr.yml =================================================================== --- lang/sr.yml (revision 1669) +++ lang/sr.yml (working copy) @@ -633,3 +633,13 @@ setting_mail_handler_api_enabled: Enable WS for incoming emails setting_mail_handler_api_key: API key text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them." +field_start_time: Start Time +field_end_time: End Time +text_in_progress: in progress +activerecord_error_field_must_be_set_if_other_is_not: must be set if %s is not +text_clear_to_recalculate_time_by_range: Clear to recalculate by start/end time +text_and: and +text_time_entry_intersecting_notice: Notice that this timelog entry (%s) intersects with %s. +text_time_entry_intersecting_notice_entry: entry +text_time_entry_intersecting_notice_entry_plural: entries +button_now: Now Index: lang/sv.yml =================================================================== --- lang/sv.yml (revision 1669) +++ lang/sv.yml (working copy) @@ -633,3 +633,13 @@ setting_mail_handler_api_enabled: Enable WS for incoming emails setting_mail_handler_api_key: API key text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them." +field_start_time: Start Time +field_end_time: End Time +text_in_progress: in progress +activerecord_error_field_must_be_set_if_other_is_not: must be set if %s is not +text_clear_to_recalculate_time_by_range: Clear to recalculate by start/end time +text_and: and +text_time_entry_intersecting_notice: Notice that this timelog entry (%s) intersects with %s. +text_time_entry_intersecting_notice_entry: entry +text_time_entry_intersecting_notice_entry_plural: entries +button_now: Now Index: lang/th.yml =================================================================== --- lang/th.yml (revision 1669) +++ lang/th.yml (working copy) @@ -635,3 +635,13 @@ setting_mail_handler_api_enabled: Enable WS for incoming emails setting_mail_handler_api_key: API key text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them." +field_start_time: Start Time +field_end_time: End Time +text_in_progress: in progress +activerecord_error_field_must_be_set_if_other_is_not: must be set if %s is not +text_clear_to_recalculate_time_by_range: Clear to recalculate by start/end time +text_and: and +text_time_entry_intersecting_notice: Notice that this timelog entry (%s) intersects with %s. +text_time_entry_intersecting_notice_entry: entry +text_time_entry_intersecting_notice_entry_plural: entries +button_now: Now Index: lang/uk.yml =================================================================== --- lang/uk.yml (revision 1669) +++ lang/uk.yml (working copy) @@ -634,3 +634,13 @@ setting_mail_handler_api_enabled: Enable WS for incoming emails setting_mail_handler_api_key: API key text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them." +field_start_time: Start Time +field_end_time: End Time +text_in_progress: in progress +activerecord_error_field_must_be_set_if_other_is_not: must be set if %s is not +text_clear_to_recalculate_time_by_range: Clear to recalculate by start/end time +text_and: and +text_time_entry_intersecting_notice: Notice that this timelog entry (%s) intersects with %s. +text_time_entry_intersecting_notice_entry: entry +text_time_entry_intersecting_notice_entry_plural: entries +button_now: Now Index: lang/zh-tw.yml =================================================================== --- lang/zh-tw.yml (revision 1669) +++ lang/zh-tw.yml (working copy) @@ -633,3 +633,14 @@ enumeration_issue_priorities: 項目優先權 enumeration_doc_categories: 文件分類 enumeration_activities: 活動 (時間追蹤) +text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them." +field_start_time: Start Time +field_end_time: End Time +text_in_progress: in progress +activerecord_error_field_must_be_set_if_other_is_not: must be set if %s is not +text_clear_to_recalculate_time_by_range: Clear to recalculate by start/end time +text_and: and +text_time_entry_intersecting_notice: Notice that this timelog entry (%s) intersects with %s. +text_time_entry_intersecting_notice_entry: entry +text_time_entry_intersecting_notice_entry_plural: entries +button_now: Now Index: lang/zh.yml =================================================================== --- lang/zh.yml (revision 1669) +++ lang/zh.yml (working copy) @@ -633,3 +633,13 @@ enumeration_doc_categories: 文档类别 enumeration_activities: 活动(时间跟踪) text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them." +field_start_time: Start Time +field_end_time: End Time +text_in_progress: in progress +activerecord_error_field_must_be_set_if_other_is_not: must be set if %s is not +text_clear_to_recalculate_time_by_range: Clear to recalculate by start/end time +text_and: and +text_time_entry_intersecting_notice: Notice that this timelog entry (%s) intersects with %s. +text_time_entry_intersecting_notice_entry: entry +text_time_entry_intersecting_notice_entry_plural: entries +button_now: Now Index: public/javascripts/time.js =================================================================== --- public/javascripts/time.js (revision 0) +++ public/javascripts/time.js (revision 0) @@ -0,0 +1,68 @@ +//taken from XPlanner +var dateFormatChars = "dMyhHmsa"; + +function formatDate(date, format) { + return formatDate2(date, format, 0); +} + +function formatDate2(date, format, offset) { + if (offset >= format.length) { + return ""; + } else if (dateFormatChars.indexOf(format.charAt(offset)) != -1) { + return formatDateElement(date, format, offset); + } else { + return formatDateLiteral(date, format, offset); + } +} + +function formatDateElement(date, format, offset) { + var end = offset; + var ch = format.charAt(offset); + while (++end < format.length && format.charAt(end) == ch); + var count = end - offset; + var value; + if (ch == 'd') { + value = padValue(count, date.getDate()); + } + else if (ch == 'M') { + value = padValue(count, date.getMonth()+1); + } + else if (ch == 'y') { + value = padValue(count, date.getFullYear()); + } + else if (ch == 'H') { + value = padValue(count, date.getHours()); + } + else if (ch == 'h') { + value = padValue(count, date.getHours() % 12); + } + else if (ch == 'm') { + value = padValue(count, date.getMinutes()); + } + else if (ch == 's') { + value = padValue(count, date.getSeconds()); + } + else if (ch == 'a') { + value = date.getHours() > 12 ? 'PM' : 'AM'; + } + return value + formatDate2(date, format, end); +} + +function padValue(count, value) { + for (i = value.toString().length; i < count; i++) { + value = '0'+value; + } + return value; +} + +function formatDateLiteral(date, format, offset) { + end = offset; + while (++end < format.length && dateFormatChars.indexOf(format.charAt(end)) == -1); + return format.substr(offset, end - offset) + formatDate2(date, format, end); +} + +function maybeSetTimeNow(field) { + if (field.value == "") { + field.value = formatDate(new Date(), "yyyy-MM-dd HH:mm"); + } +} Index: test/fixtures/time_entries.yml =================================================================== --- test/fixtures/time_entries.yml (revision 1669) +++ test/fixtures/time_entries.yml (working copy) @@ -55,4 +55,55 @@ hours: 7.65 user_id: 1 tyear: 2007 - \ No newline at end of file + +#this is to run existing controller tests with Start/End time improvement - specific entries +time_entry_in_progress: + created_on: 2007-04-22 12:20:48 +02:00 + tweek: 16 + tmonth: 4 + project_id: 1 + comments: Time spent on a subproject (in progress) + updated_on: 2007-04-22 12:20:48 +02:00 + activity_id: 10 + spent_on: 2007-04-22 + issue_id: 1 + id: 5 + user_id: 1 + tyear: 2007 + start_time: 2007-07-11 16:20 + +#same user but another issue and project, and intersects with time_entry_in_progress +intersecting_time_entry: + created_on: 2007-04-22 12:20:48 +02:00 + tweek: 16 + tmonth: 4 + project_id: 2 + comments: Time entry intersecting with time_entry_in_progress + updated_on: 2007-04-22 12:20:48 +02:00 + activity_id: 10 + spent_on: 2007-04-22 + issue_id: 4 + id: 6 + user_id: 1 + tyear: 2007 + start_time: 2007-07-11 15:20 + end_time: 2007-07-11 17:20 + hours: 0 #to not break time calculation tests + +#this spans several years - should intersect with time_entry_in_progress as well +big_intersecting_time_entry: + created_on: 2007-04-22 12:20:48 +02:00 + tweek: 16 + tmonth: 4 + project_id: 1 + comments: Time entry intersecting with time_entry_in_progress + updated_on: 2007-04-22 12:20:48 +02:00 + activity_id: 10 + spent_on: 2007-04-22 + issue_id: 1 + id: 7 + user_id: 1 + tyear: 2007 + start_time: 2005-07-11 16:30 + end_time: 2007-07-11 17:20 + hours: 0 #to not break time calculation tests Index: test/functional/issues_controller_test.rb =================================================================== --- test/functional/issues_controller_test.rb (revision 1669) +++ test/functional/issues_controller_test.rb (working copy) @@ -164,6 +164,19 @@ :content => /Notes/ } } end + def test_show_in_progress + @request.session[:user_id] = 1 + get :show, :id => 1 + assert_equal time_entries(:time_entry_in_progress), assigns(:time_entry), + 'there should be an in-progress time entry for this user' + end + + def test_show_not_in_progress + @request.session[:user_id] = 2 + get :show, :id => 1 + assert_nil assigns(:time_entry).id, 'time entry for this user should be new' + end + def test_get_new @request.session[:user_id] = 2 get :new, :project_id => 1, :tracker_id => 1 @@ -381,7 +394,7 @@ issue = Issue.find(1) assert_equal 1, issue.status_id @request.session[:user_id] = 2 - assert_difference('TimeEntry.count', 0) do + assert_difference('TimeEntry.count', 0) do #time entries count should not change because given entry is empty (hence invalid) post :edit, :id => 1, :issue => { :status_id => 2, :assigned_to_id => 3 }, @@ -399,6 +412,21 @@ assert mail.body.include?("Status changed from New to Assigned") end + def test_post_edit_with_task_in_progress + user_id = 1 + @request.session[:user_id] = user_id + issue = Issue.find(1) + time_entry = issue.time_entry_in_progress(User.find(user_id)) + assert_not_nil time_entry, 'there should be time entry in progress' + assert_difference('TimeEntry.count', 0) do #time entries count should not change because entry in progress should be updated + post :edit, + :id => 1, + :issue => { }, + :notes => 'Assigned to dlopper', + :time_entry => { :id => time_entry.id, :comments => 'xyz' } + end + end + def test_post_edit_with_note_only notes = 'Note added by IssuesControllerTest#test_update_with_note_only' # anonymous user @@ -428,11 +456,13 @@ issue = Issue.find(1) - j = issue.journals.find(:first, :order => 'id DESC') - assert_equal '2.5 hours added', j.notes - assert_equal 0, j.details.size + #:first doesn't work sometimes here with MySQL 5.0.51b + e = issue.journals.find(:all, :order => 'id DESC')[0] + assert_equal '2.5 hours added', e.notes + assert_equal 0, e.details.size - t = issue.time_entries.find(:first, :order => 'id DESC') + #:first doesn't work sometimes here with MySQL 5.0.51b + t = issue.time_entries.find(:all, :order => 'id DESC')[0] assert_not_nil t assert_equal 2.5, t.hours assert_equal spent_hours_before + 2.5, issue.spent_hours Index: test/functional/timelog_controller_test.rb =================================================================== --- test/functional/timelog_controller_test.rb (revision 1669) +++ test/functional/timelog_controller_test.rb (working copy) @@ -40,6 +40,28 @@ :content => 'Development' end + def test_get_edit_in_progress_by_entry_id + @request.session[:user_id] = 1 #to avoid 'no permission' error + get :edit, :id => time_entries(:time_entry_in_progress) + assert_response :success + assert_equal 'time_entry_end_time', assigns(:activate_field) + end + + def test_get_edit_not_in_progress + @request.session[:user_id] = 2 #there are no in-progress entries of this user for the issue + get :edit, :issue_id => 1 + assert_response :success + assert_equal 'time_entry_start_time', assigns(:activate_field) + end + + def test_get_edit_in_progress_by_issue_id + user_id = 1 + @request.session[:user_id] = user_id #there is in-progress entry of this user for the issue + issue_id = 1 + get :edit, :issue_id => issue_id + assert_redirected_to :action => "edit", :id => Issue.find(issue_id).time_entry_in_progress(User.find(user_id)).id + end + def test_post_edit @request.session[:user_id] = 3 post :edit, :project_id => 1, @@ -70,6 +92,7 @@ post :edit, :id => 1, :time_entry => {:issue_id => '2', :hours => '8'} + assert_no_errors(assigns(:time_entry)) assert_redirected_to 'projects/ecookbook/timelog/details' entry.reload @@ -165,7 +188,7 @@ assert_response :success assert_template 'details' assert_not_nil assigns(:entries) - assert_equal 4, assigns(:entries).size + assert_equal 6, assigns(:entries).size # project and subproject assert_equal [1, 3], assigns(:entries).collect(&:project_id).uniq.sort assert_not_nil assigns(:total_hours) @@ -180,7 +203,7 @@ assert_response :success assert_template 'details' assert_not_nil assigns(:entries) - assert_equal 3, assigns(:entries).size + assert_equal 5, assigns(:entries).size assert_not_nil assigns(:total_hours) assert_equal "12.90", "%.2f" % assigns(:total_hours) assert_equal '2007-03-20'.to_date, assigns(:from) @@ -202,7 +225,7 @@ assert_response :success assert_template 'details' assert_not_nil assigns(:entries) - assert_equal 2, assigns(:entries).size + assert_equal 4, assigns(:entries).size assert_not_nil assigns(:total_hours) assert_equal 154.25, assigns(:total_hours) # display all time by default Index: test/test_helper.rb =================================================================== --- test/test_helper.rb (revision 1669) +++ test/test_helper.rb (working copy) @@ -64,4 +64,19 @@ Dir.mkdir "#{RAILS_ROOT}/tmp/test/attachments" unless File.directory?("#{RAILS_ROOT}/tmp/test/attachments") Attachment.storage_path = "#{RAILS_ROOT}/tmp/test/attachments" end + + def assert_error_on(object, field) + object.valid? + assert object.errors.on(field), "expected error on #{field} attribute" + end + + def assert_no_error_on(object, field) + object.valid? + assert !object.errors.on(field), "expected no error on #{field} attribute" + end + + def assert_no_errors(object, options = {}) + object.valid? if options[:validate] + assert_equal [], object.errors.full_messages + end end Index: test/unit/issue_test.rb =================================================================== --- test/unit/issue_test.rb (revision 1669) +++ test/unit/issue_test.rb (working copy) @@ -181,4 +181,13 @@ assert_nil Issue.find_by_id(1) assert_nil TimeEntry.find_by_issue_id(1) end + + def test_find_in_progress_success + assert_equal time_entries(:time_entry_in_progress), + issues(:issues_001).time_entry_in_progress(users(:users_001)) + end + + def test_find_in_progress_failure + assert_nil issues(:issues_001).time_entry_in_progress(users(:users_002)) + end end Index: test/unit/time_entry_test.rb =================================================================== --- test/unit/time_entry_test.rb (revision 1669) +++ test/unit/time_entry_test.rb (working copy) @@ -20,6 +20,10 @@ class TimeEntryTest < Test::Unit::TestCase fixtures :issues, :projects, :users, :time_entries + def setup + User.current.language = 'en' + end + def test_hours_format assertions = { "2" => 2.0, "21.1" => 21.1, @@ -43,4 +47,80 @@ assert_equal v, t.hours end end + + def test_start_time_must_be_set_if_hours_are_not_and_reverse + entry = TimeEntry.new + assert_error_on(entry, :start_time) + assert_error_on(entry, :hours) + end + + def test_start_time_can_be_absent_if_hours_are_set_and_reverse + entry = TimeEntry.new :hours => 1 + entry.valid? + assert_no_error_on(entry, :start_time) + entry = TimeEntry.new :start_time => Time.now + entry.valid? + assert_no_error_on(entry, :hours) + end + + def successful_params + {:spent_on => '2008-07-13', :issue_id => 1, :user => users(:users_004), + :activity_id => Enumeration.get_values('ACTI').first} + end + + def test_hours_not_calculated_if_set_explicitly + #I worked on this time to time during the day, and it was 1 hour in sum + entry = TimeEntry.new successful_params.merge(:hours => 1, + :start_time => '2008-07-13 10:56', :end_time => '2008-07-14 10:56') + + entry.save! + assert_equal 1, entry.hours + end + + {['10:56', '11:56'] => 1, ['10:56', '11:26'] => 0.5, + ['10:56', '10:57'] => 0.0167, + ['2008-07-13 23:50', '2008-07-14 00:20'] => 0.5}.each do |range, hours| + + define_method "test_hours_calculated_#{range[0]}_to_#{range[1]}" do + + #add default day if not specified + range = range.map {|time| time['-'] ? time : '2008-07-13 ' + time} + + entry = TimeEntry.new successful_params.merge(:hours => nil, + :start_time => range[0], :end_time => range[1]) + entry.save! + assert_in_delta hours, entry.hours, 0.0001 + end + end + + def assert_intersects(source, dest) + intersecting = time_entries(source).find_intersecting_entries + + assert !intersecting.empty?, + "there should be intersecting entries for #{source.inspect}" + + assert intersecting.map {|e| e.id}. + include?(time_entries(dest).id), + "#{source.inspect} should intersect with #{dest.inspect}" + + intersecting + end + + def test_find_intersecting_entries_for_incomplete + assert_intersects(:time_entry_in_progress, :intersecting_time_entry) + end + + def test_find_intersecting_entries_for_complete_doesnt_find_itself + intersecting = assert_intersects(:intersecting_time_entry, + :time_entry_in_progress) + + assert !intersecting.map {|e| e.id}.include?( + time_entries(:intersecting_time_entry)), 'time entry\'s ' + + 'intersecting entries shouldn\'t include itself' + end + + def test_find_intersecting_entries_for_big + assert_intersects(:big_intersecting_time_entry, :time_entry_in_progress) + assert_intersects(:big_intersecting_time_entry, :intersecting_time_entry) + end end Index: vendor/plugins/test_utils/MIT-LICENSE =================================================================== --- vendor/plugins/test_utils/MIT-LICENSE (revision 0) +++ vendor/plugins/test_utils/MIT-LICENSE (revision 0) @@ -0,0 +1,21 @@ +Copyright (C) 2008 Texuna Technologies + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + Index: vendor/plugins/test_utils/tasks/test_utils.rake =================================================================== --- vendor/plugins/test_utils/tasks/test_utils.rake (revision 0) +++ vendor/plugins/test_utils/tasks/test_utils.rake (revision 0) @@ -0,0 +1,66 @@ +## +# based on http://nubyonrails.com/articles/foscon-and-living-dangerously-with-rake +# Run a single test (or group of tests started with given string) in Rails. +# +# rake blogs-list (or f_blogs-list) +# => Runs test_list for BlogsController (functional test; use f_blogs-list to force it if unit test found) +# +# rake blog-create (or u_blog-create) +# => Runs test_create for BlogTest (unit test; use u_blog-create to force it if functional test found)) + +#test file will be matched in the order of this array items +TEST_TYPES = [ + ['u_', "unit/[file_name]_test.rb"], + ['f_', "functional/[file_name]_controller_test.rb"], + ['i_', "integration/[file_name]_test.rb"], + ['l_', "long/[file_name]_test.rb"], +] + +rule "" do |t| + all_flags = TEST_TYPES.map { |item| item[0] } + if Regexp.new("(#{all_flags.join '|'}|)(.*)\\-([^.]+)$").match(t.name) + flag = $1 + file_name = $2 + test_name = $3 + + path_getter = lambda { |type_info| type_info[1].gsub '[file_name]', file_name} + file_path = nil + TEST_TYPES.each do |type_info| + my_file_path = path_getter.call(type_info) + if flag == type_info[0] + type_info[1].match /((.+)\/)/ + type = $2 + puts "forced #{type} test" + file_path = my_file_path + break + end + end + + if file_path && !File.exist?("test/#{file_path}") + raise "No file found for #{file_path}" + end + + if !file_path + TEST_TYPES.each do |type_info| + my_file_path = path_getter.call(type_info) + if File.exist? "test/#{my_file_path}" + puts "found #{my_file_path}" + file_path = my_file_path + break + end + end + end + + if !file_path + raise "No file found for #{file_name}" + end + + begin + sh "ruby -Ilib:test test/#{file_path} -n /^test_#{test_name}/" + rescue Exception => e + #no logger here, oops! + #log.debug "error executing tests: #{e.inspect}" + puts "error executing tests: #{e.inspect}" + end + end +end