summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--redmine/Rakefile10
-rw-r--r--redmine/app/controllers/account_controller.rb83
-rw-r--r--redmine/app/controllers/admin_controller.rb49
-rw-r--r--redmine/app/controllers/application.rb86
-rw-r--r--redmine/app/controllers/custom_fields_controller.rb58
-rw-r--r--redmine/app/controllers/documents_controller.rb65
-rw-r--r--redmine/app/controllers/enumerations_controller.rb69
-rw-r--r--redmine/app/controllers/help_controller.rb43
-rw-r--r--redmine/app/controllers/issue_categories_controller.rb42
-rw-r--r--redmine/app/controllers/issue_statuses_controller.rb68
-rw-r--r--redmine/app/controllers/issues_controller.rb102
-rw-r--r--redmine/app/controllers/members_controller.rb41
-rw-r--r--redmine/app/controllers/news_controller.rb42
-rw-r--r--redmine/app/controllers/projects_controller.rb260
-rw-r--r--redmine/app/controllers/reports_controller.rb71
-rw-r--r--redmine/app/controllers/roles_controller.rb84
-rw-r--r--redmine/app/controllers/trackers_controller.rb60
-rw-r--r--redmine/app/controllers/users_controller.rb73
-rw-r--r--redmine/app/controllers/versions_controller.rb53
-rw-r--r--redmine/app/controllers/welcome_controller.rb26
-rw-r--r--redmine/app/helpers/account_helper.rb19
-rw-r--r--redmine/app/helpers/admin_helper.rb19
-rw-r--r--redmine/app/helpers/application_helper.rb65
-rw-r--r--redmine/app/helpers/custom_fields_helper.rb36
-rw-r--r--redmine/app/helpers/documents_helper.rb19
-rw-r--r--redmine/app/helpers/enumerations_helper.rb19
-rw-r--r--redmine/app/helpers/help_helper.rb19
-rw-r--r--redmine/app/helpers/issue_categories_helper.rb19
-rw-r--r--redmine/app/helpers/issue_statuses_helper.rb19
-rw-r--r--redmine/app/helpers/issues_helper.rb19
-rw-r--r--redmine/app/helpers/members_helper.rb19
-rw-r--r--redmine/app/helpers/news_helper.rb19
-rw-r--r--redmine/app/helpers/projects_helper.rb19
-rw-r--r--redmine/app/helpers/reports_helper.rb32
-rw-r--r--redmine/app/helpers/roles_helper.rb19
-rw-r--r--redmine/app/helpers/search_filter_helper.rb55
-rw-r--r--redmine/app/helpers/sort_helper.rb157
-rw-r--r--redmine/app/helpers/trackers_helper.rb19
-rw-r--r--redmine/app/helpers/users_helper.rb19
-rw-r--r--redmine/app/helpers/versions_helper.rb19
-rw-r--r--redmine/app/helpers/welcome_helper.rb19
-rw-r--r--redmine/app/models/attachment.rb81
-rw-r--r--redmine/app/models/custom_field.rb38
-rw-r--r--redmine/app/models/custom_value.rb41
-rw-r--r--redmine/app/models/document.rb24
-rw-r--r--redmine/app/models/enumeration.rb45
-rw-r--r--redmine/app/models/issue.rb55
-rw-r--r--redmine/app/models/issue_category.rb28
-rw-r--r--redmine/app/models/issue_history.rb23
-rw-r--r--redmine/app/models/issue_status.rb47
-rw-r--r--redmine/app/models/mailer.rb36
-rw-r--r--redmine/app/models/member.rb29
-rw-r--r--redmine/app/models/news.rb28
-rw-r--r--redmine/app/models/permission.rb63
-rw-r--r--redmine/app/models/project.rb44
-rw-r--r--redmine/app/models/role.rb31
-rw-r--r--redmine/app/models/tracker.rb31
-rw-r--r--redmine/app/models/user.rb89
-rw-r--r--redmine/app/models/version.rb30
-rw-r--r--redmine/app/models/workflow.rb25
-rw-r--r--redmine/app/views/account/login.rhtml13
-rw-r--r--redmine/app/views/account/my_account.rhtml54
-rw-r--r--redmine/app/views/account/my_page.rhtml19
-rw-r--r--redmine/app/views/account/show.rhtml19
-rw-r--r--redmine/app/views/admin/index.rhtml45
-rw-r--r--redmine/app/views/admin/info.rhtml4
-rw-r--r--redmine/app/views/admin/mail_options.rhtml16
-rw-r--r--redmine/app/views/admin/projects.rhtml35
-rw-r--r--redmine/app/views/custom_fields/_form.rhtml26
-rw-r--r--redmine/app/views/custom_fields/edit.rhtml6
-rw-r--r--redmine/app/views/custom_fields/list.rhtml32
-rw-r--r--redmine/app/views/custom_fields/new.rhtml7
-rw-r--r--redmine/app/views/documents/_form.rhtml15
-rw-r--r--redmine/app/views/documents/edit.rhtml8
-rw-r--r--redmine/app/views/documents/show.rhtml45
-rw-r--r--redmine/app/views/enumerations/_form.rhtml9
-rw-r--r--redmine/app/views/enumerations/edit.rhtml10
-rw-r--r--redmine/app/views/enumerations/list.rhtml22
-rw-r--r--redmine/app/views/enumerations/new.rhtml6
-rw-r--r--redmine/app/views/issue_categories/_form.rhtml7
-rw-r--r--redmine/app/views/issue_categories/edit.rhtml6
-rw-r--r--redmine/app/views/issue_statuses/_form.rhtml17
-rw-r--r--redmine/app/views/issue_statuses/edit.rhtml6
-rw-r--r--redmine/app/views/issue_statuses/list.rhtml30
-rw-r--r--redmine/app/views/issue_statuses/new.rhtml6
-rw-r--r--redmine/app/views/issues/_list_simple.rhtml28
-rw-r--r--redmine/app/views/issues/change_status.rhtml29
-rw-r--r--redmine/app/views/issues/edit.rhtml62
-rw-r--r--redmine/app/views/issues/show.rhtml90
-rw-r--r--redmine/app/views/layouts/base.rhtml89
-rw-r--r--redmine/app/views/mailer/_issue.rhtml6
-rw-r--r--redmine/app/views/mailer/issue_add.rhtml3
-rw-r--r--redmine/app/views/mailer/issue_change_status.rhtml3
-rw-r--r--redmine/app/views/news/_form.rhtml13
-rw-r--r--redmine/app/views/news/edit.rhtml6
-rw-r--r--redmine/app/views/news/show.rhtml10
-rw-r--r--redmine/app/views/projects/_form.rhtml28
-rw-r--r--redmine/app/views/projects/add.rhtml7
-rw-r--r--redmine/app/views/projects/add_document.rhtml26
-rw-r--r--redmine/app/views/projects/add_file.rhtml13
-rw-r--r--redmine/app/views/projects/add_issue.rhtml62
-rw-r--r--redmine/app/views/projects/add_news.rhtml7
-rw-r--r--redmine/app/views/projects/add_version.rhtml7
-rw-r--r--redmine/app/views/projects/changelog.rhtml12
-rw-r--r--redmine/app/views/projects/destroy.rhtml12
-rw-r--r--redmine/app/views/projects/list.rhtml22
-rw-r--r--redmine/app/views/projects/list_documents.rhtml21
-rw-r--r--redmine/app/views/projects/list_files.rhtml47
-rw-r--r--redmine/app/views/projects/list_issues.rhtml56
-rw-r--r--redmine/app/views/projects/list_members.rhtml11
-rw-r--r--redmine/app/views/projects/list_news.rhtml17
-rw-r--r--redmine/app/views/projects/settings.rhtml105
-rw-r--r--redmine/app/views/projects/show.rhtml53
-rw-r--r--redmine/app/views/reports/_simple.rhtml34
-rw-r--r--redmine/app/views/reports/issue_report.rhtml13
-rw-r--r--redmine/app/views/roles/_form.rhtml22
-rw-r--r--redmine/app/views/roles/edit.rhtml10
-rw-r--r--redmine/app/views/roles/list.rhtml23
-rw-r--r--redmine/app/views/roles/new.rhtml8
-rw-r--r--redmine/app/views/roles/workflow.rhtml70
-rw-r--r--redmine/app/views/trackers/_form.rhtml10
-rw-r--r--redmine/app/views/trackers/edit.rhtml6
-rw-r--r--redmine/app/views/trackers/list.rhtml24
-rw-r--r--redmine/app/views/trackers/new.rhtml7
-rw-r--r--redmine/app/views/users/_form.rhtml28
-rw-r--r--redmine/app/views/users/add.rhtml6
-rw-r--r--redmine/app/views/users/edit.rhtml7
-rw-r--r--redmine/app/views/users/list.rhtml46
-rw-r--r--redmine/app/views/versions/_form.rhtml13
-rw-r--r--redmine/app/views/versions/edit.rhtml8
-rw-r--r--redmine/app/views/welcome/index.rhtml30
-rw-r--r--redmine/config/boot.rb19
-rw-r--r--redmine/config/database.yml32
-rw-r--r--redmine/config/environment.rb85
-rw-r--r--redmine/config/environments/demo.rb21
-rw-r--r--redmine/config/environments/development.rb19
-rw-r--r--redmine/config/environments/production.rb20
-rw-r--r--redmine/config/environments/test.rb15
-rw-r--r--redmine/config/help.yml21
-rw-r--r--redmine/config/routes.rb24
-rw-r--r--redmine/db/migrate/001_setup.rb254
-rw-r--r--redmine/db/migrate/002_default_configuration.rb44
-rw-r--r--redmine/db/redmine_demo.dbbin0 -> 40960 bytes
-rw-r--r--redmine/doc/CHANGELOG17
-rw-r--r--redmine/doc/COPYING339
-rw-r--r--redmine/doc/INSTALL61
-rw-r--r--redmine/doc/README49
-rw-r--r--redmine/files/delete.me1
-rw-r--r--redmine/lang/en_US.rb4
-rw-r--r--redmine/lang/es_ES.rb315
-rw-r--r--redmine/lang/fr_FR.rb316
-rw-r--r--redmine/public/.htaccess40
-rw-r--r--redmine/public/404.html7
-rw-r--r--redmine/public/500.html7
-rw-r--r--redmine/public/dispatch.cgi10
-rw-r--r--redmine/public/dispatch.fcgi24
-rw-r--r--redmine/public/dispatch.rb10
-rw-r--r--redmine/public/favicon.ico0
-rw-r--r--redmine/public/images/Copie de help.pngbin0 -> 379 bytes
-rw-r--r--redmine/public/images/admin.pngbin0 -> 716 bytes
-rw-r--r--redmine/public/images/bulletgreen.pngbin0 -> 193 bytes
-rw-r--r--redmine/public/images/bulletred.pngbin0 -> 193 bytes
-rw-r--r--redmine/public/images/delete.pngbin0 -> 320 bytes
-rw-r--r--redmine/public/images/dir.pngbin0 -> 314 bytes
-rw-r--r--redmine/public/images/dir_new.pngbin0 -> 321 bytes
-rw-r--r--redmine/public/images/dir_open.pngbin0 -> 1030 bytes
-rw-r--r--redmine/public/images/document.pngbin0 -> 1014 bytes
-rw-r--r--redmine/public/images/edit_small.pngbin0 -> 238 bytes
-rw-r--r--redmine/public/images/file_new.pngbin0 -> 253 bytes
-rw-r--r--redmine/public/images/help.pngbin0 -> 1079 bytes
-rw-r--r--redmine/public/images/home.pngbin0 -> 301 bytes
-rw-r--r--redmine/public/images/issues.pngbin0 -> 356 bytes
-rw-r--r--redmine/public/images/locked.pngbin0 -> 437 bytes
-rw-r--r--redmine/public/images/logout.pngbin0 -> 342 bytes
-rw-r--r--redmine/public/images/mailer.pngbin0 -> 294 bytes
-rw-r--r--redmine/public/images/notes.pngbin0 -> 996 bytes
-rw-r--r--redmine/public/images/options.pngbin0 -> 1005 bytes
-rw-r--r--redmine/public/images/package.pngbin0 -> 298 bytes
-rw-r--r--redmine/public/images/projects.pngbin0 -> 299 bytes
-rw-r--r--redmine/public/images/rails.pngbin0 -> 1787 bytes
-rw-r--r--redmine/public/images/rails_powered.pngbin0 -> 262 bytes
-rw-r--r--redmine/public/images/rails_small.pngbin0 -> 1140 bytes
-rw-r--r--redmine/public/images/role.pngbin0 -> 293 bytes
-rw-r--r--redmine/public/images/rss.pngbin0 -> 256 bytes
-rw-r--r--redmine/public/images/sort_asc.pngbin0 -> 215 bytes
-rw-r--r--redmine/public/images/sort_desc.pngbin0 -> 217 bytes
-rw-r--r--redmine/public/images/tracker.pngbin0 -> 356 bytes
-rw-r--r--redmine/public/images/true.pngbin0 -> 183 bytes
-rw-r--r--redmine/public/images/user.pngbin0 -> 236 bytes
-rw-r--r--redmine/public/images/user_page.pngbin0 -> 292 bytes
-rw-r--r--redmine/public/images/users.pngbin0 -> 242 bytes
-rw-r--r--redmine/public/images/workflow.pngbin0 -> 285 bytes
-rw-r--r--redmine/public/javascripts/application.js8
-rw-r--r--redmine/public/javascripts/controls.js750
-rw-r--r--redmine/public/javascripts/dragdrop.js584
-rw-r--r--redmine/public/javascripts/effects.js854
-rw-r--r--redmine/public/javascripts/prototype.js1785
-rw-r--r--redmine/public/manual/administration.html121
-rw-r--r--redmine/public/manual/images/issues_list.pngbin0 -> 8055 bytes
-rw-r--r--redmine/public/manual/images/users_edit.pngbin0 -> 2700 bytes
-rw-r--r--redmine/public/manual/images/users_list.pngbin0 -> 6502 bytes
-rw-r--r--redmine/public/manual/images/workflow.pngbin0 -> 4967 bytes
-rw-r--r--redmine/public/manual/index.html64
-rw-r--r--redmine/public/manual/projects.html109
-rw-r--r--redmine/public/manual/stylesheets/help.css70
-rw-r--r--redmine/public/robots.txt1
-rw-r--r--redmine/public/stylesheets/application.css322
-rw-r--r--redmine/public/stylesheets/rails.css56
-rw-r--r--redmine/script/about3
-rw-r--r--redmine/script/breakpointer3
-rw-r--r--redmine/script/console3
-rw-r--r--redmine/script/destroy3
-rw-r--r--redmine/script/generate3
-rw-r--r--redmine/script/performance/benchmarker3
-rw-r--r--redmine/script/performance/profiler3
-rw-r--r--redmine/script/plugin3
-rw-r--r--redmine/script/process/reaper3
-rw-r--r--redmine/script/process/spawner3
-rw-r--r--redmine/script/process/spinner3
-rw-r--r--redmine/script/runner3
-rw-r--r--redmine/script/server3
-rw-r--r--redmine/test/fixtures/attachments.yml5
-rw-r--r--redmine/test/fixtures/custom_fields.yml5
-rw-r--r--redmine/test/fixtures/documents.yml5
-rw-r--r--redmine/test/fixtures/enumerations.yml5
-rw-r--r--redmine/test/fixtures/issue_categories.yml5
-rw-r--r--redmine/test/fixtures/issue_custom_fields.yml5
-rw-r--r--redmine/test/fixtures/issue_custom_values.yml5
-rw-r--r--redmine/test/fixtures/issue_histories.yml5
-rw-r--r--redmine/test/fixtures/issue_statuses.yml5
-rw-r--r--redmine/test/fixtures/issues.yml5
-rw-r--r--redmine/test/fixtures/mailer/issue_closed3
-rw-r--r--redmine/test/fixtures/members.yml5
-rw-r--r--redmine/test/fixtures/news.yml5
-rw-r--r--redmine/test/fixtures/permissions.yml5
-rw-r--r--redmine/test/fixtures/projects.yml5
-rw-r--r--redmine/test/fixtures/roles.yml5
-rw-r--r--redmine/test/fixtures/trackers.yml5
-rw-r--r--redmine/test/fixtures/users.yml5
-rw-r--r--redmine/test/fixtures/versions.yml5
-rw-r--r--redmine/test/fixtures/workflow.yml5
-rw-r--r--redmine/test/functional/account_controller_test.rb18
-rw-r--r--redmine/test/functional/admin_controller_test.rb18
-rw-r--r--redmine/test/functional/custom_fields_controller_test.rb88
-rw-r--r--redmine/test/functional/documents_controller_test.rb88
-rw-r--r--redmine/test/functional/enumerations_controller_test.rb88
-rw-r--r--redmine/test/functional/help_controller_test.rb18
-rw-r--r--redmine/test/functional/issue_categories_controller_test.rb88
-rw-r--r--redmine/test/functional/issue_statuses_controller_test.rb88
-rw-r--r--redmine/test/functional/issues_controller_test.rb88
-rw-r--r--redmine/test/functional/members_controller_test.rb88
-rw-r--r--redmine/test/functional/news_controller_test.rb88
-rw-r--r--redmine/test/functional/projects_controller_test.rb88
-rw-r--r--redmine/test/functional/reports_controller_test.rb18
-rw-r--r--redmine/test/functional/roles_controller_test.rb88
-rw-r--r--redmine/test/functional/trackers_controller_test.rb88
-rw-r--r--redmine/test/functional/users_controller_test.rb88
-rw-r--r--redmine/test/functional/versions_controller_test.rb88
-rw-r--r--redmine/test/functional/welcome_controller_test.rb18
-rw-r--r--redmine/test/test_helper.rb28
-rw-r--r--redmine/test/unit/attachment_test.rb10
-rw-r--r--redmine/test/unit/custom_field_test.rb10
-rw-r--r--redmine/test/unit/document_test.rb10
-rw-r--r--redmine/test/unit/enumeration_test.rb10
-rw-r--r--redmine/test/unit/issue_category_test.rb10
-rw-r--r--redmine/test/unit/issue_custom_field_test.rb10
-rw-r--r--redmine/test/unit/issue_custom_value_test.rb10
-rw-r--r--redmine/test/unit/issue_history_test.rb10
-rw-r--r--redmine/test/unit/issue_status_test.rb10
-rw-r--r--redmine/test/unit/issue_test.rb10
-rw-r--r--redmine/test/unit/mailer_test.rb35
-rw-r--r--redmine/test/unit/member_test.rb10
-rw-r--r--redmine/test/unit/news_test.rb10
-rw-r--r--redmine/test/unit/packages_test.rb10
-rw-r--r--redmine/test/unit/permission_test.rb10
-rw-r--r--redmine/test/unit/project_test.rb10
-rw-r--r--redmine/test/unit/role_test.rb10
-rw-r--r--redmine/test/unit/tracker_test.rb10
-rw-r--r--redmine/test/unit/user_test.rb10
-rw-r--r--redmine/test/unit/version_test.rb10
-rw-r--r--redmine/test/unit/workflow_test.rb10
-rw-r--r--redmine/vendor/plugins/localization/README85
-rw-r--r--redmine/vendor/plugins/localization/init.rb3
-rw-r--r--redmine/vendor/plugins/localization/lib/localization.rb57
284 files changed, 12752 insertions, 0 deletions
diff --git a/redmine/Rakefile b/redmine/Rakefile
new file mode 100644
index 000000000..cffd19f0c
--- /dev/null
+++ b/redmine/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/redmine/app/controllers/account_controller.rb b/redmine/app/controllers/account_controller.rb
new file mode 100644
index 000000000..d5c98f58c
--- /dev/null
+++ b/redmine/app/controllers/account_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 AccountController < ApplicationController
+ layout 'base'
+ # prevents login action to be filtered by check_if_login_required application scope filter
+ skip_before_filter :check_if_login_required, :only => :login
+ before_filter :require_login, :except => [:show, :login]
+
+ def show
+ @user = User.find(params[:id])
+ end
+
+ # Login request and validation
+ def login
+ if request.get?
+ session[:user] = nil
+ @user = User.new
+ else
+ @user = User.new(params[:user])
+ logged_in_user = @user.try_to_login
+ if logged_in_user
+ session[:user] = logged_in_user
+ redirect_back_or_default :controller => 'account', :action => 'my_page'
+ else
+ flash[:notice] = _('Invalid user/password')
+ end
+ end
+ end
+
+ # Log out current user and redirect to welcome page
+ def logout
+ session[:user] = nil
+ redirect_to(:controller => '')
+ end
+
+ def my_page
+ @user = session[:user]
+ @reported_issues = Issue.find(:all, :conditions => ["author_id=?", @user.id], :limit => 10, :include => [ :status, :project, :tracker ], :order => 'issues.updated_on DESC')
+ @assigned_issues = Issue.find(:all, :conditions => ["assigned_to_id=?", @user.id], :limit => 10, :include => [ :status, :project, :tracker ], :order => 'issues.updated_on DESC')
+ end
+
+ # Edit current user's account
+ def my_account
+ @user = User.find(session[:user].id)
+ if request.post? and @user.update_attributes(@params[:user])
+ flash[:notice] = 'Account was successfully updated.'
+ session[:user] = @user
+ set_localization
+ end
+ end
+
+ # Change current user's password
+ def change_password
+ @user = User.find(session[:user].id)
+ if @user.check_password?(@params[:old_password])
+ if @params[:new_password] == @params[:new_password_confirmation]
+ if @user.change_password(@params[:old_password], @params[:new_password])
+ flash[:notice] = 'Password was successfully updated.'
+ end
+ else
+ flash[:notice] = 'Password confirmation doesn\'t match!'
+ end
+ else
+ flash[:notice] = 'Wrong password'
+ end
+ render :action => 'my_account'
+ end
+end
diff --git a/redmine/app/controllers/admin_controller.rb b/redmine/app/controllers/admin_controller.rb
new file mode 100644
index 000000000..fa34baff1
--- /dev/null
+++ b/redmine/app/controllers/admin_controller.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 AdminController < ApplicationController
+ layout 'base'
+ before_filter :require_admin
+
+ helper :sort
+ include SortHelper
+
+ def index
+ end
+
+ def projects
+ sort_init 'projects.name', 'asc'
+ sort_update
+ @project_pages, @projects = paginate :projects, :per_page => 15, :order => sort_clause
+ end
+
+ def mail_options
+ @actions = Permission.find(:all, :conditions => ["mail_option=?", true]) || []
+ if request.post?
+ @actions.each { |a|
+ a.mail_enabled = params[:action_ids].include? a.id.to_s
+ a.save
+ }
+ flash[:notice] = "Mail options were successfully updated."
+ end
+ end
+
+ def info
+ @adapter_name = ActiveRecord::Base.connection.adapter_name
+ end
+
+end
diff --git a/redmine/app/controllers/application.rb b/redmine/app/controllers/application.rb
new file mode 100644
index 000000000..a9dd6b80e
--- /dev/null
+++ b/redmine/app/controllers/application.rb
@@ -0,0 +1,86 @@
+# 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 ApplicationController < ActionController::Base
+ before_filter :check_if_login_required, :set_localization
+
+ # check if login is globally required to access the application
+ def check_if_login_required
+ require_login if RDM_LOGIN_REQUIRED
+ end
+
+ def set_localization
+ Localization.lang = session[:user].nil? ? RDM_DEFAULT_LANG : (session[:user].language || RDM_DEFAULT_LANG)
+ end
+
+ def require_login
+ unless session[:user]
+ store_location
+ redirect_to(:controller => "account", :action => "login")
+ end
+ end
+
+ def require_admin
+ if session[:user].nil?
+ store_location
+ redirect_to(:controller => "account", :action => "login")
+ else
+ unless session[:user].admin?
+ flash[:notice] = "Acces not allowed"
+ redirect_to(:controller => "projects", :action => "list")
+ end
+ end
+ end
+
+ # authorizes the user for the requested action.
+ def authorize
+ # check if action is allowed on public projects
+ if @project.public? and Permission.allowed_to_public "%s/%s" % [ @params[:controller], @params[:action] ]
+ return true
+ end
+ # if user is not logged in, he is redirect to login form
+ unless session[:user]
+ store_location
+ redirect_to(:controller => "account", :action => "login")
+ return false
+ end
+ # check if user is authorized
+ if session[:user].admin? or Permission.allowed_to_role( "%s/%s" % [ @params[:controller], @params[:action] ], session[:user].role_for_project(@project.id) )
+ return true
+ end
+ flash[:notice] = "Acces denied"
+ redirect_to(:controller => "")
+ return false
+ end
+
+ # store current uri in the session.
+ # we can return to this location by calling redirect_back_or_default
+ def store_location
+ session[:return_to] = @request.request_uri
+ end
+
+ # move to the last store_location call or to the passed default one
+ def redirect_back_or_default(default)
+ if session[:return_to].nil?
+ redirect_to default
+ else
+ redirect_to_url session[:return_to]
+ session[:return_to] = nil
+ end
+ end
+
+end \ No newline at end of file
diff --git a/redmine/app/controllers/custom_fields_controller.rb b/redmine/app/controllers/custom_fields_controller.rb
new file mode 100644
index 000000000..93f6353fa
--- /dev/null
+++ b/redmine/app/controllers/custom_fields_controller.rb
@@ -0,0 +1,58 @@
+# 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'
+ end
+
+ def list
+ @custom_field_pages, @custom_fields = paginate :custom_fields, :per_page => 10
+ end
+
+ def new
+ if request.get?
+ @custom_field = CustomField.new
+ else
+ @custom_field = CustomField.new(params[:custom_field])
+ if @custom_field.save
+ flash[:notice] = 'CustomField was successfully created.'
+ redirect_to :action => 'list'
+ end
+ end
+ end
+
+ def edit
+ @custom_field = CustomField.find(params[:id])
+ if request.post? and @custom_field.update_attributes(params[:custom_field])
+ flash[:notice] = 'CustomField was successfully updated.'
+ redirect_to :action => 'list'
+ end
+ end
+
+ def destroy
+ CustomField.find(params[:id]).destroy
+ redirect_to :action => 'list'
+ rescue
+ flash[:notice] = "Unable to delete custom field"
+ redirect_to :action => 'list'
+ end
+end
diff --git a/redmine/app/controllers/documents_controller.rb b/redmine/app/controllers/documents_controller.rb
new file mode 100644
index 000000000..3c76465c9
--- /dev/null
+++ b/redmine/app/controllers/documents_controller.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 DocumentsController < ApplicationController
+ layout 'base'
+ before_filter :find_project, :authorize
+
+ def show
+ end
+
+ def edit
+ @categories = Enumeration::get_values('DCAT')
+ if request.post? and @document.update_attributes(params[:document])
+ flash[:notice] = 'Document was successfully updated.'
+ 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
+ end
+
+ def add_attachment
+ # Save the attachment
+ if params[:attachment][:file].size > 0
+ @attachment = @document.attachments.build(params[:attachment])
+ @attachment.author_id = session[:user].id unless session[:user].nil?
+ @attachment.save
+ end
+ render :action => 'show'
+ end
+
+ def destroy_attachment
+ @document.attachments.find(params[:attachment_id]).destroy
+ render :action => 'show'
+ end
+
+private
+ def find_project
+ @document = Document.find(params[:id])
+ @project = @document.project
+ end
+
+end
diff --git a/redmine/app/controllers/enumerations_controller.rb b/redmine/app/controllers/enumerations_controller.rb
new file mode 100644
index 000000000..01664c85e
--- /dev/null
+++ b/redmine/app/controllers/enumerations_controller.rb
@@ -0,0 +1,69 @@
+# 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] = 'Enumeration was successfully created.'
+ 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] = 'Enumeration was successfully updated.'
+ redirect_to :action => 'list', :opt => @enumeration.opt
+ else
+ render :action => 'edit'
+ end
+ end
+
+ def destroy
+ Enumeration.find(params[:id]).destroy
+ redirect_to :action => 'list'
+ rescue
+ flash[:notice] = "Unable to delete enumeration"
+ redirect_to :action => 'list'
+ end
+end
diff --git a/redmine/app/controllers/help_controller.rb b/redmine/app/controllers/help_controller.rb
new file mode 100644
index 000000000..4b555d599
--- /dev/null
+++ b/redmine/app/controllers/help_controller.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 HelpController < ApplicationController
+
+ skip_before_filter :check_if_login_required
+ before_filter :load_help_config
+
+ def index
+ if @params[:ctrl] and @help_config[@params[:ctrl]]
+ if @params[:page] and @help_config[@params[:ctrl]][@params[:page]]
+ template = @help_config[@params[:ctrl]][@params[:page]]
+ else
+ template = @help_config[@params[:ctrl]]['index']
+ end
+ end
+
+ if template
+ redirect_to "/manual/#{template}"
+ else
+ redirect_to "/manual/"
+ end
+ end
+
+private
+ def load_help_config
+ @help_config = YAML::load(File.open("#{RAILS_ROOT}/config/help.yml"))
+ end
+end
diff --git a/redmine/app/controllers/issue_categories_controller.rb b/redmine/app/controllers/issue_categories_controller.rb
new file mode 100644
index 000000000..cf0f9dc06
--- /dev/null
+++ b/redmine/app/controllers/issue_categories_controller.rb
@@ -0,0 +1,42 @@
+# 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
+
+ def edit
+ if request.post? and @category.update_attributes(params[:category])
+ flash[:notice] = 'Issue category was successfully updated.'
+ redirect_to :controller => 'projects', :action => 'settings', :id => @project
+ end
+ end
+
+ def destroy
+ @category.destroy
+ redirect_to :controller => 'projects', :action => 'settings', :id => @project
+ rescue
+ flash[:notice] = "Categorie can't be deleted"
+ redirect_to :controller => 'projects', :action => 'settings', :id => @project
+ end
+
+private
+ def find_project
+ @category = IssueCategory.find(params[:id])
+ @project = @category.project
+ end
+end
diff --git a/redmine/app/controllers/issue_statuses_controller.rb b/redmine/app/controllers/issue_statuses_controller.rb
new file mode 100644
index 000000000..3de8158c6
--- /dev/null
+++ b/redmine/app/controllers/issue_statuses_controller.rb
@@ -0,0 +1,68 @@
+# 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
+
+ def index
+ list
+ render :action => 'list'
+ end
+
+ def list
+ @issue_status_pages, @issue_statuses = paginate :issue_statuses, :per_page => 10
+ end
+
+ def new
+ @issue_status = IssueStatus.new
+ end
+
+ def create
+ @issue_status = IssueStatus.new(params[:issue_status])
+ if @issue_status.save
+ flash[:notice] = 'IssueStatus was successfully created.'
+ 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] = 'IssueStatus was successfully updated.'
+ redirect_to :action => 'list'
+ else
+ render :action => 'edit'
+ end
+ end
+
+ def destroy
+ IssueStatus.find(params[:id]).destroy
+ redirect_to :action => 'list'
+ rescue
+ flash[:notice] = "Unable to delete issue status"
+ redirect_to :action => 'list'
+ end
+
+
+end
diff --git a/redmine/app/controllers/issues_controller.rb b/redmine/app/controllers/issues_controller.rb
new file mode 100644
index 000000000..5d5872f39
--- /dev/null
+++ b/redmine/app/controllers/issues_controller.rb
@@ -0,0 +1,102 @@
+# 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 IssuesController < ApplicationController
+ layout 'base'
+ before_filter :find_project, :authorize
+
+ helper :custom_fields
+ include CustomFieldsHelper
+
+ def show
+ @status_options = @issue.status.workflows.find(:all, :conditions => ["role_id=? and tracker_id=?", session[:user].role_for_project(@project.id), @issue.tracker.id]).collect{ |w| w.new_status } if session[:user]
+ end
+
+ def edit
+ @trackers = Tracker.find(:all)
+ @priorities = Enumeration::get_values('IPRI')
+
+ if request.get?
+ @custom_values = @project.custom_fields_for_issues.collect { |x| @issue.custom_values.find_by_custom_field_id(x.id) || CustomValue.new(:custom_field => x) }
+ else
+ # Retrieve custom fields and values
+ @custom_values = @project.custom_fields_for_issues.collect { |x| CustomValue.new(:custom_field => x, :value => params["custom_fields"][x.id.to_s]) }
+
+ @issue.custom_values = @custom_values
+ if @issue.update_attributes(params[:issue])
+ flash[:notice] = 'Issue was successfully updated.'
+ redirect_to :action => 'show', :id => @issue
+ end
+ end
+ end
+
+ def change_status
+ @history = @issue.histories.build(params[:history])
+ @status_options = @issue.status.workflows.find(:all, :conditions => ["role_id=? and tracker_id=?", session[:user].role_for_project(@project.id), @issue.tracker.id]).collect{ |w| w.new_status } if session[:user]
+
+ if params[:confirm]
+ unless session[:user].nil?
+ @history.author = session[:user]
+ end
+ if @history.save
+ @issue.status = @history.status
+ @issue.fixed_version_id = (params[:issue][:fixed_version_id])
+ @issue.assigned_to_id = (params[:issue][:assigned_to_id])
+ if @issue.save
+ flash[:notice] = 'Issue was successfully updated.'
+ Mailer.deliver_issue_change_status(@issue) if Permission.find_by_controller_and_action(@params[:controller], @params[:action]).mail_enabled?
+ redirect_to :action => 'show', :id => @issue
+ end
+ end
+ end
+ @assignable_to = @project.members.find(:all, :include => :user).collect{ |m| m.user }
+
+ end
+
+ def destroy
+ @issue.destroy
+ redirect_to :controller => 'projects', :action => 'list_issues', :id => @project
+ end
+
+ def add_attachment
+ # Save the attachment
+ if params[:attachment][:file].size > 0
+ @attachment = @issue.attachments.build(params[:attachment])
+ @attachment.author_id = session[:user].id unless session[:user].nil?
+ @attachment.save
+ end
+ redirect_to :action => 'show', :id => @issue
+ end
+
+ def destroy_attachment
+ @issue.attachments.find(params[:attachment_id]).destroy
+ redirect_to :action => 'show', :id => @issue
+ end
+
+ # Send the file in stream mode
+ def download
+ @attachment = @issue.attachments.find(params[:attachment_id])
+ send_file @attachment.diskfile, :filename => @attachment.filename
+ end
+
+private
+ def find_project
+ @issue = Issue.find(params[:id])
+ @project = @issue.project
+ end
+
+end
diff --git a/redmine/app/controllers/members_controller.rb b/redmine/app/controllers/members_controller.rb
new file mode 100644
index 000000000..ac6b08d26
--- /dev/null
+++ b/redmine/app/controllers/members_controller.rb
@@ -0,0 +1,41 @@
+# 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_project, :authorize
+
+ def edit
+ if request.post? and @member.update_attributes(params[:member])
+ flash[:notice] = 'Member was successfully updated.'
+ redirect_to :controller => 'projects', :action => 'settings', :id => @project
+ end
+ end
+
+ def destroy
+ @member.destroy
+ flash[:notice] = 'Member was successfully removed.'
+ redirect_to :controller => 'projects', :action => 'settings', :id => @project
+ end
+
+private
+ def find_project
+ @member = Member.find(params[:id])
+ @project = @member.project
+ end
+
+end
diff --git a/redmine/app/controllers/news_controller.rb b/redmine/app/controllers/news_controller.rb
new file mode 100644
index 000000000..065336f26
--- /dev/null
+++ b/redmine/app/controllers/news_controller.rb
@@ -0,0 +1,42 @@
+# 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
+
+ def show
+ end
+
+ def edit
+ if request.post? and @news.update_attributes(params[:news])
+ flash[:notice] = 'News was successfully updated.'
+ redirect_to :action => 'show', :id => @news
+ end
+ end
+
+ def destroy
+ @news.destroy
+ redirect_to :controller => 'projects', :action => 'list_news', :id => @project
+ end
+
+private
+ def find_project
+ @news = News.find(params[:id])
+ @project = @news.project
+ end
+end
diff --git a/redmine/app/controllers/projects_controller.rb b/redmine/app/controllers/projects_controller.rb
new file mode 100644
index 000000000..2e74045e9
--- /dev/null
+++ b/redmine/app/controllers/projects_controller.rb
@@ -0,0 +1,260 @@
+# 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 ProjectsController < ApplicationController
+ layout 'base'
+ before_filter :find_project, :authorize, :except => [ :index, :list, :add ]
+ before_filter :require_admin, :only => [ :add, :destroy ]
+
+ helper :sort
+ include SortHelper
+ helper :search_filter
+ include SearchFilterHelper
+ helper :custom_fields
+ include CustomFieldsHelper
+
+ def index
+ list
+ render :action => 'list'
+ end
+
+ # Lists public projects
+ def list
+ sort_init 'projects.name', 'asc'
+ sort_update
+ @project_count = Project.count(["public=?", true])
+ @project_pages = Paginator.new self, @project_count,
+ 15,
+ @params['page']
+ @projects = Project.find :all, :order => sort_clause,
+ :conditions => ["public=?", true],
+ :limit => @project_pages.items_per_page,
+ :offset => @project_pages.current.offset
+ end
+
+ # Add a new project
+ def add
+ @custom_fields = CustomField::find_all
+ @project = Project.new(params[:project])
+ if request.post?
+ @project.custom_fields = CustomField.find(@params[:custom_field_ids]) if @params[:custom_field_ids]
+ if @project.save
+ flash[:notice] = 'Project was successfully created.'
+ redirect_to :controller => 'admin', :action => 'projects'
+ end
+ end
+ end
+
+ # Show @project
+ def show
+ @members = @project.members.find(:all, :include => [:user, :role])
+ end
+
+ def settings
+ @custom_fields = CustomField::find_all
+ @issue_category ||= IssueCategory.new
+ @member ||= @project.members.new
+ @roles = Role.find_all
+ @users = User.find_all - @project.members.find(:all, :include => :user).collect{|m| m.user }
+ end
+
+ # Edit @project
+ def edit
+ if request.post?
+ @project.custom_fields = CustomField.find(@params[:custom_field_ids]) if @params[:custom_field_ids]
+ if @project.update_attributes(params[:project])
+ flash[:notice] = 'Project was successfully updated.'
+ redirect_to :action => 'settings', :id => @project
+ else
+ settings
+ render :action => 'settings'
+ end
+ end
+ end
+
+ # Delete @project
+ def destroy
+ if request.post? and params[:confirm]
+ @project.destroy
+ redirect_to :controller => 'admin', :action => 'projects'
+ end
+ end
+
+ # Add a new issue category to @project
+ def add_issue_category
+ if request.post?
+ @issue_category = @project.issue_categories.build(params[:issue_category])
+ if @issue_category.save
+ redirect_to :action => 'settings', :id => @project
+ else
+ settings
+ render :action => 'settings'
+ end
+ end
+ end
+
+ # Add a new version to @project
+ def add_version
+ @version = @project.versions.build(params[:version])
+ if request.post? and @version.save
+ redirect_to :action => 'settings', :id => @project
+ end
+ end
+
+ # Add a new member to @project
+ def add_member
+ @member = @project.members.build(params[:member])
+ if request.post?
+ if @member.save
+ flash[:notice] = 'Member was successfully added.'
+ redirect_to :action => 'settings', :id => @project
+ else
+ settings
+ render :action => 'settings'
+ end
+ end
+ end
+
+ # Show members list of @project
+ def list_members
+ @members = @project.members
+ end
+
+ # Add a new document to @project
+ def add_document
+ @categories = Enumeration::get_values('DCAT')
+ @document = @project.documents.build(params[:document])
+ if request.post?
+ # Save the attachment
+ if params[:attachment][:file].size > 0
+ @attachment = @document.attachments.build(params[:attachment])
+ @attachment.author_id = session[:user].id unless session[:user].nil?
+ end
+ if @document.save
+ redirect_to :action => 'list_documents', :id => @project
+ end
+ end
+ end
+
+ # Show documents list of @project
+ def list_documents
+ @documents = @project.documents
+ end
+
+ # Add a new issue to @project
+ def add_issue
+ @trackers = Tracker.find(:all)
+ @priorities = Enumeration::get_values('IPRI')
+ if request.get?
+ @issue = @project.issues.build
+ @custom_values = @project.custom_fields_for_issues.collect { |x| CustomValue.new(:custom_field => x) }
+ else
+ # Create the issue and set the author
+ @issue = @project.issues.build(params[:issue])
+ @issue.author = session[:user] unless session[:user].nil?
+ # Create the document if a file was sent
+ if params[:attachment][:file].size > 0
+ @attachment = @issue.attachments.build(params[:attachment])
+ @attachment.author_id = session[:user].id unless session[:user].nil?
+ end
+ @custom_values = @project.custom_fields_for_issues.collect { |x| CustomValue.new(:custom_field => x, :value => params["custom_fields"][x.id.to_s]) }
+ @issue.custom_values = @custom_values
+ if @issue.save
+ flash[:notice] = "Issue was successfully added."
+ Mailer.deliver_issue_add(@issue) if Permission.find_by_controller_and_action(@params[:controller], @params[:action]).mail_enabled?
+ redirect_to :action => 'list_issues', :id => @project
+ end
+ end
+ end
+
+ # Show issues list of @project
+ def list_issues
+ sort_init 'issues.id', 'desc'
+ sort_update
+
+ search_filter_criteria 'issues.tracker_id', :values => "Tracker.find(:all)"
+ search_filter_criteria 'issues.priority_id', :values => "Enumeration.find(:all, :conditions => ['opt=?','IPRI'])"
+ search_filter_criteria 'issues.category_id', :values => "@project.issue_categories"
+ search_filter_criteria 'issues.status_id', :values => "IssueStatus.find(:all)"
+ search_filter_criteria 'issues.author_id', :values => "User.find(:all)", :label => "display_name"
+ search_filter_update if params[:set_filter] or request.post?
+
+ @issue_count = @project.issues.count(search_filter_clause)
+ @issue_pages = Paginator.new self, @issue_count,
+ 15,
+ @params['page']
+ @issues = @project.issues.find :all, :order => sort_clause,
+ :include => [ :author, :status, :tracker ],
+ :conditions => search_filter_clause,
+ :limit => @issue_pages.items_per_page,
+ :offset => @issue_pages.current.offset
+ end
+
+ # Add a news to @project
+ def add_news
+ @news = @project.news.build(params[:news])
+ if request.post?
+ @news.author = session[:user] unless session[:user].nil?
+ if @news.save
+ redirect_to :action => 'list_news', :id => @project
+ end
+ end
+ end
+
+ # Show news list of @project
+ def list_news
+ @news_pages, @news = paginate :news, :per_page => 10, :conditions => ["project_id=?", @project.id], :include => :author, :order => "news.created_on DESC"
+ end
+
+ def add_file
+ if request.post?
+ # Save the attachment
+ if params[:attachment][:file].size > 0
+ @attachment = @project.versions.find(params[:version_id]).attachments.build(params[:attachment])
+ @attachment.author_id = session[:user].id unless session[:user].nil?
+ if @attachment.save
+ redirect_to :controller => 'projects', :action => 'list_files', :id => @project
+ end
+ end
+ end
+ @versions = @project.versions
+ end
+
+ def list_files
+ @versions = @project.versions
+ end
+
+ # Show changelog of @project
+ def changelog
+ @fixed_issues = @project.issues.find(:all,
+ :include => [ :fixed_version, :status, :tracker ],
+ :conditions => [ "issue_statuses.is_closed=? and trackers.is_in_chlog=? and issues.fixed_version_id is not null", true, true]
+ )
+ 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
+ flash[:notice] = 'Project not found.'
+ redirect_to :action => 'list'
+ end
+
+end
diff --git a/redmine/app/controllers/reports_controller.rb b/redmine/app/controllers/reports_controller.rb
new file mode 100644
index 000000000..7dd57cc3b
--- /dev/null
+++ b/redmine/app/controllers/reports_controller.rb
@@ -0,0 +1,71 @@
+# 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
+ @trackers = Tracker.find_all
+ @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
+ issues i, issue_statuses s, trackers t
+ where
+ i.status_id=s.id
+ and i.tracker_id=t.id
+ and i.project_id=#{@project.id}
+ group by s.id, t.id")
+ @priorities = Enumeration::get_values('IPRI')
+ @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
+ issues i, issue_statuses s, enumerations p
+ where
+ i.status_id=s.id
+ and i.priority_id=p.id
+ and i.project_id=#{@project.id}
+ group by s.id, p.id")
+ @categories = @project.issue_categories
+ @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
+ issues i, issue_statuses s, issue_categories c
+ where
+ i.status_id=s.id
+ and i.category_id=c.id
+ and i.project_id=#{@project.id}
+ group by s.id, c.id")
+ end
+
+
+private
+ # Find project of id params[:id]
+ def find_project
+ @project = Project.find(params[:id])
+ end
+end
diff --git a/redmine/app/controllers/roles_controller.rb b/redmine/app/controllers/roles_controller.rb
new file mode 100644
index 000000000..6e4fc74e0
--- /dev/null
+++ b/redmine/app/controllers/roles_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 RolesController < ApplicationController
+ layout 'base'
+ before_filter :require_admin
+
+ def index
+ list
+ render :action => 'list'
+ end
+
+ def list
+ @role_pages, @roles = paginate :roles, :per_page => 10
+ end
+
+ def new
+ @role = Role.new(params[:role])
+ if request.post?
+ @role.permissions = Permission.find(@params[:permission_ids]) if @params[:permission_ids]
+ if @role.save
+ flash[:notice] = 'Role was successfully created.'
+ redirect_to :action => 'list'
+ end
+ end
+ @permissions = Permission.find(:all, :order => 'sort ASC')
+ end
+
+ def edit
+ @role = Role.find(params[:id])
+ if request.post? and @role.update_attributes(params[:role])
+ @role.permissions = Permission.find(@params[:permission_ids] || [])
+ Permission.allowed_to_role_expired
+ flash[:notice] = 'Role was successfully updated.'
+ redirect_to :action => 'list'
+ end
+ @permissions = Permission.find(:all, :order => 'sort ASC')
+ end
+
+ def destroy
+ @role = Role.find(params[:id])
+ unless @role.members.empty?
+ flash[:notice] = 'Some members have this role. Can\'t delete it.'
+ else
+ @role.destroy
+ end
+ redirect_to :action => 'list'
+ end
+
+ def workflow
+ @roles = Role.find_all
+ @trackers = Tracker.find_all
+ @statuses = IssueStatus.find_all
+
+ @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] = 'Workflow was successfully updated.'
+ end
+ end
+ end
+end
diff --git a/redmine/app/controllers/trackers_controller.rb b/redmine/app/controllers/trackers_controller.rb
new file mode 100644
index 000000000..38cdb6cde
--- /dev/null
+++ b/redmine/app/controllers/trackers_controller.rb
@@ -0,0 +1,60 @@
+# 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'
+ end
+
+ # GETs should be safe (see http://www.w3.org/2001/tag/doc/whenToUseGet.html)
+ verify :method => :post, :only => [ :destroy ], :redirect_to => { :action => :list }
+
+ def list
+ @tracker_pages, @trackers = paginate :trackers, :per_page => 10
+ end
+
+ def new
+ @tracker = Tracker.new(params[:tracker])
+ if request.post? and @tracker.save
+ flash[:notice] = 'Tracker was successfully created.'
+ redirect_to :action => 'list'
+ end
+ end
+
+ def edit
+ @tracker = Tracker.find(params[:id])
+ if request.post? and @tracker.update_attributes(params[:tracker])
+ flash[:notice] = 'Tracker was successfully updated.'
+ redirect_to :action => 'list'
+ end
+ end
+
+ def destroy
+ @tracker = Tracker.find(params[:id])
+ unless @tracker.issues.empty?
+ flash[:notice] = "This tracker contains issues and can\'t be deleted."
+ else
+ @tracker.destroy
+ end
+ redirect_to :action => 'list'
+ end
+
+end
diff --git a/redmine/app/controllers/users_controller.rb b/redmine/app/controllers/users_controller.rb
new file mode 100644
index 000000000..64c9fd311
--- /dev/null
+++ b/redmine/app/controllers/users_controller.rb
@@ -0,0 +1,73 @@
+# 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 UsersController < ApplicationController
+ layout 'base'
+ before_filter :require_admin
+
+ helper :sort
+ include SortHelper
+
+ def index
+ list
+ render :action => 'list'
+ end
+
+ def list
+ sort_init 'users.login', 'asc'
+ sort_update
+ @user_count = User.count
+ @user_pages = Paginator.new self, @user_count,
+ 15,
+ @params['page']
+ @users = User.find :all, :order => sort_clause,
+ :limit => @user_pages.items_per_page,
+ :offset => @user_pages.current.offset
+ end
+
+ def add
+ if request.get?
+ @user = User.new
+ else
+ @user = User.new(params[:user])
+ @user.admin = params[:user][:admin]
+ if @user.save
+ flash[:notice] = 'User was successfully created.'
+ redirect_to :action => 'list'
+ end
+ end
+ end
+
+ def edit
+ @user = User.find(params[:id])
+ if request.post?
+ @user.admin = params[:user][:admin] if params[:user][:admin]
+ if @user.update_attributes(params[:user])
+ flash[:notice] = 'User was successfully updated.'
+ redirect_to :action => 'list'
+ end
+ end
+ end
+
+ def destroy
+ User.find(params[:id]).destroy
+ redirect_to :action => 'list'
+ rescue
+ flash[:notice] = "Unable to delete user"
+ redirect_to :action => 'list'
+ end
+end
diff --git a/redmine/app/controllers/versions_controller.rb b/redmine/app/controllers/versions_controller.rb
new file mode 100644
index 000000000..c4fd241a8
--- /dev/null
+++ b/redmine/app/controllers/versions_controller.rb
@@ -0,0 +1,53 @@
+# 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
+
+ def edit
+ if request.post? and @version.update_attributes(params[:version])
+ flash[:notice] = 'Version was successfully updated.'
+ redirect_to :controller => 'projects', :action => 'settings', :id => @project
+ end
+ end
+
+ def destroy
+ @version.destroy
+ redirect_to :controller => 'projects', :action => 'settings', :id => @project
+ rescue
+ flash[:notice] = "Unable to delete version"
+ redirect_to :controller => 'projects', :action => 'settings', :id => @project
+ end
+
+ def download
+ @attachment = @version.attachments.find(params[:attachment_id])
+ @attachment.increment_download
+ send_file @attachment.diskfile, :filename => @attachment.filename
+ end
+
+ def destroy_file
+ @version.attachments.find(params[:attachment_id]).destroy
+ redirect_to :controller => 'projects', :action => 'list_files', :id => @project
+ end
+
+private
+ def find_project
+ @version = Version.find(params[:id])
+ @project = @version.project
+ end
+end
diff --git a/redmine/app/controllers/welcome_controller.rb b/redmine/app/controllers/welcome_controller.rb
new file mode 100644
index 000000000..b266975aa
--- /dev/null
+++ b/redmine/app/controllers/welcome_controller.rb
@@ -0,0 +1,26 @@
+# 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
+ @projects = Project.latest
+ end
+
+end
diff --git a/redmine/app/helpers/account_helper.rb b/redmine/app/helpers/account_helper.rb
new file mode 100644
index 000000000..e18ab6ff4
--- /dev/null
+++ b/redmine/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/redmine/app/helpers/admin_helper.rb b/redmine/app/helpers/admin_helper.rb
new file mode 100644
index 000000000..db2777392
--- /dev/null
+++ b/redmine/app/helpers/admin_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 AdminHelper
+end
diff --git a/redmine/app/helpers/application_helper.rb b/redmine/app/helpers/application_helper.rb
new file mode 100644
index 000000000..4a50b949a
--- /dev/null
+++ b/redmine/app/helpers/application_helper.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.
+
+module ApplicationHelper
+
+ def loggedin?
+ session[:user]
+ end
+
+ def admin_loggedin?
+ session[:user] && session[:user].admin
+ end
+
+ def authorize_for(controller, action)
+ # check if action is allowed on public projects
+ if @project.public? and Permission.allowed_to_public "%s/%s" % [ controller, action ]
+ return true
+ end
+ # check if user is authorized
+ if session[:user] and (session[:user].admin? or Permission.allowed_to_role( "%s/%s" % [ controller, action ], session[:user].role_for_project(@project.id) ) )
+ return true
+ end
+ return false
+ end
+
+ 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], options[:action])
+ end
+
+ # Display a link to user's account page
+ def link_to_user(user)
+ link_to user.display_name, :controller => 'account', :action => 'show', :id => user
+ end
+
+ def format_date(date)
+ _('(date)', date) if date
+ end
+
+ def format_time(time)
+ _('(time)', time) if time
+ end
+
+ def pagination_links_full(paginator, options={}, html_options={})
+ html =''
+ html << link_to(('&#171; ' + _('Previous') ), { :page => paginator.current.previous }) + ' ' if paginator.current.previous
+ html << (pagination_links(paginator, options, html_options) || '')
+ html << ' ' + link_to((_('Next') + ' &#187;'), { :page => paginator.current.next }) if paginator.current.next
+ html
+ end
+
+end
diff --git a/redmine/app/helpers/custom_fields_helper.rb b/redmine/app/helpers/custom_fields_helper.rb
new file mode 100644
index 000000000..4e3aea50f
--- /dev/null
+++ b/redmine/app/helpers/custom_fields_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 CustomFieldsHelper
+ def custom_field_tag(custom_value)
+
+ custom_field = custom_value.custom_field
+
+ field_name = "custom_fields[#{custom_field.id}]"
+
+ case custom_field.typ
+ when 0 .. 2
+ text_field_tag field_name, custom_value.value
+ when 3
+ check_box field_name
+ when 4
+ select_tag field_name,
+ options_for_select(custom_field.possible_values.split('|'),
+ custom_value.value)
+ end
+ end
+end
diff --git a/redmine/app/helpers/documents_helper.rb b/redmine/app/helpers/documents_helper.rb
new file mode 100644
index 000000000..c9897647d
--- /dev/null
+++ b/redmine/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/redmine/app/helpers/enumerations_helper.rb b/redmine/app/helpers/enumerations_helper.rb
new file mode 100644
index 000000000..11a216a82
--- /dev/null
+++ b/redmine/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/redmine/app/helpers/help_helper.rb b/redmine/app/helpers/help_helper.rb
new file mode 100644
index 000000000..bb629316c
--- /dev/null
+++ b/redmine/app/helpers/help_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 HelpHelper
+end
diff --git a/redmine/app/helpers/issue_categories_helper.rb b/redmine/app/helpers/issue_categories_helper.rb
new file mode 100644
index 000000000..997d830d7
--- /dev/null
+++ b/redmine/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/redmine/app/helpers/issue_statuses_helper.rb b/redmine/app/helpers/issue_statuses_helper.rb
new file mode 100644
index 000000000..17704b7ba
--- /dev/null
+++ b/redmine/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/redmine/app/helpers/issues_helper.rb b/redmine/app/helpers/issues_helper.rb
new file mode 100644
index 000000000..40c0e40ae
--- /dev/null
+++ b/redmine/app/helpers/issues_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 IssuesHelper
+end
diff --git a/redmine/app/helpers/members_helper.rb b/redmine/app/helpers/members_helper.rb
new file mode 100644
index 000000000..8bf90913d
--- /dev/null
+++ b/redmine/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/redmine/app/helpers/news_helper.rb b/redmine/app/helpers/news_helper.rb
new file mode 100644
index 000000000..f4a633f4b
--- /dev/null
+++ b/redmine/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/redmine/app/helpers/projects_helper.rb b/redmine/app/helpers/projects_helper.rb
new file mode 100644
index 000000000..0c85ce24c
--- /dev/null
+++ b/redmine/app/helpers/projects_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 ProjectsHelper
+end
diff --git a/redmine/app/helpers/reports_helper.rb b/redmine/app/helpers/reports_helper.rb
new file mode 100644
index 000000000..ed7fd7884
--- /dev/null
+++ b/redmine/app/helpers/reports_helper.rb
@@ -0,0 +1,32 @@
+# 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
+ } unless criteria.nil?
+ a = a + row["total"].to_i if match == 1
+ } unless data.nil?
+ a
+ end
+
+end
diff --git a/redmine/app/helpers/roles_helper.rb b/redmine/app/helpers/roles_helper.rb
new file mode 100644
index 000000000..8ae339053
--- /dev/null
+++ b/redmine/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/redmine/app/helpers/search_filter_helper.rb b/redmine/app/helpers/search_filter_helper.rb
new file mode 100644
index 000000000..3a76b3f57
--- /dev/null
+++ b/redmine/app/helpers/search_filter_helper.rb
@@ -0,0 +1,55 @@
+# 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 SearchFilterHelper
+
+ def search_filter_criteria(field, options = {})
+ session[:search_filter] ||= {}
+ session[:search_filter][field] ||= options
+ # session[:search_filter][field][:values] = options[:values] unless options[:values].nil?
+ # session[:search_filter][field][:label] = options[:label] unless options[:label].nil?
+ end
+
+ def search_filter_update
+ session[:search_filter].each_key {|field| session[:search_filter][field][:value] = params[field] }
+ #@search_filter[:value] = params[@search_filter[:field]]
+ end
+
+ def search_filter_clause
+ clause = "1=1"
+ session[:search_filter].each {|field, criteria| clause = clause + " AND " + field + "='" + session[:search_filter][field][:value] + "'" unless session[:search_filter][field][:value].nil? || session[:search_filter][field][:value].empty? }
+ clause
+ #@search_filter[:field] + "='" + @search_filter[:value] + "'" unless @search_filter[:value].nil? || @search_filter[:value].empty?
+ end
+
+ def search_filter_tag(field)
+ option_values = []
+ #values = eval @search_filter[:values_expr]
+ option_values = eval session[:search_filter][field][:values]
+
+ content_tag("select",
+ content_tag("option", "[All]", :value => "") +
+ options_from_collection_for_select(option_values,
+ "id",
+ session[:search_filter][field][:label] || "name",
+ session[:search_filter][field][:value].to_i
+ ),
+ :name => field
+ )
+ end
+
+end \ No newline at end of file
diff --git a/redmine/app/helpers/sort_helper.rb b/redmine/app/helpers/sort_helper.rb
new file mode 100644
index 000000000..bec2117ec
--- /dev/null
+++ b/redmine/app/helpers/sort_helper.rb
@@ -0,0 +1,157 @@
+# Helpers to sort tables using clickable column headers.
+#
+# Author: Stuart Rackham <srackham@methods.co.nz>, 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):
+#
+# <thead>
+# <tr>
+# <%= 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) %>
+# </tr>
+# </thead>
+#
+# - 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 '<controller_name>_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'
+ order = 'desc'
+ else
+ icon = 'sort_desc'
+ order = 'asc'
+ end
+ else
+ icon = nil
+ order = 'desc' # changed for desc order by default
+ end
+ caption = titleize(Inflector::humanize(column)) unless caption
+ params = {:params => {:sort_key => column, :sort_order => order}}
+ link_to(caption, params) + (icon ? nbsp(2) + image_tag(icon) : '')
+ end
+
+ # Returns a table header <th> 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:
+ #
+ # <th title="Sort by contact ID" width="40">
+ # <a href="/contact/list?sort_order=desc&amp;sort_key=id">Id</a>
+ # &nbsp;&nbsp;<img alt="Sort_asc" src="/images/sort_asc.png" />
+ # </th>
+ #
+ def sort_header_tag(column, options = {})
+ if options[:caption]
+ caption = options[:caption]
+ options.delete(:caption)
+ else
+ caption = titleize(Inflector::humanize(column))
+ end
+ options[:title]= "Sort by #{caption}" unless options[:title]
+ content_tag('th', sort_link(column, caption), options)
+ end
+
+ private
+
+ # Return n non-breaking spaces.
+ def nbsp(n)
+ '&nbsp;' * n
+ end
+
+ # Return capitalized title.
+ def titleize(title)
+ title.split.map {|w| w.capitalize }.join(' ')
+ end
+
+end
diff --git a/redmine/app/helpers/trackers_helper.rb b/redmine/app/helpers/trackers_helper.rb
new file mode 100644
index 000000000..839327efe
--- /dev/null
+++ b/redmine/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/redmine/app/helpers/users_helper.rb b/redmine/app/helpers/users_helper.rb
new file mode 100644
index 000000000..035db3d00
--- /dev/null
+++ b/redmine/app/helpers/users_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 UsersHelper
+end
diff --git a/redmine/app/helpers/versions_helper.rb b/redmine/app/helpers/versions_helper.rb
new file mode 100644
index 000000000..e2724fe84
--- /dev/null
+++ b/redmine/app/helpers/versions_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 VersionsHelper
+end
diff --git a/redmine/app/helpers/welcome_helper.rb b/redmine/app/helpers/welcome_helper.rb
new file mode 100644
index 000000000..cace5f542
--- /dev/null
+++ b/redmine/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/redmine/app/models/attachment.rb b/redmine/app/models/attachment.rb
new file mode 100644
index 000000000..bc1ff5d87
--- /dev/null
+++ b/redmine/app/models/attachment.rb
@@ -0,0 +1,81 @@
+# 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 "digest/md5"
+
+class Attachment < ActiveRecord::Base
+ belongs_to :container, :polymorphic => true
+ belongs_to :author, :class_name => "User", :foreign_key => "author_id"
+
+ validates_presence_of :filename
+
+ 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
+ self.size = @temp_file.size
+ end
+ end
+ 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
+ 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
+ "#{RDM_STORAGE_PATH}/#{self.disk_filename}"
+ end
+
+ def increment_download
+ increment!(:downloads)
+ end
+
+ # returns last created projects
+ def self.most_downloaded
+ find(:all, :limit => 5, :order => "downloads DESC")
+ 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/redmine/app/models/custom_field.rb b/redmine/app/models/custom_field.rb
new file mode 100644
index 000000000..9e817d1ef
--- /dev/null
+++ b/redmine/app/models/custom_field.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 CustomField < ActiveRecord::Base
+
+ has_and_belongs_to_many :projects
+ has_many :custom_values, :dependent => true
+ has_many :issues, :through => :issue_custom_values
+
+ validates_presence_of :name, :typ
+ validates_uniqueness_of :name
+
+ TYPES = [
+ [ "Integer", 0 ],
+ [ "String", 1 ],
+ [ "Date", 2 ],
+ [ "Boolean", 3 ],
+ [ "List", 4 ]
+ ].freeze
+
+ def self.for_all
+ find(:all, :conditions => ["is_for_all=?", true])
+ end
+end
diff --git a/redmine/app/models/custom_value.rb b/redmine/app/models/custom_value.rb
new file mode 100644
index 000000000..faaa8ff82
--- /dev/null
+++ b/redmine/app/models/custom_value.rb
@@ -0,0 +1,41 @@
+# 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 :issue
+ belongs_to :custom_field
+
+protected
+ def validate
+ errors.add(custom_field.name, "can't be blank") if custom_field.is_required? and value.empty?
+ errors.add(custom_field.name, "is not valid") unless custom_field.regexp.empty? or value =~ Regexp.new(custom_field.regexp)
+
+ case custom_field.typ
+ when 0
+ errors.add(custom_field.name, "must be an integer") unless value =~ /^[0-9]*$/
+ when 1
+ errors.add(custom_field.name, "is too short") if custom_field.min_length > 0 and value.length < custom_field.min_length and value.length > 0
+ errors.add(custom_field.name, "is too long") if custom_field.max_length > 0 and value.length > custom_field.max_length
+ when 2
+ errors.add(custom_field.name, "must be a valid date") unless value =~ /^(\d+)\/(\d+)\/(\d+)$/ or value.empty?
+ when 3
+
+ when 4
+ errors.add(custom_field.name, "is not a valid value") unless custom_field.possible_values.split('|').include? value or value.empty?
+ end
+ end
+end
diff --git a/redmine/app/models/document.rb b/redmine/app/models/document.rb
new file mode 100644
index 000000000..40c3a1656
--- /dev/null
+++ b/redmine/app/models/document.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 Document < ActiveRecord::Base
+ belongs_to :project
+ belongs_to :category, :class_name => "Enumeration", :foreign_key => "category_id"
+ has_many :attachments, :as => :container, :dependent => true
+
+ validates_presence_of :title
+end
diff --git a/redmine/app/models/enumeration.rb b/redmine/app/models/enumeration.rb
new file mode 100644
index 000000000..d93db4445
--- /dev/null
+++ b/redmine/app/models/enumeration.rb
@@ -0,0 +1,45 @@
+# 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
+ before_destroy :check_integrity
+
+ validates_presence_of :opt, :name
+
+ OPTIONS = [
+ ["Issue priorities", "IPRI"],
+ ["Document categories", "DCAT"]
+ ].freeze
+
+ def self.get_values(option)
+ find(:all, :conditions => ['opt=?', option])
+ end
+
+ def name
+ _ self.attributes['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])
+ end
+ end
+end
diff --git a/redmine/app/models/issue.rb b/redmine/app/models/issue.rb
new file mode 100644
index 000000000..4a21ac03b
--- /dev/null
+++ b/redmine/app/models/issue.rb
@@ -0,0 +1,55 @@
+# 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 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 :histories, :class_name => 'IssueHistory', :dependent => true, :order => "issue_histories.created_on DESC", :include => :status
+ has_many :attachments, :as => :container, :dependent => true
+
+ has_many :custom_values, :dependent => true
+ has_many :custom_fields, :through => :custom_values
+
+ validates_presence_of :subject, :descr, :priority, :tracker, :author
+
+ # set default status for new issues
+ def before_create
+ self.status = IssueStatus.default
+ build_history
+ end
+
+ def long_id
+ "%05d" % self.id
+ end
+
+private
+ # Creates an history for the issue
+ def build_history
+ @history = self.histories.build
+ @history.status = self.status
+ @history.author = self.author
+ end
+
+end
diff --git a/redmine/app/models/issue_category.rb b/redmine/app/models/issue_category.rb
new file mode 100644
index 000000000..b7d4e2623
--- /dev/null
+++ b/redmine/app/models/issue_category.rb
@@ -0,0 +1,28 @@
+# 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
+ before_destroy :check_integrity
+ belongs_to :project
+
+ validates_presence_of :name
+
+private
+ def check_integrity
+ raise "Can't delete category" if Issue.find(:first, :conditions => ["category_id=?", self.id])
+ end
+end
diff --git a/redmine/app/models/issue_history.rb b/redmine/app/models/issue_history.rb
new file mode 100644
index 000000000..f410a39c1
--- /dev/null
+++ b/redmine/app/models/issue_history.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 IssueHistory < ActiveRecord::Base
+ belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
+ belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
+
+ validates_presence_of :status
+end
diff --git a/redmine/app/models/issue_status.rb b/redmine/app/models/issue_status.rb
new file mode 100644
index 000000000..ff34cb666
--- /dev/null
+++ b/redmine/app/models/issue_status.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 IssueStatus < ActiveRecord::Base
+ before_destroy :check_integrity
+ has_many :workflows, :foreign_key => "old_status_id"
+
+ validates_presence_of :name
+ validates_uniqueness_of :name
+
+ # 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
+ def new_statuses_allowed_to(role, tracker)
+ statuses = []
+ for workflow in self.workflows.find(:all, :include => :new_status)
+ statuses << workflow.new_status if workflow.role_id == role.id and workflow.tracker_id == tracker.id
+ end unless role.nil?
+ statuses
+ end
+
+ def name
+ _ self.attributes['name']
+ end
+
+private
+ def check_integrity
+ raise "Can't delete status" if Issue.find(:first, :conditions => ["status_id=?", self.id]) or IssueHistory.find(:first, :conditions => ["status_id=?", self.id])
+ end
+end
diff --git a/redmine/app/models/mailer.rb b/redmine/app/models/mailer.rb
new file mode 100644
index 000000000..b04ec7ebc
--- /dev/null
+++ b/redmine/app/models/mailer.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.
+
+class Mailer < ActionMailer::Base
+
+ def issue_change_status(issue)
+ # Sends to all project members
+ @recipients = issue.project.members.collect { |m| m.user.mail if m.user.mail_notification }
+ @from = 'redmine@somenet.foo'
+ @subject = "Issue ##{issue.id} has been updated"
+ @body['issue'] = issue
+ end
+
+ def issue_add(issue)
+ # Sends to all project members
+ @recipients = issue.project.members.collect { |m| m.user.mail if m.user.mail_notification }
+ @from = 'redmine@somenet.foo'
+ @subject = "Issue ##{issue.id} has been reported"
+ @body['issue'] = issue
+ end
+
+end
diff --git a/redmine/app/models/member.rb b/redmine/app/models/member.rb
new file mode 100644
index 000000000..d37936561
--- /dev/null
+++ b/redmine/app/models/member.rb
@@ -0,0 +1,29 @@
+# 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 name
+ self.user.display_name
+ end
+end
diff --git a/redmine/app/models/news.rb b/redmine/app/models/news.rb
new file mode 100644
index 000000000..0642a4bf5
--- /dev/null
+++ b/redmine/app/models/news.rb
@@ -0,0 +1,28 @@
+# 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'
+
+ validates_presence_of :title, :shortdescr, :descr
+
+ # returns last created news
+ def self.latest
+ find(:all, :limit => 5, :include => [ :author, :project ], :order => "news.created_on DESC")
+ end
+end
diff --git a/redmine/app/models/permission.rb b/redmine/app/models/permission.rb
new file mode 100644
index 000000000..f66214119
--- /dev/null
+++ b/redmine/app/models/permission.rb
@@ -0,0 +1,63 @@
+# 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 Permission < ActiveRecord::Base
+ has_and_belongs_to_many :roles
+
+ validates_presence_of :controller, :action, :descr
+
+ GROUPS = {
+ 100 => "Project",
+ 200 => "Membres",
+ 300 => "Versions",
+ 400 => "Issue categories",
+ 1000 => "Issues",
+ 1100 => "News",
+ 1200 => "Documents",
+ 1300 => "Files",
+ }.freeze
+
+ @@cached_perms_for_public = nil
+ @@cached_perms_for_roles = nil
+
+ def name
+ self.controller + "/" + self.action
+ end
+
+ def group_id
+ (self.sort / 100)*100
+ end
+
+ def self.allowed_to_public(action)
+ @@cached_perms_for_public ||= find(:all, :conditions => ["public=?", true]).collect {|p| "#{p.controller}/#{p.action}"}
+ @@cached_perms_for_public.include? action
+ end
+
+ def self.allowed_to_role(action, role)
+ @@cached_perms_for_roles ||=
+ begin
+ perms = {}
+ find(:all, :include => :roles).each {|p| perms.store "#{p.controller}/#{p.action}", p.roles.collect {|r| r.id } }
+ perms
+ end
+ @@cached_perms_for_roles[action] and @@cached_perms_for_roles[action].include? role
+ end
+
+ def self.allowed_to_role_expired
+ @@cached_perms_for_roles = nil
+ end
+end
diff --git a/redmine/app/models/project.rb b/redmine/app/models/project.rb
new file mode 100644
index 000000000..7c50ee8cb
--- /dev/null
+++ b/redmine/app/models/project.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 Project < ActiveRecord::Base
+ has_many :versions, :dependent => true, :order => "versions.date DESC"
+ has_many :members, :dependent => true
+ has_many :issues, :dependent => true, :order => "issues.created_on DESC", :include => :status
+ has_many :documents, :dependent => true
+ has_many :news, :dependent => true, :order => "news.created_on DESC", :include => :author
+ has_many :issue_categories, :dependent => true
+ has_and_belongs_to_many :custom_fields
+
+ validates_presence_of :name, :descr
+
+ # returns 5 last created projects
+ def self.latest
+ find(:all, :limit => 5, :order => "created_on DESC")
+ end
+
+ # Returns current version of the project
+ def current_version
+ versions.find(:first, :conditions => [ "date <= ?", Date.today ], :order => "date DESC, id DESC")
+ 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
+ (CustomField.for_all + custom_fields).uniq
+ end
+end
diff --git a/redmine/app/models/role.rb b/redmine/app/models/role.rb
new file mode 100644
index 000000000..ce880b5cc
--- /dev/null
+++ b/redmine/app/models/role.rb
@@ -0,0 +1,31 @@
+# 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
+ before_destroy :check_integrity
+ has_and_belongs_to_many :permissions
+ has_many :workflows, :dependent => true
+ has_many :members
+
+ validates_presence_of :name
+ validates_uniqueness_of :name
+
+private
+ def check_integrity
+ raise "Can't delete role" if Member.find(:first, :conditions =>["role_id=?", self.id])
+ end
+end
diff --git a/redmine/app/models/tracker.rb b/redmine/app/models/tracker.rb
new file mode 100644
index 000000000..6b123d79c
--- /dev/null
+++ b/redmine/app/models/tracker.rb
@@ -0,0 +1,31 @@
+# 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 => true
+
+ def name
+ _ self.attributes['name']
+ end
+
+private
+ def check_integrity
+ raise "Can't delete tracker" if Issue.find(:first, :conditions => ["tracker_id=?", self.id])
+ end
+end
diff --git a/redmine/app/models/user.rb b/redmine/app/models/user.rb
new file mode 100644
index 000000000..1bc1b5802
--- /dev/null
+++ b/redmine/app/models/user.rb
@@ -0,0 +1,89 @@
+# 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 "digest/sha1"
+
+class User < ActiveRecord::Base
+ has_many :memberships, :class_name => 'Member', :include => [ :project, :role ], :dependent => true
+
+ attr_accessor :password
+ attr_accessor :last_before_login_on
+ # Prevents unauthorized assignments
+ attr_protected :admin
+
+ validates_presence_of :login, :firstname, :lastname, :mail
+ validates_uniqueness_of :login, :mail
+
+ # Login must contain lettres, numbers, underscores only
+ validates_format_of :login, :with => /^[a-z0-9_]+$/i
+ validates_format_of :mail, :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i
+
+ def before_create
+ self.hashed_password = User.hash_password(self.password)
+ end
+
+ def after_create
+ @password = nil
+ end
+
+ # Returns the user that matches user's login and password
+ def try_to_login
+ @user = User.login(self.login, self.password)
+ unless @user.nil?
+ @user.last_before_login_on = @user.last_login_on
+ @user.update_attribute(:last_login_on, DateTime.now)
+ end
+ @user
+ end
+
+ # Return user's full name for display
+ def display_name
+ firstname + " " + lastname #+ (self.admin ? " (Admin)" : "" )
+ end
+
+ # Returns the user that matches the given login and password
+ def self.login(login, password)
+ hashed_password = hash_password(password || "")
+ find(:first,
+ :conditions => ["login = ? and hashed_password = ? and locked = ?", login, hashed_password, false])
+ end
+
+ def check_password?(clear_password)
+ User.hash_password(clear_password) == self.hashed_password
+ end
+
+ def change_password(current_password, new_password)
+ self.hashed_password = User.hash_password(new_password)
+ save
+ end
+
+ def role_for_project(project_id)
+ @role_for_projects ||=
+ begin
+ roles = {}
+ self.memberships.each { |m| roles.store m.project_id, m.role_id }
+ roles
+ end
+ @role_for_projects[project_id]
+ end
+
+private
+ # Return password digest
+ def self.hash_password(clear_password)
+ Digest::SHA1.hexdigest(clear_password)
+ end
+end
diff --git a/redmine/app/models/version.rb b/redmine/app/models/version.rb
new file mode 100644
index 000000000..02dee15c8
--- /dev/null
+++ b/redmine/app/models/version.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 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 => true
+
+ validates_presence_of :name, :descr
+
+private
+ def check_integrity
+ raise "Can't delete version" if self.fixed_issues.find(:first)
+ end
+end
diff --git a/redmine/app/models/workflow.rb b/redmine/app/models/workflow.rb
new file mode 100644
index 000000000..212e33350
--- /dev/null
+++ b/redmine/app/models/workflow.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 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/redmine/app/views/account/login.rhtml b/redmine/app/views/account/login.rhtml
new file mode 100644
index 000000000..cc360ebac
--- /dev/null
+++ b/redmine/app/views/account/login.rhtml
@@ -0,0 +1,13 @@
+<div class="box">
+<h2><%=_ 'Please login' %></h2>
+
+<%= start_form_tag :action=> "login" %>
+<p><label for="user_login"><%=_ 'Login' %>:</label><br/>
+<input type="text" name="user[login]" id="user_login" size="30" /></p>
+
+<p><label for="user_password"><%=_ 'Password' %>:</label><br/>
+<input type="password" name="user[password]" id="user_password" size="30"/></p>
+
+<p><input type="submit" name="login" value="<%=_ 'Log in' %> &#187;" class="primary" /></p>
+<%= end_form_tag %>
+</div> \ No newline at end of file
diff --git a/redmine/app/views/account/my_account.rhtml b/redmine/app/views/account/my_account.rhtml
new file mode 100644
index 000000000..34ae4885f
--- /dev/null
+++ b/redmine/app/views/account/my_account.rhtml
@@ -0,0 +1,54 @@
+<h2><%=_('My account')%></h2>
+
+<p><%=_('Login')%>: <strong><%= @user.login %></strong><br />
+<%=_('Created on')%>: <%= format_time(@user.created_on) %>,
+<%=_('Last update')%>: <%= format_time(@user.updated_on) %></p>
+
+<div class="splitcontentleft">
+ <div class="box">
+ <h3><%=_('Information')%></h3>
+ &nbsp;
+ <%= start_form_tag :action => 'my_account' %>
+ <%= error_messages_for 'user' %>
+
+ <!--[form:user]-->
+ <p><label for="user_firstname"><%=_('Firstname')%> <span class="required">*</span></label><br/>
+ <%= text_field 'user', 'firstname' %></p>
+
+ <p><label for="user_lastname"><%=_('Lastname')%> <span class="required">*</span></label><br/>
+ <%= text_field 'user', 'lastname' %></p>
+
+ <p><label for="user_mail"><%=_('Mail')%> <span class="required">*</span></label><br/>
+ <%= text_field 'user', 'mail' %></p>
+
+ <p><label for="user_language"><%=_('Language')%></label><br/>
+ <%= select("user", "language", Localization.langs) %></p>
+ <!--[eoform:user]-->
+
+ <p><%= check_box 'user', 'mail_notification' %> <label for="user_mail_notification"><%=_('Mail notifications')%></label></p>
+
+ <center><%= submit_tag _('Save') %></center>
+ <%= end_form_tag %>
+ </div>
+</div>
+
+
+<div class="splitcontentright">
+ <div class="box">
+ <h3><%=_('Password')%></h3>
+ &nbsp;
+ <%= start_form_tag :action => 'change_password' %>
+
+ <p><label for="old_password"><%=_('Password')%> <span class="required">*</span></label><br/>
+ <%= password_field_tag 'old_password' %></p>
+
+ <p><label for="new_password"><%=_('New password')%> <span class="required">*</span></label><br/>
+ <%= password_field_tag 'new_password' %></p>
+
+ <p><label for="new_password_confirmation"><%=_('Confirmation')%> <span class="required">*</span></label><br/>
+ <%= password_field_tag 'new_password_confirmation' %></p>
+
+ <center><%= submit_tag _('Save') %></center>
+ <%= end_form_tag %>
+ </div>
+</div> \ No newline at end of file
diff --git a/redmine/app/views/account/my_page.rhtml b/redmine/app/views/account/my_page.rhtml
new file mode 100644
index 000000000..7f6458262
--- /dev/null
+++ b/redmine/app/views/account/my_page.rhtml
@@ -0,0 +1,19 @@
+<h2><%=_('My page') %></h2>
+
+<p>
+<%=_('Welcome')%> <b><%= @user.firstname %></b><br />
+<% unless @user.last_before_login_on.nil? %>
+ <%=_('Last login')%>: <%= format_time(@user.last_before_login_on) %>
+<% end %>
+</p>
+
+<div class="splitcontentleft">
+ <h3><%=_('Reported issues')%></h3>
+ <%= render :partial => 'issues/list_simple', :locals => { :issues => @reported_issues } %>
+ <%= "<p>(Last #{@reported_issues.length} updated)</p>" if @reported_issues.length > 0 %>
+</div>
+<div class="splitcontentright">
+<h3><%=_('Assigned to me')%></h3>
+ <%= render :partial => 'issues/list_simple', :locals => { :issues => @assigned_issues } %>
+ <%= "<p>(Last #{@assigned_issues.length} updated)</p>" if @assigned_issues.length > 0 %>
+</div> \ No newline at end of file
diff --git a/redmine/app/views/account/show.rhtml b/redmine/app/views/account/show.rhtml
new file mode 100644
index 000000000..df918e5bf
--- /dev/null
+++ b/redmine/app/views/account/show.rhtml
@@ -0,0 +1,19 @@
+<h2><%= @user.display_name %></h2>
+
+<p>
+<%= mail_to @user.mail %><br />
+<%=_('Registered on')%>: <%= format_date(@user.created_on) %>
+</p>
+
+<h3><%=_('Projects')%></h3>
+<p>
+<% for membership in @user.memberships %>
+ <%= membership.project.name %> (<%= membership.role.name %>, <%= format_date(membership.created_on) %>)
+ <br />
+<% end %>
+</p>
+
+<h3><%=_('Activity')%></h3>
+<p>
+<%=_('Reported issues')%>: <%= Issue.count( [ "author_id=?", @user.id]) %>
+</p> \ No newline at end of file
diff --git a/redmine/app/views/admin/index.rhtml b/redmine/app/views/admin/index.rhtml
new file mode 100644
index 000000000..b3607d813
--- /dev/null
+++ b/redmine/app/views/admin/index.rhtml
@@ -0,0 +1,45 @@
+<h2><%=_('Administration')%></h2>
+
+<p>
+<%= image_tag "projects" %>
+<%= link_to _('Projects'), :controller => 'admin', :action => 'projects' %> |
+<%= link_to _('New'), :controller => 'projects', :action => 'add' %>
+</p>
+
+<p>
+<%= image_tag "users" %>
+<%= link_to _('Users'), :controller => 'users' %> |
+<%= link_to _('New'), :controller => 'users', :action => 'add' %>
+</p>
+
+<p>
+<%= image_tag "role" %>
+<%= link_to _('Roles and permissions'), :controller => 'roles' %>
+</p>
+
+<p>
+<%= image_tag "tracker" %>
+<%= link_to _('Trackers'), :controller => 'trackers' %> |
+<%= link_to _('Custom fields'), :controller => 'custom_fields' %>
+</p>
+
+<p>
+<%= image_tag "workflow" %>
+<%= link_to _('Issue Statuses'), :controller => 'issue_statuses' %> |
+<%= link_to _('Workflow'), :controller => 'roles', :action => 'workflow' %>
+</p>
+
+<p>
+<%= image_tag "options" %>
+<%= link_to _('Enumerations'), :controller => 'enumerations' %>
+</p>
+
+<p>
+<%= image_tag "mailer" %>
+<%= link_to _('Mail notifications'), :controller => 'admin', :action => 'mail_options' %>
+</p>
+
+<p>
+<%= image_tag "help" %>
+<%= link_to _('Information'), :controller => 'admin', :action => 'info' %>
+</p> \ No newline at end of file
diff --git a/redmine/app/views/admin/info.rhtml b/redmine/app/views/admin/info.rhtml
new file mode 100644
index 000000000..c73f59c25
--- /dev/null
+++ b/redmine/app/views/admin/info.rhtml
@@ -0,0 +1,4 @@
+<h2><%=_('Information')%></h2>
+
+<%=_('Version')%>: <%= RDM_APP_NAME %> <%= RDM_APP_VERSION %><br />
+<%=_('Database')%>: <%= @adapter_name %> \ No newline at end of file
diff --git a/redmine/app/views/admin/mail_options.rhtml b/redmine/app/views/admin/mail_options.rhtml
new file mode 100644
index 000000000..2d1d80ebb
--- /dev/null
+++ b/redmine/app/views/admin/mail_options.rhtml
@@ -0,0 +1,16 @@
+<h2><%=_('Mail notifications')%></h2>
+
+<p><%=_('Select actions for which mail notification should be enabled.')%></p>
+
+<%= start_form_tag ({}, :id => 'mail_options_form')%>
+<% for action in @actions %>
+ <%= check_box_tag "action_ids[]", action.id, action.mail_enabled? %>
+ <%= action.descr %><br />
+<% end %>
+<br />
+<p>
+<a href="javascript:checkAll('mail_options_form', true)"><%=_('Check all')%></a> |
+<a href="javascript:checkAll('mail_options_form', false)"><%=_('Uncheck all')%></a>
+</p>
+<%= submit_tag _('Save') %>
+<%= end_form_tag %> \ No newline at end of file
diff --git a/redmine/app/views/admin/projects.rhtml b/redmine/app/views/admin/projects.rhtml
new file mode 100644
index 000000000..dd3953518
--- /dev/null
+++ b/redmine/app/views/admin/projects.rhtml
@@ -0,0 +1,35 @@
+<h2><%=_('Projects')%></h2>
+
+<table width="100%" cellspacing="1" cellpadding="2" class="listTableContent">
+ <tr class="ListHead">
+ <%= sort_header_tag('projects.name', :caption => _('Project')) %>
+ <th><%=_('Description')%></th>
+ <th><%=_('Public')%></th>
+ <%= sort_header_tag('projects.created_on', :caption => _('Created on')) %>
+ <th></th>
+ </tr>
+
+<% odd_or_even = 1
+ for project in @projects
+ odd_or_even = 1 - odd_or_even %>
+ <tr class="ListLine<%= odd_or_even %>">
+ <td><%= link_to project.name, :controller => 'projects', :action => 'settings', :id => project %>
+ <td><%= project.descr %>
+ <td align="center"><%= image_tag 'true' if project.public? %>
+ <td align="center"><%= format_date(project.created_on) %>
+ <td align="center">
+ <%= start_form_tag({:controller => 'projects', :action => 'destroy', :id => project}) %>
+ <%= submit_tag _('Delete'), :class => "button-small" %>
+ <%= end_form_tag %>
+ </td>
+ </tr>
+<% end %>
+</table>
+
+<%= link_to ('&#171; ' + _('Previous')), { :page => @project_pages.current.previous } if @project_pages.current.previous %>
+<%= pagination_links(@project_pages) %>
+<%= link_to (_('Next') + ' &#187;'), { :page => @project_pages.current.next } if @project_pages.current.next %>
+
+<br />
+
+<%= link_to ('&#187; ' + _('New project')), :controller => 'projects', :action => 'add' %> \ No newline at end of file
diff --git a/redmine/app/views/custom_fields/_form.rhtml b/redmine/app/views/custom_fields/_form.rhtml
new file mode 100644
index 000000000..d268461fc
--- /dev/null
+++ b/redmine/app/views/custom_fields/_form.rhtml
@@ -0,0 +1,26 @@
+<%= error_messages_for 'custom_field' %>
+
+<!--[form:custom_field]-->
+<p><label for="custom_field_name"><%=_('Name')%></label><br/>
+<%= text_field 'custom_field', 'name' %></p>
+
+<p><label for="custom_field_typ"><%=_('Type')%></label><br/>
+<%= select("custom_field", "typ", CustomField::TYPES) %></p>
+
+<p><%= check_box 'custom_field', 'is_required' %>
+<label for="custom_field_is_required"><%=_('Required')%></label></p>
+
+<p><%= check_box 'custom_field', 'is_for_all' %>
+<label for="custom_field_is_for_all"><%=_('For all projects')%></label></p>
+
+<p><label for="custom_field_min_length"><%=_('Min - max length')%></label> (<%=_('0 means no restriction')%>)<br/>
+<%= text_field 'custom_field', 'min_length', :size => 5 %> -
+<%= text_field 'custom_field', 'max_length', :size => 5 %></p>
+
+<p><label for="custom_field_regexp"><%=_('Regular expression pattern')%></label> (eg. ^[A-Z0-9]+$)<br/>
+<%= text_field 'custom_field', 'regexp', :size => 50 %></p>
+
+<p><label for="custom_field_possible_values"><%=_('Possible values')%></label> (separator: |)<br/>
+<%= text_area 'custom_field', 'possible_values', :rows => 5, :cols => 60 %></p>
+<!--[eoform:custom_field]-->
+
diff --git a/redmine/app/views/custom_fields/edit.rhtml b/redmine/app/views/custom_fields/edit.rhtml
new file mode 100644
index 000000000..ab4ea8b24
--- /dev/null
+++ b/redmine/app/views/custom_fields/edit.rhtml
@@ -0,0 +1,6 @@
+<h2><%=_('Custom field')%></h2>
+
+<%= start_form_tag :action => 'edit', :id => @custom_field %>
+ <%= render :partial => 'form' %>
+ <%= submit_tag _('Save') %>
+<%= end_form_tag %>
diff --git a/redmine/app/views/custom_fields/list.rhtml b/redmine/app/views/custom_fields/list.rhtml
new file mode 100644
index 000000000..0e29a5b37
--- /dev/null
+++ b/redmine/app/views/custom_fields/list.rhtml
@@ -0,0 +1,32 @@
+<h2><%=_('Custom fields')%></h2>
+
+<table border="0" cellspacing="1" cellpadding="2" class="listTableContent">
+<tr class="ListHead">
+ <th><%=_('Name')%></th>
+ <th><%=_('Type')%></th>
+ <th><%=_('Required')%></th>
+ <th><%=_('For all projects')%></th>
+ <th><%=_('Used by')%></th>
+ <th></th>
+</tr>
+<% for custom_field in @custom_fields %>
+ <tr style="background-color:#CEE1ED">
+ <td><%= link_to custom_field.name, :action => 'edit', :id => custom_field %></td>
+ <td align="center"><%= CustomField::TYPES[custom_field.typ][0] %></td>
+ <td align="center"><%= image_tag 'true' if custom_field.is_required? %></td>
+ <td align="center"><%= image_tag 'true' if custom_field.is_for_all? %></td>
+ <td align="center"><%= custom_field.projects.count.to_s + ' ' + _('Project') + '(s)' unless custom_field.is_for_all? %></td>
+ <td align="center">
+ <%= start_form_tag :action => 'destroy', :id => custom_field %>
+ <%= submit_tag _('Delete'), :class => "button-small" %>
+ <%= end_form_tag %> </td>
+ </tr>
+<% end %>
+</table>
+
+<%= link_to ('&#171; ' + _('Previous')), { :page => @custom_field_pages.current.previous } if @custom_field_pages.current.previous %>
+<%= link_to (_('Next') + ' &#187;'), { :page => @custom_field_pages.current.next } if @custom_field_pages.current.next %>
+
+<br />
+
+<%= link_to ('&#187; ' + _('New custom field')), :action => 'new' %>
diff --git a/redmine/app/views/custom_fields/new.rhtml b/redmine/app/views/custom_fields/new.rhtml
new file mode 100644
index 000000000..0e6492a28
--- /dev/null
+++ b/redmine/app/views/custom_fields/new.rhtml
@@ -0,0 +1,7 @@
+<h2><%=_('New custom field')%></h2>
+
+<%= start_form_tag :action => 'new' %>
+ <%= render :partial => 'form' %>
+ <%= submit_tag _('Create') %>
+<%= end_form_tag %>
+
diff --git a/redmine/app/views/documents/_form.rhtml b/redmine/app/views/documents/_form.rhtml
new file mode 100644
index 000000000..4440a156e
--- /dev/null
+++ b/redmine/app/views/documents/_form.rhtml
@@ -0,0 +1,15 @@
+<%= error_messages_for 'document' %>
+
+<!--[form:document]-->
+<p><label for="document_category_id"><%=_('Category')%></label><br />
+<select name="document[category_id]">
+<%= options_from_collection_for_select @categories, "id", "name",@document.category_id %>
+</select></p>
+
+<p><label for="document_title"><%=_('Title')%> <span class="required">*</span></label><br />
+<%= text_field 'document', 'title', :size => 60 %></p>
+
+<p><label for="document_descr"><%=_('Description')%> <span class="required">*</span></label><br />
+<%= text_area 'document', 'descr', :cols => 60, :rows => 5 %></p>
+<!--[eoform:document]-->
+
diff --git a/redmine/app/views/documents/edit.rhtml b/redmine/app/views/documents/edit.rhtml
new file mode 100644
index 000000000..2f1e9a94c
--- /dev/null
+++ b/redmine/app/views/documents/edit.rhtml
@@ -0,0 +1,8 @@
+<h2><%=_('Document')%></h2>
+
+<%= start_form_tag :action => 'edit', :id => @document %>
+ <%= render :partial => 'form' %>
+ <%= submit_tag _('Save') %>
+<%= end_form_tag %>
+
+
diff --git a/redmine/app/views/documents/show.rhtml b/redmine/app/views/documents/show.rhtml
new file mode 100644
index 000000000..bc9557e55
--- /dev/null
+++ b/redmine/app/views/documents/show.rhtml
@@ -0,0 +1,45 @@
+<h2><%= @document.title %></h2>
+
+<strong><%=_('Description')%>:</strong> <%= @document.descr %><br />
+<strong><%=_('Category')%>:</strong> <%= @document.category.name %><br />
+<br />
+
+<% if authorize_for('documents', 'edit') %>
+ <%= start_form_tag ({ :controller => 'documents', :action => 'edit', :id => @document }, :method => 'get' ) %>
+ <%= submit_tag _('Edit') %>
+ <%= end_form_tag %>
+<% end %>
+
+<% if authorize_for('documents', 'destroy') %>
+ <%= start_form_tag ({ :controller => 'documents', :action => 'destroy', :id => @document } ) %>
+ <%= submit_tag _('Delete') %>
+ <%= end_form_tag %>
+<% end %>
+
+<br /><br />
+
+<table border="0" cellspacing="1" cellpadding="2" width="100%">
+<% for attachment in @document.attachments %>
+ <tr style="background-color:#CEE1ED">
+ <td><%= link_to attachment.filename, :action => 'download', :id => @document, :attachment_id => attachment %></td>
+ <td align="center"><%= format_date(attachment.created_on) %></td>
+ <td align="center"><%= attachment.author.display_name %></td>
+ <td><%= human_size(attachment.size) %><br /><%= attachment.downloads %> <%=_('download')%>(s)</td>
+ <% if authorize_for('documents', 'destroy_attachment') %>
+ <td align="center">
+ <%= start_form_tag :action => 'destroy_attachment', :id => @document, :attachment_id => attachment %>
+ <%= submit_tag _('Delete'), :class => "button-small" %>
+ <%= end_form_tag %>
+ </tr>
+ <% end %>
+<% end %>
+</table>
+<br />
+
+<% if authorize_for('documents', 'add_attachment') %>
+ <%= start_form_tag ({ :controller => 'documents', :action => 'add_attachment', :id => @document }, :multipart => true) %>
+ <%=_('Add file')%><br /><%= file_field 'attachment', 'file' %>
+ <%= submit_tag _('Add') %>
+ <%= end_form_tag %>
+<% end %>
+
diff --git a/redmine/app/views/enumerations/_form.rhtml b/redmine/app/views/enumerations/_form.rhtml
new file mode 100644
index 000000000..d78dc358c
--- /dev/null
+++ b/redmine/app/views/enumerations/_form.rhtml
@@ -0,0 +1,9 @@
+<%= error_messages_for 'enumeration' %>
+
+<!--[form:optvalue]-->
+<%= hidden_field 'enumeration', 'opt' %>
+
+<p><label for="enumeration_name"><%=_('Name')%></label><br/>
+<%= text_field 'enumeration', 'name' %></p>
+<!--[eoform:optvalue]-->
+
diff --git a/redmine/app/views/enumerations/edit.rhtml b/redmine/app/views/enumerations/edit.rhtml
new file mode 100644
index 000000000..16bd377f3
--- /dev/null
+++ b/redmine/app/views/enumerations/edit.rhtml
@@ -0,0 +1,10 @@
+<h2><%=_('Enumerations')%></h2>
+
+<%= start_form_tag :action => 'update', :id => @enumeration %>
+ <%= render :partial => 'form' %>
+ <%= submit_tag _('Save') %>
+<%= end_form_tag %>
+
+<%= start_form_tag :action => 'destroy', :id => @enumeration %>
+ <%= submit_tag _('Delete') %>
+<%= end_form_tag %> \ No newline at end of file
diff --git a/redmine/app/views/enumerations/list.rhtml b/redmine/app/views/enumerations/list.rhtml
new file mode 100644
index 000000000..b5ce65c23
--- /dev/null
+++ b/redmine/app/views/enumerations/list.rhtml
@@ -0,0 +1,22 @@
+<h2><%=_('Enumerations')%></h2>
+
+<% for option in Enumeration::OPTIONS %>
+
+ <% if @params[:opt]==option[1] %>
+
+ <p><%= image_tag 'dir_open' %> <b><%=_ option[0] %></b></p>
+ <ul>
+ <% for value in Enumeration::find(:all, :conditions => [ "opt = ?", option[1]]) %>
+ <li><%= link_to value.name, :action => 'edit', :id => value %></li>
+ <% end %>
+ </ul>
+ <ul>
+ <li><%= link_to ('&#187; ' + _('New')), :action => 'new', :opt => option[1] %></li>
+ </ul>
+
+ <% else %>
+ <p><%= image_tag 'dir' %> <%= link_to _(option[0]), :opt => option[1] %></p>
+ <% end %>
+
+<% end %>
+
diff --git a/redmine/app/views/enumerations/new.rhtml b/redmine/app/views/enumerations/new.rhtml
new file mode 100644
index 000000000..30048f2fd
--- /dev/null
+++ b/redmine/app/views/enumerations/new.rhtml
@@ -0,0 +1,6 @@
+<h2><%=_('New enumeration')%></h2>
+
+<%= start_form_tag :action => 'create' %>
+ <%= render :partial => 'form' %>
+ <%= submit_tag _('Create') %>
+<%= end_form_tag %>
diff --git a/redmine/app/views/issue_categories/_form.rhtml b/redmine/app/views/issue_categories/_form.rhtml
new file mode 100644
index 000000000..eba2f1672
--- /dev/null
+++ b/redmine/app/views/issue_categories/_form.rhtml
@@ -0,0 +1,7 @@
+<%= error_messages_for 'issue_category' %>
+
+<!--[form:issue_category]-->
+<p><label for="issue_category_name"><%=_('Name')%></label><br/>
+<%= text_field 'issue_category', 'name' %></p>
+<!--[eoform:issue_category]-->
+
diff --git a/redmine/app/views/issue_categories/edit.rhtml b/redmine/app/views/issue_categories/edit.rhtml
new file mode 100644
index 000000000..3afdd1c63
--- /dev/null
+++ b/redmine/app/views/issue_categories/edit.rhtml
@@ -0,0 +1,6 @@
+<h2>Editing issue category</h2>
+
+<%= start_form_tag :action => 'edit', :id => @category %>
+ <%= render :partial => 'form' %>
+ <%= submit_tag _('Save') %>
+<%= end_form_tag %>
diff --git a/redmine/app/views/issue_statuses/_form.rhtml b/redmine/app/views/issue_statuses/_form.rhtml
new file mode 100644
index 000000000..5f4b9ce3d
--- /dev/null
+++ b/redmine/app/views/issue_statuses/_form.rhtml
@@ -0,0 +1,17 @@
+<%= error_messages_for 'issue_status' %>
+
+<!--[form:issue_status]-->
+<p><label for="issue_status_name"><%=_('Name')%></label><br/>
+<%= text_field 'issue_status', 'name' %></p>
+
+<p><%= check_box 'issue_status', 'is_closed' %>
+<label for="issue_status_is_closed"><%=_('Issue closed')%></label></p>
+
+<p><%= check_box 'issue_status', 'is_default' %>
+<label for="issue_status_is_default"><%=_('Default status')%></label></p>
+
+<p><label for="issue_status_html_color"><%=_('Color')%></label>
+#<%= text_field 'issue_status', 'html_color', :size => 6 %></p>
+
+<!--[eoform:issue_status]-->
+
diff --git a/redmine/app/views/issue_statuses/edit.rhtml b/redmine/app/views/issue_statuses/edit.rhtml
new file mode 100644
index 000000000..d3ed92201
--- /dev/null
+++ b/redmine/app/views/issue_statuses/edit.rhtml
@@ -0,0 +1,6 @@
+<h2><%=_('Issue status')%></h2>
+
+<%= start_form_tag :action => 'update', :id => @issue_status %>
+ <%= render :partial => 'form' %>
+ <%= submit_tag _('Save') %>
+<%= end_form_tag %>
diff --git a/redmine/app/views/issue_statuses/list.rhtml b/redmine/app/views/issue_statuses/list.rhtml
new file mode 100644
index 000000000..050e08f0f
--- /dev/null
+++ b/redmine/app/views/issue_statuses/list.rhtml
@@ -0,0 +1,30 @@
+<h2><%=_('Issue statuses')%></h2>
+
+<table border="0" cellspacing="1" cellpadding="2" class="listTableContent">
+ <tr class="ListHead">
+ <th><%=_('Status')%></th>
+ <th><%=_('Default status')%></th>
+ <th><%=_('Issue closed')%></th>
+ <th><%=_('Color')%></th>
+ <th></th>
+ </tr>
+
+<% for status in @issue_statuses %>
+ <tr style="background-color:#CEE1ED">
+ <td><%= link_to status.name, :action => 'edit', :id => status %></td>
+ <td align="center"><%= image_tag 'true' if status.is_default %></td>
+ <td align="center"><%= image_tag 'true' if status.is_closed %></td>
+ <td bgcolor="#<%= status.html_color %>">&nbsp</td>
+ <td align="center">
+ <%= start_form_tag :action => 'destroy', :id => status %>
+ <%= submit_tag _('Delete'), :class => "button-small" %>
+ <%= end_form_tag %>
+ </td>
+ </tr>
+<% end %>
+</table>
+
+<%= pagination_links_full @issue_status_pages %>
+<br />
+
+<%= link_to '&#187; ' + _('New issue status'), :action => 'new' %>
diff --git a/redmine/app/views/issue_statuses/new.rhtml b/redmine/app/views/issue_statuses/new.rhtml
new file mode 100644
index 000000000..f7ac082e9
--- /dev/null
+++ b/redmine/app/views/issue_statuses/new.rhtml
@@ -0,0 +1,6 @@
+<h2><%=_('New issue status')%></h2>
+
+<%= start_form_tag :action => 'create' %>
+ <%= render :partial => 'form' %>
+ <%= submit_tag _('Create') %>
+<%= end_form_tag %>
diff --git a/redmine/app/views/issues/_list_simple.rhtml b/redmine/app/views/issues/_list_simple.rhtml
new file mode 100644
index 000000000..66b70a1da
--- /dev/null
+++ b/redmine/app/views/issues/_list_simple.rhtml
@@ -0,0 +1,28 @@
+<% if issues.length > 0 %>
+ <table cellspacing="0" cellpadding="1" width="100%" border="0" class="listTable">
+ <tr><td>
+ <table width="100%" border="0" cellspacing="1" cellpadding="2" class="listTableContent">
+ <tr class="ListHead">
+ <th>#</th>
+ <th><%=_('Tracker')%></th>
+ <th><%=_('Subject')%></th>
+ </tr>
+ <% for issue in issues %>
+ <tr bgcolor="#<%= issue.status.html_color %>">
+ <td align="center">
+ <%= link_to issue.id, :controller => 'issues', :action => 'show', :id => issue %><br />
+ </td>
+ <td><p class="small"><%= issue.project.name %> - <%= issue.tracker.name %><br />
+ <%= issue.status.name %> - <%= format_time(issue.updated_on) %></p></td>
+ <td>
+ <p class="small"><%= link_to issue.subject, :controller => 'issues', :action => 'show', :id => issue %></p>
+ </td>
+ </tr>
+ <% end %>
+ </table>
+ </td>
+ </tr>
+ </table>
+<% else %>
+ <%=_('No issue')%>
+<% end %> \ No newline at end of file
diff --git a/redmine/app/views/issues/change_status.rhtml b/redmine/app/views/issues/change_status.rhtml
new file mode 100644
index 000000000..58032cefc
--- /dev/null
+++ b/redmine/app/views/issues/change_status.rhtml
@@ -0,0 +1,29 @@
+<h2><%=_('Issue')%> #<%= @issue.id %>: <%= @issue.subject %></h2>
+
+<%= error_messages_for 'history' %>
+<%= start_form_tag :action => 'change_status', :id => @issue %>
+
+<%= hidden_field_tag 'confirm', 1 %>
+<%= hidden_field 'history', 'status_id' %>
+
+<p><%=_('New status')%>: <b><%= @history.status.name %></b></p>
+
+<div>
+<p><label for="issue_assigned_to_id"><%=_('Assigned to')%></label><br/>
+<select name="issue[assigned_to_id]">
+<option value=""></option>
+<%= options_from_collection_for_select @assignable_to, "id", "display_name", @issue.assigned_to_id %></p>
+</select></p>
+</div>
+
+<p><label for="issue_fixed_version"><%=_('Fixed in version')%></label><br/>
+<select name="issue[fixed_version_id]">
+<option value="">--none--</option>
+<%= options_from_collection_for_select @issue.project.versions, "id", "name", @issue.fixed_version_id %>
+</select></p>
+
+<p><label for="history_notes"><%=_('Notes')%></label><br />
+<%= text_area 'history', 'notes', :cols => 60, :rows => 10 %></p>
+
+<%= submit_tag _('Save') %>
+<%= end_form_tag %>
diff --git a/redmine/app/views/issues/edit.rhtml b/redmine/app/views/issues/edit.rhtml
new file mode 100644
index 000000000..1249cfc8d
--- /dev/null
+++ b/redmine/app/views/issues/edit.rhtml
@@ -0,0 +1,62 @@
+<h2><%=_('Issue')%> #<%= @issue.id %></h2>
+
+<%= error_messages_for 'issue' %>
+<%= start_form_tag :action => 'edit', :id => @issue %>
+
+<!--[form:issue]-->
+<p><%=_('Status')%>: <b><%= @issue.status.name %></b></p>
+
+<div style="float:left;margin-right:10px;">
+<p><label for="issue_tracker_id"><%=_('Tracker')%> <span class="required">*</span></label><br/>
+<select name="issue[tracker_id]">
+<%= options_from_collection_for_select @trackers, "id", "name", @issue.tracker_id %></p>
+</select></p>
+</div>
+
+<div style="float:left;margin-right:10px;">
+<p><label for="issue_priority_id"><%=_('Priority')%> <span class="required">*</span></label><br/>
+<select name="issue[priority_id]">
+<%= options_from_collection_for_select @priorities, "id", "name", @issue.priority_id %></p>
+</select></p>
+</div>
+
+<div style="float:left;margin-right:10px;">
+<p><label for="issue_assigned_to_id"><%=_('Assigned to')%></label><br/>
+<select name="issue[assigned_to_id]">
+<option value=""></option>
+<%= options_from_collection_for_select @issue.project.members, "user_id", "name", @issue.assigned_to_id %></p>
+</select></p>
+</div>
+
+<div>
+<p><label for="issue_category_id"><%=_('Category')%></label><br/>
+<select name="issue[category_id]">
+<option value=""></option>
+<%= options_from_collection_for_select @project.issue_categories, "id", "name", @issue.category_id %></p>
+</select></p>
+</div>
+
+<p><label for="issue_subject"><%=_('Subject')%></label><span class="required">*</span><br/>
+<%= text_field 'issue', 'subject', :size => 60 %></p>
+
+<p><label for="issue_descr"><%=_('Description')%></label><span class="required">*</span><br/>
+<%= text_area 'issue', 'descr', :cols => 60, :rows => 10 %></p>
+
+
+<% for custom_value in @custom_values %>
+ <p><%= content_tag "label", custom_value.custom_field.name %>
+ <% if custom_value.custom_field.is_required? %><span class="required">*</span><% end %>
+ <br />
+ <%= custom_field_tag custom_value %></p>
+<% end %>
+
+
+<p><label for="issue_fixed_version"><%=_('Fixed in version')%></label><br/>
+<select name="issue[fixed_version_id]">
+<option value="">--none--</option>
+<%= options_from_collection_for_select @project.versions, "id", "name", @issue.fixed_version_id %>
+</select></p>
+<!--[eoform:issue]-->
+
+<center><%= submit_tag _('Save') %></center>
+<%= end_form_tag %>
diff --git a/redmine/app/views/issues/show.rhtml b/redmine/app/views/issues/show.rhtml
new file mode 100644
index 000000000..1c20c92dd
--- /dev/null
+++ b/redmine/app/views/issues/show.rhtml
@@ -0,0 +1,90 @@
+
+<h2><%=_('Issue')%> #<%= @issue.id %> - <%= @issue.subject %></h2>
+
+<div class="box">
+<p><b><%=_('Tracker')%>:</b> <%= @issue.tracker.name %></p>
+<p><b><%=_('Priority')%>:</b> <%= @issue.priority.name %></p>
+<p><b><%=_('Category')%>:</b> <%= @issue.category.name unless @issue.category_id.nil? %></p>
+<p><b><%=_('Status')%>:</b> <%= @issue.status.name %></p>
+<p><b><%=_('Author')%>:</b> <%= @issue.author.display_name %></p>
+<p><b><%=_('Assigned to')%>:</b> <%= @issue.assigned_to.display_name unless @issue.assigned_to.nil? %></p>
+
+<p><b><%=_('Subject')%>:</b> <%= @issue.subject %></p>
+<p><b><%=_('Description')%>:</b> <%= @issue.descr %></p>
+<p><b><%=_('Created on')%>:</b> <%= format_date(@issue.created_on) %></p>
+
+<% if authorize_for('issues', 'edit') %>
+ <%= start_form_tag ({:controller => 'issues', :action => 'edit', :id => @issue}, :method => "get" ) %>
+ <%= submit_tag _('Edit') %>
+ <%= end_form_tag %>
+ &nbsp;&nbsp;
+<% end %>
+
+<% if authorize_for('issues', 'change_status') and @status_options and !@status_options.empty? %>
+ <%= start_form_tag ({:controller => 'issues', :action => 'change_status', :id => @issue}) %>
+ <label for="history_status_id"><%=_('Change status')%>:</label>
+ <select name="history[status_id]">
+ <%= options_from_collection_for_select @status_options, "id", "name" %>
+ </select>
+ <%= submit_tag _ "Update..." %>
+ <%= end_form_tag %>
+ &nbsp;&nbsp;
+<% end %>
+
+<% if authorize_for('issues', 'destroy') %>
+ <%= start_form_tag ({:controller => 'issues', :action => 'destroy', :id => @issue} ) %>
+ <%= submit_tag _ "Delete" %>
+ <%= end_form_tag %>
+ &nbsp;&nbsp;
+<% end %>
+
+</div>
+
+
+<div class="splitcontentleft">
+<div class="box">
+<h3><%=_('History')%></h3>
+<table width="100%">
+<% for history in @issue.histories.find(:all, :include => :author) %>
+<tr>
+<td><%= format_date(history.created_on) %></td>
+<td><%= history.author.display_name %></td>
+<td><b><%= history.status.name %></b></td>
+</tr>
+<% if history.notes? %>
+ <tr><td colspan=3><div class="notes"><%= history.notes %></td></tr>
+<% end %>
+<% end %>
+</table>
+</div>
+</div>
+
+<div class="splitcontentright">
+<div class="box">
+<h3><%=_('Attachments')%></h3>
+<table width="100%">
+<% for attachment in @issue.attachments %>
+<tr>
+<td><%= link_to attachment.filename, :action => 'download', :id => @issue, :attachment_id => attachment %> (<%= human_size(attachment.size) %>)</td>
+<td><%= format_date(attachment.created_on) %></td>
+<td><%= attachment.author.display_name %></td>
+<% if authorize_for('issues', 'destroy_attachment') %>
+ <td>
+ <%= start_form_tag :action => 'destroy_attachment', :id => @issue, :attachment_id => attachment %>
+ <%= submit_tag _('Delete'), :class => "button-small" %>
+ <%= end_form_tag %>
+ </td>
+<% end %>
+</tr>
+<% end %>
+</table>
+<br />
+<% if authorize_for('issues', 'add_attachment') %>
+ <%= start_form_tag ({ :controller => 'issues', :action => 'add_attachment', :id => @issue }, :multipart => true) %>
+ <%=_('Add file')%>: <%= file_field 'attachment', 'file' %>
+ <%= submit_tag _('Add') %>
+ <%= end_form_tag %>
+<% end %>
+</div>
+</div>
+
diff --git a/redmine/app/views/layouts/base.rhtml b/redmine/app/views/layouts/base.rhtml
new file mode 100644
index 000000000..731632fa4
--- /dev/null
+++ b/redmine/app/views/layouts/base.rhtml
@@ -0,0 +1,89 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
+<head>
+<title>redMine</title>
+<meta http-equiv="content-type" content="text/html; charset=utf-8" />
+<meta name="description" content="redMine" />
+<meta name="keywords" content="issue,bug,tracker" />
+<%= stylesheet_link_tag "application" %>
+<%= stylesheet_link_tag "rails" %>
+<%= javascript_include_tag :defaults %>
+</head>
+
+<body>
+<div id="container" >
+
+ <div id="header">
+ <div style="float: left;">
+ <h1><%= RDM_APP_NAME %></h1>
+ <h2>Project management</h2>
+ </div>
+ <div style="float: right; padding-right: 1em; padding-top: 0.2em;">
+ <% unless session[:user].nil? %><small><%=_('Logged as')%> <b><%= session[:user].login %></b></small><% end %>
+ </div>
+ </div>
+
+ <div id="navigation">
+ <ul>
+ <li class="selected"><%= link_to _('Home'), { :controller => '' }, :class => "picHome" %></li>
+ <li><%= link_to _('My page'), { :controller => 'account', :action => 'my_page'}, :class => "picUserPage" %></li>
+ <li><%= link_to _('Projects'), { :controller => 'projects' }, :class => "picProject" %></li>
+
+ <% unless session[:user].nil? %>
+ <li><%= link_to _('My account'), { :controller => 'account', :action => 'my_account' }, :class => "picUser" %></li>
+ <% end %>
+
+ <% if admin_loggedin? %>
+ <li><%= link_to _('Administration'), { :controller => 'admin' }, :class => "picAdmin" %></li>
+ <% end %>
+
+ <li class="right"><%= link_to _('Help'), { :controller => 'help', :ctrl => @params[:controller], :page => @params[:action] }, :target => "new", :class => "picHelp" %></li>
+ <% if session[:user].nil? %>
+ <li class="right"><%= link_to _('Log in'), { :controller => 'account', :action => 'login' }, :class => "picUser" %></li>
+ <% else %>
+ <li class="right"><%= link_to _('Logout'), { :controller => 'account', :action => 'logout' }, :class => "picUser" %></li>
+ <% end %>
+ </ul>
+
+ </div>
+
+ <div id="subcontent">
+
+ <% unless @project.nil? || @project.id.nil? %>
+ <h2><%= @project.name %></h2>
+ <ul class="menublock">
+ <li><%= link_to _('Overview'), :controller => 'projects', :action => 'show', :id => @project %></li>
+ <li><%= link_to _('Issues'), :controller => 'projects', :action => 'list_issues', :id => @project %></li>
+ <li><%= link_to _('Reports'), :controller => 'reports', :action => 'issue_report', :id => @project %></li>
+ <li><%= link_to _('News'), :controller => 'projects', :action => 'list_news', :id => @project %></li>
+ <li><%= link_to _('Change log'), :controller => 'projects', :action => 'changelog', :id => @project %></li>
+ <li><%= link_to _('Documents'), :controller => 'projects', :action => 'list_documents', :id => @project %></li>
+ <li><%= link_to _('Members'), :controller => 'projects', :action => 'list_members', :id => @project %></li>
+ <li><%= link_to _('Files'), :controller => 'projects', :action => 'list_files', :id => @project %></li>
+ <li><%= link_to_if_authorized _('Settings'), :controller => 'projects', :action => 'settings', :id => @project %></li>
+ </ul>
+ <% end %>
+
+ <% unless session[:user].nil? %>
+ <h2><%=_('My projects') %></h2>
+ <ul class="menublock">
+ <% for membership in session[:user].memberships %>
+ <li><%= link_to membership.project.name, :controller => 'projects', :action => 'show', :id => membership.project %></li>
+ <% end %>
+ </ul>
+ <% end %>
+
+ </div>
+
+ <div id="content">
+ <% if flash[:notice] %><p style="color: green"><%= flash[:notice] %></p><% end %>
+ <%= @content_for_layout %>
+ </div>
+
+ <div id="footer">
+ <p><a href="http://redmine.sourceforge.net/" target="_new"><%= RDM_APP_NAME %></a> <%= RDM_APP_VERSION %></p>
+ </div>
+
+</div>
+</body>
+</html> \ No newline at end of file
diff --git a/redmine/app/views/mailer/_issue.rhtml b/redmine/app/views/mailer/_issue.rhtml
new file mode 100644
index 000000000..1f238f513
--- /dev/null
+++ b/redmine/app/views/mailer/_issue.rhtml
@@ -0,0 +1,6 @@
+<%=_('Issue')%> #<%= issue.id %> - <%= issue.subject %>
+<%=_('Author')%>: <%= issue.author.display_name %>
+
+<%= issue.descr %>
+
+http://<%= RDM_HOST_NAME %>/issues/show/<%= issue.id %> \ No newline at end of file
diff --git a/redmine/app/views/mailer/issue_add.rhtml b/redmine/app/views/mailer/issue_add.rhtml
new file mode 100644
index 000000000..9efec9ad9
--- /dev/null
+++ b/redmine/app/views/mailer/issue_add.rhtml
@@ -0,0 +1,3 @@
+Issue #<%= @issue.id %> has been reported.
+----------------------------------------
+<%= render :file => "_issue", :use_full_path => true, :locals => { :issue => @issue } %> \ No newline at end of file
diff --git a/redmine/app/views/mailer/issue_change_status.rhtml b/redmine/app/views/mailer/issue_change_status.rhtml
new file mode 100644
index 000000000..e3612ea81
--- /dev/null
+++ b/redmine/app/views/mailer/issue_change_status.rhtml
@@ -0,0 +1,3 @@
+Issue #<%= @issue.id %> has been updated to "<%= @issue.status.name %>" status.
+----------------------------------------
+<%= render :file => "_issue", :use_full_path => true, :locals => { :issue => @issue } %> \ No newline at end of file
diff --git a/redmine/app/views/news/_form.rhtml b/redmine/app/views/news/_form.rhtml
new file mode 100644
index 000000000..609b15cd9
--- /dev/null
+++ b/redmine/app/views/news/_form.rhtml
@@ -0,0 +1,13 @@
+<%= error_messages_for 'news' %>
+
+<!--[form:news]-->
+<p><label for="news_title"><%=_('Title')%></label> <span class="required">*</span><br/>
+<%= text_field 'news', 'title', :size => 60 %></p>
+
+<p><label for="news_shortdescr"><%=_('Summary')%> <span class="required">*</span></label><br/>
+<%= text_area 'news', 'shortdescr', :cols => 60, :rows => 2 %></p>
+
+<p><label for="news_descr"><%=_('Description')%> <span class="required">*</span></label><br/>
+<%= text_area 'news', 'descr', :cols => 60, :rows => 10 %></p>
+<!--[eoform:news]-->
+
diff --git a/redmine/app/views/news/edit.rhtml b/redmine/app/views/news/edit.rhtml
new file mode 100644
index 000000000..2e849ab1d
--- /dev/null
+++ b/redmine/app/views/news/edit.rhtml
@@ -0,0 +1,6 @@
+<h2><%=_('News')%></h2>
+
+<%= start_form_tag :action => 'edit', :id => @news %>
+ <%= render :partial => 'form' %>
+ <%= submit_tag _('Save') %>
+<%= end_form_tag %>
diff --git a/redmine/app/views/news/show.rhtml b/redmine/app/views/news/show.rhtml
new file mode 100644
index 000000000..30aa2cda9
--- /dev/null
+++ b/redmine/app/views/news/show.rhtml
@@ -0,0 +1,10 @@
+<h2><%= @news.title %></h2>
+
+<p>
+<b><%=_('Summary')%></b>: <%= @news.shortdescr %><br />
+<b><%=_('By')%></b>: <%= @news.author.display_name %><br />
+<b><%=_('Date')%></b>: <%= format_time(@news.created_on) %>
+</p>
+
+<%= @news.descr %>
+
diff --git a/redmine/app/views/projects/_form.rhtml b/redmine/app/views/projects/_form.rhtml
new file mode 100644
index 000000000..2d38117a4
--- /dev/null
+++ b/redmine/app/views/projects/_form.rhtml
@@ -0,0 +1,28 @@
+<%= error_messages_for 'project' %>
+
+<!--[form:project]-->
+<p><label for="project_name"><%=_('Name')%> <span class="required">*</span></label><br/>
+<%= text_field 'project', 'name' %></p>
+
+<p><label for="project_descr"><%=_('Description')%> <span class="required">*</span></label><br/>
+<%= text_field 'project', 'descr', :size => 60 %></p>
+
+<p><label for="project_homepage"><%=_('Homepage')%></label><br/>
+<%= text_field 'project', 'homepage', :size => 40 %></p>
+
+<p><%= check_box 'project', 'public' %>
+<label for="project_public"><%=_('Public')%></label></p>
+
+<fieldset><legend><%=_('Custom fields')%></legend>
+<% for custom_field in @custom_fields %>
+ <input type="checkbox"
+
+ name="custom_field_ids[]"
+ value="<%= custom_field.id %>"
+ <%if @project.custom_fields.include? custom_field%>checked="checked"<%end%>
+ > <%= custom_field.name %>
+
+<% end %></fieldset>
+<br />
+
+<!--[eoform:project]-->
diff --git a/redmine/app/views/projects/add.rhtml b/redmine/app/views/projects/add.rhtml
new file mode 100644
index 000000000..6344705b0
--- /dev/null
+++ b/redmine/app/views/projects/add.rhtml
@@ -0,0 +1,7 @@
+<h2><%=_('New project')%></h2>
+
+<%= start_form_tag :action => 'add' %>
+<%= render :partial => 'form' %>
+<%= submit_tag _('Create') %>
+<%= end_form_tag %>
+
diff --git a/redmine/app/views/projects/add_document.rhtml b/redmine/app/views/projects/add_document.rhtml
new file mode 100644
index 000000000..13e75dd77
--- /dev/null
+++ b/redmine/app/views/projects/add_document.rhtml
@@ -0,0 +1,26 @@
+<h2><%=_('New document')%></h2>
+
+<%= error_messages_for 'document' %>
+<%= start_form_tag( { :action => 'add_document', :id => @project }, :multipart => true) %>
+
+<!--[form:document]-->
+<p><label for="document_category_id"><%=_('Category')%></label><br />
+<select name="document[category_id]">
+<%= options_from_collection_for_select @categories, "id", "name",@document.category_id %>
+</select></p>
+
+<p><label for="document_title"><%=_('Title')%> <span class="required">*</span></label><br />
+<%= text_field 'document', 'title', :size => 60 %></p>
+
+<p><label for="document_descr"><%=_('Description')%> <span class="required">*</span></label><br />
+<%= text_area 'document', 'descr', :cols => 60, :rows => 5 %></p>
+
+<p><label for="attachment_file"><%=_('File')%></label><br/>
+<%= file_field 'attachment', 'file' %></p>
+
+<!--[eoform:document]-->
+
+<%= submit_tag _('Create') %>
+<%= end_form_tag %>
+
+
diff --git a/redmine/app/views/projects/add_file.rhtml b/redmine/app/views/projects/add_file.rhtml
new file mode 100644
index 000000000..4e8ce9b02
--- /dev/null
+++ b/redmine/app/views/projects/add_file.rhtml
@@ -0,0 +1,13 @@
+<h2><%=_('New file')%></h2>
+
+<%= start_form_tag ({ :action => 'add_file', :project => @project }, :multipart => true) %>
+
+<p><label for="version_id"><%=_('Version')%></label><br />
+<select name="version_id">
+<%= options_from_collection_for_select @versions, "id", "name" %>
+</select></p>
+
+<p><b><%=_('File')%><b><br /><%= file_field 'attachment', 'file' %></p>
+<br/>
+<%= submit_tag _('Add') %>
+<%= end_form_tag %> \ No newline at end of file
diff --git a/redmine/app/views/projects/add_issue.rhtml b/redmine/app/views/projects/add_issue.rhtml
new file mode 100644
index 000000000..a6b37cc46
--- /dev/null
+++ b/redmine/app/views/projects/add_issue.rhtml
@@ -0,0 +1,62 @@
+<h2><%=_('New issue')%></h2>
+
+<%= start_form_tag( { :action => 'add_issue', :id => @project }, :multipart => true) %>
+<%= error_messages_for 'issue' %>
+
+<!--[form:issue]-->
+
+<div style="float:left;margin-right:10px;">
+<p><label for="issue_tracker_id"><%=_('Tracker')%> <span class="required">*</span></label><br/>
+<select name="issue[tracker_id]">
+<%= options_from_collection_for_select @trackers, "id", "name", @issue.tracker_id %></p>
+</select></p>
+</div>
+
+<div style="float:left;margin-right:10px;">
+<p><label for="issue_priority_id"><%=_('Priority')%> <span class="required">*</span></label><br/>
+<select name="issue[priority_id]">
+<%= options_from_collection_for_select @priorities, "id", "name", @issue.priority_id %></p>
+</select></p>
+</div>
+
+<div style="float:left;margin-right:10px;">
+<p><label for="issue_assigned_to_id"><%=_('Assigned to')%></label><br/>
+<select name="issue[assigned_to_id]">
+<option value=""></option>
+<%= options_from_collection_for_select @issue.project.members, "user_id", "name", @issue.assigned_to_id %></p>
+</select></p>
+</div>
+
+<div>
+<p><label for="issue_category_id"><%=_('Category')%></label><br/>
+<select name="issue[category_id]">
+<option value=""></option>
+<%= options_from_collection_for_select @project.issue_categories, "id", "name", @issue.category_id %></p>
+</select></p>
+</div>
+
+<p><label for="issue_subject"><%=_('Subject')%> <span class="required">*</span></label><br/>
+<%= text_field 'issue', 'subject', :size => 80 %></p>
+
+<p><label for="issue_descr"><%=_('Description')%> <span class="required">*</span></label><br/>
+<%= text_area 'issue', 'descr', :cols => 60, :rows => 10 %></p>
+
+
+<% for custom_value in @custom_values %>
+ <div style="float:left;margin-right:10px;">
+ <p><%= content_tag "label", custom_value.custom_field.name %>
+ <% if custom_value.custom_field.is_required? %><span class="required">*</span><% end %>
+ <br />
+ <%= custom_field_tag custom_value %></p>
+ </div>
+<% end %>
+
+<div style="clear: both;">
+<p><label for="attachment_file"><%=_('Attachment')%></label><br/>
+<%= file_field 'attachment', 'file' %></p>
+</div>
+
+<!--[eoform:issue]-->
+
+<%= submit_tag _('Create') %>
+<%= end_form_tag %> \ No newline at end of file
diff --git a/redmine/app/views/projects/add_news.rhtml b/redmine/app/views/projects/add_news.rhtml
new file mode 100644
index 000000000..c106e7c21
--- /dev/null
+++ b/redmine/app/views/projects/add_news.rhtml
@@ -0,0 +1,7 @@
+<h2><%=('Add news')%></h2>
+
+<%= start_form_tag :action => 'add_news', :id => @project %>
+ <%= render :partial => 'news/form' %>
+ <%= submit_tag _('Create') %>
+<%= end_form_tag %>
+
diff --git a/redmine/app/views/projects/add_version.rhtml b/redmine/app/views/projects/add_version.rhtml
new file mode 100644
index 000000000..27b258fab
--- /dev/null
+++ b/redmine/app/views/projects/add_version.rhtml
@@ -0,0 +1,7 @@
+<h2><%=_('New version')%></h2>
+
+<%= start_form_tag :action => 'add_version', :id => @project %>
+ <%= render :partial => 'versions/form' %>
+ <%= submit_tag _('Create') %>
+<%= end_form_tag %>
+
diff --git a/redmine/app/views/projects/changelog.rhtml b/redmine/app/views/projects/changelog.rhtml
new file mode 100644
index 000000000..3f97abccd
--- /dev/null
+++ b/redmine/app/views/projects/changelog.rhtml
@@ -0,0 +1,12 @@
+<h2><%=_('Change log')%></h2>
+
+<% fixed_issues = @fixed_issues.group_by {|i| i.fixed_version } %>
+<% fixed_issues.each do |version, issues| %>
+ <p><strong><%= version.name %></strong> - <%= format_date(version.date) %><br />
+ <%=h version.descr %></p>
+ <ul>
+ <% issues.each do |i| %>
+ <li><%= link_to i.long_id, :controller => 'issues', :action => 'show', :id => i %> [<%= i.tracker.name %>]: <%= i.subject %></li>
+ <% end %>
+ </ul>
+<% end %>
diff --git a/redmine/app/views/projects/destroy.rhtml b/redmine/app/views/projects/destroy.rhtml
new file mode 100644
index 000000000..2e26478d9
--- /dev/null
+++ b/redmine/app/views/projects/destroy.rhtml
@@ -0,0 +1,12 @@
+<h2><%=_('Confirmation')%></h2>
+<div class="box">
+<center>
+<p><%=_('Are you sure you want to delete project')%> <strong><%= @project.name %></strong> ?</p>
+<p>
+ <%= start_form_tag({:controller => 'projects', :action => 'destroy', :id => @project}) %>
+ <%= hidden_field_tag "confirm", 1 %>
+ <%= submit_tag _('Delete') %>
+ <%= end_form_tag %>
+</p>
+</center>
+</div> \ No newline at end of file
diff --git a/redmine/app/views/projects/list.rhtml b/redmine/app/views/projects/list.rhtml
new file mode 100644
index 000000000..2b2ac2d13
--- /dev/null
+++ b/redmine/app/views/projects/list.rhtml
@@ -0,0 +1,22 @@
+<h2><%=_('Public projects')%></h2>
+
+<table width="100%" cellspacing="1" cellpadding="2" class="listTableContent">
+ <tr class="ListHead">
+ <%= sort_header_tag('projects.name', :caption => _('Project')) %>
+ <th>Description</th>
+ <%= sort_header_tag('projects.created_on', :caption => _('Created on')) %>
+ </tr>
+
+<% odd_or_even = 1
+ for project in @projects
+ odd_or_even = 1 - odd_or_even %>
+ <tr class="ListLine<%= odd_or_even %>">
+ <td><%= link_to project.name, :action => 'show', :id => project %>
+ <td><%= project.descr %>
+ <td align="center"><%= format_date(project.created_on) %>
+ </tr>
+<% end %>
+</table>
+
+<%= pagination_links_full @project_pages %>
+[ <%= @project_pages.current.first_item %> - <%= @project_pages.current.last_item %> / <%= @project_count %> ] \ No newline at end of file
diff --git a/redmine/app/views/projects/list_documents.rhtml b/redmine/app/views/projects/list_documents.rhtml
new file mode 100644
index 000000000..1c7db5ec9
--- /dev/null
+++ b/redmine/app/views/projects/list_documents.rhtml
@@ -0,0 +1,21 @@
+<h2><%=_('Documents')%></h2>
+
+<% documents = @documents.group_by {|d| d.category } %>
+<% documents.each do |category, docs| %>
+<h3><%= category.name %></h3>
+<ul>
+<% docs.each do |d| %>
+ <li>
+ <b><%= link_to d.title, :controller => 'documents', :action => 'show', :id => d %></b>
+ <br />
+ <%=_('Desciption')%>: <%= d.descr %><br />
+ <%= format_time(d.created_on) %>
+ </li>
+
+<% end %>
+</ul>
+<% end %>
+
+<p>
+<%= link_to_if_authorized '&#187; ' + _('New'), :controller => 'projects', :action => 'add_document', :id => @project %>
+</p>
diff --git a/redmine/app/views/projects/list_files.rhtml b/redmine/app/views/projects/list_files.rhtml
new file mode 100644
index 000000000..217e679b5
--- /dev/null
+++ b/redmine/app/views/projects/list_files.rhtml
@@ -0,0 +1,47 @@
+<h2><%=_('Files')%></h2>
+
+<% delete_allowed = authorize_for('versions', 'destroy_file') %>
+
+<table border="0" cellspacing="1" cellpadding="2" class="listTableContent">
+ <tr class="ListHead">
+ <th><%=_('Version')%></th>
+ <th><%=_('File')%></th>
+ <th><%=_('Date')%></th>
+ <th><%=_('Size')%></th>
+ <th>D/L</th>
+ <th>MD5</th>
+ <% if delete_allowed %><th></th><% end %>
+ </tr>
+
+<% for version in @versions %>
+ <tr>
+ <td colspan="7"><%= image_tag 'package' %> <b><%= version.name %></b></td>
+ </tr>
+ <% odd_or_even = 1
+ for file in version.attachments
+ odd_or_even = 1 - odd_or_even %>
+ <tr class="ListLine<%= odd_or_even %>">
+ <td></td>
+ <td><%= link_to file.filename, :controller => 'versions', :action => 'download', :id => version, :attachment_id => file %></td>
+ <td align="center"><%= format_date(file.created_on) %></td>
+ <td align="center"><%= human_size(file.size) %></td>
+ <td align="center"><%= file.downloads %></td>
+ <td align="center"><small><%= file.digest %></small></td>
+ <% if delete_allowed %>
+ <td align="center">
+ <%= start_form_tag :controller => 'versions', :action => 'destroy_file', :id => version, :attachment_id => file %>
+ <%= submit_tag _('Delete'), :class => "button-small" %>
+ <%= end_form_tag %>
+ </td>
+ <% end %>
+ </tr>
+ <% end %>
+<% end %>
+</table>
+
+<br />
+<p>
+<%= link_to_if_authorized '&#187; ' + _('New'), :controller => 'projects', :action => 'add_file', :id => @project %>
+</p>
+
+
diff --git a/redmine/app/views/projects/list_issues.rhtml b/redmine/app/views/projects/list_issues.rhtml
new file mode 100644
index 000000000..5be81b11c
--- /dev/null
+++ b/redmine/app/views/projects/list_issues.rhtml
@@ -0,0 +1,56 @@
+<h2><%=_('Issues')%></h2>
+
+<form method="post" class="noborder">
+ <table cellpadding=2>
+ <tr>
+ <td><%=_('Status')%>:<br /><%= search_filter_tag("issues.status_id") %></td>
+ <td><%=_('Tracker')%>:<br /><%= search_filter_tag("issues.tracker_id") %></td>
+ <td><%=_('Priority')%>:<br /><%= search_filter_tag("issues.priority_id") %></td>
+ <td><%=_('Category')%>:<br /><%= search_filter_tag("issues.category_id") %></td>
+ <td><%=_('Author')%>:<br /><%= search_filter_tag("issues.author_id") %></td>
+ <td valign="bottom">
+ <%= submit_tag _('Apply filter') %>
+ <%= end_form_tag %>
+
+ <%= start_form_tag %>
+ <%= submit_tag _('Reset') %>
+ <%= end_form_tag %>
+ </td>
+ </tr>
+ </table>
+
+ &nbsp;
+
+ <table border="0" cellspacing="1" cellpadding="2" class="listTableContent">
+
+ <tr class="ListHead">
+ <%= sort_header_tag('issues.id', :caption => '#') %>
+ <%= sort_header_tag('issue_statuses.name', :caption => _('Status')) %>
+ <%= sort_header_tag('issues.tracker_id', :caption => _('Tracker')) %>
+ <th><%=_('Subject')%></th>
+ <%= sort_header_tag('users.lastname', :caption => _('Author')) %>
+ <%= sort_header_tag('issues.created_on', :caption => _('Created on')) %>
+ <%= sort_header_tag('issues.updated_on', :caption => _('Last update')) %>
+ </tr>
+
+ <% for issue in @issues %>
+ <tr bgcolor="#<%= issue.status.html_color %>">
+ <td align="center"><%= link_to issue.long_id, :controller => 'issues', :action => 'show', :id => issue %></td>
+ <td align="center"><%= issue.status.name %></td>
+ <td align="center"><%= issue.tracker.name %></td>
+ <td><%= link_to issue.subject, :controller => 'issues', :action => 'show', :id => issue %></td>
+ <td align="center"><%= issue.author.display_name %></td>
+ <td align="center"><%= format_time(issue.created_on) %></td>
+ <td align="center"><%= format_time(issue.updated_on) %></td>
+ </tr>
+ <% end %>
+ </table>
+ <p>
+ <%= pagination_links_full @issue_pages %>
+ [ <%= @issue_pages.current.first_item %> - <%= @issue_pages.current.last_item %> / <%= @issue_count %> ]
+ </p>
+
+
+<p>
+<%= link_to_if_authorized '&#187; ' + _('Report an issue'), :controller => 'projects', :action => 'add_issue', :id => @project %>
+</p> \ No newline at end of file
diff --git a/redmine/app/views/projects/list_members.rhtml b/redmine/app/views/projects/list_members.rhtml
new file mode 100644
index 000000000..6fca0d2bf
--- /dev/null
+++ b/redmine/app/views/projects/list_members.rhtml
@@ -0,0 +1,11 @@
+<h2><%=_('Project members')%></h2>
+
+<% members = @members.group_by {|m| m.role } %>
+<% members.each do |role, member| %>
+<h3><%= role.name %></h3>
+<ul>
+<% member.each do |m| %>
+<li><%= link_to m.user.display_name, :controller => 'account', :action => 'show', :id => m.user %> (<%= format_date m.created_on %>)</li>
+<% end %>
+</ul>
+<% end %>
diff --git a/redmine/app/views/projects/list_news.rhtml b/redmine/app/views/projects/list_news.rhtml
new file mode 100644
index 000000000..7db53391e
--- /dev/null
+++ b/redmine/app/views/projects/list_news.rhtml
@@ -0,0 +1,17 @@
+<h2><%=_('News')%></h2>
+
+<% for news in @news %>
+ <p>
+ <b><%= news.title %></b> <small>(<%= link_to_user news.author %> <%= format_time(news.created_on) %>)</small>
+ <%= link_to_if_authorized image_tag('edit_small'), :controller => 'news', :action => 'edit', :id => news %>
+ <%= link_to_if_authorized image_tag('delete'), { :controller => 'news', :action => 'destroy', :id => news }, :confirm => 'Are you sure?' %>
+ <br />
+ <%= news.shortdescr %>
+ <small>[<%= link_to _('Read...'), :controller => 'news', :action => 'show', :id => news %>]</small>
+ </p>
+<% end %>
+
+<%= pagination_links_full @news_pages %>
+<p>
+<%= link_to_if_authorized '&#187; ' + _('Add'), :controller => 'projects', :action => 'add_news', :id => @project %>
+</p>
diff --git a/redmine/app/views/projects/settings.rhtml b/redmine/app/views/projects/settings.rhtml
new file mode 100644
index 000000000..c130ed036
--- /dev/null
+++ b/redmine/app/views/projects/settings.rhtml
@@ -0,0 +1,105 @@
+<h2><%=_('Settings')%></h2>
+
+<div class="box">
+<%= start_form_tag :action => 'edit', :id => @project %>
+<%= render :partial => 'form' %>
+<center><%= submit_tag _('Save') %></center>
+<%= end_form_tag %>
+</div>
+
+<div class="box">
+<h3><%=_('Members')%></h3>
+<%= error_messages_for 'member' %>
+<table>
+<% for member in @project.members.find(:all, :include => :user) %>
+ <% unless member.new_record? %>
+ <tr>
+ <td><%= member.user.display_name %></td>
+ <td>
+ <%= start_form_tag :controller => 'members', :action => 'edit', :id => member %>
+ <select name="member[role_id]">
+ <%= options_from_collection_for_select @roles, "id", "name", member.role_id %>
+ </select>
+ </td>
+ <td>
+ <%= submit_tag _('Save'), :class => "button-small" %>
+ <%= end_form_tag %>
+ </td>
+ <td>
+ <%= start_form_tag :controller => 'members', :action => 'destroy', :id => member %>
+ <%= submit_tag _('Delete'), :class => "button-small" %>
+ <%= end_form_tag %>
+ </td>
+ </tr>
+ <% end %>
+<% end %>
+</table>
+<hr />
+ <label><%=_('New member')%></label><br/>
+ <%= start_form_tag :controller => 'projects', :action => 'add_member', :id => @project %>
+ <select name="member[user_id]">
+ <%= options_from_collection_for_select @users, "id", "display_name", @member.user_id %>
+ </select>
+ <select name="member[role_id]">
+ <%= options_from_collection_for_select @roles, "id", "name", @member.role_id %>
+ </select>
+ <%= submit_tag _('Add') %>
+ <%= end_form_tag %>
+</div>
+
+<div class="box">
+<h3><%=_('Versions')%></h3>
+
+<table>
+<% for version in @project.versions %>
+ <tr>
+ <td><%= link_to version.name, :controller => 'versions', :action => 'edit', :id => version %></td>
+ <td><%=h version.descr %></td>
+ <td>
+ <%= start_form_tag :controller => 'versions', :action => 'destroy', :id => version %>
+ <%= submit_tag _('Delete'), :class => "button-small" %>
+ <%= end_form_tag %>
+ </td>
+ </tr>
+<% end %>
+</table>
+<hr />
+ <%= start_form_tag ({ :controller => 'projects', :action => 'add_version', :id => @project }, :method => 'get' ) %>
+ <%= submit_tag _('New version...') %>
+ <%= end_form_tag %>
+</div>
+
+
+<div class="box">
+<h3><%=_('Issue categories')%></h3>
+<table>
+<% for @category in @project.issue_categories %>
+ <% unless @category.new_record? %>
+ <tr>
+ <td>
+ <%= start_form_tag :controller => 'issue_categories', :action => 'edit', :id => @category %>
+ <%= text_field 'category', 'name', :size => 25 %>
+ </td>
+ <td>
+ <%= submit_tag _('Save'), :class => "button-small" %>
+ <%= end_form_tag %>
+ </td>
+ <td>
+ <%= start_form_tag :controller => 'issue_categories', :action => 'destroy', :id => @category %>
+ <%= submit_tag _('Delete'), :class => "button-small" %>
+ <%= end_form_tag %>
+ </td>
+ </tr>
+ <% end %>
+<% end %>
+</table>
+<hr />
+
+<%= start_form_tag :action => 'add_issue_category', :id => @project %>
+<%= error_messages_for 'issue_category' %>
+<label for="issue_category_name"><%=_('New category')%></label><br/>
+<%= text_field 'issue_category', 'name', :size => 25 %>
+<%= submit_tag _('Create') %>
+<%= end_form_tag %>
+
+</div>
diff --git a/redmine/app/views/projects/show.rhtml b/redmine/app/views/projects/show.rhtml
new file mode 100644
index 000000000..3f10654f5
--- /dev/null
+++ b/redmine/app/views/projects/show.rhtml
@@ -0,0 +1,53 @@
+<h2><%=_('Overview')%></h2>
+
+<div class="splitcontentleft">
+ <%= @project.descr %>
+ <ul>
+ <li><%=_('Homepage')%>: <%= link_to @project.homepage, @project.homepage %></li>
+ <li><%=_('Created on')%>: <%= format_date(@project.created_on) %></li>
+ </ul>
+
+ <div class="box">
+ <h3><%= image_tag "tracker" %> <%=_('Trackers')%></h3>
+ <ul>
+ <% for tracker in Tracker.find_all %>
+ <li><%= link_to tracker.name, :controller => 'projects', :action => 'list_issues', :id => @project,
+ :set_filter => 1,
+ "issues.tracker_id" => tracker.id %>:
+ <%= tracker.issues.count(["project_id=?", @project.id]) %> <%=_('open')%>
+ </li>
+ <% end %>
+ </ul>
+ <%= link_to_if_authorized '&#187; ' + _('Report an issue'), :controller => 'projects', :action => 'add_issue', :id => @project %>
+ <br />
+ <center><small>[ <%= link_to _('View all issues'), :controller => 'projects', :action => 'list_issues', :id => @project, :set_filter => 1 %> ]</small></center>
+ </div>
+</div>
+
+<div class="splitcontentright">
+ <div class="box">
+ <h3><%= image_tag "users" %> <%=_('Members')%></h3>
+ <% for member in @members %>
+ <%= link_to_user member.user %> (<%= member.role.name %>)<br />
+ <% end %>
+ </div>
+
+ <div class="box">
+ <h3><%=_('Latest news')%></h3>
+ <% for news in @project.news %>
+ <p>
+ <b><%= news.title %></b> <small>(<%= link_to_user news.author %> <%= format_time(news.created_on) %>)</small><br />
+ <%= news.shortdescr %>
+ <small>[<%= link_to _('Read...'), :controller => 'news', :action => 'show', :id => news %>]</small>
+ </p>
+ <hr />
+ <% end %>
+ <center><small>[ <%= link_to _('View all news'), :controller => 'projects', :action => 'list_news', :id => @project %> ]</small></center>
+ </div>
+</div>
+
+
+
+
+
+
diff --git a/redmine/app/views/reports/_simple.rhtml b/redmine/app/views/reports/_simple.rhtml
new file mode 100644
index 000000000..5816284ec
--- /dev/null
+++ b/redmine/app/views/reports/_simple.rhtml
@@ -0,0 +1,34 @@
+<% col_width = 70 / (@statuses.length+3) %>
+
+<table border="0" cellspacing="1" cellpadding="2" width="100%">
+<tr>
+<td width="25%"></td>
+<% for status in @statuses %>
+ <td align="center" width="<%= col_width %>%" bgcolor="#<%= status.html_color %>"><small><%= status.name %></small></td>
+<% end %>
+<td align="center" width="<%= col_width %>%"><strong><%=_('Open')%></strong></td>
+<td align="center" width="<%= col_width %>%"><strong><%=_('Closed')%></strong></td>
+<td align="center" width="<%= col_width %>%"><strong><%=_('Total')%></strong></td>
+</tr>
+
+<% for row in rows %>
+<tr style="background-color:#CEE1ED">
+ <td><%= link_to row.name, :controller => 'projects', :action => 'list_issues', :id => @project,
+ :set_filter => 1,
+ "issues.#{field_name}" => row.id %></td>
+ <% for status in @statuses %>
+ <td align="center"><%= link_to (aggregate data, { field_name => row.id, "status_id" => status.id }),
+ :controller => 'projects', :action => 'list_issues', :id => @project,
+ :set_filter => 1,
+ "issues.status_id" => status.id,
+ "issues.#{field_name}" => row.id %></td>
+ <% end %>
+ <td align="center"><%= aggregate data, { field_name => row.id, "closed" => 0 } %></td>
+ <td align="center"><%= aggregate data, { field_name => row.id, "closed" => 1 } %></td>
+ <td align="center"><%= link_to (aggregate data, { field_name => row.id }),
+ :controller => 'projects', :action => 'list_issues', :id => @project,
+ :set_filter => 1,
+ "issues.#{field_name}" => row.id %></td>
+<% end %>
+</tr>
+</table> \ No newline at end of file
diff --git a/redmine/app/views/reports/issue_report.rhtml b/redmine/app/views/reports/issue_report.rhtml
new file mode 100644
index 000000000..32a1f55f2
--- /dev/null
+++ b/redmine/app/views/reports/issue_report.rhtml
@@ -0,0 +1,13 @@
+<h2><%=_('Reports')%></h2>
+
+<strong><%=_('Issues by tracker')%></strong>
+<%= render :partial => 'simple', :locals => { :data => @issues_by_tracker, :field_name => "tracker_id", :rows => @trackers } %>
+<br />
+
+<strong><%=_('Issues by priority')%></strong>
+<%= render :partial => 'simple', :locals => { :data => @issues_by_priority, :field_name => "priority_id", :rows => @priorities } %>
+<br />
+
+<strong><%=_('Issues by category')%></strong>
+<%= render :partial => 'simple', :locals => { :data => @issues_by_category, :field_name => "category_id", :rows => @categories } %>
+
diff --git a/redmine/app/views/roles/_form.rhtml b/redmine/app/views/roles/_form.rhtml
new file mode 100644
index 000000000..02eef4717
--- /dev/null
+++ b/redmine/app/views/roles/_form.rhtml
@@ -0,0 +1,22 @@
+<%= error_messages_for 'role' %>
+
+<!--[form:role]-->
+<p><label for="role_name"><%=_('Name')%> <span class="required">*</span></label><br />
+<%= text_field 'role', 'name' %></p>
+
+<strong><%=_('Permissions')%></strong>
+<% permissions = @permissions.group_by {|p| p.group_id } %>
+<% permissions.keys.sort.each do |group_id| %>
+<fieldset><legend><%= _(Permission::GROUPS[group_id]) %></legend>
+<% permissions[group_id].each do |p| %>
+ <div style="width:200px;float:left;"><%= check_box_tag "permission_ids[]", p.id, (@role.permissions.include? p) %>
+ <%= _(p.descr) %>
+ </div>
+<% end %>
+</fieldset>
+<% end %>
+<br />
+<a href="javascript:checkAll('role_form', true)"><%=_('Check all')%></a> |
+<a href="javascript:checkAll('role_form', false)"><%=_('Uncheck all')%></a><br />
+<!--[eoform:role]-->
+
diff --git a/redmine/app/views/roles/edit.rhtml b/redmine/app/views/roles/edit.rhtml
new file mode 100644
index 000000000..cffbe0119
--- /dev/null
+++ b/redmine/app/views/roles/edit.rhtml
@@ -0,0 +1,10 @@
+<h2><%=_('Role')%></h2>
+
+<%= start_form_tag ({ :action => 'edit', :id => @role }, :id => 'role_form') %>
+<%= render :partial => 'form' %>
+
+<br />
+<%= submit_tag _('Save') %>
+<%= end_form_tag %>
+
+
diff --git a/redmine/app/views/roles/list.rhtml b/redmine/app/views/roles/list.rhtml
new file mode 100644
index 000000000..146e45886
--- /dev/null
+++ b/redmine/app/views/roles/list.rhtml
@@ -0,0 +1,23 @@
+<h2><%=_('Roles')%></h2>
+
+<table border="0" cellspacing="1" cellpadding="2" class="listTableContent">
+ <tr class="ListHead">
+ <th><%=_('Role')%></th>
+ <th></th>
+ </tr>
+
+<% for role in @roles %>
+ <tr style="background-color:#CEE1ED">
+ <td><%= link_to role.name, :action => 'edit', :id => role %></td>
+ <td align="center">
+ <%= start_form_tag :action => 'destroy', :id => role %>
+ <%= submit_tag _('Delete'), :class => "button-small" %>
+ <%= end_form_tag %>
+ </tr>
+<% end %>
+</table>
+
+<%= pagination_links_full @role_pages %>
+<br />
+
+<%= link_to '&#187; ' + _('New role'), :action => 'new' %>
diff --git a/redmine/app/views/roles/new.rhtml b/redmine/app/views/roles/new.rhtml
new file mode 100644
index 000000000..c82fb2144
--- /dev/null
+++ b/redmine/app/views/roles/new.rhtml
@@ -0,0 +1,8 @@
+<h2><%=_('New role')%></h2>
+
+<%= start_form_tag ({ :action => 'new' }, :id => 'role_form') %>
+<%= render :partial => 'form' %>
+
+<br /><%= submit_tag _('Create') %>
+<%= end_form_tag %>
+
diff --git a/redmine/app/views/roles/workflow.rhtml b/redmine/app/views/roles/workflow.rhtml
new file mode 100644
index 000000000..652f6a0f1
--- /dev/null
+++ b/redmine/app/views/roles/workflow.rhtml
@@ -0,0 +1,70 @@
+<h2><%=_('Workflow setup')%></h2>
+
+<p><%=_('Select a workflow to edit')%>:</p>
+
+<%= start_form_tag ({:action => 'workflow'}, :method => 'get') %>
+<div style="float:left;margin-right:10px;">
+<p><label for="role_id"><%=_('Role')%></label><br/>
+<select id="role_id" name="role_id">
+ <%= options_from_collection_for_select @roles, "id", "name", (@role.id unless @role.nil?) %>
+</select></p>
+</div>
+
+<div>
+<p><label for="tracker_id"><%=_('Tracker')%></label><br/>
+<select id="tracker_id" name="tracker_id">
+ <%= options_from_collection_for_select @trackers, "id", "name", (@tracker.id unless @tracker.nil?) %>
+</select>
+
+<%= submit_tag _('Edit') %>
+</p>
+</div>
+<%= end_form_tag %>
+
+
+
+<% unless @tracker.nil? or @role.nil? %>
+<div class="box">
+ <%= form_tag ({:action => 'workflow', :role_id => @role, :tracker_id => @tracker }, :id => 'workflow_form' ) %>
+ <table>
+ <tr>
+ <td align="center"><strong><%=_('Issue status')%></strong></td>
+ <td align="center" colspan="<%= @statuses.length %>"><strong><%=_('New statuses allowed')%></strong></td>
+ </tr>
+ <tr>
+ <td></td>
+ <% for new_status in @statuses %>
+ <td width="60" align="center"><%= new_status.name %></td>
+ <% end %>
+ </tr>
+
+ <% for old_status in @statuses %>
+ <tr>
+ <td><%= old_status.name %></td>
+
+ <% for new_status in @statuses %>
+ <td align="center">
+
+ <input type="checkbox"
+ name="issue_status[<%= old_status.id %>][]"
+ value="<%= new_status.id %>"
+ <%if old_status.new_statuses_allowed_to(@role, @tracker).include? new_status%>checked="checked"<%end%>
+ <%if old_status==new_status%>disabled<%end%>
+ >
+ </td>
+ <% end %>
+
+ </tr>
+ <% end %>
+ </table>
+<br />
+<p>
+<a href="javascript:checkAll('workflow_form', true)"><%=_('Check all')%></a> |
+<a href="javascript:checkAll('workflow_form', false)"><%=_('Uncheck all')%></a>
+</p>
+<br />
+<%= submit_tag _('Save') %>
+<%= end_form_tag %>
+
+<% end %>
+</div> \ No newline at end of file
diff --git a/redmine/app/views/trackers/_form.rhtml b/redmine/app/views/trackers/_form.rhtml
new file mode 100644
index 000000000..95d06d777
--- /dev/null
+++ b/redmine/app/views/trackers/_form.rhtml
@@ -0,0 +1,10 @@
+<%= error_messages_for 'tracker' %>
+
+<!--[form:tracker]-->
+<p><label for="tracker_name"><%=_('Name')%></label><br/>
+<%= text_field 'tracker', 'name' %></p>
+
+<p><%= check_box 'tracker', 'is_in_chlog' %>
+<label for="tracker_is_in_chlog"><%=_('View issues in change log')%></label></p>
+<!--[eoform:tracker]-->
+
diff --git a/redmine/app/views/trackers/edit.rhtml b/redmine/app/views/trackers/edit.rhtml
new file mode 100644
index 000000000..a209b3f38
--- /dev/null
+++ b/redmine/app/views/trackers/edit.rhtml
@@ -0,0 +1,6 @@
+<h2><%=_('Tracker')%></h2>
+
+<%= start_form_tag :action => 'edit', :id => @tracker %>
+ <%= render :partial => 'form' %>
+ <%= submit_tag _('Save') %>
+<%= end_form_tag %>
diff --git a/redmine/app/views/trackers/list.rhtml b/redmine/app/views/trackers/list.rhtml
new file mode 100644
index 000000000..3622a40f9
--- /dev/null
+++ b/redmine/app/views/trackers/list.rhtml
@@ -0,0 +1,24 @@
+<h2><%=_('Trackers')%></h2>
+
+<table border="0" cellspacing="1" cellpadding="2" class="listTableContent">
+ <tr class="ListHead">
+ <th><%=_('Tracker')%></th>
+ <th></th>
+ </tr>
+
+<% for tracker in @trackers %>
+ <tr style="background-color:#CEE1ED">
+ <td><%= link_to tracker.name, :action => 'edit', :id => tracker %></td>
+ <td align="center">
+ <%= start_form_tag :action => 'destroy', :id => tracker %>
+ <%= submit_tag _('Delete'), :class => "button-small" %>
+ <%= end_form_tag %>
+ </td>
+ </tr>
+<% end %>
+</table>
+
+<%= pagination_links_full @tracker_pages %>
+<br />
+
+<%= link_to '&#187; ' + _('New tracker'), :action => 'new' %>
diff --git a/redmine/app/views/trackers/new.rhtml b/redmine/app/views/trackers/new.rhtml
new file mode 100644
index 000000000..a24fca738
--- /dev/null
+++ b/redmine/app/views/trackers/new.rhtml
@@ -0,0 +1,7 @@
+<h2><%=_('New tracker')%></h2>
+
+<%= start_form_tag :action => 'new' %>
+ <%= render :partial => 'form' %>
+ <%= submit_tag _('Create') %>
+<%= end_form_tag %>
+
diff --git a/redmine/app/views/users/_form.rhtml b/redmine/app/views/users/_form.rhtml
new file mode 100644
index 000000000..92f1e0e5a
--- /dev/null
+++ b/redmine/app/views/users/_form.rhtml
@@ -0,0 +1,28 @@
+<%= error_messages_for 'user' %>
+
+<!--[form:user]-->
+<p><label for="user_login"><%=_('Login')%></label><br/>
+<%= text_field 'user', 'login' %></p>
+
+<p><label for="user_password"><%=_('Password')%></label><br/>
+<%= password_field 'user', 'password' %></p>
+
+<p><label for="user_firstname"><%=_('Firstname')%></label><br/>
+<%= text_field 'user', 'firstname' %></p>
+
+<p><label for="user_lastname"><%=_('Lastname')%></label><br/>
+<%= text_field 'user', 'lastname' %></p>
+
+<p><label for="user_mail"><%=_('Mail')%></label><br/>
+<%= text_field 'user', 'mail' %></p>
+
+<p><label for="user_language"><%=_('Language')%></label><br/>
+<%= select("user", "language", Localization.langs) %></p>
+
+<p><%= check_box 'user', 'admin' %> <label for="user_admin"><%=_('Administrator')%></label></p>
+
+<p><%= check_box 'user', 'mail_notification' %> <label for="user_mail_notification"><%=_('Mail notifications')%></label></p>
+
+<p><%= check_box 'user', 'locked' %> <label for="user_locked"><%=_('Locked')%></label></p>
+<!--[eoform:user]-->
+
diff --git a/redmine/app/views/users/add.rhtml b/redmine/app/views/users/add.rhtml
new file mode 100644
index 000000000..9d0ba8d3a
--- /dev/null
+++ b/redmine/app/views/users/add.rhtml
@@ -0,0 +1,6 @@
+<h2><%=_('New user')%></h2>
+
+<%= start_form_tag :action => 'add' %>
+ <%= render :partial => 'form' %>
+ <%= submit_tag _('Create') %>
+<%= end_form_tag %>
diff --git a/redmine/app/views/users/edit.rhtml b/redmine/app/views/users/edit.rhtml
new file mode 100644
index 000000000..a033a7fda
--- /dev/null
+++ b/redmine/app/views/users/edit.rhtml
@@ -0,0 +1,7 @@
+<h2><%=_('User')%></h2>
+
+<%= start_form_tag :action => 'edit', :id => @user %>
+ <%= render :partial => 'form' %>
+ <%= submit_tag _('Save') %>
+
+<%= end_form_tag %>
diff --git a/redmine/app/views/users/list.rhtml b/redmine/app/views/users/list.rhtml
new file mode 100644
index 000000000..b1bf937aa
--- /dev/null
+++ b/redmine/app/views/users/list.rhtml
@@ -0,0 +1,46 @@
+<h2><%=_('Users')%></h2>
+
+<table border="0" cellspacing="1" cellpadding="2" class="listTableContent">
+<tr class="ListHead">
+ <%= sort_header_tag('users.login', :caption => _('Login')) %>
+ <%= sort_header_tag('users.firstname', :caption => _('Firstname')) %>
+ <%= sort_header_tag('users.lastname', :caption => _('Lastname')) %>
+ <th><%=_('Mail')%></th>
+ <%= sort_header_tag('users.admin', :caption => _('Admin')) %>
+ <%= sort_header_tag('users.locked', :caption => _('Locked')) %>
+ <%= sort_header_tag('users.created_on', :caption => _('Created on')) %>
+ <%= sort_header_tag('users.last_login_on', :caption => _('Last login')) %>
+ <th></th>
+</tr>
+<% for user in @users %>
+ <tr style="background-color:#CEE1ED">
+ <td><%= link_to user.login, :action => 'edit', :id => user %></td>
+ <td><%= user.firstname %></td>
+ <td><%= user.lastname %></td>
+ <td><%= user.mail %></td>
+ <td align="center"><%= image_tag 'true' if user.admin? %></td>
+ <td align="center"><%= image_tag 'locked' if user.locked? %></td>
+ <td align="center"><%= format_time(user.created_on) %></td>
+ <td align="center"><%= format_time(user.last_login_on) unless user.last_login_on.nil? %></td>
+ <td align="center">
+ <%= start_form_tag :action => 'edit', :id => user %>
+ <% if user.locked? %>
+ <%= hidden_field_tag 'user[locked]', false %>
+ <%= submit_tag _('Unlock'), :class => "button-small" %>
+ <% else %>
+ <%= hidden_field_tag 'user[locked]', true %>
+ <%= submit_tag _('Lock'), :class => "button-small" %>
+ <% end %>
+ <%= end_form_tag %>
+ </td>
+ </tr>
+<% end %>
+</table>
+
+<p><%= pagination_links_full @user_pages %>
+[ <%= @user_pages.current.first_item %> - <%= @user_pages.current.last_item %> / <%= @user_count %> ]
+</p>
+
+<p>
+<%= link_to '&#187; ' + _('New user'), :action => 'add' %>
+</p> \ No newline at end of file
diff --git a/redmine/app/views/versions/_form.rhtml b/redmine/app/views/versions/_form.rhtml
new file mode 100644
index 000000000..189e106cc
--- /dev/null
+++ b/redmine/app/views/versions/_form.rhtml
@@ -0,0 +1,13 @@
+<%= error_messages_for 'version' %>
+
+<!--[form:version]-->
+<p><label for="version_name"><%=_('Version')%></label><br/>
+<%= text_field 'version', 'name', :size => 20 %></p>
+
+<p><label for="version_descr"><%=_('Description')%></label><br/>
+<%= text_field 'version', 'descr', :size => 60 %></p>
+
+<p><label for="version_date"><%=_('Date')%></label><br/>
+<%= date_select 'version', 'date' %></p>
+<!--[eoform:version]-->
+
diff --git a/redmine/app/views/versions/edit.rhtml b/redmine/app/views/versions/edit.rhtml
new file mode 100644
index 000000000..6db4daa00
--- /dev/null
+++ b/redmine/app/views/versions/edit.rhtml
@@ -0,0 +1,8 @@
+<h2><%=_('Version')%></h2>
+
+<%= start_form_tag :action => 'edit', :id => @version %>
+ <%= render :partial => 'form' %>
+ <%= submit_tag _('Save') %>
+<%= end_form_tag %>
+
+
diff --git a/redmine/app/views/welcome/index.rhtml b/redmine/app/views/welcome/index.rhtml
new file mode 100644
index 000000000..cbffa82ed
--- /dev/null
+++ b/redmine/app/views/welcome/index.rhtml
@@ -0,0 +1,30 @@
+<div class="splitcontentleft">
+ <h2><%=_('Welcome')%> !</h2>
+
+ <div class="box">
+ <h3>Latest news</h3>
+ <% for news in @news %>
+ <p>
+ <b><%= news.title %></b> (<%= link_to_user news.author %> <%= format_time(news.created_on) %> - <%= news.project.name %>)<br />
+ <%= news.shortdescr %><br />
+ [<%= link_to 'Read...', :controller => 'news', :action => 'show', :id => news %>]
+ </p>
+ <hr />
+ <% end %>
+ </div>
+</div>
+
+<div class="splitcontentright">
+ <div class="box">
+ <h3>Latest projects</h3>
+ <ul>
+ <% for project in @projects %>
+ <li>
+ <%= link_to project.name, :controller => 'projects', :action => 'show', :id => project %> (added <%= format_time(project.created_on) %>)<br />
+ <%= project.descr %>
+ </li>
+ <% end %>
+ </ul>
+ </div>
+
+</div>
diff --git a/redmine/config/boot.rb b/redmine/config/boot.rb
new file mode 100644
index 000000000..9fcd50fe3
--- /dev/null
+++ b/redmine/config/boot.rb
@@ -0,0 +1,19 @@
+# Don't change this file. Configuration is done in config/environment.rb and config/environments/*.rb
+
+unless defined?(RAILS_ROOT)
+ root_path = File.join(File.dirname(__FILE__), '..')
+ unless RUBY_PLATFORM =~ /mswin32/
+ require 'pathname'
+ root_path = Pathname.new(root_path).cleanpath(true).to_s
+ end
+ RAILS_ROOT = root_path
+end
+
+if File.directory?("#{RAILS_ROOT}/vendor/rails")
+ require "#{RAILS_ROOT}/vendor/rails/railties/lib/initializer"
+else
+ require 'rubygems'
+ require 'initializer'
+end
+
+Rails::Initializer.run(:set_load_path)
diff --git a/redmine/config/database.yml b/redmine/config/database.yml
new file mode 100644
index 000000000..1702510d0
--- /dev/null
+++ b/redmine/config/database.yml
@@ -0,0 +1,32 @@
+# MySQL (default setup). Versions 4.1 and 5.0 are recommended.
+#
+# Get the fast C bindings:
+# gem install mysql
+# (on OS X: gem install mysql -- --include=/usr/local/lib)
+# And be sure to use new-style password hashing:
+# http://dev.mysql.com/doc/refman/5.0/en/old-client.html
+development:
+ adapter: mysql
+ database: redmine_development
+ host: localhost
+ username: root
+ password:
+
+test:
+ adapter: mysql
+ database: redmine_test
+ host: localhost
+ username: root
+ password:
+
+demo:
+ adapter: sqlite3
+ dbfile: db/redmine_demo.db
+
+production:
+ adapter: mysql
+ database: redmine
+ host: localhost
+ username: root
+ password:
+ \ No newline at end of file
diff --git a/redmine/config/environment.rb b/redmine/config/environment.rb
new file mode 100644
index 000000000..de12e7c34
--- /dev/null
+++ b/redmine/config/environment.rb
@@ -0,0 +1,85 @@
+# Be sure to restart your web server when you modify this file.
+
+# Uncomment below to force Rails into production mode when
+# you don't control web/app server and can't set it the proper way
+# ENV['RAILS_ENV'] ||= 'production'
+
+# Bootstrap the Rails environment, frameworks, and default configuration
+require File.join(File.dirname(__FILE__), 'boot')
+
+Rails::Initializer.run do |config|
+ # Settings in config/environments/* take precedence those specified here
+
+ # Skip frameworks you're not going to use
+ # config.frameworks -= [ :action_web_service, :action_mailer ]
+
+ # Add additional load paths for your own custom dirs
+ # config.load_paths += %W( #{RAILS_ROOT}/extras )
+
+ # Force all environments to use the same logger level
+ # (by default production uses :info, the others :debug)
+ # config.log_level = :debug
+
+ # Use the database for sessions instead of the file system
+ # (create the session table with 'rake create_sessions_table')
+ # config.action_controller.session_store = :active_record_store
+
+ # Enable page/fragment caching by setting a file-based store
+ # (remember to create the caching directory and make it readable to the application)
+ # config.action_controller.fragment_cache_store = :file_store, "#{RAILS_ROOT}/cache"
+
+ # Activate observers that should always be running
+ # config.active_record.observers = :cacher, :garbage_collector
+
+ # Make Active Record use UTC-base instead of local time
+ # config.active_record.default_timezone = :utc
+
+ # Use Active Record's schema dumper instead of SQL when creating the test database
+ # (enables use of different database adapters for development and test environments)
+ # config.active_record.schema_format = :ruby
+
+ # See Rails::Configuration for more options
+
+ # SMTP server configuration
+ config.action_mailer.server_settings = {
+ :address => "127.0.0.1",
+ :port => 25,
+ :domain => "somenet.foo",
+ :authentication => :login,
+ :user_name => "redmine",
+ :password => "redmine",
+ }
+
+ config.action_mailer.perform_deliveries = true
+
+ # Tell ActionMailer not to deliver emails to the real world.
+ # The :test delivery method accumulates sent emails in the
+ # ActionMailer::Base.deliveries array.
+ #config.action_mailer.delivery_method = :test
+ config.action_mailer.delivery_method = :smtp
+end
+
+# Add new inflection rules using the following format
+# (all these examples are active by default):
+# Inflector.inflections do |inflect|
+# inflect.plural /^(ox)$/i, '\1en'
+# inflect.singular /^(ox)en/i, '\1'
+# inflect.irregular 'person', 'people'
+# inflect.uncountable %w( fish sheep )
+# end
+
+# Include your application configuration below
+
+# application name
+RDM_APP_NAME = "redMine"
+# application version
+RDM_APP_VERSION = "0.1.0"
+# application host name
+RDM_HOST_NAME = "somenet.foo"
+# file storage path
+RDM_STORAGE_PATH = "#{RAILS_ROOT}/files"
+# if RDM_LOGIN_REQUIRED is set to true, login is required to access the application
+RDM_LOGIN_REQUIRED = false
+# default langage
+RDM_DEFAULT_LANG = 'en'
+
diff --git a/redmine/config/environments/demo.rb b/redmine/config/environments/demo.rb
new file mode 100644
index 000000000..52aef32f1
--- /dev/null
+++ b/redmine/config/environments/demo.rb
@@ -0,0 +1,21 @@
+# Settings specified here will take precedence over those in config/environment.rb
+
+# The production environment is meant for finished, "live" apps.
+# Code is not reloaded between requests
+config.cache_classes = true
+
+# Use a different logger for distributed setups
+# config.logger = SyslogLogger.new
+config.log_level = :info
+
+# Full error reports are disabled and caching is turned on
+config.action_controller.consider_all_requests_local = false
+config.action_controller.perform_caching = true
+
+# Enable serving of images, stylesheets, and javascripts from an asset server
+# config.action_controller.asset_host = "http://assets.example.com"
+
+# Disable mail delivery
+config.action_mailer.perform_deliveries = false
+config.action_mailer.raise_delivery_errors = false
+
diff --git a/redmine/config/environments/development.rb b/redmine/config/environments/development.rb
new file mode 100644
index 000000000..04b779200
--- /dev/null
+++ b/redmine/config/environments/development.rb
@@ -0,0 +1,19 @@
+# Settings specified here will take precedence over those in config/environment.rb
+
+# In the development environment your application's code is reloaded on
+# every request. This slows down response time but is perfect for development
+# since you don't have to restart the webserver when you make code changes.
+config.cache_classes = false
+
+# Log error messages when you accidentally call methods on nil.
+config.whiny_nils = true
+
+# Enable the breakpoint server that script/breakpointer connects to
+config.breakpoint_server = true
+
+# Show full error reports and disable caching
+config.action_controller.consider_all_requests_local = true
+config.action_controller.perform_caching = false
+
+# Don't care if the mailer can't send
+config.action_mailer.raise_delivery_errors = false
diff --git a/redmine/config/environments/production.rb b/redmine/config/environments/production.rb
new file mode 100644
index 000000000..4cd4e086b
--- /dev/null
+++ b/redmine/config/environments/production.rb
@@ -0,0 +1,20 @@
+# Settings specified here will take precedence over those in config/environment.rb
+
+# The production environment is meant for finished, "live" apps.
+# Code is not reloaded between requests
+config.cache_classes = true
+
+# Use a different logger for distributed setups
+# config.logger = SyslogLogger.new
+
+
+# Full error reports are disabled and caching is turned on
+config.action_controller.consider_all_requests_local = false
+config.action_controller.perform_caching = true
+
+# Enable serving of images, stylesheets, and javascripts from an asset server
+# config.action_controller.asset_host = "http://assets.example.com"
+
+# Disable delivery errors if you bad email addresses should just be ignored
+config.action_mailer.raise_delivery_errors = false
+
diff --git a/redmine/config/environments/test.rb b/redmine/config/environments/test.rb
new file mode 100644
index 000000000..0b34ef19a
--- /dev/null
+++ b/redmine/config/environments/test.rb
@@ -0,0 +1,15 @@
+# Settings specified here will take precedence over those in config/environment.rb
+
+# The test environment is used exclusively to run your application's
+# test suite. You never need to work with it otherwise. Remember that
+# your test database is "scratch space" for the test suite and is wiped
+# and recreated between test runs. Don't rely on the data there!
+config.cache_classes = true
+
+# Log error messages when you accidentally call methods on nil.
+config.whiny_nils = true
+
+# Show full error reports and disable caching
+config.action_controller.consider_all_requests_local = true
+config.action_controller.perform_caching = false
+
diff --git a/redmine/config/help.yml b/redmine/config/help.yml
new file mode 100644
index 000000000..a48798e03
--- /dev/null
+++ b/redmine/config/help.yml
@@ -0,0 +1,21 @@
+# administration
+admin:
+ index: administration.html
+ mail_options: administration.html#mail_notifications
+ info: administration.html#app_info
+users:
+ index: administration.html#users
+roles:
+ index: administration.html#roles
+ workflow: administration.html#workflow
+trackers:
+ index: administration.html#trackers
+issue_statuses:
+ index: administration.html#issue_statuses
+
+# projects
+projects:
+ add: projects.html#settings
+
+
+# issues \ No newline at end of file
diff --git a/redmine/config/routes.rb b/redmine/config/routes.rb
new file mode 100644
index 000000000..2559159f1
--- /dev/null
+++ b/redmine/config/routes.rb
@@ -0,0 +1,24 @@
+ActionController::Routing::Routes.draw do |map|
+ # Add your own custom routes here.
+ # The priority is based upon order of creation: first created -> highest priority.
+
+ # Here's a sample route:
+ # map.connect 'products/:id', :controller => 'catalog', :action => 'view'
+ # Keep in mind you can assign values other than :controller and :action
+
+ # You can have the root of your site routed by hooking up ''
+ # -- just remember to delete public/index.html.
+ map.connect '', :controller => "welcome"
+
+ map.connect 'roles/workflow/:id/:role_id/:tracker_id', :controller => 'roles', :action => 'workflow'
+ map.connect 'help/:ctrl/:page', :controller => 'help'
+ map.connect ':controller/:action/:id/:sort_key/:sort_order'
+
+ # Allow downloading Web Service WSDL as a file with an extension
+ # instead of a file named 'wsdl'
+ map.connect ':controller/service.wsdl', :action => 'wsdl'
+
+
+ # Install the default route as the lowest priority.
+ map.connect ':controller/:action/:id'
+end
diff --git a/redmine/db/migrate/001_setup.rb b/redmine/db/migrate/001_setup.rb
new file mode 100644
index 000000000..e6288f61d
--- /dev/null
+++ b/redmine/db/migrate/001_setup.rb
@@ -0,0 +1,254 @@
+class Setup < ActiveRecord::Migration
+ def self.up
+ create_table "attachments", :force => true do |t|
+ t.column "container_id", :integer, :default => 0, :null => false
+ t.column "container_type", :string, :limit => 30, :default => "", :null => false
+ t.column "filename", :string, :default => "", :null => false
+ t.column "disk_filename", :string, :default => "", :null => false
+ t.column "size", :integer, :default => 0, :null => false
+ t.column "content_type", :string, :limit => 60, :default => "", :null => false
+ t.column "digest", :string, :limit => 40, :default => "", :null => false
+ t.column "downloads", :integer, :default => 0, :null => false
+ t.column "author_id", :integer, :default => 0, :null => false
+ t.column "created_on", :timestamp
+ end
+
+ create_table "custom_fields", :force => true do |t|
+ t.column "name", :string, :limit => 30, :default => "", :null => false
+ t.column "typ", :integer, :limit => 6, :default => 0, :null => false
+ t.column "is_required", :boolean, :default => false, :null => false
+ t.column "is_for_all", :boolean, :default => false, :null => false
+ t.column "possible_values", :text, :default => "", :null => false
+ t.column "regexp", :string, :default => "", :null => false
+ t.column "min_length", :integer, :limit => 4, :default => 0, :null => false
+ t.column "max_length", :integer, :limit => 4, :default => 0, :null => false
+ end
+
+ create_table "custom_fields_projects", :id => false, :force => true do |t|
+ t.column "custom_field_id", :integer, :default => 0, :null => false
+ t.column "project_id", :integer, :default => 0, :null => false
+ end
+
+ create_table "custom_values", :force => true do |t|
+ t.column "issue_id", :integer, :default => 0, :null => false
+ t.column "custom_field_id", :integer, :default => 0, :null => false
+ t.column "value", :text, :default => "", :null => false
+ end
+
+ add_index "custom_values", ["issue_id"], :name => "custom_values_issue_id"
+
+ create_table "documents", :force => true do |t|
+ t.column "project_id", :integer, :default => 0, :null => false
+ t.column "category_id", :integer, :default => 0, :null => false
+ t.column "title", :string, :limit => 60, :default => "", :null => false
+ t.column "descr", :text, :default => "", :null => false
+ t.column "created_on", :timestamp
+ end
+
+ create_table "enumerations", :force => true do |t|
+ t.column "opt", :string, :limit => 4, :default => "", :null => false
+ t.column "name", :string, :limit => 30, :default => "", :null => false
+ end
+
+ create_table "issue_categories", :force => true do |t|
+ t.column "project_id", :integer, :default => 0, :null => false
+ t.column "name", :string, :limit => 30, :default => "", :null => false
+ end
+
+ create_table "issue_histories", :force => true do |t|
+ t.column "issue_id", :integer, :default => 0, :null => false
+ t.column "status_id", :integer, :default => 0, :null => false
+ t.column "author_id", :integer, :default => 0, :null => false
+ t.column "notes", :text, :default => "", :null => false
+ t.column "created_on", :timestamp
+ end
+
+ add_index "issue_histories", ["issue_id"], :name => "issue_histories_issue_id"
+
+ create_table "issue_statuses", :force => true do |t|
+ t.column "name", :string, :limit => 30, :default => "", :null => false
+ t.column "is_closed", :boolean, :default => false, :null => false
+ t.column "is_default", :boolean, :default => false, :null => false
+ t.column "html_color", :string, :limit => 6, :default => "FFFFFF", :null => false
+ end
+
+ create_table "issues", :force => true do |t|
+ t.column "tracker_id", :integer, :default => 0, :null => false
+ t.column "project_id", :integer, :default => 0, :null => false
+ t.column "subject", :string, :default => "", :null => false
+ t.column "descr", :text, :default => "", :null => false
+ t.column "category_id", :integer
+ t.column "status_id", :integer, :default => 0, :null => false
+ t.column "assigned_to_id", :integer
+ t.column "priority_id", :integer, :default => 0, :null => false
+ t.column "fixed_version_id", :integer
+ t.column "author_id", :integer, :default => 0, :null => false
+ t.column "created_on", :timestamp
+ t.column "updated_on", :timestamp
+ end
+
+ add_index "issues", ["project_id"], :name => "issues_project_id"
+
+ create_table "members", :force => true do |t|
+ t.column "user_id", :integer, :default => 0, :null => false
+ t.column "project_id", :integer, :default => 0, :null => false
+ t.column "role_id", :integer, :default => 0, :null => false
+ t.column "created_on", :timestamp
+ end
+
+ create_table "news", :force => true do |t|
+ t.column "project_id", :integer
+ t.column "title", :string, :limit => 60, :default => "", :null => false
+ t.column "shortdescr", :string, :default => "", :null => false
+ t.column "descr", :text, :default => "", :null => false
+ t.column "author_id", :integer, :default => 0, :null => false
+ t.column "created_on", :timestamp
+ end
+
+ create_table "permissions", :force => true do |t|
+ t.column "controller", :string, :limit => 30, :default => "", :null => false
+ t.column "action", :string, :limit => 30, :default => "", :null => false
+ t.column "descr", :string, :limit => 60, :default => "", :null => false
+ t.column "public", :boolean, :default => false, :null => false
+ t.column "sort", :integer, :default => 0, :null => false
+ t.column "mail_option", :boolean, :default => false, :null => false
+ t.column "mail_enabled", :boolean, :default => false, :null => false
+ end
+
+ create_table "permissions_roles", :id => false, :force => true do |t|
+ t.column "permission_id", :integer, :default => 0, :null => false
+ t.column "role_id", :integer, :default => 0, :null => false
+ end
+
+ add_index "permissions_roles", ["role_id"], :name => "permissions_roles_role_id"
+
+ create_table "projects", :force => true do |t|
+ t.column "name", :string, :limit => 30, :default => "", :null => false
+ t.column "descr", :string, :default => "", :null => false
+ t.column "homepage", :string, :limit => 60, :default => "", :null => false
+ t.column "public", :boolean, :default => true, :null => false
+ t.column "created_on", :timestamp
+ t.column "updated_on", :timestamp
+ end
+
+ create_table "roles", :force => true do |t|
+ t.column "name", :string, :limit => 30, :default => "", :null => false
+ end
+
+ create_table "trackers", :force => true do |t|
+ t.column "name", :string, :limit => 30, :default => "", :null => false
+ t.column "is_in_chlog", :boolean, :default => false, :null => false
+ end
+
+ create_table "users", :force => true do |t|
+ t.column "login", :string, :limit => 30, :default => "", :null => false
+ t.column "hashed_password", :string, :limit => 40, :default => "", :null => false
+ t.column "firstname", :string, :limit => 30, :default => "", :null => false
+ t.column "lastname", :string, :limit => 30, :default => "", :null => false
+ t.column "mail", :string, :limit => 60, :default => "", :null => false
+ t.column "mail_notification", :boolean, :default => true, :null => false
+ t.column "admin", :boolean, :default => false, :null => false
+ t.column "locked", :boolean, :default => false, :null => false
+ t.column "last_login_on", :datetime
+ t.column "language", :string, :limit => 2, :default => "", :null => false
+ t.column "created_on", :timestamp
+ t.column "updated_on", :timestamp
+ end
+
+ create_table "versions", :force => true do |t|
+ t.column "project_id", :integer, :default => 0, :null => false
+ t.column "name", :string, :limit => 30, :default => "", :null => false
+ t.column "descr", :string, :default => "", :null => false
+ t.column "date", :date, :null => false
+ t.column "created_on", :timestamp
+ t.column "updated_on", :timestamp
+ end
+
+ create_table "workflows", :force => true do |t|
+ t.column "tracker_id", :integer, :default => 0, :null => false
+ t.column "old_status_id", :integer, :default => 0, :null => false
+ t.column "new_status_id", :integer, :default => 0, :null => false
+ t.column "role_id", :integer, :default => 0, :null => false
+ end
+
+ # project
+ Permission.create :controller => "projects", :action => "show", :descr => "Overview", :sort => 100, :public => true
+ Permission.create :controller => "projects", :action => "changelog", :descr => "View change log", :sort => 105, :public => true
+ Permission.create :controller => "reports", :action => "issue_report", :descr => "View reports", :sort => 110, :public => true
+ Permission.create :controller => "projects", :action => "settings", :descr => "Settings", :sort => 150
+ Permission.create :controller => "projects", :action => "edit", :descr => "Edit", :sort => 151
+ # members
+ Permission.create :controller => "projects", :action => "list_members", :descr => "View list", :sort => 200, :public => true
+ Permission.create :controller => "projects", :action => "add_member", :descr => "New member", :sort => 220
+ Permission.create :controller => "members", :action => "edit", :descr => "Edit", :sort => 221
+ Permission.create :controller => "members", :action => "destroy", :descr => "Delete", :sort => 222
+ # versions
+ Permission.create :controller => "projects", :action => "add_version", :descr => "New version", :sort => 320
+ Permission.create :controller => "versions", :action => "edit", :descr => "Edit", :sort => 321
+ Permission.create :controller => "versions", :action => "destroy", :descr => "Delete", :sort => 322
+ # issue categories
+ Permission.create :controller => "projects", :action => "add_issue_category", :descr => "New issue category", :sort => 420
+ Permission.create :controller => "issue_categories", :action => "edit", :descr => "Edit", :sort => 421
+ Permission.create :controller => "issue_categories", :action => "destroy", :descr => "Delete", :sort => 422
+ # issues
+ Permission.create :controller => "projects", :action => "list_issues", :descr => "View list", :sort => 1000, :public => true
+ Permission.create :controller => "issues", :action => "show", :descr => "View", :sort => 1005, :public => true
+ Permission.create :controller => "issues", :action => "download", :descr => "Download file", :sort => 1010, :public => true
+ Permission.create :controller => "projects", :action => "add_issue", :descr => "Report an issue", :sort => 1050, :mail_option => 1, :mail_enabled => 1
+ Permission.create :controller => "issues", :action => "edit", :descr => "Edit", :sort => 1055
+ Permission.create :controller => "issues", :action => "change_status", :descr => "Change status", :sort => 1060, :mail_option => 1, :mail_enabled => 1
+ Permission.create :controller => "issues", :action => "destroy", :descr => "Delete", :sort => 1065
+ Permission.create :controller => "issues", :action => "add_attachment", :descr => "Add file", :sort => 1070
+ Permission.create :controller => "issues", :action => "destroy_attachment", :descr => "Delete file", :sort => 1075
+ # news
+ Permission.create :controller => "projects", :action => "list_news", :descr => "View list", :sort => 1100, :public => true
+ Permission.create :controller => "news", :action => "show", :descr => "View", :sort => 1101, :public => true
+ Permission.create :controller => "projects", :action => "add_news", :descr => "Add", :sort => 1120
+ Permission.create :controller => "news", :action => "edit", :descr => "Edit", :sort => 1121
+ Permission.create :controller => "news", :action => "destroy", :descr => "Delete", :sort => 1122
+ # documents
+ Permission.create :controller => "projects", :action => "list_documents", :descr => "View list", :sort => 1200, :public => true
+ Permission.create :controller => "documents", :action => "show", :descr => "View", :sort => 1201, :public => true
+ Permission.create :controller => "documents", :action => "download", :descr => "Download", :sort => 1202, :public => true
+ Permission.create :controller => "projects", :action => "add_document", :descr => "Add", :sort => 1220
+ Permission.create :controller => "documents", :action => "edit", :descr => "Edit", :sort => 1221
+ Permission.create :controller => "documents", :action => "destroy", :descr => "Delete", :sort => 1222
+ Permission.create :controller => "documents", :action => "add_attachment", :descr => "Add file", :sort => 1223
+ Permission.create :controller => "documents", :action => "destroy_attachment", :descr => "Delete file", :sort => 1224
+ # files
+ Permission.create :controller => "projects", :action => "list_files", :descr => "View list", :sort => 1300, :public => true
+ Permission.create :controller => "versions", :action => "download", :descr => "Download", :sort => 1301, :public => true
+ Permission.create :controller => "projects", :action => "add_file", :descr => "Add", :sort => 1320
+ Permission.create :controller => "versions", :action => "destroy_file", :descr => "Delete", :sort => 1322
+
+ # create default administrator account
+ user = User.create :login => "admin", :password => "admin", :firstname => "redMine", :lastname => "Admin", :mail => "admin@somenet.foo", :mail_notification => true, :language => "en"
+ user.admin = true
+ user.save
+
+
+ end
+
+ def self.down
+ drop_table :attachments
+ drop_table :custom_fields
+ drop_table :custom_fields_projects
+ drop_table :custom_values
+ drop_table :documents
+ drop_table :enumerations
+ drop_table :issue_categories
+ drop_table :issue_histories
+ drop_table :issue_statuses
+ drop_table :issues
+ drop_table :members
+ drop_table :news
+ drop_table :permissions
+ drop_table :permissions_roles
+ drop_table :projects
+ drop_table :roles
+ drop_table :trackers
+ drop_table :users
+ drop_table :versions
+ drop_table :workflows
+ end
+end
diff --git a/redmine/db/migrate/002_default_configuration.rb b/redmine/db/migrate/002_default_configuration.rb
new file mode 100644
index 000000000..8f851dfbc
--- /dev/null
+++ b/redmine/db/migrate/002_default_configuration.rb
@@ -0,0 +1,44 @@
+class DefaultConfiguration < ActiveRecord::Migration
+ def self.up
+ # roles
+ r = Role.create(:name => "Manager")
+ r.permissions = Permission.find(:all)
+ r = Role.create :name => "Developer"
+ r.permissions = Permission.find([1, 2, 3, 6, 10, 11, 12, 16, 17, 18, 19, 20, 21, 23, 25, 26, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41])
+ r = Role.create :name => "Reporter"
+ r.permissions = Permission.find([1, 2, 3, 6, 16, 17, 18, 19, 20, 21, 23, 25, 26, 30, 31, 32, 38, 39])
+ # trackers
+ Tracker.create(:name => "Bug", :is_in_chlog => true)
+ Tracker.create(:name => "Feature request", :is_in_chlog => true)
+ Tracker.create(:name => "Support request", :is_in_chlog => false)
+ # issue statuses
+ IssueStatus.create(:name => "New", :is_closed => false, :is_default => true, :html_color => 'F98787')
+ IssueStatus.create(:name => "Assigned", :is_closed => false, :is_default => false, :html_color => 'C0C0FF')
+ IssueStatus.create(:name => "Resolved", :is_closed => false, :is_default => false, :html_color => '88E0B3')
+ IssueStatus.create(:name => "Feedback", :is_closed => false, :is_default => false, :html_color => 'F3A4F4')
+ IssueStatus.create(:name => "Closed", :is_closed => true, :is_default => false, :html_color => 'DBDBDB')
+ IssueStatus.create(:name => "Rejected", :is_closed => true, :is_default => false, :html_color => 'F5C28B')
+ # workflow
+ Tracker.find(:all).each { |t|
+ Role.find(:all).each { |r|
+ IssueStatus.find(:all).each { |os|
+ IssueStatus.find(:all).each { |ns|
+ Workflow.create(:tracker_id => t.id, :role_id => r.id, :old_status_id => os.id, :new_status_id => ns.id) unless os == ns
+ }
+ }
+ }
+ }
+ # enumeartions
+ Enumeration.create(:opt => "DCAT", :name => 'Uncategorized')
+ Enumeration.create(:opt => "DCAT", :name => 'User documentation')
+ Enumeration.create(:opt => "DCAT", :name => 'Technical documentation')
+ Enumeration.create(:opt => "IPRI", :name => 'Low')
+ Enumeration.create(:opt => "IPRI", :name => 'Normal')
+ Enumeration.create(:opt => "IPRI", :name => 'High')
+ Enumeration.create(:opt => "IPRI", :name => 'Urgent')
+ Enumeration.create(:opt => "IPRI", :name => 'Immediate')
+ end
+
+ def self.down
+ end
+end
diff --git a/redmine/db/redmine_demo.db b/redmine/db/redmine_demo.db
new file mode 100644
index 000000000..6a19ec527
--- /dev/null
+++ b/redmine/db/redmine_demo.db
Binary files differ
diff --git a/redmine/doc/CHANGELOG b/redmine/doc/CHANGELOG
new file mode 100644
index 000000000..8db6fc29b
--- /dev/null
+++ b/redmine/doc/CHANGELOG
@@ -0,0 +1,17 @@
+== redMine changelog
+
+redMine - project management software
+Copyright (C) 2006 Jean-Philippe Lang
+http://redmine.sourceforge.net/
+
+
+== 06/25/2006 - v0.1.0
+
+* multiple users/multiple projects
+* role based access control
+* issue tracking system
+* fully customizable workflow
+* documents/files repository
+* email notifications on issue creation and update
+* multilanguage support (except for error messages):english, french, spanish
+* online manual in french (unfinished) \ No newline at end of file
diff --git a/redmine/doc/COPYING b/redmine/doc/COPYING
new file mode 100644
index 000000000..82fa1daad
--- /dev/null
+++ b/redmine/doc/COPYING
@@ -0,0 +1,339 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it. (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.) You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must show them these terms so they know their
+rights.
+
+ We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+ Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+ Finally, any free program is threatened constantly by software
+patents. We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary. To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License. The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language. (Hereinafter, translation is included without limitation in
+the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+ 1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+ 2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) You must cause the modified files to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in
+ whole or in part contains or is derived from the Program or any
+ part thereof, to be licensed as a whole at no charge to all third
+ parties under the terms of this License.
+
+ c) If the modified program normally reads commands interactively
+ when run, you must cause it, when started running for such
+ interactive use in the most ordinary way, to print or display an
+ announcement including an appropriate copyright notice and a
+ notice that there is no warranty (or else, saying that you provide
+ a warranty) and that users may redistribute the program under
+ these conditions, and telling the user how to view a copy of this
+ License. (Exception: if the Program itself is interactive but
+ does not normally print such an announcement, your work based on
+ the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+ a) Accompany it with the complete corresponding machine-readable
+ source code, which must be distributed under the terms of Sections
+ 1 and 2 above on a medium customarily used for software interchange; or,
+
+ b) Accompany it with a written offer, valid for at least three
+ years, to give any third party, for a charge no more than your
+ cost of physically performing source distribution, a complete
+ machine-readable copy of the corresponding source code, to be
+ distributed under the terms of Sections 1 and 2 above on a medium
+ customarily used for software interchange; or,
+
+ c) Accompany it with the information you received as to the offer
+ to distribute corresponding source code. (This alternative is
+ allowed only for noncommercial distribution and only if you
+ received the program in object code or executable form with such
+ an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable. However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License. Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+ 5. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Program or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+ 6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+ 7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all. For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded. In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+ 9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation. If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+ 10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission. For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this. Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ 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.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) year name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+ <signature of Ty Coon>, 1 April 1989
+ Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs. If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
diff --git a/redmine/doc/INSTALL b/redmine/doc/INSTALL
new file mode 100644
index 000000000..72a993687
--- /dev/null
+++ b/redmine/doc/INSTALL
@@ -0,0 +1,61 @@
+== redMine installation
+
+redMine - project management software
+Copyright (C) 2006 Jean-Philippe Lang
+http://redmine.sourceforge.net/
+
+
+== Requirements
+
+* Ruby on Rails 1.1
+* Any database supported by Rails (developped using MySQL 5)
+* (recommended) Apache/Lighttpd with FCGI support
+
+
+== Installation
+
+1. Uncompress program archive:
+ tar zxvf <filename>
+
+2. Create an empty database: "redmine" for example
+
+3. Configure database parameters in config/database.yml
+ for "production" environment
+
+4. Create database structure. Under application main directory:
+ rake migrate RAILS_ENV="production"
+ It will create tables and default configuration data
+
+5. Test the installation by running WEBrick web server:
+ ruby script/server -e production
+
+ Once WEBrick has started, point your browser to http://localhost:3000/
+ You should now see the application welcome page
+
+6. Use default administrator account to log in:
+ login: admin
+ password: admin
+
+7. Setup Apache or Lighttpd with fastcgi for best performance.
+
+
+== Configuration
+
+You can setup a few things in config/environment.rb:
+Don't forget to restart the application after any change.
+
+
+config.action_mailer.server_settings: SMTP server configuration
+config.action_mailer.perform_deliveries: set to false to disable mail delivering
+
+RDM_HOST_NAME: hostname used to provide urls in notification mails
+
+RDM_STORAGE_PATH: path for all attachments storage (default: "#{RAILS_ROOT}/files")
+ "#{RAILS_ROOT}/" represents application main directory
+
+RDM_LOGIN_REQUIRED: set to true if you want to force users to login to access
+ any part of the application (default: false)
+
+RDM_DEFAULT_LANG: default language for anonymous users: 'en' (default), 'es', 'fr' available
+
+
diff --git a/redmine/doc/README b/redmine/doc/README
new file mode 100644
index 000000000..1c794e45b
--- /dev/null
+++ b/redmine/doc/README
@@ -0,0 +1,49 @@
+== redMine
+
+redMine - project management software
+Copyright (C) 2006 Jean-Philippe Lang
+http://redmine.sourceforge.net/
+
+== License
+
+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.
+
+
+== Main features
+
+redMine is a project management software written using Ruby on Rails.
+
+* multiple users/projects
+* fully customizable role based access control
+* issue tracking system
+* fully customizable workflow
+* documents/files repository
+* email notifications
+* multilanguage support
+
+
+== Versioning
+
+redMine versioning scheme is major.minor.revision
+Versions before 1.0.0 must be considered as beta versions and upgrading support
+may not be provided for these versions.
+
+
+== Credits
+
+* Jean-Francois Boutier (spanish translation)
+* Andreas Viklund (open source XHTML layout, http://andreasviklund.com/)
+
+
diff --git a/redmine/files/delete.me b/redmine/files/delete.me
new file mode 100644
index 000000000..18beddaa8
--- /dev/null
+++ b/redmine/files/delete.me
@@ -0,0 +1 @@
+default directory for uploaded files \ No newline at end of file
diff --git a/redmine/lang/en_US.rb b/redmine/lang/en_US.rb
new file mode 100644
index 000000000..aa402421e
--- /dev/null
+++ b/redmine/lang/en_US.rb
@@ -0,0 +1,4 @@
+Localization.define('en', 'English') do |l|
+ l.store '(date)', lambda { |t| t.strftime('%m/%d/%Y') }
+ l.store '(time)', lambda { |t| t.strftime('%m/%d/%Y %I:%M%p') }
+end \ No newline at end of file
diff --git a/redmine/lang/es_ES.rb b/redmine/lang/es_ES.rb
new file mode 100644
index 000000000..ef15f9c53
--- /dev/null
+++ b/redmine/lang/es_ES.rb
@@ -0,0 +1,315 @@
+Localization.define('es', 'Español') do |l|
+
+ # trackers
+ l.store 'Bug', 'Anomalía'
+ l.store 'Feature request', 'Evolución'
+ l.store 'Support request', 'Asistencia'
+ # issue statuses
+ l.store 'New', 'Nuevo'
+ l.store 'Assigned', 'Asignada'
+ l.store 'Resolved', 'Resuelta'
+ l.store 'Closed', 'Cerrada'
+ l.store 'Rejected', 'Rechazada'
+ l.store 'Feedback', 'Comentario'
+
+ # issue priorities
+ l.store 'Issue priorities', 'Prioridad de las peticiones'
+ l.store 'Low', 'Bajo'
+ l.store 'Normal', 'Normal'
+ l.store 'High', 'Alto'
+ l.store 'Urgent', 'Urgente'
+ l.store 'Immediate', 'Ahora'
+ # document categories
+ l.store 'Document categories', 'Categorías del documento'
+ l.store 'Uncategorized', 'Sin categorías '
+ l.store 'User documentation', 'Documentación del usuario'
+ l.store 'Technical documentation', 'Documentación tecnica'
+ # dates
+ l.store '(date)', lambda { |t| t.strftime('%d/%m/%Y') }
+ l.store '(time)', lambda { |t| t.strftime('%d/%m/%Y %H:%M') }
+
+ # ./script/../config/../app/views/account/login.rhtml
+
+ # ./script/../config/../app/views/account/my_account.rhtml
+ l.store 'My account', 'Mi cuenta'
+ l.store 'Login', 'Identificador'
+ l.store 'Created on', 'Creado el'
+ l.store 'Last update', 'Actualizado'
+ l.store 'Information', 'Informaciones'
+ l.store 'Firstname', 'Nombre'
+ l.store 'Lastname', 'Apellido'
+ l.store 'Mail', 'Mail'
+ l.store 'Language', 'Lengua'
+ l.store 'Mail notifications', 'Notificación por mail'
+ l.store 'Save', 'Validar'
+ l.store 'Password', 'Contraseña'
+ l.store 'New password', 'Nueva contraseña'
+ l.store 'Confirmation', 'Confirmación'
+
+ # ./script/../config/../app/views/account/my_page.rhtml
+ l.store 'My page', 'Mi página'
+ l.store 'Welcome', 'Bienvenido'
+ l.store 'Last login', 'Última conexión'
+ l.store 'Reported issues', 'Peticiones registradas'
+ l.store 'Assigned to me', 'Peticiones que me están asignadas'
+
+ # ./script/../config/../app/views/account/show.rhtml
+ l.store 'Registered on', 'Inscrito el'
+ l.store 'Projects', 'Proyectos'
+ l.store 'Activity', 'Actividad'
+
+ # ./script/../config/../app/views/admin/index.rhtml
+ l.store 'Administration', 'Administración'
+ l.store 'Users', 'Usuarios'
+ l.store 'Roles and permissions', 'Papeles y permisos'
+ l.store 'Trackers', 'Trackers'
+ l.store 'Custom fields', 'Campos personalizados'
+ l.store 'Issue Statuses', 'Estatutos de las peticiones'
+ l.store 'Workflow', 'Workflow'
+ l.store 'Enumerations', 'Listas de valores'
+
+ # ./script/../config/../app/views/admin/info.rhtml
+ l.store 'Version', 'Versión'
+ l.store 'Database', 'Base de datos'
+
+ # ./script/../config/../app/views/admin/mail_options.rhtml
+ l.store 'Select actions for which mail notification should be enabled.', 'Seleccionar las actividades que necesitan la activación de la notificación por mail.'
+ l.store 'Check all', 'Seleccionar todo'
+ l.store 'Uncheck all', 'No seleccionar nada'
+
+ # ./script/../config/../app/views/admin/projects.rhtml
+ l.store 'Project', 'Proyecto'
+ l.store 'Description', 'Descripción'
+ l.store 'Public', 'Público'
+ l.store 'Delete', 'Suprimir'
+ l.store 'Previous', 'Precedente'
+ l.store 'Next', 'Próximo'
+
+ # ./script/../config/../app/views/custom_fields/edit.rhtml
+ l.store 'Custom field', 'Campo personalizado'
+
+ # ./script/../config/../app/views/custom_fields/list.rhtml
+ l.store 'Name', 'Nombre'
+ l.store 'Type', 'Tipo'
+ l.store 'Required', 'Obligatorio'
+ l.store 'For all projects', 'Para todos los proyectos'
+ l.store 'Used by', 'Utilizado por'
+
+ # ./script/../config/../app/views/custom_fields/new.rhtml
+ l.store 'New custom field', 'Nuevo campo personalizado'
+ l.store 'Create', 'Crear'
+
+ # ./script/../config/../app/views/custom_fields/_form.rhtml
+ l.store '0 means no restriction', '0 para ninguna restricción'
+ l.store 'Regular expression pattern', 'Expresión regular'
+ l.store 'Possible values', 'Valores posibles'
+
+ # ./script/../config/../app/views/documents/edit.rhtml
+ l.store 'Document', 'Documento'
+
+ # ./script/../config/../app/views/documents/show.rhtml
+ l.store 'Category', 'Categoría'
+ l.store 'Edit', 'Modificar'
+ l.store 'download', 'Telecarga'
+ l.store 'Add file', 'Añadir el fichero'
+ l.store 'Add', 'Añadir'
+
+ # ./script/../config/../app/views/documents/_form.rhtml
+ l.store 'Title', 'Título'
+
+ # ./script/../config/../app/views/enumerations/edit.rhtml
+
+ # ./script/../config/../app/views/enumerations/list.rhtml
+
+ # ./script/../config/../app/views/enumerations/new.rhtml
+ l.store 'New enumeration', 'Nuevo valor'
+
+ # ./script/../config/../app/views/enumerations/_form.rhtml
+
+ # ./script/../config/../app/views/issues/change_status.rhtml
+ l.store 'Issue', 'Petición'
+ l.store 'New status', 'Nuevo estatuto'
+ l.store 'Assigned to', 'Asignado a'
+ l.store 'Fixed in version', 'Versión corregida'
+ l.store 'Notes', 'Anotación'
+
+ # ./script/../config/../app/views/issues/edit.rhtml
+ l.store 'Status', 'Estatuto'
+ l.store 'Tracker', 'Tracker'
+ l.store 'Priority', 'Prioridad'
+ l.store 'Subject', 'Tema'
+
+ # ./script/../config/../app/views/issues/show.rhtml
+ l.store 'Author', 'Autor'
+ l.store 'Change status', 'Cambiar el estatuto'
+ l.store 'History', 'Histórico'
+ l.store 'Attachments', 'Ficheros'
+ l.store 'Update...', 'Actualizar...'
+
+ # ./script/../config/../app/views/issues/_list_simple.rhtml
+ l.store 'No issue', 'Ninguna petición'
+
+ # ./script/../config/../app/views/issue_categories/edit.rhtml
+
+ # ./script/../config/../app/views/issue_categories/_form.rhtml
+
+ # ./script/../config/../app/views/issue_statuses/edit.rhtml
+ l.store 'Issue status', 'Estatuto de petición'
+
+ # ./script/../config/../app/views/issue_statuses/list.rhtml
+ l.store 'Issue statuses', 'Estatutos de la petición'
+ l.store 'Default status', 'Estatuto por defecto'
+ l.store 'Issue closed', 'Petición resuelta'
+ l.store 'Color', 'Color'
+
+ # ./script/../config/../app/views/issue_statuses/new.rhtml
+ l.store 'New issue status', 'Nuevo estatuto'
+
+ # ./script/../config/../app/views/issue_statuses/_form.rhtml
+
+ # ./script/../config/../app/views/layouts/base.rhtml
+ l.store 'Home', 'Acogida'
+ l.store 'Help', 'Ayuda'
+ l.store 'Log in', 'Conexión'
+ l.store 'Logout', 'Desconexión'
+ l.store 'Overview', 'Vistazo'
+ l.store 'Issues', 'Peticiones'
+ l.store 'Reports', 'Rapports'
+ l.store 'News', 'Noticias'
+ l.store 'Change log', 'Cambios'
+ l.store 'Documents', 'Documentos'
+ l.store 'Members', 'Miembros'
+ l.store 'Files', 'Ficheros'
+ l.store 'Settings', 'Configuración'
+ l.store 'My projects', 'Mis proyectos'
+ l.store 'Logged as', 'Conectado como'
+
+ # ./script/../config/../app/views/mailer/issue_add.rhtml
+
+ # ./script/../config/../app/views/mailer/issue_change_status.rhtml
+
+ # ./script/../config/../app/views/mailer/_issue.rhtml
+
+ # ./script/../config/../app/views/news/edit.rhtml
+
+ # ./script/../config/../app/views/news/show.rhtml
+ l.store 'Summary', 'Resumen'
+ l.store 'By', 'Por'
+ l.store 'Date', 'Fecha'
+
+ # ./script/../config/../app/views/news/_form.rhtml
+
+ # ./script/../config/../app/views/projects/add.rhtml
+ l.store 'New project', 'Nuevo proyecto'
+
+ # ./script/../config/../app/views/projects/add_document.rhtml
+ l.store 'New document', 'Nuevo documento'
+ l.store 'File', 'Fichero'
+
+ # ./script/../config/../app/views/projects/add_issue.rhtml
+ l.store 'New issue', 'Nueva petición'
+ l.store 'Attachment', 'Fichero'
+
+ # ./script/../config/../app/views/projects/add_news.rhtml
+
+ # ./script/../config/../app/views/projects/add_version.rhtml
+ l.store 'New version', 'Nueva versión'
+
+ # ./script/../config/../app/views/projects/changelog.rhtml
+
+ # ./script/../config/../app/views/projects/destroy.rhtml
+ l.store 'Are you sure you want to delete project', '¿ Estás seguro de querer eliminar el proyecto ?'
+
+ # ./script/../config/../app/views/projects/list.rhtml
+ l.store 'Public projects', 'Proyectos publicos'
+
+ # ./script/../config/../app/views/projects/list_documents.rhtml
+ l.store 'Desciption', 'Descripción'
+
+ # ./script/../config/../app/views/projects/list_files.rhtml
+ l.store 'New file', 'Nuevo fichero'
+
+ # ./script/../config/../app/views/projects/list_issues.rhtml
+ l.store 'Apply filter', 'Aplicar'
+ l.store 'Reset', 'Anular'
+ l.store 'Report an issue', 'Nueva petición'
+
+ # ./script/../config/../app/views/projects/list_members.rhtml
+ l.store 'Project members', 'Miembros del proyecto'
+
+ # ./script/../config/../app/views/projects/list_news.rhtml
+ l.store 'Read...', 'Leer...'
+
+ # ./script/../config/../app/views/projects/settings.rhtml
+ l.store 'New member', 'Nuevo miembro'
+ l.store 'Versions', 'Versiónes'
+ l.store 'New version...', 'Nueva versión...'
+ l.store 'Issue categories', 'Categorías de las peticiones'
+ l.store 'New category', 'Nueva categoría'
+
+ # ./script/../config/../app/views/projects/show.rhtml
+ l.store 'Homepage', 'Sitio web'
+ l.store 'open', 'abierta(s)'
+ l.store 'View all issues', 'Ver todas las peticiones'
+ l.store 'View all news', 'Ver todas las noticias'
+ l.store 'Latest news', 'Últimas noticias'
+
+ # ./script/../config/../app/views/projects/_form.rhtml
+
+ # ./script/../config/../app/views/reports/issue_report.rhtml
+ l.store 'Issues by tracker', 'Peticiones por tracker'
+ l.store 'Issues by priority', 'Peticiones por prioridad'
+ l.store 'Issues by category', 'Peticiones por categoría'
+
+ # ./script/../config/../app/views/reports/_simple.rhtml
+ l.store 'Open', 'Abierta'
+ l.store 'Total', 'Total'
+
+ # ./script/../config/../app/views/roles/edit.rhtml
+ l.store 'Role', 'Papel'
+
+ # ./script/../config/../app/views/roles/list.rhtml
+ l.store 'Roles', 'Papeles'
+
+ # ./script/../config/../app/views/roles/new.rhtml
+ l.store 'New role', 'Nuevo papel'
+
+ # ./script/../config/../app/views/roles/workflow.rhtml
+ l.store 'Workflow setup', 'Configuración del workflow'
+ l.store 'Select a workflow to edit', 'Seleccionar un workflow para actualizar'
+ l.store 'New statuses allowed', 'Nuevos estatutos autorizados'
+
+ # ./script/../config/../app/views/roles/_form.rhtml
+ l.store 'Permissions', 'Permisos'
+
+ # ./script/../config/../app/views/trackers/edit.rhtml
+
+ # ./script/../config/../app/views/trackers/list.rhtml
+ l.store 'View issues in change log', 'Consultar las peticiones en el histórico'
+ l.store 'New tracker', 'Nuevo tracker'
+
+ # ./script/../config/../app/views/trackers/new.rhtml
+
+ # ./script/../config/../app/views/trackers/_form.rhtml
+
+ # ./script/../config/../app/views/users/add.rhtml
+ l.store 'New user', 'Nuevo usuario'
+
+ # ./script/../config/../app/views/users/edit.rhtml
+ l.store 'User', 'Usuario'
+
+ # ./script/../config/../app/views/users/list.rhtml
+ l.store 'Admin', 'Admin'
+ l.store 'Locked', 'Cerrado'
+
+ # ./script/../config/../app/views/users/_form.rhtml
+ l.store 'Administrator', 'Administrador'
+
+ # ./script/../config/../app/views/versions/edit.rhtml
+
+ # ./script/../config/../app/views/versions/_form.rhtml
+
+ # ./script/../config/../app/views/welcome/index.rhtml
+
+
+end
diff --git a/redmine/lang/fr_FR.rb b/redmine/lang/fr_FR.rb
new file mode 100644
index 000000000..5dc7a59e0
--- /dev/null
+++ b/redmine/lang/fr_FR.rb
@@ -0,0 +1,316 @@
+Localization.define('fr', 'Français') do |l|
+
+ # trackers
+ l.store 'Bug', 'Anomalie'
+ l.store 'Feature request', 'Evolution'
+ l.store 'Support request', 'Assistance'
+ # issue statuses
+ l.store 'New', 'Nouveau'
+ l.store 'Assigned', 'Assignée'
+ l.store 'Resolved', 'Résolue'
+ l.store 'Closed', 'Fermée'
+ l.store 'Rejected', 'Rejetée'
+ l.store 'Feedback', 'Commentaire'
+
+ # issue priorities
+ l.store 'Issue priorities', 'Priorités des demandes'
+ l.store 'Low', 'Bas'
+ l.store 'Normal', 'Normal'
+ l.store 'High', 'Haut'
+ l.store 'Urgent', 'Urgent'
+ l.store 'Immediate', 'Immédiat'
+ # document categories
+ l.store 'Document categories', 'Catégories de documents'
+ l.store 'Uncategorized', 'Sans catégorie'
+ l.store 'User documentation', 'Documentation utilisateur'
+ l.store 'Technical documentation', 'Documentation technique'
+ # dates
+ l.store '(date)', lambda { |t| t.strftime('%d/%m/%Y') }
+ l.store '(time)', lambda { |t| t.strftime('%d/%m/%Y %H:%M') }
+
+ # ./script/../config/../app/views/account/login.rhtml
+
+ # ./script/../config/../app/views/account/my_account.rhtml
+ l.store 'My account', 'Mon compte'
+ l.store 'Login', 'Identifiant'
+ l.store 'Created on', 'Crée le'
+ l.store 'Last update', 'Mis à jour'
+ l.store 'Information', 'Informations'
+ l.store 'Firstname', 'Prénom'
+ l.store 'Lastname', 'Nom'
+ l.store 'Mail', 'Mail'
+ l.store 'Language', 'Langue'
+ l.store 'Mail notifications', 'Notifications par mail'
+ l.store 'Save', 'Valider'
+ l.store 'Password', 'Mot de passe'
+ l.store 'New password', 'Nouveau mot de passe'
+ l.store 'Confirmation', 'Confirmation'
+
+ # ./script/../config/../app/views/account/my_page.rhtml
+ l.store 'My page', 'Ma page'
+ l.store 'Welcome', 'Bienvenue'
+ l.store 'Last login', 'Dernière connexion'
+ l.store 'Reported issues', 'Demandes soumises'
+ l.store 'Assigned to me', 'Demandes qui me sont assignées'
+
+ # ./script/../config/../app/views/account/show.rhtml
+ l.store 'Registered on', 'Inscrit le'
+ l.store 'Projects', 'Projets'
+ l.store 'Activity', 'Activité'
+
+ # ./script/../config/../app/views/admin/index.rhtml
+ l.store 'Administration', 'Administration'
+ l.store 'Users', 'Utilisateurs'
+ l.store 'Roles and permissions', 'Rôles et permissions'
+ l.store 'Trackers', 'Trackers'
+ l.store 'Custom fields', 'Champs personnalisés'
+ l.store 'Issue Statuses', 'Statuts des demandes'
+ l.store 'Workflow', 'Workflow'
+ l.store 'Enumerations', 'Listes de valeurs'
+
+ # ./script/../config/../app/views/admin/info.rhtml
+ l.store 'Version', 'Version'
+ l.store 'Database', 'Base de données'
+
+ # ./script/../config/../app/views/admin/mail_options.rhtml
+ l.store 'Select actions for which mail notification should be enabled.', 'Sélectionner les actions pour lesquelles la notification par mail doit être activée.'
+ l.store 'Check all', 'Cocher tout'
+ l.store 'Uncheck all', 'Décocher tout'
+
+ # ./script/../config/../app/views/admin/projects.rhtml
+ l.store 'Project', 'Projet'
+ l.store 'Description', 'Description'
+ l.store 'Public', 'Public'
+ l.store 'Delete', 'Supprimer'
+ l.store 'Previous', 'Précédent'
+ l.store 'Next', 'Suivant'
+
+ # ./script/../config/../app/views/custom_fields/edit.rhtml
+ l.store 'Custom field', 'Champ personnalisé'
+
+ # ./script/../config/../app/views/custom_fields/list.rhtml
+ l.store 'Name', 'Nom'
+ l.store 'Type', 'Type'
+ l.store 'Required', 'Obligatoire'
+ l.store 'For all projects', 'Pour tous les projets'
+ l.store 'Used by', 'Utilisé par'
+
+ # ./script/../config/../app/views/custom_fields/new.rhtml
+ l.store 'New custom field', 'Nouveau champ personnalisé'
+ l.store 'Create', 'Créer'
+
+ # ./script/../config/../app/views/custom_fields/_form.rhtml
+ l.store '0 means no restriction', '0 pour aucune restriction'
+ l.store 'Regular expression pattern', 'Expression régulière'
+ l.store 'Possible values', 'Valeurs possibles'
+
+ # ./script/../config/../app/views/documents/edit.rhtml
+ l.store 'Document', 'Document'
+
+ # ./script/../config/../app/views/documents/show.rhtml
+ l.store 'Category', 'Catégorie'
+ l.store 'Edit', 'Modifier'
+ l.store 'download', 'téléchargement'
+ l.store 'Add file', 'Ajouter le fichier'
+ l.store 'Add', 'Ajouter'
+
+ # ./script/../config/../app/views/documents/_form.rhtml
+ l.store 'Title', 'Titre'
+
+ # ./script/../config/../app/views/enumerations/edit.rhtml
+
+ # ./script/../config/../app/views/enumerations/list.rhtml
+
+ # ./script/../config/../app/views/enumerations/new.rhtml
+ l.store 'New enumeration', 'Nouvelle valeur'
+
+ # ./script/../config/../app/views/enumerations/_form.rhtml
+
+ # ./script/../config/../app/views/issues/change_status.rhtml
+ l.store 'Issue', 'Demande'
+ l.store 'New status', 'Nouveau statut'
+ l.store 'Assigned to', 'Assigné à'
+ l.store 'Fixed in version', 'Version corrigée'
+ l.store 'Notes', 'Remarques'
+
+ # ./script/../config/../app/views/issues/edit.rhtml
+ l.store 'Status', 'Statut'
+ l.store 'Tracker', 'Tracker'
+ l.store 'Priority', 'Priorité'
+ l.store 'Subject', 'Sujet'
+
+ # ./script/../config/../app/views/issues/show.rhtml
+ l.store 'Author', 'Auteur'
+ l.store 'Change status', 'Changer le statut'
+ l.store 'History', 'Historique'
+ l.store 'Attachments', 'Fichiers'
+ l.store 'Update...', 'Changer...'
+
+ # ./script/../config/../app/views/issues/_list_simple.rhtml
+ l.store 'No issue', 'Aucune demande'
+
+ # ./script/../config/../app/views/issue_categories/edit.rhtml
+
+ # ./script/../config/../app/views/issue_categories/_form.rhtml
+
+ # ./script/../config/../app/views/issue_statuses/edit.rhtml
+ l.store 'Issue status', 'Statut de demande'
+
+ # ./script/../config/../app/views/issue_statuses/list.rhtml
+ l.store 'Issue statuses', 'Statuts de demande'
+ l.store 'Default status', 'Statut par défaut'
+ l.store 'Issue closed', 'Demande fermée'
+ l.store 'Color', 'Couleur'
+
+ # ./script/../config/../app/views/issue_statuses/new.rhtml
+ l.store 'New issue status', 'Nouveau statut'
+
+ # ./script/../config/../app/views/issue_statuses/_form.rhtml
+
+ # ./script/../config/../app/views/layouts/base.rhtml
+ l.store 'Home', 'Accueil'
+ l.store 'Help', 'Aide'
+ l.store 'Log in', 'Connexion'
+ l.store 'Logout', 'Déconnexion'
+ l.store 'Overview', 'Aperçu'
+ l.store 'Issues', 'Demandes'
+ l.store 'Reports', 'Rapports'
+ l.store 'News', 'Annonces'
+ l.store 'Change log', 'Historique'
+ l.store 'Documents', 'Documents'
+ l.store 'Members', 'Membres'
+ l.store 'Files', 'Fichiers'
+ l.store 'Settings', 'Configuration'
+ l.store 'My projects', 'Mes projets'
+ l.store 'Logged as', 'Connecté en tant que'
+
+ # ./script/../config/../app/views/mailer/issue_add.rhtml
+
+ # ./script/../config/../app/views/mailer/issue_change_status.rhtml
+
+ # ./script/../config/../app/views/mailer/_issue.rhtml
+
+ # ./script/../config/../app/views/news/edit.rhtml
+
+ # ./script/../config/../app/views/news/show.rhtml
+ l.store 'Summary', 'Résumé'
+ l.store 'By', 'Par'
+ l.store 'Date', 'Date'
+
+ # ./script/../config/../app/views/news/_form.rhtml
+
+ # ./script/../config/../app/views/projects/add.rhtml
+ l.store 'New project', 'Nouveau projet'
+
+ # ./script/../config/../app/views/projects/add_document.rhtml
+ l.store 'New document', 'Nouveau document'
+ l.store 'File', 'Fichier'
+
+ # ./script/../config/../app/views/projects/add_issue.rhtml
+ l.store 'New issue', 'Nouvelle demande'
+ l.store 'Attachment', 'Fichier'
+
+ # ./script/../config/../app/views/projects/add_news.rhtml
+
+ # ./script/../config/../app/views/projects/add_version.rhtml
+ l.store 'New version', 'Nouvelle version'
+
+ # ./script/../config/../app/views/projects/changelog.rhtml
+
+ # ./script/../config/../app/views/projects/destroy.rhtml
+ l.store 'Are you sure you want to delete project', 'Êtes-vous sûr de vouloir supprimer le projet'
+
+ # ./script/../config/../app/views/projects/list.rhtml
+ l.store 'Public projects', 'Projets publics'
+
+ # ./script/../config/../app/views/projects/list_documents.rhtml
+ l.store 'Desciption', 'Description'
+
+ # ./script/../config/../app/views/projects/list_files.rhtml
+ l.store 'Files', 'Fichiers'
+ l.store 'New file', 'Nouveau fichier'
+
+ # ./script/../config/../app/views/projects/list_issues.rhtml
+ l.store 'Apply filter', 'Appliquer'
+ l.store 'Reset', 'Annuler'
+ l.store 'Report an issue', 'Nouvelle demande'
+
+ # ./script/../config/../app/views/projects/list_members.rhtml
+ l.store 'Project members', 'Membres du projet'
+
+ # ./script/../config/../app/views/projects/list_news.rhtml
+ l.store 'Read...', 'Lire...'
+
+ # ./script/../config/../app/views/projects/settings.rhtml
+ l.store 'New member', 'Nouveau membre'
+ l.store 'Versions', 'Versions'
+ l.store 'New version...', 'Nouvelle version...'
+ l.store 'Issue categories', 'Catégories des demandes'
+ l.store 'New category', 'Nouvelle catégorie'
+
+ # ./script/../config/../app/views/projects/show.rhtml
+ l.store 'Homepage', 'Site web'
+ l.store 'open', 'ouverte(s)'
+ l.store 'View all issues', 'Voir toutes les demandes'
+ l.store 'View all news', 'Voir toutes les annonces'
+ l.store 'Latest news', 'Dernières annonces'
+
+ # ./script/../config/../app/views/projects/_form.rhtml
+
+ # ./script/../config/../app/views/reports/issue_report.rhtml
+ l.store 'Issues by tracker', 'Demandes par tracker'
+ l.store 'Issues by priority', 'Demandes par priorité'
+ l.store 'Issues by category', 'Demandes par catégorie'
+
+ # ./script/../config/../app/views/reports/_simple.rhtml
+ l.store 'Open', 'Ouverte'
+ l.store 'Total', 'Total'
+
+ # ./script/../config/../app/views/roles/edit.rhtml
+ l.store 'Role', 'Rôle'
+
+ # ./script/../config/../app/views/roles/list.rhtml
+ l.store 'Roles', 'Rôles'
+
+ # ./script/../config/../app/views/roles/new.rhtml
+ l.store 'New role', 'Nouveau rôle'
+
+ # ./script/../config/../app/views/roles/workflow.rhtml
+ l.store 'Workflow setup', 'Configuration du workflow'
+ l.store 'Select a workflow to edit', 'Sélectionner un workflow à mettre à jour'
+ l.store 'New statuses allowed', 'Nouveaux statuts autorisés'
+
+ # ./script/../config/../app/views/roles/_form.rhtml
+ l.store 'Permissions', 'Permissions'
+
+ # ./script/../config/../app/views/trackers/edit.rhtml
+
+ # ./script/../config/../app/views/trackers/list.rhtml
+ l.store 'View issues in change log', 'Demandes affichées dans l\'historique'
+
+ # ./script/../config/../app/views/trackers/new.rhtml
+ l.store 'New tracker', 'Nouveau tracker'
+
+ # ./script/../config/../app/views/trackers/_form.rhtml
+
+ # ./script/../config/../app/views/users/add.rhtml
+ l.store 'New user', 'Nouvel utilisateur'
+
+ # ./script/../config/../app/views/users/edit.rhtml
+ l.store 'User', 'Utilisateur'
+
+ # ./script/../config/../app/views/users/list.rhtml
+ l.store 'Admin', 'Admin'
+ l.store 'Locked', 'Verrouillé'
+
+ # ./script/../config/../app/views/users/_form.rhtml
+ l.store 'Administrator', 'Administrateur'
+
+ # ./script/../config/../app/views/versions/edit.rhtml
+
+ # ./script/../config/../app/views/versions/_form.rhtml
+
+ # ./script/../config/../app/views/welcome/index.rhtml
+
+
+end
diff --git a/redmine/public/.htaccess b/redmine/public/.htaccess
new file mode 100644
index 000000000..d3c998345
--- /dev/null
+++ b/redmine/public/.htaccess
@@ -0,0 +1,40 @@
+# General Apache options
+AddHandler fastcgi-script .fcgi
+AddHandler cgi-script .cgi
+Options +FollowSymLinks +ExecCGI
+
+# If you don't want Rails to look in certain directories,
+# use the following rewrite rules so that Apache won't rewrite certain requests
+#
+# Example:
+# RewriteCond %{REQUEST_URI} ^/notrails.*
+# RewriteRule .* - [L]
+
+# Redirect all requests not available on the filesystem to Rails
+# By default the cgi dispatcher is used which is very slow
+#
+# For better performance replace the dispatcher with the fastcgi one
+#
+# Example:
+# RewriteRule ^(.*)$ dispatch.fcgi [QSA,L]
+RewriteEngine On
+
+# If your Rails application is accessed via an Alias directive,
+# then you MUST also set the RewriteBase in this htaccess file.
+#
+# Example:
+# Alias /myrailsapp /path/to/myrailsapp/public
+# RewriteBase /myrailsapp
+
+RewriteRule ^$ index.html [QSA]
+RewriteRule ^([^.]+)$ $1.html [QSA]
+RewriteCond %{REQUEST_FILENAME} !-f
+RewriteRule ^(.*)$ dispatch.cgi [QSA,L]
+
+# In case Rails experiences terminal errors
+# Instead of displaying this message you can supply a file here which will be rendered instead
+#
+# Example:
+# ErrorDocument 500 /500.html
+
+ErrorDocument 500 "<h2>Application error</h2>Rails application failed to start properly" \ No newline at end of file
diff --git a/redmine/public/404.html b/redmine/public/404.html
new file mode 100644
index 000000000..e1c8e916f
--- /dev/null
+++ b/redmine/public/404.html
@@ -0,0 +1,7 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
+ "http://www.w3.org/TR/html4/loose.dtd">
+<html>
+<body>
+ <h1>File not found</h1>
+</body>
+</html> \ No newline at end of file
diff --git a/redmine/public/500.html b/redmine/public/500.html
new file mode 100644
index 000000000..713ee8aa0
--- /dev/null
+++ b/redmine/public/500.html
@@ -0,0 +1,7 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
+ "http://www.w3.org/TR/html4/loose.dtd">
+<html>
+<body>
+ <h1>Sorry, an application error occured</h1>
+</body>
+</html> \ No newline at end of file
diff --git a/redmine/public/dispatch.cgi b/redmine/public/dispatch.cgi
new file mode 100644
index 000000000..7095803c1
--- /dev/null
+++ b/redmine/public/dispatch.cgi
@@ -0,0 +1,10 @@
+#!e:/ruby/bin/ruby
+
+require File.dirname(__FILE__) + "/../config/environment" unless defined?(RAILS_ROOT)
+
+# If you're using RubyGems and mod_ruby, this require should be changed to an absolute path one, like:
+# "/usr/local/lib/ruby/gems/1.8/gems/rails-0.8.0/lib/dispatcher" -- otherwise performance is severely impaired
+require "dispatcher"
+
+ADDITIONAL_LOAD_PATHS.reverse.each { |dir| $:.unshift(dir) if File.directory?(dir) } if defined?(Apache::RubyRun)
+Dispatcher.dispatch \ No newline at end of file
diff --git a/redmine/public/dispatch.fcgi b/redmine/public/dispatch.fcgi
new file mode 100644
index 000000000..418aa3a97
--- /dev/null
+++ b/redmine/public/dispatch.fcgi
@@ -0,0 +1,24 @@
+#!e:/ruby/bin/ruby
+#
+# You may specify the path to the FastCGI crash log (a log of unhandled
+# exceptions which forced the FastCGI instance to exit, great for debugging)
+# and the number of requests to process before running garbage collection.
+#
+# By default, the FastCGI crash log is RAILS_ROOT/log/fastcgi.crash.log
+# and the GC period is nil (turned off). A reasonable number of requests
+# could range from 10-100 depending on the memory footprint of your app.
+#
+# Example:
+# # Default log path, normal GC behavior.
+# RailsFCGIHandler.process!
+#
+# # Default log path, 50 requests between GC.
+# RailsFCGIHandler.process! nil, 50
+#
+# # Custom log path, normal GC behavior.
+# RailsFCGIHandler.process! '/var/log/myapp_fcgi_crash.log'
+#
+require File.dirname(__FILE__) + "/../config/environment"
+require 'fcgi_handler'
+
+RailsFCGIHandler.process!
diff --git a/redmine/public/dispatch.rb b/redmine/public/dispatch.rb
new file mode 100644
index 000000000..7095803c1
--- /dev/null
+++ b/redmine/public/dispatch.rb
@@ -0,0 +1,10 @@
+#!e:/ruby/bin/ruby
+
+require File.dirname(__FILE__) + "/../config/environment" unless defined?(RAILS_ROOT)
+
+# If you're using RubyGems and mod_ruby, this require should be changed to an absolute path one, like:
+# "/usr/local/lib/ruby/gems/1.8/gems/rails-0.8.0/lib/dispatcher" -- otherwise performance is severely impaired
+require "dispatcher"
+
+ADDITIONAL_LOAD_PATHS.reverse.each { |dir| $:.unshift(dir) if File.directory?(dir) } if defined?(Apache::RubyRun)
+Dispatcher.dispatch \ No newline at end of file
diff --git a/redmine/public/favicon.ico b/redmine/public/favicon.ico
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/redmine/public/favicon.ico
diff --git a/redmine/public/images/Copie de help.png b/redmine/public/images/Copie de help.png
new file mode 100644
index 000000000..6dc4f684a
--- /dev/null
+++ b/redmine/public/images/Copie de help.png
Binary files differ
diff --git a/redmine/public/images/admin.png b/redmine/public/images/admin.png
new file mode 100644
index 000000000..0c190984f
--- /dev/null
+++ b/redmine/public/images/admin.png
Binary files differ
diff --git a/redmine/public/images/bulletgreen.png b/redmine/public/images/bulletgreen.png
new file mode 100644
index 000000000..abe41592b
--- /dev/null
+++ b/redmine/public/images/bulletgreen.png
Binary files differ
diff --git a/redmine/public/images/bulletred.png b/redmine/public/images/bulletred.png
new file mode 100644
index 000000000..26a121057
--- /dev/null
+++ b/redmine/public/images/bulletred.png
Binary files differ
diff --git a/redmine/public/images/delete.png b/redmine/public/images/delete.png
new file mode 100644
index 000000000..2ed33bdf3
--- /dev/null
+++ b/redmine/public/images/delete.png
Binary files differ
diff --git a/redmine/public/images/dir.png b/redmine/public/images/dir.png
new file mode 100644
index 000000000..d078094ac
--- /dev/null
+++ b/redmine/public/images/dir.png
Binary files differ
diff --git a/redmine/public/images/dir_new.png b/redmine/public/images/dir_new.png
new file mode 100644
index 000000000..2d29814f2
--- /dev/null
+++ b/redmine/public/images/dir_new.png
Binary files differ
diff --git a/redmine/public/images/dir_open.png b/redmine/public/images/dir_open.png
new file mode 100644
index 000000000..a248cba3c
--- /dev/null
+++ b/redmine/public/images/dir_open.png
Binary files differ
diff --git a/redmine/public/images/document.png b/redmine/public/images/document.png
new file mode 100644
index 000000000..34fde6eb3
--- /dev/null
+++ b/redmine/public/images/document.png
Binary files differ
diff --git a/redmine/public/images/edit_small.png b/redmine/public/images/edit_small.png
new file mode 100644
index 000000000..dea7c92ea
--- /dev/null
+++ b/redmine/public/images/edit_small.png
Binary files differ
diff --git a/redmine/public/images/file_new.png b/redmine/public/images/file_new.png
new file mode 100644
index 000000000..9a12ca732
--- /dev/null
+++ b/redmine/public/images/file_new.png
Binary files differ
diff --git a/redmine/public/images/help.png b/redmine/public/images/help.png
new file mode 100644
index 000000000..da8feb993
--- /dev/null
+++ b/redmine/public/images/help.png
Binary files differ
diff --git a/redmine/public/images/home.png b/redmine/public/images/home.png
new file mode 100644
index 000000000..7a12add6a
--- /dev/null
+++ b/redmine/public/images/home.png
Binary files differ
diff --git a/redmine/public/images/issues.png b/redmine/public/images/issues.png
new file mode 100644
index 000000000..e6948bff7
--- /dev/null
+++ b/redmine/public/images/issues.png
Binary files differ
diff --git a/redmine/public/images/locked.png b/redmine/public/images/locked.png
new file mode 100644
index 000000000..5199dfe22
--- /dev/null
+++ b/redmine/public/images/locked.png
Binary files differ
diff --git a/redmine/public/images/logout.png b/redmine/public/images/logout.png
new file mode 100644
index 000000000..edf94abcd
--- /dev/null
+++ b/redmine/public/images/logout.png
Binary files differ
diff --git a/redmine/public/images/mailer.png b/redmine/public/images/mailer.png
new file mode 100644
index 000000000..8008bb84b
--- /dev/null
+++ b/redmine/public/images/mailer.png
Binary files differ
diff --git a/redmine/public/images/notes.png b/redmine/public/images/notes.png
new file mode 100644
index 000000000..d26b1d577
--- /dev/null
+++ b/redmine/public/images/notes.png
Binary files differ
diff --git a/redmine/public/images/options.png b/redmine/public/images/options.png
new file mode 100644
index 000000000..a907c20f1
--- /dev/null
+++ b/redmine/public/images/options.png
Binary files differ
diff --git a/redmine/public/images/package.png b/redmine/public/images/package.png
new file mode 100644
index 000000000..634d13d9f
--- /dev/null
+++ b/redmine/public/images/package.png
Binary files differ
diff --git a/redmine/public/images/projects.png b/redmine/public/images/projects.png
new file mode 100644
index 000000000..b42347a92
--- /dev/null
+++ b/redmine/public/images/projects.png
Binary files differ
diff --git a/redmine/public/images/rails.png b/redmine/public/images/rails.png
new file mode 100644
index 000000000..b8441f182
--- /dev/null
+++ b/redmine/public/images/rails.png
Binary files differ
diff --git a/redmine/public/images/rails_powered.png b/redmine/public/images/rails_powered.png
new file mode 100644
index 000000000..5255e62de
--- /dev/null
+++ b/redmine/public/images/rails_powered.png
Binary files differ
diff --git a/redmine/public/images/rails_small.png b/redmine/public/images/rails_small.png
new file mode 100644
index 000000000..aff4b7a84
--- /dev/null
+++ b/redmine/public/images/rails_small.png
Binary files differ
diff --git a/redmine/public/images/role.png b/redmine/public/images/role.png
new file mode 100644
index 000000000..ad36d1ca8
--- /dev/null
+++ b/redmine/public/images/role.png
Binary files differ
diff --git a/redmine/public/images/rss.png b/redmine/public/images/rss.png
new file mode 100644
index 000000000..89ce35f80
--- /dev/null
+++ b/redmine/public/images/rss.png
Binary files differ
diff --git a/redmine/public/images/sort_asc.png b/redmine/public/images/sort_asc.png
new file mode 100644
index 000000000..05dfa15f4
--- /dev/null
+++ b/redmine/public/images/sort_asc.png
Binary files differ
diff --git a/redmine/public/images/sort_desc.png b/redmine/public/images/sort_desc.png
new file mode 100644
index 000000000..f82d53917
--- /dev/null
+++ b/redmine/public/images/sort_desc.png
Binary files differ
diff --git a/redmine/public/images/tracker.png b/redmine/public/images/tracker.png
new file mode 100644
index 000000000..f29cfb2af
--- /dev/null
+++ b/redmine/public/images/tracker.png
Binary files differ
diff --git a/redmine/public/images/true.png b/redmine/public/images/true.png
new file mode 100644
index 000000000..9afc0b52a
--- /dev/null
+++ b/redmine/public/images/true.png
Binary files differ
diff --git a/redmine/public/images/user.png b/redmine/public/images/user.png
new file mode 100644
index 000000000..89d591c0b
--- /dev/null
+++ b/redmine/public/images/user.png
Binary files differ
diff --git a/redmine/public/images/user_page.png b/redmine/public/images/user_page.png
new file mode 100644
index 000000000..940a7b8e1
--- /dev/null
+++ b/redmine/public/images/user_page.png
Binary files differ
diff --git a/redmine/public/images/users.png b/redmine/public/images/users.png
new file mode 100644
index 000000000..3b9fc5aa9
--- /dev/null
+++ b/redmine/public/images/users.png
Binary files differ
diff --git a/redmine/public/images/workflow.png b/redmine/public/images/workflow.png
new file mode 100644
index 000000000..868332fed
--- /dev/null
+++ b/redmine/public/images/workflow.png
Binary files differ
diff --git a/redmine/public/javascripts/application.js b/redmine/public/javascripts/application.js
new file mode 100644
index 000000000..d8083a957
--- /dev/null
+++ b/redmine/public/javascripts/application.js
@@ -0,0 +1,8 @@
+function checkAll (id, checked) {
+ var el = document.getElementById(id);
+ for (var i = 0; i < el.elements.length; i++) {
+ if (el.elements[i].disabled==false) {
+ el.elements[i].checked = checked;
+ }
+ }
+} \ No newline at end of file
diff --git a/redmine/public/javascripts/controls.js b/redmine/public/javascripts/controls.js
new file mode 100644
index 000000000..9742b6918
--- /dev/null
+++ b/redmine/public/javascripts/controls.js
@@ -0,0 +1,750 @@
+// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
+// (c) 2005 Ivan Krstic (http://blogs.law.harvard.edu/ivan)
+// (c) 2005 Jon Tirsen (http://www.tirsen.com)
+// Contributors:
+// Richard Livsey
+// Rahul Bhargava
+// Rob Wills
+//
+// See scriptaculous.js for full license.
+
+// Autocompleter.Base handles all the autocompletion functionality
+// that's independent of the data source for autocompletion. This
+// includes drawing the autocompletion menu, observing keyboard
+// and mouse events, and similar.
+//
+// Specific autocompleters need to provide, at the very least,
+// a getUpdatedChoices function that will be invoked every time
+// the text inside the monitored textbox changes. This method
+// should get the text for which to provide autocompletion by
+// invoking this.getToken(), NOT by directly accessing
+// this.element.value. This is to allow incremental tokenized
+// autocompletion. Specific auto-completion logic (AJAX, etc)
+// belongs in getUpdatedChoices.
+//
+// Tokenized incremental autocompletion is enabled automatically
+// when an autocompleter is instantiated with the 'tokens' option
+// in the options parameter, e.g.:
+// new Ajax.Autocompleter('id','upd', '/url/', { tokens: ',' });
+// will incrementally autocomplete with a comma as the token.
+// Additionally, ',' in the above example can be replaced with
+// a token array, e.g. { tokens: [',', '\n'] } which
+// enables autocompletion on multiple tokens. This is most
+// useful when one of the tokens is \n (a newline), as it
+// allows smart autocompletion after linebreaks.
+
+var Autocompleter = {}
+Autocompleter.Base = function() {};
+Autocompleter.Base.prototype = {
+ baseInitialize: function(element, update, options) {
+ this.element = $(element);
+ this.update = $(update);
+ this.hasFocus = false;
+ this.changed = false;
+ this.active = false;
+ this.index = 0;
+ this.entryCount = 0;
+
+ if (this.setOptions)
+ this.setOptions(options);
+ else
+ this.options = options || {};
+
+ this.options.paramName = this.options.paramName || this.element.name;
+ this.options.tokens = this.options.tokens || [];
+ this.options.frequency = this.options.frequency || 0.4;
+ this.options.minChars = this.options.minChars || 1;
+ this.options.onShow = this.options.onShow ||
+ function(element, update){
+ if(!update.style.position || update.style.position=='absolute') {
+ update.style.position = 'absolute';
+ Position.clone(element, update, {setHeight: false, offsetTop: element.offsetHeight});
+ }
+ Effect.Appear(update,{duration:0.15});
+ };
+ this.options.onHide = this.options.onHide ||
+ function(element, update){ new Effect.Fade(update,{duration:0.15}) };
+
+ if (typeof(this.options.tokens) == 'string')
+ this.options.tokens = new Array(this.options.tokens);
+
+ this.observer = null;
+
+ this.element.setAttribute('autocomplete','off');
+
+ Element.hide(this.update);
+
+ Event.observe(this.element, "blur", this.onBlur.bindAsEventListener(this));
+ Event.observe(this.element, "keypress", this.onKeyPress.bindAsEventListener(this));
+ },
+
+ show: function() {
+ if(Element.getStyle(this.update, 'display')=='none') this.options.onShow(this.element, this.update);
+ if(!this.iefix &&
+ (navigator.appVersion.indexOf('MSIE')>0) &&
+ (navigator.userAgent.indexOf('Opera')<0) &&
+ (Element.getStyle(this.update, 'position')=='absolute')) {
+ new Insertion.After(this.update,
+ '<iframe id="' + this.update.id + '_iefix" '+
+ 'style="display:none;position:absolute;filter:progid:DXImageTransform.Microsoft.Alpha(opacity=0);" ' +
+ 'src="javascript:false;" frameborder="0" scrolling="no"></iframe>');
+ this.iefix = $(this.update.id+'_iefix');
+ }
+ if(this.iefix) setTimeout(this.fixIEOverlapping.bind(this), 50);
+ },
+
+ fixIEOverlapping: function() {
+ Position.clone(this.update, this.iefix);
+ this.iefix.style.zIndex = 1;
+ this.update.style.zIndex = 2;
+ Element.show(this.iefix);
+ },
+
+ hide: function() {
+ this.stopIndicator();
+ if(Element.getStyle(this.update, 'display')!='none') this.options.onHide(this.element, this.update);
+ if(this.iefix) Element.hide(this.iefix);
+ },
+
+ startIndicator: function() {
+ if(this.options.indicator) Element.show(this.options.indicator);
+ },
+
+ stopIndicator: function() {
+ if(this.options.indicator) Element.hide(this.options.indicator);
+ },
+
+ onKeyPress: function(event) {
+ if(this.active)
+ switch(event.keyCode) {
+ case Event.KEY_TAB:
+ case Event.KEY_RETURN:
+ this.selectEntry();
+ Event.stop(event);
+ case Event.KEY_ESC:
+ this.hide();
+ this.active = false;
+ Event.stop(event);
+ return;
+ case Event.KEY_LEFT:
+ case Event.KEY_RIGHT:
+ return;
+ case Event.KEY_UP:
+ this.markPrevious();
+ this.render();
+ if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event);
+ return;
+ case Event.KEY_DOWN:
+ this.markNext();
+ this.render();
+ if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event);
+ return;
+ }
+ else
+ if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN)
+ return;
+
+ this.changed = true;
+ this.hasFocus = true;
+
+ if(this.observer) clearTimeout(this.observer);
+ this.observer =
+ setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000);
+ },
+
+ onHover: function(event) {
+ var element = Event.findElement(event, 'LI');
+ if(this.index != element.autocompleteIndex)
+ {
+ this.index = element.autocompleteIndex;
+ this.render();
+ }
+ Event.stop(event);
+ },
+
+ onClick: function(event) {
+ var element = Event.findElement(event, 'LI');
+ this.index = element.autocompleteIndex;
+ this.selectEntry();
+ this.hide();
+ },
+
+ onBlur: function(event) {
+ // needed to make click events working
+ setTimeout(this.hide.bind(this), 250);
+ this.hasFocus = false;
+ this.active = false;
+ },
+
+ render: function() {
+ if(this.entryCount > 0) {
+ for (var i = 0; i < this.entryCount; i++)
+ this.index==i ?
+ Element.addClassName(this.getEntry(i),"selected") :
+ Element.removeClassName(this.getEntry(i),"selected");
+
+ if(this.hasFocus) {
+ this.show();
+ this.active = true;
+ }
+ } else {
+ this.active = false;
+ this.hide();
+ }
+ },
+
+ markPrevious: function() {
+ if(this.index > 0) this.index--
+ else this.index = this.entryCount-1;
+ },
+
+ markNext: function() {
+ if(this.index < this.entryCount-1) this.index++
+ else this.index = 0;
+ },
+
+ getEntry: function(index) {
+ return this.update.firstChild.childNodes[index];
+ },
+
+ getCurrentEntry: function() {
+ return this.getEntry(this.index);
+ },
+
+ selectEntry: function() {
+ this.active = false;
+ this.updateElement(this.getCurrentEntry());
+ },
+
+ updateElement: function(selectedElement) {
+ if (this.options.updateElement) {
+ this.options.updateElement(selectedElement);
+ return;
+ }
+
+ var value = Element.collectTextNodesIgnoreClass(selectedElement, 'informal');
+ var lastTokenPos = this.findLastToken();
+ if (lastTokenPos != -1) {
+ var newValue = this.element.value.substr(0, lastTokenPos + 1);
+ var whitespace = this.element.value.substr(lastTokenPos + 1).match(/^\s+/);
+ if (whitespace)
+ newValue += whitespace[0];
+ this.element.value = newValue + value;
+ } else {
+ this.element.value = value;
+ }
+ this.element.focus();
+
+ if (this.options.afterUpdateElement)
+ this.options.afterUpdateElement(this.element, selectedElement);
+ },
+
+ updateChoices: function(choices) {
+ if(!this.changed && this.hasFocus) {
+ this.update.innerHTML = choices;
+ Element.cleanWhitespace(this.update);
+ Element.cleanWhitespace(this.update.firstChild);
+
+ if(this.update.firstChild && this.update.firstChild.childNodes) {
+ this.entryCount =
+ this.update.firstChild.childNodes.length;
+ for (var i = 0; i < this.entryCount; i++) {
+ var entry = this.getEntry(i);
+ entry.autocompleteIndex = i;
+ this.addObservers(entry);
+ }
+ } else {
+ this.entryCount = 0;
+ }
+
+ this.stopIndicator();
+
+ this.index = 0;
+ this.render();
+ }
+ },
+
+ addObservers: function(element) {
+ Event.observe(element, "mouseover", this.onHover.bindAsEventListener(this));
+ Event.observe(element, "click", this.onClick.bindAsEventListener(this));
+ },
+
+ onObserverEvent: function() {
+ this.changed = false;
+ if(this.getToken().length>=this.options.minChars) {
+ this.startIndicator();
+ this.getUpdatedChoices();
+ } else {
+ this.active = false;
+ this.hide();
+ }
+ },
+
+ getToken: function() {
+ var tokenPos = this.findLastToken();
+ if (tokenPos != -1)
+ var ret = this.element.value.substr(tokenPos + 1).replace(/^\s+/,'').replace(/\s+$/,'');
+ else
+ var ret = this.element.value;
+
+ return /\n/.test(ret) ? '' : ret;
+ },
+
+ findLastToken: function() {
+ var lastTokenPos = -1;
+
+ for (var i=0; i<this.options.tokens.length; i++) {
+ var thisTokenPos = this.element.value.lastIndexOf(this.options.tokens[i]);
+ if (thisTokenPos > lastTokenPos)
+ lastTokenPos = thisTokenPos;
+ }
+ return lastTokenPos;
+ }
+}
+
+Ajax.Autocompleter = Class.create();
+Object.extend(Object.extend(Ajax.Autocompleter.prototype, Autocompleter.Base.prototype), {
+ initialize: function(element, update, url, options) {
+ this.baseInitialize(element, update, options);
+ this.options.asynchronous = true;
+ this.options.onComplete = this.onComplete.bind(this);
+ this.options.defaultParams = this.options.parameters || null;
+ this.url = url;
+ },
+
+ getUpdatedChoices: function() {
+ entry = encodeURIComponent(this.options.paramName) + '=' +
+ encodeURIComponent(this.getToken());
+
+ this.options.parameters = this.options.callback ?
+ this.options.callback(this.element, entry) : entry;
+
+ if(this.options.defaultParams)
+ this.options.parameters += '&' + this.options.defaultParams;
+
+ new Ajax.Request(this.url, this.options);
+ },
+
+ onComplete: function(request) {
+ this.updateChoices(request.responseText);
+ }
+
+});
+
+// The local array autocompleter. Used when you'd prefer to
+// inject an array of autocompletion options into the page, rather
+// than sending out Ajax queries, which can be quite slow sometimes.
+//
+// The constructor takes four parameters. The first two are, as usual,
+// the id of the monitored textbox, and id of the autocompletion menu.
+// The third is the array you want to autocomplete from, and the fourth
+// is the options block.
+//
+// Extra local autocompletion options:
+// - choices - How many autocompletion choices to offer
+//
+// - partialSearch - If false, the autocompleter will match entered
+// text only at the beginning of strings in the
+// autocomplete array. Defaults to true, which will
+// match text at the beginning of any *word* in the
+// strings in the autocomplete array. If you want to
+// search anywhere in the string, additionally set
+// the option fullSearch to true (default: off).
+//
+// - fullSsearch - Search anywhere in autocomplete array strings.
+//
+// - partialChars - How many characters to enter before triggering
+// a partial match (unlike minChars, which defines
+// how many characters are required to do any match
+// at all). Defaults to 2.
+//
+// - ignoreCase - Whether to ignore case when autocompleting.
+// Defaults to true.
+//
+// It's possible to pass in a custom function as the 'selector'
+// option, if you prefer to write your own autocompletion logic.
+// In that case, the other options above will not apply unless
+// you support them.
+
+Autocompleter.Local = Class.create();
+Autocompleter.Local.prototype = Object.extend(new Autocompleter.Base(), {
+ initialize: function(element, update, array, options) {
+ this.baseInitialize(element, update, options);
+ this.options.array = array;
+ },
+
+ getUpdatedChoices: function() {
+ this.updateChoices(this.options.selector(this));
+ },
+
+ setOptions: function(options) {
+ this.options = Object.extend({
+ choices: 10,
+ partialSearch: true,
+ partialChars: 2,
+ ignoreCase: true,
+ fullSearch: false,
+ selector: function(instance) {
+ var ret = []; // Beginning matches
+ var partial = []; // Inside matches
+ var entry = instance.getToken();
+ var count = 0;
+
+ for (var i = 0; i < instance.options.array.length &&
+ ret.length < instance.options.choices ; i++) {
+
+ var elem = instance.options.array[i];
+ var foundPos = instance.options.ignoreCase ?
+ elem.toLowerCase().indexOf(entry.toLowerCase()) :
+ elem.indexOf(entry);
+
+ while (foundPos != -1) {
+ if (foundPos == 0 && elem.length != entry.length) {
+ ret.push("<li><strong>" + elem.substr(0, entry.length) + "</strong>" +
+ elem.substr(entry.length) + "</li>");
+ break;
+ } else if (entry.length >= instance.options.partialChars &&
+ instance.options.partialSearch && foundPos != -1) {
+ if (instance.options.fullSearch || /\s/.test(elem.substr(foundPos-1,1))) {
+ partial.push("<li>" + elem.substr(0, foundPos) + "<strong>" +
+ elem.substr(foundPos, entry.length) + "</strong>" + elem.substr(
+ foundPos + entry.length) + "</li>");
+ break;
+ }
+ }
+
+ foundPos = instance.options.ignoreCase ?
+ elem.toLowerCase().indexOf(entry.toLowerCase(), foundPos + 1) :
+ elem.indexOf(entry, foundPos + 1);
+
+ }
+ }
+ if (partial.length)
+ ret = ret.concat(partial.slice(0, instance.options.choices - ret.length))
+ return "<ul>" + ret.join('') + "</ul>";
+ }
+ }, options || {});
+ }
+});
+
+// AJAX in-place editor
+//
+// see documentation on http://wiki.script.aculo.us/scriptaculous/show/Ajax.InPlaceEditor
+
+// Use this if you notice weird scrolling problems on some browsers,
+// the DOM might be a bit confused when this gets called so do this
+// waits 1 ms (with setTimeout) until it does the activation
+Field.scrollFreeActivate = function(field) {
+ setTimeout(function() {
+ Field.activate(field);
+ }, 1);
+}
+
+Ajax.InPlaceEditor = Class.create();
+Ajax.InPlaceEditor.defaultHighlightColor = "#FFFF99";
+Ajax.InPlaceEditor.prototype = {
+ initialize: function(element, url, options) {
+ this.url = url;
+ this.element = $(element);
+
+ this.options = Object.extend({
+ okText: "ok",
+ cancelText: "cancel",
+ savingText: "Saving...",
+ clickToEditText: "Click to edit",
+ okText: "ok",
+ rows: 1,
+ onComplete: function(transport, element) {
+ new Effect.Highlight(element, {startcolor: this.options.highlightcolor});
+ },
+ onFailure: function(transport) {
+ alert("Error communicating with the server: " + transport.responseText.stripTags());
+ },
+ callback: function(form) {
+ return Form.serialize(form);
+ },
+ handleLineBreaks: true,
+ loadingText: 'Loading...',
+ savingClassName: 'inplaceeditor-saving',
+ loadingClassName: 'inplaceeditor-loading',
+ formClassName: 'inplaceeditor-form',
+ highlightcolor: Ajax.InPlaceEditor.defaultHighlightColor,
+ highlightendcolor: "#FFFFFF",
+ externalControl: null,
+ ajaxOptions: {}
+ }, options || {});
+
+ if(!this.options.formId && this.element.id) {
+ this.options.formId = this.element.id + "-inplaceeditor";
+ if ($(this.options.formId)) {
+ // there's already a form with that name, don't specify an id
+ this.options.formId = null;
+ }
+ }
+
+ if (this.options.externalControl) {
+ this.options.externalControl = $(this.options.externalControl);
+ }
+
+ this.originalBackground = Element.getStyle(this.element, 'background-color');
+ if (!this.originalBackground) {
+ this.originalBackground = "transparent";
+ }
+
+ this.element.title = this.options.clickToEditText;
+
+ this.onclickListener = this.enterEditMode.bindAsEventListener(this);
+ this.mouseoverListener = this.enterHover.bindAsEventListener(this);
+ this.mouseoutListener = this.leaveHover.bindAsEventListener(this);
+ Event.observe(this.element, 'click', this.onclickListener);
+ Event.observe(this.element, 'mouseover', this.mouseoverListener);
+ Event.observe(this.element, 'mouseout', this.mouseoutListener);
+ if (this.options.externalControl) {
+ Event.observe(this.options.externalControl, 'click', this.onclickListener);
+ Event.observe(this.options.externalControl, 'mouseover', this.mouseoverListener);
+ Event.observe(this.options.externalControl, 'mouseout', this.mouseoutListener);
+ }
+ },
+ enterEditMode: function(evt) {
+ if (this.saving) return;
+ if (this.editing) return;
+ this.editing = true;
+ this.onEnterEditMode();
+ if (this.options.externalControl) {
+ Element.hide(this.options.externalControl);
+ }
+ Element.hide(this.element);
+ this.createForm();
+ this.element.parentNode.insertBefore(this.form, this.element);
+ Field.scrollFreeActivate(this.editField);
+ // stop the event to avoid a page refresh in Safari
+ if (evt) {
+ Event.stop(evt);
+ }
+ return false;
+ },
+ createForm: function() {
+ this.form = document.createElement("form");
+ this.form.id = this.options.formId;
+ Element.addClassName(this.form, this.options.formClassName)
+ this.form.onsubmit = this.onSubmit.bind(this);
+
+ this.createEditField();
+
+ if (this.options.textarea) {
+ var br = document.createElement("br");
+ this.form.appendChild(br);
+ }
+
+ okButton = document.createElement("input");
+ okButton.type = "submit";
+ okButton.value = this.options.okText;
+ this.form.appendChild(okButton);
+
+ cancelLink = document.createElement("a");
+ cancelLink.href = "#";
+ cancelLink.appendChild(document.createTextNode(this.options.cancelText));
+ cancelLink.onclick = this.onclickCancel.bind(this);
+ this.form.appendChild(cancelLink);
+ },
+ hasHTMLLineBreaks: function(string) {
+ if (!this.options.handleLineBreaks) return false;
+ return string.match(/<br/i) || string.match(/<p>/i);
+ },
+ convertHTMLLineBreaks: function(string) {
+ return string.replace(/<br>/gi, "\n").replace(/<br\/>/gi, "\n").replace(/<\/p>/gi, "\n").replace(/<p>/gi, "");
+ },
+ createEditField: function() {
+ var text;
+ if(this.options.loadTextURL) {
+ text = this.options.loadingText;
+ } else {
+ text = this.getText();
+ }
+
+ if (this.options.rows == 1 && !this.hasHTMLLineBreaks(text)) {
+ this.options.textarea = false;
+ var textField = document.createElement("input");
+ textField.type = "text";
+ textField.name = "value";
+ textField.value = text;
+ textField.style.backgroundColor = this.options.highlightcolor;
+ var size = this.options.size || this.options.cols || 0;
+ if (size != 0) textField.size = size;
+ this.editField = textField;
+ } else {
+ this.options.textarea = true;
+ var textArea = document.createElement("textarea");
+ textArea.name = "value";
+ textArea.value = this.convertHTMLLineBreaks(text);
+ textArea.rows = this.options.rows;
+ textArea.cols = this.options.cols || 40;
+ this.editField = textArea;
+ }
+
+ if(this.options.loadTextURL) {
+ this.loadExternalText();
+ }
+ this.form.appendChild(this.editField);
+ },
+ getText: function() {
+ return this.element.innerHTML;
+ },
+ loadExternalText: function() {
+ Element.addClassName(this.form, this.options.loadingClassName);
+ this.editField.disabled = true;
+ new Ajax.Request(
+ this.options.loadTextURL,
+ Object.extend({
+ asynchronous: true,
+ onComplete: this.onLoadedExternalText.bind(this)
+ }, this.options.ajaxOptions)
+ );
+ },
+ onLoadedExternalText: function(transport) {
+ Element.removeClassName(this.form, this.options.loadingClassName);
+ this.editField.disabled = false;
+ this.editField.value = transport.responseText.stripTags();
+ },
+ onclickCancel: function() {
+ this.onComplete();
+ this.leaveEditMode();
+ return false;
+ },
+ onFailure: function(transport) {
+ this.options.onFailure(transport);
+ if (this.oldInnerHTML) {
+ this.element.innerHTML = this.oldInnerHTML;
+ this.oldInnerHTML = null;
+ }
+ return false;
+ },
+ onSubmit: function() {
+ // onLoading resets these so we need to save them away for the Ajax call
+ var form = this.form;
+ var value = this.editField.value;
+
+ // do this first, sometimes the ajax call returns before we get a chance to switch on Saving...
+ // which means this will actually switch on Saving... *after* we've left edit mode causing Saving...
+ // to be displayed indefinitely
+ this.onLoading();
+
+ new Ajax.Updater(
+ {
+ success: this.element,
+ // don't update on failure (this could be an option)
+ failure: null
+ },
+ this.url,
+ Object.extend({
+ parameters: this.options.callback(form, value),
+ onComplete: this.onComplete.bind(this),
+ onFailure: this.onFailure.bind(this)
+ }, this.options.ajaxOptions)
+ );
+ // stop the event to avoid a page refresh in Safari
+ if (arguments.length > 1) {
+ Event.stop(arguments[0]);
+ }
+ return false;
+ },
+ onLoading: function() {
+ this.saving = true;
+ this.removeForm();
+ this.leaveHover();
+ this.showSaving();
+ },
+ showSaving: function() {
+ this.oldInnerHTML = this.element.innerHTML;
+ this.element.innerHTML = this.options.savingText;
+ Element.addClassName(this.element, this.options.savingClassName);
+ this.element.style.backgroundColor = this.originalBackground;
+ Element.show(this.element);
+ },
+ removeForm: function() {
+ if(this.form) {
+ if (this.form.parentNode) Element.remove(this.form);
+ this.form = null;
+ }
+ },
+ enterHover: function() {
+ if (this.saving) return;
+ this.element.style.backgroundColor = this.options.highlightcolor;
+ if (this.effect) {
+ this.effect.cancel();
+ }
+ Element.addClassName(this.element, this.options.hoverClassName)
+ },
+ leaveHover: function() {
+ if (this.options.backgroundColor) {
+ this.element.style.backgroundColor = this.oldBackground;
+ }
+ Element.removeClassName(this.element, this.options.hoverClassName)
+ if (this.saving) return;
+ this.effect = new Effect.Highlight(this.element, {
+ startcolor: this.options.highlightcolor,
+ endcolor: this.options.highlightendcolor,
+ restorecolor: this.originalBackground
+ });
+ },
+ leaveEditMode: function() {
+ Element.removeClassName(this.element, this.options.savingClassName);
+ this.removeForm();
+ this.leaveHover();
+ this.element.style.backgroundColor = this.originalBackground;
+ Element.show(this.element);
+ if (this.options.externalControl) {
+ Element.show(this.options.externalControl);
+ }
+ this.editing = false;
+ this.saving = false;
+ this.oldInnerHTML = null;
+ this.onLeaveEditMode();
+ },
+ onComplete: function(transport) {
+ this.leaveEditMode();
+ this.options.onComplete.bind(this)(transport, this.element);
+ },
+ onEnterEditMode: function() {},
+ onLeaveEditMode: function() {},
+ dispose: function() {
+ if (this.oldInnerHTML) {
+ this.element.innerHTML = this.oldInnerHTML;
+ }
+ this.leaveEditMode();
+ Event.stopObserving(this.element, 'click', this.onclickListener);
+ Event.stopObserving(this.element, 'mouseover', this.mouseoverListener);
+ Event.stopObserving(this.element, 'mouseout', this.mouseoutListener);
+ if (this.options.externalControl) {
+ Event.stopObserving(this.options.externalControl, 'click', this.onclickListener);
+ Event.stopObserving(this.options.externalControl, 'mouseover', this.mouseoverListener);
+ Event.stopObserving(this.options.externalControl, 'mouseout', this.mouseoutListener);
+ }
+ }
+};
+
+// Delayed observer, like Form.Element.Observer,
+// but waits for delay after last key input
+// Ideal for live-search fields
+
+Form.Element.DelayedObserver = Class.create();
+Form.Element.DelayedObserver.prototype = {
+ initialize: function(element, delay, callback) {
+ this.delay = delay || 0.5;
+ this.element = $(element);
+ this.callback = callback;
+ this.timer = null;
+ this.lastValue = $F(this.element);
+ Event.observe(this.element,'keyup',this.delayedListener.bindAsEventListener(this));
+ },
+ delayedListener: function(event) {
+ if(this.lastValue == $F(this.element)) return;
+ if(this.timer) clearTimeout(this.timer);
+ this.timer = setTimeout(this.onTimerEvent.bind(this), this.delay * 1000);
+ this.lastValue = $F(this.element);
+ },
+ onTimerEvent: function() {
+ this.timer = null;
+ this.callback(this.element, $F(this.element));
+ }
+}; \ No newline at end of file
diff --git a/redmine/public/javascripts/dragdrop.js b/redmine/public/javascripts/dragdrop.js
new file mode 100644
index 000000000..92d1f7316
--- /dev/null
+++ b/redmine/public/javascripts/dragdrop.js
@@ -0,0 +1,584 @@
+// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
+//
+// See scriptaculous.js for full license.
+
+/*--------------------------------------------------------------------------*/
+
+var Droppables = {
+ drops: [],
+
+ remove: function(element) {
+ this.drops = this.drops.reject(function(d) { return d.element==$(element) });
+ },
+
+ add: function(element) {
+ element = $(element);
+ var options = Object.extend({
+ greedy: true,
+ hoverclass: null
+ }, arguments[1] || {});
+
+ // cache containers
+ if(options.containment) {
+ options._containers = [];
+ var containment = options.containment;
+ if((typeof containment == 'object') &&
+ (containment.constructor == Array)) {
+ containment.each( function(c) { options._containers.push($(c)) });
+ } else {
+ options._containers.push($(containment));
+ }
+ }
+
+ if(options.accept) options.accept = [options.accept].flatten();
+
+ Element.makePositioned(element); // fix IE
+ options.element = element;
+
+ this.drops.push(options);
+ },
+
+ isContained: function(element, drop) {
+ var parentNode = element.parentNode;
+ return drop._containers.detect(function(c) { return parentNode == c });
+ },
+
+ isAffected: function(point, element, drop) {
+ return (
+ (drop.element!=element) &&
+ ((!drop._containers) ||
+ this.isContained(element, drop)) &&
+ ((!drop.accept) ||
+ (Element.classNames(element).detect(
+ function(v) { return drop.accept.include(v) } ) )) &&
+ Position.within(drop.element, point[0], point[1]) );
+ },
+
+ deactivate: function(drop) {
+ if(drop.hoverclass)
+ Element.removeClassName(drop.element, drop.hoverclass);
+ this.last_active = null;
+ },
+
+ activate: function(drop) {
+ if(drop.hoverclass)
+ Element.addClassName(drop.element, drop.hoverclass);
+ this.last_active = drop;
+ },
+
+ show: function(point, element) {
+ if(!this.drops.length) return;
+
+ if(this.last_active) this.deactivate(this.last_active);
+ this.drops.each( function(drop) {
+ if(Droppables.isAffected(point, element, drop)) {
+ if(drop.onHover)
+ drop.onHover(element, drop.element, Position.overlap(drop.overlap, drop.element));
+ if(drop.greedy) {
+ Droppables.activate(drop);
+ throw $break;
+ }
+ }
+ });
+ },
+
+ fire: function(event, element) {
+ if(!this.last_active) return;
+ Position.prepare();
+
+ if (this.isAffected([Event.pointerX(event), Event.pointerY(event)], element, this.last_active))
+ if (this.last_active.onDrop)
+ this.last_active.onDrop(element, this.last_active.element, event);
+ },
+
+ reset: function() {
+ if(this.last_active)
+ this.deactivate(this.last_active);
+ }
+}
+
+var Draggables = {
+ drags: [],
+ observers: [],
+
+ register: function(draggable) {
+ if(this.drags.length == 0) {
+ this.eventMouseUp = this.endDrag.bindAsEventListener(this);
+ this.eventMouseMove = this.updateDrag.bindAsEventListener(this);
+ this.eventKeypress = this.keyPress.bindAsEventListener(this);
+
+ Event.observe(document, "mouseup", this.eventMouseUp);
+ Event.observe(document, "mousemove", this.eventMouseMove);
+ Event.observe(document, "keypress", this.eventKeypress);
+ }
+ this.drags.push(draggable);
+ },
+
+ unregister: function(draggable) {
+ this.drags = this.drags.reject(function(d) { return d==draggable });
+ if(this.drags.length == 0) {
+ Event.stopObserving(document, "mouseup", this.eventMouseUp);
+ Event.stopObserving(document, "mousemove", this.eventMouseMove);
+ Event.stopObserving(document, "keypress", this.eventKeypress);
+ }
+ },
+
+ activate: function(draggable) {
+ window.focus(); // allows keypress events if window isn't currently focused, fails for Safari
+ this.activeDraggable = draggable;
+ },
+
+ deactivate: function(draggbale) {
+ this.activeDraggable = null;
+ },
+
+ updateDrag: function(event) {
+ if(!this.activeDraggable) return;
+ var pointer = [Event.pointerX(event), Event.pointerY(event)];
+ // Mozilla-based browsers fire successive mousemove events with
+ // the same coordinates, prevent needless redrawing (moz bug?)
+ if(this._lastPointer && (this._lastPointer.inspect() == pointer.inspect())) return;
+ this._lastPointer = pointer;
+ this.activeDraggable.updateDrag(event, pointer);
+ },
+
+ endDrag: function(event) {
+ if(!this.activeDraggable) return;
+ this._lastPointer = null;
+ this.activeDraggable.endDrag(event);
+ },
+
+ keyPress: function(event) {
+ if(this.activeDraggable)
+ this.activeDraggable.keyPress(event);
+ },
+
+ addObserver: function(observer) {
+ this.observers.push(observer);
+ this._cacheObserverCallbacks();
+ },
+
+ removeObserver: function(element) { // element instead of observer fixes mem leaks
+ this.observers = this.observers.reject( function(o) { return o.element==element });
+ this._cacheObserverCallbacks();
+ },
+
+ notify: function(eventName, draggable, event) { // 'onStart', 'onEnd', 'onDrag'
+ if(this[eventName+'Count'] > 0)
+ this.observers.each( function(o) {
+ if(o[eventName]) o[eventName](eventName, draggable, event);
+ });
+ },
+
+ _cacheObserverCallbacks: function() {
+ ['onStart','onEnd','onDrag'].each( function(eventName) {
+ Draggables[eventName+'Count'] = Draggables.observers.select(
+ function(o) { return o[eventName]; }
+ ).length;
+ });
+ }
+}
+
+/*--------------------------------------------------------------------------*/
+
+var Draggable = Class.create();
+Draggable.prototype = {
+ initialize: function(element) {
+ var options = Object.extend({
+ handle: false,
+ starteffect: function(element) {
+ new Effect.Opacity(element, {duration:0.2, from:1.0, to:0.7});
+ },
+ reverteffect: function(element, top_offset, left_offset) {
+ var dur = Math.sqrt(Math.abs(top_offset^2)+Math.abs(left_offset^2))*0.02;
+ element._revert = new Effect.MoveBy(element, -top_offset, -left_offset, {duration:dur});
+ },
+ endeffect: function(element) {
+ new Effect.Opacity(element, {duration:0.2, from:0.7, to:1.0});
+ },
+ zindex: 1000,
+ revert: false,
+ snap: false // false, or xy or [x,y] or function(x,y){ return [x,y] }
+ }, arguments[1] || {});
+
+ this.element = $(element);
+
+ if(options.handle && (typeof options.handle == 'string'))
+ this.handle = Element.childrenWithClassName(this.element, options.handle)[0];
+ if(!this.handle) this.handle = $(options.handle);
+ if(!this.handle) this.handle = this.element;
+
+ Element.makePositioned(this.element); // fix IE
+
+ this.delta = this.currentDelta();
+ this.options = options;
+ this.dragging = false;
+
+ this.eventMouseDown = this.initDrag.bindAsEventListener(this);
+ Event.observe(this.handle, "mousedown", this.eventMouseDown);
+
+ Draggables.register(this);
+ },
+
+ destroy: function() {
+ Event.stopObserving(this.handle, "mousedown", this.eventMouseDown);
+ Draggables.unregister(this);
+ },
+
+ currentDelta: function() {
+ return([
+ parseInt(this.element.style.left || '0'),
+ parseInt(this.element.style.top || '0')]);
+ },
+
+ initDrag: function(event) {
+ if(Event.isLeftClick(event)) {
+ // abort on form elements, fixes a Firefox issue
+ var src = Event.element(event);
+ if(src.tagName && (
+ src.tagName=='INPUT' ||
+ src.tagName=='SELECT' ||
+ src.tagName=='BUTTON' ||
+ src.tagName=='TEXTAREA')) return;
+
+ if(this.element._revert) {
+ this.element._revert.cancel();
+ this.element._revert = null;
+ }
+
+ var pointer = [Event.pointerX(event), Event.pointerY(event)];
+ var pos = Position.cumulativeOffset(this.element);
+ this.offset = [0,1].map( function(i) { return (pointer[i] - pos[i]) });
+
+ Draggables.activate(this);
+ Event.stop(event);
+ }
+ },
+
+ startDrag: function(event) {
+ this.dragging = true;
+
+ if(this.options.zindex) {
+ this.originalZ = parseInt(Element.getStyle(this.element,'z-index') || 0);
+ this.element.style.zIndex = this.options.zindex;
+ }
+
+ if(this.options.ghosting) {
+ this._clone = this.element.cloneNode(true);
+ Position.absolutize(this.element);
+ this.element.parentNode.insertBefore(this._clone, this.element);
+ }
+
+ Draggables.notify('onStart', this, event);
+ if(this.options.starteffect) this.options.starteffect(this.element);
+ },
+
+ updateDrag: function(event, pointer) {
+ if(!this.dragging) this.startDrag(event);
+ Position.prepare();
+ Droppables.show(pointer, this.element);
+ Draggables.notify('onDrag', this, event);
+ this.draw(pointer);
+ if(this.options.change) this.options.change(this);
+
+ // fix AppleWebKit rendering
+ if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0);
+ Event.stop(event);
+ },
+
+ finishDrag: function(event, success) {
+ this.dragging = false;
+
+ if(this.options.ghosting) {
+ Position.relativize(this.element);
+ Element.remove(this._clone);
+ this._clone = null;
+ }
+
+ if(success) Droppables.fire(event, this.element);
+ Draggables.notify('onEnd', this, event);
+
+ var revert = this.options.revert;
+ if(revert && typeof revert == 'function') revert = revert(this.element);
+
+ var d = this.currentDelta();
+ if(revert && this.options.reverteffect) {
+ this.options.reverteffect(this.element,
+ d[1]-this.delta[1], d[0]-this.delta[0]);
+ } else {
+ this.delta = d;
+ }
+
+ if(this.options.zindex)
+ this.element.style.zIndex = this.originalZ;
+
+ if(this.options.endeffect)
+ this.options.endeffect(this.element);
+
+ Draggables.deactivate(this);
+ Droppables.reset();
+ },
+
+ keyPress: function(event) {
+ if(!event.keyCode==Event.KEY_ESC) return;
+ this.finishDrag(event, false);
+ Event.stop(event);
+ },
+
+ endDrag: function(event) {
+ if(!this.dragging) return;
+ this.finishDrag(event, true);
+ Event.stop(event);
+ },
+
+ draw: function(point) {
+ var pos = Position.cumulativeOffset(this.element);
+ var d = this.currentDelta();
+ pos[0] -= d[0]; pos[1] -= d[1];
+
+ var p = [0,1].map(function(i){ return (point[i]-pos[i]-this.offset[i]) }.bind(this));
+
+ if(this.options.snap) {
+ if(typeof this.options.snap == 'function') {
+ p = this.options.snap(p[0],p[1]);
+ } else {
+ if(this.options.snap instanceof Array) {
+ p = p.map( function(v, i) {
+ return Math.round(v/this.options.snap[i])*this.options.snap[i] }.bind(this))
+ } else {
+ p = p.map( function(v) {
+ return Math.round(v/this.options.snap)*this.options.snap }.bind(this))
+ }
+ }}
+
+ var style = this.element.style;
+ if((!this.options.constraint) || (this.options.constraint=='horizontal'))
+ style.left = p[0] + "px";
+ if((!this.options.constraint) || (this.options.constraint=='vertical'))
+ style.top = p[1] + "px";
+ if(style.visibility=="hidden") style.visibility = ""; // fix gecko rendering
+ }
+}
+
+/*--------------------------------------------------------------------------*/
+
+var SortableObserver = Class.create();
+SortableObserver.prototype = {
+ initialize: function(element, observer) {
+ this.element = $(element);
+ this.observer = observer;
+ this.lastValue = Sortable.serialize(this.element);
+ },
+
+ onStart: function() {
+ this.lastValue = Sortable.serialize(this.element);
+ },
+
+ onEnd: function() {
+ Sortable.unmark();
+ if(this.lastValue != Sortable.serialize(this.element))
+ this.observer(this.element)
+ }
+}
+
+var Sortable = {
+ sortables: new Array(),
+
+ options: function(element){
+ element = $(element);
+ return this.sortables.detect(function(s) { return s.element == element });
+ },
+
+ destroy: function(element){
+ element = $(element);
+ this.sortables.findAll(function(s) { return s.element == element }).each(function(s){
+ Draggables.removeObserver(s.element);
+ s.droppables.each(function(d){ Droppables.remove(d) });
+ s.draggables.invoke('destroy');
+ });
+ this.sortables = this.sortables.reject(function(s) { return s.element == element });
+ },
+
+ create: function(element) {
+ element = $(element);
+ var options = Object.extend({
+ element: element,
+ tag: 'li', // assumes li children, override with tag: 'tagname'
+ dropOnEmpty: false,
+ tree: false, // fixme: unimplemented
+ overlap: 'vertical', // one of 'vertical', 'horizontal'
+ constraint: 'vertical', // one of 'vertical', 'horizontal', false
+ containment: element, // also takes array of elements (or id's); or false
+ handle: false, // or a CSS class
+ only: false,
+ hoverclass: null,
+ ghosting: false,
+ format: null,
+ onChange: Prototype.emptyFunction,
+ onUpdate: Prototype.emptyFunction
+ }, arguments[1] || {});
+
+ // clear any old sortable with same element
+ this.destroy(element);
+
+ // build options for the draggables
+ var options_for_draggable = {
+ revert: true,
+ ghosting: options.ghosting,
+ constraint: options.constraint,
+ handle: options.handle };
+
+ if(options.starteffect)
+ options_for_draggable.starteffect = options.starteffect;
+
+ if(options.reverteffect)
+ options_for_draggable.reverteffect = options.reverteffect;
+ else
+ if(options.ghosting) options_for_draggable.reverteffect = function(element) {
+ element.style.top = 0;
+ element.style.left = 0;
+ };
+
+ if(options.endeffect)
+ options_for_draggable.endeffect = options.endeffect;
+
+ if(options.zindex)
+ options_for_draggable.zindex = options.zindex;
+
+ // build options for the droppables
+ var options_for_droppable = {
+ overlap: options.overlap,
+ containment: options.containment,
+ hoverclass: options.hoverclass,
+ onHover: Sortable.onHover,
+ greedy: !options.dropOnEmpty
+ }
+
+ // fix for gecko engine
+ Element.cleanWhitespace(element);
+
+ options.draggables = [];
+ options.droppables = [];
+
+ // make it so
+
+ // drop on empty handling
+ if(options.dropOnEmpty) {
+ Droppables.add(element,
+ {containment: options.containment, onHover: Sortable.onEmptyHover, greedy: false});
+ options.droppables.push(element);
+ }
+
+ (this.findElements(element, options) || []).each( function(e) {
+ // handles are per-draggable
+ var handle = options.handle ?
+ Element.childrenWithClassName(e, options.handle)[0] : e;
+ options.draggables.push(
+ new Draggable(e, Object.extend(options_for_draggable, { handle: handle })));
+ Droppables.add(e, options_for_droppable);
+ options.droppables.push(e);
+ });
+
+ // keep reference
+ this.sortables.push(options);
+
+ // for onupdate
+ Draggables.addObserver(new SortableObserver(element, options.onUpdate));
+
+ },
+
+ // return all suitable-for-sortable elements in a guaranteed order
+ findElements: function(element, options) {
+ if(!element.hasChildNodes()) return null;
+ var elements = [];
+ $A(element.childNodes).each( function(e) {
+ if(e.tagName && e.tagName.toUpperCase()==options.tag.toUpperCase() &&
+ (!options.only || (Element.hasClassName(e, options.only))))
+ elements.push(e);
+ if(options.tree) {
+ var grandchildren = this.findElements(e, options);
+ if(grandchildren) elements.push(grandchildren);
+ }
+ });
+
+ return (elements.length>0 ? elements.flatten() : null);
+ },
+
+ onHover: function(element, dropon, overlap) {
+ if(overlap>0.5) {
+ Sortable.mark(dropon, 'before');
+ if(dropon.previousSibling != element) {
+ var oldParentNode = element.parentNode;
+ element.style.visibility = "hidden"; // fix gecko rendering
+ dropon.parentNode.insertBefore(element, dropon);
+ if(dropon.parentNode!=oldParentNode)
+ Sortable.options(oldParentNode).onChange(element);
+ Sortable.options(dropon.parentNode).onChange(element);
+ }
+ } else {
+ Sortable.mark(dropon, 'after');
+ var nextElement = dropon.nextSibling || null;
+ if(nextElement != element) {
+ var oldParentNode = element.parentNode;
+ element.style.visibility = "hidden"; // fix gecko rendering
+ dropon.parentNode.insertBefore(element, nextElement);
+ if(dropon.parentNode!=oldParentNode)
+ Sortable.options(oldParentNode).onChange(element);
+ Sortable.options(dropon.parentNode).onChange(element);
+ }
+ }
+ },
+
+ onEmptyHover: function(element, dropon) {
+ if(element.parentNode!=dropon) {
+ var oldParentNode = element.parentNode;
+ dropon.appendChild(element);
+ Sortable.options(oldParentNode).onChange(element);
+ Sortable.options(dropon).onChange(element);
+ }
+ },
+
+ unmark: function() {
+ if(Sortable._marker) Element.hide(Sortable._marker);
+ },
+
+ mark: function(dropon, position) {
+ // mark on ghosting only
+ var sortable = Sortable.options(dropon.parentNode);
+ if(sortable && !sortable.ghosting) return;
+
+ if(!Sortable._marker) {
+ Sortable._marker = $('dropmarker') || document.createElement('DIV');
+ Element.hide(Sortable._marker);
+ Element.addClassName(Sortable._marker, 'dropmarker');
+ Sortable._marker.style.position = 'absolute';
+ document.getElementsByTagName("body").item(0).appendChild(Sortable._marker);
+ }
+ var offsets = Position.cumulativeOffset(dropon);
+ Sortable._marker.style.left = offsets[0] + 'px';
+ Sortable._marker.style.top = offsets[1] + 'px';
+
+ if(position=='after')
+ if(sortable.overlap == 'horizontal')
+ Sortable._marker.style.left = (offsets[0]+dropon.clientWidth) + 'px';
+ else
+ Sortable._marker.style.top = (offsets[1]+dropon.clientHeight) + 'px';
+
+ Element.show(Sortable._marker);
+ },
+
+ serialize: function(element) {
+ element = $(element);
+ var sortableOptions = this.options(element);
+ var options = Object.extend({
+ tag: sortableOptions.tag,
+ only: sortableOptions.only,
+ name: element.id,
+ format: sortableOptions.format || /^[^_]*_(.*)$/
+ }, arguments[1] || {});
+ return $(this.findElements(element, options) || []).map( function(item) {
+ return (encodeURIComponent(options.name) + "[]=" +
+ encodeURIComponent(item.id.match(options.format) ? item.id.match(options.format)[1] : ''));
+ }).join("&");
+ }
+} \ No newline at end of file
diff --git a/redmine/public/javascripts/effects.js b/redmine/public/javascripts/effects.js
new file mode 100644
index 000000000..414398ce4
--- /dev/null
+++ b/redmine/public/javascripts/effects.js
@@ -0,0 +1,854 @@
+// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
+// Contributors:
+// Justin Palmer (http://encytemedia.com/)
+// Mark Pilgrim (http://diveintomark.org/)
+// Martin Bialasinki
+//
+// See scriptaculous.js for full license.
+
+/* ------------- element ext -------------- */
+
+// converts rgb() and #xxx to #xxxxxx format,
+// returns self (or first argument) if not convertable
+String.prototype.parseColor = function() {
+ var color = '#';
+ if(this.slice(0,4) == 'rgb(') {
+ var cols = this.slice(4,this.length-1).split(',');
+ var i=0; do { color += parseInt(cols[i]).toColorPart() } while (++i<3);
+ } else {
+ if(this.slice(0,1) == '#') {
+ if(this.length==4) for(var i=1;i<4;i++) color += (this.charAt(i) + this.charAt(i)).toLowerCase();
+ if(this.length==7) color = this.toLowerCase();
+ }
+ }
+ return(color.length==7 ? color : (arguments[0] || this));
+}
+
+Element.collectTextNodesIgnoreClass = function(element, ignoreclass) {
+ var children = $(element).childNodes;
+ var text = '';
+ var classtest = new RegExp('^([^ ]+ )*' + ignoreclass+ '( [^ ]+)*$','i');
+
+ for (var i = 0; i < children.length; i++) {
+ if(children[i].nodeType==3) {
+ text+=children[i].nodeValue;
+ } else {
+ if((!children[i].className.match(classtest)) && children[i].hasChildNodes())
+ text += Element.collectTextNodesIgnoreClass(children[i], ignoreclass);
+ }
+ }
+
+ return text;
+}
+
+Element.setStyle = function(element, style) {
+ element = $(element);
+ for(k in style) element.style[k.camelize()] = style[k];
+}
+
+Element.setContentZoom = function(element, percent) {
+ Element.setStyle(element, {fontSize: (percent/100) + 'em'});
+ if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0);
+}
+
+Element.getOpacity = function(element){
+ var opacity;
+ if (opacity = Element.getStyle(element, 'opacity'))
+ return parseFloat(opacity);
+ if (opacity = (Element.getStyle(element, 'filter') || '').match(/alpha\(opacity=(.*)\)/))
+ if(opacity[1]) return parseFloat(opacity[1]) / 100;
+ return 1.0;
+}
+
+Element.setOpacity = function(element, value){
+ element= $(element);
+ if (value == 1){
+ Element.setStyle(element, { opacity:
+ (/Gecko/.test(navigator.userAgent) && !/Konqueror|Safari|KHTML/.test(navigator.userAgent)) ?
+ 0.999999 : null });
+ if(/MSIE/.test(navigator.userAgent))
+ Element.setStyle(element, {filter: Element.getStyle(element,'filter').replace(/alpha\([^\)]*\)/gi,'')});
+ } else {
+ if(value < 0.00001) value = 0;
+ Element.setStyle(element, {opacity: value});
+ if(/MSIE/.test(navigator.userAgent))
+ Element.setStyle(element,
+ { filter: Element.getStyle(element,'filter').replace(/alpha\([^\)]*\)/gi,'') +
+ 'alpha(opacity='+value*100+')' });
+ }
+}
+
+Element.getInlineOpacity = function(element){
+ return $(element).style.opacity || '';
+}
+
+Element.childrenWithClassName = function(element, className) {
+ return $A($(element).getElementsByTagName('*')).select(
+ function(c) { return Element.hasClassName(c, className) });
+}
+
+Array.prototype.call = function() {
+ var args = arguments;
+ this.each(function(f){ f.apply(this, args) });
+}
+
+/*--------------------------------------------------------------------------*/
+
+var Effect = {
+ tagifyText: function(element) {
+ var tagifyStyle = 'position:relative';
+ if(/MSIE/.test(navigator.userAgent)) tagifyStyle += ';zoom:1';
+ element = $(element);
+ $A(element.childNodes).each( function(child) {
+ if(child.nodeType==3) {
+ child.nodeValue.toArray().each( function(character) {
+ element.insertBefore(
+ Builder.node('span',{style: tagifyStyle},
+ character == ' ' ? String.fromCharCode(160) : character),
+ child);
+ });
+ Element.remove(child);
+ }
+ });
+ },
+ multiple: function(element, effect) {
+ var elements;
+ if(((typeof element == 'object') ||
+ (typeof element == 'function')) &&
+ (element.length))
+ elements = element;
+ else
+ elements = $(element).childNodes;
+
+ var options = Object.extend({
+ speed: 0.1,
+ delay: 0.0
+ }, arguments[2] || {});
+ var masterDelay = options.delay;
+
+ $A(elements).each( function(element, index) {
+ new effect(element, Object.extend(options, { delay: index * options.speed + masterDelay }));
+ });
+ }
+};
+
+var Effect2 = Effect; // deprecated
+
+/* ------------- transitions ------------- */
+
+Effect.Transitions = {}
+
+Effect.Transitions.linear = function(pos) {
+ return pos;
+}
+Effect.Transitions.sinoidal = function(pos) {
+ return (-Math.cos(pos*Math.PI)/2) + 0.5;
+}
+Effect.Transitions.reverse = function(pos) {
+ return 1-pos;
+}
+Effect.Transitions.flicker = function(pos) {
+ return ((-Math.cos(pos*Math.PI)/4) + 0.75) + Math.random()/4;
+}
+Effect.Transitions.wobble = function(pos) {
+ return (-Math.cos(pos*Math.PI*(9*pos))/2) + 0.5;
+}
+Effect.Transitions.pulse = function(pos) {
+ return (Math.floor(pos*10) % 2 == 0 ?
+ (pos*10-Math.floor(pos*10)) : 1-(pos*10-Math.floor(pos*10)));
+}
+Effect.Transitions.none = function(pos) {
+ return 0;
+}
+Effect.Transitions.full = function(pos) {
+ return 1;
+}
+
+/* ------------- core effects ------------- */
+
+Effect.Queue = {
+ effects: [],
+ _each: function(iterator) {
+ this.effects._each(iterator);
+ },
+ interval: null,
+ add: function(effect) {
+ var timestamp = new Date().getTime();
+
+ switch(effect.options.queue) {
+ case 'front':
+ // move unstarted effects after this effect
+ this.effects.findAll(function(e){ return e.state=='idle' }).each( function(e) {
+ e.startOn += effect.finishOn;
+ e.finishOn += effect.finishOn;
+ });
+ break;
+ case 'end':
+ // start effect after last queued effect has finished
+ timestamp = this.effects.pluck('finishOn').max() || timestamp;
+ break;
+ }
+
+ effect.startOn += timestamp;
+ effect.finishOn += timestamp;
+ this.effects.push(effect);
+ if(!this.interval)
+ this.interval = setInterval(this.loop.bind(this), 40);
+ },
+ remove: function(effect) {
+ this.effects = this.effects.reject(function(e) { return e==effect });
+ if(this.effects.length == 0) {
+ clearInterval(this.interval);
+ this.interval = null;
+ }
+ },
+ loop: function() {
+ var timePos = new Date().getTime();
+ this.effects.invoke('loop', timePos);
+ }
+}
+Object.extend(Effect.Queue, Enumerable);
+
+Effect.Base = function() {};
+Effect.Base.prototype = {
+ position: null,
+ setOptions: function(options) {
+ this.options = Object.extend({
+ transition: Effect.Transitions.sinoidal,
+ duration: 1.0, // seconds
+ fps: 25.0, // max. 25fps due to Effect.Queue implementation
+ sync: false, // true for combining
+ from: 0.0,
+ to: 1.0,
+ delay: 0.0,
+ queue: 'parallel'
+ }, options || {});
+ },
+ start: function(options) {
+ this.setOptions(options || {});
+ this.currentFrame = 0;
+ this.state = 'idle';
+ this.startOn = this.options.delay*1000;
+ this.finishOn = this.startOn + (this.options.duration*1000);
+ this.event('beforeStart');
+ if(!this.options.sync) Effect.Queue.add(this);
+ },
+ loop: function(timePos) {
+ if(timePos >= this.startOn) {
+ if(timePos >= this.finishOn) {
+ this.render(1.0);
+ this.cancel();
+ this.event('beforeFinish');
+ if(this.finish) this.finish();
+ this.event('afterFinish');
+ return;
+ }
+ var pos = (timePos - this.startOn) / (this.finishOn - this.startOn);
+ var frame = Math.round(pos * this.options.fps * this.options.duration);
+ if(frame > this.currentFrame) {
+ this.render(pos);
+ this.currentFrame = frame;
+ }
+ }
+ },
+ render: function(pos) {
+ if(this.state == 'idle') {
+ this.state = 'running';
+ this.event('beforeSetup');
+ if(this.setup) this.setup();
+ this.event('afterSetup');
+ }
+ if(this.state == 'running') {
+ if(this.options.transition) pos = this.options.transition(pos);
+ pos *= (this.options.to-this.options.from);
+ pos += this.options.from;
+ this.position = pos;
+ this.event('beforeUpdate');
+ if(this.update) this.update(pos);
+ this.event('afterUpdate');
+ }
+ },
+ cancel: function() {
+ if(!this.options.sync) Effect.Queue.remove(this);
+ this.state = 'finished';
+ },
+ event: function(eventName) {
+ if(this.options[eventName + 'Internal']) this.options[eventName + 'Internal'](this);
+ if(this.options[eventName]) this.options[eventName](this);
+ },
+ inspect: function() {
+ return '#<Effect:' + $H(this).inspect() + ',options:' + $H(this.options).inspect() + '>';
+ }
+}
+
+Effect.Parallel = Class.create();
+Object.extend(Object.extend(Effect.Parallel.prototype, Effect.Base.prototype), {
+ initialize: function(effects) {
+ this.effects = effects || [];
+ this.start(arguments[1]);
+ },
+ update: function(position) {
+ this.effects.invoke('render', position);
+ },
+ finish: function(position) {
+ this.effects.each( function(effect) {
+ effect.render(1.0);
+ effect.cancel();
+ effect.event('beforeFinish');
+ if(effect.finish) effect.finish(position);
+ effect.event('afterFinish');
+ });
+ }
+});
+
+Effect.Opacity = Class.create();
+Object.extend(Object.extend(Effect.Opacity.prototype, Effect.Base.prototype), {
+ initialize: function(element) {
+ this.element = $(element);
+ // make this work on IE on elements without 'layout'
+ if(/MSIE/.test(navigator.userAgent) && (!this.element.hasLayout))
+ Element.setStyle(this.element, {zoom: 1});
+ var options = Object.extend({
+ from: Element.getOpacity(this.element) || 0.0,
+ to: 1.0
+ }, arguments[1] || {});
+ this.start(options);
+ },
+ update: function(position) {
+ Element.setOpacity(this.element, position);
+ }
+});
+
+Effect.MoveBy = Class.create();
+Object.extend(Object.extend(Effect.MoveBy.prototype, Effect.Base.prototype), {
+ initialize: function(element, toTop, toLeft) {
+ this.element = $(element);
+ this.toTop = toTop;
+ this.toLeft = toLeft;
+ this.start(arguments[3]);
+ },
+ setup: function() {
+ // Bug in Opera: Opera returns the "real" position of a static element or
+ // relative element that does not have top/left explicitly set.
+ // ==> Always set top and left for position relative elements in your stylesheets
+ // (to 0 if you do not need them)
+ Element.makePositioned(this.element);
+ this.originalTop = parseFloat(Element.getStyle(this.element,'top') || '0');
+ this.originalLeft = parseFloat(Element.getStyle(this.element,'left') || '0');
+ },
+ update: function(position) {
+ Element.setStyle(this.element, {
+ top: this.toTop * position + this.originalTop + 'px',
+ left: this.toLeft * position + this.originalLeft + 'px'
+ });
+ }
+});
+
+Effect.Scale = Class.create();
+Object.extend(Object.extend(Effect.Scale.prototype, Effect.Base.prototype), {
+ initialize: function(element, percent) {
+ this.element = $(element)
+ var options = Object.extend({
+ scaleX: true,
+ scaleY: true,
+ scaleContent: true,
+ scaleFromCenter: false,
+ scaleMode: 'box', // 'box' or 'contents' or {} with provided values
+ scaleFrom: 100.0,
+ scaleTo: percent
+ }, arguments[2] || {});
+ this.start(options);
+ },
+ setup: function() {
+ this.restoreAfterFinish = this.options.restoreAfterFinish || false;
+ this.elementPositioning = Element.getStyle(this.element,'position');
+
+ this.originalStyle = {};
+ ['top','left','width','height','fontSize'].each( function(k) {
+ this.originalStyle[k] = this.element.style[k];
+ }.bind(this));
+
+ this.originalTop = this.element.offsetTop;
+ this.originalLeft = this.element.offsetLeft;
+
+ var fontSize = Element.getStyle(this.element,'font-size') || '100%';
+ ['em','px','%'].each( function(fontSizeType) {
+ if(fontSize.indexOf(fontSizeType)>0) {
+ this.fontSize = parseFloat(fontSize);
+ this.fontSizeType = fontSizeType;
+ }
+ }.bind(this));
+
+ this.factor = (this.options.scaleTo - this.options.scaleFrom)/100;
+
+ this.dims = null;
+ if(this.options.scaleMode=='box')
+ this.dims = [this.element.offsetHeight, this.element.offsetWidth];
+ if(/^content/.test(this.options.scaleMode))
+ this.dims = [this.element.scrollHeight, this.element.scrollWidth];
+ if(!this.dims)
+ this.dims = [this.options.scaleMode.originalHeight,
+ this.options.scaleMode.originalWidth];
+ },
+ update: function(position) {
+ var currentScale = (this.options.scaleFrom/100.0) + (this.factor * position);
+ if(this.options.scaleContent && this.fontSize)
+ Element.setStyle(this.element, {fontSize: this.fontSize * currentScale + this.fontSizeType });
+ this.setDimensions(this.dims[0] * currentScale, this.dims[1] * currentScale);
+ },
+ finish: function(position) {
+ if (this.restoreAfterFinish) Element.setStyle(this.element, this.originalStyle);
+ },
+ setDimensions: function(height, width) {
+ var d = {};
+ if(this.options.scaleX) d.width = width + 'px';
+ if(this.options.scaleY) d.height = height + 'px';
+ if(this.options.scaleFromCenter) {
+ var topd = (height - this.dims[0])/2;
+ var leftd = (width - this.dims[1])/2;
+ if(this.elementPositioning == 'absolute') {
+ if(this.options.scaleY) d.top = this.originalTop-topd + 'px';
+ if(this.options.scaleX) d.left = this.originalLeft-leftd + 'px';
+ } else {
+ if(this.options.scaleY) d.top = -topd + 'px';
+ if(this.options.scaleX) d.left = -leftd + 'px';
+ }
+ }
+ Element.setStyle(this.element, d);
+ }
+});
+
+Effect.Highlight = Class.create();
+Object.extend(Object.extend(Effect.Highlight.prototype, Effect.Base.prototype), {
+ initialize: function(element) {
+ this.element = $(element);
+ var options = Object.extend({ startcolor: '#ffff99' }, arguments[1] || {});
+ this.start(options);
+ },
+ setup: function() {
+ // Prevent executing on elements not in the layout flow
+ if(Element.getStyle(this.element, 'display')=='none') { this.cancel(); return; }
+ // Disable background image during the effect
+ this.oldStyle = {
+ backgroundImage: Element.getStyle(this.element, 'background-image') };
+ Element.setStyle(this.element, {backgroundImage: 'none'});
+ if(!this.options.endcolor)
+ this.options.endcolor = Element.getStyle(this.element, 'background-color').parseColor('#ffffff');
+ if(!this.options.restorecolor)
+ this.options.restorecolor = Element.getStyle(this.element, 'background-color');
+ // init color calculations
+ this._base = $R(0,2).map(function(i){ return parseInt(this.options.startcolor.slice(i*2+1,i*2+3),16) }.bind(this));
+ this._delta = $R(0,2).map(function(i){ return parseInt(this.options.endcolor.slice(i*2+1,i*2+3),16)-this._base[i] }.bind(this));
+ },
+ update: function(position) {
+ Element.setStyle(this.element,{backgroundColor: $R(0,2).inject('#',function(m,v,i){
+ return m+(Math.round(this._base[i]+(this._delta[i]*position)).toColorPart()); }.bind(this)) });
+ },
+ finish: function() {
+ Element.setStyle(this.element, Object.extend(this.oldStyle, {
+ backgroundColor: this.options.restorecolor
+ }));
+ }
+});
+
+Effect.ScrollTo = Class.create();
+Object.extend(Object.extend(Effect.ScrollTo.prototype, Effect.Base.prototype), {
+ initialize: function(element) {
+ this.element = $(element);
+ this.start(arguments[1] || {});
+ },
+ setup: function() {
+ Position.prepare();
+ var offsets = Position.cumulativeOffset(this.element);
+ if(this.options.offset) offsets[1] += this.options.offset;
+ var max = window.innerHeight ?
+ window.height - window.innerHeight :
+ document.body.scrollHeight -
+ (document.documentElement.clientHeight ?
+ document.documentElement.clientHeight : document.body.clientHeight);
+ this.scrollStart = Position.deltaY;
+ this.delta = (offsets[1] > max ? max : offsets[1]) - this.scrollStart;
+ },
+ update: function(position) {
+ Position.prepare();
+ window.scrollTo(Position.deltaX,
+ this.scrollStart + (position*this.delta));
+ }
+});
+
+/* ------------- combination effects ------------- */
+
+Effect.Fade = function(element) {
+ var oldOpacity = Element.getInlineOpacity(element);
+ var options = Object.extend({
+ from: Element.getOpacity(element) || 1.0,
+ to: 0.0,
+ afterFinishInternal: function(effect) { with(Element) {
+ if(effect.options.to!=0) return;
+ hide(effect.element);
+ setStyle(effect.element, {opacity: oldOpacity}); }}
+ }, arguments[1] || {});
+ return new Effect.Opacity(element,options);
+}
+
+Effect.Appear = function(element) {
+ var options = Object.extend({
+ from: (Element.getStyle(element, 'display') == 'none' ? 0.0 : Element.getOpacity(element) || 0.0),
+ to: 1.0,
+ beforeSetup: function(effect) { with(Element) {
+ setOpacity(effect.element, effect.options.from);
+ show(effect.element); }}
+ }, arguments[1] || {});
+ return new Effect.Opacity(element,options);
+}
+
+Effect.Puff = function(element) {
+ element = $(element);
+ var oldStyle = { opacity: Element.getInlineOpacity(element), position: Element.getStyle(element, 'position') };
+ return new Effect.Parallel(
+ [ new Effect.Scale(element, 200,
+ { sync: true, scaleFromCenter: true, scaleContent: true, restoreAfterFinish: true }),
+ new Effect.Opacity(element, { sync: true, to: 0.0 } ) ],
+ Object.extend({ duration: 1.0,
+ beforeSetupInternal: function(effect) { with(Element) {
+ setStyle(effect.effects[0].element, {position: 'absolute'}); }},
+ afterFinishInternal: function(effect) { with(Element) {
+ hide(effect.effects[0].element);
+ setStyle(effect.effects[0].element, oldStyle); }}
+ }, arguments[1] || {})
+ );
+}
+
+Effect.BlindUp = function(element) {
+ element = $(element);
+ Element.makeClipping(element);
+ return new Effect.Scale(element, 0,
+ Object.extend({ scaleContent: false,
+ scaleX: false,
+ restoreAfterFinish: true,
+ afterFinishInternal: function(effect) { with(Element) {
+ [hide, undoClipping].call(effect.element); }}
+ }, arguments[1] || {})
+ );
+}
+
+Effect.BlindDown = function(element) {
+ element = $(element);
+ var oldHeight = Element.getStyle(element, 'height');
+ var elementDimensions = Element.getDimensions(element);
+ return new Effect.Scale(element, 100,
+ Object.extend({ scaleContent: false,
+ scaleX: false,
+ scaleFrom: 0,
+ scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width},
+ restoreAfterFinish: true,
+ afterSetup: function(effect) { with(Element) {
+ makeClipping(effect.element);
+ setStyle(effect.element, {height: '0px'});
+ show(effect.element);
+ }},
+ afterFinishInternal: function(effect) { with(Element) {
+ undoClipping(effect.element);
+ setStyle(effect.element, {height: oldHeight});
+ }}
+ }, arguments[1] || {})
+ );
+}
+
+Effect.SwitchOff = function(element) {
+ element = $(element);
+ var oldOpacity = Element.getInlineOpacity(element);
+ return new Effect.Appear(element, {
+ duration: 0.4,
+ from: 0,
+ transition: Effect.Transitions.flicker,
+ afterFinishInternal: function(effect) {
+ new Effect.Scale(effect.element, 1, {
+ duration: 0.3, scaleFromCenter: true,
+ scaleX: false, scaleContent: false, restoreAfterFinish: true,
+ beforeSetup: function(effect) { with(Element) {
+ [makePositioned,makeClipping].call(effect.element);
+ }},
+ afterFinishInternal: function(effect) { with(Element) {
+ [hide,undoClipping,undoPositioned].call(effect.element);
+ setStyle(effect.element, {opacity: oldOpacity});
+ }}
+ })
+ }
+ });
+}
+
+Effect.DropOut = function(element) {
+ element = $(element);
+ var oldStyle = {
+ top: Element.getStyle(element, 'top'),
+ left: Element.getStyle(element, 'left'),
+ opacity: Element.getInlineOpacity(element) };
+ return new Effect.Parallel(
+ [ new Effect.MoveBy(element, 100, 0, { sync: true }),
+ new Effect.Opacity(element, { sync: true, to: 0.0 }) ],
+ Object.extend(
+ { duration: 0.5,
+ beforeSetup: function(effect) { with(Element) {
+ makePositioned(effect.effects[0].element); }},
+ afterFinishInternal: function(effect) { with(Element) {
+ [hide, undoPositioned].call(effect.effects[0].element);
+ setStyle(effect.effects[0].element, oldStyle); }}
+ }, arguments[1] || {}));
+}
+
+Effect.Shake = function(element) {
+ element = $(element);
+ var oldStyle = {
+ top: Element.getStyle(element, 'top'),
+ left: Element.getStyle(element, 'left') };
+ return new Effect.MoveBy(element, 0, 20,
+ { duration: 0.05, afterFinishInternal: function(effect) {
+ new Effect.MoveBy(effect.element, 0, -40,
+ { duration: 0.1, afterFinishInternal: function(effect) {
+ new Effect.MoveBy(effect.element, 0, 40,
+ { duration: 0.1, afterFinishInternal: function(effect) {
+ new Effect.MoveBy(effect.element, 0, -40,
+ { duration: 0.1, afterFinishInternal: function(effect) {
+ new Effect.MoveBy(effect.element, 0, 40,
+ { duration: 0.1, afterFinishInternal: function(effect) {
+ new Effect.MoveBy(effect.element, 0, -20,
+ { duration: 0.05, afterFinishInternal: function(effect) { with(Element) {
+ undoPositioned(effect.element);
+ setStyle(effect.element, oldStyle);
+ }}}) }}) }}) }}) }}) }});
+}
+
+Effect.SlideDown = function(element) {
+ element = $(element);
+ Element.cleanWhitespace(element);
+ // SlideDown need to have the content of the element wrapped in a container element with fixed height!
+ var oldInnerBottom = Element.getStyle(element.firstChild, 'bottom');
+ var elementDimensions = Element.getDimensions(element);
+ return new Effect.Scale(element, 100, Object.extend({
+ scaleContent: false,
+ scaleX: false,
+ scaleFrom: 0,
+ scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width},
+ restoreAfterFinish: true,
+ afterSetup: function(effect) { with(Element) {
+ makePositioned(effect.element);
+ makePositioned(effect.element.firstChild);
+ if(window.opera) setStyle(effect.element, {top: ''});
+ makeClipping(effect.element);
+ setStyle(effect.element, {height: '0px'});
+ show(element); }},
+ afterUpdateInternal: function(effect) { with(Element) {
+ setStyle(effect.element.firstChild, {bottom:
+ (effect.dims[0] - effect.element.clientHeight) + 'px' }); }},
+ afterFinishInternal: function(effect) { with(Element) {
+ undoClipping(effect.element);
+ undoPositioned(effect.element.firstChild);
+ undoPositioned(effect.element);
+ setStyle(effect.element.firstChild, {bottom: oldInnerBottom}); }}
+ }, arguments[1] || {})
+ );
+}
+
+Effect.SlideUp = function(element) {
+ element = $(element);
+ Element.cleanWhitespace(element);
+ var oldInnerBottom = Element.getStyle(element.firstChild, 'bottom');
+ return new Effect.Scale(element, 0,
+ Object.extend({ scaleContent: false,
+ scaleX: false,
+ scaleMode: 'box',
+ scaleFrom: 100,
+ restoreAfterFinish: true,
+ beforeStartInternal: function(effect) { with(Element) {
+ makePositioned(effect.element);
+ makePositioned(effect.element.firstChild);
+ if(window.opera) setStyle(effect.element, {top: ''});
+ makeClipping(effect.element);
+ show(element); }},
+ afterUpdateInternal: function(effect) { with(Element) {
+ setStyle(effect.element.firstChild, {bottom:
+ (effect.dims[0] - effect.element.clientHeight) + 'px' }); }},
+ afterFinishInternal: function(effect) { with(Element) {
+ [hide, undoClipping].call(effect.element);
+ undoPositioned(effect.element.firstChild);
+ undoPositioned(effect.element);
+ setStyle(effect.element.firstChild, {bottom: oldInnerBottom}); }}
+ }, arguments[1] || {})
+ );
+}
+
+// Bug in opera makes the TD containing this element expand for a instance after finish
+Effect.Squish = function(element) {
+ return new Effect.Scale(element, window.opera ? 1 : 0,
+ { restoreAfterFinish: true,
+ beforeSetup: function(effect) { with(Element) {
+ makeClipping(effect.element); }},
+ afterFinishInternal: function(effect) { with(Element) {
+ hide(effect.element);
+ undoClipping(effect.element); }}
+ });
+}
+
+Effect.Grow = function(element) {
+ element = $(element);
+ var options = Object.extend({
+ direction: 'center',
+ moveTransistion: Effect.Transitions.sinoidal,
+ scaleTransition: Effect.Transitions.sinoidal,
+ opacityTransition: Effect.Transitions.full
+ }, arguments[1] || {});
+ var oldStyle = {
+ top: element.style.top,
+ left: element.style.left,
+ height: element.style.height,
+ width: element.style.width,
+ opacity: Element.getInlineOpacity(element) };
+
+ var dims = Element.getDimensions(element);
+ var initialMoveX, initialMoveY;
+ var moveX, moveY;
+
+ switch (options.direction) {
+ case 'top-left':
+ initialMoveX = initialMoveY = moveX = moveY = 0;
+ break;
+ case 'top-right':
+ initialMoveX = dims.width;
+ initialMoveY = moveY = 0;
+ moveX = -dims.width;
+ break;
+ case 'bottom-left':
+ initialMoveX = moveX = 0;
+ initialMoveY = dims.height;
+ moveY = -dims.height;
+ break;
+ case 'bottom-right':
+ initialMoveX = dims.width;
+ initialMoveY = dims.height;
+ moveX = -dims.width;
+ moveY = -dims.height;
+ break;
+ case 'center':
+ initialMoveX = dims.width / 2;
+ initialMoveY = dims.height / 2;
+ moveX = -dims.width / 2;
+ moveY = -dims.height / 2;
+ break;
+ }
+
+ return new Effect.MoveBy(element, initialMoveY, initialMoveX, {
+ duration: 0.01,
+ beforeSetup: function(effect) { with(Element) {
+ hide(effect.element);
+ makeClipping(effect.element);
+ makePositioned(effect.element);
+ }},
+ afterFinishInternal: function(effect) {
+ new Effect.Parallel(
+ [ new Effect.Opacity(effect.element, { sync: true, to: 1.0, from: 0.0, transition: options.opacityTransition }),
+ new Effect.MoveBy(effect.element, moveY, moveX, { sync: true, transition: options.moveTransition }),
+ new Effect.Scale(effect.element, 100, {
+ scaleMode: { originalHeight: dims.height, originalWidth: dims.width },
+ sync: true, scaleFrom: window.opera ? 1 : 0, transition: options.scaleTransition, restoreAfterFinish: true})
+ ], Object.extend({
+ beforeSetup: function(effect) { with(Element) {
+ setStyle(effect.effects[0].element, {height: '0px'});
+ show(effect.effects[0].element); }},
+ afterFinishInternal: function(effect) { with(Element) {
+ [undoClipping, undoPositioned].call(effect.effects[0].element);
+ setStyle(effect.effects[0].element, oldStyle); }}
+ }, options)
+ )
+ }
+ });
+}
+
+Effect.Shrink = function(element) {
+ element = $(element);
+ var options = Object.extend({
+ direction: 'center',
+ moveTransistion: Effect.Transitions.sinoidal,
+ scaleTransition: Effect.Transitions.sinoidal,
+ opacityTransition: Effect.Transitions.none
+ }, arguments[1] || {});
+ var oldStyle = {
+ top: element.style.top,
+ left: element.style.left,
+ height: element.style.height,
+ width: element.style.width,
+ opacity: Element.getInlineOpacity(element) };
+
+ var dims = Element.getDimensions(element);
+ var moveX, moveY;
+
+ switch (options.direction) {
+ case 'top-left':
+ moveX = moveY = 0;
+ break;
+ case 'top-right':
+ moveX = dims.width;
+ moveY = 0;
+ break;
+ case 'bottom-left':
+ moveX = 0;
+ moveY = dims.height;
+ break;
+ case 'bottom-right':
+ moveX = dims.width;
+ moveY = dims.height;
+ break;
+ case 'center':
+ moveX = dims.width / 2;
+ moveY = dims.height / 2;
+ break;
+ }
+
+ return new Effect.Parallel(
+ [ new Effect.Opacity(element, { sync: true, to: 0.0, from: 1.0, transition: options.opacityTransition }),
+ new Effect.Scale(element, window.opera ? 1 : 0, { sync: true, transition: options.scaleTransition, restoreAfterFinish: true}),
+ new Effect.MoveBy(element, moveY, moveX, { sync: true, transition: options.moveTransition })
+ ], Object.extend({
+ beforeStartInternal: function(effect) { with(Element) {
+ [makePositioned, makeClipping].call(effect.effects[0].element) }},
+ afterFinishInternal: function(effect) { with(Element) {
+ [hide, undoClipping, undoPositioned].call(effect.effects[0].element);
+ setStyle(effect.effects[0].element, oldStyle); }}
+ }, options)
+ );
+}
+
+Effect.Pulsate = function(element) {
+ element = $(element);
+ var options = arguments[1] || {};
+ var oldOpacity = Element.getInlineOpacity(element);
+ var transition = options.transition || Effect.Transitions.sinoidal;
+ var reverser = function(pos){ return transition(1-Effect.Transitions.pulse(pos)) };
+ reverser.bind(transition);
+ return new Effect.Opacity(element,
+ Object.extend(Object.extend({ duration: 3.0, from: 0,
+ afterFinishInternal: function(effect) { Element.setStyle(effect.element, {opacity: oldOpacity}); }
+ }, options), {transition: reverser}));
+}
+
+Effect.Fold = function(element) {
+ element = $(element);
+ var oldStyle = {
+ top: element.style.top,
+ left: element.style.left,
+ width: element.style.width,
+ height: element.style.height };
+ Element.makeClipping(element);
+ return new Effect.Scale(element, 5, Object.extend({
+ scaleContent: false,
+ scaleX: false,
+ afterFinishInternal: function(effect) {
+ new Effect.Scale(element, 1, {
+ scaleContent: false,
+ scaleY: false,
+ afterFinishInternal: function(effect) { with(Element) {
+ [hide, undoClipping].call(effect.element);
+ setStyle(effect.element, oldStyle);
+ }} });
+ }}, arguments[1] || {}));
+}
diff --git a/redmine/public/javascripts/prototype.js b/redmine/public/javascripts/prototype.js
new file mode 100644
index 000000000..e9ccd3c88
--- /dev/null
+++ b/redmine/public/javascripts/prototype.js
@@ -0,0 +1,1785 @@
+/* Prototype JavaScript framework, version 1.4.0
+ * (c) 2005 Sam Stephenson <sam@conio.net>
+ *
+ * THIS FILE IS AUTOMATICALLY GENERATED. When sending patches, please diff
+ * against the source tree, available from the Prototype darcs repository.
+ *
+ * Prototype is freely distributable under the terms of an MIT-style license.
+ *
+ * For details, see the Prototype web site: http://prototype.conio.net/
+ *
+/*--------------------------------------------------------------------------*/
+
+var Prototype = {
+ Version: '1.4.0',
+ ScriptFragment: '(?:<script.*?>)((\n|\r|.)*?)(?:<\/script>)',
+
+ emptyFunction: function() {},
+ K: function(x) {return x}
+}
+
+var Class = {
+ create: function() {
+ return function() {
+ this.initialize.apply(this, arguments);
+ }
+ }
+}
+
+var Abstract = new Object();
+
+Object.extend = function(destination, source) {
+ for (property in source) {
+ destination[property] = source[property];
+ }
+ return destination;
+}
+
+Object.inspect = function(object) {
+ try {
+ if (object == undefined) return 'undefined';
+ if (object == null) return 'null';
+ return object.inspect ? object.inspect() : object.toString();
+ } catch (e) {
+ if (e instanceof RangeError) return '...';
+ throw e;
+ }
+}
+
+Function.prototype.bind = function() {
+ var __method = this, args = $A(arguments), object = args.shift();
+ return function() {
+ return __method.apply(object, args.concat($A(arguments)));
+ }
+}
+
+Function.prototype.bindAsEventListener = function(object) {
+ var __method = this;
+ return function(event) {
+ return __method.call(object, event || window.event);
+ }
+}
+
+Object.extend(Number.prototype, {
+ toColorPart: function() {
+ var digits = this.toString(16);
+ if (this < 16) return '0' + digits;
+ return digits;
+ },
+
+ succ: function() {
+ return this + 1;
+ },
+
+ times: function(iterator) {
+ $R(0, this, true).each(iterator);
+ return this;
+ }
+});
+
+var Try = {
+ these: function() {
+ var returnValue;
+
+ for (var i = 0; i < arguments.length; i++) {
+ var lambda = arguments[i];
+ try {
+ returnValue = lambda();
+ break;
+ } catch (e) {}
+ }
+
+ return returnValue;
+ }
+}
+
+/*--------------------------------------------------------------------------*/
+
+var PeriodicalExecuter = Class.create();
+PeriodicalExecuter.prototype = {
+ initialize: function(callback, frequency) {
+ this.callback = callback;
+ this.frequency = frequency;
+ this.currentlyExecuting = false;
+
+ this.registerCallback();
+ },
+
+ registerCallback: function() {
+ setInterval(this.onTimerEvent.bind(this), this.frequency * 1000);
+ },
+
+ onTimerEvent: function() {
+ if (!this.currentlyExecuting) {
+ try {
+ this.currentlyExecuting = true;
+ this.callback();
+ } finally {
+ this.currentlyExecuting = false;
+ }
+ }
+ }
+}
+
+/*--------------------------------------------------------------------------*/
+
+function $() {
+ var elements = new Array();
+
+ for (var i = 0; i < arguments.length; i++) {
+ var element = arguments[i];
+ if (typeof element == 'string')
+ element = document.getElementById(element);
+
+ if (arguments.length == 1)
+ return element;
+
+ elements.push(element);
+ }
+
+ return elements;
+}
+Object.extend(String.prototype, {
+ stripTags: function() {
+ return this.replace(/<\/?[^>]+>/gi, '');
+ },
+
+ stripScripts: function() {
+ return this.replace(new RegExp(Prototype.ScriptFragment, 'img'), '');
+ },
+
+ extractScripts: function() {
+ var matchAll = new RegExp(Prototype.ScriptFragment, 'img');
+ var matchOne = new RegExp(Prototype.ScriptFragment, 'im');
+ return (this.match(matchAll) || []).map(function(scriptTag) {
+ return (scriptTag.match(matchOne) || ['', ''])[1];
+ });
+ },
+
+ evalScripts: function() {
+ return this.extractScripts().map(eval);
+ },
+
+ escapeHTML: function() {
+ var div = document.createElement('div');
+ var text = document.createTextNode(this);
+ div.appendChild(text);
+ return div.innerHTML;
+ },
+
+ unescapeHTML: function() {
+ var div = document.createElement('div');
+ div.innerHTML = this.stripTags();
+ return div.childNodes[0] ? div.childNodes[0].nodeValue : '';
+ },
+
+ toQueryParams: function() {
+ var pairs = this.match(/^\??(.*)$/)[1].split('&');
+ return pairs.inject({}, function(params, pairString) {
+ var pair = pairString.split('=');
+ params[pair[0]] = pair[1];
+ return params;
+ });
+ },
+
+ toArray: function() {
+ return this.split('');
+ },
+
+ camelize: function() {
+ var oStringList = this.split('-');
+ if (oStringList.length == 1) return oStringList[0];
+
+ var camelizedString = this.indexOf('-') == 0
+ ? oStringList[0].charAt(0).toUpperCase() + oStringList[0].substring(1)
+ : oStringList[0];
+
+ for (var i = 1, len = oStringList.length; i < len; i++) {
+ var s = oStringList[i];
+ camelizedString += s.charAt(0).toUpperCase() + s.substring(1);
+ }
+
+ return camelizedString;
+ },
+
+ inspect: function() {
+ return "'" + this.replace('\\', '\\\\').replace("'", '\\\'') + "'";
+ }
+});
+
+String.prototype.parseQuery = String.prototype.toQueryParams;
+
+var $break = new Object();
+var $continue = new Object();
+
+var Enumerable = {
+ each: function(iterator) {
+ var index = 0;
+ try {
+ this._each(function(value) {
+ try {
+ iterator(value, index++);
+ } catch (e) {
+ if (e != $continue) throw e;
+ }
+ });
+ } catch (e) {
+ if (e != $break) throw e;
+ }
+ },
+
+ all: function(iterator) {
+ var result = true;
+ this.each(function(value, index) {
+ result = result && !!(iterator || Prototype.K)(value, index);
+ if (!result) throw $break;
+ });
+ return result;
+ },
+
+ any: function(iterator) {
+ var result = true;
+ this.each(function(value, index) {
+ if (result = !!(iterator || Prototype.K)(value, index))
+ throw $break;
+ });
+ return result;
+ },
+
+ collect: function(iterator) {
+ var results = [];
+ this.each(function(value, index) {
+ results.push(iterator(value, index));
+ });
+ return results;
+ },
+
+ detect: function (iterator) {
+ var result;
+ this.each(function(value, index) {
+ if (iterator(value, index)) {
+ result = value;
+ throw $break;
+ }
+ });
+ return result;
+ },
+
+ findAll: function(iterator) {
+ var results = [];
+ this.each(function(value, index) {
+ if (iterator(value, index))
+ results.push(value);
+ });
+ return results;
+ },
+
+ grep: function(pattern, iterator) {
+ var results = [];
+ this.each(function(value, index) {
+ var stringValue = value.toString();
+ if (stringValue.match(pattern))
+ results.push((iterator || Prototype.K)(value, index));
+ })
+ return results;
+ },
+
+ include: function(object) {
+ var found = false;
+ this.each(function(value) {
+ if (value == object) {
+ found = true;
+ throw $break;
+ }
+ });
+ return found;
+ },
+
+ inject: function(memo, iterator) {
+ this.each(function(value, index) {
+ memo = iterator(memo, value, index);
+ });
+ return memo;
+ },
+
+ invoke: function(method) {
+ var args = $A(arguments).slice(1);
+ return this.collect(function(value) {
+ return value[method].apply(value, args);
+ });
+ },
+
+ max: function(iterator) {
+ var result;
+ this.each(function(value, index) {
+ value = (iterator || Prototype.K)(value, index);
+ if (value >= (result || value))
+ result = value;
+ });
+ return result;
+ },
+
+ min: function(iterator) {
+ var result;
+ this.each(function(value, index) {
+ value = (iterator || Prototype.K)(value, index);
+ if (value <= (result || value))
+ result = value;
+ });
+ return result;
+ },
+
+ partition: function(iterator) {
+ var trues = [], falses = [];
+ this.each(function(value, index) {
+ ((iterator || Prototype.K)(value, index) ?
+ trues : falses).push(value);
+ });
+ return [trues, falses];
+ },
+
+ pluck: function(property) {
+ var results = [];
+ this.each(function(value, index) {
+ results.push(value[property]);
+ });
+ return results;
+ },
+
+ reject: function(iterator) {
+ var results = [];
+ this.each(function(value, index) {
+ if (!iterator(value, index))
+ results.push(value);
+ });
+ return results;
+ },
+
+ sortBy: function(iterator) {
+ return this.collect(function(value, index) {
+ return {value: value, criteria: iterator(value, index)};
+ }).sort(function(left, right) {
+ var a = left.criteria, b = right.criteria;
+ return a < b ? -1 : a > b ? 1 : 0;
+ }).pluck('value');
+ },
+
+ toArray: function() {
+ return this.collect(Prototype.K);
+ },
+
+ zip: function() {
+ var iterator = Prototype.K, args = $A(arguments);
+ if (typeof args.last() == 'function')
+ iterator = args.pop();
+
+ var collections = [this].concat(args).map($A);
+ return this.map(function(value, index) {
+ iterator(value = collections.pluck(index));
+ return value;
+ });
+ },
+
+ inspect: function() {
+ return '#<Enumerable:' + this.toArray().inspect() + '>';
+ }
+}
+
+Object.extend(Enumerable, {
+ map: Enumerable.collect,
+ find: Enumerable.detect,
+ select: Enumerable.findAll,
+ member: Enumerable.include,
+ entries: Enumerable.toArray
+});
+var $A = Array.from = function(iterable) {
+ if (!iterable) return [];
+ if (iterable.toArray) {
+ return iterable.toArray();
+ } else {
+ var results = [];
+ for (var i = 0; i < iterable.length; i++)
+ results.push(iterable[i]);
+ return results;
+ }
+}
+
+Object.extend(Array.prototype, Enumerable);
+
+Array.prototype._reverse = Array.prototype.reverse;
+
+Object.extend(Array.prototype, {
+ _each: function(iterator) {
+ for (var i = 0; i < this.length; i++)
+ iterator(this[i]);
+ },
+
+ clear: function() {
+ this.length = 0;
+ return this;
+ },
+
+ first: function() {
+ return this[0];
+ },
+
+ last: function() {
+ return this[this.length - 1];
+ },
+
+ compact: function() {
+ return this.select(function(value) {
+ return value != undefined || value != null;
+ });
+ },
+
+ flatten: function() {
+ return this.inject([], function(array, value) {
+ return array.concat(value.constructor == Array ?
+ value.flatten() : [value]);
+ });
+ },
+
+ without: function() {
+ var values = $A(arguments);
+ return this.select(function(value) {
+ return !values.include(value);
+ });
+ },
+
+ indexOf: function(object) {
+ for (var i = 0; i < this.length; i++)
+ if (this[i] == object) return i;
+ return -1;
+ },
+
+ reverse: function(inline) {
+ return (inline !== false ? this : this.toArray())._reverse();
+ },
+
+ shift: function() {
+ var result = this[0];
+ for (var i = 0; i < this.length - 1; i++)
+ this[i] = this[i + 1];
+ this.length--;
+ return result;
+ },
+
+ inspect: function() {
+ return '[' + this.map(Object.inspect).join(', ') + ']';
+ }
+});
+var Hash = {
+ _each: function(iterator) {
+ for (key in this) {
+ var value = this[key];
+ if (typeof value == 'function') continue;
+
+ var pair = [key, value];
+ pair.key = key;
+ pair.value = value;
+ iterator(pair);
+ }
+ },
+
+ keys: function() {
+ return this.pluck('key');
+ },
+
+ values: function() {
+ return this.pluck('value');
+ },
+
+ merge: function(hash) {
+ return $H(hash).inject($H(this), function(mergedHash, pair) {
+ mergedHash[pair.key] = pair.value;
+ return mergedHash;
+ });
+ },
+
+ toQueryString: function() {
+ return this.map(function(pair) {
+ return pair.map(encodeURIComponent).join('=');
+ }).join('&');
+ },
+
+ inspect: function() {
+ return '#<Hash:{' + this.map(function(pair) {
+ return pair.map(Object.inspect).join(': ');
+ }).join(', ') + '}>';
+ }
+}
+
+function $H(object) {
+ var hash = Object.extend({}, object || {});
+ Object.extend(hash, Enumerable);
+ Object.extend(hash, Hash);
+ return hash;
+}
+ObjectRange = Class.create();
+Object.extend(ObjectRange.prototype, Enumerable);
+Object.extend(ObjectRange.prototype, {
+ initialize: function(start, end, exclusive) {
+ this.start = start;
+ this.end = end;
+ this.exclusive = exclusive;
+ },
+
+ _each: function(iterator) {
+ var value = this.start;
+ do {
+ iterator(value);
+ value = value.succ();
+ } while (this.include(value));
+ },
+
+ include: function(value) {
+ if (value < this.start)
+ return false;
+ if (this.exclusive)
+ return value < this.end;
+ return value <= this.end;
+ }
+});
+
+var $R = function(start, end, exclusive) {
+ return new ObjectRange(start, end, exclusive);
+}
+
+var Ajax = {
+ getTransport: function() {
+ return Try.these(
+ function() {return new ActiveXObject('Msxml2.XMLHTTP')},
+ function() {return new ActiveXObject('Microsoft.XMLHTTP')},
+ function() {return new XMLHttpRequest()}
+ ) || false;
+ },
+
+ activeRequestCount: 0
+}
+
+Ajax.Responders = {
+ responders: [],
+
+ _each: function(iterator) {
+ this.responders._each(iterator);
+ },
+
+ register: function(responderToAdd) {
+ if (!this.include(responderToAdd))
+ this.responders.push(responderToAdd);
+ },
+
+ unregister: function(responderToRemove) {
+ this.responders = this.responders.without(responderToRemove);
+ },
+
+ dispatch: function(callback, request, transport, json) {
+ this.each(function(responder) {
+ if (responder[callback] && typeof responder[callback] == 'function') {
+ try {
+ responder[callback].apply(responder, [request, transport, json]);
+ } catch (e) {}
+ }
+ });
+ }
+};
+
+Object.extend(Ajax.Responders, Enumerable);
+
+Ajax.Responders.register({
+ onCreate: function() {
+ Ajax.activeRequestCount++;
+ },
+
+ onComplete: function() {
+ Ajax.activeRequestCount--;
+ }
+});
+
+Ajax.Base = function() {};
+Ajax.Base.prototype = {
+ setOptions: function(options) {
+ this.options = {
+ method: 'post',
+ asynchronous: true,
+ parameters: ''
+ }
+ Object.extend(this.options, options || {});
+ },
+
+ responseIsSuccess: function() {
+ return this.transport.status == undefined
+ || this.transport.status == 0
+ || (this.transport.status >= 200 && this.transport.status < 300);
+ },
+
+ responseIsFailure: function() {
+ return !this.responseIsSuccess();
+ }
+}
+
+Ajax.Request = Class.create();
+Ajax.Request.Events =
+ ['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete'];
+
+Ajax.Request.prototype = Object.extend(new Ajax.Base(), {
+ initialize: function(url, options) {
+ this.transport = Ajax.getTransport();
+ this.setOptions(options);
+ this.request(url);
+ },
+
+ request: function(url) {
+ var parameters = this.options.parameters || '';
+ if (parameters.length > 0) parameters += '&_=';
+
+ try {
+ this.url = url;
+ if (this.options.method == 'get' && parameters.length > 0)
+ this.url += (this.url.match(/\?/) ? '&' : '?') + parameters;
+
+ Ajax.Responders.dispatch('onCreate', this, this.transport);
+
+ this.transport.open(this.options.method, this.url,
+ this.options.asynchronous);
+
+ if (this.options.asynchronous) {
+ this.transport.onreadystatechange = this.onStateChange.bind(this);
+ setTimeout((function() {this.respondToReadyState(1)}).bind(this), 10);
+ }
+
+ this.setRequestHeaders();
+
+ var body = this.options.postBody ? this.options.postBody : parameters;
+ this.transport.send(this.options.method == 'post' ? body : null);
+
+ } catch (e) {
+ this.dispatchException(e);
+ }
+ },
+
+ setRequestHeaders: function() {
+ var requestHeaders =
+ ['X-Requested-With', 'XMLHttpRequest',
+ 'X-Prototype-Version', Prototype.Version];
+
+ if (this.options.method == 'post') {
+ requestHeaders.push('Content-type',
+ 'application/x-www-form-urlencoded');
+
+ /* Force "Connection: close" for Mozilla browsers to work around
+ * a bug where XMLHttpReqeuest sends an incorrect Content-length
+ * header. See Mozilla Bugzilla #246651.
+ */
+ if (this.transport.overrideMimeType)
+ requestHeaders.push('Connection', 'close');
+ }
+
+ if (this.options.requestHeaders)
+ requestHeaders.push.apply(requestHeaders, this.options.requestHeaders);
+
+ for (var i = 0; i < requestHeaders.length; i += 2)
+ this.transport.setRequestHeader(requestHeaders[i], requestHeaders[i+1]);
+ },
+
+ onStateChange: function() {
+ var readyState = this.transport.readyState;
+ if (readyState != 1)
+ this.respondToReadyState(this.transport.readyState);
+ },
+
+ header: function(name) {
+ try {
+ return this.transport.getResponseHeader(name);
+ } catch (e) {}
+ },
+
+ evalJSON: function() {
+ try {
+ return eval(this.header('X-JSON'));
+ } catch (e) {}
+ },
+
+ evalResponse: function() {
+ try {
+ return eval(this.transport.responseText);
+ } catch (e) {
+ this.dispatchException(e);
+ }
+ },
+
+ respondToReadyState: function(readyState) {
+ var event = Ajax.Request.Events[readyState];
+ var transport = this.transport, json = this.evalJSON();
+
+ if (event == 'Complete') {
+ try {
+ (this.options['on' + this.transport.status]
+ || this.options['on' + (this.responseIsSuccess() ? 'Success' : 'Failure')]
+ || Prototype.emptyFunction)(transport, json);
+ } catch (e) {
+ this.dispatchException(e);
+ }
+
+ if ((this.header('Content-type') || '').match(/^text\/javascript/i))
+ this.evalResponse();
+ }
+
+ try {
+ (this.options['on' + event] || Prototype.emptyFunction)(transport, json);
+ Ajax.Responders.dispatch('on' + event, this, transport, json);
+ } catch (e) {
+ this.dispatchException(e);
+ }
+
+ /* Avoid memory leak in MSIE: clean up the oncomplete event handler */
+ if (event == 'Complete')
+ this.transport.onreadystatechange = Prototype.emptyFunction;
+ },
+
+ dispatchException: function(exception) {
+ (this.options.onException || Prototype.emptyFunction)(this, exception);
+ Ajax.Responders.dispatch('onException', this, exception);
+ }
+});
+
+Ajax.Updater = Class.create();
+
+Object.extend(Object.extend(Ajax.Updater.prototype, Ajax.Request.prototype), {
+ initialize: function(container, url, options) {
+ this.containers = {
+ success: container.success ? $(container.success) : $(container),
+ failure: container.failure ? $(container.failure) :
+ (container.success ? null : $(container))
+ }
+
+ this.transport = Ajax.getTransport();
+ this.setOptions(options);
+
+ var onComplete = this.options.onComplete || Prototype.emptyFunction;
+ this.options.onComplete = (function(transport, object) {
+ this.updateContent();
+ onComplete(transport, object);
+ }).bind(this);
+
+ this.request(url);
+ },
+
+ updateContent: function() {
+ var receiver = this.responseIsSuccess() ?
+ this.containers.success : this.containers.failure;
+ var response = this.transport.responseText;
+
+ if (!this.options.evalScripts)
+ response = response.stripScripts();
+
+ if (receiver) {
+ if (this.options.insertion) {
+ new this.options.insertion(receiver, response);
+ } else {
+ Element.update(receiver, response);
+ }
+ }
+
+ if (this.responseIsSuccess()) {
+ if (this.onComplete)
+ setTimeout(this.onComplete.bind(this), 10);
+ }
+ }
+});
+
+Ajax.PeriodicalUpdater = Class.create();
+Ajax.PeriodicalUpdater.prototype = Object.extend(new Ajax.Base(), {
+ initialize: function(container, url, options) {
+ this.setOptions(options);
+ this.onComplete = this.options.onComplete;
+
+ this.frequency = (this.options.frequency || 2);
+ this.decay = (this.options.decay || 1);
+
+ this.updater = {};
+ this.container = container;
+ this.url = url;
+
+ this.start();
+ },
+
+ start: function() {
+ this.options.onComplete = this.updateComplete.bind(this);
+ this.onTimerEvent();
+ },
+
+ stop: function() {
+ this.updater.onComplete = undefined;
+ clearTimeout(this.timer);
+ (this.onComplete || Prototype.emptyFunction).apply(this, arguments);
+ },
+
+ updateComplete: function(request) {
+ if (this.options.decay) {
+ this.decay = (request.responseText == this.lastText ?
+ this.decay * this.options.decay : 1);
+
+ this.lastText = request.responseText;
+ }
+ this.timer = setTimeout(this.onTimerEvent.bind(this),
+ this.decay * this.frequency * 1000);
+ },
+
+ onTimerEvent: function() {
+ this.updater = new Ajax.Updater(this.container, this.url, this.options);
+ }
+});
+document.getElementsByClassName = function(className, parentElement) {
+ var children = ($(parentElement) || document.body).getElementsByTagName('*');
+ return $A(children).inject([], function(elements, child) {
+ if (child.className.match(new RegExp("(^|\\s)" + className + "(\\s|$)")))
+ elements.push(child);
+ return elements;
+ });
+}
+
+/*--------------------------------------------------------------------------*/
+
+if (!window.Element) {
+ var Element = new Object();
+}
+
+Object.extend(Element, {
+ visible: function(element) {
+ return $(element).style.display != 'none';
+ },
+
+ toggle: function() {
+ for (var i = 0; i < arguments.length; i++) {
+ var element = $(arguments[i]);
+ Element[Element.visible(element) ? 'hide' : 'show'](element);
+ }
+ },
+
+ hide: function() {
+ for (var i = 0; i < arguments.length; i++) {
+ var element = $(arguments[i]);
+ element.style.display = 'none';
+ }
+ },
+
+ show: function() {
+ for (var i = 0; i < arguments.length; i++) {
+ var element = $(arguments[i]);
+ element.style.display = '';
+ }
+ },
+
+ remove: function(element) {
+ element = $(element);
+ element.parentNode.removeChild(element);
+ },
+
+ update: function(element, html) {
+ $(element).innerHTML = html.stripScripts();
+ setTimeout(function() {html.evalScripts()}, 10);
+ },
+
+ getHeight: function(element) {
+ element = $(element);
+ return element.offsetHeight;
+ },
+
+ classNames: function(element) {
+ return new Element.ClassNames(element);
+ },
+
+ hasClassName: function(element, className) {
+ if (!(element = $(element))) return;
+ return Element.classNames(element).include(className);
+ },
+
+ addClassName: function(element, className) {
+ if (!(element = $(element))) return;
+ return Element.classNames(element).add(className);
+ },
+
+ removeClassName: function(element, className) {
+ if (!(element = $(element))) return;
+ return Element.classNames(element).remove(className);
+ },
+
+ // removes whitespace-only text node children
+ cleanWhitespace: function(element) {
+ element = $(element);
+ for (var i = 0; i < element.childNodes.length; i++) {
+ var node = element.childNodes[i];
+ if (node.nodeType == 3 && !/\S/.test(node.nodeValue))
+ Element.remove(node);
+ }
+ },
+
+ empty: function(element) {
+ return $(element).innerHTML.match(/^\s*$/);
+ },
+
+ scrollTo: function(element) {
+ element = $(element);
+ var x = element.x ? element.x : element.offsetLeft,
+ y = element.y ? element.y : element.offsetTop;
+ window.scrollTo(x, y);
+ },
+
+ getStyle: function(element, style) {
+ element = $(element);
+ var value = element.style[style.camelize()];
+ if (!value) {
+ if (document.defaultView && document.defaultView.getComputedStyle) {
+ var css = document.defaultView.getComputedStyle(element, null);
+ value = css ? css.getPropertyValue(style) : null;
+ } else if (element.currentStyle) {
+ value = element.currentStyle[style.camelize()];
+ }
+ }
+
+ if (window.opera && ['left', 'top', 'right', 'bottom'].include(style))
+ if (Element.getStyle(element, 'position') == 'static') value = 'auto';
+
+ return value == 'auto' ? null : value;
+ },
+
+ setStyle: function(element, style) {
+ element = $(element);
+ for (name in style)
+ element.style[name.camelize()] = style[name];
+ },
+
+ getDimensions: function(element) {
+ element = $(element);
+ if (Element.getStyle(element, 'display') != 'none')
+ return {width: element.offsetWidth, height: element.offsetHeight};
+
+ // All *Width and *Height properties give 0 on elements with display none,
+ // so enable the element temporarily
+ var els = element.style;
+ var originalVisibility = els.visibility;
+ var originalPosition = els.position;
+ els.visibility = 'hidden';
+ els.position = 'absolute';
+ els.display = '';
+ var originalWidth = element.clientWidth;
+ var originalHeight = element.clientHeight;
+ els.display = 'none';
+ els.position = originalPosition;
+ els.visibility = originalVisibility;
+ return {width: originalWidth, height: originalHeight};
+ },
+
+ makePositioned: function(element) {
+ element = $(element);
+ var pos = Element.getStyle(element, 'position');
+ if (pos == 'static' || !pos) {
+ element._madePositioned = true;
+ element.style.position = 'relative';
+ // Opera returns the offset relative to the positioning context, when an
+ // element is position relative but top and left have not been defined
+ if (window.opera) {
+ element.style.top = 0;
+ element.style.left = 0;
+ }
+ }
+ },
+
+ undoPositioned: function(element) {
+ element = $(element);
+ if (element._madePositioned) {
+ element._madePositioned = undefined;
+ element.style.position =
+ element.style.top =
+ element.style.left =
+ element.style.bottom =
+ element.style.right = '';
+ }
+ },
+
+ makeClipping: function(element) {
+ element = $(element);
+ if (element._overflow) return;
+ element._overflow = element.style.overflow;
+ if ((Element.getStyle(element, 'overflow') || 'visible') != 'hidden')
+ element.style.overflow = 'hidden';
+ },
+
+ undoClipping: function(element) {
+ element = $(element);
+ if (element._overflow) return;
+ element.style.overflow = element._overflow;
+ element._overflow = undefined;
+ }
+});
+
+var Toggle = new Object();
+Toggle.display = Element.toggle;
+
+/*--------------------------------------------------------------------------*/
+
+Abstract.Insertion = function(adjacency) {
+ this.adjacency = adjacency;
+}
+
+Abstract.Insertion.prototype = {
+ initialize: function(element, content) {
+ this.element = $(element);
+ this.content = content.stripScripts();
+
+ if (this.adjacency && this.element.insertAdjacentHTML) {
+ try {
+ this.element.insertAdjacentHTML(this.adjacency, this.content);
+ } catch (e) {
+ if (this.element.tagName.toLowerCase() == 'tbody') {
+ this.insertContent(this.contentFromAnonymousTable());
+ } else {
+ throw e;
+ }
+ }
+ } else {
+ this.range = this.element.ownerDocument.createRange();
+ if (this.initializeRange) this.initializeRange();
+ this.insertContent([this.range.createContextualFragment(this.content)]);
+ }
+
+ setTimeout(function() {content.evalScripts()}, 10);
+ },
+
+ contentFromAnonymousTable: function() {
+ var div = document.createElement('div');
+ div.innerHTML = '<table><tbody>' + this.content + '</tbody></table>';
+ return $A(div.childNodes[0].childNodes[0].childNodes);
+ }
+}
+
+var Insertion = new Object();
+
+Insertion.Before = Class.create();
+Insertion.Before.prototype = Object.extend(new Abstract.Insertion('beforeBegin'), {
+ initializeRange: function() {
+ this.range.setStartBefore(this.element);
+ },
+
+ insertContent: function(fragments) {
+ fragments.each((function(fragment) {
+ this.element.parentNode.insertBefore(fragment, this.element);
+ }).bind(this));
+ }
+});
+
+Insertion.Top = Class.create();
+Insertion.Top.prototype = Object.extend(new Abstract.Insertion('afterBegin'), {
+ initializeRange: function() {
+ this.range.selectNodeContents(this.element);
+ this.range.collapse(true);
+ },
+
+ insertContent: function(fragments) {
+ fragments.reverse(false).each((function(fragment) {
+ this.element.insertBefore(fragment, this.element.firstChild);
+ }).bind(this));
+ }
+});
+
+Insertion.Bottom = Class.create();
+Insertion.Bottom.prototype = Object.extend(new Abstract.Insertion('beforeEnd'), {
+ initializeRange: function() {
+ this.range.selectNodeContents(this.element);
+ this.range.collapse(this.element);
+ },
+
+ insertContent: function(fragments) {
+ fragments.each((function(fragment) {
+ this.element.appendChild(fragment);
+ }).bind(this));
+ }
+});
+
+Insertion.After = Class.create();
+Insertion.After.prototype = Object.extend(new Abstract.Insertion('afterEnd'), {
+ initializeRange: function() {
+ this.range.setStartAfter(this.element);
+ },
+
+ insertContent: function(fragments) {
+ fragments.each((function(fragment) {
+ this.element.parentNode.insertBefore(fragment,
+ this.element.nextSibling);
+ }).bind(this));
+ }
+});
+
+/*--------------------------------------------------------------------------*/
+
+Element.ClassNames = Class.create();
+Element.ClassNames.prototype = {
+ initialize: function(element) {
+ this.element = $(element);
+ },
+
+ _each: function(iterator) {
+ this.element.className.split(/\s+/).select(function(name) {
+ return name.length > 0;
+ })._each(iterator);
+ },
+
+ set: function(className) {
+ this.element.className = className;
+ },
+
+ add: function(classNameToAdd) {
+ if (this.include(classNameToAdd)) return;
+ this.set(this.toArray().concat(classNameToAdd).join(' '));
+ },
+
+ remove: function(classNameToRemove) {
+ if (!this.include(classNameToRemove)) return;
+ this.set(this.select(function(className) {
+ return className != classNameToRemove;
+ }).join(' '));
+ },
+
+ toString: function() {
+ return this.toArray().join(' ');
+ }
+}
+
+Object.extend(Element.ClassNames.prototype, Enumerable);
+var Field = {
+ clear: function() {
+ for (var i = 0; i < arguments.length; i++)
+ $(arguments[i]).value = '';
+ },
+
+ focus: function(element) {
+ $(element).focus();
+ },
+
+ present: function() {
+ for (var i = 0; i < arguments.length; i++)
+ if ($(arguments[i]).value == '') return false;
+ return true;
+ },
+
+ select: function(element) {
+ $(element).select();
+ },
+
+ activate: function(element) {
+ element = $(element);
+ element.focus();
+ if (element.select)
+ element.select();
+ }
+}
+
+/*--------------------------------------------------------------------------*/
+
+var Form = {
+ serialize: function(form) {
+ var elements = Form.getElements($(form));
+ var queryComponents = new Array();
+
+ for (var i = 0; i < elements.length; i++) {
+ var queryComponent = Form.Element.serialize(elements[i]);
+ if (queryComponent)
+ queryComponents.push(queryComponent);
+ }
+
+ return queryComponents.join('&');
+ },
+
+ getElements: function(form) {
+ form = $(form);
+ var elements = new Array();
+
+ for (tagName in Form.Element.Serializers) {
+ var tagElements = form.getElementsByTagName(tagName);
+ for (var j = 0; j < tagElements.length; j++)
+ elements.push(tagElements[j]);
+ }
+ return elements;
+ },
+
+ getInputs: function(form, typeName, name) {
+ form = $(form);
+ var inputs = form.getElementsByTagName('input');
+
+ if (!typeName && !name)
+ return inputs;
+
+ var matchingInputs = new Array();
+ for (var i = 0; i < inputs.length; i++) {
+ var input = inputs[i];
+ if ((typeName && input.type != typeName) ||
+ (name && input.name != name))
+ continue;
+ matchingInputs.push(input);
+ }
+
+ return matchingInputs;
+ },
+
+ disable: function(form) {
+ var elements = Form.getElements(form);
+ for (var i = 0; i < elements.length; i++) {
+ var element = elements[i];
+ element.blur();
+ element.disabled = 'true';
+ }
+ },
+
+ enable: function(form) {
+ var elements = Form.getElements(form);
+ for (var i = 0; i < elements.length; i++) {
+ var element = elements[i];
+ element.disabled = '';
+ }
+ },
+
+ findFirstElement: function(form) {
+ return Form.getElements(form).find(function(element) {
+ return element.type != 'hidden' && !element.disabled &&
+ ['input', 'select', 'textarea'].include(element.tagName.toLowerCase());
+ });
+ },
+
+ focusFirstElement: function(form) {
+ Field.activate(Form.findFirstElement(form));
+ },
+
+ reset: function(form) {
+ $(form).reset();
+ }
+}
+
+Form.Element = {
+ serialize: function(element) {
+ element = $(element);
+ var method = element.tagName.toLowerCase();
+ var parameter = Form.Element.Serializers[method](element);
+
+ if (parameter) {
+ var key = encodeURIComponent(parameter[0]);
+ if (key.length == 0) return;
+
+ if (parameter[1].constructor != Array)
+ parameter[1] = [parameter[1]];
+
+ return parameter[1].map(function(value) {
+ return key + '=' + encodeURIComponent(value);
+ }).join('&');
+ }
+ },
+
+ getValue: function(element) {
+ element = $(element);
+ var method = element.tagName.toLowerCase();
+ var parameter = Form.Element.Serializers[method](element);
+
+ if (parameter)
+ return parameter[1];
+ }
+}
+
+Form.Element.Serializers = {
+ input: function(element) {
+ switch (element.type.toLowerCase()) {
+ case 'submit':
+ case 'hidden':
+ case 'password':
+ case 'text':
+ return Form.Element.Serializers.textarea(element);
+ case 'checkbox':
+ case 'radio':
+ return Form.Element.Serializers.inputSelector(element);
+ }
+ return false;
+ },
+
+ inputSelector: function(element) {
+ if (element.checked)
+ return [element.name, element.value];
+ },
+
+ textarea: function(element) {
+ return [element.name, element.value];
+ },
+
+ select: function(element) {
+ return Form.Element.Serializers[element.type == 'select-one' ?
+ 'selectOne' : 'selectMany'](element);
+ },
+
+ selectOne: function(element) {
+ var value = '', opt, index = element.selectedIndex;
+ if (index >= 0) {
+ opt = element.options[index];
+ value = opt.value;
+ if (!value && !('value' in opt))
+ value = opt.text;
+ }
+ return [element.name, value];
+ },
+
+ selectMany: function(element) {
+ var value = new Array();
+ for (var i = 0; i < element.length; i++) {
+ var opt = element.options[i];
+ if (opt.selected) {
+ var optValue = opt.value;
+ if (!optValue && !('value' in opt))
+ optValue = opt.text;
+ value.push(optValue);
+ }
+ }
+ return [element.name, value];
+ }
+}
+
+/*--------------------------------------------------------------------------*/
+
+var $F = Form.Element.getValue;
+
+/*--------------------------------------------------------------------------*/
+
+Abstract.TimedObserver = function() {}
+Abstract.TimedObserver.prototype = {
+ initialize: function(element, frequency, callback) {
+ this.frequency = frequency;
+ this.element = $(element);
+ this.callback = callback;
+
+ this.lastValue = this.getValue();
+ this.registerCallback();
+ },
+
+ registerCallback: function() {
+ setInterval(this.onTimerEvent.bind(this), this.frequency * 1000);
+ },
+
+ onTimerEvent: function() {
+ var value = this.getValue();
+ if (this.lastValue != value) {
+ this.callback(this.element, value);
+ this.lastValue = value;
+ }
+ }
+}
+
+Form.Element.Observer = Class.create();
+Form.Element.Observer.prototype = Object.extend(new Abstract.TimedObserver(), {
+ getValue: function() {
+ return Form.Element.getValue(this.element);
+ }
+});
+
+Form.Observer = Class.create();
+Form.Observer.prototype = Object.extend(new Abstract.TimedObserver(), {
+ getValue: function() {
+ return Form.serialize(this.element);
+ }
+});
+
+/*--------------------------------------------------------------------------*/
+
+Abstract.EventObserver = function() {}
+Abstract.EventObserver.prototype = {
+ initialize: function(element, callback) {
+ this.element = $(element);
+ this.callback = callback;
+
+ this.lastValue = this.getValue();
+ if (this.element.tagName.toLowerCase() == 'form')
+ this.registerFormCallbacks();
+ else
+ this.registerCallback(this.element);
+ },
+
+ onElementEvent: function() {
+ var value = this.getValue();
+ if (this.lastValue != value) {
+ this.callback(this.element, value);
+ this.lastValue = value;
+ }
+ },
+
+ registerFormCallbacks: function() {
+ var elements = Form.getElements(this.element);
+ for (var i = 0; i < elements.length; i++)
+ this.registerCallback(elements[i]);
+ },
+
+ registerCallback: function(element) {
+ if (element.type) {
+ switch (element.type.toLowerCase()) {
+ case 'checkbox':
+ case 'radio':
+ Event.observe(element, 'click', this.onElementEvent.bind(this));
+ break;
+ case 'password':
+ case 'text':
+ case 'textarea':
+ case 'select-one':
+ case 'select-multiple':
+ Event.observe(element, 'change', this.onElementEvent.bind(this));
+ break;
+ }
+ }
+ }
+}
+
+Form.Element.EventObserver = Class.create();
+Form.Element.EventObserver.prototype = Object.extend(new Abstract.EventObserver(), {
+ getValue: function() {
+ return Form.Element.getValue(this.element);
+ }
+});
+
+Form.EventObserver = Class.create();
+Form.EventObserver.prototype = Object.extend(new Abstract.EventObserver(), {
+ getValue: function() {
+ return Form.serialize(this.element);
+ }
+});
+if (!window.Event) {
+ var Event = new Object();
+}
+
+Object.extend(Event, {
+ KEY_BACKSPACE: 8,
+ KEY_TAB: 9,
+ KEY_RETURN: 13,
+ KEY_ESC: 27,
+ KEY_LEFT: 37,
+ KEY_UP: 38,
+ KEY_RIGHT: 39,
+ KEY_DOWN: 40,
+ KEY_DELETE: 46,
+
+ element: function(event) {
+ return event.target || event.srcElement;
+ },
+
+ isLeftClick: function(event) {
+ return (((event.which) && (event.which == 1)) ||
+ ((event.button) && (event.button == 1)));
+ },
+
+ pointerX: function(event) {
+ return event.pageX || (event.clientX +
+ (document.documentElement.scrollLeft || document.body.scrollLeft));
+ },
+
+ pointerY: function(event) {
+ return event.pageY || (event.clientY +
+ (document.documentElement.scrollTop || document.body.scrollTop));
+ },
+
+ stop: function(event) {
+ if (event.preventDefault) {
+ event.preventDefault();
+ event.stopPropagation();
+ } else {
+ event.returnValue = false;
+ event.cancelBubble = true;
+ }
+ },
+
+ // find the first node with the given tagName, starting from the
+ // node the event was triggered on; traverses the DOM upwards
+ findElement: function(event, tagName) {
+ var element = Event.element(event);
+ while (element.parentNode && (!element.tagName ||
+ (element.tagName.toUpperCase() != tagName.toUpperCase())))
+ element = element.parentNode;
+ return element;
+ },
+
+ observers: false,
+
+ _observeAndCache: function(element, name, observer, useCapture) {
+ if (!this.observers) this.observers = [];
+ if (element.addEventListener) {
+ this.observers.push([element, name, observer, useCapture]);
+ element.addEventListener(name, observer, useCapture);
+ } else if (element.attachEvent) {
+ this.observers.push([element, name, observer, useCapture]);
+ element.attachEvent('on' + name, observer);
+ }
+ },
+
+ unloadCache: function() {
+ if (!Event.observers) return;
+ for (var i = 0; i < Event.observers.length; i++) {
+ Event.stopObserving.apply(this, Event.observers[i]);
+ Event.observers[i][0] = null;
+ }
+ Event.observers = false;
+ },
+
+ observe: function(element, name, observer, useCapture) {
+ var element = $(element);
+ useCapture = useCapture || false;
+
+ if (name == 'keypress' &&
+ (navigator.appVersion.match(/Konqueror|Safari|KHTML/)
+ || element.attachEvent))
+ name = 'keydown';
+
+ this._observeAndCache(element, name, observer, useCapture);
+ },
+
+ stopObserving: function(element, name, observer, useCapture) {
+ var element = $(element);
+ useCapture = useCapture || false;
+
+ if (name == 'keypress' &&
+ (navigator.appVersion.match(/Konqueror|Safari|KHTML/)
+ || element.detachEvent))
+ name = 'keydown';
+
+ if (element.removeEventListener) {
+ element.removeEventListener(name, observer, useCapture);
+ } else if (element.detachEvent) {
+ element.detachEvent('on' + name, observer);
+ }
+ }
+});
+
+/* prevent memory leaks in IE */
+Event.observe(window, 'unload', Event.unloadCache, false);
+var Position = {
+ // set to true if needed, warning: firefox performance problems
+ // NOT neeeded for page scrolling, only if draggable contained in
+ // scrollable elements
+ includeScrollOffsets: false,
+
+ // must be called before calling withinIncludingScrolloffset, every time the
+ // page is scrolled
+ prepare: function() {
+ this.deltaX = window.pageXOffset
+ || document.documentElement.scrollLeft
+ || document.body.scrollLeft
+ || 0;
+ this.deltaY = window.pageYOffset
+ || document.documentElement.scrollTop
+ || document.body.scrollTop
+ || 0;
+ },
+
+ realOffset: function(element) {
+ var valueT = 0, valueL = 0;
+ do {
+ valueT += element.scrollTop || 0;
+ valueL += element.scrollLeft || 0;
+ element = element.parentNode;
+ } while (element);
+ return [valueL, valueT];
+ },
+
+ cumulativeOffset: function(element) {
+ var valueT = 0, valueL = 0;
+ do {
+ valueT += element.offsetTop || 0;
+ valueL += element.offsetLeft || 0;
+ element = element.offsetParent;
+ } while (element);
+ return [valueL, valueT];
+ },
+
+ positionedOffset: function(element) {
+ var valueT = 0, valueL = 0;
+ do {
+ valueT += element.offsetTop || 0;
+ valueL += element.offsetLeft || 0;
+ element = element.offsetParent;
+ if (element) {
+ p = Element.getStyle(element, 'position');
+ if (p == 'relative' || p == 'absolute') break;
+ }
+ } while (element);
+ return [valueL, valueT];
+ },
+
+ offsetParent: function(element) {
+ if (element.offsetParent) return element.offsetParent;
+ if (element == document.body) return element;
+
+ while ((element = element.parentNode) && element != document.body)
+ if (Element.getStyle(element, 'position') != 'static')
+ return element;
+
+ return document.body;
+ },
+
+ // caches x/y coordinate pair to use with overlap
+ within: function(element, x, y) {
+ if (this.includeScrollOffsets)
+ return this.withinIncludingScrolloffsets(element, x, y);
+ this.xcomp = x;
+ this.ycomp = y;
+ this.offset = this.cumulativeOffset(element);
+
+ return (y >= this.offset[1] &&
+ y < this.offset[1] + element.offsetHeight &&
+ x >= this.offset[0] &&
+ x < this.offset[0] + element.offsetWidth);
+ },
+
+ withinIncludingScrolloffsets: function(element, x, y) {
+ var offsetcache = this.realOffset(element);
+
+ this.xcomp = x + offsetcache[0] - this.deltaX;
+ this.ycomp = y + offsetcache[1] - this.deltaY;
+ this.offset = this.cumulativeOffset(element);
+
+ return (this.ycomp >= this.offset[1] &&
+ this.ycomp < this.offset[1] + element.offsetHeight &&
+ this.xcomp >= this.offset[0] &&
+ this.xcomp < this.offset[0] + element.offsetWidth);
+ },
+
+ // within must be called directly before
+ overlap: function(mode, element) {
+ if (!mode) return 0;
+ if (mode == 'vertical')
+ return ((this.offset[1] + element.offsetHeight) - this.ycomp) /
+ element.offsetHeight;
+ if (mode == 'horizontal')
+ return ((this.offset[0] + element.offsetWidth) - this.xcomp) /
+ element.offsetWidth;
+ },
+
+ clone: function(source, target) {
+ source = $(source);
+ target = $(target);
+ target.style.position = 'absolute';
+ var offsets = this.cumulativeOffset(source);
+ target.style.top = offsets[1] + 'px';
+ target.style.left = offsets[0] + 'px';
+ target.style.width = source.offsetWidth + 'px';
+ target.style.height = source.offsetHeight + 'px';
+ },
+
+ page: function(forElement) {
+ var valueT = 0, valueL = 0;
+
+ var element = forElement;
+ do {
+ valueT += element.offsetTop || 0;
+ valueL += element.offsetLeft || 0;
+
+ // Safari fix
+ if (element.offsetParent==document.body)
+ if (Element.getStyle(element,'position')=='absolute') break;
+
+ } while (element = element.offsetParent);
+
+ element = forElement;
+ do {
+ valueT -= element.scrollTop || 0;
+ valueL -= element.scrollLeft || 0;
+ } while (element = element.parentNode);
+
+ return [valueL, valueT];
+ },
+
+ clone: function(source, target) {
+ var options = Object.extend({
+ setLeft: true,
+ setTop: true,
+ setWidth: true,
+ setHeight: true,
+ offsetTop: 0,
+ offsetLeft: 0
+ }, arguments[2] || {})
+
+ // find page position of source
+ source = $(source);
+ var p = Position.page(source);
+
+ // find coordinate system to use
+ target = $(target);
+ var delta = [0, 0];
+ var parent = null;
+ // delta [0,0] will do fine with position: fixed elements,
+ // position:absolute needs offsetParent deltas
+ if (Element.getStyle(target,'position') == 'absolute') {
+ parent = Position.offsetParent(target);
+ delta = Position.page(parent);
+ }
+
+ // correct by body offsets (fixes Safari)
+ if (parent == document.body) {
+ delta[0] -= document.body.offsetLeft;
+ delta[1] -= document.body.offsetTop;
+ }
+
+ // set position
+ if(options.setLeft) target.style.left = (p[0] - delta[0] + options.offsetLeft) + 'px';
+ if(options.setTop) target.style.top = (p[1] - delta[1] + options.offsetTop) + 'px';
+ if(options.setWidth) target.style.width = source.offsetWidth + 'px';
+ if(options.setHeight) target.style.height = source.offsetHeight + 'px';
+ },
+
+ absolutize: function(element) {
+ element = $(element);
+ if (element.style.position == 'absolute') return;
+ Position.prepare();
+
+ var offsets = Position.positionedOffset(element);
+ var top = offsets[1];
+ var left = offsets[0];
+ var width = element.clientWidth;
+ var height = element.clientHeight;
+
+ element._originalLeft = left - parseFloat(element.style.left || 0);
+ element._originalTop = top - parseFloat(element.style.top || 0);
+ element._originalWidth = element.style.width;
+ element._originalHeight = element.style.height;
+
+ element.style.position = 'absolute';
+ element.style.top = top + 'px';;
+ element.style.left = left + 'px';;
+ element.style.width = width + 'px';;
+ element.style.height = height + 'px';;
+ },
+
+ relativize: function(element) {
+ element = $(element);
+ if (element.style.position == 'relative') return;
+ Position.prepare();
+
+ element.style.position = 'relative';
+ var top = parseFloat(element.style.top || 0) - (element._originalTop || 0);
+ var left = parseFloat(element.style.left || 0) - (element._originalLeft || 0);
+
+ element.style.top = top + 'px';
+ element.style.left = left + 'px';
+ element.style.height = element._originalHeight;
+ element.style.width = element._originalWidth;
+ }
+}
+
+// Safari returns margins on body which is incorrect if the child is absolutely
+// positioned. For performance reasons, redefine Position.cumulativeOffset for
+// KHTML/WebKit only.
+if (/Konqueror|Safari|KHTML/.test(navigator.userAgent)) {
+ Position.cumulativeOffset = function(element) {
+ var valueT = 0, valueL = 0;
+ do {
+ valueT += element.offsetTop || 0;
+ valueL += element.offsetLeft || 0;
+ if (element.offsetParent == document.body)
+ if (Element.getStyle(element, 'position') == 'absolute') break;
+
+ element = element.offsetParent;
+ } while (element);
+
+ return [valueL, valueT];
+ }
+} \ No newline at end of file
diff --git a/redmine/public/manual/administration.html b/redmine/public/manual/administration.html
new file mode 100644
index 000000000..22016c4d6
--- /dev/null
+++ b/redmine/public/manual/administration.html
@@ -0,0 +1,121 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
+
+<head>
+<title>redMine - Aide en ligne</title>
+<meta http-equiv="content-type" content="text/html; charset=iso-8859-1" />
+<meta name="description" content="redMine" />
+<meta name="keywords" content="issue,bug,tracker" />
+<link href="stylesheets/help.css" rel="stylesheet" type="text/css" />
+</head>
+
+<body>
+<p align="right">[ <a href="index.html">Index</a> ]</p>
+<h1>Administration</h1>
+Sommaire:
+<ol>
+ <li><a href="administration.html#users">Utilisateurs</a></li>
+ <li><a href="administration.html#roles">Rôles et permissions</a></li>
+ <li><a href="administration.html#trackers">Trackers</a></li>
+ <li><a href="administration.html#custom_fields">Champs personnalisés</a></li>
+ <li><a href="administration.html#issue_statuses">Statuts de demande</a></li>
+ <li><a href="administration.html#workflow">Workflow</a></li>
+ <li><a href="administration.html#enumerations">Listes de valeurs</a></li>
+ <li><a href="administration.html#mail_notifications">Notifications par mail</a></li>
+ <li><a href="administration.html#app_info">Informations</a></li>
+</ol>
+
+
+<h2><a name="users"></a>1. Utilisateurs</h2>
+<p>Ces écrans vous permettent de gérer les utilisateurs de l'application.</p>
+<h3>1.1 Liste des utilisateurs</h3>
+<center><img src="images/users_list.png"><br />
+<i><small>Liste des utilisateurs</small></i></center>
+<h3>1.2 Création ou modification d'un utilisateur</h3>
+<ul>
+ <li><b>Administrateur</b>: déclare l'utilisateur comme administrateur de l'application.</li>
+ <li><b>Notifications par mail</b>: permet d'activer ou non l'envoi automatique de notifications par mail pour cet utilisateur</li>
+ <li><b>Verrouillé</b>: désactive le compte de l'utilisateur</li>
+</ul>
+<p>En mode modification, laissez le champ <b>Password</b> vide pour laisser le mot de passe de l'utilisateur inchangé.</p>
+<p>Un utilisateur déclaré comme administrateur dispose de toutes les permissions sur l'application et sur tous les projets.</p>
+
+
+<h2><a name="roles"></a>2. Rôles et permissions</h2>
+<p>Les rôles permettent de définir les permissions des différents membres d'un projet.<br />
+Chaque membre d'un projet dispose d'un rôle unique au sein d'un projet.
+Un utilisateur peut avoir différents rôles au sein de différents projets.</p>
+<p>Sur l'écran d'édition du rôle, cochez les actions que vous souhaitez autoriser pour le rôle.</p>
+
+
+<h2><a name="trackers"></a>3. Trackers</h2>
+<p>Les trackers permettent de typer les demandes et de définir des workflows spécifiques pour chacun de ces types.</p>
+
+<h2><a name="custom_fields"></a>4. Champs personnalisés</h2>
+<p>Les champs personnalisés vous permettent d'ajouter des informations supplémentaires sur les demandes.</p>
+Un champ personnalisé peut être de l'un des types suivants:
+<ul>
+ <li><b>Integer</b>: entier positif ou négatif</li>
+ <li><b>String</b>: chaîne de caractère</li>
+ <li><b>Date</b>: date</li>
+ <li><b>Boolean</b>: booléen (case à cocher)</li>
+ <li><b>List</b>: valeur à sélectionnée parmi une liste prédéfinie (liste déroulante)</li>
+</ul>
+Des éléments de validation peuvent être définis:
+<ul>
+ <li><b>Required</b>: champ dont la saisie est obligatoire sur les demandes</li>
+ <li><b>For all projects</b>: champ automatiquement associé à l'ensemble des projets</li>
+ <li><b>Min - max length</b>: longueurs minimales et maximales pour les champs en saisie libre (0 signifie qu'il n'y a pas de restriction)</li>
+ <li><b>Regular expression</b>: expression régulière permettant de valider la valeur saisie</li>
+ <li><b>Possible values (only for lists)</b>: valeurs possibles pour les champs de type "List". Les valeurs sont séparées par le caractère |</li>
+</ul>
+<p>Si l'option <b>For all projects</b> n'est pas activée, chaque projet pourra ou non utilisé le champ personnalisé pour ses demandes
+(voir <a href="projects.html#settings">Project settings</a>).</p>
+
+
+<h2><a name="issue_statuses"></a>5. Statuts des demandes</h2>
+<p>Cet écran vous permet de définir les différents statuts possibles des demandes.</p>
+<ul>
+ <li><b>Closed</b>: indique que le statut correspond à une demande considérée comme fermée</li>
+ <li><b>Default</b>: statut appliqué par défaut aux nouvelles demandes (seul un statut peut être déclaré comme statut par défaut)</li>
+ <li><b>HTML color</b>: code de couleur HTML représentant le statut à l'affichage</li>
+</ul>
+
+
+<h2><a name="workflow"></a>6. Workflow</h2>
+<p>Le workflow permet de définir quels changements les différents membres d'un projet sont autorisés à effectuer sur le statut des demandes, en fonction de leur type.</p>
+<p>Sélectionnez le rôle et le type de demande pour lesquels vous souhaitez modifier le workflow, puis cliquez sur <b>Edit</b>.
+L'écran vous permet alors de modifier, pour le rôle et le type de demande choisi, les changements autorisés.</p>
+<p>Les lignes représentent les statuts initiaux des demandes. Les colonnes représentent les statuts autorisés à être appliqués.</p>
+<p>Dans l'exemple ci-dessous, les demandes de type Bug au statut New pourront être passées au statut Assigned ou Resolved par le rôle Développeur.<br />
+Celles au statut Assigned pourront être passées au statut Resolved.<br />
+Le statut de toutes les autres demandes de type Bug ne pourra pas être modifié par le Développeur.</p>
+<center><img src="images/workflow.png"><br />
+<i><small>Exemple de configuration d'un workflow</small></i></center>
+<p><b>Remarque</b>: pour qu'un rôle puisse changer le statut des demandes, la permission <i>"Changer le statut des demandes"</i> doit lui être explicitement donnée indépendemment de la configuration du workflow (voir <a href="#roles">Roles et permissions</a>).
+
+
+<h2><a name="enumerations"></a>7. Listes de valeurs</h2>
+<p>Les listes de valeurs utilisées par l'application (exemple: les priorités des demandes) peuvent être personnalisées en fonction de vos besoins.<br />
+Cet écran vous permet de définir les valeurs possibles pour chacune des listes suivantes:</p>
+<ul>
+ <li><b>Priorités des demandes</b></li>
+ <li><b>Catégories de documents</b></li>
+</ul>
+
+
+<h2><a name="mail_notifications"></a>8. Notifications par mail</h2>
+<p>Cet écran vous permet de sélectionner les actions qui donneront lieu à une notification par mail aux membres du projet.</p>
+<p><b>Remarque</b>: l'envoi de mails doit être activé dans la configuration de l'application si souhaitez effectuer des notifications.</p>
+
+
+<h2><a name="app_info"></a>9. Informations</h2>
+<p>Affiche des informations relatives à l'application</p>
+<ul>
+ <li><b>Version</b>: version de l'application</li>
+ <li><b>Database</b>: type de base de données utilisée</li>
+</ul>
+
+
+</body>
+</html>
+
diff --git a/redmine/public/manual/images/issues_list.png b/redmine/public/manual/images/issues_list.png
new file mode 100644
index 000000000..2fa6dc1b6
--- /dev/null
+++ b/redmine/public/manual/images/issues_list.png
Binary files differ
diff --git a/redmine/public/manual/images/users_edit.png b/redmine/public/manual/images/users_edit.png
new file mode 100644
index 000000000..3a4797e9d
--- /dev/null
+++ b/redmine/public/manual/images/users_edit.png
Binary files differ
diff --git a/redmine/public/manual/images/users_list.png b/redmine/public/manual/images/users_list.png
new file mode 100644
index 000000000..0c9ef86ec
--- /dev/null
+++ b/redmine/public/manual/images/users_list.png
Binary files differ
diff --git a/redmine/public/manual/images/workflow.png b/redmine/public/manual/images/workflow.png
new file mode 100644
index 000000000..26071cade
--- /dev/null
+++ b/redmine/public/manual/images/workflow.png
Binary files differ
diff --git a/redmine/public/manual/index.html b/redmine/public/manual/index.html
new file mode 100644
index 000000000..3f0cb1e6f
--- /dev/null
+++ b/redmine/public/manual/index.html
@@ -0,0 +1,64 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
+
+<head>
+<title>redMine - Aide en ligne</title>
+<meta http-equiv="content-type" content="text/html; charset=iso-8859-1" />
+<meta name="description" content="redMine" />
+<meta name="keywords" content="issue,bug,tracker" />
+<link href="stylesheets/help.css" rel="stylesheet" type="text/css" />
+</head>
+
+<body>
+<h1>Aide</h1>
+
+<p>Documentation en ligne redMine</p>
+
+Sommaire:
+<ul>
+
+ <li>
+ <a href="administration.html">Administration</a>
+ <ol>
+ <li><a href="administration.html#users">Utilisateur</a></li>
+ <li><a href="administration.html#roles">Rôles et permissions</a></li>
+ <li><a href="administration.html#trackers">Trackers</a></li>
+ <li><a href="administration.html#custom_fields">Champs personnalisés</a></li>
+ <li><a href="administration.html#issue_statuses">Statuts de demande</a></li>
+ <li><a href="administration.html#workflow">Workflow</a></li>
+ <li><a href="administration.html#enumerations">Liste de valeurs</a></li>
+ <li><a href="administration.html#mail_notifications">Notifications par mail</a></li>
+ <li><a href="administration.html#app_info">Informations</a></li>
+ </ol>
+ </li>
+
+ <li><a href="projects.html">Projets</a>
+ <ol>
+ <li><a href="projects.html#overview">Aperçu</a></li>
+ <li><a href="projects.html#issues">Demandes</a>
+ <ul>
+ <li>Liste des demandes</li>
+ <li>Nouvelle demande</li>
+ <li>Changer le statut d'une demande</li>
+ </ul>
+ </li>
+ <li><a href="projects.html#reports">Rapports</a></li>
+ <li><a href="projects.html#news">Annonces</a></li>
+ <li><a href="projects.html#changelog">Historique</a></li>
+ <li><a href="projects.html#documents">Documents</a></li>
+ <li><a href="projects.html#members">Membres</a></li>
+ <li><a href="projects.html#files">Fichiers</a></li>
+ <li><a href="projects.html#settings">Configuration</a></li>
+ <ul>
+ <li>Projet</li>
+ <li>Membres</li>
+ <li>Versions</li>
+ <li>Catégories de demande</li>
+ </ul>
+ </ol>
+ </li>
+
+
+</ul>
+
+</body>
+</html> \ No newline at end of file
diff --git a/redmine/public/manual/projects.html b/redmine/public/manual/projects.html
new file mode 100644
index 000000000..13922f69d
--- /dev/null
+++ b/redmine/public/manual/projects.html
@@ -0,0 +1,109 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
+
+<head>
+<title>redMine - Aide en ligne</title>
+<meta http-equiv="content-type" content="text/html; charset=iso-8859-1" />
+<meta name="description" content="redMine" />
+<meta name="keywords" content="issue,bug,tracker" />
+<link href="stylesheets/help.css" rel="stylesheet" type="text/css" />
+</head>
+
+<body>
+<p align="right">[ <a href="index.html">Index</a> ]</p>
+<h1>Projets</h1>
+Sommaire:
+<ol>
+ <li><a href="projects.html#overview">Aperçu</a></li>
+ <li><a href="projects.html#issues">Demandes</a>
+ <ul>
+ <li>Liste des demandes</li>
+ <li>Nouvelle demande</li>
+ <li>Changer le statut d'une demande</li>
+ </ul>
+ </li>
+ <li><a href="projects.html#reports">Rapports</a></li>
+ <li><a href="projects.html#news">Annonces</a></li>
+ <li><a href="projects.html#changelog">Historique</a></li>
+ <li><a href="projects.html#documents">Documents</a></li>
+ <li><a href="projects.html#members">Membres</a></li>
+ <li><a href="projects.html#files">Fichiers</a></li>
+ <li><a href="projects.html#settings">Configuration</a></li>
+ <ul>
+ <li>Projet</li>
+ <li>Membres</li>
+ <li>Versions</li>
+ <li>Catégories de demande</li>
+ </ul>
+</ol>
+
+
+<h2><a name="overview"></a>1. Aperçu</h2>
+<p>L'aperçu vous présente les informations générales relatives au projet, les principaux membres, les dernières annonces, ainsi qu'une synthèse du nombre de demandes ouvertes par tracker.
+
+
+<h2><a name="issues"></a>2. Demandes</h2>
+<h3>2.1 Liste des demandes</h3>
+Par défaut, l'ensemble des demandes sont affichées. Vous pouvez utiliser les différents filtres pour limiter l'affichage à certaines demandes seulement.<br />
+Lorsque vous appliquez un filtre, il reste en place durant toute votre session. Vous pouvez le redéfinir, ou le supprimer en cliquant sur <b>Annuler</b>.
+<center><img src="images/issues_list.png"><br />
+<i><small>Liste des demandes</small></i></center>
+
+<h3>2.2 Nouvelle demande</h3>
+<p>TODO</p>
+
+<h3>2.3 Changer le statut d'une demande</h3>
+<p>TODO</p>
+
+
+<h2><a name="reports"></a>3. Rapports</h2>
+<p>Synthèse du nombre de demandes par statut et selon différents critères (tracker, priorité, catégorie).
+Des liens directs permettent d'accéder à la liste détaillée des demandes pour chaque critère.</p>
+
+
+<h2><a name="news"></a>4. Annonces</h2>
+<p>Les nouvelles vous permettent d'informer les utilisateurs sur l'activité du projet.</p>
+
+
+<h2><a name="changelog"></a>5. Historique</h2>
+<p>Cette page présente l'ensemble des demandes résolues dans chacune des versions du projet.
+Certains types de demande peuvent être exclus de cet affichage (voir <a href="administration.html#trackers">Trackers</a>).</p>
+
+
+<h2><a name="documents"></a>6. Documents</h2>
+<p>Les documents sont groupés par catégories (voir <a href="administration.html#enumerations">Listes de valeurs</a>).<br />
+Un document peut contenir plusieurs fichiers (exemple: révisions ou versions successives)</p>
+
+
+<h2><a name="members"></a>7. Membres</h2>
+<p>Affichage de l'ensemble des membres du projet, par rôle</p>
+
+
+<h2><a name="files"></a>8. Fichiers</h2>
+<p>Ce module vous permet de publier les fichiers de l'application (sources, binaires, ...) pour chaque version de l'application .</p>
+
+<h2><a name="settings"></a>9. Configuration</h2>
+<h3>9.1 Projet</h3>
+<ul>
+ <li><b>Public</b>: si le projet est public, il sera visible (consultation des demandes, des documents, ...) pour l'ensemble des utilisateurs, y compris ceux qui ne sont pas membres du projet.<br />
+ Si le projet n'est pas public, seuls les membres du projet y ont accès, en fonction de leur rôle.</li>
+ <li><b>Champs personnalisés</b>: sélectionner les champs personnalisés que vous souhaitez utiliser au sein du projet.<br />
+ Seul l'administrateur peut ajouter de nouveaux champs personnalisés.</li>
+</ul>
+<h3>9.2 Membres</h3>
+<p>Cette section vous permet de définir les membres du projet ainsi que leurs rôles respectifs.<br />
+Un utilisateur ne peut avoir qu'un rôle au sein d'un projet donné. Le rôle d'un membre détermine les permissions dont il bénéficie sur le projet.</p>
+
+<h3>9.3 Versions</h3>
+<p>Les versions vous permettent de suivre les changements survenus tout au long du projet.
+A la fermeture d'une demande, vous pouvez indiquer quelle version la prend en compte.<br />
+Vous pouvez par ailleurs publier les différentes versions de l'application (voir <a href="projects.html#files">Fichiers</a>).
+</p>
+
+
+<h3>9.4 Catégories de demande</h3>
+<p>Les catégories de demande vous permettent de typer les demandes. Les catégories peuvent par exemple correspondre aux modules de l'application.</p>
+
+
+</body>
+</html>
+
diff --git a/redmine/public/manual/stylesheets/help.css b/redmine/public/manual/stylesheets/help.css
new file mode 100644
index 000000000..f67bc72bf
--- /dev/null
+++ b/redmine/public/manual/stylesheets/help.css
@@ -0,0 +1,70 @@
+/* andreas08 - an open source xhtml/css website layout by Andreas Viklund - http://andreasviklund.com . Free to use in any way and for any purpose as long as the proper credits are given to the original designer. Version: 1.0, November 28, 2005 */
+
+/**************** Body and tag styles ****************/
+
+
+
+body{
+font:76% Verdana,Tahoma,Arial,sans-serif;
+line-height:1.4em;
+color:#303030;
+margin: 20px;
+}
+
+a{
+color:#467aa7;
+font-weight:bold;
+text-decoration:none;
+background-color:inherit;
+}
+
+a:hover{color:#2a5a8a; text-decoration:none; background-color:inherit;}
+a img{border:none;}
+
+p{padding:0 0 0.2em 0;}
+p form{margin-top:0; margin-bottom:20px;}
+
+h1 {
+display:block;
+
+font-size:1.7em;
+font-weight:normal;
+letter-spacing:-1px;
+color:#505050;
+background-color:inherit;
+}
+
+h2 {
+display:block;
+margin: 30px 0 0 0;
+font-size:1.5em;
+font-weight:normal;
+letter-spacing:-1px;
+color:#505050;
+background-color:inherit;
+}
+
+hr { border:0px; border-bottom:1px dashed #000000; }
+
+
+/**************** Misc classes and styles ****************/
+
+.splitcontentleft{float:left; width:49%;}
+.splitcontentright{float:right; width:49%;}
+.clear{clear:both;}
+.small{font-size:0.8em;line-height:1.4em;padding:0 0 0 0;}
+.hide{display:none;}
+.textcenter{text-align:center;}
+.textright{text-align:right;}
+.important{color:#f02025; background-color:inherit; }
+
+.box{
+margin:0 0 20px 0;
+padding:10px;
+border:1px solid #c0c0c0;
+background-color:#fafbfc;
+color:#505050;
+line-height:1.5em;
+}
+
+
diff --git a/redmine/public/robots.txt b/redmine/public/robots.txt
new file mode 100644
index 000000000..4ab9e89fe
--- /dev/null
+++ b/redmine/public/robots.txt
@@ -0,0 +1 @@
+# See http://www.robotstxt.org/wc/norobots.html for documentation on how to use the robots.txt file \ No newline at end of file
diff --git a/redmine/public/stylesheets/application.css b/redmine/public/stylesheets/application.css
new file mode 100644
index 000000000..ac6610520
--- /dev/null
+++ b/redmine/public/stylesheets/application.css
@@ -0,0 +1,322 @@
+/* andreas08 - an open source xhtml/css website layout by Andreas Viklund - http://andreasviklund.com . Free to use in any way and for any purpose as long as the proper credits are given to the original designer. Version: 1.0, November 28, 2005 */
+/* Edited by Jean-Philippe Lang *>
+/**************** Body and tag styles ****************/
+
+
+#header * {margin:0; padding:0;}
+p, ul, ol, li {margin:0; padding:0;}
+
+
+body{
+font:76% Verdana,Tahoma,Arial,sans-serif;
+line-height:1.4em;
+text-align:center;
+color:#303030;
+background:#e8eaec;
+}
+
+a{
+color:#467aa7;
+font-weight:bold;
+text-decoration:none;
+background-color:inherit;
+}
+
+a:hover{color:#2a5a8a; text-decoration:none; background-color:inherit;}
+a img{border:none;}
+
+p{padding:0 0 1em 0;}
+p form{margin-top:0; margin-bottom:20px;}
+
+img.left,img.center,img.right{padding:4px; border:1px solid #a0a0a0;}
+img.left{float:left; margin:0 12px 5px 0;}
+img.center{display:block; margin:0 auto 5px auto;}
+img.right{float:right; margin:0 0 5px 12px;}
+
+/**************** Header and navigation styles ****************/
+
+#container{
+width:100%;
+min-width: 800px;
+margin:5px auto;
+padding:1px 0;
+text-align:left;
+background:#ffffff;
+color:#303030;
+border:2px solid #a0a0a0;
+}
+
+#header{
+height:5.5em;
+/*width:758px;*/
+margin:0 1px 1px 1px;
+background:#467aa7;
+color:#ffffff;
+}
+
+#header h1{
+padding:14px 0 0 20px;
+font-size:2.4em;
+background-color:inherit;
+color:#fff; /*rgb(152, 26, 33);*/
+letter-spacing:-2px;
+font-weight:normal;
+}
+
+#header h2{
+margin:10px 0 0 40px;
+font-size:1.4em;
+background-color:inherit;
+color:#f0f2f4;
+letter-spacing:-1px;
+font-weight:normal;
+}
+
+#navigation{
+height:2.2em;
+line-height:2.2em;
+/*width:758px;*/
+margin:0 1px;
+background:#578bb8;
+color:#ffffff;
+}
+
+#navigation li{
+float:left;
+list-style-type:none;
+border-right:1px solid #ffffff;
+white-space:nowrap;
+}
+
+#navigation li.right {
+ float:right;
+list-style-type:none;
+border-right:0;
+border-left:1px solid #ffffff;
+white-space:nowrap;
+}
+
+#navigation li a{
+display:block;
+padding:0px 10px 0px 22px;
+font-size:0.8em;
+font-weight:normal;
+/*text-transform:uppercase;*/
+text-decoration:none;
+background-color:inherit;
+color: #ffffff;
+}
+
+* html #navigation a {width:1%;}
+
+#navigation .selected,#navigation a:hover{
+color:#ffffff;
+text-decoration:none;
+background-color: #80b0da;
+}
+
+/**************** Icons links *******************/
+.picHome { background: url(../images/home.png) no-repeat 4px 50%; }
+.picUser { background: url(../images/user.png) no-repeat 4px 50%; }
+.picUserPage { background: url(../images/user_page.png) no-repeat 4px 50%; }
+.picAdmin { background: url(../images/admin.png) no-repeat 4px 50%; }
+.picProject { background: url(../images/projects.png) no-repeat 4px 50%; }
+.picLogout { background: url(../images/logout.png) no-repeat 4px 50%; }
+.picHelp { background: url(../images/help.png) no-repeat 4px 50%; }
+
+/**************** Content styles ****************/
+
+#content{
+/*float:right;*/
+/*width:530px;*/
+width: auto;
+min-height: 500px;
+font-size:0.9em;
+padding:20px 10px 10px 20px;
+/*position: absolute;*/
+margin: 0 0 0 140px;
+border-left: 1px dashed #c0c0c0;
+
+}
+
+#content h2{
+display:block;
+margin:0 0 16px 0;
+font-size:1.7em;
+font-weight:normal;
+letter-spacing:-1px;
+color:#505050;
+background-color:inherit;
+}
+
+#content h2 a{font-weight:normal;}
+#content h3{margin:0 0 5px 0; font-size:1.4em; letter-spacing:-1px;}
+#content a:hover,#subcontent a:hover{text-decoration:underline;}
+#content ul,#content ol{margin:0 5px 16px 35px;}
+#content dl{margin:0 5px 10px 25px;}
+#content dt{font-weight:bold; margin-bottom:5px;}
+#content dd{margin:0 0 10px 15px;}
+
+
+/***********************************************/
+
+/*
+form{
+ padding:15px;
+ margin:0 0 20px 0;
+ border:1px solid #c0c0c0;
+ background-color:#CEE1ED;
+ width:600px;
+}
+*/
+
+form {
+ display: inline;
+}
+
+.noborder {
+ border:0px;
+ background-color:#fff;
+ width:100%;
+}
+
+input {
+ vertical-align: top;
+}
+
+input.button-small
+{
+ font-size: 0.8em;
+}
+
+label {
+ font-weight: bold;
+ font-size: 1em;
+}
+
+.required {
+ color: #bb0000;
+}
+
+table.listTableContent {
+ /*margin: 2em 2em 2em 0; */
+ border:1px solid #c0c0c0;
+ width:99%;
+}
+
+table.listTableContent td {
+ margin: 2px;
+
+}
+
+tr.ListHead {
+ background-color:#467aa7;
+ color:#FFFFFF;
+ text-align:center;
+}
+
+tr.ListHead a {
+ color:#FFFFFF;
+ text-decoration:underline;
+}
+
+tr.ListLine0 {
+ background-color: #C1E2F7;
+}
+tr.ListLine1 {
+ background-color:#CEE1ED;
+}
+
+hr { border:0px; border-bottom:1px dashed #000000; }
+
+
+/**************** Sidebar styles ****************/
+
+#subcontent{
+float:left;
+clear:both;
+width:130px;
+padding:20px 20px 10px 5px;
+line-height:1.4em;
+}
+
+#subcontent h2{
+display:block;
+margin:0 0 15px 0;
+font-size:1.6em;
+font-weight:normal;
+text-align:left;
+letter-spacing:-1px;
+color:#505050;
+background-color:inherit;
+}
+
+#subcontent p{margin:0 0 16px 0; font-size:0.9em;}
+
+/**************** Menublock styles ****************/
+
+.menublock{margin:0 0 20px 8px; font-size:0.9em;}
+.menublock li{list-style:none; display:block; padding:1px; margin-bottom:0px;}
+.menublock li a{font-weight:bold; text-decoration:none;}
+.menublock li a:hover{text-decoration:none;}
+.menublock li ul{margin:3px 0 3px 15px; font-size:1em; font-weight:normal;}
+.menublock li ul li{margin-bottom:0;}
+.menublock li ul a{font-weight:normal;}
+
+/**************** Searchbar styles ****************/
+
+#searchbar{margin:0 0 20px 0;}
+#searchbar form fieldset{margin-left:10px; border:0 solid;}
+
+#searchbar #s{
+height:1.2em;
+width:110px;
+margin:0 5px 0 0;
+border:1px solid #a0a0a0;
+}
+
+#searchbar #searchbutton{
+width:auto;
+padding:0 1px;
+border:1px solid #808080;
+font-size:0.9em;
+text-align:center;
+}
+
+/**************** Footer styles ****************/
+
+#footer{
+clear:both;
+/*width:758px;*/
+padding:5px 0;
+margin:0 1px;
+font-size:0.9em;
+color:#f0f0f0;
+background:#467aa7;
+}
+
+#footer p{padding:0; margin:0; text-align:center;}
+#footer a{color:#f0f0f0; background-color:inherit; font-weight:bold;}
+#footer a:hover{color:#ffffff; background-color:inherit; text-decoration: underline;}
+
+/**************** Misc classes and styles ****************/
+
+.splitcontentleft{float:left; width:49%;}
+.splitcontentright{float:right; width:49%;}
+.clear{clear:both;}
+.small{font-size:0.8em;line-height:1.4em;padding:0 0 0 0;}
+.hide{display:none;}
+.textcenter{text-align:center;}
+.textright{text-align:right;}
+.important{color:#f02025; background-color:inherit; font-weight:bold;}
+
+.box{
+margin:0 0 20px 0;
+padding:10px;
+border:1px solid #c0c0c0;
+background-color:#fafbfc;
+color:#505050;
+line-height:1.5em;
+}
+
+
diff --git a/redmine/public/stylesheets/rails.css b/redmine/public/stylesheets/rails.css
new file mode 100644
index 000000000..e2954c9a7
--- /dev/null
+++ b/redmine/public/stylesheets/rails.css
@@ -0,0 +1,56 @@
+
+.fieldWithErrors {
+ padding: 2px;
+ background-color: red;
+ display: table;
+}
+
+#errorExplanation {
+ width: 400px;
+ border: 2px solid red;
+ padding: 7px;
+ padding-bottom: 12px;
+ margin-bottom: 20px;
+ background-color: #f0f0f0;
+}
+
+#errorExplanation h2 {
+ text-align: left;
+ font-weight: bold;
+ padding: 5px 5px 5px 15px;
+ font-size: 12px;
+ margin: -7px;
+ background-color: #c00;
+ color: #fff;
+}
+
+#errorExplanation p {
+ color: #333;
+ margin-bottom: 0;
+ padding: 5px;
+}
+
+#errorExplanation ul li {
+ font-size: 12px;
+ list-style: square;
+}
+
+div.uploadStatus {
+ margin: 5px;
+}
+
+div.progressBar {
+ margin: 5px;
+}
+
+div.progressBar div.border {
+ background-color: #fff;
+ border: 1px solid grey;
+ width: 100%;
+}
+
+div.progressBar div.background {
+ background-color: #333;
+ height: 18px;
+ width: 0%;
+}
diff --git a/redmine/script/about b/redmine/script/about
new file mode 100644
index 000000000..7b07d46a3
--- /dev/null
+++ b/redmine/script/about
@@ -0,0 +1,3 @@
+#!/usr/bin/env ruby
+require File.dirname(__FILE__) + '/../config/boot'
+require 'commands/about' \ No newline at end of file
diff --git a/redmine/script/breakpointer b/redmine/script/breakpointer
new file mode 100644
index 000000000..64af76edd
--- /dev/null
+++ b/redmine/script/breakpointer
@@ -0,0 +1,3 @@
+#!/usr/bin/env ruby
+require File.dirname(__FILE__) + '/../config/boot'
+require 'commands/breakpointer' \ No newline at end of file
diff --git a/redmine/script/console b/redmine/script/console
new file mode 100644
index 000000000..42f28f7d6
--- /dev/null
+++ b/redmine/script/console
@@ -0,0 +1,3 @@
+#!/usr/bin/env ruby
+require File.dirname(__FILE__) + '/../config/boot'
+require 'commands/console' \ No newline at end of file
diff --git a/redmine/script/destroy b/redmine/script/destroy
new file mode 100644
index 000000000..fa0e6fcd0
--- /dev/null
+++ b/redmine/script/destroy
@@ -0,0 +1,3 @@
+#!/usr/bin/env ruby
+require File.dirname(__FILE__) + '/../config/boot'
+require 'commands/destroy' \ No newline at end of file
diff --git a/redmine/script/generate b/redmine/script/generate
new file mode 100644
index 000000000..ef976e09f
--- /dev/null
+++ b/redmine/script/generate
@@ -0,0 +1,3 @@
+#!/usr/bin/env ruby
+require File.dirname(__FILE__) + '/../config/boot'
+require 'commands/generate' \ No newline at end of file
diff --git a/redmine/script/performance/benchmarker b/redmine/script/performance/benchmarker
new file mode 100644
index 000000000..c842d35d3
--- /dev/null
+++ b/redmine/script/performance/benchmarker
@@ -0,0 +1,3 @@
+#!/usr/bin/env ruby
+require File.dirname(__FILE__) + '/../../config/boot'
+require 'commands/performance/benchmarker'
diff --git a/redmine/script/performance/profiler b/redmine/script/performance/profiler
new file mode 100644
index 000000000..d855ac8b1
--- /dev/null
+++ b/redmine/script/performance/profiler
@@ -0,0 +1,3 @@
+#!/usr/bin/env ruby
+require File.dirname(__FILE__) + '/../../config/boot'
+require 'commands/performance/profiler'
diff --git a/redmine/script/plugin b/redmine/script/plugin
new file mode 100644
index 000000000..26ca64c06
--- /dev/null
+++ b/redmine/script/plugin
@@ -0,0 +1,3 @@
+#!/usr/bin/env ruby
+require File.dirname(__FILE__) + '/../config/boot'
+require 'commands/plugin' \ No newline at end of file
diff --git a/redmine/script/process/reaper b/redmine/script/process/reaper
new file mode 100644
index 000000000..c77f04535
--- /dev/null
+++ b/redmine/script/process/reaper
@@ -0,0 +1,3 @@
+#!/usr/bin/env ruby
+require File.dirname(__FILE__) + '/../../config/boot'
+require 'commands/process/reaper'
diff --git a/redmine/script/process/spawner b/redmine/script/process/spawner
new file mode 100644
index 000000000..7118f3983
--- /dev/null
+++ b/redmine/script/process/spawner
@@ -0,0 +1,3 @@
+#!/usr/bin/env ruby
+require File.dirname(__FILE__) + '/../../config/boot'
+require 'commands/process/spawner'
diff --git a/redmine/script/process/spinner b/redmine/script/process/spinner
new file mode 100644
index 000000000..6816b32ef
--- /dev/null
+++ b/redmine/script/process/spinner
@@ -0,0 +1,3 @@
+#!/usr/bin/env ruby
+require File.dirname(__FILE__) + '/../../config/boot'
+require 'commands/process/spinner'
diff --git a/redmine/script/runner b/redmine/script/runner
new file mode 100644
index 000000000..ccc30f9d2
--- /dev/null
+++ b/redmine/script/runner
@@ -0,0 +1,3 @@
+#!/usr/bin/env ruby
+require File.dirname(__FILE__) + '/../config/boot'
+require 'commands/runner' \ No newline at end of file
diff --git a/redmine/script/server b/redmine/script/server
new file mode 100644
index 000000000..dfabcb881
--- /dev/null
+++ b/redmine/script/server
@@ -0,0 +1,3 @@
+#!/usr/bin/env ruby
+require File.dirname(__FILE__) + '/../config/boot'
+require 'commands/server' \ No newline at end of file
diff --git a/redmine/test/fixtures/attachments.yml b/redmine/test/fixtures/attachments.yml
new file mode 100644
index 000000000..8794d28ae
--- /dev/null
+++ b/redmine/test/fixtures/attachments.yml
@@ -0,0 +1,5 @@
+# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
+first:
+ id: 1
+another:
+ id: 2
diff --git a/redmine/test/fixtures/custom_fields.yml b/redmine/test/fixtures/custom_fields.yml
new file mode 100644
index 000000000..8794d28ae
--- /dev/null
+++ b/redmine/test/fixtures/custom_fields.yml
@@ -0,0 +1,5 @@
+# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
+first:
+ id: 1
+another:
+ id: 2
diff --git a/redmine/test/fixtures/documents.yml b/redmine/test/fixtures/documents.yml
new file mode 100644
index 000000000..8794d28ae
--- /dev/null
+++ b/redmine/test/fixtures/documents.yml
@@ -0,0 +1,5 @@
+# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
+first:
+ id: 1
+another:
+ id: 2
diff --git a/redmine/test/fixtures/enumerations.yml b/redmine/test/fixtures/enumerations.yml
new file mode 100644
index 000000000..8794d28ae
--- /dev/null
+++ b/redmine/test/fixtures/enumerations.yml
@@ -0,0 +1,5 @@
+# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
+first:
+ id: 1
+another:
+ id: 2
diff --git a/redmine/test/fixtures/issue_categories.yml b/redmine/test/fixtures/issue_categories.yml
new file mode 100644
index 000000000..8794d28ae
--- /dev/null
+++ b/redmine/test/fixtures/issue_categories.yml
@@ -0,0 +1,5 @@
+# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
+first:
+ id: 1
+another:
+ id: 2
diff --git a/redmine/test/fixtures/issue_custom_fields.yml b/redmine/test/fixtures/issue_custom_fields.yml
new file mode 100644
index 000000000..8794d28ae
--- /dev/null
+++ b/redmine/test/fixtures/issue_custom_fields.yml
@@ -0,0 +1,5 @@
+# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
+first:
+ id: 1
+another:
+ id: 2
diff --git a/redmine/test/fixtures/issue_custom_values.yml b/redmine/test/fixtures/issue_custom_values.yml
new file mode 100644
index 000000000..8794d28ae
--- /dev/null
+++ b/redmine/test/fixtures/issue_custom_values.yml
@@ -0,0 +1,5 @@
+# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
+first:
+ id: 1
+another:
+ id: 2
diff --git a/redmine/test/fixtures/issue_histories.yml b/redmine/test/fixtures/issue_histories.yml
new file mode 100644
index 000000000..8794d28ae
--- /dev/null
+++ b/redmine/test/fixtures/issue_histories.yml
@@ -0,0 +1,5 @@
+# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
+first:
+ id: 1
+another:
+ id: 2
diff --git a/redmine/test/fixtures/issue_statuses.yml b/redmine/test/fixtures/issue_statuses.yml
new file mode 100644
index 000000000..8794d28ae
--- /dev/null
+++ b/redmine/test/fixtures/issue_statuses.yml
@@ -0,0 +1,5 @@
+# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
+first:
+ id: 1
+another:
+ id: 2
diff --git a/redmine/test/fixtures/issues.yml b/redmine/test/fixtures/issues.yml
new file mode 100644
index 000000000..8794d28ae
--- /dev/null
+++ b/redmine/test/fixtures/issues.yml
@@ -0,0 +1,5 @@
+# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
+first:
+ id: 1
+another:
+ id: 2
diff --git a/redmine/test/fixtures/mailer/issue_closed b/redmine/test/fixtures/mailer/issue_closed
new file mode 100644
index 000000000..bb5e51d95
--- /dev/null
+++ b/redmine/test/fixtures/mailer/issue_closed
@@ -0,0 +1,3 @@
+Mailer#issue_closed
+
+Find me in app/views/mailer/issue_closed.rhtml
diff --git a/redmine/test/fixtures/members.yml b/redmine/test/fixtures/members.yml
new file mode 100644
index 000000000..8794d28ae
--- /dev/null
+++ b/redmine/test/fixtures/members.yml
@@ -0,0 +1,5 @@
+# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
+first:
+ id: 1
+another:
+ id: 2
diff --git a/redmine/test/fixtures/news.yml b/redmine/test/fixtures/news.yml
new file mode 100644
index 000000000..8794d28ae
--- /dev/null
+++ b/redmine/test/fixtures/news.yml
@@ -0,0 +1,5 @@
+# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
+first:
+ id: 1
+another:
+ id: 2
diff --git a/redmine/test/fixtures/permissions.yml b/redmine/test/fixtures/permissions.yml
new file mode 100644
index 000000000..8794d28ae
--- /dev/null
+++ b/redmine/test/fixtures/permissions.yml
@@ -0,0 +1,5 @@
+# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
+first:
+ id: 1
+another:
+ id: 2
diff --git a/redmine/test/fixtures/projects.yml b/redmine/test/fixtures/projects.yml
new file mode 100644
index 000000000..8794d28ae
--- /dev/null
+++ b/redmine/test/fixtures/projects.yml
@@ -0,0 +1,5 @@
+# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
+first:
+ id: 1
+another:
+ id: 2
diff --git a/redmine/test/fixtures/roles.yml b/redmine/test/fixtures/roles.yml
new file mode 100644
index 000000000..8794d28ae
--- /dev/null
+++ b/redmine/test/fixtures/roles.yml
@@ -0,0 +1,5 @@
+# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
+first:
+ id: 1
+another:
+ id: 2
diff --git a/redmine/test/fixtures/trackers.yml b/redmine/test/fixtures/trackers.yml
new file mode 100644
index 000000000..8794d28ae
--- /dev/null
+++ b/redmine/test/fixtures/trackers.yml
@@ -0,0 +1,5 @@
+# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
+first:
+ id: 1
+another:
+ id: 2
diff --git a/redmine/test/fixtures/users.yml b/redmine/test/fixtures/users.yml
new file mode 100644
index 000000000..8794d28ae
--- /dev/null
+++ b/redmine/test/fixtures/users.yml
@@ -0,0 +1,5 @@
+# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
+first:
+ id: 1
+another:
+ id: 2
diff --git a/redmine/test/fixtures/versions.yml b/redmine/test/fixtures/versions.yml
new file mode 100644
index 000000000..8794d28ae
--- /dev/null
+++ b/redmine/test/fixtures/versions.yml
@@ -0,0 +1,5 @@
+# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
+first:
+ id: 1
+another:
+ id: 2
diff --git a/redmine/test/fixtures/workflow.yml b/redmine/test/fixtures/workflow.yml
new file mode 100644
index 000000000..8794d28ae
--- /dev/null
+++ b/redmine/test/fixtures/workflow.yml
@@ -0,0 +1,5 @@
+# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
+first:
+ id: 1
+another:
+ id: 2
diff --git a/redmine/test/functional/account_controller_test.rb b/redmine/test/functional/account_controller_test.rb
new file mode 100644
index 000000000..537eb8ffc
--- /dev/null
+++ b/redmine/test/functional/account_controller_test.rb
@@ -0,0 +1,18 @@
+require File.dirname(__FILE__) + '/../test_helper'
+require 'account_controller'
+
+# Re-raise errors caught by the controller.
+class AccountController; def rescue_action(e) raise e end; end
+
+class AccountControllerTest < Test::Unit::TestCase
+ def setup
+ @controller = AccountController.new
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+ end
+
+ # Replace this with your real tests.
+ def test_truth
+ assert true
+ end
+end
diff --git a/redmine/test/functional/admin_controller_test.rb b/redmine/test/functional/admin_controller_test.rb
new file mode 100644
index 000000000..e44ac9423
--- /dev/null
+++ b/redmine/test/functional/admin_controller_test.rb
@@ -0,0 +1,18 @@
+require File.dirname(__FILE__) + '/../test_helper'
+require 'admin_controller'
+
+# Re-raise errors caught by the controller.
+class AdminController; def rescue_action(e) raise e end; end
+
+class AdminControllerTest < Test::Unit::TestCase
+ def setup
+ @controller = AdminController.new
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+ end
+
+ # Replace this with your real tests.
+ def test_truth
+ assert true
+ end
+end
diff --git a/redmine/test/functional/custom_fields_controller_test.rb b/redmine/test/functional/custom_fields_controller_test.rb
new file mode 100644
index 000000000..f86e32569
--- /dev/null
+++ b/redmine/test/functional/custom_fields_controller_test.rb
@@ -0,0 +1,88 @@
+require File.dirname(__FILE__) + '/../test_helper'
+require 'custom_fields_controller'
+
+# Re-raise errors caught by the controller.
+class CustomFieldsController; def rescue_action(e) raise e end; end
+
+class CustomFieldsControllerTest < Test::Unit::TestCase
+ fixtures :custom_fields
+
+ def setup
+ @controller = CustomFieldsController.new
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+ end
+
+ def test_index
+ get :index
+ assert_response :success
+ assert_template 'list'
+ end
+
+ def test_list
+ get :list
+
+ assert_response :success
+ assert_template 'list'
+
+ assert_not_nil assigns(:custom_fields)
+ end
+
+ def test_show
+ get :show, :id => 1
+
+ assert_response :success
+ assert_template 'show'
+
+ assert_not_nil assigns(:custom_field)
+ assert assigns(:custom_field).valid?
+ end
+
+ def test_new
+ get :new
+
+ assert_response :success
+ assert_template 'new'
+
+ assert_not_nil assigns(:custom_field)
+ end
+
+ def test_create
+ num_custom_fields = CustomField.count
+
+ post :create, :custom_field => {}
+
+ assert_response :redirect
+ assert_redirected_to :action => 'list'
+
+ assert_equal num_custom_fields + 1, CustomField.count
+ end
+
+ def test_edit
+ get :edit, :id => 1
+
+ assert_response :success
+ assert_template 'edit'
+
+ assert_not_nil assigns(:custom_field)
+ assert assigns(:custom_field).valid?
+ end
+
+ def test_update
+ post :update, :id => 1
+ assert_response :redirect
+ assert_redirected_to :action => 'show', :id => 1
+ end
+
+ def test_destroy
+ assert_not_nil CustomField.find(1)
+
+ post :destroy, :id => 1
+ assert_response :redirect
+ assert_redirected_to :action => 'list'
+
+ assert_raise(ActiveRecord::RecordNotFound) {
+ CustomField.find(1)
+ }
+ end
+end
diff --git a/redmine/test/functional/documents_controller_test.rb b/redmine/test/functional/documents_controller_test.rb
new file mode 100644
index 000000000..c9bd463d3
--- /dev/null
+++ b/redmine/test/functional/documents_controller_test.rb
@@ -0,0 +1,88 @@
+require File.dirname(__FILE__) + '/../test_helper'
+require 'documents_controller'
+
+# Re-raise errors caught by the controller.
+class DocumentsController; def rescue_action(e) raise e end; end
+
+class DocumentsControllerTest < Test::Unit::TestCase
+ fixtures :documents
+
+ def setup
+ @controller = DocumentsController.new
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+ end
+
+ def test_index
+ get :index
+ assert_response :success
+ assert_template 'list'
+ end
+
+ def test_list
+ get :list
+
+ assert_response :success
+ assert_template 'list'
+
+ assert_not_nil assigns(:documents)
+ end
+
+ def test_show
+ get :show, :id => 1
+
+ assert_response :success
+ assert_template 'show'
+
+ assert_not_nil assigns(:document)
+ assert assigns(:document).valid?
+ end
+
+ def test_new
+ get :new
+
+ assert_response :success
+ assert_template 'new'
+
+ assert_not_nil assigns(:document)
+ end
+
+ def test_create
+ num_documents = Document.count
+
+ post :create, :document => {}
+
+ assert_response :redirect
+ assert_redirected_to :action => 'list'
+
+ assert_equal num_documents + 1, Document.count
+ end
+
+ def test_edit
+ get :edit, :id => 1
+
+ assert_response :success
+ assert_template 'edit'
+
+ assert_not_nil assigns(:document)
+ assert assigns(:document).valid?
+ end
+
+ def test_update
+ post :update, :id => 1
+ assert_response :redirect
+ assert_redirected_to :action => 'show', :id => 1
+ end
+
+ def test_destroy
+ assert_not_nil Document.find(1)
+
+ post :destroy, :id => 1
+ assert_response :redirect
+ assert_redirected_to :action => 'list'
+
+ assert_raise(ActiveRecord::RecordNotFound) {
+ Document.find(1)
+ }
+ end
+end
diff --git a/redmine/test/functional/enumerations_controller_test.rb b/redmine/test/functional/enumerations_controller_test.rb
new file mode 100644
index 000000000..e9f4b7660
--- /dev/null
+++ b/redmine/test/functional/enumerations_controller_test.rb
@@ -0,0 +1,88 @@
+require File.dirname(__FILE__) + '/../test_helper'
+require 'enumerations_controller'
+
+# Re-raise errors caught by the controller.
+class EnumerationsController; def rescue_action(e) raise e end; end
+
+class EnumerationsControllerTest < Test::Unit::TestCase
+ fixtures :enumerations
+
+ def setup
+ @controller = EnumerationsController.new
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+ end
+
+ def test_index
+ get :index
+ assert_response :success
+ assert_template 'list'
+ end
+
+ def test_list
+ get :list
+
+ assert_response :success
+ assert_template 'list'
+
+ assert_not_nil assigns(:enumerations)
+ end
+
+ def test_show
+ get :show, :id => 1
+
+ assert_response :success
+ assert_template 'show'
+
+ assert_not_nil assigns(:enumeration)
+ assert assigns(:enumeration).valid?
+ end
+
+ def test_new
+ get :new
+
+ assert_response :success
+ assert_template 'new'
+
+ assert_not_nil assigns(:enumeration)
+ end
+
+ def test_create
+ num_enumerations = Enumeration.count
+
+ post :create, :enumeration => {}
+
+ assert_response :redirect
+ assert_redirected_to :action => 'list'
+
+ assert_equal num_enumerations + 1, Enumeration.count
+ end
+
+ def test_edit
+ get :edit, :id => 1
+
+ assert_response :success
+ assert_template 'edit'
+
+ assert_not_nil assigns(:enumeration)
+ assert assigns(:enumeration).valid?
+ end
+
+ def test_update
+ post :update, :id => 1
+ assert_response :redirect
+ assert_redirected_to :action => 'show', :id => 1
+ end
+
+ def test_destroy
+ assert_not_nil Enumeration.find(1)
+
+ post :destroy, :id => 1
+ assert_response :redirect
+ assert_redirected_to :action => 'list'
+
+ assert_raise(ActiveRecord::RecordNotFound) {
+ Enumeration.find(1)
+ }
+ end
+end
diff --git a/redmine/test/functional/help_controller_test.rb b/redmine/test/functional/help_controller_test.rb
new file mode 100644
index 000000000..291838b8f
--- /dev/null
+++ b/redmine/test/functional/help_controller_test.rb
@@ -0,0 +1,18 @@
+require File.dirname(__FILE__) + '/../test_helper'
+require 'help_controller'
+
+# Re-raise errors caught by the controller.
+class HelpController; def rescue_action(e) raise e end; end
+
+class HelpControllerTest < Test::Unit::TestCase
+ def setup
+ @controller = HelpController.new
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+ end
+
+ # Replace this with your real tests.
+ def test_truth
+ assert true
+ end
+end
diff --git a/redmine/test/functional/issue_categories_controller_test.rb b/redmine/test/functional/issue_categories_controller_test.rb
new file mode 100644
index 000000000..4ea4e1c55
--- /dev/null
+++ b/redmine/test/functional/issue_categories_controller_test.rb
@@ -0,0 +1,88 @@
+require File.dirname(__FILE__) + '/../test_helper'
+require 'issue_categories_controller'
+
+# Re-raise errors caught by the controller.
+class IssueCategoriesController; def rescue_action(e) raise e end; end
+
+class IssueCategoriesControllerTest < Test::Unit::TestCase
+ fixtures :issue_categories
+
+ def setup
+ @controller = IssueCategoriesController.new
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+ end
+
+ def test_index
+ get :index
+ assert_response :success
+ assert_template 'list'
+ end
+
+ def test_list
+ get :list
+
+ assert_response :success
+ assert_template 'list'
+
+ assert_not_nil assigns(:issue_categories)
+ end
+
+ def test_show
+ get :show, :id => 1
+
+ assert_response :success
+ assert_template 'show'
+
+ assert_not_nil assigns(:issue_category)
+ assert assigns(:issue_category).valid?
+ end
+
+ def test_new
+ get :new
+
+ assert_response :success
+ assert_template 'new'
+
+ assert_not_nil assigns(:issue_category)
+ end
+
+ def test_create
+ num_issue_categories = IssueCategory.count
+
+ post :create, :issue_category => {}
+
+ assert_response :redirect
+ assert_redirected_to :action => 'list'
+
+ assert_equal num_issue_categories + 1, IssueCategory.count
+ end
+
+ def test_edit
+ get :edit, :id => 1
+
+ assert_response :success
+ assert_template 'edit'
+
+ assert_not_nil assigns(:issue_category)
+ assert assigns(:issue_category).valid?
+ end
+
+ def test_update
+ post :update, :id => 1
+ assert_response :redirect
+ assert_redirected_to :action => 'show', :id => 1
+ end
+
+ def test_destroy
+ assert_not_nil IssueCategory.find(1)
+
+ post :destroy, :id => 1
+ assert_response :redirect
+ assert_redirected_to :action => 'list'
+
+ assert_raise(ActiveRecord::RecordNotFound) {
+ IssueCategory.find(1)
+ }
+ end
+end
diff --git a/redmine/test/functional/issue_statuses_controller_test.rb b/redmine/test/functional/issue_statuses_controller_test.rb
new file mode 100644
index 000000000..17f11ef55
--- /dev/null
+++ b/redmine/test/functional/issue_statuses_controller_test.rb
@@ -0,0 +1,88 @@
+require File.dirname(__FILE__) + '/../test_helper'
+require 'issue_statuses_controller'
+
+# Re-raise errors caught by the controller.
+class IssueStatusesController; def rescue_action(e) raise e end; end
+
+class IssueStatusesControllerTest < Test::Unit::TestCase
+ fixtures :issue_statuses
+
+ def setup
+ @controller = IssueStatusesController.new
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+ end
+
+ def test_index
+ get :index
+ assert_response :success
+ assert_template 'list'
+ end
+
+ def test_list
+ get :list
+
+ assert_response :success
+ assert_template 'list'
+
+ assert_not_nil assigns(:issue_statuses)
+ end
+
+ def test_show
+ get :show, :id => 1
+
+ assert_response :success
+ assert_template 'show'
+
+ assert_not_nil assigns(:issue_status)
+ assert assigns(:issue_status).valid?
+ end
+
+ def test_new
+ get :new
+
+ assert_response :success
+ assert_template 'new'
+
+ assert_not_nil assigns(:issue_status)
+ end
+
+ def test_create
+ num_issue_statuses = IssueStatus.count
+
+ post :create, :issue_status => {}
+
+ assert_response :redirect
+ assert_redirected_to :action => 'list'
+
+ assert_equal num_issue_statuses + 1, IssueStatus.count
+ end
+
+ def test_edit
+ get :edit, :id => 1
+
+ assert_response :success
+ assert_template 'edit'
+
+ assert_not_nil assigns(:issue_status)
+ assert assigns(:issue_status).valid?
+ end
+
+ def test_update
+ post :update, :id => 1
+ assert_response :redirect
+ assert_redirected_to :action => 'show', :id => 1
+ end
+
+ def test_destroy
+ assert_not_nil IssueStatus.find(1)
+
+ post :destroy, :id => 1
+ assert_response :redirect
+ assert_redirected_to :action => 'list'
+
+ assert_raise(ActiveRecord::RecordNotFound) {
+ IssueStatus.find(1)
+ }
+ end
+end
diff --git a/redmine/test/functional/issues_controller_test.rb b/redmine/test/functional/issues_controller_test.rb
new file mode 100644
index 000000000..1be41f86a
--- /dev/null
+++ b/redmine/test/functional/issues_controller_test.rb
@@ -0,0 +1,88 @@
+require File.dirname(__FILE__) + '/../test_helper'
+require 'issues_controller'
+
+# Re-raise errors caught by the controller.
+class IssuesController; def rescue_action(e) raise e end; end
+
+class IssuesControllerTest < Test::Unit::TestCase
+ fixtures :issues
+
+ def setup
+ @controller = IssuesController.new
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+ end
+
+ def test_index
+ get :index
+ assert_response :success
+ assert_template 'list'
+ end
+
+ def test_list
+ get :list
+
+ assert_response :success
+ assert_template 'list'
+
+ assert_not_nil assigns(:issues)
+ end
+
+ def test_show
+ get :show, :id => 1
+
+ assert_response :success
+ assert_template 'show'
+
+ assert_not_nil assigns(:issue)
+ assert assigns(:issue).valid?
+ end
+
+ def test_new
+ get :new
+
+ assert_response :success
+ assert_template 'new'
+
+ assert_not_nil assigns(:issue)
+ end
+
+ def test_create
+ num_issues = Issue.count
+
+ post :create, :issue => {}
+
+ assert_response :redirect
+ assert_redirected_to :action => 'list'
+
+ assert_equal num_issues + 1, Issue.count
+ end
+
+ def test_edit
+ get :edit, :id => 1
+
+ assert_response :success
+ assert_template 'edit'
+
+ assert_not_nil assigns(:issue)
+ assert assigns(:issue).valid?
+ end
+
+ def test_update
+ post :update, :id => 1
+ assert_response :redirect
+ assert_redirected_to :action => 'show', :id => 1
+ end
+
+ def test_destroy
+ assert_not_nil Issue.find(1)
+
+ post :destroy, :id => 1
+ assert_response :redirect
+ assert_redirected_to :action => 'list'
+
+ assert_raise(ActiveRecord::RecordNotFound) {
+ Issue.find(1)
+ }
+ end
+end
diff --git a/redmine/test/functional/members_controller_test.rb b/redmine/test/functional/members_controller_test.rb
new file mode 100644
index 000000000..5f47c358d
--- /dev/null
+++ b/redmine/test/functional/members_controller_test.rb
@@ -0,0 +1,88 @@
+require File.dirname(__FILE__) + '/../test_helper'
+require 'members_controller'
+
+# Re-raise errors caught by the controller.
+class MembersController; def rescue_action(e) raise e end; end
+
+class MembersControllerTest < Test::Unit::TestCase
+ fixtures :members
+
+ def setup
+ @controller = MembersController.new
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+ end
+
+ def test_index
+ get :index
+ assert_response :success
+ assert_template 'list'
+ end
+
+ def test_list
+ get :list
+
+ assert_response :success
+ assert_template 'list'
+
+ assert_not_nil assigns(:members)
+ end
+
+ def test_show
+ get :show, :id => 1
+
+ assert_response :success
+ assert_template 'show'
+
+ assert_not_nil assigns(:member)
+ assert assigns(:member).valid?
+ end
+
+ def test_new
+ get :new
+
+ assert_response :success
+ assert_template 'new'
+
+ assert_not_nil assigns(:member)
+ end
+
+ def test_create
+ num_members = Member.count
+
+ post :create, :member => {}
+
+ assert_response :redirect
+ assert_redirected_to :action => 'list'
+
+ assert_equal num_members + 1, Member.count
+ end
+
+ def test_edit
+ get :edit, :id => 1
+
+ assert_response :success
+ assert_template 'edit'
+
+ assert_not_nil assigns(:member)
+ assert assigns(:member).valid?
+ end
+
+ def test_update
+ post :update, :id => 1
+ assert_response :redirect
+ assert_redirected_to :action => 'show', :id => 1
+ end
+
+ def test_destroy
+ assert_not_nil Member.find(1)
+
+ post :destroy, :id => 1
+ assert_response :redirect
+ assert_redirected_to :action => 'list'
+
+ assert_raise(ActiveRecord::RecordNotFound) {
+ Member.find(1)
+ }
+ end
+end
diff --git a/redmine/test/functional/news_controller_test.rb b/redmine/test/functional/news_controller_test.rb
new file mode 100644
index 000000000..b360c6cb3
--- /dev/null
+++ b/redmine/test/functional/news_controller_test.rb
@@ -0,0 +1,88 @@
+require File.dirname(__FILE__) + '/../test_helper'
+require 'news_controller'
+
+# Re-raise errors caught by the controller.
+class NewsController; def rescue_action(e) raise e end; end
+
+class NewsControllerTest < Test::Unit::TestCase
+ fixtures :news
+
+ def setup
+ @controller = NewsController.new
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+ end
+
+ def test_index
+ get :index
+ assert_response :success
+ assert_template 'list'
+ end
+
+ def test_list
+ get :list
+
+ assert_response :success
+ assert_template 'list'
+
+ assert_not_nil assigns(:news)
+ end
+
+ def test_show
+ get :show, :id => 1
+
+ assert_response :success
+ assert_template 'show'
+
+ assert_not_nil assigns(:news)
+ assert assigns(:news).valid?
+ end
+
+ def test_new
+ get :new
+
+ assert_response :success
+ assert_template 'new'
+
+ assert_not_nil assigns(:news)
+ end
+
+ def test_create
+ num_news = News.count
+
+ post :create, :news => {}
+
+ assert_response :redirect
+ assert_redirected_to :action => 'list'
+
+ assert_equal num_news + 1, News.count
+ end
+
+ def test_edit
+ get :edit, :id => 1
+
+ assert_response :success
+ assert_template 'edit'
+
+ assert_not_nil assigns(:news)
+ assert assigns(:news).valid?
+ end
+
+ def test_update
+ post :update, :id => 1
+ assert_response :redirect
+ assert_redirected_to :action => 'show', :id => 1
+ end
+
+ def test_destroy
+ assert_not_nil News.find(1)
+
+ post :destroy, :id => 1
+ assert_response :redirect
+ assert_redirected_to :action => 'list'
+
+ assert_raise(ActiveRecord::RecordNotFound) {
+ News.find(1)
+ }
+ end
+end
diff --git a/redmine/test/functional/projects_controller_test.rb b/redmine/test/functional/projects_controller_test.rb
new file mode 100644
index 000000000..8da34ec7e
--- /dev/null
+++ b/redmine/test/functional/projects_controller_test.rb
@@ -0,0 +1,88 @@
+require File.dirname(__FILE__) + '/../test_helper'
+require 'projects_controller'
+
+# Re-raise errors caught by the controller.
+class ProjectsController; def rescue_action(e) raise e end; end
+
+class ProjectsControllerTest < Test::Unit::TestCase
+ fixtures :projects
+
+ def setup
+ @controller = ProjectsController.new
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+ end
+
+ def test_index
+ get :index
+ assert_response :success
+ assert_template 'list'
+ end
+
+ def test_list
+ get :list
+
+ assert_response :success
+ assert_template 'list'
+
+ assert_not_nil assigns(:projects)
+ end
+
+ def test_show
+ get :show, :id => 1
+
+ assert_response :success
+ assert_template 'show'
+
+ assert_not_nil assigns(:project)
+ assert assigns(:project).valid?
+ end
+
+ def test_new
+ get :new
+
+ assert_response :success
+ assert_template 'new'
+
+ assert_not_nil assigns(:project)
+ end
+
+ def test_create
+ num_projects = Project.count
+
+ post :create, :project => {}
+
+ assert_response :redirect
+ assert_redirected_to :action => 'list'
+
+ assert_equal num_projects + 1, Project.count
+ end
+
+ def test_edit
+ get :edit, :id => 1
+
+ assert_response :success
+ assert_template 'edit'
+
+ assert_not_nil assigns(:project)
+ assert assigns(:project).valid?
+ end
+
+ def test_update
+ post :update, :id => 1
+ assert_response :redirect
+ assert_redirected_to :action => 'show', :id => 1
+ end
+
+ def test_destroy
+ assert_not_nil Project.find(1)
+
+ post :destroy, :id => 1
+ assert_response :redirect
+ assert_redirected_to :action => 'list'
+
+ assert_raise(ActiveRecord::RecordNotFound) {
+ Project.find(1)
+ }
+ end
+end
diff --git a/redmine/test/functional/reports_controller_test.rb b/redmine/test/functional/reports_controller_test.rb
new file mode 100644
index 000000000..4b52ffab3
--- /dev/null
+++ b/redmine/test/functional/reports_controller_test.rb
@@ -0,0 +1,18 @@
+require File.dirname(__FILE__) + '/../test_helper'
+require 'reports_controller'
+
+# Re-raise errors caught by the controller.
+class ReportsController; def rescue_action(e) raise e end; end
+
+class ReportsControllerTest < Test::Unit::TestCase
+ def setup
+ @controller = ReportsController.new
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+ end
+
+ # Replace this with your real tests.
+ def test_truth
+ assert true
+ end
+end
diff --git a/redmine/test/functional/roles_controller_test.rb b/redmine/test/functional/roles_controller_test.rb
new file mode 100644
index 000000000..299aef211
--- /dev/null
+++ b/redmine/test/functional/roles_controller_test.rb
@@ -0,0 +1,88 @@
+require File.dirname(__FILE__) + '/../test_helper'
+require 'roles_controller'
+
+# Re-raise errors caught by the controller.
+class RolesController; def rescue_action(e) raise e end; end
+
+class RolesControllerTest < Test::Unit::TestCase
+ fixtures :roles
+
+ def setup
+ @controller = RolesController.new
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+ end
+
+ def test_index
+ get :index
+ assert_response :success
+ assert_template 'list'
+ end
+
+ def test_list
+ get :list
+
+ assert_response :success
+ assert_template 'list'
+
+ assert_not_nil assigns(:roles)
+ end
+
+ def test_show
+ get :show, :id => 1
+
+ assert_response :success
+ assert_template 'show'
+
+ assert_not_nil assigns(:role)
+ assert assigns(:role).valid?
+ end
+
+ def test_new
+ get :new
+
+ assert_response :success
+ assert_template 'new'
+
+ assert_not_nil assigns(:role)
+ end
+
+ def test_create
+ num_roles = Role.count
+
+ post :create, :role => {}
+
+ assert_response :redirect
+ assert_redirected_to :action => 'list'
+
+ assert_equal num_roles + 1, Role.count
+ end
+
+ def test_edit
+ get :edit, :id => 1
+
+ assert_response :success
+ assert_template 'edit'
+
+ assert_not_nil assigns(:role)
+ assert assigns(:role).valid?
+ end
+
+ def test_update
+ post :update, :id => 1
+ assert_response :redirect
+ assert_redirected_to :action => 'show', :id => 1
+ end
+
+ def test_destroy
+ assert_not_nil Role.find(1)
+
+ post :destroy, :id => 1
+ assert_response :redirect
+ assert_redirected_to :action => 'list'
+
+ assert_raise(ActiveRecord::RecordNotFound) {
+ Role.find(1)
+ }
+ end
+end
diff --git a/redmine/test/functional/trackers_controller_test.rb b/redmine/test/functional/trackers_controller_test.rb
new file mode 100644
index 000000000..75063c6c5
--- /dev/null
+++ b/redmine/test/functional/trackers_controller_test.rb
@@ -0,0 +1,88 @@
+require File.dirname(__FILE__) + '/../test_helper'
+require 'trackers_controller'
+
+# Re-raise errors caught by the controller.
+class TrackersController; def rescue_action(e) raise e end; end
+
+class TrackersControllerTest < Test::Unit::TestCase
+ fixtures :trackers
+
+ def setup
+ @controller = TrackersController.new
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+ end
+
+ def test_index
+ get :index
+ assert_response :success
+ assert_template 'list'
+ end
+
+ def test_list
+ get :list
+
+ assert_response :success
+ assert_template 'list'
+
+ assert_not_nil assigns(:trackers)
+ end
+
+ def test_show
+ get :show, :id => 1
+
+ assert_response :success
+ assert_template 'show'
+
+ assert_not_nil assigns(:tracker)
+ assert assigns(:tracker).valid?
+ end
+
+ def test_new
+ get :new
+
+ assert_response :success
+ assert_template 'new'
+
+ assert_not_nil assigns(:tracker)
+ end
+
+ def test_create
+ num_trackers = Tracker.count
+
+ post :create, :tracker => {}
+
+ assert_response :redirect
+ assert_redirected_to :action => 'list'
+
+ assert_equal num_trackers + 1, Tracker.count
+ end
+
+ def test_edit
+ get :edit, :id => 1
+
+ assert_response :success
+ assert_template 'edit'
+
+ assert_not_nil assigns(:tracker)
+ assert assigns(:tracker).valid?
+ end
+
+ def test_update
+ post :update, :id => 1
+ assert_response :redirect
+ assert_redirected_to :action => 'show', :id => 1
+ end
+
+ def test_destroy
+ assert_not_nil Tracker.find(1)
+
+ post :destroy, :id => 1
+ assert_response :redirect
+ assert_redirected_to :action => 'list'
+
+ assert_raise(ActiveRecord::RecordNotFound) {
+ Tracker.find(1)
+ }
+ end
+end
diff --git a/redmine/test/functional/users_controller_test.rb b/redmine/test/functional/users_controller_test.rb
new file mode 100644
index 000000000..f1e22817b
--- /dev/null
+++ b/redmine/test/functional/users_controller_test.rb
@@ -0,0 +1,88 @@
+require File.dirname(__FILE__) + '/../test_helper'
+require 'users_controller'
+
+# Re-raise errors caught by the controller.
+class UsersController; def rescue_action(e) raise e end; end
+
+class UsersControllerTest < Test::Unit::TestCase
+ fixtures :users
+
+ def setup
+ @controller = UsersController.new
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+ end
+
+ def test_index
+ get :index
+ assert_response :success
+ assert_template 'list'
+ end
+
+ def test_list
+ get :list
+
+ assert_response :success
+ assert_template 'list'
+
+ assert_not_nil assigns(:users)
+ end
+
+ def test_show
+ get :show, :id => 1
+
+ assert_response :success
+ assert_template 'show'
+
+ assert_not_nil assigns(:user)
+ assert assigns(:user).valid?
+ end
+
+ def test_new
+ get :new
+
+ assert_response :success
+ assert_template 'new'
+
+ assert_not_nil assigns(:user)
+ end
+
+ def test_create
+ num_users = User.count
+
+ post :create, :user => {}
+
+ assert_response :redirect
+ assert_redirected_to :action => 'list'
+
+ assert_equal num_users + 1, User.count
+ end
+
+ def test_edit
+ get :edit, :id => 1
+
+ assert_response :success
+ assert_template 'edit'
+
+ assert_not_nil assigns(:user)
+ assert assigns(:user).valid?
+ end
+
+ def test_update
+ post :update, :id => 1
+ assert_response :redirect
+ assert_redirected_to :action => 'show', :id => 1
+ end
+
+ def test_destroy
+ assert_not_nil User.find(1)
+
+ post :destroy, :id => 1
+ assert_response :redirect
+ assert_redirected_to :action => 'list'
+
+ assert_raise(ActiveRecord::RecordNotFound) {
+ User.find(1)
+ }
+ end
+end
diff --git a/redmine/test/functional/versions_controller_test.rb b/redmine/test/functional/versions_controller_test.rb
new file mode 100644
index 000000000..85b2ef76c
--- /dev/null
+++ b/redmine/test/functional/versions_controller_test.rb
@@ -0,0 +1,88 @@
+require File.dirname(__FILE__) + '/../test_helper'
+require 'versions_controller'
+
+# Re-raise errors caught by the controller.
+class VersionsController; def rescue_action(e) raise e end; end
+
+class VersionsControllerTest < Test::Unit::TestCase
+ fixtures :versions
+
+ def setup
+ @controller = VersionsController.new
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+ end
+
+ def test_index
+ get :index
+ assert_response :success
+ assert_template 'list'
+ end
+
+ def test_list
+ get :list
+
+ assert_response :success
+ assert_template 'list'
+
+ assert_not_nil assigns(:versions)
+ end
+
+ def test_show
+ get :show, :id => 1
+
+ assert_response :success
+ assert_template 'show'
+
+ assert_not_nil assigns(:version)
+ assert assigns(:version).valid?
+ end
+
+ def test_new
+ get :new
+
+ assert_response :success
+ assert_template 'new'
+
+ assert_not_nil assigns(:version)
+ end
+
+ def test_create
+ num_versions = Version.count
+
+ post :create, :version => {}
+
+ assert_response :redirect
+ assert_redirected_to :action => 'list'
+
+ assert_equal num_versions + 1, Version.count
+ end
+
+ def test_edit
+ get :edit, :id => 1
+
+ assert_response :success
+ assert_template 'edit'
+
+ assert_not_nil assigns(:version)
+ assert assigns(:version).valid?
+ end
+
+ def test_update
+ post :update, :id => 1
+ assert_response :redirect
+ assert_redirected_to :action => 'show', :id => 1
+ end
+
+ def test_destroy
+ assert_not_nil Version.find(1)
+
+ post :destroy, :id => 1
+ assert_response :redirect
+ assert_redirected_to :action => 'list'
+
+ assert_raise(ActiveRecord::RecordNotFound) {
+ Version.find(1)
+ }
+ end
+end
diff --git a/redmine/test/functional/welcome_controller_test.rb b/redmine/test/functional/welcome_controller_test.rb
new file mode 100644
index 000000000..d773945e1
--- /dev/null
+++ b/redmine/test/functional/welcome_controller_test.rb
@@ -0,0 +1,18 @@
+require File.dirname(__FILE__) + '/../test_helper'
+require 'welcome_controller'
+
+# Re-raise errors caught by the controller.
+class WelcomeController; def rescue_action(e) raise e end; end
+
+class WelcomeControllerTest < Test::Unit::TestCase
+ def setup
+ @controller = WelcomeController.new
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+ end
+
+ # Replace this with your real tests.
+ def test_truth
+ assert true
+ end
+end
diff --git a/redmine/test/test_helper.rb b/redmine/test/test_helper.rb
new file mode 100644
index 000000000..a299c7f6d
--- /dev/null
+++ b/redmine/test/test_helper.rb
@@ -0,0 +1,28 @@
+ENV["RAILS_ENV"] = "test"
+require File.expand_path(File.dirname(__FILE__) + "/../config/environment")
+require 'test_help'
+
+class Test::Unit::TestCase
+ # Transactional fixtures accelerate your tests by wrapping each test method
+ # in a transaction that's rolled back on completion. This ensures that the
+ # test database remains unchanged so your fixtures don't have to be reloaded
+ # between every test method. Fewer database queries means faster tests.
+ #
+ # Read Mike Clark's excellent walkthrough at
+ # http://clarkware.com/cgi/blosxom/2005/10/24#Rails10FastTesting
+ #
+ # Every Active Record database supports transactions except MyISAM tables
+ # in MySQL. Turn off transactional fixtures in this case; however, if you
+ # don't care one way or the other, switching from MyISAM to InnoDB tables
+ # is recommended.
+ self.use_transactional_fixtures = true
+
+ # Instantiated fixtures are slow, but give you @david where otherwise you
+ # would need people(:david). If you don't want to migrate your existing
+ # test cases which use the @david style and don't mind the speed hit (each
+ # instantiated fixtures translates to a database query per test method),
+ # then set this back to true.
+ self.use_instantiated_fixtures = false
+
+ # Add more helper methods to be used by all tests here...
+end
diff --git a/redmine/test/unit/attachment_test.rb b/redmine/test/unit/attachment_test.rb
new file mode 100644
index 000000000..6f66833d3
--- /dev/null
+++ b/redmine/test/unit/attachment_test.rb
@@ -0,0 +1,10 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class AttachmentTest < Test::Unit::TestCase
+ fixtures :attachments
+
+ # Replace this with your real tests.
+ def test_truth
+ assert true
+ end
+end
diff --git a/redmine/test/unit/custom_field_test.rb b/redmine/test/unit/custom_field_test.rb
new file mode 100644
index 000000000..886bd517f
--- /dev/null
+++ b/redmine/test/unit/custom_field_test.rb
@@ -0,0 +1,10 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class CustomFieldTest < Test::Unit::TestCase
+ fixtures :custom_fields
+
+ # Replace this with your real tests.
+ def test_truth
+ assert_kind_of CustomField, custom_fields(:first)
+ end
+end
diff --git a/redmine/test/unit/document_test.rb b/redmine/test/unit/document_test.rb
new file mode 100644
index 000000000..acd96ddba
--- /dev/null
+++ b/redmine/test/unit/document_test.rb
@@ -0,0 +1,10 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class DocumentTest < Test::Unit::TestCase
+ fixtures :documents
+
+ # Replace this with your real tests.
+ def test_truth
+ assert true
+ end
+end
diff --git a/redmine/test/unit/enumeration_test.rb b/redmine/test/unit/enumeration_test.rb
new file mode 100644
index 000000000..ea8c01405
--- /dev/null
+++ b/redmine/test/unit/enumeration_test.rb
@@ -0,0 +1,10 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class EnumerationTest < Test::Unit::TestCase
+ fixtures :enumerations
+
+ # Replace this with your real tests.
+ def test_truth
+ assert true
+ end
+end
diff --git a/redmine/test/unit/issue_category_test.rb b/redmine/test/unit/issue_category_test.rb
new file mode 100644
index 000000000..6f5051be9
--- /dev/null
+++ b/redmine/test/unit/issue_category_test.rb
@@ -0,0 +1,10 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class IssueCategoryTest < Test::Unit::TestCase
+ fixtures :issue_categories
+
+ # Replace this with your real tests.
+ def test_truth
+ assert true
+ end
+end
diff --git a/redmine/test/unit/issue_custom_field_test.rb b/redmine/test/unit/issue_custom_field_test.rb
new file mode 100644
index 000000000..2adee1061
--- /dev/null
+++ b/redmine/test/unit/issue_custom_field_test.rb
@@ -0,0 +1,10 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class IssueCustomFieldTest < Test::Unit::TestCase
+ fixtures :issue_custom_fields
+
+ # Replace this with your real tests.
+ def test_truth
+ assert_kind_of IssueCustomField, issue_custom_fields(:first)
+ end
+end
diff --git a/redmine/test/unit/issue_custom_value_test.rb b/redmine/test/unit/issue_custom_value_test.rb
new file mode 100644
index 000000000..09c0551b6
--- /dev/null
+++ b/redmine/test/unit/issue_custom_value_test.rb
@@ -0,0 +1,10 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class IssueCustomValueTest < Test::Unit::TestCase
+ fixtures :issue_custom_values
+
+ # Replace this with your real tests.
+ def test_truth
+ assert true
+ end
+end
diff --git a/redmine/test/unit/issue_history_test.rb b/redmine/test/unit/issue_history_test.rb
new file mode 100644
index 000000000..3da38e73c
--- /dev/null
+++ b/redmine/test/unit/issue_history_test.rb
@@ -0,0 +1,10 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class IssueHistoryTest < Test::Unit::TestCase
+ fixtures :issue_histories
+
+ # Replace this with your real tests.
+ def test_truth
+ assert_kind_of IssueHistory, issue_histories(:first)
+ end
+end
diff --git a/redmine/test/unit/issue_status_test.rb b/redmine/test/unit/issue_status_test.rb
new file mode 100644
index 000000000..8e7c0b97b
--- /dev/null
+++ b/redmine/test/unit/issue_status_test.rb
@@ -0,0 +1,10 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class IssueStatusTest < Test::Unit::TestCase
+ fixtures :issue_statuses
+
+ # Replace this with your real tests.
+ def test_truth
+ assert_kind_of IssueStatus, issue_statuses(:first)
+ end
+end
diff --git a/redmine/test/unit/issue_test.rb b/redmine/test/unit/issue_test.rb
new file mode 100644
index 000000000..3b318778e
--- /dev/null
+++ b/redmine/test/unit/issue_test.rb
@@ -0,0 +1,10 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class IssueTest < Test::Unit::TestCase
+ fixtures :issues
+
+ # Replace this with your real tests.
+ def test_truth
+ assert_kind_of Issue, issues(:first)
+ end
+end
diff --git a/redmine/test/unit/mailer_test.rb b/redmine/test/unit/mailer_test.rb
new file mode 100644
index 000000000..70615d712
--- /dev/null
+++ b/redmine/test/unit/mailer_test.rb
@@ -0,0 +1,35 @@
+require File.dirname(__FILE__) + '/../test_helper'
+require 'mailer'
+
+class MailerTest < Test::Unit::TestCase
+ FIXTURES_PATH = File.dirname(__FILE__) + '/../fixtures'
+ CHARSET = "utf-8"
+
+ include ActionMailer::Quoting
+
+ def setup
+ ActionMailer::Base.delivery_method = :test
+ ActionMailer::Base.perform_deliveries = true
+ ActionMailer::Base.deliveries = []
+
+ @expected = TMail::Mail.new
+ @expected.set_content_type "text", "plain", { "charset" => CHARSET }
+ end
+
+ def test_issue_closed
+ @expected.subject = 'Mailer#issue_closed'
+ @expected.body = read_fixture('issue_closed')
+ @expected.date = Time.now
+
+ assert_equal @expected.encoded, Mailer.create_issue_closed(@expected.date).encoded
+ end
+
+ private
+ def read_fixture(action)
+ IO.readlines("#{FIXTURES_PATH}/mailer/#{action}")
+ end
+
+ def encode(subject)
+ quoted_printable(subject, CHARSET)
+ end
+end
diff --git a/redmine/test/unit/member_test.rb b/redmine/test/unit/member_test.rb
new file mode 100644
index 000000000..443c096ec
--- /dev/null
+++ b/redmine/test/unit/member_test.rb
@@ -0,0 +1,10 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class MemberTest < Test::Unit::TestCase
+ fixtures :members
+
+ # Replace this with your real tests.
+ def test_truth
+ assert_kind_of Member, members(:first)
+ end
+end
diff --git a/redmine/test/unit/news_test.rb b/redmine/test/unit/news_test.rb
new file mode 100644
index 000000000..db9b3ab0d
--- /dev/null
+++ b/redmine/test/unit/news_test.rb
@@ -0,0 +1,10 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class NewsTest < Test::Unit::TestCase
+ fixtures :news
+
+ # Replace this with your real tests.
+ def test_truth
+ assert_kind_of News, news(:first)
+ end
+end
diff --git a/redmine/test/unit/packages_test.rb b/redmine/test/unit/packages_test.rb
new file mode 100644
index 000000000..a5ebcdf1b
--- /dev/null
+++ b/redmine/test/unit/packages_test.rb
@@ -0,0 +1,10 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class PackagesTest < Test::Unit::TestCase
+ fixtures :packages
+
+ # Replace this with your real tests.
+ def test_truth
+ assert_kind_of Packages, packages(:first)
+ end
+end
diff --git a/redmine/test/unit/permission_test.rb b/redmine/test/unit/permission_test.rb
new file mode 100644
index 000000000..306c2e61c
--- /dev/null
+++ b/redmine/test/unit/permission_test.rb
@@ -0,0 +1,10 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class PermissionTest < Test::Unit::TestCase
+ fixtures :permissions
+
+ # Replace this with your real tests.
+ def test_truth
+ assert_kind_of Permission, permissions(:first)
+ end
+end
diff --git a/redmine/test/unit/project_test.rb b/redmine/test/unit/project_test.rb
new file mode 100644
index 000000000..8a9912576
--- /dev/null
+++ b/redmine/test/unit/project_test.rb
@@ -0,0 +1,10 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class ProjectTest < Test::Unit::TestCase
+ fixtures :projects
+
+ # Replace this with your real tests.
+ def test_truth
+ assert_kind_of Project, projects(:first)
+ end
+end
diff --git a/redmine/test/unit/role_test.rb b/redmine/test/unit/role_test.rb
new file mode 100644
index 000000000..90565ae80
--- /dev/null
+++ b/redmine/test/unit/role_test.rb
@@ -0,0 +1,10 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class RoleTest < Test::Unit::TestCase
+ fixtures :roles
+
+ # Replace this with your real tests.
+ def test_truth
+ assert_kind_of Role, roles(:first)
+ end
+end
diff --git a/redmine/test/unit/tracker_test.rb b/redmine/test/unit/tracker_test.rb
new file mode 100644
index 000000000..f738f288b
--- /dev/null
+++ b/redmine/test/unit/tracker_test.rb
@@ -0,0 +1,10 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class TrackerTest < Test::Unit::TestCase
+ fixtures :trackers
+
+ # Replace this with your real tests.
+ def test_truth
+ assert true
+ end
+end
diff --git a/redmine/test/unit/user_test.rb b/redmine/test/unit/user_test.rb
new file mode 100644
index 000000000..d6a2a2245
--- /dev/null
+++ b/redmine/test/unit/user_test.rb
@@ -0,0 +1,10 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class UserTest < Test::Unit::TestCase
+ fixtures :users
+
+ # Replace this with your real tests.
+ def test_truth
+ assert_kind_of User, users(:first)
+ end
+end
diff --git a/redmine/test/unit/version_test.rb b/redmine/test/unit/version_test.rb
new file mode 100644
index 000000000..91c52d4a0
--- /dev/null
+++ b/redmine/test/unit/version_test.rb
@@ -0,0 +1,10 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class VersionTest < Test::Unit::TestCase
+ fixtures :versions
+
+ # Replace this with your real tests.
+ def test_truth
+ assert_kind_of Version, versions(:first)
+ end
+end
diff --git a/redmine/test/unit/workflow_test.rb b/redmine/test/unit/workflow_test.rb
new file mode 100644
index 000000000..ff88a9763
--- /dev/null
+++ b/redmine/test/unit/workflow_test.rb
@@ -0,0 +1,10 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class WorkflowTest < Test::Unit::TestCase
+ fixtures :workflows
+
+ # Replace this with your real tests.
+ def test_truth
+ assert_kind_of Workflow, workflows(:first)
+ end
+end
diff --git a/redmine/vendor/plugins/localization/README b/redmine/vendor/plugins/localization/README
new file mode 100644
index 000000000..0996906eb
--- /dev/null
+++ b/redmine/vendor/plugins/localization/README
@@ -0,0 +1,85 @@
+= Localization Plugin for Rails
+
+This plugin provides a simple, gettext-like method to
+provide localizations.
+
+== Features
+
+ * Any number of languages or locales
+ * Simple method to defines singluar/plural translations
+ * Can use lambdas to provide Ruby-code based dynamic translations
+ * Customizable for different instances of the application
+
+== Usage
+
+If the localization plugin is installed, it is used automatically.
+
+You need to create a /lang dir in your RAILS_ROOT.
+
+The recommended way to use it is to create files that are named
+like the languages you define in them (but you can put everything in
+one big file too.)
+
+For instance-customizable strings, add overrides in files you
+put in /lang/custom.
+
+=== Simple example:
+
+Create a file /lang/translations.rb:
+
+ Localization.define('de') do |l|
+ l.store 'yes', 'Ja'
+ l.store 'no', 'Nein'
+ end
+
+ Localization.define('fr') do |l|
+ l.store 'yes', 'oui'
+ l.store 'no', 'non'
+ end
+
+In your controller or application.rb:
+
+ Localization.lang = 'de' # or 'fr'
+
+In your view:
+
+ <%=_ 'yes' %>
+ <%=_ 'no' %>
+
+Because the _ method is simply an extension to Object, you
+can use it anywhere (models/controllers/views/libs).
+
+=== Extended example:
+
+Create a file /lang/default.rb with following contents:
+
+ Localization.define do |l|
+ l.store '(time)', lambda { |t| t.strftime('%I:%M%p') }
+ end
+
+Create a file /lang/de_DE.rb with following contents:
+
+ Localization.define('de_DE') do |l|
+ l.store '%d entries', ['Ein Eintrag', '%d Einträge']
+ l.store '(time)', lambda { |t| t.strftime('%H:%M') }
+ end
+
+In your controller or application.rb:
+
+ Localization.lang = 'de_DE'
+
+In your view:
+
+ <%=_ '%d entries', 1 %> # singular variant is chosen
+ <%=_ '%d entries', 4 %> # plural variant is chosen
+ <%=_ '(time)', Time.now %> # call the block with a parameter
+
+== Translation file guesstimation
+
+You can generate a guesstimation of all strings needed to be
+translated in your views by first adding the _('blah') syntax
+everywhere and then calling:
+
+ puts Localization.generate_l10n_file
+
+in the Rails console. \ No newline at end of file
diff --git a/redmine/vendor/plugins/localization/init.rb b/redmine/vendor/plugins/localization/init.rb
new file mode 100644
index 000000000..72ed7b9ce
--- /dev/null
+++ b/redmine/vendor/plugins/localization/init.rb
@@ -0,0 +1,3 @@
+require "#{directory}/lib/localization.rb"
+
+Localization.load \ No newline at end of file
diff --git a/redmine/vendor/plugins/localization/lib/localization.rb b/redmine/vendor/plugins/localization/lib/localization.rb
new file mode 100644
index 000000000..5fe0b1ed6
--- /dev/null
+++ b/redmine/vendor/plugins/localization/lib/localization.rb
@@ -0,0 +1,57 @@
+# Original Localization plugin for Rails can be found at:
+# http://mir.aculo.us/articles/2005/10/03/ruby-on-rails-i18n-revisited
+#
+# Slightly edited by Jean-Philippe Lang
+# - added @@langs and modified self.define method to maintain an array of available
+# langages with labels, eg. { 'en' => 'English, 'fr' => 'French }
+# - modified self.generate_l10n_file method to retrieve already translated strings
+#
+
+module Localization
+ mattr_accessor :lang, :langs
+
+ @@l10s = { :default => {} }
+ @@lang = :default
+ @@langs = []
+
+ def self._(string_to_localize, *args)
+ translated = @@l10s[@@lang][string_to_localize] || string_to_localize
+ return translated.call(*args).to_s if translated.is_a? Proc
+ if translated.is_a? Array
+ translated = if translated.size == 3
+ translated[args[0]==0 ? 0 : (args[0]>1 ? 2 : 1)]
+ else
+ translated[args[0]>1 ? 1 : 0]
+ end
+ end
+ sprintf translated, *args
+ end
+
+ def self.define(lang = :default, name = :default)
+ @@l10s[lang] ||= {}
+ @@langs << [ name, lang ]
+ yield @@l10s[lang]
+ end
+
+ def self.load
+ Dir.glob("#{RAILS_ROOT}/lang/*.rb"){ |t| require t }
+ Dir.glob("#{RAILS_ROOT}/lang/custom/*.rb"){ |t| require t }
+ end
+
+ # Generates a best-estimate l10n file from all views by
+ # collecting calls to _() -- note: use the generated file only
+ # as a start (this method is only guesstimating)
+ def self.generate_l10n_file(lang)
+ "Localization.define('en_US') do |l|\n" <<
+ Dir.glob("#{RAILS_ROOT}/app/views/**/*.rhtml").collect do |f|
+ ["# #{f}"] << File.read(f).scan(/<%.*[^\w]_\s*[\(]+[\"\'](.*?)[\"\'][\)]+/)
+ end.uniq.flatten.collect do |g|
+ g.starts_with?('#') ? "\n #{g}" : " l.store '#{g}', '#{@@l10s[lang][g]}'"
+ end.uniq.join("\n") << "\nend"
+ end
+
+end
+
+class Object
+ def _(*args); Localization._(*args); end
+end \ No newline at end of file