Merge branch 'release-v3.0.0' into stable

This commit is contained in:
Holger Just 2012-02-07 00:09:59 +01:00
commit 11e93ff36a
513 changed files with 8407 additions and 2780 deletions

1
.gitignore vendored
View File

@ -4,6 +4,7 @@
/config/configuration.yml
/config/database.yml
/config/email.yml
/config/setup_load_paths.rb
/config/initializers/session_store.rb
/coverage
/db/*.db

View File

@ -1,11 +1,14 @@
# -*- coding: utf-8 -*-
source :rubygems
gem "rails", "2.3.14"
gem "coderay", "~> 0.9.7"
gem "coderay", "~> 1.0.0"
gem "i18n", "~> 0.4.2"
gem "rubytree", "~> 0.5.2", :require => 'tree'
gem "rdoc", ">= 2.4.2"
gem "liquid", "~> 2.3.0"
gem "acts-as-taggable-on", "= 2.1.0"
# Needed only on RUBY_VERSION = 1.8, ruby 1.9+ compatible interpreters should bring their csv
gem "fastercsv", "~> 1.5.0", :platforms => [:ruby_18, :jruby, :mingw_18]

View File

@ -19,6 +19,10 @@ class AdminController < ApplicationController
include SortHelper
menu_item :projects, :only => [:projects]
menu_item :plugins, :only => [:plugins]
menu_item :info, :only => [:info]
def index
@no_configuration_data = Redmine::DefaultData::Loader::no_data?
end

View File

@ -65,6 +65,9 @@ class ApplicationController < ActionController::Base
filter_parameter_logging :password
rescue_from ActionController::InvalidAuthenticityToken, :with => :invalid_authenticity_token
# FIXME: This doesn't work with Rails >= 3.0 anymore
# Possible workaround: https://github.com/rails/rails/issues/671#issuecomment-1780159
rescue_from ActionController::RoutingError, :with => proc{render_404}
include Redmine::Search::Controller
include Redmine::MenuManager::MenuController
@ -75,8 +78,6 @@ class ApplicationController < ActionController::Base
end
def user_setup
# Check the settings cache for each request
Setting.check_cache
# Find the current user
User.current = find_current_user
end

View File

@ -13,7 +13,8 @@
#++
class AutoCompletesController < ApplicationController
before_filter :find_project
before_filter :find_project, :only => :issues
before_filter :require_admin, :only => :projects
def issues
@issues = []
@ -33,6 +34,38 @@ class AutoCompletesController < ApplicationController
render :layout => false
end
def users
if params[:remove_group_members].present?
@group = Group.find(params[:remove_group_members])
@removed_users = @group.users
end
if params[:remove_watchers].present? && params[:klass].present?
watcher_class = params[:klass].constantize
if watcher_class.included_modules.include?(Redmine::Acts::Watchable) # check class is a watching class
@object = watcher_class.find(params[:remove_watchers])
@removed_users = @object.watcher_users
end
end
@removed_users ||= []
if params[:include_groups]
user_finder = Principal
else
user_finder = User
end
@users = user_finder.active.like(params[:q]).find(:all, :limit => 100) - @removed_users
render :layout => false
end
def projects
@principal = Principal.find(params[:id])
@projects = Project.active.like(params[:q]).find(:all, :limit => 100) - @principal.projects
render :layout => false
end
private
def find_project

View File

@ -44,19 +44,32 @@ class DocumentsController < ApplicationController
def new
@document = @project.documents.build(params[:document])
if request.post? and @document.save
attachments = Attachment.attach_files(@document, params[:attachments])
render_attachment_warning_if_needed(@document)
flash[:notice] = l(:notice_successful_create)
redirect_to :action => 'index', :project_id => @project
if request.post?
if User.current.allowed_to?(:add_document_watchers, @project) && params[:document]['watcher_user_ids'].present?
@document.watcher_user_ids = params[:document]['watcher_user_ids']
end
if @document.save
attachments = Attachment.attach_files(@document, params[:attachments])
render_attachment_warning_if_needed(@document)
flash[:notice] = l(:notice_successful_create)
redirect_to :action => 'index', :project_id => @project
end
end
end
def edit
@categories = DocumentCategory.all
if request.post? and @document.update_attributes(params[:document])
flash[:notice] = l(:notice_successful_update)
redirect_to :action => 'show', :id => @document
if request.post?
if User.current.allowed_to?(:add_document_watchers, @project) && params[:document]['watcher_user_ids'].present?
@document.watcher_user_ids = params[:document]['watcher_user_ids']
end
if @document.update_attributes(params[:document])
flash[:notice] = l(:notice_successful_update)
redirect_to :action => 'show', :id => @document
end
end
end
@ -69,7 +82,12 @@ class DocumentsController < ApplicationController
attachments = Attachment.attach_files(@document, params[:attachments])
render_attachment_warning_if_needed(@document)
Mailer.deliver_attachments_added(attachments[:files]) if attachments.present? && attachments[:files].present? && Setting.notified_events.include?('document_added')
if attachments.present? && attachments[:files].present? && Setting.notified_events.include?('document_added')
# TODO: refactor
attachments.first.container.recipients.each do |recipient|
Mailer.deliver_attachments_added(attachments[:files], recipient)
end
end
redirect_to :action => 'show', :id => @document
end

View File

@ -42,7 +42,11 @@ class FilesController < ApplicationController
render_attachment_warning_if_needed(container)
if !attachments.empty? && !attachments[:files].blank? && Setting.notified_events.include?('file_added')
Mailer.deliver_attachments_added(attachments[:files])
# TODO: refactor
recipients = attachments[:files].first.container.project.notified_users.select {|user| user.allowed_to?(:view_files, container.project)}.collect {|u| u.mail}
recipients.each do |recipient|
Mailer.deliver_attachments_added(attachments[:files], recipient)
end
end
redirect_to project_files_path(@project)
end

View File

@ -126,16 +126,19 @@ class GroupsController < ApplicationController
end
end
def autocomplete_for_user
@group = Group.find(params[:id])
@users = User.active.not_in_group(@group).like(params[:q]).all(:limit => 100)
render :layout => false
end
def edit_membership
@group = Group.find(params[:id])
@membership = Member.edit_membership(params[:membership_id], params[:membership], @group)
@membership.save if request.post?
if params[:project_ids] # Multiple memberships, one per project
params[:project_ids].each do |project_id|
@membership = Member.edit_membership(params[:membership_id], (params[:membership]|| {}).merge(:project_id => project_id), @group)
@membership.save if request.post?
end
else # Single membership
@membership = Member.edit_membership(params[:membership_id], params[:membership], @group)
@membership.save if request.post?
end
respond_to do |format|
if @membership.valid?
format.html { redirect_to :controller => 'groups', :action => 'edit', :id => @group, :tab => 'memberships' }

View File

@ -12,6 +12,8 @@
# See doc/COPYRIGHT.rdoc for more details.
#++
require 'diff'
class JournalsController < ApplicationController
before_filter :find_journal, :only => [:edit, :diff]
before_filter :find_issue, :only => [:new]
@ -84,6 +86,22 @@ class JournalsController < ApplicationController
end
end
def diff
if valid_field?(params[:field])
from = @journal.changes[params[:field]][0]
to = @journal.changes[params[:field]][1]
@diff = Redmine::Helpers::Diff.new(to, from)
@issue = @journal.journaled
respond_to do |format|
format.html { }
format.js { render :layout => false }
end
else
render_404
end
end
private
def find_journal
@ -100,4 +118,9 @@ class JournalsController < ApplicationController
rescue ActiveRecord::RecordNotFound
render_404
end
# Is this a valid field for diff'ing?
def valid_field?(field)
field.to_s.strip == "description"
end
end

View File

@ -14,6 +14,7 @@
class LdapAuthSourcesController < AuthSourcesController
menu_item :ldap_authentication, :only => [:index]
protected
def auth_source_class

View File

@ -110,7 +110,7 @@ class MessagesController < ApplicationController
content = "#{ll(Setting.default_language, :text_user_wrote, user)}\\n> "
content << text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]').gsub('"', '\"').gsub(/(\r?\n|\r\n?)/, "\\n> ") + "\\n\\n"
render(:update) { |page|
page << "$('reply_subject').value = \"#{subject}\";"
page << "$('message_subject').value = \"#{subject}\";"
page.<< "$('message_content').value = \"#{content}\";"
page.show 'reply'
page << "Form.Element.focus('message_content');"

View File

@ -20,6 +20,7 @@ class QueriesController < ApplicationController
def new
@query = Query.new(params[:query])
@query.project = params[:query_is_for_all] ? nil : @project
@query.display_subprojects = params[:display_subprojects] if params[:display_subprojects].present?
@query.user = User.current
@query.is_public = false unless User.current.allowed_to?(:manage_public_queries, @project) || User.current.admin?

View File

@ -72,8 +72,7 @@ class TimeEntryReportsController < ApplicationController
@periods = []
# Date#at_beginning_of_ not supported in Rails 1.2.x
date_from = @from.to_time
# 100 columns max
while date_from <= @to.to_time && @periods.length < 100
while date_from <= @to.to_time
case @columns
when 'year'
@periods << "#{date_from.year}"
@ -161,6 +160,9 @@ class TimeEntryReportsController < ApplicationController
@available_criterias = { 'project' => {:sql => "#{TimeEntry.table_name}.project_id",
:klass => Project,
:label => :label_project},
'status' => {:sql => "#{Issue.table_name}.status_id",
:klass => IssueStatus,
:label => :field_status},
'version' => {:sql => "#{Issue.table_name}.fixed_version_id",
:klass => Version,
:label => :label_version},

View File

@ -197,8 +197,16 @@ class UsersController < ApplicationController
def edit_membership
@membership = Member.edit_membership(params[:membership_id], params[:membership], @user)
@membership.save if request.post?
if params[:project_ids] # Multiple memberships, one per project
params[:project_ids].each do |project_id|
@membership = Member.edit_membership(params[:membership_id], (params[:membership] || {}).merge(:project_id => project_id), @user)
@membership.save if request.post?
end
else # Single membership
@membership = Member.edit_membership(params[:membership_id], params[:membership], @user)
@membership.save if request.post?
end
respond_to do |format|
if @membership.valid?
format.html { redirect_to :controller => 'users', :action => 'edit', :id => @user, :tab => 'memberships' }

View File

@ -16,6 +16,7 @@ class WatchersController < ApplicationController
before_filter :find_project
before_filter :require_login, :check_project_privacy, :only => [:watch, :unwatch]
before_filter :authorize, :only => [:new, :destroy]
before_filter :authorize_access_to_object, :only => [:new, :destroy]
verify :method => :post,
:only => [ :watch, :unwatch ],
@ -34,9 +35,12 @@ class WatchersController < ApplicationController
end
def new
@watcher = Watcher.new(params[:watcher])
@watcher.watchable = @watched
@watcher.save if request.post?
params[:user_ids].each do |user_id|
@watcher = Watcher.new((params[:watcher] || {}).merge({:user_id => user_id}))
@watcher.watchable = @watched
@watcher.save if request.post?
end if params[:user_ids].present?
respond_to do |format|
format.html { redirect_to :back }
format.js do
@ -50,7 +54,7 @@ class WatchersController < ApplicationController
end
def destroy
@watched.set_watcher(User.find(params[:user_id]), false) if request.post?
@watched.set_watcher(Principal.find(params[:user_id]), false) if request.post?
respond_to do |format|
format.html { redirect_to :back }
format.js do
@ -94,4 +98,24 @@ private
rescue ::ActionController::RedirectBackError
render :text => (watching ? 'Watcher added.' : 'Watcher removed.'), :layout => true
end
def authorize_access_to_object
permission = ''
case params[:action]
when 'new'
permission << 'add_'
when 'destroy'
permission << 'delete_'
end
# Ends up like: :delete_wiki_page_watchers
permission << "#{@watched.class.name.underscore}_watchers"
if User.current.allowed_to?(permission.to_sym, @project)
return true
else
deny_access
end
end
end

37
app/drops/base_drop.rb Normal file
View File

@ -0,0 +1,37 @@
#-- encoding: UTF-8
#-- copyright
# ChiliProject is a project management system.
#
# Copyright (C) 2010-2012 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
class BaseDrop < Liquid::Drop
def initialize(object)
@object = object unless object.respond_to?(:visible?) && !object.visible?
end
# Defines a Liquid method on the drop that is allowed to call the
# Ruby method directly. Best used for attributes.
#
# Based on Module#liquid_methods
def self.allowed_methods(*allowed_methods)
class_eval do
allowed_methods.each do |sym|
define_method sym do
if @object.respond_to?(:public_send)
@object.public_send(sym) rescue nil
else
@object.send(sym) rescue nil
end
end
end
end
end
end

79
app/drops/issue_drop.rb Normal file
View File

@ -0,0 +1,79 @@
#-- encoding: UTF-8
#-- copyright
# ChiliProject is a project management system.
#
# Copyright (C) 2010-2012 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
class IssueDrop < BaseDrop
allowed_methods :id
allowed_methods :subject
allowed_methods :description
allowed_methods :project
allowed_methods :tracker
allowed_methods :status
allowed_methods :due_date
allowed_methods :category
allowed_methods :assigned_to
allowed_methods :priority
allowed_methods :fixed_version
allowed_methods :author
allowed_methods :created_on
allowed_methods :updated_on
allowed_methods :start_date
allowed_methods :done_ratio
allowed_methods :estimated_hours
allowed_methods :parent
def custom_field(name)
return '' unless name.present?
custom_field = IssueCustomField.find_by_name(name.strip)
return '' unless custom_field.present?
custom_value = @object.custom_value_for(custom_field)
if custom_value.present?
return custom_value.value
else
return ''
end
end
# TODO: both required, method_missing for Ruby and before_method for Liquid
# Allows accessing custom fields by their name:
#
# - issue.the_name_of_player => CustomField(:name => "The name Of Player")
#
def before_method(method_sym)
if custom_field_with_matching_name = has_custom_field_with_matching_name?(method_sym)
custom_field(custom_field_with_matching_name.name)
else
super
end
end
# Allows accessing custom fields by their name:
#
# - issue.the_name_of_player => CustomField(:name => "The name Of Player")
#
def method_missing(method_sym, *arguments, &block)
if custom_field_with_matching_name = has_custom_field_with_matching_name?(method_sym)
custom_field(custom_field_with_matching_name.name)
else
super
end
end
private
def has_custom_field_with_matching_name?(method_sym)
custom_field_with_matching_name = @object.available_custom_fields.detect {|custom_field|
custom_field.name.downcase.underscore.gsub(' ','_') == method_sym.to_s
}
end
end

View File

@ -0,0 +1,17 @@
#-- encoding: UTF-8
#-- copyright
# ChiliProject is a project management system.
#
# Copyright (C) 2010-2012 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
class IssueStatusDrop < BaseDrop
allowed_methods :name
end

View File

@ -0,0 +1,17 @@
#-- encoding: UTF-8
#-- copyright
# ChiliProject is a project management system.
#
# Copyright (C) 2010-2012 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
class PrincipalDrop < BaseDrop
allowed_methods :name
end

17
app/drops/project_drop.rb Normal file
View File

@ -0,0 +1,17 @@
#-- encoding: UTF-8
#-- copyright
# ChiliProject is a project management system.
#
# Copyright (C) 2010-2012 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
class ProjectDrop < BaseDrop
allowed_methods :name, :identifier
end

17
app/drops/tracker_drop.rb Normal file
View File

@ -0,0 +1,17 @@
#-- encoding: UTF-8
#-- copyright
# ChiliProject is a project management system.
#
# Copyright (C) 2010-2012 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
class TrackerDrop < BaseDrop
allowed_methods :name
end

View File

@ -0,0 +1,17 @@
#-- encoding: UTF-8
#-- copyright
# ChiliProject is a project management system.
#
# Copyright (C) 2010-2012 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
class WikiPageDrop < BaseDrop
allowed_methods :title
end

View File

@ -16,7 +16,6 @@ require 'forwardable'
require 'cgi'
module ApplicationHelper
include Redmine::WikiFormatting::Macros::Definitions
include Redmine::I18n
include GravatarHelper::PublicMethods
@ -224,17 +223,15 @@ module ApplicationHelper
end
# Renders the project quick-jump box
def render_project_jump_box
projects = User.current.memberships.collect(&:project).compact.uniq
def render_project_jump_box(projects = [], html_options = {})
projects ||= User.current.memberships.collect(&:project).compact.uniq
if projects.any?
s = '<select onchange="if (this.value != \'\') { window.location = this.value; }">' +
"<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
'<option value="" disabled="disabled">---</option>'
s << project_tree_options_for_select(projects, :selected => @project) do |p|
{ :value => url_for(:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item) }
end
s << '</select>'
s
# option_tags = content_tag :option, l(:label_jump_to_a_project), :value => ""
option_tags = (content_tag :option, "", :value => "" )
option_tags << project_tree_options_for_select(projects, :selected => @project) do |p|
{ :value => url_for(:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item) }
end
select_tag "", option_tags, html_options.merge({ :onchange => "if (this.value != \'\') { window.location = this.value; }" })
end
end
@ -288,7 +285,15 @@ module ApplicationHelper
def principals_check_box_tags(name, principals)
s = ''
principals.sort.each do |principal|
s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
s << "<label style='display:block;'>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
end
s
end
def projects_check_box_tags(name, projects)
s = ''
projects.each do |project|
s << "<label>#{ check_box_tag name, project.id, false } #{h project}</label>\n"
end
s
end
@ -389,7 +394,9 @@ module ApplicationHelper
end
def page_header_title
if @project.nil? || @project.new_record?
if @page_header_title.present?
h(@page_header_title)
elsif @project.nil? || @project.new_record?
h(Setting.app_title)
else
b = []
@ -429,8 +436,8 @@ module ApplicationHelper
css << 'theme-' + theme.name
end
css << 'controller-' + params[:controller]
css << 'action-' + params[:action]
css << 'controller-' + params[:controller] if params[:controller]
css << 'action-' + params[:action] if params[:action]
css.join(' ')
end
@ -447,19 +454,51 @@ module ApplicationHelper
case args.size
when 1
obj = options[:object]
text = args.shift
input_text = args.shift
when 2
obj = args.shift
attr = args.shift
text = obj.send(attr).to_s
input_text = obj.send(attr).to_s
else
raise ArgumentError, 'invalid arguments to textilizable'
end
return '' if text.blank?
return '' if input_text.blank?
project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
only_path = options.delete(:only_path) == false ? false : true
text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr) { |macro, args| exec_macro(macro, obj, args) }
begin
text = ChiliProject::Liquid::Legacy.run_macros(input_text)
liquid_template = ChiliProject::Liquid::Template.parse(text)
liquid_variables = get_view_instance_variables_for_liquid
liquid_variables.merge!({'current_user' => User.current})
liquid_variables.merge!({'toc' => '{{toc}}'}) # Pass toc through to replace later
liquid_variables.merge!(ChiliProject::Liquid::Variables.all)
# Pass :view in a register so this view (with helpers) can be used inside of a tag
text = liquid_template.render(liquid_variables, :registers => {:view => self, :object => obj, :attribute => attr})
# Add Liquid errors to the log
if Rails.logger && Rails.logger.debug?
msg = ""
liquid_template.errors.each do |exception|
msg << "[Liquid Error] #{exception.message}\n:\n#{exception.backtrace.join("\n")}"
msg << "\n\n"
end
Rails.logger.debug msg
end
rescue Liquid::SyntaxError => exception
msg = "[Liquid Syntax Error] #{exception.message}"
if Rails.logger && Rails.logger.debug?
log_msg = "#{msg}\n"
log_msg << exception.backtrace.collect{ |str| " #{str}" }.join("\n")
log_msg << "\n\n"
Rails.logger.debug log_msg
end
# Skip Liquid if there is a syntax error
text = content_tag(:div, msg, :class => "flash error")
text << h(input_text)
end
@parsed_headings = []
text = parse_non_pre_blocks(text) do |text|
@ -712,7 +751,7 @@ module ApplicationHelper
end
end
TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
TOC_RE = /<p>\{%\s*toc(_right|_left)?\s*%\}<\/p>/i unless const_defined?(:TOC_RE)
# Renders the TOC with given headings
def replace_toc(text, headings)
@ -720,10 +759,14 @@ module ApplicationHelper
if headings.empty?
''
else
div_class = 'toc'
div_class << ' right' if $1 == '>'
div_class << ' left' if $1 == '<'
out = "<ul class=\"#{div_class}\"><li>"
toc_class = 'toc'
toc_class << ' right' if $1 == '_right'
toc_class << ' left' if $1 == '_left'
out = "<fieldset class=\"header_collapsible collapsible #{toc_class}\">"
out << "<legend onclick=\"toggleFieldset(this);\"><span>#{l(:label_toc)}</span></legend>"
out << "<div>"
out << "<ul class=\"toc\"><li>"
root = headings.map(&:first).min
current = root
started = false
@ -741,6 +784,7 @@ module ApplicationHelper
end
out << '</li></ul>' * (current - root)
out << '</li></ul>'
out << '</div></fieldset>'
end
end
end
@ -879,6 +923,12 @@ module ApplicationHelper
# +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
def avatar(user, options = { })
if Setting.gravatar_enabled?
if user.is_a?(Group)
size = options[:size] || 50
size = "#{size}x#{size}" # image_tag uses WxH
options[:class] ||= 'gravatar'
return image_tag("group.png", options.merge(:size => size))
end
options.merge!({:ssl => (defined?(request) && request.ssl?), :default => Setting.gravatar_default})
email = nil
if user.respond_to?(:mail)
@ -935,6 +985,72 @@ module ApplicationHelper
end
end
# Expands the current menu item using JavaScript based on the params
def expand_current_menu
current_menu_class =
case
when params[:controller] == "timelog"
"reports"
when params[:controller] == 'projects' && params[:action] == 'changelog'
"reports"
when params[:controller] == 'issues' && ['calendar','gantt'].include?(params[:action])
"reports"
when params[:controller] == 'projects' && params[:action] == 'roadmap'
'roadmap'
when params[:controller] == 'versions' && params[:action] == 'show'
'roadmap'
when params[:controller] == 'projects' && params[:action] == 'settings'
'settings'
when params[:controller] == 'contracts' || params[:controller] == 'deliverables'
'contracts'
else
params[:controller]
end
javascript_tag("jQuery.menu_expand({ menuItem: '.#{current_menu_class}' });")
end
# Menu items for the main top menu
def main_top_menu_items
split_top_menu_into_main_or_more_menus[:main]
end
# Menu items for the more top menu
def more_top_menu_items
split_top_menu_into_main_or_more_menus[:more]
end
def help_menu_item
split_top_menu_into_main_or_more_menus[:help]
end
# Split the :top_menu into separate :main and :more items
def split_top_menu_into_main_or_more_menus
unless @top_menu_split
items_for_main_level = []
items_for_more_level = []
help_menu = nil
menu_items_for(:top_menu) do |item|
if item.name == :home || item.name == :my_page
items_for_main_level << item
elsif item.name == :help
help_menu = item
elsif item.name == :projects
# Remove, present in layout
else
items_for_more_level << item
end
end
@top_menu_split = {
:main => items_for_main_level,
:more => items_for_more_level,
:help => help_menu
}
end
@top_menu_split
end
private
def wiki_helper
@ -946,4 +1062,20 @@ module ApplicationHelper
def link_to_content_update(text, url_params = {}, html_options = {})
link_to(text, url_params, html_options)
end
def get_view_instance_variables_for_liquid
internal_variables = %w{
@output_buffer @cookies @helpers @real_format @assigns_added @assigns
@view_paths @controller
}
self.instance_variables.collect(&:to_s).reject do |ivar|
ivar.match(/^@_/) || # Rails "internal" variables: @_foo
ivar.match(/^@template/) ||
internal_variables.include?(ivar)
end.inject({}) do |acc,ivar|
acc[ivar.sub('@','')] = instance_variable_get(ivar)
acc
end
end
end

View File

@ -52,13 +52,14 @@ module IssuesHelper
"<strong>#{@cached_label_priority}</strong>: #{h(issue.priority.name)}"
end
# TODO: deprecate and/or remove
def render_issue_subject_with_tree(issue)
s = ''
ancestors = issue.root? ? [] : issue.ancestors.all
ancestors.each do |ancestor|
s << '<div>' + content_tag('p', link_to_issue(ancestor))
s << '<div>' + content_tag('h2', link_to_issue(ancestor))
end
s << '<div>' + content_tag('h3', h(issue.subject))
s << '<div class="subject">' + content_tag('h2', h(issue.subject))
s << '</div>' * (ancestors.size + 1)
s
end

View File

@ -27,24 +27,34 @@ module JournalsHelper
def render_journal(model, journal, options = {})
return "" if journal.initial?
journal_content = render_journal_details(journal, :label_updated_time_by)
journal_content += render_notes(model, journal, options) unless journal.notes.blank?
content_tag "div", journal_content, { :id => "change-#{journal.id}", :class => journal.css_classes }
journal_classes = journal.css_classes
journal_content = render_journal_details(journal, :label_updated_time_by, model, options)
avatar = avatar(journal.user, :size => "40")
unless avatar.blank?
profile_wrap = content_tag("div", avatar, {:class => "profile-wrap"}, false)
journal_content = profile_wrap + journal_content
journal_classes << " has-avatar"
end
content_tag("div", journal_content, :id => "change-#{journal.id}", :class => journal_classes)
end
# This renders a journal entry wiht a header and details
def render_journal_details(journal, header_label = :label_updated_time_by)
# This renders a journal entry with a header and details
def render_journal_details(journal, header_label = :label_updated_time_by, model=nil, options={})
header = <<-HTML
<h4>
<div style="float:right;">#{link_to "##{journal.anchor}", :anchor => "note-#{journal.anchor}"}</div>
#{avatar(journal.user, :size => "24")}
#{content_tag('a', '', :name => "note-#{journal.anchor}")}
<div class="journal-link" style="float:right;">#{link_to "##{journal.anchor}", :anchor => "note-#{journal.anchor}"}</div>
#{authoring journal.created_at, journal.user, :label => header_label}
#{content_tag('a', '', :name => "note-#{journal.anchor}")}
</h4>
HTML
header << render_notes(model, journal, options) unless journal.notes.blank?
if journal.details.any?
details = content_tag "ul", :class => "details" do
details = content_tag "ul", :class => "journal-attributes details" do
journal.details.collect do |detail|
if d = journal.render_detail(detail)
content_tag("li", d)
@ -53,7 +63,7 @@ module JournalsHelper
end
end
content_tag("div", "#{header}#{details}", :id => "change-#{journal.id}", :class => "journal")
content_tag "div", "#{header}#{details}", :class => "journal-details"
end
def render_notes(model, journal, options={})

View File

@ -84,11 +84,12 @@ module QueriesHelper
end
end
@query.group_by = params[:group_by]
@query.display_subprojects = params[:display_subprojects] if params[:display_subprojects]
@query.column_names = params[:c] || (params[:query] && params[:query][:column_names])
session[:query] = {:project_id => @query.project_id, :filters => @query.filters, :group_by => @query.group_by, :column_names => @query.column_names}
session[:query] = {:project_id => @query.project_id, :filters => @query.filters, :group_by => @query.group_by, :column_names => @query.column_names, :display_subprojects => @query.display_subprojects}
else
@query = Query.find_by_id(session[:query][:id]) if session[:query][:id]
@query ||= Query.new(:name => "_", :project => @project, :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names])
@query ||= Query.new(:name => "_", :project => @project, :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names], :display_subprojects => session[:query][:display_subprojects])
@query.project = @project
end
end

View File

@ -24,6 +24,7 @@ class Document < ActiveRecord::Base
end)
acts_as_searchable :columns => ['title', "#{table_name}.description"], :include => :project
acts_as_watchable
validates_presence_of :project, :title, :category
validates_length_of :title, :maximum => 60
@ -48,4 +49,10 @@ class Document < ActiveRecord::Base
end
@updated_on
end
def recipients
mails = super # from acts_as_event
mails += watcher_recipients
mails.uniq
end
end

View File

@ -14,6 +14,10 @@
class DocumentObserver < ActiveRecord::Observer
def after_create(document)
Mailer.deliver_document_added(document) if Setting.notified_events.include?('document_added')
if Setting.notified_events.include?('document_added')
document.recipients.each do |recipient|
Mailer.deliver_document_added(document, recipient)
end
end
end
end

View File

@ -22,6 +22,11 @@ class Group < Principal
validates_uniqueness_of :lastname, :case_sensitive => false
validates_length_of :lastname, :maximum => 30
# Returns an array of all of the email addresses of the group's users
def mails
users.collect(&:mail)
end
def to_s
lastname.to_s
end

View File

@ -103,6 +103,10 @@ class Issue < ActiveRecord::Base
(usr || User.current).allowed_to?(:view_issues, self.project)
end
def to_liquid
IssueDrop.new(self)
end
def after_initialize
if new_record?
# set default values for new records only

View File

@ -17,7 +17,9 @@ class IssueObserver < ActiveRecord::Observer
def after_create(issue)
if self.send_notification
Mailer.deliver_issue_add(issue) if Setting.notified_events.include?('issue_added')
(issue.recipients + issue.watcher_recipients).uniq.each do |recipient|
Mailer.deliver_issue_add(issue, recipient)
end
end
clear_notification
end

View File

@ -28,6 +28,10 @@ class IssueStatus < ActiveRecord::Base
IssueStatus.update_all("is_default=#{connection.quoted_false}", ['id <> ?', id]) if self.is_default?
end
def to_liquid
IssueStatusDrop.new(self)
end
# Returns the default status for new issues
def self.default
find(:first, :conditions =>["is_default=?", true])

View File

@ -27,11 +27,15 @@ class JournalObserver < ActiveRecord::Observer
if journal.initial?
if Setting.notified_events.include?('wiki_content_added')
Mailer.deliver_wiki_content_added(wiki_content)
(wiki_content.recipients + wiki_page.wiki.watcher_recipients).uniq.each do |recipient|
Mailer.deliver_wiki_content_added(wiki_content, recipient)
end
end
else
if Setting.notified_events.include?('wiki_content_updated')
Mailer.deliver_wiki_content_updated(wiki_content)
(wiki_content.recipients + wiki_page.wiki.watcher_recipients + wiki_page.watcher_recipients).uniq.each do |recipient|
Mailer.deliver_wiki_content_updated(wiki_content, recipient)
end
end
end
end
@ -43,7 +47,10 @@ class JournalObserver < ActiveRecord::Observer
(Setting.notified_events.include?('issue_note_added') && journal.notes.present?) ||
(Setting.notified_events.include?('issue_status_updated') && journal.new_status.present?) ||
(Setting.notified_events.include?('issue_priority_updated') && journal.new_value_for('priority_id').present?)
Mailer.deliver_issue_edit(journal)
issue = journal.issue
(issue.recipients + issue.watcher_recipients).uniq.each do |recipient|
Mailer.deliver_issue_edit(journal, recipient)
end
end
end

View File

@ -69,6 +69,7 @@ class MailHandler < ActionMailer::Base
else
# Default behaviour, emails from unknown users are ignored
logger.info "MailHandler: ignoring email from unknown user [#{sender_email}]" if logger && logger.info
Mailer.deliver_mail_handler_unauthorized_action(user, email.subject.to_s, :to => sender_email) if Setting.mail_handler_confirmation_on_failure
return false
end
end
@ -102,12 +103,15 @@ class MailHandler < ActionMailer::Base
rescue ActiveRecord::RecordInvalid => e
# TODO: send a email to the user
logger.error e.message if logger
Mailer.deliver_mail_handler_missing_information(user, email.subject.to_s, e.message) if Setting.mail_handler_confirmation_on_failure
false
rescue MissingInformation => e
logger.error "MailHandler: missing information from #{user}: #{e.message}" if logger
Mailer.deliver_mail_handler_missing_information(user, email.subject.to_s, e.message) if Setting.mail_handler_confirmation_on_failure
false
rescue UnauthorizedAction => e
logger.error "MailHandler: unauthorized attempt from #{user}" if logger
Mailer.deliver_mail_handler_unauthorized_action(user, email.subject.to_s) if Setting.mail_handler_confirmation_on_failure
false
end
@ -141,6 +145,7 @@ class MailHandler < ActionMailer::Base
issue.save!
add_attachments(issue)
logger.info "MailHandler: issue ##{issue.id} created by #{user}" if logger && logger.info
Mailer.deliver_mail_handler_confirmation(issue, user, issue.subject) if Setting.mail_handler_confirmation_on_success
issue
end
@ -162,6 +167,7 @@ class MailHandler < ActionMailer::Base
add_attachments(issue)
issue.save!
logger.info "MailHandler: issue ##{issue.id} updated by #{user}" if logger && logger.info
Mailer.deliver_mail_handler_confirmation(issue.last_journal, user, email.subject) if Setting.mail_handler_confirmation_on_success
issue.last_journal
end
@ -190,6 +196,7 @@ class MailHandler < ActionMailer::Base
reply.board = message.board
message.children << reply
add_attachments(reply)
Mailer.deliver_mail_handler_confirmation(message, user, reply.subject) if Setting.mail_handler_confirmation_on_success
reply
else
logger.info "MailHandler: ignoring reply from [#{sender_email}] to a locked topic" if logger && logger.info

View File

@ -30,20 +30,19 @@ class Mailer < ActionMailer::Base
{ :host => h, :protocol => Setting.protocol }
end
# Builds a tmail object used to email recipients of the added issue.
# Builds a tmail object used to email a recipient of the added issue.
#
# Example:
# issue_add(issue) => tmail object
# Mailer.deliver_issue_add(issue) => sends an email to issue recipients
def issue_add(issue)
# issue_add(issue, 'user@example.com') => tmail object
# Mailer.deliver_issue_add(issue, 'user@example.com') => sends an email to 'user@example.com'
def issue_add(issue, recipient)
redmine_headers 'Project' => issue.project.identifier,
'Issue-Id' => issue.id,
'Issue-Author' => issue.author.login,
'Type' => "Issue"
redmine_headers 'Issue-Assignee' => issue.assigned_to.login if issue.assigned_to
message_id issue
recipients issue.recipients
cc(issue.watcher_recipients - @recipients)
recipients [recipient]
subject "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] (#{issue.status.name}) #{issue.subject}"
body :issue => issue,
:issue_url => url_for(:controller => 'issues', :action => 'show', :id => issue)
@ -53,9 +52,9 @@ class Mailer < ActionMailer::Base
# Builds a tmail object used to email recipients of the edited issue.
#
# Example:
# issue_edit(journal) => tmail object
# Mailer.deliver_issue_edit(journal) => sends an email to issue recipients
def issue_edit(journal)
# issue_edit(journal, 'user@example.com') => tmail object
# Mailer.deliver_issue_edit(journal, 'user@example.com') => sends an email to issue recipients
def issue_edit(journal, recipient)
issue = journal.journaled.reload
redmine_headers 'Project' => issue.project.identifier,
'Issue-Id' => issue.id,
@ -65,9 +64,7 @@ class Mailer < ActionMailer::Base
message_id journal
references issue
@author = journal.user
recipients issue.recipients
# Watchers in cc
cc(issue.watcher_recipients - @recipients)
recipients [recipient]
s = "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] "
s << "(#{issue.status.name}) " if journal.details['status_id']
s << issue.subject
@ -93,12 +90,12 @@ class Mailer < ActionMailer::Base
# Builds a tmail object used to email users belonging to the added document's project.
#
# Example:
# document_added(document) => tmail object
# Mailer.deliver_document_added(document) => sends an email to the document's project recipients
def document_added(document)
# document_added(document, 'test@example.com') => tmail object
# Mailer.deliver_document_added(document, 'test@example.com') => sends an email to the document's project recipients
def document_added(document, recipient)
redmine_headers 'Project' => document.project.identifier,
'Type' => "Document"
recipients document.recipients
recipients [recipient]
subject "[#{document.project.name}] #{l(:label_document_new)}: #{document.title}"
body :document => document,
:document_url => url_for(:controller => 'documents', :action => 'show', :id => document)
@ -110,7 +107,7 @@ class Mailer < ActionMailer::Base
# Example:
# attachments_added(attachments) => tmail object
# Mailer.deliver_attachments_added(attachments) => sends an email to the project's recipients
def attachments_added(attachments)
def attachments_added(attachments, recipient)
container = attachments.first.container
added_to = ''
added_to_url = ''
@ -118,16 +115,14 @@ class Mailer < ActionMailer::Base
when 'Project'
added_to_url = url_for(:controller => 'files', :action => 'index', :project_id => container)
added_to = "#{l(:label_project)}: #{container}"
recipients container.project.notified_users.select {|user| user.allowed_to?(:view_files, container.project)}.collect {|u| u.mail}
when 'Version'
added_to_url = url_for(:controller => 'files', :action => 'index', :project_id => container.project)
added_to = "#{l(:label_version)}: #{container.name}"
recipients container.project.notified_users.select {|user| user.allowed_to?(:view_files, container.project)}.collect {|u| u.mail}
when 'Document'
added_to_url = url_for(:controller => 'documents', :action => 'show', :id => container.id)
added_to = "#{l(:label_document)}: #{container.title}"
recipients container.recipients
end
recipients [recipient]
redmine_headers 'Project' => container.project.identifier,
'Type' => "Attachment"
subject "[#{container.project.name}] #{l(:label_attachment_new)}"
@ -142,11 +137,11 @@ class Mailer < ActionMailer::Base
# Example:
# news_added(news) => tmail object
# Mailer.deliver_news_added(news) => sends an email to the news' project recipients
def news_added(news)
def news_added(news, recipient)
redmine_headers 'Project' => news.project.identifier,
'Type' => "News"
message_id news
recipients news.recipients
recipients [recipient]
subject "[#{news.project.name}] #{l(:label_news)}: #{news.title}"
body :news => news,
:news_url => url_for(:controller => 'news', :action => 'show', :id => news)
@ -176,14 +171,13 @@ class Mailer < ActionMailer::Base
# Example:
# message_posted(message) => tmail object
# Mailer.deliver_message_posted(message) => sends an email to the recipients
def message_posted(message)
def message_posted(message, recipient)
redmine_headers 'Project' => message.project.identifier,
'Topic-Id' => (message.parent_id || message.id),
'Type' => "Forum"
message_id message
references message.parent unless message.parent.nil?
recipients(message.recipients)
cc((message.root.watcher_recipients + message.board.watcher_recipients).uniq - @recipients)
recipients [recipient]
subject "[#{message.board.project.name} - #{message.board.name} - msg#{message.root.id}] #{message.subject}"
body :message => message,
:message_url => url_for({ :controller => 'messages', :action => 'show', :board_id => message.board, :id => message.root, :r => message, :anchor => "message-#{message.id}" })
@ -195,13 +189,12 @@ class Mailer < ActionMailer::Base
# Example:
# wiki_content_added(wiki_content) => tmail object
# Mailer.deliver_wiki_content_added(wiki_content) => sends an email to the project's recipients
def wiki_content_added(wiki_content)
def wiki_content_added(wiki_content, recipient)
redmine_headers 'Project' => wiki_content.project.identifier,
'Wiki-Page-Id' => wiki_content.page.id,
'Type' => "Wiki"
message_id wiki_content
recipients wiki_content.recipients
cc(wiki_content.page.wiki.watcher_recipients - recipients)
recipients [recipient]
subject "[#{wiki_content.project.name}] #{l(:mail_subject_wiki_content_added, :id => wiki_content.page.pretty_title)}"
body :wiki_content => wiki_content,
:wiki_content_url => url_for(:controller => 'wiki', :action => 'show', :project_id => wiki_content.project, :id => wiki_content.page.title)
@ -213,13 +206,12 @@ class Mailer < ActionMailer::Base
# Example:
# wiki_content_updated(wiki_content) => tmail object
# Mailer.deliver_wiki_content_updated(wiki_content) => sends an email to the project's recipients
def wiki_content_updated(wiki_content)
def wiki_content_updated(wiki_content, recipient)
redmine_headers 'Project' => wiki_content.project.identifier,
'Wiki-Page-Id' => wiki_content.page.id,
'Type' => "Wiki"
message_id wiki_content
recipients wiki_content.recipients
cc(wiki_content.page.wiki.watcher_recipients + wiki_content.page.watcher_recipients - recipients)
recipients [recipient]
subject "[#{wiki_content.project.name}] #{l(:mail_subject_wiki_content_updated, :id => wiki_content.page.pretty_title)}"
body :wiki_content => wiki_content,
:wiki_content_url => url_for(:controller => 'wiki', :action => 'show', :project_id => wiki_content.project, :id => wiki_content.page.title),
@ -293,6 +285,44 @@ class Mailer < ActionMailer::Base
render_multipart('register', body)
end
def mail_handler_confirmation(object, user, email_subject)
recipients user.mail
case
when object.is_a?(Issue)
project = object.project.name
url = url_for(:controller => 'issues', :action => 'show', :id => object.id)
when object.is_a?(Journal)
project = object.project.name
url = url_for(:controller => 'issues', :action => 'show', :id => object.issue.id)
when object.class == Message
project = object.project.name
url = url_for(object.event_url)
else
project = ''
url = ''
end
subject "[#{project}] #{l(:label_mail_handler_confirmation, :subject => email_subject)}"
body(:object => object,
:url => url)
render_multipart('mail_handler_confirmation', body)
end
def mail_handler_unauthorized_action(user, email_subject, options={})
recipients options[:to] || user.mail
subject l(:label_mail_handler_failure, :subject => email_subject)
body({})
render_multipart('mail_handler_unauthorized_action', body)
end
def mail_handler_missing_information(user, email_subject, error_message)
recipients user.mail
subject l(:label_mail_handler_failure, :subject => email_subject)
body({:errors => error_message.to_s})
render_multipart('mail_handler_missing_information', body)
end
def test(user)
redmine_headers 'Type' => "Test"
set_language_if_valid(user.language)
@ -395,7 +425,7 @@ class Mailer < ActionMailer::Base
# Removes the current user from the recipients and cc
# if he doesn't want to receive notifications about what he does
@author ||= User.current
if @author.pref[:no_self_notified]
if @author && @author.mail && @author.pref[:no_self_notified]
recipients((recipients.is_a?(Array) ? recipients : [recipients]) - [@author.mail]) if recipients.present?
cc((cc.is_a?(Array) ? cc : [cc]) - [@author.mail]) if cc.present?
end
@ -403,13 +433,6 @@ class Mailer < ActionMailer::Base
notified_users = [recipients, cc].flatten.compact.uniq
# Rails would log recipients only, not cc and bcc
mylogger.info "Sending email notification to: #{notified_users.join(', ')}" if mylogger
# Blind carbon copy recipients
if Setting.bcc_recipients?
bcc(notified_users)
recipients []
cc []
end
super
end

View File

@ -14,6 +14,13 @@
class MessageObserver < ActiveRecord::Observer
def after_create(message)
Mailer.deliver_message_posted(message) if Setting.notified_events.include?('message_posted')
if Setting.notified_events.include?('message_posted')
recipients = message.recipients
recipients += message.root.watcher_recipients
recipients += message.board.watcher_recipients
recipients.uniq.each do |recipient|
Mailer.deliver_message_posted(message, recipient)
end
end
end
end

View File

@ -14,6 +14,10 @@
class NewsObserver < ActiveRecord::Observer
def after_create(news)
Mailer.deliver_news_added(news) if Setting.notified_events.include?('news_added')
if Setting.notified_events.include?('news_added')
news.recipients.each do |recipient|
Mailer.deliver_news_added(news, recipient)
end
end
end
end

View File

@ -31,6 +31,10 @@ class Principal < ActiveRecord::Base
before_create :set_default_empty_values
def to_liquid
PrincipalDrop.new(self)
end
def name(formatter = nil)
to_s
end
@ -44,6 +48,82 @@ class Principal < ActiveRecord::Base
end
end
def active?
true
end
def logged?
true # TODO: should all principals default to logged or not?
end
# Return true if the user is allowed to do the specified action on a specific context
# Action can be:
# * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
# * a permission Symbol (eg. :edit_project)
# Context can be:
# * a project : returns true if user is allowed to do the specified action on this project
# * a group of projects : returns true if user is allowed on every project
# * nil with options[:global] set : check if user has at least one role allowed for this action,
# or falls back to Non Member / Anonymous permissions depending if the user is logged
def allowed_to?(action, context, options={})
if context && context.is_a?(Project)
# No action allowed on archived projects
return false unless context.active?
# No action allowed on disabled modules
return false unless context.allows_to?(action)
# Admin users are authorized for anything else
return true if admin?
roles = roles_for_project(context)
return false unless roles
roles.detect {|role| (context.is_public? || role.member?) && role.allowed_to?(action)}
elsif context && context.is_a?(Array)
# Authorize if user is authorized on every element of the array
context.map do |project|
allowed_to?(action,project,options)
end.inject do |memo,allowed|
memo && allowed
end
elsif options[:global]
# Admin users are always authorized
return true if admin?
# authorize if user has at least one role that has this permission
roles = memberships.collect {|m| m.roles}.flatten.uniq
roles.detect {|r| r.allowed_to?(action)} || (self.logged? ? Role.non_member.allowed_to?(action) : Role.anonymous.allowed_to?(action))
else
false
end
end
# Is the user allowed to do the specified action on any project?
# See allowed_to? for the actions and valid options.
def allowed_to_globally?(action, options)
allowed_to?(action, nil, options.reverse_merge(:global => true))
end
# Return user's roles for project
def roles_for_project(project)
roles = []
# No role on archived projects
return roles unless project && project.active?
if logged?
# Find project membership
membership = memberships.detect {|m| m.project_id == project.id}
if membership
roles = membership.roles
else
@role_non_member ||= Role.non_member
roles << @role_non_member
end
else
@role_anonymous ||= Role.anonymous
roles << @role_anonymous
end
roles
end
protected
# Make sure we don't try to insert NULL values (see #4632)

View File

@ -82,6 +82,16 @@ class Project < ActiveRecord::Base
named_scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"}
named_scope :all_public, { :conditions => { :is_public => true } }
named_scope :visible, lambda { { :conditions => Project.visible_by(User.current) } }
named_scope :like, lambda {|q|
s = "%#{q.to_s.strip.downcase}%"
{
:conditions => ["LOWER(name) LIKE ?", s]
}
}
def to_liquid
ProjectDrop.new(self)
end
def initialize(attributes = nil)
super
@ -131,6 +141,11 @@ class Project < ActiveRecord::Base
end
end
# Is the project visible to the current user
def visible?
User.current.allowed_to?(:view_project, self)
end
def self.allowed_to_condition(user, permission, options={})
base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
if perm = Redmine::AccessControl.permission(permission)
@ -616,7 +631,7 @@ class Project < ActiveRecord::Base
while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
ancestors.pop
end
yield project, ancestors.size
yield project, ancestors.size if block_given?
ancestors << project
end
end

View File

@ -12,64 +12,7 @@
# See doc/COPYRIGHT.rdoc for more details.
#++
class QueryColumn
attr_accessor :name, :sortable, :groupable, :default_order
include Redmine::I18n
def initialize(name, options={})
self.name = name
self.sortable = options[:sortable]
self.groupable = options[:groupable] || false
if groupable == true
self.groupable = name.to_s
end
self.default_order = options[:default_order]
@caption_key = options[:caption] || "field_#{name}"
end
def caption
l(@caption_key)
end
# Returns true if the column is sortable, otherwise false
def sortable?
!sortable.nil?
end
def value(issue)
issue.send name
end
end
class QueryCustomFieldColumn < QueryColumn
def initialize(custom_field)
self.name = "cf_#{custom_field.id}".to_sym
self.sortable = custom_field.order_statement || false
if %w(list date bool int).include?(custom_field.field_format)
self.groupable = custom_field.order_statement
end
self.groupable ||= false
@cf = custom_field
end
def caption
@cf.name
end
def custom_field
@cf
end
def value(issue)
cv = issue.custom_values.detect {|v| v.custom_field_id == @cf.id}
cv && @cf.cast_value(cv.value)
end
end
class Query < ActiveRecord::Base
class StatementInvalid < ::ActiveRecord::StatementInvalid
end
belongs_to :project
belongs_to :user
@ -90,6 +33,7 @@ class Query < ActiveRecord::Base
"*" => :label_all,
">=" => :label_greater_or_equal,
"<=" => :label_less_or_equal,
"><" => :label_between,
"<t+" => :label_in_less_than,
">t+" => :label_in_more_than,
"t+" => :label_in,
@ -107,8 +51,8 @@ class Query < ActiveRecord::Base
:list_status => [ "o", "=", "!", "c", "*" ],
:list_optional => [ "=", "!", "!*", "*" ],
:list_subprojects => [ "*", "!*", "=" ],
:date => [ "<t+", ">t+", "t+", "t", "w", ">t-", "<t-", "t-" ],
:date_past => [ ">t-", "<t-", "t-", "t", "w" ],
:date => [ "=", ">=", "<=", "><", "<t+", ">t+", "t+", "t", "w", ">t-", "<t-", "t-" ],
:date_past => [ "=", ">=", "<=", "><", ">t-", "<t-", "t-", "t", "w" ],
:string => [ "=", "~", "!", "!~" ],
:text => [ "~", "!~" ],
:integer => [ "=", ">=", "<=", "!*", "*" ] }
@ -139,6 +83,7 @@ class Query < ActiveRecord::Base
def initialize(attributes = nil)
super attributes
self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
self.display_subprojects ||= Setting.display_subprojects_issues?
end
def after_initialize
@ -245,7 +190,7 @@ class Query < ActiveRecord::Base
def add_filter(field, operator, values)
# values must be an array
return unless values and values.is_a? Array # and !values.first.empty?
return unless values.nil? || values.is_a?(Array)
# check if field is defined as an available filter
if available_filters.has_key? field
filter_options = available_filters[field]
@ -254,7 +199,7 @@ class Query < ActiveRecord::Base
# allowed_values = values & ([""] + (filter_options[:values] || []).collect {|val| val[1]})
# filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator
#end
filters[field] = {:operator => operator, :values => values }
filters[field] = {:operator => operator, :values => (values || ['']) }
end
end
@ -266,9 +211,9 @@ class Query < ActiveRecord::Base
# Add multiple filters using +add_filter+
def add_filters(fields, operators, values)
if fields.is_a?(Array) && operators.is_a?(Hash) && values.is_a?(Hash)
if fields.is_a?(Array) && operators.is_a?(Hash) && (values.nil? || values.is_a?(Hash))
fields.each do |field|
add_filter(field, operators[field], values[field])
add_filter(field, operators[field], values && values[field])
end
end
end
@ -277,6 +222,10 @@ class Query < ActiveRecord::Base
filters and filters[field]
end
def type_for(field)
available_filters[field][:type] if available_filters.has_key?(field)
end
def operator_for(field)
has_filter?(field) ? filters[field][:operator] : nil
end
@ -285,6 +234,10 @@ class Query < ActiveRecord::Base
has_filter?(field) ? filters[field][:values] : nil
end
def value_for(field, index=0)
(values_for(field) || [])[index]
end
def label_for(field)
label = available_filters[field][:name] if available_filters.has_key?(field)
label ||= field.gsub(/\_id$/, "")
@ -410,7 +363,7 @@ class Query < ActiveRecord::Base
# all subprojects
ids += project.descendants.collect(&:id)
end
elsif Setting.display_subprojects_issues?
elsif display_subprojects?
ids += project.descendants.collect(&:id)
end
project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
@ -519,7 +472,7 @@ class Query < ActiveRecord::Base
def issue_count
Issue.count(:include => [:status, :project], :conditions => statement)
rescue ::ActiveRecord::StatementInvalid => e
raise StatementInvalid.new(e.message)
raise Query::StatementInvalid.new(e.message)
end
# Returns the issue count by group or nil if query is not grouped
@ -539,7 +492,7 @@ class Query < ActiveRecord::Base
end
r
rescue ::ActiveRecord::StatementInvalid => e
raise StatementInvalid.new(e.message)
raise Query::StatementInvalid.new(e.message)
end
# Returns the issues
@ -554,7 +507,7 @@ class Query < ActiveRecord::Base
:limit => options[:limit],
:offset => options[:offset]
rescue ::ActiveRecord::StatementInvalid => e
raise StatementInvalid.new(e.message)
raise Query::StatementInvalid.new(e.message)
end
# Returns the journals
@ -566,7 +519,7 @@ class Query < ActiveRecord::Base
:limit => options[:limit],
:offset => options[:offset]
rescue ::ActiveRecord::StatementInvalid => e
raise StatementInvalid.new(e.message)
raise Query::StatementInvalid.new(e.message)
end
# Returns the versions
@ -575,7 +528,7 @@ class Query < ActiveRecord::Base
Version.find :all, :include => :project,
:conditions => Query.merge_conditions(project_statement, options[:conditions])
rescue ::ActiveRecord::StatementInvalid => e
raise StatementInvalid.new(e.message)
raise Query::StatementInvalid.new(e.message)
end
private
@ -585,11 +538,15 @@ class Query < ActiveRecord::Base
sql = ''
case operator
when "="
if value.present?
sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
if [:date, :date_past].include?(type_for(field))
sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), (Date.parse(value.first) rescue nil))
else
# empty set of allowed values produces no result
sql = "0=1"
if value.any?
sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
else
# IN an empty set
sql = "0=1"
end
end
when "!"
if value.present?
@ -605,42 +562,58 @@ class Query < ActiveRecord::Base
sql = "#{db_table}.#{db_field} IS NOT NULL"
sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
when ">="
if is_custom_filter
sql = "#{db_table}.#{db_field} != '' AND CAST(#{db_table}.#{db_field} AS decimal(60,4)) >= #{value.first.to_f}"
if [:date, :date_past].include?(type_for(field))
sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), nil)
else
sql = "#{db_table}.#{db_field} >= #{value.first.to_f}"
if is_custom_filter
sql = "CAST(#{db_table}.#{db_field} AS decimal(60,3)) >= #{value.first.to_i}"
else
sql = "#{db_table}.#{db_field} >= #{value.first.to_i}"
end
end
when "<="
if is_custom_filter
sql = "#{db_table}.#{db_field} != '' AND CAST(#{db_table}.#{db_field} AS decimal(60,4)) <= #{value.first.to_f}"
if [:date, :date_past].include?(type_for(field))
sql = date_clause(db_table, db_field, nil, (Date.parse(value.first) rescue nil))
else
sql = "#{db_table}.#{db_field} <= #{value.first.to_f}"
if is_custom_filter
sql = "CAST(#{db_table}.#{db_field} AS decimal(60,3)) <= #{value.first.to_i}"
else
sql = "#{db_table}.#{db_field} <= #{value.first.to_i}"
end
end
when "><"
if [:date, :date_past].include?(type_for(field))
sql = date_clause(db_table, db_field, (Date.parse(value[0]) rescue nil), (Date.parse(value[1]) rescue nil))
else
if is_custom_filter
sql = "CAST(#{db_table}.#{db_field} AS decimal(60,3)) BETWEEN #{value[0].to_i} AND #{value[1].to_i}"
else
sql = "#{db_table}.#{db_field} BETWEEN #{value[0].to_i} AND #{value[1].to_i}"
end
end
when "o"
sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_false}" if field == "status_id"
when "c"
sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_true}" if field == "status_id"
when ">t-"
sql = date_range_clause(db_table, db_field, - value.first.to_i, 0)
sql = relative_date_clause(db_table, db_field, - value.first.to_i, 0)
when "<t-"
sql = date_range_clause(db_table, db_field, nil, - value.first.to_i)
sql = relative_date_clause(db_table, db_field, nil, - value.first.to_i)
when "t-"
sql = date_range_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
sql = relative_date_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
when ">t+"
sql = date_range_clause(db_table, db_field, value.first.to_i, nil)
sql = relative_date_clause(db_table, db_field, value.first.to_i, nil)
when "<t+"
sql = date_range_clause(db_table, db_field, 0, value.first.to_i)
sql = relative_date_clause(db_table, db_field, 0, value.first.to_i)
when "t+"
sql = date_range_clause(db_table, db_field, value.first.to_i, value.first.to_i)
sql = relative_date_clause(db_table, db_field, value.first.to_i, value.first.to_i)
when "t"
sql = date_range_clause(db_table, db_field, 0, 0)
sql = relative_date_clause(db_table, db_field, 0, 0)
when "w"
from = l(:general_first_day_of_week) == '7' ?
# week starts on sunday
((Date.today.cwday == 7) ? Time.now.at_beginning_of_day : Time.now.at_beginning_of_week - 1.day) :
# week starts on monday (Rails default)
Time.now.at_beginning_of_week
sql = "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(from), connection.quoted_date(from + 7.days)]
first_day_of_week = l(:general_first_day_of_week).to_i
day_of_week = Date.today.cwday
days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
sql = relative_date_clause(db_table, db_field, - days_ago, - days_ago + 6)
when "~"
sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
when "!~"
@ -676,14 +649,19 @@ class Query < ActiveRecord::Base
end
# Returns a SQL clause for a date or datetime field.
def date_range_clause(table, field, from, to)
def date_clause(table, field, from, to)
s = []
if from
s << ("#{table}.#{field} > '%s'" % [connection.quoted_date((Date.yesterday + from).to_time.end_of_day)])
s << ("#{table}.#{field} > '%s'" % [connection.quoted_date((from - 1).to_time.end_of_day)])
end
if to
s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date((Date.today + to).to_time.end_of_day)])
s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date(to.to_time.end_of_day)])
end
s.join(' AND ')
end
# Returns a SQL clause for a date or datetime field using relative dates.
def relative_date_clause(table, field, days_from, days_to)
date_clause(table, field, (days_from ? Date.today + days_from : nil), (days_to ? Date.today + days_to : nil))
end
end

View File

@ -0,0 +1,16 @@
#-- encoding: UTF-8
#-- copyright
# ChiliProject is a project management system.
#
# Copyright (C) 2010-2012 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
class Query::StatementInvalid < ActiveRecord::StatementInvalid
end

View File

@ -0,0 +1,42 @@
#-- encoding: UTF-8
#-- copyright
# ChiliProject is a project management system.
#
# Copyright (C) 2010-2012 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
class QueryColumn
attr_accessor :name, :sortable, :groupable, :default_order
include Redmine::I18n
def initialize(name, options={})
self.name = name
self.sortable = options[:sortable]
self.groupable = options[:groupable] || false
if groupable == true
self.groupable = name.to_s
end
self.default_order = options[:default_order]
@caption_key = options[:caption] || "field_#{name}"
end
def caption
l(@caption_key)
end
# Returns true if the column is sortable, otherwise false
def sortable?
!sortable.nil?
end
def value(issue)
issue.send name
end
end

View File

@ -0,0 +1,40 @@
#-- encoding: UTF-8
#-- copyright
# ChiliProject is a project management system.
#
# Copyright (C) 2010-2012 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
class QueryCustomFieldColumn < QueryColumn
def initialize(custom_field)
self.name = "cf_#{custom_field.id}".to_sym
self.sortable = custom_field.order_statement || false
if %w(list date bool int).include?(custom_field.field_format)
self.groupable = custom_field.order_statement
end
self.groupable ||= false
@cf = custom_field
end
def caption
@cf.name
end
def custom_field
@cf
end
def value(issue)
cv = issue.custom_values.detect {|v| v.custom_field_id == @cf.id}
cv && @cf.cast_value(cv.value)
end
end

View File

@ -97,13 +97,17 @@ class Setting < ActiveRecord::Base
# Returns the value of the setting named name
def self.[](name)
Marshal.load(Rails.cache.fetch("chiliproject/setting/#{name}") {Marshal.dump(find_or_default(name).value)})
if use_caching?
Marshal.load(Rails.cache.fetch(self.cache_key(name)) {Marshal.dump(find_or_default(name).value)})
else
find_or_default(name).value
end
end
def self.[]=(name, v)
setting = find_or_default(name)
setting.value = (v ? v : "")
Rails.cache.delete "chiliproject/setting/#{name}"
Rails.cache.delete self.cache_key(name)
setting.save
setting.value
end
@ -137,23 +141,42 @@ class Setting < ActiveRecord::Base
Object.const_defined?(:OpenID) && self[:openid].to_i > 0
end
# Checks if settings have changed since the values were read
# and clears the cache hash if it's the case
# Called once per request
# Deprecation Warning: This method is no longer available. There is no
# replacement.
def self.check_cache
settings_updated_on = Setting.maximum(:updated_on)
cache_cleared_on = Rails.cache.read('chiliproject/setting-cleared_on')
cache_cleared_on = cache_cleared_on ? Marshal.load(cache_cleared_on) : Time.now
if settings_updated_on && cache_cleared_on <= settings_updated_on
clear_cache
end
# DEPRECATED SINCE 3.0.0beta2
ActiveSupport::Deprecation.warn "The Setting.check_cache method is " +
"deprecated and will be removed in the future. There should be no " +
"replacement for this functionality needed."
end
# Clears all of the Setting caches
def self.clear_cache
Rails.cache.delete_matched( /^chiliproject\/setting\/.+$/ )
Rails.cache.write('chiliproject/setting-cleared_on', Marshal.dump(Time.now))
logger.info 'Settings cache cleared.' if logger
# DEPRECATED SINCE 3.0.0beta2
ActiveSupport::Deprecation.warn "The Setting.clear_cache method is " +
"deprecated and will be removed in the future. There should be no " +
"replacement for this functionality needed. To sweep the whole " +
"cache Rails.cache.clear may be used. To invalidate the Settings " +
"only, you may use Setting.first.try(:touch)"
end
# Temporarily deactivate settings caching in the block scope
def self.uncached
cache_setting = self.use_caching
self.use_caching = false
yield
ensure
self.use_caching = cache_setting
end
# Check if Setting caching should be performed
def self.use_caching?
!Thread.current['chiliproject/settings/do_not_use_caching']
end
# Dis-/En-able Setting caching. This is mainly intended to be used in tests
def self.use_caching=(new_value)
Thread.current['chiliproject/settings/do_not_use_caching'] = !new_value
end
private
@ -165,4 +188,8 @@ private
setting = find_by_name(name)
setting ||= new(:name => name, :value => @@available_settings[name]['default']) if @@available_settings.has_key? name
end
def self.cache_key(name)
"chiliproject/setting/#{Setting.maximum(:updated_on).to_i}/#{name}"
end
end

View File

@ -35,6 +35,10 @@ class Tracker < ActiveRecord::Base
name <=> tracker.name
end
def to_liquid
TrackerDrop.new(self)
end
def self.all
find(:all, :order => 'position')
end

View File

@ -345,27 +345,6 @@ class User < Principal
!logged?
end
# Return user's roles for project
def roles_for_project(project)
roles = []
# No role on archived projects
return roles unless project && project.active?
if logged?
# Find project membership
membership = memberships.detect {|m| m.project_id == project.id}
if membership
roles = membership.roles
else
@role_non_member ||= Role.non_member
roles << @role_non_member
end
else
@role_anonymous ||= Role.anonymous
roles << @role_anonymous
end
roles
end
# Return true if the user is a member of project
def member_of?(project)
!roles_for_project(project).detect {|role| role.member?}.nil?
@ -388,53 +367,6 @@ class User < Principal
@projects_by_role
end
# Return true if the user is allowed to do the specified action on a specific context
# Action can be:
# * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
# * a permission Symbol (eg. :edit_project)
# Context can be:
# * a project : returns true if user is allowed to do the specified action on this project
# * a group of projects : returns true if user is allowed on every project
# * nil with options[:global] set : check if user has at least one role allowed for this action,
# or falls back to Non Member / Anonymous permissions depending if the user is logged
def allowed_to?(action, context, options={})
if context && context.is_a?(Project)
# No action allowed on archived projects
return false unless context.active?
# No action allowed on disabled modules
return false unless context.allows_to?(action)
# Admin users are authorized for anything else
return true if admin?
roles = roles_for_project(context)
return false unless roles
roles.detect {|role| (context.is_public? || role.member?) && role.allowed_to?(action)}
elsif context && context.is_a?(Array)
# Authorize if user is authorized on every element of the array
context.map do |project|
allowed_to?(action,project,options)
end.inject do |memo,allowed|
memo && allowed
end
elsif options[:global]
# Admin users are always authorized
return true if admin?
# authorize if user has at least one role that has this permission
roles = memberships.collect {|m| m.roles}.flatten.uniq
roles.detect {|r| r.allowed_to?(action)} || (self.logged? ? Role.non_member.allowed_to?(action) : Role.anonymous.allowed_to?(action))
else
false
end
end
# Is the user allowed to do the specified action on any project?
# See allowed_to? for the actions and valid options.
def allowed_to_globally?(action, options)
allowed_to?(action, nil, options.reverse_merge(:global => true))
end
safe_attributes 'login',
'firstname',
'lastname',

View File

@ -14,7 +14,7 @@
class Watcher < ActiveRecord::Base
belongs_to :watchable, :polymorphic => true
belongs_to :user
belongs_to :user, :class_name => 'Principal', :foreign_key => 'user_id'
validates_presence_of :user
validates_uniqueness_of :user_id, :scope => [:watchable_type, :watchable_id]

View File

@ -53,6 +53,10 @@ class WikiPage < ActiveRecord::Base
end
end
def to_liquid
WikiPageDrop.new(self)
end
def visible?(user=User.current)
!user.nil? && user.allowed_to?(:view_wiki_pages, project)
end

View File

@ -0,0 +1,34 @@
<div id="nav-login-content">
<% form_tag({:controller => "account", :action=> "login"}) do %>
<%= hidden_field_tag('back_url', CGI.escape(request.url)) %>
<table>
<tr>
<td><label for="username-pulldown"><%= l(:field_login) %></label></td>
<td><label for="password-pulldown"><%= l(:field_password) %></label></td>
</tr>
<tr>
<td><%= text_field_tag 'username', nil, :tabindex => '1', :id => 'username-pulldown' %></td>
<td><%= password_field_tag 'password', nil, :tabindex => '1', :id => 'password-pulldown' %></td>
<td><input type="submit" name="login" value="<%=l(:button_login)%>" tabindex="1"/></td>
</tr>
</table>
<div id = "optional_login_fields" style = "top = 10px; white-space:nowrap">
<% if Setting.openid? %>
<%= text_field_tag "openid_url", nil, :placeholder => l(:field_identity_url), :tabindex => '1' %>
<% end %>
<% if Setting.autologin? %>
<label for="autologin"><%= check_box_tag 'autologin', 1, false, :tabindex => 1 %> <%= l(:label_stay_logged_in) %></label>
<% end %>
<% if Setting.lost_password? %>
<%= link_to l(:label_password_lost), {:controller => 'account', :action => 'lost_password'}, :tabindex => 1 %>
<% end %>
<% if !User.current.logged? && Setting.self_registration? %>
<%= "|" if Setting.lost_password? %>
<%= link_to l(:label_register), { :controller => 'account', :action => 'register' }, :tabindex => 1 %>
<% end %>
</div>
<% end %>
</div>

View File

@ -5,7 +5,7 @@
<table>
<tr>
<td align="right"><label for="username"><%=l(:field_login)%>:</label></td>
<td align="left"><%= text_field_tag 'username', nil, :tabindex => '1' %></td>
<td align="left"><%= text_field_tag 'username', nil, :tabindex => '2' %></td>
</tr>
<tr>
<td align="right"><label for="password"><%=l(:field_password)%>:</label></td>

View File

@ -1,5 +1 @@
<div id="admin-menu">
<ul>
<%= render_menu :admin_menu %>
</ul>
</div>
<%= render_menu :admin_menu %>

View File

@ -1,5 +1,3 @@
<h2><%=l(:label_administration)%></h2>
<div id="admin-index">
<%= render :partial => 'no_data' if @no_configuration_data %>
<%= render :partial => 'menu' %>

View File

@ -0,0 +1 @@
<%= projects_check_box_tags 'project_ids[]', @projects %>

View File

@ -71,3 +71,13 @@
<%= auto_discovery_link_tag(:atom, {:format => 'atom', :key => User.current.rss_key}, :title => "#{@project}: #{@board}") %>
<%= stylesheet_link_tag 'scm' %>
<% end %>
<% content_for :sidebar do %>
<% if User.current.allowed_to?(:add_board_watchers, @project) ||
(@board.watchers.present? && User.current.allowed_to?(:view_board_watchers, @project)) %>
<div id="watchers">
<%= render :partial => 'watchers/watchers', :locals => {:watched => @board} %>
</div>
<% end %>
<% end %>

View File

@ -1,52 +1,55 @@
<ul>
<ul class="menu">
<%= call_hook(:view_issues_context_menu_start, {:issues => @issues, :can => @can, :back => @back }) %>
<% if !@issue.nil? -%>
<li><%= context_menu_link l(:button_edit), {:controller => 'issues', :action => 'edit', :id => @issue},
<li class="edit"><%= context_menu_link l(:button_edit), {:controller => 'issues', :action => 'edit', :id => @issue},
:class => 'icon-edit', :disabled => !@can[:edit] %></li>
<% else %>
<li><%= context_menu_link l(:button_edit), {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id)},
<li class="edit"><%= context_menu_link l(:button_edit), {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id)},
:class => 'icon-edit', :disabled => !@can[:edit] %></li>
<% end %>
<% if @allowed_statuses.present? %>
<li class="folder">
<a href="#" class="submenu" onclick="return false;"><%= l(:field_status) %></a>
<li class="folder status">
<a href="#" class="context_item" onclick="return false;"><%= l(:field_status) %></a>
<ul>
<% @statuses.each do |s| -%>
<li><%= context_menu_link s.name, {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id), :issue => {:status_id => s}, :back_url => @back}, :method => :post,
:selected => (@issue && s == @issue.status), :disabled => !(@can[:update] && @allowed_statuses.include?(s)) %></li>
<% end -%>
</ul>
<div class="submenu"></div>
</li>
<% end %>
<% unless @trackers.nil? %>
<li class="folder">
<a href="#" class="submenu"><%= l(:field_tracker) %></a>
<li class="folder tracker">
<a href="#" class="context_item"><%= l(:field_tracker) %></a>
<ul>
<% @trackers.each do |t| -%>
<li><%= context_menu_link t.name, {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id), :issue => {'tracker_id' => t}, :back_url => @back}, :method => :post,
:selected => (@issue && t == @issue.tracker), :disabled => !@can[:edit] %></li>
<% end -%>
</ul>
<div class="submenu"></div>
</li>
<% end %>
<li class="folder">
<a href="#" class="submenu"><%= l(:field_priority) %></a>
<li class="folder priority">
<a href="#" class="context_item"><%= l(:field_priority) %></a>
<ul>
<% @priorities.each do |p| -%>
<li><%= context_menu_link p.name, {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id), :issue => {'priority_id' => p}, :back_url => @back}, :method => :post,
:selected => (@issue && p == @issue.priority), :disabled => (!@can[:edit] || @issues.detect {|i| !i.leaf?}) %></li>
<% end -%>
</ul>
<div class="submenu"></div>
</li>
<% #TODO: allow editing versions when multiple projects %>
<% unless @project.nil? || @project.shared_versions.open.empty? -%>
<li class="folder">
<a href="#" class="submenu"><%= l(:field_fixed_version) %></a>
<li class="folder fixed_version">
<a href="#" class="context_item"><%= l(:field_fixed_version) %></a>
<ul>
<% @project.shared_versions.open.sort.each do |v| -%>
<li><%= context_menu_link format_version_name(v), {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id), :issue => {'fixed_version_id' => v}, :back_url => @back}, :method => :post,
@ -55,11 +58,13 @@
<li><%= context_menu_link l(:label_none), {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id), :issue => {'fixed_version_id' => 'none'}, :back_url => @back}, :method => :post,
:selected => (@issue && @issue.fixed_version.nil?), :disabled => !@can[:update] %></li>
</ul>
<div class="submenu"></div>
</li>
<% end %>
<% if @assignables.present? -%>
<li class="folder">
<a href="#" class="submenu"><%= l(:field_assigned_to) %></a>
<li class="folder assigned">
<a href="#" class="context_item"><%= l(:field_assigned_to) %></a>
<ul>
<% @assignables.each do |u| -%>
<li><%= context_menu_link u.name, {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id), :issue => {'assigned_to_id' => u}, :back_url => @back}, :method => :post,
@ -68,11 +73,13 @@
<li><%= context_menu_link l(:label_nobody), {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id), :issue => {'assigned_to_id' => 'none'}, :back_url => @back}, :method => :post,
:selected => (@issue && @issue.assigned_to.nil?), :disabled => !@can[:update] %></li>
</ul>
<div class="submenu"></div>
</li>
<% end %>
<% unless @project.nil? || @project.issue_categories.empty? -%>
<li class="folder">
<a href="#" class="submenu"><%= l(:field_category) %></a>
<a href="#" class="context_item"><%= l(:field_category) %></a>
<ul>
<% @project.issue_categories.each do |u| -%>
<li><%= context_menu_link u.name, {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id), :issue => {'category_id' => u}, :back_url => @back}, :method => :post,
@ -81,28 +88,30 @@
<li><%= context_menu_link l(:label_none), {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id), :issue => {'category_id' => 'none'}, :back_url => @back}, :method => :post,
:selected => (@issue && @issue.category.nil?), :disabled => !@can[:update] %></li>
</ul>
</li>
<div class="submenu"></div>
</li>
<% end -%>
<% if Issue.use_field_for_done_ratio? %>
<li class="folder">
<a href="#" class="submenu"><%= l(:field_done_ratio) %></a>
<li class="folder done_ratio">
<a href="#" class="context_item"><%= l(:field_done_ratio) %></a>
<ul>
<% (0..10).map{|x|x*10}.each do |p| -%>
<li><%= context_menu_link "#{p}%", {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id), :issue => {'done_ratio' => p}, :back_url => @back}, :method => :post,
:selected => (@issue && p == @issue.done_ratio), :disabled => (!@can[:edit] || @issues.detect {|i| !i.leaf?}) %></li>
<% end -%>
</ul>
<div class="submenu"></div>
</li>
<% end %>
<% if !@issue.nil? %>
<% if @can[:log_time] -%>
<li><%= context_menu_link l(:button_log_time), {:controller => 'timelog', :action => 'new', :issue_id => @issue},
:class => 'icon-time-add' %></li>
<li class="log_time"><%= context_menu_link l(:button_log_time), {:controller => 'timelog', :action => 'new', :issue_id => @issue},
:class => 'context_item' %></li>
<% end %>
<% if User.current.logged? %>
<li><%= watcher_link(@issue, User.current) %></li>
<li class="watch"><%= watcher_link(@issue, User.current) %></li>
<% end %>
<% end %>
@ -110,12 +119,13 @@
<li><%= context_menu_link l(:button_duplicate), {:controller => 'issues', :action => 'new', :project_id => @project, :copy_from => @issue},
:class => 'icon-duplicate', :disabled => !@can[:copy] %></li>
<% end %>
<li><%= context_menu_link l(:button_copy), new_issue_move_path(:ids => @issues.collect(&:id), :copy_options => {:copy => 't'}),
:class => 'icon-copy', :disabled => !@can[:move] %></li>
<li><%= context_menu_link l(:button_move), new_issue_move_path(:ids => @issues.collect(&:id)),
:class => 'icon-move', :disabled => !@can[:move] %></li>
<li><%= context_menu_link l(:button_delete), {:controller => 'issues', :action => 'destroy', :ids => @issues.collect(&:id), :back_url => @back},
:method => :post, :confirm => l(:text_issues_destroy_confirmation), :class => 'icon-del', :disabled => !@can[:delete] %></li>
<li class="move"><%= context_menu_link l(:button_move), new_issue_move_path(:ids => @issues.collect(&:id)),
:class => 'context_item', :disabled => !@can[:move] %></li>
<li class="copy"><%= context_menu_link l(:button_copy), new_issue_move_path(:ids => @issues.collect(&:id), :copy_options => {:copy => 't'}),
:class => 'context_item' %></li>
<li class="delete"><%= context_menu_link l(:button_delete), {:controller => 'issues', :action => 'destroy', :ids => @issues.collect(&:id)},
:method => :post, :confirm => l(:text_issues_destroy_confirmation), :class => 'context_item', :disabled => !@can[:delete] %></li>
<%= call_hook(:view_issues_context_menu_end, {:issues => @issues, :can => @can, :back => @back }) %>
</ul>

View File

@ -9,6 +9,15 @@
<p><label for="document_description"><%=l(:field_description)%></label>
<%= text_area 'document', 'description', :cols => 60, :rows => 15, :class => 'wiki-edit' %></p>
<% if User.current.allowed_to?(:add_document_watchers, @project) -%>
<p id="watchers_form"><label><%= l(:label_document_watchers) %></label>
<% @document.project.users.sort.each do |user| -%>
<label class="floating"><%= check_box_tag 'document[watcher_user_ids][]', user.id, @document.watched_by?(user) %> <%=h user %></label>
<% end -%>
</p>
<% end %>
<!--[eoform:document]-->
</div>

View File

@ -27,6 +27,16 @@
<% html_title h(@document.title) -%>
<% content_for :sidebar do %>
<% if User.current.allowed_to?(:add_document_watchers, @project) ||
(@document.watchers.present? && User.current.allowed_to?(:view_document_watchers, @project)) %>
<div id="watchers">
<%= render :partial => 'watchers/watchers', :locals => {:watched => @document} %>
</div>
<% end %>
<% end %>
<% content_for :header_tags do %>
<%= stylesheet_link_tag 'scm' %>
<% end %>

View File

@ -41,17 +41,5 @@
</div>
<div class="splitcontentright">
<% if projects.any? %>
<fieldset><legend><%=l(:label_project_new)%></legend>
<% remote_form_for(:membership, :url => { :action => 'edit_membership', :id => @group }) do %>
<%= label_tag "membership_project_id", l(:description_choose_project), :class => "hidden-for-sighted" %>
<%= select_tag 'membership[project_id]', options_for_membership_project_select(@group, projects) %>
<p><%= l(:label_role_plural) %>:
<% roles.each do |role| %>
<label><%= check_box_tag 'membership[role_ids][]', role.id %> <%=h role %></label>
<% end %></p>
<p><%= submit_tag l(:button_add) %></p>
<% end %>
</fieldset>
<% end %>
<%= render :partial => 'members/membership_assignment', :locals => {:principal => @group, :projects => projects - @group.projects, :roles => roles } %>
</div>

View File

@ -33,7 +33,7 @@
<%= observe_field(:user_search,
:frequency => 0.5,
:update => :users,
:url => { :controller => 'groups', :action => 'autocomplete_for_user', :id => @group },
:url => auto_complete_users_path(:remove_group_members => @group),
:with => 'q')
%>

View File

@ -13,20 +13,22 @@
}
a.new { color: #b73535; }
.CodeRay .c { color:#666; }
.syntaxhl .line-numbers { padding: 2px 4px 2px 4px; background-color: #eee; margin:0 }
.syntaxhl .comment { color:#666; }
.CodeRay .cl { color:#B06; font-weight:bold }
.CodeRay .dl { color:black }
.CodeRay .fu { color:#06B; font-weight:bold }
.syntaxhl .class { color:#B06; font-weight:bold }
.syntaxhl .delimiter { color:black }
.syntaxhl .function { color:#06B; font-weight:bold }
.CodeRay .il { background: #eee }
.CodeRay .il .idl { font-weight: bold; color: #888 }
.syntaxhl .inline { background: #eee }
.syntaxhl .inline .inline-delimiter { font-weight: bold; color: #888 }
.CodeRay .iv { color:#33B }
.CodeRay .r { color:#080; font-weight:bold }
.syntaxhl .instance-variable { color:#33B }
.syntaxhl .reserved { color:#080; font-weight:bold }
.syntaxhl .string { background-color:#fff0f0; color: #D20; }
.syntaxhl .string .delimiter { color:#710 }
.CodeRay .s { background-color:#fff0f0 }
.CodeRay .s .dl { color:#710 }
<% end %>
<% html_title "Wiki Formatting" %>
@ -236,7 +238,7 @@ To go live, all you need to add is a database and a web server.
<h2><a name="13" class="wiki-page"></a>Code highlighting</h2>
<p>Code highlightment relies on <a href="http://coderay.rubychan.de/" class="external">CodeRay</a>, a fast syntax highlighting library written completely in Ruby. It currently supports c, cpp, css, delphi, groovy, html, java, javascript, json, php, python, rhtml, ruby, scheme, sql, xml and yaml languages.</p>
<p>The default code highlighting relies on <a href="http://coderay.rubychan.de/" class="external">CodeRay</a>, a fast syntax highlighting library written completely in Ruby. It currently supports c, cpp, css, delphi, groovy, html, java, javascript, json, php, python, rhtml, ruby, scheme, sql, xml and yaml languages.</p>
<p>You can highlight code in your wiki page using this syntax:</p>
@ -248,15 +250,14 @@ To go live, all you need to add is a database and a web server.
<p>Example:</p>
<pre><code class="ruby CodeRay"><span class="no"> 1</span> <span class="c"># The Greeter class</span>
<span class="no"> 2</span> <span class="r">class</span> <span class="cl">Greeter</span>
<span class="no"> 3</span> <span class="r">def</span> <span class="fu">initialize</span>(name)
<span class="no"> 4</span> <span class="iv">@name</span> = name.capitalize
<span class="no"> 5</span> <span class="r">end</span>
<span class="no"> 6</span>
<span class="no"> 7</span> <span class="r">def</span> <span class="fu">salute</span>
<span class="no"> 8</span> puts <span class="s"><span class="dl">"</span><span class="k">Hello </span><span class="il"><span class="idl">#{</span><span class="iv">@name</span><span class="idl">}</span></span><span class="k">!</span><span class="dl">"</span></span>
<span class="no"> 9</span> <span class="r">end</span>
<span class="no"><strong>10</strong></span> <span class="r">end</span>
</code>
<pre><code class="ruby syntaxhl"><span class="line-numbers"> 1</span> <span class="comment"># The Greeter class</span>
<span class="line-numbers"> 2</span> <span class="reserved">class</span> <span class="class">Greeter</span>
<span class="line-numbers"> 3</span> <span class="reserved">def</span> <span class="function">initialize</span>(name)
<span class="line-numbers"> 4</span> <span class="instance-variable">@name</span> = name.capitalize
<span class="line-numbers"> 5</span> <span class="reserved">end</span>
<span class="line-numbers"> 6</span>
<span class="line-numbers"> 7</span> <span class="reserved">def</span> <span class="function">salute</span>
<span class="line-numbers"> 8</span> puts <span class="string"><span class="delimiter">"</span><span class="content">Hello </span><span class="inline"><span class="inline-delimiter">#{</span><span class="instance-variable">@name</span><span class="inline-delimiter">}</span></span><span class="content">!</span><span class="delimiter">"</span></span>
<span class="line-numbers"> 9</span> <span class="reserved">end</span>
<span class="line-numbers"><strong>10</strong></span> <span class="reserved">end</span></code>
</pre>

View File

@ -38,7 +38,7 @@
<%= call_hook(:view_issues_edit_notes_bottom, { :issue => @issue, :notes => @notes, :form => f }) %>
</fieldset>
<fieldset id="attachments" class="collapsible collapsed borders"><legend onclick="toggleFieldset(this);"><%=l(:label_attachment_plural)%></legend>
<fieldset id="attachments" class="header_collapsible collapsible collapsed"><legend onclick="toggleFieldset(this);"><%=l(:label_attachment_plural)%></legend>
<div style="display: none;">
<%= render :partial => 'attachments/form' %>
</div>

View File

@ -1,10 +1,9 @@
<div class="contextual">
<% if authorize_for('issue_relations', 'new') %>
<%= toggle_link l(:button_add), 'new-relation-form', {:focus => 'relation_issue_to_id'} %>
<% end %>
</div>
<p><strong><%=l(:label_related_issues)%></strong></p>
<p>
<strong><%=l(:label_related_issues)%></strong>
<% if authorize_for('issue_relations', 'new') %>
(<%= toggle_link l(:button_add), 'new-relation-form', {:focus => 'relation_issue_to_id'} %>)
<% end %>
</p>
<% if @relations.present? %>
<table style="width:100%">

View File

@ -1,16 +1,6 @@
<h3><%= l(:label_issue_plural) %></h3>
<%= link_to l(:label_issue_view_all), { :controller => 'issues', :action => 'index', :project_id => @project, :set_filter => 1 } %><br />
<% if @project %>
<%= link_to l(:field_summary), :controller => 'reports', :action => 'issue_report', :id => @project %><br />
<% end %>
<%= call_hook(:view_issues_sidebar_issues_bottom) %>
<% if User.current.allowed_to?(:view_calendar, @project, :global => true) %>
<%= link_to(l(:label_calendar), :controller => 'calendars', :action => 'show', :project_id => @project) %><br />
<% end %>
<% if User.current.allowed_to?(:view_gantt, @project, :global => true) %>
<%= link_to(l(:label_gantt), :controller => 'gantts', :action => 'show', :project_id => @project) %><br />
<% end %>
<%= call_hook(:view_issues_sidebar_planning_bottom) %>
<%= render_sidebar_queries %>

View File

@ -1,23 +1,29 @@
<div class="title-bar">
<h2><%= @query.new_record? ? l(:label_issue_plural) : h(@query.name) %></h2>
<div class="title-bar-extras">
<div class="contextual">
<% if !@query.new_record? && @query.editable_by?(User.current) %>
<%= link_to l(:button_edit), {:controller => 'queries', :action => 'edit', :id => @query}, :class => 'icon icon-edit' %>
<%= link_to l(:button_delete), {:controller => 'queries', :action => 'destroy', :id => @query}, :confirm => l(:text_are_you_sure), :method => :post, :class => 'icon icon-del' %>
<% end %>
<%= render :partial => 'queries/new_issue_button' %>
</div>
<h2><%= @query.new_record? ? l(:label_issue_plural) : h(@query.name) %></h2>
<% html_title(@query.new_record? ? l(:label_issue_plural) : h(@query.name)) %>
<% form_tag({ :controller => 'queries', :action => 'new' }, :id => 'query_form') do %>
<%= hidden_field_tag('project_id', @project.to_param) if @project %>
<div id="query_form_content" class="hide-when-print">
<fieldset id="filters" class="collapsible <%= @query.new_record? ? "" : "collapsed" %>">
<fieldset id="filters" class="header_collapsible collapsible <%= @query.new_record? ? "" : "collapsed" %>">
<legend onclick="toggleFieldset(this);"><%= l(:label_filter_plural) %></legend>
<div style="<%= @query.new_record? ? "" : "display: none;" %>">
<div class="filter-fields" style="<%= @query.new_record? ? "" : "display: none;" %>">
<%= render :partial => 'queries/filters', :locals => {:query => @query} %>
</div>
</fieldset>
<fieldset class="collapsible collapsed">
<fieldset id="column_options" class="header_collapsible collapsible collapsed">
<legend onclick="toggleFieldset(this);"><%= l(:label_options) %></legend>
<div style="display: none;">
<table>
@ -29,6 +35,20 @@
<td><label for='group_by'><%= l(:field_group_by) %></label></td>
<td><%= select_tag('group_by', options_for_select([[]] + @query.groupable_columns.collect {|c| [c.caption, c.name.to_s]}, @query.group_by)) %></td>
</tr>
<tr>
<td><%= l(:label_project_plural) %></td>
<td>
<label>
<%= radio_button_tag('display_subprojects', '0', !@query.display_subprojects?) %>
<%= l(:text_current_project) %>
</label>
<br />
<label>
<%= radio_button_tag('display_subprojects', '1', @query.display_subprojects?) %>
<%= l(:label_and_its_subprojects, :value => l(:text_current_project)) %>
</label>
</td>
</tr>
</table>
</div>
</fieldset>
@ -55,6 +75,9 @@
</p>
<% end %>
</div><!-- .title-bar-extras -->
</div><!-- .title-bar -->
<%= error_messages_for 'query' %>
<% if @query.valid? %>
<% if @issues.empty? %>

View File

@ -1,63 +1,75 @@
<%= render :partial => 'action_menu' %>
<h2><%= h(@issue.tracker.name) %> #<%= h(@issue.id) %><%= call_hook(:view_issues_show_identifier, :issue => @issue) %></h2>
<div class="title-bar" id="upper-title-bar">
<div class="title-bar-actions">
<%= render :partial => 'action_menu' %>
</div>
</div>
<div class="<%= @issue.css_classes %> details">
<%= avatar(@issue.author, :size => "50") %>
<div class="subject">
<%= render_issue_subject_with_tree(@issue) %>
</div>
<p class="author">
<%= authoring @issue.created_on, @issue.author %>.
<% if @issue.created_on != @issue.updated_on %>
<%= l(:label_updated_time, time_tag(@issue.updated_on)) %>.
<h1 class="subject">
<%=h(@issue.subject) %> (<%= h(@issue.tracker.name) + ' #' +@issue.id.to_s %>)
</h1>
<hr />
<p class="author">
<%= avatar(@issue.author, :size => "14") %>
<%= authoring @issue.created_on, @issue.author %>.
<% if @issue.created_on != @issue.updated_on %>
<%= l(:label_updated_time, time_tag(@issue.updated_on)) %>.
<% end %>
</p>
<hr />
<div class="meta">
<table class="attributes">
<tr>
<th class="status"><%=l(:field_status)%>:</th><td class="status"><%= h(@issue.status.name) %></td>
<th class="start-date"><%=l(:field_start_date)%>:</th><td class="start-date"><%= format_date(@issue.start_date) %></td>
</tr>
<tr>
<th class="priority"><%=l(:field_priority)%>:</th><td class="priority"><%= h(@issue.priority.name) %></td>
<th class="due-date"><%=l(:field_due_date)%>:</th><td class="due-date"><%= format_date(@issue.due_date) %></td>
</tr>
<tr>
<th class="assigned-to"><%=l(:field_assigned_to)%>:</th><td class="assigned-to"><%= avatar(@issue.assigned_to, :size => "14") %><%= @issue.assigned_to ? link_to_user(@issue.assigned_to) : "-" %></td>
<th class="progress"><%=l(:field_done_ratio)%>:</th><td class="progress"><%= progress_bar @issue.done_ratio, :width => '80px', :legend => "#{@issue.done_ratio}%" %></td>
</tr>
<tr>
<th class="category"><%=l(:field_category)%>:</th><td class="category"><%=h(@issue.category ? @issue.category.name : "-") %></td>
<% if User.current.allowed_to?(:view_time_entries, @project) %>
<th class="spent-time"><%=l(:label_spent_time)%>:</th>
<td class="spent-time"><%= @issue.spent_hours > 0 ? (link_to l_hours(@issue.spent_hours), {:controller => 'timelog', :action => 'index', :project_id => @project, :issue_id => @issue}) : "-" %></td>
<% end %>
</p>
</tr>
<tr>
<th class="fixed-version"><%=l(:field_fixed_version)%>:</th><td class="fixed-version"><%= @issue.fixed_version ? link_to_version(@issue.fixed_version) : "-" %></td>
<% if @issue.estimated_hours %>
<th class="estimated-hours"><%=l(:field_estimated_hours)%>:</th><td class="estimated-hours"><%= l_hours(@issue.estimated_hours) %></td>
<% end %>
</tr>
<%= render_custom_fields_rows(@issue) %>
<%= call_hook(:view_issues_show_details_bottom, :issue => @issue) %>
</table>
</div><!-- .meta -->
<table class="attributes">
<tr>
<th class="status"><%=l(:field_status)%>:</th><td class="status"><%= h(@issue.status.name) %></td>
<th class="start-date"><%=l(:field_start_date)%>:</th><td class="start-date"><%= format_date(@issue.start_date) %></td>
</tr>
<tr>
<th class="priority"><%=l(:field_priority)%>:</th><td class="priority"><%= h(@issue.priority.name) %></td>
<th class="due-date"><%=l(:field_due_date)%>:</th><td class="due-date"><%= format_date(@issue.due_date) %></td>
</tr>
<tr>
<th class="assigned-to"><%=l(:field_assigned_to)%>:</th><td class="assigned-to"><%= avatar(@issue.assigned_to, :size => "14") %><%= @issue.assigned_to ? link_to_user(@issue.assigned_to) : "-" %></td>
<th class="progress"><%=l(:field_done_ratio)%>:</th><td class="progress"><%= progress_bar @issue.done_ratio, :width => '80px', :legend => "#{@issue.done_ratio}%" %></td>
</tr>
<tr>
<th class="category"><%=l(:field_category)%>:</th><td class="category"><%=h(@issue.category ? @issue.category.name : "-") %></td>
<% if User.current.allowed_to?(:view_time_entries, @project) %>
<th class="spent-time"><%=l(:label_spent_time)%>:</th>
<td class="spent-time"><%= @issue.spent_hours > 0 ? (link_to l_hours(@issue.spent_hours), {:controller => 'timelog', :action => 'index', :project_id => @project, :issue_id => @issue}) : "-" %></td>
<% end %>
</tr>
<tr>
<th class="fixed-version"><%=l(:field_fixed_version)%>:</th><td class="fixed-version"><%= @issue.fixed_version ? link_to_version(@issue.fixed_version) : "-" %></td>
<% if @issue.estimated_hours %>
<th class="estimated-hours"><%=l(:field_estimated_hours)%>:</th><td class="estimated-hours"><%= l_hours(@issue.estimated_hours) %></td>
<% end %>
</tr>
<%= render_custom_fields_rows(@issue) %>
<%= call_hook(:view_issues_show_details_bottom, :issue => @issue) %>
</table>
<% if @issue.description? || @issue.attachments.any? -%>
<hr />
<% if @issue.description? %>
<div class="contextual">
<%= link_to_remote_if_authorized(l(:button_quote), { :url => {:controller => 'journals', :action => 'new', :id => @issue} }, :class => 'icon icon-comment') %>
</div>
<hr />
<p><strong><%=l(:field_description)%></strong></p>
<div class="wiki">
<%= textilizable @issue, :description, :attachments => @issue.attachments %>
</div>
<div class="description">
<div class="contextual">
<%= link_to_remote_if_authorized(l(:button_quote), { :url => {:controller => 'journals', :action => 'new', :id => @issue} }, :class => 'icon icon-comment') %>
</div>
<p><strong><%=l(:field_description)%></strong></p>
<div class="wiki">
<%= textilizable @issue, :description, :attachments => @issue.attachments %>
</div>
</div>
<% end %>
<%= link_to_attachments @issue %>
<% if @issue.attachments.any? -%>
<hr />
<%= link_to_attachments @issue %>
<% end -%>
<%= call_hook(:view_issues_show_description_bottom, :issue => @issue) %>
@ -65,10 +77,11 @@
<% if !@issue.leaf? || User.current.allowed_to?(:manage_subtasks, @project) %>
<hr />
<div id="issue_tree">
<div class="contextual">
<%= link_to(l(:button_add), {:controller => 'issues', :action => 'new', :project_id => @project, :issue => {:parent_issue_id => @issue}}) if User.current.allowed_to?(:manage_subtasks, @project) %>
</div>
<p><strong><%=l(:label_subtask_plural)%></strong></p>
<p>
<strong><%=l(:label_subtask_plural)%></strong>
(<%= link_to(l(:button_add), {:controller => 'issues', :action => 'new', :project_id => @project, :issue => {:parent_issue_id => @issue}}) if User.current.allowed_to?(:manage_subtasks, @project) %>)
</p>
<%= render_descendants_tree(@issue) unless @issue.leaf? %>
</div>
<% end %>
@ -78,6 +91,7 @@
<div id="relations">
<%= render :partial => 'relations' %>
</div>
<hr />
<% end %>
</div>
@ -91,14 +105,19 @@
<% if @journals.present? %>
<div id="history">
<h3><%=l(:label_history)%></h3>
<h3 class="rounded-background"><%=l(:label_history)%></h3>
<%= render :partial => 'history', :locals => { :issue => @issue, :journals => @journals } %>
</div>
<% end %>
<div style="clear: both;"></div>
<%= render :partial => 'action_menu' %>
<div class="title-bar" id="lower-title-bar">
<div class="title-bar-actions">
<%= render :partial => 'action_menu', :locals => {:replace_watcher => 'watcher2' } %>
</div>
</div>
<div style="clear: both;"></div>
<% if authorize_for('issues', 'edit') %>

View File

@ -0,0 +1,9 @@
<p><%= authoring @journal.created_at, @journal.user, :label => :label_updated_time_by %></p>
<div class="text-diff">
<%= simple_format_without_paragraph @diff.to_html %>
</div>
<p><%= link_to(l(:button_back), issue_path(@issue)) %></p>
<% html_title "#{@issue.tracker.name} ##{@issue.id}: #{@issue.subject}" %>

View File

@ -1,8 +1,6 @@
<% unless controller_name == 'admin' && action_name == 'index' %>
<% content_for :sidebar do %>
<h3><%=l(:label_administration)%></h3>
<%= render :partial => 'admin/menu' %>
<% end %>
<% @page_header_title = l(:label_administration) %>
<% content_for :main_menu do %>
<%= render :partial => 'admin/menu' %>
<% end %>
<%= render :file => "layouts/base" %>

View File

@ -7,76 +7,149 @@
<meta name="keywords" content="issue,bug,tracker" />
<%= csrf_meta_tag %>
<%= favicon %>
<%= stylesheet_link_tag 'reset', :media => 'all' %>
<%= stylesheet_link_tag 'smoothness/jquery-ui', :media => 'all' %>
<%= stylesheet_link_tag 'application', :media => 'all' %>
<%= stylesheet_link_tag 'print', :media => 'print' %>
<%= stylesheet_link_tag 'rtl', :media => 'all' if l(:direction) == 'rtl' %>
<!--[if lte IE 6]><%= stylesheet_link_tag 'ie6', :media => 'all' %><![endif]-->
<!--[if lte IE 7]><%= stylesheet_link_tag 'ie7', :media => 'all' %><![endif]-->
<!--[if gte IE 8]><![endif]-->
<%= javascript_include_tag 'jquery.min.js' %>
<%= javascript_include_tag 'jquery-ui.min.js' %>
<%= javascript_include_tag 'jquery.menu_expand.js' %>
<%= javascript_tag('jQuery.noConflict();') %>
<%= javascript_heads %>
<%= stylesheet_link_tag 'jstoolbar' %>
<%= heads_for_theme %>
<!--[if IE 6]>
<style type="text/css">
* html body{ width: expression( document.documentElement.clientWidth < 900 ? '900px' : '100%' ); }
body {behavior: url(<%= stylesheet_path "csshover.htc" %>);}
</style>
<![endif]-->
<% heads_for_wiki_formatter %>
<%= call_hook :view_layouts_base_html_head %>
<!-- page specific tags -->
<%= yield :header_tags -%>
</head>
<body class="<%=h body_css_classes %>">
<div id="wrapper">
<div id="wrapper2">
<div id="top-menu">
<div id="account">
<%= render_menu :account_menu -%>
</div>
<%= content_tag('div', "#{l(:label_logged_as)} #{link_to_user(User.current, :format => :username)}", :id => 'loggedas') if User.current.logged? %>
<%= render_menu :top_menu if User.current.logged? || !Setting.login_required? -%>
</div>
<div id="top-menu">
<div id="header">
<div id="logo"><%= link_to(h(Setting.app_title), home_path) %></div>
<div id="top-menu-items">
<div id="search">
<%= render :partial => 'search/quick_search', :locals => {:search_term => @question} %>
</div>
<div id="header">
<% if User.current.logged? || !Setting.login_required? %>
<div id="quick-search">
<% form_tag({:controller => 'search', :action => 'index', :id => @project}, :method => :get ) do %>
<%= hidden_field_tag(controller.default_search_scope, 1, :id => nil) if controller.default_search_scope %>
<label for='q'>
<%= link_to l(:label_search), {:controller => 'search', :action => 'index', :id => @project}, :accesskey => accesskey(:search) %>:
</label>
<%= text_field_tag 'q', @question, :size => 20, :class => 'small', :accesskey => accesskey(:quick_search) %>
<% if User.current.logged? || !Setting.login_required? %>
<ul id="account-nav">
<% main_top_menu_items.each do |item| %>
<%= render_menu_node(item) %>
<% end %>
<%= render_project_jump_box %>
<li class="drop-down">
<%= link_to l(:label_project_plural), { :controller => 'projects', :action => 'index' }, :class => "projects" %>
<ul style="display:none;">
<li><%= link_to l(:label_project_all), { :controller => 'projects', :action => 'index' }, :class => "projects separator" %></li>
<%
project_content = ''
project_tree(User.current.projects.all) do |project, level|
name_prefix = (level > 0 ? ('&nbsp;' * 2 * level + '&#187; ') : '')
project_content << content_tag(:li,
link_to(name_prefix + h(project), {:controller => 'projects', :action => 'show', :id => project, :jump => current_menu_item}))
end
%>
<%= project_content %>
</ul>
</li>
<% if more_top_menu_items.present? || User.current.admin? %>
<li class="drop-down" id="more-menu">
<a class="more" href="#"><%= l(:label_more) %></a>
<ul style="display:none;">
<% more_top_menu_items.each do |item| %>
<%= render_menu_node(item) %>
<% end %>
<%# TODO: Extract to helper %>
<% if User.current.admin? %>
<% menu_items_for(:admin_menu) do |item| -%>
<li><%= link_to h(item.caption), item.url, item.html_options %></li>
<% end -%>
<% end %>
</ul>
</li>
<% end %>
<li>
<%= render_menu_node(help_menu_item) %>
<% unless User.current.logged? %>
<% if Setting.self_registration? %>
<li>
<%= link_to l(:label_register), { :controller => 'account', :action => 'register' } %>
</li>
<% end %>
<li id="login-menu" class="drop-down last-child">
<%= link_to l(:label_login), {:controller => 'account', :action => 'login'}, :class => 'login' %>
<div id="nav-login" style="display:none;">
<%= render :partial => 'account/login' %>
</div>
</li>
<% else %>
<li class="drop-down last-child">
<%= link_to_user(User.current) %>
<ul style="display:none;">
<% menu_items_for(:account_menu) do |item| %>
<%= render_menu_node(item) %>
<% end %>
</ul>
</li>
<% end %>
</li>
</ul>
<% end %>
</div>
</div>
<div id="breadcrumb">
<%= page_header_title %>
</div>
</div>
<% main_menu = render_main_menu(@project) %>
<% if (side_displayed = has_content?(:sidebar) || has_content?(:main_menu) || !main_menu.blank?) %>
<% display_sidebar = true %>
<% else %>
<% display_sidebar = false %>
<% end %>
<div id="main" class="<%= side_displayed ? '' : "nosidebar" %>">
<% if (side_displayed) %>
<div id="side-container">
<div id="main-menu">
<%= main_menu %>
<%= yield :main_menu %>
</div>
<% if display_sidebar %>
<!-- Sidebar -->
<div id="sidebar">
<%= yield :sidebar %>
<%= call_hook :view_layouts_base_sidebar %>
</div>
<% end %>
</div>
<%= expand_current_menu %>
<% end %>
<h1><%= page_header_title %></h1>
<% if display_main_menu?(@project) %>
<div id="main-menu">
<%= render_main_menu(@project) %>
<div class="<%= side_displayed ? '' : "nosidebar" %>" id="content">
<%= render_flash_messages %>
<%= yield %>
<%= call_hook :view_layouts_base_content %>
<div style="clear:both;">&nbsp;</div>
</div>
<% end %>
</div>
</div>
<%= tag('div', {:id => 'main', :class => (has_content?(:sidebar) ? '' : 'nosidebar')}, true) %>
<div id="sidebar">
<%= yield :sidebar %>
<%= call_hook :view_layouts_base_sidebar %>
</div>
<div id="footer">
<div class="bgl"><div class="bgr">
<%= l(:text_powered_by, :link => link_to(Redmine::Info.app_name, Redmine::Info.url)) %>
</div></div>
</div>
<div id="content">
<%= render_flash_messages %>
<%= yield %>
<%= call_hook :view_layouts_base_content %>
<div style="clear:both;"></div>
</div>
</div>
<div id="ajax-indicator" style="display:none;"><span><%= l(:label_loading) %></span></div>
<div id="dialog-window" style="display:none;"></div>
<div id="ajax-indicator" style="display:none;"><span><%= l(:label_loading) %></span></div>
<div id="footer">
<div class="bgl"><div class="bgr">
<%= l(:text_powered_by, :link => link_to(Redmine::Info.app_name, Redmine::Info.url)) %>
</div></div>
</div>
</div>
</div>
<%= call_hook :view_layouts_base_body_bottom %>
</body>

View File

@ -0,0 +1,2 @@
<p><%= l(:text_mail_handler_confirmation_successful) %><br />
<%= auto_link(@url) %></p>

View File

@ -0,0 +1,2 @@
<%= l(:text_mail_handler_confirmation_successful) %>
<%= @url %>

View File

@ -0,0 +1,3 @@
<p><%= l(:label_mail_handler_errors_with_submission) %></p>
<p><%= h(@errors) %></p>

View File

@ -0,0 +1,3 @@
<%= l(:label_mail_handler_errors_with_submission) %>
<%= h(@errors) %>

View File

@ -0,0 +1 @@
<%= l(:notice_not_authorized_action) %>

View File

@ -0,0 +1 @@
<%= l(:notice_not_authorized_action) %>

View File

@ -0,0 +1,23 @@
<% if projects.any? %>
<fieldset><legend><%=l(:label_project_new)%></legend>
<% remote_form_for(:membership, :url => { :action => 'edit_membership', :id => principal }) do %>
<p><%= text_field_tag 'project_search', nil, :size => "40" %></p>
<%= observe_field(:project_search,
:frequency => 0.5,
:update => :projects,
:url => { :controller => 'auto_completes', :action => 'projects', :id => principal },
:with => 'q')
%>
<div id="projects">
<%= principals_check_box_tags 'project_ids[]', projects %>
</div>
<p><%= l(:label_role_plural) %>:
<% roles.each do |role| %>
<label><%= check_box_tag 'membership[role_ids][]', role.id %> <%=h role %></label>
<% end %></p>
<p><%= submit_tag l(:button_add) %></p>
<% end %>
</fieldset>
<% end %>

View File

@ -63,4 +63,14 @@
<%= stylesheet_link_tag 'scm' %>
<% end %>
<% content_for :sidebar do %>
<% if User.current.allowed_to?(:add_message_watchers, @project) ||
(@topic.watchers.present? && User.current.allowed_to?(:view_message_watchers, @project)) %>
<div id="watchers">
<%= render :partial => 'watchers/watchers', :locals => {:watched => @topic} %>
</div>
<% end %>
<% end %>
<% html_title h(@topic.subject) %>

View File

@ -2,7 +2,7 @@
<%= auto_discovery_link_tag(:atom, {:action => 'index', :format => 'atom', :key => User.current.rss_key}) %>
<% end %>
<div class="contextual">
<div id="project-links" class="contextual">
<%= link_to(l(:label_project_new), {:controller => 'projects', :action => 'new'}, :class => 'icon icon-add') + ' |' if User.current.allowed_to?(:add_project, nil, :global => true) %>
<%= link_to(l(:label_issue_view_all), { :controller => 'issues' }) + ' |' if User.current.allowed_to?(:view_issues, nil, :global => true) %>
<%= link_to(l(:label_overall_spent_time), { :controller => 'time_entries' }) + ' |' if User.current.allowed_to?(:view_time_entries, nil, :global => true) %>
@ -13,6 +13,10 @@
<h2><%=l(:label_project_plural)%></h2>
<div class="wiki">
<%= textilizable Setting.welcome_text %>
</div>
<%= call_hook(:view_projects_show_top) %>
<%= render_project_hierarchy(@projects)%>

View File

@ -13,7 +13,7 @@
<td><%=h(category.assigned_to.name) if category.assigned_to %></td>
<td class="buttons">
<%= link_to_if_authorized l(:button_edit), { :controller => 'issue_categories', :action => 'edit', :id => category }, :class => 'icon icon-edit' %>
<%= link_to_if_authorized l(:button_delete), {:controller => 'issue_categories', :action => 'destroy', :id => category}, :method => :post, :class => 'icon icon-del' %>
<%= link_to_if_authorized l(:button_delete), {:controller => 'issue_categories', :action => 'destroy', :id => category}, :method => :post, :class => 'icon icon-del', :confirm => l(:text_are_you_sure) %>
</td>
</tr>
<% end %>

View File

@ -22,16 +22,33 @@ function toggle_filter(field) {
if (check_box.checked) {
Element.show("operators_" + field);
Form.Element.enable("operators_" + field);
Form.Element.enable("values_" + field);
toggle_operator(field);
} else {
Element.hide("operators_" + field);
Element.hide("div_values_" + field);
Form.Element.disable("operators_" + field);
Form.Element.disable("values_" + field);
enableValues(field, []);
}
}
function enableValues(field, indexes) {
var f = $$(".values_" + field);
for(var i=0;i<f.length;i++) {
if (indexes.include(i)) {
Form.Element.enable(f[i]);
f[i].up('span').show();
} else {
f[i].value = '';
Form.Element.disable(f[i]);
f[i].up('span').hide();
}
}
if (indexes.length > 0) {
Element.show("div_values_" + field);
} else {
Element.hide("div_values_" + field);
}
}
function toggle_operator(field) {
operator = $("operators_" + field);
switch (operator.value) {
@ -41,21 +58,32 @@ function toggle_operator(field) {
case "w":
case "o":
case "c":
Element.hide("div_values_" + field);
enableValues(field, []);
break;
case "><":
enableValues(field, [0,1]);
break;
case "<t+":
case ">t+":
case "t+":
case ">t-":
case "<t-":
case "t-":
enableValues(field, [2]);
break;
default:
Element.show("div_values_" + field);
enableValues(field, [0]);
break;
}
}
function toggle_multi_select(field) {
select = $('values_' + field);
if (select.multiple == true) {
select.multiple = false;
} else {
select.multiple = true;
}
function toggle_multi_select(el) {
var select = $(el);
if (select.multiple == true) {
select.multiple = false;
} else {
select.multiple = true;
}
}
function apply_filters_observer() {
@ -92,16 +120,19 @@ Event.observe(document,"dom:loaded", apply_filters_observer);
<div id="div_values_<%= field %>" style="display:none;">
<% case options[:type]
when :list, :list_optional, :list_status, :list_subprojects %>
<select <%= "multiple=true" if query.values_for(field) and query.values_for(field).length > 1 %> name="v[<%= field %>][]" id="values_<%= field %>" class="select-small" style="vertical-align: top;">
<%= options_for_select options[:values], query.values_for(field) %>
</select>
<%= link_to_function image_tag('bullet_toggle_plus.png'), "toggle_multi_select('#{field}');", :style => "vertical-align: bottom;" %>
<span class="span_values_<%= field %>">
<%= select_tag "v[#{field}][]", options_for_select(options[:values], query.values_for(field)), :class => "values_#{field}", :id => "values_#{field}_1", :multiple => (query.values_for(field) && query.values_for(field).length > 1) %>
<%= link_to_function image_tag('bullet_toggle_plus.png'), "toggle_multi_select('values_#{field}_1');", :style => "vertical-align: bottom;" %>
</span>
<% when :date, :date_past %>
<%= text_field_tag "v[#{field}][]", query.values_for(field).try(:first), :id => "values_#{field}", :size => 3, :class => "select-small" %> <%= l(:label_day_plural) %>
<span class="span_values_<%= field %>"><%= text_field_tag "v[#{field}][]", query.value_for(field), :size => 10, :class => "values_#{field}", :id => "values_#{field}_1" %> <%= calendar_for "values_#{field}_1" %></span>
<span class="span_values_<%= field %>"><%= text_field_tag "v[#{field}][]", query.value_for(field, 1), :size => 10, :class => "values_#{field}", :id => "values_#{field}_2" %> <%= calendar_for "values_#{field}_2" %></span>
<span class="span_values_<%= field %>"><%= text_field_tag "v[#{field}][]", query.value_for(field), :size => 3, :class => "values_#{field}" %> <%= l(:label_day_plural) %></span>
<% when :string, :text %>
<%= text_field_tag "v[#{field}][]", query.values_for(field).try(:first), :id => "values_#{field}", :size => 30, :class => "select-small" %>
<span class="span_values_<%= field %>"><%= text_field_tag "v[#{field}][]", query.value_for(field), :class => "values_#{field}", :id => "values_#{field}", :size => 30 %></span>
<% when :integer %>
<%= text_field_tag "v[#{field}][]", query.values_for(field).try(:first), :id => "values_#{field}", :size => 3, :class => "select-small" %>
<span class="span_values_<%= field %>"><%= text_field_tag "v[#{field}][]", query.value_for(field), :class => "values_#{field}", :id => "values_#{field}_1", :size => 3 %></span>
<span class="span_values_<%= field %>"><%= text_field_tag "v[#{field}][]", query.value_for(field, 1), :class => "values_#{field}", :id => "values_#{field}_2", :size => 3 %></span>
<% end %>
</div>
<script type="text/javascript">toggle_filter('<%= field %>');</script>

View File

@ -16,6 +16,18 @@
<%= check_box_tag 'query_is_for_all', 1, @query.project.nil?,
:disabled => (!@query.new_record? && (@query.project.nil? || (@query.is_public? && !User.current.admin?))) %></p>
<p><label><%= l(:label_project_plural) %></label>
<label style="text-align: left; float: none; margin-left: 0px;font-weight: normal">
<%= radio_button_tag('display_subprojects', '0', !@query.display_subprojects?) %>
<%= l(:text_current_project) %>
</label>
<br />
<label style="text-align: left; float: none; margin-left: 0px;font-weight: normal">
<%= radio_button_tag('display_subprojects', '1', @query.display_subprojects?) %>
<%= l(:label_and_its_subprojects, :value => l(:text_current_project)) %>
</label>
</p>
<p><label for="query_default_columns"><%=l(:label_default_columns)%></label>
<%= check_box_tag 'default_columns', 1, @query.has_default_columns?, :id => 'query_default_columns',
:onclick => 'if (this.checked) {Element.hide("columns")} else {Element.show("columns")}' %></p>

View File

@ -0,0 +1,5 @@
<% if @project %>
<div class="new-issue button-large">
<%= link_to_if_authorized(l(:label_issue_new), {:controller => 'issues', :action => 'new', :project_id => @project}, :title => l(:label_issue_new), :class => '') %>
</div>
<% end %>

View File

@ -0,0 +1,5 @@
<%= label_tag("q", l(:label_search), :class => "hidden-for-sighted") %>
<% form_tag({:controller => 'search', :action => 'index', :id => @project}, :method => :get) do %>
<%= hidden_field_tag(controller.default_search_scope, 1, :id => nil) if controller.default_search_scope %>
<%= text_field_tag 'q', search_term, :size => 20, :class => 'search_field', :placeholder => l(:search_input_placeholder), :accesskey => accesskey(:quick_search) %>
<% end %>

View File

@ -5,6 +5,9 @@
<%= setting_text_area :mail_handler_body_delimiters, :rows => 5 %>
<br /><em><%= l(:text_line_separated) %></em>
</p>
<p><%= setting_check_box :mail_handler_confirmation_on_success %></p>
<p><%= setting_check_box :mail_handler_confirmation_on_failure %></p>
</div>
<div class="box tabular settings">

View File

@ -4,8 +4,9 @@
:onchange => 'if (this.value == "selected") {Element.show("notified-projects")} else {Element.hide("notified-projects")}' %>
</p>
<% content_tag 'div', :id => 'notified-projects', :style => (@user.mail_notification == 'selected' ? '' : 'display:none;') do %>
<p><% @user.projects.each do |project| %>
<label><%= check_box_tag 'notified_project_ids[]', project.id, @user.notified_projects_ids.include?(project.id) %> <%=h project.name %></label><br />
<p><% project_tree(@user.projects.each) do |project,level| %>
<% name_prefix = (level > 0 ? ('&nbsp;' * 2 * level + '&#187; ') : '') %>
<label><%= check_box_tag 'notified_project_ids[]', project.id, @user.notified_projects_ids.include?(project.id) %> <%= name_prefix + h(project.name) %></label><br />
<% end %></p>
<p><em><%= l(:text_user_mail_option) %></em></p>
<% end %>

View File

@ -47,16 +47,5 @@
</div>
<div class="splitcontentright">
<% if projects.any? %>
<fieldset><legend><%=l(:label_project_new)%></legend>
<% remote_form_for(:membership, :url => { :action => 'edit_membership', :id => @user }) do %>
<%= select_tag 'membership[project_id]', options_for_membership_project_select(@user, projects) %>
<p><%= l(:label_role_plural) %>:
<% roles.each do |role| %>
<label><%= check_box_tag 'membership[role_ids][]', role.id %> <%=h role %></label>
<% end %></p>
<p><%= submit_tag l(:button_add) %></p>
<% end %>
</fieldset>
<% end %>
<%= render :partial => 'members/membership_assignment', :locals => {:principal => @user, :projects => projects - @user.projects, :roles => roles } %>
</div>

View File

@ -47,6 +47,7 @@
<% @versions.each do |version| %>
<%= link_to format_version_name(version), "##{version.name}" %><br />
<% end %>
<% end %>
<% html_title(l(:label_roadmap)) %>

View File

@ -1,24 +1,31 @@
<div class="contextual">
<%= link_to_remote l(:button_add),
:url => {:controller => 'watchers',
:action => 'new',
:object_type => watched.class.name.underscore,
:object_id => watched} if User.current.allowed_to?(:add_issue_watchers, @project) %>
<%= link_to_function(l(:button_add), "$('new-watcher-form').toggle();") if User.current.allowed_to?("add_#{watched.class.name.underscore}_watchers".to_sym, @project) %>
</div>
<h3><%= l(:label_issue_watchers) %> (<%= watched.watcher_users.size %>)</h3>
<% unless @watcher.nil? %>
<% if User.current.allowed_to?("add_#{watched.class.name.underscore}_watchers".to_sym, @project) %>
<% remote_form_for(:watcher, @watcher,
:url => {:controller => 'watchers',
:action => 'new',
:object_type => watched.class.name.underscore,
:object_id => watched},
:method => :post,
:html => {:id => 'new-watcher-form'}) do |f| %>
<p><%= f.select :user_id, (watched.addable_watcher_users.collect {|m| [m.name, m.id]}), :prompt => "--- #{l(:actionview_instancetag_blank_option)} ---" %>
:html => {:id => 'new-watcher-form', :style => 'display:none;'}) do |f| %>
<% users = Principal.active.find(:all, :limit => 25) - watched.watcher_users %>
<p><%= label_tag "user_search", l(:label_user_search) %><%= text_field_tag 'user_search', nil, :style => "width:98%;" %></p>
<%= observe_field(:user_search,
:frequency => 0.5,
:update => :users,
:url => auto_complete_users_path(:remove_watchers => watched.id, :klass => watched.class, :include_groups => true),
:with => 'q')
%>
<%= submit_tag l(:button_add) %>
<div id="users">
<%= principals_check_box_tags 'user_ids[]', users %>
</div>
<p><%= submit_tag l(:button_add) %>
<%= toggle_link l(:button_cancel), 'new-watcher-form'%></p>
<% end %>
<% end %>

View File

@ -59,6 +59,14 @@
<% content_for :sidebar do %>
<%= render :partial => 'wiki/sidebar' %>
<% if User.current.allowed_to?(:add_wiki_page_watchers, @project) ||
(@page.watchers.present? && User.current.allowed_to?(:view_wiki_page_watchers, @project)) %>
<div id="watchers">
<%= render :partial => 'watchers/watchers', :locals => {:watched => @page} %>
</div>
<% end %>
<% end %>
<% html_title h(@page.pretty_title) %>

View File

@ -52,6 +52,9 @@ Rails::Initializer.run do |config|
# (by default production uses :info, the others :debug)
# config.log_level = :debug
# Liquid drops
config.autoload_paths += %W( #{RAILS_ROOT}/app/drops )
# Enable page/fragment caching by setting a file-based store
# (remember to create the caching directory and make it readable to the application)
# config.action_controller.cache_store = :file_store, "#{RAILS_ROOT}/tmp/cache"

View File

@ -984,3 +984,16 @@ bg:
description_date_from: Въведете начална дата
label_deleted_custom_field: (изтрито потребителско поле)
field_custom_filter: Custom LDAP filter
text_display_subprojects: Display subprojects
text_current_project: Current project
label_toc: Contents
search_input_placeholder: search ...
setting_mail_handler_confirmation_on_success: Send confirmation email on successful incoming email
label_mail_handler_confirmation: "Confirmation of email submission: %{subject}"
label_mail_handler_errors_with_submission: "There were errors with your email submission:"
label_document_watchers: Watchers
setting_mail_handler_confirmation_on_failure: Send confirmation email on failed incoming email
label_between: between
label_mail_handler_failure: "Failed email submission: %{subject}"
notice_not_authorized_action: You are not authorized to perform this action.
text_mail_handler_confirmation_successful: Your email has been successful added at the following url

View File

@ -998,3 +998,16 @@ bs:
description_date_from: Enter start date
label_deleted_custom_field: (deleted custom field)
field_custom_filter: Custom LDAP filter
text_display_subprojects: Display subprojects
text_current_project: Current project
label_toc: Contents
search_input_placeholder: search ...
setting_mail_handler_confirmation_on_success: Send confirmation email on successful incoming email
label_mail_handler_confirmation: "Confirmation of email submission: %{subject}"
label_mail_handler_errors_with_submission: "There were errors with your email submission:"
label_document_watchers: Watchers
setting_mail_handler_confirmation_on_failure: Send confirmation email on failed incoming email
label_between: between
label_mail_handler_failure: "Failed email submission: %{subject}"
notice_not_authorized_action: You are not authorized to perform this action.
text_mail_handler_confirmation_successful: Your email has been successful added at the following url

View File

@ -987,3 +987,16 @@ ca:
description_date_from: Enter start date
label_deleted_custom_field: (deleted custom field)
field_custom_filter: Custom LDAP filter
text_display_subprojects: Display subprojects
text_current_project: Current project
label_toc: Contents
search_input_placeholder: search ...
setting_mail_handler_confirmation_on_success: Send confirmation email on successful incoming email
label_mail_handler_confirmation: "Confirmation of email submission: %{subject}"
label_mail_handler_errors_with_submission: "There were errors with your email submission:"
label_document_watchers: Watchers
setting_mail_handler_confirmation_on_failure: Send confirmation email on failed incoming email
label_between: between
label_mail_handler_failure: "Failed email submission: %{subject}"
notice_not_authorized_action: You are not authorized to perform this action.
text_mail_handler_confirmation_successful: Your email has been successful added at the following url

View File

@ -1208,3 +1208,16 @@ cs:
description_date_from: Enter start date
label_deleted_custom_field: (deleted custom field)
field_custom_filter: Custom LDAP filter
text_display_subprojects: Display subprojects
text_current_project: Current project
label_toc: Contents
search_input_placeholder: search ...
setting_mail_handler_confirmation_on_success: Send confirmation email on successful incoming email
label_mail_handler_confirmation: "Confirmation of email submission: %{subject}"
label_mail_handler_errors_with_submission: "There were errors with your email submission:"
label_document_watchers: Watchers
setting_mail_handler_confirmation_on_failure: Send confirmation email on failed incoming email
label_between: between
label_mail_handler_failure: "Failed email submission: %{subject}"
notice_not_authorized_action: You are not authorized to perform this action.
text_mail_handler_confirmation_successful: Your email has been successful added at the following url

View File

@ -1000,3 +1000,16 @@ da:
description_date_from: Enter start date
label_deleted_custom_field: (deleted custom field)
field_custom_filter: Custom LDAP filter
text_display_subprojects: Display subprojects
text_current_project: Current project
label_toc: Contents
search_input_placeholder: search ...
setting_mail_handler_confirmation_on_success: Send confirmation email on successful incoming email
label_mail_handler_confirmation: "Confirmation of email submission: %{subject}"
label_mail_handler_errors_with_submission: "There were errors with your email submission:"
label_document_watchers: Watchers
setting_mail_handler_confirmation_on_failure: Send confirmation email on failed incoming email
label_between: between
label_mail_handler_failure: "Failed email submission: %{subject}"
notice_not_authorized_action: You are not authorized to perform this action.
text_mail_handler_confirmation_successful: Your email has been successful added at the following url

View File

@ -658,6 +658,7 @@ de:
label_roadmap_overdue: "%{value} verspätet"
label_roadmap_no_issues: Keine Tickets für diese Version
label_search: Suche
search_input_placeholder: Suche ...
label_result_plural: Resultate
label_all_words: Alle Wörter
label_wiki: Wiki
@ -1001,3 +1002,15 @@ de:
description_date_from: Startdatum eintragen
description_date_to: Enddatum eintragen
field_custom_filter: Custom LDAP filter
label_toc: "Inhaltsverzeichnis"
text_display_subprojects: Display subprojects
text_current_project: Current project
setting_mail_handler_confirmation_on_success: Send confirmation email on successful incoming email
label_mail_handler_confirmation: "Confirmation of email submission: %{subject}"
label_mail_handler_errors_with_submission: "There were errors with your email submission:"
label_document_watchers: Watchers
setting_mail_handler_confirmation_on_failure: Send confirmation email on failed incoming email
label_between: between
label_mail_handler_failure: "Failed email submission: %{subject}"
notice_not_authorized_action: You are not authorized to perform this action.
text_mail_handler_confirmation_successful: Your email has been successful added at the following url

View File

@ -984,3 +984,16 @@ el:
description_date_from: Enter start date
label_deleted_custom_field: (deleted custom field)
field_custom_filter: Custom LDAP filter
text_display_subprojects: Display subprojects
text_current_project: Current project
label_toc: Contents
search_input_placeholder: search ...
setting_mail_handler_confirmation_on_success: Send confirmation email on successful incoming email
label_mail_handler_confirmation: "Confirmation of email submission: %{subject}"
label_mail_handler_errors_with_submission: "There were errors with your email submission:"
label_document_watchers: Watchers
setting_mail_handler_confirmation_on_failure: Send confirmation email on failed incoming email
label_between: between
label_mail_handler_failure: "Failed email submission: %{subject}"
notice_not_authorized_action: You are not authorized to perform this action.
text_mail_handler_confirmation_successful: Your email has been successful added at the following url

View File

@ -988,3 +988,16 @@ en-GB:
description_date_from: Enter start date
label_deleted_custom_field: (deleted custom field)
field_custom_filter: Custom LDAP filter
text_display_subprojects: Display subprojects
text_current_project: Current project
label_toc: Contents
search_input_placeholder: search ...
setting_mail_handler_confirmation_on_success: Send confirmation email on successful incoming email
label_mail_handler_confirmation: "Confirmation of email submission: %{subject}"
label_mail_handler_errors_with_submission: "There were errors with your email submission:"
label_document_watchers: Watchers
setting_mail_handler_confirmation_on_failure: Send confirmation email on failed incoming email
label_between: between
label_mail_handler_failure: "Failed email submission: %{subject}"
notice_not_authorized_action: You are not authorized to perform this action.
text_mail_handler_confirmation_successful: Your email has been successful added at the following url

View File

@ -157,6 +157,7 @@ en:
notice_file_not_found: The page you were trying to access doesn't exist or has been removed.
notice_locking_conflict: Data has been updated by another user.
notice_not_authorized: You are not authorized to access this page.
notice_not_authorized_action: You are not authorized to perform this action.
notice_not_authorized_archived_project: The project you're trying to access has been archived.
notice_email_sent: "An email was sent to %{value}"
notice_email_error: "An error occurred while sending mail (%{value})"
@ -368,6 +369,8 @@ en:
setting_commit_logtime_activity_id: Activity for logged time
setting_gantt_items_limit: Maximum number of items displayed on the gantt chart
setting_issue_startdate_is_adddate: Use current date as start date for new issues
setting_mail_handler_confirmation_on_success: "Send confirmation email on successful incoming email"
setting_mail_handler_confirmation_on_failure: "Send confirmation email on failed incoming email"
permission_add_project: Create project
permission_add_subprojects: Create subprojects
@ -610,6 +613,7 @@ en:
label_in_more_than: in more than
label_greater_or_equal: '>='
label_less_or_equal: '<='
label_between: "between"
label_in: in
label_today: today
label_all_time: all time
@ -657,6 +661,7 @@ en:
label_roadmap_overdue: "%{value} late"
label_roadmap_no_issues: No issues for this version
label_search: Search
search_input_placeholder: search ...
label_result_plural: Results
label_all_words: All words
label_wiki: Wiki
@ -772,6 +777,7 @@ en:
label_incoming_emails: Incoming emails
label_generate_key: Generate a key
label_issue_watchers: Watchers
label_document_watchers: Watchers
label_example: Example
label_display: Display
label_sort: Sort
@ -814,6 +820,10 @@ en:
label_notify_member_plural: Email issue updates
label_path_encoding: Path encoding
label_deleted_custom_field: '(deleted custom field)'
label_toc: "Contents"
label_mail_handler_confirmation: "Confirmation of email submission: %{subject}"
label_mail_handler_failure: "Failed email submission: %{subject}"
label_mail_handler_errors_with_submission: "There were errors with your email submission:"
button_login: Login
button_submit: Submit
@ -937,6 +947,9 @@ en:
text_default_encoding: "Default: UTF-8"
text_mercurial_repo_example: "local repository (e.g. /hgrepo, c:\\hgrepo)"
text_git_repo_example: "a bare and local repository (e.g. /gitrepo, c:\\gitrepo)"
text_display_subprojects: Display subprojects
text_current_project: Current project
text_mail_handler_confirmation_successful: "Your email has been successful added at the following url"
default_role_manager: Manager
default_role_developer: Developer

View File

@ -1021,3 +1021,16 @@ es:
description_date_from: Enter start date
label_deleted_custom_field: (deleted custom field)
field_custom_filter: Custom LDAP filter
text_display_subprojects: Display subprojects
text_current_project: Current project
label_toc: Contents
search_input_placeholder: search ...
setting_mail_handler_confirmation_on_success: Send confirmation email on successful incoming email
label_mail_handler_confirmation: "Confirmation of email submission: %{subject}"
label_mail_handler_errors_with_submission: "There were errors with your email submission:"
label_document_watchers: Watchers
setting_mail_handler_confirmation_on_failure: Send confirmation email on failed incoming email
label_between: between
label_mail_handler_failure: "Failed email submission: %{subject}"
notice_not_authorized_action: You are not authorized to perform this action.
text_mail_handler_confirmation_successful: Your email has been successful added at the following url

Some files were not shown because too many files have changed in this diff Show More