Index: app/controllers/issues_controller.rb =================================================================== --- app/controllers/issues_controller.rb (revision 12160) +++ app/controllers/issues_controller.rb (working copy) @@ -425,7 +425,7 @@ @issue.project ||= @issue.allowed_target_projects.first end @issue.author ||= User.current - @issue.start_date ||= Date.today if Setting.default_issue_start_date_to_creation_date? + @issue.start_date ||= DateTime.now if Setting.default_issue_start_date_to_creation_date? attrs = (params[:issue] || {}).deep_dup if action_name == 'new' && params[:was_default_status] == attrs[:status_id] Index: app/helpers/application_helper.rb =================================================================== --- app/helpers/application_helper.rb (revision 12160) +++ app/helpers/application_helper.rb (working copy) @@ -291,6 +291,42 @@ end end + def time_select_tag( name, stime, options = {} ) + time = stime.to_time(:utc) + if time.nil? + selected = {:hour => '', :min => ''} + else + zone = User.current.time_zone + time = zone ? time.in_time_zone(zone) : (time.utc? ? time.localtime : time) + selected = {:hour => time.hour, :min => time.min} + end + + out = '' + + if options[:required] + hours = [] + mins = [] + else + hours = [['', '']] + mins = [['', '']] + end + + hours += (0..23).map{|i| ['%02d' % i, i] } # Zero pad + out << select_tag( + "#{name}[hour]", + options_for_select( hours, selected[:hour] ), + :style => 'min-width: 10px;max-width: 50px;' + ) + + out << ':' + mins += (0..59).map{|i| ['%02d' % i, i] } # Zero pad + out << select_tag( + "#{name}[minute]", + options_for_select( mins, selected[:min] ), + :style => 'min-width: 10px;max-width: 50px;' + ) + end + def project_tree_options_for_select(projects, options = {}) s = '' project_tree(projects) do |project, level| Index: app/helpers/queries_helper.rb =================================================================== --- app/helpers/queries_helper.rb (revision 12160) +++ app/helpers/queries_helper.rb (working copy) @@ -151,7 +151,17 @@ value.to_s(issue) {|other| link_to_issue(other, :subject => false, :tracker => false)}.html_safe, :class => value.css_classes_for(issue)) else - format_object(value) + case value.class.name + when 'Time' + if ( column.name == :start_date or column.name == :due_date ) and + ( !issue.project.use_datetime_for_issues or value.strftime('%H%M')=='0000' ) + format_date(value) + else + format_time(value) + end + else + format_object(value) + end end end Index: app/models/issue.rb =================================================================== --- app/models/issue.rb (revision 12160) +++ app/models/issue.rb (working copy) @@ -61,6 +61,9 @@ DONE_RATIO_OPTIONS = %w(issue_field issue_status) attr_reader :current_journal + attr_accessor :start_time + attr_accessor :due_time + delegate :notes, :notes=, :private_notes, :private_notes=, :to => :current_journal, :allow_nil => true validates_presence_of :subject, :priority, :project, :tracker, :author, :status @@ -68,8 +71,8 @@ validates_length_of :subject, :maximum => 255 validates_inclusion_of :done_ratio, :in => 0..100 validates :estimated_hours, :numericality => {:greater_than_or_equal_to => 0, :allow_nil => true, :message => :invalid} - validates :start_date, :date => true - validates :due_date, :date => true + #validates :start_date, :date => true + #validates :due_date, :date => true validate :validate_issue, :validate_required_fields scope :visible, lambda {|*args| @@ -91,7 +94,8 @@ before_validation :clear_disabled_fields before_create :default_assign - before_save :close_duplicates, :update_done_ratio_from_issue_status, - :force_updated_on_change, :update_closed_on, :set_assigned_to_was + #Note very well - before_save runs for both updates AND creations (also, before_create is called after before_save) + before_save :close_duplicates, :update_done_ratio_from_issue_status, + :force_updated_on_change, :update_closed_on, :set_assigned_to_was, :add_start_and_due_time after_save {|issue| issue.send :after_project_change if !issue.id_changed? && issue.project_id_changed?} after_save :reschedule_following_issues, :update_nested_set_attributes, :update_parent_attributes, :create_journal @@ -361,7 +365,9 @@ 'subject', 'description', 'start_date', + 'start_time', 'due_date', + 'due_time', 'done_ratio', 'estimated_hours', 'custom_field_values', @@ -454,6 +460,21 @@ # mass-assignment security bypass assign_attributes attrs, :without_protection => true + + # Helper to make sure that when we load in the time it takes into account that it is supplied localised and must be converted back to utc. + def load_localised_time(date, time, time_zone) + localised_date = {:year => date.year, :month => date.month, :day => date.day} + localised_datetime = date.in_time_zone(time_zone).change({:hour => time['hour'].to_i, :min => time['minute'].to_i}) + return localised_datetime.change(localised_date).utc + end + + if (start_time = attrs.delete('start_time')) && safe_attribute?('start_time') && self.start_date.is_a?(Time) + self.start_date = load_localised_time(self.start_date, start_time, user.time_zone) + end + + if (due_time = attrs.delete('due_time')) && safe_attribute?('due_time') && self.due_date.is_a?(Time) + self.due_date = load_localised_time(self.due_date, due_time, user.time_zone) + end end def disabled_core_fields @@ -1388,6 +1409,33 @@ end end + # Callback on start and due time + def add_start_and_due_time + return if not project.use_datetime_for_issues + + # Not sure if this is a hack or not, but it works :) + time_zone = User.current.time_zone + system_time_zone = Time.zone + if time_zone + Time.zone = time_zone + end + + if st=start_time and sd=start_date + if st['hour'].to_i >= 0 or st['minute'].to_i >= 0 + self.start_date = Time.zone.parse( "#{sd.year}.#{sd.month}.#{sd.day} #{st['hour']}:#{st['minute']}:00" ).utc # Parse in as local but save as UTC + end + end + + if dt=due_time and dd=due_date + if dt['hour'].to_i >= 0 or dt['minute'].to_i >= 0 + self.due_date = Time.zone.parse( "#{dd.year}.#{dd.month}.#{dd.day} #{dt['hour']}:#{dt['minute']}:00").utc # Parse in as local but save as UTC + end + end + + # Since we fudged the timezone to get the values parsing in okay, let's reset it to the system timezone. + Time.zone = system_time_zone + end + # Default assignment based on category def default_assign if assigned_to.nil? && category && category.assigned_to Index: app/models/project.rb =================================================================== --- app/models/project.rb (revision 12160) +++ app/models/project.rb (working copy) @@ -549,9 +549,9 @@ # The earliest start date of a project, based on it's issues and versions def start_date @start_date ||= [ - issues.minimum('start_date'), + issues.minimum('start_date').nil? ? nil : issues.minimum('start_date').to_date, shared_versions.minimum('effective_date'), - Issue.fixed_version(shared_versions).minimum('start_date') + Issue.fixed_version(shared_versions).minimum('start_date').nil? ? nil : Issue.fixed_version(shared_versions).minimum('start_date').to_date ].compact.min end @@ -558,14 +558,14 @@ # The latest due date of an issue or version def due_date @due_date ||= [ - issues.maximum('due_date'), + issues.maximum('due_date').nil? ? nil : issues.maximum('due_date').to_date, shared_versions.maximum('effective_date'), - Issue.fixed_version(shared_versions).maximum('due_date') + Issue.fixed_version(shared_versions).maximum('due_date').nil? ? nil : Issue.fixed_version(shared_versions).maximum('due_date').to_date ].compact.max end def overdue? - active? && !due_date.nil? && (due_date < Date.today) + active? && !due_date.nil? && (due_date < DateTime.now) end # Returns the percent completed for this project, based on the @@ -650,6 +650,7 @@ 'description', 'homepage', 'is_public', + 'use_datetime_for_issues', 'identifier', 'custom_field_values', 'custom_fields', Index: app/models/version.rb =================================================================== --- app/models/version.rb (revision 12160) +++ app/models/version.rb (working copy) @@ -100,7 +100,7 @@ if completed_percent == 100 return false elsif due_date && start_date - done_date = start_date + ((due_date - start_date+1)* completed_percent/100).floor + done_date = start_date.to_date + ((due_date.to_date - start_date.to_date + 1) * completed_percent/100).floor return done_date <= Date.today else false # No issues so it's not late Index: app/views/issues/_attributes.html.erb =================================================================== --- app/views/issues/_attributes.html.erb (revision 12160) +++ app/views/issues/_attributes.html.erb (working copy) @@ -48,14 +48,14 @@ <% if @issue.safe_attribute? 'start_date' %>

- <%= f.text_field(:start_date, :size => 10, :required => @issue.required_attribute?('start_date')) %> + <%= f.text_field(:start_date, :value => (@issue.start_date ? localise_date(@issue.start_date).strftime('%Y-%m-%d') : ''), :size => 10, :required => @issue.required_attribute?('start_date')) %> <%= calendar_for('issue_start_date') if @issue.leaf? %>

<% end %> <% if @issue.safe_attribute? 'due_date' %>

- <%= f.text_field(:due_date, :size => 10, :required => @issue.required_attribute?('due_date')) %> + <%= f.text_field(:due_date, :value => (@issue.due_date ? localise_date(@issue.due_date).strftime('%Y-%m-%d') : ''), :size => 10, :required => @issue.required_attribute?('due_date')) %> <%= calendar_for('issue_due_date') if @issue.leaf? %>

<% end %> Index: app/views/issues/show.html.erb =================================================================== --- app/views/issues/show.html.erb (revision 12160) +++ app/views/issues/show.html.erb (working copy) @@ -47,10 +47,10 @@ end unless @issue.disabled_core_fields.include?('start_date') - rows.right l(:field_start_date), format_date(@issue.start_date), :class => 'start-date' + rows.right l(:field_start_date), (@project.use_datetime_for_issues ? format_time(@issue.start_date) : format_date(@issue.start_date)), :class => 'start-date' end unless @issue.disabled_core_fields.include?('due_date') - rows.right l(:field_due_date), format_date(@issue.due_date), :class => 'due-date' + rows.right l(:field_due_date), (@project.use_datetime_for_issues ? format_time(@issue.due_date) : format_date(@issue.due_date)), :class => 'due-date' end unless @issue.disabled_core_fields.include?('done_ratio') rows.right l(:field_done_ratio), progress_bar(@issue.done_ratio, :width => '80px', :legend => "#{@issue.done_ratio}%"), :class => 'progress' Index: app/views/projects/_form.html.erb =================================================================== --- app/views/projects/_form.html.erb (revision 12160) +++ app/views/projects/_form.html.erb (working copy) @@ -11,6 +11,7 @@ <% end %>

<%= f.text_field :homepage, :size => 60 %>

<%= f.check_box :is_public %>

+

<%= f.check_box :use_datetime_for_issues %>

<% unless @project.allowed_parents.compact.empty? %>

<%= label(:project, :parent_id, l(:field_parent)) %><%= parent_project_select_tag(@project) %>

Index: config/locales/cs.yml =================================================================== --- config/locales/cs.yml (revision 12160) +++ config/locales/cs.yml (working copy) @@ -311,7 +311,7 @@ field_assigned_to_role: Role přiřaditele field_text: Textové pole field_visible: Viditelný - + field_use_datetime_for_issues: Použít u tiketů také čas setting_app_title: Název aplikace setting_app_subtitle: Podtitulek aplikace setting_welcome_text: Uvítací text Index: config/locales/en-GB.yml =================================================================== --- config/locales/en-GB.yml (revision 12160) +++ config/locales/en-GB.yml (working copy) @@ -311,6 +311,7 @@ field_assigned_to_role: "Assignee's role" field_text: Text field field_visible: Visible + field_use_datetime_for_issues: Use time in tickets too field_warn_on_leaving_unsaved: "Warn me when leaving a page with unsaved text" setting_app_title: Application title Index: config/locales/en.yml =================================================================== --- config/locales/en.yml (revision 12160) +++ config/locales/en.yml (working copy) @@ -314,6 +314,7 @@ field_assigned_to_role: "Assignee's role" field_text: Text field field_visible: Visible + field_use_datetime_for_issues: Use time in tickets too field_warn_on_leaving_unsaved: "Warn me when leaving a page with unsaved text" field_issues_visibility: Issues visibility field_is_private: Private Index: config/locales/es.yml =================================================================== --- config/locales/es.yml (revision 12160) +++ config/locales/es.yml (working copy) @@ -1110,6 +1110,7 @@ button_hide: Ocultar setting_non_working_week_days: Días no laborables label_in_the_next_days: en los próximos + field_use_datetime_for_issues: Usar hora en prog peticiones label_in_the_past_days: en los anteriores label_attribute_of_user: "%{name} del usuario" text_turning_multiple_off: Si desactiva los valores múltiples, éstos serán eliminados para dejar un único valor por elemento. Index: db/migrate/20130531174459_add_time_to_issue_start_date_and_issue_due_date.rb =================================================================== --- db/migrate/20130531174459_add_time_to_issue_start_date_and_issue_due_date.rb (revision 0) +++ db/migrate/20130531174459_add_time_to_issue_start_date_and_issue_due_date.rb (working copy) @@ -0,0 +1,11 @@ +class AddTimeToIssueStartDateAndIssueDueDate < ActiveRecord::Migration + def self.up + change_column :issues, :start_date, :datetime + change_column :issues, :due_date, :datetime + end + + def self.down + change_column :issues, :start_date, :date + change_column :issues, :due_date, :date + end +end Index: db/migrate/20130531174549_add_use_datetime_for_issues_to_projects.rb =================================================================== --- db/migrate/20130531174549_add_use_datetime_for_issues_to_projects.rb (revision 0) +++ db/migrate/20130531174549_add_use_datetime_for_issues_to_projects.rb (working copy) @@ -0,0 +1,11 @@ +class AddUseDatetimeForIssuesToProjects < ActiveRecord::Migration + + def self.up + add_column :projects, :use_datetime_for_issues, :boolean, :default => false + end + + def self.down + remove_column :projects, :use_datetime_for_issues + end + +end Index: lib/redmine/helpers/calendar.rb =================================================================== --- lib/redmine/helpers/calendar.rb (revision 12160) +++ lib/redmine/helpers/calendar.rb (working copy) @@ -48,8 +48,8 @@ # Sets calendar events def events=(events) @events = events - @ending_events_by_days = @events.group_by {|event| event.due_date} - @starting_events_by_days = @events.group_by {|event| event.start_date} + @ending_events_by_days = @events.group_by {|event| (event.due_date.is_a?(Date) || event.due_date.nil? ? event.due_date : event.due_date.to_date) } + @starting_events_by_days = @events.group_by {|event| (event.start_date.is_a?(Date) || event.start_date.nil? ? event.start_date : event.start_date.to_date) } end # Returns events for the given day Index: lib/redmine/helpers/gantt.rb =================================================================== --- lib/redmine/helpers/gantt.rb (revision 12160) +++ lib/redmine/helpers/gantt.rb (working copy) @@ -628,6 +628,9 @@ private def coordinates(start_date, end_date, progress, zoom=nil) + start_date = start_date.to_date if not start_date.nil? + end_date = end_date.to_date if not end_date.nil? + zoom ||= @zoom coords = {} if start_date && end_date && start_date < self.date_to && end_date > self.date_from @@ -672,7 +675,7 @@ end def calc_progress_date(start_date, end_date, progress) - start_date + (end_date - start_date + 1) * (progress / 100.0) + start_date.to_date + (end_date.to_date - start_date.to_date + 1) * (progress / 100.0) end # TODO: Sorts a collection of issues by start_date, due_date, id for gantt rendering Index: lib/redmine/i18n.rb =================================================================== --- lib/redmine/i18n.rb (revision 12160) +++ lib/redmine/i18n.rb (working copy) @@ -52,6 +52,14 @@ ::I18n.t(str.to_s, :value => value, :locale => lang.to_s.gsub(%r{(.+)\-(.+)$}) { "#{$1}-#{$2.upcase}" }) end + def localise_date(date) + return if date.nil? + + zone = User.current.time_zone + local = zone ? date.in_time_zone(zone) : date + return local + end + def format_date(date) return nil unless date options = {} Index: lib/redmine/utils.rb =================================================================== --- lib/redmine/utils.rb (revision 12160) +++ lib/redmine/utils.rb (working copy) @@ -60,7 +60,7 @@ weeks = days / 7 result = weeks * (7 - non_working_week_days.size) days_left = days - weeks * 7 - start_cwday = from.cwday + start_cwday = from.to_date.cwday days_left.times do |i| unless non_working_week_days.include?(((start_cwday + i - 1) % 7) + 1) result += 1 @@ -78,7 +78,7 @@ weeks = working_days / (7 - non_working_week_days.size) result = weeks * 7 days_left = working_days - weeks * (7 - non_working_week_days.size) - cwday = date.cwday + cwday = date.to_date.cwday while days_left > 0 cwday += 1 unless non_working_week_days.include?(((cwday - 1) % 7) + 1) @@ -94,7 +94,7 @@ # Returns the date of the first day on or after the given date that is a working day def next_working_date(date) - cwday = date.cwday + cwday = date.to_date.cwday days = 0 while non_working_week_days.include?(((cwday + days - 1) % 7) + 1) days += 1 Index: test/functional/issues_controller_test.rb =================================================================== --- test/functional/issues_controller_test.rb (revision 12160) +++ test/functional/issues_controller_test.rb (working copy) @@ -1799,7 +1799,7 @@ assert_response :success assert_template 'new' assert_select 'input[name=?]', 'issue[start_date]' - assert_select 'input[name=?][value]', 'issue[start_date]', 0 + assert_select 'input[name=?][value]', 'issue[start_date]', 1 end end @@ -2018,7 +2018,7 @@ assert_equal 2, issue.author_id assert_equal 3, issue.tracker_id assert_equal 2, issue.status_id - assert_equal Date.parse('2010-11-07'), issue.start_date + assert_equal DateTime.parse('2010-11-07'), issue.start_date assert_nil issue.estimated_hours v = issue.custom_values.where(:custom_field_id => 2).first assert_not_nil v @@ -2085,7 +2085,7 @@ :id => Issue.last.id issue = Issue.find_by_subject('This is the test_new issue') assert_not_nil issue - assert_equal Date.today, issue.start_date + assert_equal Date.today, issue.start_date.to_date end end @@ -2260,7 +2260,7 @@ end issue = Issue.order('id DESC').first - assert_equal Date.parse('2012-07-14'), issue.start_date + assert_equal DateTime.parse('2012-07-14'), issue.start_date assert_nil issue.due_date assert_equal 'value1', issue.custom_field_value(cf1) assert_nil issue.custom_field_value(cf2) @@ -3675,8 +3675,8 @@ assert_equal 2, issue.project_id, "Project is incorrect" assert_equal 4, issue.assigned_to_id, "Assigned to is incorrect" assert_equal 1, issue.status_id, "Status is incorrect" - assert_equal '2009-12-01', issue.start_date.to_s, "Start date is incorrect" - assert_equal '2009-12-31', issue.due_date.to_s, "Due date is incorrect" + assert_equal '2009-12-01 00:00:00 UTC', issue.start_date.to_s, "Start date is incorrect" + assert_equal '2009-12-31 00:00:00 UTC', issue.due_date.to_s, "Due date is incorrect" end end