diff --git a/rest_sys/Rakefile b/rest_sys/Rakefile new file mode 100644 index 000000000..cffd19f0c --- /dev/null +++ b/rest_sys/Rakefile @@ -0,0 +1,10 @@ +# Add your own tasks in files placed in lib/tasks ending in .rake, +# for example lib/tasks/switchtower.rake, and they will automatically be available to Rake. + +require(File.join(File.dirname(__FILE__), 'config', 'boot')) + +require 'rake' +require 'rake/testtask' +require 'rake/rdoctask' + +require 'tasks/rails' \ No newline at end of file diff --git a/rest_sys/app/apis/sys_api.rb b/rest_sys/app/apis/sys_api.rb new file mode 100644 index 000000000..f52f9e7ef --- /dev/null +++ b/rest_sys/app/apis/sys_api.rb @@ -0,0 +1,25 @@ +# redMine - project management software +# Copyright (C) 2006-2007 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class SysApi < ActionWebService::API::Base + api_method :projects, + :expects => [], + :returns => [[Project]] + api_method :repository_created, + :expects => [:string, :string], + :returns => [:int] +end diff --git a/rest_sys/app/controllers/account_controller.rb b/rest_sys/app/controllers/account_controller.rb new file mode 100644 index 000000000..a1cbf5ffd --- /dev/null +++ b/rest_sys/app/controllers/account_controller.rb @@ -0,0 +1,175 @@ +# redMine - project management software +# Copyright (C) 2006-2007 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class AccountController < ApplicationController + layout 'base' + helper :custom_fields + include CustomFieldsHelper + + # prevents login action to be filtered by check_if_login_required application scope filter + skip_before_filter :check_if_login_required, :only => [:login, :lost_password, :register, :activate] + + # Show user's account + def show + @user = User.find_active(params[:id]) + @custom_values = @user.custom_values.find(:all, :include => :custom_field) + + # show only public projects and private projects that the logged in user is also a member of + @memberships = @user.memberships.select do |membership| + membership.project.is_public? || (User.current.role_for_project(membership.project)) + end + rescue ActiveRecord::RecordNotFound + render_404 + end + + # Login request and validation + def login + if request.get? + # Logout user + self.logged_user = nil + else + # Authenticate user + user = User.try_to_login(params[:login], params[:password]) + if user + self.logged_user = user + # generate a key and set cookie if autologin + if params[:autologin] && Setting.autologin? + token = Token.create(:user => user, :action => 'autologin') + cookies[:autologin] = { :value => token.value, :expires => 1.year.from_now } + end + redirect_back_or_default :controller => 'my', :action => 'page' + else + flash.now[:error] = l(:notice_account_invalid_creditentials) + end + end + end + + # Log out current user and redirect to welcome page + def logout + cookies.delete :autologin + Token.delete_all(["user_id = ? AND action = ?", User.current.id, 'autologin']) if User.current.logged? + self.logged_user = nil + redirect_to home_url + end + + # Enable user to choose a new password + def lost_password + redirect_to(home_url) && return unless Setting.lost_password? + if params[:token] + @token = Token.find_by_action_and_value("recovery", params[:token]) + redirect_to(home_url) && return unless @token and !@token.expired? + @user = @token.user + if request.post? + @user.password, @user.password_confirmation = params[:new_password], params[:new_password_confirmation] + if @user.save + @token.destroy + flash[:notice] = l(:notice_account_password_updated) + redirect_to :action => 'login' + return + end + end + render :template => "account/password_recovery" + return + else + if request.post? + user = User.find_by_mail(params[:mail]) + # user not found in db + flash.now[:error] = l(:notice_account_unknown_email) and return unless user + # user uses an external authentification + flash.now[:error] = l(:notice_can_t_change_password) and return if user.auth_source_id + # create a new token for password recovery + token = Token.new(:user => user, :action => "recovery") + if token.save + Mailer.deliver_lost_password(token) + flash[:notice] = l(:notice_account_lost_email_sent) + redirect_to :action => 'login' + return + end + end + end + end + + # User self-registration + def register + redirect_to(home_url) && return unless Setting.self_registration? + if request.get? + @user = User.new(:language => Setting.default_language) + @custom_values = UserCustomField.find(:all).collect { |x| CustomValue.new(:custom_field => x, :customized => @user) } + else + @user = User.new(params[:user]) + @user.admin = false + @user.login = params[:user][:login] + @user.status = User::STATUS_REGISTERED + @user.password, @user.password_confirmation = params[:password], params[:password_confirmation] + @custom_values = UserCustomField.find(:all).collect { |x| CustomValue.new(:custom_field => x, + :customized => @user, + :value => (params["custom_fields"] ? params["custom_fields"][x.id.to_s] : nil)) } + @user.custom_values = @custom_values + case Setting.self_registration + when '1' + # Email activation + token = Token.new(:user => @user, :action => "register") + if @user.save and token.save + Mailer.deliver_register(token) + flash[:notice] = l(:notice_account_register_done) + redirect_to :action => 'login' + end + when '3' + # Automatic activation + @user.status = User::STATUS_ACTIVE + if @user.save + flash[:notice] = l(:notice_account_activated) + redirect_to :action => 'login' + end + else + # Manual activation by the administrator + if @user.save + # Sends an email to the administrators + Mailer.deliver_account_activation_request(@user) + flash[:notice] = l(:notice_account_pending) + redirect_to :action => 'login' + end + end + end + end + + # Token based account activation + def activate + redirect_to(home_url) && return unless Setting.self_registration? && params[:token] + token = Token.find_by_action_and_value('register', params[:token]) + redirect_to(home_url) && return unless token and !token.expired? + user = token.user + redirect_to(home_url) && return unless user.status == User::STATUS_REGISTERED + user.status = User::STATUS_ACTIVE + if user.save + token.destroy + flash[:notice] = l(:notice_account_activated) + end + redirect_to :action => 'login' + end + +private + def logged_user=(user) + if user && user.is_a?(User) + User.current = user + session[:user_id] = user.id + else + User.current = User.anonymous + session[:user_id] = nil + end + end +end diff --git a/rest_sys/app/controllers/admin_controller.rb b/rest_sys/app/controllers/admin_controller.rb new file mode 100644 index 000000000..5ad3d696b --- /dev/null +++ b/rest_sys/app/controllers/admin_controller.rb @@ -0,0 +1,82 @@ +# redMine - project management software +# Copyright (C) 2006 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class AdminController < ApplicationController + layout 'base' + before_filter :require_admin + + helper :sort + include SortHelper + + def index + end + + def projects + sort_init 'name', 'asc' + sort_update + + @status = params[:status] ? params[:status].to_i : 0 + conditions = nil + conditions = ["status=?", @status] unless @status == 0 + + @project_count = Project.count(:conditions => conditions) + @project_pages = Paginator.new self, @project_count, + 25, + params['page'] + @projects = Project.find :all, :order => sort_clause, + :conditions => conditions, + :limit => @project_pages.items_per_page, + :offset => @project_pages.current.offset + + render :action => "projects", :layout => false if request.xhr? + end + + def mail_options + @notifiables = %w(issue_added issue_updated news_added document_added file_added message_posted) + if request.post? + settings = (params[:settings] || {}).dup.symbolize_keys + settings[:notified_events] ||= [] + settings.each { |name, value| Setting[name] = value } + flash[:notice] = l(:notice_successful_update) + redirect_to :controller => 'admin', :action => 'mail_options' + end + end + + def test_email + raise_delivery_errors = ActionMailer::Base.raise_delivery_errors + # Force ActionMailer to raise delivery errors so we can catch it + ActionMailer::Base.raise_delivery_errors = true + begin + @test = Mailer.deliver_test(User.current) + flash[:notice] = l(:notice_email_sent, User.current.mail) + rescue Exception => e + flash[:error] = l(:notice_email_error, e.message) + end + ActionMailer::Base.raise_delivery_errors = raise_delivery_errors + redirect_to :action => 'mail_options' + end + + def info + @db_adapter_name = ActiveRecord::Base.connection.adapter_name + @flags = { + :default_admin_changed => User.find(:first, :conditions => ["login=? and hashed_password=?", 'admin', User.hash_password('admin')]).nil?, + :file_repository_writable => File.writable?(Attachment.storage_path), + :rmagick_available => Object.const_defined?(:Magick) + } + @plugins = Redmine::Plugin.registered_plugins + end +end diff --git a/rest_sys/app/controllers/application.rb b/rest_sys/app/controllers/application.rb new file mode 100644 index 000000000..ad86b6b33 --- /dev/null +++ b/rest_sys/app/controllers/application.rb @@ -0,0 +1,179 @@ +# redMine - project management software +# Copyright (C) 2006-2007 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class ApplicationController < ActionController::Base + before_filter :user_setup, :check_if_login_required, :set_localization + filter_parameter_logging :password + + REDMINE_SUPPORTED_SCM.each do |scm| + require_dependency "repository/#{scm.underscore}" + end + + def current_role + @current_role ||= User.current.role_for_project(@project) + end + + def user_setup + Setting.check_cache + if session[:user_id] + # existing session + User.current = User.find(session[:user_id]) + elsif cookies[:autologin] && Setting.autologin? + # auto-login feature + User.current = User.find_by_autologin_key(cookies[:autologin]) + elsif params[:key] && accept_key_auth_actions.include?(params[:action]) + # RSS key authentication + User.current = User.find_by_rss_key(params[:key]) + else + User.current = User.anonymous + end + end + + # check if login is globally required to access the application + def check_if_login_required + # no check needed if user is already logged in + return true if User.current.logged? + require_login if Setting.login_required? + end + + def set_localization + lang = begin + if !User.current.language.blank? and GLoc.valid_languages.include? User.current.language.to_sym + User.current.language + elsif request.env['HTTP_ACCEPT_LANGUAGE'] + accept_lang = parse_qvalues(request.env['HTTP_ACCEPT_LANGUAGE']).first.split('-').first + if accept_lang and !accept_lang.empty? and GLoc.valid_languages.include? accept_lang.to_sym + accept_lang + end + end + rescue + nil + end || Setting.default_language + set_language_if_valid(lang) + end + + def require_login + if !User.current.logged? + store_location + redirect_to :controller => "account", :action => "login" + return false + end + true + end + + def require_admin + return unless require_login + if !User.current.admin? + render_403 + return false + end + true + end + + # Authorize the user for the requested action + def authorize(ctrl = params[:controller], action = params[:action]) + allowed = User.current.allowed_to?({:controller => ctrl, :action => action}, @project) + allowed ? true : (User.current.logged? ? render_403 : require_login) + end + + # make sure that the user is a member of the project (or admin) if project is private + # used as a before_filter for actions that do not require any particular permission on the project + def check_project_privacy + unless @project.active? + @project = nil + render_404 + return false + end + return true if @project.is_public? || User.current.member_of?(@project) || User.current.admin? + User.current.logged? ? render_403 : require_login + end + + # store current uri in session. + # return to this location by calling redirect_back_or_default + def store_location + session[:return_to_params] = params + end + + # move to the last store_location call or to the passed default one + def redirect_back_or_default(default) + if session[:return_to_params].nil? + redirect_to default + else + redirect_to session[:return_to_params] + session[:return_to_params] = nil + end + end + + def render_403 + @project = nil + render :template => "common/403", :layout => !request.xhr?, :status => 403 + return false + end + + def render_404 + render :template => "common/404", :layout => !request.xhr?, :status => 404 + return false + end + + def render_feed(items, options={}) + @items = items || [] + @items.sort! {|x,y| y.event_datetime <=> x.event_datetime } + @title = options[:title] || Setting.app_title + render :template => "common/feed.atom.rxml", :layout => false, :content_type => 'application/atom+xml' + end + + def self.accept_key_auth(*actions) + actions = actions.flatten.map(&:to_s) + write_inheritable_attribute('accept_key_auth_actions', actions) + end + + def accept_key_auth_actions + self.class.read_inheritable_attribute('accept_key_auth_actions') || [] + end + + # TODO: move to model + def attach_files(obj, files) + attachments = [] + if files && files.is_a?(Array) + files.each do |file| + next unless file.size > 0 + a = Attachment.create(:container => obj, :file => file, :author => User.current) + attachments << a unless a.new_record? + end + end + attachments + end + + # qvalues http header parser + # code taken from webrick + def parse_qvalues(value) + tmp = [] + if value + parts = value.split(/,\s*/) + parts.each {|part| + if m = %r{^([^\s,]+?)(?:;\s*q=(\d+(?:\.\d+)?))?$}.match(part) + val = m[1] + q = (m[2] or 1).to_f + tmp.push([val, q]) + end + } + tmp = tmp.sort_by{|val, q| -q} + tmp.collect!{|val, q| val} + end + return tmp + end +end diff --git a/rest_sys/app/controllers/attachments_controller.rb b/rest_sys/app/controllers/attachments_controller.rb new file mode 100644 index 000000000..0913de529 --- /dev/null +++ b/rest_sys/app/controllers/attachments_controller.rb @@ -0,0 +1,39 @@ +# redMine - project management software +# Copyright (C) 2006-2007 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class AttachmentsController < ApplicationController + layout 'base' + before_filter :find_project, :check_project_privacy + + def download + # images are sent inline + send_file @attachment.diskfile, :filename => @attachment.filename, + :type => @attachment.content_type, + :disposition => (@attachment.image? ? 'inline' : 'attachment') + rescue + # in case the disk file was deleted + render_404 + end + +private + def find_project + @attachment = Attachment.find(params[:id]) + @project = @attachment.project + rescue + render_404 + end +end diff --git a/rest_sys/app/controllers/auth_sources_controller.rb b/rest_sys/app/controllers/auth_sources_controller.rb new file mode 100644 index 000000000..b830f1970 --- /dev/null +++ b/rest_sys/app/controllers/auth_sources_controller.rb @@ -0,0 +1,83 @@ +# redMine - project management software +# Copyright (C) 2006 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class AuthSourcesController < ApplicationController + layout 'base' + before_filter :require_admin + + def index + list + render :action => 'list' unless request.xhr? + end + + # GETs should be safe (see http://www.w3.org/2001/tag/doc/whenToUseGet.html) + verify :method => :post, :only => [ :destroy, :create, :update ], + :redirect_to => { :action => :list } + + def list + @auth_source_pages, @auth_sources = paginate :auth_sources, :per_page => 10 + render :action => "list", :layout => false if request.xhr? + end + + def new + @auth_source = AuthSourceLdap.new + end + + def create + @auth_source = AuthSourceLdap.new(params[:auth_source]) + if @auth_source.save + flash[:notice] = l(:notice_successful_create) + redirect_to :action => 'list' + else + render :action => 'new' + end + end + + def edit + @auth_source = AuthSource.find(params[:id]) + end + + def update + @auth_source = AuthSource.find(params[:id]) + if @auth_source.update_attributes(params[:auth_source]) + flash[:notice] = l(:notice_successful_update) + redirect_to :action => 'list' + else + render :action => 'edit' + end + end + + def test_connection + @auth_method = AuthSource.find(params[:id]) + begin + @auth_method.test_connection + flash[:notice] = l(:notice_successful_connection) + rescue => text + flash[:error] = "Unable to connect (#{text})" + end + redirect_to :action => 'list' + end + + def destroy + @auth_source = AuthSource.find(params[:id]) + unless @auth_source.users.find(:first) + @auth_source.destroy + flash[:notice] = l(:notice_successful_delete) + end + redirect_to :action => 'list' + end +end diff --git a/rest_sys/app/controllers/boards_controller.rb b/rest_sys/app/controllers/boards_controller.rb new file mode 100644 index 000000000..200792370 --- /dev/null +++ b/rest_sys/app/controllers/boards_controller.rb @@ -0,0 +1,86 @@ +# redMine - project management software +# Copyright (C) 2006-2007 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class BoardsController < ApplicationController + layout 'base' + before_filter :find_project, :authorize + + helper :messages + include MessagesHelper + helper :sort + include SortHelper + helper :watchers + include WatchersHelper + + def index + @boards = @project.boards + # show the board if there is only one + if @boards.size == 1 + @board = @boards.first + show + end + end + + def show + sort_init "#{Message.table_name}.updated_on", "desc" + sort_update + + @topic_count = @board.topics.count + @topic_pages = Paginator.new self, @topic_count, 25, params['page'] + @topics = @board.topics.find :all, :order => "#{Message.table_name}.sticky DESC, #{sort_clause}", + :include => [:author, {:last_reply => :author}], + :limit => @topic_pages.items_per_page, + :offset => @topic_pages.current.offset + render :action => 'show', :layout => !request.xhr? + end + + verify :method => :post, :only => [ :destroy ], :redirect_to => { :action => :index } + + def new + @board = Board.new(params[:board]) + @board.project = @project + if request.post? && @board.save + flash[:notice] = l(:notice_successful_create) + redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'boards' + end + end + + def edit + if request.post? && @board.update_attributes(params[:board]) + case params[:position] + when 'highest'; @board.move_to_top + when 'higher'; @board.move_higher + when 'lower'; @board.move_lower + when 'lowest'; @board.move_to_bottom + end if params[:position] + redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'boards' + end + end + + def destroy + @board.destroy + redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'boards' + end + +private + def find_project + @project = Project.find(params[:project_id]) + @board = @project.boards.find(params[:id]) if params[:id] + rescue ActiveRecord::RecordNotFound + render_404 + end +end diff --git a/rest_sys/app/controllers/custom_fields_controller.rb b/rest_sys/app/controllers/custom_fields_controller.rb new file mode 100644 index 000000000..1e1c988d9 --- /dev/null +++ b/rest_sys/app/controllers/custom_fields_controller.rb @@ -0,0 +1,87 @@ +# redMine - project management software +# Copyright (C) 2006 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class CustomFieldsController < ApplicationController + layout 'base' + before_filter :require_admin + + def index + list + render :action => 'list' unless request.xhr? + end + + def list + @custom_fields_by_type = CustomField.find(:all).group_by {|f| f.class.name } + @tab = params[:tab] || 'IssueCustomField' + render :action => "list", :layout => false if request.xhr? + end + + def new + case params[:type] + when "IssueCustomField" + @custom_field = IssueCustomField.new(params[:custom_field]) + @custom_field.trackers = Tracker.find(params[:tracker_ids]) if params[:tracker_ids] + when "UserCustomField" + @custom_field = UserCustomField.new(params[:custom_field]) + when "ProjectCustomField" + @custom_field = ProjectCustomField.new(params[:custom_field]) + else + redirect_to :action => 'list' + return + end + if request.post? and @custom_field.save + flash[:notice] = l(:notice_successful_create) + redirect_to :action => 'list', :tab => @custom_field.class.name + end + @trackers = Tracker.find(:all, :order => 'position') + end + + def edit + @custom_field = CustomField.find(params[:id]) + if request.post? and @custom_field.update_attributes(params[:custom_field]) + if @custom_field.is_a? IssueCustomField + @custom_field.trackers = params[:tracker_ids] ? Tracker.find(params[:tracker_ids]) : [] + end + flash[:notice] = l(:notice_successful_update) + redirect_to :action => 'list', :tab => @custom_field.class.name + end + @trackers = Tracker.find(:all, :order => 'position') + end + + def move + @custom_field = CustomField.find(params[:id]) + case params[:position] + when 'highest' + @custom_field.move_to_top + when 'higher' + @custom_field.move_higher + when 'lower' + @custom_field.move_lower + when 'lowest' + @custom_field.move_to_bottom + end if params[:position] + redirect_to :action => 'list', :tab => @custom_field.class.name + end + + def destroy + @custom_field = CustomField.find(params[:id]).destroy + redirect_to :action => 'list', :tab => @custom_field.class.name + rescue + flash[:error] = "Unable to delete custom field" + redirect_to :action => 'list' + end +end diff --git a/rest_sys/app/controllers/documents_controller.rb b/rest_sys/app/controllers/documents_controller.rb new file mode 100644 index 000000000..104cca10c --- /dev/null +++ b/rest_sys/app/controllers/documents_controller.rb @@ -0,0 +1,65 @@ +# redMine - project management software +# Copyright (C) 2006-2007 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class DocumentsController < ApplicationController + layout 'base' + before_filter :find_project, :authorize + + def show + @attachments = @document.attachments.find(:all, :order => "created_on DESC") + end + + def edit + @categories = Enumeration::get_values('DCAT') + if request.post? and @document.update_attributes(params[:document]) + flash[:notice] = l(:notice_successful_update) + redirect_to :action => 'show', :id => @document + end + end + + def destroy + @document.destroy + redirect_to :controller => 'projects', :action => 'list_documents', :id => @project + end + + def download + @attachment = @document.attachments.find(params[:attachment_id]) + @attachment.increment_download + send_file @attachment.diskfile, :filename => @attachment.filename, :type => @attachment.content_type + rescue + render_404 + end + + def add_attachment + attachments = attach_files(@document, params[:attachments]) + Mailer.deliver_attachments_added(attachments) if !attachments.empty? && Setting.notified_events.include?('document_added') + redirect_to :action => 'show', :id => @document + end + + def destroy_attachment + @document.attachments.find(params[:attachment_id]).destroy + redirect_to :action => 'show', :id => @document + end + +private + def find_project + @document = Document.find(params[:id]) + @project = @document.project + rescue ActiveRecord::RecordNotFound + render_404 + end +end diff --git a/rest_sys/app/controllers/enumerations_controller.rb b/rest_sys/app/controllers/enumerations_controller.rb new file mode 100644 index 000000000..7a7f1685a --- /dev/null +++ b/rest_sys/app/controllers/enumerations_controller.rb @@ -0,0 +1,85 @@ +# redMine - project management software +# Copyright (C) 2006 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class EnumerationsController < ApplicationController + layout 'base' + before_filter :require_admin + + def index + list + render :action => 'list' + end + + # GETs should be safe (see http://www.w3.org/2001/tag/doc/whenToUseGet.html) + verify :method => :post, :only => [ :destroy, :create, :update ], + :redirect_to => { :action => :list } + + def list + end + + def new + @enumeration = Enumeration.new(:opt => params[:opt]) + end + + def create + @enumeration = Enumeration.new(params[:enumeration]) + if @enumeration.save + flash[:notice] = l(:notice_successful_create) + redirect_to :action => 'list', :opt => @enumeration.opt + else + render :action => 'new' + end + end + + def edit + @enumeration = Enumeration.find(params[:id]) + end + + def update + @enumeration = Enumeration.find(params[:id]) + if @enumeration.update_attributes(params[:enumeration]) + flash[:notice] = l(:notice_successful_update) + redirect_to :action => 'list', :opt => @enumeration.opt + else + render :action => 'edit' + end + end + + def move + @enumeration = Enumeration.find(params[:id]) + case params[:position] + when 'highest' + @enumeration.move_to_top + when 'higher' + @enumeration.move_higher + when 'lower' + @enumeration.move_lower + when 'lowest' + @enumeration.move_to_bottom + end if params[:position] + redirect_to :action => 'index' + end + + def destroy + Enumeration.find(params[:id]).destroy + flash[:notice] = l(:notice_successful_delete) + redirect_to :action => 'list' + rescue + flash[:error] = "Unable to delete enumeration" + redirect_to :action => 'list' + end +end diff --git a/rest_sys/app/controllers/issue_categories_controller.rb b/rest_sys/app/controllers/issue_categories_controller.rb new file mode 100644 index 000000000..2c1c6657b --- /dev/null +++ b/rest_sys/app/controllers/issue_categories_controller.rb @@ -0,0 +1,52 @@ +# redMine - project management software +# Copyright (C) 2006 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class IssueCategoriesController < ApplicationController + layout 'base' + before_filter :find_project, :authorize + + verify :method => :post, :only => :destroy + + def edit + if request.post? and @category.update_attributes(params[:category]) + flash[:notice] = l(:notice_successful_update) + redirect_to :controller => 'projects', :action => 'settings', :tab => 'categories', :id => @project + end + end + + def destroy + @issue_count = @category.issues.size + if @issue_count == 0 + # No issue assigned to this category + @category.destroy + redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'categories' + elsif params[:todo] + reassign_to = @project.issue_categories.find_by_id(params[:reassign_to_id]) if params[:todo] == 'reassign' + @category.destroy(reassign_to) + redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'categories' + end + @categories = @project.issue_categories - [@category] + end + +private + def find_project + @category = IssueCategory.find(params[:id]) + @project = @category.project + rescue ActiveRecord::RecordNotFound + render_404 + end +end diff --git a/rest_sys/app/controllers/issue_relations_controller.rb b/rest_sys/app/controllers/issue_relations_controller.rb new file mode 100644 index 000000000..cb0ad552a --- /dev/null +++ b/rest_sys/app/controllers/issue_relations_controller.rb @@ -0,0 +1,59 @@ +# redMine - project management software +# Copyright (C) 2006-2007 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class IssueRelationsController < ApplicationController + layout 'base' + before_filter :find_project, :authorize + + def new + @relation = IssueRelation.new(params[:relation]) + @relation.issue_from = @issue + @relation.save if request.post? + respond_to do |format| + format.html { redirect_to :controller => 'issues', :action => 'show', :id => @issue } + format.js do + render :update do |page| + page.replace_html "relations", :partial => 'issues/relations' + if @relation.errors.empty? + page << "$('relation_delay').value = ''" + page << "$('relation_issue_to_id').value = ''" + end + end + end + end + end + + def destroy + relation = IssueRelation.find(params[:id]) + if request.post? && @issue.relations.include?(relation) + relation.destroy + @issue.reload + end + respond_to do |format| + format.html { redirect_to :controller => 'issues', :action => 'show', :id => @issue } + format.js { render(:update) {|page| page.replace_html "relations", :partial => 'issues/relations'} } + end + end + +private + def find_project + @issue = Issue.find(params[:issue_id]) + @project = @issue.project + rescue ActiveRecord::RecordNotFound + render_404 + end +end diff --git a/rest_sys/app/controllers/issue_statuses_controller.rb b/rest_sys/app/controllers/issue_statuses_controller.rb new file mode 100644 index 000000000..d0712e7c3 --- /dev/null +++ b/rest_sys/app/controllers/issue_statuses_controller.rb @@ -0,0 +1,85 @@ +# redMine - project management software +# Copyright (C) 2006 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class IssueStatusesController < ApplicationController + layout 'base' + before_filter :require_admin + + verify :method => :post, :only => [ :destroy, :create, :update, :move ], + :redirect_to => { :action => :list } + + def index + list + render :action => 'list' unless request.xhr? + end + + def list + @issue_status_pages, @issue_statuses = paginate :issue_statuses, :per_page => 25, :order => "position" + render :action => "list", :layout => false if request.xhr? + end + + def new + @issue_status = IssueStatus.new + end + + def create + @issue_status = IssueStatus.new(params[:issue_status]) + if @issue_status.save + flash[:notice] = l(:notice_successful_create) + redirect_to :action => 'list' + else + render :action => 'new' + end + end + + def edit + @issue_status = IssueStatus.find(params[:id]) + end + + def update + @issue_status = IssueStatus.find(params[:id]) + if @issue_status.update_attributes(params[:issue_status]) + flash[:notice] = l(:notice_successful_update) + redirect_to :action => 'list' + else + render :action => 'edit' + end + end + + def move + @issue_status = IssueStatus.find(params[:id]) + case params[:position] + when 'highest' + @issue_status.move_to_top + when 'higher' + @issue_status.move_higher + when 'lower' + @issue_status.move_lower + when 'lowest' + @issue_status.move_to_bottom + end if params[:position] + redirect_to :action => 'list' + end + + def destroy + IssueStatus.find(params[:id]).destroy + redirect_to :action => 'list' + rescue + flash[:error] = "Unable to delete issue status" + redirect_to :action => 'list' + end +end diff --git a/rest_sys/app/controllers/issues_controller.rb b/rest_sys/app/controllers/issues_controller.rb new file mode 100644 index 000000000..78bcf76a7 --- /dev/null +++ b/rest_sys/app/controllers/issues_controller.rb @@ -0,0 +1,239 @@ +# redMine - project management software +# Copyright (C) 2006-2007 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class IssuesController < ApplicationController + layout 'base' + before_filter :find_project, :authorize, :except => [:index, :changes, :preview] + before_filter :find_optional_project, :only => [:index, :changes] + accept_key_auth :index, :changes + + cache_sweeper :issue_sweeper, :only => [ :edit, :change_status, :destroy ] + + helper :projects + include ProjectsHelper + helper :custom_fields + include CustomFieldsHelper + helper :ifpdf + include IfpdfHelper + helper :issue_relations + include IssueRelationsHelper + helper :watchers + include WatchersHelper + helper :attachments + include AttachmentsHelper + helper :queries + helper :sort + include SortHelper + include IssuesHelper + + def index + sort_init "#{Issue.table_name}.id", "desc" + sort_update + retrieve_query + if @query.valid? + limit = %w(pdf csv).include?(params[:format]) ? Setting.issues_export_limit.to_i : 25 + @issue_count = Issue.count(:include => [:status, :project], :conditions => @query.statement) + @issue_pages = Paginator.new self, @issue_count, limit, params['page'] + @issues = Issue.find :all, :order => sort_clause, + :include => [ :assigned_to, :status, :tracker, :project, :priority, :category, :fixed_version ], + :conditions => @query.statement, + :limit => limit, + :offset => @issue_pages.current.offset + respond_to do |format| + format.html { render :template => 'issues/index.rhtml', :layout => !request.xhr? } + format.atom { render_feed(@issues, :title => l(:label_issue_plural)) } + format.csv { send_data(issues_to_csv(@issues, @project).read, :type => 'text/csv; header=present', :filename => 'export.csv') } + format.pdf { send_data(render(:template => 'issues/index.rfpdf', :layout => false), :type => 'application/pdf', :filename => 'export.pdf') } + end + else + # Send html if the query is not valid + render(:template => 'issues/index.rhtml', :layout => !request.xhr?) + end + end + + def changes + sort_init "#{Issue.table_name}.id", "desc" + sort_update + retrieve_query + if @query.valid? + @changes = Journal.find :all, :include => [ :details, :user, {:issue => [:project, :author, :tracker, :status]} ], + :conditions => @query.statement, + :limit => 25, + :order => "#{Journal.table_name}.created_on DESC" + end + @title = (@project ? @project.name : Setting.app_title) + ": " + (@query.new_record? ? l(:label_changes_details) : @query.name) + render :layout => false, :content_type => 'application/atom+xml' + end + + def show + @custom_values = @issue.custom_values.find(:all, :include => :custom_field, :order => "#{CustomField.table_name}.position") + @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC") + @status_options = @issue.status.find_new_statuses_allowed_to(User.current.role_for_project(@project), @issue.tracker) + respond_to do |format| + format.html { render :template => 'issues/show.rhtml' } + format.pdf { send_data(render(:template => 'issues/show.rfpdf', :layout => false), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") } + end + end + + def edit + @priorities = Enumeration::get_values('IPRI') + @custom_values = [] + if request.get? + @custom_values = @project.custom_fields_for_issues(@issue.tracker).collect { |x| @issue.custom_values.find_by_custom_field_id(x.id) || CustomValue.new(:custom_field => x, :customized => @issue) } + else + begin + @issue.init_journal(User.current) + # Retrieve custom fields and values + if params["custom_fields"] + @custom_values = @project.custom_fields_for_issues(@issue.tracker).collect { |x| CustomValue.new(:custom_field => x, :customized => @issue, :value => params["custom_fields"][x.id.to_s]) } + @issue.custom_values = @custom_values + end + @issue.attributes = params[:issue] + if @issue.save + flash[:notice] = l(:notice_successful_update) + redirect_to(params[:back_to] || {:action => 'show', :id => @issue}) + end + rescue ActiveRecord::StaleObjectError + # Optimistic locking exception + flash[:error] = l(:notice_locking_conflict) + end + end + end + + def add_note + journal = @issue.init_journal(User.current, params[:notes]) + attachments = attach_files(@issue, params[:attachments]) + attachments.each {|a| journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)} + if journal.save + flash[:notice] = l(:notice_successful_update) + Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated') + redirect_to :action => 'show', :id => @issue + return + end + show + end + + def change_status + @status_options = @issue.status.find_new_statuses_allowed_to(User.current.role_for_project(@project), @issue.tracker) + @new_status = IssueStatus.find(params[:new_status_id]) + if params[:confirm] + begin + journal = @issue.init_journal(User.current, params[:notes]) + @issue.status = @new_status + if @issue.update_attributes(params[:issue]) + attachments = attach_files(@issue, params[:attachments]) + attachments.each {|a| journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)} + # Log time + if current_role.allowed_to?(:log_time) + @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today) + @time_entry.attributes = params[:time_entry] + @time_entry.save + end + + flash[:notice] = l(:notice_successful_update) + Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated') + redirect_to :action => 'show', :id => @issue + end + rescue ActiveRecord::StaleObjectError + # Optimistic locking exception + flash[:error] = l(:notice_locking_conflict) + end + end + @assignable_to = @project.members.find(:all, :include => :user).collect{ |m| m.user } + @activities = Enumeration::get_values('ACTI') + end + + def destroy + @issue.destroy + redirect_to :action => 'index', :project_id => @project + end + + def destroy_attachment + a = @issue.attachments.find(params[:attachment_id]) + a.destroy + journal = @issue.init_journal(User.current) + journal.details << JournalDetail.new(:property => 'attachment', + :prop_key => a.id, + :old_value => a.filename) + journal.save + redirect_to :action => 'show', :id => @issue + end + + def context_menu + @priorities = Enumeration.get_values('IPRI').reverse + @statuses = IssueStatus.find(:all, :order => 'position') + @allowed_statuses = @issue.status.find_new_statuses_allowed_to(User.current.role_for_project(@project), @issue.tracker) + @assignables = @issue.assignable_users + @assignables << @issue.assigned_to if @issue.assigned_to && !@assignables.include?(@issue.assigned_to) + @can = {:edit => User.current.allowed_to?(:edit_issues, @project), + :change_status => User.current.allowed_to?(:change_issue_status, @project), + :add => User.current.allowed_to?(:add_issues, @project), + :move => User.current.allowed_to?(:move_issues, @project), + :copy => (@project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)), + :delete => User.current.allowed_to?(:delete_issues, @project)} + render :layout => false + end + + def preview + issue = Issue.find_by_id(params[:id]) + @attachements = issue.attachments if issue + @text = params[:issue][:description] + render :partial => 'common/preview' + end + +private + def find_project + @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category]) + @project = @issue.project + rescue ActiveRecord::RecordNotFound + render_404 + end + + def find_optional_project + return true unless params[:project_id] + @project = Project.find(params[:project_id]) + authorize + rescue ActiveRecord::RecordNotFound + render_404 + end + + # Retrieve query from session or build a new query + def retrieve_query + if params[:query_id] + @query = Query.find(params[:query_id], :conditions => {:project_id => (@project ? @project.id : nil)}) + session[:query] = @query + else + if params[:set_filter] or !session[:query] or session[:query].project != @project + # Give it a name, required to be valid + @query = Query.new(:name => "_") + @query.project = @project + if params[:fields] and params[:fields].is_a? Array + params[:fields].each do |field| + @query.add_filter(field, params[:operators][field], params[:values][field]) + end + else + @query.available_filters.keys.each do |field| + @query.add_short_filter(field, params[field]) if params[field] + end + end + session[:query] = @query + else + @query = session[:query] + end + end + end +end diff --git a/rest_sys/app/controllers/members_controller.rb b/rest_sys/app/controllers/members_controller.rb new file mode 100644 index 000000000..a1706e601 --- /dev/null +++ b/rest_sys/app/controllers/members_controller.rb @@ -0,0 +1,62 @@ +# redMine - project management software +# Copyright (C) 2006 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class MembersController < ApplicationController + layout 'base' + before_filter :find_member, :except => :new + before_filter :find_project, :only => :new + before_filter :authorize + + def new + @project.members << Member.new(params[:member]) if request.post? + respond_to do |format| + format.html { redirect_to :action => 'settings', :tab => 'members', :id => @project } + format.js { render(:update) {|page| page.replace_html "tab-content-members", :partial => 'projects/settings/members'} } + end + end + + def edit + if request.post? and @member.update_attributes(params[:member]) + respond_to do |format| + format.html { redirect_to :controller => 'projects', :action => 'settings', :tab => 'members', :id => @project } + format.js { render(:update) {|page| page.replace_html "tab-content-members", :partial => 'projects/settings/members'} } + end + end + end + + def destroy + @member.destroy + respond_to do |format| + format.html { redirect_to :controller => 'projects', :action => 'settings', :tab => 'members', :id => @project } + format.js { render(:update) {|page| page.replace_html "tab-content-members", :partial => 'projects/settings/members'} } + end + end + +private + def find_project + @project = Project.find(params[:id]) + rescue ActiveRecord::RecordNotFound + render_404 + end + + def find_member + @member = Member.find(params[:id]) + @project = @member.project + rescue ActiveRecord::RecordNotFound + render_404 + end +end diff --git a/rest_sys/app/controllers/messages_controller.rb b/rest_sys/app/controllers/messages_controller.rb new file mode 100644 index 000000000..8078abf71 --- /dev/null +++ b/rest_sys/app/controllers/messages_controller.rb @@ -0,0 +1,98 @@ +# redMine - project management software +# Copyright (C) 2006-2007 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class MessagesController < ApplicationController + layout 'base' + before_filter :find_board, :only => :new + before_filter :find_message, :except => :new + before_filter :authorize + + verify :method => :post, :only => [ :reply, :destroy ], :redirect_to => { :action => :show } + + helper :attachments + include AttachmentsHelper + + # Show a topic and its replies + def show + @reply = Message.new(:subject => "RE: #{@message.subject}") + render :action => "show", :layout => false if request.xhr? + end + + # Create a new topic + def new + @message = Message.new(params[:message]) + @message.author = User.current + @message.board = @board + if params[:message] && User.current.allowed_to?(:edit_messages, @project) + @message.locked = params[:message]['locked'] + @message.sticky = params[:message]['sticky'] + end + if request.post? && @message.save + attach_files(@message, params[:attachments]) + redirect_to :action => 'show', :id => @message + end + end + + # Reply to a topic + def reply + @reply = Message.new(params[:reply]) + @reply.author = User.current + @reply.board = @board + @topic.children << @reply + if !@reply.new_record? + attach_files(@reply, params[:attachments]) + end + redirect_to :action => 'show', :id => @topic + end + + # Edit a message + def edit + if params[:message] && User.current.allowed_to?(:edit_messages, @project) + @message.locked = params[:message]['locked'] + @message.sticky = params[:message]['sticky'] + end + if request.post? && @message.update_attributes(params[:message]) + attach_files(@message, params[:attachments]) + flash[:notice] = l(:notice_successful_update) + redirect_to :action => 'show', :id => @topic + end + end + + # Delete a messages + def destroy + @message.destroy + redirect_to @message.parent.nil? ? + { :controller => 'boards', :action => 'show', :project_id => @project, :id => @board } : + { :action => 'show', :id => @message.parent } + end + +private + def find_message + find_board + @message = @board.messages.find(params[:id], :include => :parent) + @topic = @message.root + rescue ActiveRecord::RecordNotFound + render_404 + end + + def find_board + @board = Board.find(params[:board_id], :include => :project) + @project = @board.project + rescue ActiveRecord::RecordNotFound + render_404 + end +end diff --git a/rest_sys/app/controllers/my_controller.rb b/rest_sys/app/controllers/my_controller.rb new file mode 100644 index 000000000..cb326bc93 --- /dev/null +++ b/rest_sys/app/controllers/my_controller.rb @@ -0,0 +1,160 @@ +# redMine - project management software +# Copyright (C) 2006 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class MyController < ApplicationController + helper :issues + + layout 'base' + before_filter :require_login + + BLOCKS = { 'issuesassignedtome' => :label_assigned_to_me_issues, + 'issuesreportedbyme' => :label_reported_issues, + 'issueswatched' => :label_watched_issues, + 'news' => :label_news_latest, + 'calendar' => :label_calendar, + 'documents' => :label_document_plural + }.freeze + + DEFAULT_LAYOUT = { 'left' => ['issuesassignedtome'], + 'right' => ['issuesreportedbyme'] + }.freeze + + verify :xhr => true, + :session => :page_layout, + :only => [:add_block, :remove_block, :order_blocks] + + def index + page + render :action => 'page' + end + + # Show user's page + def page + @user = User.current + @blocks = @user.pref[:my_page_layout] || DEFAULT_LAYOUT + end + + # Edit user's account + def account + @user = User.current + @pref = @user.pref + if request.post? + @user.attributes = params[:user] + @user.mail_notification = (params[:notification_option] == 'all') + @user.pref.attributes = params[:pref] + @user.pref[:no_self_notified] = (params[:no_self_notified] == '1') + if @user.save + @user.pref.save + @user.notified_project_ids = (params[:notification_option] == 'selected' ? params[:notified_project_ids] : []) + set_language_if_valid @user.language + flash[:notice] = l(:notice_account_updated) + redirect_to :action => 'account' + return + end + end + @notification_options = [[l(:label_user_mail_option_all), 'all'], + [l(:label_user_mail_option_none), 'none']] + # Only users that belong to more than 1 project can select projects for which they are notified + # Note that @user.membership.size would fail since AR ignores :include association option when doing a count + @notification_options.insert 1, [l(:label_user_mail_option_selected), 'selected'] if @user.memberships.length > 1 + @notification_option = @user.mail_notification? ? 'all' : (@user.notified_projects_ids.empty? ? 'none' : 'selected') + end + + # Manage user's password + def password + @user = User.current + flash[:error] = l(:notice_can_t_change_password) and redirect_to :action => 'account' and return if @user.auth_source_id + if request.post? + if @user.check_password?(params[:password]) + @user.password, @user.password_confirmation = params[:new_password], params[:new_password_confirmation] + if @user.save + flash[:notice] = l(:notice_account_password_updated) + redirect_to :action => 'account' + end + else + flash[:error] = l(:notice_account_wrong_password) + end + end + end + + # Create a new feeds key + def reset_rss_key + if request.post? && User.current.rss_token + User.current.rss_token.destroy + flash[:notice] = l(:notice_feeds_access_key_reseted) + end + redirect_to :action => 'account' + end + + # User's page layout configuration + def page_layout + @user = User.current + @blocks = @user.pref[:my_page_layout] || DEFAULT_LAYOUT.dup + session[:page_layout] = @blocks + %w(top left right).each {|f| session[:page_layout][f] ||= [] } + @block_options = [] + BLOCKS.each {|k, v| @block_options << [l(v), k]} + end + + # Add a block to user's page + # The block is added on top of the page + # params[:block] : id of the block to add + def add_block + block = params[:block] + render(:nothing => true) and return unless block && (BLOCKS.keys.include? block) + @user = User.current + # remove if already present in a group + %w(top left right).each {|f| (session[:page_layout][f] ||= []).delete block } + # add it on top + session[:page_layout]['top'].unshift block + render :partial => "block", :locals => {:user => @user, :block_name => block} + end + + # Remove a block to user's page + # params[:block] : id of the block to remove + def remove_block + block = params[:block] + # remove block in all groups + %w(top left right).each {|f| (session[:page_layout][f] ||= []).delete block } + render :nothing => true + end + + # Change blocks order on user's page + # params[:group] : group to order (top, left or right) + # params[:list-(top|left|right)] : array of block ids of the group + def order_blocks + group = params[:group] + group_items = params["list-#{group}"] + if group_items and group_items.is_a? Array + # remove group blocks if they are presents in other groups + %w(top left right).each {|f| + session[:page_layout][f] = (session[:page_layout][f] || []) - group_items + } + session[:page_layout][group] = group_items + end + render :nothing => true + end + + # Save user's page layout + def page_layout_save + @user = User.current + @user.pref[:my_page_layout] = session[:page_layout] if session[:page_layout] + @user.pref.save + session[:page_layout] = nil + redirect_to :action => 'page' + end +end diff --git a/rest_sys/app/controllers/news_controller.rb b/rest_sys/app/controllers/news_controller.rb new file mode 100644 index 000000000..109afe454 --- /dev/null +++ b/rest_sys/app/controllers/news_controller.rb @@ -0,0 +1,82 @@ +# redMine - project management software +# Copyright (C) 2006 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class NewsController < ApplicationController + layout 'base' + before_filter :find_project, :authorize, :except => :index + before_filter :find_optional_project, :only => :index + accept_key_auth :index + + def index + @news_pages, @newss = paginate :news, + :per_page => 10, + :conditions => (@project ? {:project_id => @project.id} : Project.visible_by(User.current)), + :include => [:author, :project], + :order => "#{News.table_name}.created_on DESC" + respond_to do |format| + format.html { render :layout => false if request.xhr? } + format.atom { render_feed(@newss, :title => (@project ? @project.name : Setting.app_title) + ": #{l(:label_news_plural)}") } + end + end + + def show + end + + def edit + if request.post? and @news.update_attributes(params[:news]) + flash[:notice] = l(:notice_successful_update) + redirect_to :action => 'show', :id => @news + end + end + + def add_comment + @comment = Comment.new(params[:comment]) + @comment.author = User.current + if @news.comments << @comment + flash[:notice] = l(:label_comment_added) + redirect_to :action => 'show', :id => @news + else + render :action => 'show' + end + end + + def destroy_comment + @news.comments.find(params[:comment_id]).destroy + redirect_to :action => 'show', :id => @news + end + + def destroy + @news.destroy + redirect_to :action => 'index', :project_id => @project + end + +private + def find_project + @news = News.find(params[:id]) + @project = @news.project + rescue ActiveRecord::RecordNotFound + render_404 + end + + def find_optional_project + return true unless params[:project_id] + @project = Project.find(params[:project_id]) + authorize + rescue ActiveRecord::RecordNotFound + render_404 + end +end diff --git a/rest_sys/app/controllers/projects_controller.rb b/rest_sys/app/controllers/projects_controller.rb new file mode 100644 index 000000000..7b1e4ef3d --- /dev/null +++ b/rest_sys/app/controllers/projects_controller.rb @@ -0,0 +1,548 @@ +# redMine - project management software +# Copyright (C) 2006-2007 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class ProjectsController < ApplicationController + layout 'base' + before_filter :find_project, :except => [ :index, :list, :add ] + before_filter :authorize, :except => [ :index, :list, :add, :archive, :unarchive, :destroy ] + before_filter :require_admin, :only => [ :add, :archive, :unarchive, :destroy ] + accept_key_auth :activity, :calendar + + cache_sweeper :project_sweeper, :only => [ :add, :edit, :archive, :unarchive, :destroy ] + cache_sweeper :issue_sweeper, :only => [ :add_issue ] + cache_sweeper :version_sweeper, :only => [ :add_version ] + + helper :sort + include SortHelper + helper :custom_fields + include CustomFieldsHelper + helper :ifpdf + include IfpdfHelper + helper :issues + helper IssuesHelper + helper :queries + include QueriesHelper + helper :repositories + include RepositoriesHelper + include ProjectsHelper + + def index + list + render :action => 'list' unless request.xhr? + end + + # Lists visible projects + def list + projects = Project.find :all, + :conditions => Project.visible_by(User.current), + :include => :parent + @project_tree = projects.group_by {|p| p.parent || p} + @project_tree.each_key {|p| @project_tree[p] -= [p]} + end + + # Add a new project + def add + @custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position") + @trackers = Tracker.all + @root_projects = Project.find(:all, + :conditions => "parent_id IS NULL AND status = #{Project::STATUS_ACTIVE}", + :order => 'name') + @project = Project.new(params[:project]) + @project.enabled_module_names = Redmine::AccessControl.available_project_modules + if request.get? + @custom_values = ProjectCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| CustomValue.new(:custom_field => x, :customized => @project) } + @project.trackers = Tracker.all + else + @project.custom_fields = CustomField.find(params[:custom_field_ids]) if params[:custom_field_ids] + @custom_values = ProjectCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| CustomValue.new(:custom_field => x, :customized => @project, :value => (params[:custom_fields] ? params["custom_fields"][x.id.to_s] : nil)) } + @project.custom_values = @custom_values + if @project.save + @project.enabled_module_names = params[:enabled_modules] + flash[:notice] = l(:notice_successful_create) + redirect_to :controller => 'admin', :action => 'projects' + end + end + end + + # Show @project + def show + @custom_values = @project.custom_values.find(:all, :include => :custom_field, :order => "#{CustomField.table_name}.position") + @members_by_role = @project.members.find(:all, :include => [:user, :role], :order => 'position').group_by {|m| m.role} + @subprojects = @project.active_children + @news = @project.news.find(:all, :limit => 5, :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC") + @trackers = @project.trackers + @open_issues_by_tracker = Issue.count(:group => :tracker, :joins => "INNER JOIN #{IssueStatus.table_name} ON #{IssueStatus.table_name}.id = #{Issue.table_name}.status_id", :conditions => ["project_id=? and #{IssueStatus.table_name}.is_closed=?", @project.id, false]) + @total_issues_by_tracker = Issue.count(:group => :tracker, :conditions => ["project_id=?", @project.id]) + @total_hours = @project.time_entries.sum(:hours) + @key = User.current.rss_key + end + + def settings + @root_projects = Project.find(:all, + :conditions => ["parent_id IS NULL AND status = #{Project::STATUS_ACTIVE} AND id <> ?", @project.id], + :order => 'name') + @custom_fields = IssueCustomField.find(:all) + @issue_category ||= IssueCategory.new + @member ||= @project.members.new + @trackers = Tracker.all + @custom_values ||= ProjectCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| @project.custom_values.find_by_custom_field_id(x.id) || CustomValue.new(:custom_field => x) } + @repository ||= @project.repository + @wiki ||= @project.wiki + end + + # Edit @project + def edit + if request.post? + @project.custom_fields = IssueCustomField.find(params[:custom_field_ids]) if params[:custom_field_ids] + if params[:custom_fields] + @custom_values = ProjectCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| CustomValue.new(:custom_field => x, :customized => @project, :value => params["custom_fields"][x.id.to_s]) } + @project.custom_values = @custom_values + end + @project.attributes = params[:project] + if @project.save + flash[:notice] = l(:notice_successful_update) + redirect_to :action => 'settings', :id => @project + else + settings + render :action => 'settings' + end + end + end + + def modules + @project.enabled_module_names = params[:enabled_modules] + redirect_to :action => 'settings', :id => @project, :tab => 'modules' + end + + def archive + @project.archive if request.post? && @project.active? + redirect_to :controller => 'admin', :action => 'projects' + end + + def unarchive + @project.unarchive if request.post? && !@project.active? + redirect_to :controller => 'admin', :action => 'projects' + end + + # Delete @project + def destroy + @project_to_destroy = @project + if request.post? and params[:confirm] + @project_to_destroy.destroy + redirect_to :controller => 'admin', :action => 'projects' + end + # hide project in layout + @project = nil + end + + # Add a new issue category to @project + def add_issue_category + @category = @project.issue_categories.build(params[:category]) + if request.post? and @category.save + respond_to do |format| + format.html do + flash[:notice] = l(:notice_successful_create) + redirect_to :action => 'settings', :tab => 'categories', :id => @project + end + format.js do + # IE doesn't support the replace_html rjs method for select box options + render(:update) {|page| page.replace "issue_category_id", + content_tag('select', '' + options_from_collection_for_select(@project.issue_categories, 'id', 'name', @category.id), :id => 'issue_category_id', :name => 'issue[category_id]') + } + end + end + end + end + + # Add a new version to @project + def add_version + @version = @project.versions.build(params[:version]) + if request.post? and @version.save + flash[:notice] = l(:notice_successful_create) + redirect_to :action => 'settings', :tab => 'versions', :id => @project + end + end + + # Add a new document to @project + def add_document + @document = @project.documents.build(params[:document]) + if request.post? and @document.save + attach_files(@document, params[:attachments]) + flash[:notice] = l(:notice_successful_create) + Mailer.deliver_document_added(@document) if Setting.notified_events.include?('document_added') + redirect_to :action => 'list_documents', :id => @project + end + end + + # Show documents list of @project + def list_documents + @sort_by = %w(category date title author).include?(params[:sort_by]) ? params[:sort_by] : 'category' + documents = @project.documents.find :all, :include => [:attachments, :category] + case @sort_by + when 'date' + @grouped = documents.group_by {|d| d.created_on.to_date } + when 'title' + @grouped = documents.group_by {|d| d.title.first.upcase} + when 'author' + @grouped = documents.select{|d| d.attachments.any?}.group_by {|d| d.attachments.last.author} + else + @grouped = documents.group_by(&:category) + end + render :layout => false if request.xhr? + end + + # Add a new issue to @project + # The new issue will be created from an existing one if copy_from parameter is given + def add_issue + @issue = params[:copy_from] ? Issue.new.copy_from(params[:copy_from]) : Issue.new(params[:issue]) + @issue.project = @project + @issue.author = User.current + @issue.tracker ||= @project.trackers.find(params[:tracker_id]) + + default_status = IssueStatus.default + unless default_status + flash.now[:error] = 'No default issue status is defined. Please check your configuration (Go to "Administration -> Issue statuses").' + render :nothing => true, :layout => true + return + end + @issue.status = default_status + @allowed_statuses = ([default_status] + default_status.find_new_statuses_allowed_to(User.current.role_for_project(@project), @issue.tracker)) + + if request.get? + @issue.start_date ||= Date.today + @custom_values = @issue.custom_values.empty? ? + @project.custom_fields_for_issues(@issue.tracker).collect { |x| CustomValue.new(:custom_field => x, :customized => @issue) } : + @issue.custom_values + else + requested_status = IssueStatus.find_by_id(params[:issue][:status_id]) + # Check that the user is allowed to apply the requested status + @issue.status = (@allowed_statuses.include? requested_status) ? requested_status : default_status + @custom_values = @project.custom_fields_for_issues(@issue.tracker).collect { |x| CustomValue.new(:custom_field => x, :customized => @issue, :value => params["custom_fields"][x.id.to_s]) } + @issue.custom_values = @custom_values + if @issue.save + attach_files(@issue, params[:attachments]) + flash[:notice] = l(:notice_successful_create) + Mailer.deliver_issue_add(@issue) if Setting.notified_events.include?('issue_added') + redirect_to :controller => 'issues', :action => 'index', :project_id => @project + return + end + end + @priorities = Enumeration::get_values('IPRI') + end + + # Bulk edit issues + def bulk_edit_issues + if request.post? + status = params[:status_id].blank? ? nil : IssueStatus.find_by_id(params[:status_id]) + priority = params[:priority_id].blank? ? nil : Enumeration.find_by_id(params[:priority_id]) + assigned_to = params[:assigned_to_id].blank? ? nil : User.find_by_id(params[:assigned_to_id]) + category = params[:category_id].blank? ? nil : @project.issue_categories.find_by_id(params[:category_id]) + fixed_version = params[:fixed_version_id].blank? ? nil : @project.versions.find_by_id(params[:fixed_version_id]) + issues = @project.issues.find_all_by_id(params[:issue_ids]) + unsaved_issue_ids = [] + issues.each do |issue| + journal = issue.init_journal(User.current, params[:notes]) + issue.priority = priority if priority + issue.assigned_to = assigned_to if assigned_to || params[:assigned_to_id] == 'none' + issue.category = category if category + issue.fixed_version = fixed_version if fixed_version + issue.start_date = params[:start_date] unless params[:start_date].blank? + issue.due_date = params[:due_date] unless params[:due_date].blank? + issue.done_ratio = params[:done_ratio] unless params[:done_ratio].blank? + # Don't save any change to the issue if the user is not authorized to apply the requested status + if (status.nil? || (issue.status.new_status_allowed_to?(status, current_role, issue.tracker) && issue.status = status)) && issue.save + # Send notification for each issue (if changed) + Mailer.deliver_issue_edit(journal) if journal.details.any? && Setting.notified_events.include?('issue_updated') + else + # Keep unsaved issue ids to display them in flash error + unsaved_issue_ids << issue.id + end + end + if unsaved_issue_ids.empty? + flash[:notice] = l(:notice_successful_update) unless issues.empty? + else + flash[:error] = l(:notice_failed_to_save_issues, unsaved_issue_ids.size, issues.size, '#' + unsaved_issue_ids.join(', #')) + end + redirect_to :controller => 'issues', :action => 'index', :project_id => @project + return + end + if current_role && User.current.allowed_to?(:change_issue_status, @project) + # Find potential statuses the user could be allowed to switch issues to + @available_statuses = Workflow.find(:all, :include => :new_status, + :conditions => {:role_id => current_role.id}).collect(&:new_status).compact.uniq + end + render :update do |page| + page.hide 'query_form' + page.replace_html 'bulk-edit', :partial => 'issues/bulk_edit_form' + end + end + + def move_issues + @issues = @project.issues.find(params[:issue_ids]) if params[:issue_ids] + redirect_to :controller => 'issues', :action => 'index', :project_id => @project and return unless @issues + + @projects = [] + # find projects to which the user is allowed to move the issue + if User.current.admin? + # admin is allowed to move issues to any active (visible) project + @projects = Project.find(:all, :conditions => Project.visible_by(User.current), :order => 'name') + else + User.current.memberships.each {|m| @projects << m.project if m.role.allowed_to?(:move_issues)} + end + @target_project = @projects.detect {|p| p.id.to_s == params[:new_project_id]} if params[:new_project_id] + @target_project ||= @project + @trackers = @target_project.trackers + if request.post? + new_tracker = params[:new_tracker_id].blank? ? nil : @target_project.trackers.find_by_id(params[:new_tracker_id]) + unsaved_issue_ids = [] + @issues.each do |issue| + unsaved_issue_ids << issue.id unless issue.move_to(@target_project, new_tracker) + end + if unsaved_issue_ids.empty? + flash[:notice] = l(:notice_successful_update) unless @issues.empty? + else + flash[:error] = l(:notice_failed_to_save_issues, unsaved_issue_ids.size, @issues.size, '#' + unsaved_issue_ids.join(', #')) + end + redirect_to :controller => 'issues', :action => 'index', :project_id => @project + return + end + render :layout => false if request.xhr? + end + + # Add a news to @project + def add_news + @news = News.new(:project => @project, :author => User.current) + if request.post? + @news.attributes = params[:news] + if @news.save + flash[:notice] = l(:notice_successful_create) + Mailer.deliver_news_added(@news) if Setting.notified_events.include?('news_added') + redirect_to :controller => 'news', :action => 'index', :project_id => @project + end + end + end + + def add_file + if request.post? + @version = @project.versions.find_by_id(params[:version_id]) + attachments = attach_files(@issue, params[:attachments]) + Mailer.deliver_attachments_added(attachments) if !attachments.empty? && Setting.notified_events.include?('file_added') + redirect_to :controller => 'projects', :action => 'list_files', :id => @project + end + @versions = @project.versions.sort + end + + def list_files + @versions = @project.versions.sort + end + + # Show changelog for @project + def changelog + @trackers = @project.trackers.find(:all, :conditions => ["is_in_chlog=?", true], :order => 'position') + retrieve_selected_tracker_ids(@trackers) + @versions = @project.versions.sort + end + + def roadmap + @trackers = @project.trackers.find(:all, :conditions => ["is_in_roadmap=?", true]) + retrieve_selected_tracker_ids(@trackers) + @versions = @project.versions.sort + @versions = @versions.select {|v| !v.completed? } unless params[:completed] + end + + def activity + if params[:year] and params[:year].to_i > 1900 + @year = params[:year].to_i + if params[:month] and params[:month].to_i > 0 and params[:month].to_i < 13 + @month = params[:month].to_i + end + end + @year ||= Date.today.year + @month ||= Date.today.month + + case params[:format] + when 'atom' + # 30 last days + @date_from = Date.today - 30 + @date_to = Date.today + 1 + else + # current month + @date_from = Date.civil(@year, @month, 1) + @date_to = @date_from >> 1 + end + + @event_types = %w(issues news files documents changesets wiki_pages messages) + @event_types.delete('wiki_pages') unless @project.wiki + @event_types.delete('changesets') unless @project.repository + @event_types.delete('messages') unless @project.boards.any? + # only show what the user is allowed to view + @event_types = @event_types.select {|o| User.current.allowed_to?("view_#{o}".to_sym, @project)} + + @scope = @event_types.select {|t| params["show_#{t}"]} + # default events if none is specified in parameters + @scope = (@event_types - %w(wiki_pages messages))if @scope.empty? + + @events = [] + + if @scope.include?('issues') + @events += @project.issues.find(:all, :include => [:author, :tracker], :conditions => ["#{Issue.table_name}.created_on>=? and #{Issue.table_name}.created_on<=?", @date_from, @date_to] ) + @events += @project.issues_status_changes(@date_from, @date_to) + end + + if @scope.include?('news') + @events += @project.news.find(:all, :conditions => ["#{News.table_name}.created_on>=? and #{News.table_name}.created_on<=?", @date_from, @date_to], :include => :author ) + end + + if @scope.include?('files') + @events += Attachment.find(:all, :select => "#{Attachment.table_name}.*", :joins => "LEFT JOIN #{Version.table_name} ON #{Version.table_name}.id = #{Attachment.table_name}.container_id", :conditions => ["#{Attachment.table_name}.container_type='Version' and #{Version.table_name}.project_id=? and #{Attachment.table_name}.created_on>=? and #{Attachment.table_name}.created_on<=?", @project.id, @date_from, @date_to], :include => :author ) + end + + if @scope.include?('documents') + @events += @project.documents.find(:all, :conditions => ["#{Document.table_name}.created_on>=? and #{Document.table_name}.created_on<=?", @date_from, @date_to] ) + @events += Attachment.find(:all, :select => "attachments.*", :joins => "LEFT JOIN #{Document.table_name} ON #{Document.table_name}.id = #{Attachment.table_name}.container_id", :conditions => ["#{Attachment.table_name}.container_type='Document' and #{Document.table_name}.project_id=? and #{Attachment.table_name}.created_on>=? and #{Attachment.table_name}.created_on<=?", @project.id, @date_from, @date_to], :include => :author ) + end + + if @scope.include?('wiki_pages') + select = "#{WikiContent.versioned_table_name}.updated_on, #{WikiContent.versioned_table_name}.comments, " + + "#{WikiContent.versioned_table_name}.#{WikiContent.version_column}, #{WikiPage.table_name}.title, " + + "#{WikiContent.versioned_table_name}.page_id, #{WikiContent.versioned_table_name}.author_id, " + + "#{WikiContent.versioned_table_name}.id" + joins = "LEFT JOIN #{WikiPage.table_name} ON #{WikiPage.table_name}.id = #{WikiContent.versioned_table_name}.page_id " + + "LEFT JOIN #{Wiki.table_name} ON #{Wiki.table_name}.id = #{WikiPage.table_name}.wiki_id " + conditions = ["#{Wiki.table_name}.project_id = ? AND #{WikiContent.versioned_table_name}.updated_on BETWEEN ? AND ?", + @project.id, @date_from, @date_to] + + @events += WikiContent.versioned_class.find(:all, :select => select, :joins => joins, :conditions => conditions) + end + + if @scope.include?('changesets') + @events += Changeset.find(:all, :include => :repository, :conditions => ["#{Repository.table_name}.project_id = ? AND #{Changeset.table_name}.committed_on BETWEEN ? AND ?", @project.id, @date_from, @date_to]) + end + + if @scope.include?('messages') + @events += Message.find(:all, + :include => [:board, :author], + :conditions => ["#{Board.table_name}.project_id=? AND #{Message.table_name}.parent_id IS NULL AND #{Message.table_name}.created_on BETWEEN ? AND ?", @project.id, @date_from, @date_to]) + end + + @events_by_day = @events.group_by(&:event_date) + + respond_to do |format| + format.html { render :layout => false if request.xhr? } + format.atom { render_feed(@events, :title => "#{@project.name}: #{l(:label_activity)}") } + end + end + + def calendar + @trackers = Tracker.find(:all, :order => 'position') + retrieve_selected_tracker_ids(@trackers) + + if params[:year] and params[:year].to_i > 1900 + @year = params[:year].to_i + if params[:month] and params[:month].to_i > 0 and params[:month].to_i < 13 + @month = params[:month].to_i + end + end + @year ||= Date.today.year + @month ||= Date.today.month + @calendar = Redmine::Helpers::Calendar.new(Date.civil(@year, @month, 1), current_language, :month) + + events = [] + @project.issues_with_subprojects(params[:with_subprojects]) do + events += Issue.find(:all, + :include => [:tracker, :status, :assigned_to, :priority, :project], + :conditions => ["((start_date BETWEEN ? AND ?) OR (due_date BETWEEN ? AND ?)) AND #{Issue.table_name}.tracker_id IN (#{@selected_tracker_ids.join(',')})", @calendar.startdt, @calendar.enddt, @calendar.startdt, @calendar.enddt] + ) unless @selected_tracker_ids.empty? + end + events += @project.versions.find(:all, :conditions => ["effective_date BETWEEN ? AND ?", @calendar.startdt, @calendar.enddt]) + @calendar.events = events + + render :layout => false if request.xhr? + end + + def gantt + @trackers = Tracker.find(:all, :order => 'position') + retrieve_selected_tracker_ids(@trackers) + + if params[:year] and params[:year].to_i >0 + @year_from = params[:year].to_i + if params[:month] and params[:month].to_i >=1 and params[:month].to_i <= 12 + @month_from = params[:month].to_i + else + @month_from = 1 + end + else + @month_from ||= Date.today.month + @year_from ||= Date.today.year + end + + zoom = (params[:zoom] || User.current.pref[:gantt_zoom]).to_i + @zoom = (zoom > 0 && zoom < 5) ? zoom : 2 + months = (params[:months] || User.current.pref[:gantt_months]).to_i + @months = (months > 0 && months < 25) ? months : 6 + + # Save gantt paramters as user preference (zoom and months count) + if (User.current.logged? && (@zoom != User.current.pref[:gantt_zoom] || @months != User.current.pref[:gantt_months])) + User.current.pref[:gantt_zoom], User.current.pref[:gantt_months] = @zoom, @months + User.current.preference.save + end + + @date_from = Date.civil(@year_from, @month_from, 1) + @date_to = (@date_from >> @months) - 1 + + @events = [] + @project.issues_with_subprojects(params[:with_subprojects]) do + @events += Issue.find(:all, + :order => "start_date, due_date", + :include => [:tracker, :status, :assigned_to, :priority, :project], + :conditions => ["(((start_date>=? and start_date<=?) or (due_date>=? and due_date<=?) or (start_date?)) and start_date is not null and due_date is not null and #{Issue.table_name}.tracker_id in (#{@selected_tracker_ids.join(',')}))", @date_from, @date_to, @date_from, @date_to, @date_from, @date_to] + ) unless @selected_tracker_ids.empty? + end + @events += @project.versions.find(:all, :conditions => ["effective_date BETWEEN ? AND ?", @date_from, @date_to]) + @events.sort! {|x,y| x.start_date <=> y.start_date } + + if params[:format]=='pdf' + @options_for_rfpdf ||= {} + @options_for_rfpdf[:file_name] = "#{@project.identifier}-gantt.pdf" + render :template => "projects/gantt.rfpdf", :layout => false + elsif params[:format]=='png' && respond_to?('gantt_image') + image = gantt_image(@events, @date_from, @months, @zoom) + image.format = 'PNG' + send_data(image.to_blob, :disposition => 'inline', :type => 'image/png', :filename => "#{@project.identifier}-gantt.png") + else + render :template => "projects/gantt.rhtml" + end + end + +private + # Find project of id params[:id] + # if not found, redirect to project list + # Used as a before_filter + def find_project + @project = Project.find(params[:id]) + rescue ActiveRecord::RecordNotFound + render_404 + end + + def retrieve_selected_tracker_ids(selectable_trackers) + if ids = params[:tracker_ids] + @selected_tracker_ids = (ids.is_a? Array) ? ids.collect { |id| id.to_i.to_s } : ids.split('/').collect { |id| id.to_i.to_s } + else + @selected_tracker_ids = selectable_trackers.collect {|t| t.id.to_s } + end + end +end diff --git a/rest_sys/app/controllers/queries_controller.rb b/rest_sys/app/controllers/queries_controller.rb new file mode 100644 index 000000000..69bad345a --- /dev/null +++ b/rest_sys/app/controllers/queries_controller.rb @@ -0,0 +1,81 @@ +# redMine - project management software +# Copyright (C) 2006-2007 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class QueriesController < ApplicationController + layout 'base' + before_filter :find_project, :authorize + + def index + @queries = @project.queries.find(:all, + :order => "name ASC", + :conditions => ["is_public = ? or user_id = ?", true, (User.current.logged? ? User.current.id : 0)]) + end + + def new + @query = Query.new(params[:query]) + @query.project = @project + @query.user = User.current + @query.is_public = false unless current_role.allowed_to?(:manage_public_queries) + @query.column_names = nil if params[:default_columns] + + params[:fields].each do |field| + @query.add_filter(field, params[:operators][field], params[:values][field]) + end if params[:fields] + + if request.post? && params[:confirm] && @query.save + flash[:notice] = l(:notice_successful_create) + redirect_to :controller => 'issues', :action => 'index', :project_id => @project, :query_id => @query + return + end + render :layout => false if request.xhr? + end + + def edit + if request.post? + @query.filters = {} + params[:fields].each do |field| + @query.add_filter(field, params[:operators][field], params[:values][field]) + end if params[:fields] + @query.attributes = params[:query] + @query.is_public = false unless current_role.allowed_to?(:manage_public_queries) + @query.column_names = nil if params[:default_columns] + + if @query.save + flash[:notice] = l(:notice_successful_update) + redirect_to :controller => 'issues', :action => 'index', :project_id => @project, :query_id => @query + end + end + end + + def destroy + @query.destroy if request.post? + redirect_to :controller => 'queries', :project_id => @project + end + +private + def find_project + if params[:id] + @query = Query.find(params[:id]) + @project = @query.project + render_403 unless @query.editable_by?(User.current) + else + @project = Project.find(params[:project_id]) + end + rescue ActiveRecord::RecordNotFound + render_404 + end +end diff --git a/rest_sys/app/controllers/reports_controller.rb b/rest_sys/app/controllers/reports_controller.rb new file mode 100644 index 000000000..e18e117a6 --- /dev/null +++ b/rest_sys/app/controllers/reports_controller.rb @@ -0,0 +1,213 @@ +# redMine - project management software +# Copyright (C) 2006 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class ReportsController < ApplicationController + layout 'base' + before_filter :find_project, :authorize + + def issue_report + @statuses = IssueStatus.find(:all, :order => 'position') + + case params[:detail] + when "tracker" + @field = "tracker_id" + @rows = @project.trackers + @data = issues_by_tracker + @report_title = l(:field_tracker) + render :template => "reports/issue_report_details" + when "version" + @field = "fixed_version_id" + @rows = @project.versions.sort + @data = issues_by_version + @report_title = l(:field_version) + render :template => "reports/issue_report_details" + when "priority" + @field = "priority_id" + @rows = Enumeration::get_values('IPRI') + @data = issues_by_priority + @report_title = l(:field_priority) + render :template => "reports/issue_report_details" + when "category" + @field = "category_id" + @rows = @project.issue_categories + @data = issues_by_category + @report_title = l(:field_category) + render :template => "reports/issue_report_details" + when "author" + @field = "author_id" + @rows = @project.members.collect { |m| m.user } + @data = issues_by_author + @report_title = l(:field_author) + render :template => "reports/issue_report_details" + when "subproject" + @field = "project_id" + @rows = @project.active_children + @data = issues_by_subproject + @report_title = l(:field_subproject) + render :template => "reports/issue_report_details" + else + @trackers = @project.trackers + @versions = @project.versions.sort + @priorities = Enumeration::get_values('IPRI') + @categories = @project.issue_categories + @authors = @project.members.collect { |m| m.user } + @subprojects = @project.active_children + issues_by_tracker + issues_by_version + issues_by_priority + issues_by_category + issues_by_author + issues_by_subproject + + render :template => "reports/issue_report" + end + end + + def delays + @trackers = Tracker.find(:all) + if request.get? + @selected_tracker_ids = @trackers.collect {|t| t.id.to_s } + else + @selected_tracker_ids = params[:tracker_ids].collect { |id| id.to_i.to_s } if params[:tracker_ids] and params[:tracker_ids].is_a? Array + end + @selected_tracker_ids ||= [] + @raw = + ActiveRecord::Base.connection.select_all("SELECT datediff( a.created_on, b.created_on ) as delay, count(a.id) as total + FROM issue_histories a, issue_histories b, issues i + WHERE a.status_id =5 + AND a.issue_id = b.issue_id + AND a.issue_id = i.id + AND i.tracker_id in (#{@selected_tracker_ids.join(',')}) + AND b.id = ( + SELECT min( c.id ) + FROM issue_histories c + WHERE b.issue_id = c.issue_id ) + GROUP BY delay") unless @selected_tracker_ids.empty? + @raw ||=[] + + @x_from = 0 + @x_to = 0 + @y_from = 0 + @y_to = 0 + @sum_total = 0 + @sum_delay = 0 + @raw.each do |r| + @x_to = [r['delay'].to_i, @x_to].max + @y_to = [r['total'].to_i, @y_to].max + @sum_total = @sum_total + r['total'].to_i + @sum_delay = @sum_delay + r['total'].to_i * r['delay'].to_i + end + end + +private + # Find project of id params[:id] + def find_project + @project = Project.find(params[:id]) + rescue ActiveRecord::RecordNotFound + render_404 + end + + def issues_by_tracker + @issues_by_tracker ||= + ActiveRecord::Base.connection.select_all("select s.id as status_id, + s.is_closed as closed, + t.id as tracker_id, + count(i.id) as total + from + #{Issue.table_name} i, #{IssueStatus.table_name} s, #{Tracker.table_name} t + where + i.status_id=s.id + and i.tracker_id=t.id + and i.project_id=#{@project.id} + group by s.id, s.is_closed, t.id") + end + + def issues_by_version + @issues_by_version ||= + ActiveRecord::Base.connection.select_all("select s.id as status_id, + s.is_closed as closed, + v.id as fixed_version_id, + count(i.id) as total + from + #{Issue.table_name} i, #{IssueStatus.table_name} s, #{Version.table_name} v + where + i.status_id=s.id + and i.fixed_version_id=v.id + and i.project_id=#{@project.id} + group by s.id, s.is_closed, v.id") + end + + def issues_by_priority + @issues_by_priority ||= + ActiveRecord::Base.connection.select_all("select s.id as status_id, + s.is_closed as closed, + p.id as priority_id, + count(i.id) as total + from + #{Issue.table_name} i, #{IssueStatus.table_name} s, #{Enumeration.table_name} p + where + i.status_id=s.id + and i.priority_id=p.id + and i.project_id=#{@project.id} + group by s.id, s.is_closed, p.id") + end + + def issues_by_category + @issues_by_category ||= + ActiveRecord::Base.connection.select_all("select s.id as status_id, + s.is_closed as closed, + c.id as category_id, + count(i.id) as total + from + #{Issue.table_name} i, #{IssueStatus.table_name} s, #{IssueCategory.table_name} c + where + i.status_id=s.id + and i.category_id=c.id + and i.project_id=#{@project.id} + group by s.id, s.is_closed, c.id") + end + + def issues_by_author + @issues_by_author ||= + ActiveRecord::Base.connection.select_all("select s.id as status_id, + s.is_closed as closed, + a.id as author_id, + count(i.id) as total + from + #{Issue.table_name} i, #{IssueStatus.table_name} s, #{User.table_name} a + where + i.status_id=s.id + and i.author_id=a.id + and i.project_id=#{@project.id} + group by s.id, s.is_closed, a.id") + end + + def issues_by_subproject + @issues_by_subproject ||= + ActiveRecord::Base.connection.select_all("select s.id as status_id, + s.is_closed as closed, + i.project_id as project_id, + count(i.id) as total + from + #{Issue.table_name} i, #{IssueStatus.table_name} s + where + i.status_id=s.id + and i.project_id IN (#{@project.active_children.collect{|p| p.id}.join(',')}) + group by s.id, s.is_closed, i.project_id") if @project.active_children.any? + @issues_by_subproject ||= [] + end +end diff --git a/rest_sys/app/controllers/repositories_controller.rb b/rest_sys/app/controllers/repositories_controller.rb new file mode 100644 index 000000000..ef332eb37 --- /dev/null +++ b/rest_sys/app/controllers/repositories_controller.rb @@ -0,0 +1,280 @@ +# redMine - project management software +# Copyright (C) 2006-2007 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require 'SVG/Graph/Bar' +require 'SVG/Graph/BarHorizontal' +require 'digest/sha1' + +class ChangesetNotFound < Exception +end + +class RepositoriesController < ApplicationController + layout 'base' + before_filter :find_repository, :except => :edit + before_filter :find_project, :only => :edit + before_filter :authorize + accept_key_auth :revisions + + def edit + @repository = @project.repository + if !@repository + @repository = Repository.factory(params[:repository_scm]) + @repository.project = @project + end + if request.post? + @repository.attributes = params[:repository] + @repository.save + end + render(:update) {|page| page.replace_html "tab-content-repository", :partial => 'projects/settings/repository'} + end + + def destroy + @repository.destroy + redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'repository' + end + + def show + # check if new revisions have been committed in the repository + @repository.fetch_changesets if Setting.autofetch_changesets? + # get entries for the browse frame + @entries = @repository.entries('') + # latest changesets + @changesets = @repository.changesets.find(:all, :limit => 10, :order => "committed_on DESC") + show_error and return unless @entries || @changesets.any? + end + + def browse + @entries = @repository.entries(@path, @rev) + if request.xhr? + @entries ? render(:partial => 'dir_list_content') : render(:nothing => true) + else + show_error unless @entries + end + end + + def changes + @entry = @repository.scm.entry(@path, @rev) + show_error and return unless @entry + @changesets = @repository.changesets_for_path(@path) + end + + def revisions + @changeset_count = @repository.changesets.count + @changeset_pages = Paginator.new self, @changeset_count, + 25, + params['page'] + @changesets = @repository.changesets.find(:all, + :limit => @changeset_pages.items_per_page, + :offset => @changeset_pages.current.offset) + + respond_to do |format| + format.html { render :layout => false if request.xhr? } + format.atom { render_feed(@changesets, :title => "#{@project.name}: #{l(:label_revision_plural)}") } + end + end + + def entry + @content = @repository.scm.cat(@path, @rev) + show_error and return unless @content + if 'raw' == params[:format] + send_data @content, :filename => @path.split('/').last + else + # Prevent empty lines when displaying a file with Windows style eol + @content.gsub!("\r\n", "\n") + end + end + + def annotate + @annotate = @repository.scm.annotate(@path, @rev) + show_error and return if @annotate.nil? || @annotate.empty? + end + + def revision + @changeset = @repository.changesets.find_by_revision(@rev) + raise ChangesetNotFound unless @changeset + @changes_count = @changeset.changes.size + @changes_pages = Paginator.new self, @changes_count, 150, params['page'] + @changes = @changeset.changes.find(:all, + :limit => @changes_pages.items_per_page, + :offset => @changes_pages.current.offset) + + respond_to do |format| + format.html + format.js {render :layout => false} + end + rescue ChangesetNotFound + show_error + end + + def diff + @rev_to = params[:rev_to] ? params[:rev_to].to_i : (@rev - 1) + @diff_type = params[:type] || User.current.pref[:diff_type] || 'inline' + @diff_type = 'inline' unless %w(inline sbs).include?(@diff_type) + + # Save diff type as user preference + if User.current.logged? && @diff_type != User.current.pref[:diff_type] + User.current.pref[:diff_type] = @diff_type + User.current.preference.save + end + + @cache_key = "repositories/diff/#{@repository.id}/" + Digest::MD5.hexdigest("#{@path}-#{@rev}-#{@rev_to}-#{@diff_type}") + unless read_fragment(@cache_key) + @diff = @repository.diff(@path, @rev, @rev_to, @diff_type) + show_error and return unless @diff + end + end + + def stats + end + + def graph + data = nil + case params[:graph] + when "commits_per_month" + data = graph_commits_per_month(@repository) + when "commits_per_author" + data = graph_commits_per_author(@repository) + end + if data + headers["Content-Type"] = "image/svg+xml" + send_data(data, :type => "image/svg+xml", :disposition => "inline") + else + render_404 + end + end + +private + def find_project + @project = Project.find(params[:id]) + rescue ActiveRecord::RecordNotFound + render_404 + end + + def find_repository + @project = Project.find(params[:id]) + @repository = @project.repository + render_404 and return false unless @repository + @path = params[:path].join('/') unless params[:path].nil? + @path ||= '' + @rev = params[:rev].to_i if params[:rev] + rescue ActiveRecord::RecordNotFound + render_404 + end + + def show_error + flash.now[:error] = l(:notice_scm_error) + render :nothing => true, :layout => true + end + + def graph_commits_per_month(repository) + @date_to = Date.today + @date_from = @date_to << 11 + @date_from = Date.civil(@date_from.year, @date_from.month, 1) + commits_by_day = repository.changesets.count(:all, :group => :commit_date, :conditions => ["commit_date BETWEEN ? AND ?", @date_from, @date_to]) + commits_by_month = [0] * 12 + commits_by_day.each {|c| commits_by_month[c.first.to_date.months_ago] += c.last } + + changes_by_day = repository.changes.count(:all, :group => :commit_date, :conditions => ["commit_date BETWEEN ? AND ?", @date_from, @date_to]) + changes_by_month = [0] * 12 + changes_by_day.each {|c| changes_by_month[c.first.to_date.months_ago] += c.last } + + fields = [] + month_names = l(:actionview_datehelper_select_month_names_abbr).split(',') + 12.times {|m| fields << month_names[((Date.today.month - 1 - m) % 12)]} + + graph = SVG::Graph::Bar.new( + :height => 300, + :width => 500, + :fields => fields.reverse, + :stack => :side, + :scale_integers => true, + :step_x_labels => 2, + :show_data_values => false, + :graph_title => l(:label_commits_per_month), + :show_graph_title => true + ) + + graph.add_data( + :data => commits_by_month[0..11].reverse, + :title => l(:label_revision_plural) + ) + + graph.add_data( + :data => changes_by_month[0..11].reverse, + :title => l(:label_change_plural) + ) + + graph.burn + end + + def graph_commits_per_author(repository) + commits_by_author = repository.changesets.count(:all, :group => :committer) + commits_by_author.sort! {|x, y| x.last <=> y.last} + + changes_by_author = repository.changes.count(:all, :group => :committer) + h = changes_by_author.inject({}) {|o, i| o[i.first] = i.last; o} + + fields = commits_by_author.collect {|r| r.first} + commits_data = commits_by_author.collect {|r| r.last} + changes_data = commits_by_author.collect {|r| h[r.first] || 0} + + fields = fields + [""]*(10 - fields.length) if fields.length<10 + commits_data = commits_data + [0]*(10 - commits_data.length) if commits_data.length<10 + changes_data = changes_data + [0]*(10 - changes_data.length) if changes_data.length<10 + + graph = SVG::Graph::BarHorizontal.new( + :height => 300, + :width => 500, + :fields => fields, + :stack => :side, + :scale_integers => true, + :show_data_values => false, + :rotate_y_labels => false, + :graph_title => l(:label_commits_per_author), + :show_graph_title => true + ) + + graph.add_data( + :data => commits_data, + :title => l(:label_revision_plural) + ) + + graph.add_data( + :data => changes_data, + :title => l(:label_change_plural) + ) + + graph.burn + end + +end + +class Date + def months_ago(date = Date.today) + (date.year - self.year)*12 + (date.month - self.month) + end + + def weeks_ago(date = Date.today) + (date.year - self.year)*52 + (date.cweek - self.cweek) + end +end + +class String + def with_leading_slash + starts_with?('/') ? self : "/#{self}" + end +end diff --git a/rest_sys/app/controllers/roles_controller.rb b/rest_sys/app/controllers/roles_controller.rb new file mode 100644 index 000000000..a8a31cd4d --- /dev/null +++ b/rest_sys/app/controllers/roles_controller.rb @@ -0,0 +1,112 @@ +# redMine - project management software +# Copyright (C) 2006 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class RolesController < ApplicationController + layout 'base' + before_filter :require_admin + + verify :method => :post, :only => [ :destroy, :move ], + :redirect_to => { :action => :list } + + def index + list + render :action => 'list' unless request.xhr? + end + + def list + @role_pages, @roles = paginate :roles, :per_page => 25, :order => 'builtin, position' + render :action => "list", :layout => false if request.xhr? + end + + def new + # Prefills the form with 'Non member' role permissions + @role = Role.new(params[:role] || {:permissions => Role.non_member.permissions}) + if request.post? && @role.save + flash[:notice] = l(:notice_successful_create) + redirect_to :action => 'list' + end + @permissions = @role.setable_permissions + end + + def edit + @role = Role.find(params[:id]) + if request.post? and @role.update_attributes(params[:role]) + flash[:notice] = l(:notice_successful_update) + redirect_to :action => 'list' + end + @permissions = @role.setable_permissions + end + + def destroy + @role = Role.find(params[:id]) + #unless @role.members.empty? + # flash[:error] = 'Some members have this role. Can\'t delete it.' + #else + @role.destroy + #end + redirect_to :action => 'list' + end + + def move + @role = Role.find(params[:id]) + case params[:position] + when 'highest' + @role.move_to_top + when 'higher' + @role.move_higher + when 'lower' + @role.move_lower + when 'lowest' + @role.move_to_bottom + end if params[:position] + redirect_to :action => 'list' + end + + def workflow + @role = Role.find_by_id(params[:role_id]) + @tracker = Tracker.find_by_id(params[:tracker_id]) + + if request.post? + Workflow.destroy_all( ["role_id=? and tracker_id=?", @role.id, @tracker.id]) + (params[:issue_status] || []).each { |old, news| + news.each { |new| + @role.workflows.build(:tracker_id => @tracker.id, :old_status_id => old, :new_status_id => new) + } + } + if @role.save + flash[:notice] = l(:notice_successful_update) + redirect_to :action => 'workflow', :role_id => @role, :tracker_id => @tracker + end + end + @roles = Role.find(:all, :order => 'builtin, position') + @trackers = Tracker.find(:all, :order => 'position') + @statuses = IssueStatus.find(:all, :order => 'position') + end + + def report + @roles = Role.find(:all, :order => 'builtin, position') + @permissions = Redmine::AccessControl.permissions.select { |p| !p.public? } + if request.post? + @roles.each do |role| + role.permissions = params[:permissions][role.id.to_s] + role.save + end + flash[:notice] = l(:notice_successful_update) + redirect_to :action => 'list' + end + end +end diff --git a/rest_sys/app/controllers/search_controller.rb b/rest_sys/app/controllers/search_controller.rb new file mode 100644 index 000000000..69e1ee503 --- /dev/null +++ b/rest_sys/app/controllers/search_controller.rb @@ -0,0 +1,112 @@ +# redMine - project management software +# Copyright (C) 2006 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class SearchController < ApplicationController + layout 'base' + + helper :messages + include MessagesHelper + + def index + @question = params[:q] || "" + @question.strip! + @all_words = params[:all_words] || (params[:submit] ? false : true) + @titles_only = !params[:titles_only].nil? + + offset = nil + begin; offset = params[:offset].to_time if params[:offset]; rescue; end + + # quick jump to an issue + if @question.match(/^#?(\d+)$/) && Issue.find_by_id($1, :include => :project, :conditions => Project.visible_by(User.current)) + redirect_to :controller => "issues", :action => "show", :id => $1 + return + end + + if params[:id] + find_project + return unless check_project_privacy + end + + if @project + # only show what the user is allowed to view + @object_types = %w(issues news documents changesets wiki_pages messages) + @object_types = @object_types.select {|o| User.current.allowed_to?("view_#{o}".to_sym, @project)} + + @scope = @object_types.select {|t| params[t]} + @scope = @object_types if @scope.empty? + else + @object_types = @scope = %w(projects) + end + + # extract tokens from the question + # eg. hello "bye bye" => ["hello", "bye bye"] + @tokens = @question.scan(%r{((\s|^)"[\s\w]+"(\s|$)|\S+)}).collect {|m| m.first.gsub(%r{(^\s*"\s*|\s*"\s*$)}, '')} + # tokens must be at least 3 character long + @tokens = @tokens.uniq.select {|w| w.length > 2 } + + if !@tokens.empty? + # no more than 5 tokens to search for + @tokens.slice! 5..-1 if @tokens.size > 5 + # strings used in sql like statement + like_tokens = @tokens.collect {|w| "%#{w.downcase}%"} + @results = [] + limit = 10 + if @project + @scope.each do |s| + @results += s.singularize.camelcase.constantize.search(like_tokens, @project, + :all_words => @all_words, + :titles_only => @titles_only, + :limit => (limit+1), + :offset => offset, + :before => params[:previous].nil?) + end + @results = @results.sort {|a,b| b.event_datetime <=> a.event_datetime} + if params[:previous].nil? + @pagination_previous_date = @results[0].event_datetime if offset && @results[0] + if @results.size > limit + @pagination_next_date = @results[limit-1].event_datetime + @results = @results[0, limit] + end + else + @pagination_next_date = @results[-1].event_datetime if offset && @results[-1] + if @results.size > limit + @pagination_previous_date = @results[-(limit)].event_datetime + @results = @results[-(limit), limit] + end + end + else + operator = @all_words ? ' AND ' : ' OR ' + @results += Project.find(:all, + :limit => limit, + :conditions => [ (["(#{Project.visible_by(User.current)}) AND (LOWER(name) like ? OR LOWER(description) like ?)"] * like_tokens.size).join(operator), * (like_tokens * 2).sort] + ) if @scope.include? 'projects' + # if only one project is found, user is redirected to its overview + redirect_to :controller => 'projects', :action => 'show', :id => @results.first and return if @results.size == 1 + end + else + @question = "" + end + render :layout => false if request.xhr? + end + +private + def find_project + @project = Project.find(params[:id]) + rescue ActiveRecord::RecordNotFound + render_404 + end +end diff --git a/rest_sys/app/controllers/settings_controller.rb b/rest_sys/app/controllers/settings_controller.rb new file mode 100644 index 000000000..09af63176 --- /dev/null +++ b/rest_sys/app/controllers/settings_controller.rb @@ -0,0 +1,45 @@ +# redMine - project management software +# Copyright (C) 2006-2007 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class SettingsController < ApplicationController + layout 'base' + before_filter :require_admin + + def index + edit + render :action => 'edit' + end + + def edit + if request.post? and params[:settings] and params[:settings].is_a? Hash + params[:settings].each { |name, value| Setting[name] = value } + redirect_to :action => 'edit' and return + end + end + + def plugin + plugin_id = params[:id].to_sym + @plugin = Redmine::Plugin.registered_plugins[plugin_id] + if request.post? + Setting["plugin_#{plugin_id}"] = params[:settings] + flash[:notice] = l(:notice_successful_update) + redirect_to :action => 'plugin', :id => params[:id] + end + @partial = "../../vendor/plugins/#{plugin_id}/app/views/" + @plugin.settings[:partial] + @settings = Setting["plugin_#{plugin_id}"] + end +end diff --git a/rest_sys/app/controllers/sys_controller.rb b/rest_sys/app/controllers/sys_controller.rb new file mode 100644 index 000000000..6065c2833 --- /dev/null +++ b/rest_sys/app/controllers/sys_controller.rb @@ -0,0 +1,47 @@ +# redMine - project management software +# Copyright (C) 2006-2007 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class SysController < ActionController::Base + wsdl_service_name 'Sys' + web_service_api SysApi + web_service_scaffold :invoke + + before_invocation :check_enabled + + # Returns the projects list, with their repositories + def projects + Project.find(:all, :include => :repository) + end + + # Registers a repository for the given project identifier + # (Subversion specific) + def repository_created(identifier, url) + project = Project.find_by_identifier(identifier) + # Do not create the repository if the project has already one + return 0 unless project && project.repository.nil? + logger.debug "Repository for #{project.name} was created" + repository = Repository.factory('Subversion', :project => project, :url => url) + repository.save + repository.id || 0 + end + +protected + + def check_enabled(name, args) + Setting.sys_api_enabled? + end +end diff --git a/rest_sys/app/controllers/timelog_controller.rb b/rest_sys/app/controllers/timelog_controller.rb new file mode 100644 index 000000000..f90c4527e --- /dev/null +++ b/rest_sys/app/controllers/timelog_controller.rb @@ -0,0 +1,172 @@ +# redMine - project management software +# Copyright (C) 2006-2007 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class TimelogController < ApplicationController + layout 'base' + before_filter :find_project, :authorize + + helper :sort + include SortHelper + helper :issues + + def report + @available_criterias = { 'version' => {:sql => "#{Issue.table_name}.fixed_version_id", + :values => @project.versions, + :label => :label_version}, + 'category' => {:sql => "#{Issue.table_name}.category_id", + :values => @project.issue_categories, + :label => :field_category}, + 'member' => {:sql => "#{TimeEntry.table_name}.user_id", + :values => @project.users, + :label => :label_member}, + 'tracker' => {:sql => "#{Issue.table_name}.tracker_id", + :values => Tracker.find(:all), + :label => :label_tracker}, + 'activity' => {:sql => "#{TimeEntry.table_name}.activity_id", + :values => Enumeration::get_values('ACTI'), + :label => :label_activity} + } + + @criterias = params[:criterias] || [] + @criterias = @criterias.select{|criteria| @available_criterias.has_key? criteria} + @criterias.uniq! + + @columns = (params[:period] && %w(year month week).include?(params[:period])) ? params[:period] : 'month' + + if params[:date_from] + begin; @date_from = params[:date_from].to_date; rescue; end + end + if params[:date_to] + begin; @date_to = params[:date_to].to_date; rescue; end + end + @date_from ||= Date.civil(Date.today.year, 1, 1) + @date_to ||= (Date.civil(Date.today.year, Date.today.month, 1) >> 1) - 1 + + unless @criterias.empty? + sql_select = @criterias.collect{|criteria| @available_criterias[criteria][:sql] + " AS " + criteria}.join(', ') + sql_group_by = @criterias.collect{|criteria| @available_criterias[criteria][:sql]}.join(', ') + + sql = "SELECT #{sql_select}, tyear, tmonth, tweek, SUM(hours) AS hours" + sql << " FROM #{TimeEntry.table_name} LEFT JOIN #{Issue.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id" + sql << " WHERE #{TimeEntry.table_name}.project_id = %s" % @project.id + sql << " AND spent_on BETWEEN '%s' AND '%s'" % [ActiveRecord::Base.connection.quoted_date(@date_from.to_time), ActiveRecord::Base.connection.quoted_date(@date_to.to_time)] + sql << " GROUP BY #{sql_group_by}, tyear, tmonth, tweek" + + @hours = ActiveRecord::Base.connection.select_all(sql) + + @hours.each do |row| + case @columns + when 'year' + row['year'] = row['tyear'] + when 'month' + row['month'] = "#{row['tyear']}-#{row['tmonth']}" + when 'week' + row['week'] = "#{row['tyear']}-#{row['tweek']}" + end + end + end + + @periods = [] + date_from = @date_from + # 100 columns max + while date_from < @date_to && @periods.length < 100 + case @columns + when 'year' + @periods << "#{date_from.year}" + date_from = date_from >> 12 + when 'month' + @periods << "#{date_from.year}-#{date_from.month}" + date_from = date_from >> 1 + when 'week' + @periods << "#{date_from.year}-#{date_from.cweek}" + date_from = date_from + 7 + end + end + + render :layout => false if request.xhr? + end + + def details + sort_init 'spent_on', 'desc' + sort_update + + @entries = (@issue ? @issue : @project).time_entries.find(:all, :include => [:activity, :user, {:issue => [:tracker, :assigned_to, :priority]}], :order => sort_clause) + + @total_hours = @entries.inject(0) { |sum,entry| sum + entry.hours } + @owner_id = User.current.id + + send_csv and return if 'csv' == params[:export] + render :action => 'details', :layout => false if request.xhr? + end + + def edit + render_404 and return if @time_entry && @time_entry.user != User.current + @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today) + @time_entry.attributes = params[:time_entry] + if request.post? and @time_entry.save + flash[:notice] = l(:notice_successful_update) + redirect_to :action => 'details', :project_id => @time_entry.project, :issue_id => @time_entry.issue + return + end + @activities = Enumeration::get_values('ACTI') + end + +private + def find_project + if params[:id] + @time_entry = TimeEntry.find(params[:id]) + @project = @time_entry.project + elsif params[:issue_id] + @issue = Issue.find(params[:issue_id]) + @project = @issue.project + elsif params[:project_id] + @project = Project.find(params[:project_id]) + else + render_404 + return false + end + end + + def send_csv + ic = Iconv.new(l(:general_csv_encoding), 'UTF-8') + export = StringIO.new + CSV::Writer.generate(export, l(:general_csv_separator)) do |csv| + # csv header fields + headers = [l(:field_spent_on), + l(:field_user), + l(:field_activity), + l(:field_issue), + l(:field_hours), + l(:field_comments) + ] + csv << headers.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end } + # csv lines + @entries.each do |entry| + fields = [l_date(entry.spent_on), + entry.user.name, + entry.activity.name, + (entry.issue ? entry.issue.id : nil), + entry.hours, + entry.comments + ] + csv << fields.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end } + end + end + export.rewind + send_data(export.read, :type => 'text/csv; header=present', :filename => 'export.csv') + end +end diff --git a/rest_sys/app/controllers/trackers_controller.rb b/rest_sys/app/controllers/trackers_controller.rb new file mode 100644 index 000000000..46edea548 --- /dev/null +++ b/rest_sys/app/controllers/trackers_controller.rb @@ -0,0 +1,84 @@ +# redMine - project management software +# Copyright (C) 2006 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class TrackersController < ApplicationController + layout 'base' + before_filter :require_admin + + def index + list + render :action => 'list' unless request.xhr? + end + + # GETs should be safe (see http://www.w3.org/2001/tag/doc/whenToUseGet.html) + verify :method => :post, :only => [ :destroy, :move ], :redirect_to => { :action => :list } + + def list + @tracker_pages, @trackers = paginate :trackers, :per_page => 10, :order => 'position' + render :action => "list", :layout => false if request.xhr? + end + + def new + @tracker = Tracker.new(params[:tracker]) + if request.post? and @tracker.save + # workflow copy + if !params[:copy_workflow_from].blank? && (copy_from = Tracker.find_by_id(params[:copy_workflow_from])) + Workflow.transaction do + copy_from.workflows.find(:all, :include => [:role, :old_status, :new_status]).each do |w| + Workflow.create(:tracker_id => @tracker.id, :role => w.role, :old_status => w.old_status, :new_status => w.new_status) + end + end + end + flash[:notice] = l(:notice_successful_create) + redirect_to :action => 'list' + end + @trackers = Tracker.find :all + end + + def edit + @tracker = Tracker.find(params[:id]) + if request.post? and @tracker.update_attributes(params[:tracker]) + flash[:notice] = l(:notice_successful_update) + redirect_to :action => 'list' + end + end + + def move + @tracker = Tracker.find(params[:id]) + case params[:position] + when 'highest' + @tracker.move_to_top + when 'higher' + @tracker.move_higher + when 'lower' + @tracker.move_lower + when 'lowest' + @tracker.move_to_bottom + end if params[:position] + redirect_to :action => 'list' + end + + def destroy + @tracker = Tracker.find(params[:id]) + unless @tracker.issues.empty? + flash[:error] = "This tracker contains issues and can\'t be deleted." + else + @tracker.destroy + end + redirect_to :action => 'list' + end +end diff --git a/rest_sys/app/controllers/users_controller.rb b/rest_sys/app/controllers/users_controller.rb new file mode 100644 index 000000000..3f3adb57d --- /dev/null +++ b/rest_sys/app/controllers/users_controller.rb @@ -0,0 +1,112 @@ +# redMine - project management software +# Copyright (C) 2006-2007 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class UsersController < ApplicationController + layout 'base' + before_filter :require_admin + + helper :sort + include SortHelper + helper :custom_fields + include CustomFieldsHelper + + def index + list + render :action => 'list' unless request.xhr? + end + + def list + sort_init 'login', 'asc' + sort_update + + @status = params[:status] ? params[:status].to_i : 1 + conditions = nil + conditions = ["status=?", @status] unless @status == 0 + + @user_count = User.count(:conditions => conditions) + @user_pages = Paginator.new self, @user_count, + 15, + params['page'] + @users = User.find :all,:order => sort_clause, + :conditions => conditions, + :limit => @user_pages.items_per_page, + :offset => @user_pages.current.offset + + render :action => "list", :layout => false if request.xhr? + end + + def add + if request.get? + @user = User.new(:language => Setting.default_language) + @custom_values = UserCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| CustomValue.new(:custom_field => x, :customized => @user) } + else + @user = User.new(params[:user]) + @user.admin = params[:user][:admin] || false + @user.login = params[:user][:login] + @user.password, @user.password_confirmation = params[:password], params[:password_confirmation] unless @user.auth_source_id + @custom_values = UserCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| CustomValue.new(:custom_field => x, :customized => @user, :value => (params[:custom_fields] ? params["custom_fields"][x.id.to_s] : nil)) } + @user.custom_values = @custom_values + if @user.save + Mailer.deliver_account_information(@user, params[:password]) if params[:send_information] + flash[:notice] = l(:notice_successful_create) + redirect_to :action => 'list' + end + end + @auth_sources = AuthSource.find(:all) + end + + def edit + @user = User.find(params[:id]) + if request.get? + @custom_values = UserCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| @user.custom_values.find_by_custom_field_id(x.id) || CustomValue.new(:custom_field => x) } + else + @user.admin = params[:user][:admin] if params[:user][:admin] + @user.login = params[:user][:login] if params[:user][:login] + @user.password, @user.password_confirmation = params[:password], params[:password_confirmation] unless params[:password].nil? or params[:password].empty? or @user.auth_source_id + if params[:custom_fields] + @custom_values = UserCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| CustomValue.new(:custom_field => x, :customized => @user, :value => params["custom_fields"][x.id.to_s]) } + @user.custom_values = @custom_values + end + if @user.update_attributes(params[:user]) + flash[:notice] = l(:notice_successful_update) + redirect_to :action => 'list' + end + end + @auth_sources = AuthSource.find(:all) + @roles = Role.find_all_givable + @projects = Project.find(:all, :order => 'name', :conditions => "status=#{Project::STATUS_ACTIVE}") - @user.projects + @membership ||= Member.new + end + + def edit_membership + @user = User.find(params[:id]) + @membership = params[:membership_id] ? Member.find(params[:membership_id]) : Member.new(:user => @user) + @membership.attributes = params[:membership] + if request.post? and @membership.save + flash[:notice] = l(:notice_successful_update) + end + redirect_to :action => 'edit', :id => @user and return + end + + def destroy_membership + @user = User.find(params[:id]) + if request.post? and Member.find(params[:membership_id]).destroy + flash[:notice] = l(:notice_successful_update) + end + redirect_to :action => 'edit', :id => @user and return + end +end diff --git a/rest_sys/app/controllers/versions_controller.rb b/rest_sys/app/controllers/versions_controller.rb new file mode 100644 index 000000000..1365c97e8 --- /dev/null +++ b/rest_sys/app/controllers/versions_controller.rb @@ -0,0 +1,70 @@ +# redMine - project management software +# Copyright (C) 2006 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class VersionsController < ApplicationController + layout 'base' + before_filter :find_project, :authorize + + cache_sweeper :version_sweeper, :only => [ :edit, :destroy ] + + def show + end + + def edit + if request.post? and @version.update_attributes(params[:version]) + flash[:notice] = l(:notice_successful_update) + redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project + end + end + + def destroy + @version.destroy + redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project + rescue + flash[:error] = "Unable to delete version" + redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project + end + + def download + @attachment = @version.attachments.find(params[:attachment_id]) + @attachment.increment_download + send_file @attachment.diskfile, :filename => @attachment.filename, :type => @attachment.content_type + rescue + render_404 + end + + def destroy_file + @version.attachments.find(params[:attachment_id]).destroy + flash[:notice] = l(:notice_successful_delete) + redirect_to :controller => 'projects', :action => 'list_files', :id => @project + end + + def status_by + respond_to do |format| + format.html { render :action => 'show' } + format.js { render(:update) {|page| page.replace_html 'status_by', render_issue_status_by(@version, params[:status_by])} } + end + end + +private + def find_project + @version = Version.find(params[:id]) + @project = @version.project + rescue ActiveRecord::RecordNotFound + render_404 + end +end diff --git a/rest_sys/app/controllers/watchers_controller.rb b/rest_sys/app/controllers/watchers_controller.rb new file mode 100644 index 000000000..206dc0843 --- /dev/null +++ b/rest_sys/app/controllers/watchers_controller.rb @@ -0,0 +1,49 @@ +# redMine - project management software +# Copyright (C) 2006-2007 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class WatchersController < ApplicationController + layout 'base' + before_filter :require_login, :find_project, :check_project_privacy + + def add + user = User.current + @watched.add_watcher(user) + respond_to do |format| + format.html { render :text => 'Watcher added.', :layout => true } + format.js { render(:update) {|page| page.replace_html 'watcher', watcher_link(@watched, user)} } + end + end + + def remove + user = User.current + @watched.remove_watcher(user) + respond_to do |format| + format.html { render :text => 'Watcher removed.', :layout => true } + format.js { render(:update) {|page| page.replace_html 'watcher', watcher_link(@watched, user)} } + end + end + +private + def find_project + klass = Object.const_get(params[:object_type].camelcase) + return false unless klass.respond_to?('watched_by') + @watched = klass.find(params[:object_id]) + @project = @watched.project + rescue + render_404 + end +end diff --git a/rest_sys/app/controllers/welcome_controller.rb b/rest_sys/app/controllers/welcome_controller.rb new file mode 100644 index 000000000..b4be7fb1c --- /dev/null +++ b/rest_sys/app/controllers/welcome_controller.rb @@ -0,0 +1,25 @@ +# redMine - project management software +# Copyright (C) 2006 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class WelcomeController < ApplicationController + layout 'base' + + def index + @news = News.latest User.current + @projects = Project.latest User.current + end +end diff --git a/rest_sys/app/controllers/wiki_controller.rb b/rest_sys/app/controllers/wiki_controller.rb new file mode 100644 index 000000000..2ee22167d --- /dev/null +++ b/rest_sys/app/controllers/wiki_controller.rb @@ -0,0 +1,176 @@ +# redMine - project management software +# Copyright (C) 2006-2007 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require 'diff' + +class WikiController < ApplicationController + layout 'base' + before_filter :find_wiki, :authorize + + verify :method => :post, :only => [:destroy, :destroy_attachment], :redirect_to => { :action => :index } + + helper :attachments + include AttachmentsHelper + + # display a page (in editing mode if it doesn't exist) + def index + page_title = params[:page] + @page = @wiki.find_or_new_page(page_title) + if @page.new_record? + if User.current.allowed_to?(:edit_wiki_pages, @project) + edit + render :action => 'edit' + else + render_404 + end + return + end + @content = @page.content_for_version(params[:version]) + if params[:export] == 'html' + export = render_to_string :action => 'export', :layout => false + send_data(export, :type => 'text/html', :filename => "#{@page.title}.html") + return + elsif params[:export] == 'txt' + send_data(@content.text, :type => 'text/plain', :filename => "#{@page.title}.txt") + return + end + render :action => 'show' + end + + # edit an existing page or a new one + def edit + @page = @wiki.find_or_new_page(params[:page]) + @page.content = WikiContent.new(:page => @page) if @page.new_record? + + @content = @page.content_for_version(params[:version]) + @content.text = "h1. #{@page.pretty_title}" if @content.text.blank? + # don't keep previous comment + @content.comments = nil + if request.post? + if !@page.new_record? && @content.text == params[:content][:text] + # don't save if text wasn't changed + redirect_to :action => 'index', :id => @project, :page => @page.title + return + end + #@content.text = params[:content][:text] + #@content.comments = params[:content][:comments] + @content.attributes = params[:content] + @content.author = User.current + # if page is new @page.save will also save content, but not if page isn't a new record + if (@page.new_record? ? @page.save : @content.save) + redirect_to :action => 'index', :id => @project, :page => @page.title + end + end + rescue ActiveRecord::StaleObjectError + # Optimistic locking exception + flash[:error] = l(:notice_locking_conflict) + end + + # rename a page + def rename + @page = @wiki.find_page(params[:page]) + @page.redirect_existing_links = true + # used to display the *original* title if some AR validation errors occur + @original_title = @page.pretty_title + if request.post? && @page.update_attributes(params[:wiki_page]) + flash[:notice] = l(:notice_successful_update) + redirect_to :action => 'index', :id => @project, :page => @page.title + end + end + + # show page history + def history + @page = @wiki.find_page(params[:page]) + + @version_count = @page.content.versions.count + @version_pages = Paginator.new self, @version_count, 25, params['p'] + # don't load text + @versions = @page.content.versions.find :all, + :select => "id, author_id, comments, updated_on, version", + :order => 'version DESC', + :limit => @version_pages.items_per_page + 1, + :offset => @version_pages.current.offset + + render :layout => false if request.xhr? + end + + def diff + @page = @wiki.find_page(params[:page]) + @diff = @page.diff(params[:version], params[:version_from]) + render_404 unless @diff + end + + # remove a wiki page and its history + def destroy + @page = @wiki.find_page(params[:page]) + @page.destroy if @page + redirect_to :action => 'special', :id => @project, :page => 'Page_index' + end + + # display special pages + def special + page_title = params[:page].downcase + case page_title + # show pages index, sorted by title + when 'page_index', 'date_index' + # eager load information about last updates, without loading text + @pages = @wiki.pages.find :all, :select => "#{WikiPage.table_name}.*, #{WikiContent.table_name}.updated_on", + :joins => "LEFT JOIN #{WikiContent.table_name} ON #{WikiContent.table_name}.page_id = #{WikiPage.table_name}.id", + :order => 'title' + @pages_by_date = @pages.group_by {|p| p.updated_on.to_date} + # export wiki to a single html file + when 'export' + @pages = @wiki.pages.find :all, :order => 'title' + export = render_to_string :action => 'export_multiple', :layout => false + send_data(export, :type => 'text/html', :filename => "wiki.html") + return + else + # requested special page doesn't exist, redirect to default page + redirect_to :action => 'index', :id => @project, :page => nil and return + end + render :action => "special_#{page_title}" + end + + def preview + page = @wiki.find_page(params[:page]) + @attachements = page.attachments if page + @text = params[:content][:text] + render :partial => 'common/preview' + end + + def add_attachment + @page = @wiki.find_page(params[:page]) + attach_files(@page, params[:attachments]) + redirect_to :action => 'index', :page => @page.title + end + + def destroy_attachment + @page = @wiki.find_page(params[:page]) + @page.attachments.find(params[:attachment_id]).destroy + redirect_to :action => 'index', :page => @page.title + end + +private + + def find_wiki + @project = Project.find(params[:id]) + @wiki = @project.wiki + render_404 unless @wiki + rescue ActiveRecord::RecordNotFound + render_404 + end +end diff --git a/rest_sys/app/controllers/wikis_controller.rb b/rest_sys/app/controllers/wikis_controller.rb new file mode 100644 index 000000000..a222570ef --- /dev/null +++ b/rest_sys/app/controllers/wikis_controller.rb @@ -0,0 +1,44 @@ +# redMine - project management software +# Copyright (C) 2006-2007 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class WikisController < ApplicationController + layout 'base' + before_filter :find_project, :authorize + + # Create or update a project's wiki + def edit + @wiki = @project.wiki || Wiki.new(:project => @project) + @wiki.attributes = params[:wiki] + @wiki.save if request.post? + render(:update) {|page| page.replace_html "tab-content-wiki", :partial => 'projects/settings/wiki'} + end + + # Delete a project's wiki + def destroy + if request.post? && params[:confirm] && @project.wiki + @project.wiki.destroy + redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'wiki' + end + end + +private + def find_project + @project = Project.find(params[:id]) + rescue ActiveRecord::RecordNotFound + render_404 + end +end diff --git a/rest_sys/app/helpers/account_helper.rb b/rest_sys/app/helpers/account_helper.rb new file mode 100644 index 000000000..ff18b76a4 --- /dev/null +++ b/rest_sys/app/helpers/account_helper.rb @@ -0,0 +1,19 @@ +# redMine - project management software +# Copyright (C) 2006 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +module AccountHelper +end diff --git a/rest_sys/app/helpers/admin_helper.rb b/rest_sys/app/helpers/admin_helper.rb new file mode 100644 index 000000000..1b41d8374 --- /dev/null +++ b/rest_sys/app/helpers/admin_helper.rb @@ -0,0 +1,23 @@ +# redMine - project management software +# Copyright (C) 2006 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +module AdminHelper + def project_status_options_for_select(selected) + options_for_select([[l(:label_all), "*"], + [l(:status_active), 1]], selected) + end +end diff --git a/rest_sys/app/helpers/application_helper.rb b/rest_sys/app/helpers/application_helper.rb new file mode 100644 index 000000000..8deed9000 --- /dev/null +++ b/rest_sys/app/helpers/application_helper.rb @@ -0,0 +1,407 @@ +# redMine - project management software +# Copyright (C) 2006-2007 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +module ApplicationHelper + include Redmine::WikiFormatting::Macros::Definitions + + def current_role + @current_role ||= User.current.role_for_project(@project) + end + + # Return true if user is authorized for controller/action, otherwise false + def authorize_for(controller, action) + User.current.allowed_to?({:controller => controller, :action => action}, @project) + end + + # Display a link if user is authorized + def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference) + link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action]) + end + + # Display a link to user's account page + def link_to_user(user) + user ? link_to(user, :controller => 'account', :action => 'show', :id => user) : 'Anonymous' + end + + def link_to_issue(issue) + link_to "#{issue.tracker.name} ##{issue.id}", :controller => "issues", :action => "show", :id => issue + end + + def toggle_link(name, id, options={}) + onclick = "Element.toggle('#{id}'); " + onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ") + onclick << "return false;" + link_to(name, "#", :onclick => onclick) + end + + def show_and_goto_link(name, id, options={}) + onclick = "Element.show('#{id}'); " + onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ") + onclick << "location.href='##{id}-anchor'; " + onclick << "return false;" + link_to(name, "#", options.merge(:onclick => onclick)) + end + + def image_to_function(name, function, html_options = {}) + html_options.symbolize_keys! + tag(:input, html_options.merge({ + :type => "image", :src => image_path(name), + :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};" + })) + end + + def prompt_to_remote(name, text, param, url, html_options = {}) + html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;" + link_to name, {}, html_options + end + + def format_date(date) + return nil unless date + # "Setting.date_format.size < 2" is a temporary fix (content of date_format setting changed) + @date_format ||= (Setting.date_format.blank? || Setting.date_format.size < 2 ? l(:general_fmt_date) : Setting.date_format) + date.strftime(@date_format) + end + + def format_time(time, include_date = true) + return nil unless time + time = time.to_time if time.is_a?(String) + zone = User.current.time_zone + if time.utc? + local = zone ? zone.adjust(time) : time.getlocal + else + local = zone ? zone.adjust(time.getutc) : time + end + @date_format ||= (Setting.date_format.blank? || Setting.date_format.size < 2 ? l(:general_fmt_date) : Setting.date_format) + @time_format ||= (Setting.time_format.blank? ? l(:general_fmt_time) : Setting.time_format) + include_date ? local.strftime("#{@date_format} #{@time_format}") : local.strftime(@time_format) + end + + def authoring(created, author) + time_tag = content_tag('acronym', distance_of_time_in_words(Time.now, created), :title => format_time(created)) + l(:label_added_time_by, author || 'Anonymous', time_tag) + end + + def day_name(day) + l(:general_day_names).split(',')[day-1] + end + + def month_name(month) + l(:actionview_datehelper_select_month_names).split(',')[month-1] + end + + def pagination_links_full(paginator, options={}, html_options={}) + page_param = options.delete(:page_param) || :page + + html = '' + html << link_to_remote(('« ' + l(:label_previous)), + {:update => "content", :url => options.merge(page_param => paginator.current.previous)}, + {:href => url_for(:params => options.merge(page_param => paginator.current.previous))}) + ' ' if paginator.current.previous + + html << (pagination_links_each(paginator, options) do |n| + link_to_remote(n.to_s, + {:url => {:params => options.merge(page_param => n)}, :update => 'content'}, + {:href => url_for(:params => options.merge(page_param => n))}) + end || '') + + html << ' ' + link_to_remote((l(:label_next) + ' »'), + {:update => "content", :url => options.merge(page_param => paginator.current.next)}, + {:href => url_for(:params => options.merge(page_param => paginator.current.next))}) if paginator.current.next + html + end + + def set_html_title(text) + @html_header_title = text + end + + def html_title + title = [] + title << @project.name if @project + title << @html_header_title + title << Setting.app_title + title.compact.join(' - ') + end + + ACCESSKEYS = {:edit => 'e', + :preview => 'r', + :quick_search => 'f', + :search => '4', + }.freeze unless const_defined?(:ACCESSKEYS) + + def accesskey(s) + ACCESSKEYS[s] + end + + # Formats text according to system settings. + # 2 ways to call this method: + # * with a String: textilizable(text, options) + # * with an object and one of its attribute: textilizable(issue, :description, options) + def textilizable(*args) + options = args.last.is_a?(Hash) ? args.pop : {} + case args.size + when 1 + obj = nil + text = args.shift || '' + when 2 + obj = args.shift + text = obj.send(args.shift) + else + raise ArgumentError, 'invalid arguments to textilizable' + end + + # when using an image link, try to use an attachment, if possible + attachments = options[:attachments] + if attachments + text = text.gsub(/!([<>=]*)(\S+\.(gif|jpg|jpeg|png))!/) do |m| + align = $1 + filename = $2 + rf = Regexp.new(filename, Regexp::IGNORECASE) + # search for the picture in attachments + if found = attachments.detect { |att| att.filename =~ rf } + image_url = url_for :controller => 'attachments', :action => 'download', :id => found.id + "!#{align}#{image_url}!" + else + "!#{align}#{filename}!" + end + end + end + + text = (Setting.text_formatting == 'textile') ? + Redmine::WikiFormatting.to_html(text) { |macro, args| exec_macro(macro, obj, args) } : + simple_format(auto_link(h(text))) + + # different methods for formatting wiki links + case options[:wiki_links] + when :local + # used for local links to html files + format_wiki_link = Proc.new {|project, title| "#{title}.html" } + when :anchor + # used for single-file wiki export + format_wiki_link = Proc.new {|project, title| "##{title}" } + else + format_wiki_link = Proc.new {|project, title| url_for :controller => 'wiki', :action => 'index', :id => project, :page => title } + end + + project = options[:project] || @project + + # turn wiki links into html links + # example: + # [[mypage]] + # [[mypage|mytext]] + # wiki links can refer other project wikis, using project name or identifier: + # [[project:]] -> wiki starting page + # [[project:|mytext]] + # [[project:mypage]] + # [[project:mypage|mytext]] + text = text.gsub(/\[\[([^\]\|]+)(\|([^\]\|]+))?\]\]/) do |m| + link_project = project + page = $1 + title = $3 + if page =~ /^([^\:]+)\:(.*)$/ + link_project = Project.find_by_name($1) || Project.find_by_identifier($1) + page = title || $2 + title = $1 if page.blank? + end + + if link_project && link_project.wiki + # check if page exists + wiki_page = link_project.wiki.find_page(page) + link_to((title || page), format_wiki_link.call(link_project, Wiki.titleize(page)), + :class => ('wiki-page' + (wiki_page ? '' : ' new'))) + else + # project or wiki doesn't exist + title || page + end + end + + # turn issue and revision ids into links + # example: + # #52 -> #52 + # r52 -> r52 (project.id is 6) + text = text.gsub(%r{([\s\(,-^])(#|r)(\d+)(?=[[:punct:]]|\s|<|$)}) do |m| + leading, otype, oid = $1, $2, $3 + link = nil + if otype == 'r' + if project && (changeset = project.changesets.find_by_revision(oid)) + link = link_to("r#{oid}", {:controller => 'repositories', :action => 'revision', :id => project.id, :rev => oid}, :class => 'changeset', + :title => truncate(changeset.comments, 100)) + end + else + if issue = Issue.find_by_id(oid.to_i, :include => [:project, :status], :conditions => Project.visible_by(User.current)) + link = link_to("##{oid}", {:controller => 'issues', :action => 'show', :id => oid}, :class => 'issue', + :title => "#{truncate(issue.subject, 100)} (#{issue.status.name})") + link = content_tag('del', link) if issue.closed? + end + end + leading + (link || "#{otype}#{oid}") + end + + text + end + + # Same as Rails' simple_format helper without using paragraphs + def simple_format_without_paragraph(text) + text.to_s. + gsub(/\r\n?/, "\n"). # \r\n and \r -> \n + gsub(/\n\n+/, "

"). # 2+ newline -> 2 br + gsub(/([^\n]\n)(?=[^\n])/, '\1
') # 1 newline -> br + end + + def error_messages_for(object_name, options = {}) + options = options.symbolize_keys + object = instance_variable_get("@#{object_name}") + if object && !object.errors.empty? + # build full_messages here with controller current language + full_messages = [] + object.errors.each do |attr, msg| + next if msg.nil? + msg = msg.first if msg.is_a? Array + if attr == "base" + full_messages << l(msg) + else + full_messages << "« " + (l_has_string?("field_" + attr) ? l("field_" + attr) : object.class.human_attribute_name(attr)) + " » " + l(msg) unless attr == "custom_values" + end + end + # retrieve custom values error messages + if object.errors[:custom_values] + object.custom_values.each do |v| + v.errors.each do |attr, msg| + next if msg.nil? + msg = msg.first if msg.is_a? Array + full_messages << "« " + v.custom_field.name + " » " + l(msg) + end + end + end + content_tag("div", + content_tag( + options[:header_tag] || "span", lwr(:gui_validation_error, full_messages.length) + ":" + ) + + content_tag("ul", full_messages.collect { |msg| content_tag("li", msg) }), + "id" => options[:id] || "errorExplanation", "class" => options[:class] || "errorExplanation" + ) + else + "" + end + end + + def lang_options_for_select(blank=true) + (blank ? [["(auto)", ""]] : []) + + GLoc.valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last } + end + + def label_tag_for(name, option_tags = nil, options = {}) + label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "") + content_tag("label", label_text) + end + + def labelled_tabular_form_for(name, object, options, &proc) + options[:html] ||= {} + options[:html].store :class, "tabular" + form_for(name, object, options.merge({ :builder => TabularFormBuilder, :lang => current_language}), &proc) + end + + def check_all_links(form_name) + link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") + + " | " + + link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)") + end + + def progress_bar(pcts, options={}) + pcts = [pcts, pcts] unless pcts.is_a?(Array) + pcts[1] = pcts[1] - pcts[0] + pcts << (100 - pcts[1] - pcts[0]) + width = options[:width] || '100px;' + legend = options[:legend] || '' + content_tag('table', + content_tag('tr', + (pcts[0] > 0 ? content_tag('td', '', :width => "#{pcts[0].floor}%;", :class => 'closed') : '') + + (pcts[1] > 0 ? content_tag('td', '', :width => "#{pcts[1].floor}%;", :class => 'done') : '') + + (pcts[2] > 0 ? content_tag('td', '', :width => "#{pcts[2].floor}%;", :class => 'todo') : '') + ), :class => 'progress', :style => "width: #{width};") + + content_tag('p', legend, :class => 'pourcent') + end + + def context_menu_link(name, url, options={}) + options[:class] ||= '' + if options.delete(:selected) + options[:class] << ' icon-checked disabled' + options[:disabled] = true + end + if options.delete(:disabled) + options.delete(:method) + options.delete(:confirm) + options.delete(:onclick) + options[:class] << ' disabled' + url = '#' + end + link_to name, url, options + end + + def calendar_for(field_id) + image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) + + javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });") + end + + def wikitoolbar_for(field_id) + return '' unless Setting.text_formatting == 'textile' + javascript_include_tag('jstoolbar') + javascript_tag("var toolbar = new jsToolBar($('#{field_id}')); toolbar.draw();") + end + + def content_for(name, content = nil, &block) + @has_content ||= {} + @has_content[name] = true + super(name, content, &block) + end + + def has_content?(name) + (@has_content && @has_content[name]) || false + end +end + +class TabularFormBuilder < ActionView::Helpers::FormBuilder + include GLoc + + def initialize(object_name, object, template, options, proc) + set_language_if_valid options.delete(:lang) + @object_name, @object, @template, @options, @proc = object_name, object, template, options, proc + end + + (field_helpers - %w(radio_button hidden_field) + %w(date_select)).each do |selector| + src = <<-END_SRC + def #{selector}(field, options = {}) + return super if options.delete :no_label + label_text = l(options[:label]) if options[:label] + label_text ||= l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + label_text << @template.content_tag("span", " *", :class => "required") if options.delete(:required) + label = @template.content_tag("label", label_text, + :class => (@object && @object.errors[field] ? "error" : nil), + :for => (@object_name.to_s + "_" + field.to_s)) + label + super + end + END_SRC + class_eval src, __FILE__, __LINE__ + end + + def select(field, choices, options = {}, html_options = {}) + label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "") + label = @template.content_tag("label", label_text, + :class => (@object && @object.errors[field] ? "error" : nil), + :for => (@object_name.to_s + "_" + field.to_s)) + label + super + end + +end + diff --git a/rest_sys/app/helpers/attachments_helper.rb b/rest_sys/app/helpers/attachments_helper.rb new file mode 100644 index 000000000..989cd3e66 --- /dev/null +++ b/rest_sys/app/helpers/attachments_helper.rb @@ -0,0 +1,25 @@ +# redMine - project management software +# Copyright (C) 2006-2007 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +module AttachmentsHelper + # displays the links to a collection of attachments + def link_to_attachments(attachments, options = {}) + if attachments.any? + render :partial => 'attachments/links', :locals => {:attachments => attachments, :options => options} + end + end +end diff --git a/rest_sys/app/helpers/auth_sources_helper.rb b/rest_sys/app/helpers/auth_sources_helper.rb new file mode 100644 index 000000000..d47e9856a --- /dev/null +++ b/rest_sys/app/helpers/auth_sources_helper.rb @@ -0,0 +1,19 @@ +# redMine - project management software +# Copyright (C) 2006 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +module AuthSourcesHelper +end diff --git a/rest_sys/app/helpers/boards_helper.rb b/rest_sys/app/helpers/boards_helper.rb new file mode 100644 index 000000000..3719e0fe8 --- /dev/null +++ b/rest_sys/app/helpers/boards_helper.rb @@ -0,0 +1,19 @@ +# redMine - project management software +# Copyright (C) 2006-2007 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +module BoardsHelper +end diff --git a/rest_sys/app/helpers/custom_fields_helper.rb b/rest_sys/app/helpers/custom_fields_helper.rb new file mode 100644 index 000000000..b1b176107 --- /dev/null +++ b/rest_sys/app/helpers/custom_fields_helper.rb @@ -0,0 +1,77 @@ +# redMine - project management software +# Copyright (C) 2006 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +module CustomFieldsHelper + + # Return custom field html tag corresponding to its format + def custom_field_tag(custom_value) + custom_field = custom_value.custom_field + field_name = "custom_fields[#{custom_field.id}]" + field_id = "custom_fields_#{custom_field.id}" + + case custom_field.field_format + when "date" + text_field('custom_value', 'value', :name => field_name, :id => field_id, :size => 10) + + calendar_for(field_id) + when "text" + text_area 'custom_value', 'value', :name => field_name, :id => field_id, :cols => 60, :rows => 3 + when "bool" + check_box 'custom_value', 'value', :name => field_name, :id => field_id + when "list" + select 'custom_value', 'value', custom_field.possible_values, { :include_blank => true }, :name => field_name, :id => field_id + else + text_field 'custom_value', 'value', :name => field_name, :id => field_id + end + end + + # Return custom field label tag + def custom_field_label_tag(custom_value) + content_tag "label", custom_value.custom_field.name + + (custom_value.custom_field.is_required? ? " *" : ""), + :for => "custom_fields_#{custom_value.custom_field.id}", + :class => (custom_value.errors.empty? ? nil : "error" ) + end + + # Return custom field tag with its label tag + def custom_field_tag_with_label(custom_value) + custom_field_label_tag(custom_value) + custom_field_tag(custom_value) + end + + # Return a string used to display a custom value + def show_value(custom_value) + return "" unless custom_value + format_value(custom_value.value, custom_value.custom_field.field_format) + end + + # Return a string used to display a custom value + def format_value(value, field_format) + return "" unless value && !value.empty? + case field_format + when "date" + begin; l_date(value.to_date); rescue; value end + when "bool" + l_YesNo(value == "1") + else + value + end + end + + # Return an array of custom field formats which can be used in select_tag + def custom_field_formats_for_select + CustomField::FIELD_FORMATS.sort {|a,b| a[1][:order]<=>b[1][:order]}.collect { |k| [ l(k[1][:name]), k[0] ] } + end +end diff --git a/rest_sys/app/helpers/documents_helper.rb b/rest_sys/app/helpers/documents_helper.rb new file mode 100644 index 000000000..7e96a6db3 --- /dev/null +++ b/rest_sys/app/helpers/documents_helper.rb @@ -0,0 +1,19 @@ +# redMine - project management software +# Copyright (C) 2006 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +module DocumentsHelper +end diff --git a/rest_sys/app/helpers/enumerations_helper.rb b/rest_sys/app/helpers/enumerations_helper.rb new file mode 100644 index 000000000..c0daf64d2 --- /dev/null +++ b/rest_sys/app/helpers/enumerations_helper.rb @@ -0,0 +1,19 @@ +# redMine - project management software +# Copyright (C) 2006 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +module EnumerationsHelper +end diff --git a/rest_sys/app/helpers/ifpdf_helper.rb b/rest_sys/app/helpers/ifpdf_helper.rb new file mode 100644 index 000000000..585dbeeca --- /dev/null +++ b/rest_sys/app/helpers/ifpdf_helper.rb @@ -0,0 +1,80 @@ +# redMine - project management software +# Copyright (C) 2006 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require 'iconv' +require 'rfpdf/chinese' + +module IfpdfHelper + + class IFPDF < FPDF + include GLoc + attr_accessor :footer_date + + def initialize(lang) + super() + set_language_if_valid lang + case current_language + when :ja + extend(PDF_Japanese) + AddSJISFont() + @font_for_content = 'SJIS' + @font_for_footer = 'SJIS' + when :zh + extend(PDF_Chinese) + AddBig5Font() + @font_for_content = 'Big5' + @font_for_footer = 'Big5' + else + @font_for_content = 'Arial' + @font_for_footer = 'Helvetica' + end + SetCreator("redMine #{Redmine::VERSION}") + SetFont(@font_for_content) + end + + def SetFontStyle(style, size) + SetFont(@font_for_content, style, size) + end + + def Cell(w,h=0,txt='',border=0,ln=0,align='',fill=0,link='') + @ic ||= Iconv.new(l(:general_pdf_encoding), 'UTF-8') + # these quotation marks are not correctly rendered in the pdf + txt = txt.gsub(/[“â€]/, '"') if txt + txt = begin + # 0x5c char handling + txtar = txt.split('\\') + txtar << '' if txt[-1] == ?\\ + txtar.collect {|x| @ic.iconv(x)}.join('\\').gsub(/\\/, "\\\\\\\\") + rescue + txt + end || '' + super w,h,txt,border,ln,align,fill,link + end + + def Footer + SetFont(@font_for_footer, 'I', 8) + SetY(-15) + SetX(15) + Cell(0, 5, @footer_date, 0, 0, 'L') + SetY(-15) + SetX(-30) + Cell(0, 5, PageNo().to_s + '/{nb}', 0, 0, 'C') + end + + end + +end diff --git a/rest_sys/app/helpers/issue_categories_helper.rb b/rest_sys/app/helpers/issue_categories_helper.rb new file mode 100644 index 000000000..0109e7fae --- /dev/null +++ b/rest_sys/app/helpers/issue_categories_helper.rb @@ -0,0 +1,19 @@ +# redMine - project management software +# Copyright (C) 2006 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +module IssueCategoriesHelper +end diff --git a/rest_sys/app/helpers/issue_relations_helper.rb b/rest_sys/app/helpers/issue_relations_helper.rb new file mode 100644 index 000000000..377059d5f --- /dev/null +++ b/rest_sys/app/helpers/issue_relations_helper.rb @@ -0,0 +1,23 @@ +# redMine - project management software +# Copyright (C) 2006-2007 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +module IssueRelationsHelper + def collection_for_relation_type_select + values = IssueRelation::TYPES + values.keys.sort{|x,y| values[x][:order] <=> values[y][:order]}.collect{|k| [l(values[k][:name]), k]} + end +end diff --git a/rest_sys/app/helpers/issue_statuses_helper.rb b/rest_sys/app/helpers/issue_statuses_helper.rb new file mode 100644 index 000000000..859b09911 --- /dev/null +++ b/rest_sys/app/helpers/issue_statuses_helper.rb @@ -0,0 +1,19 @@ +# redMine - project management software +# Copyright (C) 2006 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +module IssueStatusesHelper +end diff --git a/rest_sys/app/helpers/issues_helper.rb b/rest_sys/app/helpers/issues_helper.rb new file mode 100644 index 000000000..197760b53 --- /dev/null +++ b/rest_sys/app/helpers/issues_helper.rb @@ -0,0 +1,159 @@ +# redMine - project management software +# Copyright (C) 2006 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require 'csv' + +module IssuesHelper + include ApplicationHelper + + def render_issue_tooltip(issue) + @cached_label_start_date ||= l(:field_start_date) + @cached_label_due_date ||= l(:field_due_date) + @cached_label_assigned_to ||= l(:field_assigned_to) + @cached_label_priority ||= l(:field_priority) + + link_to_issue(issue) + ": #{h(issue.subject)}

" + + "#{@cached_label_start_date}: #{format_date(issue.start_date)}
" + + "#{@cached_label_due_date}: #{format_date(issue.due_date)}
" + + "#{@cached_label_assigned_to}: #{issue.assigned_to}
" + + "#{@cached_label_priority}: #{issue.priority.name}" + end + + def show_detail(detail, no_html=false) + case detail.property + when 'attr' + label = l(("field_" + detail.prop_key.to_s.gsub(/\_id$/, "")).to_sym) + case detail.prop_key + when 'due_date', 'start_date' + value = format_date(detail.value.to_date) if detail.value + old_value = format_date(detail.old_value.to_date) if detail.old_value + when 'status_id' + s = IssueStatus.find_by_id(detail.value) and value = s.name if detail.value + s = IssueStatus.find_by_id(detail.old_value) and old_value = s.name if detail.old_value + when 'assigned_to_id' + u = User.find_by_id(detail.value) and value = u.name if detail.value + u = User.find_by_id(detail.old_value) and old_value = u.name if detail.old_value + when 'priority_id' + e = Enumeration.find_by_id(detail.value) and value = e.name if detail.value + e = Enumeration.find_by_id(detail.old_value) and old_value = e.name if detail.old_value + when 'category_id' + c = IssueCategory.find_by_id(detail.value) and value = c.name if detail.value + c = IssueCategory.find_by_id(detail.old_value) and old_value = c.name if detail.old_value + when 'fixed_version_id' + v = Version.find_by_id(detail.value) and value = v.name if detail.value + v = Version.find_by_id(detail.old_value) and old_value = v.name if detail.old_value + end + when 'cf' + custom_field = CustomField.find_by_id(detail.prop_key) + if custom_field + label = custom_field.name + value = format_value(detail.value, custom_field.field_format) if detail.value + old_value = format_value(detail.old_value, custom_field.field_format) if detail.old_value + end + when 'attachment' + label = l(:label_attachment) + end + + label ||= detail.prop_key + value ||= detail.value + old_value ||= detail.old_value + + unless no_html + label = content_tag('strong', label) + old_value = content_tag("i", h(old_value)) if detail.old_value + old_value = content_tag("strike", old_value) if detail.old_value and (!detail.value or detail.value.empty?) + if detail.property == 'attachment' && !value.blank? && Attachment.find_by_id(detail.prop_key) + # Link to the attachment if it has not been removed + value = link_to(value, :controller => 'attachments', :action => 'download', :id => detail.prop_key) + else + value = content_tag("i", h(value)) if value + end + end + + if !detail.value.blank? + case detail.property + when 'attr', 'cf' + if !detail.old_value.blank? + label + " " + l(:text_journal_changed, old_value, value) + else + label + " " + l(:text_journal_set_to, value) + end + when 'attachment' + "#{label} #{value} #{l(:label_added)}" + end + else + case detail.property + when 'attr', 'cf' + label + " " + l(:text_journal_deleted) + " (#{old_value})" + when 'attachment' + "#{label} #{old_value} #{l(:label_deleted)}" + end + end + end + + def issues_to_csv(issues, project = nil) + ic = Iconv.new(l(:general_csv_encoding), 'UTF-8') + export = StringIO.new + CSV::Writer.generate(export, l(:general_csv_separator)) do |csv| + # csv header fields + headers = [ "#", + l(:field_status), + l(:field_project), + l(:field_tracker), + l(:field_priority), + l(:field_subject), + l(:field_assigned_to), + l(:field_category), + l(:field_fixed_version), + l(:field_author), + l(:field_start_date), + l(:field_due_date), + l(:field_done_ratio), + l(:field_created_on), + l(:field_updated_on) + ] + # Export project custom fields if project is given + # otherwise export custom fields marked as "For all projects" + custom_fields = project.nil? ? IssueCustomField.for_all : project.all_custom_fields + custom_fields.each {|f| headers << f.name} + csv << headers.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end } + # csv lines + issues.each do |issue| + fields = [issue.id, + issue.status.name, + issue.project.name, + issue.tracker.name, + issue.priority.name, + issue.subject, + issue.assigned_to, + issue.category, + issue.fixed_version, + issue.author.name, + format_date(issue.start_date), + format_date(issue.due_date), + issue.done_ratio, + format_time(issue.created_on), + format_time(issue.updated_on) + ] + custom_fields.each {|f| fields << show_value(issue.custom_value_for(f)) } + csv << fields.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end } + end + end + export.rewind + export + end +end diff --git a/rest_sys/app/helpers/members_helper.rb b/rest_sys/app/helpers/members_helper.rb new file mode 100644 index 000000000..fcf9c92e6 --- /dev/null +++ b/rest_sys/app/helpers/members_helper.rb @@ -0,0 +1,19 @@ +# redMine - project management software +# Copyright (C) 2006 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +module MembersHelper +end diff --git a/rest_sys/app/helpers/messages_helper.rb b/rest_sys/app/helpers/messages_helper.rb new file mode 100644 index 000000000..bf23275c3 --- /dev/null +++ b/rest_sys/app/helpers/messages_helper.rb @@ -0,0 +1,28 @@ +# redMine - project management software +# Copyright (C) 2006-2007 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +module MessagesHelper + + def link_to_message(message) + return '' unless message + link_to h(truncate(message.subject, 60)), :controller => 'messages', + :action => 'show', + :board_id => message.board_id, + :id => message.root, + :anchor => (message.parent_id ? "message-#{message.id}" : nil) + end +end diff --git a/rest_sys/app/helpers/my_helper.rb b/rest_sys/app/helpers/my_helper.rb new file mode 100644 index 000000000..9098f67bc --- /dev/null +++ b/rest_sys/app/helpers/my_helper.rb @@ -0,0 +1,19 @@ +# redMine - project management software +# Copyright (C) 2006 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +module MyHelper +end diff --git a/rest_sys/app/helpers/news_helper.rb b/rest_sys/app/helpers/news_helper.rb new file mode 100644 index 000000000..28d36f31a --- /dev/null +++ b/rest_sys/app/helpers/news_helper.rb @@ -0,0 +1,19 @@ +# redMine - project management software +# Copyright (C) 2006 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +module NewsHelper +end diff --git a/rest_sys/app/helpers/projects_helper.rb b/rest_sys/app/helpers/projects_helper.rb new file mode 100644 index 000000000..df4b9c334 --- /dev/null +++ b/rest_sys/app/helpers/projects_helper.rb @@ -0,0 +1,199 @@ +# redMine - project management software +# Copyright (C) 2006 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +module ProjectsHelper + def link_to_version(version, options = {}) + return '' unless version && version.is_a?(Version) + link_to version.name, {:controller => 'projects', + :action => 'roadmap', + :id => version.project_id, + :completed => (version.completed? ? 1 : nil), + :anchor => version.name + }, options + end + + def project_settings_tabs + tabs = [{:name => 'info', :action => :edit_project, :partial => 'projects/edit', :label => :label_information_plural}, + {:name => 'modules', :action => :select_project_modules, :partial => 'projects/settings/modules', :label => :label_module_plural}, + {:name => 'members', :action => :manage_members, :partial => 'projects/settings/members', :label => :label_member_plural}, + {:name => 'versions', :action => :manage_versions, :partial => 'projects/settings/versions', :label => :label_version_plural}, + {:name => 'categories', :action => :manage_categories, :partial => 'projects/settings/issue_categories', :label => :label_issue_category_plural}, + {:name => 'wiki', :action => :manage_wiki, :partial => 'projects/settings/wiki', :label => :label_wiki}, + {:name => 'repository', :action => :manage_repository, :partial => 'projects/settings/repository', :label => :label_repository}, + {:name => 'boards', :action => :manage_boards, :partial => 'projects/settings/boards', :label => :label_board_plural} + ] + tabs.select {|tab| User.current.allowed_to?(tab[:action], @project)} + end + + # Generates a gantt image + # Only defined if RMagick is avalaible + def gantt_image(events, date_from, months, zoom) + date_to = (date_from >> months)-1 + show_weeks = zoom > 1 + show_days = zoom > 2 + + subject_width = 320 + header_heigth = 18 + # width of one day in pixels + zoom = zoom*2 + g_width = (date_to - date_from + 1)*zoom + g_height = 20 * events.length + 20 + headers_heigth = (show_weeks ? 2*header_heigth : header_heigth) + height = g_height + headers_heigth + + imgl = Magick::ImageList.new + imgl.new_image(subject_width+g_width+1, height) + gc = Magick::Draw.new + + # Subjects + top = headers_heigth + 20 + gc.fill('black') + gc.stroke('transparent') + gc.stroke_width(1) + events.each do |i| + gc.text(4, top + 2, (i.is_a?(Issue) ? i.subject : i.name)) + top = top + 20 + end + + # Months headers + month_f = date_from + left = subject_width + months.times do + width = ((month_f >> 1) - month_f) * zoom + gc.fill('white') + gc.stroke('grey') + gc.stroke_width(1) + gc.rectangle(left, 0, left + width, height) + gc.fill('black') + gc.stroke('transparent') + gc.stroke_width(1) + gc.text(left.round + 8, 14, "#{month_f.year}-#{month_f.month}") + left = left + width + month_f = month_f >> 1 + end + + # Weeks headers + if show_weeks + left = subject_width + height = header_heigth + if date_from.cwday == 1 + # date_from is monday + week_f = date_from + else + # find next monday after date_from + week_f = date_from + (7 - date_from.cwday + 1) + width = (7 - date_from.cwday + 1) * zoom + gc.fill('white') + gc.stroke('grey') + gc.stroke_width(1) + gc.rectangle(left, header_heigth, left + width, 2*header_heigth + g_height-1) + left = left + width + end + while week_f <= date_to + width = (week_f + 6 <= date_to) ? 7 * zoom : (date_to - week_f + 1) * zoom + gc.fill('white') + gc.stroke('grey') + gc.stroke_width(1) + gc.rectangle(left.round, header_heigth, left.round + width, 2*header_heigth + g_height-1) + gc.fill('black') + gc.stroke('transparent') + gc.stroke_width(1) + gc.text(left.round + 2, header_heigth + 14, week_f.cweek.to_s) + left = left + width + week_f = week_f+7 + end + end + + # Days details (week-end in grey) + if show_days + left = subject_width + height = g_height + header_heigth - 1 + wday = date_from.cwday + (date_to - date_from + 1).to_i.times do + width = zoom + gc.fill(wday == 6 || wday == 7 ? '#eee' : 'white') + gc.stroke('grey') + gc.stroke_width(1) + gc.rectangle(left, 2*header_heigth, left + width, 2*header_heigth + g_height-1) + left = left + width + wday = wday + 1 + wday = 1 if wday > 7 + end + end + + # border + gc.fill('transparent') + gc.stroke('grey') + gc.stroke_width(1) + gc.rectangle(0, 0, subject_width+g_width, headers_heigth) + gc.stroke('black') + gc.rectangle(0, 0, subject_width+g_width, g_height+ headers_heigth-1) + + # content + top = headers_heigth + 20 + gc.stroke('transparent') + events.each do |i| + if i.is_a?(Issue) + i_start_date = (i.start_date >= date_from ? i.start_date : date_from ) + i_end_date = (i.due_date <= date_to ? i.due_date : date_to ) + i_done_date = i.start_date + ((i.due_date - i.start_date+1)*i.done_ratio/100).floor + i_done_date = (i_done_date <= date_from ? date_from : i_done_date ) + i_done_date = (i_done_date >= date_to ? date_to : i_done_date ) + i_late_date = [i_end_date, Date.today].min if i_start_date < Date.today + + i_left = subject_width + ((i_start_date - date_from)*zoom).floor + i_width = ((i_end_date - i_start_date + 1)*zoom).floor # total width of the issue + d_width = ((i_done_date - i_start_date)*zoom).floor # done width + l_width = i_late_date ? ((i_late_date - i_start_date+1)*zoom).floor : 0 # delay width + + gc.fill('grey') + gc.rectangle(i_left, top, i_left + i_width, top - 6) + gc.fill('red') + gc.rectangle(i_left, top, i_left + l_width, top - 6) if l_width > 0 + gc.fill('blue') + gc.rectangle(i_left, top, i_left + d_width, top - 6) if d_width > 0 + gc.fill('black') + gc.text(i_left + i_width + 5,top + 1, "#{i.status.name} #{i.done_ratio}%") + else + i_left = subject_width + ((i.start_date - date_from)*zoom).floor + gc.fill('green') + gc.rectangle(i_left, top, i_left + 6, top - 6) + gc.fill('black') + gc.text(i_left + 11, top + 1, i.name) + end + top = top + 20 + end + + # today red line + if Date.today >= date_from and Date.today <= date_to + gc.stroke('red') + x = (Date.today-date_from+1)*zoom + subject_width + gc.line(x, headers_heigth, x, headers_heigth + g_height-1) + end + + gc.draw(imgl) + imgl + end if Object.const_defined?(:Magick) + + def new_issue_selector + trackers = @project.trackers + # can't use form tag inside helper + content_tag('form', + select_tag('tracker_id', '' + options_from_collection_for_select(trackers, 'id', 'name'), :onchange => "if (this.value != '') {this.form.submit()}"), + :action => url_for(:controller => 'projects', :action => 'add_issue', :id => @project), :method => 'get') + end +end diff --git a/rest_sys/app/helpers/queries_helper.rb b/rest_sys/app/helpers/queries_helper.rb new file mode 100644 index 000000000..3011d3aec --- /dev/null +++ b/rest_sys/app/helpers/queries_helper.rb @@ -0,0 +1,51 @@ +# redMine - project management software +# Copyright (C) 2006-2007 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +module QueriesHelper + + def operators_for_select(filter_type) + Query.operators_by_filter_type[filter_type].collect {|o| [l(Query.operators[o]), o]} + end + + def column_header(column) + column.sortable ? sort_header_tag(column.sortable, :caption => column.caption) : content_tag('th', column.caption) + end + + def column_content(column, issue) + if column.is_a?(QueryCustomFieldColumn) + cv = issue.custom_values.detect {|v| v.custom_field_id == column.custom_field.id} + show_value(cv) + else + value = issue.send(column.name) + if value.is_a?(Date) + format_date(value) + elsif value.is_a?(Time) + format_time(value) + else + case column.name + when :subject + h((@project.nil? || @project != issue.project) ? "#{issue.project.name} - " : '') + + link_to(h(value), :controller => 'issues', :action => 'show', :id => issue) + when :done_ratio + progress_bar(value, :width => '80px') + else + h(value) + end + end + end + end +end diff --git a/rest_sys/app/helpers/reports_helper.rb b/rest_sys/app/helpers/reports_helper.rb new file mode 100644 index 000000000..c733a0634 --- /dev/null +++ b/rest_sys/app/helpers/reports_helper.rb @@ -0,0 +1,36 @@ +# redMine - project management software +# Copyright (C) 2006 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +module ReportsHelper + + def aggregate(data, criteria) + a = 0 + data.each { |row| + match = 1 + criteria.each { |k, v| + match = 0 unless (row[k].to_s == v.to_s) || (k == 'closed' && row[k] == (v == 0 ? "f" : "t")) + } unless criteria.nil? + a = a + row["total"].to_i if match == 1 + } unless data.nil? + a + end + + def aggregate_link(data, criteria, *args) + a = aggregate data, criteria + a > 0 ? link_to(a, *args) : '-' + end +end diff --git a/rest_sys/app/helpers/repositories_helper.rb b/rest_sys/app/helpers/repositories_helper.rb new file mode 100644 index 000000000..d2d04604d --- /dev/null +++ b/rest_sys/app/helpers/repositories_helper.rb @@ -0,0 +1,87 @@ +# redMine - project management software +# Copyright (C) 2006 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require 'coderay' +require 'coderay/helpers/file_type' +require 'iconv' + +module RepositoriesHelper + def syntax_highlight(name, content) + type = CodeRay::FileType[name] + type ? CodeRay.scan(content, type).html : h(content) + end + + def to_utf8(str) + return str if /\A[\r\n\t\x20-\x7e]*\Z/n.match(str) # for us-ascii + @encodings ||= Setting.repositories_encodings.split(',').collect(&:strip) + @encodings.each do |encoding| + begin + return Iconv.conv('UTF-8', encoding, str) + rescue Iconv::Failure + # do nothing here and try the next encoding + end + end + str + end + + def repository_field_tags(form, repository) + method = repository.class.name.demodulize.underscore + "_field_tags" + send(method, form, repository) if repository.is_a?(Repository) && respond_to?(method) + end + + def scm_select_tag(repository) + container = [[]] + REDMINE_SUPPORTED_SCM.each {|scm| container << ["Repository::#{scm}".constantize.scm_name, scm]} + select_tag('repository_scm', + options_for_select(container, repository.class.name.demodulize), + :disabled => (repository && !repository.new_record?), + :onchange => remote_function(:url => { :controller => 'repositories', :action => 'edit', :id => @project }, :method => :get, :with => "Form.serialize(this.form)") + ) + end + + def with_leading_slash(path) + path ||= '' + path.starts_with?("/") ? "/#{path}" : path + end + + def subversion_field_tags(form, repository) + content_tag('p', form.text_field(:url, :size => 60, :required => true, :disabled => (repository && !repository.root_url.blank?)) + + '
(http://, https://, svn://, file:///)') + + content_tag('p', form.text_field(:login, :size => 30)) + + content_tag('p', form.password_field(:password, :size => 30, :name => 'ignore', + :value => ((repository.new_record? || repository.password.blank?) ? '' : ('x'*15)), + :onfocus => "this.value=''; this.name='repository[password]';", + :onchange => "this.name='repository[password]';")) + end + + def darcs_field_tags(form, repository) + content_tag('p', form.text_field(:url, :label => 'Root directory', :size => 60, :required => true, :disabled => (repository && !repository.new_record?))) + end + + def mercurial_field_tags(form, repository) + content_tag('p', form.text_field(:url, :label => 'Root directory', :size => 60, :required => true, :disabled => (repository && !repository.root_url.blank?))) + end + + def cvs_field_tags(form, repository) + content_tag('p', form.text_field(:root_url, :label => 'CVSROOT', :size => 60, :required => true, :disabled => !repository.new_record?)) + + content_tag('p', form.text_field(:url, :label => 'Module', :size => 30, :required => true, :disabled => !repository.new_record?)) + end + + def bazaar_field_tags(form, repository) + content_tag('p', form.text_field(:url, :label => 'Root directory', :size => 60, :required => true, :disabled => (repository && !repository.new_record?))) + end +end diff --git a/rest_sys/app/helpers/roles_helper.rb b/rest_sys/app/helpers/roles_helper.rb new file mode 100644 index 000000000..ab3a7ff03 --- /dev/null +++ b/rest_sys/app/helpers/roles_helper.rb @@ -0,0 +1,19 @@ +# redMine - project management software +# Copyright (C) 2006 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +module RolesHelper +end diff --git a/rest_sys/app/helpers/search_helper.rb b/rest_sys/app/helpers/search_helper.rb new file mode 100644 index 000000000..ed2f40b69 --- /dev/null +++ b/rest_sys/app/helpers/search_helper.rb @@ -0,0 +1,38 @@ +# redMine - project management software +# Copyright (C) 2006-2007 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +module SearchHelper + def highlight_tokens(text, tokens) + return text unless text && tokens && !tokens.empty? + regexp = Regexp.new "(#{tokens.join('|')})", Regexp::IGNORECASE + result = '' + text.split(regexp).each_with_index do |words, i| + if result.length > 1200 + # maximum length of the preview reached + result << '...' + break + end + if i.even? + result << h(words.length > 100 ? "#{words[0..44]} ... #{words[-45..-1]}" : words) + else + t = (tokens.index(words.downcase) || 0) % 4 + result << content_tag('span', h(words), :class => "highlight token-#{t}") + end + end + result + end +end diff --git a/rest_sys/app/helpers/settings_helper.rb b/rest_sys/app/helpers/settings_helper.rb new file mode 100644 index 000000000..f53314c40 --- /dev/null +++ b/rest_sys/app/helpers/settings_helper.rb @@ -0,0 +1,19 @@ +# redMine - project management software +# Copyright (C) 2006-2007 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +module SettingsHelper +end diff --git a/rest_sys/app/helpers/sort_helper.rb b/rest_sys/app/helpers/sort_helper.rb new file mode 100644 index 000000000..dfd681fff --- /dev/null +++ b/rest_sys/app/helpers/sort_helper.rb @@ -0,0 +1,157 @@ +# Helpers to sort tables using clickable column headers. +# +# Author: Stuart Rackham , March 2005. +# License: This source code is released under the MIT license. +# +# - Consecutive clicks toggle the column's sort order. +# - Sort state is maintained by a session hash entry. +# - Icon image identifies sort column and state. +# - Typically used in conjunction with the Pagination module. +# +# Example code snippets: +# +# Controller: +# +# helper :sort +# include SortHelper +# +# def list +# sort_init 'last_name' +# sort_update +# @items = Contact.find_all nil, sort_clause +# end +# +# Controller (using Pagination module): +# +# helper :sort +# include SortHelper +# +# def list +# sort_init 'last_name' +# sort_update +# @contact_pages, @items = paginate :contacts, +# :order_by => sort_clause, +# :per_page => 10 +# end +# +# View (table header in list.rhtml): +# +# +# +# <%= sort_header_tag('id', :title => 'Sort by contact ID') %> +# <%= sort_header_tag('last_name', :caption => 'Name') %> +# <%= sort_header_tag('phone') %> +# <%= sort_header_tag('address', :width => 200) %> +# +# +# +# - The ascending and descending sort icon images are sort_asc.png and +# sort_desc.png and reside in the application's images directory. +# - Introduces instance variables: @sort_name, @sort_default. +# - Introduces params :sort_key and :sort_order. +# +module SortHelper + + # Initializes the default sort column (default_key) and sort order + # (default_order). + # + # - default_key is a column attribute name. + # - default_order is 'asc' or 'desc'. + # - name is the name of the session hash entry that stores the sort state, + # defaults to '_sort'. + # + def sort_init(default_key, default_order='asc', name=nil) + @sort_name = name || params[:controller] + params[:action] + '_sort' + @sort_default = {:key => default_key, :order => default_order} + end + + # Updates the sort state. Call this in the controller prior to calling + # sort_clause. + # + def sort_update() + if params[:sort_key] + sort = {:key => params[:sort_key], :order => params[:sort_order]} + elsif session[@sort_name] + sort = session[@sort_name] # Previous sort. + else + sort = @sort_default + end + session[@sort_name] = sort + end + + # Returns an SQL sort clause corresponding to the current sort state. + # Use this to sort the controller's table items collection. + # + def sort_clause() + session[@sort_name][:key] + ' ' + session[@sort_name][:order] + end + + # Returns a link which sorts by the named column. + # + # - column is the name of an attribute in the sorted record collection. + # - The optional caption explicitly specifies the displayed link text. + # - A sort icon image is positioned to the right of the sort link. + # + def sort_link(column, caption=nil) + key, order = session[@sort_name][:key], session[@sort_name][:order] + if key == column + if order.downcase == 'asc' + icon = 'sort_asc.png' + order = 'desc' + else + icon = 'sort_desc.png' + order = 'asc' + end + else + icon = nil + order = 'desc' # changed for desc order by default + end + caption = titleize(Inflector::humanize(column)) unless caption + + url = {:sort_key => column, :sort_order => order, :issue_id => params[:issue_id], :project_id => params[:project_id]} + + link_to_remote(caption, + {:update => "content", :url => url}, + {:href => url_for(url)}) + + (icon ? nbsp(2) + image_tag(icon) : '') + end + + # Returns a table header tag with a sort link for the named column + # attribute. + # + # Options: + # :caption The displayed link name (defaults to titleized column name). + # :title The tag's 'title' attribute (defaults to 'Sort by :caption'). + # + # Other options hash entries generate additional table header tag attributes. + # + # Example: + # + # <%= sort_header_tag('id', :title => 'Sort by contact ID', :width => 40) %> + # + # Renders: + # + # + # Id + #   Sort_asc + # + # + def sort_header_tag(column, options = {}) + caption = options.delete(:caption) || titleize(Inflector::humanize(column)) + options[:title]= l(:label_sort_by, "\"#{caption}\"") unless options[:title] + content_tag('th', sort_link(column, caption), options) + end + + private + + # Return n non-breaking spaces. + def nbsp(n) + ' ' * n + end + + # Return capitalized title. + def titleize(title) + title.split.map {|w| w.capitalize }.join(' ') + end + +end diff --git a/rest_sys/app/helpers/timelog_helper.rb b/rest_sys/app/helpers/timelog_helper.rb new file mode 100644 index 000000000..22e4eba0b --- /dev/null +++ b/rest_sys/app/helpers/timelog_helper.rb @@ -0,0 +1,30 @@ +# redMine - project management software +# Copyright (C) 2006 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +module TimelogHelper + def select_hours(data, criteria, value) + data.select {|row| row[criteria] == value.to_s} + end + + def sum_hours(data) + sum = 0 + data.each do |row| + sum += row['hours'].to_f + end + sum + end +end diff --git a/rest_sys/app/helpers/trackers_helper.rb b/rest_sys/app/helpers/trackers_helper.rb new file mode 100644 index 000000000..89f92e333 --- /dev/null +++ b/rest_sys/app/helpers/trackers_helper.rb @@ -0,0 +1,19 @@ +# redMine - project management software +# Copyright (C) 2006 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +module TrackersHelper +end diff --git a/rest_sys/app/helpers/users_helper.rb b/rest_sys/app/helpers/users_helper.rb new file mode 100644 index 000000000..9dc87c5cc --- /dev/null +++ b/rest_sys/app/helpers/users_helper.rb @@ -0,0 +1,25 @@ +# redMine - project management software +# Copyright (C) 2006 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +module UsersHelper + def status_options_for_select(selected) + options_for_select([[l(:label_all), "*"], + [l(:status_active), 1], + [l(:status_registered), 2], + [l(:status_locked), 3]], selected) + end +end diff --git a/rest_sys/app/helpers/versions_helper.rb b/rest_sys/app/helpers/versions_helper.rb new file mode 100644 index 000000000..0fcc6407c --- /dev/null +++ b/rest_sys/app/helpers/versions_helper.rb @@ -0,0 +1,47 @@ +# redMine - project management software +# Copyright (C) 2006 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +module VersionsHelper + + STATUS_BY_CRITERIAS = %w(category tracker priority author assigned_to) + + def render_issue_status_by(version, criteria) + criteria ||= 'category' + raise 'Unknown criteria' unless STATUS_BY_CRITERIAS.include?(criteria) + + h = Hash.new {|k,v| k[v] = [0, 0]} + begin + # Total issue count + Issue.count(:group => criteria, + :conditions => ["#{Issue.table_name}.fixed_version_id = ?", version.id]).each {|c,s| h[c][0] = s} + # Open issues count + Issue.count(:group => criteria, + :include => :status, + :conditions => ["#{Issue.table_name}.fixed_version_id = ? AND #{IssueStatus.table_name}.is_closed = ?", version.id, false]).each {|c,s| h[c][1] = s} + rescue ActiveRecord::RecordNotFound + # When grouping by an association, Rails throws this exception if there's no result (bug) + end + counts = h.keys.compact.sort.collect {|k| {:group => k, :total => h[k][0], :open => h[k][1], :closed => (h[k][0] - h[k][1])}} + max = counts.collect {|c| c[:total]}.max + + render :partial => 'issue_counts', :locals => {:version => version, :criteria => criteria, :counts => counts, :max => max} + end + + def status_by_options_for_select(value) + options_for_select(STATUS_BY_CRITERIAS.collect {|criteria| [l("field_#{criteria}".to_sym), criteria]}, value) + end +end diff --git a/rest_sys/app/helpers/watchers_helper.rb b/rest_sys/app/helpers/watchers_helper.rb new file mode 100644 index 000000000..c83c785fc --- /dev/null +++ b/rest_sys/app/helpers/watchers_helper.rb @@ -0,0 +1,36 @@ +# redMine - project management software +# Copyright (C) 2006-2007 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +module WatchersHelper + def watcher_tag(object, user) + content_tag("span", watcher_link(object, user), :id => 'watcher') + end + + def watcher_link(object, user) + return '' unless user && user.logged? && object.respond_to?('watched_by?') + watched = object.watched_by?(user) + url = {:controller => 'watchers', + :action => (watched ? 'remove' : 'add'), + :object_type => object.class.to_s.underscore, + :object_id => object.id} + link_to_remote((watched ? l(:button_unwatch) : l(:button_watch)), + {:url => url}, + :href => url_for(url), + :class => (watched ? 'icon icon-fav' : 'icon icon-fav-off')) + + end +end diff --git a/rest_sys/app/helpers/welcome_helper.rb b/rest_sys/app/helpers/welcome_helper.rb new file mode 100644 index 000000000..753e1f127 --- /dev/null +++ b/rest_sys/app/helpers/welcome_helper.rb @@ -0,0 +1,19 @@ +# redMine - project management software +# Copyright (C) 2006 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +module WelcomeHelper +end diff --git a/rest_sys/app/helpers/wiki_helper.rb b/rest_sys/app/helpers/wiki_helper.rb new file mode 100644 index 000000000..980035bd4 --- /dev/null +++ b/rest_sys/app/helpers/wiki_helper.rb @@ -0,0 +1,56 @@ +# redMine - project management software +# Copyright (C) 2006-2007 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +module WikiHelper + + def html_diff(wdiff) + words = wdiff.words.collect{|word| h(word)} + words_add = 0 + words_del = 0 + dels = 0 + del_off = 0 + wdiff.diff.diffs.each do |diff| + add_at = nil + add_to = nil + del_at = nil + deleted = "" + diff.each do |change| + pos = change[1] + if change[0] == "+" + add_at = pos + dels unless add_at + add_to = pos + dels + words_add += 1 + else + del_at = pos unless del_at + deleted << ' ' + change[2] + words_del += 1 + end + end + if add_at + words[add_at] = '' + words[add_at] + words[add_to] = words[add_to] + '' + end + if del_at + words.insert del_at - del_off + dels + words_add, '' + deleted + '' + dels += 1 + del_off += words_del + words_del = 0 + end + end + simple_format_without_paragraph(words.join(' ')) + end +end diff --git a/rest_sys/app/models/attachment.rb b/rest_sys/app/models/attachment.rb new file mode 100644 index 000000000..927aa1735 --- /dev/null +++ b/rest_sys/app/models/attachment.rb @@ -0,0 +1,105 @@ +# redMine - project management software +# Copyright (C) 2006-2007 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require "digest/md5" + +class Attachment < ActiveRecord::Base + belongs_to :container, :polymorphic => true + belongs_to :author, :class_name => "User", :foreign_key => "author_id" + + validates_presence_of :container, :filename, :author + validates_length_of :filename, :maximum => 255 + validates_length_of :disk_filename, :maximum => 255 + + acts_as_event :title => :filename, + :description => :filename, + :url => Proc.new {|o| {:controller => 'attachments', :action => 'download', :id => o.id}} + + cattr_accessor :storage_path + @@storage_path = "#{RAILS_ROOT}/files" + + def validate + errors.add_to_base :too_long if self.filesize > Setting.attachment_max_size.to_i.kilobytes + end + + def file=(incomming_file) + unless incomming_file.nil? + @temp_file = incomming_file + if @temp_file.size > 0 + self.filename = sanitize_filename(@temp_file.original_filename) + self.disk_filename = DateTime.now.strftime("%y%m%d%H%M%S") + "_" + self.filename + self.content_type = @temp_file.content_type.chomp + self.filesize = @temp_file.size + end + end + end + + def file + nil + end + + # Copy temp file to its final location + def before_save + if @temp_file && (@temp_file.size > 0) + logger.debug("saving '#{self.diskfile}'") + File.open(diskfile, "wb") do |f| + f.write(@temp_file.read) + end + self.digest = Digest::MD5.hexdigest(File.read(diskfile)) + end + # Don't save the content type if it's longer than the authorized length + if self.content_type && self.content_type.length > 255 + self.content_type = nil + end + end + + # Deletes file on the disk + def after_destroy + if self.filename? + File.delete(diskfile) if File.exist?(diskfile) + end + end + + # Returns file's location on disk + def diskfile + "#{@@storage_path}/#{self.disk_filename}" + end + + def increment_download + increment!(:downloads) + end + + def project + container.project + end + + def image? + self.filename =~ /\.(jpeg|jpg|gif|png)$/i + end + +private + def sanitize_filename(value) + # get only the filename, not the whole path + just_filename = value.gsub(/^.*(\\|\/)/, '') + # NOTE: File.basename doesn't work right with Windows paths on Unix + # INCORRECT: just_filename = File.basename(value.gsub('\\\\', '/')) + + # Finally, replace all non alphanumeric, underscore or periods with underscore + @filename = just_filename.gsub(/[^\w\.\-]/,'_') + end + +end diff --git a/rest_sys/app/models/auth_source.rb b/rest_sys/app/models/auth_source.rb new file mode 100644 index 000000000..47c121a13 --- /dev/null +++ b/rest_sys/app/models/auth_source.rb @@ -0,0 +1,51 @@ +# redMine - project management software +# Copyright (C) 2006 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class AuthSource < ActiveRecord::Base + has_many :users + + validates_presence_of :name + validates_uniqueness_of :name + validates_length_of :name, :host, :maximum => 60 + validates_length_of :account_password, :maximum => 60, :allow_nil => true + validates_length_of :account, :base_dn, :maximum => 255 + validates_length_of :attr_login, :attr_firstname, :attr_lastname, :attr_mail, :maximum => 30 + + def authenticate(login, password) + end + + def test_connection + end + + def auth_method_name + "Abstract" + end + + # Try to authenticate a user not yet registered against available sources + def self.authenticate(login, password) + AuthSource.find(:all, :conditions => ["onthefly_register=?", true]).each do |source| + begin + logger.debug "Authenticating '#{login}' against '#{source.name}'" if logger && logger.debug? + attrs = source.authenticate(login, password) + rescue + attrs = nil + end + return attrs if attrs + end + return nil + end +end diff --git a/rest_sys/app/models/auth_source_ldap.rb b/rest_sys/app/models/auth_source_ldap.rb new file mode 100644 index 000000000..b79b3ced0 --- /dev/null +++ b/rest_sys/app/models/auth_source_ldap.rb @@ -0,0 +1,82 @@ +# redMine - project management software +# Copyright (C) 2006 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require 'net/ldap' +require 'iconv' + +class AuthSourceLdap < AuthSource + validates_presence_of :host, :port, :attr_login + validates_presence_of :attr_firstname, :attr_lastname, :attr_mail, :if => Proc.new { |a| a.onthefly_register? } + + def after_initialize + self.port = 389 if self.port == 0 + end + + def authenticate(login, password) + attrs = [] + # get user's DN + ldap_con = initialize_ldap_con(self.account, self.account_password) + login_filter = Net::LDAP::Filter.eq( self.attr_login, login ) + object_filter = Net::LDAP::Filter.eq( "objectClass", "*" ) + dn = String.new + ldap_con.search( :base => self.base_dn, + :filter => object_filter & login_filter, + # only ask for the DN if on-the-fly registration is disabled + :attributes=> (onthefly_register? ? ['dn', self.attr_firstname, self.attr_lastname, self.attr_mail] : ['dn'])) do |entry| + dn = entry.dn + attrs = [:firstname => AuthSourceLdap.get_attr(entry, self.attr_firstname), + :lastname => AuthSourceLdap.get_attr(entry, self.attr_lastname), + :mail => AuthSourceLdap.get_attr(entry, self.attr_mail), + :auth_source_id => self.id ] if onthefly_register? + end + return nil if dn.empty? + logger.debug "DN found for #{login}: #{dn}" if logger && logger.debug? + # authenticate user + ldap_con = initialize_ldap_con(dn, password) + return nil unless ldap_con.bind + # return user's attributes + logger.debug "Authentication successful for '#{login}'" if logger && logger.debug? + attrs + rescue Net::LDAP::LdapError => text + raise "LdapError: " + text + end + + # test the connection to the LDAP + def test_connection + ldap_con = initialize_ldap_con(self.account, self.account_password) + ldap_con.open { } + rescue Net::LDAP::LdapError => text + raise "LdapError: " + text + end + + def auth_method_name + "LDAP" + end + +private + def initialize_ldap_con(ldap_user, ldap_password) + Net::LDAP.new( {:host => self.host, + :port => self.port, + :auth => { :method => :simple, :username => ldap_user, :password => ldap_password }, + :encryption => (self.tls ? :simple_tls : nil)} + ) + end + + def self.get_attr(entry, attr_name) + entry[attr_name].is_a?(Array) ? entry[attr_name].first : entry[attr_name] + end +end diff --git a/rest_sys/app/models/board.rb b/rest_sys/app/models/board.rb new file mode 100644 index 000000000..26e2004d3 --- /dev/null +++ b/rest_sys/app/models/board.rb @@ -0,0 +1,29 @@ +# redMine - project management software +# Copyright (C) 2006-2007 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class Board < ActiveRecord::Base + belongs_to :project + has_many :topics, :class_name => 'Message', :conditions => "#{Message.table_name}.parent_id IS NULL", :order => "#{Message.table_name}.created_on DESC" + has_many :messages, :dependent => :delete_all, :order => "#{Message.table_name}.created_on DESC" + belongs_to :last_message, :class_name => 'Message', :foreign_key => :last_message_id + acts_as_list :scope => :project_id + acts_as_watchable + + validates_presence_of :name, :description + validates_length_of :name, :maximum => 30 + validates_length_of :description, :maximum => 255 +end diff --git a/rest_sys/app/models/change.rb b/rest_sys/app/models/change.rb new file mode 100644 index 000000000..d14f435a4 --- /dev/null +++ b/rest_sys/app/models/change.rb @@ -0,0 +1,22 @@ +# redMine - project management software +# Copyright (C) 2006-2007 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class Change < ActiveRecord::Base + belongs_to :changeset + + validates_presence_of :changeset_id, :action, :path +end diff --git a/rest_sys/app/models/changeset.rb b/rest_sys/app/models/changeset.rb new file mode 100644 index 000000000..1b79104c4 --- /dev/null +++ b/rest_sys/app/models/changeset.rb @@ -0,0 +1,104 @@ +# redMine - project management software +# Copyright (C) 2006-2007 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class Changeset < ActiveRecord::Base + belongs_to :repository + has_many :changes, :dependent => :delete_all + has_and_belongs_to_many :issues + + acts_as_event :title => Proc.new {|o| "#{l(:label_revision)} #{o.revision}" + (o.comments.blank? ? '' : (': ' + o.comments))}, + :description => :comments, + :datetime => :committed_on, + :author => :committer, + :url => Proc.new {|o| {:controller => 'repositories', :action => 'revision', :id => o.repository.project_id, :rev => o.revision}} + + acts_as_searchable :columns => 'comments', + :include => :repository, + :project_key => "#{Repository.table_name}.project_id", + :date_column => 'committed_on' + + validates_presence_of :repository_id, :revision, :committed_on, :commit_date + validates_numericality_of :revision, :only_integer => true + validates_uniqueness_of :revision, :scope => :repository_id + validates_uniqueness_of :scmid, :scope => :repository_id, :allow_nil => true + + def comments=(comment) + write_attribute(:comments, comment.strip) + end + + def committed_on=(date) + self.commit_date = date + super + end + + def after_create + scan_comment_for_issue_ids + end + + def scan_comment_for_issue_ids + return if comments.blank? + # keywords used to reference issues + ref_keywords = Setting.commit_ref_keywords.downcase.split(",").collect(&:strip) + # keywords used to fix issues + fix_keywords = Setting.commit_fix_keywords.downcase.split(",").collect(&:strip) + # status and optional done ratio applied + fix_status = IssueStatus.find_by_id(Setting.commit_fix_status_id) + done_ratio = Setting.commit_fix_done_ratio.blank? ? nil : Setting.commit_fix_done_ratio.to_i + + kw_regexp = (ref_keywords + fix_keywords).collect{|kw| Regexp.escape(kw)}.join("|") + return if kw_regexp.blank? + + referenced_issues = [] + + if ref_keywords.delete('*') + # find any issue ID in the comments + target_issue_ids = [] + comments.scan(%r{([\s\(,-^])#(\d+)(?=[[:punct:]]|\s|<|$)}).each { |m| target_issue_ids << m[1] } + referenced_issues += repository.project.issues.find_all_by_id(target_issue_ids) + end + + comments.scan(Regexp.new("(#{kw_regexp})[\s:]+(([\s,;&]*#?\\d+)+)", Regexp::IGNORECASE)).each do |match| + action = match[0] + target_issue_ids = match[1].scan(/\d+/) + target_issues = repository.project.issues.find_all_by_id(target_issue_ids) + if fix_status && fix_keywords.include?(action.downcase) + # update status of issues + logger.debug "Issues fixed by changeset #{self.revision}: #{issue_ids.join(', ')}." if logger && logger.debug? + target_issues.each do |issue| + # don't change the status is the issue is already closed + next if issue.status.is_closed? + issue.status = fix_status + issue.done_ratio = done_ratio if done_ratio + issue.save + end + end + referenced_issues += target_issues + end + + self.issues = referenced_issues.uniq + end + + # Returns the previous changeset + def previous + @previous ||= Changeset.find(:first, :conditions => ['revision < ? AND repository_id = ?', self.revision, self.repository_id], :order => 'revision DESC') + end + + # Returns the next changeset + def next + @next ||= Changeset.find(:first, :conditions => ['revision > ? AND repository_id = ?', self.revision, self.repository_id], :order => 'revision ASC') + end +end diff --git a/rest_sys/app/models/comment.rb b/rest_sys/app/models/comment.rb new file mode 100644 index 000000000..88d5348da --- /dev/null +++ b/rest_sys/app/models/comment.rb @@ -0,0 +1,23 @@ +# redMine - project management software +# Copyright (C) 2006 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class Comment < ActiveRecord::Base + belongs_to :commented, :polymorphic => true, :counter_cache => true + belongs_to :author, :class_name => 'User', :foreign_key => 'author_id' + + validates_presence_of :commented, :author, :comments +end diff --git a/rest_sys/app/models/custom_field.rb b/rest_sys/app/models/custom_field.rb new file mode 100644 index 000000000..5a134c4ec --- /dev/null +++ b/rest_sys/app/models/custom_field.rb @@ -0,0 +1,70 @@ +# redMine - project management software +# Copyright (C) 2006 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class CustomField < ActiveRecord::Base + has_many :custom_values, :dependent => :delete_all + acts_as_list :scope => 'type = \'#{self.class}\'' + serialize :possible_values + + FIELD_FORMATS = { "string" => { :name => :label_string, :order => 1 }, + "text" => { :name => :label_text, :order => 2 }, + "int" => { :name => :label_integer, :order => 3 }, + "float" => { :name => :label_float, :order => 4 }, + "list" => { :name => :label_list, :order => 5 }, + "date" => { :name => :label_date, :order => 6 }, + "bool" => { :name => :label_boolean, :order => 7 } + }.freeze + + validates_presence_of :name, :field_format + validates_uniqueness_of :name + validates_length_of :name, :maximum => 30 + validates_format_of :name, :with => /^[\w\s\'\-]*$/i + validates_inclusion_of :field_format, :in => FIELD_FORMATS.keys + + def initialize(attributes = nil) + super + self.possible_values ||= [] + end + + def before_validation + # remove empty values + self.possible_values = self.possible_values.collect{|v| v unless v.empty?}.compact + # make sure these fields are not searchable + self.searchable = false if %w(int float date bool).include?(field_format) + true + end + + def validate + if self.field_format == "list" + errors.add(:possible_values, :activerecord_error_blank) if self.possible_values.nil? || self.possible_values.empty? + errors.add(:possible_values, :activerecord_error_invalid) unless self.possible_values.is_a? Array + end + end + + def <=>(field) + position <=> field.position + end + + # to move in project_custom_field + def self.for_all + find(:all, :conditions => ["is_for_all=?", true]) + end + + def type_name + nil + end +end diff --git a/rest_sys/app/models/custom_value.rb b/rest_sys/app/models/custom_value.rb new file mode 100644 index 000000000..c3d6b7bb9 --- /dev/null +++ b/rest_sys/app/models/custom_value.rb @@ -0,0 +1,39 @@ +# redMine - project management software +# Copyright (C) 2006 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class CustomValue < ActiveRecord::Base + belongs_to :custom_field + belongs_to :customized, :polymorphic => true + +protected + def validate + errors.add(:value, :activerecord_error_blank) and return if custom_field.is_required? and value.blank? + errors.add(:value, :activerecord_error_invalid) unless custom_field.regexp.blank? or value =~ Regexp.new(custom_field.regexp) + errors.add(:value, :activerecord_error_too_short) if custom_field.min_length > 0 and value.length < custom_field.min_length and value.length > 0 + errors.add(:value, :activerecord_error_too_long) if custom_field.max_length > 0 and value.length > custom_field.max_length + case custom_field.field_format + when 'int' + errors.add(:value, :activerecord_error_not_a_number) unless value.blank? || value =~ /^[+-]?\d+$/ + when 'float' + begin; !value.blank? && Kernel.Float(value); rescue; errors.add(:value, :activerecord_error_invalid) end + when 'date' + errors.add(:value, :activerecord_error_not_a_date) unless value =~ /^\d{4}-\d{2}-\d{2}$/ or value.blank? + when 'list' + errors.add(:value, :activerecord_error_inclusion) unless custom_field.possible_values.include?(value) or value.blank? + end + end +end diff --git a/rest_sys/app/models/document.rb b/rest_sys/app/models/document.rb new file mode 100644 index 000000000..7a432b46b --- /dev/null +++ b/rest_sys/app/models/document.rb @@ -0,0 +1,30 @@ +# redMine - project management software +# Copyright (C) 2006 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class Document < ActiveRecord::Base + belongs_to :project + belongs_to :category, :class_name => "Enumeration", :foreign_key => "category_id" + has_many :attachments, :as => :container, :dependent => :destroy + + acts_as_searchable :columns => ['title', 'description'] + acts_as_event :title => Proc.new {|o| "#{l(:label_document)}: #{o.title}"}, + :author => Proc.new {|o| (a = o.attachments.find(:first, :order => "#{Attachment.table_name}.created_on ASC")) ? a.author : nil }, + :url => Proc.new {|o| {:controller => 'documents', :action => 'show', :id => o.id}} + + validates_presence_of :project, :title, :category + validates_length_of :title, :maximum => 60 +end diff --git a/rest_sys/app/models/enabled_module.rb b/rest_sys/app/models/enabled_module.rb new file mode 100644 index 000000000..3c05c76c1 --- /dev/null +++ b/rest_sys/app/models/enabled_module.rb @@ -0,0 +1,23 @@ +# redMine - project management software +# Copyright (C) 2006-2007 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class EnabledModule < ActiveRecord::Base + belongs_to :project + + validates_presence_of :name + validates_uniqueness_of :name, :scope => :project_id +end diff --git a/rest_sys/app/models/enumeration.rb b/rest_sys/app/models/enumeration.rb new file mode 100644 index 000000000..400681a43 --- /dev/null +++ b/rest_sys/app/models/enumeration.rb @@ -0,0 +1,67 @@ +# redMine - project management software +# Copyright (C) 2006 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class Enumeration < ActiveRecord::Base + acts_as_list :scope => 'opt = \'#{opt}\'' + + before_destroy :check_integrity + + validates_presence_of :opt, :name + validates_uniqueness_of :name, :scope => [:opt] + validates_length_of :name, :maximum => 30 + validates_format_of :name, :with => /^[\w\s\'\-]*$/i + + OPTIONS = { + "IPRI" => :enumeration_issue_priorities, + "DCAT" => :enumeration_doc_categories, + "ACTI" => :enumeration_activities + }.freeze + + def self.get_values(option) + find(:all, :conditions => {:opt => option}, :order => 'position') + end + + def self.default(option) + find(:first, :conditions => {:opt => option, :is_default => true}, :order => 'position') + end + + def option_name + OPTIONS[self.opt] + end + + def before_save + Enumeration.update_all("is_default = #{connection.quoted_false}", {:opt => opt}) if is_default? + end + + def <=>(enumeration) + position <=> enumeration.position + end + + def to_s; name end + +private + def check_integrity + case self.opt + when "IPRI" + raise "Can't delete enumeration" if Issue.find(:first, :conditions => ["priority_id=?", self.id]) + when "DCAT" + raise "Can't delete enumeration" if Document.find(:first, :conditions => ["category_id=?", self.id]) + when "ACTI" + raise "Can't delete enumeration" if TimeEntry.find(:first, :conditions => ["activity_id=?", self.id]) + end + end +end diff --git a/rest_sys/app/models/issue.rb b/rest_sys/app/models/issue.rb new file mode 100644 index 000000000..f7b01ea6a --- /dev/null +++ b/rest_sys/app/models/issue.rb @@ -0,0 +1,221 @@ +# redMine - project management software +# Copyright (C) 2006-2007 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class Issue < ActiveRecord::Base + belongs_to :project + belongs_to :tracker + belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id' + belongs_to :author, :class_name => 'User', :foreign_key => 'author_id' + belongs_to :assigned_to, :class_name => 'User', :foreign_key => 'assigned_to_id' + belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id' + belongs_to :priority, :class_name => 'Enumeration', :foreign_key => 'priority_id' + belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id' + + has_many :journals, :as => :journalized, :dependent => :destroy + has_many :attachments, :as => :container, :dependent => :destroy + has_many :time_entries, :dependent => :nullify + has_many :custom_values, :dependent => :delete_all, :as => :customized + has_many :custom_fields, :through => :custom_values + has_and_belongs_to_many :changesets, :order => "revision ASC" + + has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all + has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all + + acts_as_watchable + acts_as_searchable :columns => ['subject', 'description'], :with => {:journal => :issue} + acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id}: #{o.subject}"}, + :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}} + + validates_presence_of :subject, :description, :priority, :project, :tracker, :author, :status + validates_length_of :subject, :maximum => 255 + validates_inclusion_of :done_ratio, :in => 0..100 + validates_numericality_of :estimated_hours, :allow_nil => true + validates_associated :custom_values, :on => :update + + def after_initialize + if new_record? + # set default values for new records only + self.status ||= IssueStatus.default + self.priority ||= Enumeration.default('IPRI') + end + end + + def copy_from(arg) + issue = arg.is_a?(Issue) ? arg : Issue.find(arg) + self.attributes = issue.attributes.dup + self.custom_values = issue.custom_values.collect {|v| v.clone} + self + end + + # Move an issue to a new project and tracker + def move_to(new_project, new_tracker = nil) + transaction do + if new_project && project_id != new_project.id + # delete issue relations + self.relations_from.clear + self.relations_to.clear + # issue is moved to another project + self.category = nil + self.fixed_version = nil + self.project = new_project + end + if new_tracker + self.tracker = new_tracker + end + if save + # Manually update project_id on related time entries + TimeEntry.update_all("project_id = #{new_project.id}", {:issue_id => id}) + else + rollback_db_transaction + return false + end + end + return true + end + + def priority_id=(pid) + self.priority = nil + write_attribute(:priority_id, pid) + end + + def validate + if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty? + errors.add :due_date, :activerecord_error_not_a_date + end + + if self.due_date and self.start_date and self.due_date < self.start_date + errors.add :due_date, :activerecord_error_greater_than_start_date + end + + if start_date && soonest_start && start_date < soonest_start + errors.add :start_date, :activerecord_error_invalid + end + end + + def validate_on_create + errors.add :tracker_id, :activerecord_error_invalid unless project.trackers.include?(tracker) + end + + def before_create + # default assignment based on category + if assigned_to.nil? && category && category.assigned_to + self.assigned_to = category.assigned_to + end + end + + def before_save + if @current_journal + # attributes changes + (Issue.column_names - %w(id description)).each {|c| + @current_journal.details << JournalDetail.new(:property => 'attr', + :prop_key => c, + :old_value => @issue_before_change.send(c), + :value => send(c)) unless send(c)==@issue_before_change.send(c) + } + # custom fields changes + custom_values.each {|c| + next if (@custom_values_before_change[c.custom_field_id]==c.value || + (@custom_values_before_change[c.custom_field_id].blank? && c.value.blank?)) + @current_journal.details << JournalDetail.new(:property => 'cf', + :prop_key => c.custom_field_id, + :old_value => @custom_values_before_change[c.custom_field_id], + :value => c.value) + } + @current_journal.save + end + # Save the issue even if the journal is not saved (because empty) + true + end + + def after_save + # Update start/due dates of following issues + relations_from.each(&:set_issue_to_dates) + + # Close duplicates if the issue was closed + if @issue_before_change && !@issue_before_change.closed? && self.closed? + duplicates.each do |duplicate| + # Don't re-close it if it's already closed + next if duplicate.closed? + # Same user and notes + duplicate.init_journal(@current_journal.user, @current_journal.notes) + duplicate.update_attribute :status, self.status + end + end + end + + def custom_value_for(custom_field) + self.custom_values.each {|v| return v if v.custom_field_id == custom_field.id } + return nil + end + + def init_journal(user, notes = "") + @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes) + @issue_before_change = self.clone + @custom_values_before_change = {} + self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value } + @current_journal + end + + # Return true if the issue is closed, otherwise false + def closed? + self.status.is_closed? + end + + # Users the issue can be assigned to + def assignable_users + project.assignable_users + end + + # Returns the mail adresses of users that should be notified for the issue + def recipients + recipients = project.recipients + # Author and assignee are always notified + recipients << author.mail if author + recipients << assigned_to.mail if assigned_to + recipients.compact.uniq + end + + def spent_hours + @spent_hours ||= time_entries.sum(:hours) || 0 + end + + def relations + (relations_from + relations_to).sort + end + + def all_dependent_issues + dependencies = [] + relations_from.each do |relation| + dependencies << relation.issue_to + dependencies += relation.issue_to.all_dependent_issues + end + dependencies + end + + # Returns an array of the duplicate issues + def duplicates + relations.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.other_issue(self)} + end + + def duration + (start_date && due_date) ? due_date - start_date : 0 + end + + def soonest_start + @soonest_start ||= relations_to.collect{|relation| relation.successor_soonest_start}.compact.min + end +end diff --git a/rest_sys/app/models/issue_category.rb b/rest_sys/app/models/issue_category.rb new file mode 100644 index 000000000..51baeb419 --- /dev/null +++ b/rest_sys/app/models/issue_category.rb @@ -0,0 +1,43 @@ +# redMine - project management software +# Copyright (C) 2006 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class IssueCategory < ActiveRecord::Base + belongs_to :project + belongs_to :assigned_to, :class_name => 'User', :foreign_key => 'assigned_to_id' + has_many :issues, :foreign_key => 'category_id', :dependent => :nullify + + validates_presence_of :name + validates_uniqueness_of :name, :scope => [:project_id] + validates_length_of :name, :maximum => 30 + + alias :destroy_without_reassign :destroy + + # Destroy the category + # If a category is specified, issues are reassigned to this category + def destroy(reassign_to = nil) + if reassign_to && reassign_to.is_a?(IssueCategory) && reassign_to.project == self.project + Issue.update_all("category_id = #{reassign_to.id}", "category_id = #{id}") + end + destroy_without_reassign + end + + def <=>(category) + name <=> category.name + end + + def to_s; name end +end diff --git a/rest_sys/app/models/issue_custom_field.rb b/rest_sys/app/models/issue_custom_field.rb new file mode 100644 index 000000000..d087768a4 --- /dev/null +++ b/rest_sys/app/models/issue_custom_field.rb @@ -0,0 +1,27 @@ +# redMine - project management software +# Copyright (C) 2006 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class IssueCustomField < CustomField + has_and_belongs_to_many :projects, :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}", :foreign_key => "custom_field_id" + has_and_belongs_to_many :trackers, :join_table => "#{table_name_prefix}custom_fields_trackers#{table_name_suffix}", :foreign_key => "custom_field_id" + has_many :issues, :through => :issue_custom_values + + def type_name + :label_issue_plural + end +end + diff --git a/rest_sys/app/models/issue_relation.rb b/rest_sys/app/models/issue_relation.rb new file mode 100644 index 000000000..07e940b85 --- /dev/null +++ b/rest_sys/app/models/issue_relation.rb @@ -0,0 +1,79 @@ +# redMine - project management software +# Copyright (C) 2006-2007 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class IssueRelation < ActiveRecord::Base + belongs_to :issue_from, :class_name => 'Issue', :foreign_key => 'issue_from_id' + belongs_to :issue_to, :class_name => 'Issue', :foreign_key => 'issue_to_id' + + TYPE_RELATES = "relates" + TYPE_DUPLICATES = "duplicates" + TYPE_BLOCKS = "blocks" + TYPE_PRECEDES = "precedes" + + TYPES = { TYPE_RELATES => { :name => :label_relates_to, :sym_name => :label_relates_to, :order => 1 }, + TYPE_DUPLICATES => { :name => :label_duplicates, :sym_name => :label_duplicates, :order => 2 }, + TYPE_BLOCKS => { :name => :label_blocks, :sym_name => :label_blocked_by, :order => 3 }, + TYPE_PRECEDES => { :name => :label_precedes, :sym_name => :label_follows, :order => 4 }, + }.freeze + + validates_presence_of :issue_from, :issue_to, :relation_type + validates_inclusion_of :relation_type, :in => TYPES.keys + validates_numericality_of :delay, :allow_nil => true + validates_uniqueness_of :issue_to_id, :scope => :issue_from_id + + def validate + if issue_from && issue_to + errors.add :issue_to_id, :activerecord_error_invalid if issue_from_id == issue_to_id + errors.add :issue_to_id, :activerecord_error_not_same_project unless issue_from.project_id == issue_to.project_id || Setting.cross_project_issue_relations? + errors.add_to_base :activerecord_error_circular_dependency if issue_to.all_dependent_issues.include? issue_from + end + end + + def other_issue(issue) + (self.issue_from_id == issue.id) ? issue_to : issue_from + end + + def label_for(issue) + TYPES[relation_type] ? TYPES[relation_type][(self.issue_from_id == issue.id) ? :name : :sym_name] : :unknow + end + + def before_save + if TYPE_PRECEDES == relation_type + self.delay ||= 0 + else + self.delay = nil + end + set_issue_to_dates + end + + def set_issue_to_dates + soonest_start = self.successor_soonest_start + if soonest_start && (!issue_to.start_date || issue_to.start_date < soonest_start) + issue_to.start_date, issue_to.due_date = successor_soonest_start, successor_soonest_start + issue_to.duration + issue_to.save + end + end + + def successor_soonest_start + return nil unless (TYPE_PRECEDES == self.relation_type) && (issue_from.start_date || issue_from.due_date) + (issue_from.due_date || issue_from.start_date) + 1 + delay + end + + def <=>(relation) + TYPES[self.relation_type][:order] <=> TYPES[relation.relation_type][:order] + end +end diff --git a/rest_sys/app/models/issue_status.rb b/rest_sys/app/models/issue_status.rb new file mode 100644 index 000000000..a5d228405 --- /dev/null +++ b/rest_sys/app/models/issue_status.rb @@ -0,0 +1,65 @@ +# redMine - project management software +# Copyright (C) 2006 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class IssueStatus < ActiveRecord::Base + before_destroy :check_integrity + has_many :workflows, :foreign_key => "old_status_id", :dependent => :delete_all + acts_as_list + + validates_presence_of :name + validates_uniqueness_of :name + validates_length_of :name, :maximum => 30 + validates_format_of :name, :with => /^[\w\s\'\-]*$/i + + def before_save + IssueStatus.update_all "is_default=#{connection.quoted_false}" if self.is_default? + end + + # Returns the default status for new issues + def self.default + find(:first, :conditions =>["is_default=?", true]) + end + + # Returns an array of all statuses the given role can switch to + # Uses association cache when called more than one time + def new_statuses_allowed_to(role, tracker) + new_statuses = workflows.select {|w| w.role_id == role.id && w.tracker_id == tracker.id}.collect{|w| w.new_status} if role && tracker + new_statuses ? new_statuses.compact.sort{|x, y| x.position <=> y.position } : [] + end + + # Same thing as above but uses a database query + # More efficient than the previous method if called just once + def find_new_statuses_allowed_to(role, tracker) + new_statuses = workflows.find(:all, + :include => :new_status, + :conditions => ["role_id=? and tracker_id=?", role.id, tracker.id]).collect{ |w| w.new_status }.compact if role && tracker + new_statuses ? new_statuses.sort{|x, y| x.position <=> y.position } : [] + end + + def new_status_allowed_to?(status, role, tracker) + status && role && tracker ? + !workflows.find(:first, :conditions => {:new_status_id => status.id, :role_id => role.id, :tracker_id => tracker.id}).nil? : + false + end + + def to_s; name end + +private + def check_integrity + raise "Can't delete status" if Issue.find(:first, :conditions => ["status_id=?", self.id]) + end +end diff --git a/rest_sys/app/models/journal.rb b/rest_sys/app/models/journal.rb new file mode 100644 index 000000000..64483d21d --- /dev/null +++ b/rest_sys/app/models/journal.rb @@ -0,0 +1,47 @@ +# redMine - project management software +# Copyright (C) 2006 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class Journal < ActiveRecord::Base + belongs_to :journalized, :polymorphic => true + # added as a quick fix to allow eager loading of the polymorphic association + # since always associated to an issue, for now + belongs_to :issue, :foreign_key => :journalized_id + + belongs_to :user + has_many :details, :class_name => "JournalDetail", :dependent => :delete_all + + acts_as_searchable :columns => 'notes', + :include => :issue, + :project_key => "#{Issue.table_name}.project_id", + :date_column => "#{Issue.table_name}.created_on" + + acts_as_event :title => Proc.new {|o| "#{o.issue.tracker.name} ##{o.issue.id}: #{o.issue.subject}" + ((s = o.new_status) ? " (#{s})" : '') }, + :description => :notes, + :author => :user, + :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.issue.id}} + + def save + # Do not save an empty journal + (details.empty? && notes.blank?) ? false : super + end + + # Returns the new status if the journal contains a status change, otherwise nil + def new_status + c = details.detect {|detail| detail.prop_key == 'status_id'} + (c && c.value) ? IssueStatus.find_by_id(c.value.to_i) : nil + end +end diff --git a/rest_sys/app/models/journal_detail.rb b/rest_sys/app/models/journal_detail.rb new file mode 100644 index 000000000..58239006b --- /dev/null +++ b/rest_sys/app/models/journal_detail.rb @@ -0,0 +1,25 @@ +# redMine - project management software +# Copyright (C) 2006 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class JournalDetail < ActiveRecord::Base + belongs_to :journal + + def before_save + self.value = value[0..254] if value && value.is_a?(String) + self.old_value = old_value[0..254] if old_value && old_value.is_a?(String) + end +end diff --git a/rest_sys/app/models/mail_handler.rb b/rest_sys/app/models/mail_handler.rb new file mode 100644 index 000000000..7a1d73244 --- /dev/null +++ b/rest_sys/app/models/mail_handler.rb @@ -0,0 +1,40 @@ +# redMine - project management software +# Copyright (C) 2006-2007 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class MailHandler < ActionMailer::Base + + # Processes incoming emails + # Currently, it only supports adding a note to an existing issue + # by replying to the initial notification message + def receive(email) + # find related issue by parsing the subject + m = email.subject.match %r{\[.*#(\d+)\]} + return unless m + issue = Issue.find_by_id(m[1]) + return unless issue + + # find user + user = User.find_active(:first, :conditions => {:mail => email.from.first}) + return unless user + # check permission + return unless user.allowed_to?(:add_issue_notes, issue.project) + + # add the note + issue.init_journal(user, email.body.chomp) + issue.save + end +end diff --git a/rest_sys/app/models/mailer.rb b/rest_sys/app/models/mailer.rb new file mode 100644 index 000000000..257f57726 --- /dev/null +++ b/rest_sys/app/models/mailer.rb @@ -0,0 +1,160 @@ +# redMine - project management software +# Copyright (C) 2006-2007 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class Mailer < ActionMailer::Base + helper ApplicationHelper + helper IssuesHelper + helper CustomFieldsHelper + + include ActionController::UrlWriter + + def issue_add(issue) + recipients issue.recipients + 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) + end + + def issue_edit(journal) + issue = journal.journalized + recipients issue.recipients + # Watchers in cc + cc(issue.watcher_recipients - @recipients) + subject "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] #{issue.status.name} - #{issue.subject}" + body :issue => issue, + :journal => journal, + :issue_url => url_for(:controller => 'issues', :action => 'show', :id => issue) + end + + def document_added(document) + recipients document.project.recipients + subject "[#{document.project.name}] #{l(:label_document_new)}: #{document.title}" + body :document => document, + :document_url => url_for(:controller => 'documents', :action => 'show', :id => document) + end + + def attachments_added(attachments) + container = attachments.first.container + added_to = '' + added_to_url = '' + case container.class.name + when 'Version' + added_to_url = url_for(:controller => 'projects', :action => 'list_files', :id => container.project_id) + added_to = "#{l(:label_version)}: #{container.name}" + when 'Document' + added_to_url = url_for(:controller => 'documents', :action => 'show', :id => container.id) + added_to = "#{l(:label_document)}: #{container.title}" + end + recipients container.project.recipients + subject "[#{container.project.name}] #{l(:label_attachment_new)}" + body :attachments => attachments, + :added_to => added_to, + :added_to_url => added_to_url + end + + def news_added(news) + recipients news.project.recipients + subject "[#{news.project.name}] #{l(:label_news)}: #{news.title}" + body :news => news, + :news_url => url_for(:controller => 'news', :action => 'show', :id => news) + end + + def message_posted(message, recipients) + recipients(recipients) + subject "[#{message.board.project.name} - #{message.board.name}] #{message.subject}" + body :message => message, + :message_url => url_for(:controller => 'messages', :action => 'show', :board_id => message.board_id, :id => message.root) + end + + def account_information(user, password) + set_language_if_valid user.language + recipients user.mail + subject l(:mail_subject_register) + body :user => user, + :password => password, + :login_url => url_for(:controller => 'account', :action => 'login') + end + + def account_activation_request(user) + # Send the email to all active administrators + recipients User.find_active(:all, :conditions => {:admin => true}).collect { |u| u.mail }.compact + subject l(:mail_subject_account_activation_request) + body :user => user, + :url => url_for(:controller => 'users', :action => 'index', :status => User::STATUS_REGISTERED, :sort_key => 'created_on', :sort_order => 'desc') + end + + def lost_password(token) + set_language_if_valid(token.user.language) + recipients token.user.mail + subject l(:mail_subject_lost_password) + body :token => token, + :url => url_for(:controller => 'account', :action => 'lost_password', :token => token.value) + end + + def register(token) + set_language_if_valid(token.user.language) + recipients token.user.mail + subject l(:mail_subject_register) + body :token => token, + :url => url_for(:controller => 'account', :action => 'activate', :token => token.value) + end + + def test(user) + set_language_if_valid(user.language) + recipients user.mail + subject 'Redmine test' + body :url => url_for(:controller => 'welcome') + end + + private + def initialize_defaults(method_name) + super + set_language_if_valid Setting.default_language + from Setting.mail_from + default_url_options[:host] = Setting.host_name + default_url_options[:protocol] = Setting.protocol + end + + # Overrides the create_mail method + def create_mail + # Removes the current user from the recipients and cc + # if he doesn't want to receive notifications about what he does + if User.current.pref[:no_self_notified] + recipients.delete(User.current.mail) if recipients + cc.delete(User.current.mail) if cc + end + # Blind carbon copy recipients + if Setting.bcc_recipients? + bcc([recipients, cc].flatten.compact.uniq) + recipients [] + cc [] + end + super + end + + # Renders a message with the corresponding layout + def render_message(method_name, body) + layout = method_name.match(%r{text\.html\.(rhtml|rxml)}) ? 'layout.text.html.rhtml' : 'layout.text.plain.rhtml' + body[:content_for_layout] = render(:file => method_name, :body => body) + ActionView::Base.new(template_root, body, self).render(:file => "mailer/#{layout}") + end + + # Makes partial rendering work with Rails 1.2 (retro-compatibility) + def self.controller_path + '' + end unless respond_to?('controller_path') +end diff --git a/rest_sys/app/models/member.rb b/rest_sys/app/models/member.rb new file mode 100644 index 000000000..39703147d --- /dev/null +++ b/rest_sys/app/models/member.rb @@ -0,0 +1,38 @@ +# redMine - project management software +# Copyright (C) 2006 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class Member < ActiveRecord::Base + belongs_to :user + belongs_to :role + belongs_to :project + + validates_presence_of :role, :user, :project + validates_uniqueness_of :user_id, :scope => :project_id + + def validate + errors.add :role_id, :activerecord_error_invalid if role && !role.member? + end + + def name + self.user.name + end + + def before_destroy + # remove category based auto assignments for this member + project.issue_categories.update_all "assigned_to_id = NULL", ["assigned_to_id = ?", self.user.id] + end +end diff --git a/rest_sys/app/models/message.rb b/rest_sys/app/models/message.rb new file mode 100644 index 000000000..038665cce --- /dev/null +++ b/rest_sys/app/models/message.rb @@ -0,0 +1,67 @@ +# redMine - project management software +# Copyright (C) 2006-2007 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class Message < ActiveRecord::Base + belongs_to :board + belongs_to :author, :class_name => 'User', :foreign_key => 'author_id' + acts_as_tree :counter_cache => :replies_count, :order => "#{Message.table_name}.created_on ASC" + has_many :attachments, :as => :container, :dependent => :destroy + belongs_to :last_reply, :class_name => 'Message', :foreign_key => 'last_reply_id' + + acts_as_searchable :columns => ['subject', 'content'], + :include => :board, + :project_key => 'project_id', + :date_column => 'created_on' + acts_as_event :title => Proc.new {|o| "#{o.board.name}: #{o.subject}"}, + :description => :content, + :url => Proc.new {|o| {:controller => 'messages', :action => 'show', :board_id => o.board_id, :id => o.id}} + + attr_protected :locked, :sticky + validates_presence_of :subject, :content + validates_length_of :subject, :maximum => 255 + + def validate_on_create + # Can not reply to a locked topic + errors.add_to_base 'Topic is locked' if root.locked? + end + + def after_create + board.update_attribute(:last_message_id, self.id) + board.increment! :messages_count + if parent + parent.reload.update_attribute(:last_reply_id, self.id) + else + board.increment! :topics_count + end + end + + def after_destroy + # The following line is required so that the previous counter + # updates (due to children removal) are not overwritten + board.reload + board.decrement! :messages_count + board.decrement! :topics_count unless parent + end + + def sticky? + sticky == 1 + end + + def project + board.project + end +end diff --git a/rest_sys/app/models/message_observer.rb b/rest_sys/app/models/message_observer.rb new file mode 100644 index 000000000..1c311e25f --- /dev/null +++ b/rest_sys/app/models/message_observer.rb @@ -0,0 +1,27 @@ +# redMine - project management software +# Copyright (C) 2006-2007 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class MessageObserver < ActiveRecord::Observer + def after_create(message) + # send notification to the authors of the thread + recipients = ([message.root] + message.root.children).collect {|m| m.author.mail if m.author} + # send notification to the board watchers + recipients += message.board.watcher_recipients + recipients = recipients.compact.uniq + Mailer.deliver_message_posted(message, recipients) if !recipients.empty? && Setting.notified_events.include?('message_posted') + end +end diff --git a/rest_sys/app/models/news.rb b/rest_sys/app/models/news.rb new file mode 100644 index 000000000..3d8c4d661 --- /dev/null +++ b/rest_sys/app/models/news.rb @@ -0,0 +1,34 @@ +# redMine - project management software +# Copyright (C) 2006 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class News < ActiveRecord::Base + belongs_to :project + belongs_to :author, :class_name => 'User', :foreign_key => 'author_id' + has_many :comments, :as => :commented, :dependent => :delete_all, :order => "created_on" + + validates_presence_of :title, :description + validates_length_of :title, :maximum => 60 + validates_length_of :summary, :maximum => 255 + + acts_as_searchable :columns => ['title', 'description'] + acts_as_event :url => Proc.new {|o| {:controller => 'news', :action => 'show', :id => o.id}} + + # returns latest news for projects visible by user + def self.latest(user=nil, count=5) + find(:all, :limit => count, :conditions => Project.visible_by(user), :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC") + end +end diff --git a/rest_sys/app/models/project.rb b/rest_sys/app/models/project.rb new file mode 100644 index 000000000..84eeefee4 --- /dev/null +++ b/rest_sys/app/models/project.rb @@ -0,0 +1,202 @@ +# redMine - project management software +# Copyright (C) 2006 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class Project < ActiveRecord::Base + # Project statuses + STATUS_ACTIVE = 1 + STATUS_ARCHIVED = 9 + + has_many :members, :include => :user, :conditions => "#{User.table_name}.status=#{User::STATUS_ACTIVE}" + has_many :users, :through => :members + has_many :custom_values, :dependent => :delete_all, :as => :customized + has_many :enabled_modules, :dependent => :delete_all + has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position" + has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker] + has_many :issue_changes, :through => :issues, :source => :journals + has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC" + has_many :time_entries, :dependent => :delete_all + has_many :queries, :dependent => :delete_all + has_many :documents, :dependent => :destroy + has_many :news, :dependent => :delete_all, :include => :author + has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name" + has_many :boards, :order => "position ASC" + has_one :repository, :dependent => :destroy + has_many :changesets, :through => :repository + has_one :wiki, :dependent => :destroy + # Custom field for the project issues + has_and_belongs_to_many :custom_fields, + :class_name => 'IssueCustomField', + :order => "#{CustomField.table_name}.position", + :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}", + :association_foreign_key => 'custom_field_id' + + acts_as_tree :order => "name", :counter_cache => true + + acts_as_searchable :columns => ['name', 'description'], :project_key => 'id' + acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"}, + :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o.id}} + + attr_protected :status, :enabled_module_names + + validates_presence_of :name, :description, :identifier + validates_uniqueness_of :name, :identifier + validates_associated :custom_values, :on => :update + validates_associated :repository, :wiki + validates_length_of :name, :maximum => 30 + validates_length_of :description, :maximum => 255 + validates_length_of :homepage, :maximum => 60 + validates_length_of :identifier, :in => 3..20 + validates_format_of :identifier, :with => /^[a-z0-9\-]*$/ + + before_destroy :delete_all_members + + def identifier=(identifier) + super unless identifier_frozen? + end + + def identifier_frozen? + errors[:identifier].nil? && !(new_record? || identifier.blank?) + end + + def issues_with_subprojects(include_subprojects=false) + conditions = nil + if include_subprojects && !active_children.empty? + ids = [id] + active_children.collect {|c| c.id} + conditions = ["#{Issue.table_name}.project_id IN (#{ids.join(',')})"] + end + conditions ||= ["#{Issue.table_name}.project_id = ?", id] + # Quick and dirty fix for Rails 2 compatibility + Issue.send(:with_scope, :find => { :conditions => conditions }) do + yield + end + end + + # Return all issues status changes for the project between the 2 given dates + def issues_status_changes(from, to) + Journal.find(:all, :include => [:issue, :details, :user], + :conditions => ["#{Journal.table_name}.journalized_type = 'Issue'" + + " AND #{Issue.table_name}.project_id = ?" + + " AND #{JournalDetail.table_name}.prop_key = 'status_id'" + + " AND #{Journal.table_name}.created_on BETWEEN ? AND ?", + id, from, to+1]) + end + + # returns latest created projects + # non public projects will be returned only if user is a member of those + def self.latest(user=nil, count=5) + find(:all, :limit => count, :conditions => visible_by(user), :order => "created_on DESC") + end + + def self.visible_by(user=nil) + if user && user.admin? + return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}" + elsif user && user.memberships.any? + return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND (#{Project.table_name}.is_public = #{connection.quoted_true} or #{Project.table_name}.id IN (#{user.memberships.collect{|m| m.project_id}.join(',')}))" + else + return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND #{Project.table_name}.is_public = #{connection.quoted_true}" + end + end + + def active? + self.status == STATUS_ACTIVE + end + + def archive + # Archive subprojects if any + children.each do |subproject| + subproject.archive + end + update_attribute :status, STATUS_ARCHIVED + end + + def unarchive + return false if parent && !parent.active? + update_attribute :status, STATUS_ACTIVE + end + + def active_children + children.select {|child| child.active?} + end + + # Deletes all project's members + def delete_all_members + Member.delete_all(['project_id = ?', id]) + end + + # Users issues can be assigned to + def assignable_users + members.select {|m| m.role.assignable?}.collect {|m| m.user}.sort + end + + # Returns the mail adresses of users that should be always notified on project events + def recipients + members.select {|m| m.mail_notification? || m.user.mail_notification?}.collect {|m| m.user.mail} + end + + # Returns an array of all custom fields enabled for project issues + # (explictly associated custom fields and custom fields enabled for all projects) + def custom_fields_for_issues(tracker) + all_custom_fields.select {|c| tracker.custom_fields.include? c } + end + + def all_custom_fields + @all_custom_fields ||= (IssueCustomField.for_all + custom_fields).uniq + end + + def <=>(project) + name.downcase <=> project.name.downcase + end + + def allows_to?(action) + if action.is_a? Hash + allowed_actions.include? "#{action[:controller]}/#{action[:action]}" + else + allowed_permissions.include? action + end + end + + def module_enabled?(module_name) + module_name = module_name.to_s + enabled_modules.detect {|m| m.name == module_name} + end + + def enabled_module_names=(module_names) + enabled_modules.clear + module_names = [] unless module_names && module_names.is_a?(Array) + module_names.each do |name| + enabled_modules << EnabledModule.new(:name => name.to_s) + end + end + +protected + def validate + errors.add(parent_id, " must be a root project") if parent and parent.parent + errors.add_to_base("A project with subprojects can't be a subproject") if parent and children.size > 0 + end + +private + def allowed_permissions + @allowed_permissions ||= begin + module_names = enabled_modules.collect {|m| m.name} + Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name} + end + end + + def allowed_actions + @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten + end +end diff --git a/rest_sys/app/models/project_custom_field.rb b/rest_sys/app/models/project_custom_field.rb new file mode 100644 index 000000000..f0dab6913 --- /dev/null +++ b/rest_sys/app/models/project_custom_field.rb @@ -0,0 +1,22 @@ +# redMine - project management software +# Copyright (C) 2006 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class ProjectCustomField < CustomField + def type_name + :label_project_plural + end +end diff --git a/rest_sys/app/models/query.rb b/rest_sys/app/models/query.rb new file mode 100644 index 000000000..ded8be770 --- /dev/null +++ b/rest_sys/app/models/query.rb @@ -0,0 +1,342 @@ +# redMine - project management software +# Copyright (C) 2006-2007 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class QueryColumn + attr_accessor :name, :sortable + include GLoc + + def initialize(name, options={}) + self.name = name + self.sortable = options[:sortable] + end + + def caption + set_language_if_valid(User.current.language) + l("field_#{name}") + end +end + +class QueryCustomFieldColumn < QueryColumn + + def initialize(custom_field) + self.name = "cf_#{custom_field.id}".to_sym + self.sortable = false + @cf = custom_field + end + + def caption + @cf.name + end + + def custom_field + @cf + end +end + +class Query < ActiveRecord::Base + belongs_to :project + belongs_to :user + serialize :filters + serialize :column_names + + attr_protected :project, :user + + validates_presence_of :name, :on => :save + validates_length_of :name, :maximum => 255 + + @@operators = { "=" => :label_equals, + "!" => :label_not_equals, + "o" => :label_open_issues, + "c" => :label_closed_issues, + "!*" => :label_none, + "*" => :label_all, + ">=" => '>=', + "<=" => '<=', + " :label_in_less_than, + ">t+" => :label_in_more_than, + "t+" => :label_in, + "t" => :label_today, + "w" => :label_this_week, + ">t-" => :label_less_than_ago, + " :label_more_than_ago, + "t-" => :label_ago, + "~" => :label_contains, + "!~" => :label_not_contains } + + cattr_reader :operators + + @@operators_by_filter_type = { :list => [ "=", "!" ], + :list_status => [ "o", "=", "!", "c", "*" ], + :list_optional => [ "=", "!", "!*", "*" ], + :list_one_or_more => [ "*", "=" ], + :date => [ "t+", "t+", "t", "w", ">t-", " [ ">t-", " [ "=", "~", "!", "!~" ], + :text => [ "~", "!~" ], + :integer => [ "=", ">=", "<=" ] } + + cattr_reader :operators_by_filter_type + + @@available_columns = [ + QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position"), + QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position"), + QueryColumn.new(:priority, :sortable => "#{Enumeration.table_name}.position"), + QueryColumn.new(:subject), + QueryColumn.new(:assigned_to, :sortable => "#{User.table_name}.lastname"), + QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on"), + QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name"), + QueryColumn.new(:fixed_version), + QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"), + QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"), + QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"), + QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio"), + QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on"), + ] + cattr_reader :available_columns + + def initialize(attributes = nil) + super attributes + self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} } + set_language_if_valid(User.current.language) + end + + def validate + filters.each_key do |field| + errors.add label_for(field), :activerecord_error_blank unless + # filter requires one or more values + (values_for(field) and !values_for(field).first.empty?) or + # filter doesn't require any value + ["o", "c", "!*", "*", "t", "w"].include? operator_for(field) + end if filters + end + + def editable_by?(user) + return false unless user + return true if !is_public && self.user_id == user.id + is_public && user.allowed_to?(:manage_public_queries, project) + end + + def available_filters + return @available_filters if @available_filters + @available_filters = { "status_id" => { :type => :list_status, :order => 1, :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } }, + "tracker_id" => { :type => :list, :order => 2, :values => Tracker.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } }, + "priority_id" => { :type => :list, :order => 3, :values => Enumeration.find(:all, :conditions => ['opt=?','IPRI']).collect{|s| [s.name, s.id.to_s] } }, + "subject" => { :type => :text, :order => 8 }, + "created_on" => { :type => :date_past, :order => 9 }, + "updated_on" => { :type => :date_past, :order => 10 }, + "start_date" => { :type => :date, :order => 11 }, + "due_date" => { :type => :date, :order => 12 }, + "done_ratio" => { :type => :integer, :order => 13 }} + + user_values = [] + user_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged? + if project + user_values += project.users.sort.collect{|s| [s.name, s.id.to_s] } + else + # members of the user's projects + user_values += User.current.projects.collect(&:users).flatten.uniq.sort.collect{|s| [s.name, s.id.to_s] } + end + @available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => user_values } unless user_values.empty? + @available_filters["author_id"] = { :type => :list, :order => 5, :values => user_values } unless user_values.empty? + + if project + # project specific filters + @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => @project.issue_categories.collect{|s| [s.name, s.id.to_s] } } + @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => @project.versions.sort.collect{|s| [s.name, s.id.to_s] } } + unless @project.active_children.empty? + @available_filters["subproject_id"] = { :type => :list_one_or_more, :order => 13, :values => @project.active_children.collect{|s| [s.name, s.id.to_s] } } + end + @project.all_custom_fields.select(&:is_filter?).each do |field| + case field.field_format + when "text" + options = { :type => :text, :order => 20 } + when "list" + options = { :type => :list_optional, :values => field.possible_values, :order => 20} + when "date" + options = { :type => :date, :order => 20 } + when "bool" + options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 } + else + options = { :type => :string, :order => 20 } + end + @available_filters["cf_#{field.id}"] = options.merge({ :name => field.name }) + end + # remove category filter if no category defined + @available_filters.delete "category_id" if @available_filters["category_id"][:values].empty? + end + @available_filters + end + + def add_filter(field, operator, values) + # values must be an array + return unless values and values.is_a? Array # and !values.first.empty? + # check if field is defined as an available filter + if available_filters.has_key? field + filter_options = available_filters[field] + # check if operator is allowed for that filter + #if @@operators_by_filter_type[filter_options[:type]].include? operator + # 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 } + end + end + + def add_short_filter(field, expression) + return unless expression + parms = expression.scan(/^(o|c|\!|\*)?(.*)$/).first + add_filter field, (parms[0] || "="), [parms[1] || ""] + end + + def has_filter?(field) + filters and filters[field] + end + + def operator_for(field) + has_filter?(field) ? filters[field][:operator] : nil + end + + def values_for(field) + has_filter?(field) ? filters[field][:values] : nil + end + + def label_for(field) + label = @available_filters[field][:name] if @available_filters.has_key?(field) + label ||= field.gsub(/\_id$/, "") + end + + def available_columns + return @available_columns if @available_columns + @available_columns = Query.available_columns + @available_columns += (project ? + project.custom_fields : + IssueCustomField.find(:all, :conditions => {:is_for_all => true}) + ).collect {|cf| QueryCustomFieldColumn.new(cf) } + end + + def columns + if has_default_columns? + available_columns.select {|c| Setting.issue_list_default_columns.include?(c.name.to_s) } + else + # preserve the column_names order + column_names.collect {|name| available_columns.find {|col| col.name == name}}.compact + end + end + + def column_names=(names) + names = names.select {|n| n.is_a?(Symbol) || !n.blank? } if names + names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym } if names + write_attribute(:column_names, names) + end + + def has_column?(column) + column_names && column_names.include?(column.name) + end + + def has_default_columns? + column_names.nil? || column_names.empty? + end + + def statement + # project/subprojects clause + clause = '' + if project && has_filter?("subproject_id") + subproject_ids = [] + if operator_for("subproject_id") == "=" + subproject_ids = values_for("subproject_id").each(&:to_i) + else + subproject_ids = project.active_children.collect{|p| p.id} + end + clause << "#{Issue.table_name}.project_id IN (%d,%s)" % [project.id, subproject_ids.join(",")] if project + elsif project + clause << "#{Issue.table_name}.project_id=%d" % project.id + else + clause << Project.visible_by(User.current) + end + + # filters clauses + filters_clauses = [] + filters.each_key do |field| + next if field == "subproject_id" + v = values_for(field).clone + next unless v and !v.empty? + + sql = '' + if field =~ /^cf_(\d+)$/ + # custom field + db_table = CustomValue.table_name + db_field = 'value' + sql << "#{Issue.table_name}.id IN (SELECT #{db_table}.customized_id FROM #{db_table} where #{db_table}.customized_type='Issue' AND #{db_table}.customized_id=#{Issue.table_name}.id AND #{db_table}.custom_field_id=#{$1} AND " + else + # regular field + db_table = Issue.table_name + db_field = field + sql << '(' + end + + # "me" value subsitution + if %w(assigned_to_id author_id).include?(field) + v.push(User.current.logged? ? User.current.id.to_s : "0") if v.delete("me") + end + + case operator_for field + when "=" + sql = sql + "#{db_table}.#{db_field} IN (" + v.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")" + when "!" + sql = sql + "#{db_table}.#{db_field} NOT IN (" + v.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")" + when "!*" + sql = sql + "#{db_table}.#{db_field} IS NULL" + when "*" + sql = sql + "#{db_table}.#{db_field} IS NOT NULL" + when ">=" + sql = sql + "#{db_table}.#{db_field} >= #{v.first.to_i}" + when "<=" + sql = sql + "#{db_table}.#{db_field} <= #{v.first.to_i}" + when "o" + sql = sql + "#{IssueStatus.table_name}.is_closed=#{connection.quoted_false}" if field == "status_id" + when "c" + sql = sql + "#{IssueStatus.table_name}.is_closed=#{connection.quoted_true}" if field == "status_id" + when ">t-" + sql = sql + "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date((Date.today - v.first.to_i).to_time), connection.quoted_date((Date.today + 1).to_time)] + when "t+" + sql = sql + "#{db_table}.#{db_field} >= '%s'" % connection.quoted_date((Date.today + v.first.to_i).to_time) + when " :destroy, :order => "#{Changeset.table_name}.revision DESC" + has_many :changes, :through => :changesets + + def scm + @scm ||= self.scm_adapter.new url, root_url, login, password + update_attribute(:root_url, @scm.root_url) if root_url.blank? + @scm + end + + def scm_name + self.class.scm_name + end + + def supports_cat? + scm.supports_cat? + end + + def supports_annotate? + scm.supports_annotate? + end + + def entries(path=nil, identifier=nil) + scm.entries(path, identifier) + end + + def diff(path, rev, rev_to, type) + scm.diff(path, rev, rev_to, type) + end + + # Default behaviour: we search in cached changesets + def changesets_for_path(path) + path = "/#{path}" unless path.starts_with?('/') + Change.find(:all, :include => :changeset, + :conditions => ["repository_id = ? AND path = ?", id, path], + :order => "committed_on DESC, #{Changeset.table_name}.revision DESC").collect(&:changeset) + end + + def latest_changeset + @latest_changeset ||= changesets.find(:first) + end + + def scan_changesets_for_issue_ids + self.changesets.each(&:scan_comment_for_issue_ids) + end + + # fetch new changesets for all repositories + # can be called periodically by an external script + # eg. ruby script/runner "Repository.fetch_changesets" + def self.fetch_changesets + find(:all).each(&:fetch_changesets) + end + + # scan changeset comments to find related and fixed issues for all repositories + def self.scan_changesets_for_issue_ids + find(:all).each(&:scan_changesets_for_issue_ids) + end + + def self.scm_name + 'Abstract' + end + + def self.available_scm + subclasses.collect {|klass| [klass.scm_name, klass.name]} + end + + def self.factory(klass_name, *args) + klass = "Repository::#{klass_name}".constantize + klass.new(*args) + rescue + nil + end +end diff --git a/rest_sys/app/models/repository/bazaar.rb b/rest_sys/app/models/repository/bazaar.rb new file mode 100644 index 000000000..6e387f957 --- /dev/null +++ b/rest_sys/app/models/repository/bazaar.rb @@ -0,0 +1,86 @@ +# redMine - project management software +# Copyright (C) 2006-2007 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require 'redmine/scm/adapters/bazaar_adapter' + +class Repository::Bazaar < Repository + attr_protected :root_url + validates_presence_of :url + + def scm_adapter + Redmine::Scm::Adapters::BazaarAdapter + end + + def self.scm_name + 'Bazaar' + end + + def entries(path=nil, identifier=nil) + entries = scm.entries(path, identifier) + if entries + entries.each do |e| + next if e.lastrev.revision.blank? + c = Change.find(:first, + :include => :changeset, + :conditions => ["#{Change.table_name}.revision = ? and #{Changeset.table_name}.repository_id = ?", e.lastrev.revision, id], + :order => "#{Changeset.table_name}.revision DESC") + if c + e.lastrev.identifier = c.changeset.revision + e.lastrev.name = c.changeset.revision + e.lastrev.author = c.changeset.committer + end + end + end + end + + def fetch_changesets + scm_info = scm.info + if scm_info + # latest revision found in database + db_revision = latest_changeset ? latest_changeset.revision : 0 + # latest revision in the repository + scm_revision = scm_info.lastrev.identifier.to_i + if db_revision < scm_revision + logger.debug "Fetching changesets for repository #{url}" if logger && logger.debug? + identifier_from = db_revision + 1 + while (identifier_from <= scm_revision) + # loads changesets by batches of 200 + identifier_to = [identifier_from + 199, scm_revision].min + revisions = scm.revisions('', identifier_to, identifier_from, :with_paths => true) + transaction do + revisions.reverse_each do |revision| + changeset = Changeset.create(:repository => self, + :revision => revision.identifier, + :committer => revision.author, + :committed_on => revision.time, + :scmid => revision.scmid, + :comments => revision.message) + + revision.paths.each do |change| + Change.create(:changeset => changeset, + :action => change[:action], + :path => change[:path], + :revision => change[:revision]) + end + end + end unless revisions.nil? + identifier_from = identifier_to + 1 + end + end + end + end +end diff --git a/rest_sys/app/models/repository/cvs.rb b/rest_sys/app/models/repository/cvs.rb new file mode 100644 index 000000000..16d906316 --- /dev/null +++ b/rest_sys/app/models/repository/cvs.rb @@ -0,0 +1,150 @@ +# redMine - project management software +# Copyright (C) 2006-2007 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require 'redmine/scm/adapters/cvs_adapter' +require 'digest/sha1' + +class Repository::Cvs < Repository + validates_presence_of :url, :root_url + + def scm_adapter + Redmine::Scm::Adapters::CvsAdapter + end + + def self.scm_name + 'CVS' + end + + def entry(path, identifier) + e = entries(path, identifier) + e ? e.first : nil + end + + def entries(path=nil, identifier=nil) + entries=scm.entries(path, identifier) + if entries + entries.each() do |entry| + unless entry.lastrev.nil? || entry.lastrev.identifier + change=changes.find_by_revision_and_path( entry.lastrev.revision, scm.with_leading_slash(entry.path) ) + if change + entry.lastrev.identifier=change.changeset.revision + entry.lastrev.author=change.changeset.committer + entry.lastrev.revision=change.revision + entry.lastrev.branch=change.branch + end + end + end + end + entries + end + + def diff(path, rev, rev_to, type) + #convert rev to revision. CVS can't handle changesets here + diff=[] + changeset_from=changesets.find_by_revision(rev) + if rev_to.to_i > 0 + changeset_to=changesets.find_by_revision(rev_to) + end + changeset_from.changes.each() do |change_from| + + revision_from=nil + revision_to=nil + + revision_from=change_from.revision if path.nil? || (change_from.path.starts_with? scm.with_leading_slash(path)) + + if revision_from + if changeset_to + changeset_to.changes.each() do |change_to| + revision_to=change_to.revision if change_to.path==change_from.path + end + end + unless revision_to + revision_to=scm.get_previous_revision(revision_from) + end + diff=diff+scm.diff(change_from.path, revision_from, revision_to, type) + end + end + return diff + end + + def fetch_changesets + #not the preferred way with CVS. maybe we should introduce always a cron-job for this + last_commit = changesets.maximum(:committed_on) + + # some nifty bits to introduce a commit-id with cvs + # natively cvs doesn't provide any kind of changesets, there is only a revision per file. + # we now take a guess using the author, the commitlog and the commit-date. + + # last one is the next step to take. the commit-date is not equal for all + # commits in one changeset. cvs update the commit-date when the *,v file was touched. so + # we use a small delta here, to merge all changes belonging to _one_ changeset + time_delta=10.seconds + + transaction do + scm.revisions('', last_commit, nil, :with_paths => true) do |revision| + # only add the change to the database, if it doen't exists. the cvs log + # is not exclusive at all. + unless changes.find_by_path_and_revision(scm.with_leading_slash(revision.paths[0][:path]), revision.paths[0][:revision]) + revision + cs = changesets.find(:first, :conditions=>{ + :committed_on=>revision.time-time_delta..revision.time+time_delta, + :committer=>revision.author, + :comments=>revision.message + }) + + # create a new changeset.... + unless cs + # we use a negative changeset-number here (just for inserting) + # later on, we calculate a continous positive number + next_rev = changesets.minimum(:revision) + next_rev = 0 if next_rev.nil? or next_rev > 0 + next_rev = next_rev - 1 + + cs=Changeset.create(:repository => self, + :revision => next_rev, + :committer => revision.author, + :committed_on => revision.time, + :comments => revision.message) + end + + #convert CVS-File-States to internal Action-abbrevations + #default action is (M)odified + action="M" + if revision.paths[0][:action]=="Exp" && revision.paths[0][:revision]=="1.1" + action="A" #add-action always at first revision (= 1.1) + elsif revision.paths[0][:action]=="dead" + action="D" #dead-state is similar to Delete + end + + Change.create(:changeset => cs, + :action => action, + :path => scm.with_leading_slash(revision.paths[0][:path]), + :revision => revision.paths[0][:revision], + :branch => revision.paths[0][:branch] + ) + end + end + + next_rev = [changesets.maximum(:revision) || 0, 0].max + changesets.find(:all, :conditions=>["revision < 0"], :order=>"committed_on ASC").each() do |changeset| + next_rev = next_rev + 1 + changeset.revision = next_rev + changeset.save! + end + end + end +end diff --git a/rest_sys/app/models/repository/darcs.rb b/rest_sys/app/models/repository/darcs.rb new file mode 100644 index 000000000..48cc246fb --- /dev/null +++ b/rest_sys/app/models/repository/darcs.rb @@ -0,0 +1,90 @@ +# redMine - project management software +# Copyright (C) 2006-2007 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require 'redmine/scm/adapters/darcs_adapter' + +class Repository::Darcs < Repository + validates_presence_of :url + + def scm_adapter + Redmine::Scm::Adapters::DarcsAdapter + end + + def self.scm_name + 'Darcs' + end + + def entries(path=nil, identifier=nil) + entries=scm.entries(path, identifier) + if entries + entries.each do |entry| + # Search the DB for the entry's last change + changeset = changesets.find_by_scmid(entry.lastrev.scmid) if entry.lastrev && !entry.lastrev.scmid.blank? + if changeset + entry.lastrev.identifier = changeset.revision + entry.lastrev.name = changeset.revision + entry.lastrev.time = changeset.committed_on + entry.lastrev.author = changeset.committer + end + end + end + entries + end + + def diff(path, rev, rev_to, type) + patch_from = changesets.find_by_revision(rev) + patch_to = changesets.find_by_revision(rev_to) if rev_to + if path.blank? + path = patch_from.changes.collect{|change| change.path}.join(' ') + end + scm.diff(path, patch_from.scmid, patch_to.scmid, type) + end + + def fetch_changesets + scm_info = scm.info + if scm_info + db_last_id = latest_changeset ? latest_changeset.scmid : nil + next_rev = latest_changeset ? latest_changeset.revision + 1 : 1 + # latest revision in the repository + scm_revision = scm_info.lastrev.scmid + unless changesets.find_by_scmid(scm_revision) + revisions = scm.revisions('', db_last_id, nil, :with_path => true) + transaction do + revisions.reverse_each do |revision| + changeset = Changeset.create(:repository => self, + :revision => next_rev, + :scmid => revision.scmid, + :committer => revision.author, + :committed_on => revision.time, + :comments => revision.message) + + next if changeset.new_record? + + revision.paths.each do |change| + Change.create(:changeset => changeset, + :action => change[:action], + :path => change[:path], + :from_path => change[:from_path], + :from_revision => change[:from_revision]) + end + next_rev += 1 + end if revisions + end + end + end + end +end diff --git a/rest_sys/app/models/repository/mercurial.rb b/rest_sys/app/models/repository/mercurial.rb new file mode 100644 index 000000000..5d9ea9cd4 --- /dev/null +++ b/rest_sys/app/models/repository/mercurial.rb @@ -0,0 +1,81 @@ +# redMine - project management software +# Copyright (C) 2006-2007 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require 'redmine/scm/adapters/mercurial_adapter' + +class Repository::Mercurial < Repository + attr_protected :root_url + validates_presence_of :url + + def scm_adapter + Redmine::Scm::Adapters::MercurialAdapter + end + + def self.scm_name + 'Mercurial' + end + + def entries(path=nil, identifier=nil) + entries=scm.entries(path, identifier) + if entries + entries.each do |entry| + next unless entry.is_file? + # Search the DB for the entry's last change + change = changes.find(:first, :conditions => ["path = ?", scm.with_leading_slash(entry.path)], :order => "#{Changeset.table_name}.committed_on DESC") + if change + entry.lastrev.identifier = change.changeset.revision + entry.lastrev.name = change.changeset.revision + entry.lastrev.author = change.changeset.committer + entry.lastrev.revision = change.revision + end + end + end + entries + end + + def fetch_changesets + scm_info = scm.info + if scm_info + # latest revision found in database + db_revision = latest_changeset ? latest_changeset.revision : nil + # latest revision in the repository + scm_revision = scm_info.lastrev.identifier.to_i + + unless changesets.find_by_revision(scm_revision) + revisions = scm.revisions('', db_revision, nil) + transaction do + revisions.reverse_each do |revision| + changeset = Changeset.create(:repository => self, + :revision => revision.identifier, + :scmid => revision.scmid, + :committer => revision.author, + :committed_on => revision.time, + :comments => revision.message) + + revision.paths.each do |change| + Change.create(:changeset => changeset, + :action => change[:action], + :path => change[:path], + :from_path => change[:from_path], + :from_revision => change[:from_revision]) + end + end + end + end + end + end +end diff --git a/rest_sys/app/models/repository/subversion.rb b/rest_sys/app/models/repository/subversion.rb new file mode 100644 index 000000000..a0485608d --- /dev/null +++ b/rest_sys/app/models/repository/subversion.rb @@ -0,0 +1,74 @@ +# redMine - project management software +# Copyright (C) 2006-2007 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require 'redmine/scm/adapters/subversion_adapter' + +class Repository::Subversion < Repository + attr_protected :root_url + validates_presence_of :url + validates_format_of :url, :with => /^(http|https|svn|svn\+ssh|file):\/\/.+/i + + def scm_adapter + Redmine::Scm::Adapters::SubversionAdapter + end + + def self.scm_name + 'Subversion' + end + + def changesets_for_path(path) + revisions = scm.revisions(path) + revisions ? changesets.find_all_by_revision(revisions.collect(&:identifier), :order => "committed_on DESC") : [] + end + + def fetch_changesets + scm_info = scm.info + if scm_info + # latest revision found in database + db_revision = latest_changeset ? latest_changeset.revision : 0 + # latest revision in the repository + scm_revision = scm_info.lastrev.identifier.to_i + if db_revision < scm_revision + logger.debug "Fetching changesets for repository #{url}" if logger && logger.debug? + identifier_from = db_revision + 1 + while (identifier_from <= scm_revision) + # loads changesets by batches of 200 + identifier_to = [identifier_from + 199, scm_revision].min + revisions = scm.revisions('', identifier_to, identifier_from, :with_paths => true) + transaction do + revisions.reverse_each do |revision| + changeset = Changeset.create(:repository => self, + :revision => revision.identifier, + :committer => revision.author, + :committed_on => revision.time, + :comments => revision.message) + + revision.paths.each do |change| + Change.create(:changeset => changeset, + :action => change[:action], + :path => change[:path], + :from_path => change[:from_path], + :from_revision => change[:from_revision]) + end + end + end unless revisions.nil? + identifier_from = identifier_to + 1 + end + end + end + end +end diff --git a/rest_sys/app/models/role.rb b/rest_sys/app/models/role.rb new file mode 100644 index 000000000..015146dc4 --- /dev/null +++ b/rest_sys/app/models/role.rb @@ -0,0 +1,108 @@ +# redMine - project management software +# Copyright (C) 2006 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class Role < ActiveRecord::Base + # Built-in roles + BUILTIN_NON_MEMBER = 1 + BUILTIN_ANONYMOUS = 2 + + before_destroy :check_deletable + has_many :workflows, :dependent => :delete_all + has_many :members + acts_as_list + + serialize :permissions + attr_protected :builtin + + validates_presence_of :name + validates_uniqueness_of :name + validates_length_of :name, :maximum => 30 + validates_format_of :name, :with => /^[\w\s\'\-]*$/i + + def permissions + read_attribute(:permissions) || [] + end + + def permissions=(perms) + perms = perms.collect {|p| p.to_sym unless p.blank? }.compact if perms + write_attribute(:permissions, perms) + end + + def <=>(role) + position <=> role.position + end + + # Return true if the role is a builtin role + def builtin? + self.builtin != 0 + end + + # Return true if the role is a project member role + def member? + !self.builtin? + end + + # Return true if role is allowed to do the specified action + # action can be: + # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit') + # * a permission Symbol (eg. :edit_project) + def allowed_to?(action) + if action.is_a? Hash + allowed_actions.include? "#{action[:controller]}/#{action[:action]}" + else + allowed_permissions.include? action + end + end + + # Return all the permissions that can be given to the role + def setable_permissions + setable_permissions = Redmine::AccessControl.permissions - Redmine::AccessControl.public_permissions + setable_permissions -= Redmine::AccessControl.members_only_permissions if self.builtin == BUILTIN_NON_MEMBER + setable_permissions -= Redmine::AccessControl.loggedin_only_permissions if self.builtin == BUILTIN_ANONYMOUS + setable_permissions + end + + # Find all the roles that can be given to a project member + def self.find_all_givable + find(:all, :conditions => {:builtin => 0}, :order => 'position') + end + + # Return the builtin 'non member' role + def self.non_member + find(:first, :conditions => {:builtin => BUILTIN_NON_MEMBER}) || raise('Missing non-member builtin role.') + end + + # Return the builtin 'anonymous' role + def self.anonymous + find(:first, :conditions => {:builtin => BUILTIN_ANONYMOUS}) || raise('Missing anonymous builtin role.') + end + + +private + def allowed_permissions + @allowed_permissions ||= permissions + Redmine::AccessControl.public_permissions.collect {|p| p.name} + end + + def allowed_actions + @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten + end + + def check_deletable + raise "Can't delete role" if members.any? + raise "Can't delete builtin role" if builtin? + end +end diff --git a/rest_sys/app/models/setting.rb b/rest_sys/app/models/setting.rb new file mode 100644 index 000000000..4d4cf0045 --- /dev/null +++ b/rest_sys/app/models/setting.rb @@ -0,0 +1,119 @@ +# redMine - project management software +# Copyright (C) 2006-2007 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class Setting < ActiveRecord::Base + + DATE_FORMATS = [ + '%Y-%m-%d', + '%d/%m/%Y', + '%d.%m.%Y', + '%d-%m-%Y', + '%m/%d/%Y', + '%d %b %Y', + '%d %B %Y', + '%b %d, %Y', + '%B %d, %Y' + ] + + TIME_FORMATS = [ + '%H:%M', + '%I:%M %p' + ] + + cattr_accessor :available_settings + @@available_settings = YAML::load(File.open("#{RAILS_ROOT}/config/settings.yml")) + Redmine::Plugin.registered_plugins.each do |id, plugin| + next unless plugin.settings + @@available_settings["plugin_#{id}"] = {'default' => plugin.settings[:default], 'serialized' => true} + end + + validates_uniqueness_of :name + validates_inclusion_of :name, :in => @@available_settings.keys + validates_numericality_of :value, :only_integer => true, :if => Proc.new { |setting| @@available_settings[setting.name]['format'] == 'int' } + + # Hash used to cache setting values + @cached_settings = {} + @cached_cleared_on = Time.now + + def value + v = read_attribute(:value) + # Unserialize serialized settings + v = YAML::load(v) if @@available_settings[name]['serialized'] && v.is_a?(String) + v + end + + def value=(v) + v = v.to_yaml if v && @@available_settings[name]['serialized'] + write_attribute(:value, v) + end + + # Returns the value of the setting named name + def self.[](name) + v = @cached_settings[name] + v ? v : (@cached_settings[name] = find_or_default(name).value) + end + + def self.[]=(name, v) + setting = find_or_default(name) + setting.value = (v ? v : "") + @cached_settings[name] = nil + setting.save + setting.value + end + + # Defines getter and setter for each setting + # Then setting values can be read using: Setting.some_setting_name + # or set using Setting.some_setting_name = "some value" + @@available_settings.each do |name, params| + src = <<-END_SRC + def self.#{name} + self[:#{name}] + end + + def self.#{name}? + self[:#{name}].to_i > 0 + end + + def self.#{name}=(value) + self[:#{name}] = value + end + END_SRC + class_eval src, __FILE__, __LINE__ + 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 + def self.check_cache + settings_updated_on = Setting.maximum(:updated_on) + if settings_updated_on && @cached_cleared_on <= settings_updated_on + @cached_settings.clear + @cached_cleared_on = Time.now + logger.info "Settings cache cleared." if logger + end + end + +private + # Returns the Setting instance for the setting named name + # (record found in database or new record with default value) + def self.find_or_default(name) + name = name.to_s + raise "There's no setting named #{name}" unless @@available_settings.has_key?(name) + setting = find_by_name(name) + setting ||= new(:name => name, :value => @@available_settings[name]['default']) if @@available_settings.has_key? name + end +end diff --git a/rest_sys/app/models/time_entry.rb b/rest_sys/app/models/time_entry.rb new file mode 100644 index 000000000..905857073 --- /dev/null +++ b/rest_sys/app/models/time_entry.rb @@ -0,0 +1,50 @@ +# redMine - project management software +# Copyright (C) 2006-2007 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class TimeEntry < ActiveRecord::Base + # could have used polymorphic association + # project association here allows easy loading of time entries at project level with one database trip + belongs_to :project + belongs_to :issue + belongs_to :user + belongs_to :activity, :class_name => 'Enumeration', :foreign_key => :activity_id + + attr_protected :project_id, :user_id, :tyear, :tmonth, :tweek + + validates_presence_of :user_id, :activity_id, :project_id, :hours, :spent_on + validates_numericality_of :hours, :allow_nil => true + validates_length_of :comments, :maximum => 255 + + def before_validation + self.project = issue.project if issue && project.nil? + end + + def validate + errors.add :hours, :activerecord_error_invalid if hours && (hours < 0 || hours >= 1000) + errors.add :project_id, :activerecord_error_invalid if project.nil? + errors.add :issue_id, :activerecord_error_invalid if (issue_id && !issue) || (issue && project!=issue.project) + end + + # tyear, tmonth, tweek assigned where setting spent_on attributes + # these attributes make time aggregations easier + def spent_on=(date) + super + self.tyear = spent_on ? spent_on.year : nil + self.tmonth = spent_on ? spent_on.month : nil + self.tweek = spent_on ? Date.civil(spent_on.year, spent_on.month, spent_on.day).cweek : nil + end +end diff --git a/rest_sys/app/models/token.rb b/rest_sys/app/models/token.rb new file mode 100644 index 000000000..0e8c2c3e2 --- /dev/null +++ b/rest_sys/app/models/token.rb @@ -0,0 +1,44 @@ +# redMine - project management software +# Copyright (C) 2006 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class Token < ActiveRecord::Base + belongs_to :user + + @@validity_time = 1.day + + def before_create + self.value = Token.generate_token_value + end + + # Return true if token has expired + def expired? + return Time.now > self.created_on + @@validity_time + end + + # Delete all expired tokens + def self.destroy_expired + Token.delete_all ["action <> 'feeds' AND created_on < ?", Time.now - @@validity_time] + end + +private + def self.generate_token_value + chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a + token_value = '' + 40.times { |i| token_value << chars[rand(chars.size-1)] } + token_value + end +end diff --git a/rest_sys/app/models/tracker.rb b/rest_sys/app/models/tracker.rb new file mode 100644 index 000000000..8d8647747 --- /dev/null +++ b/rest_sys/app/models/tracker.rb @@ -0,0 +1,44 @@ +# redMine - project management software +# Copyright (C) 2006 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class Tracker < ActiveRecord::Base + before_destroy :check_integrity + has_many :issues + has_many :workflows, :dependent => :delete_all + has_and_belongs_to_many :custom_fields, :class_name => 'IssueCustomField', :join_table => "#{table_name_prefix}custom_fields_trackers#{table_name_suffix}", :association_foreign_key => 'custom_field_id' + acts_as_list + + validates_presence_of :name + validates_uniqueness_of :name + validates_length_of :name, :maximum => 30 + validates_format_of :name, :with => /^[\w\s\'\-]*$/i + + def to_s; name end + + def <=>(tracker) + name <=> tracker.name + end + + def self.all + find(:all, :order => 'position') + end + +private + def check_integrity + raise "Can't delete tracker" if Issue.find(:first, :conditions => ["tracker_id=?", self.id]) + end +end diff --git a/rest_sys/app/models/user.rb b/rest_sys/app/models/user.rb new file mode 100644 index 000000000..737a8cc8e --- /dev/null +++ b/rest_sys/app/models/user.rb @@ -0,0 +1,255 @@ +# redMine - project management software +# Copyright (C) 2006-2007 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require "digest/sha1" + +class User < ActiveRecord::Base + # Account statuses + STATUS_ANONYMOUS = 0 + STATUS_ACTIVE = 1 + STATUS_REGISTERED = 2 + STATUS_LOCKED = 3 + + has_many :memberships, :class_name => 'Member', :include => [ :project, :role ], :conditions => "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}", :order => "#{Project.table_name}.name", :dependent => :delete_all + has_many :projects, :through => :memberships + has_many :custom_values, :dependent => :delete_all, :as => :customized + has_many :issue_categories, :foreign_key => 'assigned_to_id', :dependent => :nullify + has_one :preference, :dependent => :destroy, :class_name => 'UserPreference' + has_one :rss_token, :dependent => :destroy, :class_name => 'Token', :conditions => "action='feeds'" + belongs_to :auth_source + + attr_accessor :password, :password_confirmation + attr_accessor :last_before_login_on + # Prevents unauthorized assignments + attr_protected :login, :admin, :password, :password_confirmation, :hashed_password + + validates_presence_of :login, :firstname, :lastname, :mail, :if => Proc.new { |user| !user.is_a?(AnonymousUser) } + validates_uniqueness_of :login, :mail + # Login must contain lettres, numbers, underscores only + validates_format_of :login, :with => /^[a-z0-9_\-@\.]*$/i + validates_length_of :login, :maximum => 30 + validates_format_of :firstname, :lastname, :with => /^[\w\s\'\-]*$/i + validates_length_of :firstname, :lastname, :maximum => 30 + validates_format_of :mail, :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i, :allow_nil => true + validates_length_of :mail, :maximum => 60, :allow_nil => true + validates_length_of :password, :minimum => 4, :allow_nil => true + validates_confirmation_of :password, :allow_nil => true + validates_associated :custom_values, :on => :update + + def before_create + self.mail_notification = false + true + end + + def before_save + # update hashed_password if password was set + self.hashed_password = User.hash_password(self.password) if self.password + end + + def self.active + with_scope :find => { :conditions => [ "status = ?", STATUS_ACTIVE ] } do + yield + end + end + + def self.find_active(*args) + active do + find(*args) + end + end + + # Returns the user that matches provided login and password, or nil + def self.try_to_login(login, password) + user = find(:first, :conditions => ["login=?", login]) + if user + # user is already in local database + return nil if !user.active? + if user.auth_source + # user has an external authentication method + return nil unless user.auth_source.authenticate(login, password) + else + # authentication with local password + return nil unless User.hash_password(password) == user.hashed_password + end + else + # user is not yet registered, try to authenticate with available sources + attrs = AuthSource.authenticate(login, password) + if attrs + onthefly = new(*attrs) + onthefly.login = login + onthefly.language = Setting.default_language + if onthefly.save + user = find(:first, :conditions => ["login=?", login]) + logger.info("User '#{user.login}' created on the fly.") if logger + end + end + end + user.update_attribute(:last_login_on, Time.now) if user + user + + rescue => text + raise text + end + + # Return user's full name for display + def name + "#{firstname} #{lastname}" + end + + def active? + self.status == STATUS_ACTIVE + end + + def registered? + self.status == STATUS_REGISTERED + end + + def locked? + self.status == STATUS_LOCKED + end + + def check_password?(clear_password) + User.hash_password(clear_password) == self.hashed_password + end + + def pref + self.preference ||= UserPreference.new(:user => self) + end + + def time_zone + self.pref.time_zone.nil? ? nil : TimeZone[self.pref.time_zone] + end + + # Return user's RSS key (a 40 chars long string), used to access feeds + def rss_key + token = self.rss_token || Token.create(:user => self, :action => 'feeds') + token.value + end + + # Return an array of project ids for which the user has explicitly turned mail notifications on + def notified_projects_ids + @notified_projects_ids ||= memberships.select {|m| m.mail_notification?}.collect(&:project_id) + end + + def notified_project_ids=(ids) + Member.update_all("mail_notification = #{connection.quoted_false}", ['user_id = ?', id]) + Member.update_all("mail_notification = #{connection.quoted_true}", ['user_id = ? AND project_id IN (?)', id, ids]) if ids && !ids.empty? + @notified_projects_ids = nil + notified_projects_ids + end + + def self.find_by_rss_key(key) + token = Token.find_by_value(key) + token && token.user.active? ? token.user : nil + end + + def self.find_by_autologin_key(key) + token = Token.find_by_action_and_value('autologin', key) + token && (token.created_on > Setting.autologin.to_i.day.ago) && token.user.active? ? token.user : nil + end + + def <=>(user) + user.nil? ? -1 : (lastname == user.lastname ? firstname <=> user.firstname : lastname <=> user.lastname) + end + + def to_s + name + end + + def logged? + true + end + + # Return user's role for project + def role_for_project(project) + # No role on archived projects + return nil unless project && project.active? + if logged? + # Find project membership + membership = memberships.detect {|m| m.project_id == project.id} + if membership + membership.role + else + @role_non_member ||= Role.non_member + end + else + @role_anonymous ||= Role.anonymous + end + end + + # Return true if the user is a member of project + def member_of?(project) + role_for_project(project).member? + end + + # Return true if the user is allowed to do the specified action on project + # action can be: + # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit') + # * a permission Symbol (eg. :edit_project) + def allowed_to?(action, project) + # No action allowed on archived projects + return false unless project.active? + # No action allowed on disabled modules + return false unless project.allows_to?(action) + # Admin users are authorized for anything else + return true if admin? + + role = role_for_project(project) + return false unless role + role.allowed_to?(action) && (project.is_public? || role.member?) + end + + def self.current=(user) + @current_user = user + end + + def self.current + @current_user ||= User.anonymous + end + + def self.anonymous + return @anonymous_user if @anonymous_user + anonymous_user = AnonymousUser.find(:first) + if anonymous_user.nil? + anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :mail => '', :login => '', :status => 0) + raise 'Unable to create the anonymous user.' if anonymous_user.new_record? + end + @anonymous_user = anonymous_user + end + +private + # Return password digest + def self.hash_password(clear_password) + Digest::SHA1.hexdigest(clear_password || "") + end +end + +class AnonymousUser < User + + def validate_on_create + # There should be only one AnonymousUser in the database + errors.add_to_base 'An anonymous user already exists.' if AnonymousUser.find(:first) + end + + # Overrides a few properties + def logged?; false end + def admin; false end + def name; 'Anonymous' end + def mail; nil end + def time_zone; nil end + def rss_key; nil end +end diff --git a/rest_sys/app/models/user_custom_field.rb b/rest_sys/app/models/user_custom_field.rb new file mode 100644 index 000000000..99e76eea5 --- /dev/null +++ b/rest_sys/app/models/user_custom_field.rb @@ -0,0 +1,23 @@ +# redMine - project management software +# Copyright (C) 2006 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class UserCustomField < CustomField + def type_name + :label_user_plural + end +end + diff --git a/rest_sys/app/models/user_preference.rb b/rest_sys/app/models/user_preference.rb new file mode 100644 index 000000000..1ed9e0fd9 --- /dev/null +++ b/rest_sys/app/models/user_preference.rb @@ -0,0 +1,49 @@ +# redMine - project management software +# Copyright (C) 2006 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class UserPreference < ActiveRecord::Base + belongs_to :user + serialize :others + + attr_protected :others + + def initialize(attributes = nil) + super + self.others ||= {} + end + + def before_save + self.others ||= {} + end + + def [](attr_name) + if attribute_present? attr_name + super + else + others ? others[attr_name] : nil + end + end + + def []=(attr_name, value) + if attribute_present? attr_name + super + else + self.others ||= {} + self.others.store attr_name, value + end + end +end diff --git a/rest_sys/app/models/version.rb b/rest_sys/app/models/version.rb new file mode 100644 index 000000000..266346c7b --- /dev/null +++ b/rest_sys/app/models/version.rb @@ -0,0 +1,96 @@ +# redMine - project management software +# Copyright (C) 2006 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class Version < ActiveRecord::Base + before_destroy :check_integrity + belongs_to :project + has_many :fixed_issues, :class_name => 'Issue', :foreign_key => 'fixed_version_id' + has_many :attachments, :as => :container, :dependent => :destroy + + validates_presence_of :name + validates_uniqueness_of :name, :scope => [:project_id] + validates_length_of :name, :maximum => 30 + validates_format_of :effective_date, :with => /^\d{4}-\d{2}-\d{2}$/, :message => :activerecord_error_not_a_date, :allow_nil => true + + def start_date + effective_date + end + + def due_date + effective_date + end + + # Returns true if the version is completed: due date reached and no open issues + def completed? + effective_date && (effective_date <= Date.today) && (open_issues_count == 0) + end + + def completed_pourcent + if fixed_issues.count == 0 + 0 + elsif open_issues_count == 0 + 100 + else + (closed_issues_count * 100 + Issue.sum('done_ratio', :include => 'status', :conditions => ["fixed_version_id = ? AND is_closed = ?", id, false]).to_f) / fixed_issues.count + end + end + + def closed_pourcent + if fixed_issues.count == 0 + 0 + else + closed_issues_count * 100.0 / fixed_issues.count + end + end + + # Returns true if the version is overdue: due date reached and some open issues + def overdue? + effective_date && (effective_date < Date.today) && (open_issues_count > 0) + end + + def open_issues_count + @open_issues_count ||= Issue.count(:all, :conditions => ["fixed_version_id = ? AND is_closed = ?", self.id, false], :include => :status) + end + + def closed_issues_count + @closed_issues_count ||= Issue.count(:all, :conditions => ["fixed_version_id = ? AND is_closed = ?", self.id, true], :include => :status) + end + + def wiki_page + if project.wiki && !wiki_page_title.blank? + @wiki_page ||= project.wiki.find_page(wiki_page_title) + end + @wiki_page + end + + def to_s; name end + + # Versions are sorted by effective_date + # Those with no effective_date are at the end, sorted by name + def <=>(version) + if self.effective_date + version.effective_date ? (self.effective_date <=> version.effective_date) : -1 + else + version.effective_date ? 1 : (self.name <=> version.name) + end + end + +private + def check_integrity + raise "Can't delete version" if self.fixed_issues.find(:first) + end +end diff --git a/rest_sys/app/models/watcher.rb b/rest_sys/app/models/watcher.rb new file mode 100644 index 000000000..cb6ff52ea --- /dev/null +++ b/rest_sys/app/models/watcher.rb @@ -0,0 +1,23 @@ +# redMine - project management software +# Copyright (C) 2006-2007 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class Watcher < ActiveRecord::Base + belongs_to :watchable, :polymorphic => true + belongs_to :user + + validates_uniqueness_of :user_id, :scope => [:watchable_type, :watchable_id] +end diff --git a/rest_sys/app/models/wiki.rb b/rest_sys/app/models/wiki.rb new file mode 100644 index 000000000..b6d6a9b50 --- /dev/null +++ b/rest_sys/app/models/wiki.rb @@ -0,0 +1,54 @@ +# redMine - project management software +# Copyright (C) 2006-2007 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class Wiki < ActiveRecord::Base + belongs_to :project + has_many :pages, :class_name => 'WikiPage', :dependent => :destroy + has_many :redirects, :class_name => 'WikiRedirect', :dependent => :delete_all + + validates_presence_of :start_page + validates_format_of :start_page, :with => /^[^,\.\/\?\;\|\:]*$/ + + # find the page with the given title + # if page doesn't exist, return a new page + def find_or_new_page(title) + title = start_page if title.blank? + find_page(title) || WikiPage.new(:wiki => self, :title => Wiki.titleize(title)) + end + + # find the page with the given title + def find_page(title, options = {}) + title = start_page if title.blank? + title = Wiki.titleize(title) + page = pages.find_by_title(title) + if !page && !(options[:with_redirect] == false) + # search for a redirect + redirect = redirects.find_by_title(title) + page = find_page(redirect.redirects_to, :with_redirect => false) if redirect + end + page + end + + # turn a string into a valid page title + def self.titleize(title) + # replace spaces with _ and remove unwanted caracters + title = title.gsub(/\s+/, '_').delete(',./?;|:') if title + # upcase the first letter + title = (title.slice(0..0).upcase + (title.slice(1..-1) || '')) if title + title + end +end diff --git a/rest_sys/app/models/wiki_content.rb b/rest_sys/app/models/wiki_content.rb new file mode 100644 index 000000000..d0a48467b --- /dev/null +++ b/rest_sys/app/models/wiki_content.rb @@ -0,0 +1,65 @@ +# redMine - project management software +# Copyright (C) 2006-2007 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require 'zlib' + +class WikiContent < ActiveRecord::Base + set_locking_column :version + belongs_to :page, :class_name => 'WikiPage', :foreign_key => 'page_id' + belongs_to :author, :class_name => 'User', :foreign_key => 'author_id' + validates_presence_of :text + + acts_as_versioned + class Version + belongs_to :page, :class_name => '::WikiPage', :foreign_key => 'page_id' + belongs_to :author, :class_name => '::User', :foreign_key => 'author_id' + attr_protected :data + + acts_as_event :title => Proc.new {|o| "#{l(:label_wiki_edit)}: #{o.page.title} (##{o.version})"}, + :description => :comments, + :datetime => :updated_on, + :url => Proc.new {|o| {:controller => 'wiki', :id => o.page.wiki.project_id, :page => o.page.title, :version => o.version}} + + def text=(plain) + case Setting.wiki_compression + when 'gzip' + begin + self.data = Zlib::Deflate.deflate(plain, Zlib::BEST_COMPRESSION) + self.compression = 'gzip' + rescue + self.data = plain + self.compression = '' + end + else + self.data = plain + self.compression = '' + end + plain + end + + def text + @text ||= case compression + when 'gzip' + Zlib::Inflate.inflate(data) + else + # uncompressed data + data + end + end + end + +end diff --git a/rest_sys/app/models/wiki_page.rb b/rest_sys/app/models/wiki_page.rb new file mode 100644 index 000000000..cbca4fd68 --- /dev/null +++ b/rest_sys/app/models/wiki_page.rb @@ -0,0 +1,115 @@ +# redMine - project management software +# Copyright (C) 2006-2007 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require 'diff' + +class WikiPage < ActiveRecord::Base + belongs_to :wiki + has_one :content, :class_name => 'WikiContent', :foreign_key => 'page_id', :dependent => :destroy + has_many :attachments, :as => :container, :dependent => :destroy + + acts_as_event :title => Proc.new {|o| "#{l(:label_wiki)}: #{o.title}"}, + :description => :text, + :datetime => :created_on, + :url => Proc.new {|o| {:controller => 'wiki', :id => o.wiki.project_id, :page => o.title}} + + acts_as_searchable :columns => ['title', 'text'], + :include => [:wiki, :content], + :project_key => "#{Wiki.table_name}.project_id" + + attr_accessor :redirect_existing_links + + validates_presence_of :title + validates_format_of :title, :with => /^[^,\.\/\?\;\|\s]*$/ + validates_uniqueness_of :title, :scope => :wiki_id, :case_sensitive => false + validates_associated :content + + def title=(value) + value = Wiki.titleize(value) + @previous_title = read_attribute(:title) if @previous_title.blank? + write_attribute(:title, value) + end + + def before_save + self.title = Wiki.titleize(title) + # Manage redirects if the title has changed + if !@previous_title.blank? && (@previous_title != title) && !new_record? + # Update redirects that point to the old title + wiki.redirects.find_all_by_redirects_to(@previous_title).each do |r| + r.redirects_to = title + r.title == r.redirects_to ? r.destroy : r.save + end + # Remove redirects for the new title + wiki.redirects.find_all_by_title(title).each(&:destroy) + # Create a redirect to the new title + wiki.redirects << WikiRedirect.new(:title => @previous_title, :redirects_to => title) unless redirect_existing_links == "0" + @previous_title = nil + end + end + + def before_destroy + # Remove redirects to this page + wiki.redirects.find_all_by_redirects_to(title).each(&:destroy) + end + + def pretty_title + WikiPage.pretty_title(title) + end + + def content_for_version(version=nil) + result = content.versions.find_by_version(version.to_i) if version + result ||= content + result + end + + def diff(version_to=nil, version_from=nil) + version_to = version_to ? version_to.to_i : self.content.version + version_from = version_from ? version_from.to_i : version_to - 1 + version_to, version_from = version_from, version_to unless version_from < version_to + + content_to = content.versions.find_by_version(version_to) + content_from = content.versions.find_by_version(version_from) + + (content_to && content_from) ? WikiDiff.new(content_to, content_from) : nil + end + + def self.pretty_title(str) + (str && str.is_a?(String)) ? str.tr('_', ' ') : str + end + + def project + wiki.project + end + + def text + content.text if content + end +end + +class WikiDiff + attr_reader :diff, :words, :content_to, :content_from + + def initialize(content_to, content_from) + @content_to = content_to + @content_from = content_from + @words = content_to.text.split(/(\s+)/) + @words = @words.select {|word| word != ' '} + words_from = content_from.text.split(/(\s+)/) + words_from = words_from.select {|word| word != ' '} + @diff = words_from.diff @words + end +end diff --git a/rest_sys/app/models/wiki_redirect.rb b/rest_sys/app/models/wiki_redirect.rb new file mode 100644 index 000000000..adc2b24c1 --- /dev/null +++ b/rest_sys/app/models/wiki_redirect.rb @@ -0,0 +1,23 @@ +# redMine - project management software +# Copyright (C) 2006-2007 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class WikiRedirect < ActiveRecord::Base + belongs_to :wiki + + validates_presence_of :title, :redirects_to + validates_length_of :title, :redirects_to, :maximum => 255 +end diff --git a/rest_sys/app/models/workflow.rb b/rest_sys/app/models/workflow.rb new file mode 100644 index 000000000..89322aa58 --- /dev/null +++ b/rest_sys/app/models/workflow.rb @@ -0,0 +1,24 @@ +# redMine - project management software +# Copyright (C) 2006 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class Workflow < ActiveRecord::Base + belongs_to :role + belongs_to :old_status, :class_name => 'IssueStatus', :foreign_key => 'old_status_id' + belongs_to :new_status, :class_name => 'IssueStatus', :foreign_key => 'new_status_id' + + validates_presence_of :role, :old_status, :new_status +end diff --git a/rest_sys/app/sweepers/issue_sweeper.rb b/rest_sys/app/sweepers/issue_sweeper.rb new file mode 100644 index 000000000..dc9020535 --- /dev/null +++ b/rest_sys/app/sweepers/issue_sweeper.rb @@ -0,0 +1,38 @@ +# redMine - project management software +# Copyright (C) 2006-2007 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class IssueSweeper < ActionController::Caching::Sweeper + observe Issue + + def after_save(issue) + expire_cache_for(issue) + end + + def after_destroy(issue) + expire_cache_for(issue) + end + +private + def expire_cache_for(issue) + # fragments of the main project + expire_fragment(Regexp.new("projects/(calendar|gantt)/#{issue.project_id}\\.")) + # fragments of the root project that include subprojects issues + unless issue.project.parent_id.nil? + expire_fragment(Regexp.new("projects/(calendar|gantt)/#{issue.project.parent_id}\\..*subprojects")) + end + end +end diff --git a/rest_sys/app/sweepers/project_sweeper.rb b/rest_sys/app/sweepers/project_sweeper.rb new file mode 100644 index 000000000..f64f6f564 --- /dev/null +++ b/rest_sys/app/sweepers/project_sweeper.rb @@ -0,0 +1,40 @@ +# redMine - project management software +# Copyright (C) 2006-2007 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class ProjectSweeper < ActionController::Caching::Sweeper + observe Project + + def before_save(project) + if project.new_record? + expire_cache_for(project.parent) if project.parent + else + project_before_update = Project.find(project.id) + return if project_before_update.parent_id == project.parent_id && project_before_update.status == project.status + expire_cache_for(project.parent) if project.parent + expire_cache_for(project_before_update.parent) if project_before_update.parent + end + end + + def after_destroy(project) + expire_cache_for(project.parent) if project.parent + end + +private + def expire_cache_for(project) + expire_fragment(Regexp.new("projects/(calendar|gantt)/#{project.id}\\.")) + end +end diff --git a/rest_sys/app/sweepers/version_sweeper.rb b/rest_sys/app/sweepers/version_sweeper.rb new file mode 100644 index 000000000..e1323e261 --- /dev/null +++ b/rest_sys/app/sweepers/version_sweeper.rb @@ -0,0 +1,34 @@ +# redMine - project management software +# Copyright (C) 2006-2007 Jean-Philippe Lang +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class VersionSweeper < ActionController::Caching::Sweeper + observe Version + + def after_save(version) + expire_cache_for(version) + end + + def after_destroy(version) + expire_cache_for(version) + end + +private + def expire_cache_for(version) + # calendar and gantt fragments of the project + expire_fragment(Regexp.new("projects/(calendar|gantt)/#{version.project_id}\\.")) + end +end diff --git a/rest_sys/app/views/account/login.rhtml b/rest_sys/app/views/account/login.rhtml new file mode 100644 index 000000000..5bfbfb8d6 --- /dev/null +++ b/rest_sys/app/views/account/login.rhtml @@ -0,0 +1,33 @@ +
+<% form_tag({:action=> "login"}) do %> + + + + + + + + + + + + + + + + + +

<%= text_field_tag 'login', nil, :size => 40 %>

<%= password_field_tag 'password', nil, :size => 40 %>
+ <% if Setting.autologin? %> + + <% end %> +
+ <% if Setting.lost_password? %> + <%= link_to l(:label_password_lost), :controller => 'account', :action => 'lost_password' %> + <% end %> + + +
+<%= javascript_tag "Form.Element.focus('login');" %> +<% end %> +
diff --git a/rest_sys/app/views/account/lost_password.rhtml b/rest_sys/app/views/account/lost_password.rhtml new file mode 100644 index 000000000..420e8f9b1 --- /dev/null +++ b/rest_sys/app/views/account/lost_password.rhtml @@ -0,0 +1,11 @@ +

<%=l(:label_password_lost)%>

+ +
+<% form_tag({:action=> "lost_password"}, :class => "tabular") do %> + +

+<%= text_field_tag 'mail', nil, :size => 40 %> +<%= submit_tag l(:button_submit) %>

+ +<% end %> +
diff --git a/rest_sys/app/views/account/password_recovery.rhtml b/rest_sys/app/views/account/password_recovery.rhtml new file mode 100644 index 000000000..7fdd2b2fd --- /dev/null +++ b/rest_sys/app/views/account/password_recovery.rhtml @@ -0,0 +1,15 @@ +

<%=l(:label_password_lost)%>

+ +<%= error_messages_for 'user' %> + +<% form_tag({:token => @token.value}) do %> +
+

+<%= password_field_tag 'new_password', nil, :size => 25 %>
+<%= l(:text_caracters_minimum, 4) %>

+ +

+<%= password_field_tag 'new_password_confirmation', nil, :size => 25 %>

+
+

<%= submit_tag l(:button_save) %>

+<% end %> diff --git a/rest_sys/app/views/account/register.rhtml b/rest_sys/app/views/account/register.rhtml new file mode 100644 index 000000000..c1425a380 --- /dev/null +++ b/rest_sys/app/views/account/register.rhtml @@ -0,0 +1,44 @@ +

<%=l(:label_register)%>

+ +<% form_tag({:action => 'register'}, :class => "tabular") do %> +<%= error_messages_for 'user' %> + +
+ +

+<%= text_field 'user', 'login', :size => 25 %>

+ +

+<%= password_field_tag 'password', nil, :size => 25 %>
+<%= l(:text_caracters_minimum, 4) %>

+ +

+<%= password_field_tag 'password_confirmation', nil, :size => 25 %>

+ +

+<%= text_field 'user', 'firstname' %>

+ +

+<%= text_field 'user', 'lastname' %>

+ +

+<%= text_field 'user', 'mail' %>

+ +

+<%= select("user", "language", lang_options_for_select) %>

+ +<% for @custom_value in @custom_values %> +

<%= custom_field_tag_with_label @custom_value %>

+<% end %> + +
+ +<%= submit_tag l(:button_submit) %> +<% end %> + +<% content_for :header_tags do %> +<%= javascript_include_tag 'calendar/calendar' %> +<%= javascript_include_tag "calendar/lang/calendar-#{current_language}.js" %> +<%= javascript_include_tag 'calendar/calendar-setup' %> +<%= stylesheet_link_tag 'calendar' %> +<% end %> diff --git a/rest_sys/app/views/account/show.rhtml b/rest_sys/app/views/account/show.rhtml new file mode 100644 index 000000000..97212b377 --- /dev/null +++ b/rest_sys/app/views/account/show.rhtml @@ -0,0 +1,28 @@ +

<%=h @user.name %>

+ +

+<%= mail_to @user.mail unless @user.pref.hide_mail %> +

    +
  • <%=l(:label_registered_on)%>: <%= format_date(@user.created_on) %>
  • +<% for custom_value in @custom_values %> +<% if !custom_value.value.empty? %> +
  • <%= custom_value.custom_field.name%>: <%=h show_value(custom_value) %>
  • +<% end %> +<% end %> +
+

+ +<% unless @memberships.empty? %> +

<%=l(:label_project_plural)%>

+
    +<% for membership in @memberships %> +
  • <%= link_to membership.project.name, :controller => 'projects', :action => 'show', :id => membership.project %> + (<%= membership.role.name %>, <%= format_date(membership.created_on) %>)
  • +<% end %> +
+<% end %> + +

<%=l(:label_activity)%>

+

+<%=l(:label_reported_issues)%>: <%= Issue.count(:conditions => ["author_id=?", @user.id]) %> +

\ No newline at end of file diff --git a/rest_sys/app/views/admin/_menu.rhtml b/rest_sys/app/views/admin/_menu.rhtml new file mode 100644 index 000000000..ef2abbc56 --- /dev/null +++ b/rest_sys/app/views/admin/_menu.rhtml @@ -0,0 +1,25 @@ + + + + diff --git a/rest_sys/app/views/admin/index.rhtml b/rest_sys/app/views/admin/index.rhtml new file mode 100644 index 000000000..933e288a0 --- /dev/null +++ b/rest_sys/app/views/admin/index.rhtml @@ -0,0 +1,47 @@ +

<%=l(:label_administration)%>

+ +

+<%= link_to l(:label_project_plural), :controller => 'admin', :action => 'projects' %> | +<%= link_to l(:label_new), :controller => 'projects', :action => 'add' %> +

+ +

+<%= link_to l(:label_user_plural), :controller => 'users' %> | +<%= link_to l(:label_new), :controller => 'users', :action => 'add' %> +

+ +

+<%= link_to l(:label_role_and_permissions), :controller => 'roles' %> +

+ +

+<%= link_to l(:label_tracker_plural), :controller => 'trackers' %> | +<%= link_to l(:label_issue_status_plural), :controller => 'issue_statuses' %> | +<%= link_to l(:label_workflow), :controller => 'roles', :action => 'workflow' %> +

+ +

+<%= link_to l(:label_custom_field_plural), :controller => 'custom_fields' %> +

+ +

+<%= link_to l(:label_enumerations), :controller => 'enumerations' %> +

+ +

+<%= link_to l(:field_mail_notification), :controller => 'admin', :action => 'mail_options' %> +

+ +

+<%= link_to l(:label_authentication), :controller => 'auth_sources' %> +

+ +

+<%= link_to l(:label_settings), :controller => 'settings' %> +

+ +

+<%= link_to l(:label_information_plural), :controller => 'admin', :action => 'info' %> +

+ +<% set_html_title l(:label_administration) -%> diff --git a/rest_sys/app/views/admin/info.rhtml b/rest_sys/app/views/admin/info.rhtml new file mode 100644 index 000000000..4d442f5ad --- /dev/null +++ b/rest_sys/app/views/admin/info.rhtml @@ -0,0 +1,27 @@ +

<%=l(:label_information_plural)%>

+ +

<%=l(:field_version)%>: <%= Redmine::Info.versioned_name %> (<%= @db_adapter_name %>)

+ + + + + +
Default administrator account changed<%= image_tag (@flags[:default_admin_changed] ? 'true.png' : 'false.png'), :style => "vertical-align:bottom;" %>
File repository writable<%= image_tag (@flags[:file_repository_writable] ? 'true.png' : 'false.png'), :style => "vertical-align:bottom;" %>
RMagick available<%= image_tag (@flags[:rmagick_available] ? 'true.png' : 'false.png'), :style => "vertical-align:bottom;" %>
+ +<% if @plugins.any? %> +  +

Plugins

+ + <% @plugins.keys.sort {|x,y| x.to_s <=> y.to_s}.each do |plugin| %> + + + + + + + + <% end %> +
<%=h @plugins[plugin].name %><%=h @plugins[plugin].description %><%=h @plugins[plugin].author %><%=h @plugins[plugin].version %><%= link_to('Configure', :controller => 'settings', :action => 'plugin', :id => plugin.to_s) if @plugins[plugin].configurable? %>
+<% end %> + +<% set_html_title(l(:label_information_plural)) -%> diff --git a/rest_sys/app/views/admin/mail_options.rhtml b/rest_sys/app/views/admin/mail_options.rhtml new file mode 100644 index 000000000..a4b923873 --- /dev/null +++ b/rest_sys/app/views/admin/mail_options.rhtml @@ -0,0 +1,33 @@ +
+<%= link_to l(:label_send_test_email), :action => 'test_email' %> +
+ +

<%=l(:field_mail_notification)%>

+ +<% form_tag({:action => 'mail_options'}, :id => 'mail-options-form') do %> + +
<%=l(:label_settings)%> +

+<%= text_field_tag 'settings[mail_from]', Setting.mail_from, :size => 60 %>

+ +

+<%= check_box_tag 'settings[bcc_recipients]', 1, Setting.bcc_recipients? %> +<%= hidden_field_tag 'settings[bcc_recipients]', 0 %>

+
+ +
<%=l(:text_select_mail_notifications)%> +<% @notifiables.each do |notifiable| %> +
+<% end %> +

<%= check_all_links('mail-options-form') %>

+
+ +
<%= l(:setting_emails_footer) %> +<%= text_area_tag 'settings[emails_footer]', Setting.emails_footer, :class => 'wiki-edit', :rows => 5 %> +
+ +<%= submit_tag l(:button_save) %> +<% end %> + +<% set_html_title(l(:field_mail_notification)) -%> diff --git a/rest_sys/app/views/admin/projects.rhtml b/rest_sys/app/views/admin/projects.rhtml new file mode 100644 index 000000000..e9d5e8537 --- /dev/null +++ b/rest_sys/app/views/admin/projects.rhtml @@ -0,0 +1,51 @@ +
+<%= link_to l(:label_project_new), {:controller => 'projects', :action => 'add'}, :class => 'icon icon-add' %> +
+ +

<%=l(:label_project_plural)%>

+ +<% form_tag() do %> +
<%= l(:label_filter_plural) %> + +<%= select_tag 'status', project_status_options_for_select(@status), :class => "small", :onchange => "this.form.submit(); return false;" %> +<%= submit_tag l(:button_apply), :class => "small" %> +
+<% end %> +  + + + + <%= sort_header_tag('name', :caption => l(:label_project)) %> + + + + <%= sort_header_tag('created_on', :caption => l(:field_created_on)) %> + + + + +<% for project in @projects %> + "> + + + +<% end %> + +
<%=l(:field_description)%><%=l(:field_is_public)%><%=l(:label_subproject_plural)%>
<%= project.active? ? link_to(h(project.name), :controller => 'projects', :action => 'settings', :id => project) : h(project.name) %> + <%= textilizable project.description, :project => project %> + <%= image_tag 'true.png' if project.is_public? %> + <%= project.children.size %> + <%= format_date(project.created_on) %> + + + <%= link_to(l(:button_archive), { :controller => 'projects', :action => 'archive', :id => project }, :confirm => l(:text_are_you_sure), :method => :post, :class => 'icon icon-lock') if project.active? %> + <%= link_to(l(:button_unarchive), { :controller => 'projects', :action => 'unarchive', :id => project }, :method => :post, :class => 'icon icon-unlock') if !project.active? && (project.parent.nil? || project.parent.active?) %> + + + <%= link_to(l(:button_delete), { :controller => 'projects', :action => 'destroy', :id => project }, :class => 'icon icon-del') %> +
+ +

<%= pagination_links_full @project_pages, :status => @status %> +[ <%= @project_pages.current.first_item %> - <%= @project_pages.current.last_item %> / <%= @project_count %> ]

+ +<% set_html_title l(:label_project_plural) -%> diff --git a/rest_sys/app/views/attachments/_form.rhtml b/rest_sys/app/views/attachments/_form.rhtml new file mode 100644 index 000000000..18f08c6be --- /dev/null +++ b/rest_sys/app/views/attachments/_form.rhtml @@ -0,0 +1,4 @@ +

+ +<%= file_field_tag 'attachments[]', :size => 30 %> (<%= l(:label_max_size) %>: <%= number_to_human_size(Setting.attachment_max_size.to_i.kilobytes) %>)

diff --git a/rest_sys/app/views/attachments/_links.rhtml b/rest_sys/app/views/attachments/_links.rhtml new file mode 100644 index 000000000..cce11292e --- /dev/null +++ b/rest_sys/app/views/attachments/_links.rhtml @@ -0,0 +1,13 @@ +
+<% for attachment in attachments %> +

<%= link_to attachment.filename, {:controller => 'attachments', :action => 'download', :id => attachment }, :class => 'icon icon-attachment' %> + (<%= number_to_human_size attachment.filesize %>) + <% unless options[:no_author] %> + <%= attachment.author.name %>, <%= format_date(attachment.created_on) %> + <% end %> + <% if options[:delete_url] %> + <%= link_to image_tag('delete.png'), options[:delete_url].update({:attachment_id => attachment}), :confirm => l(:text_are_you_sure), :method => :post %> + <% end %> +

+<% end %> +
diff --git a/rest_sys/app/views/auth_sources/_form.rhtml b/rest_sys/app/views/auth_sources/_form.rhtml new file mode 100644 index 000000000..3d148c11f --- /dev/null +++ b/rest_sys/app/views/auth_sources/_form.rhtml @@ -0,0 +1,48 @@ +<%= error_messages_for 'auth_source' %> + +
+ +

+<%= text_field 'auth_source', 'name' %>

+ +

+<%= text_field 'auth_source', 'host' %>

+ +

+<%= text_field 'auth_source', 'port', :size => 6 %> <%= check_box 'auth_source', 'tls' %> LDAPS

+ +

+<%= text_field 'auth_source', 'account' %>

+ +

+<%= password_field 'auth_source', 'account_password', :name => 'ignore', + :value => ((@auth_source.new_record? || @auth_source.account_password.blank?) ? '' : ('x'*15)), + :onfocus => "this.value=''; this.name='auth_source[account_password]';", + :onchange => "this.name='auth_source[account_password]';" %>

+ +

+<%= text_field 'auth_source', 'base_dn', :size => 60 %>

+
+ +
+

+<%= check_box 'auth_source', 'onthefly_register' %>

+ +

+

<%=l(:label_attribute_plural)%> +

+<%= text_field 'auth_source', 'attr_login', :size => 20 %>

+ +

+<%= text_field 'auth_source', 'attr_firstname', :size => 20 %>

+ +

+<%= text_field 'auth_source', 'attr_lastname', :size => 20 %>

+ +

+<%= text_field 'auth_source', 'attr_mail', :size => 20 %>

+
+

+
+ + diff --git a/rest_sys/app/views/auth_sources/edit.rhtml b/rest_sys/app/views/auth_sources/edit.rhtml new file mode 100644 index 000000000..165fd4f3e --- /dev/null +++ b/rest_sys/app/views/auth_sources/edit.rhtml @@ -0,0 +1,7 @@ +

<%=l(:label_auth_source)%> (<%= @auth_source.auth_method_name %>)

+ +<% form_tag({:action => 'update', :id => @auth_source}, :class => "tabular") do %> + <%= render :partial => 'form' %> + <%= submit_tag l(:button_save) %> +<% end %> + diff --git a/rest_sys/app/views/auth_sources/list.rhtml b/rest_sys/app/views/auth_sources/list.rhtml new file mode 100644 index 000000000..f486f45b7 --- /dev/null +++ b/rest_sys/app/views/auth_sources/list.rhtml @@ -0,0 +1,28 @@ +
+<%= link_to l(:label_auth_source_new), {:action => 'new'}, :class => 'icon icon-add' %> +
+ +

<%=l(:label_auth_source_plural)%>

+ + + + + + + + + + +<% for source in @auth_sources %> + "> + + + + + + +<% end %> + +
<%=l(:field_name)%><%=l(:field_type)%><%=l(:field_host)%>
<%= link_to source.name, :action => 'edit', :id => source%><%= source.auth_method_name %><%= source.host %><%= link_to l(:button_test), :action => 'test_connection', :id => source %><%= button_to l(:button_delete), { :action => 'destroy', :id => source }, :confirm => l(:text_are_you_sure), :class => "button-small" %>
+ +<%= pagination_links_full @auth_source_pages %> diff --git a/rest_sys/app/views/auth_sources/new.rhtml b/rest_sys/app/views/auth_sources/new.rhtml new file mode 100644 index 000000000..2d493dc3a --- /dev/null +++ b/rest_sys/app/views/auth_sources/new.rhtml @@ -0,0 +1,6 @@ +

<%=l(:label_auth_source_new)%> (<%= @auth_source.auth_method_name %>)

+ +<% form_tag({:action => 'create'}, :class => "tabular") do %> + <%= render :partial => 'form' %> + <%= submit_tag l(:button_create) %> +<% end %> diff --git a/rest_sys/app/views/boards/_form.rhtml b/rest_sys/app/views/boards/_form.rhtml new file mode 100644 index 000000000..7ede589ab --- /dev/null +++ b/rest_sys/app/views/boards/_form.rhtml @@ -0,0 +1,8 @@ +<%= error_messages_for 'board' %> + + +
+

<%= f.text_field :name, :required => true %>

+

<%= f.text_field :description, :required => true, :size => 80 %>

+
+ diff --git a/rest_sys/app/views/boards/edit.rhtml b/rest_sys/app/views/boards/edit.rhtml new file mode 100644 index 000000000..ba4c8b5ac --- /dev/null +++ b/rest_sys/app/views/boards/edit.rhtml @@ -0,0 +1,6 @@ +

<%= l(:label_board) %>

+ +<% labelled_tabular_form_for :board, @board, :url => {:action => 'edit', :id => @board} do |f| %> + <%= render :partial => 'form', :locals => {:f => f} %> + <%= submit_tag l(:button_save) %> +<% end %> diff --git a/rest_sys/app/views/boards/index.rhtml b/rest_sys/app/views/boards/index.rhtml new file mode 100644 index 000000000..cd4e85e9a --- /dev/null +++ b/rest_sys/app/views/boards/index.rhtml @@ -0,0 +1,30 @@ +

<%= l(:label_board_plural) %>

+ + + + + + + + + +<% for board in @boards %> + + + + + + +<% end %> + +
<%= l(:label_board) %><%= l(:label_topic_plural) %><%= l(:label_message_plural) %><%= l(:label_message_last) %>
+ <%= link_to h(board.name), {:action => 'show', :id => board}, :class => "icon22 icon22-comment" %>
+ <%=h board.description %> +
<%= board.topics_count %><%= board.messages_count %> + + <% if board.last_message %> + <%= authoring board.last_message.created_on, board.last_message.author %>
+ <%= link_to_message board.last_message %> + <% end %> +
+
diff --git a/rest_sys/app/views/boards/new.rhtml b/rest_sys/app/views/boards/new.rhtml new file mode 100644 index 000000000..b89121880 --- /dev/null +++ b/rest_sys/app/views/boards/new.rhtml @@ -0,0 +1,6 @@ +

<%= l(:label_board_new) %>

+ +<% labelled_tabular_form_for :board, @board, :url => {:action => 'new'} do |f| %> + <%= render :partial => 'form', :locals => {:f => f} %> + <%= submit_tag l(:button_create) %> +<% end %> diff --git a/rest_sys/app/views/boards/show.rhtml b/rest_sys/app/views/boards/show.rhtml new file mode 100644 index 000000000..8bcf960b2 --- /dev/null +++ b/rest_sys/app/views/boards/show.rhtml @@ -0,0 +1,50 @@ +
+<%= link_to_if_authorized l(:label_message_new), + {:controller => 'messages', :action => 'new', :board_id => @board}, + :class => 'icon icon-add', + :onclick => 'Element.show("add-message"); return false;' %> +<%= watcher_tag(@board, User.current) %> +
+ + + +

<%=h @board.name %>

+ +<% if @topics.any? %> + + + + + <%= sort_header_tag("#{Message.table_name}.created_on", :caption => l(:field_created_on)) %> + + <%= sort_header_tag("#{Message.table_name}.updated_on", :caption => l(:label_message_last)) %> + + + <% @topics.each do |topic| %> + + + + + + + + <% end %> + +
<%= l(:field_subject) %><%= l(:field_author) %><%= l(:label_reply_plural) %>
<%= link_to h(topic.subject), { :controller => 'messages', :action => 'show', :board_id => @board, :id => topic }, :class => 'icon' %><%= topic.author %><%= format_time(topic.created_on) %><%= topic.replies_count %> + <% if topic.last_reply %> + <%= authoring topic.last_reply.created_on, topic.last_reply.author %>
+ <%= link_to_message topic.last_reply %> + <% end %> +
+

<%= pagination_links_full @topic_pages %> +[ <%= @topic_pages.current.first_item %> - <%= @topic_pages.current.last_item %> / <%= @topic_count %> ]

+<% else %> +

<%= l(:label_no_data) %>

+<% end %> diff --git a/rest_sys/app/views/common/403.rhtml b/rest_sys/app/views/common/403.rhtml new file mode 100644 index 000000000..d2d03f811 --- /dev/null +++ b/rest_sys/app/views/common/403.rhtml @@ -0,0 +1,6 @@ +

403

+ +

<%= l(:notice_not_authorized) %>

+

Back

+ +<% set_html_title '403' %> diff --git a/rest_sys/app/views/common/404.rhtml b/rest_sys/app/views/common/404.rhtml new file mode 100644 index 000000000..080b04842 --- /dev/null +++ b/rest_sys/app/views/common/404.rhtml @@ -0,0 +1,6 @@ +

404

+ +

<%= l(:notice_file_not_found) %>

+

Back

+ +<% set_html_title '404' %> diff --git a/rest_sys/app/views/common/_attachments_form.rhtml b/rest_sys/app/views/common/_attachments_form.rhtml new file mode 100644 index 000000000..673f4a52e --- /dev/null +++ b/rest_sys/app/views/common/_attachments_form.rhtml @@ -0,0 +1,6 @@ +

+ +<%= file_field_tag 'attachments[]', :size => 30 %> +(<%= l(:label_max_size) %>: <%= number_to_human_size(Setting.attachment_max_size.to_i.kilobytes) %>) +

diff --git a/rest_sys/app/views/common/_calendar.rhtml b/rest_sys/app/views/common/_calendar.rhtml new file mode 100644 index 000000000..7534a1223 --- /dev/null +++ b/rest_sys/app/views/common/_calendar.rhtml @@ -0,0 +1,36 @@ + + +<% 7.times do |i| %><% end %> + + + +<% day = calendar.startdt +while day <= calendar.enddt %> +<%= "" if day.cwday == calendar.first_wday %> + +<%= '' if day.cwday==calendar.last_wday and day!=calendar.enddt %> +<% day = day + 1 +end %> + + +
<%= day_name( (calendar.first_wday+i)%7 ) %>
#{day.cweek} +

<%= day.day %>

+<% calendar.events_on(day).each do |i| %> + <% if i.is_a? Issue %> +
+ <%= if day == i.start_date && day == i.due_date + image_tag('arrow_bw.png') + elsif day == i.start_date + image_tag('arrow_from.png') + elsif day == i.due_date + image_tag('arrow_to.png') + end %> + <%= h("#{i.project.name} -") unless @project && @project == i.project %> + <%= link_to_issue i %>: <%= h(truncate(i.subject, 30)) %> + <%= render_issue_tooltip i %> +
+ <% else %> + <%= link_to_version i, :class => "icon icon-package" %> + <% end %> +<% end %> +
diff --git a/rest_sys/app/views/common/_preview.rhtml b/rest_sys/app/views/common/_preview.rhtml new file mode 100644 index 000000000..e3bfc3a25 --- /dev/null +++ b/rest_sys/app/views/common/_preview.rhtml @@ -0,0 +1,3 @@ +
<%= l(:label_preview) %> +<%= textilizable @text, :attachments => @attachements %> +
diff --git a/rest_sys/app/views/common/feed.atom.rxml b/rest_sys/app/views/common/feed.atom.rxml new file mode 100644 index 000000000..59b3163f4 --- /dev/null +++ b/rest_sys/app/views/common/feed.atom.rxml @@ -0,0 +1,26 @@ +xml.instruct! +xml.feed "xmlns" => "http://www.w3.org/2005/Atom" do + xml.title @title + xml.link "rel" => "self", "href" => url_for(params.merge({:format => nil, :only_path => false})) + xml.link "rel" => "alternate", "href" => url_for(:controller => 'welcome', :only_path => false) + xml.id url_for(:controller => 'welcome', :only_path => false) + xml.updated((@items.first ? @items.first.event_datetime : Time.now).xmlschema) + xml.author { xml.name "#{Setting.app_title}" } + xml.generator(:uri => Redmine::Info.url, :version => Redmine::VERSION) { xml.text! Redmine::Info.versioned_name; } + @items.each do |item| + xml.entry do + xml.title truncate(item.event_title, 100) + xml.link "rel" => "alternate", "href" => url_for(item.event_url(:only_path => false)) + xml.id url_for(item.event_url(:only_path => false)) + xml.updated item.event_datetime.xmlschema + author = item.event_author if item.respond_to?(:author) + xml.author do + xml.name(author.is_a?(User) ? author.name : author) + xml.email(author.mail) if author.is_a?(User) + end if author + xml.content "type" => "html" do + xml.text! textilizable(item.event_description) + end + end + end +end diff --git a/rest_sys/app/views/custom_fields/_form.rhtml b/rest_sys/app/views/custom_fields/_form.rhtml new file mode 100644 index 000000000..915daab32 --- /dev/null +++ b/rest_sys/app/views/custom_fields/_form.rhtml @@ -0,0 +1,98 @@ +<%= error_messages_for 'custom_field' %> + + + +
+

<%= f.text_field :name, :required => true %>

+

<%= f.select :field_format, custom_field_formats_for_select, {}, :onchange => "toggle_custom_field_format();" %>

+

+ <%= f.text_field :min_length, :size => 5, :no_label => true %> - + <%= f.text_field :max_length, :size => 5, :no_label => true %>
(<%=l(:text_min_max_length_info)%>)

+

<%= f.text_field :regexp, :size => 50 %>
(<%=l(:text_regexp_info)%>)

+

+<% (@custom_field.possible_values.to_a + [""]).each do |value| %> +<%= text_field_tag 'custom_field[possible_values][]', value, :size => 30 %> <%= image_to_function "delete.png", "deleteValueField(this);return false" %>
+<% end %> +

+
+ +
+<% case @custom_field.type.to_s +when "IssueCustomField" %> + +
<%=l(:label_tracker_plural)%> + <% for tracker in @trackers %> + <%= check_box_tag "tracker_ids[]", tracker.id, (@custom_field.trackers.include? tracker) %> <%= tracker.name %> + <% end %> +
+   +

<%= f.check_box :is_required %>

+

<%= f.check_box :is_for_all %>

+

<%= f.check_box :is_filter %>

+

<%= f.check_box :searchable %>

+ +<% when "UserCustomField" %> +

<%= f.check_box :is_required %>

+ +<% when "ProjectCustomField" %> +

<%= f.check_box :is_required %>

+ +<% end %> +
+<%= javascript_tag "toggle_custom_field_format();" %> diff --git a/rest_sys/app/views/custom_fields/edit.rhtml b/rest_sys/app/views/custom_fields/edit.rhtml new file mode 100644 index 000000000..ef056fa41 --- /dev/null +++ b/rest_sys/app/views/custom_fields/edit.rhtml @@ -0,0 +1,6 @@ +

<%=l(:label_custom_field)%> (<%=l(@custom_field.type_name)%>)

+ +<% labelled_tabular_form_for :custom_field, @custom_field, :url => { :action => "edit", :id => @custom_field } do |f| %> +<%= render :partial => 'form', :locals => { :f => f } %> +<%= submit_tag l(:button_save) %> +<% end %> diff --git a/rest_sys/app/views/custom_fields/list.rhtml b/rest_sys/app/views/custom_fields/list.rhtml new file mode 100644 index 000000000..bbdfeffb4 --- /dev/null +++ b/rest_sys/app/views/custom_fields/list.rhtml @@ -0,0 +1,55 @@ +

<%=l(:label_custom_field_plural)%>

+ +
+
    +
  • <%= link_to l(:label_issue_plural), {}, :id=> "tab-IssueCustomField", :onclick => "showTab('IssueCustomField'); this.blur(); return false;" %>
  • +
  • <%= link_to l(:label_project_plural), {}, :id=> "tab-ProjectCustomField", :onclick => "showTab('ProjectCustomField'); this.blur(); return false;" %>
  • +
  • <%= link_to l(:label_user_plural), {}, :id=> "tab-UserCustomField", :onclick => "showTab('UserCustomField'); this.blur(); return false;" %>
  • +
+
+ +<% %w(IssueCustomField ProjectCustomField UserCustomField).each do |type| %> +
+ + + + + + <% if type == 'IssueCustomField' %> + + + <% end %> + + + + +<% for custom_field in (@custom_fields_by_type[type] || []).sort %> + "> + + + + <% if type == 'IssueCustomField' %> + + + <% end %> + + + +<% end; reset_cycle %> + +
<%=l(:field_name)%><%=l(:field_field_format)%><%=l(:field_is_required)%><%=l(:field_is_for_all)%><%=l(:label_used_by)%><%=l(:button_sort)%>
<%= link_to custom_field.name, :action => 'edit', :id => custom_field %><%= l(CustomField::FIELD_FORMATS[custom_field.field_format][:name]) %><%= image_tag 'true.png' if custom_field.is_required? %><%= image_tag 'true.png' if custom_field.is_for_all? %><%= custom_field.projects.count.to_s + ' ' + lwr(:label_project, custom_field.projects.count) if custom_field.is_a? IssueCustomField and !custom_field.is_for_all? %> + <%= link_to image_tag('2uparrow.png', :alt => l(:label_sort_highest)), {:action => 'move', :id => custom_field, :position => 'highest'}, :method => :post, :title => l(:label_sort_highest) %> + <%= link_to image_tag('1uparrow.png', :alt => l(:label_sort_higher)), {:action => 'move', :id => custom_field, :position => 'higher'}, :method => :post, :title => l(:label_sort_higher) %> - + <%= link_to image_tag('1downarrow.png', :alt => l(:label_sort_lower)), {:action => 'move', :id => custom_field, :position => 'lower'}, :method => :post, :title => l(:label_sort_lower) %> + <%= link_to image_tag('2downarrow.png', :alt => l(:label_sort_lowest)), {:action => 'move', :id => custom_field, :position => 'lowest'}, :method => :post, :title => l(:label_sort_lowest) %> + + <%= button_to l(:button_delete), { :action => 'destroy', :id => custom_field }, :confirm => l(:text_are_you_sure), :class => "button-small" %> +
+
+<%= link_to l(:label_custom_field_new), {:action => 'new', :type => type}, :class => 'icon icon-add' %> +
+<% end %> + +<%= javascript_tag "showTab('#{@tab}');" %> + +<% set_html_title(l(:label_custom_field_plural)) -%> diff --git a/rest_sys/app/views/custom_fields/new.rhtml b/rest_sys/app/views/custom_fields/new.rhtml new file mode 100644 index 000000000..2e8aa2750 --- /dev/null +++ b/rest_sys/app/views/custom_fields/new.rhtml @@ -0,0 +1,7 @@ +

<%=l(:label_custom_field_new)%> (<%=l(@custom_field.type_name)%>)

+ +<% labelled_tabular_form_for :custom_field, @custom_field, :url => { :action => "new" } do |f| %> +<%= render :partial => 'form', :locals => { :f => f } %> +<%= hidden_field_tag 'type', @custom_field.type %> +<%= submit_tag l(:button_save) %> +<% end %> diff --git a/rest_sys/app/views/documents/_document.rhtml b/rest_sys/app/views/documents/_document.rhtml new file mode 100644 index 000000000..ddfdb9eec --- /dev/null +++ b/rest_sys/app/views/documents/_document.rhtml @@ -0,0 +1,3 @@ +

<%= link_to h(document.title), :controller => 'documents', :action => 'show', :id => document %>
+<% unless document.description.blank? %><%=h(truncate(document.description, 250)) %>
<% end %> +<%= format_time(document.created_on) %>

\ No newline at end of file diff --git a/rest_sys/app/views/documents/_form.rhtml b/rest_sys/app/views/documents/_form.rhtml new file mode 100644 index 000000000..d45e295b0 --- /dev/null +++ b/rest_sys/app/views/documents/_form.rhtml @@ -0,0 +1,15 @@ +<%= error_messages_for 'document' %> +
+ +

+<%= select('document', 'category_id', Enumeration.get_values('DCAT').collect {|c| [c.name, c.id]}) %>

+ +

+<%= text_field 'document', 'title', :size => 60 %>

+ +

+<%= text_area 'document', 'description', :cols => 60, :rows => 15, :class => 'wiki-edit' %>

+ +
+ +<%= wikitoolbar_for 'document_description' %> diff --git a/rest_sys/app/views/documents/edit.rhtml b/rest_sys/app/views/documents/edit.rhtml new file mode 100644 index 000000000..0b9f31f84 --- /dev/null +++ b/rest_sys/app/views/documents/edit.rhtml @@ -0,0 +1,8 @@ +

<%=l(:label_document)%>

+ +<% form_tag({:action => 'edit', :id => @document}, :class => "tabular") do %> + <%= render :partial => 'form' %> + <%= submit_tag l(:button_save) %> +<% end %> + + diff --git a/rest_sys/app/views/documents/show.rhtml b/rest_sys/app/views/documents/show.rhtml new file mode 100644 index 000000000..8f53f1abe --- /dev/null +++ b/rest_sys/app/views/documents/show.rhtml @@ -0,0 +1,38 @@ +
+<%= link_to_if_authorized l(:button_edit), {:controller => 'documents', :action => 'edit', :id => @document}, :class => 'icon icon-edit', :accesskey => accesskey(:edit) %> +<%= link_to_if_authorized l(:button_delete), {:controller => 'documents', :action => 'destroy', :id => @document}, :confirm => l(:text_are_you_sure), :method => :post, :class => 'icon icon-del' %> +
+ +

<%=h @document.title %>

+ +

<%=h @document.category.name %>
+<%= format_date @document.created_on %>

+<%= textilizable @document.description, :attachments => @document.attachments %> +
+ +

<%= l(:label_attachment_plural) %>

+
    +<% for attachment in @attachments %> +
  • +
    + <%= link_to_if_authorized l(:button_delete), {:controller => 'documents', :action => 'destroy_attachment', :id => @document, :attachment_id => attachment}, :confirm => l(:text_are_you_sure), :method => :post, :class => 'icon icon-del' %> +
    + <%= link_to attachment.filename, :action => 'download', :id => @document, :attachment_id => attachment %> + (<%= number_to_human_size attachment.filesize %>)
    + <%= authoring attachment.created_on, attachment.author %>
    + <%= lwr(:label_download, attachment.downloads) %> +
  • +<% end %> +
+
+ + +<% if authorize_for('documents', 'add_attachment') %> +

<%= toggle_link l(:label_attachment_new), "add_attachment_form" %>

+ <% form_tag({ :controller => 'documents', :action => 'add_attachment', :id => @document }, :multipart => true, :class => "tabular", :id => "add_attachment_form", :style => "display:none;") do %> + <%= render :partial => 'attachments/form' %> + <%= submit_tag l(:button_add) %> + <% end %> +<% end %> + +<% set_html_title h(@document.title) -%> diff --git a/rest_sys/app/views/enumerations/_form.rhtml b/rest_sys/app/views/enumerations/_form.rhtml new file mode 100644 index 000000000..3f98f5213 --- /dev/null +++ b/rest_sys/app/views/enumerations/_form.rhtml @@ -0,0 +1,12 @@ +<%= error_messages_for 'enumeration' %> +
+ +<%= hidden_field 'enumeration', 'opt' %> + +

+<%= text_field 'enumeration', 'name' %>

+ +

+<%= check_box 'enumeration', 'is_default' %>

+ +
\ No newline at end of file diff --git a/rest_sys/app/views/enumerations/edit.rhtml b/rest_sys/app/views/enumerations/edit.rhtml new file mode 100644 index 000000000..7baea028a --- /dev/null +++ b/rest_sys/app/views/enumerations/edit.rhtml @@ -0,0 +1,10 @@ +

<%=l(:label_enumerations)%>

+ +<% form_tag({:action => 'update', :id => @enumeration}, :class => "tabular") do %> + <%= render :partial => 'form' %> + <%= submit_tag l(:button_save) %> +<% end %> + +<% form_tag({:action => 'destroy', :id => @enumeration}) do %> + <%= submit_tag l(:button_delete) %> +<% end %> \ No newline at end of file diff --git a/rest_sys/app/views/enumerations/list.rhtml b/rest_sys/app/views/enumerations/list.rhtml new file mode 100644 index 000000000..2e069f392 --- /dev/null +++ b/rest_sys/app/views/enumerations/list.rhtml @@ -0,0 +1,28 @@ +

<%=l(:label_enumerations)%>

+ +<% Enumeration::OPTIONS.each do |option, name| %> +

<%= l(name) %>

+ +<% enumerations = Enumeration.get_values(option) %> +<% if enumerations.any? %> + +<% enumerations.each do |enumeration| %> + + + + + +<% end %> +
<%= link_to enumeration.name, :action => 'edit', :id => enumeration %><%= image_tag('true.png') if enumeration.is_default? %> + <%= link_to image_tag('2uparrow.png', :alt => l(:label_sort_highest)), {:action => 'move', :id => enumeration, :position => 'highest'}, :method => :post, :title => l(:label_sort_highest) %> + <%= link_to image_tag('1uparrow.png', :alt => l(:label_sort_higher)), {:action => 'move', :id => enumeration, :position => 'higher'}, :method => :post, :title => l(:label_sort_higher) %> - + <%= link_to image_tag('1downarrow.png', :alt => l(:label_sort_lower)), {:action => 'move', :id => enumeration, :position => 'lower'}, :method => :post, :title => l(:label_sort_lower) %> + <%= link_to image_tag('2downarrow.png', :alt => l(:label_sort_lowest)), {:action => 'move', :id => enumeration, :position => 'lowest'}, :method => :post, :title => l(:label_sort_lowest) %> +
+<% reset_cycle %> +<% end %> + +

<%= link_to l(:label_enumeration_new), { :action => 'new', :opt => option } %>

+<% end %> + +<% set_html_title(l(:label_enumerations)) -%> diff --git a/rest_sys/app/views/enumerations/new.rhtml b/rest_sys/app/views/enumerations/new.rhtml new file mode 100644 index 000000000..5c2ccd133 --- /dev/null +++ b/rest_sys/app/views/enumerations/new.rhtml @@ -0,0 +1,6 @@ +

<%= l(@enumeration.option_name) %>: <%=l(:label_enumeration_new)%>

+ +<% form_tag({:action => 'create'}, :class => "tabular") do %> + <%= render :partial => 'form' %> + <%= submit_tag l(:button_create) %> +<% end %> diff --git a/rest_sys/app/views/issue_categories/_form.rhtml b/rest_sys/app/views/issue_categories/_form.rhtml new file mode 100644 index 000000000..dc62c2000 --- /dev/null +++ b/rest_sys/app/views/issue_categories/_form.rhtml @@ -0,0 +1,6 @@ +<%= error_messages_for 'category' %> + +
+

<%= f.text_field :name, :size => 30, :required => true %>

+

<%= f.select :assigned_to_id, @project.users.collect{|u| [u.name, u.id]}, :include_blank => true %>

+
diff --git a/rest_sys/app/views/issue_categories/destroy.rhtml b/rest_sys/app/views/issue_categories/destroy.rhtml new file mode 100644 index 000000000..a563736e2 --- /dev/null +++ b/rest_sys/app/views/issue_categories/destroy.rhtml @@ -0,0 +1,15 @@ +

<%=l(:label_issue_category)%>: <%=h @category.name %>

+ +<% form_tag({}) do %> +
+

<%= l(:text_issue_category_destroy_question, @issue_count) %>

+

<%= radio_button_tag 'todo', 'nullify', true %> <%= l(:text_issue_category_destroy_assignments) %>
+<% if @categories.size > 0 %> +<%= radio_button_tag 'todo', 'reassign', false %> <%= l(:text_issue_category_reassign_to) %>: +<%= select_tag 'reassign_to_id', options_from_collection_for_select(@categories, 'id', 'name') %>

+<% end %> +
+ +<%= submit_tag l(:button_apply) %> +<%= link_to l(:button_cancel), :controller => 'projects', :action => 'settings', :id => @project, :tab => 'categories' %> +<% end %> diff --git a/rest_sys/app/views/issue_categories/edit.rhtml b/rest_sys/app/views/issue_categories/edit.rhtml new file mode 100644 index 000000000..bc627797b --- /dev/null +++ b/rest_sys/app/views/issue_categories/edit.rhtml @@ -0,0 +1,6 @@ +

<%=l(:label_issue_category)%>

+ +<% labelled_tabular_form_for :category, @category, :url => { :action => 'edit', :id => @category } do |f| %> +<%= render :partial => 'issue_categories/form', :locals => { :f => f } %> +<%= submit_tag l(:button_save) %> +<% end %> diff --git a/rest_sys/app/views/issue_relations/_form.rhtml b/rest_sys/app/views/issue_relations/_form.rhtml new file mode 100644 index 000000000..0de386306 --- /dev/null +++ b/rest_sys/app/views/issue_relations/_form.rhtml @@ -0,0 +1,12 @@ +<%= error_messages_for 'relation' %> + +

<%= f.select :relation_type, collection_for_relation_type_select, {}, :onchange => "setPredecessorFieldsVisibility();" %> +<%= l(:label_issue) %> #<%= f.text_field :issue_to_id, :size => 6 %> + +<%= submit_tag l(:button_add) %> +<%= toggle_link l(:button_cancel), 'new-relation-form'%> +

+ +<%= javascript_tag "setPredecessorFieldsVisibility();" %> diff --git a/rest_sys/app/views/issue_statuses/_form.rhtml b/rest_sys/app/views/issue_statuses/_form.rhtml new file mode 100644 index 000000000..6ae0a7c33 --- /dev/null +++ b/rest_sys/app/views/issue_statuses/_form.rhtml @@ -0,0 +1,15 @@ +<%= error_messages_for 'issue_status' %> + +
+ +

+<%= text_field 'issue_status', 'name' %>

+ +

+<%= check_box 'issue_status', 'is_closed' %>

+ +

+<%= check_box 'issue_status', 'is_default' %>

+ + +
\ No newline at end of file diff --git a/rest_sys/app/views/issue_statuses/edit.rhtml b/rest_sys/app/views/issue_statuses/edit.rhtml new file mode 100644 index 000000000..b81426a02 --- /dev/null +++ b/rest_sys/app/views/issue_statuses/edit.rhtml @@ -0,0 +1,6 @@ +

<%=l(:label_issue_status)%>

+ +<% form_tag({:action => 'update', :id => @issue_status}, :class => "tabular") do %> + <%= render :partial => 'form' %> + <%= submit_tag l(:button_save) %> +<% end %> diff --git a/rest_sys/app/views/issue_statuses/list.rhtml b/rest_sys/app/views/issue_statuses/list.rhtml new file mode 100644 index 000000000..05506f3c7 --- /dev/null +++ b/rest_sys/app/views/issue_statuses/list.rhtml @@ -0,0 +1,37 @@ +
+<%= link_to l(:label_issue_status_new), {:action => 'new'}, :class => 'icon icon-add' %> +
+ +

<%=l(:label_issue_status_plural)%>

+ + + + + + + + + + +<% for status in @issue_statuses %> + "> + + + + + + +<% end %> + +
<%=l(:field_status)%><%=l(:field_is_default)%><%=l(:field_is_closed)%><%=l(:button_sort)%>
<%= link_to status.name, :action => 'edit', :id => status %><%= image_tag 'true.png' if status.is_default? %><%= image_tag 'true.png' if status.is_closed? %> + <%= link_to image_tag('2uparrow.png', :alt => l(:label_sort_highest)), {:action => 'move', :id => status, :position => 'highest'}, :method => :post, :title => l(:label_sort_highest) %> + <%= link_to image_tag('1uparrow.png', :alt => l(:label_sort_higher)), {:action => 'move', :id => status, :position => 'higher'}, :method => :post, :title => l(:label_sort_higher) %> - + <%= link_to image_tag('1downarrow.png', :alt => l(:label_sort_lower)), {:action => 'move', :id => status, :position => 'lower'}, :method => :post, :title => l(:label_sort_lower) %> + <%= link_to image_tag('2downarrow.png', :alt => l(:label_sort_lowest)), {:action => 'move', :id => status, :position => 'lowest'}, :method => :post, :title => l(:label_sort_lowest) %> + + <%= button_to l(:button_delete), { :action => 'destroy', :id => status }, :confirm => l(:text_are_you_sure), :class => "button-small" %> +
+ +<%= pagination_links_full @issue_status_pages %> + +<% set_html_title(l(:label_issue_status_plural)) -%> diff --git a/rest_sys/app/views/issue_statuses/new.rhtml b/rest_sys/app/views/issue_statuses/new.rhtml new file mode 100644 index 000000000..ede1699b0 --- /dev/null +++ b/rest_sys/app/views/issue_statuses/new.rhtml @@ -0,0 +1,6 @@ +

<%=l(:label_issue_status_new)%>

+ +<% form_tag({:action => 'create'}, :class => "tabular") do %> + <%= render :partial => 'form' %> + <%= submit_tag l(:button_create) %> +<% end %> diff --git a/rest_sys/app/views/issues/_bulk_edit_form.rhtml b/rest_sys/app/views/issues/_bulk_edit_form.rhtml new file mode 100644 index 000000000..bc3f62e6d --- /dev/null +++ b/rest_sys/app/views/issues/_bulk_edit_form.rhtml @@ -0,0 +1,38 @@ +
+
<%= l(:label_bulk_edit_selected_issues) %> + +

+<% if @available_statuses %> + +<% end %> + + +

+

+ + +

+ +

+ + + +

+ +
+<%= text_area_tag 'notes', '', :cols => 80, :rows => 5 %> + +
+

<%= submit_tag l(:button_apply) %> +<%= link_to l(:button_cancel), {}, :onclick => 'Element.hide("bulk-edit-fields"); if ($("query_form")) {Element.show("query_form")}; return false;' %>

+
diff --git a/rest_sys/app/views/issues/_form.rhtml b/rest_sys/app/views/issues/_form.rhtml new file mode 100644 index 000000000..203d1cca3 --- /dev/null +++ b/rest_sys/app/views/issues/_form.rhtml @@ -0,0 +1,52 @@ +<%= error_messages_for 'issue' %> +
+ +
+<% if @issue.new_record? %> +

<%= f.select :status_id, (@allowed_statuses.collect {|p| [p.name, p.id]}), :required => true %>

+<% else %> +

<%= @issue.status.name %>

+<% end %> + +

<%= f.select :priority_id, (@priorities.collect {|p| [p.name, p.id]}), :required => true %>

+

<%= f.select :assigned_to_id, (@issue.assignable_users.collect {|m| [m.name, m.id]}), :include_blank => true %>

+

<%= f.select :category_id, (@project.issue_categories.collect {|c| [c.name, c.id]}), :include_blank => true %> +<%= prompt_to_remote(l(:label_issue_category_new), + l(:label_issue_category_new), 'category[name]', + {:controller => 'projects', :action => 'add_issue_category', :id => @project}, + :class => 'small') if authorize_for('projects', 'add_issue_category') %>

+
+ +
+

<%= f.text_field :start_date, :size => 10 %><%= calendar_for('issue_start_date') %>

+

<%= f.text_field :due_date, :size => 10 %><%= calendar_for('issue_due_date') %>

+

<%= f.text_field :estimated_hours, :size => 3 %> <%= l(:field_hours) %>

+

<%= f.select :done_ratio, ((0..10).to_a.collect {|r| ["#{r*10} %", r*10] }) %>

+
+ +

<%= f.text_field :subject, :size => 80, :required => true %>

+

<%= f.text_area :description, :required => true, + :cols => 60, + :rows => (@issue.description.blank? ? 10 : [[10, @issue.description.length / 50].max, 100].min), + :accesskey => accesskey(:edit), + :class => 'wiki-edit' %>

+

<%= f.select :fixed_version_id, (@project.versions.sort.collect {|v| [v.name, v.id]}), { :include_blank => true } %>

+<% for @custom_value in @custom_values %> +

<%= custom_field_tag_with_label @custom_value %>

+<% end %> + +<% if @issue.new_record? %> +

+<%= file_field_tag 'attachments[]', :size => 30 %> (<%= l(:label_max_size) %>: <%= number_to_human_size(Setting.attachment_max_size.to_i.kilobytes) %>)

+<% end %> +
+ +<%= wikitoolbar_for 'issue_description' %> + +<% content_for :header_tags do %> + <%= javascript_include_tag 'calendar/calendar' %> + <%= javascript_include_tag "calendar/lang/calendar-#{current_language}.js" %> + <%= javascript_include_tag 'calendar/calendar-setup' %> + <%= stylesheet_link_tag 'calendar' %> +<% end %> diff --git a/rest_sys/app/views/issues/_history.rhtml b/rest_sys/app/views/issues/_history.rhtml new file mode 100644 index 000000000..bab37b4fd --- /dev/null +++ b/rest_sys/app/views/issues/_history.rhtml @@ -0,0 +1,13 @@ +<% note_id = 1 %> +<% for journal in journals %> +

<%= link_to "##{note_id}", :anchor => "note-#{note_id}" %>
+ <%= content_tag('a', '', :name => "note-#{note_id}")%> + <%= format_time(journal.created_on) %> - <%= journal.user.name %>

+
    + <% for detail in journal.details %> +
  • <%= show_detail(detail) %>
  • + <% end %> +
+ <%= textilizable(journal.notes) unless journal.notes.blank? %> + <% note_id += 1 %> +<% end %> diff --git a/rest_sys/app/views/issues/_list.rhtml b/rest_sys/app/views/issues/_list.rhtml new file mode 100644 index 000000000..d8e3102df --- /dev/null +++ b/rest_sys/app/views/issues/_list.rhtml @@ -0,0 +1,25 @@ +
+ + + + <%= sort_header_tag("#{Issue.table_name}.id", :caption => '#') %> + <% query.columns.each do |column| %> + <%= column_header(column) %> + <% end %> + + + <% issues.each do |issue| %> + "> + + + <% query.columns.each do |column| %> + <%= content_tag 'td', column_content(column, issue), :class => column.name %> + <% end %> + + <% end %> + +
<%= link_to_remote(image_tag('edit.png'), + {:url => { :controller => 'projects', :action => 'bulk_edit_issues', :id => @project }, + :method => :get}, + {:title => l(:label_bulk_edit_selected_issues)}) if @project && User.current.allowed_to?(:edit_issues, @project) %> +
<%= check_box_tag("issue_ids[]", issue.id, false, :id => "issue_#{issue.id}", :disabled => (!@project || @project != issue.project)) %><%= link_to issue.id, :controller => 'issues', :action => 'show', :id => issue %>
diff --git a/rest_sys/app/views/issues/_list_simple.rhtml b/rest_sys/app/views/issues/_list_simple.rhtml new file mode 100644 index 000000000..eb93f8ea1 --- /dev/null +++ b/rest_sys/app/views/issues/_list_simple.rhtml @@ -0,0 +1,25 @@ +<% if issues.length > 0 %> + + + + + + + + <% for issue in issues %> + "> + + + + + <% end %> + +
#<%=l(:field_tracker)%><%=l(:field_subject)%>
+ <%= link_to issue.id, :controller => 'issues', :action => 'show', :id => issue %> + <%=h issue.project.name %> - <%= issue.tracker.name %>
+ <%= issue.status.name %> - <%= format_time(issue.updated_on) %>
+ <%= link_to h(issue.subject), :controller => 'issues', :action => 'show', :id => issue %> +
+<% else %> + <%=l(:label_no_data)%> +<% end %> \ No newline at end of file diff --git a/rest_sys/app/views/issues/_pdf.rfpdf b/rest_sys/app/views/issues/_pdf.rfpdf new file mode 100644 index 000000000..558399abb --- /dev/null +++ b/rest_sys/app/views/issues/_pdf.rfpdf @@ -0,0 +1,102 @@ +<% pdf.SetFontStyle('B',11) + pdf.Cell(190,10, "#{issue.project.name} - #{issue.tracker.name} # #{issue.id}: #{issue.subject}") + pdf.Ln + + y0 = pdf.GetY + + pdf.SetFontStyle('B',9) + pdf.Cell(35,5, l(:field_status) + ":","LT") + pdf.SetFontStyle('',9) + pdf.Cell(60,5, issue.status.name,"RT") + pdf.SetFontStyle('B',9) + pdf.Cell(35,5, l(:field_priority) + ":","LT") + pdf.SetFontStyle('',9) + pdf.Cell(60,5, issue.priority.name,"RT") + pdf.Ln + + pdf.SetFontStyle('B',9) + pdf.Cell(35,5, l(:field_author) + ":","L") + pdf.SetFontStyle('',9) + pdf.Cell(60,5, issue.author.name,"R") + pdf.SetFontStyle('B',9) + pdf.Cell(35,5, l(:field_category) + ":","L") + pdf.SetFontStyle('',9) + pdf.Cell(60,5, (issue.category ? issue.category.name : "-"),"R") + pdf.Ln + + pdf.SetFontStyle('B',9) + pdf.Cell(35,5, l(:field_created_on) + ":","L") + pdf.SetFontStyle('',9) + pdf.Cell(60,5, format_date(issue.created_on),"R") + pdf.SetFontStyle('B',9) + pdf.Cell(35,5, l(:field_assigned_to) + ":","L") + pdf.SetFontStyle('',9) + pdf.Cell(60,5, (issue.assigned_to ? issue.assigned_to.name : "-"),"R") + pdf.Ln + + pdf.SetFontStyle('B',9) + pdf.Cell(35,5, l(:field_updated_on) + ":","LB") + pdf.SetFontStyle('',9) + pdf.Cell(60,5, format_date(issue.updated_on),"RB") + pdf.SetFontStyle('B',9) + pdf.Cell(35,5, l(:field_due_date) + ":","LB") + pdf.SetFontStyle('',9) + pdf.Cell(60,5, format_date(issue.due_date),"RB") + pdf.Ln + + for custom_value in issue.custom_values + pdf.SetFontStyle('B',9) + pdf.Cell(35,5, custom_value.custom_field.name + ":","L") + pdf.SetFontStyle('',9) + pdf.MultiCell(155,5, (show_value custom_value),"R") + end + + pdf.SetFontStyle('B',9) + pdf.Cell(35,5, l(:field_subject) + ":","LTB") + pdf.SetFontStyle('',9) + pdf.Cell(155,5, issue.subject,"RTB") + pdf.Ln + + pdf.SetFontStyle('B',9) + pdf.Cell(35,5, l(:field_description) + ":") + pdf.SetFontStyle('',9) + pdf.MultiCell(155,5, issue.description,"BR") + + pdf.Line(pdf.GetX, y0, pdf.GetX, pdf.GetY) + pdf.Line(pdf.GetX, pdf.GetY, 170, pdf.GetY) + + pdf.Ln + + pdf.SetFontStyle('B',9) + pdf.Cell(190,5, l(:label_history), "B") + pdf.Ln + for journal in issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC") + pdf.SetFontStyle('B',8) + pdf.Cell(190,5, format_time(journal.created_on) + " - " + journal.user.name) + pdf.Ln + pdf.SetFontStyle('I',8) + for detail in journal.details + pdf.Cell(190,5, "- " + show_detail(detail, true)) + pdf.Ln + end + if journal.notes? + pdf.SetFontStyle('',8) + pdf.MultiCell(190,5, journal.notes) + end + pdf.Ln + end + + if issue.attachments.any? + pdf.SetFontStyle('B',9) + pdf.Cell(190,5, l(:label_attachment_plural), "B") + pdf.Ln + for attachment in issue.attachments + pdf.SetFontStyle('',8) + pdf.Cell(80,5, attachment.filename) + pdf.Cell(20,5, number_to_human_size(attachment.filesize),0,0,"R") + pdf.Cell(25,5, format_date(attachment.created_on),0,0,"R") + pdf.Cell(65,5, attachment.author.name,0,0,"R") + pdf.Ln + end + end +%> diff --git a/rest_sys/app/views/issues/_relations.rhtml b/rest_sys/app/views/issues/_relations.rhtml new file mode 100644 index 000000000..d4b3e5aa6 --- /dev/null +++ b/rest_sys/app/views/issues/_relations.rhtml @@ -0,0 +1,31 @@ +
+<% if authorize_for('issue_relations', 'new') %> + <%= toggle_link l(:button_add), 'new-relation-form'%> +<% end %> +
+ +

<%=l(:label_related_issues)%>

+ +<% if @issue.relations.any? %> + +<% @issue.relations.each do |relation| %> + + + + + + + + +<% end %> +
<%= l(relation.label_for(@issue)) %> <%= "(#{lwr(:actionview_datehelper_time_in_words_day, relation.delay)})" if relation.delay && relation.delay != 0 %> <%= link_to_issue relation.other_issue(@issue) %><%=h relation.other_issue(@issue).subject %><%= relation.other_issue(@issue).status.name %><%= format_date(relation.other_issue(@issue).start_date) %><%= format_date(relation.other_issue(@issue).due_date) %><%= link_to_remote(image_tag('delete.png'), { :url => {:controller => 'issue_relations', :action => 'destroy', :issue_id => @issue, :id => relation}, + :method => :post + }, :title => l(:label_relation_delete)) if authorize_for('issue_relations', 'destroy') %>
+<% end %> + +<% remote_form_for(:relation, @relation, + :url => {:controller => 'issue_relations', :action => 'new', :issue_id => @issue}, + :method => :post, + :html => {:id => 'new-relation-form', :style => (@relation ? '' : 'display: none;')}) do |f| %> +<%= render :partial => 'issue_relations/form', :locals => {:f => f}%> +<% end %> diff --git a/rest_sys/app/views/issues/_sidebar.rhtml b/rest_sys/app/views/issues/_sidebar.rhtml new file mode 100644 index 000000000..e6c63896b --- /dev/null +++ b/rest_sys/app/views/issues/_sidebar.rhtml @@ -0,0 +1,18 @@ +<% if authorize_for('projects', 'add_issue') && @project.trackers.any? %> +

<%= l(:label_issue_new) %>

+<%= l(:label_tracker) %>: <%= new_issue_selector %> +<% end %> + +

<%= l(:label_issue_plural) %>

+<%= link_to l(:label_issue_view_all), { :controller => 'issues', :action => 'index', :project_id => @project, :set_filter => 1 } %>
+<%= link_to l(:field_summary), :controller => 'reports', :action => 'issue_report', :id => @project %>
+<%= link_to l(:label_change_log), :controller => 'projects', :action => 'changelog', :id => @project %> + +

<%= l(:label_query_plural) %>

+ +<% queries = @project.queries.find(:all, + :order => "name ASC", + :conditions => ["is_public = ? or user_id = ?", true, (User.current.logged? ? User.current.id : 0)]) + queries.each do |query| %> +<%= link_to query.name, :controller => 'issues', :action => 'index', :project_id => @project, :query_id => query %>
+<% end %> diff --git a/rest_sys/app/views/issues/change_status.rhtml b/rest_sys/app/views/issues/change_status.rhtml new file mode 100644 index 000000000..a1e294556 --- /dev/null +++ b/rest_sys/app/views/issues/change_status.rhtml @@ -0,0 +1,38 @@ +

<%=l(:label_issue)%> #<%= @issue.id %>: <%=h @issue.subject %>

+ +<%= error_messages_for 'issue' %> +<% labelled_tabular_form_for(:issue, @issue, :url => {:action => 'change_status', :id => @issue}, :html => {:multipart => true}) do |f| %> + +<%= hidden_field_tag 'confirm', 1 %> +<%= hidden_field_tag 'new_status_id', @new_status.id %> +<%= f.hidden_field :lock_version %> + +
+
+

<%= @new_status.name %>

+

<%= f.select :assigned_to_id, (@issue.assignable_users.collect {|m| [m.name, m.id]}), :include_blank => true %>

+

<%= f.select :done_ratio, ((0..10).to_a.collect {|r| ["#{r*10} %", r*10] }) %>

+

<%= f.select :fixed_version_id, (@project.versions.sort.collect {|v| [v.name, v.id]}), { :include_blank => true } %>

+
+
+<% if authorize_for('timelog', 'edit') %> +<% fields_for :time_entry, @time_entry, { :builder => TabularFormBuilder, :lang => current_language} do |time_entry| %> +

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

+

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

+

<%= time_entry.select :activity_id, (@activities.collect {|p| [p.name, p.id]}) %>

+<% end %> +<% end %> +
+ +
+ +

+<%= text_area_tag 'notes', @notes, :cols => 60, :rows => 10, :class => 'wiki-edit' %>

+ +

+<%= file_field_tag 'attachments[]', :size => 30 %> (<%= l(:label_max_size) %>: <%= number_to_human_size(Setting.attachment_max_size.to_i.kilobytes) %>)

+
+ +<%= submit_tag l(:button_save) %> +<% end %> diff --git a/rest_sys/app/views/issues/changes.rxml b/rest_sys/app/views/issues/changes.rxml new file mode 100644 index 000000000..f1aa5d206 --- /dev/null +++ b/rest_sys/app/views/issues/changes.rxml @@ -0,0 +1,30 @@ +xml.instruct! +xml.feed "xmlns" => "http://www.w3.org/2005/Atom" do + xml.title @title + xml.link "rel" => "self", "href" => url_for(:controller => 'feeds', :action => 'history', :format => 'atom', :only_path => false) + xml.link "rel" => "alternate", "href" => url_for(:controller => 'welcome', :only_path => false) + xml.id url_for(:controller => 'welcome', :only_path => false) + xml.updated((@changes.first ? @changes.first.event_datetime : Time.now).xmlschema) + xml.author { xml.name "#{Setting.app_title}" } + @changes.each do |change| + issue = change.issue + xml.entry do + xml.title "#{issue.project.name} - #{issue.tracker.name} ##{issue.id}: #{issue.subject}" + xml.link "rel" => "alternate", "href" => url_for(:controller => 'issues' , :action => 'show', :id => issue, :only_path => false) + xml.id url_for(:controller => 'issues' , :action => 'show', :id => issue, :journal_id => change, :only_path => false) + xml.updated change.created_on.xmlschema + xml.author do + xml.name change.user.name + xml.email(change.user.mail) + end + xml.content "type" => "html" do + xml.text! '
    ' + change.details.each do |detail| + xml.text! '
  • ' + show_detail(detail, false) + '
  • ' + end + xml.text! '
' + xml.text! textilizable(change.notes) unless change.notes.blank? + end + end + end +end \ No newline at end of file diff --git a/rest_sys/app/views/issues/context_menu.rhtml b/rest_sys/app/views/issues/context_menu.rhtml new file mode 100644 index 000000000..3af49fb04 --- /dev/null +++ b/rest_sys/app/views/issues/context_menu.rhtml @@ -0,0 +1,40 @@ +<% back_to = url_for(:controller => 'issues', :action => 'index', :project_id => @project) %> +
    +
  • <%= context_menu_link l(:button_edit), {:controller => 'issues', :action => 'edit', :id => @issue}, + :class => 'icon-edit', :disabled => !@can[:edit] %>
  • +
  • + <%= l(:field_status) %> +
      + <% @statuses.each do |s| %> +
    • <%= context_menu_link s.name, {:controller => 'issues', :action => 'change_status', :id => @issue, :new_status_id => s}, + :selected => (s == @issue.status), :disabled => !(@can[:change_status] && @allowed_statuses.include?(s)) %>
    • + <% end %> +
    +
  • +
  • + <%= l(:field_priority) %> +
      + <% @priorities.each do |p| %> +
    • <%= context_menu_link p.name, {:controller => 'issues', :action => 'edit', :id => @issue, 'issue[priority_id]' => p, :back_to => back_to}, :method => :post, + :selected => (p == @issue.priority), :disabled => !@can[:edit] %>
    • + <% end %> +
    +
  • +
  • + <%= l(:field_assigned_to) %> +
      + <% @assignables.each do |u| %> +
    • <%= context_menu_link u.name, {:controller => 'issues', :action => 'edit', :id => @issue, 'issue[assigned_to_id]' => u, :back_to => back_to}, :method => :post, + :selected => (u == @issue.assigned_to), :disabled => !(@can[:edit] || @can[:change_status]) %>
    • + <% end %> +
    • <%= context_menu_link l(:label_nobody), {:controller => 'issues', :action => 'edit', :id => @issue, 'issue[assigned_to_id]' => '', :back_to => back_to}, :method => :post, + :selected => @issue.assigned_to.nil?, :disabled => !(@can[:edit] || @can[:change_status]) %>
    • +
    +
  • +
  • <%= context_menu_link l(:button_copy), {:controller => 'projects', :action => 'add_issue', :id => @project, :copy_from => @issue}, + :class => 'icon-copy', :disabled => !@can[:copy] %>
  • +
  • <%= context_menu_link l(:button_move), {:controller => 'projects', :action => 'move_issues', :id => @project, "issue_ids[]" => @issue.id }, + :class => 'icon-move', :disabled => !@can[:move] %> +
  • <%= context_menu_link l(:button_delete), {:controller => 'issues', :action => 'destroy', :id => @issue}, + :method => :post, :confirm => l(:text_are_you_sure), :class => 'icon-del', :disabled => !@can[:delete] %>
  • +
diff --git a/rest_sys/app/views/issues/edit.rhtml b/rest_sys/app/views/issues/edit.rhtml new file mode 100644 index 000000000..1577216ed --- /dev/null +++ b/rest_sys/app/views/issues/edit.rhtml @@ -0,0 +1,19 @@ +

<%=h "#{@issue.tracker.name} ##{@issue.id}" %>

+ +<% labelled_tabular_form_for :issue, @issue, + :url => {:action => 'edit'}, + :html => {:id => 'issue-form'} do |f| %> + <%= render :partial => 'form', :locals => {:f => f} %> + <%= f.hidden_field :lock_version %> + <%= submit_tag l(:button_save) %> + <%= link_to_remote l(:label_preview), + { :url => { :controller => 'issues', :action => 'preview', :id => @issue }, + :method => 'post', + :update => 'preview', + :with => "Form.serialize('issue-form')", + :complete => "location.href='#preview-top'" + }, :accesskey => accesskey(:preview) %> +<% end %> + + +
diff --git a/rest_sys/app/views/issues/index.rfpdf b/rest_sys/app/views/issues/index.rfpdf new file mode 100644 index 000000000..d5a8d3c31 --- /dev/null +++ b/rest_sys/app/views/issues/index.rfpdf @@ -0,0 +1,50 @@ +<% pdf=IfpdfHelper::IFPDF.new(current_language) + title = @project ? "#{@project.name} - #{l(:label_issue_plural)}" : "#{l(:label_issue_plural)}" + pdf.SetTitle(title) + pdf.AliasNbPages + pdf.footer_date = format_date(Date.today) + pdf.AddPage("L") + row_height = 7 + + # + # title + # + pdf.SetFontStyle('B',11) + pdf.Cell(190,10, title) + pdf.Ln + + # + # headers + # + pdf.SetFontStyle('B',10) + pdf.SetFillColor(230, 230, 230) + pdf.Cell(15, row_height, "#", 0, 0, 'L', 1) + pdf.Cell(30, row_height, l(:field_tracker), 0, 0, 'L', 1) + pdf.Cell(30, row_height, l(:field_status), 0, 0, 'L', 1) + pdf.Cell(30, row_height, l(:field_priority), 0, 0, 'L', 1) + pdf.Cell(40, row_height, l(:field_assigned_to), 0, 0, 'L', 1) + pdf.Cell(25, row_height, l(:field_updated_on), 0, 0, 'L', 1) + pdf.Cell(0, row_height, l(:field_subject), 0, 0, 'L', 1) + pdf.Line(10, pdf.GetY, 287, pdf.GetY) + pdf.Ln + pdf.Line(10, pdf.GetY, 287, pdf.GetY) + pdf.SetY(pdf.GetY() + 1) + + # + # rows + # + pdf.SetFontStyle('',9) + pdf.SetFillColor(255, 255, 255) + @issues.each do |issue| + pdf.Cell(15, row_height, issue.id.to_s, 0, 0, 'L', 1) + pdf.Cell(30, row_height, issue.tracker.name, 0, 0, 'L', 1) + pdf.Cell(30, row_height, issue.status.name, 0, 0, 'L', 1) + pdf.Cell(30, row_height, issue.priority.name, 0, 0, 'L', 1) + pdf.Cell(40, row_height, issue.assigned_to ? issue.assigned_to.name : '', 0, 0, 'L', 1) + pdf.Cell(25, row_height, format_date(issue.updated_on), 0, 0, 'L', 1) + pdf.MultiCell(0, row_height, (@project == issue.project ? issue.subject : "#{issue.project.name} - #{issue.subject}")) + pdf.Line(10, pdf.GetY, 287, pdf.GetY) + pdf.SetY(pdf.GetY() + 1) + end +%> +<%= pdf.Output %> \ No newline at end of file diff --git a/rest_sys/app/views/issues/index.rhtml b/rest_sys/app/views/issues/index.rhtml new file mode 100644 index 000000000..de0fd4add --- /dev/null +++ b/rest_sys/app/views/issues/index.rhtml @@ -0,0 +1,73 @@ +<% if @query.new_record? %> +

<%=l(:label_issue_plural)%>

+ <% set_html_title l(:label_issue_plural) %> + + <% form_tag({ :controller => 'queries', :action => 'new' }, :id => 'query_form') do %> + <%= hidden_field_tag('project_id', @project.id) if @project %> + <%= render :partial => 'queries/filters', :locals => {:query => @query} %> +
+ <%= link_to_remote l(:button_apply), + { :url => { :set_filter => 1 }, + :update => "content", + :with => "Form.serialize('query_form')" + }, :class => 'icon icon-checked' %> + + <%= link_to_remote l(:button_clear), + { :url => { :set_filter => 1 }, + :update => "content", + }, :class => 'icon icon-reload' %> + + <% if current_role && current_role.allowed_to?(:save_queries) %> + <%= link_to l(:button_save), {}, :onclick => "$('query_form').submit(); return false;", :class => 'icon icon-save' %> + <% end %> +
+
+   + <% end %> +<% else %> +
+ <% if @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 %> +
+ +

<%= @query.name %>

+
+ <% set_html_title @query.name %> +<% end %> +<%= error_messages_for 'query' %> +<% if @query.valid? %> +<% if @issues.empty? %> +

<%= l(:label_no_data) %>

+<% else %> +<% form_tag({:controller => 'projects', :action => 'bulk_edit_issues', :id => @project}, :id => 'issues_form', :onsubmit => "if (!checkBulkEdit(this)) {alert('#{l(:notice_no_issue_selected)}'); return false;}" ) do %> +<%= render :partial => 'issues/list', :locals => {:issues => @issues, :query => @query} %> +
+<%= l(:label_export_to) %> +<%= link_to 'CSV', {:format => 'csv'}, :class => 'icon icon-csv' %>, +<%= link_to 'PDF', {:format => 'pdf'}, :class => 'icon icon-pdf' %> +
+

<%= pagination_links_full @issue_pages %> +[ <%= @issue_pages.current.first_item %> - <%= @issue_pages.current.last_item %> / <%= @issue_count %> ]

+<% end %> +<% end %> +<% end %> + +<% content_for :sidebar do %> + <%= render :partial => 'issues/sidebar' %> +<% end if @project%> + +<% content_for :header_tags do %> + <%= auto_discovery_link_tag(:atom, {:query_id => @query, :format => 'atom', :page => nil, :key => User.current.rss_key}, :title => l(:label_issue_plural)) %> + <%= auto_discovery_link_tag(:atom, {:action => 'changes', :query_id => @query, :format => 'atom', :page => nil, :key => User.current.rss_key}, :title => l(:label_changes_details)) %> + <%= javascript_include_tag 'calendar/calendar' %> + <%= javascript_include_tag "calendar/lang/calendar-#{current_language}.js" %> + <%= javascript_include_tag 'calendar/calendar-setup' %> + <%= stylesheet_link_tag 'calendar' %> + <%= javascript_include_tag 'context_menu' %> + <%= stylesheet_link_tag 'context_menu' %> +<% end %> + + +<%= javascript_tag 'new ContextMenu({})' %> diff --git a/rest_sys/app/views/issues/show.rfpdf b/rest_sys/app/views/issues/show.rfpdf new file mode 100644 index 000000000..08f2cb92d --- /dev/null +++ b/rest_sys/app/views/issues/show.rfpdf @@ -0,0 +1,10 @@ +<% pdf=IfpdfHelper::IFPDF.new(current_language) + pdf.SetTitle("#{@project.name} - ##{@issue.tracker.name} #{@issue.id}") + pdf.AliasNbPages + pdf.footer_date = format_date(Date.today) + pdf.AddPage + + render :partial => 'issues/pdf', :locals => { :pdf => pdf, :issue => @issue } +%> + +<%= pdf.Output %> diff --git a/rest_sys/app/views/issues/show.rhtml b/rest_sys/app/views/issues/show.rhtml new file mode 100644 index 000000000..6e4f22574 --- /dev/null +++ b/rest_sys/app/views/issues/show.rhtml @@ -0,0 +1,123 @@ +
+<%= show_and_goto_link(l(:label_add_note), 'add-note', :class => 'icon icon-note') if authorize_for('issues', 'add_note') %> +<%= link_to_if_authorized l(:button_edit), {:controller => 'issues', :action => 'edit', :id => @issue}, :class => 'icon icon-edit', :accesskey => accesskey(:edit) %> +<%= link_to_if_authorized l(:button_log_time), {:controller => 'timelog', :action => 'edit', :issue_id => @issue}, :class => 'icon icon-time' %> +<%= watcher_tag(@issue, User.current) %> +<%= link_to_if_authorized l(:button_copy), {:controller => 'projects', :action => 'add_issue', :id => @project, :copy_from => @issue }, :class => 'icon icon-copy' %> +<%= link_to_if_authorized l(:button_move), {:controller => 'projects', :action => 'move_issues', :id => @project, "issue_ids[]" => @issue.id }, :class => 'icon icon-move' %> +<%= link_to_if_authorized l(:button_delete), {:controller => 'issues', :action => 'destroy', :id => @issue}, :confirm => l(:text_are_you_sure), :method => :post, :class => 'icon icon-del' %> +
+ +

<%= @issue.tracker.name %> #<%= @issue.id %>

+ +
"> +

<%=h @issue.subject %>

+

+ <%= authoring @issue.created_on, @issue.author %>. + <%= l(:label_updated_time, distance_of_time_in_words(Time.now, @issue.updated_on)) if @issue.created_on != @issue.updated_on %>. +

+ + + + + + + + + + + + + + + + + <% if User.current.allowed_to?(:view_time_entries, @project) %> + + + <% end %> + + + + <% if @issue.estimated_hours %> + + <% end %> + + +<% n = 0 +for custom_value in @custom_values %> + +<% n = n + 1 + if (n > 1) + n = 0 %> + + <%end +end %> + +
<%=l(:field_status)%> :<%= @issue.status.name %><%=l(:field_start_date)%> :<%= format_date(@issue.start_date) %>
<%=l(:field_priority)%> :<%= @issue.priority.name %><%=l(:field_due_date)%> :<%= format_date(@issue.due_date) %>
<%=l(:field_assigned_to)%> :<%= @issue.assigned_to ? link_to_user(@issue.assigned_to) : "-" %><%=l(:field_done_ratio)%> :<%= progress_bar @issue.done_ratio, :width => '80px', :legend => "#{@issue.done_ratio}%" %>
<%=l(:field_category)%> :<%=h @issue.category ? @issue.category.name : "-" %><%=l(:label_spent_time)%> :<%= @issue.spent_hours > 0 ? (link_to lwr(:label_f_hour, @issue.spent_hours), {:controller => 'timelog', :action => 'details', :issue_id => @issue}, :class => 'icon icon-time') : "-" %>
<%=l(:field_fixed_version)%> :<%= @issue.fixed_version ? link_to_version(@issue.fixed_version) : "-" %><%=l(:field_estimated_hours)%> :<%= lwr(:label_f_hour, @issue.estimated_hours) %>
<%= custom_value.custom_field.name %> :<%= simple_format(h(show_value(custom_value))) %>
+
+ +<% if @issue.changesets.any? %> +
+ <%= l(:label_revision_plural) %>: <%= @issue.changesets.collect{|changeset| link_to(changeset.revision, :controller => 'repositories', :action => 'revision', :id => @project, :rev => changeset.revision)}.join(", ") %> +
+<% end %> + +

<%=l(:field_description)%>

+<%= textilizable @issue, :description, :attachments => @issue.attachments %> + +<% if @issue.attachments.any? %> +<%= link_to_attachments @issue.attachments, :delete_url => (authorize_for('issues', 'destroy_attachment') ? {:controller => 'issues', :action => 'destroy_attachment', :id => @issue} : nil) %> +<% end %> + +<% if authorize_for('issue_relations', 'new') || @issue.relations.any? %> +
+
+<%= render :partial => 'relations' %> +
+<% end %> + +
+ +<% if authorize_for('issues', 'change_status') and @status_options and !@status_options.empty? %> + <% form_tag({:controller => 'issues', :action => 'change_status', :id => @issue}) do %> +

<%=l(:label_change_status)%> : + + <%= submit_tag l(:button_change) %>

+ <% end %> +<% end %> + +<% if @journals.any? %> +
+

<%=l(:label_history)%>

+<%= render :partial => 'history', :locals => { :journals => @journals } %> +
+<% end %> + +<% if authorize_for('issues', 'add_note') %> + + +<% end %> + +
+<%= l(:label_export_to) %><%= link_to 'PDF', {:format => 'pdf'}, :class => 'icon icon-pdf' %> +
+  + +<% set_html_title "#{@issue.tracker.name} ##{@issue.id}: #{@issue.subject}" %> + +<% content_for :sidebar do %> + <%= render :partial => 'issues/sidebar' %> +<% end %> diff --git a/rest_sys/app/views/layouts/_project_selector.rhtml b/rest_sys/app/views/layouts/_project_selector.rhtml new file mode 100644 index 000000000..ce2f15e03 --- /dev/null +++ b/rest_sys/app/views/layouts/_project_selector.rhtml @@ -0,0 +1,12 @@ +<% user_projects_by_root = User.current.projects.find(:all, :include => :parent).group_by(&:root) %> + diff --git a/rest_sys/app/views/layouts/base.rhtml b/rest_sys/app/views/layouts/base.rhtml new file mode 100644 index 000000000..df7b2daf9 --- /dev/null +++ b/rest_sys/app/views/layouts/base.rhtml @@ -0,0 +1,79 @@ + + + +<%=h html_title %> + + + +<%= stylesheet_link_tag 'application', :media => 'all' %> +<%= javascript_include_tag :defaults %> +<%= stylesheet_link_tag 'jstoolbar' %> + + +<%= yield :header_tags %> + + +
+
+ <% if User.current.logged? %> + <%=l(:label_logged_as)%> <%= User.current.login %> - + <%= link_to l(:label_my_account), { :controller => 'my', :action => 'account' }, :class => 'myaccount' %> + <%= link_to l(:label_logout), { :controller => 'account', :action => 'logout' }, :class => 'logout' %> + <% else %> + <%= link_to l(:label_login), { :controller => 'account', :action => 'login' }, :class => 'signin' %> + <%= link_to(l(:label_register), { :controller => 'account',:action => 'register' }, :class => 'register') if Setting.self_registration? %> + <% end %> +
+ <%= link_to l(:label_home), home_url, :class => 'home' %> + <%= link_to l(:label_my_page), { :controller => 'my', :action => 'page'}, :class => 'mypage' if User.current.logged? %> + <%= link_to l(:label_project_plural), { :controller => 'projects' }, :class => 'projects' %> + <%= link_to l(:label_administration), { :controller => 'admin' }, :class => 'admin' if User.current.admin? %> + <%= link_to l(:label_help), Redmine::Info.help_url, :class => 'help' %> +
+ + + +<%= tag('div', {:id => 'main', :class => (has_content?(:sidebar) ? '' : 'nosidebar')}, true) %> + + +
+ <%= content_tag('div', flash[:error], :class => 'flash error') if flash[:error] %> + <%= content_tag('div', flash[:notice], :class => 'flash notice') if flash[:notice] %> + <%= yield %> +
+ + + + + + + diff --git a/rest_sys/app/views/mailer/_issue_text_html.rhtml b/rest_sys/app/views/mailer/_issue_text_html.rhtml new file mode 100644 index 000000000..a3eb05b01 --- /dev/null +++ b/rest_sys/app/views/mailer/_issue_text_html.rhtml @@ -0,0 +1,15 @@ +

<%= link_to "#{issue.tracker.name} ##{issue.id}: #{issue.subject}", issue_url %>

+ +
    +
  • <%=l(:field_author)%>: <%= issue.author %>
  • +
  • <%=l(:field_status)%>: <%= issue.status %>
  • +
  • <%=l(:field_priority)%>: <%= issue.priority %>
  • +
  • <%=l(:field_assigned_to)%>: <%= issue.assigned_to %>
  • +
  • <%=l(:field_category)%>: <%= issue.category %>
  • +
  • <%=l(:field_fixed_version)%>: <%= issue.fixed_version %>
  • +<% issue.custom_values.each do |c| %> +
  • <%= c.custom_field.name %>: <%= show_value(c) %>
  • +<% end %> +
+ +<%= textilizable(issue.description) %> diff --git a/rest_sys/app/views/mailer/_issue_text_plain.rhtml b/rest_sys/app/views/mailer/_issue_text_plain.rhtml new file mode 100644 index 000000000..6b87c1808 --- /dev/null +++ b/rest_sys/app/views/mailer/_issue_text_plain.rhtml @@ -0,0 +1,13 @@ +<%= "#{issue.tracker.name} ##{issue.id}: #{issue.subject}" %> +<%= issue_url %> + +<%=l(:field_author)%>: <%= issue.author %> +<%=l(:field_status)%>: <%= issue.status %> +<%=l(:field_priority)%>: <%= issue.priority %> +<%=l(:field_assigned_to)%>: <%= issue.assigned_to %> +<%=l(:field_category)%>: <%= issue.category %> +<%=l(:field_fixed_version)%>: <%= issue.fixed_version %> +<% issue.custom_values.each do |c| %><%= c.custom_field.name %>: <%= show_value(c) %> +<% end %> + +<%= issue.description %> diff --git a/rest_sys/app/views/mailer/account_activation_request.text.html.rhtml b/rest_sys/app/views/mailer/account_activation_request.text.html.rhtml new file mode 100644 index 000000000..145ecfc8e --- /dev/null +++ b/rest_sys/app/views/mailer/account_activation_request.text.html.rhtml @@ -0,0 +1,2 @@ +

<%= l(:mail_body_account_activation_request, @user.login) %>

+

<%= link_to @url, @url %>

diff --git a/rest_sys/app/views/mailer/account_activation_request.text.plain.rhtml b/rest_sys/app/views/mailer/account_activation_request.text.plain.rhtml new file mode 100644 index 000000000..f431e22d3 --- /dev/null +++ b/rest_sys/app/views/mailer/account_activation_request.text.plain.rhtml @@ -0,0 +1,2 @@ +<%= l(:mail_body_account_activation_request, @user.login) %> +<%= @url %> diff --git a/rest_sys/app/views/mailer/account_information.text.html.rhtml b/rest_sys/app/views/mailer/account_information.text.html.rhtml new file mode 100644 index 000000000..3b6ab6a9d --- /dev/null +++ b/rest_sys/app/views/mailer/account_information.text.html.rhtml @@ -0,0 +1,11 @@ +<% if @user.auth_source %> +

<%= l(:mail_body_account_information_external, @user.auth_source.name) %>

+<% else %> +

<%= l(:mail_body_account_information) %>:

+
    +
  • <%= l(:field_login) %>: <%= @user.login %>
  • +
  • <%= l(:field_password) %>: <%= @password %>
  • +
+<% end %> + +

<%= l(:label_login) %>: <%= auto_link(@login_url) %>

diff --git a/rest_sys/app/views/mailer/account_information.text.plain.rhtml b/rest_sys/app/views/mailer/account_information.text.plain.rhtml new file mode 100644 index 000000000..0a02566d9 --- /dev/null +++ b/rest_sys/app/views/mailer/account_information.text.plain.rhtml @@ -0,0 +1,6 @@ +<% if @user.auth_source %><%= l(:mail_body_account_information_external, @user.auth_source.name) %> +<% else %><%= l(:mail_body_account_information) %>: +* <%= l(:field_login) %>: <%= @user.login %> +* <%= l(:field_password) %>: <%= @password %> +<% end %> +<%= l(:label_login) %>: <%= @login_url %> diff --git a/rest_sys/app/views/mailer/attachments_added.text.html.rhtml b/rest_sys/app/views/mailer/attachments_added.text.html.rhtml new file mode 100644 index 000000000..d2355b1c4 --- /dev/null +++ b/rest_sys/app/views/mailer/attachments_added.text.html.rhtml @@ -0,0 +1,5 @@ +<%= link_to @added_to, @added_to_url %>
+ +
    <% @attachments.each do |attachment | %> +
  • <%= attachment.filename %>
  • +<% end %>
diff --git a/rest_sys/app/views/mailer/attachments_added.text.plain.rhtml b/rest_sys/app/views/mailer/attachments_added.text.plain.rhtml new file mode 100644 index 000000000..28cb8285e --- /dev/null +++ b/rest_sys/app/views/mailer/attachments_added.text.plain.rhtml @@ -0,0 +1,4 @@ +<%= @added_to %><% @attachments.each do |attachment | %> +- <%= attachment.filename %><% end %> + +<%= @added_to_url %> diff --git a/rest_sys/app/views/mailer/document_added.text.html.rhtml b/rest_sys/app/views/mailer/document_added.text.html.rhtml new file mode 100644 index 000000000..2ef63012b --- /dev/null +++ b/rest_sys/app/views/mailer/document_added.text.html.rhtml @@ -0,0 +1,3 @@ +<%= link_to @document.title, @document_url %> (<%= @document.category.name %>)
+
+<%= textilizable(@document.description) %> diff --git a/rest_sys/app/views/mailer/document_added.text.plain.rhtml b/rest_sys/app/views/mailer/document_added.text.plain.rhtml new file mode 100644 index 000000000..a6a72829e --- /dev/null +++ b/rest_sys/app/views/mailer/document_added.text.plain.rhtml @@ -0,0 +1,4 @@ +<%= @document.title %> (<%= @document.category.name %>) +<%= @document_url %> + +<%= @document.description %> diff --git a/rest_sys/app/views/mailer/issue_add.text.html.rhtml b/rest_sys/app/views/mailer/issue_add.text.html.rhtml new file mode 100644 index 000000000..6d20c333f --- /dev/null +++ b/rest_sys/app/views/mailer/issue_add.text.html.rhtml @@ -0,0 +1,3 @@ +<%= l(:text_issue_added, "##{@issue.id}") %> +
+<%= render :partial => "issue_text_html", :locals => { :issue => @issue, :issue_url => @issue_url } %> diff --git a/rest_sys/app/views/mailer/issue_add.text.plain.rhtml b/rest_sys/app/views/mailer/issue_add.text.plain.rhtml new file mode 100644 index 000000000..38c17e777 --- /dev/null +++ b/rest_sys/app/views/mailer/issue_add.text.plain.rhtml @@ -0,0 +1,3 @@ +<%= l(:text_issue_added, "##{@issue.id}") %> +---------------------------------------- +<%= render :partial => "issue_text_plain", :locals => { :issue => @issue, :issue_url => @issue_url } %> diff --git a/rest_sys/app/views/mailer/issue_edit.text.html.rhtml b/rest_sys/app/views/mailer/issue_edit.text.html.rhtml new file mode 100644 index 000000000..9af8592b4 --- /dev/null +++ b/rest_sys/app/views/mailer/issue_edit.text.html.rhtml @@ -0,0 +1,10 @@ +<%= l(:text_issue_updated, "##{@issue.id}") %>
+<%= @journal.user.name %> +
    +<% for detail in @journal.details %> +
  • <%= show_detail(detail, true) %>
  • +<% end %> +
+<%= textilizable(@journal.notes) %> +
+<%= render :partial => "issue_text_html", :locals => { :issue => @issue, :issue_url => @issue_url } %> diff --git a/rest_sys/app/views/mailer/issue_edit.text.plain.rhtml b/rest_sys/app/views/mailer/issue_edit.text.plain.rhtml new file mode 100644 index 000000000..04b1bc54a --- /dev/null +++ b/rest_sys/app/views/mailer/issue_edit.text.plain.rhtml @@ -0,0 +1,8 @@ +<%= l(:text_issue_updated, "##{@issue.id}") %> +<%= @journal.user.name %> +<% for detail in @journal.details %> +<%= show_detail(detail, true) %> +<% end %> +<%= @journal.notes if @journal.notes? %> +---------------------------------------- +<%= render :partial => "issue_text_plain", :locals => { :issue => @issue, :issue_url => @issue_url } %> diff --git a/rest_sys/app/views/mailer/layout.text.html.rhtml b/rest_sys/app/views/mailer/layout.text.html.rhtml new file mode 100644 index 000000000..b78e92bdd --- /dev/null +++ b/rest_sys/app/views/mailer/layout.text.html.rhtml @@ -0,0 +1,17 @@ + + + + + +<%= yield %> +
+<%= Redmine::WikiFormatting.to_html(Setting.emails_footer) %> + + diff --git a/rest_sys/app/views/mailer/layout.text.plain.rhtml b/rest_sys/app/views/mailer/layout.text.plain.rhtml new file mode 100644 index 000000000..ec3e1bfa0 --- /dev/null +++ b/rest_sys/app/views/mailer/layout.text.plain.rhtml @@ -0,0 +1,3 @@ +<%= yield %> +---------------------------------------- +<%= Setting.emails_footer %> diff --git a/rest_sys/app/views/mailer/lost_password.text.html.rhtml b/rest_sys/app/views/mailer/lost_password.text.html.rhtml new file mode 100644 index 000000000..26eacfa92 --- /dev/null +++ b/rest_sys/app/views/mailer/lost_password.text.html.rhtml @@ -0,0 +1,2 @@ +

<%= l(:mail_body_lost_password) %>
+<%= auto_link(@url) %>

diff --git a/rest_sys/app/views/mailer/lost_password.text.plain.rhtml b/rest_sys/app/views/mailer/lost_password.text.plain.rhtml new file mode 100644 index 000000000..aec1b5b86 --- /dev/null +++ b/rest_sys/app/views/mailer/lost_password.text.plain.rhtml @@ -0,0 +1,2 @@ +<%= l(:mail_body_lost_password) %> +<%= @url %> diff --git a/rest_sys/app/views/mailer/message_posted.text.html.rhtml b/rest_sys/app/views/mailer/message_posted.text.html.rhtml new file mode 100644 index 000000000..837272c0a --- /dev/null +++ b/rest_sys/app/views/mailer/message_posted.text.html.rhtml @@ -0,0 +1,4 @@ +

<%=h @message.board.project.name %> - <%=h @message.board.name %>: <%= link_to @message.subject, @message_url %>

+<%= @message.author %> + +<%= textilizable @message.content %> diff --git a/rest_sys/app/views/mailer/message_posted.text.plain.rhtml b/rest_sys/app/views/mailer/message_posted.text.plain.rhtml new file mode 100644 index 000000000..ef6a3b3ae --- /dev/null +++ b/rest_sys/app/views/mailer/message_posted.text.plain.rhtml @@ -0,0 +1,4 @@ +<%= @message_url %> +<%= @message.author %> + +<%= @message.content %> diff --git a/rest_sys/app/views/mailer/news_added.text.html.rhtml b/rest_sys/app/views/mailer/news_added.text.html.rhtml new file mode 100644 index 000000000..010ef8ee1 --- /dev/null +++ b/rest_sys/app/views/mailer/news_added.text.html.rhtml @@ -0,0 +1,4 @@ +

<%= link_to @news.title, @news_url %>

+<%= @news.author.name %> + +<%= textilizable(@news.description) %> diff --git a/rest_sys/app/views/mailer/news_added.text.plain.rhtml b/rest_sys/app/views/mailer/news_added.text.plain.rhtml new file mode 100644 index 000000000..c8ae3035f --- /dev/null +++ b/rest_sys/app/views/mailer/news_added.text.plain.rhtml @@ -0,0 +1,5 @@ +<%= @news.title %> +<%= @news_url %> +<%= @news.author.name %> + +<%= @news.description %> diff --git a/rest_sys/app/views/mailer/register.text.html.rhtml b/rest_sys/app/views/mailer/register.text.html.rhtml new file mode 100644 index 000000000..145c3d7c9 --- /dev/null +++ b/rest_sys/app/views/mailer/register.text.html.rhtml @@ -0,0 +1,2 @@ +

<%= l(:mail_body_register) %>
+<%= auto_link(@url) %>

diff --git a/rest_sys/app/views/mailer/register.text.plain.rhtml b/rest_sys/app/views/mailer/register.text.plain.rhtml new file mode 100644 index 000000000..102a15ee3 --- /dev/null +++ b/rest_sys/app/views/mailer/register.text.plain.rhtml @@ -0,0 +1,2 @@ +<%= l(:mail_body_register) %> +<%= @url %> diff --git a/rest_sys/app/views/mailer/test.text.html.rhtml b/rest_sys/app/views/mailer/test.text.html.rhtml new file mode 100644 index 000000000..25ad20c51 --- /dev/null +++ b/rest_sys/app/views/mailer/test.text.html.rhtml @@ -0,0 +1,2 @@ +

This is a test email sent by Redmine.
+Redmine URL: <%= auto_link(@url) %>

diff --git a/rest_sys/app/views/mailer/test.text.plain.rhtml b/rest_sys/app/views/mailer/test.text.plain.rhtml new file mode 100644 index 000000000..790d6ab22 --- /dev/null +++ b/rest_sys/app/views/mailer/test.text.plain.rhtml @@ -0,0 +1,2 @@ +This is a test email sent by Redmine. +Redmine URL: <%= @url %> diff --git a/rest_sys/app/views/messages/_form.rhtml b/rest_sys/app/views/messages/_form.rhtml new file mode 100644 index 000000000..c2f7fb569 --- /dev/null +++ b/rest_sys/app/views/messages/_form.rhtml @@ -0,0 +1,21 @@ +<%= error_messages_for 'message' %> + +
+ +


+<%= f.text_field :subject, :required => true, :size => 120 %> + +<% if User.current.allowed_to?(:edit_messages, @project) %> + + +<% end %> +

+ +

<%= f.text_area :content, :required => true, :cols => 80, :rows => 15, :class => 'wiki-edit', :id => 'message_content' %>

+<%= wikitoolbar_for 'message_content' %> + + + +<%= render :partial => 'attachments/form' %> + +
diff --git a/rest_sys/app/views/messages/edit.rhtml b/rest_sys/app/views/messages/edit.rhtml new file mode 100644 index 000000000..808b6ea27 --- /dev/null +++ b/rest_sys/app/views/messages/edit.rhtml @@ -0,0 +1,6 @@ +

<%= link_to h(@board.name), :controller => 'boards', :action => 'show', :project_id => @project, :id => @board %> » <%=h @message.subject %>

+ +<% form_for :message, @message, :url => {:action => 'edit'}, :html => {:multipart => true} do |f| %> + <%= render :partial => 'form', :locals => {:f => f} %> + <%= submit_tag l(:button_save) %> +<% end %> diff --git a/rest_sys/app/views/messages/new.rhtml b/rest_sys/app/views/messages/new.rhtml new file mode 100644 index 000000000..5c688f465 --- /dev/null +++ b/rest_sys/app/views/messages/new.rhtml @@ -0,0 +1,6 @@ +

<%= link_to h(@board.name), :controller => 'boards', :action => 'show', :project_id => @project, :id => @board %> » <%= l(:label_message_new) %>

+ +<% form_for :message, @message, :url => {:action => 'new'}, :html => {:multipart => true} do |f| %> + <%= render :partial => 'form', :locals => {:f => f} %> + <%= submit_tag l(:button_create) %> +<% end %> diff --git a/rest_sys/app/views/messages/show.rhtml b/rest_sys/app/views/messages/show.rhtml new file mode 100644 index 000000000..bb7e2b7f3 --- /dev/null +++ b/rest_sys/app/views/messages/show.rhtml @@ -0,0 +1,39 @@ +
+ <%= link_to_if_authorized l(:button_edit), {:action => 'edit', :id => @topic}, :class => 'icon icon-edit' %> + <%= link_to_if_authorized l(:button_delete), {:action => 'destroy', :id => @topic}, :method => :post, :confirm => l(:text_are_you_sure), :class => 'icon icon-del' %> +
+ +

<%= link_to h(@board.name), :controller => 'boards', :action => 'show', :project_id => @project, :id => @board %> » <%=h @topic.subject %>

+ +
+

<%= authoring @topic.created_on, @topic.author %>

+
+<%= textilizable(@topic.content, :attachments => @topic.attachments) %> +
+<%= link_to_attachments @topic.attachments, :no_author => true %> +
+
+ +

<%= l(:label_reply_plural) %>

+<% @topic.children.each do |message| %> + "> +
+ <%= link_to_if_authorized l(:button_edit), {:action => 'edit', :id => message}, :class => 'icon icon-edit' %> + <%= link_to_if_authorized l(:button_delete), {:action => 'destroy', :id => message}, :method => :post, :confirm => l(:text_are_you_sure), :class => 'icon icon-del' %> +
+
+

<%=h message.subject %> - <%= authoring message.created_on, message.author %>

+
<%= textilizable message.content %>
+ <%= link_to_attachments message.attachments, :no_author => true %> +
+<% end %> + +<% if !@topic.locked? && authorize_for('messages', 'reply') %> +

<%= toggle_link l(:button_reply), "reply", :focus => 'message_content' %>

+ +<% end %> diff --git a/rest_sys/app/views/my/_block.rhtml b/rest_sys/app/views/my/_block.rhtml new file mode 100644 index 000000000..bb5dd1f9f --- /dev/null +++ b/rest_sys/app/views/my/_block.rhtml @@ -0,0 +1,14 @@ +
+ +
+ <%= link_to_remote "", { + :url => { :action => "remove_block", :block => block_name }, + :complete => "removeBlock('block_#{block_name}')" }, + :class => "close-icon" + %> +
+ +
+ <%= render :partial => "my/blocks/#{block_name}", :locals => { :user => user } %> +
+
\ No newline at end of file diff --git a/rest_sys/app/views/my/_sidebar.rhtml b/rest_sys/app/views/my/_sidebar.rhtml new file mode 100644 index 000000000..d30eacf90 --- /dev/null +++ b/rest_sys/app/views/my/_sidebar.rhtml @@ -0,0 +1,8 @@ +

<%=l(:label_my_account)%>

+ +

<%=l(:field_login)%>: <%= @user.login %>
+<%=l(:field_created_on)%>: <%= format_time(@user.created_on) %>

+<% if @user.rss_token %> +

<%= l(:label_feeds_access_key_created_on, distance_of_time_in_words(Time.now, @user.rss_token.created_on)) %> +(<%= link_to l(:button_reset), {:action => 'reset_rss_key'}, :method => :post %>)

+<% end %> diff --git a/rest_sys/app/views/my/account.rhtml b/rest_sys/app/views/my/account.rhtml new file mode 100644 index 000000000..fe2e5625b --- /dev/null +++ b/rest_sys/app/views/my/account.rhtml @@ -0,0 +1,48 @@ +
+<%= link_to(l(:button_change_password), :action => 'password') unless @user.auth_source_id %> +
+

<%=l(:label_my_account)%>

+<%= error_messages_for 'user' %> + +<% form_for :user, @user, :url => { :action => "account" }, + :builder => TabularFormBuilder, + :lang => current_language, + :html => { :id => 'my_account_form' } do |f| %> +
+

<%=l(:label_information_plural)%>

+
+

<%= f.text_field :firstname, :required => true %>

+

<%= f.text_field :lastname, :required => true %>

+

<%= f.text_field :mail, :required => true %>

+

<%= f.select :language, lang_options_for_select %>

+ +<% fields_for :pref, @user.pref, :builder => TabularFormBuilder, :lang => current_language do |pref_fields| %> +

<%= pref_fields.select :time_zone, TimeZone.all.collect {|z| [ z.to_s, z.name ]}, :include_blank => true %>

+

<%= pref_fields.check_box :hide_mail %>

+<% end %> +
+ +<%= submit_tag l(:button_save) %> +
+ +
+

<%=l(:field_mail_notification)%>

+
+<%= select_tag 'notification_option', options_for_select(@notification_options, @notification_option), + :onchange => 'if ($("notification_option").value == "selected") {Element.show("notified-projects")} else {Element.hide("notified-projects")}' %> +<% content_tag 'div', :id => 'notified-projects', :style => (@notification_option == 'selected' ? '' : 'display:none;') do %> +

<% User.current.projects.each do |project| %> +
+<% end %>

+

<%= l(:text_user_mail_option) %>

+<% end %> +

+
+
+<% end %> + +<% content_for :sidebar do %> +<%= render :partial => 'sidebar' %> +<% end %> + +<% set_html_title l(:label_my_account) -%> diff --git a/rest_sys/app/views/my/blocks/_calendar.rhtml b/rest_sys/app/views/my/blocks/_calendar.rhtml new file mode 100644 index 000000000..bad729363 --- /dev/null +++ b/rest_sys/app/views/my/blocks/_calendar.rhtml @@ -0,0 +1,8 @@ +

<%= l(:label_calendar) %>

+ +<% calendar = Redmine::Helpers::Calendar.new(Date.today, current_language, :week) + calendar.events = Issue.find :all, + :conditions => ["#{Issue.table_name}.project_id in (#{@user.projects.collect{|m| m.id}.join(',')}) AND ((start_date>=? and start_date<=?) or (due_date>=? and due_date<=?))", calendar.startdt, calendar.enddt, calendar.startdt, calendar.enddt], + :include => [:project, :tracker, :priority, :assigned_to] unless @user.projects.empty? %> + +<%= render :partial => 'common/calendar', :locals => {:calendar => calendar } %> diff --git a/rest_sys/app/views/my/blocks/_documents.rhtml b/rest_sys/app/views/my/blocks/_documents.rhtml new file mode 100644 index 000000000..a34be936f --- /dev/null +++ b/rest_sys/app/views/my/blocks/_documents.rhtml @@ -0,0 +1,8 @@ +

<%=l(:label_document_plural)%>

+ +<%= render(:partial => 'documents/document', + :collection => Document.find(:all, + :limit => 10, + :order => "#{Document.table_name}.created_on DESC", + :conditions => "#{Document.table_name}.project_id in (#{@user.projects.collect{|m| m.id}.join(',')})", + :include => [:project])) unless @user.projects.empty? %> \ No newline at end of file diff --git a/rest_sys/app/views/my/blocks/_issuesassignedtome.rhtml b/rest_sys/app/views/my/blocks/_issuesassignedtome.rhtml new file mode 100644 index 000000000..99812f6d0 --- /dev/null +++ b/rest_sys/app/views/my/blocks/_issuesassignedtome.rhtml @@ -0,0 +1,17 @@ +

<%=l(:label_assigned_to_me_issues)%>

+<% assigned_issues = Issue.find(:all, + :conditions => ["assigned_to_id=? AND #{IssueStatus.table_name}.is_closed=? AND #{Project.table_name}.status=#{Project::STATUS_ACTIVE}", user.id, false], + :limit => 10, + :include => [ :status, :project, :tracker, :priority ], + :order => "#{Enumeration.table_name}.position DESC, #{Issue.table_name}.updated_on DESC") %> +<%= render :partial => 'issues/list_simple', :locals => { :issues => assigned_issues } %> +<% if assigned_issues.length > 0 %> +

<%= link_to l(:label_issue_view_all), :controller => 'issues', :action => 'index', :set_filter => 1, :assigned_to_id => 'me' %>

+<% end %> + +<% content_for :header_tags do %> +<%= auto_discovery_link_tag(:atom, + {:controller => 'issues', :action => 'index', :set_filter => 1, + :assigned_to_id => 'me', :format => 'atom', :key => User.current.rss_key}, + {:title => l(:label_assigned_to_me_issues)}) %> +<% end %> diff --git a/rest_sys/app/views/my/blocks/_issuesreportedbyme.rhtml b/rest_sys/app/views/my/blocks/_issuesreportedbyme.rhtml new file mode 100644 index 000000000..317aebbfc --- /dev/null +++ b/rest_sys/app/views/my/blocks/_issuesreportedbyme.rhtml @@ -0,0 +1,17 @@ +

<%=l(:label_reported_issues)%>

+<% reported_issues = Issue.find(:all, + :conditions => ["author_id=? AND #{Project.table_name}.status=#{Project::STATUS_ACTIVE}", user.id], + :limit => 10, + :include => [ :status, :project, :tracker ], + :order => "#{Issue.table_name}.updated_on DESC") %> +<%= render :partial => 'issues/list_simple', :locals => { :issues => reported_issues } %> +<% if reported_issues.length > 0 %> +

<%= link_to l(:label_issue_view_all), :controller => 'issues', :action => 'index', :set_filter => 1, :author_id => 'me' %>

+<% end %> + +<% content_for :header_tags do %> +<%= auto_discovery_link_tag(:atom, + {:controller => 'issues', :action => 'index', :set_filter => 1, + :author_id => 'me', :format => 'atom', :key => User.current.rss_key}, + {:title => l(:label_reported_issues)}) %> +<% end %> diff --git a/rest_sys/app/views/my/blocks/_issueswatched.rhtml b/rest_sys/app/views/my/blocks/_issueswatched.rhtml new file mode 100644 index 000000000..e5c2f23ab --- /dev/null +++ b/rest_sys/app/views/my/blocks/_issueswatched.rhtml @@ -0,0 +1,10 @@ +

<%=l(:label_watched_issues)%>

+<% watched_issues = Issue.find(:all, + :include => [:status, :project, :tracker, :watchers], + :limit => 10, + :conditions => ["#{Watcher.table_name}.user_id = ? AND #{Project.table_name}.status=#{Project::STATUS_ACTIVE}", user.id], + :order => "#{Issue.table_name}.updated_on DESC") %> +<%= render :partial => 'issues/list_simple', :locals => { :issues => watched_issues } %> +<% if watched_issues.length > 0 %> +

<%=lwr(:label_last_updates, watched_issues.length)%>

+<% end %> diff --git a/rest_sys/app/views/my/blocks/_news.rhtml b/rest_sys/app/views/my/blocks/_news.rhtml new file mode 100644 index 000000000..d86cced92 --- /dev/null +++ b/rest_sys/app/views/my/blocks/_news.rhtml @@ -0,0 +1,8 @@ +

<%=l(:label_news_latest)%>

+ +<%= render(:partial => 'news/news', + :collection => News.find(:all, + :limit => 10, + :order => "#{News.table_name}.created_on DESC", + :conditions => "#{News.table_name}.project_id in (#{@user.projects.collect{|m| m.id}.join(',')})", + :include => [:project, :author])) unless @user.projects.empty? %> \ No newline at end of file diff --git a/rest_sys/app/views/my/page.rhtml b/rest_sys/app/views/my/page.rhtml new file mode 100644 index 000000000..db292da37 --- /dev/null +++ b/rest_sys/app/views/my/page.rhtml @@ -0,0 +1,42 @@ +
+ <%= link_to l(:label_personalize_page), :action => 'page_layout' %> +
+ +

<%=l(:label_my_page)%>

+ +
+ <% @blocks['top'].each do |b| + next unless MyController::BLOCKS.keys.include? b %> +
+ <%= render :partial => "my/blocks/#{b}", :locals => { :user => @user } %> +
+ <% end if @blocks['top'] %> +
+ +
+ <% @blocks['left'].each do |b| + next unless MyController::BLOCKS.keys.include? b %> +
+ <%= render :partial => "my/blocks/#{b}", :locals => { :user => @user } %> +
+ <% end if @blocks['left'] %> +
+ +
+ <% @blocks['right'].each do |b| + next unless MyController::BLOCKS.keys.include? b %> +
+ <%= render :partial => "my/blocks/#{b}", :locals => { :user => @user } %> +
+ <% end if @blocks['right'] %> +
+ +<% content_for :header_tags do %> + <%= javascript_include_tag 'context_menu' %> + <%= stylesheet_link_tag 'context_menu' %> +<% end %> + + +<%= javascript_tag 'new ContextMenu({})' %> + +<% set_html_title l(:label_my_page) -%> diff --git a/rest_sys/app/views/my/page_layout.rhtml b/rest_sys/app/views/my/page_layout.rhtml new file mode 100644 index 000000000..1e348bf5b --- /dev/null +++ b/rest_sys/app/views/my/page_layout.rhtml @@ -0,0 +1,107 @@ + + +
+<% form_tag({:action => "add_block"}, :id => "block-form") do %> +<%= select_tag 'block', "" + options_for_select(@block_options), :id => "block-select" %> +<%= link_to_remote l(:button_add), + {:url => { :action => "add_block" }, + :with => "Form.serialize('block-form')", + :update => "list-top", + :position => :top, + :complete => "afterAddBlock();" + }, :class => 'icon icon-add' + %> +<% end %> +<%= link_to l(:button_save), {:action => 'page_layout_save'}, :class => 'icon icon-save' %> +<%= link_to l(:button_cancel), {:action => 'page'}, :class => 'icon icon-cancel' %> +
+ +

<%=l(:label_my_page)%>

+ +
+ <% @blocks['top'].each do |b| + next unless MyController::BLOCKS.keys.include? b %> + <%= render :partial => 'block', :locals => {:user => @user, :block_name => b} %> + <% end if @blocks['top'] %> +
+ +
+ <% @blocks['left'].each do |b| + next unless MyController::BLOCKS.keys.include? b %> + <%= render :partial => 'block', :locals => {:user => @user, :block_name => b} %> + <% end if @blocks['left'] %> +
+ +
+ <% @blocks['right'].each do |b| + next unless MyController::BLOCKS.keys.include? b %> + <%= render :partial => 'block', :locals => {:user => @user, :block_name => b} %> + <% end if @blocks['right'] %> +
+ +<%= sortable_element 'list-top', + :tag => 'div', + :only => 'mypage-box', + :handle => "handle", + :dropOnEmpty => true, + :containment => ['list-top', 'list-left', 'list-right'], + :constraint => false, + :url => { :action => "order_blocks", :group => "top" } + %> + + +<%= sortable_element 'list-left', + :tag => 'div', + :only => 'mypage-box', + :handle => "handle", + :dropOnEmpty => true, + :containment => ['list-top', 'list-left', 'list-right'], + :constraint => false, + :url => { :action => "order_blocks", :group => "left" } + %> + +<%= sortable_element 'list-right', + :tag => 'div', + :only => 'mypage-box', + :handle => "handle", + :dropOnEmpty => true, + :containment => ['list-top', 'list-left', 'list-right'], + :constraint => false, + :url => { :action => "order_blocks", :group => "right" } + %> + +<%= javascript_tag "updateSelect()" %> diff --git a/rest_sys/app/views/my/password.rhtml b/rest_sys/app/views/my/password.rhtml new file mode 100644 index 000000000..2e9fd0fa4 --- /dev/null +++ b/rest_sys/app/views/my/password.rhtml @@ -0,0 +1,22 @@ +

<%=l(:button_change_password)%>

+ +<%= error_messages_for 'user' %> + +<% form_tag({}, :class => "tabular") do %> +
+

+<%= password_field_tag 'password', nil, :size => 25 %>

+ +

+<%= password_field_tag 'new_password', nil, :size => 25 %>
+<%= l(:text_caracters_minimum, 4) %>

+ +

+<%= password_field_tag 'new_password_confirmation', nil, :size => 25 %>

+
+<%= submit_tag l(:button_apply) %> +<% end %> + +<% content_for :sidebar do %> +<%= render :partial => 'sidebar' %> +<% end %> diff --git a/rest_sys/app/views/news/_form.rhtml b/rest_sys/app/views/news/_form.rhtml new file mode 100644 index 000000000..0cfe7a6d3 --- /dev/null +++ b/rest_sys/app/views/news/_form.rhtml @@ -0,0 +1,8 @@ +<%= error_messages_for 'news' %> +
+

<%= f.text_field :title, :required => true, :size => 60 %>

+

<%= f.text_area :summary, :cols => 60, :rows => 2 %>

+

<%= f.text_area :description, :required => true, :cols => 60, :rows => 15, :class => 'wiki-edit' %>

+
+ +<%= wikitoolbar_for 'news_description' %> diff --git a/rest_sys/app/views/news/_news.rhtml b/rest_sys/app/views/news/_news.rhtml new file mode 100644 index 000000000..e48b81ac3 --- /dev/null +++ b/rest_sys/app/views/news/_news.rhtml @@ -0,0 +1,6 @@ +

<%= link_to(h(news.project.name), :controller => 'projects', :action => 'show', :id => news.project) + ': ' unless @project %> +<%= link_to h(news.title), :controller => 'news', :action => 'show', :id => news %> +<%= "(#{news.comments_count} #{lwr(:label_comment, news.comments_count).downcase})" if news.comments_count > 0 %> +
+<% unless news.summary.blank? %><%=h news.summary %>
<% end %> +<%= authoring news.created_on, news.author %>

diff --git a/rest_sys/app/views/news/edit.rhtml b/rest_sys/app/views/news/edit.rhtml new file mode 100644 index 000000000..5e015c4c7 --- /dev/null +++ b/rest_sys/app/views/news/edit.rhtml @@ -0,0 +1,6 @@ +

<%=l(:label_news)%>

+ +<% labelled_tabular_form_for :news, @news, :url => { :action => "edit" } do |f| %> +<%= render :partial => 'form', :locals => { :f => f } %> +<%= submit_tag l(:button_save) %> +<% end %> \ No newline at end of file diff --git a/rest_sys/app/views/news/index.rhtml b/rest_sys/app/views/news/index.rhtml new file mode 100644 index 000000000..0b677d241 --- /dev/null +++ b/rest_sys/app/views/news/index.rhtml @@ -0,0 +1,36 @@ +
+<%= link_to_if_authorized(l(:label_news_new), + {:controller => 'projects', :action => 'add_news', :id => @project}, + :class => 'icon icon-add', + :onclick => 'Element.show("add-news"); return false;') if @project %> +
+ + + +

<%=l(:label_news_plural)%>

+ +<% if @newss.empty? %> +

<%= l(:label_no_data) %>

+<% else %> +<% @newss.each do |news| %> +

<%= link_to(h(news.project.name), :controller => 'projects', :action => 'show', :id => news.project) + ': ' unless news.project == @project %> + <%= link_to h(news.title), :controller => 'news', :action => 'show', :id => news %> + <%= "(#{news.comments_count} #{lwr(:label_comment, news.comments_count).downcase})" if news.comments_count > 0 %>

+

<%= authoring news.created_on, news.author %>

+ <%= textilizable(news.description) %> +<% end %> +<% end %> +<%= pagination_links_full @news_pages %> + +<% content_for :header_tags do %> + <%= auto_discovery_link_tag(:atom, params.merge({:format => 'atom', :page => nil, :key => User.current.rss_key})) %> +<% end %> + +<% set_html_title l(:label_news_plural) -%> diff --git a/rest_sys/app/views/news/show.rhtml b/rest_sys/app/views/news/show.rhtml new file mode 100644 index 000000000..2b71c48ad --- /dev/null +++ b/rest_sys/app/views/news/show.rhtml @@ -0,0 +1,46 @@ +
+<%= link_to_if_authorized l(:button_edit), + {:controller => 'news', :action => 'edit', :id => @news}, + :class => 'icon icon-edit', + :accesskey => accesskey(:edit), + :onclick => 'Element.show("edit-news"); return false;' %> +<%= link_to_if_authorized l(:button_delete), {:controller => 'news', :action => 'destroy', :id => @news}, :confirm => l(:text_are_you_sure), :method => :post, :class => 'icon icon-del' %> +
+ +

<%=h @news.title %>

+ + + +

<% unless @news.summary.empty? %><%=h @news.summary %>
<% end %> +<%= authoring @news.created_on, @news.author %>

+<%= textilizable(@news.description) %> +
+ +
+

<%= l(:label_comment_plural) %>

+<% @news.comments.each do |comment| %> + <% next if comment.new_record? %> +

<%= authoring comment.created_on, comment.author %>

+
+ <%= link_to_if_authorized l(:button_delete), {:controller => 'news', :action => 'destroy_comment', :id => @news, :comment_id => comment}, :confirm => l(:text_are_you_sure), :method => :post, :class => 'icon icon-del' %> +
+ <%= textilizable(comment.comments) %> +<% end if @news.comments_count > 0 %> +
+ +<% if authorize_for 'news', 'add_comment' %> +

<%= toggle_link l(:label_comment_add), "add_comment_form", :focus => "comment_comments" %>

+<% form_tag({:action => 'add_comment', :id => @news}, :id => "add_comment_form", :style => "display:none;") do %> +<%= text_area 'comment', 'comments', :cols => 80, :rows => 15, :class => 'wiki-edit' %> +<%= wikitoolbar_for 'comment_comments' %> +

<%= submit_tag l(:button_add) %>

+<% end %> +<% end %> + +<% set_html_title(h(@news.title)) -%> diff --git a/rest_sys/app/views/projects/_edit.rhtml b/rest_sys/app/views/projects/_edit.rhtml new file mode 100644 index 000000000..b7c2987d2 --- /dev/null +++ b/rest_sys/app/views/projects/_edit.rhtml @@ -0,0 +1,4 @@ +<% labelled_tabular_form_for :project, @project, :url => { :action => "edit", :id => @project } do |f| %> +<%= render :partial => 'form', :locals => { :f => f } %> +<%= submit_tag l(:button_save) %> +<% end %> diff --git a/rest_sys/app/views/projects/_form.rhtml b/rest_sys/app/views/projects/_form.rhtml new file mode 100644 index 000000000..e29777af4 --- /dev/null +++ b/rest_sys/app/views/projects/_form.rhtml @@ -0,0 +1,52 @@ +<%= error_messages_for 'project' %> + +
+ +

<%= f.text_field :name, :required => true %>
<%= l(:text_caracters_maximum, 30) %>

+ +<% if User.current.admin? and !@root_projects.empty? %> +

<%= f.select :parent_id, (@root_projects.collect {|p| [p.name, p.id]}), { :include_blank => true } %>

+<% end %> + +

<%= f.text_area :description, :required => true, :cols => 60, :rows => 5 %><%= l(:text_caracters_maximum, 255) %>

+

<%= f.text_field :identifier, :required => true, :disabled => @project.identifier_frozen? %>
<%= l(:text_length_between, 3, 20) %> <%= l(:text_project_identifier_info) unless @project.identifier_frozen? %>

+

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

+

<%= f.check_box :is_public %>

+<%= wikitoolbar_for 'project_description' %> + +<% for @custom_value in @custom_values %> +

<%= custom_field_tag_with_label @custom_value %>

+<% end %> +
+ +<% unless @trackers.empty? %> +
<%=l(:label_tracker_plural)%> +<% @trackers.each do |tracker| %> + +<% end %> +<%= hidden_field_tag 'project[tracker_ids][]', '' %> +
+<% end %> + +<% unless @custom_fields.empty? %> +
<%=l(:label_custom_field_plural)%> +<% for custom_field in @custom_fields %> + +<% end %> +
+<% end %> + + + +<% content_for :header_tags do %> +<%= javascript_include_tag 'calendar/calendar' %> +<%= javascript_include_tag "calendar/lang/calendar-#{current_language}.js" %> +<%= javascript_include_tag 'calendar/calendar-setup' %> +<%= stylesheet_link_tag 'calendar' %> +<% end %> diff --git a/rest_sys/app/views/projects/activity.rhtml b/rest_sys/app/views/projects/activity.rhtml new file mode 100644 index 000000000..d22443bc0 --- /dev/null +++ b/rest_sys/app/views/projects/activity.rhtml @@ -0,0 +1,43 @@ +

<%=l(:label_activity)%>: <%= "#{month_name(@month).downcase} #{@year}" %>

+ +<% @events_by_day.keys.sort {|x,y| y <=> x }.each do |day| %> +

<%= day_name(day.cwday) %> <%= day.day %>

+
    + <% @events_by_day[day].sort {|x,y| y.event_datetime <=> x.event_datetime }.each do |e| %> +
  • <%= format_time(e.event_datetime, false) %> <%= link_to truncate(e.event_title, 100), e.event_url %>
    + <% unless e.event_description.blank? %><%= truncate(e.event_description, 500) %>
    <% end %> + <%= e.event_author if e.respond_to?(:event_author) %>

  • + <% end %> +
+<% end %> + +<% if @events_by_day.empty? %>

<%= l(:label_no_data) %>

<% end %> + +
+<% prev_params = params.clone.update :year => (@month==1 ? @year-1 : @year), :month =>(@month==1 ? 12 : @month-1) %> +<%= link_to_remote ('« ' + (@month==1 ? "#{month_name(12)} #{@year-1}" : "#{month_name(@month-1)}")), + {:update => "content", :url => prev_params}, {:href => url_for(prev_params)} %> +
+
+<% next_params = params.clone.update :year => (@month==12 ? @year+1 : @year), :month =>(@month==12 ? 1 : @month+1) %> +<%= link_to_remote ((@month==12 ? "#{month_name(1)} #{@year+1}" : "#{month_name(@month+1)}") + ' »'), + {:update => "content", :url => next_params}, {:href => url_for(next_params)} %> +  +
+
+ +<% content_for :header_tags do %> +<%= auto_discovery_link_tag(:atom, params.merge({:format => 'atom', :year => nil, :month => nil, :key => User.current.rss_key})) %> +<% end %> + +<% content_for :sidebar do %> +<% form_tag do %> +

<%= l(:label_activity) %>

+

<% @event_types.each do |t| %> +
+<% end %>

+

<%= submit_tag l(:button_apply), :class => 'button-small' %>

+<% end %> +<% end %> + +<% set_html_title l(:label_activity) -%> diff --git a/rest_sys/app/views/projects/add.rhtml b/rest_sys/app/views/projects/add.rhtml new file mode 100644 index 000000000..e3ee0b591 --- /dev/null +++ b/rest_sys/app/views/projects/add.rhtml @@ -0,0 +1,15 @@ +

<%=l(:label_project_new)%>

+ +<% labelled_tabular_form_for :project, @project, :url => { :action => "add" } do |f| %> +<%= render :partial => 'form', :locals => { :f => f } %> + +
<%= l(:label_module_plural) %> +<% Redmine::AccessControl.available_project_modules.each do |m| %> + +<% end %> +
+ +<%= submit_tag l(:button_save) %> +<% end %> diff --git a/rest_sys/app/views/projects/add_document.rhtml b/rest_sys/app/views/projects/add_document.rhtml new file mode 100644 index 000000000..6c3fe2c7b --- /dev/null +++ b/rest_sys/app/views/projects/add_document.rhtml @@ -0,0 +1,13 @@ +

<%=l(:label_document_new)%>

+ +<% form_tag( { :action => 'add_document', :id => @project }, :class => "tabular", :multipart => true) do %> +<%= render :partial => 'documents/form' %> + +
+<%= render :partial => 'common/attachments_form'%> +
+ +<%= submit_tag l(:button_create) %> +<% end %> + + diff --git a/rest_sys/app/views/projects/add_file.rhtml b/rest_sys/app/views/projects/add_file.rhtml new file mode 100644 index 000000000..839275373 --- /dev/null +++ b/rest_sys/app/views/projects/add_file.rhtml @@ -0,0 +1,13 @@ +

<%=l(:label_attachment_new)%>

+ +<%= error_messages_for 'attachment' %> +
+<% form_tag({ :action => 'add_file', :id => @project }, :multipart => true, :class => "tabular") do %> + +

+<%= select_tag "version_id", options_from_collection_for_select(@versions, "id", "name") %>

+ +<%= render :partial => 'attachments/form' %> +
+<%= submit_tag l(:button_add) %> +<% end %> \ No newline at end of file diff --git a/rest_sys/app/views/projects/add_issue.rhtml b/rest_sys/app/views/projects/add_issue.rhtml new file mode 100644 index 000000000..a68922906 --- /dev/null +++ b/rest_sys/app/views/projects/add_issue.rhtml @@ -0,0 +1,19 @@ +

<%=l(:label_issue_new)%>: <%= @issue.tracker %>

+ +<% labelled_tabular_form_for :issue, @issue, + :url => {:action => 'add_issue'}, + :html => {:multipart => true, :id => 'issue-form'} do |f| %> + <%= f.hidden_field :tracker_id %> + <%= render :partial => 'issues/form', :locals => {:f => f} %> + <%= submit_tag l(:button_create) %> + <%= link_to_remote l(:label_preview), + { :url => { :controller => 'issues', :action => 'preview', :id => @issue }, + :method => 'post', + :update => 'preview', + :with => "Form.serialize('issue-form')", + :complete => "location.href='#preview-top'" + }, :accesskey => accesskey(:preview) %> +<% end %> + + +
diff --git a/rest_sys/app/views/projects/add_issue_category.rhtml b/rest_sys/app/views/projects/add_issue_category.rhtml new file mode 100644 index 000000000..08bc6d0e3 --- /dev/null +++ b/rest_sys/app/views/projects/add_issue_category.rhtml @@ -0,0 +1,6 @@ +

<%=l(:label_issue_category_new)%>

+ +<% labelled_tabular_form_for :category, @category, :url => { :action => 'add_issue_category' } do |f| %> +<%= render :partial => 'issue_categories/form', :locals => { :f => f } %> +<%= submit_tag l(:button_create) %> +<% end %> diff --git a/rest_sys/app/views/projects/add_news.rhtml b/rest_sys/app/views/projects/add_news.rhtml new file mode 100644 index 000000000..a6ecd3da7 --- /dev/null +++ b/rest_sys/app/views/projects/add_news.rhtml @@ -0,0 +1,6 @@ +

<%=l(:label_news_new)%>

+ +<% labelled_tabular_form_for :news, @news, :url => { :action => "add_news" } do |f| %> +<%= render :partial => 'news/form', :locals => { :f => f } %> +<%= submit_tag l(:button_create) %> +<% end %> \ No newline at end of file diff --git a/rest_sys/app/views/projects/add_version.rhtml b/rest_sys/app/views/projects/add_version.rhtml new file mode 100644 index 000000000..c038b7de9 --- /dev/null +++ b/rest_sys/app/views/projects/add_version.rhtml @@ -0,0 +1,6 @@ +

<%=l(:label_version_new)%>

+ +<% labelled_tabular_form_for :version, @version, :url => { :action => 'add_version' } do |f| %> +<%= render :partial => 'versions/form', :locals => { :f => f } %> +<%= submit_tag l(:button_create) %> +<% end %> \ No newline at end of file diff --git a/rest_sys/app/views/projects/calendar.rhtml b/rest_sys/app/views/projects/calendar.rhtml new file mode 100644 index 000000000..39f171626 --- /dev/null +++ b/rest_sys/app/views/projects/calendar.rhtml @@ -0,0 +1,42 @@ +<% cache(:year => @year, :month => @month, :tracker_ids => @selected_tracker_ids, :subprojects => params[:with_subprojects], :lang => current_language) do %> +

<%= l(:label_calendar) %>: <%= "#{month_name(@month).downcase} #{@year}" %>

+ + + +
+ <%= link_to_remote ('« ' + (@month==1 ? "#{month_name(12)} #{@year-1}" : "#{month_name(@month-1)}")), + {:update => "content", :url => { :year => (@month==1 ? @year-1 : @year), :month =>(@month==1 ? 12 : @month-1), :tracker_ids => @selected_tracker_ids, :with_subprojects => params[:with_subprojects] }}, + {:href => url_for(:action => 'calendar', :year => (@month==1 ? @year-1 : @year), :month =>(@month==1 ? 12 : @month-1), :tracker_ids => @selected_tracker_ids, :with_subprojects => params[:with_subprojects])} + %> + + <%= link_to_remote ((@month==12 ? "#{month_name(1)} #{@year+1}" : "#{month_name(@month+1)}") + ' »'), + {:update => "content", :url => { :year => (@month==12 ? @year+1 : @year), :month =>(@month==12 ? 1 : @month+1), :tracker_ids => @selected_tracker_ids, :with_subprojects => params[:with_subprojects] }}, + {:href => url_for(:action => 'calendar', :year => (@month==12 ? @year+1 : @year), :month =>(@month==12 ? 1 : @month+1), :tracker_ids => @selected_tracker_ids, :with_subprojects => params[:with_subprojects])} + %> +
+ +<%= render :partial => 'common/calendar', :locals => {:calendar => @calendar} %> + +<%= image_tag 'arrow_from.png' %>  <%= l(:text_tip_task_begin_day) %>
+<%= image_tag 'arrow_to.png' %>  <%= l(:text_tip_task_end_day) %>
+<%= image_tag 'arrow_bw.png' %>  <%= l(:text_tip_task_begin_end_day) %>
+<% end %> + +<% content_for :sidebar do %> +

<%= l(:label_calendar) %>

+ + <% form_tag() do %> +

<%= select_month(@month, :prefix => "month", :discard_type => true) %> + <%= select_year(@year, :prefix => "year", :discard_type => true) %>

+ + <% @trackers.each do |tracker| %> +
+ <% end %> + <% if @project.active_children.any? %> +
+ <% end %> +

<%= submit_tag l(:button_apply), :class => 'button-small' %>

+ <% end %> +<% end %> + +<% set_html_title l(:label_calendar) -%> diff --git a/rest_sys/app/views/projects/changelog.rhtml b/rest_sys/app/views/projects/changelog.rhtml new file mode 100644 index 000000000..e4d32a393 --- /dev/null +++ b/rest_sys/app/views/projects/changelog.rhtml @@ -0,0 +1,42 @@ +

<%=l(:label_change_log)%>

+ +<% if @versions.empty? %> +

<%= l(:label_no_data) %>

+<% end %> + +<% @versions.each do |version| %> +

<%= version.name %>

+ <% if version.effective_date %> +

<%= format_date(version.effective_date) %>

+ <% end %> +

<%=h version.description %>

+ <% issues = version.fixed_issues.find(:all, + :include => [:status, :tracker], + :conditions => ["#{IssueStatus.table_name}.is_closed=? AND #{Issue.table_name}.tracker_id in (#{@selected_tracker_ids.join(',')})", true], + :order => "#{Tracker.table_name}.position") unless @selected_tracker_ids.empty? + issues ||= [] + %> + <% if !issues.empty? %> +
    + <% issues.each do |issue| %> +
  • <%= link_to_issue(issue) %>: <%=h issue.subject %>
  • + <% end %> +
+ <% end %> +<% end %> + +<% content_for :sidebar do %> +<% form_tag do %> +

<%= l(:label_change_log) %>

+<% @trackers.each do |tracker| %> +
+<% end %> +

<%= submit_tag l(:button_apply), :class => 'button-small' %>

+<% end %> + +

<%= l(:label_version_plural) %>

+<% @versions.each do |version| %> +<%= link_to version.name, :anchor => version.name %>
+<% end %> +<% end %> diff --git a/rest_sys/app/views/projects/destroy.rhtml b/rest_sys/app/views/projects/destroy.rhtml new file mode 100644 index 000000000..4531cb845 --- /dev/null +++ b/rest_sys/app/views/projects/destroy.rhtml @@ -0,0 +1,14 @@ +

<%=l(:label_confirmation)%>

+
+
+

<%=h @project_to_destroy.name %>
+<%=l(:text_project_destroy_confirmation)%>

+ +

+ <% form_tag({:controller => 'projects', :action => 'destroy', :id => @project_to_destroy}) do %> + <%= hidden_field_tag "confirm", 1 %> + <%= submit_tag l(:button_delete) %> + <% end %> +

+
+
\ No newline at end of file diff --git a/rest_sys/app/views/projects/gantt.rfpdf b/rest_sys/app/views/projects/gantt.rfpdf new file mode 100644 index 000000000..a293906ba --- /dev/null +++ b/rest_sys/app/views/projects/gantt.rfpdf @@ -0,0 +1,188 @@ +<% +pdf=IfpdfHelper::IFPDF.new(current_language) +pdf.SetTitle("#{@project.name} - #{l(:label_gantt)}") +pdf.AliasNbPages +pdf.footer_date = format_date(Date.today) +pdf.AddPage("L") +pdf.SetFontStyle('B',12) +pdf.SetX(15) +pdf.Cell(70, 20, @project.name) +pdf.Ln +pdf.SetFontStyle('B',9) + +subject_width = 70 +header_heigth = 5 + +headers_heigth = header_heigth +show_weeks = false +show_days = false + +if @months < 7 + show_weeks = true + headers_heigth = 2*header_heigth + if @months < 3 + show_days = true + headers_heigth = 3*header_heigth + end +end + +g_width = 210 +zoom = (g_width) / (@date_to - @date_from + 1) +g_height = 120 +t_height = g_height + headers_heigth + +y_start = pdf.GetY + + +# +# Months headers +# +month_f = @date_from +left = subject_width +height = header_heigth +@months.times do + width = ((month_f >> 1) - month_f) * zoom + pdf.SetY(y_start) + pdf.SetX(left) + pdf.Cell(width, height, "#{month_f.year}-#{month_f.month}", "LTR", 0, "C") + left = left + width + month_f = month_f >> 1 +end + +# +# Weeks headers +# +if show_weeks + left = subject_width + height = header_heigth + if @date_from.cwday == 1 + # @date_from is monday + week_f = @date_from + else + # find next monday after @date_from + week_f = @date_from + (7 - @date_from.cwday + 1) + width = (7 - @date_from.cwday + 1) * zoom-1 + pdf.SetY(y_start + header_heigth) + pdf.SetX(left) + pdf.Cell(width + 1, height, "", "LTR") + left = left + width+1 + end + while week_f <= @date_to + width = (week_f + 6 <= @date_to) ? 7 * zoom : (@date_to - week_f + 1) * zoom + pdf.SetY(y_start + header_heigth) + pdf.SetX(left) + pdf.Cell(width, height, (width >= 5 ? week_f.cweek.to_s : ""), "LTR", 0, "C") + left = left + width + week_f = week_f+7 + end +end + +# +# Days headers +# +if show_days + left = subject_width + height = header_heigth + wday = @date_from.cwday + pdf.SetFontStyle('B',7) + (@date_to - @date_from + 1).to_i.times do + width = zoom + pdf.SetY(y_start + 2 * header_heigth) + pdf.SetX(left) + pdf.Cell(width, height, day_name(wday).first, "LTR", 0, "C") + left = left + width + wday = wday + 1 + wday = 1 if wday > 7 + end +end + +pdf.SetY(y_start) +pdf.SetX(15) +pdf.Cell(subject_width+g_width-15, headers_heigth, "", 1) + + +# +# Tasks +# +top = headers_heigth + y_start +pdf.SetFontStyle('B',7) +@events.each do |i| + pdf.SetY(top) + pdf.SetX(15) + + if i.is_a? Issue + pdf.Cell(subject_width-15, 5, "#{i.tracker.name} #{i.id}: #{i.subject}".sub(/^(.{30}[^\s]*\s).*$/, '\1 (...)'), "LR") + else + pdf.Cell(subject_width-15, 5, "#{l(:label_version)}: #{i.name}", "LR") + end + + pdf.SetY(top) + pdf.SetX(subject_width) + pdf.Cell(g_width, 5, "", "LR") + + pdf.SetY(top+1.5) + + if i.is_a? Issue + i_start_date = (i.start_date >= @date_from ? i.start_date : @date_from ) + i_end_date = (i.due_date <= @date_to ? i.due_date : @date_to ) + + i_done_date = i.start_date + ((i.due_date - i.start_date+1)*i.done_ratio/100).floor + i_done_date = (i_done_date <= @date_from ? @date_from : i_done_date ) + i_done_date = (i_done_date >= @date_to ? @date_to : i_done_date ) + + i_late_date = [i_end_date, Date.today].min if i_start_date < Date.today + + i_left = ((i_start_date - @date_from)*zoom) + i_width = ((i_end_date - i_start_date + 1)*zoom) + d_width = ((i_done_date - i_start_date)*zoom) + l_width = ((i_late_date - i_start_date+1)*zoom) if i_late_date + l_width ||= 0 + + pdf.SetX(subject_width + i_left) + pdf.SetFillColor(200,200,200) + pdf.Cell(i_width, 2, "", 0, 0, "", 1) + + if l_width > 0 + pdf.SetY(top+1.5) + pdf.SetX(subject_width + i_left) + pdf.SetFillColor(255,100,100) + pdf.Cell(l_width, 2, "", 0, 0, "", 1) + end + if d_width > 0 + pdf.SetY(top+1.5) + pdf.SetX(subject_width + i_left) + pdf.SetFillColor(100,100,255) + pdf.Cell(d_width, 2, "", 0, 0, "", 1) + end + + pdf.SetY(top+1.5) + pdf.SetX(subject_width + i_left + i_width) + pdf.Cell(30, 2, "#{i.status.name} #{i.done_ratio}%") + else + i_left = ((i.start_date - @date_from)*zoom) + + pdf.SetX(subject_width + i_left) + pdf.SetFillColor(50,200,50) + pdf.Cell(2, 2, "", 0, 0, "", 1) + + pdf.SetY(top+1.5) + pdf.SetX(subject_width + i_left + 3) + pdf.Cell(30, 2, "#{i.name}") + end + + + top = top + 5 + pdf.SetDrawColor(200, 200, 200) + pdf.Line(15, top, subject_width+g_width, top) + if pdf.GetY() > 180 + pdf.AddPage("L") + top = 20 + pdf.Line(15, top, subject_width+g_width, top) + end + pdf.SetDrawColor(0, 0, 0) +end + +pdf.Line(15, top, subject_width+g_width, top) + +%> +<%= pdf.Output %> \ No newline at end of file diff --git a/rest_sys/app/views/projects/gantt.rhtml b/rest_sys/app/views/projects/gantt.rhtml new file mode 100644 index 000000000..395514b61 --- /dev/null +++ b/rest_sys/app/views/projects/gantt.rhtml @@ -0,0 +1,249 @@ +<% zoom = 1 +@zoom.times { zoom = zoom * 2 } + +subject_width = 330 +header_heigth = 18 + +headers_height = header_heigth +show_weeks = false +show_days = false + +if @zoom >1 + show_weeks = true + headers_height = 2*header_heigth + if @zoom > 2 + show_days = true + headers_height = 3*header_heigth + end +end + +g_width = (@date_to - @date_from + 1)*zoom +g_height = [(20 * @events.length + 6)+150, 206].max +t_height = g_height + headers_height +%> + +
+
+ +

<%= l(:label_gantt) %>

+ +<% form_tag(params.merge(:month => nil, :year => nil, :months => nil)) do %> + + + + + + +
+ + <%= l(:label_months_from) %> + <%= select_month(@month_from, :prefix => "month", :discard_type => true) %> + <%= select_year(@year_from, :prefix => "year", :discard_type => true) %> + <%= hidden_field_tag 'zoom', @zoom %> + <%= submit_tag l(:button_submit), :class => "button-small" %> + +<%= if @zoom < 4 + link_to image_tag('zoom_in.png'), {:zoom => (@zoom+1), :year => @year_from, :month => @month_from, :months => @months, :tracker_ids => @selected_tracker_ids, :with_subprojects => params[:with_subprojects]} + else + image_tag 'zoom_in_g.png' + end %> +<%= if @zoom > 1 + link_to image_tag('zoom_out.png'),{:zoom => (@zoom-1), :year => @year_from, :month => @month_from, :months => @months, :tracker_ids => @selected_tracker_ids, :with_subprojects => params[:with_subprojects]} + else + image_tag 'zoom_out_g.png' + end %> +
+<% end %> + +<% cache(:year => @year_from, :month => @month_from, :months => @months, :zoom => @zoom, :tracker_ids => @selected_tracker_ids, :subprojects => params[:with_subprojects], :lang => current_language) do %> + + + + + + +
+ +
+
+
+<% +# +# Tasks subjects +# +top = headers_height + 8 +@events.each do |i| %> +
+ <% if i.is_a? Issue %> + <%= h("#{i.project.name} -") unless @project && @project == i.project %> + <%= link_to_issue i %>: <%=h i.subject %> + <% else %> + <%= link_to_version i, :class => "icon icon-package" %> + <% end %> +
+ <% top = top + 20 +end %> +
+
+ +
+
 
+<% +# +# Months headers +# +month_f = @date_from +left = 0 +height = (show_weeks ? header_heigth : header_heigth + g_height) +@months.times do + width = ((month_f >> 1) - month_f) * zoom - 1 + %> +
+ <%= link_to "#{month_f.year}-#{month_f.month}", { :year => month_f.year, :month => month_f.month, :zoom => @zoom, :months => @months, :tracker_ids => @selected_tracker_ids, :with_subprojects => params[:with_subprojects] }, :title => "#{month_name(month_f.month)} #{month_f.year}"%> +
+ <% + left = left + width + 1 + month_f = month_f >> 1 +end %> + +<% +# +# Weeks headers +# +if show_weeks + left = 0 + height = (show_days ? header_heigth-1 : header_heigth-1 + g_height) + if @date_from.cwday == 1 + # @date_from is monday + week_f = @date_from + else + # find next monday after @date_from + week_f = @date_from + (7 - @date_from.cwday + 1) + width = (7 - @date_from.cwday + 1) * zoom-1 + %> +
 
+ <% + left = left + width+1 + end %> + <% + while week_f <= @date_to + width = (week_f + 6 <= @date_to) ? 7 * zoom -1 : (@date_to - week_f + 1) * zoom-1 + %> +
+ <%= week_f.cweek if width >= 16 %> +
+ <% + left = left + width+1 + week_f = week_f+7 + end +end %> + +<% +# +# Days headers +# +if show_days + left = 0 + height = g_height + header_heigth - 1 + wday = @date_from.cwday + (@date_to - @date_from + 1).to_i.times do + width = zoom - 1 + %> +
5 %>" class="gantt_hdr"> + <%= day_name(wday).first %> +
+ <% + left = left + width+1 + wday = wday + 1 + wday = 1 if wday > 7 + end +end %> + +<% +# +# Tasks +# +top = headers_height + 10 +@events.each do |i| + if i.is_a? Issue + i_start_date = (i.start_date >= @date_from ? i.start_date : @date_from ) + i_end_date = (i.due_date <= @date_to ? i.due_date : @date_to ) + + i_done_date = i.start_date + ((i.due_date - i.start_date+1)*i.done_ratio/100).floor + i_done_date = (i_done_date <= @date_from ? @date_from : i_done_date ) + i_done_date = (i_done_date >= @date_to ? @date_to : i_done_date ) + + i_late_date = [i_end_date, Date.today].min if i_start_date < Date.today + + i_left = ((i_start_date - @date_from)*zoom).floor + i_width = ((i_end_date - i_start_date + 1)*zoom).floor - 2 # total width of the issue (- 2 for left and right borders) + d_width = ((i_done_date - i_start_date)*zoom).floor - 2 # done width + l_width = i_late_date ? ((i_late_date - i_start_date+1)*zoom).floor - 2 : 0 # delay width + %> +
 
+ <% if l_width > 0 %> +
 
+ <% end %> + <% if d_width > 0 %> +
 
+ <% end %> +
+ <%= i.status.name %> + <%= (i.done_ratio).to_i %>% +
+ <% # === tooltip === %> +
+ + <%= render_issue_tooltip i %> +
+<% else + i_left = ((i.start_date - @date_from)*zoom).floor + %> +
 
+
+ <%= i.name %> +
+<% end %> + <% top = top + 20 +end %> + +<% end # cache +%> + +<% +# +# Today red line (excluded from cache) +# +if Date.today >= @date_from and Date.today <= @date_to %> +
 
+<% end %> + +
+
+ + + + + + +
<%= link_to ('« ' + l(:label_previous)), :year => (@date_from << @months).year, :month => (@date_from << @months).month, :zoom => @zoom, :months => @months, :tracker_ids => @selected_tracker_ids, :with_subprojects => params[:with_subprojects] %><%= link_to (l(:label_next) + ' »'), :year => (@date_from >> @months).year, :month => (@date_from >> @months).month, :zoom => @zoom, :months => @months, :tracker_ids => @selected_tracker_ids, :with_subprojects => params[:with_subprojects] %>
+ +
<%= l(:label_export_to) %> +<%= link_to 'PDF', {:zoom => @zoom, :year => @year_from, :month => @month_from, :months => @months, :tracker_ids => @selected_tracker_ids, :with_subprojects => params[:with_subprojects], :format => 'pdf'}, :class => 'icon icon-pdf' %> +<%= link_to 'PNG', {:zoom => @zoom, :year => @year_from, :month => @month_from, :months => @months, :tracker_ids => @selected_tracker_ids, :with_subprojects => params[:with_subprojects], :format => 'png'}, :class => 'icon icon-image' if respond_to?('gantt_image') %> +
+ +<% content_for :sidebar do %> +

<%= l(:label_gantt) %>

+ <% form_tag(params.merge(:tracker_ids => nil, :with_subprojects => nil)) do %> + <% @trackers.each do |tracker| %> +
+ <% end %> + <% if @project.active_children.any? %> +
+ <% end %> +

<%= submit_tag l(:button_apply), :class => 'button-small' %>

+ <% end %> +<% end %> + +<% set_html_title l(:label_gantt) -%> diff --git a/rest_sys/app/views/projects/list.rhtml b/rest_sys/app/views/projects/list.rhtml new file mode 100644 index 000000000..c7630c0a2 --- /dev/null +++ b/rest_sys/app/views/projects/list.rhtml @@ -0,0 +1,20 @@ +

<%=l(:label_project_plural)%>

+ +<% @project_tree.keys.sort.each do |project| %> +

<%= link_to h(project.name), {:action => 'show', :id => project}, :class => (User.current.member_of?(project) ? "icon icon-fav" : "") %>

+<%= textilizable(project.description, :project => project) %> + +<% if @project_tree[project].any? %> +

<%= l(:label_subproject_plural) %>: + <%= @project_tree[project].sort.collect {|subproject| + link_to(h(subproject.name), {:action => 'show', :id => subproject}, :class => (User.current.member_of?(subproject) ? "icon icon-fav" : ""))}.join(', ') %>

+<% end %> +<% end %> + +<% if User.current.logged? %> +
+<%= l(:label_my_projects) %> +
+<% end %> + +<% set_html_title l(:label_project_plural) -%> diff --git a/rest_sys/app/views/projects/list_documents.rhtml b/rest_sys/app/views/projects/list_documents.rhtml new file mode 100644 index 000000000..6829b9bf5 --- /dev/null +++ b/rest_sys/app/views/projects/list_documents.rhtml @@ -0,0 +1,39 @@ +
+<%= link_to_if_authorized l(:label_document_new), + {:controller => 'projects', :action => 'add_document', :id => @project}, + :class => 'icon icon-add', + :onclick => 'Element.show("add-document"); return false;' %> +
+ + + +

<%=l(:label_document_plural)%>

+ +<% if @grouped.empty? %>

<%= l(:label_no_data) %>

<% end %> + +<% @grouped.keys.sort.each do |group| %> +

<%= group %>

+ <%= render :partial => 'documents/document', :collection => @grouped[group] %> +<% end %> + +<% content_for :sidebar do %> +

<%= l(:label_sort_by, '') %>

+ <% form_tag({}, :method => :get) do %> +
+
+
+ + <% end %> +<% end %> + +<% set_html_title l(:label_document_plural) -%> diff --git a/rest_sys/app/views/projects/list_files.rhtml b/rest_sys/app/views/projects/list_files.rhtml new file mode 100644 index 000000000..b9f688509 --- /dev/null +++ b/rest_sys/app/views/projects/list_files.rhtml @@ -0,0 +1,46 @@ +
+<%= link_to_if_authorized l(:label_attachment_new), {:controller => 'projects', :action => 'add_file', :id => @project}, :class => 'icon icon-add' %> +
+ +

<%=l(:label_attachment_plural)%>

+ +<% delete_allowed = authorize_for('versions', 'destroy_file') %> + + + + + + + + + + <% if delete_allowed %><% end %> + + +<% for version in @versions %> + <% unless version.attachments.empty? %> + + <% for file in version.attachments %> + "> + + + + + + + <% if delete_allowed %> + + <% end %> + + <% end + reset_cycle %> + <% end %> +<% end %> + +
<%=l(:field_version)%><%=l(:field_filename)%><%=l(:label_date)%><%=l(:field_filesize)%>D/LMD5
<%= version.name %>
<%= link_to file.filename, :controller => 'versions', :action => 'download', :id => version, :attachment_id => file %><%= format_date(file.created_on) %><%= number_to_human_size(file.filesize) %><%= file.downloads %><%= file.digest %> +
+ <%= link_to_if_authorized image_tag('delete.png'), {:controller => 'versions', :action => 'destroy_file', :id => version, :attachment_id => file}, :confirm => l(:text_are_you_sure), :method => :post %> +
+
+ +<% set_html_title l(:label_attachment_plural) -%> diff --git a/rest_sys/app/views/projects/list_members.rhtml b/rest_sys/app/views/projects/list_members.rhtml new file mode 100644 index 000000000..fcfb4f7c0 --- /dev/null +++ b/rest_sys/app/views/projects/list_members.rhtml @@ -0,0 +1,13 @@ +

<%=l(:label_member_plural)%>

+ +<% if @members.empty? %>

<%= l(:label_no_data) %>

<% end %> + +<% members = @members.group_by {|m| m.role } %> +<% members.keys.sort{|x,y| x.position <=> y.position}.each do |role| %> +

<%= role.name %>

+
    +<% members[role].each do |m| %> +
  • <%= link_to m.name, :controller => 'account', :action => 'show', :id => m.user %> (<%= format_date m.created_on %>)
  • +<% end %> +
+<% end %> diff --git a/rest_sys/app/views/projects/move_issues.rhtml b/rest_sys/app/views/projects/move_issues.rhtml new file mode 100644 index 000000000..95eaf9dec --- /dev/null +++ b/rest_sys/app/views/projects/move_issues.rhtml @@ -0,0 +1,29 @@ +

<%=l(:button_move)%>

+ + +<% form_tag({:action => 'move_issues', :id => @project}, :class => 'tabular', :id => 'move_form') do %> + +
+

+<% for issue in @issues %> + <%= link_to_issue issue %>: <%=h issue.subject %> + <%= hidden_field_tag "issue_ids[]", issue.id %>
+<% end %> +(<%= @issues.length%> <%= lwr(:label_issue, @issues.length)%>)

+ +  + + +

+<%= select_tag "new_project_id", + options_from_collection_for_select(@projects, 'id', 'name', @target_project.id), + :onchange => remote_function(:url => {:action => 'move_issues' , :id => @project}, + :method => :get, + :update => 'content', + :with => "Form.serialize('move_form')") %>

+ +

+<%= select_tag "new_tracker_id", "" + options_from_collection_for_select(@trackers, "id", "name") %>

+
+<%= submit_tag l(:button_move) %> +<% end %> diff --git a/rest_sys/app/views/projects/roadmap.rhtml b/rest_sys/app/views/projects/roadmap.rhtml new file mode 100644 index 000000000..daf7639fc --- /dev/null +++ b/rest_sys/app/views/projects/roadmap.rhtml @@ -0,0 +1,50 @@ +

<%=l(:label_roadmap)%>

+ +<% if @versions.empty? %> +

<%= l(:label_no_data) %>

+<% end %> + +<% @versions.each do |version| %> + <%= tag 'a', :name => version.name %> +

<%= link_to h(version.name), :controller => 'versions', :action => 'show', :id => version %>

+ <%= render :partial => 'versions/overview', :locals => {:version => version} %> + <%= render(:partial => "wiki/content", :locals => {:content => version.wiki_page.content}) if version.wiki_page %> + + <% issues = version.fixed_issues.find(:all, + :include => [:status, :tracker], + :conditions => ["tracker_id in (#{@selected_tracker_ids.join(',')})"], + :order => "#{Tracker.table_name}.position") unless @selected_tracker_ids.empty? + issues ||= [] + %> +
    + <% if issues.size > 0 %> + <% issues.each do |issue| %> +
  • + <%= link = link_to_issue(issue) + issue.status.is_closed? ? content_tag("del", link) : link %>: <%=h issue.subject %> + <%= content_tag "em", "(#{l(:label_closed_issues)})" if issue.status.is_closed? %> +
  • + <% end %> + <% end %> +
+<% end %> + +<% content_for :sidebar do %> +<% form_tag do %> +

<%= l(:label_roadmap) %>

+<% @trackers.each do |tracker| %> +
+<% end %> +
+