Redmine 4.1.7

This commit is contained in:
Manuel Cillero 2023-07-07 08:08:27 +02:00
parent 55458d3479
commit 3ca3c37487
103 changed files with 2426 additions and 431 deletions

View file

@ -297,6 +297,7 @@ class AccountController < ApplicationController
:value => token,
:expires => 1.year.from_now,
:path => (Redmine::Configuration['autologin_cookie_path'] || RedmineApp::Application.config.relative_url_root || '/'),
:same_site => :lax,
:secure => secure,
:httponly => true
}

View file

@ -33,7 +33,7 @@ class ActivitiesController < ApplicationController
@date_from = @date_to - @days
@with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1')
if params[:user_id].present?
@author = User.active.find(params[:user_id])
@author = User.visible.active.find(params[:user_id])
end
@activity = Redmine::Activity::Fetcher.new(User.current, :project => @project,
@ -55,7 +55,12 @@ class ActivitiesController < ApplicationController
end
end
events = @activity.events(@date_from, @date_to)
events =
if params[:format] == 'atom'
@activity.events(nil, nil, :limit => Setting.feeds_limit.to_i)
else
@activity.events(@date_from, @date_to)
end
if events.empty? || stale?(:etag => [@activity.scope, @date_to, @date_from, @with_subprojects, @author, events.first, events.size, User.current, current_language])
respond_to do |format|

View file

@ -101,7 +101,7 @@ class AttachmentsController < ApplicationController
return
end
@attachment = Attachment.new(:file => request.raw_post)
@attachment = Attachment.new(:file => raw_request_body)
@attachment.author = User.current
@attachment.filename = params[:filename].presence || Redmine::Utils.random_hex(16)
@attachment.content_type = params[:content_type].presence
@ -265,4 +265,14 @@ class AttachmentsController < ApplicationController
def update_all_params
params.permit(:attachments => [:filename, :description]).require(:attachments)
end
# Get an IO-like object for the request body which is usable to create a new
# attachment. We try to avoid having to read the whole body into memory.
def raw_request_body
if request.body.respond_to?(:size)
request.body
else
request.raw_post
end
end
end

View file

@ -18,6 +18,8 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class MailHandlerController < ActionController::Base
include ActiveSupport::SecurityUtils
before_action :check_credential
# Displays the email submission form
@ -39,7 +41,7 @@ class MailHandlerController < ActionController::Base
def check_credential
User.current = nil
unless Setting.mail_handler_api_enabled? && params[:key].to_s == Setting.mail_handler_api_key
unless Setting.mail_handler_api_enabled? && secure_compare(params[:key].to_s, Setting.mail_handler_api_key.to_s)
render :plain => 'Access denied. Incoming emails WS is disabled or key is invalid.', :status => 403
end
end

View file

@ -307,7 +307,7 @@ class RepositoriesController < ApplicationController
render_404
end
REV_PARAM_RE = %r{\A[a-f0-9]*\Z}i
REV_PARAM_RE = %r{\A[a-f0-9]*\z}i
def find_project_repository
@project = Project.find(params[:id])
@ -318,14 +318,12 @@ class RepositoriesController < ApplicationController
end
(render_404; return false) unless @repository
@path = params[:path].is_a?(Array) ? params[:path].join('/') : params[:path].to_s
@rev = params[:rev].blank? ? @repository.default_branch : params[:rev].to_s.strip
@rev_to = params[:rev_to]
unless REV_PARAM_RE.match?(@rev.to_s) && REV_PARAM_RE.match?(@rev_to.to_s)
if @repository.branches.blank?
raise InvalidRevisionParam
end
end
@rev = params[:rev].to_s.strip.presence || @repository.default_branch
raise InvalidRevisionParam unless valid_name?(@rev)
@rev_to = params[:rev_to].to_s.strip.presence
raise InvalidRevisionParam unless valid_name?(@rev_to)
rescue ActiveRecord::RecordNotFound
render_404
rescue InvalidRevisionParam
@ -410,4 +408,11 @@ class RepositoriesController < ApplicationController
'attachment'
end
end
def valid_name?(rev)
return true if rev.nil?
return true if REV_PARAM_RE.match?(rev)
@repository ? @repository.valid_name?(rev) : true
end
end

View file

@ -63,7 +63,7 @@ class SearchController < ApplicationController
@object_types = @object_types.select {|o| User.current.allowed_to?("view_#{o}".to_sym, projects_to_search)}
end
@scope = @object_types.select {|t| params[t]}
@scope = @object_types.select {|t| params[t].present?}
@scope = @object_types if @scope.empty?
fetcher = Redmine::Search::Fetcher.new(

View file

@ -18,6 +18,8 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class SysController < ActionController::Base
include ActiveSupport::SecurityUtils
before_action :check_enabled
def projects
@ -76,7 +78,7 @@ class SysController < ActionController::Base
def check_enabled
User.current = nil
unless Setting.sys_api_enabled? && params[:key].to_s == Setting.sys_api_key
unless Setting.sys_api_enabled? && secure_compare(params[:key].to_s, Setting.sys_api_key.to_s)
render :plain => 'Access denied. Repository management WS is disabled or key is invalid.', :status => 403
return false
end

View file

@ -134,7 +134,9 @@ class WatchersController < ApplicationController
def find_objets_from_params
klass = Object.const_get(params[:object_type].camelcase) rescue nil
return unless klass && klass.respond_to?('watched_by')
return unless klass && Class === klass # rubocop:disable Style/CaseEquality
return unless klass < ActiveRecord::Base
return unless klass < Redmine::Acts::Watchable::InstanceMethods
scope = klass.where(:id => Array.wrap(params[:object_id]))
if klass.reflect_on_association(:project)

View file

@ -44,8 +44,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
@ -91,7 +89,7 @@ class WikiController < ApplicationController
end
@content = @page.content_for_version(params[:version])
if @content.nil?
if User.current.allowed_to?(:edit_wiki_pages, @project) && editable? && !api_request?
if params[:version].blank? && User.current.allowed_to?(:edit_wiki_pages, @project) && editable? && !api_request?
edit
render :action => 'edit'
else
@ -111,7 +109,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

View file

@ -1190,7 +1190,7 @@ module ApplicationHelper
)|
(
(?<sep4>@)
(?<identifier3>[A-Za-z0-9_\-@\.]*)
(?<identifier3>[A-Za-z0-9_\-@\.]*?)
)
)
(?=
@ -1596,7 +1596,7 @@ module ApplicationHelper
# Returns the javascript tags that are included in the html layout head
def javascript_heads
tags = javascript_include_tag('jquery-2.2.4-ui-1.11.0-ujs-5.2.3', 'tribute-3.7.3.min', 'application', 'responsive')
tags = javascript_include_tag('jquery-2.2.4-ui-1.11.0-ujs-5.2.4.5', 'tribute-3.7.3.min', 'application', 'responsive')
unless User.current.pref.warn_on_leaving_unsaved == '0'
tags << "\n".html_safe + javascript_tag("$(window).on('load', function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });")
end

View file

@ -217,6 +217,11 @@ module IssuesHelper
if issue.total_spent_hours == issue.spent_hours
link_to(l_hours_short(issue.spent_hours), path)
else
# link to global time entries if cross-project subtasks are allowed
# in order to show time entries from not descendents projects
if %w(system tree hierarchy).include?(Setting.cross_project_subtasks)
path = time_entries_path(:issue_id => "~#{issue.id}")
end
s = issue.spent_hours > 0 ? l_hours_short(issue.spent_hours) : ""
s += " (#{l(:label_total)}: #{link_to l_hours_short(issue.total_spent_hours), path})"
s.html_safe
@ -551,7 +556,7 @@ module IssuesHelper
unless no_html
diff_link =
link_to(
'diff',
l(:label_diff),
diff_journal_url(detail.journal_id, :detail_id => detail.id,
:only_path => options[:only_path]),
:title => l(:label_view_diff))
@ -577,6 +582,7 @@ module IssuesHelper
end
# Find the name of an associated record stored in the field attribute
# For project, return the associated record only if is visible for the current User
def find_name_by_reflection(field, id)
return nil if id.blank?
@detail_value_name_by_reflection ||= Hash.new do |hash, key|
@ -584,7 +590,7 @@ module IssuesHelper
name = nil
if association
record = association.klass.find_by_id(key.last)
if record
if (record && !record.is_a?(Project)) || (record.is_a?(Project) && record.visible?)
name = record.name.force_encoding('UTF-8')
end
end

View file

@ -22,7 +22,7 @@ module JournalsHelper
# Returns the attachments of a journal that are displayed as thumbnails
def journal_thumbnail_attachments(journal)
ids = journal.details.select {|d| d.property == 'attachment' && d.value.present?}.map(&:prop_key)
ids.any? ? Attachment.where(:id => ids).select(&:thumbnailable?) : []
ids.any? ? Attachment.where(:id => ids).select(&:thumbnailable?).sort_by{|a| ids.index(a.id.to_s)} : []
end
# Returns the action links for an issue journal

View file

@ -167,7 +167,7 @@ module QueriesHelper
def total_tag(column, value)
label = content_tag('span', "#{column.caption}:")
value =
if [:hours, :spent_hours, :total_spent_hours, :estimated_hours].include? column.name
if [:hours, :spent_hours, :total_spent_hours, :estimated_hours, :total_estimated_hours].include? column.name
format_hours(value)
else
format_object(value)
@ -238,7 +238,7 @@ module QueriesHelper
'span',
value.to_s(item) {|other| link_to_issue(other, :subject => false, :tracker => false)}.html_safe,
:class => value.css_classes_for(item))
when :hours, :estimated_hours
when :hours, :estimated_hours, :total_estimated_hours
format_hours(value)
when :spent_hours
link_to_if(value > 0, format_hours(value), project_time_entries_path(item.project, :issue_id => "#{item.id}"))

View file

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

View file

@ -44,7 +44,7 @@ module TimelogHelper
def user_collection_for_select_options(time_entry)
collection = time_entry.assignable_users
collection << time_entry.user unless time_entry.user.nil? && !collection.include?(time_entry.user)
collection << time_entry.user if time_entry.user && !collection.include?(time_entry.user)
principals_options_for_select(collection, time_entry.user_id.to_s)
end

View file

@ -29,7 +29,8 @@ class Attachment < ActiveRecord::Base
validates_length_of :filename, :maximum => 255
validates_length_of :disk_filename, :maximum => 255
validates_length_of :description, :maximum => 255
validate :validate_max_file_size, :validate_file_extension
validate :validate_max_file_size
validate :validate_file_extension, :if => :filename_changed?
acts_as_event :title => :filename,
:url => Proc.new {|o| {:controller => 'attachments', :action => 'show', :id => o.id, :filename => o.filename}}
@ -76,11 +77,9 @@ class Attachment < ActiveRecord::Base
end
def validate_file_extension
if @temp_file
extension = File.extname(filename)
unless self.class.valid_extension?(extension)
errors.add(:base, l(:error_attachment_extension_not_allowed, :extension => extension))
end
extension = File.extname(filename)
unless self.class.valid_extension?(extension)
errors.add(:base, l(:error_attachment_extension_not_allowed, :extension => extension))
end
end
@ -436,15 +435,15 @@ class Attachment < ActiveRecord::Base
private
def reuse_existing_file_if_possible
original_diskfile = nil
original_diskfile = diskfile
original_filename = disk_filename
reused = with_lock do
if existing = Attachment
.where(digest: self.digest, filesize: self.filesize)
.where('id <> ? and disk_filename <> ?',
self.id, self.disk_filename)
.first
.where.not(disk_filename: original_filename)
.order(:id)
.last
existing.with_lock do
original_diskfile = self.diskfile
existing_diskfile = existing.diskfile
if File.readable?(original_diskfile) &&
File.readable?(existing_diskfile) &&
@ -455,7 +454,7 @@ class Attachment < ActiveRecord::Base
end
end
end
if reused
if reused && Attachment.where(disk_filename: original_filename).none?
File.delete(original_diskfile)
end
rescue ActiveRecord::StatementInvalid, ActiveRecord::RecordNotFound

View file

@ -201,6 +201,11 @@ class Issue < ActiveRecord::Base
user_tracker_permission?(user, :delete_issues)
end
# Overrides Redmine::Acts::Attachable::InstanceMethods#attachments_deletable?
def attachments_deletable?(user=User.current)
attributes_editable?(user)
end
def initialize(attributes=nil, *args)
super
if new_record?
@ -471,7 +476,6 @@ class Issue < ActiveRecord::Base
'custom_field_values',
'custom_fields',
'lock_version',
'notes',
:if => lambda {|issue, user| issue.new_record? || issue.attributes_editable?(user)})
safe_attributes(
'notes',
@ -722,7 +726,7 @@ class Issue < ActiveRecord::Base
errors.add :start_date, :earlier_than_minimum_start_date, :date => format_date(soonest_start)
end
if fixed_version
if project && fixed_version
if !assignable_versions.include?(fixed_version)
errors.add :fixed_version_id, :inclusion
elsif reopening? && fixed_version.closed?
@ -737,7 +741,7 @@ class Issue < ActiveRecord::Base
end
end
if assigned_to_id_changed? && assigned_to_id.present?
if project && assigned_to_id_changed? && assigned_to_id.present?
unless assignable_users.include?(assigned_to)
errors.add :assigned_to_id, :invalid
end
@ -937,6 +941,8 @@ class Issue < ActiveRecord::Base
# Users the issue can be assigned to
def assignable_users
return [] if project.nil?
users = project.assignable_users(tracker).to_a
users << author if author && author.active?
if assigned_to_id_was.present? && assignee = Principal.find_by_id(assigned_to_id_was)
@ -948,6 +954,7 @@ class Issue < ActiveRecord::Base
# Versions that the issue can be assigned to
def assignable_versions
return @assignable_versions if @assignable_versions
return [] if project.nil?
versions = project.shared_versions.open.to_a
if fixed_version
@ -1704,12 +1711,12 @@ class Issue < ActiveRecord::Base
if children.any?
child_with_total_estimated_hours = children.select {|c| c.total_estimated_hours.to_f > 0.0}
if child_with_total_estimated_hours.any?
average = child_with_total_estimated_hours.map(&:total_estimated_hours).sum.to_f / child_with_total_estimated_hours.count
average = child_with_total_estimated_hours.map(&:total_estimated_hours).sum.to_d / child_with_total_estimated_hours.count
else
average = 1.0
average = 1.0.to_d
end
done = children.map {|c|
estimated = c.total_estimated_hours.to_f
estimated = (c.total_estimated_hours || 0.0).to_d
estimated = average unless estimated > 0.0
ratio = c.closed? ? 100 : (c.done_ratio || 0)
estimated * ratio
@ -1734,8 +1741,8 @@ class Issue < ActiveRecord::Base
# a different project and that is not systemwide shared
Issue.joins(:project, :fixed_version).
where("#{Issue.table_name}.fixed_version_id IS NOT NULL" +
" AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
" AND #{Version.table_name}.sharing <> 'system'").
" AND #{Issue.table_name}.project_id <> #{::Version.table_name}.project_id" +
" AND #{::Version.table_name}.sharing <> 'system'").
where(conditions).each do |issue|
next if issue.project.nil? || issue.fixed_version.nil?
unless issue.project.shared_versions.include?(issue.fixed_version)
@ -1756,7 +1763,7 @@ class Issue < ActiveRecord::Base
# Callback on file attachment
def attachment_added(attachment)
if current_journal && !attachment.new_record?
if current_journal && !attachment.new_record? && !copy?
current_journal.journalize_attachment(attachment, :added)
end
end

View file

@ -513,7 +513,8 @@ class IssueQuery < Query
def sql_for_fixed_version_status_field(field, operator, value)
where = sql_for_field(field, operator, value, Version.table_name, "status")
version_ids = versions(:conditions => [where]).map(&:id)
version_id_scope = project ? project.shared_versions : Version.visible
version_ids = version_id_scope.where(where).pluck(:id)
nl = operator == "!" ? "#{Issue.table_name}.fixed_version_id IS NULL OR" : ''
"(#{nl} #{sql_for_field("fixed_version_id", "=", version_ids, Issue.table_name, "fixed_version_id")})"
@ -521,7 +522,8 @@ class IssueQuery < Query
def sql_for_fixed_version_due_date_field(field, operator, value)
where = sql_for_field(field, operator, value, Version.table_name, "effective_date")
version_ids = versions(:conditions => [where]).map(&:id)
version_id_scope = project ? project.shared_versions : Version.visible
version_ids = version_id_scope.where(where).pluck(:id)
nl = operator == "!*" ? "#{Issue.table_name}.fixed_version_id IS NULL OR" : ''
"(#{nl} #{sql_for_field("fixed_version_id", "=", version_ids, Issue.table_name, "fixed_version_id")})"

7
app/models/mail_handler.rb Executable file → Normal file
View file

@ -225,9 +225,8 @@ class MailHandler < ActionMailer::Base
# check permission
unless handler_options[:no_permission_check]
unless user.allowed_to?(:add_issue_notes, issue.project) ||
user.allowed_to?(:edit_issues, issue.project)
raise UnauthorizedAction, "not allowed to add notes on issues to project [#{project.name}]"
unless issue.notes_addable?
raise UnauthorizedAction, "not allowed to add notes on issues to project [#{issue.project.name}]"
end
end
@ -276,7 +275,7 @@ class MailHandler < ActionMailer::Base
end
unless handler_options[:no_permission_check]
raise UnauthorizedAction, "not allowed to add messages to project [#{project.name}]" unless user.allowed_to?(:add_messages, message.project)
raise UnauthorizedAction, "not allowed to add messages to project [#{message.project.name}]" unless user.allowed_to?(:add_messages, message.project)
end
if !message.locked?

View file

@ -883,6 +883,17 @@ class Project < ActiveRecord::Base
end
end
# Overrides Redmine::Acts::Customizable::InstanceMethods#validate_custom_field_values
# so that custom values that are not editable are not validated (eg. a custom field that
# is marked as required should not trigger a validation error if the user is not allowed
# to edit this field).
def validate_custom_field_values
user = User.current
if new_record? || custom_field_values_changed?
editable_custom_field_values(user).each(&:validate_value)
end
end
# Returns the custom_field_values that can be edited by the given user
def editable_custom_field_values(user=nil)
visible_custom_field_values(user)

View file

@ -70,7 +70,7 @@ class ProjectQuery < Query
def available_columns
return @available_columns if @available_columns
@available_columns = self.class.available_columns.dup
@available_columns += ProjectCustomField.visible.
@available_columns += project_custom_fields.visible.
map {|cf| QueryCustomFieldColumn.new(cf) }
@available_columns
end

View file

@ -51,7 +51,7 @@ class QueryColumn
# Returns true if the column is sortable, otherwise false
def sortable?
!@sortable.nil?
@sortable.present?
end
def sortable
@ -501,7 +501,7 @@ class Query < ActiveRecord::Base
if has_filter?(field) || !filter.remote
options[:values] = filter.values
if options[:values] && values_for(field)
missing = Array(values_for(field)).select(&:present?) - options[:values].map(&:last)
missing = Array(values_for(field)).select(&:present?) - options[:values].map{|v| v[1]}
if missing.any? && respond_to?(method = "find_#{field}_filter_values")
options[:values] += send(method, missing)
end
@ -609,13 +609,18 @@ class Query < ActiveRecord::Base
if project
project.rolled_up_custom_fields
else
IssueCustomField.all
IssueCustomField.sorted
end
end
# Returns a scope of project custom fields that are available as columns or filters
def project_custom_fields
ProjectCustomField.all
ProjectCustomField.sorted
end
# Returns a scope of time entry custom fields that are available as columns or filters
def time_entry_custom_fields
TimeEntryCustomField.sorted
end
# Returns a scope of project statuses that are available as columns or filters
@ -866,10 +871,10 @@ class Query < ActiveRecord::Base
def project_statement
project_clauses = []
active_subprojects_ids = []
subprojects_ids = []
active_subprojects_ids = project.descendants.active.map(&:id) if project
if active_subprojects_ids.any?
subprojects_ids = project.descendants.where.not(status: Project::STATUS_ARCHIVED).ids if project
if subprojects_ids.any?
if has_filter?("subproject_id")
case operator_for("subproject_id")
when '='
@ -878,7 +883,7 @@ class Query < ActiveRecord::Base
project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
when '!'
# exclude the selected subprojects
ids = [project.id] + active_subprojects_ids - values_for("subproject_id").map(&:to_i)
ids = [project.id] + subprojects_ids - values_for("subproject_id").map(&:to_i)
project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
when '!*'
# main project only

View file

@ -461,6 +461,10 @@ class Repository < ActiveRecord::Base
scope
end
def valid_name?(name)
scm.valid_name?(name)
end
protected
# Validates repository url based against an optional regular expression

View file

@ -100,7 +100,8 @@ class Setting < ActiveRecord::Base
v = read_attribute(:value)
# Unserialize serialized settings
if available_settings[name]['serialized'] && v.is_a?(String)
v = YAML::load(v)
# YAML.load works as YAML.safe_load if Psych >= 4.0 is installed
v = YAML.respond_to?(:unsafe_load) ? YAML.unsafe_load(v) : YAML.load(v)
v = force_utf8_strings(v)
end
v = v.to_sym if available_settings[name]['format'] == 'symbol' && !v.blank?

View file

@ -113,6 +113,13 @@ class TimeEntry < ActiveRecord::Base
self.project_id = issue.project_id
end
@invalid_issue_id = nil
elsif user.allowed_to?(:log_time, issue.project) && issue.assigned_to_id_changed? && issue.previous_assignee == User.current
current_assignee = issue.assigned_to
issue.assigned_to = issue.previous_assignee
unless issue.visible?(user)
@invalid_issue_id = issue_id
end
issue.assigned_to = current_assignee
else
@invalid_issue_id = issue_id
end
@ -122,7 +129,14 @@ class TimeEntry < ActiveRecord::Base
else
@invalid_user_id = nil
end
# Delete assigned custom fields not visible by the user
editable_custom_field_ids = editable_custom_field_values(user).map {|v| v.custom_field_id.to_s}
self.custom_field_values.delete_if do |c|
!editable_custom_field_ids.include?(c.custom_field.id.to_s)
end
end
attrs
end
@ -193,7 +207,7 @@ class TimeEntry < ActiveRecord::Base
# Returns the custom_field_values that can be edited by the given user
def editable_custom_field_values(user=nil)
visible_custom_field_values
visible_custom_field_values(user)
end
# Returns the custom fields that can be edited by the given user

View file

@ -89,7 +89,7 @@ class TimeEntryQuery < Query
activities = (project ? project.activities : TimeEntryActivity.shared)
add_available_filter(
"activity_id",
:type => :list, :values => activities.map {|a| [a.name, a.id.to_s]}
:type => :list, :values => activities.map {|a| [a.name, (a.parent_id || a.id).to_s]}
)
add_available_filter(
"project.status",
@ -101,7 +101,7 @@ class TimeEntryQuery < Query
add_available_filter "comments", :type => :text
add_available_filter "hours", :type => :float
add_custom_fields_filters(TimeEntryCustomField)
add_custom_fields_filters(time_entry_custom_fields)
add_associations_custom_fields_filters :project
add_custom_fields_filters(issue_custom_fields, :issue)
add_associations_custom_fields_filters :user
@ -110,11 +110,11 @@ class TimeEntryQuery < Query
def available_columns
return @available_columns if @available_columns
@available_columns = self.class.available_columns.dup
@available_columns += TimeEntryCustomField.visible.
@available_columns += time_entry_custom_fields.visible.
map {|cf| QueryCustomFieldColumn.new(cf) }
@available_columns += issue_custom_fields.visible.
map {|cf| QueryAssociationCustomFieldColumn.new(:issue, cf, :totalable => false) }
@available_columns += ProjectCustomField.visible.
@available_columns += project_custom_fields.visible.
map {|cf| QueryAssociationCustomFieldColumn.new(:project, cf) }
@available_columns
end

View file

@ -115,11 +115,13 @@ class Token < ActiveRecord::Base
return nil unless action.present? && /\A[a-z0-9]+\z/i.match?(key)
token = Token.find_by(:action => action, :value => key)
if token && (token.action == action) && (token.value == key) && token.user
if validity_days.nil? || (token.created_on > validity_days.days.ago)
token
end
end
return unless token
return unless token.action == action
return unless ActiveSupport::SecurityUtils.secure_compare(token.value.to_s, key)
return unless token.user
return unless validity_days.nil? || (token.created_on > validity_days.days.ago)
token
end
def self.generate_token_value

View file

@ -155,11 +155,7 @@ class WikiPage < ActiveRecord::Base
end
def content_for_version(version=nil)
if content
result = content.versions.find_by_version(version.to_i) if version
result ||= content
result
end
(content && version) ? content.versions.find_by_version(version.to_i) : content
end
def diff(version_to=nil, version_from=nil)

View file

@ -1,28 +1,27 @@
<%= call_hook :view_account_login_top %>
<div id="login-form">
<h2><%= l(:label_login) %></h2>
<%= form_tag(signin_path, onsubmit: 'return keepAnchorOnSignIn(this);') do %>
<%= back_url_hidden_field_tag %>
<label for="username"><%=l(:field_login)%></label>
<%= text_field_tag 'username', params[:username], :tabindex => '1' %>
<label for="password">
<%=l(:field_password)%>
<%= link_to l(:label_password_lost), lost_password_path, :class => "lost_password" if Setting.lost_password? %>
</label>
<%= password_field_tag 'password', nil, :tabindex => '2' %>
<% if Setting.openid? %>
<label for="openid_url"><%=l(:field_identity_url)%></label>
<%= text_field_tag "openid_url", nil, :tabindex => '3' %>
<% end %>
<% if Setting.autologin? %>
<label for="autologin"><%= check_box_tag 'autologin', 1, false, :tabindex => 4 %> <%= l(:label_stay_logged_in) %></label>
<% end %>
<input type="submit" name="login" value="<%=l(:button_login)%>" tabindex="5" id="login-submit" />
<% end %>
</div>

View file

@ -1,5 +1,5 @@
<p>
<%= f.text_area :possible_values, :value => @custom_field.possible_values.to_a.join("\n"), :rows => 15 %>
<%= f.text_area :possible_values, :value => @custom_field.possible_values.to_a.join("\n"), :rows => 15, :required => true %>
<em class="info"><%= l(:text_custom_field_possible_values_info) %></em>
</p>
<p><%= f.text_field(:default_value) %></p>

View file

@ -1,24 +1,22 @@
<%= error_messages_for @document %>
<div class="box tabular">
<p><%= f.select :category_id, DocumentCategory.active.collect {|c| [c.name, c.id]} %></p>
<p><%= f.text_field :title, :required => true, :size => 60 %></p>
<p><%= f.text_area :description, :cols => 60, :rows => 15, :class => 'wiki-edit',
:data => {
:auto_complete => true,
:issues_url => auto_complete_issues_path(:project_id => @project, :q => '')
}
%></p>
<p><%= f.select :category_id, DocumentCategory.active.collect {|c| [c.name, c.id]} %></p>
<p><%= f.text_field :title, :required => true, :size => 60 %></p>
<p><%= f.text_area :description, :cols => 60, :rows => 15, :class => 'wiki-edit',
:data => {
:auto_complete => true,
:issues_url => auto_complete_issues_path(:project_id => @project, :q => '')
}
%></p>
<% @document.custom_field_values.each do |value| %>
<p><%= custom_field_tag_with_label :document, value %></p>
<% end %>
<% @document.custom_field_values.each do |value| %>
<p><%= custom_field_tag_with_label :document, value %></p>
<% end %>
<% if @document.new_record? %>
<p><label><%=l(:label_attachment_plural)%></label><%= render :partial => 'attachments/form', :locals => {:container => @document} %></p>
<% end %>
</div>
<%= wikitoolbar_for 'document_description' %>
<% if @document.new_record? %>
<div class="box tabular">
<p><label><%=l(:label_attachment_plural)%></label><%= render :partial => 'attachments/form', :locals => {:container => @document} %></p>
</div>
<% end %>

View file

@ -177,14 +177,17 @@
<% end %>
<% end %>
</td>
<% @query.columns.each do |column| %>
<% next if Redmine::Helpers::Gantt::UNAVAILABLE_COLUMNS.include?(column.name) %>
<td class="gantt_<%= column.name %>_column gantt_selected_column <%= 'last_gantt_selected_column' if @query.columns.last == column %>" id="<%= column.name %>">
<%
@query.columns.each do |column|
next if Redmine::Helpers::Gantt::UNAVAILABLE_COLUMNS.include?(column.name)
column_name = column.name.to_s.tr('.', '_')
%>
<td class="gantt_<%= column_name %>_column gantt_selected_column <%= 'last_gantt_selected_column' if @query.columns.last == column %>" id="<%= column_name %>">
<%
style = "position: relative;"
style += "height: #{t_height + 24}px;"
%>
<%= content_tag(:div, :style => style, :class => "gantt_#{column.name}_container gantt_selected_column_container") do %>
<%= content_tag(:div, :style => style, :class => "gantt_#{column_name}_container gantt_selected_column_container") do %>
<%
style = "height: #{t_height}px;"
style += 'overflow: hidden;'
@ -195,7 +198,7 @@
style += 'background: #eee;'
%>
<%= content_tag(:div, content_tag(:p, column.caption, :class => 'gantt_hdr_selected_column_name'), :style => style, :class => "gantt_hdr") %>
<%= content_tag(:div, :class => "gantt_#{column.name} gantt_selected_column_content") do %>
<%= content_tag(:div, :class => "gantt_#{column_name} gantt_selected_column_content") do %>
<%= @gantt.selected_column_content({:column => column, :top => headers_height + 8, :zoom => zoom, :g_width => g_width}).html_safe %>
<% end %>
<% end %>

View file

@ -21,7 +21,7 @@
</div>
</div>
<p><%= time_entry.text_field :comments, :size => 60 %></p>
<% @time_entry.custom_field_values.each do |value| %>
<% @time_entry.editable_custom_field_values.each do |value| %>
<p><%= custom_field_tag_with_label :time_entry, value %></p>
<% end %>
<% end %>

View file

@ -13,7 +13,7 @@
<%= link_to_if_authorized l(:label_settings),
{:controller => 'projects', :action => 'settings', :id => @project, :tab => 'issues'},
:class => 'icon icon-settings' if User.current.allowed_to?(:manage_categories, @project) %>
:class => 'icon icon-settings' if User.current.allowed_to?(:edit_project, @project) %>
<% end %>
</div>

View file

@ -16,7 +16,9 @@
:rev => changeset.identifier) %>)
<% end %></p>
<div class="wiki changeset-comments"><%= format_changeset_comments changeset %></div>
<div class="wiki changeset-comments">
<%= format_changeset_comments changeset %>
</div>
</div>
<%= call_hook(:view_issues_history_changeset_bottom, { :changeset => changeset }) %>
<% end %>

View file

@ -2,27 +2,22 @@
<html lang="<%= current_language %>">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<title><%= html_title %></title>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="<%= Redmine::Info.app_name %>" />
<meta name="keywords" content="issue,bug,tracker" />
<%= csrf_meta_tag %>
<%= favicon %>
<%= stylesheet_link_tag 'jquery/jquery-ui-1.11.0', 'cookieconsent.min', 'tribute-3.7.3', 'application', 'responsive', :media => 'all' %>
<%= stylesheet_link_tag 'jquery/jquery-ui-1.11.0', 'tribute-3.7.3', 'application', 'responsive', :media => 'all' %>
<%= stylesheet_link_tag 'rtl', :media => 'all' if l(:direction) == 'rtl' %>
<% is_welcome = !User.current.logged? && current_page?(:controller => 'welcome', :action => 'index') %>
<%= stylesheet_link_tag 'frontpage', :media => 'all' if is_welcome %>
<%= javascript_heads %>
<script src="/themes/circlepro/javascripts/cookieconsent.min.js"></script>
<%= heads_for_theme %>
<%= call_hook :view_layouts_base_html_head %>
<!-- page specific tags -->
<%= yield :header_tags -%>
</head>
<body class="<%= body_css_classes %><%= ' is-preload' if is_welcome %>">
<body class="<%= body_css_classes %>">
<%= call_hook :view_layouts_base_body_top %>
<div id="wrapper">
@ -65,31 +60,20 @@
<div id="wrapper2">
<div id="wrapper3">
<div id="top-menu">
<div id="wrapper-top-menu">
<ul class="social-menu">
<li class="social-link-blog"><a href="https://manuel.cillero.es" title="<%= l(:link_my_blog) %>" class="icon-blog"><span><%= l(:link_my_blog) %></span></a></li>
<li class="social-link-mastodon"><a href="https://noc.social/@manuelcillero" title="Mastodon" target="_blank" class="icon-mastodon"><span>Mastodon</span></a></li>
<li class="social-link-linkedin"><a href="https://es.linkedin.com/in/manuelcillero" title="Linkedin" target="_blank" class="icon-linkedin"><span>Linkedin</span></a></li>
<li class="social-link-github"><a href="https://github.com/manuelcillero" title="Github" target="_blank" class="icon-github"><span>Github</span></a></li>
<li class="social-link-mail"><a href="https://manuel.cillero.es/contacto/#suitepro" title="Mail" class="icon-mail"><span>Mail</span></a></li>
</ul>
<div id="account">
<%= render_menu :account_menu -%>
</div>
<%= content_tag('div', "#{l(:label_logged_as)} #{link_to_user(User.current, :format => :username)}".html_safe, :id => 'loggedas') if User.current.logged? %>
<%= render_menu :top_menu if User.current.logged? || !Setting.login_required? -%>
</div>
</div>
<div id="header">
<a href="#" class="mobile-toggle-button js-flyout-menu-toggle-button"></a>
<div id="wrapper-header">
<% if User.current.logged? || !Setting.login_required? %>
<div id="quick-search" class="hide-when-print">
<div id="quick-search">
<%= form_tag({:controller => 'search', :action => 'index', :id => @project}, :method => :get ) do %>
<%= hidden_field_tag 'scope', default_search_project_scope, :id => nil %>
<%= hidden_field_tag(controller.default_search_scope, 1, :id => nil) if controller.default_search_scope %>
@ -107,17 +91,14 @@
<% end %>
<h1><%= page_header_title %></h1>
</div>
<% if display_main_menu?(@project) %>
<div id="main-menu" class="tabs">
<div id="wrapper-main-menu">
<%= render_main_menu(@project) %>
<div class="tabs-buttons" style="display:none;">
<button class="tab-left" onclick="moveTabLeft(this); return false;"></button>
<button class="tab-right" onclick="moveTabRight(this); return false;"></button>
</div>
</div>
</div>
<% end %>
</div>
@ -135,54 +116,16 @@
<div style="clear:both;"></div>
</div>
</div>
</div> <!-- #wrapper3 -->
<a href="#" id="scrollup" class="hide-when-print" style="display: none;"><%=l(:label_sort_higher)%></a><%= javascript_tag "$('#scrollup').click(function(){$('html,body').animate({scrollTop:0},600);return false;});" %>
<div id="footer">
Powered by <%= link_to Redmine::Info.app_name, Redmine::Info.url %> &copy; 2006-2022 Jean-Philippe Lang
</div>
</div>
<div id="ajax-indicator" style="display:none;"><span><%= l(:label_loading) %></span></div>
<div id="ajax-modal" style="display:none;"></div>
<div id="footer">
<div id="wrapper-footer">
<ul class="social-menu">
<li class="social-link-blog"><a href="https://manuel.cillero.es" title="<%= l(:link_my_blog) %>" class="icon-blog"><span><%= l(:link_my_blog) %></span></a></li>
<li class="social-link-mastodon"><a href="https://noc.social/@manuelcillero" title="Mastodon" target="_blank" class="icon-mastodon"><span>Mastodon</span></a></li>
<li class="social-link-linkedin"><a href="https://es.linkedin.com/in/manuelcillero" title="Linkedin" target="_blank" class="icon-linkedin"><span>Linkedin</span></a></li>
<li class="social-link-github"><a href="https://github.com/manuelcillero" title="Github" target="_blank" class="icon-github"><span>Github</span></a></li>
<li class="social-link-mail"><a href="https://manuel.cillero.es/contacto/#suitepro" title="Mail" class="icon-mail"><span>Mail</span></a></li>
</ul>
<div class="bgl"><div class="bgr">
<%= Time.current.year %> &copy; SuitePro (powered by <%= link_to Redmine::Info.app_name, Redmine::Info.url %>)
<div id="legal">
<span class="legal-legal"><a href="/projects/suitepro/wiki/Legal"><%= l(:label_legal) %></a></span>
<span class="legal-terms"> &nbsp;|&nbsp; <a href="/projects/suitepro/wiki/Condiciones_de_uso"><%= l(:label_legal_terms) %></a></span>
<span class="legal-privacy"> &nbsp;|&nbsp; <a href="/projects/suitepro/wiki/Política_de_privacidad"><%= l(:label_legal_privacy) %></a></span>
<span class="legal-cookies"> &nbsp;|&nbsp; <a href="/projects/suitepro/wiki/Política_de_cookies"><%= l(:label_legal_cookies) %></a></span>
</div>
</div></div>
</div>
</div>
</div> <!-- #wrapper2 -->
</div> <!-- #wrapper -->
<%= call_hook :view_layouts_base_body_bottom %>
<script>
//<![CDATA[
window.addEventListener("load", function(){
window.cookieconsent.initialise({
"palette": { "popup": { "background": "rgba(20,20,20,0.8)" }, "button": { "background": "#fff" } },
"theme": "classic",
"position": "bottom-left",
"content": { "message": "<a href='https://suitepro.cillero.es'>SuitePro</a> requiere el uso de cookies para ofrecer la mejor experiencia de acceso a sus contenidos. Puedes aceptar su uso o abandonar la página si lo deseas.", "dismiss": "ACEPTO SU USO", "link": "Más información", "href": "/projects/suitepro/wiki/Pol%C3%ADtica_de_cookies", "target": "_self" }
})});
//]]>
</script>
</body>
</html>

View file

@ -31,7 +31,7 @@
<%= textilizable @project.description %>
</div>
<% end %>
<% if @project.homepage.present? || @project.visible_custom_field_values.any?(&:present?) %>
<% if @project.homepage.present? || @project.visible_custom_field_values.any? { |o| o.value.present? } %>
<ul>
<% unless @project.homepage.blank? %>
<li><span class="label"><%=l(:field_homepage)%>:</span> <%= link_to_if uri_with_safe_scheme?(@project.homepage), @project.homepage, @project.homepage %></li>

View file

@ -36,7 +36,7 @@
<%= javascript_tag do %>
$(document).ready(function(){
$('.query-columns').closest('form').submit(function(){
$('#<%= selected_tag_id %> option').prop('selected', true);
$('#<%= selected_tag_id %> option:not(:disabled)').prop('selected', true);
});
});
<% end %>

View file

@ -25,10 +25,10 @@
<% end %>
</tbody>
</table>
<div class="issue-report-graph">
<div class="issue-report-graph hide-when-print">
<canvas id="issues_by_<%= params[:detail] %>"></canvas>
</div>
<div class="issue-report-graph">
<div class="issue-report-graph hide-when-print">
<canvas id="issues_by_status"></canvas>
</div>
<%= javascript_tag do %>

View file

@ -33,7 +33,9 @@
</div>
<div class="wiki changeset-comments"><%= format_changeset_comments @changeset %></div>
<div class="wiki changeset-comments">
<%= format_changeset_comments @changeset %>
</div>
<% if @changeset.issues.visible.any? || User.current.allowed_to?(:manage_related_issues, @repository.project) %>
<%= render :partial => 'related_issues' %>

View file

@ -32,13 +32,13 @@
<% end %>
<div id="password_fields" style="<%= 'display:none;' if @user.auth_source %>">
<p>
<%= f.password_field :password, :required => true, :size => 25 %>
<%= f.password_field :password, :required => @user.new_record?, :size => 25 %>
<em class="info"><%= l(:text_caracters_minimum, :count => Setting.password_min_length) %></em>
<% if Setting.password_required_char_classes.any? %>
<em class="info"><%= l(:text_characters_must_contain, :character_classes => Setting.password_required_char_classes.collect{|c| l("label_password_char_class_#{c}")}.join(", ")) %></em>
<% end %>
</p>
<p><%= f.password_field :password_confirmation, :required => true, :size => 25 %></p>
<p><%= f.password_field :password_confirmation, :required => @user.new_record?, :size => 25 %></p>
<p><%= f.check_box :generate_password %></p>
<p><%= f.check_box :must_change_passwd %></p>
</div>

View file

@ -7,7 +7,7 @@
<%=
page_title = title [l(:label_user_plural), users_path], @user.login
page_title.insert(page_title.rindex(' ') + 1, avatar(@user))
page_title.insert(page_title.rindex(' ') + 1, avatar(@user).to_s)
%>
<%= render_tabs user_settings_tabs %>

View file

@ -10,3 +10,6 @@ Disallow: <%= url_for(issues_gantt_path) %>
Disallow: <%= url_for(issues_calendar_path) %>
Disallow: <%= url_for(activity_path) %>
Disallow: <%= url_for(search_path) %>
Disallow: <%= url_for(issues_path(:sort => '')) %>
Disallow: <%= url_for(issues_path(:query_id => '')) %>
Disallow: <%= url_for(issues_path) %>?*set_filter=

View file

@ -5,7 +5,7 @@ api.wiki_page do
end
api.text @content.text
api.version @content.version
api.author(:id => @content.author_id, :name => @content.author.name)
api.author(:id => @content.author_id, :name => @content.author.name) unless @content.author_id.nil?
api.comments @content.comments
api.created_on @page.created_on
api.updated_on @content.updated_on

View file

@ -61,8 +61,7 @@
<%= render(:partial => "wiki/content", :locals => {:content => @content}) %>
<% if @page.attachments.length > 0 || (@editable && authorize_for('wiki', 'add_attachment')) %>
<fieldset class="collapsible collapsed<% if @page.attachments.length == 0 %> hide-when-print<% end %>">
<fieldset class="collapsible collapsed hide-when-print">
<legend onclick="toggleFieldset(this);" class="icon icon-collapsed"><%= l(:label_attachment_plural) %> (<%= @page.attachments.length %>)</legend>
<div style="display: none;">
@ -82,7 +81,6 @@
<% end %>
</div>
</fieldset>
<% end %>
<p class="wiki-update-info">
<% if User.current.allowed_to?(:view_wiki_edits, @project) %>

View file

@ -4,7 +4,7 @@
<th>
<%= link_to_function('', "toggleCheckboxesBySelector('table.transitions-#{name} input[type=checkbox]:not(:disabled)')",
:title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}",
:class => 'icon-only icon-checked') %>
:class => 'no-tooltip icon-only icon-checked') %>
<%=l(:label_current_status)%>
</th>
<th colspan="<%= @statuses.length %>"><%=l(:label_new_statuses_allowed)%></th>
@ -15,7 +15,7 @@
<td style="width:<%= 75 / @statuses.size %>%;">
<%= link_to_function('', "toggleCheckboxesBySelector('table.transitions-#{name} input[type=checkbox]:not(:disabled).new-status-#{new_status.id}')",
:title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}",
:class => 'icon-only icon-checked') %>
:class => 'no-tooltip icon-only icon-checked') %>
<%= new_status.name %>
</td>
<% end %>
@ -29,7 +29,7 @@
<td class="name">
<%= link_to_function('', "toggleCheckboxesBySelector('table.transitions-#{name} input[type=checkbox]:not(:disabled).old-status-#{old_status.try(:id) || 0}')",
:title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}",
:class => 'icon-only icon-checked') %>
:class => 'no-tooltip icon-only icon-checked') %>
<% if old_status %>
<% old_status_name = old_status.name %>
<%= old_status_name %>
@ -40,7 +40,7 @@
</td>
<% for new_status in @statuses -%>
<% checked = (old_status == new_status) || (transition_counts[[old_status, new_status]] > 0) %>
<td class="<%= checked ? 'enabled' : '' %>" title="<%= old_status_name %> &#187; <%= new_status.name %>">
<td class="no-tooltip <%= checked ? 'enabled' : '' %>" title="<%= old_status_name %> &#187; <%= new_status.name %>">
<%= transition_tag transition_counts[[old_status, new_status]], old_status, new_status, name %>
</td>
<% end -%>

View file

@ -67,7 +67,7 @@
<% for status in @statuses -%>
<td class="<%= @permissions[status.id][field].try(:join, ' ') %>" title="<%= name %> (<%= status.name %>)">
<%= field_permission_tag(@permissions, status, field, @roles) %>
<% unless status == @statuses.last %><a href="#" class="repeat-value">&#187;</a><% end %>
<% unless status == @statuses.last %><a href="#" class="repeat-value" title="<%= l(:button_copy) %>">&#187;</a><% end %>
</td>
<% end -%>
</tr>