<%= render_main_menu(@project) %>
-
diff --git a/Gemfile b/Gemfile index f2c01a6..68b1b43 100644 --- a/Gemfile +++ b/Gemfile @@ -1,10 +1,8 @@ source 'https://rubygems.org' -if Gem::Version.new(Bundler::VERSION) < Gem::Version.new('1.5.0') - abort "Redmine requires Bundler 1.5.0 or higher (you're using #{Bundler::VERSION}).\nPlease update with 'gem update bundler'." -end +gem "bundler", ">= 1.5.0", "< 2.0.0" -gem "rails", "4.2.8" +gem "rails", "4.2.11.1" gem "addressable", "2.4.0" if RUBY_VERSION < "2.0" if RUBY_VERSION < "2.1" gem "public_suffix", (RUBY_VERSION < "2.0" ? "~> 1.4" : "~> 2.0.5") @@ -23,13 +21,17 @@ gem "mail", "~> 2.6.4" gem "nokogiri", (RUBY_VERSION >= "2.1" ? "~> 1.8.1" : "~> 1.6.8") gem "i18n", "~> 0.7.0" gem "ffi", "1.9.14", :platforms => :mingw if RUBY_VERSION < "2.0" +gem "xpath", "< 3.2.0" if RUBY_VERSION < "2.3" # Request at least rails-html-sanitizer 1.0.3 because of security advisories gem "rails-html-sanitizer", ">= 1.0.3" +# TODO: Remove the following line when #32223 is fixed +gem "sprockets", "~> 3.7.2" + # Windows does not include zoneinfo files, so bundle the tzinfo-data gem gem 'tzinfo-data', platforms: [:mingw, :x64_mingw, :mswin] -gem "rbpdf", "~> 1.19.3" +gem "rbpdf", "~> 1.19.6" # Optional gem for LDAP authentication group :ldap do @@ -38,14 +40,14 @@ end # Optional gem for OpenID authentication group :openid do - gem "ruby-openid", "~> 2.3.0", :require => "openid" + gem "ruby-openid", "~> 2.9.2", :require => "openid" gem "rack-openid" end platforms :mri, :mingw, :x64_mingw do # Optional gem for exporting the gantt to a PNG file, not supported with jruby group :rmagick do - gem "rmagick", ">= 2.14.0" + gem "rmagick", "~> 2.16.0" end # Optional Markdown support, not for JRuby @@ -94,12 +96,12 @@ end group :test do gem "minitest" gem "rails-dom-testing" - gem "mocha" + gem 'mocha', '>= 1.4.0' gem "simplecov", "~> 0.9.1", :require => false # TODO: remove this after upgrading to Rails 5 gem "test_after_commit", "~> 0.4.2" # For running UI tests - gem "capybara" + gem "capybara", '~> 2.13' gem "selenium-webdriver", "~> 2.53.4" end diff --git a/app/controllers/account_controller.rb b/app/controllers/account_controller.rb index d6e1da8..5070295 100644 --- a/app/controllers/account_controller.rb +++ b/app/controllers/account_controller.rb @@ -87,7 +87,7 @@ class AccountController < ApplicationController @user.must_change_passwd = false if @user.save @token.destroy - Mailer.password_updated(@user) + Mailer.password_updated(@user, { remote_ip: request.remote_ip }) flash[:notice] = l(:notice_account_password_updated) redirect_to signin_path return diff --git a/app/controllers/attachments_controller.rb b/app/controllers/attachments_controller.rb index 1b18662..64cb05e 100644 --- a/app/controllers/attachments_controller.rb +++ b/app/controllers/attachments_controller.rb @@ -60,7 +60,7 @@ class AttachmentsController < ApplicationController @attachment.increment_download end - if stale?(:etag => @attachment.digest) + if stale?(:etag => @attachment.digest, :template => false) # images are sent inline send_file @attachment.diskfile, :filename => filename_for_content_disposition(@attachment.filename), :type => detect_content_type(@attachment), @@ -70,7 +70,7 @@ class AttachmentsController < ApplicationController def thumbnail if @attachment.thumbnailable? && tbnail = @attachment.thumbnail(:size => params[:size]) - if stale?(:etag => tbnail) + if stale?(:etag => tbnail, :template => false) send_file tbnail, :filename => filename_for_content_disposition(@attachment.filename), :type => detect_content_type(@attachment), diff --git a/app/controllers/auto_completes_controller.rb b/app/controllers/auto_completes_controller.rb index 293ac20..2d707f9 100644 --- a/app/controllers/auto_completes_controller.rb +++ b/app/controllers/auto_completes_controller.rb @@ -19,7 +19,7 @@ class AutoCompletesController < ApplicationController before_action :find_project def issues - @issues = [] + issues = [] q = (params[:q] || params[:term]).to_s.strip status = params[:status].to_s issue_id = params[:issue_id].to_s @@ -32,13 +32,14 @@ class AutoCompletesController < ApplicationController scope = scope.where.not(:id => issue_id.to_i) end if q.match(/\A#?(\d+)\z/) - @issues << scope.find_by_id($1.to_i) + issues << scope.find_by_id($1.to_i) end - @issues += scope.like(q).order(:id => :desc).limit(10).to_a - @issues.compact! + issues += scope.like(q).order(:id => :desc).limit(10).to_a + issues.compact! end - render :layout => false + + render :json => format_issues_json(issues) end private @@ -50,4 +51,13 @@ class AutoCompletesController < ApplicationController rescue ActiveRecord::RecordNotFound render_404 end + + def format_issues_json(issues) + issues.map {|issue| { + 'id' => issue.id, + 'label' => "#{issue.tracker} ##{issue.id}: #{issue.subject.to_s.truncate(60)}", + 'value' => issue.id + } + } + end end diff --git a/app/controllers/enumerations_controller.rb b/app/controllers/enumerations_controller.rb index e5e3cc3..a04d7b1 100644 --- a/app/controllers/enumerations_controller.rb +++ b/app/controllers/enumerations_controller.rb @@ -91,8 +91,10 @@ class EnumerationsController < ApplicationController def build_new_enumeration class_name = params[:enumeration] && params[:enumeration][:type] || params[:type] - @enumeration = Enumeration.new_subclass_instance(class_name, enumeration_params) - if @enumeration.nil? + @enumeration = Enumeration.new_subclass_instance(class_name) + if @enumeration + @enumeration.attributes = enumeration_params || {} + else render_404 end end @@ -105,6 +107,7 @@ class EnumerationsController < ApplicationController def enumeration_params # can't require enumeration on #new action - params.permit(:enumeration => [:name, :active, :is_default, :position])[:enumeration] + cf_ids = @enumeration.available_custom_fields.map{|c| c.id.to_s} + params.permit(:enumeration => [:name, :active, :is_default, :position, :custom_field_values => cf_ids])[:enumeration] end end diff --git a/app/controllers/issues_controller.rb b/app/controllers/issues_controller.rb index 23d3992..69a947b 100644 --- a/app/controllers/issues_controller.rb +++ b/app/controllers/issues_controller.rb @@ -40,7 +40,8 @@ class IssuesController < ApplicationController helper :timelog def index - retrieve_query + use_session = !request.format.csv? + retrieve_query(IssueQuery, use_session) if @query.valid? respond_to do |format| @@ -367,7 +368,12 @@ class IssuesController < ApplicationController when 'destroy' # nothing to do when 'nullify' + if Setting.timelog_required_fields.include?('issue_id') + flash.now[:error] = l(:field_issue) + " " + ::I18n.t('activerecord.errors.messages.blank') + return + else time_entries.update_all(:issue_id => nil) + end when 'reassign' reassign_to = @project && @project.issues.find_by_id(params[:reassign_to_id]) if reassign_to.nil? diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index 36bae86..ce9e21f 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -37,7 +37,7 @@ class SearchController < ApplicationController end # quick jump to an issue - if (m = @question.match(/^#?(\d+)$/)) && (issue = Issue.visible.find_by_id(m[1].to_i)) + if !api_request? && (m = @question.match(/^#?(\d+)$/)) && (issue = Issue.visible.find_by_id(m[1].to_i)) redirect_to issue_path(issue) return end @@ -49,7 +49,7 @@ class SearchController < ApplicationController when 'my_projects' User.current.projects when 'subprojects' - @project ? (@project.self_and_descendants.active.to_a) : nil + @project ? (@project.self_and_descendants.to_a) : nil else @project end diff --git a/app/controllers/timelog_controller.rb b/app/controllers/timelog_controller.rb index 18b911c..df6b618 100644 --- a/app/controllers/timelog_controller.rb +++ b/app/controllers/timelog_controller.rb @@ -114,6 +114,7 @@ class TimelogController < ApplicationController :time_entry => { :project_id => params[:time_entry][:project_id], :issue_id => @time_entry.issue_id, + :spent_on => @time_entry.spent_on, :activity_id => @time_entry.activity_id }, :back_url => params[:back_url] diff --git a/app/controllers/trackers_controller.rb b/app/controllers/trackers_controller.rb index b0e6979..caf6b11 100644 --- a/app/controllers/trackers_controller.rb +++ b/app/controllers/trackers_controller.rb @@ -106,6 +106,6 @@ class TrackersController < ApplicationController return end @trackers = Tracker.sorted.to_a - @custom_fields = IssueCustomField.all.sort + @custom_fields = IssueCustomField.sorted end end diff --git a/app/controllers/wiki_controller.rb b/app/controllers/wiki_controller.rb index 8bad792..a28e2bd 100644 --- a/app/controllers/wiki_controller.rb +++ b/app/controllers/wiki_controller.rb @@ -32,7 +32,7 @@ class WikiController < ApplicationController default_search_scope :wiki_pages before_action :find_wiki, :authorize - before_action :find_existing_or_new_page, :only => [:show, :edit, :update] + before_action :find_existing_or_new_page, :only => [:show, :edit] before_action :find_existing_page, :only => [:rename, :protect, :history, :diff, :annotate, :add_attachment, :destroy, :destroy_version] before_action :find_attachments, :only => [:preview] accept_api_auth :index, :show, :update, :destroy @@ -42,8 +42,6 @@ class WikiController < ApplicationController helper :watchers include Redmine::Export::PDF - include ActionView::Helpers::SanitizeHelper - # List of pages, sorted alphabetically and by parent (hierarchy) def index load_pages_for_index @@ -109,7 +107,7 @@ class WikiController < ApplicationController send_data(export, :type => 'text/html', :filename => filename_for_content_disposition("#{@page.title}.html")) return elsif params[:format] == 'txt' - send_data(strip_tags(@content.text), :type => 'text/plain', :filename => filename_for_content_disposition("#{@page.title}.txt")) + send_data(@content.text, :type => 'text/plain', :filename => filename_for_content_disposition("#{@page.title}.txt")) return end end @@ -152,6 +150,8 @@ class WikiController < ApplicationController # Creates a new page or updates an existing one def update + @page = @wiki.find_or_new_page(params[:id]) + return render_403 unless editable? was_new_page = @page.new_record? @page.safe_attributes = params[:wiki_page] diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index c5d4c76..792af98 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -1272,7 +1272,7 @@ module ApplicationHelper link_to_function '', "toggleCheckboxesBySelector('#{selector}')", :title => "#{l(:button_check_all)} / #{l(:button_uncheck_all)}", - :class => 'toggle-checkboxes' + :class => 'icon icon-checked' end def progress_bar(pcts, options={}) @@ -1423,10 +1423,13 @@ module ApplicationHelper end if email.present? gravatar(email.to_s.downcase, options) rescue nil - else + elsif user.is_a?(AnonymousUser) + options[:size] &&= options[:size].to_s image_tag 'anonymous.png', GravatarHelper::DEFAULT_OPTIONS .except(:default, :rating, :ssl).merge(options) + else + nil end else '' diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index 020fed7..181907e 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -127,8 +127,8 @@ module IssuesHelper content_tag('td', check_box_tag("ids[]", other_issue.id, false, :id => nil), :class => 'checkbox') + content_tag('td', relation.to_s(@issue) {|other| link_to_issue(other, :project => Setting.cross_project_issue_relations?)}.html_safe, :class => 'subject', :style => 'width: 50%') + content_tag('td', other_issue.status, :class => 'status') + - content_tag('td', other_issue.start_date, :class => 'start_date') + - content_tag('td', other_issue.due_date, :class => 'due_date') + + content_tag('td', format_date(other_issue.start_date), :class => 'start_date') + + content_tag('td', format_date(other_issue.due_date), :class => 'due_date') + content_tag('td', other_issue.disabled_core_fields.include?('done_ratio') ? '' : progress_bar(other_issue.done_ratio), :class=> 'done_ratio') + content_tag('td', link, :class => 'buttons'), :id => "relation-#{relation.id}", @@ -246,8 +246,12 @@ module IssuesHelper issue_fields_rows do |rows| values.each_with_index do |value, i| css = "cf_#{value.custom_field.id}" + attr_value = show_value(value) + if value.custom_field.text_formatting == 'full' + attr_value = content_tag('div', attr_value, class: 'wiki') + end m = (i < half ? :left : :right) - rows.send m, custom_field_name_tag(value.custom_field), show_value(value), :class => css + rows.send m, custom_field_name_tag(value.custom_field), attr_value, :class => css end end end @@ -310,7 +314,7 @@ module IssuesHelper # Returns an array of users that are proposed as watchers # on the new issue form def users_for_new_issue_watchers(issue) - users = issue.watcher_users + users = issue.watcher_users.select{|u| u.status == User::STATUS_ACTIVE} if issue.project.users.count <= 20 users = (users + issue.project.users.sort).uniq end diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index 40c59f2..8788711 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -18,14 +18,11 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. module SearchHelper - include ActionView::Helpers::SanitizeHelper - def highlight_tokens(text, tokens) return text unless text && tokens && !tokens.empty? re_tokens = tokens.collect {|t| Regexp.escape(t)} regexp = Regexp.new "(#{re_tokens.join('|')})", Regexp::IGNORECASE result = '' - text = strip_tags(text) text.split(regexp).each_with_index do |words, i| if result.length > 1200 # maximum length of the preview reached diff --git a/app/models/attachment.rb b/app/models/attachment.rb index eea8013..64ac2c2 100644 --- a/app/models/attachment.rb +++ b/app/models/attachment.rb @@ -156,7 +156,7 @@ class Attachment < ActiveRecord::Base end def title - title = filename.to_s + title = filename.dup if description.present? title << " (#{description})" end diff --git a/app/models/import.rb b/app/models/import.rb index d2c53ba..71bc3c1 100644 --- a/app/models/import.rb +++ b/app/models/import.rb @@ -217,6 +217,7 @@ class Import < ActiveRecord::Base csv_options = {:headers => false} csv_options[:encoding] = settings['encoding'].to_s.presence || 'UTF-8' + csv_options[:encoding] = 'bom|UTF-8' if csv_options[:encoding] == 'UTF-8' separator = settings['separator'].to_s csv_options[:col_sep] = separator if separator.size == 1 wrapper = settings['wrapper'].to_s diff --git a/app/models/issue.rb b/app/models/issue.rb index c84377d..f995903 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -275,7 +275,8 @@ class Issue < ActiveRecord::Base end end unless options[:watchers] == false - self.watcher_user_ids = issue.watcher_user_ids.dup + self.watcher_user_ids = + issue.watcher_users.select{|u| u.status == User::STATUS_ACTIVE}.map(&:id) end @copied_from = issue @copy_options = options @@ -1086,7 +1087,7 @@ class Issue < ActiveRecord::Base if leaf? estimated_hours else - @total_estimated_hours ||= self_and_descendants.sum(:estimated_hours) + @total_estimated_hours ||= self_and_descendants.visible.sum(:estimated_hours) end end @@ -1300,7 +1301,7 @@ class Issue < ActiveRecord::Base # Reschedules the issue on the given date or the next working day and saves the record. # If the issue is a parent task, this is done by rescheduling its subtasks. - def reschedule_on!(date) + def reschedule_on!(date, journal=nil) return if date.nil? if leaf? || !dates_derived? if start_date.nil? || start_date != date @@ -1308,6 +1309,9 @@ class Issue < ActiveRecord::Base # Issue can not be moved earlier than its soonest start date date = [soonest_start(true), date].compact.max end + if journal + init_journal(journal.user) + end reschedule_on(date) begin save @@ -1631,6 +1635,8 @@ class Issue < ActiveRecord::Base copy.author = author copy.project = project copy.parent_issue_id = copied_issue_ids[child.parent_id] + copy.fixed_version_id = nil unless child.fixed_version.present? && child.fixed_version.status == 'open' + copy.assigned_to = nil unless child.assigned_to_id.present? && child.assigned_to.status == User::STATUS_ACTIVE unless copy.save logger.error "Could not copy subtask ##{child.id} while copying ##{@copied_from.id} to ##{id} due to validation errors: #{copy.errors.full_messages.join(', ')}" if logger next @@ -1789,7 +1795,7 @@ class Issue < ActiveRecord::Base def reschedule_following_issues if start_date_changed? || due_date_changed? relations_from.each do |relation| - relation.set_issue_to_dates + relation.set_issue_to_dates(@current_journal) end end end diff --git a/app/models/issue_import.rb b/app/models/issue_import.rb index ad04c0b..9fc4f55 100644 --- a/app/models/issue_import.rb +++ b/app/models/issue_import.rb @@ -122,7 +122,10 @@ class IssueImport < Import end end if issue.project && version_name = row_value(row, 'fixed_version') - if version = issue.project.versions.named(version_name).first + version = + issue.project.versions.named(version_name).first || + issue.project.shared_versions.named(version_name).first + if version attributes['fixed_version_id'] = version.id elsif create_versions? version = issue.project.versions.build diff --git a/app/models/issue_query.rb b/app/models/issue_query.rb index f852c14..c833607 100644 --- a/app/models/issue_query.rb +++ b/app/models/issue_query.rb @@ -37,8 +37,8 @@ class IssueQuery < Query QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"), QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours", :totalable => true), QueryColumn.new(:total_estimated_hours, - :sortable => "COALESCE((SELECT SUM(estimated_hours) FROM #{Issue.table_name} subtasks" + - " WHERE subtasks.root_id = #{Issue.table_name}.root_id AND subtasks.lft >= #{Issue.table_name}.lft AND subtasks.rgt <= #{Issue.table_name}.rgt), 0)", + :sortable => -> { "COALESCE((SELECT SUM(estimated_hours) FROM #{Issue.table_name} subtasks" + + " WHERE #{Issue.visible_condition(User.current).gsub(/\bissues\b/, 'subtasks')} AND subtasks.root_id = #{Issue.table_name}.root_id AND subtasks.lft >= #{Issue.table_name}.lft AND subtasks.rgt <= #{Issue.table_name}.rgt), 0)" }, :default_order => 'desc'), QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true), QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'), @@ -221,6 +221,7 @@ class IssueQuery < Query end disabled_fields = Tracker.disabled_core_fields(trackers).map {|field| field.sub(/_id$/, '')} + disabled_fields << "total_estimated_hours" if disabled_fields.include?("estimated_hours") @available_columns.reject! {|column| disabled_fields.include?(column.name.to_s) } @@ -373,7 +374,7 @@ class IssueQuery < Query neg = (operator == '!' ? 'NOT' : '') subquery = "SELECT 1 FROM #{Journal.table_name} sj" + " WHERE sj.journalized_type='Issue' AND sj.journalized_id=#{Issue.table_name}.id AND (#{sql_for_field field, '=', value, 'sj', 'user_id'})" + - " AND sj.id = (SELECT MAX(#{Journal.table_name}.id) FROM #{Journal.table_name}" + + " AND sj.id IN (SELECT MAX(#{Journal.table_name}.id) FROM #{Journal.table_name}" + " WHERE #{Journal.table_name}.journalized_type='Issue' AND #{Journal.table_name}.journalized_id=#{Issue.table_name}.id" + " AND (#{Journal.visible_notes_condition(User.current, :skip_pre_condition => true)}))" @@ -382,8 +383,26 @@ class IssueQuery < Query def sql_for_watcher_id_field(field, operator, value) db_table = Watcher.table_name - "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND " + - sql_for_field(field, '=', value, db_table, 'user_id') + ')' + + me, others = value.partition { |id| ['0', User.current.id.to_s].include?(id) } + sql = if others.any? + "SELECT #{Issue.table_name}.id FROM #{Issue.table_name} " + + "INNER JOIN #{db_table} ON #{Issue.table_name}.id = #{db_table}.watchable_id AND #{db_table}.watchable_type = 'Issue' " + + "LEFT OUTER JOIN #{Project.table_name} ON #{Project.table_name}.id = #{Issue.table_name}.project_id " + + "WHERE (" + + sql_for_field(field, '=', me, db_table, 'user_id') + + ') OR (' + + Project.allowed_to_condition(User.current, :view_issue_watchers) + + ' AND ' + + sql_for_field(field, '=', others, db_table, 'user_id') + + ')' + else + "SELECT #{db_table}.watchable_id FROM #{db_table} " + + "WHERE #{db_table}.watchable_type='Issue' AND " + + sql_for_field(field, '=', me, db_table, 'user_id') + end + + "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (#{sql})" end def sql_for_member_of_group_field(field, operator, value) diff --git a/app/models/issue_relation.rb b/app/models/issue_relation.rb index d38825e..f23f744 100644 --- a/app/models/issue_relation.rb +++ b/app/models/issue_relation.rb @@ -176,10 +176,10 @@ class IssueRelation < ActiveRecord::Base set_issue_to_dates end - def set_issue_to_dates + def set_issue_to_dates(journal=nil) soonest_start = self.successor_soonest_start if soonest_start && issue_to - issue_to.reschedule_on!(soonest_start) + issue_to.reschedule_on!(soonest_start, journal) end end diff --git a/app/models/mail_handler.rb b/app/models/mail_handler.rb index 518e8c8..2390339 100755 --- a/app/models/mail_handler.rb +++ b/app/models/mail_handler.rb @@ -54,7 +54,7 @@ class MailHandler < ActionMailer::Base def self.safe_receive(*args) receive(*args) rescue Exception => e - logger.error "MailHandler: an unexpected error occurred when receiving email: #{e.message}" if logger + Rails.logger.error "MailHandler: an unexpected error occurred when receiving email: #{e.message}" return false end @@ -65,7 +65,7 @@ class MailHandler < ActionMailer::Base %w(project status tracker category priority assigned_to fixed_version).each do |option| options[:issue][option.to_sym] = env[option] if env[option] end - %w(allow_override unknown_user no_permission_check no_account_notice default_group project_from_subaddress).each do |option| + %w(allow_override unknown_user no_permission_check no_account_notice no_notification default_group project_from_subaddress).each do |option| options[option.to_sym] = env[option] if env[option] end if env['private'] @@ -250,8 +250,8 @@ class MailHandler < ActionMailer::Base # add To and Cc as watchers before saving so the watchers can reply to Redmine add_watchers(issue) - add_attachments(issue) issue.save! + add_attachments(issue) if logger logger.info "MailHandler: issue ##{issue.id} updated by #{user}" end @@ -286,7 +286,7 @@ class MailHandler < ActionMailer::Base reply else if logger - logger.info "MailHandler: ignoring reply from [#{sender_email}] to a locked topic" + logger.info "MailHandler: ignoring reply from [#{email.from.first}] to a locked topic" end end end diff --git a/app/models/mailer.rb b/app/models/mailer.rb index 316b4e3..0c8c55c 100644 --- a/app/models/mailer.rb +++ b/app/models/mailer.rb @@ -311,7 +311,7 @@ class Mailer < ActionMailer::Base end # Notifies user that his password was updated - def self.password_updated(user) + def self.password_updated(user, options={}) # Don't send a notification to the dummy email address when changing the password # of the default admin account which is required after the first login # TODO: maybe not the best way to handle this @@ -320,6 +320,8 @@ class Mailer < ActionMailer::Base security_notification(user, message: :mail_body_password_updated, title: :button_change_password, + remote_ip: options[:remote_ip], + originator: user, url: {controller: 'my', action: 'password'} ).deliver end @@ -333,7 +335,6 @@ class Mailer < ActionMailer::Base end def security_notification(recipients, options={}) - redmine_headers 'Sender' => User.current.login @user = Array(recipients).detect{|r| r.is_a? User } set_language_if_valid(@user.try :language) @message = l(options[:message], @@ -341,7 +342,11 @@ class Mailer < ActionMailer::Base value: options[:value] ) @title = options[:title] && l(options[:title]) + @originator = options[:originator] || User.current + @remote_ip = options[:remote_ip] || @originator.remote_ip @url = options[:url] && (options[:url].is_a?(Hash) ? url_for(options[:url]) : options[:url]) + redmine_headers 'Sender' => @originator.login + redmine_headers 'Url' => @url mail :to => recipients, :subject => "[#{Setting.app_title}] #{l(:mail_subject_security_notification)}" end diff --git a/app/models/query.rb b/app/models/query.rb index c8c8986..bfa010d 100644 --- a/app/models/query.rb +++ b/app/models/query.rb @@ -319,9 +319,10 @@ class Query < ActiveRecord::Base " INNER JOIN #{table_name_prefix}queries_roles#{table_name_suffix} qr on qr.query_id = q.id" + " INNER JOIN #{MemberRole.table_name} mr ON mr.role_id = qr.role_id" + " INNER JOIN #{Member.table_name} m ON m.id = mr.member_id AND m.user_id = ?" + + " INNER JOIN #{Project.table_name} p ON p.id = m.project_id AND p.status <> ?" + " WHERE q.project_id IS NULL OR q.project_id = m.project_id))" + " OR #{table_name}.user_id = ?", - VISIBILITY_PUBLIC, VISIBILITY_ROLES, user.id, user.id) + VISIBILITY_PUBLIC, VISIBILITY_ROLES, user.id, Project::STATUS_ARCHIVED, user.id) elsif user.logged? scope.where("#{table_name}.visibility = ? OR #{table_name}.user_id = ?", VISIBILITY_PUBLIC, user.id) else @@ -340,7 +341,7 @@ class Query < ActiveRecord::Base if project (user.roles_for_project(project) & roles).any? else - Member.where(:user_id => user.id).joins(:roles).where(:member_roles => {:role_id => roles.map(&:id)}).any? + user.memberships.joins(:member_roles).where(:member_roles => {:role_id => roles.map(&:id)}).any? end else user == self.user @@ -398,6 +399,8 @@ class Query < ActiveRecord::Base params[:v][field] = options[:values] end params[:c] = column_names + params[:group_by] = group_by.to_s if group_by.present? + params[:t] = totalable_names.map(&:to_s) if totalable_names.any? params[:sort] = sort_criteria.to_param params[:set_filter] = 1 params diff --git a/app/models/version.rb b/app/models/version.rb index b489389..ee1d3da 100644 --- a/app/models/version.rb +++ b/app/models/version.rb @@ -45,7 +45,7 @@ class Version < ActiveRecord::Base scope :like, lambda {|arg| if arg.present? pattern = "%#{arg.to_s.strip}%" - where("LOWER(#{Version.table_name}.name) LIKE :p", :p => pattern) + where([Redmine::Database.like("#{Version.table_name}.name", '?'), pattern]) end } scope :open, lambda { where(:status => 'open') } @@ -268,7 +268,7 @@ class Version < ActiveRecord::Base end def deletable? - fixed_issues.empty? && !referenced_by_a_custom_field? + fixed_issues.empty? && !referenced_by_a_custom_field? && attachments.empty? end def default_project_version diff --git a/app/views/account/login.html.erb b/app/views/account/login.html.erb index 5e48de8..1440e32 100644 --- a/app/views/account/login.html.erb +++ b/app/views/account/login.html.erb @@ -1,7 +1,6 @@ <%= call_hook :view_account_login_top %>
<%= Redmine::Info.environment %>
<%= plugin.name %> - <%= content_tag('span', plugin.description, :class => 'description') unless plugin.description.blank? %> - <%= content_tag('span', link_to(plugin.url, plugin.url), :class => 'url') unless plugin.url.blank? %> - | -<%= plugin.author_url.blank? ? plugin.author : link_to(plugin.author, plugin.author_url) %> | -<%= plugin.version %> | -<%= link_to(l(:button_configure), plugin_settings_path(plugin)) if plugin.configurable? %> | -
<%= plugin.name %> + <%= content_tag('span', plugin.description, :class => 'description') unless plugin.description.blank? %> + <%= content_tag('span', link_to(plugin.url, plugin.url), :class => 'url') unless plugin.url.blank? %> + | +<%= plugin.author_url.blank? ? plugin.author : link_to(plugin.author, plugin.author_url) %> | +<%= plugin.version %> | +<%= link_to(l(:button_configure), plugin_settings_path(plugin)) if plugin.configurable? %> | +
<%= l(:label_check_for_updates) %>
<% else %><%= l(:label_no_data) %>
@@ -27,7 +29,7 @@ $(document).ready(function(){ dataType: "jsonp", url: "https://www.redmine.org/plugins/check_updates", data: <%= raw_json plugin_data_for_updates(@plugins) %>, - timeout: 3000, + timeout: 10000, beforeSend: function(){ $('#ajax-indicator').show(); }, diff --git a/app/views/context_menus/issues.html.erb b/app/views/context_menus/issues.html.erb index c24a7ae..244f9c5 100644 --- a/app/views/context_menus/issues.html.erb +++ b/app/views/context_menus/issues.html.erb @@ -39,7 +39,7 @@<%= l(:field_digest) %> | -- | ||||
---|---|---|---|---|---|
- <%= link_to(container, {:controller => 'versions', :action => 'show', :id => container}, :class => "icon icon-package") %> - | -|||||
<%= link_to_attachment file, :title => file.description -%> | -<%= format_time(file.created_on) %> | -<%= number_to_human_size(file.filesize) %> | -<%= file.downloads %> | -<%= file.digest_type %>: <%= file.digest %> | -- <%= link_to_attachment file, class: 'icon-only icon-download', title: l(:button_download), download: true %> - <%= link_to(l(:button_delete), attachment_path(file), :class => 'icon-only icon-del', - :data => {:confirm => l(:text_are_you_sure)}, :method => :delete) if delete_allowed %> - | -
<%= l(:field_digest) %> | ++ | ||||
---|---|---|---|---|---|
+ <%= link_to(container, {:controller => 'versions', :action => 'show', :id => container}, :class => "icon icon-package") %> + | +|||||
<%= link_to_attachment file, :title => file.description -%> | +<%= format_time(file.created_on) %> | +<%= number_to_human_size(file.filesize) %> | +<%= file.downloads %> | +<%= file.digest_type %>: <%= file.digest %> | ++ <%= link_to_attachment file, class: 'icon-only icon-download', title: l(:button_download), download: true %> + <%= link_to(l(:button_delete), attachment_path(file), :class => 'icon-only icon-del', + :data => {:confirm => l(:text_are_you_sure)}, :method => :delete) if delete_allowed %> + | +
<%= l(:text_destroy_time_entries_question, :hours => number_with_precision(@hours, :precision => 2)) %>
+<% unless Setting.timelog_required_fields.include?('issue_id') %>
+<% end %>
<% if @project %>
<%= text_field_tag 'reassign_to_id', params[:reassign_to_id], :size => 6, :onfocus => '$("#todo_reassign").attr("checked", true);' %>
diff --git a/app/views/layouts/base.html.erb b/app/views/layouts/base.html.erb
index aeb10d9..48ba562 100644
--- a/app/views/layouts/base.html.erb
+++ b/app/views/layouts/base.html.erb
@@ -2,31 +2,28 @@